├── README.md ├── build_model.py ├── configure.py ├── evaluation_code ├── output │ └── scores.txt ├── program │ ├── __init__.py │ ├── evaluate.py │ ├── helpers │ │ ├── __init__.py │ │ ├── calc_metric.py │ │ ├── surface.py │ │ └── utils.py │ ├── metadata │ └── readme.txt └── test_scoring.sh ├── inferlivertumor.py ├── plot_3d.py ├── preprocess.py ├── surface.py ├── train.py └── utils ├── brain_utils.py ├── extract_patches.py └── utils.py /README.md: -------------------------------------------------------------------------------- 1 | ### Introduction 2 | 3 | '[RA-UNet: A hybrid deep attention-aware network to extract liver and tumor in CT scans](https://arxiv.org/abs/1811.01328)' submitted to PR on 22-May-2019 4 | 5 | by [Qiangguo Jin](https://scholar.google.com/citations?user=USoKG48AAAAJ), [Zhaopeng Meng](http://scs.tju.edu.cn/plus/view.php?aid=723), [Changming Sun](http://vision-cdc.csiro.au/changming.sun/), [Leyi Wei](http://cs.tju.edu.cn/csweb/admin_teacher/view?id=185), [Ran Su](http://www.escience.cn/people/suran/index.html). 6 | 7 | ### Usage 8 | 9 | - [ ] Detailed usage instruction 10 | 11 | 12 | ### Dataset 13 | [LiTS](https://competitions.codalab.org/competitions/17094), [BraTS2018](https://www.med.upenn.edu/sbia/brats2018.html), [BraTS2017](https://www.med.upenn.edu/sbia/brats2017.html) 14 | 15 | ## Citation 16 | 17 | This repository is under development, please don't use this code 18 | 19 | 20 | ### Questions 21 | 22 | Please contact 'qgking@tju.edu.cn' 23 | -------------------------------------------------------------------------------- /build_model.py: -------------------------------------------------------------------------------- 1 | from keras.layers import Input, concatenate, add, \ 2 | Multiply, Lambda 3 | from keras.layers.convolutional import Conv3D, MaxPooling3D, MaxPooling2D, UpSampling2D, \ 4 | UpSampling3D, Conv2D 5 | from keras.layers.core import Activation 6 | from keras.layers.normalization import BatchNormalization 7 | from keras.models import Model 8 | 9 | 10 | # Get neural network 11 | def get_net(inp_shape, algorithm): 12 | if algorithm == 'liver_att_resunet_2d': 13 | return build_res_atten_unet_2d(inp_shape, filter_num=4) 14 | elif algorithm == 'liver_att_resunet_3d' or algorithm == 'liver_tumor_att_resunet_3d': 15 | return build_res_atten_unet_3d(inp_shape) 16 | elif algorithm == 'brain_tumor_res_atten_unet_3d': 17 | return build_brain_tumor_res_atten_unet_3d(inp_shape, filter_num=8) 18 | 19 | 20 | # ============================================================ 21 | # ======================Attention ResUnet 3D================================# 22 | # ============================================================ 23 | 24 | 25 | def attention_block(input, input_channels=None, output_channels=None, encoder_depth=1, name='out'): 26 | """ 27 | attention block 28 | https://arxiv.org/abs/1704.06904 29 | """ 30 | p = 1 31 | t = 2 32 | r = 1 33 | 34 | if input_channels is None: 35 | input_channels = input.get_shape()[-1].value 36 | if output_channels is None: 37 | output_channels = input_channels 38 | 39 | # First Residual Block 40 | for i in range(p): 41 | input = residual_block(input) 42 | 43 | # Trunc Branch 44 | output_trunk = input 45 | for i in range(t): 46 | output_trunk = residual_block(output_trunk, output_channels=output_channels) 47 | 48 | # Soft Mask Branch 49 | 50 | ## encoder 51 | ### first down sampling 52 | output_soft_mask = MaxPooling3D(padding='same')(input) # 32x32 53 | for i in range(r): 54 | output_soft_mask = residual_block(output_soft_mask) 55 | 56 | skip_connections = [] 57 | for i in range(encoder_depth - 1): 58 | 59 | ## skip connections 60 | output_skip_connection = residual_block(output_soft_mask) 61 | skip_connections.append(output_skip_connection) 62 | # print ('skip shape:', output_skip_connection.get_shape()) 63 | 64 | ## down sampling 65 | output_soft_mask = MaxPooling3D(padding='same')(output_soft_mask) 66 | for _ in range(r): 67 | output_soft_mask = residual_block(output_soft_mask) 68 | 69 | ## decoder 70 | skip_connections = list(reversed(skip_connections)) 71 | for i in range(encoder_depth - 1): 72 | ## upsampling 73 | for _ in range(r): 74 | output_soft_mask = residual_block(output_soft_mask) 75 | output_soft_mask = UpSampling3D()(output_soft_mask) 76 | ## skip connections 77 | output_soft_mask = add([output_soft_mask, skip_connections[i]]) 78 | 79 | ### last upsampling 80 | for i in range(r): 81 | output_soft_mask = residual_block(output_soft_mask) 82 | output_soft_mask = UpSampling3D()(output_soft_mask) 83 | 84 | ## Output 85 | output_soft_mask = Conv3D(input_channels, (1, 1, 1))(output_soft_mask) 86 | output_soft_mask = Conv3D(input_channels, (1, 1, 1))(output_soft_mask) 87 | output_soft_mask = Activation('sigmoid')(output_soft_mask) 88 | 89 | # Attention: (1 + output_soft_mask) * output_trunk 90 | output = Lambda(lambda x: x + 1)(output_soft_mask) 91 | output = Multiply()([output, output_trunk]) # 92 | 93 | # Last Residual Block 94 | for i in range(p): 95 | output = residual_block(output, name=name) 96 | 97 | return output 98 | 99 | 100 | def residual_block(input, input_channels=None, output_channels=None, kernel_size=(3, 3, 3), stride=1, name='out'): 101 | """ 102 | full pre-activation residual block 103 | https://arxiv.org/pdf/1603.05027.pdf 104 | """ 105 | if output_channels is None: 106 | output_channels = input.get_shape()[-1].value 107 | if input_channels is None: 108 | input_channels = output_channels // 4 109 | 110 | strides = (stride, stride, stride) 111 | 112 | x = BatchNormalization()(input) 113 | x = Activation('relu')(x) 114 | x = Conv3D(input_channels, (1, 1, 1))(x) 115 | 116 | x = BatchNormalization()(x) 117 | x = Activation('relu')(x) 118 | x = Conv3D(input_channels, kernel_size, padding='same', strides=stride)(x) 119 | 120 | x = BatchNormalization()(x) 121 | x = Activation('relu')(x) 122 | x = Conv3D(output_channels, (1, 1, 1), padding='same')(x) 123 | 124 | if input_channels != output_channels or stride != 1: 125 | input = Conv3D(output_channels, (1, 1, 1), padding='same', strides=strides)(input) 126 | if name == 'out': 127 | x = add([x, input]) 128 | else: 129 | x = add([x, input], name=name) 130 | return x 131 | 132 | 133 | def build_brain_tumor_res_atten_unet_3d(input_shape, filter_num=8, merge_axis=-1): 134 | data = Input(shape=input_shape) 135 | pool_size = (2, 2, 2) 136 | up_size = (2, 2, 2) 137 | conv1 = Conv3D(filter_num * 4, 3, padding='same')(data) 138 | conv1 = BatchNormalization()(conv1) 139 | conv1 = Activation('relu')(conv1) 140 | # conv1 = Dropout(0.5)(conv1) 141 | 142 | pool = MaxPooling3D(pool_size=pool_size)(conv1) 143 | 144 | res1 = residual_block(pool, output_channels=filter_num * 8) 145 | # res1 = Dropout(0.5)(res1) 146 | 147 | pool1 = MaxPooling3D(pool_size=pool_size)(res1) 148 | 149 | res2 = residual_block(pool1, output_channels=filter_num * 16) 150 | # res2 = Dropout(0.5)(res2) 151 | 152 | pool2 = MaxPooling3D(pool_size=pool_size)(res2) 153 | 154 | res3 = residual_block(pool2, output_channels=filter_num * 32) 155 | # res3 = Dropout(0.5)(res3) 156 | 157 | pool3 = MaxPooling3D(pool_size=pool_size)(res3) 158 | 159 | res4 = residual_block(pool3, output_channels=filter_num * 64) 160 | # res4 = Dropout(0.5)(res4) 161 | 162 | pool4 = MaxPooling3D(pool_size=pool_size)(res4) 163 | 164 | res5 = residual_block(pool4, output_channels=filter_num * 64) 165 | res5 = residual_block(res5, output_channels=filter_num * 64) 166 | 167 | atb5 = attention_block(res4, encoder_depth=1, name='atten1') 168 | up1 = UpSampling3D(size=up_size)(res5) 169 | merged1 = concatenate([up1, atb5], axis=merge_axis) 170 | 171 | res5 = residual_block(merged1, output_channels=filter_num * 64) 172 | # res5 = Dropout(0.5)(res5) 173 | 174 | atb6 = attention_block(res3, encoder_depth=2, name='atten2') 175 | up2 = UpSampling3D(size=up_size)(res5) 176 | merged2 = concatenate([up2, atb6], axis=merge_axis) 177 | 178 | res6 = residual_block(merged2, output_channels=filter_num * 32) 179 | # res6 = Dropout(0.5)(res6) 180 | 181 | atb7 = attention_block(res2, encoder_depth=3, name='atten3') 182 | up3 = UpSampling3D(size=up_size)(res6) 183 | merged3 = concatenate([up3, atb7], axis=merge_axis) 184 | 185 | res7 = residual_block(merged3, output_channels=filter_num * 16) 186 | # res7 = Dropout(0.5)(res7) 187 | 188 | atb8 = attention_block(res1, encoder_depth=4, name='atten4') 189 | up4 = UpSampling3D(size=up_size)(res7) 190 | merged4 = concatenate([up4, atb8], axis=merge_axis) 191 | 192 | res8 = residual_block(merged4, output_channels=filter_num * 8) 193 | # res8 = Dropout(0.5)(res8) 194 | 195 | up = UpSampling3D(size=up_size)(res8) 196 | merged = concatenate([up, conv1], axis=merge_axis) 197 | conv9 = Conv3D(filter_num * 4, 3, padding='same')(merged) 198 | conv9 = BatchNormalization()(conv9) 199 | conv9 = Activation('relu')(conv9) 200 | # conv9 = Dropout(0.5)(conv9) 201 | 202 | output = Conv3D(1, 3, padding='same', activation='sigmoid')(conv9) 203 | model = Model(data, output) 204 | return model 205 | 206 | 207 | # liver network do not modify 208 | def build_res_atten_unet_3d(input_shape, filter_num=8, merge_axis=-1, pool_size=(2, 2, 2) 209 | , up_size=(2, 2, 2)): 210 | data = Input(shape=input_shape) 211 | 212 | conv1 = Conv3D(filter_num * 4, 3, padding='same')(data) 213 | conv1 = BatchNormalization()(conv1) 214 | conv1 = Activation('relu')(conv1) 215 | 216 | pool = MaxPooling3D(pool_size=pool_size)(conv1) 217 | 218 | res1 = residual_block(pool, output_channels=filter_num * 4) 219 | 220 | pool1 = MaxPooling3D(pool_size=pool_size)(res1) 221 | 222 | res2 = residual_block(pool1, output_channels=filter_num * 8) 223 | 224 | pool2 = MaxPooling3D(pool_size=pool_size)(res2) 225 | 226 | res3 = residual_block(pool2, output_channels=filter_num * 16) 227 | pool3 = MaxPooling3D(pool_size=pool_size)(res3) 228 | 229 | res4 = residual_block(pool3, output_channels=filter_num * 32) 230 | 231 | pool4 = MaxPooling3D(pool_size=pool_size)(res4) 232 | 233 | res5 = residual_block(pool4, output_channels=filter_num * 64) 234 | res5 = residual_block(res5, output_channels=filter_num * 64) 235 | 236 | atb5 = attention_block(res4, encoder_depth=1, name='atten1') 237 | up1 = UpSampling3D(size=up_size)(res5) 238 | merged1 = concatenate([up1, atb5], axis=merge_axis) 239 | 240 | res5 = residual_block(merged1, output_channels=filter_num * 32) 241 | 242 | atb6 = attention_block(res3, encoder_depth=2, name='atten2') 243 | up2 = UpSampling3D(size=up_size)(res5) 244 | merged2 = concatenate([up2, atb6], axis=merge_axis) 245 | 246 | res6 = residual_block(merged2, output_channels=filter_num * 16) 247 | atb7 = attention_block(res2, encoder_depth=3, name='atten3') 248 | up3 = UpSampling3D(size=up_size)(res6) 249 | merged3 = concatenate([up3, atb7], axis=merge_axis) 250 | 251 | res7 = residual_block(merged3, output_channels=filter_num * 8) 252 | atb8 = attention_block(res1, encoder_depth=4, name='atten4') 253 | up4 = UpSampling3D(size=up_size)(res7) 254 | merged4 = concatenate([up4, atb8], axis=merge_axis) 255 | 256 | res8 = residual_block(merged4, output_channels=filter_num * 4) 257 | up = UpSampling3D(size=up_size)(res8) 258 | merged = concatenate([up, conv1], axis=merge_axis) 259 | conv9 = Conv3D(filter_num * 4, 3, padding='same')(merged) 260 | conv9 = BatchNormalization()(conv9) 261 | conv9 = Activation('relu')(conv9) 262 | 263 | output = Conv3D(1, 3, padding='same', activation='sigmoid')(conv9) 264 | model = Model(data, output) 265 | return model 266 | 267 | 268 | # ============================================================ 269 | # ======================Attention ResUnet 2D================================# 270 | # ============================================================ 271 | 272 | 273 | def attention_block_2d(input, input_channels=None, output_channels=None, encoder_depth=1, name='at'): 274 | """ 275 | attention block 276 | https://arxiv.org/abs/1704.06904 277 | """ 278 | p = 1 279 | t = 2 280 | r = 1 281 | 282 | if input_channels is None: 283 | input_channels = input.get_shape()[-1].value 284 | if output_channels is None: 285 | output_channels = input_channels 286 | 287 | # First Residual Block 288 | for i in range(p): 289 | input = residual_block_2d(input) 290 | 291 | # Trunc Branch 292 | output_trunk = input 293 | for i in range(t): 294 | output_trunk = residual_block_2d(output_trunk) 295 | 296 | # Soft Mask Branch 297 | 298 | ## encoder 299 | ### first down sampling 300 | output_soft_mask = MaxPooling2D(padding='same')(input) # 32x32 301 | for i in range(r): 302 | output_soft_mask = residual_block_2d(output_soft_mask) 303 | 304 | skip_connections = [] 305 | for i in range(encoder_depth - 1): 306 | 307 | ## skip connections 308 | output_skip_connection = residual_block_2d(output_soft_mask) 309 | skip_connections.append(output_skip_connection) 310 | 311 | ## down sampling 312 | output_soft_mask = MaxPooling2D(padding='same')(output_soft_mask) 313 | for _ in range(r): 314 | output_soft_mask = residual_block_2d(output_soft_mask) 315 | 316 | ## decoder 317 | skip_connections = list(reversed(skip_connections)) 318 | for i in range(encoder_depth - 1): 319 | ## upsampling 320 | for _ in range(r): 321 | output_soft_mask = residual_block_2d(output_soft_mask) 322 | output_soft_mask = UpSampling2D()(output_soft_mask) 323 | ## skip connections 324 | output_soft_mask = add([output_soft_mask, skip_connections[i]]) 325 | 326 | ### last upsampling 327 | for i in range(r): 328 | output_soft_mask = residual_block_2d(output_soft_mask) 329 | output_soft_mask = UpSampling2D()(output_soft_mask) 330 | 331 | ## Output 332 | output_soft_mask = Conv2D(input_channels, (1, 1))(output_soft_mask) 333 | output_soft_mask = Conv2D(input_channels, (1, 1))(output_soft_mask) 334 | output_soft_mask = Activation('sigmoid')(output_soft_mask) 335 | 336 | # Attention: (1 + output_soft_mask) * output_trunk 337 | output = Lambda(lambda x: x + 1)(output_soft_mask) 338 | output = Multiply()([output, output_trunk]) # 339 | 340 | # Last Residual Block 341 | for i in range(p): 342 | output = residual_block_2d(output, name=name) 343 | 344 | return output 345 | 346 | 347 | def residual_block_2d(input, input_channels=None, output_channels=None, kernel_size=(3, 3), stride=1, name='out'): 348 | """ 349 | full pre-activation residual block 350 | https://arxiv.org/pdf/1603.05027.pdf 351 | """ 352 | if output_channels is None: 353 | output_channels = input.get_shape()[-1].value 354 | if input_channels is None: 355 | input_channels = output_channels // 4 356 | 357 | strides = (stride, stride) 358 | 359 | x = BatchNormalization()(input) 360 | x = Activation('relu')(x) 361 | x = Conv2D(input_channels, (1, 1))(x) 362 | 363 | x = BatchNormalization()(x) 364 | x = Activation('relu')(x) 365 | x = Conv2D(input_channels, kernel_size, padding='same', strides=stride)(x) 366 | 367 | x = BatchNormalization()(x) 368 | x = Activation('relu')(x) 369 | x = Conv2D(output_channels, (1, 1), padding='same')(x) 370 | 371 | if input_channels != output_channels or stride != 1: 372 | input = Conv2D(output_channels, (1, 1), padding='same', strides=strides)(input) 373 | if name == 'out': 374 | x = add([x, input]) 375 | else: 376 | x = add([x, input], name=name) 377 | return x 378 | 379 | 380 | def build_res_atten_unet_2d(input_shape, filter_num=8): 381 | merge_axis = -1 # Feature maps are concatenated along last axis (for tf backend) 382 | data = Input(shape=input_shape) 383 | 384 | conv1 = Conv2D(filter_num * 4, 3, padding='same')(data) 385 | conv1 = BatchNormalization()(conv1) 386 | conv1 = Activation('relu')(conv1) 387 | 388 | # res0 = residual_block_2d(data, output_channels=filter_num * 2) 389 | 390 | pool = MaxPooling2D(pool_size=(2, 2))(conv1) 391 | 392 | res1 = residual_block_2d(pool, output_channels=filter_num * 4) 393 | 394 | # res1 = residual_block_2d(atb1, output_channels=filter_num * 4) 395 | 396 | pool1 = MaxPooling2D(pool_size=(2, 2))(res1) 397 | # pool1 = MaxPooling2D(pool_size=(2, 2))(atb1) 398 | 399 | res2 = residual_block_2d(pool1, output_channels=filter_num * 8) 400 | 401 | # res2 = residual_block_2d(atb2, output_channels=filter_num * 8) 402 | pool2 = MaxPooling2D(pool_size=(2, 2))(res2) 403 | # pool2 = MaxPooling2D(pool_size=(2, 2))(atb2) 404 | 405 | res3 = residual_block_2d(pool2, output_channels=filter_num * 16) 406 | # res3 = residual_block_2d(atb3, output_channels=filter_num * 16) 407 | pool3 = MaxPooling2D(pool_size=(2, 2))(res3) 408 | # pool3 = MaxPooling2D(pool_size=(2, 2))(atb3) 409 | 410 | res4 = residual_block_2d(pool3, output_channels=filter_num * 32) 411 | 412 | # res4 = residual_block_2d(atb4, output_channels=filter_num * 32) 413 | pool4 = MaxPooling2D(pool_size=(2, 2))(res4) 414 | # pool4 = MaxPooling2D(pool_size=(2, 2))(atb4) 415 | 416 | res5 = residual_block_2d(pool4, output_channels=filter_num * 64) 417 | # res5 = residual_block_2d(res5, output_channels=filter_num * 64) 418 | res5 = residual_block_2d(res5, output_channels=filter_num * 64) 419 | 420 | atb5 = attention_block_2d(res4, encoder_depth=1, name='atten1') 421 | up1 = UpSampling2D(size=(2, 2))(res5) 422 | merged1 = concatenate([up1, atb5], axis=merge_axis) 423 | # merged1 = concatenate([up1, atb4], axis=merge_axis) 424 | 425 | res5 = residual_block_2d(merged1, output_channels=filter_num * 32) 426 | # atb5 = attention_block_2d(res5, encoder_depth=1) 427 | 428 | atb6 = attention_block_2d(res3, encoder_depth=2, name='atten2') 429 | up2 = UpSampling2D(size=(2, 2))(res5) 430 | # up2 = UpSampling2D(size=(2, 2))(atb5) 431 | merged2 = concatenate([up2, atb6], axis=merge_axis) 432 | # merged2 = concatenate([up2, atb3], axis=merge_axis) 433 | 434 | res6 = residual_block_2d(merged2, output_channels=filter_num * 16) 435 | # atb6 = attention_block_2d(res6, encoder_depth=2) 436 | 437 | # atb6 = attention_block_2d(res6, encoder_depth=2) 438 | atb7 = attention_block_2d(res2, encoder_depth=3, name='atten3') 439 | up3 = UpSampling2D(size=(2, 2))(res6) 440 | # up3 = UpSampling2D(size=(2, 2))(atb6) 441 | merged3 = concatenate([up3, atb7], axis=merge_axis) 442 | # merged3 = concatenate([up3, atb2], axis=merge_axis) 443 | 444 | res7 = residual_block_2d(merged3, output_channels=filter_num * 8) 445 | # atb7 = attention_block_2d(res7, encoder_depth=3) 446 | 447 | # atb7 = attention_block_2d(res7, encoder_depth=3) 448 | atb8 = attention_block_2d(res1, encoder_depth=4, name='atten4') 449 | up4 = UpSampling2D(size=(2, 2))(res7) 450 | # up4 = UpSampling2D(size=(2, 2))(atb7) 451 | merged4 = concatenate([up4, atb8], axis=merge_axis) 452 | # merged4 = concatenate([up4, atb1], axis=merge_axis) 453 | 454 | res8 = residual_block_2d(merged4, output_channels=filter_num * 4) 455 | # atb8 = attention_block_2d(res8, encoder_depth=4) 456 | 457 | # atb8 = attention_block_2d(res8, encoder_depth=4) 458 | up = UpSampling2D(size=(2, 2))(res8) 459 | # up = UpSampling2D(size=(2, 2))(atb8) 460 | merged = concatenate([up, conv1], axis=merge_axis) 461 | # res9 = residual_block_2d(merged, output_channels=filter_num * 2) 462 | 463 | conv9 = Conv2D(filter_num * 4, 3, padding='same')(merged) 464 | conv9 = BatchNormalization()(conv9) 465 | conv9 = Activation('relu')(conv9) 466 | 467 | output = Conv2D(1, 3, padding='same', activation='sigmoid')(conv9) 468 | model = Model(data, output) 469 | return model 470 | -------------------------------------------------------------------------------- /configure.py: -------------------------------------------------------------------------------- 1 | cfg = { 2 | 'log_file': 'Demo/log' 3 | } 4 | -------------------------------------------------------------------------------- /evaluation_code/output/scores.txt: -------------------------------------------------------------------------------- 1 | lesion_precision: 0.667 2 | lesion_recall: 0.667 3 | lesion_precision_greater_zero: 0.958 4 | lesion_recall_greater_zero: 0.958 5 | lesion_dice: 0.996 6 | lesion_jaccard: 0.992 7 | lesion_voe: 0.008 8 | lesion_rvd: 0.009 9 | lesion_assd: 0.018 10 | lesion_rmsd: 0.053 11 | lesion_msd: 0.302 12 | lesion_dice_per_case: 0.669 13 | lesion_dice_global: 0.682 14 | liver_dice: 0.980 15 | liver_jaccard: 0.961 16 | liver_voe: 0.039 17 | liver_rvd: -0.039 18 | liver_assd: 0.709 19 | liver_rmsd: 3.672 20 | liver_msd: 64.493 21 | liver_dice_per_case: 0.980 22 | liver_dice_global: 0.990 23 | RMSE_Tumorburden: 0.013 24 | MAXERROR_Tumorburden: 0.001 25 | -------------------------------------------------------------------------------- /evaluation_code/program/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RanSuLab/RAUNet-tumor-segmentation/8856353e45746796dba80e8d0ec182e75a444c9d/evaluation_code/program/__init__.py -------------------------------------------------------------------------------- /evaluation_code/program/evaluate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import sys 4 | import os 5 | import nibabel as nb 6 | import numpy as np 7 | from scipy.ndimage.measurements import label as label_connected_components 8 | import glob 9 | import gc 10 | import pandas as pd 11 | from helpers.calc_metric import (dice, 12 | detect_lesions, 13 | compute_segmentation_scores, 14 | compute_tumor_burden, 15 | LARGE) 16 | from helpers.utils import time_elapsed 17 | 18 | # Check input directories. 19 | csv_path = os.path.join('/home01/weileyi/jinqiangguo/jqg/py3EnvRoad/lung-segmentation-3d/Demo/',sys.argv[1]) 20 | print(csv_path) 21 | # Create output directory. 22 | output_dir = sys.argv[2] 23 | if not os.path.exists(output_dir): 24 | os.makedirs(output_dir) 25 | 26 | # Segmentation metrics and their default values for when there are no detected 27 | # objects on which to evaluate them. 28 | # 29 | # Surface distance (and volume difference) metrics between two masks are 30 | # meaningless when any one of the masks is empty. Assign maximum (infinite) 31 | # penalty. The average score for these metrics, over all objects, will thus 32 | # also not be finite as it also loses meaning. 33 | segmentation_metrics = {'dice': 0, 34 | 'jaccard': 0, 35 | 'voe': 1, 36 | 'rvd': LARGE, 37 | 'assd': LARGE, 38 | 'rmsd': LARGE, 39 | 'msd': LARGE} 40 | 41 | # Initialize results dictionaries 42 | lesion_detection_stats = {0: {'TP': 0, 'FP': 0, 'FN': 0}, 43 | 0.5: {'TP': 0, 'FP': 0, 'FN': 0}} 44 | lesion_segmentation_scores = {} 45 | liver_segmentation_scores = {} 46 | dice_per_case = {'lesion': [], 'liver': []} 47 | dice_global_x = {'lesion': {'I': 0, 'S': 0}, 48 | 'liver': {'I': 0, 'S': 0}} # 2*I/S 49 | tumor_burden_list = [] 50 | # Iterate over all volumes in the reference list. 51 | df = pd.read_csv(csv_path) 52 | for i, item in df.iterrows(): 53 | print("Found corresponding submission file {} for reference file {}" 54 | "".format(item[3], item[5])) 55 | t = time_elapsed() 56 | 57 | # Load reference and submission volumes with Nibabel. 58 | reference_volume = nb.load('/home01/weileyi/jinqiangguo/jqg/py3EnvRoad/lung-segmentation-3d/'+item[3]) 59 | submission_volume = nb.load('/home01/weileyi/jinqiangguo/jqg/py3EnvRoad/lung-segmentation-3d/'+item[5]) 60 | 61 | # Get the current voxel spacing. 62 | voxel_spacing = reference_volume.header.get_zooms()[:3] 63 | 64 | # Get Numpy data and compress to int8. 65 | reference_volume = (reference_volume.get_data()).astype(np.int8) 66 | submission_volume = (submission_volume.get_data()).astype(np.int8) 67 | 68 | # Ensure that the shapes of the masks match. 69 | if submission_volume.shape!=reference_volume.shape: 70 | raise AttributeError("Shapes do not match! Prediction mask {}, " 71 | "ground truth mask {}" 72 | "".format(submission_volume.shape, 73 | reference_volume.shape)) 74 | print("Done loading files ({:.2f} seconds)".format(t())) 75 | 76 | # Create lesion and liver masks with labeled connected components. 77 | # (Assuming there is always exactly one liver - one connected comp.) 78 | pred_mask_lesion, num_predicted = label_connected_components( \ 79 | submission_volume==2, output=np.int16) 80 | true_mask_lesion, num_reference = label_connected_components( \ 81 | reference_volume==2, output=np.int16) 82 | pred_mask_liver = submission_volume>=1 83 | true_mask_liver = reference_volume>=1 84 | liver_prediction_exists = np.any(submission_volume==1) 85 | print("Done finding connected components ({:.2f} seconds)".format(t())) 86 | 87 | # Identify detected lesions. 88 | # Retain detected_mask_lesion for overlap > 0.5 89 | for overlap in [0, 0.5]: 90 | detected_mask_lesion, mod_ref_mask, num_detected = detect_lesions( \ 91 | prediction_mask=pred_mask_lesion, 92 | reference_mask=true_mask_lesion, 93 | min_overlap=overlap) 94 | 95 | # Count true/false positive and false negative detections. 96 | lesion_detection_stats[overlap]['TP']+=num_detected 97 | lesion_detection_stats[overlap]['FP']+=num_predicted-num_detected 98 | lesion_detection_stats[overlap]['FN']+=num_reference-num_detected 99 | print("Done identifying detected lesions ({:.2f} seconds)".format(t())) 100 | 101 | # Compute segmentation scores for DETECTED lesions. 102 | if num_detected>0: 103 | lesion_scores = compute_segmentation_scores( \ 104 | prediction_mask=detected_mask_lesion, 105 | reference_mask=mod_ref_mask, 106 | voxel_spacing=voxel_spacing) 107 | for metric in segmentation_metrics: 108 | if metric not in lesion_segmentation_scores: 109 | lesion_segmentation_scores[metric] = [] 110 | lesion_segmentation_scores[metric].extend(lesion_scores[metric]) 111 | print("Done computing lesion scores ({:.2f} seconds)".format(t())) 112 | else: 113 | print("No lesions detected, skipping lesion score evaluation") 114 | 115 | # Compute liver segmentation scores. 116 | if liver_prediction_exists: 117 | liver_scores = compute_segmentation_scores( \ 118 | prediction_mask=pred_mask_liver, 119 | reference_mask=true_mask_liver, 120 | voxel_spacing=voxel_spacing) 121 | for metric in segmentation_metrics: 122 | if metric not in liver_segmentation_scores: 123 | liver_segmentation_scores[metric] = [] 124 | liver_segmentation_scores[metric].extend(liver_scores[metric]) 125 | print("Done computing liver scores ({:.2f} seconds)".format(t())) 126 | else: 127 | # No liver label. Record default score values (zeros, inf). 128 | # NOTE: This will make some metrics evaluate to inf over the entire 129 | # dataset. 130 | for metric in segmentation_metrics: 131 | if metric not in liver_segmentation_scores: 132 | liver_segmentation_scores[metric] = [] 133 | liver_segmentation_scores[metric].append(\ 134 | segmentation_metrics[metric]) 135 | print("No liver label provided, skipping liver score evaluation") 136 | 137 | # Compute per-case (per patient volume) dice. 138 | if not np.any(pred_mask_lesion) and not np.any(true_mask_lesion): 139 | dice_per_case['lesion'].append(1.) 140 | else: 141 | dice_per_case['lesion'].append(dice(pred_mask_lesion, 142 | true_mask_lesion)) 143 | if liver_prediction_exists: 144 | dice_per_case['liver'].append(dice(pred_mask_liver, 145 | true_mask_liver)) 146 | else: 147 | dice_per_case['liver'].append(0) 148 | 149 | # Accumulate stats for global (dataset-wide) dice score. 150 | dice_global_x['lesion']['I'] += np.count_nonzero( \ 151 | np.logical_and(pred_mask_lesion, true_mask_lesion)) 152 | dice_global_x['lesion']['S'] += np.count_nonzero(pred_mask_lesion) + \ 153 | np.count_nonzero(true_mask_lesion) 154 | if liver_prediction_exists: 155 | dice_global_x['liver']['I'] += np.count_nonzero( \ 156 | np.logical_and(pred_mask_liver, true_mask_liver)) 157 | dice_global_x['liver']['S'] += np.count_nonzero(pred_mask_liver) + \ 158 | np.count_nonzero(true_mask_liver) 159 | else: 160 | # NOTE: This value should never be zero. 161 | dice_global_x['liver']['S'] += np.count_nonzero(true_mask_liver) 162 | 163 | 164 | print("Done computing additional dice scores ({:.2f} seconds)" 165 | "".format(t())) 166 | 167 | # Compute tumor burden. 168 | tumor_burden = compute_tumor_burden(prediction_mask=submission_volume, 169 | reference_mask=reference_volume) 170 | tumor_burden_list.append(tumor_burden) 171 | print("Done computing tumor burden diff ({:.2f} seconds)".format(t())) 172 | 173 | print("Done processing volume (total time: {:.2f} seconds)" 174 | "".format(t.total_elapsed())) 175 | gc.collect() 176 | 177 | 178 | # Compute lesion detection metrics. 179 | _det = {} 180 | for overlap in [0, 0.5]: 181 | TP = lesion_detection_stats[overlap]['TP'] 182 | FP = lesion_detection_stats[overlap]['FP'] 183 | FN = lesion_detection_stats[overlap]['FN'] 184 | precision = float(TP)/(TP+FP) if TP+FP else 0 185 | recall = float(TP)/(TP+FN) if TP+FN else 0 186 | _det[overlap] = {'p': precision, 'r': recall} 187 | lesion_detection_metrics = {'precision': _det[0.5]['p'], 188 | 'recall': _det[0.5]['r'], 189 | 'precision_greater_zero': _det[0]['p'], 190 | 'recall_greater_zero': _det[0]['r']} 191 | 192 | # Compute lesion segmentation metrics. 193 | lesion_segmentation_metrics = {} 194 | for m in lesion_segmentation_scores: 195 | lesion_segmentation_metrics[m] = np.mean(lesion_segmentation_scores[m]) 196 | if len(lesion_segmentation_scores)==0: 197 | # Nothing detected - set default values. 198 | lesion_segmentation_metrics.update(segmentation_metrics) 199 | lesion_segmentation_metrics['dice_per_case'] = np.mean(dice_per_case['lesion']) 200 | dice_global = 2.*dice_global_x['lesion']['I']/dice_global_x['lesion']['S'] 201 | lesion_segmentation_metrics['dice_global'] = dice_global 202 | 203 | # Compute liver segmentation metrics. 204 | liver_segmentation_metrics = {} 205 | for m in liver_segmentation_scores: 206 | liver_segmentation_metrics[m] = np.mean(liver_segmentation_scores[m]) 207 | if len(liver_segmentation_scores)==0: 208 | # Nothing detected - set default values. 209 | liver_segmentation_metrics.update(segmentation_metrics) 210 | liver_segmentation_metrics['dice_per_case'] = np.mean(dice_per_case['liver']) 211 | dice_global = 2.*dice_global_x['liver']['I']/dice_global_x['liver']['S'] 212 | liver_segmentation_metrics['dice_global'] = dice_global 213 | 214 | # Compute tumor burden. 215 | tumor_burden_rmse = np.sqrt(np.mean(np.square(tumor_burden_list))) 216 | tumor_burden_max = np.max(tumor_burden_list) 217 | 218 | 219 | # Print results to stdout. 220 | print("Computed LESION DETECTION metrics:") 221 | for metric, value in lesion_detection_metrics.items(): 222 | print("{}: {:.3f}".format(metric, float(value))) 223 | print("Computed LESION SEGMENTATION metrics (for detected lesions):") 224 | for metric, value in lesion_segmentation_metrics.items(): 225 | print("{}: {:.3f}".format(metric, float(value))) 226 | print("Computed LIVER SEGMENTATION metrics:") 227 | for metric, value in liver_segmentation_metrics.items(): 228 | print("{}: {:.3f}".format(metric, float(value))) 229 | print("Computed TUMOR BURDEN: \n" 230 | "rmse: {:.3f}\nmax: {:.3f}".format(tumor_burden_rmse, tumor_burden_max)) 231 | 232 | # Write metrics to file. 233 | output_filename = os.path.join(output_dir, 'scores.txt') 234 | output_file = open(output_filename, 'w') 235 | for metric, value in lesion_detection_metrics.items(): 236 | output_file.write("lesion_{}: {:.3f}\n".format(metric, float(value))) 237 | for metric, value in lesion_segmentation_metrics.items(): 238 | output_file.write("lesion_{}: {:.3f}\n".format(metric, float(value))) 239 | for metric, value in liver_segmentation_metrics.items(): 240 | output_file.write("liver_{}: {:.3f}\n".format(metric, float(value))) 241 | 242 | #Tumorburden 243 | output_file.write("RMSE_Tumorburden: {:.3f}\n".format(tumor_burden_rmse)) 244 | output_file.write("MAXERROR_Tumorburden: {:.3f}\n".format(tumor_burden_max)) 245 | 246 | output_file.close() 247 | -------------------------------------------------------------------------------- /evaluation_code/program/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RanSuLab/RAUNet-tumor-segmentation/8856353e45746796dba80e8d0ec182e75a444c9d/evaluation_code/program/helpers/__init__.py -------------------------------------------------------------------------------- /evaluation_code/program/helpers/calc_metric.py: -------------------------------------------------------------------------------- 1 | from medpy import metric 2 | import numpy as np 3 | from scipy import ndimage 4 | import time 5 | 6 | from .surface import Surface 7 | 8 | 9 | LARGE = 9001 10 | 11 | 12 | def dice(input1, input2): 13 | return metric.dc(input1, input2) 14 | 15 | 16 | def detect_lesions(prediction_mask, reference_mask, min_overlap=0.5): 17 | """ 18 | Produces a mask for predicted lesions and a mask for reference lesions, 19 | with label IDs matching lesions together. 20 | 21 | Given a prediction and a reference mask, output a modified version of 22 | each where objects that overlap between the two mask share a label. This 23 | requires merging labels in the reference mask that are spanned by a single 24 | prediction and merging labels in the prediction mask that are spanned by 25 | a single reference. In cases where a label can be merged, separately, with 26 | more than one other label, a single merge option (label) is chosen to 27 | accord the greatest overlap between the reference and prediction objects. 28 | 29 | After merging and matching, objects in the reference are considered 30 | detected if their respective predictions overlap them by more than 31 | `min_overlap` (intersection over union). 32 | 33 | :param prediction_mask: numpy.array 34 | :param reference_mask: numpy.array 35 | :param min_overlap: float in range [0, 1.] 36 | :return: prediction mask (int), 37 | reference mask (int), 38 | num_detected 39 | """ 40 | 41 | # Initialize 42 | detected_mask = np.zeros(prediction_mask.shape, dtype=np.uint8) 43 | mod_reference_mask = np.copy(reference_mask) 44 | num_detected = 0 45 | if not np.any(reference_mask): 46 | return detected_mask, num_detected, 0 47 | 48 | if not min_overlap>0 and not min_overlap<=1: 49 | raise ValueError("min_overlap must be in [0, 1.]") 50 | 51 | # Get available IDs (excluding 0) 52 | # 53 | # To reduce computation time, check only those lesions in the prediction 54 | # that have any overlap with the ground truth. 55 | p_id_list = np.unique(prediction_mask[reference_mask.nonzero()]) 56 | if p_id_list[0]==0: 57 | p_id_list = p_id_list[1:] 58 | g_id_list = np.unique(reference_mask) 59 | if g_id_list[0]==0: 60 | g_id_list = g_id_list[1:] 61 | 62 | # To reduce computation time, get views into reduced size masks. 63 | reduced_prediction_mask = rpm = prediction_mask.copy() 64 | for p_id in np.unique(prediction_mask): 65 | if p_id not in p_id_list and p_id!=0: 66 | reduced_prediction_mask[(rpm==p_id).nonzero()] = 0 67 | target_mask = np.logical_or(reference_mask, reduced_prediction_mask) 68 | bounding_box = ndimage.find_objects(target_mask)[0] 69 | r = reference_mask[bounding_box] 70 | p = prediction_mask[bounding_box] 71 | d = detected_mask[bounding_box] 72 | m = mod_reference_mask[bounding_box] 73 | 74 | # Compute intersection of predicted lesions with reference lesions. 75 | intersection_matrix = np.zeros((len(p_id_list), len(g_id_list)), 76 | dtype=np.int32) 77 | for i, p_id in enumerate(p_id_list): 78 | for j, g_id in enumerate(g_id_list): 79 | intersection = np.count_nonzero(np.logical_and(p==p_id, r==g_id)) 80 | intersection_matrix[i, j] = intersection 81 | 82 | def sum_dims(x, axis, dims): 83 | ''' 84 | Given an array x, collapses dimensions listed in dims along the 85 | specified axis, summing them together. Returns the reduced array. 86 | ''' 87 | x = np.array(x) 88 | if len(dims) < 2: 89 | return x 90 | 91 | # Initialize output 92 | new_shape = list(x.shape) 93 | new_shape[axis] -= len(dims)-1 94 | x_ret = np.zeros(new_shape, dtype=x.dtype) 95 | 96 | # Sum over dims on axis 97 | sum_slices = [slice(None)]*x.ndim 98 | sum_slices[axis] = dims 99 | dim_sum = np.sum(x[sum_slices], axis=axis, keepdims=True) 100 | 101 | # Remove all but first dim in dims 102 | mask = np.ones(x.shape, dtype=np.bool) 103 | mask_slices = [slice(None)]*x.ndim 104 | mask_slices[axis] = dims[1:] 105 | mask[mask_slices] = 0 106 | x_ret.ravel()[...] = x[mask] 107 | 108 | # Put dim_sum into array at first dim 109 | replace_slices = [slice(None)]*x.ndim 110 | replace_slices[axis] = [dims[0]] 111 | x_ret[replace_slices] = dim_sum 112 | 113 | return x_ret 114 | 115 | # Merge and label reference lesions that are connected by predicted 116 | # lesions. 117 | g_merge_count = dict([(g_id, 1) for g_id in g_id_list]) 118 | for i, p_id in enumerate(p_id_list): 119 | # Identify g_id intersected by p_id 120 | g_id_indices = intersection_matrix[i].nonzero()[0] 121 | g_id_intersected = g_id_list[g_id_indices] 122 | 123 | # Make sure g_id are matched to p_id deterministically regardless of 124 | # label order. Only merge those g_id which overlap this p_id more than 125 | # others. 126 | g_id_merge = [] 127 | g_id_merge_indices = [] 128 | for k, g_id in enumerate(g_id_intersected): 129 | idx = g_id_indices[k] 130 | if np.argmax(intersection_matrix[:, idx], axis=0)==i: 131 | # This g_id has the largest overlap with this p_id: merge. 132 | g_id_merge.append(g_id) 133 | g_id_merge_indices.append(idx) 134 | 135 | # Update merge count 136 | for g_id in g_id_merge: 137 | g_merge_count[g_id] = len(g_id_merge) 138 | 139 | # Merge. Update g_id_list, intersection matrix, mod_reference_mask. 140 | # Merge columns in intersection_matrix. 141 | g_id_list = np.delete(g_id_list, obj=g_id_merge_indices[1:]) 142 | for g_id in g_id_merge: 143 | m[m==g_id] = g_id_merge[0] 144 | intersection_matrix = sum_dims(intersection_matrix, 145 | axis=1, 146 | dims=g_id_merge_indices) 147 | 148 | # Match each predicted lesion to a single (merged) reference lesion. 149 | max_val = np.max(intersection_matrix, axis=1) 150 | max_indices = np.argmax(intersection_matrix, axis=1) 151 | intersection_matrix[...] = 0 152 | intersection_matrix[np.arange(len(p_id_list)), max_indices] = max_val 153 | 154 | # Merge and label predicted lesions that are connected by reference 155 | # lesions. 156 | # 157 | # Merge rows in intersection_matrix. 158 | # 159 | # Here, it's fine to merge all p_id that are connected by a g_id since 160 | # each p_id has already been associated with only one g_id. 161 | for j, g_id in enumerate(g_id_list): 162 | p_id_indices = intersection_matrix[:,j].nonzero()[0] 163 | p_id_intersected = p_id_list[p_id_indices] 164 | intersection_matrix = sum_dims(intersection_matrix, 165 | axis=0, 166 | dims=p_id_indices) 167 | p_id_list = np.delete(p_id_list, obj=p_id_indices[1:]) 168 | for p_id in p_id_intersected: 169 | d[p==p_id] = g_id 170 | 171 | # Trim away lesions deemed undetected. 172 | num_detected = len(p_id_list) 173 | for i, p_id in enumerate(p_id_list): 174 | for j, g_id in enumerate(g_id_list): 175 | intersection = intersection_matrix[i, j] 176 | if intersection==0: 177 | continue 178 | union = np.count_nonzero(np.logical_or(d==p_id, m==g_id)) 179 | overlap_fraction = float(intersection)/union 180 | if overlap_fraction <= min_overlap: 181 | d[d==g_id] = 0 # Assuming one-to-one p_id <--> g_id 182 | num_detected -= g_merge_count[g_id] 183 | 184 | return detected_mask, mod_reference_mask, num_detected 185 | 186 | 187 | def compute_tumor_burden(prediction_mask, reference_mask): 188 | """ 189 | Calculates the tumor_burden and evalutes the tumor burden metrics RMSE and 190 | max error. 191 | 192 | :param prediction_mask: numpy.array 193 | :param reference_mask: numpy.array 194 | :return: dict with RMSE and Max error 195 | """ 196 | def calc_tumor_burden(vol): 197 | num_liv_pix=np.count_nonzero(vol>=1) 198 | num_les_pix=np.count_nonzero(vol==2) 199 | return num_les_pix/float(num_liv_pix) 200 | tumor_burden_r = calc_tumor_burden(reference_mask) 201 | if np.count_nonzero(prediction_mask==1): 202 | tumor_burden_p = calc_tumor_burden(prediction_mask) 203 | else: 204 | tumor_burden_p = LARGE 205 | 206 | tumor_burden_diff = tumor_burden_r - tumor_burden_p 207 | return tumor_burden_diff 208 | 209 | 210 | def compute_segmentation_scores(prediction_mask, reference_mask, 211 | voxel_spacing): 212 | """ 213 | Calculates metrics scores from numpy arrays and returns an dict. 214 | 215 | Assumes that each object in the input mask has an integer label that 216 | defines object correspondence between prediction_mask and 217 | reference_mask. 218 | 219 | :param prediction_mask: numpy.array, int 220 | :param reference_mask: numpy.array, int 221 | :param voxel_spacing: list with x,y and z spacing 222 | :return: dict with dice, jaccard, voe, rvd, assd, rmsd, and msd 223 | """ 224 | 225 | scores = {'dice': [], 226 | 'jaccard': [], 227 | 'voe': [], 228 | 'rvd': [], 229 | 'assd': [], 230 | 'rmsd': [], 231 | 'msd': []} 232 | 233 | for i, obj_id in enumerate(np.unique(prediction_mask)): 234 | if obj_id==0: 235 | continue # 0 is background, not an object; skip 236 | 237 | # Limit processing to the bounding box containing both the prediction 238 | # and reference objects. 239 | target_mask = (reference_mask==obj_id)+(prediction_mask==obj_id) 240 | bounding_box = ndimage.find_objects(target_mask)[0] 241 | p = (prediction_mask==obj_id)[bounding_box] 242 | r = (reference_mask==obj_id)[bounding_box] 243 | if np.any(p) and np.any(r): 244 | dice = metric.dc(p,r) 245 | jaccard = dice/(2.-dice) 246 | scores['dice'].append(dice) 247 | scores['jaccard'].append(jaccard) 248 | scores['voe'].append(1.-jaccard) 249 | scores['rvd'].append(metric.ravd(r,p)) 250 | evalsurf = Surface(p, r, 251 | physical_voxel_spacing=voxel_spacing, 252 | mask_offset=[0.,0.,0.], 253 | reference_offset=[0.,0.,0.]) 254 | assd = evalsurf.get_average_symmetric_surface_distance() 255 | rmsd = evalsurf.get_root_mean_square_symmetric_surface_distance() 256 | msd = evalsurf.get_maximum_symmetric_surface_distance() 257 | scores['assd'].append(assd) 258 | scores['rmsd'].append(rmsd) 259 | scores['msd'].append(msd) 260 | else: 261 | # There are no objects in the prediction, in the reference, or both 262 | scores['dice'].append(0) 263 | scores['jaccard'].append(0) 264 | scores['voe'].append(1.) 265 | 266 | # Surface distance (and volume difference) metrics between the two 267 | # masks are meaningless when any one of the masks is empty. Assign 268 | # maximum penalty. The average score for these metrics, over all 269 | # objects, will thus also not be finite as it also loses meaning. 270 | scores['rvd'].append(LARGE) 271 | scores['assd'].append(LARGE) 272 | scores['rmsd'].append(LARGE) 273 | scores['msd'].append(LARGE) 274 | 275 | return scores 276 | -------------------------------------------------------------------------------- /evaluation_code/program/helpers/surface.py: -------------------------------------------------------------------------------- 1 | """ 2 | @package medpy.metric.surface 3 | Holds a metrics class computing surface metrics over two 3D-images contain each a binary object. 4 | 5 | Classes: 6 | - Surface: Computes different surface metrics between two 3D-images contain each an object. 7 | 8 | @author Oskar Maier 9 | @version r0.4.1 10 | @since 2011-12-01 11 | @status Release 12 | """ 13 | 14 | # build-in modules 15 | import math 16 | 17 | # third-party modules 18 | import scipy.spatial 19 | import scipy.ndimage.morphology 20 | 21 | # own modules 22 | 23 | # code 24 | class Surface(object): 25 | """ 26 | Computes different surface metrics between two 3D-images contain each an object. 27 | The surface of the objects is computed using a 18-neighbourhood edge detection. 28 | The distance metrics are computed over all points of the surfaces using the nearest 29 | neighbour approach. 30 | Beside this provides a number of statistics of the two images. 31 | 32 | During the initialization the edge detection is run for both images, taking up to 33 | 5 min (on 512^3 images). The first call to one of the metric measures triggers the 34 | computation of the nearest neighbours, taking up to 7 minutes (based on 250.000 edge 35 | point for each of the objects, which corresponds to a typical liver mask). All 36 | subsequent calls to one of the metrics measures can be expected be in the 37 | sub-millisecond area. 38 | 39 | Metrics defined in: 40 | Heimann, T.; van Ginneken, B.; Styner, M.A.; Arzhaeva, Y.; Aurich, V.; Bauer, C.; Beck, A.; Becker, C.; Beichel, R.; Bekes, G.; Bello, F.; Binnig, G.; Bischof, H.; Bornik, A.; Cashman, P.; Ying Chi; Cordova, A.; Dawant, B.M.; Fidrich, M.; Furst, J.D.; Furukawa, D.; Grenacher, L.; Hornegger, J.; Kainmuller, D.; Kitney, R.I.; Kobatake, H.; Lamecker, H.; Lange, T.; Jeongjin Lee; Lennon, B.; Rui Li; Senhu Li; Meinzer, H.-P.; Nemeth, G.; Raicu, D.S.; Rau, A.-M.; van Rikxoort, E.M.; Rousson, M.; Rusko, L.; Saddi, K.A.; Schmidt, G.; Seghers, D.; Shimizu, A.; Slagmolen, P.; Sorantin, E.; Soza, G.; Susomboon, R.; Waite, J.M.; Wimmer, A.; Wolf, I.; , "Comparison and Evaluation of Methods for Liver Segmentation From CT Datasets," Medical Imaging, IEEE Transactions on , vol.28, no.8, pp.1251-1265, Aug. 2009 41 | doi: 10.1109/TMI.2009.2013851 42 | """ 43 | 44 | # The edge points of the mask object. 45 | __mask_edge_points = None 46 | # The edge points of the reference object. 47 | __reference_edge_points = None 48 | # The nearest neighbours distances between mask and reference edge points. 49 | __mask_reference_nn = None 50 | # The nearest neighbours distances between reference and mask edge points. 51 | __reference_mask_nn = None 52 | # Distances of the two objects surface points. 53 | __distance_matrix = None 54 | 55 | def __init__(self, mask, reference, physical_voxel_spacing = [1,1,1], mask_offset = [0,0,0], reference_offset = [0,0,0]): 56 | """ 57 | Initialize the class with two binary images, each containing a single object. 58 | Assumes the input to be a representation of a 3D image, that fits one of the 59 | following formats: 60 | - 1. all 0 values denoting background, all others the foreground/object 61 | - 2. all False values denoting the background, all others the foreground/object 62 | The first image passed is referred to as 'mask', the second as 'reference'. This 63 | is only important for some metrics that are not symmetric (and therefore not 64 | really metrics). 65 | @param mask binary mask as an scipy array (3D image) 66 | @param reference binary reference as an scipy array (3D image) 67 | @param physical_voxel_spacing The physical voxel spacing of the two images 68 | (must be the same for both) 69 | @param mask_offset offset of the mask array to 0,0,0-origin 70 | @param reference_offset offset of the reference array to 0,0,0-origin 71 | """ 72 | # compute edge images 73 | mask_edge_image = Surface.compute_contour(mask) 74 | reference_edge_image = Surface.compute_contour(reference) 75 | 76 | # collect the object edge voxel positions 77 | # !TODO: When the distance matrix is already calculated here 78 | # these points don't have to be actually stored, only their number. 79 | # But there might be some later metric implementation that requires the 80 | # points and then it would be good to have them. What is better? 81 | mask_pts = mask_edge_image.nonzero() 82 | mask_edge_points = list(zip(mask_pts[0], mask_pts[1], mask_pts[2])) 83 | reference_pts = reference_edge_image.nonzero() 84 | reference_edge_points = list(zip(reference_pts[0], reference_pts[1], reference_pts[2])) 85 | 86 | # check if there is actually an object present 87 | if 0 >= len(mask_edge_points): 88 | raise Exception('The mask image does not seem to contain an object.') 89 | if 0 >= len(reference_edge_points): 90 | raise Exception('The reference image does not seem to contain an object.') 91 | 92 | # add offsets to the voxels positions and multiply with physical voxel spacing 93 | # to get the real positions in millimeters 94 | physical_voxel_spacing = scipy.array(physical_voxel_spacing) 95 | mask_edge_points += scipy.array(mask_offset) 96 | mask_edge_points *= physical_voxel_spacing 97 | reference_edge_points += scipy.array(reference_offset) 98 | reference_edge_points *= physical_voxel_spacing 99 | 100 | # set member vars 101 | self.__mask_edge_points = mask_edge_points 102 | self.__reference_edge_points = reference_edge_points 103 | 104 | def get_maximum_symmetric_surface_distance(self): 105 | """ 106 | Computes the maximum symmetric surface distance, also known as Hausdorff 107 | distance, between the two objects surfaces. 108 | 109 | @return the maximum symmetric surface distance in millimeters 110 | 111 | For a perfect segmentation this distance is 0. This metric is sensitive to 112 | outliers and returns the true maximum error. 113 | 114 | Metric definition: 115 | Let \f$S(A)\f$ denote the set of surface voxels of \f$A\f$. The shortest 116 | distance of an arbitrary voxel \f$v\f$ to \f$S(A)\f$ is defined as: 117 | \f[ 118 | d(v,S(A)) = \min_{s_A\in S(A)} ||v-s_A|| 119 | \f] 120 | where \f$||.||\f$ denotes the Euclidean distance. The maximum symmetric 121 | surface distance is then given by: 122 | \f[ 123 | MSD(A,B) = \max 124 | \left\{ 125 | \max_{s_A\in S(A)} d(s_A,S(B)), 126 | \max_{s_B\in S(B)} d(s_B,S(A)), 127 | \right\} 128 | \f] 129 | """ 130 | # Get the maximum of the nearest neighbour distances 131 | A_B_distance = self.get_mask_reference_nn().max() 132 | B_A_distance = self.get_reference_mask_nn().max() 133 | 134 | # compute result and return 135 | return max(A_B_distance, B_A_distance) 136 | 137 | def get_root_mean_square_symmetric_surface_distance(self): 138 | """ 139 | Computes the root mean square symmetric surface distance between the 140 | two objects surfaces. 141 | 142 | @return root mean square symmetric surface distance in millimeters 143 | 144 | For a perfect segmentation this distance is 0. This metric punishes large 145 | deviations from the true contour stronger than the average symmetric surface 146 | distance. 147 | 148 | Metric definition: 149 | Let \f$S(A)\f$ denote the set of surface voxels of \f$A\f$. The shortest 150 | distance of an arbitrary voxel \f$v\f$ to \f$S(A)\f$ is defined as: 151 | \f[ 152 | d(v,S(A)) = \min_{s_A\in S(A)} ||v-s_A|| 153 | \f] 154 | where \f$||.||\f$ denotes the Euclidean distance. The root mean square 155 | symmetric surface distance is then given by: 156 | \f[ 157 | RMSD(A,B) = 158 | \sqrt{\frac{1}{|S(A)|+|S(B)|}} 159 | \times 160 | \sqrt{ 161 | \sum_{s_A\in S(A)} d^2(s_A,S(B)) 162 | + 163 | \sum_{s_B\in S(B)} d^2(s_B,S(A)) 164 | } 165 | \f] 166 | """ 167 | # get object sizes 168 | mask_surface_size = len(self.get_mask_edge_points()) 169 | reference_surface_sice = len(self.get_reference_edge_points()) 170 | 171 | # get minimal nearest neighbours distances 172 | A_B_distances = self.get_mask_reference_nn() 173 | B_A_distances = self.get_reference_mask_nn() 174 | 175 | # square the distances 176 | A_B_distances_sqrt = A_B_distances * A_B_distances 177 | B_A_distances_sqrt = B_A_distances * B_A_distances 178 | 179 | # sum the minimal distances 180 | A_B_distances_sum = A_B_distances_sqrt.sum() 181 | B_A_distances_sum = B_A_distances_sqrt.sum() 182 | 183 | # compute result and return 184 | return math.sqrt(1. / (mask_surface_size + reference_surface_sice)) * math.sqrt(A_B_distances_sum + B_A_distances_sum) 185 | 186 | def get_average_symmetric_surface_distance(self): 187 | """ 188 | Computes the average symmetric surface distance between the 189 | two objects surfaces. 190 | 191 | @return average symmetric surface distance in millimeters 192 | 193 | For a perfect segmentation this distance is 0. 194 | 195 | Metric definition: 196 | Let \f$S(A)\f$ denote the set of surface voxels of \f$A\f$. The shortest 197 | distance of an arbitrary voxel \f$v\f$ to \f$S(A)\f$ is defined as: 198 | \f[ 199 | d(v,S(A)) = \min_{s_A\in S(A)} ||v-s_A|| 200 | \f] 201 | where \f$||.||\f$ denotes the Euclidean distance. The average symmetric 202 | surface distance is then given by: 203 | \f[ 204 | ASD(A,B) = 205 | \frac{1}{|S(A)|+|S(B)|} 206 | \left( 207 | \sum_{s_A\in S(A)} d(s_A,S(B)) 208 | + 209 | \sum_{s_B\in S(B)} d(s_B,S(A)) 210 | \right) 211 | \f] 212 | """ 213 | # get object sizes 214 | mask_surface_size = len(self.get_mask_edge_points()) 215 | reference_surface_sice = len(self.get_reference_edge_points()) 216 | 217 | # get minimal nearest neighbours distances 218 | A_B_distances = self.get_mask_reference_nn() 219 | B_A_distances = self.get_reference_mask_nn() 220 | 221 | # sum the minimal distances 222 | A_B_distances = A_B_distances.sum() 223 | B_A_distances = B_A_distances.sum() 224 | 225 | # compute result and return 226 | return 1. / (mask_surface_size + reference_surface_sice) * (A_B_distances + B_A_distances) 227 | 228 | def get_mask_reference_nn(self): 229 | """ 230 | @return The distances of the nearest neighbours of all mask edge points to all 231 | reference edge points. 232 | """ 233 | # Note: see note for @see get_reference_mask_nn 234 | if None == self.__mask_reference_nn: 235 | tree = scipy.spatial.cKDTree(self.get_mask_edge_points()) 236 | self.__mask_reference_nn, _ = tree.query(self.get_reference_edge_points()) 237 | return self.__mask_reference_nn 238 | 239 | def get_reference_mask_nn(self): 240 | """ 241 | @return The distances of the nearest neighbours of all reference edge points 242 | to all mask edge points. 243 | 244 | The underlying algorithm used for the scipy.spatial.KDTree implementation is 245 | based on: 246 | Sunil Arya, David M. Mount, Nathan S. Netanyahu, Ruth Silverman, and 247 | Angela Y. Wu. 1998. An optimal algorithm for approximate nearest neighbor 248 | searching fixed dimensions. J. ACM 45, 6 (November 1998), 891-923 249 | """ 250 | # Note: KDTree is faster than scipy.spatial.distance.cdist when the number of 251 | # voxels exceeds 10.000 (computationally tested). The maximum complexity is 252 | # O(D*N^2) vs. O(D*N*log(N), where D=3 and N=number of voxels 253 | if None == self.__reference_mask_nn: 254 | tree = scipy.spatial.cKDTree(self.get_reference_edge_points()) 255 | self.__reference_mask_nn, _ = tree.query(self.get_mask_edge_points()) 256 | return self.__reference_mask_nn 257 | 258 | def get_mask_edge_points(self): 259 | """ 260 | @return The edge points of the mask object. 261 | """ 262 | return self.__mask_edge_points 263 | 264 | def get_reference_edge_points(self): 265 | """ 266 | @return The edge points of the reference object. 267 | """ 268 | return self.__reference_edge_points 269 | 270 | @staticmethod 271 | def compute_contour(array): 272 | """ 273 | Uses a 18-neighbourhood filter to create an edge image of the input object. 274 | Assumes the input to be a representation of a 3D image, that fits one of the 275 | following formats: 276 | - 1. all 0 values denoting background, all others the foreground/object 277 | - 2. all False values denoting the background, all others the foreground/object 278 | The area outside the array is assumed to contain background voxels. The method 279 | does not ensure that the object voxels are actually connected, this is silently 280 | assumed. 281 | 282 | @param array a numpy array with only 0/N\{0} or False/True values. 283 | @return a boolean numpy array with the input objects edges 284 | """ 285 | # set 18-neighbourhood/conectivity (for 3D images) alias face-and-edge kernel 286 | # all values covered by 1/True passed to the function 287 | # as a 1D array in order left-right, top-down 288 | # Note: all in all 19 ones, as the center value 289 | # also has to be checked (if it is a masked pixel) 290 | # [[[0, 1, 0], [[1, 1, 1], [[0, 1, 0], 291 | # [1, 1, 1], [1, 1, 1], [1, 1, 1], 292 | # [0, 1, 0]], [1, 1, 1]], [0, 1, 0]]] 293 | footprint = scipy.ndimage.morphology.generate_binary_structure(3, 2) 294 | 295 | # create an erode version of the array 296 | erode_array = scipy.ndimage.morphology.binary_erosion(array, footprint) 297 | 298 | 299 | # xor the erode_array with the original and return 300 | return array ^ erode_array 301 | -------------------------------------------------------------------------------- /evaluation_code/program/helpers/utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | class time_elapsed(object): 4 | """ 5 | Debug tool that returns time elapsed since last call 6 | (or since init at first call). 7 | """ 8 | 9 | def __init__(self): 10 | self.initial_time = time.time() 11 | self.time = self.initial_time 12 | 13 | def __call__(self): 14 | this_time = time.time() 15 | elapsed = this_time-self.time 16 | self.time = this_time 17 | return elapsed 18 | 19 | def total_elapsed(self): 20 | return time.time()-self.initial_time 21 | -------------------------------------------------------------------------------- /evaluation_code/program/metadata: -------------------------------------------------------------------------------- 1 | command: python $program/evaluate.py $input $output 2 | description: Example competition evaluation program. -------------------------------------------------------------------------------- /evaluation_code/program/readme.txt: -------------------------------------------------------------------------------- 1 | Building an evaluation program that works with CodaLab 2 | 3 | This example uses python. It assumes python is installed on the codalab worker machines. 4 | 5 | evaluate.py - is an example that loads a single value from each of the gold files, looks for a corresponding submission, and finds the difference. 6 | metadata - this is a file that lists the contents of the program.zip bundle for the CodaLab system. 7 | 8 | Once these pieces are assembled they are packages as program.zip which CodaLab can then use to evaluate the submissions for a competition. -------------------------------------------------------------------------------- /evaluation_code/test_scoring.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | program=program 3 | input=idx-3Dircadb1-full.csv 4 | output=output 5 | python $program/evaluate.py $input $output -------------------------------------------------------------------------------- /inferlivertumor.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from keras.models import model_from_json 3 | from scipy import signal 4 | 5 | from preprocess import * 6 | 7 | os.environ["CUDA_VISIBLE_DEVICES"] = "1" 8 | config = tf.ConfigProto() 9 | config.gpu_options.allow_growth = True 10 | sess = tf.Session(config=config) 11 | IDEX = 0 12 | 13 | 14 | def interpolate_pred(raws_gt, pr, kernel): 15 | dsfactor = [w / float(f) for w, f in zip(raws_gt.shape, pr.shape)] 16 | predre = nd.interpolation.zoom(255 * pr.astype('float'), zoom=dsfactor) 17 | jj = np.where(predre > 200) 18 | predre[jj] = 255 19 | jj = np.where(predre <= 200) 20 | predre[jj] = 0 21 | # save pred nii files in the raw shape 22 | predre = signal.convolve(predre, kernel, mode="same") 23 | jj = np.where(predre > 1) 24 | predre[jj] = 1 25 | jj = np.where(predre < 1) 26 | predre[jj] = 0 27 | return predre 28 | 29 | 30 | def liver_boundarybox_inference(model, image_file, im_shape=(256, 256), pred_threhold=0.5): 31 | global IDEX 32 | scan_shape = np.array(image_file.shape, dtype=np.float32) 33 | new_shape = np.array([im_shape[0], im_shape[0], scan_shape[-1]], dtype=np.float32) 34 | resize_factor = new_shape / [scan_shape[0], scan_shape[1], scan_shape[2]] 35 | infer_image = nd.interpolation.zoom(image_file, resize_factor, mode='nearest') 36 | infer_image = np.expand_dims(infer_image, -1) 37 | infer_image = np.transpose(infer_image, (2, 0, 1, 3)) 38 | pred = model.predict(infer_image, batch_size=20) 39 | pred = pred[..., 0] > pred_threhold 40 | pred = label_connected_component(pred) 41 | seg = largest_label_volume(pred) 42 | pred[np.where(pred != seg)] = 0 43 | pred[np.where(pred > 0)] = 1. 44 | pred = np.transpose(pred, (1, 2, 0)) 45 | pred = nd.interpolation.zoom(pred, 1 / resize_factor, mode='nearest') 46 | pred[np.where(pred > 0.2)] = 1. 47 | minx, maxx, miny, maxy, minz, maxz = min_max_voi_with_liver(pred, superior=10, inferior=10) 48 | min_max_box = (int(minx), int(maxx), int(miny), 49 | int(maxy), minz, maxz) 50 | return pred, min_max_box 51 | 52 | 53 | def liver_voi_inference_5fold(model, boundaried_file, cube_shape=(224, 224, 32), step=(20, 20, 20), 54 | pred_threhold=0.6): 55 | scan_shape = np.array(boundaried_file.shape, dtype=np.float32) 56 | new_shape = np.array([cube_shape[0], cube_shape[1], scan_shape[-1]], dtype=np.float32) 57 | resize_factor = new_shape / scan_shape 58 | boundaried_file = nd.interpolation.zoom(boundaried_file, resize_factor, mode='nearest') 59 | liver_pred_containers = np.zeros(boundaried_file.shape) 60 | sp = boundaried_file.shape 61 | zstep = step[2] # 16 62 | deep_slices = np.arange(cube_shape[2] // 2, sp[2] - cube_shape[2] // 2 + zstep, zstep) 63 | if deep_slices.size == 0 or sp[-1] < cube_shape[-1]: 64 | raise Exception('narrow shape') 65 | for mi in range(len(model)): 66 | count_used = np.zeros(sp) + 1e-5 67 | liver_pred_container = np.zeros(boundaried_file.shape) 68 | for i in range(len(deep_slices)): 69 | deep = deep_slices[i] 70 | deep = deep if deep + cube_shape[2] // 2 <= sp[2] else -(cube_shape[2] // 2 - sp[2]) 71 | raw_patches = boundaried_file[:, :, 72 | deep - cube_shape[2] // 2:deep + cube_shape[2] // 2] 73 | raw_patches = raw_patches.astype(np.float32) 74 | X = [] 75 | X.append(raw_patches) 76 | X = np.expand_dims(X, -1) 77 | # temp_predic = model[mi].predict(X, batch_size=1)[0, ..., 0] > pred_threhold 78 | temp_predic = model[mi].predict(X, batch_size=1)[0, ..., 0] 79 | # Major voting https://github.com/ginobilinie/infantSeg 80 | # currLabelMat = np.where(temp_predic == True, 1, 0) # true, vote for 1, otherwise 0 81 | liver_pred_container[:, :, deep - cube_shape[2] // 2:deep + cube_shape[2] // 2] += temp_predic 82 | 83 | count_used[:, :, deep - cube_shape[2] // 2:deep + cube_shape[2] // 2] += 1 84 | # ============= 85 | # np.save("Demo/Predictions/test/img-preds-final-3D-" + str(i) + ".npy", model.predict(X, batch_size=1)) 86 | # np.save("Demo/Predictions/test/img-raw-3D-" + str(i) + ".npy", X) 87 | # ============= 88 | # np.save("Demo/Predictions/test/img-atten4-3D-" + str(i) + ".npy", 89 | # plot_layer_outputs(X, 622, model)) 90 | # np.save("Demo/Predictions/test/img-atten3-3D-" + str(i) + ".npy", 91 | # plot_layer_outputs(X, 592, model)) 92 | # np.save("Demo/Predictions/test/img-atten2-3D-" + str(i) + ".npy", 93 | # plot_layer_outputs(X, 536, model)) 94 | # ============= 95 | liver_pred_container = liver_pred_container / count_used 96 | liver_pred_containers += liver_pred_container 97 | # liver_pred_container = np.where(liver_pred_container > 0.5, 1, 0) 98 | # liver_pred_container = label_connected_component(liver_pred_container) 99 | # seg = largest_label_volume(liver_pred_container) 100 | # liver_pred_container = np.where(liver_pred_container != seg, 0, 1) 101 | liver_pred_container = liver_pred_containers / len(model) 102 | liver_pred_container = np.where(liver_pred_container > pred_threhold, 1, 0) 103 | # liver_pred_container = np.where(liver_pred_container > (2 / len(model)), 1, 0) 104 | 105 | resize_factor = scan_shape / new_shape 106 | liver_pred_container = nd.interpolation.zoom(liver_pred_container, resize_factor, mode='nearest') 107 | liver_pred_container = np.where(liver_pred_container < 0.2, 0, 1) 108 | # liver_pred_container = sm.binary_erosion(liver_pred_container, sm.ball(5)) 109 | liver_pred_container = label_connected_component(liver_pred_container) 110 | seg = largest_label_volume(liver_pred_container) 111 | liver_pred_container = np.where(liver_pred_container != seg, 0, 1) 112 | # liver_pred_container = sm.binary_dilation(liver_pred_container, sm.ball(5)) 113 | return liver_pred_container 114 | 115 | 116 | def liver_tumor_inference(model, whole_img, pred_liver_tumor, cube_shape=(64, 64, 64), 117 | step=(55, 55, 55), pred_threhold=0.5): 118 | minx, maxx, miny, maxy, minz, maxz = min_max_voi_with_liver(pred_liver_tumor, superior=10, inferior=10) 119 | liver_voi = whole_img[minx:maxx, miny:maxy, minz:maxz] 120 | pred_liver_voi = pred_liver_tumor[minx:maxx, miny:maxy, minz:maxz] 121 | patch_size = cube_shape 122 | patch_stride = 2 123 | tumor_pred_comtainers = np.zeros(pred_liver_voi.shape) 124 | locations, padding = generate_test_locations(patch_size, patch_stride, liver_voi.shape) 125 | pad_image = np.pad(np.expand_dims(liver_voi, -1), padding + ((0, 0),), 'constant') 126 | pad_result = np.zeros((pad_image.shape[:-1]), dtype=np.float32) 127 | pad_add = np.zeros((pad_image.shape[:-1]), dtype=np.float32) 128 | for mi in range(len(model)): 129 | for x in locations[0]: 130 | for y in locations[1]: 131 | for z in locations[2]: 132 | patch = pad_image[int(x - patch_size[0] / 2): int(x + patch_size[0] / 2), 133 | int(y - patch_size[1] / 2): int(y + patch_size[1] / 2), 134 | int(z - patch_size[2] / 2): int(z + patch_size[2] / 2), :] 135 | 136 | patch = np.expand_dims(patch, axis=0) 137 | probs = model[mi].predict(patch, batch_size=1)[0, ..., 0] 138 | pad_result[int(x - patch_size[0] / 2): int(x + patch_size[0] / 2), 139 | int(y - patch_size[1] / 2): int(y + patch_size[1] / 2), 140 | int(z - patch_size[2] / 2): int(z + patch_size[2] / 2)] += probs 141 | pad_add[int(x - patch_size[0] / 2): int(x + patch_size[0] / 2), 142 | int(y - patch_size[1] / 2): int(y + patch_size[1] / 2), 143 | int(z - patch_size[2] / 2): int(z + patch_size[2] / 2)] += 1 144 | pad_result = pad_result / pad_add 145 | result = pad_result[padding[0][0]: padding[0][0] + liver_voi.shape[0], 146 | padding[1][0]: padding[1][0] + liver_voi.shape[1], 147 | padding[2][0]: padding[2][0] + liver_voi.shape[2]] 148 | tumor_pred_comtainers += result 149 | tumor_pred_container = tumor_pred_comtainers / len(model) 150 | tumor_pred_container = np.where(tumor_pred_container >= pred_threhold, 1, 0) 151 | tumor_idx = np.where(tumor_pred_container > 0) 152 | no_liver_idx = np.where(pred_liver_voi == 0) 153 | pred_liver_voi[tumor_idx] = 2 154 | pred_liver_voi[no_liver_idx] = 0 155 | pred_liver_tumor[minx:maxx, miny:maxy, minz:maxz] = pred_liver_voi 156 | return pred_liver_tumor, (int(minx), int(maxx), int(miny), 157 | int(maxy), minz, maxz) 158 | 159 | 160 | def infer_slice_entrance(u_model, whole_img, whole_msk, index): 161 | pred_img, min_max_box = liver_boundarybox_inference(u_model, whole_img) 162 | 163 | return pred_img, min_max_box 164 | 165 | 166 | def infer_voi_entrance(g_model, whole_img, whole_msk, index): 167 | pred_img = liver_voi_inference_5fold(g_model, whole_img) 168 | return pred_img 169 | 170 | 171 | def infer_tumor_entrance(c_model, whole_img, pred_img, whole_msk, index=0, pred_threhold=0.6, cube_shape=(64, 64, 64), 172 | step=(55, 55, 55)): 173 | pred_img, min_max_box = liver_tumor_inference(c_model, whole_img, pred_img, 174 | cube_shape=cube_shape, 175 | step=step, pred_threhold=pred_threhold) 176 | 177 | return pred_img 178 | 179 | 180 | def infer_liver(csv_path, out_path, out_csv_path): 181 | out_csv_path = out_csv_path + '_liver.csv' 182 | u_model = 'Demo/log/liver_att_resunet_2d_good_order/model_0.289/g_0.289.json' 183 | u_weight = 'Demo/log/liver_att_resunet_2d_good_order/model_at_epoch_00053.hdf5' 184 | with open(u_model, 'r') as f: 185 | # u_model = model_from_json(f.read()) 186 | from build_model import get_net 187 | u_model = get_net((256, 256, 1), 'liver_att_resunet_2d') 188 | # u_model.summary() 189 | u_model.load_weights(u_weight) 190 | 191 | g_models = [] 192 | 193 | g_model = 'Demo/log/liver_att_resunet_3d/model_0.2/g_0.2.json' 194 | g_weight = 'Demo/log/liver_att_resunet_3d/model_at_2018-10-27_01:07:28_epoch_00049_f1.hdf5' 195 | with open(g_model, 'r') as f: 196 | g_model = model_from_json(f.read()) 197 | g_model.load_weights(g_weight) 198 | g_models.append(g_model) 199 | 200 | g_model = 'Demo/log/liver_att_resunet_3d/model_0.2/g_0.2.json' 201 | g_weight = 'Demo/log/liver_att_resunet_3d/model_at_2018-10-29_20:45:12_epoch_00049_f2.hdf5' 202 | with open(g_model, 'r') as f: 203 | g_model = model_from_json(f.read()) 204 | g_model.load_weights(g_weight) 205 | g_models.append(g_model) 206 | 207 | g_model = 'Demo/log/liver_att_resunet_3d/model_0.2/g_0.2.json' 208 | g_weight = 'Demo/log/liver_att_resunet_3d/model_at_2018-10-30_08:43:05_epoch_00047_f3.hdf5' 209 | with open(g_model, 'r') as f: 210 | g_model = model_from_json(f.read()) 211 | g_model.load_weights(g_weight) 212 | g_models.append(g_model) 213 | 214 | g_model = 'Demo/log/liver_att_resunet_3d/model_0.2/g_0.2.json' 215 | g_weight = 'Demo/log/liver_att_resunet_3d/model_at_2018-11-02_08:41:20_epoch_00049_f4.hdf5' 216 | with open(g_model, 'r') as f: 217 | g_model = model_from_json(f.read()) 218 | g_model.load_weights(g_weight) 219 | g_models.append(g_model) 220 | 221 | g_model = 'Demo/log/liver_att_resunet_3d/model_0.2/g_0.2.json' 222 | g_weight = 'Demo/log/liver_att_resunet_3d/model_at_2018-11-02_00:47:25_epoch_00036.hdf5' 223 | with open(g_model, 'r') as f: 224 | g_model = model_from_json(f.read()) 225 | g_model.load_weights(g_weight) 226 | g_models.append(g_model) 227 | 228 | df = pd.read_csv(csv_path) 229 | cvscores = [] 230 | for i, item in df.iterrows(): 231 | print('load raw select file:' + item[0]) 232 | # train 233 | imgnii = nib.load(item[0]) 234 | whole_img = imgnii.get_data() 235 | whole_img = set_bounds(whole_img, MIN_IMG_BOUND, MAX_IMG_BOUND) 236 | whole_img = normalize_with_mean(whole_img) 237 | try: 238 | whole_msk = nib.load(item[1]).get_data() 239 | except Exception as err: 240 | print(err) 241 | whole_msk = None 242 | pred_img, min_max_box = infer_slice_entrance(u_model, whole_img, whole_msk, i) 243 | print(min_max_box) 244 | # min_max_box1 = min_max_voi_with_liver(whole_msk) 245 | # print(min_max_box1) 246 | # print(np.array(min_max_box) - np.array(min_max_box1)) 247 | liver_voi = whole_img[min_max_box[0]: min_max_box[1], min_max_box[2]: min_max_box[3], min_max_box[4]: 248 | min_max_box[5]] 249 | if whole_msk is None: 250 | msk_voi = None 251 | else: 252 | msk_voi = whole_msk[min_max_box[0]: min_max_box[1], min_max_box[2]: min_max_box[3], min_max_box[4]: 253 | min_max_box[5]] 254 | 255 | try: 256 | pred_img = infer_voi_entrance(g_models, liver_voi, msk_voi, i) 257 | except Exception as err: 258 | print(err) 259 | continue 260 | 261 | pred_container = np.zeros(whole_img.shape) 262 | pred_container[min_max_box[0]: min_max_box[1], min_max_box[2]: min_max_box[3], 263 | min_max_box[4]:min_max_box[5]] = pred_img 264 | 265 | # pred_container = pred_img 266 | if whole_msk is not None: 267 | msk_img_path = item[1] 268 | msknii = nib.load(msk_img_path) 269 | # print(np.unique(pred_container)) 270 | liver_scores = scorer(pred_container >= 1, msknii.get_data() >= 1, 271 | msknii.header.get_zooms()[:3]) 272 | print("Liver dice", liver_scores['dice']) 273 | cvscores.append(liver_scores['dice']) 274 | 275 | outstr = str(i) + ',' 276 | for l in [liver_scores]: 277 | for k, v in l.items(): 278 | outstr += str(v) + ',' 279 | outstr += '\n' 280 | 281 | if not os.path.isfile(out_csv_path): 282 | headerstr = 'Volume,' 283 | for k, v in liver_scores.items(): 284 | headerstr += 'Liver_' + k + ',' 285 | headerstr += '\n' 286 | outstr = headerstr + outstr 287 | f = open(out_csv_path, 'a+') 288 | f.write(outstr) 289 | f.close() 290 | nib.save(nib.Nifti1Image(pred_container.astype('float'), affine=imgnii.get_affine()), 291 | out_path + 'test-segmentation-' + str(i) + '.nii.gz') 292 | print("%.4f%%" % (np.mean(cvscores))) 293 | 294 | 295 | def infer_tumor(csv_path, out_path, out_csv_path): 296 | out_csv_path = out_csv_path + '_tumor.csv' 297 | c_models = [] 298 | 299 | c_model = 'Demo/log/liver_tumor_res_att_unet_3d/model_0.2/g_0.2.json' 300 | c_weight = 'Demo/log/liver_tumor_res_att_unet_3d/model_at_2018-10-21_01:06:21_epoch_00083.hdf5' 301 | with open(c_model, 'r') as f: 302 | c_model = model_from_json(f.read()) 303 | c_model.load_weights(c_weight) 304 | c_models.append(c_model) 305 | 306 | c_model = 'Demo/log/liver_tumor_res_att_unet_3d/model_0.2/g_0.2.json' 307 | c_weight = 'Demo/log/liver_tumor_res_att_unet_3d/model_at_2018-10-21_09:28:37_epoch_00041.hdf5' 308 | with open(c_model, 'r') as f: 309 | c_model = model_from_json(f.read()) 310 | c_model.load_weights(c_weight) 311 | c_models.append(c_model) 312 | 313 | c_model = 'Demo/log/liver_tumor_res_att_unet_3d/model_0.2/g_0.2.json' 314 | c_weight = 'Demo/log/liver_tumor_res_att_unet_3d/model_at_2018-10-22_11:50:41_epoch_00046.hdf5' 315 | with open(c_model, 'r') as f: 316 | c_model = model_from_json(f.read()) 317 | c_model.load_weights(c_weight) 318 | c_models.append(c_model) 319 | 320 | c_model = 'Demo/log/liver_tumor_res_att_unet_3d/model_0.2/g_0.2.json' 321 | c_weight = 'Demo/log/liver_tumor_res_att_unet_3d/model_at_2018-07-06_13:24:50_epoch_00099.hdf5' 322 | with open(c_model, 'r') as f: 323 | c_model = model_from_json(f.read()) 324 | c_model.load_weights(c_weight) 325 | c_models.append(c_model) 326 | 327 | c_model = 'Demo/log/liver_tumor_res_att_unet_3d/model_0.2/g_0.2.json' 328 | c_weight = 'Demo/log/liver_tumor_res_att_unet_3d/model_at_2018-10-23_07:56:48_epoch_00047.hdf5' 329 | with open(c_model, 'r') as f: 330 | c_model = model_from_json(f.read()) 331 | c_model.load_weights(c_weight) 332 | c_models.append(c_model) 333 | 334 | out_path = out_path + 'tumor/' 335 | if not os.path.isdir(out_path): 336 | os.mkdir(out_path) 337 | df = pd.read_csv(csv_path) 338 | for i, item in df.iterrows(): 339 | print('load raw select file:' + item[0]) 340 | print('load pred liver container file:' + item[2]) 341 | # train 342 | # whole_img = np.load(item[0]) 343 | whole_img = nib.load(item[0]).get_data() 344 | whole_img = set_bounds(whole_img, MIN_IMG_BOUND, MAX_IMG_BOUND) 345 | whole_img = normalize_with_mean(whole_img) 346 | 347 | try: 348 | whole_msk = nib.load(item[1]).get_data() 349 | except Exception as err: 350 | print(err) 351 | whole_msk = None 352 | pred_container_nii = nib.load(item[2]) 353 | pred_container = pred_container_nii.get_data() 354 | # std, mean = cal_mean(whole_img) 355 | # whole_img = normalize(whole_img, mean, std) 356 | pred_container = infer_tumor_entrance(c_models, whole_img, pred_container, whole_msk, i, pred_threhold=0.5, 357 | cube_shape=(64, 64, 64), 358 | step=(55, 55, 55)) 359 | if whole_msk is not None: 360 | tumor_scores = scorer(pred_container > 1, whole_msk > 1, 361 | pred_container_nii.header.get_zooms()[:3]) 362 | print("Tumor dice", tumor_scores['dice']) 363 | if tumor_scores['dice'] < 0.02: 364 | continue 365 | outstr = str(i) + ',' 366 | for l in [tumor_scores]: 367 | for k, v in l.items(): 368 | outstr += str(v) + ',' 369 | outstr += '\n' 370 | 371 | if not os.path.isfile(out_csv_path): 372 | headerstr = 'Volume,' 373 | for k, v in tumor_scores.items(): 374 | headerstr += 'Tumor_' + k + ',' 375 | headerstr += '\n' 376 | outstr = headerstr + outstr 377 | f = open(out_csv_path, 'a+') 378 | f.write(outstr) 379 | nib.save(nib.Nifti1Image(pred_container.astype('float'), affine=pred_container_nii.get_affine()), 380 | out_path + 'test-segmentation-' + str(i) + '.nii.gz') 381 | 382 | 383 | def infer_Brain_Tumor(out_path, rootdir="/root/share/datasets/brain/Brats17ValidationData/"): 384 | c_models = [] 385 | 386 | c_model = 'Demo/log/brain_tumor_res_atten_unet_3d/model_0.2/g_0.2.json' 387 | c_weight = 'Demo/log/brain_tumor_res_atten_unet_3d/model_at_2018-10-10_05:47:51_epoch_00049.hdf5' 388 | with open(c_model, 'r') as f: 389 | c_model = model_from_json(f.read()) 390 | c_model.load_weights(c_weight) 391 | c_models.append(c_model) 392 | 393 | if "17" in rootdir: 394 | out_path = out_path + 'tumor/2017/' 395 | elif "18" in rootdir: 396 | out_path = out_path + 'tumor/2018/' 397 | if not os.path.isdir(out_path): 398 | os.mkdir(out_path) 399 | files = os.listdir(rootdir) 400 | patch_size = (64, 64, 64) 401 | patch_stride = 4 402 | for name in files: 403 | path = rootdir + name + '/' 404 | print('load raw select file:' + name) 405 | image = read_image(path, is_training=False) 406 | locations, padding = generate_test_locations(patch_size, patch_stride, image.shape[:-1]) 407 | pad_image = np.pad(image, padding + ((0, 0),), 'constant') 408 | pad_result = np.zeros((pad_image.shape[:-1]), dtype=np.float32) 409 | pad_add = np.zeros((pad_image.shape[:-1]), dtype=np.float32) 410 | for x in locations[0]: 411 | for y in locations[1]: 412 | for z in locations[2]: 413 | patch = pad_image[int(x - patch_size[0] / 2): int(x + patch_size[0] / 2), 414 | int(y - patch_size[1] / 2): int(y + patch_size[1] / 2), 415 | int(z - patch_size[2] / 2): int(z + patch_size[2] / 2), :] 416 | 417 | patch = np.expand_dims(patch, axis=0) 418 | 419 | probs = c_models[0].predict(patch, batch_size=1)[0, ..., 0] 420 | 421 | pad_result[int(x - patch_size[0] / 2): int(x + patch_size[0] / 2), 422 | int(y - patch_size[1] / 2): int(y + patch_size[1] / 2), 423 | int(z - patch_size[2] / 2): int(z + patch_size[2] / 2)] += probs 424 | pad_add[int(x - patch_size[0] / 2): int(x + patch_size[0] / 2), 425 | int(y - patch_size[1] / 2): int(y + patch_size[1] / 2), 426 | int(z - patch_size[2] / 2): int(z + patch_size[2] / 2)] += 1 427 | pad_result = pad_result / pad_add 428 | result = pad_result[padding[0][0]: padding[0][0] + image.shape[0], 429 | padding[1][0]: padding[1][0] + image.shape[1], 430 | padding[2][0]: padding[2][0] + image.shape[2]] 431 | pred = np.where(result > 0.5, 1, 0) 432 | # pred = clean_contour(result, pred) 433 | # print(path) 434 | # pred_container = np.where(whole_img > 0, 1, 0) 435 | nib.save(nib.Nifti1Image(pred.astype('float'), np.eye(4)), 436 | out_path + name + '.nii.gz') 437 | 438 | 439 | def infer_Brain_TCIA_Tumor(out_path, rootdir="../datasets/brain/MICCAI_BraTS_2018_Data_Training/HGG/"): 440 | c_models = [] 441 | from keras.models import Model 442 | c_model = 'Demo/log/brain_tumor_res_atten_unet_3d/model_0.2/g_0.2.json' 443 | c_weight = 'Demo/log/brain_tumor_res_atten_unet_3d/model_at_2018-10-11_13:58:37_epoch_00010.hdf5' 444 | with open(c_model, 'r') as f: 445 | c_model = model_from_json(f.read()) 446 | c_model.load_weights(c_weight) 447 | c_models.append(c_model) 448 | 449 | out_path = out_path + 'TCIA/' 450 | if not os.path.isdir(out_path): 451 | os.mkdir(out_path) 452 | image_paths = [] 453 | files = os.listdir(rootdir) 454 | for name in files: 455 | image_paths.append(rootdir + name + '/') 456 | file_path = rootdir.replace('HGG', 'LGG') 457 | files = os.listdir(file_path) 458 | for name in files: 459 | image_paths.append(file_path + name + '/') 460 | files = image_paths 461 | cube_shape = (64, 64, 64) 462 | step = (60, 60, 60) 463 | files = sorted(files) 464 | intermediate_layer_model = Model(c_models[0].input, 465 | c_models[0].layers[639].output) 466 | for ff in range(len(files)): 467 | name = files[ff] 468 | if "TCIA" not in name: 469 | continue 470 | print(ff) 471 | print('load raw select file:' + name) 472 | image = read_image(name, is_training=True) 473 | fileName = name[name.rfind('/Brats') + 1:len(name) - 1] 474 | print('load raw select file:' + fileName) 475 | mask = image[..., 4] 476 | image = image[..., 0:4] 477 | minx, maxx, miny, maxy, minz, maxz = min_max_voi_with_liver(mask, superior=20, inferior=20) 478 | mask = mask[minx:maxx, miny:maxy, minz:maxz] 479 | image = image[minx:maxx, miny:maxy, minz:maxz, :] 480 | imagec = np.zeros((image.shape[0], image.shape[1], image.shape[2], 5)) 481 | imagec[..., 0:4] = image 482 | imagec[..., 4] = mask 483 | image = imagec 484 | sp = image.shape 485 | xstep = step[0] # 16 486 | ystep = step[1] # 16 487 | zstep = step[2] # 16 488 | width_slices = np.arange(cube_shape[0] // 2, sp[0] - cube_shape[0] // 2 + xstep, xstep) 489 | height_slices = np.arange(cube_shape[1] // 2, sp[1] - cube_shape[1] // 2 + ystep, ystep) 490 | deep_slices = np.arange(cube_shape[2] // 2, sp[2] - cube_shape[2] // 2 + zstep, zstep) 491 | print(len(width_slices) * len(deep_slices) * len(height_slices)) 492 | for i in range(len(deep_slices)): 493 | deep = deep_slices[i] 494 | deep = deep if deep + cube_shape[2] // 2 <= sp[2] else -(cube_shape[2] // 2 - sp[2]) 495 | for j in range(len(height_slices)): 496 | height = height_slices[j] 497 | height = height if height + cube_shape[1] // 2 <= sp[1] else -(cube_shape[1] // 2 - sp[1]) 498 | for k in range(len(width_slices)): 499 | width = width_slices[k] 500 | width = width if width + cube_shape[0] // 2 <= sp[0] else -(cube_shape[0] // 2 - sp[0]) 501 | patch = image[width - cube_shape[0] // 2:width + cube_shape[0] // 2, 502 | height - cube_shape[1] // 2:height + cube_shape[1] // 2, 503 | deep - cube_shape[2] // 2:deep + cube_shape[2] // 2] 504 | msk = patch[..., 4] 505 | patch = patch[..., 0:4] 506 | patch = np.expand_dims(patch, axis=0) 507 | # 635 concate 636 conv 32number 639 activation later 508 | intermediate_output = intermediate_layer_model.predict(patch, batch_size=1)[0, ...] 509 | # out_list = get_i_layer_outputs(patch, c_models[0], 639) 510 | features = np.concatenate([intermediate_output, np.expand_dims(msk, -1)], axis=-1) 511 | np.save(out_path + fileName + str(i) + '_' + str(j) + '_' + str(k) + ".npy", features) 512 | 513 | 514 | if __name__ == '__main__': 515 | 516 | data_dir = 'lits_test' 517 | if data_dir == 'lits_validation': 518 | csv_path = 'Demo/idx-test-lits-npy.csv' 519 | out_path = 'Demo/Predictions/Liver/LiTS_validation/' 520 | out_csv_path = 'Demo/Predictions/LiTS_validation_results' 521 | elif data_dir == 'lits_test': 522 | csv_path = 'Demo/idx-test-full.csv' 523 | out_path = 'Demo/Predictions/Liver/LiTS_test/' 524 | out_csv_path = 'Demo/Predictions/LiTS_test_results' 525 | elif data_dir == '3DIRCADb': 526 | csv_path = 'Demo/idx-3Dircadb1-full.csv' 527 | out_path = 'Demo/Predictions/Liver/3DIRCADb/' 528 | out_csv_path = 'Demo/Predictions/3DIRCADb_results' 529 | elif data_dir == 'Brain': 530 | out_path = 'Demo/Predictions/Brain/' 531 | # infer_liver(csv_path, out_path, out_csv_path) 532 | if data_dir == 'Brain': 533 | infer_Brain_Tumor(out_path) 534 | # infer_Brain_TCIA_Tumor(out_path) 535 | else: 536 | infer_tumor(csv_path, out_path, out_csv_path) 537 | -------------------------------------------------------------------------------- /plot_3d.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import os 4 | from matplotlib import pyplot as plt 5 | import nibabel as nib 6 | from skimage import measure, morphology 7 | from mpl_toolkits.mplot3d.art3d import Poly3DCollection 8 | import gc 9 | import matplotlib.cm as cm 10 | import matplotlib 11 | from sklearn.metrics import auc 12 | from PIL import Image 13 | import cv2 14 | 15 | """""""""""""""""""""""""""""""""""""""""""""""" 16 | """ 3D Reconstructing the picture """ 17 | """""""""""""""""""""""""""""""""""""""""""""""" 18 | 19 | 20 | def miccaiimshow(img, seg, preds, fname, titles=None, plot_separate_img=True): 21 | """Takes raw image img, seg in range 0-2, list of predictions in range 0-2""" 22 | plt.figure(figsize=(25, 25)) 23 | ALPHA = 1 24 | 25 | if len(preds.shape) == 3: 26 | n_plots = len(preds) 27 | else: 28 | n_plots = 1 29 | subplot_offset = 0 30 | 31 | plt.set_cmap('gray') 32 | 33 | if plot_separate_img: 34 | n_plots += 1 35 | subplot_offset = 1 36 | plt.subplot(1, n_plots, 1) 37 | plt.subplots_adjust(wspace=0, hspace=0) 38 | plt.title("Image") 39 | plt.axis('off') 40 | plt.imshow(img, cmap="gray") 41 | if type(preds) != list: 42 | preds = [preds] 43 | for i, pred in enumerate(preds): 44 | # Order of overaly 45 | ########## OLD 46 | # lesion= pred==2 47 | # difflesion = set_minus(seg==2,lesion) 48 | # liver = set_minus(pred==1, [lesion, difflesion]) 49 | # diffliver = set_minus(seg==1, [liver,lesion,difflesion]) 50 | ########## 51 | 52 | lesion = pred == 2 53 | difflesion = np.logical_xor(seg == 2, lesion) 54 | liver = pred == 1 55 | diffliver = np.logical_xor(seg == 1, liver) 56 | 57 | plt.subplot(1, n_plots, i + 1 + subplot_offset) 58 | title = titles[i] if titles is not None and i < len(titles) else "" 59 | plt.title(title) 60 | plt.axis('off') 61 | plt.imshow(img); 62 | plt.hold(True) 63 | # Liver prediction 64 | plt.imshow(np.ma.masked_where(liver == 0, liver), cmap="Greens", vmin=0.1, vmax=1.2, alpha=ALPHA) 65 | plt.hold(True) 66 | # Liver : Pixels in ground truth, not in prediction 67 | plt.imshow(np.ma.masked_where(diffliver == 0, diffliver), cmap="Spectral", vmin=0.1, vmax=2.2, alpha=ALPHA) 68 | plt.hold(True) 69 | 70 | # Lesion prediction 71 | plt.imshow(np.ma.masked_where(lesion == 0, lesion), cmap="Blues", vmin=0.1, vmax=1.2, alpha=ALPHA) 72 | plt.hold(True) 73 | # Lesion : Pixels in ground truth, not in prediction 74 | plt.imshow(np.ma.masked_where(difflesion == 0, difflesion), cmap="Reds", vmin=0.1, vmax=1.5, alpha=ALPHA) 75 | 76 | plt.savefig(fname, transparent=True) 77 | plt.close() 78 | 79 | 80 | def plot_AUC_ROC(fprs, tprs, method_names, fig_dir, op_pts): 81 | # set font style 82 | font = {'family': 'serif'} 83 | matplotlib.rc('font', **font) 84 | 85 | # sort the order of plots manually for eye-pleasing plots 86 | colors = ['r', 'b', 'y', 'g', '#7e7e7e', 'm', 'c', 'k', '#cd919e'] if len(fprs) == 9 else ['r', 'y', 'm', 'g', 'k'] 87 | indices = [7, 2, 5, 3, 4, 6, 1, 8, 0] if len(fprs) == 9 else [4, 1, 2, 3, 0] 88 | 89 | # print auc 90 | print("****** ROC AUC ******") 91 | print( 92 | "CAVEAT : AUC with 8bit images might be lower than the floating point array (check /pretrained/auc_roc*.npy)") 93 | for index in indices: 94 | if method_names[index] != 'CRFs' and method_names[index] != '2nd_manual': 95 | print("{} : {:04}".format(method_names[index], auc(fprs[index], tprs[index]))) 96 | 97 | # plot results 98 | for index in indices: 99 | if method_names[index] == 'CRFs': 100 | plt.plot(fprs[index], tprs[index], colors[index] + '*', label=method_names[index].replace("_", " ")) 101 | elif method_names[index] == '2nd_manual': 102 | plt.plot(fprs[index], tprs[index], colors[index] + '*', label='Human') 103 | else: 104 | plt.step(fprs[index], tprs[index], colors[index], where='post', label=method_names[index].replace("_", " "), 105 | linewidth=1.5) 106 | 107 | # plot individual operation points 108 | for op_pt in op_pts: 109 | plt.plot(op_pt[0], op_pt[1], 'r.') 110 | 111 | plt.title('ROC Curve') 112 | plt.xlabel("1-Specificity") 113 | plt.ylabel("Sensitivity") 114 | plt.xlim(0, 0.3) 115 | plt.ylim(0.7, 1.0) 116 | plt.legend(loc="lower right") 117 | plt.savefig(os.path.join(fig_dir, "ROC.png")) 118 | plt.close() 119 | 120 | 121 | def plot_AUC_PR(precisions, recalls, method_names, fig_dir, op_pts): 122 | # set font style 123 | font = {'family': 'serif'} 124 | matplotlib.rc('font', **font) 125 | 126 | # sort the order of plots manually for eye-pleasing plots 127 | colors = ['r', 'b', 'y', 'g', '#7e7e7e', 'm', 'c', 'k', '#cd919e'] if len(precisions) == 9 else ['r', 'y', 'm', 'g', 128 | 'k'] 129 | indices = [7, 2, 5, 3, 4, 6, 1, 8, 0] if len(precisions) == 9 else [4, 1, 2, 3, 0] 130 | 131 | # print auc 132 | print("****** Precision Recall AUC ******") 133 | print( 134 | "CAVEAT : AUC with 8bit images might be lower than the floating point array (check /pretrained/auc_pr*.npy)") 135 | for index in indices: 136 | if method_names[index] != 'CRFs' and method_names[index] != '2nd_manual': 137 | print("{} : {:04}".format(method_names[index], auc(recalls[index], precisions[index]))) 138 | 139 | # plot results 140 | for index in indices: 141 | if method_names[index] == 'CRFs': 142 | plt.plot(recalls[index], precisions[index], colors[index] + '*', 143 | label=method_names[index].replace("_", " ")) 144 | elif method_names[index] == '2nd_manual': 145 | plt.plot(recalls[index], precisions[index], colors[index] + '*', label='Human') 146 | else: 147 | plt.step(recalls[index], precisions[index], colors[index], where='post', 148 | label=method_names[index].replace("_", " "), linewidth=1.5) 149 | 150 | # plot individual operation points 151 | for op_pt in op_pts: 152 | plt.plot(op_pt[0], op_pt[1], 'r.') 153 | 154 | plt.title('Precision Recall Curve') 155 | plt.xlabel("Recall") 156 | plt.ylabel("Precision") 157 | plt.xlim(0.5, 1.0) 158 | plt.ylim(0.5, 1.0) 159 | plt.legend(loc="lower left") 160 | plt.savefig(os.path.join(fig_dir, "Precision_recall.png")) 161 | plt.close() 162 | 163 | 164 | # 3D plot segmentation of liver and nodules 165 | def plot_3d_seg(image, name, threshold=1, save=False): 166 | # Position the scan upright, 167 | # so the head of the patient would be at the top facing the camera 168 | check = np.max(np.unique(image)) > 1 169 | verts, faces = measure.marching_cubes(image, threshold - 1) 170 | if check: 171 | verts2, faces2 = measure.marching_cubes(image, threshold) 172 | fig = plt.figure(figsize=(15, 15)) 173 | ax = fig.add_subplot(111, projection='3d') 174 | # Fancy indexing: `verts[faces]` to generate a collection of triangles 175 | mesh = Poly3DCollection(verts[faces], alpha=0.3) 176 | if check: 177 | mesh2 = Poly3DCollection(verts2[faces2], alpha=0.3) 178 | face_color = [1, 0.2, 0.2] 179 | if check: 180 | face_color2 = [0.3, 0.3, 1] 181 | mesh.set_facecolor(face_color) 182 | if check: 183 | mesh2.set_facecolor(face_color2) 184 | ax.add_collection3d(mesh) 185 | if check: 186 | ax.add_collection3d(mesh2) 187 | mesh_z = np.mean(image, axis=2) 188 | # mesh_y = np.mean(image,axis=1) 189 | # mesh_x = np.mean(image,axis=0) 190 | 191 | X = np.linspace(0, image.shape[0] - 1, image.shape[0]) 192 | Y = np.linspace(0, image.shape[1] - 1, image.shape[1]) 193 | Z = np.linspace(0, image.shape[2] - 1, image.shape[2]) 194 | # a,b=np.meshgrid(Y,Z) 195 | c, d = np.meshgrid(X, Y) 196 | # e,f=np.meshgrid(X,Z) 197 | cest = ax.contourf(c, d, np.transpose(mesh_z), zdir='z', offset=0, cmap="Blues") 198 | # cest = ax.contourf(np.transpose(mesh_x),b,a,zdir='x', offset=0, cmap="Greys") 199 | # cest = ax.contourf(e,np.transpose(mesh_y),f,zdir="y", offset=image.shape[1], cmap="Greys") 200 | ax.tick_params(axis='both', which='major', labelsize=18) 201 | ax.set_ylim(0, image.shape[1]) 202 | ax.set_xlim(0, image.shape[0]) 203 | ax.set_zlim(0, image.shape[2]) 204 | ax.set_title(name + ": 3D nodules and liver") 205 | if save: 206 | fig.savefig( 207 | "/home01/weileyi/jinqiangguo/jqg/py3EnvRoad/lung-segmentation-3d/" + name + "_3D_nodules_and_liver.png", 208 | bbox_inches='tight') 209 | plt.close(fig) 210 | del mesh, verts, faces, face_color 211 | if check: 212 | del mesh2, verts2, faces2, face_color2 213 | 214 | 215 | # 3D Plot the complete image 216 | def plot_3d_vol(image, name="Check", threshold=320, save=False): 217 | # Position the scan upright, 218 | p = image 219 | # so the head of the patient would be at the top facing the camera 220 | verts, faces = measure.marching_cubes(p, threshold - 1) 221 | fig = plt.figure(figsize=(10, 10)) 222 | ax = fig.add_subplot(111, projection='3d') 223 | # Fancy indexing: `verts[faces]` to generate a collection of triangles 224 | mesh = Poly3DCollection(verts[faces], alpha=0.1) 225 | face_color = '#0099ff' # [0.5, 0.5, 1] 226 | mesh.set_facecolor(face_color) 227 | ax.add_collection3d(mesh) 228 | ax.view_init(30, 35) 229 | ax.set_xlim(0, p.shape[0]) 230 | ax.set_ylim(0, p.shape[1]) 231 | ax.set_zlim(0, p.shape[2]) 232 | ax.set_zlabel('Z') 233 | ax.set_ylabel('Y') 234 | ax.set_xlabel('X') 235 | ax.set_title(name + "_3D_Volume_Scan") 236 | if save: 237 | fig.savefig("data/" + name + "_3D_Volume_Scan.png", 238 | bbox_inches='tight') 239 | del mesh, verts, faces 240 | plt.close(fig) 241 | 242 | 243 | def plot_3d_all(image, segm, name="Complete", threshold_bones=320, save=False): 244 | check = np.max(np.unique(segm)) > 1 245 | print("Finding marching cubes...") 246 | verts, faces = measure.marching_cubes(segm, 0) 247 | if check: 248 | verts2, faces2 = measure.marching_cubes(segm, 1) 249 | verts_vol, faces_vol = measure.marching_cubes(image, threshold_bones) 250 | fig = plt.figure(figsize=(15, 20)) 251 | ax = fig.add_subplot(111, projection='3d') 252 | # Fancy indexing: `verts[faces]` to generate a collection of triangles 253 | print("Computing polygons...") 254 | mesh = Poly3DCollection(verts[faces], alpha=0.4) 255 | if check: 256 | mesh2 = Poly3DCollection(verts2[faces2], alpha=0.7) 257 | mesh_vol = Poly3DCollection(verts_vol[faces_vol], alpha=0.25) 258 | print("Plotting...") 259 | face_color = [1, 0.2, 0.2] 260 | if check: 261 | face_color2 = [0.3, 0.3, 1] 262 | face_color_vol = [0, 0, 0] 263 | mesh.set_facecolor(face_color) 264 | if check: 265 | mesh2.set_facecolor(face_color2) 266 | mesh_vol.set_facecolor(face_color_vol) 267 | ax.add_collection3d(mesh) 268 | if check: 269 | ax.add_collection3d(mesh2) 270 | ax.add_collection3d(mesh_vol) 271 | mesh_z = np.mean(segm, axis=2) 272 | # mesh_y = np.mean(image,axis=1) 273 | # mesh_x = np.mean(image,axis=0) 274 | 275 | X = np.linspace(0, image.shape[0] - 1, image.shape[0]) 276 | Y = np.linspace(0, image.shape[1] - 1, image.shape[1]) 277 | Z = np.linspace(0, image.shape[2] - 1, image.shape[2]) 278 | # a,b=np.meshgrid(Y,Z) 279 | c, d = np.meshgrid(X, Y) 280 | # e,f=np.meshgrid(X,Z) 281 | cest = ax.contourf(c, d, np.transpose(mesh_z), zdir='z', offset=0, cmap="Blues") 282 | # cest = ax.contourf(np.transpose(mesh_x),b,a,zdir='x', offset=0, cmap="Greys") 283 | # cest = ax.contourf(e,np.transpose(mesh_y),f,zdir="y", offset=image.shape[1], cmap="Greys") 284 | ax.tick_params(axis='both', which='major', labelsize=18) 285 | ax.set_xlim(0, image.shape[0]) 286 | ax.set_ylim(0, image.shape[1]) 287 | ax.set_zlim(0, image.shape[2]) 288 | ax.set_title(name + "_3D_Complete.png") 289 | if save: 290 | fig.savefig("data/" + name + "_3D_Complete.png", 291 | bbox_inches='tight') 292 | plt.close("all") 293 | del mesh, mesh_vol, face_color, face_color_vol 294 | if check: 295 | del mesh2, face_color2, verts2, faces2 296 | del verts, verts_vol, faces, faces_vol 297 | gc.collect() 298 | 299 | 300 | def make_hist(img, title, name, xfrom=-1200, xto=2000): 301 | import matplotlib 302 | matplotlib.rcParams['font.family'] = 'Times New Roman' 303 | plt.figure(figsize=(6, 4.5)) 304 | plt.hist(img.ravel(), normed=0, bins=40, facecolor='blue') 305 | plt.xticks(fontsize=20) 306 | plt.yticks(fontsize=20) 307 | plt.xlabel("Hounsfield units (HU)", fontsize=30) 308 | plt.ylabel("Frequency", fontsize=30) 309 | plt.xlim(xfrom, xto) 310 | plt.tight_layout() 311 | plt.title(title + " Windowing", fontsize=20) 312 | plt.savefig("data/" + name + "_Hist.png") 313 | plt.close() 314 | # plt.show() 315 | 316 | 317 | # img = imgnii.get_data() 318 | # mask = masknii.get_data() 319 | # plot_3d_vol(img) 320 | def draw_for_HU(): 321 | df = pd.read_csv('Demo/idx-hu.csv') 322 | for i, item in df.iterrows(): 323 | imgnii = nib.load(item[0]) 324 | print('train data select nii file:' + item[0]) 325 | niiName = item[0][5:] 326 | img = imgnii.get_data() 327 | # plot_3d_vol(img, name='Befor HU',threshold=40,save=True) 328 | make_hist(img, "Before", 'Before_HU_' + niiName, xfrom=-1200, xto=2000) 329 | sp = img.shape 330 | print(sp) 331 | # if not os.path.isdir("data/" + niiName + "/"): 332 | # os.mkdir("data/" + niiName + "/") 333 | # for j in range(sp[-1]): 334 | # cv2.imwrite("data/" + niiName + "/" + niiName + "_z_before_hu_" + str(j) + ".png", img[:, :, j]) 335 | # for j in range(sp[-2]): 336 | # cv2.imwrite("data/" + niiName + "/" + niiName + "_y_before_hu_" + str(j) + ".png", img[:, j, :]) 337 | # for j in range(sp[0]): 338 | # cv2.imwrite("data/" + niiName + "/" + niiName + "_x_before_hu_" + str(j) + ".png", img[j, :, :]) 339 | img = np.clip(img, -100, 200) 340 | img = img.astype(np.float32) 341 | make_hist(img, "After", 'After_HU_' + niiName, xfrom=-120, xto=250) 342 | # for j in range(sp[-1]): 343 | # cv2.imwrite("data/" + niiName + "/" + niiName + "_z_after_hu_" + str(j) + ".png", img[:, :, j]) 344 | # for j in range(sp[-2]): 345 | # cv2.imwrite("data/" + niiName + "/" + niiName + "_y_after_hu_" + str(j) + ".png", img[:, j, :]) 346 | # for j in range(sp[0]): 347 | # cv2.imwrite("data/" + niiName + "/" + niiName + "_x_after_hu_" + str(j) + ".png", img[j, :, :]) 348 | # nib.save(nib.Nifti1Image(img.astype('float'), affine=imgnii.get_affine()), 349 | # "data/" + niiName[:len(niiName) - 4] + '.nii.gz') 350 | 351 | 352 | def plotNNFilterOverlay(input_im, units, figure_id, interp='bilinear', 353 | colormap=cm.jet, colormap_lim=None, title='', alpha=0.5, row=2): 354 | plt.ion() 355 | filters = units.shape[2] 356 | f, ax = plt.subplots(row, filters // row) 357 | for i in range(row): 358 | for j in range(filters // row): 359 | ax[i, j].set_xticks([]) 360 | ax[i, j].set_yticks([]) 361 | ax[i, j].imshow(input_im[:, :, i * filters // row + j], interpolation=interp, cmap='gray') 362 | ax[i, j].imshow(units[:, :, i * filters // row + j], interpolation=interp, cmap=colormap, alpha=alpha) 363 | if colormap_lim: 364 | ax[i, j].clim(colormap_lim[0], colormap_lim[1]) 365 | 366 | # for i in range(filters): 367 | # plt.imshow(input_im[:, :, i], interpolation=interp, cmap='gray') 368 | # plt.imshow(units[:, :, i], interpolation=interp, cmap=colormap, alpha=alpha) 369 | # plt.axis('off') 370 | # plt.title(title, fontsize='small') 371 | 372 | plt.subplots_adjust(wspace=0, hspace=0) 373 | plt.tight_layout() 374 | # plt.tight_layout(0.01,0.001) 375 | plt.savefig(title, format='png', transparent=True) 376 | 377 | 378 | def plot_feature_map(img_slice, L, imgname, row=4): 379 | # ======pic one 380 | 381 | image = Image.fromarray(img_slice[:, :, 0]) 382 | image = image.resize((L[0].shape[0], L[0].shape[1])) 383 | image = np.expand_dims(np.array(image), -1) 384 | plotNNFilterOverlay(np.tile(image, len(L)), np.transpose(L, (1, 2, 0)), 1, title=imgname + '-1.png', 385 | row=row) 386 | 387 | # ======pic two 388 | f, ax = plt.subplots(row, len(L) // row) 389 | for i in range(row): 390 | for j in range(len(L) // row): 391 | ax[i, j].set_xticks([]) 392 | ax[i, j].set_yticks([]) 393 | ax[i, j].imshow(L[i * len(L) // row + j], interpolation='nearest', cmap='jet') 394 | plt.subplots_adjust(wspace=0, hspace=0) 395 | plt.tight_layout() 396 | plt.savefig(imgname + '-2.png', format='png', transparent=True) 397 | 398 | 399 | def plot_feature_map_entrance(dir='../vis/3dircad/'): 400 | image_label = np.load(dir + 'img-grads-final-0.npy') 401 | image_file = np.load(dir + 'img-raw-0.npy') 402 | imgla = np.load(dir + 'msk-0.npy') 403 | img_slice = np.load(dir + 'img-raw-slice-0.npy')[0] 404 | from keras.preprocessing.image import array_to_img 405 | im = array_to_img(img_slice) 406 | im.save(dir + 'raw_img.png') 407 | # ======================================================= 408 | L = np.load(dir + 'img-atten4-0.npy') 409 | plot_feature_map(img_slice, L, dir + "img_atten4_layer", row=4) 410 | # # ======================================================= 411 | L = np.load(dir + 'img-atten3-0.npy') 412 | plot_feature_map(img_slice, L, dir + "img_atten3_layer", row=4) 413 | # # ======================================================= 414 | L = np.load(dir + 'img-atten2-0.npy') 415 | plot_feature_map(img_slice, L, dir + "img_atten2_layer", row=8) 416 | # ======================================================= 417 | tp = np.transpose(np.nonzero(imgla[:, :, :])) 418 | minx, miny, minz = np.min(tp, axis=0) 419 | maxx, maxy, maxz = np.max(tp, axis=0) 420 | num_vis = 8 421 | img_vis = np.zeros((image_file.shape[1], image_file.shape[2], num_vis)) 422 | feature_vis = np.zeros((image_file.shape[1], image_file.shape[2], num_vis)) 423 | for i in range(num_vis): 424 | x = minz + i * (maxz - minz) // (num_vis + 1) 425 | img_vis[:, :, i] = image_file[x, :, :, 0] * 255. 426 | feature_vis[:, :, i] = image_label[x, :, :, 0] 427 | plotNNFilterOverlay(img_vis, feature_vis, 1, title=dir + "img_final_layer.png", row=2) 428 | 429 | 430 | def plot_probability_map(dir='../vis/3dircad/'): 431 | import nibabel as nib 432 | img_soft = nib.load( 433 | 'F:\\xunleixiazai\\xftp_tmp\\3DIRCADb\\RA-UNet\\predciton_final\\soft\\test-segmentation-0.nii.gz').get_data() 434 | image_gt = np.load(dir + 'msk-0.npy') 435 | image_file = np.load(dir + 'img-0.npy') 436 | tp = np.transpose(np.nonzero(image_gt[:, :, :])) 437 | minx, miny, minz = np.min(tp, axis=0) 438 | maxx, maxy, maxz = np.max(tp, axis=0) 439 | num_vis = 8 440 | img_vis = np.zeros((image_file.shape[0], image_file.shape[1], num_vis)) 441 | feature_vis = np.zeros((image_file.shape[0], image_file.shape[1], num_vis)) 442 | for i in range(num_vis): 443 | x = minz + i * (maxz - minz) // (num_vis + 1) 444 | img_vis[:, :, i] = image_file[:, :, x] * 255. 445 | feature_vis[:, :, i] = img_soft[:, :, x] 446 | 447 | plotNNFilterOverlay(img_vis, feature_vis, 1, title=dir + "img_final_3d_layer.png", row=2) 448 | 449 | 450 | draw_for_HU() 451 | -------------------------------------------------------------------------------- /preprocess.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from scipy import ndimage as nd 3 | import configure 4 | from utils.utils import * 5 | from utils.extract_patches import * 6 | 7 | CFG = configure.cfg 8 | LOG_DIR = CFG['log_file'] 9 | 10 | MIN_IMG_BOUND = -100.0 # Everything below: Water -62 -200 11 | MAX_IMG_BOUND = 200.0 # Everything above corresponds to bones 238 200 12 | MIN_MSK_BOUND = 0.0 # Everything above corresponds 13 | MAX_MSK_BOUND = 2.0 # Everything above corresponds 14 | 15 | 16 | def set_bounds(img, MIN_BOUND, MAX_BOUND): 17 | image = np.clip(img, MIN_BOUND, MAX_BOUND) 18 | image = image.astype(np.float32) 19 | return image 20 | 21 | 22 | def process_liver_tumor_nii_file(csv_path, liver_patch=False, size=224, superior=10, 23 | inferior=10): 24 | df = pd.read_csv(csv_path) 25 | patches_per_image = 500 26 | patch_size = 32 27 | imgs, masks = [], [] 28 | for idx, item in df.iterrows(): 29 | img = nib.load(item[0]).get_data() 30 | msk = nib.load(item[1]).get_data() 31 | img = set_bounds(img, MIN_IMG_BOUND, MAX_IMG_BOUND) 32 | img = normalize_with_mean(img) 33 | print(item) 34 | if liver_patch: 35 | minx, maxx, miny, maxy, minz, maxz = min_max_voi_with_liver(msk, superior=superior, inferior=inferior) 36 | img = img[minx: maxx, miny: maxy, minz: maxz] 37 | msk = msk[minx: maxx, miny: maxy, minz: maxz] 38 | scan_shape = np.array(img.shape, dtype=np.float32) 39 | new_shape = np.array([size, size, scan_shape[-1]], dtype=np.float32) 40 | resize_factor = new_shape / [scan_shape[0], scan_shape[1], scan_shape[2]] 41 | img = nd.interpolation.zoom(img, resize_factor, mode='nearest') 42 | msk = nd.interpolation.zoom(msk, resize_factor, mode='nearest') 43 | if not liver_patch: 44 | if np.unique(msk).size == 2: 45 | continue 46 | 47 | imgs.append(img) 48 | masks.append(msk) 49 | # liver_tumor_generator(patches_per_image, patch_size, img, mask, idx, 50 | # rootdir="data/patches/tumor_" + str(patch_size) + "_" + str(patches_per_image) + "/") 51 | # get_data_with_random_224(imgs, masks, 32) 52 | get_liver_tumor_data(imgs, 53 | masks, 54 | 128, 55 | 128, patch_size, 56 | 300, 57 | 50, 58 | fcn=True) 59 | 60 | 61 | def process_dicom_file(csv_path): 62 | imgs, msks = [], [] 63 | df = pd.read_csv(csv_path) 64 | for i, item in df.iterrows(): 65 | img = read_dicom_series(item[0]) 66 | mask = read_liver_lesion_masks(item[1]) 67 | print('train data select dicom file:' + item[0]) 68 | img = set_bounds(img, MIN_IMG_BOUND, MAX_IMG_BOUND) 69 | mask = set_bounds(mask, MIN_MSK_BOUND, MAX_MSK_BOUND) 70 | imgs.append(img) 71 | msks.append(mask) 72 | 73 | 74 | def process_test_nii_file(csv_path): 75 | imgs, msks = [], [] 76 | df = pd.read_csv(csv_path) 77 | for i, item in df.iterrows(): 78 | imgnii = nib.load(item[0]) 79 | print('train data select nii file:' + item[0]) 80 | img = imgnii.get_data() 81 | img = set_bounds(img, MIN_IMG_BOUND, MAX_IMG_BOUND) 82 | imgs.append(img) 83 | 84 | 85 | def resample(img, seg, scan, new_voxel_dim=[1, 1, 1]): 86 | # Get voxel size 87 | voxel_dim = np.array(scan.header.structarr["pixdim"][1:4], dtype=np.float32) 88 | # Resample to optimal [1,1,1] voxel size 89 | resize_factor = voxel_dim / new_voxel_dim 90 | scan_shape = np.array(scan.header.get_data_shape()) 91 | new_scan_shape = scan_shape * resize_factor 92 | rounded_new_scan_shape = np.round(new_scan_shape) 93 | rounded_resize_factor = rounded_new_scan_shape / scan_shape # Change resizing due to round off error 94 | new_voxel_dim = voxel_dim / rounded_resize_factor 95 | 96 | img = nd.interpolation.zoom(img, rounded_resize_factor, mode='nearest') 97 | seg = nd.interpolation.zoom(seg, rounded_resize_factor, mode='nearest') 98 | return img, seg, new_voxel_dim 99 | 100 | 101 | def min_max_voi_with_liver(mask, superior=10, inferior=10): 102 | sp = mask.shape 103 | tp = np.transpose(np.nonzero(mask)) 104 | minx, miny, minz = np.min(tp, axis=0) 105 | maxx, maxy, maxz = np.max(tp, axis=0) 106 | minz = 0 if minz - superior < 0 else minz - superior 107 | maxz = sp[-1] if maxz + inferior > sp[-1] else maxz + inferior + 1 108 | miny = 0 if miny - superior < 0 else miny - superior 109 | maxy = sp[1] if maxy + inferior > sp[1] else maxy + inferior + 1 110 | minx = 0 if minx - superior < 0 else minx - superior 111 | maxx = sp[0] if maxx + inferior > sp[0] else maxx + inferior + 1 112 | return minx, maxx, miny, maxy, minz, maxz 113 | 114 | 115 | def generate_nii_from_dicom(): 116 | # generate nii from dicom 117 | # import dicom2nifti 118 | for i in range(1, 21): 119 | lbl = read_liver_lesion_masks( 120 | 'data/3Dircadb1/3Dircadb1.' + str(i) + '/MASKS_DICOM') 121 | imgnii = nib.load( 122 | 'data/3Dircadb1/3Dircadb1.' + str(i) + '/3Dircadb1.' + str( 123 | i) + '.nii') 124 | img = imgnii.get_data() 125 | sp = img.shape 126 | 127 | nib.save(nib.Nifti1Image(lbl, affine=imgnii.get_affine()), 128 | 'data/3Dircadb1/3Dircadb1.' + str( 129 | i) + '/3Dircadb_gt_1.' + str(i) + '.nii') 130 | 131 | 132 | def preprocess_mri_nii(file_path='../datasets/brain/MICCAI_BraTS_2018_Data_Training/HGG/', ): 133 | image_paths = [] 134 | patches_per_image = 500 135 | patch_size = 64 136 | image_size = (240, 240, 155) 137 | rootdir = "data/patches/tumor_" + str(patch_size) + "_" + str(patches_per_image) + "/" 138 | if not os.path.isdir(rootdir): 139 | os.mkdir(rootdir) 140 | if not os.path.isdir(rootdir + "raw"): 141 | os.mkdir(rootdir + "raw") 142 | if not os.path.isdir(rootdir + "seg"): 143 | os.mkdir(rootdir + "seg") 144 | 145 | base_locs = generate_patch_locations(patches_per_image, patch_size, image_size) 146 | x, y, z = perturb_patch_locations(base_locs, patch_size / 16) 147 | files = os.listdir(file_path) 148 | for name in files: 149 | image_paths.append(file_path + name + '/') 150 | file_path = file_path.replace('HGG', 'LGG') 151 | files = os.listdir(file_path) 152 | for name in files: 153 | image_paths.append(file_path + name + '/') 154 | random.shuffle(image_paths) 155 | vol = 0 156 | for item in image_paths: 157 | # print(i) 158 | print(item) 159 | probs = generate_patch_probs(item, (x, y, z), patch_size, image_size) 160 | selections = np.random.choice(range(len(probs)), size=patches_per_image, replace=False, p=probs) 161 | image = read_image(item) 162 | 163 | for num, sel in enumerate(selections): 164 | i, j, k = np.unravel_index(sel, (len(x), len(y), len(z))) 165 | patch = image[int(x[i] - patch_size / 2): int(x[i] + patch_size / 2), 166 | int(y[j] - patch_size / 2): int(y[j] + patch_size / 2), 167 | int(z[k] - patch_size / 2): int(z[k] + patch_size / 2), :] 168 | np.save( 169 | rootdir + 'raw/img-' + '{0:0>3}'.format(vol) + '_' + '{0:0>3}'.format( 170 | num) + '.npy', patch[:, :, :, 0:4]) 171 | np.save( 172 | rootdir + 'seg/msk-' + '{0:0>3}'.format(vol) + '_' + '{0:0>3}'.format( 173 | num) + '.npy', 174 | np.expand_dims(np.where(patch[:, :, :, 4] > 0, 1, 0), -1)) 175 | vol += 1 176 | 177 | 178 | if __name__ == '__main__': 179 | root = 'Demo/idx-train-full.csv' 180 | process_liver_tumor_nii_file(root) 181 | -------------------------------------------------------------------------------- /surface.py: -------------------------------------------------------------------------------- 1 | """ 2 | @package medpy.metric.surface 3 | Holds a metrics class computing surface metrics over two 3D-images contain each a binary object. 4 | 5 | Classes: 6 | - Surface: Computes different surface metrics between two 3D-images contain each an object. 7 | 8 | @author Oskar Maier 9 | @version r0.4.1 10 | @since 2011-12-01 11 | @status Release 12 | """ 13 | 14 | # build-in modules 15 | import math 16 | 17 | # third-party modules 18 | import scipy.spatial 19 | import scipy.ndimage.morphology 20 | 21 | # own modules 22 | 23 | # code 24 | class Surface(object): 25 | """ 26 | Computes different surface metrics between two 3D-images contain each an object. 27 | The surface of the objects is computed using a 18-neighbourhood edge detection. 28 | The distance metrics are computed over all points of the surfaces using the nearest 29 | neighbour approach. 30 | Beside this provides a number of statistics of the two images. 31 | 32 | During the initialization the edge detection is run for both images, taking up to 33 | 5 min (on 512^3 images). The first call to one of the metric measures triggers the 34 | computation of the nearest neighbours, taking up to 7 minutes (based on 250.000 edge 35 | point for each of the objects, which corresponds to a typical liver mask). All 36 | subsequent calls to one of the metrics measures can be expected be in the 37 | sub-millisecond area. 38 | 39 | Metrics defined in: 40 | Heimann, T.; van Ginneken, B.; Styner, M.A.; Arzhaeva, Y.; Aurich, V.; Bauer, C.; Beck, A.; Becker, C.; Beichel, R.; Bekes, G.; Bello, F.; Binnig, G.; Bischof, H.; Bornik, A.; Cashman, P.; Ying Chi; Cordova, A.; Dawant, B.M.; Fidrich, M.; Furst, J.D.; Furukawa, D.; Grenacher, L.; Hornegger, J.; Kainmuller, D.; Kitney, R.I.; Kobatake, H.; Lamecker, H.; Lange, T.; Jeongjin Lee; Lennon, B.; Rui Li; Senhu Li; Meinzer, H.-P.; Nemeth, G.; Raicu, D.S.; Rau, A.-M.; van Rikxoort, E.M.; Rousson, M.; Rusko, L.; Saddi, K.A.; Schmidt, G.; Seghers, D.; Shimizu, A.; Slagmolen, P.; Sorantin, E.; Soza, G.; Susomboon, R.; Waite, J.M.; Wimmer, A.; Wolf, I.; , "Comparison and Evaluation of Methods for Liver Segmentation From CT Datasets," Medical Imaging, IEEE Transactions on , vol.28, no.8, pp.1251-1265, Aug. 2009 41 | doi: 10.1109/TMI.2009.2013851 42 | """ 43 | 44 | # The edge points of the mask object. 45 | __mask_edge_points = None 46 | # The edge points of the reference object. 47 | __reference_edge_points = None 48 | # The nearest neighbours distances between mask and reference edge points. 49 | __mask_reference_nn = None 50 | # The nearest neighbours distances between reference and mask edge points. 51 | __reference_mask_nn = None 52 | # Distances of the two objects surface points. 53 | __distance_matrix = None 54 | 55 | def __init__(self, mask, reference, physical_voxel_spacing = [1,1,1], mask_offset = [0,0,0], reference_offset = [0,0,0]): 56 | """ 57 | Initialize the class with two binary images, each containing a single object. 58 | Assumes the input to be a representation of a 3D image, that fits one of the 59 | following formats: 60 | - 1. all 0 values denoting background, all others the foreground/object 61 | - 2. all False values denoting the background, all others the foreground/object 62 | The first image passed is referred to as 'mask', the second as 'reference'. This 63 | is only important for some metrics that are not symmetric (and therefore not 64 | really metrics). 65 | @param mask binary mask as an scipy array (3D image) 66 | @param reference binary reference as an scipy array (3D image) 67 | @param physical_voxel_spacing The physical voxel spacing of the two images 68 | (must be the same for both) 69 | @param mask_offset offset of the mask array to 0,0,0-origin 70 | @param reference_offset offset of the reference array to 0,0,0-origin 71 | """ 72 | # compute edge images 73 | mask_edge_image = Surface.compute_contour(mask) 74 | reference_edge_image = Surface.compute_contour(reference) 75 | 76 | # collect the object edge voxel positions 77 | # !TODO: When the distance matrix is already calculated here 78 | # these points don't have to be actually stored, only their number. 79 | # But there might be some later metric implementation that requires the 80 | # points and then it would be good to have them. What is better? 81 | mask_pts = mask_edge_image.nonzero() 82 | mask_edge_points = list(zip(mask_pts[0], mask_pts[1], mask_pts[2])) 83 | reference_pts = reference_edge_image.nonzero() 84 | reference_edge_points = list(zip(reference_pts[0], reference_pts[1], reference_pts[2])) 85 | 86 | # check if there is actually an object present 87 | if 0 >= len(mask_edge_points): 88 | raise Exception('The mask image does not seem to contain an object.') 89 | if 0 >= len(reference_edge_points): 90 | raise Exception('The reference image does not seem to contain an object.') 91 | 92 | # add offsets to the voxels positions and multiply with physical voxel spacing 93 | # to get the real positions in millimeters 94 | physical_voxel_spacing = scipy.array(physical_voxel_spacing) 95 | mask_edge_points += scipy.array(mask_offset) 96 | mask_edge_points *= physical_voxel_spacing 97 | reference_edge_points += scipy.array(reference_offset) 98 | reference_edge_points *= physical_voxel_spacing 99 | 100 | # set member vars 101 | self.__mask_edge_points = mask_edge_points 102 | self.__reference_edge_points = reference_edge_points 103 | 104 | def get_maximum_symmetric_surface_distance(self): 105 | """ 106 | Computes the maximum symmetric surface distance, also known as Hausdorff 107 | distance, between the two objects surfaces. 108 | 109 | @return the maximum symmetric surface distance in millimeters 110 | 111 | For a perfect segmentation this distance is 0. This metric is sensitive to 112 | outliers and returns the true maximum error. 113 | 114 | Metric definition: 115 | Let \f$S(A)\f$ denote the set of surface voxels of \f$A\f$. The shortest 116 | distance of an arbitrary voxel \f$v\f$ to \f$S(A)\f$ is defined as: 117 | \f[ 118 | d(v,S(A)) = \min_{s_A\in S(A)} ||v-s_A|| 119 | \f] 120 | where \f$||.||\f$ denotes the Euclidean distance. The maximum symmetric 121 | surface distance is then given by: 122 | \f[ 123 | MSD(A,B) = \max 124 | \left\{ 125 | \max_{s_A\in S(A)} d(s_A,S(B)), 126 | \max_{s_B\in S(B)} d(s_B,S(A)), 127 | \right\} 128 | \f] 129 | """ 130 | # Get the maximum of the nearest neighbour distances 131 | A_B_distance = self.get_mask_reference_nn().max() 132 | B_A_distance = self.get_reference_mask_nn().max() 133 | 134 | # compute result and return 135 | return max(A_B_distance, B_A_distance) 136 | 137 | def get_root_mean_square_symmetric_surface_distance(self): 138 | """ 139 | Computes the root mean square symmetric surface distance between the 140 | two objects surfaces. 141 | 142 | @return root mean square symmetric surface distance in millimeters 143 | 144 | For a perfect segmentation this distance is 0. This metric punishes large 145 | deviations from the true contour stronger than the average symmetric surface 146 | distance. 147 | 148 | Metric definition: 149 | Let \f$S(A)\f$ denote the set of surface voxels of \f$A\f$. The shortest 150 | distance of an arbitrary voxel \f$v\f$ to \f$S(A)\f$ is defined as: 151 | \f[ 152 | d(v,S(A)) = \min_{s_A\in S(A)} ||v-s_A|| 153 | \f] 154 | where \f$||.||\f$ denotes the Euclidean distance. The root mean square 155 | symmetric surface distance is then given by: 156 | \f[ 157 | RMSD(A,B) = 158 | \sqrt{\frac{1}{|S(A)|+|S(B)|}} 159 | \times 160 | \sqrt{ 161 | \sum_{s_A\in S(A)} d^2(s_A,S(B)) 162 | + 163 | \sum_{s_B\in S(B)} d^2(s_B,S(A)) 164 | } 165 | \f] 166 | """ 167 | # get object sizes 168 | mask_surface_size = len(self.get_mask_edge_points()) 169 | reference_surface_sice = len(self.get_reference_edge_points()) 170 | 171 | # get minimal nearest neighbours distances 172 | A_B_distances = self.get_mask_reference_nn() 173 | B_A_distances = self.get_reference_mask_nn() 174 | 175 | # square the distances 176 | A_B_distances_sqrt = A_B_distances * A_B_distances 177 | B_A_distances_sqrt = B_A_distances * B_A_distances 178 | 179 | # sum the minimal distances 180 | A_B_distances_sum = A_B_distances_sqrt.sum() 181 | B_A_distances_sum = B_A_distances_sqrt.sum() 182 | 183 | # compute result and return 184 | return math.sqrt(1. / (mask_surface_size + reference_surface_sice)) * math.sqrt(A_B_distances_sum + B_A_distances_sum) 185 | 186 | def get_average_symmetric_surface_distance(self): 187 | """ 188 | Computes the average symmetric surface distance between the 189 | two objects surfaces. 190 | 191 | @return average symmetric surface distance in millimeters 192 | 193 | For a perfect segmentation this distance is 0. 194 | 195 | Metric definition: 196 | Let \f$S(A)\f$ denote the set of surface voxels of \f$A\f$. The shortest 197 | distance of an arbitrary voxel \f$v\f$ to \f$S(A)\f$ is defined as: 198 | \f[ 199 | d(v,S(A)) = \min_{s_A\in S(A)} ||v-s_A|| 200 | \f] 201 | where \f$||.||\f$ denotes the Euclidean distance. The average symmetric 202 | surface distance is then given by: 203 | \f[ 204 | ASD(A,B) = 205 | \frac{1}{|S(A)|+|S(B)|} 206 | \left( 207 | \sum_{s_A\in S(A)} d(s_A,S(B)) 208 | + 209 | \sum_{s_B\in S(B)} d(s_B,S(A)) 210 | \right) 211 | \f] 212 | """ 213 | # get object sizes 214 | mask_surface_size = len(self.get_mask_edge_points()) 215 | reference_surface_sice = len(self.get_reference_edge_points()) 216 | 217 | # get minimal nearest neighbours distances 218 | A_B_distances = self.get_mask_reference_nn() 219 | B_A_distances = self.get_reference_mask_nn() 220 | 221 | # sum the minimal distances 222 | A_B_distances = A_B_distances.sum() 223 | B_A_distances = B_A_distances.sum() 224 | 225 | # compute result and return 226 | return 1. / (mask_surface_size + reference_surface_sice) * (A_B_distances + B_A_distances) 227 | 228 | def get_mask_reference_nn(self): 229 | """ 230 | @return The distances of the nearest neighbours of all mask edge points to all 231 | reference edge points. 232 | """ 233 | # Note: see note for @see get_reference_mask_nn 234 | if None == self.__mask_reference_nn: 235 | tree = scipy.spatial.cKDTree(self.get_mask_edge_points()) 236 | self.__mask_reference_nn, _ = tree.query(self.get_reference_edge_points()) 237 | return self.__mask_reference_nn 238 | 239 | def get_reference_mask_nn(self): 240 | """ 241 | @return The distances of the nearest neighbours of all reference edge points 242 | to all mask edge points. 243 | 244 | The underlying algorithm used for the scipy.spatial.KDTree implementation is 245 | based on: 246 | Sunil Arya, David M. Mount, Nathan S. Netanyahu, Ruth Silverman, and 247 | Angela Y. Wu. 1998. An optimal algorithm for approximate nearest neighbor 248 | searching fixed dimensions. J. ACM 45, 6 (November 1998), 891-923 249 | """ 250 | # Note: KDTree is faster than scipy.spatial.distance.cdist when the number of 251 | # voxels exceeds 10.000 (computationally tested). The maximum complexity is 252 | # O(D*N^2) vs. O(D*N*log(N), where D=3 and N=number of voxels 253 | if None == self.__reference_mask_nn: 254 | tree = scipy.spatial.cKDTree(self.get_reference_edge_points()) 255 | self.__reference_mask_nn, _ = tree.query(self.get_mask_edge_points()) 256 | return self.__reference_mask_nn 257 | 258 | def get_mask_edge_points(self): 259 | """ 260 | @return The edge points of the mask object. 261 | """ 262 | return self.__mask_edge_points 263 | 264 | def get_reference_edge_points(self): 265 | """ 266 | @return The edge points of the reference object. 267 | """ 268 | return self.__reference_edge_points 269 | 270 | @staticmethod 271 | def compute_contour(array): 272 | """ 273 | Uses a 18-neighbourhood filter to create an edge image of the input object. 274 | Assumes the input to be a representation of a 3D image, that fits one of the 275 | following formats: 276 | - 1. all 0 values denoting background, all others the foreground/object 277 | - 2. all False values denoting the background, all others the foreground/object 278 | The area outside the array is assumed to contain background voxels. The method 279 | does not ensure that the object voxels are actually connected, this is silently 280 | assumed. 281 | 282 | @param array a numpy array with only 0/N\{0} or False/True values. 283 | @return a boolean numpy array with the input objects edges 284 | """ 285 | # set 18-neighbourhood/conectivity (for 3D images) alias face-and-edge kernel 286 | # all values covered by 1/True passed to the function 287 | # as a 1D array in order left-right, top-down 288 | # Note: all in all 19 ones, as the center value 289 | # also has to be checked (if it is a masked pixel) 290 | # [[[0, 1, 0], [[1, 1, 1], [[0, 1, 0], 291 | # [1, 1, 1], [1, 1, 1], [1, 1, 1], 292 | # [0, 1, 0]], [1, 1, 1]], [0, 1, 0]]] 293 | footprint = scipy.ndimage.morphology.generate_binary_structure(3, 2) 294 | 295 | # create an erode version of the array 296 | erode_array = scipy.ndimage.morphology.binary_erosion(array, footprint) 297 | 298 | # xor the erode_array with the original and return 299 | return array ^ erode_array -------------------------------------------------------------------------------- /train.py: -------------------------------------------------------------------------------- 1 | from keras.callbacks import ReduceLROnPlateau, ModelCheckpoint, Callback, LambdaCallback, \ 2 | CSVLogger 3 | from keras.utils import multi_gpu_model 4 | import os 5 | import time 6 | 7 | os.environ['KERAS_BACKEND'] = 'tensorflow' 8 | os.environ["CUDA_VISIBLE_DEVICES"] = "1" 9 | # os.environ["CUDA_VISIBLE_DEVICES"] = "0,1" 10 | from keras.utils.vis_utils import plot_model 11 | from keras.optimizers import * 12 | from build_model import get_net 13 | from preprocess import * 14 | from utils.utils import * 15 | from keras.models import model_from_json 16 | import json 17 | 18 | config = tf.ConfigProto() 19 | config.gpu_options.allow_growth = True 20 | sess = tf.Session(config=config) 21 | 22 | CFG = configure.cfg 23 | LOG_FILE = CFG['log_file'] 24 | log_path_experiment = '' 25 | log_tensorboard_filepath = '' 26 | logger = '' 27 | 28 | 29 | def get_log_path(): 30 | return log_path_experiment 31 | 32 | 33 | class ParallelModelCheckpoint(ModelCheckpoint): 34 | def __init__(self, model, filepath, monitor='val_loss', verbose=0, 35 | save_best_only=False, save_weights_only=False, 36 | mode='auto', period=1): 37 | self.single_model = model 38 | super(ParallelModelCheckpoint, self).__init__(filepath, monitor, verbose, save_best_only, save_weights_only, 39 | mode, period) 40 | 41 | def set_model(self, model): 42 | super(ParallelModelCheckpoint, self).set_model(self.single_model) 43 | 44 | 45 | # because the keras bug. if para we must use the origion model to save the shared weights 46 | class ModelCallBackForMultiGPU(Callback): 47 | def __init__(self, model, log_path_experiment): 48 | self.model_to_save = model 49 | self.log_path = log_path_experiment 50 | 51 | def on_epoch_end(self, epoch, logs=None): 52 | if epoch % 1 == 0: 53 | self.model_to_save.save_weights( 54 | self.log_path + '/model_at_' + time.strftime('%Y-%m-%d_%H:%M:%S', time.localtime( 55 | time.time())) + '_epoch_%05d.hdf5' % epoch) 56 | # print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))) 57 | 58 | 59 | def train_liver_slice(algorithm='unet', patch_data_dir='data/slices/slices_filtered_0.66', val_ratio=0.289, 60 | init_lr=1e-3, gpus=2, n_rounds=60, batch_size=90): 61 | # 'unet vgg_fcn' 62 | log_path_experiment = LOG_FILE + '/' + algorithm 63 | define_log(LOG_DIR, algorithm) 64 | # 70 for 3gpu 65 | model_out_dir = "{}/model_{}".format(log_path_experiment, val_ratio) 66 | if not os.path.isdir(model_out_dir): 67 | os.makedirs(model_out_dir) 68 | train_batch_fetcher = TrainBatchFetcher(patch_data_dir, val_ratio) 69 | val_imgs, val_labels = train_batch_fetcher.vali_data() 70 | print(val_imgs.shape) 71 | print(val_labels.shape) 72 | # Build model 73 | model = get_net(val_imgs[0].shape, algorithm) 74 | with open(os.path.join(model_out_dir, "g_{}.json".format(val_ratio)), 'w') as f: 75 | f.write(model.to_json()) 76 | parallel_model = multi_gpu_model(model, gpus=gpus) 77 | # parallel_model = model 78 | adam = Adam(lr=init_lr, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0) 79 | # parallel_model.compile(optimizer=adam, loss='binary_crossentropy', metrics=['accuracy']) 80 | parallel_model.compile(optimizer=adam, loss=dice_coef_loss, metrics=['accuracy', dice_coef]) 81 | checkpointer = ModelCallBackForMultiGPU(model, log_path_experiment) 82 | csv_logger = CSVLogger(log_path_experiment + '/training.csv', append=True) 83 | reduce = ReduceLROnPlateau(factor=0.1, patience=20, verbose=2) 84 | cbks = [checkpointer, reduce, csv_logger] 85 | # Visualize model 86 | plot_model(model, to_file=log_path_experiment + '/model.pdf', show_shapes=True) 87 | 88 | model.summary() 89 | ########################################################################################## 90 | # Train the model 91 | parallel_model.fit_generator(generator=train_batch_fetcher.next(batch_size), 92 | steps_per_epoch=train_batch_fetcher.num_training // batch_size, 93 | epochs=n_rounds, 94 | verbose=0, 95 | callbacks=cbks, 96 | validation_data=(val_imgs, val_labels), 97 | validation_steps=train_batch_fetcher.num_validation // batch_size) 98 | 99 | # Evaluate the model 100 | score = parallel_model.evaluate( 101 | val_imgs, val_labels, 102 | batch_size=batch_size, verbose=2 103 | ) 104 | print('**********************************************') 105 | print('Test score:', score) 106 | 107 | 108 | def train_liver_patches_with_alg(algorithm='resunet', patch_data_dir='data/patches', val_ratio=0.2, 109 | pre_trained_weight=None, 110 | model_path=None, init_lr=1e-3, batch_size=60, n_rounds=200, gpu=2, iskfold=False, k=5, 111 | patience=20): 112 | # algorithm = '3dunet' 113 | log_path_experiment = LOG_FILE + '/' + algorithm 114 | define_log(LOG_DIR, algorithm) 115 | # 7-8 per gpu 21 64*64 3gpu;;;; for attention res-unet 3 per gpu 116 | model_out_dir = "{}/model_{}".format(log_path_experiment, val_ratio) 117 | if not os.path.isdir(model_out_dir): 118 | os.makedirs(model_out_dir) 119 | if iskfold: 120 | cvscores = [] 121 | for i in range(0, k): 122 | print(str(i) + 'th fold training') 123 | train_batch_fetcher = TrainBatchFetcher(patch_data_dir, 1. / k * (i + 1), valid_from=1. / k * i) 124 | val_imgs, val_labels = train_batch_fetcher.get_one() 125 | model = get_net(val_imgs[0].shape, algorithm) 126 | with open(os.path.join(model_out_dir, "g_{}.json".format(val_ratio)), 'w') as f: 127 | f.write(model.to_json()) 128 | plot_model(model, to_file=log_path_experiment + '/model.pdf', show_shapes=True) 129 | checkpointer = ModelCallBackForMultiGPU(model, log_path_experiment) 130 | reduce = ReduceLROnPlateau(factor=0.1, patience=patience, verbose=2) 131 | json_log = open(log_path_experiment + '/val_loss_log.json', mode='at', buffering=1) 132 | csv_logger = CSVLogger(log_path_experiment + '/training.csv', append=True) 133 | cbks = [checkpointer, reduce, csv_logger] 134 | adam = Adam(lr=init_lr, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.000000199) 135 | # parallel_model = multi_gpu_model(model, gpus=gpu) 136 | parallel_model = model 137 | # if val_imgs[0].shape[0] != 224: 138 | # parallel_model.compile(optimizer=adam, loss='binary_crossentropy', metrics=['accuracy']) 139 | # else: 140 | parallel_model.compile(optimizer=adam, loss=dice_coef_loss, metrics=['accuracy', dice_coef]) 141 | parallel_model.fit_generator(generator=train_batch_fetcher.next(batch_size), 142 | steps_per_epoch=train_batch_fetcher.num_training // batch_size, 143 | epochs=n_rounds, 144 | verbose=2, 145 | callbacks=cbks, 146 | validation_data=train_batch_fetcher.next(batch_size, 'vali'), 147 | validation_steps=train_batch_fetcher.num_validation // batch_size) 148 | 149 | # evaluate the model 150 | # scores = parallel_model.evaluate(val_imgs, val_labels, verbose=2) 151 | # print("%s: %.2f%%" % (parallel_model.metrics_names[1], scores[1] * 100)) 152 | # del parallel_model, model 153 | # gc.collect() 154 | # cvscores.append(scores[1] * 100) 155 | # print("%.2f%% (+/- %.2f%%)" % (np.mean(cvscores), np.std(cvscores))) 156 | else: 157 | train_batch_fetcher = TrainBatchFetcher(patch_data_dir, val_ratio) 158 | val_imgs, val_labels = train_batch_fetcher.get_one() 159 | # with tf.device('/cpu:0'): 160 | if model_path == None: 161 | model = get_net(val_imgs[0].shape, algorithm) 162 | with open(os.path.join(model_out_dir, "g_{}.json".format(val_ratio)), 'w') as f: 163 | f.write(model.to_json()) 164 | model.summary() 165 | else: 166 | with open(model_path, 'r') as f: 167 | model = model_from_json(f.read()) 168 | model.load_weights(pre_trained_weight) 169 | print('load pretrained_weight') 170 | checkpointer = ModelCallBackForMultiGPU(model, log_path_experiment) 171 | # checkpointer = ModelCheckpoint(filepath=log_path_experiment+"/checkpoint-{epoch:02d}e- val_acc_{val_acc: .4f}.hdf5", save_best_only=False, verbose=1, 172 | # period=1) 173 | json_log = open(log_path_experiment + '/val_loss_log.json', mode='at', buffering=1) 174 | json_logging_callback = LambdaCallback( 175 | on_epoch_end=lambda epoch, logs: json_log.write( 176 | json.dumps({'epoch': epoch, 'val_loss': logs['val_loss'], 177 | 'val_acc': logs['val_acc'], 178 | 'val_dice_coef': logs['val_dice_coef']}) + '\n') 179 | # on_train_end=lambda logs: json_log.close() 180 | ) 181 | reduce = ReduceLROnPlateau(factor=0.1, verbose=1, patience=20) 182 | csv_logger = CSVLogger(log_path_experiment + '/training.csv', append=True) 183 | cbks = [checkpointer, reduce, csv_logger] 184 | plot_model(model, to_file=log_path_experiment + '/model.pdf', show_shapes=True) 185 | parallel_model = multi_gpu_model(model, gpus=gpu) 186 | # parallel_model = model 187 | # adam = Adam(lr=init_lr) 188 | # sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True) 189 | # if val_imgs[0].shape[0] != 224: 190 | # parallel_model.compile(optimizer=adam, loss='binary_crossentropy', metrics=['accuracy']) 191 | # else: 192 | # parallel_model.compile(optimizer=adam, loss=dice_coef_loss, metrics=['accuracy', dice_coef]) 193 | # parallel_model.compile(optimizer=Adam(lr=1e-5, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.000000199), 194 | # loss='binary_crossentropy', metrics=['accuracy']) 195 | parallel_model.compile(optimizer=Adam(lr=init_lr, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.000000199), 196 | loss=dice_coef_loss, metrics=['accuracy', dice_coef]) 197 | 198 | parallel_model.fit_generator(generator=train_batch_fetcher.next(batch_size), 199 | steps_per_epoch=train_batch_fetcher.num_training // batch_size, 200 | epochs=n_rounds, 201 | verbose=2, 202 | callbacks=cbks, 203 | validation_data=train_batch_fetcher.next(batch_size, 'vali'), 204 | validation_steps=train_batch_fetcher.num_validation // batch_size 205 | ) 206 | print('**********************************************') 207 | 208 | 209 | if __name__ == '__main__': 210 | # train slices raunet1 211 | # train_liver_slice(algorithm='liver_att_resunet_2d', val_ratio=0.2, 212 | # patch_data_dir='data/patches/liver_slices', n_rounds=100) 213 | # train slices raunet2 214 | # train_liver_patches_with_alg(algorithm='liver_att_resunet_3d', 215 | # patch_data_dir='data/patches/liver_224', 216 | # gpu=2, init_lr=1e-3, batch_size=2, n_rounds=50, iskfold=True, patience=15) 217 | 218 | train_liver_patches_with_alg(algorithm='liver_tumor_att_resunet_3d', 219 | patch_data_dir='data/patches/liver_tumor_128_350', 220 | gpu=2, init_lr=1e-3, batch_size=6, n_rounds=60, iskfold=True, patience=15) 221 | -------------------------------------------------------------------------------- /utils/brain_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import os, glob 3 | import numpy as np 4 | from skimage.transform import resize, warp 5 | from transforms3d.euler import euler2mat 6 | from transforms3d.affines import compose 7 | import nibabel as nib 8 | 9 | 10 | def get_random_transformation(): 11 | T = [0, np.random.uniform(-8, 8), np.random.uniform(-8, 8)] 12 | R = euler2mat(np.random.uniform(-5, 5) / 180.0 * np.pi, 0, 0, 'sxyz') 13 | Z = [1, np.random.uniform(0.9, 1.1), np.random.uniform(0.9, 1.1)] 14 | A = compose(T, R, Z) 15 | return A 16 | 17 | 18 | def get_tform_coords(im_size): 19 | coords0, coords1, coords2 = np.mgrid[:im_size[0], :im_size[1], :im_size[2]] 20 | coords = np.array([coords0 - im_size[0] / 2, coords1 - im_size[1] / 2, coords2 - im_size[2] / 2]) 21 | return np.append(coords.reshape(3, -1), np.ones((1, np.prod(im_size))), axis=0) 22 | 23 | 24 | def remove_low_high(im_input): 25 | im_output = im_input 26 | low = np.percentile(im_input, 1) 27 | high = np.percentile(im_output, 99) 28 | im_output[im_input < low] = low 29 | im_output[im_input > high] = high 30 | return im_output 31 | 32 | 33 | def normalize(im_input): 34 | x_start = im_input.shape[0] // 4 35 | x_range = im_input.shape[0] // 2 36 | y_start = im_input.shape[1] // 4 37 | y_range = im_input.shape[1] // 2 38 | z_start = im_input.shape[2] // 4 39 | z_range = im_input.shape[2] // 2 40 | roi = im_input[x_start: x_start + x_range, y_start: y_start + y_range, z_start: z_start + z_range] 41 | im_output = (im_input - np.mean(roi)) / np.std(roi) 42 | return im_output 43 | 44 | 45 | def read_label(path, is_training=True): 46 | seg = nib.load(glob.glob(os.path.join(path, '*_seg.nii.gz'))[0]).get_data().astype(np.float32) 47 | # Crop to 128*128*64 48 | crop_size = (128, 128, 64) 49 | crop = [int((seg.shape[0] - crop_size[0]) / 2), int((seg.shape[1] - crop_size[1]) / 2), 50 | int((seg.shape[2] - crop_size[2]) / 2)] 51 | seg = seg[crop[0]: crop[0] + crop_size[0], crop[1]: crop[1] + crop_size[1], crop[2]: crop[2] + crop_size[2]] 52 | label = np.zeros((seg.shape[0], seg.shape[1], seg.shape[2], 3), dtype=np.float32) 53 | label[seg == 1, 0] = 1 54 | label[seg == 2, 1] = 1 55 | label[seg == 4, 2] = 1 56 | 57 | final_label = np.empty((16, 16, 16, 3), dtype=np.float32) 58 | for z in range(label.shape[3]): 59 | final_label[..., z] = resize(label[..., z], (16, 16, 16), mode='constant') 60 | 61 | # Augmentation 62 | if is_training: 63 | im_size = final_label.shape[:-1] 64 | translation = [np.random.uniform(-2, 2), np.random.uniform(-2, 2), np.random.uniform(-2, 2)] 65 | rotation = euler2mat(0, 0, np.random.uniform(-5, 5) / 180.0 * np.pi, 'sxyz') 66 | scale = [1, 1, 1] 67 | warp_mat = compose(translation, rotation, scale) 68 | tform_coords = get_tform_coords(im_size) 69 | w = np.dot(warp_mat, tform_coords) 70 | w[0] = w[0] + im_size[0] / 2 71 | w[1] = w[1] + im_size[1] / 2 72 | w[2] = w[2] + im_size[2] / 2 73 | warp_coords = w[0:3].reshape(3, im_size[0], im_size[1], im_size[2]) 74 | for z in range(label.shape[3]): 75 | final_label[..., z] = warp(final_label[..., z], warp_coords) 76 | 77 | return final_label 78 | 79 | 80 | def read_seg(path): 81 | seg = nib.load(glob.glob(os.path.join(path, '*_seg.nii.gz'))[0]).get_data().astype(np.float32) 82 | return seg 83 | 84 | 85 | def read_image(path, is_training=True): 86 | t1 = nib.load(glob.glob(os.path.join(path, '*_t1_corrected.nii.gz'))[0]).get_data().astype(np.float32) 87 | t1ce = nib.load(glob.glob(os.path.join(path, '*_t1ce_corrected.nii.gz'))[0]).get_data().astype(np.float32) 88 | t2 = nib.load(glob.glob(os.path.join(path, '*_t2.nii.gz'))[0]).get_data().astype(np.float32) 89 | flair = nib.load(glob.glob(os.path.join(path, '*_flair.nii.gz'))[0]).get_data().astype(np.float32) 90 | assert t1.shape == t1ce.shape == t2.shape == flair.shape 91 | if is_training: 92 | seg = nib.load(glob.glob(os.path.join(path, '*_seg.nii.gz'))[0]).get_data().astype(np.float32) 93 | assert t1.shape == seg.shape 94 | nchannel = 5 95 | else: 96 | nchannel = 4 97 | 98 | image = np.empty((t1.shape[0], t1.shape[1], t1.shape[2], nchannel), dtype=np.float32) 99 | 100 | # image[..., 0] = remove_low_high(t1) 101 | # image[..., 1] = remove_low_high(t1ce) 102 | # image[..., 2] = remove_low_high(t2) 103 | # image[..., 3] = remove_low_high(flair) 104 | image[..., 0] = normalize(t1) 105 | image[..., 1] = normalize(t1ce) 106 | image[..., 2] = normalize(t2) 107 | image[..., 3] = normalize(flair) 108 | 109 | if is_training: 110 | image[..., 4] = seg 111 | 112 | return image 113 | 114 | 115 | def read_patch(path): 116 | image = np.load(path + '.npy') 117 | seg = image[..., -1] 118 | label = np.zeros((image.shape[0], image.shape[1], image.shape[2], 4), dtype=np.float32) 119 | label[seg == 0, 0] = 1 120 | label[seg == 1, 1] = 1 121 | label[seg == 2, 2] = 1 122 | label[seg == 4, 3] = 1 123 | return image[..., :-1], label 124 | 125 | 126 | def generate_patch_locations(patches, patch_size, im_size): 127 | nx = round((patches * 8 * im_size[0] * im_size[0] / im_size[1] / im_size[2]) ** (1.0 / 3)) 128 | ny = round(nx * im_size[1] / im_size[0]) 129 | nz = round(nx * im_size[2] / im_size[0]) 130 | x = np.rint(np.linspace(patch_size, im_size[0] - patch_size, num=nx)) 131 | y = np.rint(np.linspace(patch_size, im_size[1] - patch_size, num=ny)) 132 | z = np.rint(np.linspace(patch_size, im_size[2] - patch_size, num=nz)) 133 | return x, y, z 134 | 135 | 136 | def generate_test_locations(patch_size, stride, im_size): 137 | stride_size_x = patch_size[0] / stride 138 | stride_size_y = patch_size[1] / stride 139 | stride_size_z = patch_size[2] / stride 140 | pad_x = ( 141 | int(patch_size[0] / 2), int(np.ceil(im_size[0] / stride_size_x) * stride_size_x - im_size[0] + patch_size[0] / 2)) 142 | pad_y = ( 143 | int(patch_size[1] / 2), int(np.ceil(im_size[1] / stride_size_y) * stride_size_y - im_size[1] + patch_size[1] / 2)) 144 | pad_z = ( 145 | int(patch_size[2] / 2), int(np.ceil(im_size[2] / stride_size_z) * stride_size_z - im_size[2] + patch_size[2] / 2)) 146 | x = np.arange(patch_size[0] / 2, im_size[0] + pad_x[0] + pad_x[1] - patch_size[0] / 2 + 1, stride_size_x) 147 | y = np.arange(patch_size[1] / 2, im_size[1] + pad_y[0] + pad_y[1] - patch_size[1] / 2 + 1, stride_size_y) 148 | z = np.arange(patch_size[2] / 2, im_size[2] + pad_z[0] + pad_z[1] - patch_size[2] / 2 + 1, stride_size_z) 149 | return (x, y, z), (pad_x, pad_y, pad_z) 150 | 151 | 152 | def perturb_patch_locations(patch_locations, radius): 153 | x, y, z = patch_locations 154 | x = np.rint(x + np.random.uniform(-radius, radius, len(x))) 155 | y = np.rint(y + np.random.uniform(-radius, radius, len(y))) 156 | z = np.rint(z + np.random.uniform(-radius, radius, len(z))) 157 | return x, y, z 158 | 159 | 160 | def generate_patch_probs(path, patch_locations, patch_size, im_size): 161 | x, y, z = patch_locations 162 | seg = nib.load(glob.glob(os.path.join(path, '*_seg.nii.gz'))[0]).get_data().astype(np.float32) 163 | p = [] 164 | for i in range(len(x)): 165 | for j in range(len(y)): 166 | for k in range(len(z)): 167 | patch = seg[int(x[i] - patch_size / 2): int(x[i] + patch_size / 2), 168 | int(y[j] - patch_size / 2): int(y[j] + patch_size / 2), 169 | int(z[k] - patch_size / 2): int(z[k] + patch_size / 2)] 170 | patch = (patch > 0).astype(np.float32) 171 | percent = np.sum(patch) / (patch_size * patch_size * patch_size) 172 | p.append((1 - np.abs(percent - 0.5)) * percent) 173 | p = np.asarray(p, dtype=np.float32) 174 | p[p == 0] = np.amin(p[np.nonzero(p)]) 175 | p = p / np.sum(p) 176 | return p -------------------------------------------------------------------------------- /utils/extract_patches.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import random 3 | import os 4 | from utils.utils import generate_patch_locations, perturb_patch_locations 5 | from keras.utils import np_utils 6 | 7 | np.random.seed(1337) 8 | 9 | 10 | def liver_tumor_generator(patches_per_image, patch_size, img, mask, idx, rootdir="data/patches/tumor_32_400/"): 11 | if not os.path.isdir(rootdir): 12 | os.mkdir(rootdir) 13 | if not os.path.isdir(rootdir + "raw"): 14 | os.mkdir(rootdir + "raw") 15 | if not os.path.isdir(rootdir + "seg"): 16 | os.mkdir(rootdir + "seg") 17 | base_locs = generate_patch_locations(patches_per_image, patch_size, mask.shape) 18 | x, y, z = perturb_patch_locations(base_locs, patch_size / 16) 19 | # generate_patch_probs 20 | p = [] 21 | for i in range(len(x)): 22 | for j in range(len(y)): 23 | for k in range(len(z)): 24 | patch = mask[int(x[i] - patch_size / 2): int(x[i] + patch_size / 2), 25 | int(y[j] - patch_size / 2): int(y[j] + patch_size / 2), 26 | int(z[k] - patch_size / 2): int(z[k] + patch_size / 2)] 27 | patch = (patch > 1).astype(np.float32) 28 | percent = np.sum(patch) / (patch_size * patch_size * patch_size) 29 | p.append((1 - np.abs(percent - 0.5)) * percent) 30 | p = np.asarray(p, dtype=np.float32) 31 | p[p == 0] = np.amin(p[np.nonzero(p)]) 32 | probs = p / np.sum(p) 33 | # generate_patch_probs 34 | selections = np.random.choice(range(len(probs)), size=patches_per_image, replace=False, p=probs) 35 | imgs_normalized = (img - np.mean(img)) / np.std(img) 36 | img_save = (imgs_normalized - np.min(imgs_normalized)) / (np.max(imgs_normalized) - np.min(imgs_normalized)) 37 | for num, sel in enumerate(selections): 38 | i, j, k = np.unravel_index(sel, (len(x), len(y), len(z))) 39 | patch = img_save[int(x[i] - patch_size / 2): int(x[i] + patch_size / 2), 40 | int(y[j] - patch_size / 2): int(y[j] + patch_size / 2), 41 | int(z[k] - patch_size / 2): int(z[k] + patch_size / 2)] 42 | patch_msk = mask[int(x[i] - patch_size / 2): int(x[i] + patch_size / 2), 43 | int(y[j] - patch_size / 2): int(y[j] + patch_size / 2), 44 | int(z[k] - patch_size / 2): int(z[k] + patch_size / 2)] 45 | assert patch.shape == (patch_size, patch_size, patch_size) 46 | assert patch_msk.shape == (patch_size, patch_size, patch_size) 47 | np.save(rootdir + 'raw/img-' + '{0:0>3}'.format(idx) + '_' + '{0:0>3}'.format(num) + '.npy', 48 | np.expand_dims(patch, -1)) 49 | np.save(rootdir + 'seg/msk-' + '{0:0>3}'.format(idx) + '_' + '{0:0>3}'.format(num) + '.npy', 50 | np.expand_dims(np.where(patch_msk > 1, 1, 0), -1)) 51 | 52 | 53 | def get_liver_tumor_data(train_imgs_original, 54 | train_groudTruth, 55 | patch_height, 56 | patch_width, patch_depth, 57 | N_subimgs_positive, 58 | N_subimgs_negative, 59 | fcn=False): 60 | train_masks = train_groudTruth 61 | train_imgs = train_imgs_original 62 | 63 | # extract the TRAINING patches from the full images 64 | patches_imgs_train, patches_masks_train = extract_random_with_balance(train_imgs, train_masks, patch_height, 65 | patch_width, patch_depth, 66 | N_subimgs_positive, N_subimgs_negative, 67 | fcn=fcn) 68 | data_consistency_check_patches(patches_imgs_train, patches_masks_train) 69 | ##Fourier transform of patches 70 | if not fcn: 71 | patches_masks_train = np_utils.to_categorical(patches_masks_train, 2) 72 | else: 73 | patches_masks_train = np.concatenate((1 - patches_masks_train, patches_masks_train), -1) 74 | # already -= in the save npy phase 75 | # patches_imgs_train -= np.mean(patches_imgs_train) 76 | # patches_imgs_train /= np.max(patches_imgs_train) 77 | return patches_imgs_train, patches_masks_train # , patches_imgs_test, patches_masks_test 78 | 79 | 80 | # extract patches randomly in the full training images 81 | # -- Inside OR in full image 82 | def extract_random_with_balance(full_imgs, full_masks, patch_x, patch_y, patch_z, N_patches_positive=200, 83 | N_patches_negative=100, N_patches_border=0, fcn=False, liver=1.): 84 | patch_per_img_positive = int(N_patches_positive) 85 | patch_per_img_negative = int(N_patches_negative) 86 | patch_per_img_border = int(N_patches_border) 87 | patches = [] 88 | patches_masks = [] 89 | rootdir = "data/patches/liver_tumor_" + str(patch_x) + "_" + str(N_patches_positive + N_patches_negative) + "/" 90 | if not os.path.isdir(rootdir): 91 | os.mkdir(rootdir) 92 | if not os.path.isdir(rootdir + "raw"): 93 | os.mkdir(rootdir + "raw") 94 | if not os.path.isdir(rootdir + "seg"): 95 | os.mkdir(rootdir + "seg") 96 | print(len(full_imgs)) 97 | if not fcn: 98 | # Extract patch of 29*29*29 for liver tumor 99 | for i in range(len(full_imgs)): # loop over the full images 100 | data_consistency_check(full_imgs[i], full_masks[i]) 101 | k = 0 102 | idx_p = np.where(full_masks[i] > 1) 103 | if idx_p[0].size < 10: 104 | continue 105 | idx_n = np.where(full_masks[i] == 1.) 106 | # scales = math.ceil(idx_p[0].size / (patch_x * patch_y * patch_z)) 107 | # print(scales) 108 | # patch_per_img_positive=scales*150 109 | # patch_per_img_negative=patch_per_img_positive 110 | print("positive patches per full image: " + str(patch_per_img_positive)) 111 | print("negative patches per full image: " + str(patch_per_img_negative)) 112 | while k < (patch_per_img_positive + patch_per_img_negative): 113 | if k < patch_per_img_positive: 114 | # positive patch 115 | index = np.random.randint(len(idx_p[0]), size=1)[0] 116 | x_center, y_center, z_center = idx_p[0][index], idx_p[1][index], idx_p[2][index] 117 | else: 118 | # patch_per_img_negative patch 119 | index = np.random.randint(len(idx_n[0]), size=1)[0] 120 | x_center, y_center, z_center = idx_n[0][index], idx_n[1][index], idx_n[2][index] 121 | patch = full_imgs[i][x_center - int(patch_x / 2):x_center + int(patch_x / 2) + 1, 122 | y_center - int(patch_y / 2):y_center + int(patch_y / 2) + 1, 123 | z_center - int(patch_z / 2):z_center + int(patch_z / 2) + 1] 124 | # print(patch.shape) 125 | if patch.shape != (patch_x, patch_y, patch_z): 126 | continue 127 | patch_mask = full_masks[i][x_center, y_center, z_center] - 1 128 | patches.append(patch) 129 | patches_masks.append(patch_mask) 130 | k += 1 # per full_img 131 | patches = np.expand_dims(patches, -1) 132 | patches_masks = np.clip(np.array(patches_masks), 0, 1) 133 | else: 134 | # Extract patch of patch_x*patch_y*patch_z for liver tumor with class balance 135 | for i in range(len(full_imgs)): # loop over the full images 136 | data_consistency_check(full_imgs[i], full_masks[i]) 137 | idx_p = np.where(full_masks[i] > liver) 138 | if idx_p[0].size < 10: 139 | continue 140 | if full_imgs[i].shape[-1] < patch_z: 141 | continue 142 | k = 0 143 | print(i) 144 | # if scales < 0.5: 145 | # patch_per_img_positive = 400 146 | # elif 0.5 < scales < 1: 147 | # patch_per_img_positive = 300 148 | # else: 149 | # patch_per_img_positive = int(N_patches_positive) 150 | print('=========') 151 | idx_n = np.where(full_masks[i] == liver) 152 | idx_b = np.where(full_masks[i] == 0.) 153 | while k < (patch_per_img_positive + patch_per_img_negative + patch_per_img_border): 154 | if k < patch_per_img_positive: 155 | # positive patch 156 | index = np.random.randint(len(idx_p[0]), size=1)[0] 157 | x_center, y_center, z_center = idx_p[0][index], idx_p[1][index], idx_p[2][index] 158 | # from -patch_x to patch_x contain tumor patch 159 | x_center += random.randint(-patch_x / 2, patch_x / 2) 160 | y_center += random.randint(-patch_y / 2, patch_y / 2) 161 | z_center += random.randint(-patch_z / 2, patch_z / 2) 162 | elif patch_per_img_positive <= k <= patch_per_img_positive + patch_per_img_negative: 163 | # patch_per_img_negative patch 164 | index = np.random.randint(len(idx_n[0]), size=1)[0] 165 | x_center, y_center, z_center = idx_n[0][index], idx_n[1][index], idx_n[2][index] 166 | else: 167 | while (True): 168 | index = np.random.randint(len(idx_b[0]), size=1)[0] 169 | x_center, y_center, z_center = idx_b[0][index], idx_b[1][index], idx_b[2][index] 170 | msk = full_masks[i][x_center - int(patch_x / 2):x_center + int(patch_x / 2), 171 | y_center - int(patch_y / 2):y_center + int(patch_y / 2), 172 | z_center - int(patch_z / 2):z_center + int(patch_z / 2)] 173 | sizeofmsk = np.where(msk > 0) 174 | if len(sizeofmsk[0]) / patch_y / patch_x / patch_z > 0.3: 175 | break 176 | 177 | patch = full_imgs[i][x_center - int(patch_x / 2):x_center + int(patch_x / 2), 178 | y_center - int(patch_y / 2):y_center + int(patch_y / 2), 179 | z_center - int(patch_z / 2):z_center + int(patch_z / 2)] 180 | msk = full_masks[i][x_center - int(patch_x / 2):x_center + int(patch_x / 2), 181 | y_center - int(patch_y / 2):y_center + int(patch_y / 2), 182 | z_center - int(patch_z / 2):z_center + int(patch_z / 2)] 183 | # print(patch.shape) 184 | if patch.shape != (patch_x, patch_y, patch_z): 185 | continue 186 | np.save(rootdir + 'raw/img-' + '{0:0>3}'.format(i) + '_' + '{0:0>3}'.format(k) + '.npy', 187 | np.expand_dims(patch, -1)) 188 | np.save(rootdir + 'seg/msk-' + '{0:0>3}'.format(i) + '_' + '{0:0>3}'.format(k) + '.npy', 189 | np.expand_dims(np.where(msk > 1, 1, 0), -1)) 190 | k += 1 # per full_img 191 | return patches, patches_masks 192 | 193 | 194 | def get_data_with_random_224(train_imgs_original, 195 | train_groudTruth, patch_z, 196 | zstep=25): 197 | rootdir = "data/patches/liver_" + str(224) + "/" 198 | if not os.path.isdir(rootdir): 199 | os.mkdir(rootdir) 200 | if not os.path.isdir(rootdir + "raw"): 201 | os.mkdir(rootdir + "raw") 202 | if not os.path.isdir(rootdir + "seg"): 203 | os.mkdir(rootdir + "seg") 204 | 205 | full_masks = train_groudTruth 206 | full_imgs = train_imgs_original 207 | pid = 0 208 | # Extract patch of 64*64*64 for liver(inside of voi) 209 | for i in range(len(full_imgs)): # loop over the full images 210 | data_consistency_check(full_imgs[i], full_masks[i]) 211 | if full_imgs[i].shape[-1] < patch_z: 212 | continue 213 | k = 0 214 | patch_per_img = int(full_imgs[i].shape[-1] / 20 * 2) * 4 215 | while k < patch_per_img: 216 | z_center = np.random.randint(int(patch_z / 2), full_imgs[i].shape[-1] - int(patch_z / 2)) 217 | patch = full_imgs[i][:, 218 | :, 219 | z_center - int(patch_z / 2):z_center + int(patch_z / 2)] 220 | msk = full_masks[i][:, 221 | :, 222 | z_center - int(patch_z / 2):z_center + int(patch_z / 2)] 223 | # print(patch.shape) 224 | assert (patch.shape == (224, 224, patch_z)) 225 | # patches.append(patch) 226 | # patches_masks.append(np.clip(msk, 0., 1.)) 227 | k += 1 # per full_img 228 | np.save(rootdir + 'raw/img-%03d' % i + '_' + str(pid) + '.npy', np.expand_dims(patch, -1)) 229 | msk = np.where(msk < 0.5, 0, 1) 230 | np.save(rootdir + 'seg/msk-%03d' % i + '_' + str(pid) + '.npy', np.expand_dims(msk, -1)) 231 | pid += 1 232 | deep_slices = np.arange(patch_z // 2, full_imgs[i].shape[-1] - patch_z // 2 + zstep, zstep) 233 | if deep_slices.size == 0 or full_imgs[i].shape[-1] < patch_z: 234 | continue 235 | for j in range(len(deep_slices)): 236 | deep = deep_slices[j] 237 | deep = deep if deep + patch_z // 2 <= full_imgs[i].shape[-1] else -(patch_z // 2 - full_imgs[i].shape[-1]) 238 | patch = full_imgs[i][:, :, 239 | deep - patch_z // 2:deep + patch_z // 2] 240 | msk = full_masks[i][:, :, 241 | deep - patch_z // 2:deep + patch_z // 2] 242 | k += 1 243 | np.save(rootdir + 'raw/img-%03d' % i + '_' + str(pid) + '.npy', np.expand_dims(patch, -1)) 244 | msk = np.where(msk < 0.5, 0, 1) 245 | np.save(rootdir + 'seg/msk-%03d' % i + '_' + str(pid) + '.npy', np.expand_dims(msk, -1)) 246 | pid += 1 247 | 248 | 249 | # extract patches randomly in the full validation images 250 | # -- Inside OR in full image 251 | def extract_random_patches(full_imgs, full_masks, patch_x, patch_y, patch_z, N_patches, patch_voi=False): 252 | patch_per_img = int(N_patches) 253 | print("patches per full image: " + str(patch_per_img)) 254 | patches = [] 255 | patches_masks = [] 256 | if not patch_voi: 257 | # Extract patch of 29*29*29 for liver tumor 258 | for i in range(len(full_imgs)): # loop over the full images 259 | data_consistency_check(full_imgs[i], full_masks[i]) 260 | k = 0 261 | idx = np.where(full_masks[i] >= 1.) 262 | while k < patch_per_img: 263 | index = np.random.randint(len(idx[0]), size=1)[0] 264 | x_center, y_center, z_center = idx[0][index], idx[1][index], idx[2][index] 265 | # x_center = np.random.randint(low = 0+int(patch_w/2),high = img_w-int(patch_w/2)) 266 | # # print "x_center " +str(x_center) 267 | # y_center = np.random.randint(low=0 + int(patch_h / 2), high=img_h - int(patch_h / 2)) 268 | # # print "y_center " +str(y_center) 269 | # z_center = np.random.randint(low=0 + int(patch_z / 2), high=img_z - int(patch_z / 2)) 270 | patch = full_imgs[i][x_center - int(patch_x / 2):x_center + int(patch_x / 2) + 1, 271 | y_center - int(patch_y / 2):y_center + int(patch_y / 2) + 1, 272 | z_center - int(patch_z / 2):z_center + int(patch_z / 2) + 1] 273 | # print(patch.shape) 274 | if patch.shape != (patch_x, patch_y, patch_z): 275 | continue 276 | assert (patch.shape == (patch_x, patch_y, patch_z)) 277 | patch_mask = full_masks[i][x_center, y_center, z_center] 278 | patches.append(patch) 279 | patches_masks.append(patch_mask - 1) 280 | k += 1 # per full_img 281 | patches = np.expand_dims(patches, -1) 282 | patches_masks = np.array(patches_masks) 283 | else: 284 | # Extract patch of 64*64*64 for liver(inside of voi) 285 | for i in range(len(full_imgs)): # loop over the full images 286 | data_consistency_check(full_imgs[i], full_masks[i]) 287 | if full_imgs[i].shape[-1] < patch_z: 288 | continue 289 | k = 0 290 | idx = np.where(full_masks[i] >= 1.) 291 | while k < patch_per_img: 292 | index = np.random.randint(len(idx[0]), size=1)[0] 293 | x_center, y_center, z_center = idx[0][index], idx[1][index], idx[2][index] 294 | patch = full_imgs[i][x_center - int(patch_x / 2):x_center + int(patch_x / 2), 295 | y_center - int(patch_y / 2):y_center + int(patch_y / 2), 296 | z_center - int(patch_z / 2):z_center + int(patch_z / 2)] 297 | msk = full_masks[i][x_center - int(patch_x / 2):x_center + int(patch_x / 2), 298 | y_center - int(patch_y / 2):y_center + int(patch_y / 2), 299 | z_center - int(patch_z / 2):z_center + int(patch_z / 2)] 300 | # print(patch.shape) 301 | if patch.shape != (patch_x, patch_y, patch_z): 302 | print(patch.shape) 303 | continue 304 | np.save('data/patches/tumor_32_much_more/raw/img-' + '{0:0>3}'.format(i) + '_' + str(k) + '.npy', 305 | np.expand_dims(patch, -1)) 306 | np.save('data/patches/tumor_32_much_more/seg/msk-' + '{0:0>3}'.format(i) + '_' + str(k) + '.npy', 307 | np.expand_dims(np.where(msk > 1, 1, 0), -1)) 308 | print('data/patches/tumor_32_much_more/raw/img-' + '{0:0>3}'.format(i) + '_' + str(k)) 309 | print('data/patches/tumor_32_much_more/seg/msk-' + '{0:0>3}'.format(i) + '_' + str(k)) 310 | 311 | k += 1 # per full_img 312 | # patches = np.expand_dims(patches, -1) 313 | # patches_masks = np.expand_dims(patches_masks, -1) 314 | return patches, patches_masks 315 | 316 | 317 | def data_consistency_check(imgs, masks): 318 | assert (len(imgs.shape) == len(masks.shape)) 319 | assert (imgs.shape[0] == masks.shape[0]) 320 | assert (imgs.shape[1] == masks.shape[1]) 321 | assert (imgs.shape[2] == masks.shape[2]) 322 | 323 | 324 | def data_consistency_check_patches(imgs, masks): 325 | assert (len(imgs.shape) == 5) 326 | assert (imgs.shape[0] == masks.shape[0]) 327 | # image.shape[1] >1 in using gabor wavelet, so cannot have fixed number of channels 328 | # assert(imgs.shape[1]==1 or imgs.shape[1]==3) 329 | -------------------------------------------------------------------------------- /utils/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from keras import backend as K 3 | import os 4 | import nibabel as nib 5 | import sys 6 | import scipy.misc 7 | from keras.preprocessing.image import Iterator 8 | from scipy.ndimage import rotate 9 | from medpy import metric 10 | from surface import Surface 11 | import random 12 | from glob import glob 13 | from skimage import measure 14 | 15 | IMG_DTYPE = np.float 16 | SEG_DTYPE = np.uint8 17 | 18 | import dicom 19 | import natsort 20 | import re 21 | 22 | 23 | # ==================================================== 24 | # ======================volume preprocessing method=== 25 | # ==================================================== 26 | def to_scale(img, slice_shape, shape=None): 27 | if shape is None: 28 | shape = slice_shape 29 | 30 | height, width = shape 31 | if img.dtype == SEG_DTYPE: 32 | return scipy.misc.imresize(img, (height, width), interp="nearest").astype(SEG_DTYPE) 33 | elif img.dtype == IMG_DTYPE: 34 | factor = 256.0 / np.max(img) 35 | return (scipy.misc.imresize(img, (height, width), interp="nearest") / factor).astype(IMG_DTYPE) 36 | else: 37 | raise TypeError( 38 | 'Error. To scale the image array, its type must be np.uint8 or np.float64. (' + str(img.dtype) + ')') 39 | 40 | 41 | def norm_hounsfield_dyn(arr, c_min=0.1, c_max=0.3): 42 | """ Converts from hounsfield units to float64 image with range 0.0 to 1.0 """ 43 | # calc min and max 44 | min, max = np.amin(arr), np.amax(arr) 45 | if min <= 0: 46 | arr = np.clip(arr, min * c_min, max * c_max) 47 | # right shift to zero 48 | arr = np.abs(min * c_min) + arr 49 | else: 50 | arr = np.clip(arr, min, max * c_max) 51 | # left shift to zero 52 | arr = arr - min 53 | # normalization 54 | norm_fac = np.amax(arr) 55 | if norm_fac != 0: 56 | norm = np.divide( 57 | np.multiply(arr, 255), 58 | np.amax(arr)) 59 | else: # don't divide through 0 60 | norm = np.multiply(arr, 255) 61 | 62 | norm = np.clip(np.multiply(norm, 0.00390625), 0, 1) 63 | return norm 64 | 65 | 66 | def histeq_processor(img): 67 | """Histogram equalization""" 68 | nbr_bins = 256 69 | # get image histogram 70 | imhist, bins = np.histogram(img.flatten(), nbr_bins, normed=True) 71 | cdf = imhist.cumsum() # cumulative distribution function 72 | cdf = 255 * cdf / cdf[-1] # normalize 73 | # use linear interpolation of cdf to find new pixel values 74 | original_shape = img.shape 75 | img = np.interp(img.flatten(), bins[:-1], cdf) 76 | img = img / 256.0 77 | return img.reshape(original_shape) 78 | 79 | 80 | def read_dicom_series(directory, filepattern="image_*"): 81 | """ Reads a DICOM Series files in the given directory. 82 | Only filesnames matching filepattern will be considered""" 83 | 84 | if not os.path.exists(directory) or not os.path.isdir(directory): 85 | raise ValueError("Given directory does not exist or is a file : " + str(directory)) 86 | print('\tRead Dicom', directory) 87 | lstFilesDCM = natsort.natsorted(glob(os.path.join(directory, filepattern))) 88 | print('\tLength dicom series', len(lstFilesDCM)) 89 | # Get ref file 90 | RefDs = dicom.read_file(lstFilesDCM[0]) 91 | # Load dimensions based on the number of rows, columns, and slices (along the Z axis) 92 | ConstPixelDims = (int(RefDs.Rows), int(RefDs.Columns), len(lstFilesDCM)) 93 | # The array is sized based on 'ConstPixelDims' 94 | ArrayDicom = np.zeros(ConstPixelDims, dtype=RefDs.pixel_array.dtype) 95 | 96 | # loop through all the DICOM files 97 | for filenameDCM in lstFilesDCM: 98 | # read the file 99 | ds = dicom.read_file(filenameDCM) 100 | # store the raw image data 101 | ArrayDicom[:, :, lstFilesDCM.index(filenameDCM)] = ds.pixel_array 102 | 103 | return ArrayDicom 104 | 105 | 106 | def rotate(matrix): 107 | """ 108 | :type matrix: List[List[int]] 109 | :rtype: void Do not return anything, modify matrix in-place instead. 110 | """ 111 | matrix[:] = map(list, zip(*matrix[::-1])) 112 | 113 | 114 | def read_liver_lesion_masks(masks_dirname): 115 | """Since 3DIRCAD provides an individual mask for each tissue type (in DICOM series format), 116 | we merge multiple tissue types into one Tumor mask, and merge this mask with the liver mask 117 | 118 | Args: 119 | masks_dirname : MASKS_DICOM directory containing multiple DICOM series directories, 120 | one for each labelled mask 121 | Returns: 122 | numpy array with 0's for background pixels, 1's for liver pixels and 2's for tumor pixels 123 | """ 124 | tumor_volume = None 125 | liver_volume = None 126 | 127 | # For each relevant organ in the current volume 128 | for organ in os.listdir(masks_dirname): 129 | organ_path = os.path.join(masks_dirname, organ) 130 | if not os.path.isdir(organ_path): 131 | continue 132 | 133 | organ = organ.lower() 134 | 135 | if organ.startswith("livertumor") or re.match("liver.yst.*", organ) or organ.startswith( 136 | "stone") or organ.startswith("metastasecto"): 137 | print('Organ', masks_dirname, organ) 138 | current_tumor = read_dicom_series(organ_path) 139 | current_tumor = np.clip(current_tumor, 0, 1) 140 | # Merge different tumor masks into a single mask volume 141 | tumor_volume = current_tumor if tumor_volume is None else np.logical_or(tumor_volume, current_tumor) 142 | elif organ == 'liver': 143 | print('Organ', masks_dirname, organ) 144 | liver_volume = read_dicom_series(organ_path) 145 | liver_volume = np.clip(liver_volume, 0, 1) 146 | 147 | # Merge liver and tumor into 1 volume with background=0, liver=1, tumor=2 148 | label_volume = np.zeros(liver_volume.shape) 149 | label_volume[liver_volume == 1] = 1 150 | label_volume[tumor_volume == 1] = 2 151 | label_final = np.zeros(label_volume.shape) 152 | for j in range(label_volume.shape[-1]): 153 | im = label_volume[:, :, j] 154 | rotate(im) 155 | label_final[:, :, j] = im 156 | return label_final 157 | 158 | 159 | # Get majority label in image 160 | def largest_label_volume(img, bg=0): 161 | vals, counts = np.unique(img, return_counts=True) 162 | counts = counts[vals != bg] 163 | vals = vals[vals != bg] 164 | if len(counts) > 0: 165 | return vals[np.argmax(counts)] 166 | else: 167 | return None 168 | 169 | 170 | def label_connected_component(pred): 171 | seg = measure.label(pred, neighbors=8, background=0) 172 | return seg 173 | 174 | 175 | def scorer(pred, label, vxlspacing): 176 | volscores = {} 177 | 178 | volscores['dice'] = metric.dc(pred, label) 179 | volscores['jaccard'] = metric.binary.jc(pred, label) 180 | volscores['voe'] = 1. - volscores['jaccard'] 181 | volscores['rvd'] = metric.ravd(label, pred) 182 | 183 | if np.count_nonzero(pred) == 0 or np.count_nonzero(label) == 0: 184 | volscores['assd'] = 0 185 | volscores['msd'] = 0 186 | else: 187 | evalsurf = Surface(pred, label, physical_voxel_spacing=vxlspacing, mask_offset=[0., 0., 0.], 188 | reference_offset=[0., 0., 0.]) 189 | volscores['assd'] = evalsurf.get_average_symmetric_surface_distance() 190 | 191 | volscores['msd'] = metric.hd(label, pred, voxelspacing=vxlspacing) 192 | 193 | return volscores 194 | 195 | 196 | def dice_coef(y_true, y_pred, smooth=1.): 197 | y_true_f = K.flatten(y_true) 198 | y_pred_f = K.flatten(y_pred) 199 | intersection = K.sum(y_true_f * y_pred_f) 200 | return (2. * intersection + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth) 201 | 202 | 203 | def dice_coef_loss(y_true, y_pred): 204 | return 1 - dice_coef(y_true, y_pred) 205 | 206 | 207 | def normalize_with_mean(img): 208 | imgs_normalized = (img - np.mean(img)) / np.std(img) 209 | # imgs_normalized =img 210 | imgs_normalized = ( 211 | (imgs_normalized - np.min(imgs_normalized)) / (np.max(imgs_normalized) - np.min(imgs_normalized))) 212 | return imgs_normalized 213 | 214 | 215 | def define_log(basic_path, exp): 216 | LOG_FILE = basic_path + '/' + exp 217 | if not os.path.exists(LOG_FILE): 218 | print("DIRECTORY Created") 219 | os.makedirs(LOG_FILE) 220 | f = open(LOG_FILE + '/' + exp + '_sys_out.log', 'a') 221 | sys.stdout = Tee(sys.stdout, f) 222 | 223 | # handler = logging.handlers.RotatingFileHandler(LOG_FILE + '/' + exp + '.log', maxBytes=50 * 1024 * 1024, 224 | # backupCount=5) 225 | # fmt = '%(asctime)s - %(filename)s:%(lineno)s - %(message)s' 226 | # formatter = logging.Formatter(fmt) 227 | # handler.setFormatter(formatter) 228 | # logger = logging.getLogger(LOG_FILE) 229 | # logger.addHandler(handler) 230 | # logger.setLevel(logging.DEBUG) 231 | # return logger 232 | 233 | 234 | class Tee(object): 235 | def __init__(self, *files): 236 | self.files = files 237 | 238 | def write(self, obj): 239 | for f in self.files: 240 | f.write(obj) 241 | 242 | def flush(self): 243 | pass 244 | 245 | 246 | def get_layer_outputs(test_image, model): 247 | outputs = [layer.output for layer in model.layers] # all layer outputs 248 | comp_graph = [K.function([model.input] + [K.learning_phase()], [output]) for output in 249 | outputs] # evaluation functions 250 | 251 | # Testing 252 | comp_graph = comp_graph[1:len(comp_graph)] 253 | layer_outputs_list = [op([test_image, 1.]) for op in comp_graph] 254 | layer_outputs = [] 255 | 256 | for layer_output in layer_outputs_list: 257 | # print(layer_output[0][0].shape, end='\n-------------------\n') 258 | layer_outputs.append(layer_output[0][0]) 259 | 260 | return layer_outputs 261 | 262 | 263 | def get_i_layer_outputs(test_image, model, i): 264 | outputs = [layer.output for layer in model.layers] # all layer outputs 265 | comp_graph = [K.function([model.input] + [K.learning_phase()], [output]) for output in 266 | outputs] # evaluation functions 267 | # Testing 268 | comp_graph = comp_graph[1:len(comp_graph)] 269 | layer_output = comp_graph[i - 1]([test_image, 1.])[0][0] 270 | return layer_output 271 | 272 | 273 | def plot_layer_outputs(test_image, layer_number, model, c=3): 274 | layer_outputs = get_layer_outputs(test_image, model) 275 | 276 | x_max = layer_outputs[layer_number].shape[0] 277 | y_max = layer_outputs[layer_number].shape[1] 278 | n = layer_outputs[layer_number].shape[2] 279 | 280 | L = [] 281 | for i in range(n): 282 | L.append(np.zeros((x_max, y_max))) 283 | 284 | for i in range(n): 285 | for x in range(x_max): 286 | for y in range(y_max): 287 | L[i][x][y] = layer_outputs[layer_number][x][y][i] 288 | 289 | # for img in L: 290 | # plt.figure() 291 | # plt.imshow(img, interpolation='nearest') 292 | return L 293 | 294 | 295 | class TrainBatchFetcher(Iterator): 296 | """ 297 | fetch batch of original images and liver images 298 | """ 299 | 300 | def __init__(self, data_dir, valid_fraction, valid_from=0): 301 | # self.train_imgs = train_imgs 302 | # self.train_livers = train_livers 303 | # self.n_train_imgs = self.train_imgs.shape[0] 304 | images = data_dir + '/raw/img-*.npy' 305 | labels = data_dir + '/seg/msk-*.npy' 306 | image_paths = [] 307 | for name in sorted(glob(images), reverse=True): 308 | image_paths.append(name) 309 | label_paths = { 310 | os.path.basename(path).replace('msk-', 'img-'): path 311 | for path in sorted(glob(labels), reverse=True)} 312 | self.label_paths = label_paths 313 | 314 | num_images = len(image_paths) 315 | # random.shuffle(image_paths) 316 | if num_images == 0: 317 | raise RuntimeError('No data files found in ' + data_dir) 318 | 319 | self.valid_images = image_paths[int(valid_from * num_images):int(valid_fraction * num_images)] 320 | # print(self.valid_images[0]) 321 | # print(self.valid_images[len(self.valid_images)-1]) 322 | # self.train_images = image_paths[int(valid_fraction * num_images):] 323 | self.train_images = list(set(image_paths).difference(set(self.valid_images))) 324 | random.shuffle(self.valid_images) 325 | random.shuffle(self.train_images) 326 | self.num_training = len(self.train_images) 327 | self.num_validation = len(self.valid_images) 328 | print('number of training :' + str(self.num_training)) 329 | print('number of validation :' + str(self.num_validation)) 330 | 331 | def next(self, batch_size, type='train'): 332 | # indices = list(np.random.choice(self.n_train_imgs, batch_size)) 333 | # return self.train_imgs[indices, :, :, :], self.train_livers[indices, :, :, :] 334 | image_paths = self.train_images 335 | num_training = self.num_training 336 | if type != 'train': 337 | image_paths = self.valid_images 338 | num_training = self.num_validation 339 | print('vali training' + str(num_training)) 340 | 341 | while (1): 342 | random.shuffle(image_paths) 343 | indices = list(np.random.choice(num_training, batch_size)) 344 | images = [] 345 | labels = [] 346 | for index in indices: 347 | # label_file = np.load(self.label_paths[os.path.basename(self.train_images[index])]) 348 | # image_file = np.load(self.train_images[index]) 349 | label_file = np.load(self.label_paths[os.path.basename(image_paths[index])]) 350 | image_file = np.load(image_paths[index]) 351 | images.append(image_file) 352 | labels.append(label_file) 353 | # im = array_to_img(image_file) 354 | # im.save('Demo/c_slices_prediction/raw_slice_vol_img/vol_img_' + str(index) + '.jpg', 'jpeg') 355 | # im = array_to_img(label_file) 356 | # im.save('Demo/c_slices_prediction/msk_slice_vol_img/vol_img_' + str(index) + '.jpg', 357 | # 'jpeg') 358 | images = np.array(images) 359 | labels = np.array(labels) 360 | # labels = np.concatenate((1 - labels, labels), -1) 361 | yield images, labels 362 | 363 | def nextVali(self, batch_size): 364 | image_paths = self.valid_images 365 | while (1): 366 | random.shuffle(image_paths) 367 | indices = list(np.random.choice(self.num_validation, batch_size)) 368 | images = [] 369 | labels = [] 370 | for index in indices: 371 | label_file = np.load(self.label_paths[os.path.basename(self.valid_images[index])]) 372 | image_file = np.load(self.valid_images[index]) 373 | images.append(image_file) 374 | labels.append(label_file) 375 | images = np.array(images) 376 | labels = np.array(labels) 377 | yield images, labels 378 | 379 | def vali_data(self): 380 | image_paths = self.valid_images 381 | random.shuffle(image_paths) 382 | images = [] 383 | labels = [] 384 | for image_file in image_paths: 385 | label_file = np.load(self.label_paths[os.path.basename(image_file)]) 386 | image_file = np.load(image_file) # image_file 387 | images.append(image_file) 388 | labels.append(label_file) 389 | images = np.array(images) 390 | labels = np.array(labels) 391 | # labels = np.concatenate((1 - labels, labels), -1) 392 | return images, labels 393 | 394 | def get_one(self): 395 | image_paths = self.valid_images 396 | random.shuffle(image_paths) 397 | images = [] 398 | labels = [] 399 | label_file = np.load(self.label_paths[os.path.basename(image_paths[0])]) 400 | image_file = np.load(image_paths[0]) # image_file 401 | images.append(image_file) 402 | labels.append(label_file) 403 | images = np.array(images) 404 | labels = np.array(labels) 405 | return images, labels 406 | 407 | 408 | # brain utils 409 | def generate_patch_locations(patches, patch_size, im_size): 410 | nx = round((patches * 8 * im_size[0] * im_size[0] / im_size[1] / im_size[2]) ** (1.0 / 3)) 411 | ny = round(nx * im_size[1] / im_size[0]) 412 | nz = round(nx * im_size[2] / im_size[0]) 413 | x = np.rint(np.linspace(patch_size, im_size[0] - patch_size, num=nx)) 414 | y = np.rint(np.linspace(patch_size, im_size[1] - patch_size, num=ny)) 415 | z = np.rint(np.linspace(patch_size, im_size[2] - patch_size, num=nz)) 416 | return x, y, z 417 | 418 | 419 | def perturb_patch_locations(patch_locations, radius): 420 | x, y, z = patch_locations 421 | x = np.rint(x + np.random.uniform(-radius, radius, len(x))) 422 | y = np.rint(y + np.random.uniform(-radius, radius, len(y))) 423 | z = np.rint(z + np.random.uniform(-radius, radius, len(z))) 424 | return x, y, z 425 | 426 | 427 | def generate_patch_probs(path, patch_locations, patch_size, im_size, tagTumor=0): 428 | x, y, z = patch_locations 429 | seg = nib.load(glob(os.path.join(path, '*_seg.nii.gz'))[0]).get_data().astype(np.float32) 430 | p = [] 431 | for i in range(len(x)): 432 | for j in range(len(y)): 433 | for k in range(len(z)): 434 | patch = seg[int(x[i] - patch_size / 2): int(x[i] + patch_size / 2), 435 | int(y[j] - patch_size / 2): int(y[j] + patch_size / 2), 436 | int(z[k] - patch_size / 2): int(z[k] + patch_size / 2)] 437 | patch = (patch > tagTumor).astype(np.float32) 438 | percent = np.sum(patch) / (patch_size * patch_size * patch_size) 439 | p.append((1 - np.abs(percent - 0.5)) * percent) 440 | p = np.asarray(p, dtype=np.float32) 441 | p[p == 0] = np.amin(p[np.nonzero(p)]) 442 | p = p / np.sum(p) 443 | return p 444 | 445 | 446 | def normalize(im_input): 447 | superior = 10 448 | inferior = 10 449 | sp = im_input.shape 450 | tp = np.transpose(np.nonzero(im_input)) 451 | minx, miny, minz = np.min(tp, axis=0) 452 | maxx, maxy, maxz = np.max(tp, axis=0) 453 | minz = 0 if minz - superior < 0 else minz - superior 454 | maxz = sp[-1] if maxz + inferior > sp[-1] else maxz + inferior + 1 455 | miny = 0 if miny - superior < 0 else miny - superior 456 | maxy = sp[1] if maxy + inferior > sp[1] else maxy + inferior + 1 457 | minx = 0 if minx - superior < 0 else minx - superior 458 | maxx = sp[0] if maxx + inferior > sp[0] else maxx + inferior + 1 459 | 460 | roi = im_input[minx: maxx, miny: maxy, minz: maxz] 461 | # im_output = (im_input - np.mean(roi)) / np.std(roi) 462 | im_output = (im_input - np.min(roi)) / (np.max(roi) - np.min(roi)) 463 | return im_output 464 | 465 | 466 | def read_image(path, is_training=True): 467 | t1 = nib.load(glob(os.path.join(path, '*_t1.nii.gz'))[0]).get_data().astype(np.float32) 468 | t1ce = nib.load(glob(os.path.join(path, '*_t1ce.nii.gz'))[0]).get_data().astype(np.float32) 469 | t2 = nib.load(glob(os.path.join(path, '*_t2.nii.gz'))[0]).get_data().astype(np.float32) 470 | flair = nib.load(glob(os.path.join(path, '*_flair.nii.gz'))[0]).get_data().astype(np.float32) 471 | assert t1.shape == t1ce.shape == t2.shape == flair.shape 472 | if is_training: 473 | seg = nib.load(glob(os.path.join(path, '*_seg.nii.gz'))[0]).get_data().astype(np.float32) 474 | assert t1.shape == seg.shape 475 | nchannel = 5 476 | else: 477 | nchannel = 4 478 | 479 | image = np.empty((t1.shape[0], t1.shape[1], t1.shape[2], nchannel), dtype=np.float32) 480 | 481 | # image[..., 0] = remove_low_high(t1) 482 | # image[..., 1] = remove_low_high(t1ce) 483 | # image[..., 2] = remove_low_high(t2) 484 | # image[..., 3] = remove_low_high(flair) 485 | image[..., 0] = normalize(t1) 486 | image[..., 1] = normalize(t1ce) 487 | image[..., 2] = normalize(t2) 488 | image[..., 3] = normalize(flair) 489 | 490 | if is_training: 491 | image[..., 4] = seg 492 | 493 | return image 494 | 495 | 496 | def generate_test_locations(patch_size, stride, im_size): 497 | stride_size_x = patch_size[0] / stride 498 | stride_size_y = patch_size[1] / stride 499 | stride_size_z = patch_size[2] / stride 500 | pad_x = ( 501 | int(patch_size[0] / 2), 502 | int(np.ceil(im_size[0] / stride_size_x) * stride_size_x - im_size[0] + patch_size[0] / 2)) 503 | pad_y = ( 504 | int(patch_size[1] / 2), 505 | int(np.ceil(im_size[1] / stride_size_y) * stride_size_y - im_size[1] + patch_size[1] / 2)) 506 | pad_z = ( 507 | int(patch_size[2] / 2), 508 | int(np.ceil(im_size[2] / stride_size_z) * stride_size_z - im_size[2] + patch_size[2] / 2)) 509 | x = np.arange(patch_size[0] / 2, im_size[0] + pad_x[0] + pad_x[1] - patch_size[0] / 2 + 1, stride_size_x) 510 | y = np.arange(patch_size[1] / 2, im_size[1] + pad_y[0] + pad_y[1] - patch_size[1] / 2 + 1, stride_size_y) 511 | z = np.arange(patch_size[2] / 2, im_size[2] + pad_z[0] + pad_z[1] - patch_size[2] / 2 + 1, stride_size_z) 512 | return (x, y, z), (pad_x, pad_y, pad_z) 513 | --------------------------------------------------------------------------------