Skip to content

Instantly share code, notes, and snippets.

@marcan
Last active December 17, 2021 11:48
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save marcan/8f0f2fd1344d1b01b8c401e6f5a263c8 to your computer and use it in GitHub Desktop.
Save marcan/8f0f2fd1344d1b01b8c401e6f5a263c8 to your computer and use it in GitHub Desktop.
#!/usr/bin/python3
# Solution to the challenge at https://gist.github.com/ehmo/7f515ac6461c1c4d3e5a74f12e6eb5ea
# Sample solution: https://twitter.com/marcan42/status/1428933147660492800
#
# Given an input base image, computes two derivative images that have different
# perceptual hashes, yet differ by only one pixel.
#
# Usage: hash_bisector.py <input.png> <output_a.png> <output_b.png>
#
# Licensed under the terms of the STRONGEST PUBLIC LICENSE, Draft 1:
# ======================================================================
# THE STRONGEST PUBLIC LICENSE
# Draft 1, November 2010
#
# Everyone is permitted to copy and distribute verbatim or modified
# copies of this license document, and changing it is allowed as long
# as the name is changed.
#
# THE STRONGEST PUBLIC LICENSE
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
#
# ⑨. This license document permits you to DO WHAT THE FUCK YOU WANT TO
# as long as you APPRECIATE CIRNO AS THE STRONGEST IN GENSOKYO.
#
# This program is distributed in the hope that it will be THE STRONGEST,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# USEFULNESS or FITNESS FOR A PARTICULAR PURPOSE.
# ======================================================================
# At your option, you may choose to use it under the terms of the MIT
# license instead.
import sys, imagehash
from PIL import Image, ImageEnhance
# This can be replaced with any arbitrary hash construction, as long as the
# initial bisected transform produces a hash change (if it does not, it can be
# replaced with something else that does - a crossfade between two disparate
# images, for example, will always work).
def target_hash(im):
im = im.resize((12,12))
return imagehash.phash(im)
# Bisect a transform on a target image to identify a pair of arguments closer
# than epsilon that yield a different hash
def bisect(func, epsilon=0, low=0, high=1):
hlow = target_hash(func(low))
hhigh = target_hash(func(high))
if hlow == hhigh:
raise Exception("No initial difference")
while (high - low) > epsilon:
mid = (low + high) / 2
hmid = target_hash(func(mid))
if hmid != hlow:
high, hhigh = mid, hmid
elif hmid != hhigh:
low, hlow = mid, hmid
return low, high
im = Image.open(sys.argv[1]).convert("RGB")
# Step 1: rotate (could be anything)
def rotate(fac):
cfac = 0.03 # crop factor for rotation
box = (int(im.width * cfac), int(im.height * cfac),
int(im.width * (1 - cfac)), int(im.height * (1 - cfac)))
return im.rotate(fac, resample=Image.BICUBIC).crop(box)
l, h = bisect(rotate, 0.00001, 0.0, 10.0)
ima, imb = rotate(l), rotate(h)
# Step 2: blend
def blend(fac):
return Image.blend(ima, imb, fac)
l, h = bisect(blend, 0.001, 0.0, 1.0)
ima, imb = blend(l), blend(h)
# Step 3: byte
def wipe(fac):
da = ima.tobytes()
db = imb.tobytes()
d = da[:int(fac)] + db[int(fac):]
return Image.frombytes(ima.mode, ima.size, d)
l, h = bisect(wipe, 1, 0, len(ima.tobytes()))
ima, imb = wipe(l), wipe(h)
off = int(l)
pix = off // 3
x, y = pix % ima.width, pix // ima.width
def hexpix(im):
return "#" + "".join("%02x" % i for i in im.getpixel((x, y)))
print(f"Byte {off:#x}, pixel {x},{y}: {hexpix(ima)} / {hexpix(imb)}")
ima.save(sys.argv[2])
imb.save(sys.argv[3])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment