Chris Xiao commited on
Commit
c642393
·
1 Parent(s): 1b45c44

upload files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. LICENSE +201 -0
  2. README.md +252 -3
  3. assets/method.png +0 -0
  4. metrics/distanceVertex2Mesh.py +64 -0
  5. metrics/get_probability_map.py +194 -0
  6. metrics/lookup_tables.py +463 -0
  7. metrics/metrics.py +355 -0
  8. metrics/surface_distance.py +424 -0
  9. nnunet/__init__.py +7 -0
  10. nnunet/configuration.py +5 -0
  11. nnunet/dataset_conversion/Task017_BeyondCranialVaultAbdominalOrganSegmentation.py +94 -0
  12. nnunet/dataset_conversion/Task024_Promise2012.py +81 -0
  13. nnunet/dataset_conversion/Task027_AutomaticCardiacDetectionChallenge.py +106 -0
  14. nnunet/dataset_conversion/Task029_LiverTumorSegmentationChallenge.py +123 -0
  15. nnunet/dataset_conversion/Task032_BraTS_2018.py +176 -0
  16. nnunet/dataset_conversion/Task035_ISBI_MSLesionSegmentationChallenge.py +162 -0
  17. nnunet/dataset_conversion/Task037_038_Chaos_Challenge.py +460 -0
  18. nnunet/dataset_conversion/Task040_KiTS.py +240 -0
  19. nnunet/dataset_conversion/Task043_BraTS_2019.py +164 -0
  20. nnunet/dataset_conversion/Task055_SegTHOR.py +98 -0
  21. nnunet/dataset_conversion/Task056_VerSe2019.py +274 -0
  22. nnunet/dataset_conversion/Task056_Verse_normalize_orientation.py +98 -0
  23. nnunet/dataset_conversion/Task058_ISBI_EM_SEG.py +105 -0
  24. nnunet/dataset_conversion/Task059_EPFL_EM_MITO_SEG.py +99 -0
  25. nnunet/dataset_conversion/Task061_CREMI.py +146 -0
  26. nnunet/dataset_conversion/Task062_NIHPancreas.py +89 -0
  27. nnunet/dataset_conversion/Task064_KiTS_labelsFixed.py +84 -0
  28. nnunet/dataset_conversion/Task065_KiTS_NicksLabels.py +87 -0
  29. nnunet/dataset_conversion/Task069_CovidSeg.py +68 -0
  30. nnunet/dataset_conversion/Task075_Fluo_C3DH_A549_ManAndSim.py +137 -0
  31. nnunet/dataset_conversion/Task076_Fluo_N3DH_SIM.py +312 -0
  32. nnunet/dataset_conversion/Task082_BraTS_2020.py +751 -0
  33. nnunet/dataset_conversion/Task083_VerSe2020.py +138 -0
  34. nnunet/dataset_conversion/Task089_Fluo-N2DH-SIM.py +290 -0
  35. nnunet/dataset_conversion/Task114_heart_MNMs.py +262 -0
  36. nnunet/dataset_conversion/Task115_COVIDSegChallenge.py +344 -0
  37. nnunet/dataset_conversion/Task120_Massachusetts_RoadSegm.py +103 -0
  38. nnunet/dataset_conversion/Task135_KiTS2021.py +49 -0
  39. nnunet/dataset_conversion/Task154_RibFrac_multi_label.py +172 -0
  40. nnunet/dataset_conversion/Task155_RibFrac_binary.py +174 -0
  41. nnunet/dataset_conversion/Task156_RibSeg.py +140 -0
  42. nnunet/dataset_conversion/Task159_MyoPS2020.py +106 -0
  43. nnunet/dataset_conversion/__init__.py +3 -0
  44. nnunet/dataset_conversion/utils.py +76 -0
  45. nnunet/evaluation/__init__.py +2 -0
  46. nnunet/evaluation/add_dummy_task_with_mean_over_all_tasks.py +77 -0
  47. nnunet/evaluation/add_mean_dice_to_json.py +51 -0
  48. nnunet/evaluation/collect_results_files.py +48 -0
  49. nnunet/evaluation/evaluator.py +483 -0
  50. nnunet/evaluation/metrics.py +406 -0
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md CHANGED
@@ -1,3 +1,252 @@
1
- ---
2
- license: mit
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <h2 align="center"> [OTO–HNS2024] A Deep Learning Framework for Analysis of the Eustachian Tube and the Internal Carotid Artery </h2>
2
+ <p align="center">
3
+ <a href="https://aao-hnsfjournals.onlinelibrary.wiley.com/doi/10.1002/ohn.789"><img src="https://img.shields.io/badge/Wiley-Paper-red"></a>
4
+ <a href="https://pubmed.ncbi.nlm.nih.gov/38686594/"><img src="https://img.shields.io/badge/PubMed-Link-blue"></a>
5
+ <a href="https://github.com/mikami520/AutoSeg4ETICA"><img src="https://img.shields.io/badge/Code-Page-magenta"></a>
6
+ </p>
7
+ <h5 align="center"><em>Ameen Amanian, Aseem Jain, Yuliang Xiao, Chanha Kim, Andy S. Ding, Manish Sahu, Russell Taylor, Mathias Unberath, Bryan K. Ward, Deepa Galaiya, Masaru Ishii, Francis X. Creighton</em></h5>
8
+ <p align="center">
9
+ <a href="#news">News</a> |
10
+ <a href="#abstract">Abstract</a> |
11
+ <a href="#installation">Installation</a> |
12
+ <a href="#train">Train</a> |
13
+ <a href="#inference">Inference</a> |
14
+ <a href="#evaluation">Evaluation</a>
15
+ </p>
16
+
17
+ ## News
18
+
19
+ **2024.04.30** - The data preprocessing , training, inference, and evaluation code are released.
20
+
21
+ **2024.04.05** - Our paper is accepted to **American Academy of Otolaryngology–Head and Neck Surgery 2024 (OTO-HNS2024)**.
22
+
23
+ ## Abstract
24
+ - Objective: Obtaining automated, objective 3-dimensional (3D)
25
+ models of the Eustachian tube (ET) and the internal carotid
26
+ artery (ICA) from computed tomography (CT) scans could
27
+ provide useful navigational and diagnostic information for ET
28
+ pathologies and interventions. We aim to develop a deep
29
+ learning (DL) pipeline to automatically segment the ET and
30
+ ICA and use these segmentations to compute distances
31
+ between these structures.
32
+
33
+ - Methods: From a database of 30 CT scans, 60 ET and ICA pairs
34
+ were manually segmented and used to train an nnU-Net model,
35
+ a DL segmentation framework. These segmentations were also
36
+ used to develop a quantitative tool to capture the magnitude
37
+ and location of the minimum distance point (MDP) between ET
38
+ and ICA. Performance metrics for the nnU-Net automated
39
+ segmentations were calculated via the average Hausdorff
40
+ distance (AHD) and dice similarity coefficient (DSC).
41
+
42
+ - Results: The AHD for the ETand ICA were 0.922 and 0.246 mm,
43
+ respectively. Similarly, the DSC values for the ET and ICA were
44
+ 0.578 and 0.884. The mean MDP from ET to ICA in the
45
+ cartilaginous region was 2.6 mm (0.7-5.3 mm) and was located
46
+ on average 1.9 mm caudal from the bony cartilaginous junction.
47
+
48
+ - Conclusion: This study describes the first end-to-end DL
49
+ pipeline for automated ET and ICA segmentation and analyzes
50
+ distances between these structures. In addition to helping to
51
+ ensure the safe selection of patients for ET dilation, this
52
+ method can facilitate large-scale studies exploring the
53
+ relationship between ET pathologies and the 3D shape of
54
+ the ET.
55
+
56
+ <p align="center">
57
+ <img src="assets/method.png" />
58
+ <b>Figure 1: Overview of Workflow
59
+ </b>
60
+ </p>
61
+
62
+ ## Installation
63
+
64
+ ### Step 1: Fork This GitHub Repository
65
+
66
+ ```bash
67
+ git clone https://github.com/mikami520/AutoSeg4ETICA.git && cd AutoSeg4ETICA
68
+ ```
69
+
70
+ ### Step 2: Set Up Two Environments Using requirements.txt Files (virtual environment is recommended)
71
+
72
+ ```bash
73
+ pip install -r requirements.txt
74
+ source /path/to/VIRTUAL_ENVIRONMENT/bin/activate
75
+ ```
76
+
77
+ ## Preprocessing
78
+
79
+ ### Step 1: Register Data to Template
80
+
81
+ ```bash
82
+ cd <path to repo>/preprocessing
83
+ ```
84
+
85
+ Register data to template (can be used for multiple segmentations propagation)
86
+
87
+ ```bash
88
+ python registration.py -bp <full path of base dir> -ip <relative path to nifti images dir> -sp <relative path to segmentations dir>
89
+ ```
90
+
91
+ If you want to make sure correspondence of the name and value of segmentations, you can add the following commands after above command
92
+
93
+ ```bash
94
+ -sl LabelValue1 LabelName1 LabelValue2 LabelName2 LabelValue3 LabelName3 ...
95
+ ```
96
+
97
+ For example, if I have two labels for maxillary sinus named L-MS and R-MS
98
+
99
+ ```bash
100
+ python registration.py -bp /Users/mikamixiao/Desktop -ip images -sp labels -sl 1 L-MS 2 R-MS
101
+ ```
102
+
103
+ Final output of registered images and segmentations will be saved in
104
+
105
+ ```text
106
+ imagesRS/ && labelsRS/
107
+ ```
108
+
109
+ ### Step 2: Create Datasplit for Training/Testing. Validation will be chosen automatically by nnUNet (filename format should be taskname_xxx.nii.gz)
110
+
111
+ ```bash
112
+ python split_data.py -bp <full path of base dir> -ip <relative path to nifti images dir (imagesRS)> -sp <relative path to nifti segmentations dir (labelsRS)> -sl <a list of label name and corresponding label value> -ti <task id for nnUNet preprocessing> -tn <name of task>
113
+ ```
114
+
115
+ For example
116
+
117
+ ```bash
118
+ python split_data.py -bp /Users/mikamixiao/Desktop -ip imagesRS -sp labelsRS -sl 1 L-MS 2 R-MS -ti 001 -tn Sinus
119
+ ```
120
+
121
+ ### Step 3: Setup Bashrc
122
+
123
+ Edit your `~/.bashrc` file with `gedit ~/.bashrc` or `nano ~/.bashrc`. At the end of the file, add the following lines:
124
+
125
+ ```bash
126
+ export nnUNet_raw_data_base="<ABSOLUTE PATH TO BASE_DIR>/nnUnet/nnUNet_raw_data_base"
127
+ export nnUNet_preprocessed="<ABSOLUTE PATH TO BASE_DIR>/nnUNet_preprocessed"
128
+ export RESULTS_FOLDER="<ABSOLUTE PATH TO BASE_DIR>/nnUnet/nnUNet_trained_models"
129
+ ```
130
+
131
+ After updating this you will need to source your `~/.bashrc` file.
132
+
133
+ ```bash
134
+ source ~/.bashrc
135
+ ```
136
+
137
+ This will deactivate your current conda environment.
138
+
139
+ ### Step 4: Verify and Preprocess Data
140
+
141
+ Activate nnUNet environment
142
+
143
+ ```bash
144
+ source /path/to/VIRTUAL_ENVIRONMENT/bin/activate
145
+ ```
146
+
147
+ Run nnUNet preprocessing script.
148
+
149
+ ```bash
150
+ nnUNet_plan_and_preprocess -t <task_id> --verify_dataset_integrity
151
+ ```
152
+
153
+ Potential Error: You may need to edit the dataset.json file so that the labels are sequential. If you have at least 10 labels, then labels `10, 11, 12,...` will be arranged before labels `2, 3, 4, ...`. Doing this in a text editor is completely fine!
154
+
155
+ ## Train
156
+
157
+ To train the model:
158
+
159
+ ```bash
160
+ nnUNet_train 3d_fullres nnUNetTrainerV2 Task<task_num>_TemporalBone Y --npz
161
+ ```
162
+
163
+ `Y` refers to the number of folds for cross-validation. If `Y` is set to `all` then all of the data will be used for training. If you want to try 5-folds cross validation, you should define Y as `0, 1, 2, 3, 4 ` for five times.
164
+
165
+ `--npz` makes the models save the softmax outputs (uncompressed, large files) during the final validation. It should only be used if you are training multiple configurations, which requires `nnUNet_find_best_configuration` to find the best model. We omit this by default.
166
+
167
+ ## Inference
168
+
169
+ To run inference on trained checkpoints and obtain evaluation results:
170
+ `nnUNet_find_best_configuration` will print a string to the terminal with the inference commands you need to use.
171
+ The easiest way to run inference is to simply use these commands.
172
+
173
+ If you wish to manually specify the configuration(s) used for inference, use the following commands:
174
+
175
+ For each of the desired configurations, run:
176
+
177
+ ```bash
178
+ nnUNet_predict -i INPUT_FOLDER -o OUTPUT_FOLDER -t TASK_NAME_OR_ID -m CONFIGURATION --save_npz
179
+ ```
180
+
181
+ Only specify `--save_npz` if you intend to use ensembling. `--save_npz` will make the command save the softmax
182
+ probabilities alongside of the predicted segmentation masks requiring a lot of disk space.
183
+
184
+ Please select a separate `OUTPUT_FOLDER` for each configuration!
185
+
186
+ If you wish to run ensembling, you can ensemble the predictions from several configurations with the following command:
187
+
188
+ ```bash
189
+ nnUNet_ensemble -f FOLDER1 FOLDER2 ... -o OUTPUT_FOLDER -pp POSTPROCESSING_FILE
190
+ ```
191
+
192
+ You can specify an arbitrary number of folders, but remember that each folder needs to contain npz files that were
193
+ generated by `nnUNet_predict`. For ensembling you can also specify a file that tells the command how to postprocess.
194
+ These files are created when running `nnUNet_find_best_configuration` and are located in the respective trained model directory `(RESULTS_FOLDER/nnUNet/CONFIGURATION/TaskXXX_MYTASK/TRAINER_CLASS_NAME__PLANS_FILE_IDENTIFIER/postprocessing.json or RESULTS_FOLDER/nnUNet/ensembles/TaskXXX_MYTASK/ensemble_X__Y__Z--X__Y__Z/postprocessing.json)`. You can also choose to not provide a file (simply omit -pp) and nnU-Net will not run postprocessing.
195
+
196
+ Note that per default, inference will be done with all available folds. We very strongly recommend you use all 5 folds.
197
+ Thus, all 5 folds must have been trained prior to running inference. The list of available folds nnU-Net found will be
198
+ printed at the start of the inference.
199
+
200
+ ## Evaluation
201
+
202
+ To compute the dice score, average hausdorff distance and weighted hausdorff distance:
203
+
204
+ ```bash
205
+ cd <path to repo>/metrics
206
+ ```
207
+
208
+ Run the metrics.py to output a CSV file that contain the dice score and hausdorff distance for each segmentation:
209
+
210
+ ```bash
211
+ python metrics.py -bp <full path of base dir> -gp <relative path of ground truth dir> -pp <relative path of predicted segmentations dir> -sp <save dir> -vt <Validation type: 'dsc', 'ahd', 'whd'>
212
+ ```
213
+
214
+ Users can choose any combinations of evaluation types among these three choices.
215
+
216
+ ```text
217
+ dsc: Dice Score
218
+ ahd: Average Hausdorff Distance
219
+ whd: Weighted Hausdorff Distance
220
+ ```
221
+
222
+ If choosing ```whd``` and you do not have a probability map, you can use ```get_probability_map.py```to obtain one. Here is the way to use:
223
+
224
+ ```bash
225
+ python get_probability_map.py -bp <full path of base dir> -pp <relative path of predicted segmentations dir> -rr <ratio to split skeleton> -ps <probability sequences>
226
+ ```
227
+
228
+ Currently, we split the skeleton alongside the x axis and from ear end to nasal. Please make sure the probability sequences are matched to the splitted regions. The output probability map which is a text file will be stored in ```output/```under the ```base directory```. Once obtaining the probability map, you can import your customized probability map by adding following command when using ```metrics.py```:
229
+
230
+ ```bash
231
+ -pm <relative path of probability map>
232
+ ```
233
+
234
+ #### To draw the heat map to see the failing part of prediction:
235
+
236
+ ```bash
237
+ python distanceVertex2Mesh.py -bp <full path of base dir> -gp <relative path of ground truth dir> -pp <relative path of predicted segmentations dir>
238
+ ```
239
+
240
+ Once you get the closest distance (save in ```output/``` under ```base directory```) from prediction to ground truth, you can easily draw the heat map and use the color bar to show the change of differences (```ParaView``` is recommended)
241
+
242
+ ## Citing Paper
243
+
244
+ If you find this paper helpful, please consider citing:
245
+ ```bibtex
246
+ @article{amanian2024deep,
247
+ title={A Deep Learning Framework for Analysis of the Eustachian Tube and the Internal Carotid Artery},
248
+ author={Amanian, Ameen and Jain, Aseem and Xiao, Yuliang and Kim, Chanha and Ding, Andy S and Sahu, Manish and Taylor, Russell and Unberath, Mathias and Ward, Bryan K and Galaiya, Deepa and others},
249
+ journal={Otolaryngology--Head and Neck Surgery},
250
+ publisher={Wiley Online Library}
251
+ }
252
+ ```
assets/method.png ADDED
metrics/distanceVertex2Mesh.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pyvista as pv
3
+ import argparse
4
+ import os
5
+ import glob
6
+ import trimesh
7
+
8
+
9
+ def parse_command_line():
10
+ print('---'*10)
11
+ print('Parsing Command Line Arguments')
12
+ parser = argparse.ArgumentParser(description='Defacing protocol')
13
+ parser.add_argument('-bp', metavar='base path', type=str,
14
+ help="Absolute path of the base directory")
15
+ parser.add_argument('-gp', metavar='ground truth path', type=str,
16
+ help="Relative path of the ground truth model")
17
+ parser.add_argument('-pp', metavar='prediction path', type=str,
18
+ help="Relative path of the prediction model")
19
+ argv = parser.parse_args()
20
+ return argv
21
+
22
+
23
+ def distanceVertex2Mesh(mesh, vertex):
24
+ faces_as_array = mesh.faces.reshape((mesh.n_faces, 4))[:, 1:]
25
+ mesh_box = trimesh.Trimesh(vertices=mesh.points,
26
+ faces=faces_as_array)
27
+ cp, cd, ci = trimesh.proximity.closest_point(mesh_box, vertex)
28
+ return cd
29
+
30
+
31
+ def main():
32
+ args = parse_command_line()
33
+ base = args.bp
34
+ gt_path = args.gp
35
+ pred_path = args.pp
36
+ output_dir = os.path.join(base, 'output')
37
+ try:
38
+ os.mkdir(output_dir)
39
+ except:
40
+ print(f'{output_dir} already exists')
41
+
42
+ for i in glob.glob(os.path.join(base, gt_path) + '/*.vtk'):
43
+ filename = os.path.basename(i).split('.')[0]
44
+ #side = os.path.basename(i).split('.')[0].split('_')[0]
45
+ #scan_name = os.path.basename(i).split('.')[0].split('_')[0]
46
+ #scan_id = os.path.basename(i).split('.')[0].split('_')[1]
47
+ output_sub_dir = os.path.join(
48
+ base, 'output', filename)
49
+ try:
50
+ os.mkdir(output_sub_dir)
51
+ except:
52
+ print(f'{output_sub_dir} already exists')
53
+
54
+ gt_mesh = pv.read(i)
55
+ pred_mesh = pv.read(os.path.join(
56
+ base, pred_path, filename + '.vtk'))
57
+ pred_vertices = np.array(pred_mesh.points)
58
+ cd = distanceVertex2Mesh(gt_mesh, pred_vertices)
59
+ pred_mesh['dist'] = cd
60
+ pred_mesh.save(os.path.join(output_sub_dir, filename + '.vtk'))
61
+
62
+
63
+ if __name__ == '__main__':
64
+ main()
metrics/get_probability_map.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pyvista as pv
3
+ import argparse
4
+ import os
5
+ import glob
6
+ import skeletor as sk
7
+ import trimesh
8
+ import navis
9
+
10
+
11
+ def parse_command_line():
12
+ print('---'*10)
13
+ print('Parsing Command Line Arguments')
14
+ parser = argparse.ArgumentParser(description='Defacing protocol')
15
+ parser.add_argument('-bp', metavar='base path', type=str,
16
+ help="Absolute path of the base directory")
17
+ parser.add_argument('-gp', metavar='ground truth path', type=str,
18
+ help="Relative path of the ground truth model")
19
+ parser.add_argument('-pp', metavar='prediction path', type=str,
20
+ help="Relative path of the prediction model")
21
+ parser.add_argument('-rr', metavar='ratio to split skeleton', type=int, nargs='+',
22
+ help="Ratio to split the skeleton")
23
+ parser.add_argument('-ps', metavar='probability sequences', type=float, nargs='+',
24
+ help="Proability sequences for each splitted region")
25
+ argv = parser.parse_args()
26
+ return argv
27
+
28
+
29
+ def distanceVertex2Path(mesh, skeleton, probability_map):
30
+ if len(probability_map) == 0:
31
+ print('empty probability_map !!!')
32
+ return np.inf
33
+
34
+ if not mesh.is_all_triangles():
35
+ print('only triangulations is allowed (Faces do not have 3 Vertices)!')
36
+ return np.inf
37
+
38
+ if hasattr(mesh, 'points'):
39
+ points = np.array(mesh.points)
40
+ else:
41
+ print('mesh structure must contain fields ''vertices'' and ''faces''!')
42
+ return np.inf
43
+
44
+ if hasattr(skeleton, 'vertices'):
45
+ vertex = skeleton.vertices
46
+ else:
47
+ print('skeleton structure must contain fields ''vertices'' !!!')
48
+ return np.inf
49
+
50
+ numV, dim = points.shape
51
+ numT, dimT = vertex.shape
52
+
53
+ if dim != dimT or dim != 3:
54
+ print('mesh and vertices must be in 3D space!')
55
+ return np.inf
56
+
57
+ d_min = np.ones(numV, dtype=np.float64) * np.inf
58
+ pm = []
59
+ # first check: find closest distance from vertex to vertex
60
+ for i in range(numV):
61
+ min_idx = -1
62
+ for j in range(numT):
63
+ v1 = points[i, :]
64
+ v2 = vertex[j, :]
65
+ d = distance3DV2V(v1, v2)
66
+ if d < d_min[i]:
67
+ d_min[i] = d
68
+ min_idx = j
69
+
70
+ pm.append(probability_map[min_idx])
71
+
72
+ print("check is finished !!!")
73
+ return pm
74
+
75
+
76
+ def generate_probability_map(skeleton, split_ratio, probability):
77
+ points = skeleton.vertices
78
+ center = skeleton.skeleton.centroid
79
+ x = sorted(points[:, 0])
80
+ left = []
81
+ right = []
82
+ for i in range(len(x)):
83
+ if x[i] < center[0]:
84
+ left.append(x[i])
85
+ else:
86
+ right.append(x[i])
87
+
88
+ right_map = []
89
+ left_map = []
90
+ sec_old = 0
91
+ for j in range(len(split_ratio)):
92
+ if j == len(split_ratio) - 1:
93
+ sec_len = len(left) - sec_old
94
+ else:
95
+ sec_len = int(round(len(left) * split_ratio[j] / 100))
96
+
97
+ for k in range(sec_old, sec_old + sec_len):
98
+ left_map.append(probability[j])
99
+
100
+ sec_old += sec_len
101
+
102
+ sec_old = 0
103
+ for j in range(len(split_ratio)-1, -1, -1):
104
+ if j == 0:
105
+ sec_len = len(right) - sec_old
106
+ else:
107
+ sec_len = int(round(len(right) * split_ratio[j] / 100))
108
+
109
+ for k in range(sec_old, sec_old + sec_len):
110
+ right_map.append(probability[j])
111
+
112
+ sec_old += sec_len
113
+
114
+ final_map = []
115
+ row = points.shape[0]
116
+ assert len(left) + len(right) == row
117
+ for m in range(row):
118
+ ver_x = points[m, 0]
119
+ if ver_x in left:
120
+ index = left.index(ver_x)
121
+ final_map.append(left_map[index])
122
+ else:
123
+ index = right.index(ver_x)
124
+ final_map.append(right_map[index])
125
+
126
+ return final_map
127
+
128
+
129
+ def skeleton(mesh):
130
+ faces_as_array = mesh.faces.reshape((mesh.n_faces, 4))[:, 1:]
131
+ trmesh = trimesh.Trimesh(mesh.points, faces_as_array)
132
+ fixed = sk.pre.fix_mesh(trmesh, remove_disconnected=5, inplace=False)
133
+ skel = sk.skeletonize.by_wavefront(fixed, waves=1, step_size=1)
134
+ # Create a neuron from your skeleton
135
+ n = navis.TreeNeuron(skel, soma=None)
136
+ # keep only the two longest linear section in your skeleton
137
+ long2 = navis.longest_neurite(n, n=2, from_root=False)
138
+
139
+ # This renumbers nodes
140
+ swc = navis.io.swc_io.make_swc_table(long2)
141
+ # We also need to rename some columns
142
+ swc = swc.rename({'PointNo': 'node_id', 'Parent': 'parent_id', 'X': 'x',
143
+ 'Y': 'y', 'Z': 'z', 'Radius': 'radius'}, axis=1).drop('Label', axis=1)
144
+ # Skeletor excepts node IDs to start with 0, but navis starts at 1 for SWC
145
+ swc['node_id'] -= 1
146
+ swc.loc[swc.parent_id > 0, 'parent_id'] -= 1
147
+ # Create the skeletor.Skeleton
148
+ skel2 = sk.Skeleton(swc)
149
+ return skel2
150
+
151
+
152
+ def distance3DV2V(v1, v2):
153
+ d = np.linalg.norm(v1-v2)
154
+ return d
155
+
156
+
157
+ def main():
158
+ args = parse_command_line()
159
+ base = args.bp
160
+ gt_path = args.gp
161
+ pred_path = args.pp
162
+ area_ratio = args.rr
163
+ prob_sequences = args.ps
164
+ output_dir = os.path.join(base, 'output')
165
+ try:
166
+ os.mkdir(output_dir)
167
+ except:
168
+ print(f'{output_dir} already exists')
169
+
170
+ for i in glob.glob(os.path.join(base, gt_path) + '/*.vtk'):
171
+ scan_name = os.path.basename(i).split('.')[0].split('_')[1]
172
+ scan_id = os.path.basename(i).split('.')[0].split('_')[2]
173
+ output_sub_dir = os.path.join(
174
+ base, 'output', scan_name + '_' + scan_id)
175
+ try:
176
+ os.mkdir(output_sub_dir)
177
+ except:
178
+ print(f'{output_sub_dir} already exists')
179
+
180
+ gt_mesh = pv.read(i)
181
+ pred_mesh = pv.read(os.path.join(
182
+ base, pred_path, 'pred_' + scan_name + '_' + scan_id + '.vtk'))
183
+ pred_skel = skeleton(pred_mesh)
184
+ prob_map = generate_probability_map(
185
+ pred_skel, area_ratio, prob_sequences)
186
+ pm = distanceVertex2Path(pred_mesh, pred_skel, prob_map)
187
+ if(pm == np.Inf):
188
+ print('something with mesh, probability map and skeleton are wrong !!!')
189
+ return
190
+ np.savetxt(os.path.join(base, output_sub_dir, scan_id + '.txt'), pm)
191
+
192
+
193
+ if __name__ == '__main__':
194
+ main()
metrics/lookup_tables.py ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2018 Google Inc. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS-IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from __future__ import absolute_import
15
+ from __future__ import division
16
+ from __future__ import print_function
17
+
18
+ import math
19
+ import numpy as np
20
+ ENCODE_NEIGHBOURHOOD_3D_KERNEL = np.array([[[128, 64], [32, 16]], [[8, 4],
21
+ [2, 1]]])
22
+
23
+ """
24
+
25
+ lookup_tables.py
26
+
27
+ all of the lookup-tables functions are borrowed from DeepMind surface_distance repository
28
+
29
+ """
30
+
31
+
32
+ # _NEIGHBOUR_CODE_TO_NORMALS is a lookup table.
33
+ # For every binary neighbour code
34
+ # (2x2x2 neighbourhood = 8 neighbours = 8 bits = 256 codes)
35
+ # it contains the surface normals of the triangles (called "surfel" for
36
+ # "surface element" in the following). The length of the normal
37
+ # vector encodes the surfel area.
38
+ #
39
+ # created using the marching_cube algorithm
40
+ # see e.g. https://en.wikipedia.org/wiki/Marching_cubes
41
+ # pylint: disable=line-too-long
42
+ _NEIGHBOUR_CODE_TO_NORMALS = [
43
+ [[0, 0, 0]],
44
+ [[0.125, 0.125, 0.125]],
45
+ [[-0.125, -0.125, 0.125]],
46
+ [[-0.25, -0.25, 0.0], [0.25, 0.25, -0.0]],
47
+ [[0.125, -0.125, 0.125]],
48
+ [[-0.25, -0.0, -0.25], [0.25, 0.0, 0.25]],
49
+ [[0.125, -0.125, 0.125], [-0.125, -0.125, 0.125]],
50
+ [[0.5, 0.0, -0.0], [0.25, 0.25, 0.25], [0.125, 0.125, 0.125]],
51
+ [[-0.125, 0.125, 0.125]],
52
+ [[0.125, 0.125, 0.125], [-0.125, 0.125, 0.125]],
53
+ [[-0.25, 0.0, 0.25], [-0.25, 0.0, 0.25]],
54
+ [[0.5, 0.0, 0.0], [-0.25, -0.25, 0.25], [-0.125, -0.125, 0.125]],
55
+ [[0.25, -0.25, 0.0], [0.25, -0.25, 0.0]],
56
+ [[0.5, 0.0, 0.0], [0.25, -0.25, 0.25], [-0.125, 0.125, -0.125]],
57
+ [[-0.5, 0.0, 0.0], [-0.25, 0.25, 0.25], [-0.125, 0.125, 0.125]],
58
+ [[0.5, 0.0, 0.0], [0.5, 0.0, 0.0]],
59
+ [[0.125, -0.125, -0.125]],
60
+ [[0.0, -0.25, -0.25], [0.0, 0.25, 0.25]],
61
+ [[-0.125, -0.125, 0.125], [0.125, -0.125, -0.125]],
62
+ [[0.0, -0.5, 0.0], [0.25, 0.25, 0.25], [0.125, 0.125, 0.125]],
63
+ [[0.125, -0.125, 0.125], [0.125, -0.125, -0.125]],
64
+ [[0.0, 0.0, -0.5], [0.25, 0.25, 0.25], [-0.125, -0.125, -0.125]],
65
+ [[-0.125, -0.125, 0.125], [0.125, -0.125, 0.125], [0.125, -0.125, -0.125]],
66
+ [[-0.125, -0.125, -0.125], [-0.25, -0.25, -0.25],
67
+ [0.25, 0.25, 0.25], [0.125, 0.125, 0.125]],
68
+ [[-0.125, 0.125, 0.125], [0.125, -0.125, -0.125]],
69
+ [[0.0, -0.25, -0.25], [0.0, 0.25, 0.25], [-0.125, 0.125, 0.125]],
70
+ [[-0.25, 0.0, 0.25], [-0.25, 0.0, 0.25], [0.125, -0.125, -0.125]],
71
+ [[0.125, 0.125, 0.125], [0.375, 0.375, 0.375],
72
+ [0.0, -0.25, 0.25], [-0.25, 0.0, 0.25]],
73
+ [[0.125, -0.125, -0.125], [0.25, -0.25, 0.0], [0.25, -0.25, 0.0]],
74
+ [[0.375, 0.375, 0.375], [0.0, 0.25, -0.25],
75
+ [-0.125, -0.125, -0.125], [-0.25, 0.25, 0.0]],
76
+ [[-0.5, 0.0, 0.0], [-0.125, -0.125, -0.125],
77
+ [-0.25, -0.25, -0.25], [0.125, 0.125, 0.125]],
78
+ [[-0.5, 0.0, 0.0], [-0.125, -0.125, -0.125], [-0.25, -0.25, -0.25]],
79
+ [[0.125, -0.125, 0.125]],
80
+ [[0.125, 0.125, 0.125], [0.125, -0.125, 0.125]],
81
+ [[0.0, -0.25, 0.25], [0.0, 0.25, -0.25]],
82
+ [[0.0, -0.5, 0.0], [0.125, 0.125, -0.125], [0.25, 0.25, -0.25]],
83
+ [[0.125, -0.125, 0.125], [0.125, -0.125, 0.125]],
84
+ [[0.125, -0.125, 0.125], [-0.25, -0.0, -0.25], [0.25, 0.0, 0.25]],
85
+ [[0.0, -0.25, 0.25], [0.0, 0.25, -0.25], [0.125, -0.125, 0.125]],
86
+ [[-0.375, -0.375, 0.375], [-0.0, 0.25, 0.25],
87
+ [0.125, 0.125, -0.125], [-0.25, -0.0, -0.25]],
88
+ [[-0.125, 0.125, 0.125], [0.125, -0.125, 0.125]],
89
+ [[0.125, 0.125, 0.125], [0.125, -0.125, 0.125], [-0.125, 0.125, 0.125]],
90
+ [[-0.0, 0.0, 0.5], [-0.25, -0.25, 0.25], [-0.125, -0.125, 0.125]],
91
+ [[0.25, 0.25, -0.25], [0.25, 0.25, -0.25],
92
+ [0.125, 0.125, -0.125], [-0.125, -0.125, 0.125]],
93
+ [[0.125, -0.125, 0.125], [0.25, -0.25, 0.0], [0.25, -0.25, 0.0]],
94
+ [[0.5, 0.0, 0.0], [0.25, -0.25, 0.25],
95
+ [-0.125, 0.125, -0.125], [0.125, -0.125, 0.125]],
96
+ [[0.0, 0.25, -0.25], [0.375, -0.375, -0.375],
97
+ [-0.125, 0.125, 0.125], [0.25, 0.25, 0.0]],
98
+ [[-0.5, 0.0, 0.0], [-0.25, -0.25, 0.25], [-0.125, -0.125, 0.125]],
99
+ [[0.25, -0.25, 0.0], [-0.25, 0.25, 0.0]],
100
+ [[0.0, 0.5, 0.0], [-0.25, 0.25, 0.25], [0.125, -0.125, -0.125]],
101
+ [[0.0, 0.5, 0.0], [0.125, -0.125, 0.125], [-0.25, 0.25, -0.25]],
102
+ [[0.0, 0.5, 0.0], [0.0, -0.5, 0.0]],
103
+ [[0.25, -0.25, 0.0], [-0.25, 0.25, 0.0], [0.125, -0.125, 0.125]],
104
+ [[-0.375, -0.375, -0.375], [-0.25, 0.0, 0.25],
105
+ [-0.125, -0.125, -0.125], [-0.25, 0.25, 0.0]],
106
+ [[0.125, 0.125, 0.125], [0.0, -0.5, 0.0],
107
+ [-0.25, -0.25, -0.25], [-0.125, -0.125, -0.125]],
108
+ [[0.0, -0.5, 0.0], [-0.25, -0.25, -0.25], [-0.125, -0.125, -0.125]],
109
+ [[-0.125, 0.125, 0.125], [0.25, -0.25, 0.0], [-0.25, 0.25, 0.0]],
110
+ [[0.0, 0.5, 0.0], [0.25, 0.25, -0.25],
111
+ [-0.125, -0.125, 0.125], [-0.125, -0.125, 0.125]],
112
+ [[-0.375, 0.375, -0.375], [-0.25, -0.25, 0.0],
113
+ [-0.125, 0.125, -0.125], [-0.25, 0.0, 0.25]],
114
+ [[0.0, 0.5, 0.0], [0.25, 0.25, -0.25], [-0.125, -0.125, 0.125]],
115
+ [[0.25, -0.25, 0.0], [-0.25, 0.25, 0.0],
116
+ [0.25, -0.25, 0.0], [0.25, -0.25, 0.0]],
117
+ [[-0.25, -0.25, 0.0], [-0.25, -0.25, 0.0], [-0.125, -0.125, 0.125]],
118
+ [[0.125, 0.125, 0.125], [-0.25, -0.25, 0.0], [-0.25, -0.25, 0.0]],
119
+ [[-0.25, -0.25, 0.0], [-0.25, -0.25, 0.0]],
120
+ [[-0.125, -0.125, 0.125]],
121
+ [[0.125, 0.125, 0.125], [-0.125, -0.125, 0.125]],
122
+ [[-0.125, -0.125, 0.125], [-0.125, -0.125, 0.125]],
123
+ [[-0.125, -0.125, 0.125], [-0.25, -0.25, 0.0], [0.25, 0.25, -0.0]],
124
+ [[0.0, -0.25, 0.25], [0.0, -0.25, 0.25]],
125
+ [[0.0, 0.0, 0.5], [0.25, -0.25, 0.25], [0.125, -0.125, 0.125]],
126
+ [[0.0, -0.25, 0.25], [0.0, -0.25, 0.25], [-0.125, -0.125, 0.125]],
127
+ [[0.375, -0.375, 0.375], [0.0, -0.25, -0.25],
128
+ [-0.125, 0.125, -0.125], [0.25, 0.25, 0.0]],
129
+ [[-0.125, -0.125, 0.125], [-0.125, 0.125, 0.125]],
130
+ [[0.125, 0.125, 0.125], [-0.125, -0.125, 0.125], [-0.125, 0.125, 0.125]],
131
+ [[-0.125, -0.125, 0.125], [-0.25, 0.0, 0.25], [-0.25, 0.0, 0.25]],
132
+ [[0.5, 0.0, 0.0], [-0.25, -0.25, 0.25],
133
+ [-0.125, -0.125, 0.125], [-0.125, -0.125, 0.125]],
134
+ [[-0.0, 0.5, 0.0], [-0.25, 0.25, -0.25], [0.125, -0.125, 0.125]],
135
+ [[-0.25, 0.25, -0.25], [-0.25, 0.25, -0.25],
136
+ [-0.125, 0.125, -0.125], [-0.125, 0.125, -0.125]],
137
+ [[-0.25, 0.0, -0.25], [0.375, -0.375, -0.375],
138
+ [0.0, 0.25, -0.25], [-0.125, 0.125, 0.125]],
139
+ [[0.5, 0.0, 0.0], [-0.25, 0.25, -0.25], [0.125, -0.125, 0.125]],
140
+ [[-0.25, 0.0, 0.25], [0.25, 0.0, -0.25]],
141
+ [[-0.0, 0.0, 0.5], [-0.25, 0.25, 0.25], [-0.125, 0.125, 0.125]],
142
+ [[-0.125, -0.125, 0.125], [-0.25, 0.0, 0.25], [0.25, 0.0, -0.25]],
143
+ [[-0.25, -0.0, -0.25], [-0.375, 0.375, 0.375],
144
+ [-0.25, -0.25, 0.0], [-0.125, 0.125, 0.125]],
145
+ [[0.0, 0.0, -0.5], [0.25, 0.25, -0.25], [-0.125, -0.125, 0.125]],
146
+ [[-0.0, 0.0, 0.5], [0.0, 0.0, 0.5]],
147
+ [[0.125, 0.125, 0.125], [0.125, 0.125, 0.125],
148
+ [0.25, 0.25, 0.25], [0.0, 0.0, 0.5]],
149
+ [[0.125, 0.125, 0.125], [0.25, 0.25, 0.25], [0.0, 0.0, 0.5]],
150
+ [[-0.25, 0.0, 0.25], [0.25, 0.0, -0.25], [-0.125, 0.125, 0.125]],
151
+ [[-0.0, 0.0, 0.5], [0.25, -0.25, 0.25],
152
+ [0.125, -0.125, 0.125], [0.125, -0.125, 0.125]],
153
+ [[-0.25, 0.0, 0.25], [-0.25, 0.0, 0.25],
154
+ [-0.25, 0.0, 0.25], [0.25, 0.0, -0.25]],
155
+ [[0.125, -0.125, 0.125], [0.25, 0.0, 0.25], [0.25, 0.0, 0.25]],
156
+ [[0.25, 0.0, 0.25], [-0.375, -0.375, 0.375],
157
+ [-0.25, 0.25, 0.0], [-0.125, -0.125, 0.125]],
158
+ [[-0.0, 0.0, 0.5], [0.25, -0.25, 0.25], [0.125, -0.125, 0.125]],
159
+ [[0.125, 0.125, 0.125], [0.25, 0.0, 0.25], [0.25, 0.0, 0.25]],
160
+ [[0.25, 0.0, 0.25], [0.25, 0.0, 0.25]],
161
+ [[-0.125, -0.125, 0.125], [0.125, -0.125, 0.125]],
162
+ [[0.125, 0.125, 0.125], [-0.125, -0.125, 0.125], [0.125, -0.125, 0.125]],
163
+ [[-0.125, -0.125, 0.125], [0.0, -0.25, 0.25], [0.0, 0.25, -0.25]],
164
+ [[0.0, -0.5, 0.0], [0.125, 0.125, -0.125],
165
+ [0.25, 0.25, -0.25], [-0.125, -0.125, 0.125]],
166
+ [[0.0, -0.25, 0.25], [0.0, -0.25, 0.25], [0.125, -0.125, 0.125]],
167
+ [[0.0, 0.0, 0.5], [0.25, -0.25, 0.25],
168
+ [0.125, -0.125, 0.125], [0.125, -0.125, 0.125]],
169
+ [[0.0, -0.25, 0.25], [0.0, -0.25, 0.25],
170
+ [0.0, -0.25, 0.25], [0.0, 0.25, -0.25]],
171
+ [[0.0, 0.25, 0.25], [0.0, 0.25, 0.25], [0.125, -0.125, -0.125]],
172
+ [[-0.125, 0.125, 0.125], [0.125, -0.125, 0.125], [-0.125, -0.125, 0.125]],
173
+ [[-0.125, 0.125, 0.125], [0.125, -0.125, 0.125],
174
+ [-0.125, -0.125, 0.125], [0.125, 0.125, 0.125]],
175
+ [[-0.0, 0.0, 0.5], [-0.25, -0.25, 0.25],
176
+ [-0.125, -0.125, 0.125], [-0.125, -0.125, 0.125]],
177
+ [[0.125, 0.125, 0.125], [0.125, -0.125, 0.125], [0.125, -0.125, -0.125]],
178
+ [[-0.0, 0.5, 0.0], [-0.25, 0.25, -0.25],
179
+ [0.125, -0.125, 0.125], [0.125, -0.125, 0.125]],
180
+ [[0.125, 0.125, 0.125], [-0.125, -0.125, 0.125], [0.125, -0.125, -0.125]],
181
+ [[0.0, -0.25, -0.25], [0.0, 0.25, 0.25], [0.125, 0.125, 0.125]],
182
+ [[0.125, 0.125, 0.125], [0.125, -0.125, -0.125]],
183
+ [[0.5, 0.0, -0.0], [0.25, -0.25, -0.25], [0.125, -0.125, -0.125]],
184
+ [[-0.25, 0.25, 0.25], [-0.125, 0.125, 0.125],
185
+ [-0.25, 0.25, 0.25], [0.125, -0.125, -0.125]],
186
+ [[0.375, -0.375, 0.375], [0.0, 0.25, 0.25],
187
+ [-0.125, 0.125, -0.125], [-0.25, 0.0, 0.25]],
188
+ [[0.0, -0.5, 0.0], [-0.25, 0.25, 0.25], [-0.125, 0.125, 0.125]],
189
+ [[-0.375, -0.375, 0.375], [0.25, -0.25, 0.0],
190
+ [0.0, 0.25, 0.25], [-0.125, -0.125, 0.125]],
191
+ [[-0.125, 0.125, 0.125], [-0.25, 0.25, 0.25], [0.0, 0.0, 0.5]],
192
+ [[0.125, 0.125, 0.125], [0.0, 0.25, 0.25], [0.0, 0.25, 0.25]],
193
+ [[0.0, 0.25, 0.25], [0.0, 0.25, 0.25]],
194
+ [[0.5, 0.0, -0.0], [0.25, 0.25, 0.25],
195
+ [0.125, 0.125, 0.125], [0.125, 0.125, 0.125]],
196
+ [[0.125, -0.125, 0.125], [-0.125, -0.125, 0.125], [0.125, 0.125, 0.125]],
197
+ [[-0.25, -0.0, -0.25], [0.25, 0.0, 0.25], [0.125, 0.125, 0.125]],
198
+ [[0.125, 0.125, 0.125], [0.125, -0.125, 0.125]],
199
+ [[-0.25, -0.25, 0.0], [0.25, 0.25, -0.0], [0.125, 0.125, 0.125]],
200
+ [[0.125, 0.125, 0.125], [-0.125, -0.125, 0.125]],
201
+ [[0.125, 0.125, 0.125], [0.125, 0.125, 0.125]],
202
+ [[0.125, 0.125, 0.125]],
203
+ [[0.125, 0.125, 0.125]],
204
+ [[0.125, 0.125, 0.125], [0.125, 0.125, 0.125]],
205
+ [[0.125, 0.125, 0.125], [-0.125, -0.125, 0.125]],
206
+ [[-0.25, -0.25, 0.0], [0.25, 0.25, -0.0], [0.125, 0.125, 0.125]],
207
+ [[0.125, 0.125, 0.125], [0.125, -0.125, 0.125]],
208
+ [[-0.25, -0.0, -0.25], [0.25, 0.0, 0.25], [0.125, 0.125, 0.125]],
209
+ [[0.125, -0.125, 0.125], [-0.125, -0.125, 0.125], [0.125, 0.125, 0.125]],
210
+ [[0.5, 0.0, -0.0], [0.25, 0.25, 0.25],
211
+ [0.125, 0.125, 0.125], [0.125, 0.125, 0.125]],
212
+ [[0.0, 0.25, 0.25], [0.0, 0.25, 0.25]],
213
+ [[0.125, 0.125, 0.125], [0.0, 0.25, 0.25], [0.0, 0.25, 0.25]],
214
+ [[-0.125, 0.125, 0.125], [-0.25, 0.25, 0.25], [0.0, 0.0, 0.5]],
215
+ [[-0.375, -0.375, 0.375], [0.25, -0.25, 0.0],
216
+ [0.0, 0.25, 0.25], [-0.125, -0.125, 0.125]],
217
+ [[0.0, -0.5, 0.0], [-0.25, 0.25, 0.25], [-0.125, 0.125, 0.125]],
218
+ [[0.375, -0.375, 0.375], [0.0, 0.25, 0.25],
219
+ [-0.125, 0.125, -0.125], [-0.25, 0.0, 0.25]],
220
+ [[-0.25, 0.25, 0.25], [-0.125, 0.125, 0.125],
221
+ [-0.25, 0.25, 0.25], [0.125, -0.125, -0.125]],
222
+ [[0.5, 0.0, -0.0], [0.25, -0.25, -0.25], [0.125, -0.125, -0.125]],
223
+ [[0.125, 0.125, 0.125], [0.125, -0.125, -0.125]],
224
+ [[0.0, -0.25, -0.25], [0.0, 0.25, 0.25], [0.125, 0.125, 0.125]],
225
+ [[0.125, 0.125, 0.125], [-0.125, -0.125, 0.125], [0.125, -0.125, -0.125]],
226
+ [[-0.0, 0.5, 0.0], [-0.25, 0.25, -0.25],
227
+ [0.125, -0.125, 0.125], [0.125, -0.125, 0.125]],
228
+ [[0.125, 0.125, 0.125], [0.125, -0.125, 0.125], [0.125, -0.125, -0.125]],
229
+ [[-0.0, 0.0, 0.5], [-0.25, -0.25, 0.25],
230
+ [-0.125, -0.125, 0.125], [-0.125, -0.125, 0.125]],
231
+ [[-0.125, 0.125, 0.125], [0.125, -0.125, 0.125],
232
+ [-0.125, -0.125, 0.125], [0.125, 0.125, 0.125]],
233
+ [[-0.125, 0.125, 0.125], [0.125, -0.125, 0.125], [-0.125, -0.125, 0.125]],
234
+ [[0.0, 0.25, 0.25], [0.0, 0.25, 0.25], [0.125, -0.125, -0.125]],
235
+ [[0.0, -0.25, -0.25], [0.0, 0.25, 0.25], [0.0, 0.25, 0.25], [0.0, 0.25, 0.25]],
236
+ [[0.0, 0.0, 0.5], [0.25, -0.25, 0.25],
237
+ [0.125, -0.125, 0.125], [0.125, -0.125, 0.125]],
238
+ [[0.0, -0.25, 0.25], [0.0, -0.25, 0.25], [0.125, -0.125, 0.125]],
239
+ [[0.0, -0.5, 0.0], [0.125, 0.125, -0.125],
240
+ [0.25, 0.25, -0.25], [-0.125, -0.125, 0.125]],
241
+ [[-0.125, -0.125, 0.125], [0.0, -0.25, 0.25], [0.0, 0.25, -0.25]],
242
+ [[0.125, 0.125, 0.125], [-0.125, -0.125, 0.125], [0.125, -0.125, 0.125]],
243
+ [[-0.125, -0.125, 0.125], [0.125, -0.125, 0.125]],
244
+ [[0.25, 0.0, 0.25], [0.25, 0.0, 0.25]],
245
+ [[0.125, 0.125, 0.125], [0.25, 0.0, 0.25], [0.25, 0.0, 0.25]],
246
+ [[-0.0, 0.0, 0.5], [0.25, -0.25, 0.25], [0.125, -0.125, 0.125]],
247
+ [[0.25, 0.0, 0.25], [-0.375, -0.375, 0.375],
248
+ [-0.25, 0.25, 0.0], [-0.125, -0.125, 0.125]],
249
+ [[0.125, -0.125, 0.125], [0.25, 0.0, 0.25], [0.25, 0.0, 0.25]],
250
+ [[-0.25, -0.0, -0.25], [0.25, 0.0, 0.25],
251
+ [0.25, 0.0, 0.25], [0.25, 0.0, 0.25]],
252
+ [[-0.0, 0.0, 0.5], [0.25, -0.25, 0.25],
253
+ [0.125, -0.125, 0.125], [0.125, -0.125, 0.125]],
254
+ [[-0.25, 0.0, 0.25], [0.25, 0.0, -0.25], [-0.125, 0.125, 0.125]],
255
+ [[0.125, 0.125, 0.125], [0.25, 0.25, 0.25], [0.0, 0.0, 0.5]],
256
+ [[0.125, 0.125, 0.125], [0.125, 0.125, 0.125],
257
+ [0.25, 0.25, 0.25], [0.0, 0.0, 0.5]],
258
+ [[-0.0, 0.0, 0.5], [0.0, 0.0, 0.5]],
259
+ [[0.0, 0.0, -0.5], [0.25, 0.25, -0.25], [-0.125, -0.125, 0.125]],
260
+ [[-0.25, -0.0, -0.25], [-0.375, 0.375, 0.375],
261
+ [-0.25, -0.25, 0.0], [-0.125, 0.125, 0.125]],
262
+ [[-0.125, -0.125, 0.125], [-0.25, 0.0, 0.25], [0.25, 0.0, -0.25]],
263
+ [[-0.0, 0.0, 0.5], [-0.25, 0.25, 0.25], [-0.125, 0.125, 0.125]],
264
+ [[-0.25, 0.0, 0.25], [0.25, 0.0, -0.25]],
265
+ [[0.5, 0.0, 0.0], [-0.25, 0.25, -0.25], [0.125, -0.125, 0.125]],
266
+ [[-0.25, 0.0, -0.25], [0.375, -0.375, -0.375],
267
+ [0.0, 0.25, -0.25], [-0.125, 0.125, 0.125]],
268
+ [[-0.25, 0.25, -0.25], [-0.25, 0.25, -0.25],
269
+ [-0.125, 0.125, -0.125], [-0.125, 0.125, -0.125]],
270
+ [[-0.0, 0.5, 0.0], [-0.25, 0.25, -0.25], [0.125, -0.125, 0.125]],
271
+ [[0.5, 0.0, 0.0], [-0.25, -0.25, 0.25],
272
+ [-0.125, -0.125, 0.125], [-0.125, -0.125, 0.125]],
273
+ [[-0.125, -0.125, 0.125], [-0.25, 0.0, 0.25], [-0.25, 0.0, 0.25]],
274
+ [[0.125, 0.125, 0.125], [-0.125, -0.125, 0.125], [-0.125, 0.125, 0.125]],
275
+ [[-0.125, -0.125, 0.125], [-0.125, 0.125, 0.125]],
276
+ [[0.375, -0.375, 0.375], [0.0, -0.25, -0.25],
277
+ [-0.125, 0.125, -0.125], [0.25, 0.25, 0.0]],
278
+ [[0.0, -0.25, 0.25], [0.0, -0.25, 0.25], [-0.125, -0.125, 0.125]],
279
+ [[0.0, 0.0, 0.5], [0.25, -0.25, 0.25], [0.125, -0.125, 0.125]],
280
+ [[0.0, -0.25, 0.25], [0.0, -0.25, 0.25]],
281
+ [[-0.125, -0.125, 0.125], [-0.25, -0.25, 0.0], [0.25, 0.25, -0.0]],
282
+ [[-0.125, -0.125, 0.125], [-0.125, -0.125, 0.125]],
283
+ [[0.125, 0.125, 0.125], [-0.125, -0.125, 0.125]],
284
+ [[-0.125, -0.125, 0.125]],
285
+ [[-0.25, -0.25, 0.0], [-0.25, -0.25, 0.0]],
286
+ [[0.125, 0.125, 0.125], [-0.25, -0.25, 0.0], [-0.25, -0.25, 0.0]],
287
+ [[-0.25, -0.25, 0.0], [-0.25, -0.25, 0.0], [-0.125, -0.125, 0.125]],
288
+ [[-0.25, -0.25, 0.0], [-0.25, -0.25, 0.0],
289
+ [-0.25, -0.25, 0.0], [0.25, 0.25, -0.0]],
290
+ [[0.0, 0.5, 0.0], [0.25, 0.25, -0.25], [-0.125, -0.125, 0.125]],
291
+ [[-0.375, 0.375, -0.375], [-0.25, -0.25, 0.0],
292
+ [-0.125, 0.125, -0.125], [-0.25, 0.0, 0.25]],
293
+ [[0.0, 0.5, 0.0], [0.25, 0.25, -0.25],
294
+ [-0.125, -0.125, 0.125], [-0.125, -0.125, 0.125]],
295
+ [[-0.125, 0.125, 0.125], [0.25, -0.25, 0.0], [-0.25, 0.25, 0.0]],
296
+ [[0.0, -0.5, 0.0], [-0.25, -0.25, -0.25], [-0.125, -0.125, -0.125]],
297
+ [[0.125, 0.125, 0.125], [0.0, -0.5, 0.0],
298
+ [-0.25, -0.25, -0.25], [-0.125, -0.125, -0.125]],
299
+ [[-0.375, -0.375, -0.375], [-0.25, 0.0, 0.25],
300
+ [-0.125, -0.125, -0.125], [-0.25, 0.25, 0.0]],
301
+ [[0.25, -0.25, 0.0], [-0.25, 0.25, 0.0], [0.125, -0.125, 0.125]],
302
+ [[0.0, 0.5, 0.0], [0.0, -0.5, 0.0]],
303
+ [[0.0, 0.5, 0.0], [0.125, -0.125, 0.125], [-0.25, 0.25, -0.25]],
304
+ [[0.0, 0.5, 0.0], [-0.25, 0.25, 0.25], [0.125, -0.125, -0.125]],
305
+ [[0.25, -0.25, 0.0], [-0.25, 0.25, 0.0]],
306
+ [[-0.5, 0.0, 0.0], [-0.25, -0.25, 0.25], [-0.125, -0.125, 0.125]],
307
+ [[0.0, 0.25, -0.25], [0.375, -0.375, -0.375],
308
+ [-0.125, 0.125, 0.125], [0.25, 0.25, 0.0]],
309
+ [[0.5, 0.0, 0.0], [0.25, -0.25, 0.25],
310
+ [-0.125, 0.125, -0.125], [0.125, -0.125, 0.125]],
311
+ [[0.125, -0.125, 0.125], [0.25, -0.25, 0.0], [0.25, -0.25, 0.0]],
312
+ [[0.25, 0.25, -0.25], [0.25, 0.25, -0.25],
313
+ [0.125, 0.125, -0.125], [-0.125, -0.125, 0.125]],
314
+ [[-0.0, 0.0, 0.5], [-0.25, -0.25, 0.25], [-0.125, -0.125, 0.125]],
315
+ [[0.125, 0.125, 0.125], [0.125, -0.125, 0.125], [-0.125, 0.125, 0.125]],
316
+ [[-0.125, 0.125, 0.125], [0.125, -0.125, 0.125]],
317
+ [[-0.375, -0.375, 0.375], [-0.0, 0.25, 0.25],
318
+ [0.125, 0.125, -0.125], [-0.25, -0.0, -0.25]],
319
+ [[0.0, -0.25, 0.25], [0.0, 0.25, -0.25], [0.125, -0.125, 0.125]],
320
+ [[0.125, -0.125, 0.125], [-0.25, -0.0, -0.25], [0.25, 0.0, 0.25]],
321
+ [[0.125, -0.125, 0.125], [0.125, -0.125, 0.125]],
322
+ [[0.0, -0.5, 0.0], [0.125, 0.125, -0.125], [0.25, 0.25, -0.25]],
323
+ [[0.0, -0.25, 0.25], [0.0, 0.25, -0.25]],
324
+ [[0.125, 0.125, 0.125], [0.125, -0.125, 0.125]],
325
+ [[0.125, -0.125, 0.125]],
326
+ [[-0.5, 0.0, 0.0], [-0.125, -0.125, -0.125], [-0.25, -0.25, -0.25]],
327
+ [[-0.5, 0.0, 0.0], [-0.125, -0.125, -0.125],
328
+ [-0.25, -0.25, -0.25], [0.125, 0.125, 0.125]],
329
+ [[0.375, 0.375, 0.375], [0.0, 0.25, -0.25],
330
+ [-0.125, -0.125, -0.125], [-0.25, 0.25, 0.0]],
331
+ [[0.125, -0.125, -0.125], [0.25, -0.25, 0.0], [0.25, -0.25, 0.0]],
332
+ [[0.125, 0.125, 0.125], [0.375, 0.375, 0.375],
333
+ [0.0, -0.25, 0.25], [-0.25, 0.0, 0.25]],
334
+ [[-0.25, 0.0, 0.25], [-0.25, 0.0, 0.25], [0.125, -0.125, -0.125]],
335
+ [[0.0, -0.25, -0.25], [0.0, 0.25, 0.25], [-0.125, 0.125, 0.125]],
336
+ [[-0.125, 0.125, 0.125], [0.125, -0.125, -0.125]],
337
+ [[-0.125, -0.125, -0.125], [-0.25, -0.25, -0.25],
338
+ [0.25, 0.25, 0.25], [0.125, 0.125, 0.125]],
339
+ [[-0.125, -0.125, 0.125], [0.125, -0.125, 0.125], [0.125, -0.125, -0.125]],
340
+ [[0.0, 0.0, -0.5], [0.25, 0.25, 0.25], [-0.125, -0.125, -0.125]],
341
+ [[0.125, -0.125, 0.125], [0.125, -0.125, -0.125]],
342
+ [[0.0, -0.5, 0.0], [0.25, 0.25, 0.25], [0.125, 0.125, 0.125]],
343
+ [[-0.125, -0.125, 0.125], [0.125, -0.125, -0.125]],
344
+ [[0.0, -0.25, -0.25], [0.0, 0.25, 0.25]],
345
+ [[0.125, -0.125, -0.125]],
346
+ [[0.5, 0.0, 0.0], [0.5, 0.0, 0.0]],
347
+ [[-0.5, 0.0, 0.0], [-0.25, 0.25, 0.25], [-0.125, 0.125, 0.125]],
348
+ [[0.5, 0.0, 0.0], [0.25, -0.25, 0.25], [-0.125, 0.125, -0.125]],
349
+ [[0.25, -0.25, 0.0], [0.25, -0.25, 0.0]],
350
+ [[0.5, 0.0, 0.0], [-0.25, -0.25, 0.25], [-0.125, -0.125, 0.125]],
351
+ [[-0.25, 0.0, 0.25], [-0.25, 0.0, 0.25]],
352
+ [[0.125, 0.125, 0.125], [-0.125, 0.125, 0.125]],
353
+ [[-0.125, 0.125, 0.125]],
354
+ [[0.5, 0.0, -0.0], [0.25, 0.25, 0.25], [0.125, 0.125, 0.125]],
355
+ [[0.125, -0.125, 0.125], [-0.125, -0.125, 0.125]],
356
+ [[-0.25, -0.0, -0.25], [0.25, 0.0, 0.25]],
357
+ [[0.125, -0.125, 0.125]],
358
+ [[-0.25, -0.25, 0.0], [0.25, 0.25, -0.0]],
359
+ [[-0.125, -0.125, 0.125]],
360
+ [[0.125, 0.125, 0.125]],
361
+ [[0, 0, 0]]]
362
+ # pylint: enable=line-too-long
363
+
364
+
365
+ def create_table_neighbour_code_to_surface_area(spacing_mm):
366
+ """Returns an array mapping neighbourhood code to the surface elements area.
367
+ Note that the normals encode the initial surface area. This function computes
368
+ the area corresponding to the given `spacing_mm`.
369
+ Args:
370
+ spacing_mm: 3-element list-like structure. Voxel spacing in x0, x1 and x2
371
+ direction.
372
+ """
373
+ # compute the area for all 256 possible surface elements
374
+ # (given a 2x2x2 neighbourhood) according to the spacing_mm
375
+ neighbour_code_to_surface_area = np.zeros([256])
376
+ for code in range(256):
377
+ normals = np.array(_NEIGHBOUR_CODE_TO_NORMALS[code])
378
+ sum_area = 0
379
+ for normal_idx in range(normals.shape[0]):
380
+ # normal vector
381
+ n = np.zeros([3])
382
+ n[0] = normals[normal_idx, 0] * spacing_mm[1] * spacing_mm[2]
383
+ n[1] = normals[normal_idx, 1] * spacing_mm[0] * spacing_mm[2]
384
+ n[2] = normals[normal_idx, 2] * spacing_mm[0] * spacing_mm[1]
385
+ area = np.linalg.norm(n)
386
+ sum_area += area
387
+ neighbour_code_to_surface_area[code] = sum_area
388
+
389
+ return neighbour_code_to_surface_area
390
+
391
+
392
+ # In the neighbourhood, points are ordered: top left, top right, bottom left,
393
+ # bottom right.
394
+ ENCODE_NEIGHBOURHOOD_2D_KERNEL = np.array([[8, 4], [2, 1]])
395
+
396
+
397
+ def create_table_neighbour_code_to_contour_length(spacing_mm):
398
+ """Returns an array mapping neighbourhood code to the contour length.
399
+ For the list of possible cases and their figures, see page 38 from:
400
+ https://nccastaff.bournemouth.ac.uk/jmacey/MastersProjects/MSc14/06/thesis.pdf
401
+ In 2D, each point has 4 neighbors. Thus, are 16 configurations. A
402
+ configuration is encoded with '1' meaning "inside the object" and '0' "outside
403
+ the object". The points are ordered: top left, top right, bottom left, bottom
404
+ right.
405
+ The x0 axis is assumed vertical downward, and the x1 axis is horizontal to the
406
+ right:
407
+ (0, 0) --> (0, 1)
408
+ |
409
+ (1, 0)
410
+ Args:
411
+ spacing_mm: 2-element list-like structure. Voxel spacing in x0 and x1
412
+ directions.
413
+ """
414
+ neighbour_code_to_contour_length = np.zeros([16])
415
+
416
+ vertical = spacing_mm[0]
417
+ horizontal = spacing_mm[1]
418
+ diag = 0.5 * math.sqrt(spacing_mm[0]**2 + spacing_mm[1]**2)
419
+ # pyformat: disable
420
+ neighbour_code_to_contour_length[int("00"
421
+ "01", 2)] = diag
422
+
423
+ neighbour_code_to_contour_length[int("00"
424
+ "10", 2)] = diag
425
+
426
+ neighbour_code_to_contour_length[int("00"
427
+ "11", 2)] = horizontal
428
+
429
+ neighbour_code_to_contour_length[int("01"
430
+ "00", 2)] = diag
431
+
432
+ neighbour_code_to_contour_length[int("01"
433
+ "01", 2)] = vertical
434
+
435
+ neighbour_code_to_contour_length[int("01"
436
+ "10", 2)] = 2*diag
437
+
438
+ neighbour_code_to_contour_length[int("01"
439
+ "11", 2)] = diag
440
+
441
+ neighbour_code_to_contour_length[int("10"
442
+ "00", 2)] = diag
443
+
444
+ neighbour_code_to_contour_length[int("10"
445
+ "01", 2)] = 2*diag
446
+
447
+ neighbour_code_to_contour_length[int("10"
448
+ "10", 2)] = vertical
449
+
450
+ neighbour_code_to_contour_length[int("10"
451
+ "11", 2)] = diag
452
+
453
+ neighbour_code_to_contour_length[int("11"
454
+ "00", 2)] = horizontal
455
+
456
+ neighbour_code_to_contour_length[int("11"
457
+ "01", 2)] = diag
458
+
459
+ neighbour_code_to_contour_length[int("11"
460
+ "10", 2)] = diag
461
+ # pyformat: enable
462
+
463
+ return neighbour_code_to_contour_length
metrics/metrics.py ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import nibabel as nib
3
+ import ants
4
+ import argparse
5
+ import pandas as pd
6
+ import glob
7
+ import os
8
+ import surface_distance
9
+ import nrrd
10
+ import shutil
11
+ import distanceVertex2Mesh
12
+ import textwrap
13
+
14
+
15
+ def parse_command_line():
16
+ print('---'*10)
17
+ print('Parsing Command Line Arguments')
18
+ parser = argparse.ArgumentParser(
19
+ description='Inference evaluation pipeline for image registration-segmentation', formatter_class=argparse.RawTextHelpFormatter)
20
+ parser.add_argument('-bp', metavar='base path', type=str,
21
+ help="Absolute path of the base directory")
22
+ parser.add_argument('-gp', metavar='ground truth path', type=str,
23
+ help="Relative path of the ground truth segmentation directory")
24
+ parser.add_argument('-pp', metavar='predicted path', type=str,
25
+ help="Relative path of predicted segmentation directory")
26
+ parser.add_argument('-sp', metavar='save path', type=str,
27
+ help="Relative path of CSV file directory to save, if not specify, default is base directory")
28
+ parser.add_argument('-vt', metavar='validation type', type=str, nargs='+',
29
+ help=textwrap.dedent('''Validation type:
30
+ dsc: Dice Score
31
+ ahd: Average Hausdorff Distance
32
+ whd: Weighted Hausdorff Distance
33
+ '''))
34
+ parser.add_argument('-pm', metavar='probability map path', type=str,
35
+ help="Relative path of text file directory of probability map")
36
+ parser.add_argument('-fn', metavar='file name', type=str,
37
+ help="name of output file")
38
+ parser.add_argument('-reg', action='store_true',
39
+ help="check if the input files are registration predictions")
40
+ parser.add_argument('-tp', metavar='type of segmentation', type=str,
41
+ help=textwrap.dedent('''Segmentation type:
42
+ ET: Eustachian Tube
43
+ NC: Nasal Cavity
44
+ HT: Head Tumor
45
+ '''))
46
+ parser.add_argument('-sl', metavar='segmentation information list', type=str, nargs='+',
47
+ help='a list of label name and corresponding value')
48
+ parser.add_argument('-cp', metavar='current prefix of filenames', type=str,
49
+ help='current prefix of filenames')
50
+ argv = parser.parse_args()
51
+ return argv
52
+
53
+
54
+ def rename(prefix, filename):
55
+ name = filename.split('.')[0][-3:]
56
+ name = prefix + '_' + name
57
+ return name
58
+
59
+ def dice_coefficient_and_hausdorff_distance(filename, img_np_pred, img_np_gt, num_classes, spacing, probability_map, dsc, ahd, whd, average_DSC, average_HD):
60
+ df = pd.DataFrame()
61
+ data_gt, bool_gt = make_one_hot(img_np_gt, num_classes)
62
+ data_pred, bool_pred = make_one_hot(img_np_pred, num_classes)
63
+ for i in range(1, num_classes):
64
+ df1 = pd.DataFrame([[filename, i]], columns=[
65
+ 'File ID', 'Label Value'])
66
+ if dsc:
67
+ if data_pred[i].any():
68
+ volume_sum = data_gt[i].sum() + data_pred[i].sum()
69
+ if volume_sum == 0:
70
+ return np.NaN
71
+
72
+ volume_intersect = (data_gt[i] & data_pred[i]).sum()
73
+ dice = 2*volume_intersect / volume_sum
74
+ df1['Dice Score'] = dice
75
+ average_DSC[i-1] += dice
76
+ else:
77
+ dice = 0.0
78
+ df1['Dice Score'] = dice
79
+ average_DSC[i-1] += dice
80
+ if ahd:
81
+ if data_pred[i].any():
82
+ avd = average_hausdorff_distance(bool_gt[i], bool_pred[i], spacing)
83
+ df1['Average Hausdorff Distance'] = avd
84
+ average_HD[i-1] += avd
85
+ else:
86
+ avd = np.nan
87
+ df1['Average Hausdorff Distance'] = avd
88
+ average_HD[i-1] += avd
89
+ if whd:
90
+ # wgd = weighted_hausdorff_distance(gt, pred, probability_map)
91
+ # df1['Weighted Hausdorff Distance'] = wgd
92
+ pass
93
+
94
+ df = pd.concat([df, df1])
95
+ return df, average_DSC, average_HD
96
+
97
+
98
+ def make_one_hot(img_np, num_classes):
99
+ img_one_hot_dice = np.zeros(
100
+ (num_classes, img_np.shape[0], img_np.shape[1], img_np.shape[2]), dtype=np.int8)
101
+ img_one_hot_hd = np.zeros(
102
+ (num_classes, img_np.shape[0], img_np.shape[1], img_np.shape[2]), dtype=bool)
103
+ for i in range(num_classes):
104
+ a = (img_np == i)
105
+ img_one_hot_dice[i, :, :, :] = a
106
+ img_one_hot_hd[i, :, :, :] = a
107
+
108
+ return img_one_hot_dice, img_one_hot_hd
109
+
110
+
111
+ def average_hausdorff_distance(img_np_gt, img_np_pred, spacing):
112
+ surf_distance = surface_distance.compute_surface_distances(
113
+ img_np_gt, img_np_pred, spacing)
114
+ gp, pg = surface_distance.compute_average_surface_distance(surf_distance)
115
+ return (gp + pg) / 2
116
+
117
+
118
+ def checkSegFormat(base, segmentation, type, prefix=None):
119
+ if type == 'gt':
120
+ save_dir = os.path.join(base, 'gt_reformat_labels')
121
+ path = segmentation
122
+ else:
123
+ save_dir = os.path.join(base, 'pred_reformat_labels')
124
+ path = os.path.join(base, segmentation)
125
+ try:
126
+ os.mkdir(save_dir)
127
+ except:
128
+ print(f'{save_dir} already exists')
129
+
130
+ for file in os.listdir(path):
131
+ if type == 'gt':
132
+ if prefix is not None:
133
+ name = rename(prefix, file)
134
+ else:
135
+ name = file.split('.')[0]
136
+ else:
137
+ name = file.split('.')[0]
138
+
139
+ if file.endswith('seg.nrrd'):
140
+ ants_img = ants.image_read(os.path.join(path, file))
141
+ header = nrrd.read_header(os.path.join(path, file))
142
+ filename = os.path.join(save_dir, name + '.nii.gz')
143
+ nrrd2nifti(ants_img, header, filename)
144
+ elif file.endswith('nii'):
145
+ image = ants.image_read(os.path.join(path, file))
146
+ image.to_file(os.path.join(save_dir, name + '.nii.gz'))
147
+ elif file.endswith('nii.gz'):
148
+ shutil.copy(os.path.join(path, file), os.path.join(save_dir, name + '.nii.gz'))
149
+
150
+ return save_dir
151
+
152
+
153
+ def nrrd2nifti(img, header, filename):
154
+ img_as_np = img.view(single_components=True)
155
+ data = convert_to_one_hot(img_as_np, header)
156
+ foreground = np.max(data, axis=0)
157
+ labelmap = np.multiply(np.argmax(data, axis=0) + 1,
158
+ foreground).astype('uint8')
159
+ segmentation_img = ants.from_numpy(
160
+ labelmap, origin=img.origin, spacing=img.spacing, direction=img.direction)
161
+ print('-- Saving NII Segmentations')
162
+ segmentation_img.to_file(filename)
163
+
164
+
165
+ def convert_to_one_hot(data, header, segment_indices=None):
166
+ print('---'*10)
167
+ print("converting to one hot")
168
+
169
+ layer_values = get_layer_values(header)
170
+ label_values = get_label_values(header)
171
+
172
+ # Newer Slicer NRRD (compressed layers)
173
+ if layer_values and label_values:
174
+
175
+ assert len(layer_values) == len(label_values)
176
+ if len(data.shape) == 3:
177
+ x_dim, y_dim, z_dim = data.shape
178
+ elif len(data.shape) == 4:
179
+ x_dim, y_dim, z_dim = data.shape[1:]
180
+
181
+ num_segments = len(layer_values)
182
+ one_hot = np.zeros((num_segments, x_dim, y_dim, z_dim))
183
+
184
+ if segment_indices is None:
185
+ segment_indices = list(range(num_segments))
186
+
187
+ elif isinstance(segment_indices, int):
188
+ segment_indices = [segment_indices]
189
+
190
+ elif not isinstance(segment_indices, list):
191
+ print("incorrectly specified segment indices")
192
+ return
193
+
194
+ # Check if NRRD is composed of one layer 0
195
+ if np.max(layer_values) == 0:
196
+ for i, seg_idx in enumerate(segment_indices):
197
+ layer = layer_values[seg_idx]
198
+ label = label_values[seg_idx]
199
+ one_hot[i] = 1*(data == label).astype(np.uint8)
200
+
201
+ else:
202
+ for i, seg_idx in enumerate(segment_indices):
203
+ layer = layer_values[seg_idx]
204
+ label = label_values[seg_idx]
205
+ one_hot[i] = 1*(data[layer] == label).astype(np.uint8)
206
+
207
+ # Binary labelmap
208
+ elif len(data.shape) == 3:
209
+ x_dim, y_dim, z_dim = data.shape
210
+ num_segments = np.max(data)
211
+ one_hot = np.zeros((num_segments, x_dim, y_dim, z_dim))
212
+
213
+ if segment_indices is None:
214
+ segment_indices = list(range(1, num_segments + 1))
215
+
216
+ elif isinstance(segment_indices, int):
217
+ segment_indices = [segment_indices]
218
+
219
+ elif not isinstance(segment_indices, list):
220
+ print("incorrectly specified segment indices")
221
+ return
222
+
223
+ for i, seg_idx in enumerate(segment_indices):
224
+ one_hot[i] = 1*(data == seg_idx).astype(np.uint8)
225
+
226
+ # Older Slicer NRRD (already one-hot)
227
+ else:
228
+ return data
229
+
230
+ return one_hot
231
+
232
+
233
+ def get_layer_values(header):
234
+ layer_values = []
235
+ num_segments = len([key for key in header.keys() if "Layer" in key])
236
+ for i in range(num_segments):
237
+ layer_values.append(int(header['Segment{}_Layer'.format(i)]))
238
+ return layer_values
239
+
240
+
241
+ def get_label_values(header):
242
+ label_values = []
243
+ num_segments = len([key for key in header.keys() if "LabelValue" in key])
244
+ for i in range(num_segments):
245
+ label_values.append(int(header['Segment{}_LabelValue'.format(i)]))
246
+ return label_values
247
+
248
+
249
+ def main():
250
+ args = parse_command_line()
251
+ base = args.bp
252
+ gt_path = args.gp
253
+ pred_path = args.pp
254
+ if args.sp is None:
255
+ save_path = base
256
+ else:
257
+ save_path = args.sp
258
+ validation_type = args.vt
259
+ probability_map_path = args.pm
260
+ filename = args.fn
261
+ reg = args.reg
262
+ seg_type = args.tp
263
+ label_list = args.sl
264
+ current_prefix = args.cp
265
+ if probability_map_path is not None:
266
+ probability_map = np.loadtxt(os.path.join(base, probability_map_path))
267
+ else:
268
+ probability_map = None
269
+ dsc = False
270
+ ahd = False
271
+ whd = False
272
+ for i in range(len(validation_type)):
273
+ if validation_type[i] == 'dsc':
274
+ dsc = True
275
+ elif validation_type[i] == 'ahd':
276
+ ahd = True
277
+ elif validation_type[i] == 'whd':
278
+ whd = True
279
+ else:
280
+ print('wrong validation type, please choose correct one !!!')
281
+ return
282
+
283
+ filepath = os.path.join(base, save_path, 'output_' + filename + '.csv')
284
+ save_dir = os.path.join(base, save_path)
285
+ gt_output_path = checkSegFormat(base, gt_path, 'gt', current_prefix)
286
+ pred_output_path = checkSegFormat(base, pred_path, 'pred', current_prefix)
287
+ try:
288
+ os.mkdir(save_dir)
289
+ except:
290
+ print(f'{save_dir} already exists')
291
+
292
+ try:
293
+ os.mknod(filepath)
294
+ except:
295
+ print(f'{filepath} already exists')
296
+
297
+ DSC = pd.DataFrame()
298
+ file = glob.glob(os.path.join(base, gt_output_path) + '/*nii.gz')[0]
299
+ seg_file = ants.image_read(file)
300
+ num_class = np.unique(seg_file.numpy().ravel()).shape[0]
301
+ average_DSC = np.zeros((num_class-1))
302
+ average_HD = np.zeros((num_class-1))
303
+ k = 0
304
+ for i in glob.glob(os.path.join(base, pred_output_path) + '/*nii.gz'):
305
+ k += 1
306
+ pred_img = ants.image_read(i)
307
+ pred_spacing = list(pred_img.spacing)
308
+ if reg and seg_type == 'ET':
309
+ file_name = os.path.basename(i).split('.')[0].split('_')[4] + '_' + os.path.basename(
310
+ i).split('.')[0].split('_')[5] + '_' + os.path.basename(i).split('.')[0].split('_')[6]
311
+ file_name1 = os.path.basename(i).split('.')[0]
312
+ elif reg and seg_type == 'NC':
313
+ file_name = os.path.basename(i).split(
314
+ '.')[0].split('_')[3] + '_' + os.path.basename(i).split('.')[0].split('_')[4]
315
+ file_name1 = os.path.basename(i).split('.')[0]
316
+ elif reg and seg_type == 'HT':
317
+ file_name = os.path.basename(i).split('.')[0].split('_')[2]
318
+ file_name1 = os.path.basename(i).split('.')[0]
319
+ else:
320
+ file_name = os.path.basename(i).split('.')[0]
321
+ file_name1 = os.path.basename(i).split('.')[0]
322
+ gt_seg = os.path.join(base, gt_output_path, file_name + '.nii.gz')
323
+ gt_img = ants.image_read(gt_seg)
324
+ gt_spacing = list(gt_img.spacing)
325
+
326
+ if gt_spacing != pred_spacing:
327
+ print(
328
+ "Spacing of prediction and ground_truth is not matched, please check again !!!")
329
+ return
330
+
331
+ ref = pred_img
332
+ data_ref = ref.numpy()
333
+
334
+ pred = gt_img
335
+ data_pred = pred.numpy()
336
+
337
+ num_class = len(np.unique(data_pred))
338
+ ds, aver_DSC, aver_HD = dice_coefficient_and_hausdorff_distance(
339
+ file_name1, data_ref, data_pred, num_class, pred_spacing, probability_map, dsc, ahd, whd, average_DSC, average_HD)
340
+ DSC = pd.concat([DSC, ds])
341
+ average_DSC = aver_DSC
342
+ average_HD = aver_HD
343
+
344
+ avg_DSC = average_DSC / k
345
+ avg_HD = average_HD / k
346
+ print(avg_DSC)
347
+ with open(os.path.join(base, save_path, "metric.txt"), 'w') as f:
348
+ f.write("Label Value Label Name Average Dice Score Average Mean HD\n")
349
+ for i in range(len(avg_DSC)):
350
+ f.write(f'{str(i+1):^12}{str(label_list[2*i+1]):^12}{str(avg_DSC[i]):^20}{str(avg_HD[i]):^18}\n')
351
+ DSC.to_csv(filepath)
352
+
353
+
354
+ if __name__ == '__main__':
355
+ main()
metrics/surface_distance.py ADDED
@@ -0,0 +1,424 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2018 Google Inc. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS-IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language
13
+ from __future__ import absolute_import
14
+ from __future__ import division
15
+ from __future__ import print_function
16
+
17
+ import lookup_tables # pylint: disable=relative-beyond-top-level
18
+ import numpy as np
19
+ from scipy import ndimage
20
+
21
+ """
22
+
23
+ surface_distance.py
24
+
25
+ all of the surface_distance functions are borrowed from DeepMind surface_distance repository
26
+
27
+ """
28
+ def _assert_is_numpy_array(name, array):
29
+ """Raises an exception if `array` is not a numpy array."""
30
+ if not isinstance(array, np.ndarray):
31
+ raise ValueError("The argument {!r} should be a numpy array, not a "
32
+ "{}".format(name, type(array)))
33
+
34
+
35
+ def _check_nd_numpy_array(name, array, num_dims):
36
+ """Raises an exception if `array` is not a `num_dims`-D numpy array."""
37
+ if len(array.shape) != num_dims:
38
+ raise ValueError("The argument {!r} should be a {}D array, not of "
39
+ "shape {}".format(name, num_dims, array.shape))
40
+
41
+
42
+ def _check_2d_numpy_array(name, array):
43
+ _check_nd_numpy_array(name, array, num_dims=2)
44
+
45
+
46
+ def _check_3d_numpy_array(name, array):
47
+ _check_nd_numpy_array(name, array, num_dims=3)
48
+
49
+
50
+ def _assert_is_bool_numpy_array(name, array):
51
+ _assert_is_numpy_array(name, array)
52
+ if array.dtype != np.bool:
53
+ raise ValueError("The argument {!r} should be a numpy array of type bool, "
54
+ "not {}".format(name, array.dtype))
55
+
56
+
57
+ def _compute_bounding_box(mask):
58
+ """Computes the bounding box of the masks.
59
+ This function generalizes to arbitrary number of dimensions great or equal
60
+ to 1.
61
+ Args:
62
+ mask: The 2D or 3D numpy mask, where '0' means background and non-zero means
63
+ foreground.
64
+ Returns:
65
+ A tuple:
66
+ - The coordinates of the first point of the bounding box (smallest on all
67
+ axes), or `None` if the mask contains only zeros.
68
+ - The coordinates of the second point of the bounding box (greatest on all
69
+ axes), or `None` if the mask contains only zeros.
70
+ """
71
+ num_dims = len(mask.shape)
72
+ bbox_min = np.zeros(num_dims, np.int64)
73
+ bbox_max = np.zeros(num_dims, np.int64)
74
+
75
+ # max projection to the x0-axis
76
+ proj_0 = np.amax(mask, axis=tuple(range(num_dims))[1:])
77
+ idx_nonzero_0 = np.nonzero(proj_0)[0]
78
+ if len(idx_nonzero_0) == 0: # pylint: disable=g-explicit-length-test
79
+ return None, None
80
+
81
+ bbox_min[0] = np.min(idx_nonzero_0)
82
+ bbox_max[0] = np.max(idx_nonzero_0)
83
+
84
+ # max projection to the i-th-axis for i in {1, ..., num_dims - 1}
85
+ for axis in range(1, num_dims):
86
+ max_over_axes = list(range(num_dims)) # Python 3 compatible
87
+ max_over_axes.pop(axis) # Remove the i-th dimension from the max
88
+ max_over_axes = tuple(max_over_axes) # numpy expects a tuple of ints
89
+ proj = np.amax(mask, axis=max_over_axes)
90
+ idx_nonzero = np.nonzero(proj)[0]
91
+ bbox_min[axis] = np.min(idx_nonzero)
92
+ bbox_max[axis] = np.max(idx_nonzero)
93
+
94
+ return bbox_min, bbox_max
95
+
96
+
97
+ def _crop_to_bounding_box(mask, bbox_min, bbox_max):
98
+ """Crops a 2D or 3D mask to the bounding box specified by `bbox_{min,max}`."""
99
+ # we need to zeropad the cropped region with 1 voxel at the lower,
100
+ # the right (and the back on 3D) sides. This is required to obtain the
101
+ # "full" convolution result with the 2x2 (or 2x2x2 in 3D) kernel.
102
+ # TODO: This is correct only if the object is interior to the
103
+ # bounding box.
104
+ cropmask = np.zeros((bbox_max - bbox_min) + 2, np.uint8)
105
+
106
+ num_dims = len(mask.shape)
107
+ # pyformat: disable
108
+ if num_dims == 2:
109
+ cropmask[0:-1, 0:-1] = mask[bbox_min[0]:bbox_max[0] + 1,
110
+ bbox_min[1]:bbox_max[1] + 1]
111
+ elif num_dims == 3:
112
+ cropmask[0:-1, 0:-1, 0:-1] = mask[bbox_min[0]:bbox_max[0] + 1,
113
+ bbox_min[1]:bbox_max[1] + 1,
114
+ bbox_min[2]:bbox_max[2] + 1]
115
+ # pyformat: enable
116
+ else:
117
+ assert False
118
+
119
+ return cropmask
120
+
121
+
122
+ def _sort_distances_surfels(distances, surfel_areas):
123
+ """Sorts the two list with respect to the tuple of (distance, surfel_area).
124
+ Args:
125
+ distances: The distances from A to B (e.g. `distances_gt_to_pred`).
126
+ surfel_areas: The surfel areas for A (e.g. `surfel_areas_gt`).
127
+ Returns:
128
+ A tuple of the sorted (distances, surfel_areas).
129
+ """
130
+ sorted_surfels = np.array(sorted(zip(distances, surfel_areas)))
131
+ return sorted_surfels[:, 0], sorted_surfels[:, 1]
132
+
133
+
134
+ def compute_surface_distances(mask_gt,
135
+ mask_pred,
136
+ spacing_mm):
137
+ """Computes closest distances from all surface points to the other surface.
138
+ This function can be applied to 2D or 3D tensors. For 2D, both masks must be
139
+ 2D and `spacing_mm` must be a 2-element list. For 3D, both masks must be 3D
140
+ and `spacing_mm` must be a 3-element list. The description is done for the 2D
141
+ case, and the formulation for the 3D case is present is parenthesis,
142
+ introduced by "resp.".
143
+ Finds all contour elements (resp surface elements "surfels" in 3D) in the
144
+ ground truth mask `mask_gt` and the predicted mask `mask_pred`, computes their
145
+ length in mm (resp. area in mm^2) and the distance to the closest point on the
146
+ other contour (resp. surface). It returns two sorted lists of distances
147
+ together with the corresponding contour lengths (resp. surfel areas). If one
148
+ of the masks is empty, the corresponding lists are empty and all distances in
149
+ the other list are `inf`.
150
+ Args:
151
+ mask_gt: 2-dim (resp. 3-dim) bool Numpy array. The ground truth mask.
152
+ mask_pred: 2-dim (resp. 3-dim) bool Numpy array. The predicted mask.
153
+ spacing_mm: 2-element (resp. 3-element) list-like structure. Voxel spacing
154
+ in x0 anx x1 (resp. x0, x1 and x2) directions.
155
+ Returns:
156
+ A dict with:
157
+ "distances_gt_to_pred": 1-dim numpy array of type float. The distances in mm
158
+ from all ground truth surface elements to the predicted surface,
159
+ sorted from smallest to largest.
160
+ "distances_pred_to_gt": 1-dim numpy array of type float. The distances in mm
161
+ from all predicted surface elements to the ground truth surface,
162
+ sorted from smallest to largest.
163
+ "surfel_areas_gt": 1-dim numpy array of type float. The length of the
164
+ of the ground truth contours in mm (resp. the surface elements area in
165
+ mm^2) in the same order as distances_gt_to_pred.
166
+ "surfel_areas_pred": 1-dim numpy array of type float. The length of the
167
+ of the predicted contours in mm (resp. the surface elements area in
168
+ mm^2) in the same order as distances_gt_to_pred.
169
+ Raises:
170
+ ValueError: If the masks and the `spacing_mm` arguments are of incompatible
171
+ shape or type. Or if the masks are not 2D or 3D.
172
+ """
173
+ # The terms used in this function are for the 3D case. In particular, surface
174
+ # in 2D stands for contours in 3D. The surface elements in 3D correspond to
175
+ # the line elements in 2D.
176
+
177
+ _assert_is_bool_numpy_array("mask_gt", mask_gt)
178
+ _assert_is_bool_numpy_array("mask_pred", mask_pred)
179
+
180
+ if not len(mask_gt.shape) == len(mask_pred.shape) == len(spacing_mm):
181
+ raise ValueError("The arguments must be of compatible shape. Got mask_gt "
182
+ "with {} dimensions ({}) and mask_pred with {} dimensions "
183
+ "({}), while the spacing_mm was {} elements.".format(
184
+ len(mask_gt.shape),
185
+ mask_gt.shape, len(
186
+ mask_pred.shape), mask_pred.shape,
187
+ len(spacing_mm)))
188
+
189
+ num_dims = len(spacing_mm)
190
+ if num_dims == 2:
191
+ _check_2d_numpy_array("mask_gt", mask_gt)
192
+ _check_2d_numpy_array("mask_pred", mask_pred)
193
+
194
+ # compute the area for all 16 possible surface elements
195
+ # (given a 2x2 neighbourhood) according to the spacing_mm
196
+ neighbour_code_to_surface_area = (
197
+ lookup_tables.create_table_neighbour_code_to_contour_length(spacing_mm))
198
+ kernel = lookup_tables.ENCODE_NEIGHBOURHOOD_2D_KERNEL
199
+ full_true_neighbours = 0b1111
200
+ elif num_dims == 3:
201
+ _check_3d_numpy_array("mask_gt", mask_gt)
202
+ _check_3d_numpy_array("mask_pred", mask_pred)
203
+
204
+ # compute the area for all 256 possible surface elements
205
+ # (given a 2x2x2 neighbourhood) according to the spacing_mm
206
+ neighbour_code_to_surface_area = (
207
+ lookup_tables.create_table_neighbour_code_to_surface_area(spacing_mm))
208
+ kernel = lookup_tables.ENCODE_NEIGHBOURHOOD_3D_KERNEL
209
+ full_true_neighbours = 0b11111111
210
+ else:
211
+ raise ValueError("Only 2D and 3D masks are supported, not "
212
+ "{}D.".format(num_dims))
213
+
214
+ # compute the bounding box of the masks to trim the volume to the smallest
215
+ # possible processing subvolume
216
+ bbox_min, bbox_max = _compute_bounding_box(mask_gt | mask_pred)
217
+ # Both the min/max bbox are None at the same time, so we only check one.
218
+ if bbox_min is None:
219
+ return {
220
+ "distances_gt_to_pred": np.array([]),
221
+ "distances_pred_to_gt": np.array([]),
222
+ "surfel_areas_gt": np.array([]),
223
+ "surfel_areas_pred": np.array([]),
224
+ }
225
+
226
+ # crop the processing subvolume.
227
+ cropmask_gt = _crop_to_bounding_box(mask_gt, bbox_min, bbox_max)
228
+ cropmask_pred = _crop_to_bounding_box(mask_pred, bbox_min, bbox_max)
229
+
230
+ # compute the neighbour code (local binary pattern) for each voxel
231
+ # the resulting arrays are spacially shifted by minus half a voxel in each
232
+ # axis.
233
+ # i.e. the points are located at the corners of the original voxels
234
+ neighbour_code_map_gt = ndimage.filters.correlate(
235
+ cropmask_gt.astype(np.uint8), kernel, mode="constant", cval=0)
236
+ neighbour_code_map_pred = ndimage.filters.correlate(
237
+ cropmask_pred.astype(np.uint8), kernel, mode="constant", cval=0)
238
+
239
+ # create masks with the surface voxels
240
+ borders_gt = ((neighbour_code_map_gt != 0) &
241
+ (neighbour_code_map_gt != full_true_neighbours))
242
+ borders_pred = ((neighbour_code_map_pred != 0) &
243
+ (neighbour_code_map_pred != full_true_neighbours))
244
+
245
+ # compute the distance transform (closest distance of each voxel to the
246
+ # surface voxels)
247
+ if borders_gt.any():
248
+ distmap_gt = ndimage.morphology.distance_transform_edt(
249
+ ~borders_gt, sampling=spacing_mm)
250
+ else:
251
+ distmap_gt = np.Inf * np.ones(borders_gt.shape)
252
+
253
+ if borders_pred.any():
254
+ distmap_pred = ndimage.morphology.distance_transform_edt(
255
+ ~borders_pred, sampling=spacing_mm)
256
+ else:
257
+ distmap_pred = np.Inf * np.ones(borders_pred.shape)
258
+
259
+ # compute the area of each surface element
260
+ surface_area_map_gt = neighbour_code_to_surface_area[neighbour_code_map_gt]
261
+ surface_area_map_pred = neighbour_code_to_surface_area[
262
+ neighbour_code_map_pred]
263
+
264
+ # create a list of all surface elements with distance and area
265
+ distances_gt_to_pred = distmap_pred[borders_gt]
266
+ distances_pred_to_gt = distmap_gt[borders_pred]
267
+ surfel_areas_gt = surface_area_map_gt[borders_gt]
268
+ surfel_areas_pred = surface_area_map_pred[borders_pred]
269
+
270
+ # sort them by distance
271
+ if distances_gt_to_pred.shape != (0,):
272
+ distances_gt_to_pred, surfel_areas_gt = _sort_distances_surfels(
273
+ distances_gt_to_pred, surfel_areas_gt)
274
+
275
+ if distances_pred_to_gt.shape != (0,):
276
+ distances_pred_to_gt, surfel_areas_pred = _sort_distances_surfels(
277
+ distances_pred_to_gt, surfel_areas_pred)
278
+
279
+ return {
280
+ "distances_gt_to_pred": distances_gt_to_pred,
281
+ "distances_pred_to_gt": distances_pred_to_gt,
282
+ "surfel_areas_gt": surfel_areas_gt,
283
+ "surfel_areas_pred": surfel_areas_pred,
284
+ }
285
+
286
+
287
+ def compute_average_surface_distance(surface_distances):
288
+ """Returns the average surface distance.
289
+ Computes the average surface distances by correctly taking the area of each
290
+ surface element into account. Call compute_surface_distances(...) before, to
291
+ obtain the `surface_distances` dict.
292
+ Args:
293
+ surface_distances: dict with "distances_gt_to_pred", "distances_pred_to_gt"
294
+ "surfel_areas_gt", "surfel_areas_pred" created by
295
+ compute_surface_distances()
296
+ Returns:
297
+ A tuple with two float values:
298
+ - the average distance (in mm) from the ground truth surface to the
299
+ predicted surface
300
+ - the average distance from the predicted surface to the ground truth
301
+ surface.
302
+ """
303
+ distances_gt_to_pred = surface_distances["distances_gt_to_pred"]
304
+ distances_pred_to_gt = surface_distances["distances_pred_to_gt"]
305
+ surfel_areas_gt = surface_distances["surfel_areas_gt"]
306
+ surfel_areas_pred = surface_distances["surfel_areas_pred"]
307
+ average_distance_gt_to_pred = (
308
+ np.sum(distances_gt_to_pred * surfel_areas_gt) / np.sum(surfel_areas_gt))
309
+ average_distance_pred_to_gt = (
310
+ np.sum(distances_pred_to_gt * surfel_areas_pred) /
311
+ np.sum(surfel_areas_pred))
312
+ return (average_distance_gt_to_pred, average_distance_pred_to_gt)
313
+
314
+
315
+ def compute_robust_hausdorff(surface_distances, percent):
316
+ """Computes the robust Hausdorff distance.
317
+ Computes the robust Hausdorff distance. "Robust", because it uses the
318
+ `percent` percentile of the distances instead of the maximum distance. The
319
+ percentage is computed by correctly taking the area of each surface element
320
+ into account.
321
+ Args:
322
+ surface_distances: dict with "distances_gt_to_pred", "distances_pred_to_gt"
323
+ "surfel_areas_gt", "surfel_areas_pred" created by
324
+ compute_surface_distances()
325
+ percent: a float value between 0 and 100.
326
+ Returns:
327
+ a float value. The robust Hausdorff distance in mm.
328
+ """
329
+ distances_gt_to_pred = surface_distances["distances_gt_to_pred"]
330
+ distances_pred_to_gt = surface_distances["distances_pred_to_gt"]
331
+ surfel_areas_gt = surface_distances["surfel_areas_gt"]
332
+ surfel_areas_pred = surface_distances["surfel_areas_pred"]
333
+ if len(distances_gt_to_pred) > 0: # pylint: disable=g-explicit-length-test
334
+ surfel_areas_cum_gt = np.cumsum(
335
+ surfel_areas_gt) / np.sum(surfel_areas_gt)
336
+ idx = np.searchsorted(surfel_areas_cum_gt, percent/100.0)
337
+ perc_distance_gt_to_pred = distances_gt_to_pred[
338
+ min(idx, len(distances_gt_to_pred)-1)]
339
+ else:
340
+ perc_distance_gt_to_pred = np.Inf
341
+
342
+ if len(distances_pred_to_gt) > 0: # pylint: disable=g-explicit-length-test
343
+ surfel_areas_cum_pred = (np.cumsum(surfel_areas_pred) /
344
+ np.sum(surfel_areas_pred))
345
+ idx = np.searchsorted(surfel_areas_cum_pred, percent/100.0)
346
+ perc_distance_pred_to_gt = distances_pred_to_gt[
347
+ min(idx, len(distances_pred_to_gt)-1)]
348
+ else:
349
+ perc_distance_pred_to_gt = np.Inf
350
+
351
+ return max(perc_distance_gt_to_pred, perc_distance_pred_to_gt)
352
+
353
+
354
+ def compute_surface_overlap_at_tolerance(surface_distances, tolerance_mm):
355
+ """Computes the overlap of the surfaces at a specified tolerance.
356
+ Computes the overlap of the ground truth surface with the predicted surface
357
+ and vice versa allowing a specified tolerance (maximum surface-to-surface
358
+ distance that is regarded as overlapping). The overlapping fraction is
359
+ computed by correctly taking the area of each surface element into account.
360
+ Args:
361
+ surface_distances: dict with "distances_gt_to_pred", "distances_pred_to_gt"
362
+ "surfel_areas_gt", "surfel_areas_pred" created by
363
+ compute_surface_distances()
364
+ tolerance_mm: a float value. The tolerance in mm
365
+ Returns:
366
+ A tuple of two float values. The overlap fraction in [0.0, 1.0] of the
367
+ ground truth surface with the predicted surface and vice versa.
368
+ """
369
+ distances_gt_to_pred = surface_distances["distances_gt_to_pred"]
370
+ distances_pred_to_gt = surface_distances["distances_pred_to_gt"]
371
+ surfel_areas_gt = surface_distances["surfel_areas_gt"]
372
+ surfel_areas_pred = surface_distances["surfel_areas_pred"]
373
+ rel_overlap_gt = (
374
+ np.sum(surfel_areas_gt[distances_gt_to_pred <= tolerance_mm]) /
375
+ np.sum(surfel_areas_gt))
376
+ rel_overlap_pred = (
377
+ np.sum(surfel_areas_pred[distances_pred_to_gt <= tolerance_mm]) /
378
+ np.sum(surfel_areas_pred))
379
+ return (rel_overlap_gt, rel_overlap_pred)
380
+
381
+
382
+ def compute_surface_dice_at_tolerance(surface_distances, tolerance_mm):
383
+ """Computes the _surface_ DICE coefficient at a specified tolerance.
384
+ Computes the _surface_ DICE coefficient at a specified tolerance. Not to be
385
+ confused with the standard _volumetric_ DICE coefficient. The surface DICE
386
+ measures the overlap of two surfaces instead of two volumes. A surface
387
+ element is counted as overlapping (or touching), when the closest distance to
388
+ the other surface is less or equal to the specified tolerance. The DICE
389
+ coefficient is in the range between 0.0 (no overlap) to 1.0 (perfect overlap).
390
+ Args:
391
+ surface_distances: dict with "distances_gt_to_pred", "distances_pred_to_gt"
392
+ "surfel_areas_gt", "surfel_areas_pred" created by
393
+ compute_surface_distances()
394
+ tolerance_mm: a float value. The tolerance in mm
395
+ Returns:
396
+ A float value. The surface DICE coefficient in [0.0, 1.0].
397
+ """
398
+ distances_gt_to_pred = surface_distances["distances_gt_to_pred"]
399
+ distances_pred_to_gt = surface_distances["distances_pred_to_gt"]
400
+ surfel_areas_gt = surface_distances["surfel_areas_gt"]
401
+ surfel_areas_pred = surface_distances["surfel_areas_pred"]
402
+ overlap_gt = np.sum(surfel_areas_gt[distances_gt_to_pred <= tolerance_mm])
403
+ overlap_pred = np.sum(
404
+ surfel_areas_pred[distances_pred_to_gt <= tolerance_mm])
405
+ surface_dice = (overlap_gt + overlap_pred) / (
406
+ np.sum(surfel_areas_gt) + np.sum(surfel_areas_pred))
407
+ return surface_dice
408
+
409
+
410
+ def compute_dice_coefficient(mask_gt, mask_pred):
411
+ """Computes soerensen-dice coefficient.
412
+ compute the soerensen-dice coefficient between the ground truth mask `mask_gt`
413
+ and the predicted mask `mask_pred`.
414
+ Args:
415
+ mask_gt: 3-dim Numpy array of type bool. The ground truth mask.
416
+ mask_pred: 3-dim Numpy array of type bool. The predicted mask.
417
+ Returns:
418
+ the dice coeffcient as float. If both masks are empty, the result is NaN.
419
+ """
420
+ volume_sum = mask_gt.sum() + mask_pred.sum()
421
+ if volume_sum == 0:
422
+ return np.NaN
423
+ volume_intersect = (mask_gt & mask_pred).sum()
424
+ return 2*volume_intersect / volume_sum
nnunet/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from __future__ import absolute_import
2
+ print("\n\nPlease cite the following paper when using nnUNet:\n\nIsensee, F., Jaeger, P.F., Kohl, S.A.A. et al. "
3
+ "\"nnU-Net: a self-configuring method for deep learning-based biomedical image segmentation.\" "
4
+ "Nat Methods (2020). https://doi.org/10.1038/s41592-020-01008-z\n\n")
5
+ print("If you have questions or suggestions, feel free to open an issue at https://github.com/MIC-DKFZ/nnUNet\n")
6
+
7
+ from . import *
nnunet/configuration.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import os
2
+
3
+ default_num_threads = 8 if 'nnUNet_def_n_proc' not in os.environ else int(os.environ['nnUNet_def_n_proc'])
4
+ RESAMPLING_SEPARATE_Z_ANISO_THRESHOLD = 3 # determines what threshold to use for resampling the low resolution axis
5
+ # separately (with NN)
nnunet/dataset_conversion/Task017_BeyondCranialVaultAbdominalOrganSegmentation.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from collections import OrderedDict
17
+ from nnunet.paths import nnUNet_raw_data
18
+ from batchgenerators.utilities.file_and_folder_operations import *
19
+ import shutil
20
+
21
+
22
+ if __name__ == "__main__":
23
+ base = "/media/yunlu/10TB/research/other_data/Multi-Atlas Labeling Beyond the Cranial Vault/RawData/"
24
+
25
+ task_id = 17
26
+ task_name = "AbdominalOrganSegmentation"
27
+ prefix = 'ABD'
28
+
29
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
30
+
31
+ out_base = join(nnUNet_raw_data, foldername)
32
+ imagestr = join(out_base, "imagesTr")
33
+ imagests = join(out_base, "imagesTs")
34
+ labelstr = join(out_base, "labelsTr")
35
+ maybe_mkdir_p(imagestr)
36
+ maybe_mkdir_p(imagests)
37
+ maybe_mkdir_p(labelstr)
38
+
39
+ train_folder = join(base, "Training/img")
40
+ label_folder = join(base, "Training/label")
41
+ test_folder = join(base, "Test/img")
42
+ train_patient_names = []
43
+ test_patient_names = []
44
+ train_patients = subfiles(train_folder, join=False, suffix = 'nii.gz')
45
+ for p in train_patients:
46
+ serial_number = int(p[3:7])
47
+ train_patient_name = f'{prefix}_{serial_number:03d}.nii.gz'
48
+ label_file = join(label_folder, f'label{p[3:]}')
49
+ image_file = join(train_folder, p)
50
+ shutil.copy(image_file, join(imagestr, f'{train_patient_name[:7]}_0000.nii.gz'))
51
+ shutil.copy(label_file, join(labelstr, train_patient_name))
52
+ train_patient_names.append(train_patient_name)
53
+
54
+ test_patients = subfiles(test_folder, join=False, suffix=".nii.gz")
55
+ for p in test_patients:
56
+ p = p[:-7]
57
+ image_file = join(test_folder, p + ".nii.gz")
58
+ serial_number = int(p[3:7])
59
+ test_patient_name = f'{prefix}_{serial_number:03d}.nii.gz'
60
+ shutil.copy(image_file, join(imagests, f'{test_patient_name[:7]}_0000.nii.gz'))
61
+ test_patient_names.append(test_patient_name)
62
+
63
+ json_dict = OrderedDict()
64
+ json_dict['name'] = "AbdominalOrganSegmentation"
65
+ json_dict['description'] = "Multi-Atlas Labeling Beyond the Cranial Vault Abdominal Organ Segmentation"
66
+ json_dict['tensorImageSize'] = "3D"
67
+ json_dict['reference'] = "https://www.synapse.org/#!Synapse:syn3193805/wiki/217789"
68
+ json_dict['licence'] = "see challenge website"
69
+ json_dict['release'] = "0.0"
70
+ json_dict['modality'] = {
71
+ "0": "CT",
72
+ }
73
+ json_dict['labels'] = OrderedDict({
74
+ "00": "background",
75
+ "01": "spleen",
76
+ "02": "right kidney",
77
+ "03": "left kidney",
78
+ "04": "gallbladder",
79
+ "05": "esophagus",
80
+ "06": "liver",
81
+ "07": "stomach",
82
+ "08": "aorta",
83
+ "09": "inferior vena cava",
84
+ "10": "portal vein and splenic vein",
85
+ "11": "pancreas",
86
+ "12": "right adrenal gland",
87
+ "13": "left adrenal gland"}
88
+ )
89
+ json_dict['numTraining'] = len(train_patient_names)
90
+ json_dict['numTest'] = len(test_patient_names)
91
+ json_dict['training'] = [{'image': "./imagesTr/%s" % train_patient_name, "label": "./labelsTr/%s" % train_patient_name} for i, train_patient_name in enumerate(train_patient_names)]
92
+ json_dict['test'] = ["./imagesTs/%s" % test_patient_name for test_patient_name in test_patient_names]
93
+
94
+ save_json(json_dict, os.path.join(out_base, "dataset.json"))
nnunet/dataset_conversion/Task024_Promise2012.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from collections import OrderedDict
15
+ import SimpleITK as sitk
16
+ from batchgenerators.utilities.file_and_folder_operations import *
17
+
18
+
19
+ def export_for_submission(source_dir, target_dir):
20
+ """
21
+ promise wants mhd :-/
22
+ :param source_dir:
23
+ :param target_dir:
24
+ :return:
25
+ """
26
+ files = subfiles(source_dir, suffix=".nii.gz", join=False)
27
+ target_files = [join(target_dir, i[:-7] + ".mhd") for i in files]
28
+ maybe_mkdir_p(target_dir)
29
+ for f, t in zip(files, target_files):
30
+ img = sitk.ReadImage(join(source_dir, f))
31
+ sitk.WriteImage(img, t)
32
+
33
+
34
+ if __name__ == "__main__":
35
+ folder = "/media/fabian/My Book/datasets/promise2012"
36
+ out_folder = "/media/fabian/My Book/MedicalDecathlon/MedicalDecathlon_raw_splitted/Task024_Promise"
37
+
38
+ maybe_mkdir_p(join(out_folder, "imagesTr"))
39
+ maybe_mkdir_p(join(out_folder, "imagesTs"))
40
+ maybe_mkdir_p(join(out_folder, "labelsTr"))
41
+ # train
42
+ current_dir = join(folder, "train")
43
+ segmentations = subfiles(current_dir, suffix="segmentation.mhd")
44
+ raw_data = [i for i in subfiles(current_dir, suffix="mhd") if not i.endswith("segmentation.mhd")]
45
+ for i in raw_data:
46
+ out_fname = join(out_folder, "imagesTr", i.split("/")[-1][:-4] + "_0000.nii.gz")
47
+ sitk.WriteImage(sitk.ReadImage(i), out_fname)
48
+ for i in segmentations:
49
+ out_fname = join(out_folder, "labelsTr", i.split("/")[-1][:-17] + ".nii.gz")
50
+ sitk.WriteImage(sitk.ReadImage(i), out_fname)
51
+
52
+ # test
53
+ current_dir = join(folder, "test")
54
+ test_data = subfiles(current_dir, suffix="mhd")
55
+ for i in test_data:
56
+ out_fname = join(out_folder, "imagesTs", i.split("/")[-1][:-4] + "_0000.nii.gz")
57
+ sitk.WriteImage(sitk.ReadImage(i), out_fname)
58
+
59
+
60
+ json_dict = OrderedDict()
61
+ json_dict['name'] = "PROMISE12"
62
+ json_dict['description'] = "prostate"
63
+ json_dict['tensorImageSize'] = "4D"
64
+ json_dict['reference'] = "see challenge website"
65
+ json_dict['licence'] = "see challenge website"
66
+ json_dict['release'] = "0.0"
67
+ json_dict['modality'] = {
68
+ "0": "MRI",
69
+ }
70
+ json_dict['labels'] = {
71
+ "0": "background",
72
+ "1": "prostate"
73
+ }
74
+ json_dict['numTraining'] = len(raw_data)
75
+ json_dict['numTest'] = len(test_data)
76
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i.split("/")[-1][:-4], "label": "./labelsTr/%s.nii.gz" % i.split("/")[-1][:-4]} for i in
77
+ raw_data]
78
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i.split("/")[-1][:-4] for i in test_data]
79
+
80
+ save_json(json_dict, os.path.join(out_folder, "dataset.json"))
81
+
nnunet/dataset_conversion/Task027_AutomaticCardiacDetectionChallenge.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from collections import OrderedDict
16
+ from batchgenerators.utilities.file_and_folder_operations import *
17
+ import shutil
18
+ import numpy as np
19
+ from sklearn.model_selection import KFold
20
+
21
+
22
+ def convert_to_submission(source_dir, target_dir):
23
+ niftis = subfiles(source_dir, join=False, suffix=".nii.gz")
24
+ patientids = np.unique([i[:10] for i in niftis])
25
+ maybe_mkdir_p(target_dir)
26
+ for p in patientids:
27
+ files_of_that_patient = subfiles(source_dir, prefix=p, suffix=".nii.gz", join=False)
28
+ assert len(files_of_that_patient)
29
+ files_of_that_patient.sort()
30
+ # first is ED, second is ES
31
+ shutil.copy(join(source_dir, files_of_that_patient[0]), join(target_dir, p + "_ED.nii.gz"))
32
+ shutil.copy(join(source_dir, files_of_that_patient[1]), join(target_dir, p + "_ES.nii.gz"))
33
+
34
+
35
+ if __name__ == "__main__":
36
+ folder = "/media/fabian/My Book/datasets/ACDC/training"
37
+ folder_test = "/media/fabian/My Book/datasets/ACDC/testing/testing"
38
+ out_folder = "/media/fabian/My Book/MedicalDecathlon/MedicalDecathlon_raw_splitted/Task027_ACDC"
39
+
40
+ maybe_mkdir_p(join(out_folder, "imagesTr"))
41
+ maybe_mkdir_p(join(out_folder, "imagesTs"))
42
+ maybe_mkdir_p(join(out_folder, "labelsTr"))
43
+
44
+ # train
45
+ all_train_files = []
46
+ patient_dirs_train = subfolders(folder, prefix="patient")
47
+ for p in patient_dirs_train:
48
+ current_dir = p
49
+ data_files_train = [i for i in subfiles(current_dir, suffix=".nii.gz") if i.find("_gt") == -1 and i.find("_4d") == -1]
50
+ corresponding_seg_files = [i[:-7] + "_gt.nii.gz" for i in data_files_train]
51
+ for d, s in zip(data_files_train, corresponding_seg_files):
52
+ patient_identifier = d.split("/")[-1][:-7]
53
+ all_train_files.append(patient_identifier + "_0000.nii.gz")
54
+ shutil.copy(d, join(out_folder, "imagesTr", patient_identifier + "_0000.nii.gz"))
55
+ shutil.copy(s, join(out_folder, "labelsTr", patient_identifier + ".nii.gz"))
56
+
57
+ # test
58
+ all_test_files = []
59
+ patient_dirs_test = subfolders(folder_test, prefix="patient")
60
+ for p in patient_dirs_test:
61
+ current_dir = p
62
+ data_files_test = [i for i in subfiles(current_dir, suffix=".nii.gz") if i.find("_gt") == -1 and i.find("_4d") == -1]
63
+ for d in data_files_test:
64
+ patient_identifier = d.split("/")[-1][:-7]
65
+ all_test_files.append(patient_identifier + "_0000.nii.gz")
66
+ shutil.copy(d, join(out_folder, "imagesTs", patient_identifier + "_0000.nii.gz"))
67
+
68
+
69
+ json_dict = OrderedDict()
70
+ json_dict['name'] = "ACDC"
71
+ json_dict['description'] = "cardias cine MRI segmentation"
72
+ json_dict['tensorImageSize'] = "4D"
73
+ json_dict['reference'] = "see ACDC challenge"
74
+ json_dict['licence'] = "see ACDC challenge"
75
+ json_dict['release'] = "0.0"
76
+ json_dict['modality'] = {
77
+ "0": "MRI",
78
+ }
79
+ json_dict['labels'] = {
80
+ "0": "background",
81
+ "1": "RV",
82
+ "2": "MLV",
83
+ "3": "LVC"
84
+ }
85
+ json_dict['numTraining'] = len(all_train_files)
86
+ json_dict['numTest'] = len(all_test_files)
87
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i.split("/")[-1][:-12], "label": "./labelsTr/%s.nii.gz" % i.split("/")[-1][:-12]} for i in
88
+ all_train_files]
89
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i.split("/")[-1][:-12] for i in all_test_files]
90
+
91
+ save_json(json_dict, os.path.join(out_folder, "dataset.json"))
92
+
93
+ # create a dummy split (patients need to be separated)
94
+ splits = []
95
+ patients = np.unique([i[:10] for i in all_train_files])
96
+ patientids = [i[:-12] for i in all_train_files]
97
+
98
+ kf = KFold(5, True, 12345)
99
+ for tr, val in kf.split(patients):
100
+ splits.append(OrderedDict())
101
+ tr_patients = patients[tr]
102
+ splits[-1]['train'] = [i[:-12] for i in all_train_files if i[:10] in tr_patients]
103
+ val_patients = patients[val]
104
+ splits[-1]['val'] = [i[:-12] for i in all_train_files if i[:10] in val_patients]
105
+
106
+ save_pickle(splits, "/media/fabian/nnunet/Task027_ACDC/splits_final.pkl")
nnunet/dataset_conversion/Task029_LiverTumorSegmentationChallenge.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from collections import OrderedDict
16
+ import SimpleITK as sitk
17
+ from batchgenerators.utilities.file_and_folder_operations import *
18
+ from multiprocessing import Pool
19
+ import numpy as np
20
+ from nnunet.configuration import default_num_threads
21
+ from scipy.ndimage import label
22
+
23
+
24
+ def export_segmentations(indir, outdir):
25
+ niftis = subfiles(indir, suffix='nii.gz', join=False)
26
+ for n in niftis:
27
+ identifier = str(n.split("_")[-1][:-7])
28
+ outfname = join(outdir, "test-segmentation-%s.nii" % identifier)
29
+ img = sitk.ReadImage(join(indir, n))
30
+ sitk.WriteImage(img, outfname)
31
+
32
+
33
+ def export_segmentations_postprocess(indir, outdir):
34
+ maybe_mkdir_p(outdir)
35
+ niftis = subfiles(indir, suffix='nii.gz', join=False)
36
+ for n in niftis:
37
+ print("\n", n)
38
+ identifier = str(n.split("_")[-1][:-7])
39
+ outfname = join(outdir, "test-segmentation-%s.nii" % identifier)
40
+ img = sitk.ReadImage(join(indir, n))
41
+ img_npy = sitk.GetArrayFromImage(img)
42
+ lmap, num_objects = label((img_npy > 0).astype(int))
43
+ sizes = []
44
+ for o in range(1, num_objects + 1):
45
+ sizes.append((lmap == o).sum())
46
+ mx = np.argmax(sizes) + 1
47
+ print(sizes)
48
+ img_npy[lmap != mx] = 0
49
+ img_new = sitk.GetImageFromArray(img_npy)
50
+ img_new.CopyInformation(img)
51
+ sitk.WriteImage(img_new, outfname)
52
+
53
+
54
+ if __name__ == "__main__":
55
+ train_dir = "/media/fabian/DeepLearningData/tmp/LITS-Challenge-Train-Data"
56
+ test_dir = "/media/fabian/My Book/datasets/LiTS/test_data"
57
+
58
+
59
+ output_folder = "/media/fabian/My Book/MedicalDecathlon/MedicalDecathlon_raw_splitted/Task029_LITS"
60
+ img_dir = join(output_folder, "imagesTr")
61
+ lab_dir = join(output_folder, "labelsTr")
62
+ img_dir_te = join(output_folder, "imagesTs")
63
+ maybe_mkdir_p(img_dir)
64
+ maybe_mkdir_p(lab_dir)
65
+ maybe_mkdir_p(img_dir_te)
66
+
67
+
68
+ def load_save_train(args):
69
+ data_file, seg_file = args
70
+ pat_id = data_file.split("/")[-1]
71
+ pat_id = "train_" + pat_id.split("-")[-1][:-4]
72
+
73
+ img_itk = sitk.ReadImage(data_file)
74
+ sitk.WriteImage(img_itk, join(img_dir, pat_id + "_0000.nii.gz"))
75
+
76
+ img_itk = sitk.ReadImage(seg_file)
77
+ sitk.WriteImage(img_itk, join(lab_dir, pat_id + ".nii.gz"))
78
+ return pat_id
79
+
80
+ def load_save_test(args):
81
+ data_file = args
82
+ pat_id = data_file.split("/")[-1]
83
+ pat_id = "test_" + pat_id.split("-")[-1][:-4]
84
+
85
+ img_itk = sitk.ReadImage(data_file)
86
+ sitk.WriteImage(img_itk, join(img_dir_te, pat_id + "_0000.nii.gz"))
87
+ return pat_id
88
+
89
+ nii_files_tr_data = subfiles(train_dir, True, "volume", "nii", True)
90
+ nii_files_tr_seg = subfiles(train_dir, True, "segmen", "nii", True)
91
+
92
+ nii_files_ts = subfiles(test_dir, True, "test-volume", "nii", True)
93
+
94
+ p = Pool(default_num_threads)
95
+ train_ids = p.map(load_save_train, zip(nii_files_tr_data, nii_files_tr_seg))
96
+ test_ids = p.map(load_save_test, nii_files_ts)
97
+ p.close()
98
+ p.join()
99
+
100
+ json_dict = OrderedDict()
101
+ json_dict['name'] = "LITS"
102
+ json_dict['description'] = "LITS"
103
+ json_dict['tensorImageSize'] = "4D"
104
+ json_dict['reference'] = "see challenge website"
105
+ json_dict['licence'] = "see challenge website"
106
+ json_dict['release'] = "0.0"
107
+ json_dict['modality'] = {
108
+ "0": "CT"
109
+ }
110
+
111
+ json_dict['labels'] = {
112
+ "0": "background",
113
+ "1": "liver",
114
+ "2": "tumor"
115
+ }
116
+
117
+ json_dict['numTraining'] = len(train_ids)
118
+ json_dict['numTest'] = len(test_ids)
119
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i, "label": "./labelsTr/%s.nii.gz" % i} for i in train_ids]
120
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i for i in test_ids]
121
+
122
+ with open(os.path.join(output_folder, "dataset.json"), 'w') as f:
123
+ json.dump(json_dict, f, indent=4, sort_keys=True)
nnunet/dataset_conversion/Task032_BraTS_2018.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from multiprocessing.pool import Pool
15
+
16
+ import numpy as np
17
+ from collections import OrderedDict
18
+
19
+ from batchgenerators.utilities.file_and_folder_operations import *
20
+ from nnunet.dataset_conversion.Task043_BraTS_2019 import copy_BraTS_segmentation_and_convert_labels
21
+ from nnunet.paths import nnUNet_raw_data
22
+ import SimpleITK as sitk
23
+ import shutil
24
+
25
+
26
+ def convert_labels_back_to_BraTS(seg: np.ndarray):
27
+ new_seg = np.zeros_like(seg)
28
+ new_seg[seg == 1] = 2
29
+ new_seg[seg == 3] = 4
30
+ new_seg[seg == 2] = 1
31
+ return new_seg
32
+
33
+
34
+ def load_convert_save(filename, input_folder, output_folder):
35
+ a = sitk.ReadImage(join(input_folder, filename))
36
+ b = sitk.GetArrayFromImage(a)
37
+ c = convert_labels_back_to_BraTS(b)
38
+ d = sitk.GetImageFromArray(c)
39
+ d.CopyInformation(a)
40
+ sitk.WriteImage(d, join(output_folder, filename))
41
+
42
+
43
+ def convert_labels_back_to_BraTS_2018_2019_convention(input_folder: str, output_folder: str, num_processes: int = 12):
44
+ """
45
+ reads all prediction files (nifti) in the input folder, converts the labels back to BraTS convention and saves the
46
+ result in output_folder
47
+ :param input_folder:
48
+ :param output_folder:
49
+ :return:
50
+ """
51
+ maybe_mkdir_p(output_folder)
52
+ nii = subfiles(input_folder, suffix='.nii.gz', join=False)
53
+ p = Pool(num_processes)
54
+ p.starmap(load_convert_save, zip(nii, [input_folder] * len(nii), [output_folder] * len(nii)))
55
+ p.close()
56
+ p.join()
57
+
58
+
59
+ if __name__ == "__main__":
60
+ """
61
+ REMEMBER TO CONVERT LABELS BACK TO BRATS CONVENTION AFTER PREDICTION!
62
+ """
63
+
64
+ task_name = "Task032_BraTS2018"
65
+ downloaded_data_dir = "/home/fabian/Downloads/BraTS2018_train_val_test_data/MICCAI_BraTS_2018_Data_Training"
66
+
67
+ target_base = join(nnUNet_raw_data, task_name)
68
+ target_imagesTr = join(target_base, "imagesTr")
69
+ target_imagesVal = join(target_base, "imagesVal")
70
+ target_imagesTs = join(target_base, "imagesTs")
71
+ target_labelsTr = join(target_base, "labelsTr")
72
+
73
+ maybe_mkdir_p(target_imagesTr)
74
+ maybe_mkdir_p(target_imagesVal)
75
+ maybe_mkdir_p(target_imagesTs)
76
+ maybe_mkdir_p(target_labelsTr)
77
+
78
+ patient_names = []
79
+ for tpe in ["HGG", "LGG"]:
80
+ cur = join(downloaded_data_dir, tpe)
81
+ for p in subdirs(cur, join=False):
82
+ patdir = join(cur, p)
83
+ patient_name = tpe + "__" + p
84
+ patient_names.append(patient_name)
85
+ t1 = join(patdir, p + "_t1.nii.gz")
86
+ t1c = join(patdir, p + "_t1ce.nii.gz")
87
+ t2 = join(patdir, p + "_t2.nii.gz")
88
+ flair = join(patdir, p + "_flair.nii.gz")
89
+ seg = join(patdir, p + "_seg.nii.gz")
90
+
91
+ assert all([
92
+ isfile(t1),
93
+ isfile(t1c),
94
+ isfile(t2),
95
+ isfile(flair),
96
+ isfile(seg)
97
+ ]), "%s" % patient_name
98
+
99
+ shutil.copy(t1, join(target_imagesTr, patient_name + "_0000.nii.gz"))
100
+ shutil.copy(t1c, join(target_imagesTr, patient_name + "_0001.nii.gz"))
101
+ shutil.copy(t2, join(target_imagesTr, patient_name + "_0002.nii.gz"))
102
+ shutil.copy(flair, join(target_imagesTr, patient_name + "_0003.nii.gz"))
103
+
104
+ copy_BraTS_segmentation_and_convert_labels(seg, join(target_labelsTr, patient_name + ".nii.gz"))
105
+
106
+ json_dict = OrderedDict()
107
+ json_dict['name'] = "BraTS2018"
108
+ json_dict['description'] = "nothing"
109
+ json_dict['tensorImageSize'] = "4D"
110
+ json_dict['reference'] = "see BraTS2018"
111
+ json_dict['licence'] = "see BraTS2019 license"
112
+ json_dict['release'] = "0.0"
113
+ json_dict['modality'] = {
114
+ "0": "T1",
115
+ "1": "T1ce",
116
+ "2": "T2",
117
+ "3": "FLAIR"
118
+ }
119
+ json_dict['labels'] = {
120
+ "0": "background",
121
+ "1": "edema",
122
+ "2": "non-enhancing",
123
+ "3": "enhancing",
124
+ }
125
+ json_dict['numTraining'] = len(patient_names)
126
+ json_dict['numTest'] = 0
127
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i, "label": "./labelsTr/%s.nii.gz" % i} for i in
128
+ patient_names]
129
+ json_dict['test'] = []
130
+
131
+ save_json(json_dict, join(target_base, "dataset.json"))
132
+
133
+ del tpe, cur
134
+ downloaded_data_dir = "/home/fabian/Downloads/BraTS2018_train_val_test_data/MICCAI_BraTS_2018_Data_Validation"
135
+
136
+ for p in subdirs(downloaded_data_dir, join=False):
137
+ patdir = join(downloaded_data_dir, p)
138
+ patient_name = p
139
+ t1 = join(patdir, p + "_t1.nii.gz")
140
+ t1c = join(patdir, p + "_t1ce.nii.gz")
141
+ t2 = join(patdir, p + "_t2.nii.gz")
142
+ flair = join(patdir, p + "_flair.nii.gz")
143
+
144
+ assert all([
145
+ isfile(t1),
146
+ isfile(t1c),
147
+ isfile(t2),
148
+ isfile(flair),
149
+ ]), "%s" % patient_name
150
+
151
+ shutil.copy(t1, join(target_imagesVal, patient_name + "_0000.nii.gz"))
152
+ shutil.copy(t1c, join(target_imagesVal, patient_name + "_0001.nii.gz"))
153
+ shutil.copy(t2, join(target_imagesVal, patient_name + "_0002.nii.gz"))
154
+ shutil.copy(flair, join(target_imagesVal, patient_name + "_0003.nii.gz"))
155
+
156
+ downloaded_data_dir = "/home/fabian/Downloads/BraTS2018_train_val_test_data/MICCAI_BraTS_2018_Data_Testing_FIsensee"
157
+
158
+ for p in subdirs(downloaded_data_dir, join=False):
159
+ patdir = join(downloaded_data_dir, p)
160
+ patient_name = p
161
+ t1 = join(patdir, p + "_t1.nii.gz")
162
+ t1c = join(patdir, p + "_t1ce.nii.gz")
163
+ t2 = join(patdir, p + "_t2.nii.gz")
164
+ flair = join(patdir, p + "_flair.nii.gz")
165
+
166
+ assert all([
167
+ isfile(t1),
168
+ isfile(t1c),
169
+ isfile(t2),
170
+ isfile(flair),
171
+ ]), "%s" % patient_name
172
+
173
+ shutil.copy(t1, join(target_imagesTs, patient_name + "_0000.nii.gz"))
174
+ shutil.copy(t1c, join(target_imagesTs, patient_name + "_0001.nii.gz"))
175
+ shutil.copy(t2, join(target_imagesTs, patient_name + "_0002.nii.gz"))
176
+ shutil.copy(flair, join(target_imagesTs, patient_name + "_0003.nii.gz"))
nnunet/dataset_conversion/Task035_ISBI_MSLesionSegmentationChallenge.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import shutil
16
+ from collections import OrderedDict
17
+ import numpy as np
18
+ import SimpleITK as sitk
19
+ import multiprocessing
20
+ from batchgenerators.utilities.file_and_folder_operations import *
21
+
22
+
23
+ def convert_to_nii_gz(filename):
24
+ f = sitk.ReadImage(filename)
25
+ sitk.WriteImage(f, os.path.splitext(filename)[0] + ".nii.gz")
26
+ os.remove(filename)
27
+
28
+
29
+ def convert_for_submission(source_dir, target_dir):
30
+ files = subfiles(source_dir, suffix=".nii.gz", join=False)
31
+ maybe_mkdir_p(target_dir)
32
+ for f in files:
33
+ splitted = f.split("__")
34
+ case_id = int(splitted[1])
35
+ timestep = int(splitted[2][:-7])
36
+ t = join(target_dir, "test%02d_%02d_nnUNet.nii" % (case_id, timestep))
37
+ img = sitk.ReadImage(join(source_dir, f))
38
+ sitk.WriteImage(img, t)
39
+
40
+
41
+ if __name__ == "__main__":
42
+ # convert to nifti.gz
43
+ dirs = ['/media/fabian/My Book/MedicalDecathlon/Task035_ISBILesionSegmentation/imagesTr',
44
+ '/media/fabian/My Book/MedicalDecathlon/Task035_ISBILesionSegmentation/imagesTs',
45
+ '/media/fabian/My Book/MedicalDecathlon/Task035_ISBILesionSegmentation/labelsTr']
46
+
47
+ p = multiprocessing.Pool(3)
48
+
49
+ for d in dirs:
50
+ nii_files = subfiles(d, suffix='.nii')
51
+ p.map(convert_to_nii_gz, nii_files)
52
+
53
+ p.close()
54
+ p.join()
55
+
56
+
57
+ def rename_files(folder):
58
+ all_files = subfiles(folder, join=False)
59
+ # there are max 14 patients per folder, starting with 1
60
+ for patientid in range(1, 15):
61
+ # there are certainly no more than 10 time steps per patient, starting with 1
62
+ for t in range(1, 10):
63
+ patient_files = [i for i in all_files if i.find("%02.0d_%02.0d_" % (patientid, t)) != -1]
64
+ if not len(patient_files) == 4:
65
+ continue
66
+
67
+ flair_file = [i for i in patient_files if i.endswith("_flair_pp.nii.gz")][0]
68
+ mprage_file = [i for i in patient_files if i.endswith("_mprage_pp.nii.gz")][0]
69
+ pd_file = [i for i in patient_files if i.endswith("_pd_pp.nii.gz")][0]
70
+ t2_file = [i for i in patient_files if i.endswith("_t2_pp.nii.gz")][0]
71
+
72
+ os.rename(join(folder, flair_file), join(folder, "case__%02.0d__%02.0d_0000.nii.gz" % (patientid, t)))
73
+ os.rename(join(folder, mprage_file), join(folder, "case__%02.0d__%02.0d_0001.nii.gz" % (patientid, t)))
74
+ os.rename(join(folder, pd_file), join(folder, "case__%02.0d__%02.0d_0002.nii.gz" % (patientid, t)))
75
+ os.rename(join(folder, t2_file), join(folder, "case__%02.0d__%02.0d_0003.nii.gz" % (patientid, t)))
76
+
77
+
78
+ for d in dirs[:-1]:
79
+ rename_files(d)
80
+
81
+
82
+ # now we have to deal with the training masks, we do it the quick and dirty way here by just creating copies of the
83
+ # training data
84
+
85
+ train_folder = '/media/fabian/My Book/MedicalDecathlon/Task035_ISBILesionSegmentation/imagesTr'
86
+
87
+ for patientid in range(1, 6):
88
+ for t in range(1, 6):
89
+ fnames_original = subfiles(train_folder, prefix="case__%02.0d__%02.0d" % (patientid, t), suffix=".nii.gz", sort=True)
90
+ for f in fnames_original:
91
+ for mask in [1, 2]:
92
+ fname_target = f[:-12] + "__mask%d" % mask + f[-12:]
93
+ shutil.copy(f, fname_target)
94
+ os.remove(f)
95
+
96
+
97
+ labels_folder = '/media/fabian/My Book/MedicalDecathlon/Task035_ISBILesionSegmentation/labelsTr'
98
+
99
+ for patientid in range(1, 6):
100
+ for t in range(1, 6):
101
+ for mask in [1, 2]:
102
+ f = join(labels_folder, "training%02d_%02d_mask%d.nii.gz" % (patientid, t, mask))
103
+ if isfile(f):
104
+ os.rename(f, join(labels_folder, "case__%02.0d__%02.0d__mask%d.nii.gz" % (patientid, t, mask)))
105
+
106
+
107
+
108
+ tr_files = []
109
+ for patientid in range(1, 6):
110
+ for t in range(1, 6):
111
+ for mask in [1, 2]:
112
+ if isfile(join(labels_folder, "case__%02.0d__%02.0d__mask%d.nii.gz" % (patientid, t, mask))):
113
+ tr_files.append("case__%02.0d__%02.0d__mask%d.nii.gz" % (patientid, t, mask))
114
+
115
+
116
+ ts_files = []
117
+ for patientid in range(1, 20):
118
+ for t in range(1, 20):
119
+ if isfile(join("/media/fabian/My Book/MedicalDecathlon/Task035_ISBILesionSegmentation/imagesTs",
120
+ "case__%02.0d__%02.0d_0000.nii.gz" % (patientid, t))):
121
+ ts_files.append("case__%02.0d__%02.0d.nii.gz" % (patientid, t))
122
+
123
+
124
+ out_base = '/media/fabian/My Book/MedicalDecathlon/Task035_ISBILesionSegmentation/'
125
+
126
+ json_dict = OrderedDict()
127
+ json_dict['name'] = "ISBI_Lesion_Segmentation_Challenge_2015"
128
+ json_dict['description'] = "nothing"
129
+ json_dict['tensorImageSize'] = "4D"
130
+ json_dict['reference'] = "see challenge website"
131
+ json_dict['licence'] = "see challenge website"
132
+ json_dict['release'] = "0.0"
133
+ json_dict['modality'] = {
134
+ "0": "flair",
135
+ "1": "mprage",
136
+ "2": "pd",
137
+ "3": "t2"
138
+ }
139
+ json_dict['labels'] = {
140
+ "0": "background",
141
+ "1": "lesion"
142
+ }
143
+ json_dict['numTraining'] = len(subfiles(labels_folder))
144
+ json_dict['numTest'] = len(subfiles('/media/fabian/My Book/MedicalDecathlon/Task035_ISBILesionSegmentation/imagesTs')) // 4
145
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i[:-7], "label": "./labelsTr/%s.nii.gz" % i[:-7]} for i in
146
+ tr_files]
147
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i[:-7] for i in ts_files]
148
+
149
+ save_json(json_dict, join(out_base, "dataset.json"))
150
+
151
+ case_identifiers = np.unique([i[:-12] for i in subfiles("/media/fabian/My Book/MedicalDecathlon/MedicalDecathlon_raw_splitted/Task035_ISBILesionSegmentation/imagesTr", suffix='.nii.gz', join=False)])
152
+
153
+ splits = []
154
+ for f in range(5):
155
+ cases = [i for i in range(1, 6) if i != f+1]
156
+ splits.append(OrderedDict())
157
+ splits[-1]['val'] = np.array([i for i in case_identifiers if i.startswith("case__%02d__" % (f + 1))])
158
+ remaining = [i for i in case_identifiers if i not in splits[-1]['val']]
159
+ splits[-1]['train'] = np.array(remaining)
160
+
161
+ maybe_mkdir_p("/media/fabian/nnunet/Task035_ISBILesionSegmentation")
162
+ save_pickle(splits, join("/media/fabian/nnunet/Task035_ISBILesionSegmentation", "splits_final.pkl"))
nnunet/dataset_conversion/Task037_038_Chaos_Challenge.py ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from PIL import Image
17
+ import shutil
18
+ from collections import OrderedDict
19
+
20
+ import dicom2nifti
21
+ import numpy as np
22
+ from batchgenerators.utilities.data_splitting import get_split_deterministic
23
+ from batchgenerators.utilities.file_and_folder_operations import *
24
+ from PIL import Image
25
+ import SimpleITK as sitk
26
+ from nnunet.paths import preprocessing_output_dir, nnUNet_raw_data
27
+ from nnunet.utilities.sitk_stuff import copy_geometry
28
+ from nnunet.inference.ensemble_predictions import merge
29
+
30
+
31
+ def load_png_stack(folder):
32
+ pngs = subfiles(folder, suffix="png")
33
+ pngs.sort()
34
+ loaded = []
35
+ for p in pngs:
36
+ loaded.append(np.array(Image.open(p)))
37
+ loaded = np.stack(loaded, 0)[::-1]
38
+ return loaded
39
+
40
+
41
+ def convert_CT_seg(loaded_png):
42
+ return loaded_png.astype(np.uint16)
43
+
44
+
45
+ def convert_MR_seg(loaded_png):
46
+ result = np.zeros(loaded_png.shape)
47
+ result[(loaded_png > 55) & (loaded_png <= 70)] = 1 # liver
48
+ result[(loaded_png > 110) & (loaded_png <= 135)] = 2 # right kidney
49
+ result[(loaded_png > 175) & (loaded_png <= 200)] = 3 # left kidney
50
+ result[(loaded_png > 240) & (loaded_png <= 255)] = 4 # spleen
51
+ return result
52
+
53
+
54
+ def convert_seg_to_intensity_task5(seg):
55
+ seg_new = np.zeros(seg.shape, dtype=np.uint8)
56
+ seg_new[seg == 1] = 63
57
+ seg_new[seg == 2] = 126
58
+ seg_new[seg == 3] = 189
59
+ seg_new[seg == 4] = 252
60
+ return seg_new
61
+
62
+
63
+ def convert_seg_to_intensity_task3(seg):
64
+ seg_new = np.zeros(seg.shape, dtype=np.uint8)
65
+ seg_new[seg == 1] = 63
66
+ return seg_new
67
+
68
+
69
+ def write_pngs_from_nifti(nifti, output_folder, converter=convert_seg_to_intensity_task3):
70
+ npy = sitk.GetArrayFromImage(sitk.ReadImage(nifti))
71
+ seg_new = converter(npy)
72
+ for z in range(len(npy)):
73
+ Image.fromarray(seg_new[z]).save(join(output_folder, "img%03.0d.png" % z))
74
+
75
+
76
+ def convert_variant2_predicted_test_to_submission_format(folder_with_predictions,
77
+ output_folder="/home/fabian/drives/datasets/results/nnUNet/test_sets/Task038_CHAOS_Task_3_5_Variant2/ready_to_submit",
78
+ postprocessing_file="/home/fabian/drives/datasets/results/nnUNet/ensembles/Task038_CHAOS_Task_3_5_Variant2/ensemble_2d__nnUNetTrainerV2__nnUNetPlansv2.1--3d_fullres__nnUNetTrainerV2__nnUNetPlansv2.1/postprocessing.json"):
79
+ """
80
+ output_folder is where the extracted template is
81
+ :param folder_with_predictions:
82
+ :param output_folder:
83
+ :return:
84
+ """
85
+ postprocessing_file = "/media/fabian/Results/nnUNet/3d_fullres/Task039_CHAOS_Task_3_5_Variant2_highres/" \
86
+ "nnUNetTrainerV2__nnUNetPlansfixed/postprocessing.json"
87
+
88
+ # variant 2 treats in and out phase as two training examples, so we need to ensemble these two again
89
+ final_predictions_folder = join(output_folder, "final")
90
+ maybe_mkdir_p(final_predictions_folder)
91
+ t1_patient_names = [i.split("_")[-1][:-7] for i in subfiles(folder_with_predictions, prefix="T1", suffix=".nii.gz", join=False)]
92
+ folder_for_ensembing0 = join(output_folder, "ens0")
93
+ folder_for_ensembing1 = join(output_folder, "ens1")
94
+ maybe_mkdir_p(folder_for_ensembing0)
95
+ maybe_mkdir_p(folder_for_ensembing1)
96
+ # now copy all t1 out phases in ens0 and all in phases in ens1. Name them the same.
97
+ for t1 in t1_patient_names:
98
+ shutil.copy(join(folder_with_predictions, "T1_in_%s.npz" % t1), join(folder_for_ensembing1, "T1_%s.npz" % t1))
99
+ shutil.copy(join(folder_with_predictions, "T1_in_%s.pkl" % t1), join(folder_for_ensembing1, "T1_%s.pkl" % t1))
100
+ shutil.copy(join(folder_with_predictions, "T1_out_%s.npz" % t1), join(folder_for_ensembing0, "T1_%s.npz" % t1))
101
+ shutil.copy(join(folder_with_predictions, "T1_out_%s.pkl" % t1), join(folder_for_ensembing0, "T1_%s.pkl" % t1))
102
+ shutil.copy(join(folder_with_predictions, "plans.pkl"), join(folder_for_ensembing0, "plans.pkl"))
103
+ shutil.copy(join(folder_with_predictions, "plans.pkl"), join(folder_for_ensembing1, "plans.pkl"))
104
+
105
+ # there is a problem with T1_35 that I need to correct manually (different crop size, will not negatively impact results)
106
+ #ens0_softmax = np.load(join(folder_for_ensembing0, "T1_35.npz"))['softmax']
107
+ ens1_softmax = np.load(join(folder_for_ensembing1, "T1_35.npz"))['softmax']
108
+ #ens0_props = load_pickle(join(folder_for_ensembing0, "T1_35.pkl"))
109
+ #ens1_props = load_pickle(join(folder_for_ensembing1, "T1_35.pkl"))
110
+ ens1_softmax = ens1_softmax[:, :, :-1, :]
111
+ np.savez_compressed(join(folder_for_ensembing1, "T1_35.npz"), softmax=ens1_softmax)
112
+ shutil.copy(join(folder_for_ensembing0, "T1_35.pkl"), join(folder_for_ensembing1, "T1_35.pkl"))
113
+
114
+ # now call my ensemble function
115
+ merge((folder_for_ensembing0, folder_for_ensembing1), final_predictions_folder, 8, True,
116
+ postprocessing_file=postprocessing_file)
117
+ # copy t2 files to final_predictions_folder as well
118
+ t2_files = subfiles(folder_with_predictions, prefix="T2", suffix=".nii.gz", join=False)
119
+ for t2 in t2_files:
120
+ shutil.copy(join(folder_with_predictions, t2), join(final_predictions_folder, t2))
121
+
122
+ # apply postprocessing
123
+ from nnunet.postprocessing.connected_components import apply_postprocessing_to_folder, load_postprocessing
124
+ postprocessed_folder = join(output_folder, "final_postprocessed")
125
+ for_which_classes, min_valid_obj_size = load_postprocessing(postprocessing_file)
126
+ apply_postprocessing_to_folder(final_predictions_folder, postprocessed_folder,
127
+ for_which_classes, min_valid_obj_size, 8)
128
+
129
+ # now export the niftis in the weird png format
130
+ # task 3
131
+ output_dir = join(output_folder, "CHAOS_submission_template_new", "Task3", "MR")
132
+ for t1 in t1_patient_names:
133
+ output_folder_here = join(output_dir, t1, "T1DUAL", "Results")
134
+ nifti_file = join(postprocessed_folder, "T1_%s.nii.gz" % t1)
135
+ write_pngs_from_nifti(nifti_file, output_folder_here, converter=convert_seg_to_intensity_task3)
136
+ for t2 in t2_files:
137
+ patname = t2.split("_")[-1][:-7]
138
+ output_folder_here = join(output_dir, patname, "T2SPIR", "Results")
139
+ nifti_file = join(postprocessed_folder, "T2_%s.nii.gz" % patname)
140
+ write_pngs_from_nifti(nifti_file, output_folder_here, converter=convert_seg_to_intensity_task3)
141
+
142
+ # task 5
143
+ output_dir = join(output_folder, "CHAOS_submission_template_new", "Task5", "MR")
144
+ for t1 in t1_patient_names:
145
+ output_folder_here = join(output_dir, t1, "T1DUAL", "Results")
146
+ nifti_file = join(postprocessed_folder, "T1_%s.nii.gz" % t1)
147
+ write_pngs_from_nifti(nifti_file, output_folder_here, converter=convert_seg_to_intensity_task5)
148
+ for t2 in t2_files:
149
+ patname = t2.split("_")[-1][:-7]
150
+ output_folder_here = join(output_dir, patname, "T2SPIR", "Results")
151
+ nifti_file = join(postprocessed_folder, "T2_%s.nii.gz" % patname)
152
+ write_pngs_from_nifti(nifti_file, output_folder_here, converter=convert_seg_to_intensity_task5)
153
+
154
+
155
+
156
+ if __name__ == "__main__":
157
+ """
158
+ This script only prepares data to participate in Task 5 and Task 5. I don't like the CT task because
159
+ 1) there are
160
+ no abdominal organs in the ground truth. In the case of CT we are supposed to train only liver while on MRI we are
161
+ supposed to train all organs. This would require manual modification of nnU-net to deal with this dataset. This is
162
+ not what nnU-net is about.
163
+ 2) CT Liver or multiorgan segmentation is too easy to get external data for. Therefore the challenges comes down
164
+ to who gets the b est external data, not who has the best algorithm. Not super interesting.
165
+
166
+ Task 3 is a subtask of Task 5 so we need to prepare the data only once.
167
+ Difficulty: We need to process both T1 and T2, but T1 has 2 'modalities' (phases). nnU-Net cannot handly varying
168
+ number of input channels. We need to be creative.
169
+ We deal with this by preparing 2 Variants:
170
+ 1) pretend we have 2 modalities for T2 as well by simply stacking a copy of the data
171
+ 2) treat all MRI sequences independently, so we now have 3*20 training data instead of 2*20. In inference we then
172
+ ensemble the results for the two t1 modalities.
173
+
174
+ Careful: We need to split manually here to ensure we stratify by patient
175
+ """
176
+
177
+ root = "/media/fabian/My Book/datasets/CHAOS_challenge/Train_Sets"
178
+ root_test = "/media/fabian/My Book/datasets/CHAOS_challenge/Test_Sets"
179
+ out_base = nnUNet_raw_data
180
+ # CT
181
+ # we ignore CT because
182
+
183
+ ##############################################################
184
+ # Variant 1
185
+ ##############################################################
186
+ patient_ids = []
187
+ patient_ids_test = []
188
+
189
+ output_folder = join(out_base, "Task037_CHAOS_Task_3_5_Variant1")
190
+ output_images = join(output_folder, "imagesTr")
191
+ output_labels = join(output_folder, "labelsTr")
192
+ output_imagesTs = join(output_folder, "imagesTs")
193
+ maybe_mkdir_p(output_images)
194
+ maybe_mkdir_p(output_labels)
195
+ maybe_mkdir_p(output_imagesTs)
196
+
197
+
198
+ # Process T1 train
199
+ d = join(root, "MR")
200
+ patients = subdirs(d, join=False)
201
+ for p in patients:
202
+ patient_name = "T1_" + p
203
+ gt_dir = join(d, p, "T1DUAL", "Ground")
204
+ seg = convert_MR_seg(load_png_stack(gt_dir)[::-1])
205
+
206
+ img_dir = join(d, p, "T1DUAL", "DICOM_anon", "InPhase")
207
+ img_outfile = join(output_images, patient_name + "_0000.nii.gz")
208
+ _ = dicom2nifti.convert_dicom.dicom_series_to_nifti(img_dir, img_outfile, reorient_nifti=False)
209
+
210
+ img_dir = join(d, p, "T1DUAL", "DICOM_anon", "OutPhase")
211
+ img_outfile = join(output_images, patient_name + "_0001.nii.gz")
212
+ _ = dicom2nifti.convert_dicom.dicom_series_to_nifti(img_dir, img_outfile, reorient_nifti=False)
213
+
214
+ img_sitk = sitk.ReadImage(img_outfile)
215
+ img_sitk_npy = sitk.GetArrayFromImage(img_sitk)
216
+ seg_itk = sitk.GetImageFromArray(seg.astype(np.uint8))
217
+ seg_itk = copy_geometry(seg_itk, img_sitk)
218
+ sitk.WriteImage(seg_itk, join(output_labels, patient_name + ".nii.gz"))
219
+ patient_ids.append(patient_name)
220
+
221
+ # Process T1 test
222
+ d = join(root_test, "MR")
223
+ patients = subdirs(d, join=False)
224
+ for p in patients:
225
+ patient_name = "T1_" + p
226
+
227
+ img_dir = join(d, p, "T1DUAL", "DICOM_anon", "InPhase")
228
+ img_outfile = join(output_imagesTs, patient_name + "_0000.nii.gz")
229
+ _ = dicom2nifti.convert_dicom.dicom_series_to_nifti(img_dir, img_outfile, reorient_nifti=False)
230
+
231
+ img_dir = join(d, p, "T1DUAL", "DICOM_anon", "OutPhase")
232
+ img_outfile = join(output_imagesTs, patient_name + "_0001.nii.gz")
233
+ _ = dicom2nifti.convert_dicom.dicom_series_to_nifti(img_dir, img_outfile, reorient_nifti=False)
234
+
235
+ img_sitk = sitk.ReadImage(img_outfile)
236
+ img_sitk_npy = sitk.GetArrayFromImage(img_sitk)
237
+ patient_ids_test.append(patient_name)
238
+
239
+ # Process T2 train
240
+ d = join(root, "MR")
241
+ patients = subdirs(d, join=False)
242
+ for p in patients:
243
+ patient_name = "T2_" + p
244
+
245
+ gt_dir = join(d, p, "T2SPIR", "Ground")
246
+ seg = convert_MR_seg(load_png_stack(gt_dir)[::-1])
247
+
248
+ img_dir = join(d, p, "T2SPIR", "DICOM_anon")
249
+ img_outfile = join(output_images, patient_name + "_0000.nii.gz")
250
+ _ = dicom2nifti.convert_dicom.dicom_series_to_nifti(img_dir, img_outfile, reorient_nifti=False)
251
+ shutil.copy(join(output_images, patient_name + "_0000.nii.gz"), join(output_images, patient_name + "_0001.nii.gz"))
252
+
253
+ img_sitk = sitk.ReadImage(img_outfile)
254
+ img_sitk_npy = sitk.GetArrayFromImage(img_sitk)
255
+ seg_itk = sitk.GetImageFromArray(seg.astype(np.uint8))
256
+ seg_itk = copy_geometry(seg_itk, img_sitk)
257
+ sitk.WriteImage(seg_itk, join(output_labels, patient_name + ".nii.gz"))
258
+ patient_ids.append(patient_name)
259
+
260
+ # Process T2 test
261
+ d = join(root_test, "MR")
262
+ patients = subdirs(d, join=False)
263
+ for p in patients:
264
+ patient_name = "T2_" + p
265
+
266
+ gt_dir = join(d, p, "T2SPIR", "Ground")
267
+
268
+ img_dir = join(d, p, "T2SPIR", "DICOM_anon")
269
+ img_outfile = join(output_imagesTs, patient_name + "_0000.nii.gz")
270
+ _ = dicom2nifti.convert_dicom.dicom_series_to_nifti(img_dir, img_outfile, reorient_nifti=False)
271
+ shutil.copy(join(output_imagesTs, patient_name + "_0000.nii.gz"), join(output_imagesTs, patient_name + "_0001.nii.gz"))
272
+
273
+ img_sitk = sitk.ReadImage(img_outfile)
274
+ img_sitk_npy = sitk.GetArrayFromImage(img_sitk)
275
+ patient_ids_test.append(patient_name)
276
+
277
+ json_dict = OrderedDict()
278
+ json_dict['name'] = "Chaos Challenge Task3/5 Variant 1"
279
+ json_dict['description'] = "nothing"
280
+ json_dict['tensorImageSize'] = "4D"
281
+ json_dict['reference'] = "https://chaos.grand-challenge.org/Data/"
282
+ json_dict['licence'] = "see https://chaos.grand-challenge.org/Data/"
283
+ json_dict['release'] = "0.0"
284
+ json_dict['modality'] = {
285
+ "0": "MRI",
286
+ "1": "MRI",
287
+ }
288
+ json_dict['labels'] = {
289
+ "0": "background",
290
+ "1": "liver",
291
+ "2": "right kidney",
292
+ "3": "left kidney",
293
+ "4": "spleen",
294
+ }
295
+ json_dict['numTraining'] = len(patient_ids)
296
+ json_dict['numTest'] = 0
297
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i, "label": "./labelsTr/%s.nii.gz" % i} for i in
298
+ patient_ids]
299
+ json_dict['test'] = []
300
+
301
+ save_json(json_dict, join(output_folder, "dataset.json"))
302
+
303
+ ##############################################################
304
+ # Variant 2
305
+ ##############################################################
306
+
307
+ patient_ids = []
308
+ patient_ids_test = []
309
+
310
+ output_folder = join(out_base, "Task038_CHAOS_Task_3_5_Variant2")
311
+ output_images = join(output_folder, "imagesTr")
312
+ output_imagesTs = join(output_folder, "imagesTs")
313
+ output_labels = join(output_folder, "labelsTr")
314
+ maybe_mkdir_p(output_images)
315
+ maybe_mkdir_p(output_imagesTs)
316
+ maybe_mkdir_p(output_labels)
317
+
318
+ # Process T1 train
319
+ d = join(root, "MR")
320
+ patients = subdirs(d, join=False)
321
+ for p in patients:
322
+ patient_name_in = "T1_in_" + p
323
+ patient_name_out = "T1_out_" + p
324
+ gt_dir = join(d, p, "T1DUAL", "Ground")
325
+ seg = convert_MR_seg(load_png_stack(gt_dir)[::-1])
326
+
327
+ img_dir = join(d, p, "T1DUAL", "DICOM_anon", "InPhase")
328
+ img_outfile = join(output_images, patient_name_in + "_0000.nii.gz")
329
+ _ = dicom2nifti.convert_dicom.dicom_series_to_nifti(img_dir, img_outfile, reorient_nifti=False)
330
+
331
+ img_dir = join(d, p, "T1DUAL", "DICOM_anon", "OutPhase")
332
+ img_outfile = join(output_images, patient_name_out + "_0000.nii.gz")
333
+ _ = dicom2nifti.convert_dicom.dicom_series_to_nifti(img_dir, img_outfile, reorient_nifti=False)
334
+
335
+ img_sitk = sitk.ReadImage(img_outfile)
336
+ img_sitk_npy = sitk.GetArrayFromImage(img_sitk)
337
+ seg_itk = sitk.GetImageFromArray(seg.astype(np.uint8))
338
+ seg_itk = copy_geometry(seg_itk, img_sitk)
339
+ sitk.WriteImage(seg_itk, join(output_labels, patient_name_in + ".nii.gz"))
340
+ sitk.WriteImage(seg_itk, join(output_labels, patient_name_out + ".nii.gz"))
341
+ patient_ids.append(patient_name_out)
342
+ patient_ids.append(patient_name_in)
343
+
344
+ # Process T1 test
345
+ d = join(root_test, "MR")
346
+ patients = subdirs(d, join=False)
347
+ for p in patients:
348
+ patient_name_in = "T1_in_" + p
349
+ patient_name_out = "T1_out_" + p
350
+ gt_dir = join(d, p, "T1DUAL", "Ground")
351
+
352
+ img_dir = join(d, p, "T1DUAL", "DICOM_anon", "InPhase")
353
+ img_outfile = join(output_imagesTs, patient_name_in + "_0000.nii.gz")
354
+ _ = dicom2nifti.convert_dicom.dicom_series_to_nifti(img_dir, img_outfile, reorient_nifti=False)
355
+
356
+ img_dir = join(d, p, "T1DUAL", "DICOM_anon", "OutPhase")
357
+ img_outfile = join(output_imagesTs, patient_name_out + "_0000.nii.gz")
358
+ _ = dicom2nifti.convert_dicom.dicom_series_to_nifti(img_dir, img_outfile, reorient_nifti=False)
359
+
360
+ img_sitk = sitk.ReadImage(img_outfile)
361
+ img_sitk_npy = sitk.GetArrayFromImage(img_sitk)
362
+ patient_ids_test.append(patient_name_out)
363
+ patient_ids_test.append(patient_name_in)
364
+
365
+ # Process T2 train
366
+ d = join(root, "MR")
367
+ patients = subdirs(d, join=False)
368
+ for p in patients:
369
+ patient_name = "T2_" + p
370
+
371
+ gt_dir = join(d, p, "T2SPIR", "Ground")
372
+ seg = convert_MR_seg(load_png_stack(gt_dir)[::-1])
373
+
374
+ img_dir = join(d, p, "T2SPIR", "DICOM_anon")
375
+ img_outfile = join(output_images, patient_name + "_0000.nii.gz")
376
+ _ = dicom2nifti.convert_dicom.dicom_series_to_nifti(img_dir, img_outfile, reorient_nifti=False)
377
+
378
+ img_sitk = sitk.ReadImage(img_outfile)
379
+ img_sitk_npy = sitk.GetArrayFromImage(img_sitk)
380
+ seg_itk = sitk.GetImageFromArray(seg.astype(np.uint8))
381
+ seg_itk = copy_geometry(seg_itk, img_sitk)
382
+ sitk.WriteImage(seg_itk, join(output_labels, patient_name + ".nii.gz"))
383
+ patient_ids.append(patient_name)
384
+
385
+ # Process T2 test
386
+ d = join(root_test, "MR")
387
+ patients = subdirs(d, join=False)
388
+ for p in patients:
389
+ patient_name = "T2_" + p
390
+
391
+ gt_dir = join(d, p, "T2SPIR", "Ground")
392
+
393
+ img_dir = join(d, p, "T2SPIR", "DICOM_anon")
394
+ img_outfile = join(output_imagesTs, patient_name + "_0000.nii.gz")
395
+ _ = dicom2nifti.convert_dicom.dicom_series_to_nifti(img_dir, img_outfile, reorient_nifti=False)
396
+
397
+ img_sitk = sitk.ReadImage(img_outfile)
398
+ img_sitk_npy = sitk.GetArrayFromImage(img_sitk)
399
+ patient_ids_test.append(patient_name)
400
+
401
+ json_dict = OrderedDict()
402
+ json_dict['name'] = "Chaos Challenge Task3/5 Variant 2"
403
+ json_dict['description'] = "nothing"
404
+ json_dict['tensorImageSize'] = "4D"
405
+ json_dict['reference'] = "https://chaos.grand-challenge.org/Data/"
406
+ json_dict['licence'] = "see https://chaos.grand-challenge.org/Data/"
407
+ json_dict['release'] = "0.0"
408
+ json_dict['modality'] = {
409
+ "0": "MRI",
410
+ }
411
+ json_dict['labels'] = {
412
+ "0": "background",
413
+ "1": "liver",
414
+ "2": "right kidney",
415
+ "3": "left kidney",
416
+ "4": "spleen",
417
+ }
418
+ json_dict['numTraining'] = len(patient_ids)
419
+ json_dict['numTest'] = 0
420
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i, "label": "./labelsTr/%s.nii.gz" % i} for i in
421
+ patient_ids]
422
+ json_dict['test'] = []
423
+
424
+ save_json(json_dict, join(output_folder, "dataset.json"))
425
+
426
+ #################################################
427
+ # custom split
428
+ #################################################
429
+ patients = subdirs(join(root, "MR"), join=False)
430
+ task_name_variant1 = "Task037_CHAOS_Task_3_5_Variant1"
431
+ task_name_variant2 = "Task038_CHAOS_Task_3_5_Variant2"
432
+
433
+ output_preprocessed_v1 = join(preprocessing_output_dir, task_name_variant1)
434
+ maybe_mkdir_p(output_preprocessed_v1)
435
+
436
+ output_preprocessed_v2 = join(preprocessing_output_dir, task_name_variant2)
437
+ maybe_mkdir_p(output_preprocessed_v2)
438
+
439
+ splits = []
440
+ for fold in range(5):
441
+ tr, val = get_split_deterministic(patients, fold, 5, 12345)
442
+ train = ["T2_" + i for i in tr] + ["T1_" + i for i in tr]
443
+ validation = ["T2_" + i for i in val] + ["T1_" + i for i in val]
444
+ splits.append({
445
+ 'train': train,
446
+ 'val': validation
447
+ })
448
+ save_pickle(splits, join(output_preprocessed_v1, "splits_final.pkl"))
449
+
450
+ splits = []
451
+ for fold in range(5):
452
+ tr, val = get_split_deterministic(patients, fold, 5, 12345)
453
+ train = ["T2_" + i for i in tr] + ["T1_in_" + i for i in tr] + ["T1_out_" + i for i in tr]
454
+ validation = ["T2_" + i for i in val] + ["T1_in_" + i for i in val] + ["T1_out_" + i for i in val]
455
+ splits.append({
456
+ 'train': train,
457
+ 'val': validation
458
+ })
459
+ save_pickle(splits, join(output_preprocessed_v2, "splits_final.pkl"))
460
+
nnunet/dataset_conversion/Task040_KiTS.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from copy import deepcopy
17
+
18
+ from batchgenerators.utilities.file_and_folder_operations import *
19
+ import shutil
20
+ import SimpleITK as sitk
21
+ from multiprocessing import Pool
22
+ from medpy.metric import dc
23
+ import numpy as np
24
+ from nnunet.paths import network_training_output_dir
25
+ from scipy.ndimage import label
26
+
27
+
28
+ def compute_dice_scores(ref: str, pred: str):
29
+ ref = sitk.GetArrayFromImage(sitk.ReadImage(ref))
30
+ pred = sitk.GetArrayFromImage(sitk.ReadImage(pred))
31
+ kidney_mask_ref = ref > 0
32
+ kidney_mask_pred = pred > 0
33
+ if np.sum(kidney_mask_pred) == 0 and kidney_mask_ref.sum() == 0:
34
+ kidney_dice = np.nan
35
+ else:
36
+ kidney_dice = dc(kidney_mask_pred, kidney_mask_ref)
37
+
38
+ tumor_mask_ref = ref == 2
39
+ tumor_mask_pred = pred == 2
40
+ if np.sum(tumor_mask_ref) == 0 and tumor_mask_pred.sum() == 0:
41
+ tumor_dice = np.nan
42
+ else:
43
+ tumor_dice = dc(tumor_mask_ref, tumor_mask_pred)
44
+
45
+ geometric_mean = np.mean((kidney_dice, tumor_dice))
46
+ return kidney_dice, tumor_dice, geometric_mean
47
+
48
+
49
+ def evaluate_folder(folder_gt: str, folder_pred: str):
50
+ p = Pool(8)
51
+ niftis = subfiles(folder_gt, suffix=".nii.gz", join=False)
52
+ images_gt = [join(folder_gt, i) for i in niftis]
53
+ images_pred = [join(folder_pred, i) for i in niftis]
54
+ results = p.starmap(compute_dice_scores, zip(images_gt, images_pred))
55
+ p.close()
56
+ p.join()
57
+
58
+ with open(join(folder_pred, "results.csv"), 'w') as f:
59
+ for i, ni in enumerate(niftis):
60
+ f.write("%s,%0.4f,%0.4f,%0.4f\n" % (ni, *results[i]))
61
+
62
+
63
+ def remove_all_but_the_two_largest_conn_comp(img_itk_file: str, file_out: str):
64
+ """
65
+ This was not used. I was just curious because others used this. Turns out this is not necessary for my networks
66
+ """
67
+ img_itk = sitk.ReadImage(img_itk_file)
68
+ img_npy = sitk.GetArrayFromImage(img_itk)
69
+
70
+ labelmap, num_labels = label((img_npy > 0).astype(int))
71
+
72
+ if num_labels > 2:
73
+ label_sizes = []
74
+ for i in range(1, num_labels + 1):
75
+ label_sizes.append(np.sum(labelmap == i))
76
+ argsrt = np.argsort(label_sizes)[::-1] # two largest are now argsrt[0] and argsrt[1]
77
+ keep_mask = (labelmap == argsrt[0] + 1) | (labelmap == argsrt[1] + 1)
78
+ img_npy[~keep_mask] = 0
79
+ new = sitk.GetImageFromArray(img_npy)
80
+ new.CopyInformation(img_itk)
81
+ sitk.WriteImage(new, file_out)
82
+ print(os.path.basename(img_itk_file), num_labels, label_sizes)
83
+ else:
84
+ shutil.copy(img_itk_file, file_out)
85
+
86
+
87
+ def manual_postprocess(folder_in,
88
+ folder_out):
89
+ """
90
+ This was not used. I was just curious because others used this. Turns out this is not necessary for my networks
91
+ """
92
+ maybe_mkdir_p(folder_out)
93
+ infiles = subfiles(folder_in, suffix=".nii.gz", join=False)
94
+
95
+ outfiles = [join(folder_out, i) for i in infiles]
96
+ infiles = [join(folder_in, i) for i in infiles]
97
+
98
+ p = Pool(8)
99
+ _ = p.starmap_async(remove_all_but_the_two_largest_conn_comp, zip(infiles, outfiles))
100
+ _ = _.get()
101
+ p.close()
102
+ p.join()
103
+
104
+
105
+
106
+
107
+ def copy_npz_fom_valsets():
108
+ '''
109
+ this is preparation for ensembling
110
+ :return:
111
+ '''
112
+ base = join(network_training_output_dir, "3d_lowres/Task048_KiTS_clean")
113
+ folders = ['nnUNetTrainerNewCandidate23_FabiansPreActResNet__nnUNetPlans',
114
+ 'nnUNetTrainerNewCandidate23_FabiansResNet__nnUNetPlans',
115
+ 'nnUNetTrainerNewCandidate23__nnUNetPlans']
116
+ for f in folders:
117
+ out = join(base, f, 'crossval_npz')
118
+ maybe_mkdir_p(out)
119
+ shutil.copy(join(base, f, 'plans.pkl'), out)
120
+ for fold in range(5):
121
+ cur = join(base, f, 'fold_%d' % fold, 'validation_raw')
122
+ npz_files = subfiles(cur, suffix='.npz', join=False)
123
+ pkl_files = [i[:-3] + 'pkl' for i in npz_files]
124
+ assert all([isfile(join(cur, i)) for i in pkl_files])
125
+ for n in npz_files:
126
+ corresponding_pkl = n[:-3] + 'pkl'
127
+ shutil.copy(join(cur, n), out)
128
+ shutil.copy(join(cur, corresponding_pkl), out)
129
+
130
+
131
+ def ensemble(experiments=('nnUNetTrainerNewCandidate23_FabiansPreActResNet__nnUNetPlans',
132
+ 'nnUNetTrainerNewCandidate23_FabiansResNet__nnUNetPlans'), out_dir="/media/fabian/Results/nnUNet/3d_lowres/Task048_KiTS_clean/ensemble_preactres_and_res"):
133
+ from nnunet.inference.ensemble_predictions import merge
134
+ folders = [join(network_training_output_dir, "3d_lowres/Task048_KiTS_clean", i, 'crossval_npz') for i in experiments]
135
+ merge(folders, out_dir, 8)
136
+
137
+
138
+ def prepare_submission(fld= "/home/fabian/drives/datasets/results/nnUNet/test_sets/Task048_KiTS_clean/predicted_ens_3d_fullres_3d_cascade_fullres_postprocessed", # '/home/fabian/datasets_fabian/predicted_KiTS_nnUNetTrainerNewCandidate23_FabiansResNet',
139
+ out='/home/fabian/drives/datasets/results/nnUNet/test_sets/Task048_KiTS_clean/submission'):
140
+ nii = subfiles(fld, join=False, suffix='.nii.gz')
141
+ maybe_mkdir_p(out)
142
+ for n in nii:
143
+ outfname = n.replace('case', 'prediction')
144
+ shutil.copy(join(fld, n), join(out, outfname))
145
+
146
+
147
+ def pretent_to_be_nnUNetTrainer(base, folds=(0, 1, 2, 3, 4)):
148
+ """
149
+ changes best checkpoint pickle nnunettrainer class name to nnUNetTrainer
150
+ :param experiments:
151
+ :return:
152
+ """
153
+ for fold in folds:
154
+ cur = join(base, "fold_%d" % fold)
155
+ pkl_file = join(cur, 'model_best.model.pkl')
156
+ a = load_pickle(pkl_file)
157
+ a['name_old'] = deepcopy(a['name'])
158
+ a['name'] = 'nnUNetTrainer'
159
+ save_pickle(a, pkl_file)
160
+
161
+
162
+ def reset_trainerName(base, folds=(0, 1, 2, 3, 4)):
163
+ for fold in folds:
164
+ cur = join(base, "fold_%d" % fold)
165
+ pkl_file = join(cur, 'model_best.model.pkl')
166
+ a = load_pickle(pkl_file)
167
+ a['name'] = a['name_old']
168
+ del a['name_old']
169
+ save_pickle(a, pkl_file)
170
+
171
+
172
+ def nnUNetTrainer_these(experiments=('nnUNetTrainerNewCandidate23_FabiansPreActResNet__nnUNetPlans',
173
+ 'nnUNetTrainerNewCandidate23_FabiansResNet__nnUNetPlans',
174
+ 'nnUNetTrainerNewCandidate23__nnUNetPlans')):
175
+ """
176
+ changes best checkpoint pickle nnunettrainer class name to nnUNetTrainer
177
+ :param experiments:
178
+ :return:
179
+ """
180
+ base = join(network_training_output_dir, "3d_lowres/Task048_KiTS_clean")
181
+ for exp in experiments:
182
+ cur = join(base, exp)
183
+ pretent_to_be_nnUNetTrainer(cur)
184
+
185
+
186
+ def reset_trainerName_these(experiments=('nnUNetTrainerNewCandidate23_FabiansPreActResNet__nnUNetPlans',
187
+ 'nnUNetTrainerNewCandidate23_FabiansResNet__nnUNetPlans',
188
+ 'nnUNetTrainerNewCandidate23__nnUNetPlans')):
189
+ """
190
+ changes best checkpoint pickle nnunettrainer class name to nnUNetTrainer
191
+ :param experiments:
192
+ :return:
193
+ """
194
+ base = join(network_training_output_dir, "3d_lowres/Task048_KiTS_clean")
195
+ for exp in experiments:
196
+ cur = join(base, exp)
197
+ reset_trainerName(cur)
198
+
199
+
200
+ if __name__ == "__main__":
201
+ base = "/media/fabian/My Book/datasets/KiTS2019_Challenge/kits19/data"
202
+ out = "/media/fabian/My Book/MedicalDecathlon/nnUNet_raw_splitted/Task040_KiTS"
203
+ cases = subdirs(base, join=False)
204
+
205
+ maybe_mkdir_p(out)
206
+ maybe_mkdir_p(join(out, "imagesTr"))
207
+ maybe_mkdir_p(join(out, "imagesTs"))
208
+ maybe_mkdir_p(join(out, "labelsTr"))
209
+
210
+ for c in cases:
211
+ case_id = int(c.split("_")[-1])
212
+ if case_id < 210:
213
+ shutil.copy(join(base, c, "imaging.nii.gz"), join(out, "imagesTr", c + "_0000.nii.gz"))
214
+ shutil.copy(join(base, c, "segmentation.nii.gz"), join(out, "labelsTr", c + ".nii.gz"))
215
+ else:
216
+ shutil.copy(join(base, c, "imaging.nii.gz"), join(out, "imagesTs", c + "_0000.nii.gz"))
217
+
218
+ json_dict = {}
219
+ json_dict['name'] = "KiTS"
220
+ json_dict['description'] = "kidney and kidney tumor segmentation"
221
+ json_dict['tensorImageSize'] = "4D"
222
+ json_dict['reference'] = "KiTS data for nnunet"
223
+ json_dict['licence'] = ""
224
+ json_dict['release'] = "0.0"
225
+ json_dict['modality'] = {
226
+ "0": "CT",
227
+ }
228
+ json_dict['labels'] = {
229
+ "0": "background",
230
+ "1": "Kidney",
231
+ "2": "Tumor"
232
+ }
233
+ json_dict['numTraining'] = len(cases)
234
+ json_dict['numTest'] = 0
235
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i, "label": "./labelsTr/%s.nii.gz" % i} for i in
236
+ cases]
237
+ json_dict['test'] = []
238
+
239
+ save_json(json_dict, os.path.join(out, "dataset.json"))
240
+
nnunet/dataset_conversion/Task043_BraTS_2019.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import numpy as np
17
+ from collections import OrderedDict
18
+
19
+ from batchgenerators.utilities.file_and_folder_operations import *
20
+ from nnunet.paths import nnUNet_raw_data
21
+ import SimpleITK as sitk
22
+ import shutil
23
+
24
+
25
+ def copy_BraTS_segmentation_and_convert_labels(in_file, out_file):
26
+ # use this for segmentation only!!!
27
+ # nnUNet wants the labels to be continuous. BraTS is 0, 1, 2, 4 -> we make that into 0, 1, 2, 3
28
+ img = sitk.ReadImage(in_file)
29
+ img_npy = sitk.GetArrayFromImage(img)
30
+
31
+ uniques = np.unique(img_npy)
32
+ for u in uniques:
33
+ if u not in [0, 1, 2, 4]:
34
+ raise RuntimeError('unexpected label')
35
+
36
+ seg_new = np.zeros_like(img_npy)
37
+ seg_new[img_npy == 4] = 3
38
+ seg_new[img_npy == 2] = 1
39
+ seg_new[img_npy == 1] = 2
40
+ img_corr = sitk.GetImageFromArray(seg_new)
41
+ img_corr.CopyInformation(img)
42
+ sitk.WriteImage(img_corr, out_file)
43
+
44
+
45
+ if __name__ == "__main__":
46
+ """
47
+ REMEMBER TO CONVERT LABELS BACK TO BRATS CONVENTION AFTER PREDICTION!
48
+ """
49
+
50
+ task_name = "Task043_BraTS2019"
51
+ downloaded_data_dir = "/home/sdp/MLPERF/Brats2019_DATA/MICCAI_BraTS_2019_Data_Training"
52
+
53
+ target_base = join(nnUNet_raw_data, task_name)
54
+ target_imagesTr = join(target_base, "imagesTr")
55
+ target_imagesVal = join(target_base, "imagesVal")
56
+ target_imagesTs = join(target_base, "imagesTs")
57
+ target_labelsTr = join(target_base, "labelsTr")
58
+
59
+ maybe_mkdir_p(target_imagesTr)
60
+ maybe_mkdir_p(target_imagesVal)
61
+ maybe_mkdir_p(target_imagesTs)
62
+ maybe_mkdir_p(target_labelsTr)
63
+
64
+ patient_names = []
65
+ for tpe in ["HGG", "LGG"]:
66
+ cur = join(downloaded_data_dir, tpe)
67
+ for p in subdirs(cur, join=False):
68
+ patdir = join(cur, p)
69
+ patient_name = tpe + "__" + p
70
+ patient_names.append(patient_name)
71
+ t1 = join(patdir, p + "_t1.nii.gz")
72
+ t1c = join(patdir, p + "_t1ce.nii.gz")
73
+ t2 = join(patdir, p + "_t2.nii.gz")
74
+ flair = join(patdir, p + "_flair.nii.gz")
75
+ seg = join(patdir, p + "_seg.nii.gz")
76
+
77
+ assert all([
78
+ isfile(t1),
79
+ isfile(t1c),
80
+ isfile(t2),
81
+ isfile(flair),
82
+ isfile(seg)
83
+ ]), "%s" % patient_name
84
+
85
+ shutil.copy(t1, join(target_imagesTr, patient_name + "_0000.nii.gz"))
86
+ shutil.copy(t1c, join(target_imagesTr, patient_name + "_0001.nii.gz"))
87
+ shutil.copy(t2, join(target_imagesTr, patient_name + "_0002.nii.gz"))
88
+ shutil.copy(flair, join(target_imagesTr, patient_name + "_0003.nii.gz"))
89
+
90
+ copy_BraTS_segmentation_and_convert_labels(seg, join(target_labelsTr, patient_name + ".nii.gz"))
91
+
92
+
93
+ json_dict = OrderedDict()
94
+ json_dict['name'] = "BraTS2019"
95
+ json_dict['description'] = "nothing"
96
+ json_dict['tensorImageSize'] = "4D"
97
+ json_dict['reference'] = "see BraTS2019"
98
+ json_dict['licence'] = "see BraTS2019 license"
99
+ json_dict['release'] = "0.0"
100
+ json_dict['modality'] = {
101
+ "0": "T1",
102
+ "1": "T1ce",
103
+ "2": "T2",
104
+ "3": "FLAIR"
105
+ }
106
+ json_dict['labels'] = {
107
+ "0": "background",
108
+ "1": "edema",
109
+ "2": "non-enhancing",
110
+ "3": "enhancing",
111
+ }
112
+ json_dict['numTraining'] = len(patient_names)
113
+ json_dict['numTest'] = 0
114
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i, "label": "./labelsTr/%s.nii.gz" % i} for i in
115
+ patient_names]
116
+ json_dict['test'] = []
117
+
118
+ save_json(json_dict, join(target_base, "dataset.json"))
119
+
120
+ downloaded_data_dir = "/home/sdp/MLPERF/Brats2019_DATA/MICCAI_BraTS_2019_Data_Validation"
121
+
122
+ for p in subdirs(downloaded_data_dir, join=False):
123
+ patdir = join(downloaded_data_dir, p)
124
+ patient_name = p
125
+ t1 = join(patdir, p + "_t1.nii.gz")
126
+ t1c = join(patdir, p + "_t1ce.nii.gz")
127
+ t2 = join(patdir, p + "_t2.nii.gz")
128
+ flair = join(patdir, p + "_flair.nii.gz")
129
+
130
+ assert all([
131
+ isfile(t1),
132
+ isfile(t1c),
133
+ isfile(t2),
134
+ isfile(flair),
135
+ ]), "%s" % patient_name
136
+
137
+ shutil.copy(t1, join(target_imagesVal, patient_name + "_0000.nii.gz"))
138
+ shutil.copy(t1c, join(target_imagesVal, patient_name + "_0001.nii.gz"))
139
+ shutil.copy(t2, join(target_imagesVal, patient_name + "_0002.nii.gz"))
140
+ shutil.copy(flair, join(target_imagesVal, patient_name + "_0003.nii.gz"))
141
+
142
+ """
143
+ #I dont have the testing data
144
+ downloaded_data_dir = "/home/fabian/Downloads/BraTS2018_train_val_test_data/MICCAI_BraTS_2018_Data_Testing_FIsensee"
145
+
146
+ for p in subdirs(downloaded_data_dir, join=False):
147
+ patdir = join(downloaded_data_dir, p)
148
+ patient_name = p
149
+ t1 = join(patdir, p + "_t1.nii.gz")
150
+ t1c = join(patdir, p + "_t1ce.nii.gz")
151
+ t2 = join(patdir, p + "_t2.nii.gz")
152
+ flair = join(patdir, p + "_flair.nii.gz")
153
+
154
+ assert all([
155
+ isfile(t1),
156
+ isfile(t1c),
157
+ isfile(t2),
158
+ isfile(flair),
159
+ ]), "%s" % patient_name
160
+
161
+ shutil.copy(t1, join(target_imagesTs, patient_name + "_0000.nii.gz"))
162
+ shutil.copy(t1c, join(target_imagesTs, patient_name + "_0001.nii.gz"))
163
+ shutil.copy(t2, join(target_imagesTs, patient_name + "_0002.nii.gz"))
164
+ shutil.copy(flair, join(target_imagesTs, patient_name + "_0003.nii.gz"))"""
nnunet/dataset_conversion/Task055_SegTHOR.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from collections import OrderedDict
17
+ from nnunet.paths import nnUNet_raw_data
18
+ from batchgenerators.utilities.file_and_folder_operations import *
19
+ import shutil
20
+ import SimpleITK as sitk
21
+
22
+
23
+ def convert_for_submission(source_dir, target_dir):
24
+ """
25
+ I believe they want .nii, not .nii.gz
26
+ :param source_dir:
27
+ :param target_dir:
28
+ :return:
29
+ """
30
+ files = subfiles(source_dir, suffix=".nii.gz", join=False)
31
+ maybe_mkdir_p(target_dir)
32
+ for f in files:
33
+ img = sitk.ReadImage(join(source_dir, f))
34
+ out_file = join(target_dir, f[:-7] + ".nii")
35
+ sitk.WriteImage(img, out_file)
36
+
37
+
38
+
39
+ if __name__ == "__main__":
40
+ base = "/media/fabian/DeepLearningData/SegTHOR"
41
+
42
+ task_id = 55
43
+ task_name = "SegTHOR"
44
+
45
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
46
+
47
+ out_base = join(nnUNet_raw_data, foldername)
48
+ imagestr = join(out_base, "imagesTr")
49
+ imagests = join(out_base, "imagesTs")
50
+ labelstr = join(out_base, "labelsTr")
51
+ maybe_mkdir_p(imagestr)
52
+ maybe_mkdir_p(imagests)
53
+ maybe_mkdir_p(labelstr)
54
+
55
+ train_patient_names = []
56
+ test_patient_names = []
57
+ train_patients = subfolders(join(base, "train"), join=False)
58
+ for p in train_patients:
59
+ curr = join(base, "train", p)
60
+ label_file = join(curr, "GT.nii.gz")
61
+ image_file = join(curr, p + ".nii.gz")
62
+ shutil.copy(image_file, join(imagestr, p + "_0000.nii.gz"))
63
+ shutil.copy(label_file, join(labelstr, p + ".nii.gz"))
64
+ train_patient_names.append(p)
65
+
66
+ test_patients = subfiles(join(base, "test"), join=False, suffix=".nii.gz")
67
+ for p in test_patients:
68
+ p = p[:-7]
69
+ curr = join(base, "test")
70
+ image_file = join(curr, p + ".nii.gz")
71
+ shutil.copy(image_file, join(imagests, p + "_0000.nii.gz"))
72
+ test_patient_names.append(p)
73
+
74
+
75
+ json_dict = OrderedDict()
76
+ json_dict['name'] = "SegTHOR"
77
+ json_dict['description'] = "SegTHOR"
78
+ json_dict['tensorImageSize'] = "4D"
79
+ json_dict['reference'] = "see challenge website"
80
+ json_dict['licence'] = "see challenge website"
81
+ json_dict['release'] = "0.0"
82
+ json_dict['modality'] = {
83
+ "0": "CT",
84
+ }
85
+ json_dict['labels'] = {
86
+ "0": "background",
87
+ "1": "esophagus",
88
+ "2": "heart",
89
+ "3": "trachea",
90
+ "4": "aorta",
91
+ }
92
+ json_dict['numTraining'] = len(train_patient_names)
93
+ json_dict['numTest'] = len(test_patient_names)
94
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i.split("/")[-1], "label": "./labelsTr/%s.nii.gz" % i.split("/")[-1]} for i in
95
+ train_patient_names]
96
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i.split("/")[-1] for i in test_patient_names]
97
+
98
+ save_json(json_dict, os.path.join(out_base, "dataset.json"))
nnunet/dataset_conversion/Task056_VerSe2019.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from collections import OrderedDict
17
+ import SimpleITK as sitk
18
+ from multiprocessing.pool import Pool
19
+ from nnunet.configuration import default_num_threads
20
+ from nnunet.paths import nnUNet_raw_data
21
+ from batchgenerators.utilities.file_and_folder_operations import *
22
+ import shutil
23
+ from medpy import metric
24
+ import numpy as np
25
+ from nnunet.utilities.image_reorientation import reorient_all_images_in_folder_to_ras
26
+
27
+
28
+ def check_if_all_in_good_orientation(imagesTr_folder: str, labelsTr_folder: str, output_folder: str) -> None:
29
+ maybe_mkdir_p(output_folder)
30
+ filenames = subfiles(labelsTr_folder, suffix='.nii.gz', join=False)
31
+ import matplotlib.pyplot as plt
32
+ for n in filenames:
33
+ img = sitk.GetArrayFromImage(sitk.ReadImage(join(imagesTr_folder, n[:-7] + '_0000.nii.gz')))
34
+ lab = sitk.GetArrayFromImage(sitk.ReadImage(join(labelsTr_folder, n)))
35
+ assert np.all([i == j for i, j in zip(img.shape, lab.shape)])
36
+ z_slice = img.shape[0] // 2
37
+ img_slice = img[z_slice]
38
+ lab_slice = lab[z_slice]
39
+ lab_slice[lab_slice != 0] = 1
40
+ img_slice = img_slice - img_slice.min()
41
+ img_slice = img_slice / img_slice.max()
42
+ stacked = np.vstack((img_slice, lab_slice))
43
+ print(stacked.shape)
44
+ plt.imsave(join(output_folder, n[:-7] + '.png'), stacked, cmap='gray')
45
+
46
+
47
+ def evaluate_verse_case(sitk_file_ref:str, sitk_file_test:str):
48
+ """
49
+ Only vertebra that are present in the reference will be evaluated
50
+ :param sitk_file_ref:
51
+ :param sitk_file_test:
52
+ :return:
53
+ """
54
+ gt_npy = sitk.GetArrayFromImage(sitk.ReadImage(sitk_file_ref))
55
+ pred_npy = sitk.GetArrayFromImage(sitk.ReadImage(sitk_file_test))
56
+ dice_scores = []
57
+ for label in range(1, 26):
58
+ mask_gt = gt_npy == label
59
+ if np.sum(mask_gt) > 0:
60
+ mask_pred = pred_npy == label
61
+ dc = metric.dc(mask_pred, mask_gt)
62
+ else:
63
+ dc = np.nan
64
+ dice_scores.append(dc)
65
+ return dice_scores
66
+
67
+
68
+ def evaluate_verse_folder(folder_pred, folder_gt, out_json="/home/fabian/verse.json"):
69
+ p = Pool(default_num_threads)
70
+ files_gt_bare = subfiles(folder_gt, join=False)
71
+ assert all([isfile(join(folder_pred, i)) for i in files_gt_bare]), "some files are missing in the predicted folder"
72
+ files_pred = [join(folder_pred, i) for i in files_gt_bare]
73
+ files_gt = [join(folder_gt, i) for i in files_gt_bare]
74
+
75
+ results = p.starmap_async(evaluate_verse_case, zip(files_gt, files_pred))
76
+
77
+ results = results.get()
78
+
79
+ dct = {i: j for i, j in zip(files_gt_bare, results)}
80
+
81
+ results_stacked = np.vstack(results)
82
+ results_mean = np.nanmean(results_stacked, 0)
83
+ overall_mean = np.nanmean(results_mean)
84
+
85
+ save_json((dct, list(results_mean), overall_mean), out_json)
86
+ p.close()
87
+ p.join()
88
+
89
+
90
+ def print_unique_labels_and_their_volumes(image: str, print_only_if_vol_smaller_than: float = None):
91
+ img = sitk.ReadImage(image)
92
+ voxel_volume = np.prod(img.GetSpacing())
93
+ img_npy = sitk.GetArrayFromImage(img)
94
+ uniques = [i for i in np.unique(img_npy) if i != 0]
95
+ volumes = {i: np.sum(img_npy == i) * voxel_volume for i in uniques}
96
+ print('')
97
+ print(image.split('/')[-1])
98
+ print('uniques:', uniques)
99
+ for k in volumes.keys():
100
+ v = volumes[k]
101
+ if print_only_if_vol_smaller_than is not None and v > print_only_if_vol_smaller_than:
102
+ pass
103
+ else:
104
+ print('k:', k, '\tvol:', volumes[k])
105
+
106
+
107
+ def remove_label(label_file: str, remove_this: int, replace_with: int = 0):
108
+ img = sitk.ReadImage(label_file)
109
+ img_npy = sitk.GetArrayFromImage(img)
110
+ img_npy[img_npy == remove_this] = replace_with
111
+ img2 = sitk.GetImageFromArray(img_npy)
112
+ img2.CopyInformation(img)
113
+ sitk.WriteImage(img2, label_file)
114
+
115
+
116
+ if __name__ == "__main__":
117
+ ### First we create a nnunet dataset from verse. After this the images will be all willy nilly in their
118
+ # orientation because that's how VerSe comes
119
+ base = '/media/fabian/DeepLearningData/VerSe2019'
120
+ base = "/home/fabian/data/VerSe2019"
121
+
122
+ # correct orientation
123
+ train_files_base = subfiles(join(base, "train"), join=False, suffix="_seg.nii.gz")
124
+ train_segs = [i[:-len("_seg.nii.gz")] + "_seg.nii.gz" for i in train_files_base]
125
+ train_data = [i[:-len("_seg.nii.gz")] + ".nii.gz" for i in train_files_base]
126
+ test_files_base = [i[:-len(".nii.gz")] for i in subfiles(join(base, "test"), join=False, suffix=".nii.gz")]
127
+ test_data = [i + ".nii.gz" for i in test_files_base]
128
+
129
+ task_id = 56
130
+ task_name = "VerSe"
131
+
132
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
133
+
134
+ out_base = join(nnUNet_raw_data, foldername)
135
+ imagestr = join(out_base, "imagesTr")
136
+ imagests = join(out_base, "imagesTs")
137
+ labelstr = join(out_base, "labelsTr")
138
+ maybe_mkdir_p(imagestr)
139
+ maybe_mkdir_p(imagests)
140
+ maybe_mkdir_p(labelstr)
141
+
142
+ train_patient_names = [i[:-len("_seg.nii.gz")] for i in subfiles(join(base, "train"), join=False, suffix="_seg.nii.gz")]
143
+ for p in train_patient_names:
144
+ curr = join(base, "train")
145
+ label_file = join(curr, p + "_seg.nii.gz")
146
+ image_file = join(curr, p + ".nii.gz")
147
+ shutil.copy(image_file, join(imagestr, p + "_0000.nii.gz"))
148
+ shutil.copy(label_file, join(labelstr, p + ".nii.gz"))
149
+
150
+ test_patient_names = [i[:-7] for i in subfiles(join(base, "test"), join=False, suffix=".nii.gz")]
151
+ for p in test_patient_names:
152
+ curr = join(base, "test")
153
+ image_file = join(curr, p + ".nii.gz")
154
+ shutil.copy(image_file, join(imagests, p + "_0000.nii.gz"))
155
+
156
+
157
+ json_dict = OrderedDict()
158
+ json_dict['name'] = "VerSe2019"
159
+ json_dict['description'] = "VerSe2019"
160
+ json_dict['tensorImageSize'] = "4D"
161
+ json_dict['reference'] = "see challenge website"
162
+ json_dict['licence'] = "see challenge website"
163
+ json_dict['release'] = "0.0"
164
+ json_dict['modality'] = {
165
+ "0": "CT",
166
+ }
167
+ json_dict['labels'] = {i: str(i) for i in range(26)}
168
+
169
+ json_dict['numTraining'] = len(train_patient_names)
170
+ json_dict['numTest'] = len(test_patient_names)
171
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i.split("/")[-1], "label": "./labelsTr/%s.nii.gz" % i.split("/")[-1]} for i in
172
+ train_patient_names]
173
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i.split("/")[-1] for i in test_patient_names]
174
+
175
+ save_json(json_dict, os.path.join(out_base, "dataset.json"))
176
+
177
+ # now we reorient all those images to ras. This saves a pkl with the original affine. We need this information to
178
+ # bring our predictions into the same geometry for submission
179
+ reorient_all_images_in_folder_to_ras(imagestr)
180
+ reorient_all_images_in_folder_to_ras(imagests)
181
+ reorient_all_images_in_folder_to_ras(labelstr)
182
+
183
+ # sanity check
184
+ check_if_all_in_good_orientation(imagestr, labelstr, join(out_base, 'sanitycheck'))
185
+ # looks good to me - proceed
186
+
187
+ # check the volumes of the vertebrae
188
+ _ = [print_unique_labels_and_their_volumes(i, 1000) for i in subfiles(labelstr, suffix='.nii.gz')]
189
+
190
+ # some cases appear fishy. For example, verse063.nii.gz has labels [1, 20, 21, 22, 23, 24] and 1 only has a volume
191
+ # of 63mm^3
192
+
193
+ #let's correct those
194
+
195
+ # 19 is connected to the image border and should not be segmented. Only one slice of 19 is segmented in the
196
+ # reference. Looks wrong
197
+ remove_label(join(labelstr, 'verse031.nii.gz'), 19, 0)
198
+
199
+ # spurious annotation of 18 (vol: 8.00)
200
+ remove_label(join(labelstr, 'verse060.nii.gz'), 18, 0)
201
+
202
+ # spurious annotation of 16 (vol: 3.00)
203
+ remove_label(join(labelstr, 'verse061.nii.gz'), 16, 0)
204
+
205
+ # spurious annotation of 1 (vol: 63.00) although the rest of the vertebra is [20, 21, 22, 23, 24]
206
+ remove_label(join(labelstr, 'verse063.nii.gz'), 1, 0)
207
+
208
+ # spurious annotation of 3 (vol: 9.53) although the rest of the vertebra is
209
+ # [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
210
+ remove_label(join(labelstr, 'verse074.nii.gz'), 3, 0)
211
+
212
+ # spurious annotation of 3 (vol: 15.00)
213
+ remove_label(join(labelstr, 'verse097.nii.gz'), 3, 0)
214
+
215
+ # spurious annotation of 3 (vol: 10) although the rest of the vertebra is
216
+ # [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
217
+ remove_label(join(labelstr, 'verse151.nii.gz'), 3, 0)
218
+
219
+ # spurious annotation of 25 (vol: 4) although the rest of the vertebra is
220
+ # [1, 2, 3, 4, 5, 6, 7, 8, 9]
221
+ remove_label(join(labelstr, 'verse201.nii.gz'), 25, 0)
222
+
223
+ # spurious annotation of 23 (vol: 8) although the rest of the vertebra is
224
+ # [1, 2, 3, 4, 5, 6, 7, 8]
225
+ remove_label(join(labelstr, 'verse207.nii.gz'), 23, 0)
226
+
227
+ # spurious annotation of 23 (vol: 12) although the rest of the vertebra is
228
+ # [1, 2, 3, 4, 5, 6, 7, 8, 9]
229
+ remove_label(join(labelstr, 'verse208.nii.gz'), 23, 0)
230
+
231
+ # spurious annotation of 23 (vol: 2) although the rest of the vertebra is
232
+ # [1, 2, 3, 4, 5, 6, 7, 8, 9]
233
+ remove_label(join(labelstr, 'verse212.nii.gz'), 23, 0)
234
+
235
+ # spurious annotation of 20 (vol: 4) although the rest of the vertebra is
236
+ # [1, 2, 3, 4, 5, 6, 7, 8, 9]
237
+ remove_label(join(labelstr, 'verse214.nii.gz'), 20, 0)
238
+
239
+ # spurious annotation of 23 (vol: 15) although the rest of the vertebra is
240
+ # [1, 2, 3, 4, 5, 6, 7, 8]
241
+ remove_label(join(labelstr, 'verse223.nii.gz'), 23, 0)
242
+
243
+ # spurious annotation of 23 (vol: 1) and 25 (vol: 7) although the rest of the vertebra is
244
+ # [1, 2, 3, 4, 5, 6, 7, 8, 9]
245
+ remove_label(join(labelstr, 'verse226.nii.gz'), 23, 0)
246
+ remove_label(join(labelstr, 'verse226.nii.gz'), 25, 0)
247
+
248
+ # spurious annotation of 25 (vol: 27) although the rest of the vertebra is
249
+ # [1, 2, 3, 4, 5, 6, 7, 8]
250
+ remove_label(join(labelstr, 'verse227.nii.gz'), 25, 0)
251
+
252
+ # spurious annotation of 20 (vol: 24) although the rest of the vertebra is
253
+ # [1, 2, 3, 4, 5, 6, 7, 8]
254
+ remove_label(join(labelstr, 'verse232.nii.gz'), 20, 0)
255
+
256
+
257
+ # Now we are ready to run nnU-Net
258
+
259
+
260
+ """# run this part of the code once training is done
261
+ folder_gt = "/media/fabian/My Book/MedicalDecathlon/nnUNet_raw_splitted/Task056_VerSe/labelsTr"
262
+
263
+ folder_pred = "/home/fabian/drives/datasets/results/nnUNet/3d_fullres/Task056_VerSe/nnUNetTrainerV2__nnUNetPlansv2.1/cv_niftis_raw"
264
+ out_json = "/home/fabian/Task056_VerSe_3d_fullres_summary.json"
265
+ evaluate_verse_folder(folder_pred, folder_gt, out_json)
266
+
267
+ folder_pred = "/home/fabian/drives/datasets/results/nnUNet/3d_lowres/Task056_VerSe/nnUNetTrainerV2__nnUNetPlansv2.1/cv_niftis_raw"
268
+ out_json = "/home/fabian/Task056_VerSe_3d_lowres_summary.json"
269
+ evaluate_verse_folder(folder_pred, folder_gt, out_json)
270
+
271
+ folder_pred = "/home/fabian/drives/datasets/results/nnUNet/3d_cascade_fullres/Task056_VerSe/nnUNetTrainerV2CascadeFullRes__nnUNetPlansv2.1/cv_niftis_raw"
272
+ out_json = "/home/fabian/Task056_VerSe_3d_cascade_fullres_summary.json"
273
+ evaluate_verse_folder(folder_pred, folder_gt, out_json)"""
274
+
nnunet/dataset_conversion/Task056_Verse_normalize_orientation.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ """
17
+ This code is copied from https://gist.github.com/nlessmann/24d405eaa82abba6676deb6be839266c. All credits go to the
18
+ original author (user nlessmann on GitHub)
19
+ """
20
+
21
+ import numpy as np
22
+ import SimpleITK as sitk
23
+
24
+
25
+ def reverse_axes(image):
26
+ return np.transpose(image, tuple(reversed(range(image.ndim))))
27
+
28
+
29
+ def read_image(imagefile):
30
+ image = sitk.ReadImage(imagefile)
31
+ data = reverse_axes(sitk.GetArrayFromImage(image)) # switch from zyx to xyz
32
+ header = {
33
+ 'spacing': image.GetSpacing(),
34
+ 'origin': image.GetOrigin(),
35
+ 'direction': image.GetDirection()
36
+ }
37
+ return data, header
38
+
39
+
40
+ def save_image(img: np.ndarray, header: dict, output_file: str):
41
+ """
42
+ CAREFUL you need to restore_original_slice_orientation before saving!
43
+ :param img:
44
+ :param header:
45
+ :return:
46
+ """
47
+ # reverse back
48
+ img = reverse_axes(img) # switch from zyx to xyz
49
+ img_itk = sitk.GetImageFromArray(img)
50
+ img_itk.SetSpacing(header['spacing'])
51
+ img_itk.SetOrigin(header['origin'])
52
+ if not isinstance(header['direction'], tuple):
53
+ img_itk.SetDirection(header['direction'].flatten())
54
+ else:
55
+ img_itk.SetDirection(header['direction'])
56
+
57
+ sitk.WriteImage(img_itk, output_file)
58
+
59
+
60
+ def swap_flip_dimensions(cosine_matrix, image, header=None):
61
+ # Compute swaps and flips
62
+ swap = np.argmax(abs(cosine_matrix), axis=0)
63
+ flip = np.sum(cosine_matrix, axis=0)
64
+
65
+ # Apply transformation to image volume
66
+ image = np.transpose(image, tuple(swap))
67
+ image = image[tuple(slice(None, None, int(f)) for f in flip)]
68
+
69
+ if header is None:
70
+ return image
71
+
72
+ # Apply transformation to header
73
+ header['spacing'] = tuple(header['spacing'][s] for s in swap)
74
+ header['direction'] = np.eye(3)
75
+
76
+ return image, header
77
+
78
+
79
+ def normalize_slice_orientation(image, header):
80
+ # Preserve original header so that we can easily transform back
81
+ header['original'] = header.copy()
82
+
83
+ # Compute inverse of cosine (round first because we assume 0/1 values only)
84
+ # to determine how the image has to be transposed and flipped for cosine = identity
85
+ cosine = np.asarray(header['direction']).reshape(3, 3)
86
+ cosine_inv = np.linalg.inv(np.round(cosine))
87
+
88
+ return swap_flip_dimensions(cosine_inv, image, header)
89
+
90
+
91
+ def restore_original_slice_orientation(mask, header):
92
+ # Use original orientation for transformation because we assume the image to be in
93
+ # normalized orientation, i.e., identity cosine)
94
+ cosine = np.asarray(header['original']['direction']).reshape(3, 3)
95
+ cosine_rnd = np.round(cosine)
96
+
97
+ # Apply transformations to both the image and the mask
98
+ return swap_flip_dimensions(cosine_rnd, mask), header['original']
nnunet/dataset_conversion/Task058_ISBI_EM_SEG.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from collections import OrderedDict
17
+
18
+ import SimpleITK as sitk
19
+ import numpy as np
20
+ from batchgenerators.utilities.file_and_folder_operations import *
21
+ from nnunet.paths import nnUNet_raw_data
22
+ from skimage import io
23
+
24
+
25
+ def export_for_submission(predicted_npz, out_file):
26
+ """
27
+ they expect us to submit a 32 bit 3d tif image with values between 0 (100% membrane certainty) and 1
28
+ (100% non-membrane certainty). We use the softmax output for that
29
+ :return:
30
+ """
31
+ a = np.load(predicted_npz)['softmax']
32
+ a = a / a.sum(0)[None]
33
+ # channel 0 is non-membrane prob
34
+ nonmembr_prob = a[0]
35
+ assert out_file.endswith(".tif")
36
+ io.imsave(out_file, nonmembr_prob.astype(np.float32))
37
+
38
+
39
+
40
+ if __name__ == "__main__":
41
+ # download from here http://brainiac2.mit.edu/isbi_challenge/downloads
42
+
43
+ base = "/media/fabian/My Book/datasets/ISBI_EM_SEG"
44
+ # the orientation of VerSe is all fing over the place. run fslreorient2std to correct that (hopefully!)
45
+ # THIS CAN HAVE CONSEQUENCES FOR THE TEST SET SUBMISSION! CAREFUL!
46
+ train_volume = io.imread(join(base, "train-volume.tif"))
47
+ train_labels = io.imread(join(base, "train-labels.tif"))
48
+ train_labels[train_labels == 255] = 1
49
+ test_volume = io.imread(join(base, "test-volume.tif"))
50
+
51
+ task_id = 58
52
+ task_name = "ISBI_EM_SEG"
53
+
54
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
55
+
56
+ out_base = join(nnUNet_raw_data, foldername)
57
+ imagestr = join(out_base, "imagesTr")
58
+ imagests = join(out_base, "imagesTs")
59
+ labelstr = join(out_base, "labelsTr")
60
+ maybe_mkdir_p(imagestr)
61
+ maybe_mkdir_p(imagests)
62
+ maybe_mkdir_p(labelstr)
63
+
64
+ img_tr_itk = sitk.GetImageFromArray(train_volume.astype(np.float32))
65
+ lab_tr_itk = sitk.GetImageFromArray(1 - train_labels) # walls are foreground, cells background
66
+ img_te_itk = sitk.GetImageFromArray(test_volume.astype(np.float32))
67
+
68
+ img_tr_itk.SetSpacing((4, 4, 50))
69
+ lab_tr_itk.SetSpacing((4, 4, 50))
70
+ img_te_itk.SetSpacing((4, 4, 50))
71
+
72
+ # 5 copies, otherwise we cannot run nnunet (5 fold cv needs that)
73
+ sitk.WriteImage(img_tr_itk, join(imagestr, "training0_0000.nii.gz"))
74
+ sitk.WriteImage(img_tr_itk, join(imagestr, "training1_0000.nii.gz"))
75
+ sitk.WriteImage(img_tr_itk, join(imagestr, "training2_0000.nii.gz"))
76
+ sitk.WriteImage(img_tr_itk, join(imagestr, "training3_0000.nii.gz"))
77
+ sitk.WriteImage(img_tr_itk, join(imagestr, "training4_0000.nii.gz"))
78
+
79
+ sitk.WriteImage(lab_tr_itk, join(labelstr, "training0.nii.gz"))
80
+ sitk.WriteImage(lab_tr_itk, join(labelstr, "training1.nii.gz"))
81
+ sitk.WriteImage(lab_tr_itk, join(labelstr, "training2.nii.gz"))
82
+ sitk.WriteImage(lab_tr_itk, join(labelstr, "training3.nii.gz"))
83
+ sitk.WriteImage(lab_tr_itk, join(labelstr, "training4.nii.gz"))
84
+
85
+ sitk.WriteImage(img_te_itk, join(imagests, "testing.nii.gz"))
86
+
87
+ json_dict = OrderedDict()
88
+ json_dict['name'] = task_name
89
+ json_dict['description'] = task_name
90
+ json_dict['tensorImageSize'] = "4D"
91
+ json_dict['reference'] = "see challenge website"
92
+ json_dict['licence'] = "see challenge website"
93
+ json_dict['release'] = "0.0"
94
+ json_dict['modality'] = {
95
+ "0": "EM",
96
+ }
97
+ json_dict['labels'] = {i: str(i) for i in range(2)}
98
+
99
+ json_dict['numTraining'] = 5
100
+ json_dict['numTest'] = 1
101
+ json_dict['training'] = [{'image': "./imagesTr/training%d.nii.gz" % i, "label": "./labelsTr/training%d.nii.gz" % i} for i in
102
+ range(5)]
103
+ json_dict['test'] = ["./imagesTs/testing.nii.gz"]
104
+
105
+ save_json(json_dict, os.path.join(out_base, "dataset.json"))
nnunet/dataset_conversion/Task059_EPFL_EM_MITO_SEG.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import numpy as np
17
+ import subprocess
18
+ from collections import OrderedDict
19
+ from nnunet.paths import nnUNet_raw_data
20
+ from batchgenerators.utilities.file_and_folder_operations import *
21
+ import shutil
22
+ from skimage import io
23
+ import SimpleITK as sitk
24
+ import shutil
25
+
26
+
27
+ if __name__ == "__main__":
28
+ # download from here https://www.epfl.ch/labs/cvlab/data/data-em/
29
+
30
+ base = "/media/fabian/My Book/datasets/EPFL_MITO_SEG"
31
+ # the orientation of VerSe is all fing over the place. run fslreorient2std to correct that (hopefully!)
32
+ # THIS CAN HAVE CONSEQUENCES FOR THE TEST SET SUBMISSION! CAREFUL!
33
+ train_volume = io.imread(join(base, "training.tif"))
34
+ train_labels = io.imread(join(base, "training_groundtruth.tif"))
35
+ train_labels[train_labels == 255] = 1
36
+ test_volume = io.imread(join(base, "testing.tif"))
37
+ test_labels = io.imread(join(base, "testing_groundtruth.tif"))
38
+ test_labels[test_labels == 255] = 1
39
+
40
+ task_id = 59
41
+ task_name = "EPFL_EM_MITO_SEG"
42
+
43
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
44
+
45
+ out_base = join(nnUNet_raw_data, foldername)
46
+ imagestr = join(out_base, "imagesTr")
47
+ imagests = join(out_base, "imagesTs")
48
+ labelstr = join(out_base, "labelsTr")
49
+ labelste = join(out_base, "labelsTs")
50
+ maybe_mkdir_p(imagestr)
51
+ maybe_mkdir_p(imagests)
52
+ maybe_mkdir_p(labelstr)
53
+ maybe_mkdir_p(labelste)
54
+
55
+ img_tr_itk = sitk.GetImageFromArray(train_volume.astype(np.float32))
56
+ lab_tr_itk = sitk.GetImageFromArray(train_labels.astype(np.uint8))
57
+ img_te_itk = sitk.GetImageFromArray(test_volume.astype(np.float32))
58
+ lab_te_itk = sitk.GetImageFromArray(test_labels.astype(np.uint8))
59
+
60
+ img_tr_itk.SetSpacing((5, 5, 5))
61
+ lab_tr_itk.SetSpacing((5, 5, 5))
62
+ img_te_itk.SetSpacing((5, 5, 5))
63
+ lab_te_itk.SetSpacing((5, 5, 5))
64
+
65
+ # 5 copies, otherwise we cannot run nnunet (5 fold cv needs that)
66
+ sitk.WriteImage(img_tr_itk, join(imagestr, "training0_0000.nii.gz"))
67
+ shutil.copy(join(imagestr, "training0_0000.nii.gz"), join(imagestr, "training1_0000.nii.gz"))
68
+ shutil.copy(join(imagestr, "training0_0000.nii.gz"), join(imagestr, "training2_0000.nii.gz"))
69
+ shutil.copy(join(imagestr, "training0_0000.nii.gz"), join(imagestr, "training3_0000.nii.gz"))
70
+ shutil.copy(join(imagestr, "training0_0000.nii.gz"), join(imagestr, "training4_0000.nii.gz"))
71
+
72
+ sitk.WriteImage(lab_tr_itk, join(labelstr, "training0.nii.gz"))
73
+ shutil.copy(join(labelstr, "training0.nii.gz"), join(labelstr, "training1.nii.gz"))
74
+ shutil.copy(join(labelstr, "training0.nii.gz"), join(labelstr, "training2.nii.gz"))
75
+ shutil.copy(join(labelstr, "training0.nii.gz"), join(labelstr, "training3.nii.gz"))
76
+ shutil.copy(join(labelstr, "training0.nii.gz"), join(labelstr, "training4.nii.gz"))
77
+
78
+ sitk.WriteImage(img_te_itk, join(imagests, "testing.nii.gz"))
79
+ sitk.WriteImage(lab_te_itk, join(labelste, "testing.nii.gz"))
80
+
81
+ json_dict = OrderedDict()
82
+ json_dict['name'] = task_name
83
+ json_dict['description'] = task_name
84
+ json_dict['tensorImageSize'] = "4D"
85
+ json_dict['reference'] = "see challenge website"
86
+ json_dict['licence'] = "see challenge website"
87
+ json_dict['release'] = "0.0"
88
+ json_dict['modality'] = {
89
+ "0": "EM",
90
+ }
91
+ json_dict['labels'] = {i: str(i) for i in range(2)}
92
+
93
+ json_dict['numTraining'] = 5
94
+ json_dict['numTest'] = 1
95
+ json_dict['training'] = [{'image': "./imagesTr/training%d.nii.gz" % i, "label": "./labelsTr/training%d.nii.gz" % i} for i in
96
+ range(5)]
97
+ json_dict['test'] = ["./imagesTs/testing.nii.gz"]
98
+
99
+ save_json(json_dict, os.path.join(out_base, "dataset.json"))
nnunet/dataset_conversion/Task061_CREMI.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from collections import OrderedDict
17
+
18
+ from batchgenerators.utilities.file_and_folder_operations import *
19
+ import numpy as np
20
+ from nnunet.paths import nnUNet_raw_data, preprocessing_output_dir
21
+ import shutil
22
+ import SimpleITK as sitk
23
+
24
+ try:
25
+ import h5py
26
+ except ImportError:
27
+ h5py = None
28
+
29
+
30
+ def load_sample(filename):
31
+ # we need raw data and seg
32
+ f = h5py.File(filename, 'r')
33
+ data = np.array(f['volumes']['raw'])
34
+
35
+ if 'labels' in f['volumes'].keys():
36
+ labels = np.array(f['volumes']['labels']['clefts'])
37
+ # clefts are low values, background is high
38
+ labels = (labels < 100000).astype(np.uint8)
39
+ else:
40
+ labels = None
41
+ return data, labels
42
+
43
+
44
+ def save_as_nifti(arr, filename, spacing):
45
+ itk_img = sitk.GetImageFromArray(arr)
46
+ itk_img.SetSpacing(spacing)
47
+ sitk.WriteImage(itk_img, filename)
48
+
49
+
50
+ def prepare_submission():
51
+ from cremi.io import CremiFile
52
+ from cremi.Volume import Volume
53
+
54
+ base = "/home/fabian/drives/datasets/results/nnUNet/test_sets/Task061_CREMI/"
55
+ # a+
56
+ pred = sitk.GetArrayFromImage(sitk.ReadImage(join(base, 'results_3d_fullres', "sample_a+.nii.gz"))).astype(np.uint64)
57
+ pred[pred == 0] = 0xffffffffffffffff
58
+ out_a = CremiFile(join(base, 'sample_A+_20160601.hdf'), 'w')
59
+ clefts = Volume(pred, (40., 4., 4.))
60
+ out_a.write_clefts(clefts)
61
+ out_a.close()
62
+
63
+ pred = sitk.GetArrayFromImage(sitk.ReadImage(join(base, 'results_3d_fullres', "sample_b+.nii.gz"))).astype(np.uint64)
64
+ pred[pred == 0] = 0xffffffffffffffff
65
+ out_b = CremiFile(join(base, 'sample_B+_20160601.hdf'), 'w')
66
+ clefts = Volume(pred, (40., 4., 4.))
67
+ out_b.write_clefts(clefts)
68
+ out_b.close()
69
+
70
+ pred = sitk.GetArrayFromImage(sitk.ReadImage(join(base, 'results_3d_fullres', "sample_c+.nii.gz"))).astype(np.uint64)
71
+ pred[pred == 0] = 0xffffffffffffffff
72
+ out_c = CremiFile(join(base, 'sample_C+_20160601.hdf'), 'w')
73
+ clefts = Volume(pred, (40., 4., 4.))
74
+ out_c.write_clefts(clefts)
75
+ out_c.close()
76
+
77
+
78
+ if __name__ == "__main__":
79
+ assert h5py is not None, "you need h5py for this. Install with 'pip install h5py'"
80
+
81
+ foldername = "Task061_CREMI"
82
+ out_base = join(nnUNet_raw_data, foldername)
83
+ imagestr = join(out_base, "imagesTr")
84
+ imagests = join(out_base, "imagesTs")
85
+ labelstr = join(out_base, "labelsTr")
86
+ maybe_mkdir_p(imagestr)
87
+ maybe_mkdir_p(imagests)
88
+ maybe_mkdir_p(labelstr)
89
+
90
+ base = "/media/fabian/My Book/datasets/CREMI"
91
+
92
+ # train
93
+ img, label = load_sample(join(base, "sample_A_20160501.hdf"))
94
+ save_as_nifti(img, join(imagestr, "sample_a_0000.nii.gz"), (4, 4, 40))
95
+ save_as_nifti(label, join(labelstr, "sample_a.nii.gz"), (4, 4, 40))
96
+ img, label = load_sample(join(base, "sample_B_20160501.hdf"))
97
+ save_as_nifti(img, join(imagestr, "sample_b_0000.nii.gz"), (4, 4, 40))
98
+ save_as_nifti(label, join(labelstr, "sample_b.nii.gz"), (4, 4, 40))
99
+ img, label = load_sample(join(base, "sample_C_20160501.hdf"))
100
+ save_as_nifti(img, join(imagestr, "sample_c_0000.nii.gz"), (4, 4, 40))
101
+ save_as_nifti(label, join(labelstr, "sample_c.nii.gz"), (4, 4, 40))
102
+
103
+ save_as_nifti(img, join(imagestr, "sample_d_0000.nii.gz"), (4, 4, 40))
104
+ save_as_nifti(label, join(labelstr, "sample_d.nii.gz"), (4, 4, 40))
105
+
106
+ save_as_nifti(img, join(imagestr, "sample_e_0000.nii.gz"), (4, 4, 40))
107
+ save_as_nifti(label, join(labelstr, "sample_e.nii.gz"), (4, 4, 40))
108
+
109
+ # test
110
+ img, label = load_sample(join(base, "sample_A+_20160601.hdf"))
111
+ save_as_nifti(img, join(imagests, "sample_a+_0000.nii.gz"), (4, 4, 40))
112
+ img, label = load_sample(join(base, "sample_B+_20160601.hdf"))
113
+ save_as_nifti(img, join(imagests, "sample_b+_0000.nii.gz"), (4, 4, 40))
114
+ img, label = load_sample(join(base, "sample_C+_20160601.hdf"))
115
+ save_as_nifti(img, join(imagests, "sample_c+_0000.nii.gz"), (4, 4, 40))
116
+
117
+ json_dict = OrderedDict()
118
+ json_dict['name'] = foldername
119
+ json_dict['description'] = foldername
120
+ json_dict['tensorImageSize'] = "4D"
121
+ json_dict['reference'] = "see challenge website"
122
+ json_dict['licence'] = "see challenge website"
123
+ json_dict['release'] = "0.0"
124
+ json_dict['modality'] = {
125
+ "0": "EM",
126
+ }
127
+ json_dict['labels'] = {i: str(i) for i in range(2)}
128
+
129
+ json_dict['numTraining'] = 5
130
+ json_dict['numTest'] = 1
131
+ json_dict['training'] = [{'image': "./imagesTr/sample_%s.nii.gz" % i, "label": "./labelsTr/sample_%s.nii.gz" % i} for i in
132
+ ['a', 'b', 'c', 'd', 'e']]
133
+
134
+ json_dict['test'] = ["./imagesTs/sample_a+.nii.gz", "./imagesTs/sample_b+.nii.gz", "./imagesTs/sample_c+.nii.gz"]
135
+
136
+ save_json(json_dict, os.path.join(out_base, "dataset.json"))
137
+
138
+ out_preprocessed = join(preprocessing_output_dir, foldername)
139
+ maybe_mkdir_p(out_preprocessed)
140
+ # manual splits. we train 5 models on all three datasets
141
+ splits = [{'train': ["sample_a", "sample_b", "sample_c"], 'val': ["sample_a", "sample_b", "sample_c"]},
142
+ {'train': ["sample_a", "sample_b", "sample_c"], 'val': ["sample_a", "sample_b", "sample_c"]},
143
+ {'train': ["sample_a", "sample_b", "sample_c"], 'val': ["sample_a", "sample_b", "sample_c"]},
144
+ {'train': ["sample_a", "sample_b", "sample_c"], 'val': ["sample_a", "sample_b", "sample_c"]},
145
+ {'train': ["sample_a", "sample_b", "sample_c"], 'val': ["sample_a", "sample_b", "sample_c"]}]
146
+ save_pickle(splits, join(out_preprocessed, "splits_final.pkl"))
nnunet/dataset_conversion/Task062_NIHPancreas.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from collections import OrderedDict
17
+ from nnunet.paths import nnUNet_raw_data
18
+ from batchgenerators.utilities.file_and_folder_operations import *
19
+ import shutil
20
+ from multiprocessing import Pool
21
+ import nibabel
22
+
23
+
24
+ def reorient(filename):
25
+ img = nibabel.load(filename)
26
+ img = nibabel.as_closest_canonical(img)
27
+ nibabel.save(img, filename)
28
+
29
+
30
+ if __name__ == "__main__":
31
+ base = "/media/fabian/DeepLearningData/Pancreas-CT"
32
+
33
+ # reorient
34
+ p = Pool(8)
35
+ results = []
36
+
37
+ for f in subfiles(join(base, "data"), suffix=".nii.gz"):
38
+ results.append(p.map_async(reorient, (f, )))
39
+ _ = [i.get() for i in results]
40
+
41
+ for f in subfiles(join(base, "TCIA_pancreas_labels-02-05-2017"), suffix=".nii.gz"):
42
+ results.append(p.map_async(reorient, (f, )))
43
+ _ = [i.get() for i in results]
44
+
45
+ task_id = 62
46
+ task_name = "NIHPancreas"
47
+
48
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
49
+
50
+ out_base = join(nnUNet_raw_data, foldername)
51
+ imagestr = join(out_base, "imagesTr")
52
+ imagests = join(out_base, "imagesTs")
53
+ labelstr = join(out_base, "labelsTr")
54
+ maybe_mkdir_p(imagestr)
55
+ maybe_mkdir_p(imagests)
56
+ maybe_mkdir_p(labelstr)
57
+
58
+ train_patient_names = []
59
+ test_patient_names = []
60
+ cases = list(range(1, 83))
61
+ folder_data = join(base, "data")
62
+ folder_labels = join(base, "TCIA_pancreas_labels-02-05-2017")
63
+ for c in cases:
64
+ casename = "pancreas_%04.0d" % c
65
+ shutil.copy(join(folder_data, "PANCREAS_%04.0d.nii.gz" % c), join(imagestr, casename + "_0000.nii.gz"))
66
+ shutil.copy(join(folder_labels, "label%04.0d.nii.gz" % c), join(labelstr, casename + ".nii.gz"))
67
+ train_patient_names.append(casename)
68
+
69
+ json_dict = OrderedDict()
70
+ json_dict['name'] = task_name
71
+ json_dict['description'] = task_name
72
+ json_dict['tensorImageSize'] = "4D"
73
+ json_dict['reference'] = "see website"
74
+ json_dict['licence'] = "see website"
75
+ json_dict['release'] = "0.0"
76
+ json_dict['modality'] = {
77
+ "0": "CT",
78
+ }
79
+ json_dict['labels'] = {
80
+ "0": "background",
81
+ "1": "Pancreas",
82
+ }
83
+ json_dict['numTraining'] = len(train_patient_names)
84
+ json_dict['numTest'] = len(test_patient_names)
85
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i.split("/")[-1], "label": "./labelsTr/%s.nii.gz" % i.split("/")[-1]} for i in
86
+ train_patient_names]
87
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i.split("/")[-1] for i in test_patient_names]
88
+
89
+ save_json(json_dict, os.path.join(out_base, "dataset.json"))
nnunet/dataset_conversion/Task064_KiTS_labelsFixed.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import shutil
17
+ from batchgenerators.utilities.file_and_folder_operations import *
18
+ from nnunet.paths import nnUNet_raw_data
19
+
20
+
21
+ if __name__ == "__main__":
22
+ """
23
+ This is the KiTS dataset after Nick fixed all the labels that had errors. Downloaded on Jan 6th 2020
24
+ """
25
+
26
+ base = "/media/fabian/My Book/datasets/KiTS_clean/kits19/data"
27
+
28
+ task_id = 64
29
+ task_name = "KiTS_labelsFixed"
30
+
31
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
32
+
33
+ out_base = join(nnUNet_raw_data, foldername)
34
+ imagestr = join(out_base, "imagesTr")
35
+ imagests = join(out_base, "imagesTs")
36
+ labelstr = join(out_base, "labelsTr")
37
+ maybe_mkdir_p(imagestr)
38
+ maybe_mkdir_p(imagests)
39
+ maybe_mkdir_p(labelstr)
40
+
41
+ train_patient_names = []
42
+ test_patient_names = []
43
+ all_cases = subfolders(base, join=False)
44
+
45
+ train_patients = all_cases[:210]
46
+ test_patients = all_cases[210:]
47
+
48
+ for p in train_patients:
49
+ curr = join(base, p)
50
+ label_file = join(curr, "segmentation.nii.gz")
51
+ image_file = join(curr, "imaging.nii.gz")
52
+ shutil.copy(image_file, join(imagestr, p + "_0000.nii.gz"))
53
+ shutil.copy(label_file, join(labelstr, p + ".nii.gz"))
54
+ train_patient_names.append(p)
55
+
56
+ for p in test_patients:
57
+ curr = join(base, p)
58
+ image_file = join(curr, "imaging.nii.gz")
59
+ shutil.copy(image_file, join(imagests, p + "_0000.nii.gz"))
60
+ test_patient_names.append(p)
61
+
62
+ json_dict = {}
63
+ json_dict['name'] = "KiTS"
64
+ json_dict['description'] = "kidney and kidney tumor segmentation"
65
+ json_dict['tensorImageSize'] = "4D"
66
+ json_dict['reference'] = "KiTS data for nnunet"
67
+ json_dict['licence'] = ""
68
+ json_dict['release'] = "0.0"
69
+ json_dict['modality'] = {
70
+ "0": "CT",
71
+ }
72
+ json_dict['labels'] = {
73
+ "0": "background",
74
+ "1": "Kidney",
75
+ "2": "Tumor"
76
+ }
77
+
78
+ json_dict['numTraining'] = len(train_patient_names)
79
+ json_dict['numTest'] = len(test_patient_names)
80
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i.split("/")[-1], "label": "./labelsTr/%s.nii.gz" % i.split("/")[-1]} for i in
81
+ train_patient_names]
82
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i.split("/")[-1] for i in test_patient_names]
83
+
84
+ save_json(json_dict, os.path.join(out_base, "dataset.json"))
nnunet/dataset_conversion/Task065_KiTS_NicksLabels.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import shutil
17
+
18
+ from batchgenerators.utilities.file_and_folder_operations import *
19
+ from nnunet.paths import nnUNet_raw_data
20
+
21
+ if __name__ == "__main__":
22
+ """
23
+ Nick asked me to rerun the training with other labels (the Kidney region is defined differently).
24
+
25
+ These labels operate in interpolated spacing. I don't like that but that's how it is
26
+ """
27
+
28
+ base = "/media/fabian/My Book/datasets/KiTS_NicksLabels/kits19/data"
29
+ labelsdir = "/media/fabian/My Book/datasets/KiTS_NicksLabels/filled_labels"
30
+
31
+ task_id = 65
32
+ task_name = "KiTS_NicksLabels"
33
+
34
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
35
+
36
+ out_base = join(nnUNet_raw_data, foldername)
37
+ imagestr = join(out_base, "imagesTr")
38
+ imagests = join(out_base, "imagesTs")
39
+ labelstr = join(out_base, "labelsTr")
40
+ maybe_mkdir_p(imagestr)
41
+ maybe_mkdir_p(imagests)
42
+ maybe_mkdir_p(labelstr)
43
+
44
+ train_patient_names = []
45
+ test_patient_names = []
46
+ all_cases = subfolders(base, join=False)
47
+
48
+ train_patients = all_cases[:210]
49
+ test_patients = all_cases[210:]
50
+
51
+ for p in train_patients:
52
+ curr = join(base, p)
53
+ label_file = join(labelsdir, p + ".nii.gz")
54
+ image_file = join(curr, "imaging.nii.gz")
55
+ shutil.copy(image_file, join(imagestr, p + "_0000.nii.gz"))
56
+ shutil.copy(label_file, join(labelstr, p + ".nii.gz"))
57
+ train_patient_names.append(p)
58
+
59
+ for p in test_patients:
60
+ curr = join(base, p)
61
+ image_file = join(curr, "imaging.nii.gz")
62
+ shutil.copy(image_file, join(imagests, p + "_0000.nii.gz"))
63
+ test_patient_names.append(p)
64
+
65
+ json_dict = {}
66
+ json_dict['name'] = "KiTS"
67
+ json_dict['description'] = "kidney and kidney tumor segmentation"
68
+ json_dict['tensorImageSize'] = "4D"
69
+ json_dict['reference'] = "KiTS data for nnunet"
70
+ json_dict['licence'] = ""
71
+ json_dict['release'] = "0.0"
72
+ json_dict['modality'] = {
73
+ "0": "CT",
74
+ }
75
+ json_dict['labels'] = {
76
+ "0": "background",
77
+ "1": "Kidney",
78
+ "2": "Tumor"
79
+ }
80
+
81
+ json_dict['numTraining'] = len(train_patient_names)
82
+ json_dict['numTest'] = len(test_patient_names)
83
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i.split("/")[-1], "label": "./labelsTr/%s.nii.gz" % i.split("/")[-1]} for i in
84
+ train_patient_names]
85
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i.split("/")[-1] for i in test_patient_names]
86
+
87
+ save_json(json_dict, os.path.join(out_base, "dataset.json"))
nnunet/dataset_conversion/Task069_CovidSeg.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import shutil
2
+
3
+ from batchgenerators.utilities.file_and_folder_operations import *
4
+ import SimpleITK as sitk
5
+ from nnunet.paths import nnUNet_raw_data
6
+
7
+ if __name__ == '__main__':
8
+ #data is available at http://medicalsegmentation.com/covid19/
9
+ download_dir = '/home/fabian/Downloads'
10
+
11
+ task_id = 69
12
+ task_name = "CovidSeg"
13
+
14
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
15
+
16
+ out_base = join(nnUNet_raw_data, foldername)
17
+ imagestr = join(out_base, "imagesTr")
18
+ imagests = join(out_base, "imagesTs")
19
+ labelstr = join(out_base, "labelsTr")
20
+ maybe_mkdir_p(imagestr)
21
+ maybe_mkdir_p(imagests)
22
+ maybe_mkdir_p(labelstr)
23
+
24
+ train_patient_names = []
25
+ test_patient_names = []
26
+
27
+ # the niftis are 3d, but they are just stacks of 2d slices from different patients. So no 3d U-Net, please
28
+
29
+ # the training stack has 100 slices, so we split it into 5 equally sized parts (20 slices each) for cross-validation
30
+ training_data = sitk.GetArrayFromImage(sitk.ReadImage(join(download_dir, 'tr_im.nii.gz')))
31
+ training_labels = sitk.GetArrayFromImage(sitk.ReadImage(join(download_dir, 'tr_mask.nii.gz')))
32
+
33
+ for f in range(5):
34
+ this_name = 'part_%d' % f
35
+ data = training_data[f::5]
36
+ labels = training_labels[f::5]
37
+ sitk.WriteImage(sitk.GetImageFromArray(data), join(imagestr, this_name + '_0000.nii.gz'))
38
+ sitk.WriteImage(sitk.GetImageFromArray(labels), join(labelstr, this_name + '.nii.gz'))
39
+ train_patient_names.append(this_name)
40
+
41
+ shutil.copy(join(download_dir, 'val_im.nii.gz'), join(imagests, 'val_im.nii.gz'))
42
+
43
+ test_patient_names.append('val_im')
44
+
45
+ json_dict = {}
46
+ json_dict['name'] = task_name
47
+ json_dict['description'] = ""
48
+ json_dict['tensorImageSize'] = "4D"
49
+ json_dict['reference'] = ""
50
+ json_dict['licence'] = ""
51
+ json_dict['release'] = "0.0"
52
+ json_dict['modality'] = {
53
+ "0": "nonct",
54
+ }
55
+ json_dict['labels'] = {
56
+ "0": "background",
57
+ "1": "stuff1",
58
+ "2": "stuff2",
59
+ "3": "stuff3",
60
+ }
61
+
62
+ json_dict['numTraining'] = len(train_patient_names)
63
+ json_dict['numTest'] = len(test_patient_names)
64
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i.split("/")[-1], "label": "./labelsTr/%s.nii.gz" % i.split("/")[-1]} for i in
65
+ train_patient_names]
66
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i.split("/")[-1] for i in test_patient_names]
67
+
68
+ save_json(json_dict, os.path.join(out_base, "dataset.json"))
nnunet/dataset_conversion/Task075_Fluo_C3DH_A549_ManAndSim.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from multiprocessing import Pool
16
+ import SimpleITK as sitk
17
+ import numpy as np
18
+ from batchgenerators.utilities.file_and_folder_operations import *
19
+ from nnunet.paths import nnUNet_raw_data
20
+ from nnunet.paths import preprocessing_output_dir
21
+ from skimage.io import imread
22
+
23
+
24
+ def load_tiff_convert_to_nifti(img_file, lab_file, img_out_base, anno_out, spacing):
25
+ img = imread(img_file)
26
+ img_itk = sitk.GetImageFromArray(img.astype(np.float32))
27
+ img_itk.SetSpacing(np.array(spacing)[::-1])
28
+ sitk.WriteImage(img_itk, join(img_out_base + "_0000.nii.gz"))
29
+
30
+ if lab_file is not None:
31
+ l = imread(lab_file)
32
+ l[l > 0] = 1
33
+ l_itk = sitk.GetImageFromArray(l.astype(np.uint8))
34
+ l_itk.SetSpacing(np.array(spacing)[::-1])
35
+ sitk.WriteImage(l_itk, anno_out)
36
+
37
+
38
+ def prepare_task(base, task_id, task_name, spacing):
39
+ p = Pool(16)
40
+
41
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
42
+
43
+ out_base = join(nnUNet_raw_data, foldername)
44
+ imagestr = join(out_base, "imagesTr")
45
+ imagests = join(out_base, "imagesTs")
46
+ labelstr = join(out_base, "labelsTr")
47
+ maybe_mkdir_p(imagestr)
48
+ maybe_mkdir_p(imagests)
49
+ maybe_mkdir_p(labelstr)
50
+
51
+ train_patient_names = []
52
+ test_patient_names = []
53
+ res = []
54
+
55
+ for train_sequence in [i for i in subfolders(base + "_train", join=False) if not i.endswith("_GT")]:
56
+ train_cases = subfiles(join(base + '_train', train_sequence), suffix=".tif", join=False)
57
+ for t in train_cases:
58
+ casename = train_sequence + "_" + t[:-4]
59
+ img_file = join(base + '_train', train_sequence, t)
60
+ lab_file = join(base + '_train', train_sequence + "_GT", "SEG", "man_seg" + t[1:])
61
+ if not isfile(lab_file):
62
+ continue
63
+ img_out_base = join(imagestr, casename)
64
+ anno_out = join(labelstr, casename + ".nii.gz")
65
+ res.append(
66
+ p.starmap_async(load_tiff_convert_to_nifti, ((img_file, lab_file, img_out_base, anno_out, spacing),)))
67
+ train_patient_names.append(casename)
68
+
69
+ for test_sequence in [i for i in subfolders(base + "_test", join=False) if not i.endswith("_GT")]:
70
+ test_cases = subfiles(join(base + '_test', test_sequence), suffix=".tif", join=False)
71
+ for t in test_cases:
72
+ casename = test_sequence + "_" + t[:-4]
73
+ img_file = join(base + '_test', test_sequence, t)
74
+ lab_file = None
75
+ img_out_base = join(imagests, casename)
76
+ anno_out = None
77
+ res.append(
78
+ p.starmap_async(load_tiff_convert_to_nifti, ((img_file, lab_file, img_out_base, anno_out, spacing),)))
79
+ test_patient_names.append(casename)
80
+
81
+ _ = [i.get() for i in res]
82
+
83
+ json_dict = {}
84
+ json_dict['name'] = task_name
85
+ json_dict['description'] = ""
86
+ json_dict['tensorImageSize'] = "4D"
87
+ json_dict['reference'] = ""
88
+ json_dict['licence'] = ""
89
+ json_dict['release'] = "0.0"
90
+ json_dict['modality'] = {
91
+ "0": "BF",
92
+ }
93
+ json_dict['labels'] = {
94
+ "0": "background",
95
+ "1": "cell",
96
+ }
97
+
98
+ json_dict['numTraining'] = len(train_patient_names)
99
+ json_dict['numTest'] = len(test_patient_names)
100
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i, "label": "./labelsTr/%s.nii.gz" % i} for i in
101
+ train_patient_names]
102
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i for i in test_patient_names]
103
+
104
+ save_json(json_dict, os.path.join(out_base, "dataset.json"))
105
+ p.close()
106
+ p.join()
107
+
108
+
109
+ if __name__ == "__main__":
110
+ base = "/media/fabian/My Book/datasets/CellTrackingChallenge/Fluo-C3DH-A549_ManAndSim"
111
+ task_id = 75
112
+ task_name = 'Fluo_C3DH_A549_ManAndSim'
113
+ spacing = (1, 0.126, 0.126)
114
+ prepare_task(base, task_id, task_name, spacing)
115
+
116
+ task_name = "Task075_Fluo_C3DH_A549_ManAndSim"
117
+ labelsTr = join(nnUNet_raw_data, task_name, "labelsTr")
118
+ cases = subfiles(labelsTr, suffix='.nii.gz', join=False)
119
+ splits = []
120
+ splits.append(
121
+ {'train': [i[:-7] for i in cases if i.startswith('01_') or i.startswith('02_SIM')],
122
+ 'val': [i[:-7] for i in cases if i.startswith('02_') and not i.startswith('02_SIM')]}
123
+ )
124
+ splits.append(
125
+ {'train': [i[:-7] for i in cases if i.startswith('02_') or i.startswith('01_SIM')],
126
+ 'val': [i[:-7] for i in cases if i.startswith('01_') and not i.startswith('01_SIM')]}
127
+ )
128
+ splits.append(
129
+ {'train': [i[:-7] for i in cases if i.startswith('01_') or i.startswith('02_') and not i.startswith('02_SIM')],
130
+ 'val': [i[:-7] for i in cases if i.startswith('02_SIM')]}
131
+ )
132
+ splits.append(
133
+ {'train': [i[:-7] for i in cases if i.startswith('02_') or i.startswith('01_') and not i.startswith('01_SIM')],
134
+ 'val': [i[:-7] for i in cases if i.startswith('01_SIM')]}
135
+ )
136
+ save_pickle(splits, join(preprocessing_output_dir, task_name, "splits_final.pkl"))
137
+
nnunet/dataset_conversion/Task076_Fluo_N3DH_SIM.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from multiprocessing import Pool
17
+ from multiprocessing.dummy import Pool
18
+
19
+ import SimpleITK as sitk
20
+ import numpy as np
21
+ from batchgenerators.utilities.file_and_folder_operations import *
22
+ from skimage.io import imread
23
+ from skimage.io import imsave
24
+ from skimage.morphology import ball
25
+ from skimage.morphology import erosion
26
+ from skimage.transform import resize
27
+
28
+ from nnunet.paths import nnUNet_raw_data
29
+ from nnunet.paths import preprocessing_output_dir
30
+
31
+
32
+ def load_bmp_convert_to_nifti_borders(img_file, lab_file, img_out_base, anno_out, spacing, border_thickness=0.7):
33
+ img = imread(img_file)
34
+ img_itk = sitk.GetImageFromArray(img.astype(np.float32))
35
+ img_itk.SetSpacing(np.array(spacing)[::-1])
36
+ sitk.WriteImage(img_itk, join(img_out_base + "_0000.nii.gz"))
37
+
38
+ if lab_file is not None:
39
+ l = imread(lab_file)
40
+ borders = generate_border_as_suggested_by_twollmann(l, spacing, border_thickness)
41
+ l[l > 0] = 1
42
+ l[borders == 1] = 2
43
+ l_itk = sitk.GetImageFromArray(l.astype(np.uint8))
44
+ l_itk.SetSpacing(np.array(spacing)[::-1])
45
+ sitk.WriteImage(l_itk, anno_out)
46
+
47
+
48
+ def generate_ball(spacing, radius, dtype=int):
49
+ radius_in_voxels = np.round(radius / np.array(spacing)).astype(int)
50
+ n = 2 * radius_in_voxels + 1
51
+ ball_iso = ball(max(n) * 2, dtype=np.float64)
52
+ ball_resampled = resize(ball_iso, n, 1, 'constant', 0, clip=True, anti_aliasing=False, preserve_range=True)
53
+ ball_resampled[ball_resampled > 0.5] = 1
54
+ ball_resampled[ball_resampled <= 0.5] = 0
55
+ return ball_resampled.astype(dtype)
56
+
57
+
58
+ def generate_border_as_suggested_by_twollmann(label_img: np.ndarray, spacing, border_thickness: float = 2) -> np.ndarray:
59
+ border = np.zeros_like(label_img)
60
+ selem = generate_ball(spacing, border_thickness)
61
+ for l in np.unique(label_img):
62
+ if l == 0: continue
63
+ mask = (label_img == l).astype(int)
64
+ eroded = erosion(mask, selem)
65
+ border[(eroded == 0) & (mask != 0)] = 1
66
+ return border
67
+
68
+
69
+ def find_differences(labelstr1, labelstr2):
70
+ for n in subfiles(labelstr1, suffix='.nii.gz', join=False):
71
+ a = sitk.GetArrayFromImage(sitk.ReadImage(join(labelstr1, n)))
72
+ b = sitk.GetArrayFromImage(sitk.ReadImage(join(labelstr2, n)))
73
+ print(n, np.sum(a != b))
74
+
75
+
76
+ def prepare_task(base, task_id, task_name, spacing, border_thickness: float = 15, processes: int = 16):
77
+ p = Pool(processes)
78
+
79
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
80
+
81
+ out_base = join(nnUNet_raw_data, foldername)
82
+ imagestr = join(out_base, "imagesTr")
83
+ imagests = join(out_base, "imagesTs")
84
+ labelstr = join(out_base, "labelsTr")
85
+ maybe_mkdir_p(imagestr)
86
+ maybe_mkdir_p(imagests)
87
+ maybe_mkdir_p(labelstr)
88
+
89
+ train_patient_names = []
90
+ test_patient_names = []
91
+ res = []
92
+
93
+ for train_sequence in [i for i in subfolders(base + "_train", join=False) if not i.endswith("_GT")]:
94
+ train_cases = subfiles(join(base + '_train', train_sequence), suffix=".tif", join=False)
95
+ for t in train_cases:
96
+ casename = train_sequence + "_" + t[:-4]
97
+ img_file = join(base + '_train', train_sequence, t)
98
+ lab_file = join(base + '_train', train_sequence + "_GT", "SEG", "man_seg" + t[1:])
99
+ if not isfile(lab_file):
100
+ continue
101
+ img_out_base = join(imagestr, casename)
102
+ anno_out = join(labelstr, casename + ".nii.gz")
103
+ res.append(
104
+ p.starmap_async(load_bmp_convert_to_nifti_borders, ((img_file, lab_file, img_out_base, anno_out, spacing, border_thickness),)))
105
+ train_patient_names.append(casename)
106
+
107
+ for test_sequence in [i for i in subfolders(base + "_test", join=False) if not i.endswith("_GT")]:
108
+ test_cases = subfiles(join(base + '_test', test_sequence), suffix=".tif", join=False)
109
+ for t in test_cases:
110
+ casename = test_sequence + "_" + t[:-4]
111
+ img_file = join(base + '_test', test_sequence, t)
112
+ lab_file = None
113
+ img_out_base = join(imagests, casename)
114
+ anno_out = None
115
+ res.append(
116
+ p.starmap_async(load_bmp_convert_to_nifti_borders, ((img_file, lab_file, img_out_base, anno_out, spacing, border_thickness),)))
117
+ test_patient_names.append(casename)
118
+
119
+ _ = [i.get() for i in res]
120
+
121
+ json_dict = {}
122
+ json_dict['name'] = task_name
123
+ json_dict['description'] = ""
124
+ json_dict['tensorImageSize'] = "4D"
125
+ json_dict['reference'] = ""
126
+ json_dict['licence'] = ""
127
+ json_dict['release'] = "0.0"
128
+ json_dict['modality'] = {
129
+ "0": "BF",
130
+ }
131
+ json_dict['labels'] = {
132
+ "0": "background",
133
+ "1": "cell",
134
+ "2": "border",
135
+ }
136
+
137
+ json_dict['numTraining'] = len(train_patient_names)
138
+ json_dict['numTest'] = len(test_patient_names)
139
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i, "label": "./labelsTr/%s.nii.gz" % i} for i in
140
+ train_patient_names]
141
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i for i in test_patient_names]
142
+
143
+ save_json(json_dict, os.path.join(out_base, "dataset.json"))
144
+ p.close()
145
+ p.join()
146
+
147
+
148
+ def plot_images(folder, output_folder):
149
+ maybe_mkdir_p(output_folder)
150
+ import matplotlib.pyplot as plt
151
+ for i in subfiles(folder, suffix='.nii.gz', join=False):
152
+ img = sitk.GetArrayFromImage(sitk.ReadImage(join(folder, i)))
153
+ center_slice = img[img.shape[0]//2]
154
+ plt.imsave(join(output_folder, i[:-7] + '.png'), center_slice)
155
+
156
+
157
+ def convert_to_tiff(nifti_image: str, output_name: str):
158
+ npy = sitk.GetArrayFromImage(sitk.ReadImage(nifti_image))
159
+ imsave(output_name, npy.astype(np.uint16), compress=6)
160
+
161
+
162
+ def convert_to_instance_seg(arr: np.ndarray, spacing: tuple = (0.2, 0.125, 0.125)):
163
+ from skimage.morphology import label, dilation
164
+ # 1 is core, 2 is border
165
+ objects = label((arr == 1).astype(int))
166
+ final = np.copy(objects)
167
+ remaining_border = arr == 2
168
+ current = np.copy(objects)
169
+ dilated_mm = np.array((0, 0, 0))
170
+ spacing = np.array(spacing)
171
+
172
+ while np.sum(remaining_border) > 0:
173
+ strel_size = [0, 0, 0]
174
+ maximum_dilation = max(dilated_mm)
175
+ for i in range(3):
176
+ if spacing[i] == min(spacing):
177
+ strel_size[i] = 1
178
+ continue
179
+ if dilated_mm[i] + spacing[i] / 2 < maximum_dilation:
180
+ strel_size[i] = 1
181
+ ball_here = ball(1)
182
+
183
+ if strel_size[0] == 0: ball_here = ball_here[1:2]
184
+ if strel_size[1] == 0: ball_here = ball_here[:, 1:2]
185
+ if strel_size[2] == 0: ball_here = ball_here[:, :, 1:2]
186
+
187
+ #print(1)
188
+ dilated = dilation(current, ball_here)
189
+ diff = (current == 0) & (dilated != current)
190
+ final[diff & remaining_border] = dilated[diff & remaining_border]
191
+ remaining_border[diff] = 0
192
+ current = dilated
193
+ dilated_mm = [dilated_mm[i] + spacing[i] if strel_size[i] == 1 else dilated_mm[i] for i in range(3)]
194
+ return final.astype(np.uint32)
195
+
196
+
197
+ def convert_to_instance_seg2(arr: np.ndarray, spacing: tuple = (0.2, 0.125, 0.125), small_center_threshold=30,
198
+ isolated_border_as_separate_instance_threshold: int = 15):
199
+ from skimage.morphology import label, dilation
200
+ # we first identify centers that are too small and set them to be border. This should remove false positive instances
201
+ objects = label((arr == 1).astype(int))
202
+ for o in np.unique(objects):
203
+ if o > 0 and np.sum(objects == o) <= small_center_threshold:
204
+ arr[objects == o] = 2
205
+
206
+ # 1 is core, 2 is border
207
+ objects = label((arr == 1).astype(int))
208
+ final = np.copy(objects)
209
+ remaining_border = arr == 2
210
+ current = np.copy(objects)
211
+ dilated_mm = np.array((0, 0, 0))
212
+ spacing = np.array(spacing)
213
+
214
+ while np.sum(remaining_border) > 0:
215
+ strel_size = [0, 0, 0]
216
+ maximum_dilation = max(dilated_mm)
217
+ for i in range(3):
218
+ if spacing[i] == min(spacing):
219
+ strel_size[i] = 1
220
+ continue
221
+ if dilated_mm[i] + spacing[i] / 2 < maximum_dilation:
222
+ strel_size[i] = 1
223
+ ball_here = ball(1)
224
+
225
+ if strel_size[0] == 0: ball_here = ball_here[1:2]
226
+ if strel_size[1] == 0: ball_here = ball_here[:, 1:2]
227
+ if strel_size[2] == 0: ball_here = ball_here[:, :, 1:2]
228
+
229
+ #print(1)
230
+ dilated = dilation(current, ball_here)
231
+ diff = (current == 0) & (dilated != current)
232
+ final[diff & remaining_border] = dilated[diff & remaining_border]
233
+ remaining_border[diff] = 0
234
+ current = dilated
235
+ dilated_mm = [dilated_mm[i] + spacing[i] if strel_size[i] == 1 else dilated_mm[i] for i in range(3)]
236
+
237
+ # what can happen is that a cell is so small that the network only predicted border and no core. This cell will be
238
+ # fused with the nearest other instance, which we don't want. Therefore we identify isolated border predictions and
239
+ # give them a separate instance id
240
+ # we identify isolated border predictions by checking each foreground object in arr and see whether this object
241
+ # also contains label 1
242
+ max_label = np.max(final)
243
+
244
+ foreground_objects = label((arr != 0).astype(int))
245
+ for i in np.unique(foreground_objects):
246
+ if i > 0 and (1 not in np.unique(arr[foreground_objects==i])):
247
+ size_of_object = np.sum(foreground_objects==i)
248
+ if size_of_object >= isolated_border_as_separate_instance_threshold:
249
+ final[foreground_objects == i] = max_label + 1
250
+ max_label += 1
251
+ #print('yeah boi')
252
+
253
+ return final.astype(np.uint32)
254
+
255
+
256
+ def load_instanceseg_save(in_file: str, out_file:str, better: bool):
257
+ itk_img = sitk.ReadImage(in_file)
258
+ if not better:
259
+ instanceseg = convert_to_instance_seg(sitk.GetArrayFromImage(itk_img))
260
+ else:
261
+ instanceseg = convert_to_instance_seg2(sitk.GetArrayFromImage(itk_img))
262
+ itk_out = sitk.GetImageFromArray(instanceseg)
263
+ itk_out.CopyInformation(itk_img)
264
+ sitk.WriteImage(itk_out, out_file)
265
+
266
+
267
+ def convert_all_to_instance(input_folder: str, output_folder: str, processes: int = 24, better: bool = False):
268
+ maybe_mkdir_p(output_folder)
269
+ p = Pool(processes)
270
+ files = subfiles(input_folder, suffix='.nii.gz', join=False)
271
+ output_files = [join(output_folder, i) for i in files]
272
+ input_files = [join(input_folder, i) for i in files]
273
+ better = [better] * len(files)
274
+ r = p.starmap_async(load_instanceseg_save, zip(input_files, output_files, better))
275
+ _ = r.get()
276
+ p.close()
277
+ p.join()
278
+
279
+
280
+ if __name__ == "__main__":
281
+ base = "/home/fabian/data/Fluo-N3DH-SIM"
282
+ task_id = 76
283
+ task_name = 'Fluo_N3DH_SIM'
284
+ spacing = (0.2, 0.125, 0.125)
285
+ border_thickness = 0.5
286
+
287
+ prepare_task(base, task_id, task_name, spacing, border_thickness, 12)
288
+
289
+ # we need custom splits
290
+ task_name = "Task076_Fluo_N3DH_SIM"
291
+ labelsTr = join(nnUNet_raw_data, task_name, "labelsTr")
292
+ cases = subfiles(labelsTr, suffix='.nii.gz', join=False)
293
+ splits = []
294
+ splits.append(
295
+ {'train': [i[:-7] for i in cases if i.startswith('01_')],
296
+ 'val': [i[:-7] for i in cases if i.startswith('02_')]}
297
+ )
298
+ splits.append(
299
+ {'train': [i[:-7] for i in cases if i.startswith('02_')],
300
+ 'val': [i[:-7] for i in cases if i.startswith('01_')]}
301
+ )
302
+
303
+ maybe_mkdir_p(join(preprocessing_output_dir, task_name))
304
+
305
+ save_pickle(splits, join(preprocessing_output_dir, task_name, "splits_final.pkl"))
306
+
307
+ # test set was converted to instance seg with convert_all_to_instance with better=True
308
+
309
+ # convert to tiff with convert_to_tiff
310
+
311
+
312
+
nnunet/dataset_conversion/Task082_BraTS_2020.py ADDED
@@ -0,0 +1,751 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import shutil
15
+ from collections import OrderedDict
16
+ from copy import deepcopy
17
+ from multiprocessing.pool import Pool
18
+ from typing import Tuple
19
+
20
+ import SimpleITK as sitk
21
+ import numpy as np
22
+ import scipy.stats as ss
23
+ from batchgenerators.utilities.file_and_folder_operations import *
24
+ from medpy.metric import dc, hd95
25
+ from nnunet.dataset_conversion.Task032_BraTS_2018 import convert_labels_back_to_BraTS_2018_2019_convention
26
+ from nnunet.dataset_conversion.Task043_BraTS_2019 import copy_BraTS_segmentation_and_convert_labels
27
+ from nnunet.evaluation.region_based_evaluation import get_brats_regions, evaluate_regions
28
+ from nnunet.paths import nnUNet_raw_data
29
+ from nnunet.postprocessing.consolidate_postprocessing import collect_cv_niftis
30
+
31
+
32
+ def apply_brats_threshold(fname, out_dir, threshold, replace_with):
33
+ img_itk = sitk.ReadImage(fname)
34
+ img_npy = sitk.GetArrayFromImage(img_itk)
35
+ s = np.sum(img_npy == 3)
36
+ if s < threshold:
37
+ # print(s, fname)
38
+ img_npy[img_npy == 3] = replace_with
39
+ img_itk_postprocessed = sitk.GetImageFromArray(img_npy)
40
+ img_itk_postprocessed.CopyInformation(img_itk)
41
+ sitk.WriteImage(img_itk_postprocessed, join(out_dir, fname.split("/")[-1]))
42
+
43
+
44
+ def load_niftis_threshold_compute_dice(gt_file, pred_file, thresholds: Tuple[list, tuple]):
45
+ gt = sitk.GetArrayFromImage(sitk.ReadImage(gt_file))
46
+ pred = sitk.GetArrayFromImage(sitk.ReadImage(pred_file))
47
+ mask_pred = pred == 3
48
+ mask_gt = gt == 3
49
+ num_pred = np.sum(mask_pred)
50
+
51
+ num_gt = np.sum(mask_gt)
52
+ dice = dc(mask_pred, mask_gt)
53
+
54
+ res_dice = {}
55
+ res_was_smaller = {}
56
+
57
+ for t in thresholds:
58
+ was_smaller = False
59
+
60
+ if num_pred < t:
61
+ was_smaller = True
62
+ if num_gt == 0:
63
+ dice_here = 1.
64
+ else:
65
+ dice_here = 0.
66
+ else:
67
+ dice_here = deepcopy(dice)
68
+
69
+ res_dice[t] = dice_here
70
+ res_was_smaller[t] = was_smaller
71
+
72
+ return res_was_smaller, res_dice
73
+
74
+
75
+ def apply_threshold_to_folder(folder_in, folder_out, threshold, replace_with, processes=24):
76
+ maybe_mkdir_p(folder_out)
77
+ niftis = subfiles(folder_in, suffix='.nii.gz', join=True)
78
+
79
+ p = Pool(processes)
80
+ p.starmap(apply_brats_threshold, zip(niftis, [folder_out]*len(niftis), [threshold]*len(niftis), [replace_with] * len(niftis)))
81
+
82
+ p.close()
83
+ p.join()
84
+
85
+
86
+ def determine_brats_postprocessing(folder_with_preds, folder_with_gt, postprocessed_output_dir, processes=8,
87
+ thresholds=(0, 10, 50, 100, 200, 500, 750, 1000, 1500, 2500, 10000), replace_with=2):
88
+ # find pairs
89
+ nifti_gt = subfiles(folder_with_gt, suffix=".nii.gz", sort=True)
90
+
91
+ p = Pool(processes)
92
+
93
+ nifti_pred = subfiles(folder_with_preds, suffix='.nii.gz', sort=True)
94
+
95
+ results = p.starmap_async(load_niftis_threshold_compute_dice, zip(nifti_gt, nifti_pred, [thresholds] * len(nifti_pred)))
96
+ results = results.get()
97
+
98
+ all_dc_per_threshold = {}
99
+ for t in thresholds:
100
+ all_dc_per_threshold[t] = np.array([i[1][t] for i in results])
101
+ print(t, np.mean(all_dc_per_threshold[t]))
102
+
103
+ means = [np.mean(all_dc_per_threshold[t]) for t in thresholds]
104
+ best_threshold = thresholds[np.argmax(means)]
105
+ print('best', best_threshold, means[np.argmax(means)])
106
+
107
+ maybe_mkdir_p(postprocessed_output_dir)
108
+
109
+ p.starmap(apply_brats_threshold, zip(nifti_pred, [postprocessed_output_dir]*len(nifti_pred), [best_threshold]*len(nifti_pred), [replace_with] * len(nifti_pred)))
110
+
111
+ p.close()
112
+ p.join()
113
+
114
+ save_pickle((thresholds, means, best_threshold, all_dc_per_threshold), join(postprocessed_output_dir, "threshold.pkl"))
115
+
116
+
117
+ def collect_and_prepare(base_dir, num_processes = 12, clean=False):
118
+ """
119
+ collect all cv_niftis, compute brats metrics, compute enh tumor thresholds and summarize in csv
120
+ :param base_dir:
121
+ :return:
122
+ """
123
+ out = join(base_dir, 'cv_results')
124
+ out_pp = join(base_dir, 'cv_results_pp')
125
+ experiments = subfolders(base_dir, join=False, prefix='nnUNetTrainer')
126
+ regions = get_brats_regions()
127
+ gt_dir = join(base_dir, 'gt_niftis')
128
+ replace_with = 2
129
+
130
+ failed = []
131
+ successful = []
132
+ for e in experiments:
133
+ print(e)
134
+ try:
135
+ o = join(out, e)
136
+ o_p = join(out_pp, e)
137
+ maybe_mkdir_p(o)
138
+ maybe_mkdir_p(o_p)
139
+ collect_cv_niftis(join(base_dir, e), o)
140
+ if clean or not isfile(join(o, 'summary.csv')):
141
+ evaluate_regions(o, gt_dir, regions, num_processes)
142
+ if clean or not isfile(join(o_p, 'threshold.pkl')):
143
+ determine_brats_postprocessing(o, gt_dir, o_p, num_processes, thresholds=list(np.arange(0, 760, 10)), replace_with=replace_with)
144
+ if clean or not isfile(join(o_p, 'summary.csv')):
145
+ evaluate_regions(o_p, gt_dir, regions, num_processes)
146
+ successful.append(e)
147
+ except Exception as ex:
148
+ print("\nERROR\n", e, ex, "\n")
149
+ failed.append(e)
150
+
151
+ # we are interested in the mean (nan is 1) column
152
+ with open(join(base_dir, 'cv_summary.csv'), 'w') as f:
153
+ f.write('name,whole,core,enh,mean\n')
154
+ for e in successful:
155
+ expected_nopp = join(out, e, 'summary.csv')
156
+ expected_pp = join(out, out_pp, e, 'summary.csv')
157
+ if isfile(expected_nopp):
158
+ res = np.loadtxt(expected_nopp, dtype=str, skiprows=0, delimiter=',')[-2]
159
+ as_numeric = [float(i) for i in res[1:]]
160
+ f.write(e + '_noPP,')
161
+ f.write("%0.4f," % as_numeric[0])
162
+ f.write("%0.4f," % as_numeric[1])
163
+ f.write("%0.4f," % as_numeric[2])
164
+ f.write("%0.4f\n" % np.mean(as_numeric))
165
+ if isfile(expected_pp):
166
+ res = np.loadtxt(expected_pp, dtype=str, skiprows=0, delimiter=',')[-2]
167
+ as_numeric = [float(i) for i in res[1:]]
168
+ f.write(e + '_PP,')
169
+ f.write("%0.4f," % as_numeric[0])
170
+ f.write("%0.4f," % as_numeric[1])
171
+ f.write("%0.4f," % as_numeric[2])
172
+ f.write("%0.4f\n" % np.mean(as_numeric))
173
+
174
+ # this just crawls the folders and evaluates what it finds
175
+ with open(join(base_dir, 'cv_summary2.csv'), 'w') as f:
176
+ for folder in ['cv_results', 'cv_results_pp']:
177
+ for ex in subdirs(join(base_dir, folder), join=False):
178
+ print(folder, ex)
179
+ expected = join(base_dir, folder, ex, 'summary.csv')
180
+ if clean or not isfile(expected):
181
+ evaluate_regions(join(base_dir, folder, ex), gt_dir, regions, num_processes)
182
+ if isfile(expected):
183
+ res = np.loadtxt(expected, dtype=str, skiprows=0, delimiter=',')[-2]
184
+ as_numeric = [float(i) for i in res[1:]]
185
+ f.write('%s__%s,' % (folder, ex))
186
+ f.write("%0.4f," % as_numeric[0])
187
+ f.write("%0.4f," % as_numeric[1])
188
+ f.write("%0.4f," % as_numeric[2])
189
+ f.write("%0.4f\n" % np.mean(as_numeric))
190
+
191
+ f.write('name,whole,core,enh,mean\n')
192
+ for e in successful:
193
+ expected_nopp = join(out, e, 'summary.csv')
194
+ expected_pp = join(out, out_pp, e, 'summary.csv')
195
+ if isfile(expected_nopp):
196
+ res = np.loadtxt(expected_nopp, dtype=str, skiprows=0, delimiter=',')[-2]
197
+ as_numeric = [float(i) for i in res[1:]]
198
+ f.write(e + '_noPP,')
199
+ f.write("%0.4f," % as_numeric[0])
200
+ f.write("%0.4f," % as_numeric[1])
201
+ f.write("%0.4f," % as_numeric[2])
202
+ f.write("%0.4f\n" % np.mean(as_numeric))
203
+ if isfile(expected_pp):
204
+ res = np.loadtxt(expected_pp, dtype=str, skiprows=0, delimiter=',')[-2]
205
+ as_numeric = [float(i) for i in res[1:]]
206
+ f.write(e + '_PP,')
207
+ f.write("%0.4f," % as_numeric[0])
208
+ f.write("%0.4f," % as_numeric[1])
209
+ f.write("%0.4f," % as_numeric[2])
210
+ f.write("%0.4f\n" % np.mean(as_numeric))
211
+
212
+ # apply threshold to val set
213
+ expected_num_cases = 125
214
+ missing_valset = []
215
+ has_val_pred = []
216
+ for e in successful:
217
+ if isdir(join(base_dir, 'predVal', e)):
218
+ currdir = join(base_dir, 'predVal', e)
219
+ files = subfiles(currdir, suffix='.nii.gz', join=False)
220
+ if len(files) != expected_num_cases:
221
+ print(e, 'prediction not done, found %d files, expected %s' % (len(files), expected_num_cases))
222
+ continue
223
+ output_folder = join(base_dir, 'predVal_PP', e)
224
+ maybe_mkdir_p(output_folder)
225
+ threshold = load_pickle(join(out_pp, e, 'threshold.pkl'))[2]
226
+ if threshold > 1000: threshold = 750 # don't make it too big!
227
+ apply_threshold_to_folder(currdir, output_folder, threshold, replace_with, num_processes)
228
+ has_val_pred.append(e)
229
+ else:
230
+ print(e, 'has no valset predictions')
231
+ missing_valset.append(e)
232
+
233
+ # 'nnUNetTrainerV2BraTSRegions_DA3_BN__nnUNetPlansv2.1_bs5_15fold' needs special treatment
234
+ e = 'nnUNetTrainerV2BraTSRegions_DA3_BN__nnUNetPlansv2.1_bs5'
235
+ currdir = join(base_dir, 'predVal', 'nnUNetTrainerV2BraTSRegions_DA3_BN__nnUNetPlansv2.1_bs5_15fold')
236
+ output_folder = join(base_dir, 'predVal_PP', 'nnUNetTrainerV2BraTSRegions_DA3_BN__nnUNetPlansv2.1_bs5_15fold')
237
+ maybe_mkdir_p(output_folder)
238
+ threshold = load_pickle(join(out_pp, e, 'threshold.pkl'))[2]
239
+ if threshold > 1000: threshold = 750 # don't make it too big!
240
+ apply_threshold_to_folder(currdir, output_folder, threshold, replace_with, num_processes)
241
+
242
+ # 'nnUNetTrainerV2BraTSRegions_DA3_BN__nnUNetPlansv2.1_bs5_15fold' needs special treatment
243
+ e = 'nnUNetTrainerV2BraTSRegions_DA4_BN__nnUNetPlansv2.1_bs5'
244
+ currdir = join(base_dir, 'predVal', 'nnUNetTrainerV2BraTSRegions_DA4_BN__nnUNetPlansv2.1_bs5_15fold')
245
+ output_folder = join(base_dir, 'predVal_PP', 'nnUNetTrainerV2BraTSRegions_DA4_BN__nnUNetPlansv2.1_bs5_15fold')
246
+ maybe_mkdir_p(output_folder)
247
+ threshold = load_pickle(join(out_pp, e, 'threshold.pkl'))[2]
248
+ if threshold > 1000: threshold = 750 # don't make it too big!
249
+ apply_threshold_to_folder(currdir, output_folder, threshold, replace_with, num_processes)
250
+
251
+ # convert val set to brats labels for submission
252
+ output_converted = join(base_dir, 'converted_valSet')
253
+
254
+ for source in ['predVal', 'predVal_PP']:
255
+ for e in has_val_pred + ['nnUNetTrainerV2BraTSRegions_DA3_BN__nnUNetPlansv2.1_bs5_15fold', 'nnUNetTrainerV2BraTSRegions_DA4_BN__nnUNetPlansv2.1_bs5_15fold']:
256
+ expected_source_folder = join(base_dir, source, e)
257
+ if not isdir(expected_source_folder):
258
+ print(e, 'has no', source)
259
+ raise RuntimeError()
260
+ files = subfiles(expected_source_folder, suffix='.nii.gz', join=False)
261
+ if len(files) != expected_num_cases:
262
+ print(e, 'prediction not done, found %d files, expected %s' % (len(files), expected_num_cases))
263
+ continue
264
+ target_folder = join(output_converted, source, e)
265
+ maybe_mkdir_p(target_folder)
266
+ convert_labels_back_to_BraTS_2018_2019_convention(expected_source_folder, target_folder)
267
+
268
+ summarize_validation_set_predictions(output_converted)
269
+
270
+
271
+ def summarize_validation_set_predictions(base):
272
+ with open(join(base, 'summary.csv'), 'w') as f:
273
+ f.write('name,whole,core,enh,mean,whole,core,enh,mean\n')
274
+ for subf in subfolders(base, join=False):
275
+ for e in subfolders(join(base, subf), join=False):
276
+ expected = join(base, subf, e, 'Stats_Validation_final.csv')
277
+ if not isfile(expected):
278
+ print(subf, e, 'has missing csv')
279
+ continue
280
+ a = np.loadtxt(expected, delimiter=',', dtype=str)
281
+ assert a.shape[0] == 131, 'did not evaluate all 125 cases!'
282
+ selected_row = a[-5]
283
+ values = [float(i) for i in selected_row[1:4]]
284
+ f.write(e + "_" + subf + ',')
285
+ f.write("%0.4f," % values[1])
286
+ f.write("%0.4f," % values[2])
287
+ f.write("%0.4f," % values[0])
288
+ f.write("%0.4f," % np.mean(values))
289
+ values = [float(i) for i in selected_row[-3:]]
290
+ f.write("%0.4f," % values[1])
291
+ f.write("%0.4f," % values[2])
292
+ f.write("%0.4f," % values[0])
293
+ f.write("%0.4f\n" % np.mean(values))
294
+
295
+
296
+ def compute_BraTS_dice(ref, pred):
297
+ """
298
+ ref and gt are binary integer numpy.ndarray s
299
+ :param ref:
300
+ :param gt:
301
+ :return:
302
+ """
303
+ num_ref = np.sum(ref)
304
+ num_pred = np.sum(pred)
305
+
306
+ if num_ref == 0:
307
+ if num_pred == 0:
308
+ return 1
309
+ else:
310
+ return 0
311
+ else:
312
+ return dc(pred, ref)
313
+
314
+
315
+ def convert_all_to_BraTS(input_folder, output_folder, expected_num_cases=125):
316
+ for s in subdirs(input_folder, join=False):
317
+ nii = subfiles(join(input_folder, s), suffix='.nii.gz', join=False)
318
+ if len(nii) != expected_num_cases:
319
+ print(s)
320
+ else:
321
+ target_dir = join(output_folder, s)
322
+ convert_labels_back_to_BraTS_2018_2019_convention(join(input_folder, s), target_dir, num_processes=6)
323
+
324
+
325
+ def compute_BraTS_HD95(ref, pred):
326
+ """
327
+ ref and gt are binary integer numpy.ndarray s
328
+ spacing is assumed to be (1, 1, 1)
329
+ :param ref:
330
+ :param pred:
331
+ :return:
332
+ """
333
+ num_ref = np.sum(ref)
334
+ num_pred = np.sum(pred)
335
+
336
+ if num_ref == 0:
337
+ if num_pred == 0:
338
+ return 0
339
+ else:
340
+ return 373.12866
341
+ elif num_pred == 0 and num_ref != 0:
342
+ return 373.12866
343
+ else:
344
+ return hd95(pred, ref, (1, 1, 1))
345
+
346
+
347
+ def evaluate_BraTS_case(arr: np.ndarray, arr_gt: np.ndarray):
348
+ """
349
+ attempting to reimplement the brats evaluation scheme
350
+ assumes edema=1, non_enh=2, enh=3
351
+ :param arr:
352
+ :param arr_gt:
353
+ :return:
354
+ """
355
+ # whole tumor
356
+ mask_gt = (arr_gt != 0).astype(int)
357
+ mask_pred = (arr != 0).astype(int)
358
+ dc_whole = compute_BraTS_dice(mask_gt, mask_pred)
359
+ hd95_whole = compute_BraTS_HD95(mask_gt, mask_pred)
360
+ del mask_gt, mask_pred
361
+
362
+ # tumor core
363
+ mask_gt = (arr_gt > 1).astype(int)
364
+ mask_pred = (arr > 1).astype(int)
365
+ dc_core = compute_BraTS_dice(mask_gt, mask_pred)
366
+ hd95_core = compute_BraTS_HD95(mask_gt, mask_pred)
367
+ del mask_gt, mask_pred
368
+
369
+ # enhancing
370
+ mask_gt = (arr_gt == 3).astype(int)
371
+ mask_pred = (arr == 3).astype(int)
372
+ dc_enh = compute_BraTS_dice(mask_gt, mask_pred)
373
+ hd95_enh = compute_BraTS_HD95(mask_gt, mask_pred)
374
+ del mask_gt, mask_pred
375
+
376
+ return dc_whole, dc_core, dc_enh, hd95_whole, hd95_core, hd95_enh
377
+
378
+
379
+ def load_evaluate(filename_gt: str, filename_pred: str):
380
+ arr_pred = sitk.GetArrayFromImage(sitk.ReadImage(filename_pred))
381
+ arr_gt = sitk.GetArrayFromImage(sitk.ReadImage(filename_gt))
382
+ return evaluate_BraTS_case(arr_pred, arr_gt)
383
+
384
+
385
+ def evaluate_BraTS_folder(folder_pred, folder_gt, num_processes: int = 24, strict=False):
386
+ nii_pred = subfiles(folder_pred, suffix='.nii.gz', join=False)
387
+ if len(nii_pred) == 0:
388
+ return
389
+ nii_gt = subfiles(folder_gt, suffix='.nii.gz', join=False)
390
+ assert all([i in nii_gt for i in nii_pred]), 'not all predicted niftis have a reference file!'
391
+ if strict:
392
+ assert all([i in nii_pred for i in nii_gt]), 'not all gt niftis have a predicted file!'
393
+ p = Pool(num_processes)
394
+ nii_pred_fullpath = [join(folder_pred, i) for i in nii_pred]
395
+ nii_gt_fullpath = [join(folder_gt, i) for i in nii_pred]
396
+ results = p.starmap(load_evaluate, zip(nii_gt_fullpath, nii_pred_fullpath))
397
+ # now write to output file
398
+ with open(join(folder_pred, 'results.csv'), 'w') as f:
399
+ f.write("name,dc_whole,dc_core,dc_enh,hd95_whole,hd95_core,hd95_enh\n")
400
+ for fname, r in zip(nii_pred, results):
401
+ f.write(fname)
402
+ f.write(",%0.4f,%0.4f,%0.4f,%3.3f,%3.3f,%3.3f\n" % r)
403
+
404
+
405
+ def load_csv_for_ranking(csv_file: str):
406
+ res = np.loadtxt(csv_file, dtype='str', delimiter=',')
407
+ scores = res[1:, [1, 2, 3, -3, -2, -1]].astype(float)
408
+ scores[:, -3:] *= -1
409
+ scores[:, -3:] += 373.129
410
+ assert np.all(scores <= 373.129)
411
+ assert np.all(scores >= 0)
412
+ return scores
413
+
414
+
415
+ def rank_algorithms(data:np.ndarray):
416
+ """
417
+ data is (metrics x experiments x cases)
418
+ :param data:
419
+ :return:
420
+ """
421
+ num_metrics, num_experiments, num_cases = data.shape
422
+ ranks = np.zeros((num_metrics, num_experiments))
423
+ for m in range(6):
424
+ r = np.apply_along_axis(ss.rankdata, 0, -data[m], 'min')
425
+ ranks[m] = r.mean(1)
426
+ average_rank = np.mean(ranks, 0)
427
+ final_ranks = ss.rankdata(average_rank, 'min')
428
+ return final_ranks, average_rank, ranks
429
+
430
+
431
+ def score_and_postprocess_model_based_on_rank_then_aggregate():
432
+ """
433
+ Similarly to BraTS 2017 - BraTS 2019, each participant will be ranked for each of the X test cases. Each case
434
+ includes 3 regions of evaluation, and the metrics used to produce the rankings will be the Dice Similarity
435
+ Coefficient and the 95% Hausdorff distance. Thus, for X number of cases included in the BraTS 2020, each
436
+ participant ends up having X*3*2 rankings. The final ranking score is the average of all these rankings normalized
437
+ by the number of teams.
438
+ https://zenodo.org/record/3718904
439
+
440
+ -> let's optimize for this.
441
+
442
+ Important: the outcome very much depends on the competing models. We need some references. We only got our own,
443
+ so let's hope this still works
444
+ :return:
445
+ """
446
+ base = "/media/fabian/Results/nnUNet/3d_fullres/Task082_BraTS2020"
447
+ replace_with = 2
448
+ num_processes = 24
449
+ expected_num_cases_val = 125
450
+
451
+ # use a separate output folder from the previous experiments to ensure we are not messing things up
452
+ output_base_here = join(base, 'use_brats_ranking')
453
+ maybe_mkdir_p(output_base_here)
454
+
455
+ # collect cv niftis and compute metrics with evaluate_BraTS_folder to ensure we work with the same metrics as brats
456
+ out = join(output_base_here, 'cv_results')
457
+ experiments = subfolders(base, join=False, prefix='nnUNetTrainer')
458
+ gt_dir = join(base, 'gt_niftis')
459
+
460
+ experiments_with_full_cv = []
461
+ for e in experiments:
462
+ print(e)
463
+ o = join(out, e)
464
+ maybe_mkdir_p(o)
465
+ try:
466
+ collect_cv_niftis(join(base, e), o)
467
+ if not isfile(join(o, 'results.csv')):
468
+ evaluate_BraTS_folder(o, gt_dir, num_processes, strict=True)
469
+ experiments_with_full_cv.append(e)
470
+ except Exception as ex:
471
+ print("\nERROR\n", e, ex, "\n")
472
+ if isfile(join(o, 'results.csv')):
473
+ os.remove(join(o, 'results.csv'))
474
+
475
+ # rank the non-postprocessed models
476
+ tmp = np.loadtxt(join(out, experiments_with_full_cv[0], 'results.csv'), dtype='str', delimiter=',')
477
+ num_cases = len(tmp) - 1
478
+ data_for_ranking = np.zeros((6, len(experiments_with_full_cv), num_cases))
479
+ for i, e in enumerate(experiments_with_full_cv):
480
+ scores = load_csv_for_ranking(join(out, e, 'results.csv'))
481
+ for metric in range(6):
482
+ data_for_ranking[metric, i] = scores[:, metric]
483
+
484
+ final_ranks, average_rank, ranks = rank_algorithms(data_for_ranking)
485
+
486
+ for t in np.argsort(final_ranks):
487
+ print(final_ranks[t], average_rank[t], experiments_with_full_cv[t])
488
+
489
+ # for each model, create output directories with different thresholds. evaluate ALL OF THEM (might take a while lol)
490
+ thresholds = np.arange(25, 751, 25)
491
+ output_pp_tmp = join(output_base_here, 'cv_determine_pp_thresholds')
492
+ for e in experiments_with_full_cv:
493
+ input_folder = join(out, e)
494
+ for t in thresholds:
495
+ output_directory = join(output_pp_tmp, e, str(t))
496
+ maybe_mkdir_p(output_directory)
497
+ if not isfile(join(output_directory, 'results.csv')):
498
+ apply_threshold_to_folder(input_folder, output_directory, t, replace_with, processes=16)
499
+ evaluate_BraTS_folder(output_directory, gt_dir, num_processes)
500
+
501
+ # load ALL the results!
502
+ results = []
503
+ experiment_names = []
504
+ for e in experiments_with_full_cv:
505
+ for t in thresholds:
506
+ output_directory = join(output_pp_tmp, e, str(t))
507
+ expected_file = join(output_directory, 'results.csv')
508
+ if not isfile(expected_file):
509
+ print(e, 'does not have a results file for threshold', t)
510
+ continue
511
+ results.append(load_csv_for_ranking(expected_file))
512
+ experiment_names.append("%s___%d" % (e, t))
513
+ all_results = np.concatenate([i[None] for i in results], 0).transpose((2, 0, 1))
514
+
515
+ # concatenate with non postprocessed models
516
+ all_results = np.concatenate((data_for_ranking, all_results), 1)
517
+ experiment_names += experiments_with_full_cv
518
+
519
+ final_ranks, average_rank, ranks = rank_algorithms(all_results)
520
+
521
+ for t in np.argsort(final_ranks):
522
+ print(final_ranks[t], average_rank[t], experiment_names[t])
523
+
524
+ # for each model, print the non postprocessed model as well as the best postprocessed model. If there are
525
+ # validation set predictions, apply the best threshold to the validation set
526
+ pred_val_base = join(base, 'predVal_PP_rank')
527
+ has_val_pred = []
528
+ for e in experiments_with_full_cv:
529
+ rank_nonpp = final_ranks[experiment_names.index(e)]
530
+ avg_rank_nonpp = average_rank[experiment_names.index(e)]
531
+ print(e, avg_rank_nonpp, rank_nonpp)
532
+ predicted_val = join(base, 'predVal', e)
533
+
534
+ pp_models = [j for j, i in enumerate(experiment_names) if i.split("___")[0] == e and i != e]
535
+ if len(pp_models) > 0:
536
+ ranks = [final_ranks[i] for i in pp_models]
537
+ best_idx = np.argmin(ranks)
538
+ best = experiment_names[pp_models[best_idx]]
539
+ best_avg_rank = average_rank[pp_models[best_idx]]
540
+ print(best, best_avg_rank, min(ranks))
541
+ print('')
542
+ # apply threshold to validation set
543
+ best_threshold = int(best.split('___')[-1])
544
+ if not isdir(predicted_val):
545
+ print(e, 'has not valset predictions')
546
+ else:
547
+ files = subfiles(predicted_val, suffix='.nii.gz')
548
+ if len(files) != expected_num_cases_val:
549
+ print(e, 'has missing val cases. found: %d expected: %d' % (len(files), expected_num_cases_val))
550
+ else:
551
+ apply_threshold_to_folder(predicted_val, join(pred_val_base, e), best_threshold, replace_with, num_processes)
552
+ has_val_pred.append(e)
553
+ else:
554
+ print(e, 'not found in ranking')
555
+
556
+ # apply nnUNetTrainerV2BraTSRegions_DA3_BN__nnUNetPlansv2.1_bs5 to nnUNetTrainerV2BraTSRegions_DA3_BN__nnUNetPlansv2.1_bs5_15fold
557
+ e = 'nnUNetTrainerV2BraTSRegions_DA3_BN__nnUNetPlansv2.1_bs5'
558
+ pp_models = [j for j, i in enumerate(experiment_names) if i.split("___")[0] == e and i != e]
559
+ ranks = [final_ranks[i] for i in pp_models]
560
+ best_idx = np.argmin(ranks)
561
+ best = experiment_names[pp_models[best_idx]]
562
+ best_avg_rank = average_rank[pp_models[best_idx]]
563
+ best_threshold = int(best.split('___')[-1])
564
+ predicted_val = join(base, 'predVal', 'nnUNetTrainerV2BraTSRegions_DA3_BN__nnUNetPlansv2.1_bs5_15fold')
565
+ apply_threshold_to_folder(predicted_val, join(pred_val_base, 'nnUNetTrainerV2BraTSRegions_DA3_BN__nnUNetPlansv2.1_bs5_15fold'), best_threshold, replace_with, num_processes)
566
+ has_val_pred.append('nnUNetTrainerV2BraTSRegions_DA3_BN__nnUNetPlansv2.1_bs5_15fold')
567
+
568
+ # apply nnUNetTrainerV2BraTSRegions_DA4_BN__nnUNetPlansv2.1_bs5 to nnUNetTrainerV2BraTSRegions_DA4_BN__nnUNetPlansv2.1_bs5_15fold
569
+ e = 'nnUNetTrainerV2BraTSRegions_DA4_BN__nnUNetPlansv2.1_bs5'
570
+ pp_models = [j for j, i in enumerate(experiment_names) if i.split("___")[0] == e and i != e]
571
+ ranks = [final_ranks[i] for i in pp_models]
572
+ best_idx = np.argmin(ranks)
573
+ best = experiment_names[pp_models[best_idx]]
574
+ best_avg_rank = average_rank[pp_models[best_idx]]
575
+ best_threshold = int(best.split('___')[-1])
576
+ predicted_val = join(base, 'predVal', 'nnUNetTrainerV2BraTSRegions_DA4_BN__nnUNetPlansv2.1_bs5_15fold')
577
+ apply_threshold_to_folder(predicted_val, join(pred_val_base, 'nnUNetTrainerV2BraTSRegions_DA4_BN__nnUNetPlansv2.1_bs5_15fold'), best_threshold, replace_with, num_processes)
578
+ has_val_pred.append('nnUNetTrainerV2BraTSRegions_DA4_BN__nnUNetPlansv2.1_bs5_15fold')
579
+
580
+ # convert valsets
581
+ output_converted = join(base, 'converted_valSet')
582
+ for e in has_val_pred:
583
+ expected_source_folder = join(base, 'predVal_PP_rank', e)
584
+ if not isdir(expected_source_folder):
585
+ print(e, 'has no predVal_PP_rank')
586
+ raise RuntimeError()
587
+ files = subfiles(expected_source_folder, suffix='.nii.gz', join=False)
588
+ if len(files) != expected_num_cases_val:
589
+ print(e, 'prediction not done, found %d files, expected %s' % (len(files), expected_num_cases_val))
590
+ continue
591
+ target_folder = join(output_converted, 'predVal_PP_rank', e)
592
+ maybe_mkdir_p(target_folder)
593
+ convert_labels_back_to_BraTS_2018_2019_convention(expected_source_folder, target_folder)
594
+
595
+ # now load all the csvs for the validation set (obtained from evaluation platform) and rank our models on the
596
+ # validation set
597
+ flds = subdirs(output_converted, join=False)
598
+ results_valset = []
599
+ names_valset = []
600
+ for f in flds:
601
+ curr = join(output_converted, f)
602
+ experiments = subdirs(curr, join=False)
603
+ for e in experiments:
604
+ currr = join(curr, e)
605
+ expected_file = join(currr, 'Stats_Validation_final.csv')
606
+ if not isfile(expected_file):
607
+ print(f, e, "has not been evaluated yet!")
608
+ else:
609
+ res = load_csv_for_ranking(expected_file)[:-5]
610
+ assert res.shape[0] == expected_num_cases_val
611
+ results_valset.append(res[None])
612
+ names_valset.append("%s___%s" % (f, e))
613
+ results_valset = np.concatenate(results_valset, 0) # experiments x cases x metrics
614
+ # convert to metrics x experiments x cases
615
+ results_valset = results_valset.transpose((2, 0, 1))
616
+ final_ranks, average_rank, ranks = rank_algorithms(results_valset)
617
+ for t in np.argsort(final_ranks):
618
+ print(final_ranks[t], average_rank[t], names_valset[t])
619
+
620
+
621
+ if __name__ == "__main__":
622
+ """
623
+ THIS CODE IS A MESS. IT IS PROVIDED AS IS WITH NO GUARANTEES. YOU HAVE TO DIG THROUGH IT YOURSELF. GOOD LUCK ;-)
624
+
625
+ REMEMBER TO CONVERT LABELS BACK TO BRATS CONVENTION AFTER PREDICTION!
626
+ """
627
+
628
+ task_name = "Task082_BraTS2020"
629
+ downloaded_data_dir = "/home/fabian/Downloads/MICCAI_BraTS2020_TrainingData"
630
+ downloaded_data_dir_val = "/home/fabian/Downloads/MICCAI_BraTS2020_ValidationData"
631
+
632
+ target_base = join(nnUNet_raw_data, task_name)
633
+ target_imagesTr = join(target_base, "imagesTr")
634
+ target_imagesVal = join(target_base, "imagesVal")
635
+ target_imagesTs = join(target_base, "imagesTs")
636
+ target_labelsTr = join(target_base, "labelsTr")
637
+
638
+ maybe_mkdir_p(target_imagesTr)
639
+ maybe_mkdir_p(target_imagesVal)
640
+ maybe_mkdir_p(target_imagesTs)
641
+ maybe_mkdir_p(target_labelsTr)
642
+
643
+ patient_names = []
644
+ cur = join(downloaded_data_dir)
645
+ for p in subdirs(cur, join=False):
646
+ patdir = join(cur, p)
647
+ patient_name = p
648
+ patient_names.append(patient_name)
649
+ t1 = join(patdir, p + "_t1.nii.gz")
650
+ t1c = join(patdir, p + "_t1ce.nii.gz")
651
+ t2 = join(patdir, p + "_t2.nii.gz")
652
+ flair = join(patdir, p + "_flair.nii.gz")
653
+ seg = join(patdir, p + "_seg.nii.gz")
654
+
655
+ assert all([
656
+ isfile(t1),
657
+ isfile(t1c),
658
+ isfile(t2),
659
+ isfile(flair),
660
+ isfile(seg)
661
+ ]), "%s" % patient_name
662
+
663
+ shutil.copy(t1, join(target_imagesTr, patient_name + "_0000.nii.gz"))
664
+ shutil.copy(t1c, join(target_imagesTr, patient_name + "_0001.nii.gz"))
665
+ shutil.copy(t2, join(target_imagesTr, patient_name + "_0002.nii.gz"))
666
+ shutil.copy(flair, join(target_imagesTr, patient_name + "_0003.nii.gz"))
667
+
668
+ copy_BraTS_segmentation_and_convert_labels(seg, join(target_labelsTr, patient_name + ".nii.gz"))
669
+
670
+
671
+ json_dict = OrderedDict()
672
+ json_dict['name'] = "BraTS2020"
673
+ json_dict['description'] = "nothing"
674
+ json_dict['tensorImageSize'] = "4D"
675
+ json_dict['reference'] = "see BraTS2020"
676
+ json_dict['licence'] = "see BraTS2020 license"
677
+ json_dict['release'] = "0.0"
678
+ json_dict['modality'] = {
679
+ "0": "T1",
680
+ "1": "T1ce",
681
+ "2": "T2",
682
+ "3": "FLAIR"
683
+ }
684
+ json_dict['labels'] = {
685
+ "0": "background",
686
+ "1": "edema",
687
+ "2": "non-enhancing",
688
+ "3": "enhancing",
689
+ }
690
+ json_dict['numTraining'] = len(patient_names)
691
+ json_dict['numTest'] = 0
692
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i, "label": "./labelsTr/%s.nii.gz" % i} for i in
693
+ patient_names]
694
+ json_dict['test'] = []
695
+
696
+ save_json(json_dict, join(target_base, "dataset.json"))
697
+
698
+ if downloaded_data_dir_val is not None:
699
+ for p in subdirs(downloaded_data_dir_val, join=False):
700
+ patdir = join(downloaded_data_dir_val, p)
701
+ patient_name = p
702
+ t1 = join(patdir, p + "_t1.nii.gz")
703
+ t1c = join(patdir, p + "_t1ce.nii.gz")
704
+ t2 = join(patdir, p + "_t2.nii.gz")
705
+ flair = join(patdir, p + "_flair.nii.gz")
706
+
707
+ assert all([
708
+ isfile(t1),
709
+ isfile(t1c),
710
+ isfile(t2),
711
+ isfile(flair),
712
+ ]), "%s" % patient_name
713
+
714
+ shutil.copy(t1, join(target_imagesVal, patient_name + "_0000.nii.gz"))
715
+ shutil.copy(t1c, join(target_imagesVal, patient_name + "_0001.nii.gz"))
716
+ shutil.copy(t2, join(target_imagesVal, patient_name + "_0002.nii.gz"))
717
+ shutil.copy(flair, join(target_imagesVal, patient_name + "_0003.nii.gz"))
718
+
719
+
720
+ downloaded_data_dir_test = "/home/fabian/Downloads/MICCAI_BraTS2020_TestingData"
721
+
722
+ if isdir(downloaded_data_dir_test):
723
+ for p in subdirs(downloaded_data_dir_test, join=False):
724
+ patdir = join(downloaded_data_dir_test, p)
725
+ patient_name = p
726
+ t1 = join(patdir, p + "_t1.nii.gz")
727
+ t1c = join(patdir, p + "_t1ce.nii.gz")
728
+ t2 = join(patdir, p + "_t2.nii.gz")
729
+ flair = join(patdir, p + "_flair.nii.gz")
730
+
731
+ assert all([
732
+ isfile(t1),
733
+ isfile(t1c),
734
+ isfile(t2),
735
+ isfile(flair),
736
+ ]), "%s" % patient_name
737
+
738
+ shutil.copy(t1, join(target_imagesTs, patient_name + "_0000.nii.gz"))
739
+ shutil.copy(t1c, join(target_imagesTs, patient_name + "_0001.nii.gz"))
740
+ shutil.copy(t2, join(target_imagesTs, patient_name + "_0002.nii.gz"))
741
+ shutil.copy(flair, join(target_imagesTs, patient_name + "_0003.nii.gz"))
742
+
743
+ # test set
744
+ # nnUNet_ensemble -f nnUNetTrainerV2BraTSRegions_DA3_BN_BD__nnUNetPlansv2.1_bs5_5fold nnUNetTrainerV2BraTSRegions_DA4_BN_BD__nnUNetPlansv2.1_bs5_5fold nnUNetTrainerV2BraTSRegions_DA4_BN__nnUNetPlansv2.1_bs5_15fold -o ensembled_nnUNetTrainerV2BraTSRegions_DA3_BN_BD__nnUNetPlansv2.1_bs5_5fold__nnUNetTrainerV2BraTSRegions_DA4_BN_BD__nnUNetPlansv2.1_bs5_5fold__nnUNetTrainerV2BraTSRegions_DA4_BN__nnUNetPlansv2.1_bs5_15fold
745
+ # apply_threshold_to_folder('ensembled_nnUNetTrainerV2BraTSRegions_DA3_BN_BD__nnUNetPlansv2.1_bs5_5fold__nnUNetTrainerV2BraTSRegions_DA4_BN_BD__nnUNetPlansv2.1_bs5_5fold__nnUNetTrainerV2BraTSRegions_DA4_BN__nnUNetPlansv2.1_bs5_15fold/', 'ensemble_PP200/', 200, 2)
746
+ # convert_labels_back_to_BraTS_2018_2019_convention('ensemble_PP200/', 'ensemble_PP200_converted')
747
+
748
+ # export for publication of weights
749
+ # nnUNet_export_model_to_zip -tr nnUNetTrainerV2BraTSRegions_DA4_BN -pl nnUNetPlansv2.1_bs5 -f 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 -t 82 -o nnUNetTrainerV2BraTSRegions_DA4_BN__nnUNetPlansv2.1_bs5_15fold.zip --disable_strict
750
+ # nnUNet_export_model_to_zip -tr nnUNetTrainerV2BraTSRegions_DA3_BN_BD -pl nnUNetPlansv2.1_bs5 -f 0 1 2 3 4 -t 82 -o nnUNetTrainerV2BraTSRegions_DA3_BN_BD__nnUNetPlansv2.1_bs5_5fold.zip --disable_strict
751
+ # nnUNet_export_model_to_zip -tr nnUNetTrainerV2BraTSRegions_DA4_BN_BD -pl nnUNetPlansv2.1_bs5 -f 0 1 2 3 4 -t 82 -o nnUNetTrainerV2BraTSRegions_DA4_BN_BD__nnUNetPlansv2.1_bs5_5fold.zip --disable_strict
nnunet/dataset_conversion/Task083_VerSe2020.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import shutil
17
+ from collections import OrderedDict
18
+ from copy import deepcopy
19
+ from multiprocessing.pool import Pool
20
+
21
+ from batchgenerators.utilities.file_and_folder_operations import *
22
+ from nnunet.dataset_conversion.Task056_VerSe2019 import check_if_all_in_good_orientation, \
23
+ print_unique_labels_and_their_volumes
24
+ from nnunet.paths import nnUNet_raw_data, preprocessing_output_dir
25
+ from nnunet.utilities.image_reorientation import reorient_all_images_in_folder_to_ras
26
+
27
+
28
+ def manually_change_plans():
29
+ pp_out_folder = join(preprocessing_output_dir, "Task083_VerSe2020")
30
+ original_plans = join(pp_out_folder, "nnUNetPlansv2.1_plans_3D.pkl")
31
+ assert isfile(original_plans)
32
+ original_plans = load_pickle(original_plans)
33
+
34
+ # let's change the network topology for lowres and fullres
35
+ new_plans = deepcopy(original_plans)
36
+ stages = len(new_plans['plans_per_stage'])
37
+ for s in range(stages):
38
+ new_plans['plans_per_stage'][s]['patch_size'] = (224, 160, 160)
39
+ new_plans['plans_per_stage'][s]['pool_op_kernel_sizes'] = [[2, 2, 2],
40
+ [2, 2, 2],
41
+ [2, 2, 2],
42
+ [2, 2, 2],
43
+ [2, 2, 2]] # bottleneck of 7x5x5
44
+ new_plans['plans_per_stage'][s]['conv_kernel_sizes'] = [[3, 3, 3],
45
+ [3, 3, 3],
46
+ [3, 3, 3],
47
+ [3, 3, 3],
48
+ [3, 3, 3],
49
+ [3, 3, 3]]
50
+ save_pickle(new_plans, join(pp_out_folder, "custom_plans_3D.pkl"))
51
+
52
+
53
+ if __name__ == "__main__":
54
+ ### First we create a nnunet dataset from verse. After this the images will be all willy nilly in their
55
+ # orientation because that's how VerSe comes
56
+ base = '/home/fabian/Downloads/osfstorage-archive/'
57
+
58
+ task_id = 83
59
+ task_name = "VerSe2020"
60
+
61
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
62
+
63
+ out_base = join(nnUNet_raw_data, foldername)
64
+ imagestr = join(out_base, "imagesTr")
65
+ imagests = join(out_base, "imagesTs")
66
+ labelstr = join(out_base, "labelsTr")
67
+ maybe_mkdir_p(imagestr)
68
+ maybe_mkdir_p(imagests)
69
+ maybe_mkdir_p(labelstr)
70
+
71
+ train_patient_names = []
72
+
73
+ for t in subdirs(join(base, 'training_data'), join=False):
74
+ train_patient_names_here = [i[:-len("_seg.nii.gz")] for i in
75
+ subfiles(join(base, "training_data", t), join=False, suffix="_seg.nii.gz")]
76
+ for p in train_patient_names_here:
77
+ curr = join(base, "training_data", t)
78
+ label_file = join(curr, p + "_seg.nii.gz")
79
+ image_file = join(curr, p + ".nii.gz")
80
+ shutil.copy(image_file, join(imagestr, p + "_0000.nii.gz"))
81
+ shutil.copy(label_file, join(labelstr, p + ".nii.gz"))
82
+
83
+ train_patient_names += train_patient_names_here
84
+
85
+ json_dict = OrderedDict()
86
+ json_dict['name'] = "VerSe2020"
87
+ json_dict['description'] = "VerSe2020"
88
+ json_dict['tensorImageSize'] = "4D"
89
+ json_dict['reference'] = "see challenge website"
90
+ json_dict['licence'] = "see challenge website"
91
+ json_dict['release'] = "0.0"
92
+ json_dict['modality'] = {
93
+ "0": "CT",
94
+ }
95
+ json_dict['labels'] = {i: str(i) for i in range(29)}
96
+
97
+ json_dict['numTraining'] = len(train_patient_names)
98
+ json_dict['numTest'] = []
99
+ json_dict['training'] = [
100
+ {'image': "./imagesTr/%s.nii.gz" % i.split("/")[-1], "label": "./labelsTr/%s.nii.gz" % i.split("/")[-1]} for i
101
+ in
102
+ train_patient_names]
103
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i.split("/")[-1] for i in []]
104
+
105
+ save_json(json_dict, os.path.join(out_base, "dataset.json"))
106
+
107
+ # now we reorient all those images to ras. This saves a pkl with the original affine. We need this information to
108
+ # bring our predictions into the same geometry for submission
109
+ reorient_all_images_in_folder_to_ras(imagestr, 16)
110
+ reorient_all_images_in_folder_to_ras(imagests, 16)
111
+ reorient_all_images_in_folder_to_ras(labelstr, 16)
112
+
113
+ # sanity check
114
+ check_if_all_in_good_orientation(imagestr, labelstr, join(out_base, 'sanitycheck'))
115
+ # looks good to me - proceed
116
+
117
+ # check the volumes of the vertebrae
118
+ p = Pool(6)
119
+ _ = p.starmap(print_unique_labels_and_their_volumes, zip(subfiles(labelstr, suffix='.nii.gz'), [1000] * 113))
120
+
121
+ # looks good
122
+
123
+ # Now we are ready to run nnU-Net
124
+
125
+ """# run this part of the code once training is done
126
+ folder_gt = "/media/fabian/My Book/MedicalDecathlon/nnUNet_raw_splitted/Task056_VerSe/labelsTr"
127
+
128
+ folder_pred = "/home/fabian/drives/datasets/results/nnUNet/3d_fullres/Task056_VerSe/nnUNetTrainerV2__nnUNetPlansv2.1/cv_niftis_raw"
129
+ out_json = "/home/fabian/Task056_VerSe_3d_fullres_summary.json"
130
+ evaluate_verse_folder(folder_pred, folder_gt, out_json)
131
+
132
+ folder_pred = "/home/fabian/drives/datasets/results/nnUNet/3d_lowres/Task056_VerSe/nnUNetTrainerV2__nnUNetPlansv2.1/cv_niftis_raw"
133
+ out_json = "/home/fabian/Task056_VerSe_3d_lowres_summary.json"
134
+ evaluate_verse_folder(folder_pred, folder_gt, out_json)
135
+
136
+ folder_pred = "/home/fabian/drives/datasets/results/nnUNet/3d_cascade_fullres/Task056_VerSe/nnUNetTrainerV2CascadeFullRes__nnUNetPlansv2.1/cv_niftis_raw"
137
+ out_json = "/home/fabian/Task056_VerSe_3d_cascade_fullres_summary.json"
138
+ evaluate_verse_folder(folder_pred, folder_gt, out_json)"""
nnunet/dataset_conversion/Task089_Fluo-N2DH-SIM.py ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import shutil
16
+ from multiprocessing import Pool
17
+
18
+ import SimpleITK as sitk
19
+ import numpy as np
20
+ from batchgenerators.utilities.file_and_folder_operations import *
21
+ from skimage.io import imread
22
+ from skimage.io import imsave
23
+ from skimage.morphology import disk
24
+ from skimage.morphology import erosion
25
+ from skimage.transform import resize
26
+
27
+ from nnunet.paths import nnUNet_raw_data
28
+
29
+
30
+ def load_bmp_convert_to_nifti_borders_2d(img_file, lab_file, img_out_base, anno_out, spacing, border_thickness=0.7):
31
+ img = imread(img_file)
32
+ img_itk = sitk.GetImageFromArray(img.astype(np.float32)[None])
33
+ img_itk.SetSpacing(list(spacing)[::-1] + [999])
34
+ sitk.WriteImage(img_itk, join(img_out_base + "_0000.nii.gz"))
35
+
36
+ if lab_file is not None:
37
+ l = imread(lab_file)
38
+ borders = generate_border_as_suggested_by_twollmann_2d(l, spacing, border_thickness)
39
+ l[l > 0] = 1
40
+ l[borders == 1] = 2
41
+ l_itk = sitk.GetImageFromArray(l.astype(np.uint8)[None])
42
+ l_itk.SetSpacing(list(spacing)[::-1] + [999])
43
+ sitk.WriteImage(l_itk, anno_out)
44
+
45
+
46
+ def generate_disk(spacing, radius, dtype=int):
47
+ radius_in_voxels = np.round(radius / np.array(spacing)).astype(int)
48
+ n = 2 * radius_in_voxels + 1
49
+ disk_iso = disk(max(n) * 2, dtype=np.float64)
50
+ disk_resampled = resize(disk_iso, n, 1, 'constant', 0, clip=True, anti_aliasing=False, preserve_range=True)
51
+ disk_resampled[disk_resampled > 0.5] = 1
52
+ disk_resampled[disk_resampled <= 0.5] = 0
53
+ return disk_resampled.astype(dtype)
54
+
55
+
56
+ def generate_border_as_suggested_by_twollmann_2d(label_img: np.ndarray, spacing,
57
+ border_thickness: float = 2) -> np.ndarray:
58
+ border = np.zeros_like(label_img)
59
+ selem = generate_disk(spacing, border_thickness)
60
+ for l in np.unique(label_img):
61
+ if l == 0: continue
62
+ mask = (label_img == l).astype(int)
63
+ eroded = erosion(mask, selem)
64
+ border[(eroded == 0) & (mask != 0)] = 1
65
+ return border
66
+
67
+
68
+ def prepare_task(base, task_id, task_name, spacing, border_thickness: float = 15):
69
+ p = Pool(16)
70
+
71
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
72
+
73
+ out_base = join(nnUNet_raw_data, foldername)
74
+ imagestr = join(out_base, "imagesTr")
75
+ imagests = join(out_base, "imagesTs")
76
+ labelstr = join(out_base, "labelsTr")
77
+ maybe_mkdir_p(imagestr)
78
+ maybe_mkdir_p(imagests)
79
+ maybe_mkdir_p(labelstr)
80
+
81
+ train_patient_names = []
82
+ test_patient_names = []
83
+ res = []
84
+
85
+ for train_sequence in [i for i in subfolders(base + "_train", join=False) if not i.endswith("_GT")]:
86
+ train_cases = subfiles(join(base + '_train', train_sequence), suffix=".tif", join=False)
87
+ for t in train_cases:
88
+ casename = train_sequence + "_" + t[:-4]
89
+ img_file = join(base + '_train', train_sequence, t)
90
+ lab_file = join(base + '_train', train_sequence + "_GT", "SEG", "man_seg" + t[1:])
91
+ if not isfile(lab_file):
92
+ continue
93
+ img_out_base = join(imagestr, casename)
94
+ anno_out = join(labelstr, casename + ".nii.gz")
95
+ res.append(
96
+ p.starmap_async(load_bmp_convert_to_nifti_borders_2d,
97
+ ((img_file, lab_file, img_out_base, anno_out, spacing, border_thickness),)))
98
+ train_patient_names.append(casename)
99
+
100
+ for test_sequence in [i for i in subfolders(base + "_test", join=False) if not i.endswith("_GT")]:
101
+ test_cases = subfiles(join(base + '_test', test_sequence), suffix=".tif", join=False)
102
+ for t in test_cases:
103
+ casename = test_sequence + "_" + t[:-4]
104
+ img_file = join(base + '_test', test_sequence, t)
105
+ lab_file = None
106
+ img_out_base = join(imagests, casename)
107
+ anno_out = None
108
+ res.append(
109
+ p.starmap_async(load_bmp_convert_to_nifti_borders_2d,
110
+ ((img_file, lab_file, img_out_base, anno_out, spacing, border_thickness),)))
111
+ test_patient_names.append(casename)
112
+
113
+ _ = [i.get() for i in res]
114
+
115
+ json_dict = {}
116
+ json_dict['name'] = task_name
117
+ json_dict['description'] = ""
118
+ json_dict['tensorImageSize'] = "4D"
119
+ json_dict['reference'] = ""
120
+ json_dict['licence'] = ""
121
+ json_dict['release'] = "0.0"
122
+ json_dict['modality'] = {
123
+ "0": "BF",
124
+ }
125
+ json_dict['labels'] = {
126
+ "0": "background",
127
+ "1": "cell",
128
+ "2": "border",
129
+ }
130
+
131
+ json_dict['numTraining'] = len(train_patient_names)
132
+ json_dict['numTest'] = len(test_patient_names)
133
+ json_dict['training'] = [{'image': "./imagesTr/%s.nii.gz" % i, "label": "./labelsTr/%s.nii.gz" % i} for i in
134
+ train_patient_names]
135
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i for i in test_patient_names]
136
+
137
+ save_json(json_dict, os.path.join(out_base, "dataset.json"))
138
+ p.close()
139
+ p.join()
140
+
141
+
142
+ def convert_to_instance_seg(arr: np.ndarray, spacing: tuple = (0.125, 0.125), small_center_threshold: int = 30,
143
+ isolated_border_as_separate_instance_threshold=15):
144
+ from skimage.morphology import label, dilation
145
+
146
+ # we first identify centers that are too small and set them to be border. This should remove false positive instances
147
+ objects = label((arr == 1).astype(int))
148
+ for o in np.unique(objects):
149
+ if o > 0 and np.sum(objects == o) <= small_center_threshold:
150
+ arr[objects == o] = 2
151
+
152
+ # 1 is core, 2 is border
153
+ objects = label((arr == 1).astype(int))
154
+ final = np.copy(objects)
155
+ remaining_border = arr == 2
156
+ current = np.copy(objects)
157
+ dilated_mm = np.array((0, 0))
158
+ spacing = np.array(spacing)
159
+
160
+ while np.sum(remaining_border) > 0:
161
+ strel_size = [0, 0]
162
+ maximum_dilation = max(dilated_mm)
163
+ for i in range(2):
164
+ if spacing[i] == min(spacing):
165
+ strel_size[i] = 1
166
+ continue
167
+ if dilated_mm[i] + spacing[i] / 2 < maximum_dilation:
168
+ strel_size[i] = 1
169
+ ball_here = disk(1)
170
+
171
+ if strel_size[0] == 0: ball_here = ball_here[1:2]
172
+ if strel_size[1] == 0: ball_here = ball_here[:, 1:2]
173
+
174
+ #print(1)
175
+ dilated = dilation(current, ball_here)
176
+ diff = (current == 0) & (dilated != current)
177
+ final[diff & remaining_border] = dilated[diff & remaining_border]
178
+ remaining_border[diff] = 0
179
+ current = dilated
180
+ dilated_mm = [dilated_mm[i] + spacing[i] if strel_size[i] == 1 else dilated_mm[i] for i in range(2)]
181
+
182
+ # what can happen is that a cell is so small that the network only predicted border and no core. This cell will be
183
+ # fused with the nearest other instance, which we don't want. Therefore we identify isolated border predictions and
184
+ # give them a separate instance id
185
+ # we identify isolated border predictions by checking each foreground object in arr and see whether this object
186
+ # also contains label 1
187
+ max_label = np.max(final)
188
+
189
+ foreground_objects = label((arr != 0).astype(int))
190
+ for i in np.unique(foreground_objects):
191
+ if i > 0 and (1 not in np.unique(arr[foreground_objects==i])):
192
+ size_of_object = np.sum(foreground_objects==i)
193
+ if size_of_object >= isolated_border_as_separate_instance_threshold:
194
+ final[foreground_objects == i] = max_label + 1
195
+ max_label += 1
196
+ #print('yeah boi')
197
+
198
+ return final.astype(np.uint32)
199
+
200
+
201
+ def load_convert_to_instance_save(file_in: str, file_out: str, spacing):
202
+ img = sitk.ReadImage(file_in)
203
+ img_npy = sitk.GetArrayFromImage(img)
204
+ out = convert_to_instance_seg(img_npy[0], spacing)[None]
205
+ out_itk = sitk.GetImageFromArray(out.astype(np.int16))
206
+ out_itk.CopyInformation(img)
207
+ sitk.WriteImage(out_itk, file_out)
208
+
209
+
210
+ def convert_folder_to_instanceseg(folder_in: str, folder_out: str, spacing, processes: int = 12):
211
+ input_files = subfiles(folder_in, suffix=".nii.gz", join=False)
212
+ maybe_mkdir_p(folder_out)
213
+ output_files = [join(folder_out, i) for i in input_files]
214
+ input_files = [join(folder_in, i) for i in input_files]
215
+ p = Pool(processes)
216
+ r = []
217
+ for i, o in zip(input_files, output_files):
218
+ r.append(
219
+ p.starmap_async(
220
+ load_convert_to_instance_save,
221
+ ((i, o, spacing),)
222
+ )
223
+ )
224
+ _ = [i.get() for i in r]
225
+ p.close()
226
+ p.join()
227
+
228
+
229
+ def convert_to_tiff(nifti_image: str, output_name: str):
230
+ npy = sitk.GetArrayFromImage(sitk.ReadImage(nifti_image))
231
+ imsave(output_name, npy[0].astype(np.uint16), compress=6)
232
+
233
+
234
+ if __name__ == "__main__":
235
+ base = "/home/fabian/Downloads/Fluo-N2DH-SIM+"
236
+ task_name = 'Fluo-N2DH-SIM'
237
+ spacing = (0.125, 0.125)
238
+
239
+ task_id = 999
240
+ border_thickness = 0.7
241
+ prepare_task(base, task_id, task_name, spacing, border_thickness)
242
+
243
+ task_id = 89
244
+ additional_time_steps = 4
245
+ task_name = 'Fluo-N2DH-SIM_thickborder_time'
246
+ full_taskname = 'Task%03.0d_' % task_id + task_name
247
+ output_raw = join(nnUNet_raw_data, full_taskname)
248
+ shutil.rmtree(output_raw)
249
+ shutil.copytree(join(nnUNet_raw_data, 'Task999_Fluo-N2DH-SIM_thickborder'), output_raw)
250
+
251
+ shutil.rmtree(join(nnUNet_raw_data, 'Task999_Fluo-N2DH-SIM_thickborder'))
252
+
253
+ # now add additional time information
254
+ for fld in ['imagesTr', 'imagesTs']:
255
+ curr = join(output_raw, fld)
256
+ for seq in ['01', '02']:
257
+ images = subfiles(curr, prefix=seq, join=False)
258
+ for i in images:
259
+ current_timestep = int(i.split('_')[1][1:])
260
+ renamed = join(curr, i.replace("_0000", "_%04.0d" % additional_time_steps))
261
+ shutil.move(join(curr, i), renamed)
262
+ for previous_timestep in range(-additional_time_steps, 0):
263
+ # previous time steps will already have been processed and renamed!
264
+ expected_filename = join(curr, seq + "_t%03.0d" % (
265
+ current_timestep + previous_timestep) + "_%04.0d" % additional_time_steps + ".nii.gz")
266
+ if not isfile(expected_filename):
267
+ # create empty image
268
+ img = sitk.ReadImage(renamed)
269
+ empty = sitk.GetImageFromArray(np.zeros_like(sitk.GetArrayFromImage(img)))
270
+ empty.CopyInformation(img)
271
+ sitk.WriteImage(empty, join(curr, i.replace("_0000", "_%04.0d" % (
272
+ additional_time_steps + previous_timestep))))
273
+ else:
274
+ shutil.copy(expected_filename, join(curr, i.replace("_0000", "_%04.0d" % (
275
+ additional_time_steps + previous_timestep))))
276
+ dataset = load_json(join(output_raw, 'dataset.json'))
277
+ dataset['modality'] = {
278
+ '0': 't_minus 4',
279
+ '1': 't_minus 3',
280
+ '2': 't_minus 2',
281
+ '3': 't_minus 1',
282
+ '4': 'frame of interest',
283
+ }
284
+ save_json(dataset, join(output_raw, 'dataset.json'))
285
+
286
+ # we do not need custom splits since we train on all training cases
287
+
288
+ # test set predictions are converted to instance seg with convert_folder_to_instanceseg
289
+
290
+ # test set predictions are converted to tiff with convert_to_tiff
nnunet/dataset_conversion/Task114_heart_MNMs.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import shutil
16
+ from collections import OrderedDict
17
+
18
+ import numpy as np
19
+ import pandas as pd
20
+ from batchgenerators.utilities.file_and_folder_operations import *
21
+ from numpy.random.mtrand import RandomState
22
+
23
+ from nnunet.experiment_planning.common_utils import split_4d_nifti
24
+
25
+
26
+ def get_mnms_data(data_root):
27
+ files_raw = []
28
+ files_gt = []
29
+ for r, dirs, files in os.walk(data_root):
30
+ for f in files:
31
+ if f.endswith('nii.gz'):
32
+ file_path = os.path.join(r, f)
33
+ if '_gt' in f:
34
+ files_gt.append(file_path)
35
+ else:
36
+ files_raw.append(file_path)
37
+ return files_raw, files_gt
38
+
39
+
40
+ def generate_filename_for_nnunet(pat_id, ts, pat_folder=None, add_zeros=False, vendor=None, centre=None, mode='mnms',
41
+ data_format='nii.gz'):
42
+ if not vendor or not centre:
43
+ if add_zeros:
44
+ filename = "{}_{}_0000.{}".format(pat_id, str(ts).zfill(4), data_format)
45
+ else:
46
+ filename = "{}_{}.{}".format(pat_id, str(ts).zfill(4), data_format)
47
+ else:
48
+ if mode == 'mnms':
49
+ if add_zeros:
50
+ filename = "{}_{}_{}_{}_0000.{}".format(pat_id, str(ts).zfill(4), vendor, centre, data_format)
51
+ else:
52
+ filename = "{}_{}_{}_{}.{}".format(pat_id, str(ts).zfill(4), vendor, centre, data_format)
53
+ else:
54
+ if add_zeros:
55
+ filename = "{}_{}_{}_{}_0000.{}".format(vendor, centre, pat_id, str(ts).zfill(4), data_format)
56
+ else:
57
+ filename = "{}_{}_{}_{}.{}".format(vendor, centre, pat_id, str(ts).zfill(4), data_format)
58
+
59
+ if pat_folder:
60
+ filename = os.path.join(pat_folder, filename)
61
+ return filename
62
+
63
+
64
+ def select_annotated_frames_mms(data_folder, out_folder, add_zeros=False, is_gt=False,
65
+ df_path="/media/full/tera2/data/challenges/mms/Training-corrected_original/M&Ms Dataset Information.xlsx",
66
+ mode='mnms',):
67
+ table = pd.read_excel(df_path, index_col='External code')
68
+
69
+ for idx in table.index:
70
+ ed = table.loc[idx, 'ED']
71
+ es = table.loc[idx, 'ES']
72
+ vendor = table.loc[idx, 'Vendor']
73
+ centre = table.loc[idx, 'Centre']
74
+
75
+ if vendor != "C": # vendor C is for test data
76
+
77
+ # this step is needed in case of M&Ms data to adjust it to the nnUNet frame work
78
+ # generate old filename (w/o vendor and centre)
79
+ if is_gt:
80
+ add_to_name = 'sa_gt'
81
+ else:
82
+ add_to_name = 'sa'
83
+ filename_ed_original = os.path.join(
84
+ data_folder, "{}_{}_{}.nii.gz".format(idx, add_to_name, str(ed).zfill(4)))
85
+ filename_es_original = os.path.join(
86
+ data_folder, "{}_{}_{}.nii.gz".format(idx, add_to_name, str(es).zfill(4)))
87
+
88
+ # generate new filename with vendor and centre
89
+ filename_ed = generate_filename_for_nnunet(pat_id=idx, ts=ed, pat_folder=out_folder,
90
+ vendor=vendor, centre=centre, add_zeros=add_zeros, mode=mode)
91
+ filename_es = generate_filename_for_nnunet(pat_id=idx, ts=es, pat_folder=out_folder,
92
+ vendor=vendor, centre=centre, add_zeros=add_zeros, mode=mode)
93
+
94
+ shutil.copy(filename_ed_original, filename_ed)
95
+ shutil.copy(filename_es_original, filename_es)
96
+
97
+
98
+ def create_custom_splits_for_experiments(task_path):
99
+ data_keys = [i[:-4] for i in
100
+ subfiles(os.path.join(task_path, "nnUNetData_plans_v2.1_2D_stage0"),
101
+ join=False, suffix='npz')]
102
+ existing_splits = os.path.join(task_path, "splits_final.pkl")
103
+
104
+ splits = load_pickle(existing_splits)
105
+ splits = splits[:5] # discard old changes
106
+
107
+ unique_a_only = np.unique([i.split('_')[0] for i in data_keys if i.find('_A_') != -1])
108
+ unique_b_only = np.unique([i.split('_')[0] for i in data_keys if i.find('_B_') != -1])
109
+
110
+ num_train_a = int(np.round(0.8 * len(unique_a_only)))
111
+ num_train_b = int(np.round(0.8 * len(unique_b_only)))
112
+
113
+ p = RandomState(1234)
114
+ idx_a_train = p.choice(len(unique_a_only), num_train_a, replace=False)
115
+ idx_b_train = p.choice(len(unique_b_only), num_train_b, replace=False)
116
+
117
+ identifiers_a_train = [unique_a_only[i] for i in idx_a_train]
118
+ identifiers_b_train = [unique_b_only[i] for i in idx_b_train]
119
+
120
+ identifiers_a_val = [i for i in unique_a_only if i not in identifiers_a_train]
121
+ identifiers_b_val = [i for i in unique_b_only if i not in identifiers_b_train]
122
+
123
+ # fold 5 will be train on a and eval on val sets of a and b
124
+ splits.append({'train': [i for i in data_keys if i.split("_")[0] in identifiers_a_train],
125
+ 'val': [i for i in data_keys if i.split("_")[0] in identifiers_a_val] + [i for i in data_keys if
126
+ i.split("_")[
127
+ 0] in identifiers_b_val]})
128
+
129
+ # fold 6 will be train on b and eval on val sets of a and b
130
+ splits.append({'train': [i for i in data_keys if i.split("_")[0] in identifiers_b_train],
131
+ 'val': [i for i in data_keys if i.split("_")[0] in identifiers_a_val] + [i for i in data_keys if
132
+ i.split("_")[
133
+ 0] in identifiers_b_val]})
134
+
135
+ # fold 7 train on both, eval on both
136
+ splits.append({'train': [i for i in data_keys if i.split("_")[0] in identifiers_b_train] + [i for i in data_keys if i.split("_")[0] in identifiers_a_train],
137
+ 'val': [i for i in data_keys if i.split("_")[0] in identifiers_a_val] + [i for i in data_keys if
138
+ i.split("_")[
139
+ 0] in identifiers_b_val]})
140
+ save_pickle(splits, existing_splits)
141
+
142
+
143
+ if __name__ == "__main__":
144
+ # this script will split 4d data from the M&Ms data set into 3d images for both, raw images and gt annotations.
145
+ # after this script you will be able to start a training on the M&Ms data.
146
+ # use this script as inspiration in case other data than M&Ms data is use for training.
147
+ #
148
+ # check also the comments at the END of the script for instructions on how to run the actual training after this
149
+ # script
150
+ #
151
+
152
+ # define a task ID for your experiment (I have choosen 114)
153
+ task_name = "Task679_heart_mnms"
154
+ # this is where the downloaded data from the M&Ms challenge shall be placed
155
+ raw_data_dir = "/media/full/tera2/data"
156
+ # set path to official ***M&Ms Dataset Information.xlsx*** file
157
+ df_path = "/media/full/tera2/data/challenges/mms/Training-corrected_original/M&Ms Dataset Information.xlsx"
158
+ # don't make changes here
159
+ folder_imagesTr = "imagesTr"
160
+ train_dir = os.path.join(raw_data_dir, task_name, folder_imagesTr)
161
+
162
+ # this is where our your splitted files WITH annotation will be stored. Dont make changes here. Otherwise nnUNet
163
+ # might have problems finding the training data later during the training process
164
+ out_dir = os.path.join(os.environ.get('nnUNet_raw_data_base'), 'nnUNet_raw_data', task_name)
165
+
166
+ files_raw, files_gt = get_mnms_data(data_root=train_dir)
167
+
168
+ filesTs, _ = get_mnms_data(data_root=train_dir)
169
+
170
+ split_path_raw_all_ts = os.path.join(raw_data_dir, task_name, "splitted_all_timesteps", folder_imagesTr,
171
+ "split_raw_images")
172
+ split_path_gt_all_ts = os.path.join(raw_data_dir, task_name, "splitted_all_timesteps", folder_imagesTr,
173
+ "split_annotation")
174
+ maybe_mkdir_p(split_path_raw_all_ts)
175
+ maybe_mkdir_p(split_path_gt_all_ts)
176
+
177
+ # for fast splitting of many patients use the following lines
178
+ # however keep in mind that these lines cause problems for some users.
179
+ # If problems occur use the code for loops below
180
+ # print("splitting raw 4d images into 3d images")
181
+ # split_4d_for_all_pat(files_raw, split_path_raw)
182
+ # print("splitting ground truth 4d into 3d files")
183
+ # split_4d_for_all_pat(files_gt, split_path_gt_all_ts)
184
+
185
+ print("splitting raw 4d images into 3d images")
186
+ for f in files_raw:
187
+ print("splitting {}".format(f))
188
+ split_4d_nifti(f, split_path_raw_all_ts)
189
+ print("splitting ground truth 4d into 3d files")
190
+ for gt in files_gt:
191
+ split_4d_nifti(gt, split_path_gt_all_ts)
192
+ print("splitting {}".format(gt))
193
+
194
+ print("prepared data will be saved at: {}".format(out_dir))
195
+ maybe_mkdir_p(join(out_dir, "imagesTr"))
196
+ maybe_mkdir_p(join(out_dir, "labelsTr"))
197
+
198
+ imagesTr_path = os.path.join(out_dir, "imagesTr")
199
+ labelsTr_path = os.path.join(out_dir, "labelsTr")
200
+ # only a small fraction of all timestep in the cardiac cycle possess gt annotation. These timestep will now be
201
+ # selected
202
+ select_annotated_frames_mms(split_path_raw_all_ts, imagesTr_path, add_zeros=True, is_gt=False, df_path=df_path)
203
+ select_annotated_frames_mms(split_path_gt_all_ts, labelsTr_path, add_zeros=False, is_gt=True, df_path=df_path)
204
+
205
+ labelsTr = subfiles(labelsTr_path)
206
+
207
+ # create a json file that will be needed by nnUNet to initiate the preprocessing process
208
+ json_dict = OrderedDict()
209
+ json_dict['name'] = "M&Ms"
210
+ json_dict['description'] = "short axis cardiac cine MRI segmentation"
211
+ json_dict['tensorImageSize'] = "4D"
212
+ json_dict['reference'] = "Campello, Victor M et al. “Multi-Centre, Multi-Vendor and Multi-Disease Cardiac " \
213
+ "Segmentation: The M&Ms Challenge.” IEEE transactions on " \
214
+ "medical imaging vol. 40,12 (2021): 3543-3554. doi:10.1109/TMI.2021.3090082"
215
+ json_dict['licence'] = "see M&Ms challenge"
216
+ json_dict['release'] = "0.0"
217
+ json_dict['modality'] = {
218
+ "0": "MRI",
219
+ }
220
+ # labels differ for ACDC challenge
221
+ json_dict['labels'] = {
222
+ "0": "background",
223
+ "1": "LVBP",
224
+ "2": "LVM",
225
+ "3": "RV"
226
+ }
227
+ json_dict['numTraining'] = len(labelsTr)
228
+ json_dict['numTest'] = 0
229
+ json_dict['training'] = [{'image': "./imagesTr/%s" % i.split("/")[-1],
230
+ "label": "./labelsTr/%s" % i.split("/")[-1]} for i in labelsTr]
231
+ json_dict['test'] = []
232
+
233
+ save_json(json_dict, os.path.join(out_dir, "dataset.json"))
234
+
235
+ #
236
+ # now the data is ready to be preprocessed by the nnUNet
237
+ # the following steps are only needed if you want to reproduce the exact results from the MMS challenge
238
+ #
239
+
240
+
241
+ # then preprocess data and plan training.
242
+ # run in terminal
243
+ # nnUNet_plan_and_preprocess -t 114 --verify_dataset_integrity # for 2d
244
+ # nnUNet_plan_and_preprocess -t 114 --verify_dataset_integrity -pl3d ExperimentPlannerTargetSpacingForAnisoAxis # for 3d
245
+
246
+ # start training and stop it immediately to get a split.pkl file
247
+ # nnUNet_train 2d nnUNetTrainerV2_MMS 114 0
248
+
249
+ #
250
+ # then create custom splits as used for the final M&Ms submission
251
+ #
252
+
253
+ # in this file comment everything except for the following line
254
+ # create_custom_splits_for_experiments(out_dir)
255
+
256
+ # then start training with
257
+ #
258
+ # nnUNet_train 3d_fullres nnUNetTrainerV2_MMS Task114_heart_mnms -p nnUNetPlanstargetSpacingForAnisoAxis 0 # for 3d and fold 0
259
+ # and
260
+ # nnUNet_train 2d nnUNetTrainerV2_MMS Task114_heart_mnms 0 # for 2d and fold 0
261
+
262
+
nnunet/dataset_conversion/Task115_COVIDSegChallenge.py ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import shutil
15
+ import subprocess
16
+
17
+ import SimpleITK as sitk
18
+ import numpy as np
19
+ from batchgenerators.utilities.file_and_folder_operations import *
20
+
21
+ from nnunet.dataset_conversion.utils import generate_dataset_json
22
+ from nnunet.paths import nnUNet_raw_data
23
+ from nnunet.paths import preprocessing_output_dir
24
+ from nnunet.utilities.task_name_id_conversion import convert_id_to_task_name
25
+
26
+
27
+ def increase_batch_size(plans_file: str, save_as: str, bs_factor: int):
28
+ a = load_pickle(plans_file)
29
+ stages = list(a['plans_per_stage'].keys())
30
+ for s in stages:
31
+ a['plans_per_stage'][s]['batch_size'] *= bs_factor
32
+ save_pickle(a, save_as)
33
+
34
+
35
+ def prepare_submission(folder_in, folder_out):
36
+ nii = subfiles(folder_in, suffix='.gz', join=False)
37
+ maybe_mkdir_p(folder_out)
38
+ for n in nii:
39
+ i = n.split('-')[-1][:-10]
40
+ shutil.copy(join(folder_in, n), join(folder_out, i + '.nii.gz'))
41
+
42
+
43
+ def get_ids_from_folder(folder):
44
+ cts = subfiles(folder, suffix='_ct.nii.gz', join=False)
45
+ ids = []
46
+ for c in cts:
47
+ ids.append(c.split('-')[-1][:-10])
48
+ return ids
49
+
50
+
51
+ def postprocess_submission(folder_ct, folder_pred, folder_postprocessed, bbox_distance_to_seg_in_cm=7.5):
52
+ """
53
+ segment with lung mask, get bbox from that, use bbox to remove predictions in background
54
+
55
+ WE EXPERIMENTED WITH THAT ON THE VALIDATION SET AND FOUND THAT IT DOESN'T DO ANYTHING. NOT USED FOR TEST SET
56
+ """
57
+ # pip install git+https://github.com/JoHof/lungmask
58
+ cts = subfiles(folder_ct, suffix='_ct.nii.gz', join=False)
59
+ output_files = [i[:-10] + '_lungmask.nii.gz' for i in cts]
60
+
61
+ # run lungmask on everything
62
+ for i, o in zip(cts, output_files):
63
+ if not isfile(join(folder_ct, o)):
64
+ subprocess.call(['lungmask', join(folder_ct, i), join(folder_ct, o), '--modelname', 'R231CovidWeb'])
65
+
66
+ if not isdir(folder_postprocessed):
67
+ maybe_mkdir_p(folder_postprocessed)
68
+
69
+ ids = get_ids_from_folder(folder_ct)
70
+ for i in ids:
71
+ # find lungmask
72
+ lungmask_file = join(folder_ct, 'volume-covid19-A-' + i + '_lungmask.nii.gz')
73
+ if not isfile(lungmask_file):
74
+ raise RuntimeError('missing lung')
75
+ seg_file = join(folder_pred, 'volume-covid19-A-' + i + '_ct.nii.gz')
76
+ if not isfile(seg_file):
77
+ raise RuntimeError('missing seg')
78
+
79
+ lung_mask = sitk.GetArrayFromImage(sitk.ReadImage(lungmask_file))
80
+ seg_itk = sitk.ReadImage(seg_file)
81
+ seg = sitk.GetArrayFromImage(seg_itk)
82
+
83
+ where = np.argwhere(lung_mask != 0)
84
+ bbox = [
85
+ [min(where[:, 0]), max(where[:, 0])],
86
+ [min(where[:, 1]), max(where[:, 1])],
87
+ [min(where[:, 2]), max(where[:, 2])],
88
+ ]
89
+
90
+ spacing = np.array(seg_itk.GetSpacing())[::-1]
91
+ # print(bbox)
92
+ for dim in range(3):
93
+ sp = spacing[dim]
94
+ voxels_extend = max(int(np.ceil(bbox_distance_to_seg_in_cm / sp)), 1)
95
+ bbox[dim][0] = max(0, bbox[dim][0] - voxels_extend)
96
+ bbox[dim][1] = min(seg.shape[dim], bbox[dim][1] + voxels_extend)
97
+ # print(bbox)
98
+
99
+ seg_old = np.copy(seg)
100
+ seg[0:bbox[0][0], :, :] = 0
101
+ seg[bbox[0][1]:, :, :] = 0
102
+ seg[:, 0:bbox[1][0], :] = 0
103
+ seg[:, bbox[1][1]:, :] = 0
104
+ seg[:, :, 0:bbox[2][0]] = 0
105
+ seg[:, :, bbox[2][1]:] = 0
106
+ if np.any(seg_old != seg):
107
+ print('changed seg', i)
108
+ argwhere = np.argwhere(seg != seg_old)
109
+ print(argwhere[np.random.choice(len(argwhere), 10)])
110
+
111
+ seg_corr = sitk.GetImageFromArray(seg)
112
+ seg_corr.CopyInformation(seg_itk)
113
+ sitk.WriteImage(seg_corr, join(folder_postprocessed, 'volume-covid19-A-' + i + '_ct.nii.gz'))
114
+
115
+
116
+ def manually_set_configurations():
117
+ """
118
+ ALSO NOT USED!
119
+ :return:
120
+ """
121
+ task115_dir = join(preprocessing_output_dir, convert_id_to_task_name(115))
122
+
123
+ ## larger patch size
124
+
125
+ # task115 3d_fullres default is:
126
+ """
127
+ {'batch_size': 2,
128
+ 'num_pool_per_axis': [2, 6, 6],
129
+ 'patch_size': array([ 28, 256, 256]),
130
+ 'median_patient_size_in_voxels': array([ 62, 512, 512]),
131
+ 'current_spacing': array([5. , 0.74199998, 0.74199998]),
132
+ 'original_spacing': array([5. , 0.74199998, 0.74199998]),
133
+ 'do_dummy_2D_data_aug': True,
134
+ 'pool_op_kernel_sizes': [[1, 2, 2], [1, 2, 2], [2, 2, 2], [2, 2, 2], [1, 2, 2], [1, 2, 2]],
135
+ 'conv_kernel_sizes': [[1, 3, 3], [1, 3, 3], [3, 3, 3], [3, 3, 3], [3, 3, 3], [3, 3, 3], [3, 3, 3]]}
136
+ """
137
+ plans = load_pickle(join(task115_dir, 'nnUNetPlansv2.1_plans_3D.pkl'))
138
+ fullres_stage = plans['plans_per_stage'][1]
139
+ fullres_stage['patch_size'] = np.array([ 64, 320, 320])
140
+ fullres_stage['num_pool_per_axis'] = [4, 6, 6]
141
+ fullres_stage['pool_op_kernel_sizes'] = [[1, 2, 2],
142
+ [1, 2, 2],
143
+ [2, 2, 2],
144
+ [2, 2, 2],
145
+ [2, 2, 2],
146
+ [2, 2, 2]]
147
+ fullres_stage['conv_kernel_sizes'] = [[1, 3, 3],
148
+ [1, 3, 3],
149
+ [3, 3, 3],
150
+ [3, 3, 3],
151
+ [3, 3, 3],
152
+ [3, 3, 3],
153
+ [3, 3, 3]]
154
+
155
+ save_pickle(plans, join(task115_dir, 'nnUNetPlansv2.1_custom_plans_3D.pkl'))
156
+
157
+ ## larger batch size
158
+ # (default for all 3d trainings is batch size 2)
159
+ increase_batch_size(join(task115_dir, 'nnUNetPlansv2.1_plans_3D.pkl'), join(task115_dir, 'nnUNetPlansv2.1_bs3x_plans_3D.pkl'), 3)
160
+ increase_batch_size(join(task115_dir, 'nnUNetPlansv2.1_plans_3D.pkl'), join(task115_dir, 'nnUNetPlansv2.1_bs5x_plans_3D.pkl'), 5)
161
+
162
+ # residual unet
163
+ """
164
+ default is:
165
+ Out[7]:
166
+ {'batch_size': 2,
167
+ 'num_pool_per_axis': [2, 6, 5],
168
+ 'patch_size': array([ 28, 256, 224]),
169
+ 'median_patient_size_in_voxels': array([ 62, 512, 512]),
170
+ 'current_spacing': array([5. , 0.74199998, 0.74199998]),
171
+ 'original_spacing': array([5. , 0.74199998, 0.74199998]),
172
+ 'do_dummy_2D_data_aug': True,
173
+ 'pool_op_kernel_sizes': [[1, 1, 1],
174
+ [1, 2, 2],
175
+ [1, 2, 2],
176
+ [2, 2, 2],
177
+ [2, 2, 2],
178
+ [1, 2, 2],
179
+ [1, 2, 1]],
180
+ 'conv_kernel_sizes': [[1, 3, 3],
181
+ [1, 3, 3],
182
+ [3, 3, 3],
183
+ [3, 3, 3],
184
+ [3, 3, 3],
185
+ [3, 3, 3],
186
+ [3, 3, 3]],
187
+ 'num_blocks_encoder': (1, 2, 3, 4, 4, 4, 4),
188
+ 'num_blocks_decoder': (1, 1, 1, 1, 1, 1)}
189
+ """
190
+ plans = load_pickle(join(task115_dir, 'nnUNetPlans_FabiansResUNet_v2.1_plans_3D.pkl'))
191
+ fullres_stage = plans['plans_per_stage'][1]
192
+ fullres_stage['patch_size'] = np.array([ 56, 256, 256])
193
+ fullres_stage['num_pool_per_axis'] = [3, 6, 6]
194
+ fullres_stage['pool_op_kernel_sizes'] = [[1, 1, 1],
195
+ [1, 2, 2],
196
+ [1, 2, 2],
197
+ [2, 2, 2],
198
+ [2, 2, 2],
199
+ [2, 2, 2],
200
+ [1, 2, 2]]
201
+ fullres_stage['conv_kernel_sizes'] = [[1, 3, 3],
202
+ [1, 3, 3],
203
+ [3, 3, 3],
204
+ [3, 3, 3],
205
+ [3, 3, 3],
206
+ [3, 3, 3],
207
+ [3, 3, 3]]
208
+ save_pickle(plans, join(task115_dir, 'nnUNetPlans_FabiansResUNet_v2.1_custom_plans_3D.pkl'))
209
+
210
+
211
+ def check_same(img1: str, img2: str):
212
+ """
213
+ checking initial vs corrected dataset
214
+ :param img1:
215
+ :param img2:
216
+ :return:
217
+ """
218
+ img1 = sitk.GetArrayFromImage(sitk.ReadImage(img1))
219
+ img2 = sitk.GetArrayFromImage(sitk.ReadImage(img2))
220
+ if not np.all([i==j for i, j in zip(img1.shape, img2.shape)]):
221
+ print('shape')
222
+ return False
223
+ else:
224
+ same = np.all(img1==img2)
225
+ if same: return True
226
+ else:
227
+ diffs = np.argwhere(img1!=img2)
228
+ print('content in', diffs.shape[0], 'voxels')
229
+ print('random disagreements:')
230
+ print(diffs[np.random.choice(len(diffs), min(3, diffs.shape[0]), replace=False)])
231
+ return False
232
+
233
+
234
+ def check_dataset_same(dataset_old='/home/fabian/Downloads/COVID-19-20/Train',
235
+ dataset_new='/home/fabian/data/COVID-19-20_officialCorrected/COVID-19-20_v2/Train'):
236
+ """
237
+ :param dataset_old:
238
+ :param dataset_new:
239
+ :return:
240
+ """
241
+ cases = [i[:-10] for i in subfiles(dataset_new, suffix='_ct.nii.gz', join=False)]
242
+ for c in cases:
243
+ data_file = join(dataset_old, c + '_ct_corrDouble.nii.gz')
244
+ corrected_double = False
245
+ if not isfile(data_file):
246
+ data_file = join(dataset_old, c+'_ct.nii.gz')
247
+ else:
248
+ corrected_double = True
249
+ data_file_new = join(dataset_new, c+'_ct.nii.gz')
250
+
251
+ same = check_same(data_file, data_file_new)
252
+ if not same: print('data differs in case', c, '\n')
253
+
254
+ seg_file = join(dataset_old, c + '_seg_corrDouble_corrected.nii.gz')
255
+ if not isfile(seg_file):
256
+ seg_file = join(dataset_old, c + '_seg_corrected_auto.nii.gz')
257
+ if isfile(seg_file):
258
+ assert ~corrected_double
259
+ else:
260
+ seg_file = join(dataset_old, c + '_seg_corrected.nii.gz')
261
+ if isfile(seg_file):
262
+ assert ~corrected_double
263
+ else:
264
+ seg_file = join(dataset_old, c + '_seg_corrDouble.nii.gz')
265
+ if isfile(seg_file):
266
+ assert ~corrected_double
267
+ else:
268
+ seg_file = join(dataset_old, c + '_seg.nii.gz')
269
+ seg_file_new = join(dataset_new, c + '_seg.nii.gz')
270
+ same = check_same(seg_file, seg_file_new)
271
+ if not same: print('seg differs in case', c, '\n')
272
+
273
+
274
+ if __name__ == '__main__':
275
+ # this is the folder containing the data as downloaded from https://covid-segmentation.grand-challenge.org/COVID-19-20/
276
+ # (zip file was decompressed!)
277
+ downloaded_data_dir = '/home/fabian/data/COVID-19-20_officialCorrected/COVID-19-20_v2/'
278
+
279
+ task_name = "Task115_COVIDSegChallenge"
280
+
281
+ target_base = join(nnUNet_raw_data, task_name)
282
+
283
+ target_imagesTr = join(target_base, "imagesTr")
284
+ target_imagesVal = join(target_base, "imagesVal")
285
+ target_labelsTr = join(target_base, "labelsTr")
286
+
287
+ maybe_mkdir_p(target_imagesTr)
288
+ maybe_mkdir_p(target_imagesVal)
289
+ maybe_mkdir_p(target_labelsTr)
290
+
291
+ train_orig = join(downloaded_data_dir, "Train")
292
+
293
+ # convert training set
294
+ cases = [i[:-10] for i in subfiles(train_orig, suffix='_ct.nii.gz', join=False)]
295
+ for c in cases:
296
+ data_file = join(train_orig, c+'_ct.nii.gz')
297
+
298
+ # before there was the official corrected dataset we did some corrections of our own. These corrections were
299
+ # dropped when the official dataset was revised.
300
+ seg_file = join(train_orig, c + '_seg_corrected.nii.gz')
301
+ if not isfile(seg_file):
302
+ seg_file = join(train_orig, c + '_seg.nii.gz')
303
+
304
+ shutil.copy(data_file, join(target_imagesTr, c + "_0000.nii.gz"))
305
+ shutil.copy(seg_file, join(target_labelsTr, c + '.nii.gz'))
306
+
307
+ val_orig = join(downloaded_data_dir, "Validation")
308
+ cases = [i[:-10] for i in subfiles(val_orig, suffix='_ct.nii.gz', join=False)]
309
+ for c in cases:
310
+ data_file = join(val_orig, c + '_ct.nii.gz')
311
+
312
+ shutil.copy(data_file, join(target_imagesVal, c + "_0000.nii.gz"))
313
+
314
+ generate_dataset_json(
315
+ join(target_base, 'dataset.json'),
316
+ target_imagesTr,
317
+ None,
318
+ ("CT", ),
319
+ {0: 'background', 1: 'covid'},
320
+ task_name,
321
+ dataset_reference='https://covid-segmentation.grand-challenge.org/COVID-19-20/'
322
+ )
323
+
324
+ # performance summary (train set 5-fold cross-validation)
325
+
326
+ # baselines
327
+ # 3d_fullres nnUNetTrainerV2__nnUNetPlans_v2.1 0.7441
328
+ # 3d_lowres nnUNetTrainerV2__nnUNetPlans_v2.1 0.745
329
+
330
+ # models used for test set prediction
331
+ # 3d_fullres nnUNetTrainerV2_ResencUNet_DA3__nnUNetPlans_FabiansResUNet_v2.1 0.7543
332
+ # 3d_fullres nnUNetTrainerV2_ResencUNet__nnUNetPlans_FabiansResUNet_v2.1 0.7527
333
+ # 3d_lowres nnUNetTrainerV2_ResencUNet_DA3_BN__nnUNetPlans_FabiansResUNet_v2.1 0.7513
334
+ # 3d_fullres nnUNetTrainerV2_DA3_BN__nnUNetPlans_v2.1 0.7498
335
+ # 3d_fullres nnUNetTrainerV2_DA3__nnUNetPlans_v2.1 0.7532
336
+
337
+ # Test set prediction
338
+ # nnUNet_predict -i COVID-19-20_TestSet -o covid_testset_predictions/3d_fullres/nnUNetTrainerV2_ResencUNet_DA3__nnUNetPlans_FabiansResUNet_v2.1 -tr nnUNetTrainerV2_ResencUNet_DA3 -p nnUNetPlans_FabiansResUNet_v2.1 -m 3d_fullres -f 0 1 2 3 4 5 6 7 8 9 -t 115 -z
339
+ # nnUNet_predict -i COVID-19-20_TestSet -o covid_testset_predictions/3d_fullres/nnUNetTrainerV2_ResencUNet__nnUNetPlans_FabiansResUNet_v2.1 -tr nnUNetTrainerV2_ResencUNet -p nnUNetPlans_FabiansResUNet_v2.1 -m 3d_fullres -f 0 1 2 3 4 5 6 7 8 9 -t 115 -z
340
+ # nnUNet_predict -i COVID-19-20_TestSet -o covid_testset_predictions/3d_lowres/nnUNetTrainerV2_ResencUNet_DA3_BN__nnUNetPlans_FabiansResUNet_v2.1 -tr nnUNetTrainerV2_ResencUNet_DA3_BN -p nnUNetPlans_FabiansResUNet_v2.1 -m 3d_lowres -f 0 1 2 3 4 5 6 7 8 9 -t 115 -z
341
+ # nnUNet_predict -i COVID-19-20_TestSet -o covid_testset_predictions/3d_fullres/nnUNetTrainerV2_DA3_BN__nnUNetPlans_v2.1 -tr nnUNetTrainerV2_DA3_BN -m 3d_fullres -f 0 1 2 3 4 5 6 7 8 9 -t 115 -z
342
+ # nnUNet_predict -i COVID-19-20_TestSet -o covid_testset_predictions/3d_fullres/nnUNetTrainerV2_DA3__nnUNetPlans_v2.1 -tr nnUNetTrainerV2_DA3 -m 3d_fullres -f 0 1 2 3 4 5 6 7 8 9 -t 115 -z
343
+
344
+ # nnUNet_ensemble -f 3d_lowres/nnUNetTrainerV2_ResencUNet_DA3_BN__nnUNetPlans_FabiansResUNet_v2.1/ 3d_fullres/nnUNetTrainerV2_ResencUNet__nnUNetPlans_FabiansResUNet_v2.1/ 3d_fullres/nnUNetTrainerV2_ResencUNet_DA3__nnUNetPlans_FabiansResUNet_v2.1/ 3d_fullres/nnUNetTrainerV2_DA3_BN__nnUNetPlans_v2.1/ 3d_fullres/nnUNetTrainerV2_DA3__nnUNetPlans_v2.1/ -o ensembled
nnunet/dataset_conversion/Task120_Massachusetts_RoadSegm.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from batchgenerators.utilities.file_and_folder_operations import *
3
+ from nnunet.dataset_conversion.utils import generate_dataset_json
4
+ from nnunet.paths import nnUNet_raw_data, preprocessing_output_dir
5
+ from nnunet.utilities.file_conversions import convert_2d_image_to_nifti
6
+
7
+ if __name__ == '__main__':
8
+ """
9
+ nnU-Net was originally built for 3D images. It is also strongest when applied to 3D segmentation problems because a
10
+ large proportion of its design choices were built with 3D in mind. Also note that many 2D segmentation problems,
11
+ especially in the non-biomedical domain, may benefit from pretrained network architectures which nnU-Net does not
12
+ support.
13
+ Still, there is certainly a need for an out of the box segmentation solution for 2D segmentation problems. And
14
+ also on 2D segmentation tasks nnU-Net cam perform extremely well! We have, for example, won a 2D task in the cell
15
+ tracking challenge with nnU-Net (see our Nature Methods paper) and we have also successfully applied nnU-Net to
16
+ histopathological segmentation problems.
17
+ Working with 2D data in nnU-Net requires a small workaround in the creation of the dataset. Essentially, all images
18
+ must be converted to pseudo 3D images (so an image with shape (X, Y) needs to be converted to an image with shape
19
+ (1, X, Y). The resulting image must be saved in nifti format. Hereby it is important to set the spacing of the
20
+ first axis (the one with shape 1) to a value larger than the others. If you are working with niftis anyways, then
21
+ doing this should be easy for you. This example here is intended for demonstrating how nnU-Net can be used with
22
+ 'regular' 2D images. We selected the massachusetts road segmentation dataset for this because it can be obtained
23
+ easily, it comes with a good amount of training cases but is still not too large to be difficult to handle.
24
+ """
25
+
26
+ # download dataset from https://www.kaggle.com/insaff/massachusetts-roads-dataset
27
+ # extract the zip file, then set the following path according to your system:
28
+ base = '/media/fabian/data/road_segmentation_ideal'
29
+ # this folder should have the training and testing subfolders
30
+
31
+ # now start the conversion to nnU-Net:
32
+ task_name = 'Task120_MassRoadsSeg'
33
+ target_base = join(nnUNet_raw_data, task_name)
34
+ target_imagesTr = join(target_base, "imagesTr")
35
+ target_imagesTs = join(target_base, "imagesTs")
36
+ target_labelsTs = join(target_base, "labelsTs")
37
+ target_labelsTr = join(target_base, "labelsTr")
38
+
39
+ maybe_mkdir_p(target_imagesTr)
40
+ maybe_mkdir_p(target_labelsTs)
41
+ maybe_mkdir_p(target_imagesTs)
42
+ maybe_mkdir_p(target_labelsTr)
43
+
44
+ # convert the training examples. Not all training images have labels, so we just take the cases for which there are
45
+ # labels
46
+ labels_dir_tr = join(base, 'training', 'output')
47
+ images_dir_tr = join(base, 'training', 'input')
48
+ training_cases = subfiles(labels_dir_tr, suffix='.png', join=False)
49
+ for t in training_cases:
50
+ unique_name = t[:-4] # just the filename with the extension cropped away, so img-2.png becomes img-2 as unique_name
51
+ input_segmentation_file = join(labels_dir_tr, t)
52
+ input_image_file = join(images_dir_tr, t)
53
+
54
+ output_image_file = join(target_imagesTr, unique_name) # do not specify a file ending! This will be done for you
55
+ output_seg_file = join(target_labelsTr, unique_name) # do not specify a file ending! This will be done for you
56
+
57
+ # this utility will convert 2d images that can be read by skimage.io.imread to nifti. You don't need to do anything.
58
+ # if this throws an error for your images, please just look at the code for this function and adapt it to your needs
59
+ convert_2d_image_to_nifti(input_image_file, output_image_file, is_seg=False)
60
+
61
+ # the labels are stored as 0: background, 255: road. We need to convert the 255 to 1 because nnU-Net expects
62
+ # the labels to be consecutive integers. This can be achieved with setting a transform
63
+ convert_2d_image_to_nifti(input_segmentation_file, output_seg_file, is_seg=True,
64
+ transform=lambda x: (x == 255).astype(int))
65
+
66
+ # now do the same for the test set
67
+ labels_dir_ts = join(base, 'testing', 'output')
68
+ images_dir_ts = join(base, 'testing', 'input')
69
+ testing_cases = subfiles(labels_dir_ts, suffix='.png', join=False)
70
+ for ts in testing_cases:
71
+ unique_name = ts[:-4]
72
+ input_segmentation_file = join(labels_dir_ts, ts)
73
+ input_image_file = join(images_dir_ts, ts)
74
+
75
+ output_image_file = join(target_imagesTs, unique_name)
76
+ output_seg_file = join(target_labelsTs, unique_name)
77
+
78
+ convert_2d_image_to_nifti(input_image_file, output_image_file, is_seg=False)
79
+ convert_2d_image_to_nifti(input_segmentation_file, output_seg_file, is_seg=True,
80
+ transform=lambda x: (x == 255).astype(int))
81
+
82
+ # finally we can call the utility for generating a dataset.json
83
+ generate_dataset_json(join(target_base, 'dataset.json'), target_imagesTr, target_imagesTs, ('Red', 'Green', 'Blue'),
84
+ labels={0: 'background', 1: 'street'}, dataset_name=task_name, license='hands off!')
85
+
86
+ """
87
+ once this is completed, you can use the dataset like any other nnU-Net dataset. Note that since this is a 2D
88
+ dataset there is no need to run preprocessing for 3D U-Nets. You should therefore run the
89
+ `nnUNet_plan_and_preprocess` command like this:
90
+
91
+ > nnUNet_plan_and_preprocess -t 120 -pl3d None
92
+
93
+ once that is completed, you can run the trainings as follows:
94
+ > nnUNet_train 2d nnUNetTrainerV2 120 FOLD
95
+
96
+ (where fold is again 0, 1, 2, 3 and 4 - 5-fold cross validation)
97
+
98
+ there is no need to run nnUNet_find_best_configuration because there is only one model to choose from.
99
+ Note that without running nnUNet_find_best_configuration, nnU-Net will not have determined a postprocessing
100
+ for the whole cross-validation. Spoiler: it will determine not to run postprocessing anyways. If you are using
101
+ a different 2D dataset, you can make nnU-Net determine the postprocessing by using the
102
+ `nnUNet_determine_postprocessing` command
103
+ """
nnunet/dataset_conversion/Task135_KiTS2021.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from batchgenerators.utilities.file_and_folder_operations import *
2
+ import shutil
3
+
4
+ from nnunet.paths import nnUNet_raw_data
5
+ from nnunet.dataset_conversion.utils import generate_dataset_json
6
+
7
+ if __name__ == '__main__':
8
+ # this is the data folder from the kits21 github repository, see https://github.com/neheller/kits21
9
+ kits_data_dir = '/home/fabian/git_repos/kits21/kits21/data'
10
+
11
+ # This script uses the majority voted segmentation as ground truth
12
+ kits_segmentation_filename = 'aggregated_MAJ_seg.nii.gz'
13
+
14
+ # Arbitrary task id. This is just to ensure each dataset ha a unique number. Set this to whatever ([0-999]) you
15
+ # want
16
+ task_id = 135
17
+ task_name = "KiTS2021"
18
+
19
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
20
+
21
+ # setting up nnU-Net folders
22
+ out_base = join(nnUNet_raw_data, foldername)
23
+ imagestr = join(out_base, "imagesTr")
24
+ labelstr = join(out_base, "labelsTr")
25
+ maybe_mkdir_p(imagestr)
26
+ maybe_mkdir_p(labelstr)
27
+
28
+ case_ids = subdirs(kits_data_dir, prefix='case_', join=False)
29
+ for c in case_ids:
30
+ if isfile(join(kits_data_dir, c, kits_segmentation_filename)):
31
+ shutil.copy(join(kits_data_dir, c, kits_segmentation_filename), join(labelstr, c + '.nii.gz'))
32
+ shutil.copy(join(kits_data_dir, c, 'imaging.nii.gz'), join(imagestr, c + '_0000.nii.gz'))
33
+
34
+ generate_dataset_json(join(out_base, 'dataset.json'),
35
+ imagestr,
36
+ None,
37
+ ('CT',),
38
+ {
39
+ 0: 'background',
40
+ 1: "kidney",
41
+ 2: "tumor",
42
+ 3: "cyst",
43
+ },
44
+ task_name,
45
+ license='see https://kits21.kits-challenge.org/participate#download-block',
46
+ dataset_description='see https://kits21.kits-challenge.org/',
47
+ dataset_reference='https://www.sciencedirect.com/science/article/abs/pii/S1361841520301857, '
48
+ 'https://kits21.kits-challenge.org/',
49
+ dataset_release='0')
nnunet/dataset_conversion/Task154_RibFrac_multi_label.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import SimpleITK as sitk
2
+ from natsort import natsorted
3
+ import numpy as np
4
+ from pathlib import Path
5
+ import pandas as pd
6
+ from collections import defaultdict
7
+ from shutil import copyfile
8
+ import os
9
+ from os.path import join
10
+ from tqdm import tqdm
11
+ import gc
12
+ import multiprocessing as mp
13
+ from nnunet.dataset_conversion.utils import generate_dataset_json
14
+ from functools import partial
15
+
16
+
17
+ def preprocess_dataset(dataset_load_path, dataset_save_path, pool):
18
+ train_image_load_path = join(dataset_load_path, "imagesTr")
19
+ train_mask_load_path = join(dataset_load_path, "labelsTr")
20
+ test_image_load_path = join(dataset_load_path, "imagesTs")
21
+
22
+ ribfrac_train_info_1_path = join(dataset_load_path, "ribfrac-train-info-1.csv")
23
+ ribfrac_train_info_2_path = join(dataset_load_path, "ribfrac-train-info-2.csv")
24
+ ribfrac_val_info_path = join(dataset_load_path, "ribfrac-val-info.csv")
25
+
26
+ train_image_save_path = join(dataset_save_path, "imagesTr")
27
+ train_mask_save_path = join(dataset_save_path, "labelsTr")
28
+ test_image_save_path = join(dataset_save_path, "imagesTs")
29
+ Path(train_image_save_path).mkdir(parents=True, exist_ok=True)
30
+ Path(train_mask_save_path).mkdir(parents=True, exist_ok=True)
31
+ Path(test_image_save_path).mkdir(parents=True, exist_ok=True)
32
+
33
+ meta_data = preprocess_csv(ribfrac_train_info_1_path, ribfrac_train_info_2_path, ribfrac_val_info_path)
34
+ preprocess_train(train_image_load_path, train_mask_load_path, meta_data, dataset_save_path, pool)
35
+ preprocess_test(test_image_load_path, dataset_save_path)
36
+
37
+
38
+ def preprocess_csv(ribfrac_train_info_1_path, ribfrac_train_info_2_path, ribfrac_val_info_path):
39
+ print("Processing csv...")
40
+ meta_data = defaultdict(list)
41
+ for csv_path in [ribfrac_train_info_1_path, ribfrac_train_info_2_path, ribfrac_val_info_path]:
42
+ df = pd.read_csv(csv_path)
43
+ for index, row in df.iterrows():
44
+ name = row["public_id"]
45
+ instance = row["label_id"]
46
+ class_label = row["label_code"]
47
+ meta_data[name].append({"instance": instance, "class_label": class_label})
48
+ print("Finished csv processing.")
49
+ return meta_data
50
+
51
+
52
+ def preprocess_train(image_path, mask_path, meta_data, save_path, pool):
53
+ print("Processing train data...")
54
+ pool.map(partial(preprocess_train_single, image_path=image_path, mask_path=mask_path, meta_data=meta_data, save_path=save_path), meta_data.keys())
55
+ print("Finished processing train data.")
56
+
57
+
58
+ def preprocess_train_single(name, image_path, mask_path, meta_data, save_path):
59
+ id = int(name[7:])
60
+ image, _, _, _ = load_image(join(image_path, name + "-image.nii.gz"), return_meta=True, is_seg=False)
61
+ instance_seg_mask, spacing, _, _ = load_image(join(mask_path, name + "-label.nii.gz"), return_meta=True, is_seg=True)
62
+ semantic_seg_mask = np.zeros_like(instance_seg_mask, dtype=int)
63
+ for entry in meta_data[name]:
64
+ semantic_seg_mask[instance_seg_mask == entry["instance"]] = entry["class_label"]
65
+ semantic_seg_mask[semantic_seg_mask == -1] = 5 # Set ignore label to 5
66
+ save_image(join(save_path, "imagesTr/RibFrac_" + str(id).zfill(4) + "_0000.nii.gz"), image, spacing=spacing, is_seg=False)
67
+ save_image(join(save_path, "labelsTr/RibFrac_" + str(id).zfill(4) + ".nii.gz"), semantic_seg_mask, spacing=spacing, is_seg=True)
68
+
69
+
70
+ def preprocess_test(load_test_image_dir, save_path):
71
+ print("Processing test data...")
72
+ filenames = load_filenames(load_test_image_dir)
73
+ for filename in tqdm(filenames):
74
+ id = int(os.path.basename(filename)[8:-13])
75
+ copyfile(filename, join(save_path, "imagesTs/RibFrac_" + str(id).zfill(4) + "_0000.nii.gz"))
76
+ print("Finished processing test data.")
77
+
78
+
79
+ def load_filenames(img_dir, extensions=None):
80
+ _img_dir = fix_path(img_dir)
81
+ img_filenames = []
82
+
83
+ for file in os.listdir(_img_dir):
84
+ if extensions is None or file.endswith(extensions):
85
+ img_filenames.append(_img_dir + file)
86
+ img_filenames = np.asarray(img_filenames)
87
+ img_filenames = natsorted(img_filenames)
88
+
89
+ return img_filenames
90
+
91
+
92
+ def fix_path(path):
93
+ if path[-1] != "/":
94
+ path += "/"
95
+ return path
96
+
97
+
98
+ def load_image(filepath, return_meta=False, is_seg=False):
99
+ image = sitk.ReadImage(filepath)
100
+ image_np = sitk.GetArrayFromImage(image)
101
+
102
+ if is_seg:
103
+ image_np = np.rint(image_np)
104
+ image_np = image_np.astype(np.int8) # In special cases segmentations can contain negative labels, so no np.uint8
105
+
106
+ if not return_meta:
107
+ return image_np
108
+ else:
109
+ spacing = image.GetSpacing()
110
+ keys = image.GetMetaDataKeys()
111
+ header = {key:image.GetMetaData(key) for key in keys}
112
+ affine = None # How do I get the affine transform with SimpleITK? With NiBabel it is just image.affine
113
+ return image_np, spacing, affine, header
114
+
115
+
116
+ def save_image(filename, image, spacing=None, affine=None, header=None, is_seg=False, mp_pool=None, free_mem=False):
117
+ if is_seg:
118
+ image = np.rint(image)
119
+ image = image.astype(np.int8) # In special cases segmentations can contain negative labels, so no np.uint8
120
+
121
+ image = sitk.GetImageFromArray(image)
122
+
123
+ if header is not None:
124
+ [image.SetMetaData(key, header[key]) for key in header.keys()]
125
+
126
+ if spacing is not None:
127
+ image.SetSpacing(spacing)
128
+
129
+ if affine is not None:
130
+ pass # How do I set the affine transform with SimpleITK? With NiBabel it is just nib.Nifti1Image(img, affine=affine, header=header)
131
+
132
+ if mp_pool is None:
133
+ sitk.WriteImage(image, filename)
134
+ if free_mem:
135
+ del image
136
+ gc.collect()
137
+ else:
138
+ mp_pool.apply_async(_save, args=(filename, image, free_mem,))
139
+ if free_mem:
140
+ del image
141
+ gc.collect()
142
+
143
+
144
+ def _save(filename, image, free_mem):
145
+ sitk.WriteImage(image, filename)
146
+ if free_mem:
147
+ del image
148
+ gc.collect()
149
+
150
+
151
+ if __name__ == "__main__":
152
+ # Note: Due to a bug in SimpleITK 2.1.x a version of SimpleITK < 2.1.0 is required for loading images. Further, we can't copy the images and masks, but have to load them and resample both to the same spacing.
153
+ # Conversion instructions:
154
+ # 1. All sets, parts and CSVs need to be downloaded from https://ribfrac.grand-challenge.org/dataset/
155
+ # 2. Unzip ribfrac-train-images-1.zip (will be unzipped as Part1) and ribfrac-train-images-2.zip (will be unzipped as Part2), move content from Part2 to Part1 and rename the folder to imagesTr
156
+ # 3. Unzip ribfrac-train-labels-1.zip (will be unzipped as Part1) and ribfrac-train-labels-2.zip (will be unzipped as Part2), move content from Part2 to Part1 and rename the folder to labelsTr
157
+ # 4. Unzip ribfrac-val-images.zip and add content to imagesTr, repeat with ribfrac-val-labels.zip
158
+ # 5. Unzip ribfrac-test-images.zip and rename it to imagesTs
159
+
160
+ pool = mp.Pool(processes=20)
161
+
162
+ dataset_load_path = "/home/k539i/Documents/network_drives/E132-Projekte/Projects/2021_Gotkowski_RibFrac_RibSeg/original/RibFrac/"
163
+ dataset_save_path = "/home/k539i/Documents/network_drives/E132-Projekte/Projects/2021_Gotkowski_RibFrac_RibSeg/preprocessed/Task154_RibFrac_multi_label/"
164
+ preprocess_dataset(dataset_load_path, dataset_save_path, pool)
165
+
166
+ print("Still saving images in background...")
167
+ pool.close()
168
+ pool.join()
169
+ print("All tasks finished.")
170
+
171
+ labels = {0: "background", 1: "displaced_rib_fracture", 2: "non_displaced_rib_fracture", 3: "buckle_rib_fracture", 4: "segmental_rib_fracture", 5: "unidentified_rib_fracture"}
172
+ generate_dataset_json(join(dataset_save_path, 'dataset.json'), join(dataset_save_path, "imagesTr"), None, ('CT',), labels, "Task154_RibFrac_multi_label")
nnunet/dataset_conversion/Task155_RibFrac_binary.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import SimpleITK as sitk
2
+ from natsort import natsorted
3
+ import numpy as np
4
+ from pathlib import Path
5
+ import pandas as pd
6
+ from collections import defaultdict
7
+ from shutil import copyfile
8
+ import os
9
+ from os.path import join
10
+ from tqdm import tqdm
11
+ import gc
12
+ import multiprocessing as mp
13
+ from nnunet.dataset_conversion.utils import generate_dataset_json
14
+ from functools import partial
15
+
16
+
17
+ def preprocess_dataset(dataset_load_path, dataset_save_path, pool):
18
+ train_image_load_path = join(dataset_load_path, "imagesTr")
19
+ train_mask_load_path = join(dataset_load_path, "labelsTr")
20
+ test_image_load_path = join(dataset_load_path, "imagesTs")
21
+
22
+ ribfrac_train_info_1_path = join(dataset_load_path, "ribfrac-train-info-1.csv")
23
+ ribfrac_train_info_2_path = join(dataset_load_path, "ribfrac-train-info-2.csv")
24
+ ribfrac_val_info_path = join(dataset_load_path, "ribfrac-val-info.csv")
25
+
26
+ train_image_save_path = join(dataset_save_path, "imagesTr")
27
+ train_mask_save_path = join(dataset_save_path, "labelsTr")
28
+ test_image_save_path = join(dataset_save_path, "imagesTs")
29
+ Path(train_image_save_path).mkdir(parents=True, exist_ok=True)
30
+ Path(train_mask_save_path).mkdir(parents=True, exist_ok=True)
31
+ Path(test_image_save_path).mkdir(parents=True, exist_ok=True)
32
+
33
+ meta_data = preprocess_csv(ribfrac_train_info_1_path, ribfrac_train_info_2_path, ribfrac_val_info_path)
34
+ preprocess_train(train_image_load_path, train_mask_load_path, meta_data, dataset_save_path, pool)
35
+ preprocess_test(test_image_load_path, dataset_save_path)
36
+
37
+
38
+ def preprocess_csv(ribfrac_train_info_1_path, ribfrac_train_info_2_path, ribfrac_val_info_path):
39
+ print("Processing csv...")
40
+ meta_data = defaultdict(list)
41
+ for csv_path in [ribfrac_train_info_1_path, ribfrac_train_info_2_path, ribfrac_val_info_path]:
42
+ df = pd.read_csv(csv_path)
43
+ for index, row in df.iterrows():
44
+ name = row["public_id"]
45
+ instance = row["label_id"]
46
+ class_label = row["label_code"]
47
+ meta_data[name].append({"instance": instance, "class_label": class_label})
48
+ print("Finished csv processing.")
49
+ return meta_data
50
+
51
+
52
+ def preprocess_train(image_path, mask_path, meta_data, save_path, pool):
53
+ print("Processing train data...")
54
+ pool.map(partial(preprocess_train_single, image_path=image_path, mask_path=mask_path, meta_data=meta_data, save_path=save_path), meta_data.keys())
55
+ print("Finished processing train data.")
56
+
57
+
58
+ def preprocess_train_single(name, image_path, mask_path, meta_data, save_path):
59
+ id = int(name[7:])
60
+ image, _, _, _ = load_image(join(image_path, name + "-image.nii.gz"), return_meta=True, is_seg=False)
61
+ instance_seg_mask, spacing, _, _ = load_image(join(mask_path, name + "-label.nii.gz"), return_meta=True, is_seg=True)
62
+ semantic_seg_mask = np.zeros_like(instance_seg_mask, dtype=int)
63
+ for entry in meta_data[name]:
64
+ class_label = entry["class_label"]
65
+ if class_label > 0:
66
+ class_label = 1
67
+ semantic_seg_mask[instance_seg_mask == entry["instance"]] = class_label
68
+ save_image(join(save_path, "imagesTr/RibFrac_" + str(id).zfill(4) + "_0000.nii.gz"), image, spacing=spacing, is_seg=False)
69
+ save_image(join(save_path, "labelsTr/RibFrac_" + str(id).zfill(4) + ".nii.gz"), semantic_seg_mask, spacing=spacing, is_seg=True)
70
+
71
+
72
+ def preprocess_test(load_test_image_dir, save_path):
73
+ print("Processing test data...")
74
+ filenames = load_filenames(load_test_image_dir)
75
+ for filename in tqdm(filenames):
76
+ id = int(os.path.basename(filename)[8:-13])
77
+ copyfile(filename, join(save_path, "imagesTs/RibFrac_" + str(id).zfill(4) + "_0000.nii.gz"))
78
+ print("Finished processing test data.")
79
+
80
+
81
+ def load_filenames(img_dir, extensions=None):
82
+ _img_dir = fix_path(img_dir)
83
+ img_filenames = []
84
+
85
+ for file in os.listdir(_img_dir):
86
+ if extensions is None or file.endswith(extensions):
87
+ img_filenames.append(_img_dir + file)
88
+ img_filenames = np.asarray(img_filenames)
89
+ img_filenames = natsorted(img_filenames)
90
+
91
+ return img_filenames
92
+
93
+
94
+ def fix_path(path):
95
+ if path[-1] != "/":
96
+ path += "/"
97
+ return path
98
+
99
+
100
+ def load_image(filepath, return_meta=False, is_seg=False):
101
+ image = sitk.ReadImage(filepath)
102
+ image_np = sitk.GetArrayFromImage(image)
103
+
104
+ if is_seg:
105
+ image_np = np.rint(image_np)
106
+ image_np = image_np.astype(np.int8) # In special cases segmentations can contain negative labels, so no np.uint8
107
+
108
+ if not return_meta:
109
+ return image_np
110
+ else:
111
+ spacing = image.GetSpacing()
112
+ keys = image.GetMetaDataKeys()
113
+ header = {key:image.GetMetaData(key) for key in keys}
114
+ affine = None # How do I get the affine transform with SimpleITK? With NiBabel it is just image.affine
115
+ return image_np, spacing, affine, header
116
+
117
+
118
+ def save_image(filename, image, spacing=None, affine=None, header=None, is_seg=False, mp_pool=None, free_mem=False):
119
+ if is_seg:
120
+ image = np.rint(image)
121
+ image = image.astype(np.int8) # In special cases segmentations can contain negative labels, so no np.uint8
122
+
123
+ image = sitk.GetImageFromArray(image)
124
+
125
+ if header is not None:
126
+ [image.SetMetaData(key, header[key]) for key in header.keys()]
127
+
128
+ if spacing is not None:
129
+ image.SetSpacing(spacing)
130
+
131
+ if affine is not None:
132
+ pass # How do I set the affine transform with SimpleITK? With NiBabel it is just nib.Nifti1Image(img, affine=affine, header=header)
133
+
134
+ if mp_pool is None:
135
+ sitk.WriteImage(image, filename)
136
+ if free_mem:
137
+ del image
138
+ gc.collect()
139
+ else:
140
+ mp_pool.apply_async(_save, args=(filename, image, free_mem,))
141
+ if free_mem:
142
+ del image
143
+ gc.collect()
144
+
145
+
146
+ def _save(filename, image, free_mem):
147
+ sitk.WriteImage(image, filename)
148
+ if free_mem:
149
+ del image
150
+ gc.collect()
151
+
152
+
153
+ if __name__ == "__main__":
154
+ # Note: Due to a bug in SimpleITK 2.1.x a version of SimpleITK < 2.1.0 is required for loading images. Further, we can't copy the images and masks, but have to load them and resample both to the same spacing.
155
+ # Conversion instructions:
156
+ # 1. All sets, parts and CSVs need to be downloaded from https://ribfrac.grand-challenge.org/dataset/
157
+ # 2. Unzip ribfrac-train-images-1.zip (will be unzipped as Part1) and ribfrac-train-images-2.zip (will be unzipped as Part2), move content from Part2 to Part1 and rename the folder to imagesTr
158
+ # 3. Unzip ribfrac-train-labels-1.zip (will be unzipped as Part1) and ribfrac-train-labels-2.zip (will be unzipped as Part2), move content from Part2 to Part1 and rename the folder to labelsTr
159
+ # 4. Unzip ribfrac-val-images.zip and add content to imagesTr, repeat with ribfrac-val-labels.zip
160
+ # 5. Unzip ribfrac-test-images.zip and rename it to imagesTs
161
+
162
+ pool = mp.Pool(processes=20)
163
+
164
+ dataset_load_path = "/home/k539i/Documents/network_drives/E132-Projekte/Projects/2021_Gotkowski_RibFrac_RibSeg/original/RibFrac/"
165
+ dataset_save_path = "/home/k539i/Documents/network_drives/E132-Projekte/Projects/2021_Gotkowski_RibFrac_RibSeg/preprocessed/Task155_RibFrac_binary/"
166
+ preprocess_dataset(dataset_load_path, dataset_save_path, pool)
167
+
168
+ print("Still saving images in background...")
169
+ pool.close()
170
+ pool.join()
171
+ print("All tasks finished.")
172
+
173
+ labels = {0: "background", 1: "fracture"}
174
+ generate_dataset_json(join(dataset_save_path, 'dataset.json'), join(dataset_save_path, "imagesTr"), None, ('CT',), labels, "Task155_RibFrac_binary")
nnunet/dataset_conversion/Task156_RibSeg.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from natsort import natsorted
2
+ import numpy as np
3
+ from pathlib import Path
4
+ import os
5
+ from os.path import join
6
+ from nnunet.dataset_conversion.utils import generate_dataset_json
7
+ import SimpleITK as sitk
8
+ import gc
9
+ import multiprocessing as mp
10
+ from functools import partial
11
+
12
+
13
+ def preprocess_dataset(ribfrac_load_path, ribseg_load_path, dataset_save_path, pool):
14
+ mask_load_path = join(ribseg_load_path, "labelsTr")
15
+
16
+ train_image_save_path = join(dataset_save_path, "imagesTr")
17
+ train_mask_save_path = join(dataset_save_path, "labelsTr")
18
+ test_image_save_path = join(dataset_save_path, "imagesTs")
19
+ test_labels_save_path = join(dataset_save_path, "labelsTs")
20
+ Path(train_image_save_path).mkdir(parents=True, exist_ok=True)
21
+ Path(train_mask_save_path).mkdir(parents=True, exist_ok=True)
22
+ Path(test_image_save_path).mkdir(parents=True, exist_ok=True)
23
+ Path(test_labels_save_path).mkdir(parents=True, exist_ok=True)
24
+
25
+ mask_filenames = load_filenames(mask_load_path)
26
+ pool.map(partial(preprocess_single, image_load_path=ribfrac_load_path), mask_filenames)
27
+
28
+
29
+ def preprocess_single(filename, image_load_path):
30
+ name = os.path.basename(filename)
31
+ if "-cl.nii.gz" in name:
32
+ return
33
+ id = int(name.split("-")[0][7:])
34
+ image_set = "imagesTr"
35
+ mask_set = "labelsTr"
36
+ if id > 500:
37
+ image_set = "imagesTs"
38
+ mask_set = "labelsTs"
39
+ image, _, _, _ = load_image(join(image_load_path, image_set, "RibFrac{}-image.nii.gz".format(id)), return_meta=True, is_seg=False)
40
+ mask, spacing, _, _ = load_image(filename, return_meta=True, is_seg=True)
41
+ save_image(join(dataset_save_path, image_set, "RibSeg_" + str(id).zfill(4) + "_0000.nii.gz"), image, spacing=spacing, is_seg=False)
42
+ save_image(join(dataset_save_path, mask_set, "RibSeg_" + str(id).zfill(4) + ".nii.gz"), mask, spacing=spacing, is_seg=True)
43
+
44
+
45
+ def load_filenames(img_dir, extensions=None):
46
+ _img_dir = fix_path(img_dir)
47
+ img_filenames = []
48
+
49
+ for file in os.listdir(_img_dir):
50
+ if extensions is None or file.endswith(extensions):
51
+ img_filenames.append(_img_dir + file)
52
+ img_filenames = np.asarray(img_filenames)
53
+ img_filenames = natsorted(img_filenames)
54
+
55
+ return img_filenames
56
+
57
+
58
+ def fix_path(path):
59
+ if path[-1] != "/":
60
+ path += "/"
61
+ return path
62
+
63
+
64
+ def load_image(filepath, return_meta=False, is_seg=False):
65
+ image = sitk.ReadImage(filepath)
66
+ image_np = sitk.GetArrayFromImage(image)
67
+
68
+ if is_seg:
69
+ image_np = np.rint(image_np)
70
+ image_np = image_np.astype(np.int8) # In special cases segmentations can contain negative labels, so no np.uint8
71
+
72
+ if not return_meta:
73
+ return image_np
74
+ else:
75
+ spacing = image.GetSpacing()
76
+ keys = image.GetMetaDataKeys()
77
+ header = {key:image.GetMetaData(key) for key in keys}
78
+ affine = None # How do I get the affine transform with SimpleITK? With NiBabel it is just image.affine
79
+ return image_np, spacing, affine, header
80
+
81
+
82
+ def save_image(filename, image, spacing=None, affine=None, header=None, is_seg=False, mp_pool=None, free_mem=False):
83
+ if is_seg:
84
+ image = np.rint(image)
85
+ image = image.astype(np.int8) # In special cases segmentations can contain negative labels, so no np.uint8
86
+
87
+ image = sitk.GetImageFromArray(image)
88
+
89
+ if header is not None:
90
+ [image.SetMetaData(key, header[key]) for key in header.keys()]
91
+
92
+ if spacing is not None:
93
+ image.SetSpacing(spacing)
94
+
95
+ if affine is not None:
96
+ pass # How do I set the affine transform with SimpleITK? With NiBabel it is just nib.Nifti1Image(img, affine=affine, header=header)
97
+
98
+ if mp_pool is None:
99
+ sitk.WriteImage(image, filename)
100
+ if free_mem:
101
+ del image
102
+ gc.collect()
103
+ else:
104
+ mp_pool.apply_async(_save, args=(filename, image, free_mem,))
105
+ if free_mem:
106
+ del image
107
+ gc.collect()
108
+
109
+
110
+ def _save(filename, image, free_mem):
111
+ sitk.WriteImage(image, filename)
112
+ if free_mem:
113
+ del image
114
+ gc.collect()
115
+
116
+
117
+ if __name__ == "__main__":
118
+ # Note: Due to a bug in SimpleITK 2.1.x a version of SimpleITK < 2.1.0 is required for loading images. Further, we can't copy the images and masks, but have to load them and resample both to the same spacing.
119
+ # Conversion instructions:
120
+ # 1. All images from both training and validation set of the RibFrac dataset need to be downloaded from https://ribfrac.grand-challenge.org/dataset/ into a new folder named RibFrac
121
+ # 2. The RibSeg masks need to be downloaded from https://zenodo.org/record/5336592 into a new folder named RibSeg
122
+ # 3. Follow unpacking instruction for the RibFrac dataset as in Task154_RibFrac
123
+ # 4. Unzip RibSeg_490_nii.zip from the RibSeg dataset and rename the folder labelsTr
124
+
125
+ ribfrac_load_path = "/home/k539i/Documents/datasets/original/RibFrac/"
126
+ ribseg_load_path = "/home/k539i/Documents/datasets/original/RibSeg/"
127
+ dataset_save_path = "/home/k539i/Documents/datasets/preprocessed/Task156_RibSeg/"
128
+
129
+ max_imagesTr_id = 500
130
+
131
+ pool = mp.Pool(processes=20)
132
+
133
+ preprocess_dataset(ribfrac_load_path, ribseg_load_path, dataset_save_path, pool)
134
+
135
+ print("Still saving images in background...")
136
+ pool.close()
137
+ pool.join()
138
+ print("All tasks finished.")
139
+
140
+ generate_dataset_json(join(dataset_save_path, 'dataset.json'), join(dataset_save_path, "imagesTr"), None, ('CT',), {0: 'bg', 1: 'rib'}, "Task156_RibSeg")
nnunet/dataset_conversion/Task159_MyoPS2020.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import SimpleITK
2
+ import numpy as np
3
+ from batchgenerators.utilities.file_and_folder_operations import *
4
+ import shutil
5
+
6
+ import SimpleITK as sitk
7
+ from nnunet.paths import nnUNet_raw_data
8
+ from nnunet.dataset_conversion.utils import generate_dataset_json
9
+ from nnunet.utilities.sitk_stuff import copy_geometry
10
+
11
+
12
+ def convert_labels_to_nnunet(source_nifti: str, target_nifti: str):
13
+ img = sitk.ReadImage(source_nifti)
14
+ img_npy = sitk.GetArrayFromImage(img)
15
+ nnunet_seg = np.zeros(img_npy.shape, dtype=np.uint8)
16
+ # why are they not using normal labels and instead use random numbers???
17
+ nnunet_seg[img_npy == 500] = 1 # left ventricular (LV) blood pool (500)
18
+ nnunet_seg[img_npy == 600] = 2 # right ventricular blood pool (600)
19
+ nnunet_seg[img_npy == 200] = 3 # LV normal myocardium (200)
20
+ nnunet_seg[img_npy == 1220] = 4 # LV myocardial edema (1220)
21
+ nnunet_seg[img_npy == 2221] = 5 # LV myocardial scars (2221)
22
+ nnunet_seg_itk = sitk.GetImageFromArray(nnunet_seg)
23
+ nnunet_seg_itk = copy_geometry(nnunet_seg_itk, img)
24
+ sitk.WriteImage(nnunet_seg_itk, target_nifti)
25
+
26
+
27
+ def convert_labels_back_to_myops(source_nifti: str, target_nifti: str):
28
+ nnunet_itk = sitk.ReadImage(source_nifti)
29
+ nnunet_npy = sitk.GetArrayFromImage(nnunet_itk)
30
+ myops_seg = np.zeros(nnunet_npy.shape, dtype=np.uint8)
31
+ # why are they not using normal labels and instead use random numbers???
32
+ myops_seg[nnunet_npy == 1] = 500 # left ventricular (LV) blood pool (500)
33
+ myops_seg[nnunet_npy == 2] = 600 # right ventricular blood pool (600)
34
+ myops_seg[nnunet_npy == 3] = 200 # LV normal myocardium (200)
35
+ myops_seg[nnunet_npy == 4] = 1220 # LV myocardial edema (1220)
36
+ myops_seg[nnunet_npy == 5] = 2221 # LV myocardial scars (2221)
37
+ myops_seg_itk = sitk.GetImageFromArray(myops_seg)
38
+ myops_seg_itk = copy_geometry(myops_seg_itk, nnunet_itk)
39
+ sitk.WriteImage(myops_seg_itk, target_nifti)
40
+
41
+
42
+ if __name__ == '__main__':
43
+ # this is where we extracted all the archives. This folder must have the subfolders test20, train25,
44
+ # train25_myops_gd. We do not use test_data_gd because the test GT is encoded and cannot be used as it is
45
+ base = '/home/fabian/Downloads/MyoPS 2020 Dataset'
46
+
47
+ # Arbitrary task id. This is just to ensure each dataset ha a unique number. Set this to whatever ([0-999]) you
48
+ # want
49
+ task_id = 159
50
+ task_name = "MyoPS2020"
51
+
52
+ foldername = "Task%03.0d_%s" % (task_id, task_name)
53
+
54
+ # setting up nnU-Net folders
55
+ out_base = join(nnUNet_raw_data, foldername)
56
+ imagestr = join(out_base, "imagesTr")
57
+ imagests = join(out_base, "imagesTs")
58
+ labelstr = join(out_base, "labelsTr")
59
+ maybe_mkdir_p(imagestr)
60
+ maybe_mkdir_p(imagests)
61
+ maybe_mkdir_p(labelstr)
62
+
63
+ imagestr_source = join(base, 'train25')
64
+ imagests_source = join(base, 'test20')
65
+ labelstr_source = join(base, 'train25_myops_gd')
66
+
67
+ # convert training set
68
+ nii_files = nifti_files(imagestr_source, join=False)
69
+ # remove their modality identifier. Conveniently it's always 2 characters. np.unique to get the identifiers
70
+ identifiers = np.unique([i[:-len('_C0.nii.gz')] for i in nii_files])
71
+ for i in identifiers:
72
+ shutil.copy(join(imagestr_source, i + "_C0.nii.gz"), join(imagestr, i + '_0000.nii.gz'))
73
+ shutil.copy(join(imagestr_source, i + "_DE.nii.gz"), join(imagestr, i + '_0001.nii.gz'))
74
+ shutil.copy(join(imagestr_source, i + "_T2.nii.gz"), join(imagestr, i + '_0002.nii.gz'))
75
+ convert_labels_to_nnunet(join(labelstr_source, i + '_gd.nii.gz'), join(labelstr, i + '.nii.gz'))
76
+
77
+ # test set
78
+ nii_files = nifti_files(imagests_source, join=False)
79
+ # remove their modality identifier. Conveniently it's always 2 characters. np.unique to get the identifiers
80
+ identifiers = np.unique([i[:-len('_C0.nii.gz')] for i in nii_files])
81
+ for i in identifiers:
82
+ shutil.copy(join(imagests_source, i + "_C0.nii.gz"), join(imagests, i + '_0000.nii.gz'))
83
+ shutil.copy(join(imagests_source, i + "_DE.nii.gz"), join(imagests, i + '_0001.nii.gz'))
84
+ shutil.copy(join(imagests_source, i + "_T2.nii.gz"), join(imagests, i + '_0002.nii.gz'))
85
+
86
+ generate_dataset_json(join(out_base, 'dataset.json'),
87
+ imagestr,
88
+ None,
89
+ ('C0', 'DE', 'T2'),
90
+ {
91
+ 0: 'background',
92
+ 1: "left ventricular (LV) blood pool",
93
+ 2: "right ventricular blood pool",
94
+ 3: "LV normal myocardium",
95
+ 4: "LV myocardial edema",
96
+ 5: "LV myocardial scars",
97
+ },
98
+ task_name,
99
+ license='see http://www.sdspeople.fudan.edu.cn/zhuangxiahai/0/myops20/index.html',
100
+ dataset_description='see http://www.sdspeople.fudan.edu.cn/zhuangxiahai/0/myops20/index.html',
101
+ dataset_reference='http://www.sdspeople.fudan.edu.cn/zhuangxiahai/0/myops20/index.html',
102
+ dataset_release='0')
103
+
104
+ # REMEMBER THAT TEST SET INFERENCE WILL REQUIRE YOU CONVERT THE LABELS BACK TO THEIR CONVENTION
105
+ # use convert_labels_back_to_myops for that!
106
+ # man I am such a nice person. Love you guys.
nnunet/dataset_conversion/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from __future__ import absolute_import
2
+
3
+ from . import *
nnunet/dataset_conversion/utils.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ from typing import Tuple
18
+ import numpy as np
19
+ from batchgenerators.utilities.file_and_folder_operations import *
20
+
21
+
22
+ def get_identifiers_from_splitted_files(folder: str):
23
+ uniques = np.unique([i[:-12] for i in subfiles(folder, suffix='.nii.gz', join=False)])
24
+ return uniques
25
+
26
+
27
+ def generate_dataset_json(output_file: str, imagesTr_dir: str, imagesTs_dir: str, modalities: Tuple,
28
+ labels: dict, dataset_name: str, sort_keys=True, license: str = "hands off!", dataset_description: str = "",
29
+ dataset_reference="", dataset_release='0.0'):
30
+ """
31
+ :param output_file: This needs to be the full path to the dataset.json you intend to write, so
32
+ output_file='DATASET_PATH/dataset.json' where the folder DATASET_PATH points to is the one with the
33
+ imagesTr and labelsTr subfolders
34
+ :param imagesTr_dir: path to the imagesTr folder of that dataset
35
+ :param imagesTs_dir: path to the imagesTs folder of that dataset. Can be None
36
+ :param modalities: tuple of strings with modality names. must be in the same order as the images (first entry
37
+ corresponds to _0000.nii.gz, etc). Example: ('T1', 'T2', 'FLAIR').
38
+ :param labels: dict with int->str (key->value) mapping the label IDs to label names. Note that 0 is always
39
+ supposed to be background! Example: {0: 'background', 1: 'edema', 2: 'enhancing tumor'}
40
+ :param dataset_name: The name of the dataset. Can be anything you want
41
+ :param sort_keys: In order to sort or not, the keys in dataset.json
42
+ :param license:
43
+ :param dataset_description:
44
+ :param dataset_reference: website of the dataset, if available
45
+ :param dataset_release:
46
+ :return:
47
+ """
48
+ train_identifiers = get_identifiers_from_splitted_files(imagesTr_dir)
49
+
50
+ if imagesTs_dir is not None:
51
+ test_identifiers = get_identifiers_from_splitted_files(imagesTs_dir)
52
+ else:
53
+ test_identifiers = []
54
+
55
+ json_dict = {}
56
+ json_dict['name'] = dataset_name
57
+ json_dict['description'] = dataset_description
58
+ json_dict['tensorImageSize'] = "4D"
59
+ json_dict['reference'] = dataset_reference
60
+ json_dict['licence'] = license
61
+ json_dict['release'] = dataset_release
62
+ json_dict['modality'] = {str(i): modalities[i] for i in range(len(modalities))}
63
+ json_dict['labels'] = {str(i): labels[i] for i in labels.keys()}
64
+
65
+ json_dict['numTraining'] = len(train_identifiers)
66
+ json_dict['numTest'] = len(test_identifiers)
67
+ json_dict['training'] = [
68
+ {'image': "./imagesTr/%s.nii.gz" % i, "label": "./labelsTr/%s.nii.gz" % i} for i
69
+ in
70
+ train_identifiers]
71
+ json_dict['test'] = ["./imagesTs/%s.nii.gz" % i for i in test_identifiers]
72
+
73
+ if not output_file.endswith("dataset.json"):
74
+ print("WARNING: output file name is not dataset.json! This may be intentional or not. You decide. "
75
+ "Proceeding anyways...")
76
+ save_json(json_dict, os.path.join(output_file), sort_keys=sort_keys)
nnunet/evaluation/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from __future__ import absolute_import
2
+ from . import *
nnunet/evaluation/add_dummy_task_with_mean_over_all_tasks.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ import numpy as np
17
+ from batchgenerators.utilities.file_and_folder_operations import subfiles
18
+ import os
19
+ from collections import OrderedDict
20
+
21
+ folder = "/home/fabian/drives/E132-Projekte/Projects/2018_MedicalDecathlon/Leaderboard"
22
+ task_descriptors = ['2D final 2',
23
+ '2D final, less pool, dc and topK, fold0',
24
+ '2D final pseudo3d 7, fold0',
25
+ '2D final, less pool, dc and ce, fold0',
26
+ '3D stage0 final 2, fold0',
27
+ '3D fullres final 2, fold0']
28
+ task_ids_with_no_stage0 = ["Task001_BrainTumour", "Task004_Hippocampus", "Task005_Prostate"]
29
+
30
+ mean_scores = OrderedDict()
31
+ for t in task_descriptors:
32
+ mean_scores[t] = OrderedDict()
33
+
34
+ json_files = subfiles(folder, True, None, ".json", True)
35
+ json_files = [i for i in json_files if not i.split("/")[-1].startswith(".")] # stupid mac
36
+ for j in json_files:
37
+ with open(j, 'r') as f:
38
+ res = json.load(f)
39
+ task = res['task']
40
+ if task != "Task999_ALL":
41
+ name = res['name']
42
+ if name in task_descriptors:
43
+ if task not in list(mean_scores[name].keys()):
44
+ mean_scores[name][task] = res['results']['mean']['mean']
45
+ else:
46
+ raise RuntimeError("duplicate task %s for description %s" % (task, name))
47
+
48
+ for t in task_ids_with_no_stage0:
49
+ mean_scores["3D stage0 final 2, fold0"][t] = mean_scores["3D fullres final 2, fold0"][t]
50
+
51
+ a = set()
52
+ for i in mean_scores.keys():
53
+ a = a.union(list(mean_scores[i].keys()))
54
+
55
+ for i in mean_scores.keys():
56
+ try:
57
+ for t in list(a):
58
+ assert t in mean_scores[i].keys(), "did not find task %s for experiment %s" % (t, i)
59
+ new_res = OrderedDict()
60
+ new_res['name'] = i
61
+ new_res['author'] = "Fabian"
62
+ new_res['task'] = "Task999_ALL"
63
+ new_res['results'] = OrderedDict()
64
+ new_res['results']['mean'] = OrderedDict()
65
+ new_res['results']['mean']['mean'] = OrderedDict()
66
+ tasks = list(mean_scores[i].keys())
67
+ metrics = mean_scores[i][tasks[0]].keys()
68
+ for m in metrics:
69
+ foreground_values = [mean_scores[i][n][m] for n in tasks]
70
+ new_res['results']['mean']["mean"][m] = np.nanmean(foreground_values)
71
+ output_fname = i.replace(" ", "_") + "_globalMean.json"
72
+ with open(os.path.join(folder, output_fname), 'w') as f:
73
+ json.dump(new_res, f)
74
+ except AssertionError:
75
+ print("could not process experiment %s" % i)
76
+ print("did not find task %s for experiment %s" % (t, i))
77
+
nnunet/evaluation/add_mean_dice_to_json.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ import numpy as np
17
+ from batchgenerators.utilities.file_and_folder_operations import subfiles
18
+ from collections import OrderedDict
19
+
20
+
21
+ def foreground_mean(filename):
22
+ with open(filename, 'r') as f:
23
+ res = json.load(f)
24
+ class_ids = np.array([int(i) for i in res['results']['mean'].keys() if (i != 'mean')])
25
+ class_ids = class_ids[class_ids != 0]
26
+ class_ids = class_ids[class_ids != -1]
27
+ class_ids = class_ids[class_ids != 99]
28
+
29
+ tmp = res['results']['mean'].get('99')
30
+ if tmp is not None:
31
+ _ = res['results']['mean'].pop('99')
32
+
33
+ metrics = res['results']['mean']['1'].keys()
34
+ res['results']['mean']["mean"] = OrderedDict()
35
+ for m in metrics:
36
+ foreground_values = [res['results']['mean'][str(i)][m] for i in class_ids]
37
+ res['results']['mean']["mean"][m] = np.nanmean(foreground_values)
38
+ with open(filename, 'w') as f:
39
+ json.dump(res, f, indent=4, sort_keys=True)
40
+
41
+
42
+ def run_in_folder(folder):
43
+ json_files = subfiles(folder, True, None, ".json", True)
44
+ json_files = [i for i in json_files if not i.split("/")[-1].startswith(".") and not i.endswith("_globalMean.json")] # stupid mac
45
+ for j in json_files:
46
+ foreground_mean(j)
47
+
48
+
49
+ if __name__ == "__main__":
50
+ folder = "/media/fabian/Results/nnUNetOutput_final/summary_jsons"
51
+ run_in_folder(folder)
nnunet/evaluation/collect_results_files.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import shutil
17
+ from batchgenerators.utilities.file_and_folder_operations import subdirs, subfiles
18
+
19
+
20
+ def crawl_and_copy(current_folder, out_folder, prefix="fabian_", suffix="ummary.json"):
21
+ """
22
+ This script will run recursively through all subfolders of current_folder and copy all files that end with
23
+ suffix with some automatically generated prefix into out_folder
24
+ :param current_folder:
25
+ :param out_folder:
26
+ :param prefix:
27
+ :return:
28
+ """
29
+ s = subdirs(current_folder, join=False)
30
+ f = subfiles(current_folder, join=False)
31
+ f = [i for i in f if i.endswith(suffix)]
32
+ if current_folder.find("fold0") != -1:
33
+ for fl in f:
34
+ shutil.copy(os.path.join(current_folder, fl), os.path.join(out_folder, prefix+fl))
35
+ for su in s:
36
+ if prefix == "":
37
+ add = su
38
+ else:
39
+ add = "__" + su
40
+ crawl_and_copy(os.path.join(current_folder, su), out_folder, prefix=prefix+add)
41
+
42
+
43
+ if __name__ == "__main__":
44
+ from nnunet.paths import network_training_output_dir
45
+ output_folder = "/home/fabian/PhD/results/nnUNetV2/leaderboard"
46
+ crawl_and_copy(network_training_output_dir, output_folder)
47
+ from nnunet.evaluation.add_mean_dice_to_json import run_in_folder
48
+ run_in_folder(output_folder)
nnunet/evaluation/evaluator.py ADDED
@@ -0,0 +1,483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import collections
17
+ import inspect
18
+ import json
19
+ import hashlib
20
+ from datetime import datetime
21
+ from multiprocessing.pool import Pool
22
+ import numpy as np
23
+ import pandas as pd
24
+ import SimpleITK as sitk
25
+ from nnunet.evaluation.metrics import ConfusionMatrix, ALL_METRICS
26
+ from batchgenerators.utilities.file_and_folder_operations import save_json, subfiles, join
27
+ from collections import OrderedDict
28
+
29
+
30
+ class Evaluator:
31
+ """Object that holds test and reference segmentations with label information
32
+ and computes a number of metrics on the two. 'labels' must either be an
33
+ iterable of numeric values (or tuples thereof) or a dictionary with string
34
+ names and numeric values.
35
+ """
36
+
37
+ default_metrics = [
38
+ "False Positive Rate",
39
+ "Dice",
40
+ "Jaccard",
41
+ "Precision",
42
+ "Recall",
43
+ "Accuracy",
44
+ "False Omission Rate",
45
+ "Negative Predictive Value",
46
+ "False Negative Rate",
47
+ "True Negative Rate",
48
+ "False Discovery Rate",
49
+ "Total Positives Test",
50
+ "Total Positives Reference"
51
+ ]
52
+
53
+ default_advanced_metrics = [
54
+ #"Hausdorff Distance",
55
+ "Hausdorff Distance 95",
56
+ #"Avg. Surface Distance",
57
+ #"Avg. Symmetric Surface Distance"
58
+ ]
59
+
60
+ def __init__(self,
61
+ test=None,
62
+ reference=None,
63
+ labels=None,
64
+ metrics=None,
65
+ advanced_metrics=None,
66
+ nan_for_nonexisting=True):
67
+
68
+ self.test = None
69
+ self.reference = None
70
+ self.confusion_matrix = ConfusionMatrix()
71
+ self.labels = None
72
+ self.nan_for_nonexisting = nan_for_nonexisting
73
+ self.result = None
74
+
75
+ self.metrics = []
76
+ if metrics is None:
77
+ for m in self.default_metrics:
78
+ self.metrics.append(m)
79
+ else:
80
+ for m in metrics:
81
+ self.metrics.append(m)
82
+
83
+ self.advanced_metrics = []
84
+ if advanced_metrics is None:
85
+ for m in self.default_advanced_metrics:
86
+ self.advanced_metrics.append(m)
87
+ else:
88
+ for m in advanced_metrics:
89
+ self.advanced_metrics.append(m)
90
+
91
+ self.set_reference(reference)
92
+ self.set_test(test)
93
+ if labels is not None:
94
+ self.set_labels(labels)
95
+ else:
96
+ if test is not None and reference is not None:
97
+ self.construct_labels()
98
+
99
+ def set_test(self, test):
100
+ """Set the test segmentation."""
101
+
102
+ self.test = test
103
+
104
+ def set_reference(self, reference):
105
+ """Set the reference segmentation."""
106
+
107
+ self.reference = reference
108
+
109
+ def set_labels(self, labels):
110
+ """Set the labels.
111
+ :param labels= may be a dictionary (int->str), a set (of ints), a tuple (of ints) or a list (of ints). Labels
112
+ will only have names if you pass a dictionary"""
113
+
114
+ if isinstance(labels, dict):
115
+ self.labels = collections.OrderedDict(labels)
116
+ elif isinstance(labels, set):
117
+ self.labels = list(labels)
118
+ elif isinstance(labels, np.ndarray):
119
+ self.labels = [i for i in labels]
120
+ elif isinstance(labels, (list, tuple)):
121
+ self.labels = labels
122
+ else:
123
+ raise TypeError("Can only handle dict, list, tuple, set & numpy array, but input is of type {}".format(type(labels)))
124
+
125
+ def construct_labels(self):
126
+ """Construct label set from unique entries in segmentations."""
127
+
128
+ if self.test is None and self.reference is None:
129
+ raise ValueError("No test or reference segmentations.")
130
+ elif self.test is None:
131
+ labels = np.unique(self.reference)
132
+ else:
133
+ labels = np.union1d(np.unique(self.test),
134
+ np.unique(self.reference))
135
+ self.labels = list(map(lambda x: int(x), labels))
136
+
137
+ def set_metrics(self, metrics):
138
+ """Set evaluation metrics"""
139
+
140
+ if isinstance(metrics, set):
141
+ self.metrics = list(metrics)
142
+ elif isinstance(metrics, (list, tuple, np.ndarray)):
143
+ self.metrics = metrics
144
+ else:
145
+ raise TypeError("Can only handle list, tuple, set & numpy array, but input is of type {}".format(type(metrics)))
146
+
147
+ def add_metric(self, metric):
148
+
149
+ if metric not in self.metrics:
150
+ self.metrics.append(metric)
151
+
152
+ def evaluate(self, test=None, reference=None, advanced=False, **metric_kwargs):
153
+ """Compute metrics for segmentations."""
154
+ if test is not None:
155
+ self.set_test(test)
156
+
157
+ if reference is not None:
158
+ self.set_reference(reference)
159
+
160
+ if self.test is None or self.reference is None:
161
+ raise ValueError("Need both test and reference segmentations.")
162
+
163
+ if self.labels is None:
164
+ self.construct_labels()
165
+
166
+ self.metrics.sort()
167
+
168
+ # get functions for evaluation
169
+ # somewhat convoluted, but allows users to define additonal metrics
170
+ # on the fly, e.g. inside an IPython console
171
+ _funcs = {m: ALL_METRICS[m] for m in self.metrics + self.advanced_metrics}
172
+ frames = inspect.getouterframes(inspect.currentframe())
173
+ for metric in self.metrics:
174
+ for f in frames:
175
+ if metric in f[0].f_locals:
176
+ _funcs[metric] = f[0].f_locals[metric]
177
+ break
178
+ else:
179
+ if metric in _funcs:
180
+ continue
181
+ else:
182
+ raise NotImplementedError(
183
+ "Metric {} not implemented.".format(metric))
184
+
185
+ # get results
186
+ self.result = OrderedDict()
187
+
188
+ eval_metrics = self.metrics
189
+ if advanced:
190
+ eval_metrics += self.advanced_metrics
191
+
192
+ if isinstance(self.labels, dict):
193
+
194
+ for label, name in self.labels.items():
195
+ k = str(name)
196
+ self.result[k] = OrderedDict()
197
+ if not hasattr(label, "__iter__"):
198
+ self.confusion_matrix.set_test(self.test == label)
199
+ self.confusion_matrix.set_reference(self.reference == label)
200
+ else:
201
+ current_test = 0
202
+ current_reference = 0
203
+ for l in label:
204
+ current_test += (self.test == l)
205
+ current_reference += (self.reference == l)
206
+ self.confusion_matrix.set_test(current_test)
207
+ self.confusion_matrix.set_reference(current_reference)
208
+ for metric in eval_metrics:
209
+ self.result[k][metric] = _funcs[metric](confusion_matrix=self.confusion_matrix,
210
+ nan_for_nonexisting=self.nan_for_nonexisting,
211
+ **metric_kwargs)
212
+
213
+ else:
214
+
215
+ for i, l in enumerate(self.labels):
216
+ k = str(l)
217
+ self.result[k] = OrderedDict()
218
+ self.confusion_matrix.set_test(self.test == l)
219
+ self.confusion_matrix.set_reference(self.reference == l)
220
+ for metric in eval_metrics:
221
+ self.result[k][metric] = _funcs[metric](confusion_matrix=self.confusion_matrix,
222
+ nan_for_nonexisting=self.nan_for_nonexisting,
223
+ **metric_kwargs)
224
+
225
+ return self.result
226
+
227
+ def to_dict(self):
228
+
229
+ if self.result is None:
230
+ self.evaluate()
231
+ return self.result
232
+
233
+ def to_array(self):
234
+ """Return result as numpy array (labels x metrics)."""
235
+
236
+ if self.result is None:
237
+ self.evaluate
238
+
239
+ result_metrics = sorted(self.result[list(self.result.keys())[0]].keys())
240
+
241
+ a = np.zeros((len(self.labels), len(result_metrics)), dtype=np.float32)
242
+
243
+ if isinstance(self.labels, dict):
244
+ for i, label in enumerate(self.labels.keys()):
245
+ for j, metric in enumerate(result_metrics):
246
+ a[i][j] = self.result[self.labels[label]][metric]
247
+ else:
248
+ for i, label in enumerate(self.labels):
249
+ for j, metric in enumerate(result_metrics):
250
+ a[i][j] = self.result[label][metric]
251
+
252
+ return a
253
+
254
+ def to_pandas(self):
255
+ """Return result as pandas DataFrame."""
256
+
257
+ a = self.to_array()
258
+
259
+ if isinstance(self.labels, dict):
260
+ labels = list(self.labels.values())
261
+ else:
262
+ labels = self.labels
263
+
264
+ result_metrics = sorted(self.result[list(self.result.keys())[0]].keys())
265
+
266
+ return pd.DataFrame(a, index=labels, columns=result_metrics)
267
+
268
+
269
+ class NiftiEvaluator(Evaluator):
270
+
271
+ def __init__(self, *args, **kwargs):
272
+
273
+ self.test_nifti = None
274
+ self.reference_nifti = None
275
+ super(NiftiEvaluator, self).__init__(*args, **kwargs)
276
+
277
+ def set_test(self, test):
278
+ """Set the test segmentation."""
279
+
280
+ if test is not None:
281
+ self.test_nifti = sitk.ReadImage(test)
282
+ super(NiftiEvaluator, self).set_test(sitk.GetArrayFromImage(self.test_nifti))
283
+ else:
284
+ self.test_nifti = None
285
+ super(NiftiEvaluator, self).set_test(test)
286
+
287
+ def set_reference(self, reference):
288
+ """Set the reference segmentation."""
289
+
290
+ if reference is not None:
291
+ self.reference_nifti = sitk.ReadImage(reference)
292
+ super(NiftiEvaluator, self).set_reference(sitk.GetArrayFromImage(self.reference_nifti))
293
+ else:
294
+ self.reference_nifti = None
295
+ super(NiftiEvaluator, self).set_reference(reference)
296
+
297
+ def evaluate(self, test=None, reference=None, voxel_spacing=None, **metric_kwargs):
298
+
299
+ if voxel_spacing is None:
300
+ voxel_spacing = np.array(self.test_nifti.GetSpacing())[::-1]
301
+ metric_kwargs["voxel_spacing"] = voxel_spacing
302
+
303
+ return super(NiftiEvaluator, self).evaluate(test, reference, **metric_kwargs)
304
+
305
+
306
+ def run_evaluation(args):
307
+ test, ref, evaluator, metric_kwargs = args
308
+ # evaluate
309
+ evaluator.set_test(test)
310
+ evaluator.set_reference(ref)
311
+ if evaluator.labels is None:
312
+ evaluator.construct_labels()
313
+ current_scores = evaluator.evaluate(**metric_kwargs)
314
+ if type(test) == str:
315
+ current_scores["test"] = test
316
+ if type(ref) == str:
317
+ current_scores["reference"] = ref
318
+ return current_scores
319
+
320
+
321
+ def aggregate_scores(test_ref_pairs,
322
+ evaluator=NiftiEvaluator,
323
+ labels=None,
324
+ nanmean=True,
325
+ json_output_file=None,
326
+ json_name="",
327
+ json_description="",
328
+ json_author="Fabian",
329
+ json_task="",
330
+ num_threads=2,
331
+ **metric_kwargs):
332
+ """
333
+ test = predicted image
334
+ :param test_ref_pairs:
335
+ :param evaluator:
336
+ :param labels: must be a dict of int-> str or a list of int
337
+ :param nanmean:
338
+ :param json_output_file:
339
+ :param json_name:
340
+ :param json_description:
341
+ :param json_author:
342
+ :param json_task:
343
+ :param metric_kwargs:
344
+ :return:
345
+ """
346
+
347
+ if type(evaluator) == type:
348
+ evaluator = evaluator()
349
+
350
+ if labels is not None:
351
+ evaluator.set_labels(labels)
352
+
353
+ all_scores = OrderedDict()
354
+ all_scores["all"] = []
355
+ all_scores["mean"] = OrderedDict()
356
+
357
+ test = [i[0] for i in test_ref_pairs]
358
+ ref = [i[1] for i in test_ref_pairs]
359
+ p = Pool(num_threads)
360
+ all_res = p.map(run_evaluation, zip(test, ref, [evaluator]*len(ref), [metric_kwargs]*len(ref)))
361
+ p.close()
362
+ p.join()
363
+
364
+ for i in range(len(all_res)):
365
+ all_scores["all"].append(all_res[i])
366
+
367
+ # append score list for mean
368
+ for label, score_dict in all_res[i].items():
369
+ if label in ("test", "reference"):
370
+ continue
371
+ if label not in all_scores["mean"]:
372
+ all_scores["mean"][label] = OrderedDict()
373
+ for score, value in score_dict.items():
374
+ if score not in all_scores["mean"][label]:
375
+ all_scores["mean"][label][score] = []
376
+ all_scores["mean"][label][score].append(value)
377
+
378
+ for label in all_scores["mean"]:
379
+ for score in all_scores["mean"][label]:
380
+ if nanmean:
381
+ all_scores["mean"][label][score] = float(np.nanmean(all_scores["mean"][label][score]))
382
+ else:
383
+ all_scores["mean"][label][score] = float(np.mean(all_scores["mean"][label][score]))
384
+
385
+ # save to file if desired
386
+ # we create a hopefully unique id by hashing the entire output dictionary
387
+ if json_output_file is not None:
388
+ json_dict = OrderedDict()
389
+ json_dict["name"] = json_name
390
+ json_dict["description"] = json_description
391
+ timestamp = datetime.today()
392
+ json_dict["timestamp"] = str(timestamp)
393
+ json_dict["task"] = json_task
394
+ json_dict["author"] = json_author
395
+ json_dict["results"] = all_scores
396
+ json_dict["id"] = hashlib.md5(json.dumps(json_dict).encode("utf-8")).hexdigest()[:12]
397
+ save_json(json_dict, json_output_file)
398
+
399
+
400
+ return all_scores
401
+
402
+
403
+ def aggregate_scores_for_experiment(score_file,
404
+ labels=None,
405
+ metrics=Evaluator.default_metrics,
406
+ nanmean=True,
407
+ json_output_file=None,
408
+ json_name="",
409
+ json_description="",
410
+ json_author="Fabian",
411
+ json_task=""):
412
+
413
+ scores = np.load(score_file)
414
+ scores_mean = scores.mean(0)
415
+ if labels is None:
416
+ labels = list(map(str, range(scores.shape[1])))
417
+
418
+ results = []
419
+ results_mean = OrderedDict()
420
+ for i in range(scores.shape[0]):
421
+ results.append(OrderedDict())
422
+ for l, label in enumerate(labels):
423
+ results[-1][label] = OrderedDict()
424
+ results_mean[label] = OrderedDict()
425
+ for m, metric in enumerate(metrics):
426
+ results[-1][label][metric] = float(scores[i][l][m])
427
+ results_mean[label][metric] = float(scores_mean[l][m])
428
+
429
+ json_dict = OrderedDict()
430
+ json_dict["name"] = json_name
431
+ json_dict["description"] = json_description
432
+ timestamp = datetime.today()
433
+ json_dict["timestamp"] = str(timestamp)
434
+ json_dict["task"] = json_task
435
+ json_dict["author"] = json_author
436
+ json_dict["results"] = {"all": results, "mean": results_mean}
437
+ json_dict["id"] = hashlib.md5(json.dumps(json_dict).encode("utf-8")).hexdigest()[:12]
438
+ if json_output_file is not None:
439
+ json_output_file = open(json_output_file, "w")
440
+ json.dump(json_dict, json_output_file, indent=4, separators=(",", ": "))
441
+ json_output_file.close()
442
+
443
+ return json_dict
444
+
445
+
446
+ def evaluate_folder(folder_with_gts: str, folder_with_predictions: str, labels: tuple, **metric_kwargs):
447
+ """
448
+ writes a summary.json to folder_with_predictions
449
+ :param folder_with_gts: folder where the ground truth segmentations are saved. Must be nifti files.
450
+ :param folder_with_predictions: folder where the predicted segmentations are saved. Must be nifti files.
451
+ :param labels: tuple of int with the labels in the dataset. For example (0, 1, 2, 3) for Task001_BrainTumour.
452
+ :return:
453
+ """
454
+ files_gt = subfiles(folder_with_gts, suffix=".nii.gz", join=False)
455
+ files_pred = subfiles(folder_with_predictions, suffix=".nii.gz", join=False)
456
+ assert all([i in files_pred for i in files_gt]), "files missing in folder_with_predictions"
457
+ assert all([i in files_gt for i in files_pred]), "files missing in folder_with_gts"
458
+ test_ref_pairs = [(join(folder_with_predictions, i), join(folder_with_gts, i)) for i in files_pred]
459
+ res = aggregate_scores(test_ref_pairs, json_output_file=join(folder_with_predictions, "summary.json"),
460
+ num_threads=8, labels=labels, **metric_kwargs)
461
+ return res
462
+
463
+
464
+ def nnunet_evaluate_folder():
465
+ import argparse
466
+ parser = argparse.ArgumentParser("Evaluates the segmentations located in the folder pred. Output of this script is "
467
+ "a json file. At the very bottom of the json file is going to be a 'mean' "
468
+ "entry with averages metrics across all cases")
469
+ parser.add_argument('-ref', required=True, type=str, help="Folder containing the reference segmentations in nifti "
470
+ "format.")
471
+ parser.add_argument('-pred', required=True, type=str, help="Folder containing the predicted segmentations in nifti "
472
+ "format. File names must match between the folders!")
473
+ parser.add_argument('-l', nargs='+', type=int, required=True, help="List of label IDs (integer values) that should "
474
+ "be evaluated. Best practice is to use all int "
475
+ "values present in the dataset, so for example "
476
+ "for LiTS the labels are 0: background, 1: "
477
+ "liver, 2: tumor. So this argument "
478
+ "should be -l 1 2. You can if you want also "
479
+ "evaluate the background label (0) but in "
480
+ "this case that would not give any useful "
481
+ "information.")
482
+ args = parser.parse_args()
483
+ return evaluate_folder(args.ref, args.pred, args.l)
nnunet/evaluation/metrics.py ADDED
@@ -0,0 +1,406 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Division of Medical Image Computing, German Cancer Research Center (DKFZ), Heidelberg, Germany
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import numpy as np
16
+ from medpy import metric
17
+
18
+
19
+ def assert_shape(test, reference):
20
+
21
+ assert test.shape == reference.shape, "Shape mismatch: {} and {}".format(
22
+ test.shape, reference.shape)
23
+
24
+
25
+ class ConfusionMatrix:
26
+
27
+ def __init__(self, test=None, reference=None):
28
+
29
+ self.tp = None
30
+ self.fp = None
31
+ self.tn = None
32
+ self.fn = None
33
+ self.size = None
34
+ self.reference_empty = None
35
+ self.reference_full = None
36
+ self.test_empty = None
37
+ self.test_full = None
38
+ self.set_reference(reference)
39
+ self.set_test(test)
40
+
41
+ def set_test(self, test):
42
+
43
+ self.test = test
44
+ self.reset()
45
+
46
+ def set_reference(self, reference):
47
+
48
+ self.reference = reference
49
+ self.reset()
50
+
51
+ def reset(self):
52
+
53
+ self.tp = None
54
+ self.fp = None
55
+ self.tn = None
56
+ self.fn = None
57
+ self.size = None
58
+ self.test_empty = None
59
+ self.test_full = None
60
+ self.reference_empty = None
61
+ self.reference_full = None
62
+
63
+ def compute(self):
64
+
65
+ if self.test is None or self.reference is None:
66
+ raise ValueError("'test' and 'reference' must both be set to compute confusion matrix.")
67
+
68
+ assert_shape(self.test, self.reference)
69
+
70
+ self.tp = int(((self.test != 0) * (self.reference != 0)).sum())
71
+ self.fp = int(((self.test != 0) * (self.reference == 0)).sum())
72
+ self.tn = int(((self.test == 0) * (self.reference == 0)).sum())
73
+ self.fn = int(((self.test == 0) * (self.reference != 0)).sum())
74
+ self.size = int(np.prod(self.reference.shape, dtype=np.int64))
75
+ self.test_empty = not np.any(self.test)
76
+ self.test_full = np.all(self.test)
77
+ self.reference_empty = not np.any(self.reference)
78
+ self.reference_full = np.all(self.reference)
79
+
80
+ def get_matrix(self):
81
+
82
+ for entry in (self.tp, self.fp, self.tn, self.fn):
83
+ if entry is None:
84
+ self.compute()
85
+ break
86
+
87
+ return self.tp, self.fp, self.tn, self.fn
88
+
89
+ def get_size(self):
90
+
91
+ if self.size is None:
92
+ self.compute()
93
+ return self.size
94
+
95
+ def get_existence(self):
96
+
97
+ for case in (self.test_empty, self.test_full, self.reference_empty, self.reference_full):
98
+ if case is None:
99
+ self.compute()
100
+ break
101
+
102
+ return self.test_empty, self.test_full, self.reference_empty, self.reference_full
103
+
104
+
105
+ def dice(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, **kwargs):
106
+ """2TP / (2TP + FP + FN)"""
107
+
108
+ if confusion_matrix is None:
109
+ confusion_matrix = ConfusionMatrix(test, reference)
110
+
111
+ tp, fp, tn, fn = confusion_matrix.get_matrix()
112
+ test_empty, test_full, reference_empty, reference_full = confusion_matrix.get_existence()
113
+
114
+ if test_empty and reference_empty:
115
+ if nan_for_nonexisting:
116
+ return float("NaN")
117
+ else:
118
+ return 0.
119
+
120
+ return float(2. * tp / (2 * tp + fp + fn))
121
+
122
+
123
+ def jaccard(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, **kwargs):
124
+ """TP / (TP + FP + FN)"""
125
+
126
+ if confusion_matrix is None:
127
+ confusion_matrix = ConfusionMatrix(test, reference)
128
+
129
+ tp, fp, tn, fn = confusion_matrix.get_matrix()
130
+ test_empty, test_full, reference_empty, reference_full = confusion_matrix.get_existence()
131
+
132
+ if test_empty and reference_empty:
133
+ if nan_for_nonexisting:
134
+ return float("NaN")
135
+ else:
136
+ return 0.
137
+
138
+ return float(tp / (tp + fp + fn))
139
+
140
+
141
+ def precision(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, **kwargs):
142
+ """TP / (TP + FP)"""
143
+
144
+ if confusion_matrix is None:
145
+ confusion_matrix = ConfusionMatrix(test, reference)
146
+
147
+ tp, fp, tn, fn = confusion_matrix.get_matrix()
148
+ test_empty, test_full, reference_empty, reference_full = confusion_matrix.get_existence()
149
+
150
+ if test_empty:
151
+ if nan_for_nonexisting:
152
+ return float("NaN")
153
+ else:
154
+ return 0.
155
+
156
+ return float(tp / (tp + fp))
157
+
158
+
159
+ def sensitivity(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, **kwargs):
160
+ """TP / (TP + FN)"""
161
+
162
+ if confusion_matrix is None:
163
+ confusion_matrix = ConfusionMatrix(test, reference)
164
+
165
+ tp, fp, tn, fn = confusion_matrix.get_matrix()
166
+ test_empty, test_full, reference_empty, reference_full = confusion_matrix.get_existence()
167
+
168
+ if reference_empty:
169
+ if nan_for_nonexisting:
170
+ return float("NaN")
171
+ else:
172
+ return 0.
173
+
174
+ return float(tp / (tp + fn))
175
+
176
+
177
+ def recall(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, **kwargs):
178
+ """TP / (TP + FN)"""
179
+
180
+ return sensitivity(test, reference, confusion_matrix, nan_for_nonexisting, **kwargs)
181
+
182
+
183
+ def specificity(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, **kwargs):
184
+ """TN / (TN + FP)"""
185
+
186
+ if confusion_matrix is None:
187
+ confusion_matrix = ConfusionMatrix(test, reference)
188
+
189
+ tp, fp, tn, fn = confusion_matrix.get_matrix()
190
+ test_empty, test_full, reference_empty, reference_full = confusion_matrix.get_existence()
191
+
192
+ if reference_full:
193
+ if nan_for_nonexisting:
194
+ return float("NaN")
195
+ else:
196
+ return 0.
197
+
198
+ return float(tn / (tn + fp))
199
+
200
+
201
+ def accuracy(test=None, reference=None, confusion_matrix=None, **kwargs):
202
+ """(TP + TN) / (TP + FP + FN + TN)"""
203
+
204
+ if confusion_matrix is None:
205
+ confusion_matrix = ConfusionMatrix(test, reference)
206
+
207
+ tp, fp, tn, fn = confusion_matrix.get_matrix()
208
+
209
+ return float((tp + tn) / (tp + fp + tn + fn))
210
+
211
+
212
+ def fscore(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, beta=1., **kwargs):
213
+ """(1 + b^2) * TP / ((1 + b^2) * TP + b^2 * FN + FP)"""
214
+
215
+ precision_ = precision(test, reference, confusion_matrix, nan_for_nonexisting)
216
+ recall_ = recall(test, reference, confusion_matrix, nan_for_nonexisting)
217
+
218
+ return (1 + beta*beta) * precision_ * recall_ /\
219
+ ((beta*beta * precision_) + recall_)
220
+
221
+
222
+ def false_positive_rate(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, **kwargs):
223
+ """FP / (FP + TN)"""
224
+
225
+ return 1 - specificity(test, reference, confusion_matrix, nan_for_nonexisting)
226
+
227
+
228
+ def false_omission_rate(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, **kwargs):
229
+ """FN / (TN + FN)"""
230
+
231
+ if confusion_matrix is None:
232
+ confusion_matrix = ConfusionMatrix(test, reference)
233
+
234
+ tp, fp, tn, fn = confusion_matrix.get_matrix()
235
+ test_empty, test_full, reference_empty, reference_full = confusion_matrix.get_existence()
236
+
237
+ if test_full:
238
+ if nan_for_nonexisting:
239
+ return float("NaN")
240
+ else:
241
+ return 0.
242
+
243
+ return float(fn / (fn + tn))
244
+
245
+
246
+ def false_negative_rate(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, **kwargs):
247
+ """FN / (TP + FN)"""
248
+
249
+ return 1 - sensitivity(test, reference, confusion_matrix, nan_for_nonexisting)
250
+
251
+
252
+ def true_negative_rate(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, **kwargs):
253
+ """TN / (TN + FP)"""
254
+
255
+ return specificity(test, reference, confusion_matrix, nan_for_nonexisting)
256
+
257
+
258
+ def false_discovery_rate(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, **kwargs):
259
+ """FP / (TP + FP)"""
260
+
261
+ return 1 - precision(test, reference, confusion_matrix, nan_for_nonexisting)
262
+
263
+
264
+ def negative_predictive_value(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, **kwargs):
265
+ """TN / (TN + FN)"""
266
+
267
+ return 1 - false_omission_rate(test, reference, confusion_matrix, nan_for_nonexisting)
268
+
269
+
270
+ def total_positives_test(test=None, reference=None, confusion_matrix=None, **kwargs):
271
+ """TP + FP"""
272
+
273
+ if confusion_matrix is None:
274
+ confusion_matrix = ConfusionMatrix(test, reference)
275
+
276
+ tp, fp, tn, fn = confusion_matrix.get_matrix()
277
+
278
+ return tp + fp
279
+
280
+
281
+ def total_negatives_test(test=None, reference=None, confusion_matrix=None, **kwargs):
282
+ """TN + FN"""
283
+
284
+ if confusion_matrix is None:
285
+ confusion_matrix = ConfusionMatrix(test, reference)
286
+
287
+ tp, fp, tn, fn = confusion_matrix.get_matrix()
288
+
289
+ return tn + fn
290
+
291
+
292
+ def total_positives_reference(test=None, reference=None, confusion_matrix=None, **kwargs):
293
+ """TP + FN"""
294
+
295
+ if confusion_matrix is None:
296
+ confusion_matrix = ConfusionMatrix(test, reference)
297
+
298
+ tp, fp, tn, fn = confusion_matrix.get_matrix()
299
+
300
+ return tp + fn
301
+
302
+
303
+ def total_negatives_reference(test=None, reference=None, confusion_matrix=None, **kwargs):
304
+ """TN + FP"""
305
+
306
+ if confusion_matrix is None:
307
+ confusion_matrix = ConfusionMatrix(test, reference)
308
+
309
+ tp, fp, tn, fn = confusion_matrix.get_matrix()
310
+
311
+ return tn + fp
312
+
313
+
314
+ def hausdorff_distance(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, voxel_spacing=None, connectivity=1, **kwargs):
315
+
316
+ if confusion_matrix is None:
317
+ confusion_matrix = ConfusionMatrix(test, reference)
318
+
319
+ test_empty, test_full, reference_empty, reference_full = confusion_matrix.get_existence()
320
+
321
+ if test_empty or test_full or reference_empty or reference_full:
322
+ if nan_for_nonexisting:
323
+ return float("NaN")
324
+ else:
325
+ return 0
326
+
327
+ test, reference = confusion_matrix.test, confusion_matrix.reference
328
+
329
+ return metric.hd(test, reference, voxel_spacing, connectivity)
330
+
331
+
332
+ def hausdorff_distance_95(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, voxel_spacing=None, connectivity=1, **kwargs):
333
+
334
+ if confusion_matrix is None:
335
+ confusion_matrix = ConfusionMatrix(test, reference)
336
+
337
+ test_empty, test_full, reference_empty, reference_full = confusion_matrix.get_existence()
338
+
339
+ if test_empty or test_full or reference_empty or reference_full:
340
+ if nan_for_nonexisting:
341
+ return float("NaN")
342
+ else:
343
+ return 0
344
+
345
+ test, reference = confusion_matrix.test, confusion_matrix.reference
346
+
347
+ return metric.hd95(test, reference, voxel_spacing, connectivity)
348
+
349
+
350
+ def avg_surface_distance(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, voxel_spacing=None, connectivity=1, **kwargs):
351
+
352
+ if confusion_matrix is None:
353
+ confusion_matrix = ConfusionMatrix(test, reference)
354
+
355
+ test_empty, test_full, reference_empty, reference_full = confusion_matrix.get_existence()
356
+
357
+ if test_empty or test_full or reference_empty or reference_full:
358
+ if nan_for_nonexisting:
359
+ return float("NaN")
360
+ else:
361
+ return 0
362
+
363
+ test, reference = confusion_matrix.test, confusion_matrix.reference
364
+
365
+ return metric.asd(test, reference, voxel_spacing, connectivity)
366
+
367
+
368
+ def avg_surface_distance_symmetric(test=None, reference=None, confusion_matrix=None, nan_for_nonexisting=True, voxel_spacing=None, connectivity=1, **kwargs):
369
+
370
+ if confusion_matrix is None:
371
+ confusion_matrix = ConfusionMatrix(test, reference)
372
+
373
+ test_empty, test_full, reference_empty, reference_full = confusion_matrix.get_existence()
374
+
375
+ if test_empty or test_full or reference_empty or reference_full:
376
+ if nan_for_nonexisting:
377
+ return float("NaN")
378
+ else:
379
+ return 0
380
+
381
+ test, reference = confusion_matrix.test, confusion_matrix.reference
382
+
383
+ return metric.assd(test, reference, voxel_spacing, connectivity)
384
+
385
+
386
+ ALL_METRICS = {
387
+ "False Positive Rate": false_positive_rate,
388
+ "Dice": dice,
389
+ "Jaccard": jaccard,
390
+ "Hausdorff Distance": hausdorff_distance,
391
+ "Hausdorff Distance 95": hausdorff_distance_95,
392
+ "Precision": precision,
393
+ "Recall": recall,
394
+ "Avg. Symmetric Surface Distance": avg_surface_distance_symmetric,
395
+ "Avg. Surface Distance": avg_surface_distance,
396
+ "Accuracy": accuracy,
397
+ "False Omission Rate": false_omission_rate,
398
+ "Negative Predictive Value": negative_predictive_value,
399
+ "False Negative Rate": false_negative_rate,
400
+ "True Negative Rate": true_negative_rate,
401
+ "False Discovery Rate": false_discovery_rate,
402
+ "Total Positives Test": total_positives_test,
403
+ "Total Negatives Test": total_negatives_test,
404
+ "Total Positives Reference": total_positives_reference,
405
+ "total Negatives Reference": total_negatives_reference
406
+ }