Last active
December 17, 2021 11:48
-
-
Save marcan/8f0f2fd1344d1b01b8c401e6f5a263c8 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 | |
# 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