Spaces:
Runtime error
Runtime error
aliabd
commited on
Commit
•
56aa5b9
1
Parent(s):
aa2f1ff
full demo working
Browse files- .idea/generate-neural-hash-collision.iml +8 -0
- .idea/inspectionProfiles/Project_Default.xml +16 -0
- .idea/inspectionProfiles/profiles_settings.xml +6 -0
- .idea/modules.xml +8 -0
- LICENSE.md +22 -0
- README.md +4 -4
- __pycache__/app.cpython-37.pyc +0 -0
- __pycache__/app.cpython-38.pyc +0 -0
- __pycache__/util.cpython-38.pyc +0 -0
- app.py +256 -0
- cached/example1.png +0 -0
- cached/example2.png +0 -0
- cached/example3.png +0 -0
- collide.py +171 -0
- images/apple.png +0 -0
- images/cat.png +0 -0
- images/dog.png +0 -0
- images/iphone.png +0 -0
- images/lena.png +0 -0
- images/stevejobs.png +0 -0
- model.onnx +0 -0
- model.onnx.1 +0 -0
- neuralhash_128x96_seed1.dat +0 -0
- neuralhash_128x96_seed1.dat.1 +0 -0
- requirements.txt +9 -0
- util.py +49 -0
.idea/generate-neural-hash-collision.iml
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<module type="PYTHON_MODULE" version="4">
|
3 |
+
<component name="NewModuleRootManager">
|
4 |
+
<content url="file://$MODULE_DIR$" />
|
5 |
+
<orderEntry type="inheritedJdk" />
|
6 |
+
<orderEntry type="sourceFolder" forTests="false" />
|
7 |
+
</component>
|
8 |
+
</module>
|
.idea/inspectionProfiles/Project_Default.xml
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<component name="InspectionProjectProfileManager">
|
2 |
+
<profile version="1.0">
|
3 |
+
<option name="myName" value="Project Default" />
|
4 |
+
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
5 |
+
<option name="ignoredPackages">
|
6 |
+
<value>
|
7 |
+
<list size="3">
|
8 |
+
<item index="0" class="java.lang.String" itemvalue="onnxruntime" />
|
9 |
+
<item index="1" class="java.lang.String" itemvalue="onnx_tf" />
|
10 |
+
<item index="2" class="java.lang.String" itemvalue="onnx" />
|
11 |
+
</list>
|
12 |
+
</value>
|
13 |
+
</option>
|
14 |
+
</inspection_tool>
|
15 |
+
</profile>
|
16 |
+
</component>
|
.idea/inspectionProfiles/profiles_settings.xml
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<component name="InspectionProjectProfileManager">
|
2 |
+
<settings>
|
3 |
+
<option name="USE_PROJECT_PROFILE" value="false" />
|
4 |
+
<version value="1.0" />
|
5 |
+
</settings>
|
6 |
+
</component>
|
.idea/modules.xml
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
+
<project version="4">
|
3 |
+
<component name="ProjectModuleManager">
|
4 |
+
<modules>
|
5 |
+
<module fileurl="file://$PROJECT_DIR$/.idea/generate-neural-hash-collision.iml" filepath="$PROJECT_DIR$/.idea/generate-neural-hash-collision.iml" />
|
6 |
+
</modules>
|
7 |
+
</component>
|
8 |
+
</project>
|
LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
The MIT License (MIT)
|
2 |
+
=====================
|
3 |
+
|
4 |
+
**Copyright (c) 2021 Anish Athalye (me@anishathalye.com)**
|
5 |
+
|
6 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
7 |
+
this software and associated documentation files (the "Software"), to deal in
|
8 |
+
the Software without restriction, including without limitation the rights to
|
9 |
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
10 |
+
of the Software, and to permit persons to whom the Software is furnished to do
|
11 |
+
so, subject to the following conditions:
|
12 |
+
|
13 |
+
The above copyright notice and this permission notice shall be included in all
|
14 |
+
copies or substantial portions of the Software.
|
15 |
+
|
16 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22 |
+
SOFTWARE.
|
README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: gradio
|
7 |
app_file: app.py
|
8 |
pinned: false
|
1 |
---
|
2 |
+
title: Neural Hash Collisions
|
3 |
+
emoji: 🐠
|
4 |
+
colorFrom: green
|
5 |
+
colorTo: pink
|
6 |
sdk: gradio
|
7 |
app_file: app.py
|
8 |
pinned: false
|
__pycache__/app.cpython-37.pyc
ADDED
Binary file (3.36 kB). View file
|
__pycache__/app.cpython-38.pyc
ADDED
Binary file (3.39 kB). View file
|
__pycache__/util.cpython-38.pyc
ADDED
Binary file (1.9 kB). View file
|
app.py
ADDED
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
import onnxruntime
|
3 |
+
import numpy as np
|
4 |
+
from PIL import Image
|
5 |
+
import gradio as gr
|
6 |
+
import torch
|
7 |
+
import os
|
8 |
+
import random
|
9 |
+
|
10 |
+
# os.system('wget https://www.dropbox.com/s/ggf6ok63u7hywhc/neuralhash_128x96_seed1.dat')
|
11 |
+
# os.system('wget https://www.dropbox.com/s/1jug4wtevz1rol0/model.onnx')
|
12 |
+
|
13 |
+
|
14 |
+
|
15 |
+
|
16 |
+
# Copyright (c) 2021 Anish Athalye. Released under the MIT license.
|
17 |
+
|
18 |
+
import tensorflow as tf
|
19 |
+
from scipy.ndimage.filters import gaussian_filter
|
20 |
+
import time
|
21 |
+
|
22 |
+
from util import *
|
23 |
+
|
24 |
+
|
25 |
+
DEFAULT_MODEL_PATH = 'model.onnx'
|
26 |
+
DEFAULT_SEED_PATH = 'neuralhash_128x96_seed1.dat'
|
27 |
+
DEFAULT_TARGET_HASH = '59a34eabe31910abfb06f308'
|
28 |
+
DEFAULT_ITERATIONS = 1000
|
29 |
+
DEFAULT_SAVE_ITERATIONS = 0
|
30 |
+
DEFAULT_LR = 2.0
|
31 |
+
DEFAULT_COMBINED_THRESHOLD = 2
|
32 |
+
DEFAULT_K = 10.0
|
33 |
+
DEFAULT_CLIP_RANGE = 0.1
|
34 |
+
DEFAULT_W_L2 = 2e-3
|
35 |
+
DEFAULT_W_TV = 1e-4
|
36 |
+
DEFAULT_W_HASH = 0.8
|
37 |
+
DEFAULT_BLUR = 0
|
38 |
+
|
39 |
+
|
40 |
+
|
41 |
+
tf.compat.v1.disable_eager_execution()
|
42 |
+
|
43 |
+
|
44 |
+
model = load_model(DEFAULT_MODEL_PATH)
|
45 |
+
image = model.tensor_dict['image']
|
46 |
+
logits = model.tensor_dict['leaf/logits']
|
47 |
+
seed = load_seed(DEFAULT_SEED_PATH)
|
48 |
+
|
49 |
+
|
50 |
+
def generate_image(first_img, second_hash_hex):
|
51 |
+
|
52 |
+
with model.graph.as_default():
|
53 |
+
with tf.compat.v1.Session() as sess:
|
54 |
+
sess.run(tf.compat.v1.global_variables_initializer())
|
55 |
+
|
56 |
+
target = hash_from_hex(second_hash_hex)
|
57 |
+
original = load_image(first_img.name)
|
58 |
+
h = hash_from_hex(second_hash_hex)
|
59 |
+
proj = tf.reshape(tf.linalg.matmul(seed, tf.reshape(logits, (128, 1))), (96,))
|
60 |
+
# proj is in R^96; it's interpreted as a 96-bit hash by mapping
|
61 |
+
# entries < 0 to the bit '0', and entries >= 0 to the bit '1'
|
62 |
+
normalized, _ = tf.linalg.normalize(proj)
|
63 |
+
hash_output = tf.sigmoid(normalized * DEFAULT_K)
|
64 |
+
# now, hash_output has entries in (0, 1); it's interpreted by
|
65 |
+
# mapping entries < 0.5 to the bit '0' and entries >= 0.5 to the
|
66 |
+
# bit '1'
|
67 |
+
|
68 |
+
# we clip hash_output to (clip_range, 1-clip_range); this seems to
|
69 |
+
# improve the search (we don't "waste" perturbation tweaking
|
70 |
+
# "strong" bits); the sigmoid already does this to some degree, but
|
71 |
+
# this seems to help
|
72 |
+
hash_output = tf.clip_by_value(hash_output, DEFAULT_CLIP_RANGE, 1.0 - DEFAULT_CLIP_RANGE) - 0.5
|
73 |
+
hash_output = hash_output * (0.5 / (0.5 - DEFAULT_CLIP_RANGE))
|
74 |
+
hash_output = hash_output + 0.5
|
75 |
+
|
76 |
+
|
77 |
+
# hash loss: how far away we are from the target hash
|
78 |
+
hash_loss = tf.math.reduce_sum(tf.math.squared_difference(hash_output, h))
|
79 |
+
|
80 |
+
|
81 |
+
perturbation = image - original
|
82 |
+
# image loss: how big / noticeable is the perturbation?
|
83 |
+
img_loss = DEFAULT_W_L2 * tf.nn.l2_loss(perturbation) + DEFAULT_W_TV * tf.image.total_variation(perturbation)[0]
|
84 |
+
|
85 |
+
|
86 |
+
# combined loss: try to minimize both at once
|
87 |
+
combined_loss = DEFAULT_W_HASH * hash_loss + (1 - DEFAULT_W_HASH) * img_loss
|
88 |
+
|
89 |
+
|
90 |
+
# gradients of all the losses
|
91 |
+
g_hash_loss, = tf.gradients(hash_loss, image)
|
92 |
+
g_img_loss, = tf.gradients(img_loss, image)
|
93 |
+
g_combined_loss, = tf.gradients(combined_loss, image)
|
94 |
+
|
95 |
+
# perform attack
|
96 |
+
|
97 |
+
x = original
|
98 |
+
best = (float('inf'), 0) # (distance, image quality loss)
|
99 |
+
dist = float('inf')
|
100 |
+
|
101 |
+
|
102 |
+
for i in range(DEFAULT_ITERATIONS):
|
103 |
+
# we do an alternating projections style attack here; if we
|
104 |
+
# haven't found a colliding image yet, only optimize for that;
|
105 |
+
# if we have a colliding image, then minimize the size of the
|
106 |
+
# perturbation; if we're close, then do both at once
|
107 |
+
|
108 |
+
|
109 |
+
if dist == 0:
|
110 |
+
loss_name, loss, g = 'image', img_loss, g_img_loss
|
111 |
+
elif best[0] == 0 and dist <= DEFAULT_COMBINED_THRESHOLD:
|
112 |
+
loss_name, loss, g = 'combined', combined_loss, g_combined_loss
|
113 |
+
else:
|
114 |
+
loss_name, loss, g = 'hash', hash_loss, g_hash_loss
|
115 |
+
|
116 |
+
# compute loss values and gradient
|
117 |
+
xq = quantize(x) # take derivatives wrt the quantized version of the image
|
118 |
+
hash_output_v, img_loss_v, loss_v, g_v = sess.run([hash_output, img_loss, loss, g], feed_dict={image: xq})
|
119 |
+
dist = np.sum((hash_output_v >= 0.5) != (h >= 0.5))
|
120 |
+
|
121 |
+
# if it's better than any image found so far, save it
|
122 |
+
score = (dist, img_loss_v)
|
123 |
+
if score < best:
|
124 |
+
best = score
|
125 |
+
|
126 |
+
if dist == 0:
|
127 |
+
# if True:
|
128 |
+
arr = x.reshape([3, 360, 360]).transpose(1, 2, 0)
|
129 |
+
arr = (arr + 1.0) * (255.0 / 2.0)
|
130 |
+
arr = arr.astype(np.uint8)
|
131 |
+
im = Image.fromarray(arr)
|
132 |
+
return im, arr
|
133 |
+
# gradient descent step
|
134 |
+
g_v_norm = g_v / np.linalg.norm(g_v)
|
135 |
+
x = x - DEFAULT_LR * g_v_norm
|
136 |
+
if DEFAULT_BLUR > 0:
|
137 |
+
x = blur_perturbation(original, x, DEFAULT_BLUR)
|
138 |
+
x = x.clip(-1, 1)
|
139 |
+
# print('iteration: {}/{}, best: ({}, {:.3f}), hash: {}, distance: {}, loss: {:.3f} ({})'.format(
|
140 |
+
# i+1,
|
141 |
+
# DEFAULT_ITERATIONS,
|
142 |
+
# best[0],
|
143 |
+
# best[1],
|
144 |
+
# hash_to_hex(hash_output_v),
|
145 |
+
# dist,
|
146 |
+
# loss_v,
|
147 |
+
# loss_name
|
148 |
+
# ))
|
149 |
+
|
150 |
+
return None, None
|
151 |
+
|
152 |
+
|
153 |
+
def quantize(x):
|
154 |
+
x = (x + 1.0) * (255.0 / 2.0)
|
155 |
+
x = x.astype(np.uint8).astype(np.float32)
|
156 |
+
x = x / (255.0 / 2.0) - 1.0
|
157 |
+
return x
|
158 |
+
|
159 |
+
|
160 |
+
def blur_perturbation(original, x, sigma):
|
161 |
+
perturbation = x - original
|
162 |
+
perturbation = gaussian_filter_by_channel(perturbation, sigma=sigma)
|
163 |
+
return original + perturbation
|
164 |
+
|
165 |
+
|
166 |
+
def gaussian_filter_by_channel(x, sigma):
|
167 |
+
return np.stack([gaussian_filter(x[0,ch,:,:], sigma) for ch in range(x.shape[1])])[np.newaxis]
|
168 |
+
|
169 |
+
|
170 |
+
# Load ONNX model
|
171 |
+
session = onnxruntime.InferenceSession('model.onnx')
|
172 |
+
# Load output hash matrix
|
173 |
+
seed1 = open('neuralhash_128x96_seed1.dat', 'rb').read()[128:]
|
174 |
+
seed1 = np.frombuffer(seed1, dtype=np.float32)
|
175 |
+
seed1 = seed1.reshape([96, 128])
|
176 |
+
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>"
|
177 |
+
pre_image = """
|
178 |
+
<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>
|
179 |
+
<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>
|
180 |
+
|
181 |
+
<div style="text-align:center;">
|
182 |
+
<img style="display: inline-block; margin-left: auto; margin-right: auto;" src="best.png"/>
|
183 |
+
<img style="display: inline-block; margin-left: auto; margin-right: auto;" src="images/dog.png"/>
|
184 |
+
</div>
|
185 |
+
"""
|
186 |
+
|
187 |
+
|
188 |
+
|
189 |
+
def get_hash(img):
|
190 |
+
if isinstance(img, str):
|
191 |
+
img = Image.open(img)
|
192 |
+
img = img.convert('RGB')
|
193 |
+
image = img.resize([360, 360])
|
194 |
+
arr = np.array(image).astype(np.float32) / 255.0
|
195 |
+
arr = arr * 2.0 - 1.0
|
196 |
+
arr = arr.transpose(2, 0, 1).reshape([1, 3, 360, 360])
|
197 |
+
|
198 |
+
# Run model
|
199 |
+
inputs = {session.get_inputs()[0].name: arr}
|
200 |
+
outs = session.run(None, inputs)
|
201 |
+
|
202 |
+
# Convert model output to hex hash
|
203 |
+
hash_output = seed1.dot(outs[0].flatten())
|
204 |
+
hash_bits = ''.join(['1' if it >= 0 else '0' for it in hash_output])
|
205 |
+
hash_hex = '{:0{}x}'.format(int(hash_bits, 2), len(hash_bits) // 4)
|
206 |
+
return hash_hex
|
207 |
+
|
208 |
+
|
209 |
+
|
210 |
+
cache = {
|
211 |
+
# "732777d208dff6dd3268cb5a59a34eabe31910abfb06f308": (pre_text.format("732777d208dff6dd3268cb5a"), pre_text.format("59a34eabe31910abfb06f308"), ["cached/example1.png", "images/dog.png"]),
|
212 |
+
# "32dac883f7b91bbf45a4829635f7238ba05c404756bb33ee": (
|
213 |
+
# pre_text.format("32dac883f7b91bbf45a48296"), pre_text.format("35f7238ba05c404756bb33ee"),
|
214 |
+
# ["cached/example2.png", "images/apple.png"]),
|
215 |
+
# "f16d358106da998227b323f2a73e6ec2303af3d801f9133a": (
|
216 |
+
# pre_text.format("f16d358106da998227b323f2"), pre_text.format("a73e6ec2303af3d801f9133a"),
|
217 |
+
# ["cached/example3.png", "images/iphone.png"])
|
218 |
+
}
|
219 |
+
|
220 |
+
def inference(first_img, second_img):
|
221 |
+
first_hash_hex = get_hash(first_img.name)
|
222 |
+
second_hash_hex = get_hash(second_img.name)
|
223 |
+
if first_hash_hex + second_hash_hex in cache:
|
224 |
+
return cache[first_hash_hex + second_hash_hex]
|
225 |
+
generated_image, arr = generate_image(first_img, second_hash_hex)
|
226 |
+
if generated_image is None:
|
227 |
+
return pre_text.format(first_hash_hex), pre_text.format("Could not generate a collision.. :("), None
|
228 |
+
generated_image.save(first_img.name, format="PNG")
|
229 |
+
new_hash = get_hash(generated_image)
|
230 |
+
if new_hash != second_hash_hex:
|
231 |
+
return pre_text.format(first_hash_hex), pre_text.format("Could not generate a collision.. :("), None
|
232 |
+
return pre_text.format(first_hash_hex), pre_text.format(new_hash), [first_img.name, second_img.name]
|
233 |
+
|
234 |
+
|
235 |
+
title = "Generate a Neural Hash Collision"
|
236 |
+
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 1-2 minutes. Read more at the links below."
|
237 |
+
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> "
|
238 |
+
examples = [
|
239 |
+
["images/cat.png", "images/dog.png"],
|
240 |
+
["images/lena.png", "images/apple.png"],
|
241 |
+
["images/stevejobs.png", "images/iphone.png"],
|
242 |
+
]
|
243 |
+
inputs = [gr.inputs.Image(type="file", label="First Image"), gr.inputs.Image(type="file", label="Second Image")]
|
244 |
+
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..")]
|
245 |
+
|
246 |
+
gr.Interface(
|
247 |
+
inference,
|
248 |
+
inputs, outputs,
|
249 |
+
title=title,
|
250 |
+
description=description,
|
251 |
+
article=article,
|
252 |
+
examples=examples,
|
253 |
+
allow_flagging=False,
|
254 |
+
theme="huggingface",
|
255 |
+
capture_session=True
|
256 |
+
).launch(share=True, inbrowser=True)
|
cached/example1.png
ADDED
cached/example2.png
ADDED
cached/example3.png
ADDED
collide.py
ADDED
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) 2021 Anish Athalye. Released under the MIT license.
|
2 |
+
|
3 |
+
import numpy as np
|
4 |
+
import tensorflow as tf
|
5 |
+
from scipy.ndimage.filters import gaussian_filter
|
6 |
+
import argparse
|
7 |
+
import os
|
8 |
+
import time
|
9 |
+
|
10 |
+
from util import *
|
11 |
+
|
12 |
+
|
13 |
+
DEFAULT_MODEL_PATH = 'model.onnx'
|
14 |
+
DEFAULT_SEED_PATH = 'neuralhash_128x96_seed1.dat'
|
15 |
+
DEFAULT_TARGET_HASH = '59a34eabe31910abfb06f308'
|
16 |
+
DEFAULT_ITERATIONS = 500
|
17 |
+
DEFAULT_SAVE_ITERATIONS = 0
|
18 |
+
DEFAULT_LR = 2.0
|
19 |
+
DEFAULT_COMBINED_THRESHOLD = 2
|
20 |
+
DEFAULT_K = 10.0
|
21 |
+
DEFAULT_CLIP_RANGE = 0.1
|
22 |
+
DEFAULT_W_L2 = 2e-3
|
23 |
+
DEFAULT_W_TV = 1e-4
|
24 |
+
DEFAULT_W_HASH = 0.8
|
25 |
+
DEFAULT_BLUR = 0
|
26 |
+
|
27 |
+
|
28 |
+
def main():
|
29 |
+
tf.compat.v1.disable_eager_execution()
|
30 |
+
options = get_options()
|
31 |
+
|
32 |
+
model = load_model(options.model)
|
33 |
+
image = model.tensor_dict['image']
|
34 |
+
logits = model.tensor_dict['leaf/logits']
|
35 |
+
seed = load_seed(options.seed)
|
36 |
+
|
37 |
+
target = hash_from_hex(options.target)
|
38 |
+
|
39 |
+
original = load_image(options.image)
|
40 |
+
h = hash_from_hex(options.target)
|
41 |
+
|
42 |
+
with model.graph.as_default():
|
43 |
+
with tf.compat.v1.Session() as sess:
|
44 |
+
sess.run(tf.compat.v1.global_variables_initializer())
|
45 |
+
|
46 |
+
proj = tf.reshape(tf.linalg.matmul(seed, tf.reshape(logits, (128, 1))), (96,))
|
47 |
+
# proj is in R^96; it's interpreted as a 96-bit hash by mapping
|
48 |
+
# entries < 0 to the bit '0', and entries >= 0 to the bit '1'
|
49 |
+
normalized, _ = tf.linalg.normalize(proj)
|
50 |
+
hash_output = tf.sigmoid(normalized * options.k)
|
51 |
+
# now, hash_output has entries in (0, 1); it's interpreted by
|
52 |
+
# mapping entries < 0.5 to the bit '0' and entries >= 0.5 to the
|
53 |
+
# bit '1'
|
54 |
+
|
55 |
+
# we clip hash_output to (clip_range, 1-clip_range); this seems to
|
56 |
+
# improve the search (we don't "waste" perturbation tweaking
|
57 |
+
# "strong" bits); the sigmoid already does this to some degree, but
|
58 |
+
# this seems to help
|
59 |
+
hash_output = tf.clip_by_value(hash_output, options.clip_range, 1.0 - options.clip_range) - 0.5
|
60 |
+
hash_output = hash_output * (0.5 / (0.5 - options.clip_range))
|
61 |
+
hash_output = hash_output + 0.5
|
62 |
+
|
63 |
+
# hash loss: how far away we are from the target hash
|
64 |
+
hash_loss = tf.math.reduce_sum(tf.math.squared_difference(hash_output, h))
|
65 |
+
|
66 |
+
perturbation = image - original
|
67 |
+
# image loss: how big / noticeable is the perturbation?
|
68 |
+
img_loss = options.l2_weight * tf.nn.l2_loss(perturbation) + options.tv_weight * tf.image.total_variation(perturbation)[0]
|
69 |
+
|
70 |
+
# combined loss: try to minimize both at once
|
71 |
+
combined_loss = options.hash_weight * hash_loss + (1 - options.hash_weight) * img_loss
|
72 |
+
|
73 |
+
# gradients of all the losses
|
74 |
+
g_hash_loss, = tf.gradients(hash_loss, image)
|
75 |
+
g_img_loss, = tf.gradients(img_loss, image)
|
76 |
+
g_combined_loss, = tf.gradients(combined_loss, image)
|
77 |
+
|
78 |
+
# perform attack
|
79 |
+
|
80 |
+
x = original
|
81 |
+
best = (float('inf'), 0) # (distance, image quality loss)
|
82 |
+
dist = float('inf')
|
83 |
+
|
84 |
+
for i in range(options.iterations):
|
85 |
+
# we do an alternating projections style attack here; if we
|
86 |
+
# haven't found a colliding image yet, only optimize for that;
|
87 |
+
# if we have a colliding image, then minimize the size of the
|
88 |
+
# perturbation; if we're close, then do both at once
|
89 |
+
if dist == 0:
|
90 |
+
loss_name, loss, g = 'image', img_loss, g_img_loss
|
91 |
+
elif best[0] == 0 and dist <= options.combined_threshold:
|
92 |
+
loss_name, loss, g = 'combined', combined_loss, g_combined_loss
|
93 |
+
else:
|
94 |
+
loss_name, loss, g = 'hash', hash_loss, g_hash_loss
|
95 |
+
|
96 |
+
# compute loss values and gradient
|
97 |
+
xq = quantize(x) # take derivatives wrt the quantized version of the image
|
98 |
+
hash_output_v, img_loss_v, loss_v, g_v = sess.run([hash_output, img_loss, loss, g], feed_dict={image: xq})
|
99 |
+
dist = np.sum((hash_output_v >= 0.5) != (h >= 0.5))
|
100 |
+
if dist == 0:
|
101 |
+
save_image(x, os.path.join(options.save_directory, 'best.png'))
|
102 |
+
break
|
103 |
+
|
104 |
+
# if it's better than any image found so far, save it
|
105 |
+
score = (dist, img_loss_v)
|
106 |
+
if score < best or (options.save_iterations > 0 and (i+1) % options.save_iterations == 0):
|
107 |
+
# save_image(x, os.path.join(options.save_directory, 'best.png'))
|
108 |
+
pass
|
109 |
+
if score < best:
|
110 |
+
best = score
|
111 |
+
|
112 |
+
# gradient descent step
|
113 |
+
g_v_norm = g_v / np.linalg.norm(g_v)
|
114 |
+
x = x - options.learning_rate * g_v_norm
|
115 |
+
if options.blur > 0:
|
116 |
+
x = blur_perturbation(original, x, options.blur)
|
117 |
+
x = x.clip(-1, 1)
|
118 |
+
print('iteration: {}/{}, best: ({}, {:.3f}), hash: {}, distance: {}, loss: {:.3f} ({})'.format(
|
119 |
+
i+1,
|
120 |
+
options.iterations,
|
121 |
+
best[0],
|
122 |
+
best[1],
|
123 |
+
hash_to_hex(hash_output_v),
|
124 |
+
dist,
|
125 |
+
loss_v,
|
126 |
+
loss_name
|
127 |
+
))
|
128 |
+
|
129 |
+
|
130 |
+
def quantize(x):
|
131 |
+
x = (x + 1.0) * (255.0 / 2.0)
|
132 |
+
x = x.astype(np.uint8).astype(np.float32)
|
133 |
+
x = x / (255.0 / 2.0) - 1.0
|
134 |
+
return x
|
135 |
+
|
136 |
+
|
137 |
+
def blur_perturbation(original, x, sigma):
|
138 |
+
perturbation = x - original
|
139 |
+
perturbation = gaussian_filter_by_channel(perturbation, sigma=sigma)
|
140 |
+
return original + perturbation
|
141 |
+
|
142 |
+
|
143 |
+
def gaussian_filter_by_channel(x, sigma):
|
144 |
+
return np.stack([gaussian_filter(x[0,ch,:,:], sigma) for ch in range(x.shape[1])])[np.newaxis]
|
145 |
+
|
146 |
+
|
147 |
+
def get_options():
|
148 |
+
parser = argparse.ArgumentParser()
|
149 |
+
parser.add_argument('--image', type=str, help='path to starting image', required=True)
|
150 |
+
parser.add_argument('--model', type=str, help='path to model', default=DEFAULT_MODEL_PATH)
|
151 |
+
parser.add_argument('--seed', type=str, help='path to seed', default=DEFAULT_SEED_PATH)
|
152 |
+
parser.add_argument('--target', type=str, help='target hash', default=DEFAULT_TARGET_HASH)
|
153 |
+
parser.add_argument('--learning-rate', type=float, help='learning rate', default=DEFAULT_LR)
|
154 |
+
parser.add_argument('--combined-threshold', type=int, help='threshold to start using combined loss', default=DEFAULT_COMBINED_THRESHOLD)
|
155 |
+
parser.add_argument('--k', type=float, help='k parameter', default=DEFAULT_K)
|
156 |
+
parser.add_argument('--l2-weight', type=float, help='perturbation l2 loss weight', default=DEFAULT_W_L2)
|
157 |
+
parser.add_argument('--tv-weight', type=float, help='perturbation total variation loss weight', default=DEFAULT_W_TV)
|
158 |
+
parser.add_argument('--hash-weight', type=float, help='relative weight (0.0 to 1.0) of hash in combined loss', default=DEFAULT_W_HASH)
|
159 |
+
parser.add_argument('--clip-range', type=float, help='clip range parameter', default=DEFAULT_CLIP_RANGE)
|
160 |
+
parser.add_argument('--iterations', type=int, help='max number of iterations', default=DEFAULT_ITERATIONS)
|
161 |
+
parser.add_argument('--save-directory', type=str, help='directory to save output images', default='.')
|
162 |
+
parser.add_argument('--save-iterations', type=int, help='save this frequently, regardless of improvement', default=DEFAULT_SAVE_ITERATIONS)
|
163 |
+
parser.add_argument('--blur', type=float, help='apply Gaussian blur with this sigma on every step', default=DEFAULT_BLUR)
|
164 |
+
return parser.parse_args()
|
165 |
+
|
166 |
+
|
167 |
+
if __name__ == '__main__':
|
168 |
+
start = time.time()
|
169 |
+
main()
|
170 |
+
end = time.time()
|
171 |
+
print(end - start)
|
images/apple.png
ADDED
images/cat.png
ADDED
images/dog.png
ADDED
images/iphone.png
ADDED
images/lena.png
ADDED
images/stevejobs.png
ADDED
model.onnx
ADDED
Binary file (7.27 MB). View file
|
model.onnx.1
ADDED
Binary file (7.27 MB). View file
|
neuralhash_128x96_seed1.dat
ADDED
Binary file (49.3 kB). View file
|
neuralhash_128x96_seed1.dat.1
ADDED
Binary file (49.3 kB). View file
|
requirements.txt
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Pillow
|
2 |
+
numpy
|
3 |
+
onnx
|
4 |
+
onnx_tf
|
5 |
+
scipy
|
6 |
+
tensorflow
|
7 |
+
torchtext
|
8 |
+
onnxruntime
|
9 |
+
torch
|
util.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (c) 2021 Anish Athalye. Released under the MIT license.
|
2 |
+
|
3 |
+
import numpy as np
|
4 |
+
import onnx
|
5 |
+
from onnx_tf.backend import prepare
|
6 |
+
from PIL import Image
|
7 |
+
|
8 |
+
|
9 |
+
def load_model(path):
|
10 |
+
onnx_model = onnx.load(path)
|
11 |
+
model = prepare(onnx_model, training_mode=True)
|
12 |
+
return model
|
13 |
+
|
14 |
+
|
15 |
+
def load_seed(path):
|
16 |
+
seed = open(path, 'rb').read()[128:]
|
17 |
+
seed = np.frombuffer(seed, dtype=np.float32)
|
18 |
+
seed = seed.reshape([96, 128])
|
19 |
+
return seed
|
20 |
+
|
21 |
+
|
22 |
+
def load_image(path):
|
23 |
+
im = Image.open(path).convert('RGB')
|
24 |
+
im = im.resize([360, 360])
|
25 |
+
arr = np.array(im).astype(np.float32) / 255.0
|
26 |
+
arr = arr * 2.0 - 1.0
|
27 |
+
arr = arr.transpose(2, 0, 1).reshape([1, 3, 360, 360])
|
28 |
+
return arr
|
29 |
+
|
30 |
+
|
31 |
+
def save_image(arr, path):
|
32 |
+
arr = arr.reshape([3, 360, 360]).transpose(1, 2, 0)
|
33 |
+
arr = (arr + 1.0) * (255.0 / 2.0)
|
34 |
+
arr = arr.astype(np.uint8)
|
35 |
+
im = Image.fromarray(arr)
|
36 |
+
im.save(path)
|
37 |
+
|
38 |
+
|
39 |
+
def hash_from_hex(hex_repr):
|
40 |
+
n = int(hex_repr, 16)
|
41 |
+
h = np.zeros(96)
|
42 |
+
for i in range(96):
|
43 |
+
h[i] = (n >> (95 - i)) & 1
|
44 |
+
return h
|
45 |
+
|
46 |
+
|
47 |
+
def hash_to_hex(h):
|
48 |
+
bits = ''.join(['1' if i >= 0.5 else '0' for i in h])
|
49 |
+
return '{:0{}x}'.format(int(bits, 2), len(bits) // 4)
|