Spaces:
Runtime error
Runtime error
import sys | |
import onnxruntime | |
import numpy as np | |
from PIL import Image | |
import gradio as gr | |
import torch | |
import os | |
import random | |
# os.system('wget https://www.dropbox.com/s/ggf6ok63u7hywhc/neuralhash_128x96_seed1.dat') | |
# os.system('wget https://www.dropbox.com/s/1jug4wtevz1rol0/model.onnx') | |
# Copyright (c) 2021 Anish Athalye. Released under the MIT license. | |
import tensorflow as tf | |
from scipy.ndimage.filters import gaussian_filter | |
import time | |
from util import * | |
DEFAULT_MODEL_PATH = 'model.onnx' | |
DEFAULT_SEED_PATH = 'neuralhash_128x96_seed1.dat' | |
DEFAULT_TARGET_HASH = '59a34eabe31910abfb06f308' | |
DEFAULT_ITERATIONS = 1000 | |
DEFAULT_SAVE_ITERATIONS = 0 | |
DEFAULT_LR = 2.0 | |
DEFAULT_COMBINED_THRESHOLD = 2 | |
DEFAULT_K = 10.0 | |
DEFAULT_CLIP_RANGE = 0.1 | |
DEFAULT_W_L2 = 2e-3 | |
DEFAULT_W_TV = 1e-4 | |
DEFAULT_W_HASH = 0.8 | |
DEFAULT_BLUR = 0 | |
tf.compat.v1.disable_eager_execution() | |
model = load_model(DEFAULT_MODEL_PATH) | |
image = model.tensor_dict['image'] | |
logits = model.tensor_dict['leaf/logits'] | |
seed = load_seed(DEFAULT_SEED_PATH) | |
def generate_image(first_img, second_hash_hex): | |
with model.graph.as_default(): | |
with tf.compat.v1.Session() as sess: | |
sess.run(tf.compat.v1.global_variables_initializer()) | |
target = hash_from_hex(second_hash_hex) | |
original = load_image(first_img.name) | |
h = hash_from_hex(second_hash_hex) | |
proj = tf.reshape(tf.linalg.matmul(seed, tf.reshape(logits, (128, 1))), (96,)) | |
# proj is in R^96; it's interpreted as a 96-bit hash by mapping | |
# entries < 0 to the bit '0', and entries >= 0 to the bit '1' | |
normalized, _ = tf.linalg.normalize(proj) | |
hash_output = tf.sigmoid(normalized * DEFAULT_K) | |
# now, hash_output has entries in (0, 1); it's interpreted by | |
# mapping entries < 0.5 to the bit '0' and entries >= 0.5 to the | |
# bit '1' | |
# we clip hash_output to (clip_range, 1-clip_range); this seems to | |
# improve the search (we don't "waste" perturbation tweaking | |
# "strong" bits); the sigmoid already does this to some degree, but | |
# this seems to help | |
hash_output = tf.clip_by_value(hash_output, DEFAULT_CLIP_RANGE, 1.0 - DEFAULT_CLIP_RANGE) - 0.5 | |
hash_output = hash_output * (0.5 / (0.5 - DEFAULT_CLIP_RANGE)) | |
hash_output = hash_output + 0.5 | |
# hash loss: how far away we are from the target hash | |
hash_loss = tf.math.reduce_sum(tf.math.squared_difference(hash_output, h)) | |
perturbation = image - original | |
# image loss: how big / noticeable is the perturbation? | |
img_loss = DEFAULT_W_L2 * tf.nn.l2_loss(perturbation) + DEFAULT_W_TV * tf.image.total_variation(perturbation)[0] | |
# combined loss: try to minimize both at once | |
combined_loss = DEFAULT_W_HASH * hash_loss + (1 - DEFAULT_W_HASH) * img_loss | |
# gradients of all the losses | |
g_hash_loss, = tf.gradients(hash_loss, image) | |
g_img_loss, = tf.gradients(img_loss, image) | |
g_combined_loss, = tf.gradients(combined_loss, image) | |
# perform attack | |
x = original | |
best = (float('inf'), 0) # (distance, image quality loss) | |
dist = float('inf') | |
for i in range(DEFAULT_ITERATIONS): | |
# we do an alternating projections style attack here; if we | |
# haven't found a colliding image yet, only optimize for that; | |
# if we have a colliding image, then minimize the size of the | |
# perturbation; if we're close, then do both at once | |
if dist == 0: | |
loss_name, loss, g = 'image', img_loss, g_img_loss | |
elif best[0] == 0 and dist <= DEFAULT_COMBINED_THRESHOLD: | |
loss_name, loss, g = 'combined', combined_loss, g_combined_loss | |
else: | |
loss_name, loss, g = 'hash', hash_loss, g_hash_loss | |
# compute loss values and gradient | |
xq = quantize(x) # take derivatives wrt the quantized version of the image | |
hash_output_v, img_loss_v, loss_v, g_v = sess.run([hash_output, img_loss, loss, g], feed_dict={image: xq}) | |
dist = np.sum((hash_output_v >= 0.5) != (h >= 0.5)) | |
# if it's better than any image found so far, save it | |
score = (dist, img_loss_v) | |
if score < best: | |
best = score | |
if dist == 0: | |
# if True: | |
arr = x.reshape([3, 360, 360]).transpose(1, 2, 0) | |
arr = (arr + 1.0) * (255.0 / 2.0) | |
arr = arr.astype(np.uint8) | |
im = Image.fromarray(arr) | |
return im, arr | |
# gradient descent step | |
g_v_norm = g_v / np.linalg.norm(g_v) | |
x = x - DEFAULT_LR * g_v_norm | |
if DEFAULT_BLUR > 0: | |
x = blur_perturbation(original, x, DEFAULT_BLUR) | |
x = x.clip(-1, 1) | |
# print('iteration: {}/{}, best: ({}, {:.3f}), hash: {}, distance: {}, loss: {:.3f} ({})'.format( | |
# i+1, | |
# DEFAULT_ITERATIONS, | |
# best[0], | |
# best[1], | |
# hash_to_hex(hash_output_v), | |
# dist, | |
# loss_v, | |
# loss_name | |
# )) | |
return None, None | |
def quantize(x): | |
x = (x + 1.0) * (255.0 / 2.0) | |
x = x.astype(np.uint8).astype(np.float32) | |
x = x / (255.0 / 2.0) - 1.0 | |
return x | |
def blur_perturbation(original, x, sigma): | |
perturbation = x - original | |
perturbation = gaussian_filter_by_channel(perturbation, sigma=sigma) | |
return original + perturbation | |
def gaussian_filter_by_channel(x, sigma): | |
return np.stack([gaussian_filter(x[0,ch,:,:], sigma) for ch in range(x.shape[1])])[np.newaxis] | |
# Load ONNX model | |
session = onnxruntime.InferenceSession('model.onnx') | |
# Load output hash matrix | |
seed1 = open('neuralhash_128x96_seed1.dat', 'rb').read()[128:] | |
seed1 = np.frombuffer(seed1, dtype=np.float32) | |
seed1 = seed1.reshape([96, 128]) | |
pre_text = "<p style='display: flex; flex-grow: 1; align-items: center; justify-content: center; padding: 2rem 1rem; font-size: 1.5rem; line-height: 2rem; font-weight: 400;'>{}</p>" | |
pre_image = """ | |
<p style='display: flex; flex-grow: 1; align-items: center; justify-content: center; padding: 2rem 1rem; font-size: 1.5rem; line-height: 2rem; font-weight: 400;'>{}</p> | |
<p style='display: flex; flex-grow: 1; align-items: center; justify-content: center; padding: 2rem 1rem; font-size: 1.5rem; line-height: 2rem; font-weight: 400;'>{}</p> | |
<div style="text-align:center;"> | |
<img style="display: inline-block; margin-left: auto; margin-right: auto;" src="best.png"/> | |
<img style="display: inline-block; margin-left: auto; margin-right: auto;" src="images/dog.png"/> | |
</div> | |
""" | |
def get_hash(img): | |
if isinstance(img, str): | |
img = Image.open(img) | |
img = img.convert('RGB') | |
image = img.resize([360, 360]) | |
arr = np.array(image).astype(np.float32) / 255.0 | |
arr = arr * 2.0 - 1.0 | |
arr = arr.transpose(2, 0, 1).reshape([1, 3, 360, 360]) | |
# Run model | |
inputs = {session.get_inputs()[0].name: arr} | |
outs = session.run(None, inputs) | |
# Convert model output to hex hash | |
hash_output = seed1.dot(outs[0].flatten()) | |
hash_bits = ''.join(['1' if it >= 0 else '0' for it in hash_output]) | |
hash_hex = '{:0{}x}'.format(int(hash_bits, 2), len(hash_bits) // 4) | |
return hash_hex | |
cache = { | |
"732777d208dff6dd3268cb5a59a34eabe31910abfb06f308": (pre_text.format("732777d208dff6dd3268cb5a"), pre_text.format("59a34eabe31910abfb06f308"), ["cached/example1.png", "images/dog.png"]), | |
"32dac883f7b91bbf45a4829635f7238ba05c404756bb33ee": ( | |
pre_text.format("32dac883f7b91bbf45a48296"), pre_text.format("35f7238ba05c404756bb33ee"), | |
["cached/example2.png", "images/apple.png"]), | |
"f16d358106da998227b323f2a73e6ec2303af3d801f9133a": ( | |
pre_text.format("f16d358106da998227b323f2"), pre_text.format("a73e6ec2303af3d801f9133a"), | |
["cached/example3.png", "images/iphone.png"]) | |
} | |
def inference(first_img, second_img): | |
first_hash_hex = get_hash(first_img.name) | |
second_hash_hex = get_hash(second_img.name) | |
if first_hash_hex + second_hash_hex in cache: | |
return cache[first_hash_hex + second_hash_hex] | |
generated_image, arr = generate_image(first_img, second_hash_hex) | |
if generated_image is None: | |
return pre_text.format(first_hash_hex), pre_text.format("Could not generate a collision.. :("), None | |
generated_image.save(first_img.name, format="PNG") | |
new_hash = get_hash(generated_image) | |
if new_hash != second_hash_hex: | |
return pre_text.format(first_hash_hex), pre_text.format("Could not generate a collision.. :("), None | |
return pre_text.format(first_hash_hex), pre_text.format(new_hash), [first_img.name, second_img.name] | |
title = "Generate a Neural Hash Collision" | |
description = "Apple's NeuralHash, a perceptual hashing method for images based on neural networks, has been criticized heavily by researchers. You can use this demo to generate a hash collision of any two images. Upload your own (or click one of the examples to load them), and an adverserial image will be created from the first one to match the hash of the second. Note: In some cases the generation times out (we set a limit of 1000 iterations). The examples are cached, but submitting your own images should take about a minute. Read more at the links below." | |
article = "<p style='text-align: center'><a href='https://www.apple.com/child-safety/pdf/CSAM_Detection_Technical_Summary.pdf'>CSAM Detection Technical Summary</a> | <a href='https://github.com/AsuharietYgvar/AppleNeuralHash2ONNX'>AppleNeuralHash2ONNX Github Repo</a> | <a href='https://github.com/anishathalye/neural-hash-collider'>Neural Hash Collider Repo</a> | <a href='https://github.com/AsuharietYgvar/AppleNeuralHash2ONNX/issues/1'>Working Collision example images from github issue</a></p> " | |
examples = [ | |
["images/cat.png", "images/dog.png"], | |
["images/lena.png", "images/apple.png"], | |
["images/stevejobs.png", "images/iphone.png"], | |
] | |
inputs = [gr.inputs.Image(type="file", label="First Image"), gr.inputs.Image(type="file", label="Second Image")] | |
outputs = [gr.outputs.HTML(label="Initial Hash of First Image.."), gr.outputs.HTML(label="Generated a collision at.."), gr.outputs.Carousel(components=[gr.outputs.Image(type="file"), gr.outputs.Image(type="file")], label="Images at this hash..")] | |
gr.Interface( | |
inference, | |
inputs, outputs, | |
title=title, | |
description=description, | |
article=article, | |
examples=examples, | |
allow_flagging=False, | |
theme="huggingface", | |
capture_session=True | |
).launch(share=True) |