Chris Xiao
commited on
Commit
·
c642393
1
Parent(s):
1b45c44
upload files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- LICENSE +201 -0
- README.md +252 -3
- assets/method.png +0 -0
- metrics/distanceVertex2Mesh.py +64 -0
- metrics/get_probability_map.py +194 -0
- metrics/lookup_tables.py +463 -0
- metrics/metrics.py +355 -0
- metrics/surface_distance.py +424 -0
- nnunet/__init__.py +7 -0
- nnunet/configuration.py +5 -0
- nnunet/dataset_conversion/Task017_BeyondCranialVaultAbdominalOrganSegmentation.py +94 -0
- nnunet/dataset_conversion/Task024_Promise2012.py +81 -0
- nnunet/dataset_conversion/Task027_AutomaticCardiacDetectionChallenge.py +106 -0
- nnunet/dataset_conversion/Task029_LiverTumorSegmentationChallenge.py +123 -0
- nnunet/dataset_conversion/Task032_BraTS_2018.py +176 -0
- nnunet/dataset_conversion/Task035_ISBI_MSLesionSegmentationChallenge.py +162 -0
- nnunet/dataset_conversion/Task037_038_Chaos_Challenge.py +460 -0
- nnunet/dataset_conversion/Task040_KiTS.py +240 -0
- nnunet/dataset_conversion/Task043_BraTS_2019.py +164 -0
- nnunet/dataset_conversion/Task055_SegTHOR.py +98 -0
- nnunet/dataset_conversion/Task056_VerSe2019.py +274 -0
- nnunet/dataset_conversion/Task056_Verse_normalize_orientation.py +98 -0
- nnunet/dataset_conversion/Task058_ISBI_EM_SEG.py +105 -0
- nnunet/dataset_conversion/Task059_EPFL_EM_MITO_SEG.py +99 -0
- nnunet/dataset_conversion/Task061_CREMI.py +146 -0
- nnunet/dataset_conversion/Task062_NIHPancreas.py +89 -0
- nnunet/dataset_conversion/Task064_KiTS_labelsFixed.py +84 -0
- nnunet/dataset_conversion/Task065_KiTS_NicksLabels.py +87 -0
- nnunet/dataset_conversion/Task069_CovidSeg.py +68 -0
- nnunet/dataset_conversion/Task075_Fluo_C3DH_A549_ManAndSim.py +137 -0
- nnunet/dataset_conversion/Task076_Fluo_N3DH_SIM.py +312 -0
- nnunet/dataset_conversion/Task082_BraTS_2020.py +751 -0
- nnunet/dataset_conversion/Task083_VerSe2020.py +138 -0
- nnunet/dataset_conversion/Task089_Fluo-N2DH-SIM.py +290 -0
- nnunet/dataset_conversion/Task114_heart_MNMs.py +262 -0
- nnunet/dataset_conversion/Task115_COVIDSegChallenge.py +344 -0
- nnunet/dataset_conversion/Task120_Massachusetts_RoadSegm.py +103 -0
- nnunet/dataset_conversion/Task135_KiTS2021.py +49 -0
- nnunet/dataset_conversion/Task154_RibFrac_multi_label.py +172 -0
- nnunet/dataset_conversion/Task155_RibFrac_binary.py +174 -0
- nnunet/dataset_conversion/Task156_RibSeg.py +140 -0
- nnunet/dataset_conversion/Task159_MyoPS2020.py +106 -0
- nnunet/dataset_conversion/__init__.py +3 -0
- nnunet/dataset_conversion/utils.py +76 -0
- nnunet/evaluation/__init__.py +2 -0
- nnunet/evaluation/add_dummy_task_with_mean_over_all_tasks.py +77 -0
- nnunet/evaluation/add_mean_dice_to_json.py +51 -0
- nnunet/evaluation/collect_results_files.py +48 -0
- nnunet/evaluation/evaluator.py +483 -0
- 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 |
-
|
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 |
+
}
|