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 = "

{}

" pre_image = """

{}

{}

""" 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 = "

CSAM Detection Technical Summary | AppleNeuralHash2ONNX Github Repo | Neural Hash Collider Repo | Working Collision example images from github issue

" 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)