aliabd commited on
Commit
56aa5b9
1 Parent(s): aa2f1ff

full demo working

Browse files
.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: Generate Neural Hash Collision
3
- emoji: 😻
4
- colorFrom: gray
5
- colorTo: indigo
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)