|
a |
|
b/procedures/attack_pipeline.py |
|
|
1 |
# MIT License |
|
|
2 |
# |
|
|
3 |
# Copyright (c) 2019 Yisroel Mirsky |
|
|
4 |
# |
|
|
5 |
# Permission is hereby granted, free of charge, to any person obtaining a copy |
|
|
6 |
# of this software and associated documentation files (the "Software"), to deal |
|
|
7 |
# in the Software without restriction, including without limitation the rights |
|
|
8 |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
|
9 |
# copies of the Software, and to permit persons to whom the Software is |
|
|
10 |
# furnished to do so, subject to the following conditions: |
|
|
11 |
# |
|
|
12 |
# The above copyright notice and this permission notice shall be included in all |
|
|
13 |
# copies or substantial portions of the Software. |
|
|
14 |
# |
|
|
15 |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
|
16 |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
|
17 |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
|
18 |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
|
19 |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
|
20 |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
|
21 |
# SOFTWARE. |
|
|
22 |
|
|
|
23 |
from config import * #user configurations |
|
|
24 |
from keras.models import load_model |
|
|
25 |
import os |
|
|
26 |
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" |
|
|
27 |
os.environ["CUDA_VISIBLE_DEVICES"] = config['gpus'] |
|
|
28 |
from utils.equalizer import * |
|
|
29 |
import pickle |
|
|
30 |
import numpy as np |
|
|
31 |
import time |
|
|
32 |
import scipy.ndimage |
|
|
33 |
from utils.dicom_utils import * |
|
|
34 |
from utils.utils import * |
|
|
35 |
|
|
|
36 |
# in this version: coords must be provided manually (for autnomaic candiate location selection, use[x]) |
|
|
37 |
# in this version: we scale the entire scan. For faster tampering, one should only scale the cube that is being tampred. |
|
|
38 |
# in this version: dicom->dicom, dicom->numpy, mhd/raw->numpy supported |
|
|
39 |
|
|
|
40 |
class scan_manipulator: |
|
|
41 |
def __init__(self): |
|
|
42 |
print("===Init Tamperer===") |
|
|
43 |
self.scan = None |
|
|
44 |
self.load_path = None |
|
|
45 |
self.m_zlims = config['mask_zlims'] |
|
|
46 |
self.m_ylims = config['mask_ylims'] |
|
|
47 |
self.m_xlims = config['mask_xlims'] |
|
|
48 |
|
|
|
49 |
#load model and parameters |
|
|
50 |
self.model_inj_path = config['modelpath_inject'] |
|
|
51 |
self.model_rem_path = config['modelpath_remove'] |
|
|
52 |
|
|
|
53 |
#load models |
|
|
54 |
print("Loading models") |
|
|
55 |
if os.path.exists(os.path.join(self.model_inj_path,"G_model.h5")): |
|
|
56 |
self.generator_inj = load_model(os.path.join(self.model_inj_path,"G_model.h5")) |
|
|
57 |
# load normalization params |
|
|
58 |
self.norm_inj = np.load(os.path.join(self.model_inj_path, 'normalization.npy')) |
|
|
59 |
# load equalization params |
|
|
60 |
self.eq_inj = histEq([], path=os.path.join(self.model_inj_path, 'equalization.pkl')) |
|
|
61 |
print("Loaded Injector Model") |
|
|
62 |
else: |
|
|
63 |
self.generator_inj = None |
|
|
64 |
print("Failed to Load Injector Model") |
|
|
65 |
if os.path.exists(os.path.join(self.model_rem_path,"G_model.h5")): |
|
|
66 |
self.generator_rem = load_model(os.path.join(self.model_rem_path,"G_model.h5")) |
|
|
67 |
# load normalization params |
|
|
68 |
self.norm_rem = np.load(os.path.join(self.model_rem_path, 'normalization.npy')) |
|
|
69 |
# load equalization params |
|
|
70 |
self.eq_rem = histEq([], path=os.path.join(self.model_rem_path, 'equalization.pkl')) |
|
|
71 |
print("Loaded Remover Model") |
|
|
72 |
else: |
|
|
73 |
self.generator_rem = None |
|
|
74 |
print("Failed to Load Remover Model") |
|
|
75 |
|
|
|
76 |
# loads dicom/mhd to be tampered |
|
|
77 |
# Provide path to a *.dcm file or the *mhd file. The contaitning folder should have the other slices) |
|
|
78 |
def load_target_scan(self, load_path): |
|
|
79 |
self.load_path = load_path |
|
|
80 |
print('Loading scan') |
|
|
81 |
self.scan, self.scan_spacing, self.scan_orientation, self.scan_origin, self.scan_raw_slices = load_scan(load_path) |
|
|
82 |
self.scan = self.scan.astype(float) |
|
|
83 |
|
|
|
84 |
# saves tampered scan as 'dicom' series or 'numpy' serialization |
|
|
85 |
def save_tampered_scan(self, save_dir, output_type='dicom'): |
|
|
86 |
if self.scan is None: |
|
|
87 |
print('Cannot save: load a target scan first.') |
|
|
88 |
return |
|
|
89 |
|
|
|
90 |
print('Saving scan') |
|
|
91 |
if output_type == 'dicom': |
|
|
92 |
if self.load_path.split('.')[-1]=="mhd": |
|
|
93 |
toDicom(save_dir=save_dir, img_array=self.scan, pixel_spacing=self.scan_spacing, orientation=self.scan_orientation) |
|
|
94 |
else: #input was dicom |
|
|
95 |
save_dicom(self.scan, origional_raw_slices=self.scan_raw_slices, dst_directory=save_dir) |
|
|
96 |
else: #save as numpy |
|
|
97 |
os.makedirs(save_dir, exist_ok=True) |
|
|
98 |
np.save(os.path.join(save_dir,'tampered_scan.np'),self.scan) |
|
|
99 |
print('Done.') |
|
|
100 |
|
|
|
101 |
|
|
|
102 |
# tamper loaded scan at given voxel (index) coordinate |
|
|
103 |
# coord: E.g. vox: slice_indx, y_indx, x_indx world: -324.3, 23, -234 |
|
|
104 |
# action: 'inject' or 'remove' |
|
|
105 |
def tamper(self, coord, action="inject", isVox=True): |
|
|
106 |
if self.scan is None: |
|
|
107 |
print('Cannot tamper: load a target scan first.') |
|
|
108 |
return |
|
|
109 |
if (action == 'inject') and (self.generator_inj is None): |
|
|
110 |
print('Cannot inject: no injection model loaded.') |
|
|
111 |
return |
|
|
112 |
if (action == 'remove') and (self.generator_rem is None): |
|
|
113 |
print('Cannot inject: no removal model loaded.') |
|
|
114 |
return |
|
|
115 |
|
|
|
116 |
if action == 'inject': |
|
|
117 |
print('===Injecting Evidence===') |
|
|
118 |
else: |
|
|
119 |
print('===Removing Evidence===') |
|
|
120 |
if not isVox: |
|
|
121 |
coord = world2vox(coord, self.scan_spacing, self.scan_orientation, self.scan_origin) |
|
|
122 |
|
|
|
123 |
### Cut Location |
|
|
124 |
print("Cutting out target region") |
|
|
125 |
cube_shape = get_scaled_shape(config["cube_shape"], 1/self.scan_spacing) |
|
|
126 |
clean_cube_unscaled = cutCube(self.scan, coord, cube_shape) |
|
|
127 |
clean_cube, resize_factor = scale_scan(clean_cube_unscaled,self.scan_spacing) |
|
|
128 |
# Store backup reference |
|
|
129 |
sdim = int(np.max(cube_shape)*1.3) |
|
|
130 |
clean_cube_unscaled2 = cutCube(self.scan, coord, np.array([sdim,sdim,sdim])) #for noise touch ups later |
|
|
131 |
|
|
|
132 |
### Normalize/Equalize Location |
|
|
133 |
print("Normalizing sample") |
|
|
134 |
if action == 'inject': |
|
|
135 |
clean_cube_eq = self.eq_inj.equalize(clean_cube) |
|
|
136 |
clean_cube_norm = (clean_cube_eq - self.norm_inj[0]) / ((self.norm_inj[2] - self.norm_inj[1])) |
|
|
137 |
else: |
|
|
138 |
clean_cube_eq = self.eq_rem.equalize(clean_cube) |
|
|
139 |
clean_cube_norm = (clean_cube_eq - self.norm_rem[0]) / ((self.norm_rem[2] - self.norm_rem[1])) |
|
|
140 |
|
|
|
141 |
######## Inject Cancer ########## |
|
|
142 |
|
|
|
143 |
### Inject/Remove evidence |
|
|
144 |
if action == 'inject': |
|
|
145 |
print("Injecting evidence") |
|
|
146 |
else: |
|
|
147 |
print("Removing evidence") |
|
|
148 |
|
|
|
149 |
x = np.copy(clean_cube_norm) |
|
|
150 |
x[self.m_zlims[0]:self.m_zlims[1], self.m_xlims[0]:self.m_xlims[1], self.m_ylims[0]:self.m_ylims[1]] = 0 |
|
|
151 |
x = x.reshape((1, config['cube_shape'][0], config['cube_shape'][1], config['cube_shape'][2], 1)) |
|
|
152 |
if action == 'inject': |
|
|
153 |
x_mal = self.generator_inj.predict([x]) |
|
|
154 |
else: |
|
|
155 |
x_mal = self.generator_rem.predict([x]) |
|
|
156 |
x_mal = x_mal.reshape(config['cube_shape']) |
|
|
157 |
|
|
|
158 |
### De-Norm/De-equalize |
|
|
159 |
print("De-normalizing sample") |
|
|
160 |
x_mal[x_mal > .5] = .5 # fix boundry overflow |
|
|
161 |
x_mal[x_mal < -.5] = -.5 |
|
|
162 |
if action == 'inject': |
|
|
163 |
mal_cube_eq = x_mal * ((self.norm_inj[2] - self.norm_inj[1])) + self.norm_inj[0] |
|
|
164 |
mal_cube = self.eq_inj.dequalize(mal_cube_eq) |
|
|
165 |
else: |
|
|
166 |
mal_cube_eq = x_mal * ((self.norm_rem[2] - self.norm_rem[1])) + self.norm_rem[0] |
|
|
167 |
mal_cube = self.eq_rem.dequalize(mal_cube_eq) |
|
|
168 |
# Correct for pixel norm error |
|
|
169 |
# fix overflow |
|
|
170 |
bad = np.where(mal_cube > 2000) |
|
|
171 |
# mal_cube[bad] = np.median(clean_cube) |
|
|
172 |
for i in range(len(bad[0])): |
|
|
173 |
neiborhood = cutCube(mal_cube, np.array([bad[0][i], bad[1][i], bad[2][i]]), (np.ones(3)*5).astype(int),-1000) |
|
|
174 |
mal_cube[bad[0][i], bad[1][i], bad[2][i]] = np.mean(neiborhood) |
|
|
175 |
# fix underflow |
|
|
176 |
mal_cube[mal_cube < -1000] = -1000 |
|
|
177 |
|
|
|
178 |
### Paste Location |
|
|
179 |
print("Pasting sample into scan") |
|
|
180 |
mal_cube_scaled, resize_factor = scale_scan(mal_cube,1/self.scan_spacing) |
|
|
181 |
self.scan = pasteCube(self.scan, mal_cube_scaled, coord) |
|
|
182 |
|
|
|
183 |
### Noise Touch-ups |
|
|
184 |
print("Adding noise touch-ups...") |
|
|
185 |
noise_map_dim = clean_cube_unscaled2.shape |
|
|
186 |
ben_cube_ext = clean_cube_unscaled2 |
|
|
187 |
mal_cube_ext = cutCube(self.scan, coord, noise_map_dim) |
|
|
188 |
local_sample = clean_cube_unscaled |
|
|
189 |
|
|
|
190 |
# Init Touch-ups |
|
|
191 |
if action == 'inject': #inject type |
|
|
192 |
noisemap = np.random.randn(150, 200, 300) * np.std(local_sample[local_sample < -600]) * .6 |
|
|
193 |
kernel_size = 3 |
|
|
194 |
factors = sigmoid((mal_cube_ext + 700) / 70) |
|
|
195 |
k = kern01(mal_cube_ext.shape[0], kernel_size) |
|
|
196 |
for i in range(factors.shape[0]): |
|
|
197 |
factors[i, :, :] = factors[i, :, :] * k |
|
|
198 |
else: #remove type |
|
|
199 |
noisemap = np.random.randn(150, 200, 200) * 30 |
|
|
200 |
kernel_size = .1 |
|
|
201 |
k = kern01(mal_cube_ext.shape[0], kernel_size) |
|
|
202 |
factors = None |
|
|
203 |
|
|
|
204 |
# Perform touch-ups |
|
|
205 |
if config['copynoise']: # copying similar noise from hard coded location over this lcoation (usually more realistic) |
|
|
206 |
benm = cutCube(self.scan, np.array([int(self.scan.shape[0] / 2), int(self.scan.shape[1]*.43), int(self.scan.shape[2]*.27)]), noise_map_dim) |
|
|
207 |
x = np.copy(benm) |
|
|
208 |
x[x > -800] = np.mean(x[x < -800]) |
|
|
209 |
noise = x - np.mean(x) |
|
|
210 |
else: # gaussian interpolated noise is used |
|
|
211 |
rf = np.ones((3,)) * (60 / np.std(local_sample[local_sample < -600])) * 1.3 |
|
|
212 |
np.random.seed(np.int64(time.time())) |
|
|
213 |
noisemap_s = scipy.ndimage.interpolation.zoom(noisemap, rf, mode='nearest') |
|
|
214 |
noise = noisemap_s[:noise_map_dim, :noise_map_dim, :noise_map_dim] |
|
|
215 |
mal_cube_ext += noise |
|
|
216 |
|
|
|
217 |
if action == 'inject': # Injection |
|
|
218 |
final_cube_s = np.maximum((mal_cube_ext * factors + ben_cube_ext * (1 - factors)), ben_cube_ext) |
|
|
219 |
else: #Removal |
|
|
220 |
minv = np.min((np.min(mal_cube_ext), np.min(ben_cube_ext))) |
|
|
221 |
final_cube_s = (mal_cube_ext + minv) * k + (ben_cube_ext + minv) * (1 - k) - minv |
|
|
222 |
|
|
|
223 |
self.scan = pasteCube(self.scan, final_cube_s, coord) |
|
|
224 |
print('touch-ups complete') |
|
|
225 |
|
|
|
226 |
|