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