-
-
Save alyti/3818fda9f5f7813e7fbe70f27a94b154 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# Solution to the challenge at https://gist.github.com/ehmo/7f515ac6461c1c4d3e5a74f12e6eb5ea | |
# | |
# 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> | |
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