yzluka commited on
Commit
30ce2fc
·
verified ·
1 Parent(s): fa038cd

Add portable PE segmentation model

Browse files
.gitattributes CHANGED
@@ -1,35 +1 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ pytorch_model.bin filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: transformers
3
+ pipeline_tag: image-segmentation
4
+ tags:
5
+ - ct-pulmonary-angiography
6
+ - medical-imaging
7
+ - ct
8
+ - pulmonary-embolism
9
+ - segmentation
10
+ - nnunet
11
+ - pytorch
12
+ license: apache-2.0
13
+ datasets:
14
+ - mazurowski-lab/PulmonaryEmbolismSegmentation
15
+ paperswithcode_id: pulmonary-embolism-segmentation
16
+ model-index:
17
+ - name: Pulmonary Embolism Segmentation
18
+ results:
19
+ - task:
20
+ type: image-segmentation
21
+ name: 3D medical image segmentation
22
+ dataset:
23
+ name: Pulmonary Embolism Segmentation
24
+ type: mazurowski-lab/PulmonaryEmbolismSegmentation
25
+ metrics:
26
+ - type: dice
27
+ name: Mean Dice
28
+ value: 0.5357
29
+ ---
30
+
31
+ # Pulmonary Embolism Segmentation
32
+
33
+ This repository contains a portable Hugging Face-compatible version of the
34
+ Pulmonary Embolism Segmentation model from Mazurowski Lab.
35
+
36
+ The model is a 3D Residual Encoder U-Net trained with nnU-Net v2. The runtime
37
+ model code is implemented directly in PyTorch, so inference does not require
38
+ `nnunetv2` or `dynamic-network-architectures`.
39
+
40
+ - GitHub: https://github.com/mazurowski-lab/PulmonaryEmbolismSegmentation
41
+ - Paper: https://link.springer.com/article/10.1007/s10278-026-01958-4
42
+ - Contact: Yixin Zhang, yz696@duke.edu
43
+
44
+ ## Model
45
+
46
+ - Architecture: 3D ResidualEncoderUNet
47
+ - Input: single-channel CT volume
48
+ - Output: 2 logits, background and pulmonary embolism
49
+ - Training patch size: `[224, 320, 320]`
50
+ - Plan spacing: `[1.0, 0.7373045682907104, 0.7373045682907104]`
51
+ - Published checkpoint: `fold_all`
52
+
53
+ ## Usage
54
+
55
+ ```python
56
+ from pulmonary_embolism_segmentation import PulmonaryEmbolismSegmentationModel
57
+
58
+ model = PulmonaryEmbolismSegmentationModel.from_pretrained(
59
+ "PATH_OR_REPO_ID",
60
+ trust_remote_code=True,
61
+ )
62
+ ```
63
+
64
+ For full DICOM inference, use the included helper code from the project
65
+ repository:
66
+
67
+ ```powershell
68
+ python scripts/run_inference.py `
69
+ --model-dir hf_pe_segmentation_fold_all `
70
+ --input sample_data/02GE/dicom `
71
+ --output outputs/02GE_segmentation_nnunet_preprocess.npz `
72
+ --tile-size 128,256,256
73
+ ```
74
+
75
+ The inference helper follows the nnU-Net v2 preprocessing order:
76
+
77
+ 1. read image and spacing
78
+ 2. crop nonzero region
79
+ 3. CT clip and normalize
80
+ 4. resample image to plan spacing
81
+ 5. sliding-window prediction
82
+ 6. resample logits back to cropped source grid
83
+ 7. argmax
84
+ 8. insert crop back into the original image shape
85
+
86
+ ## Dependencies
87
+
88
+ Core model loading:
89
+
90
+ - `torch`
91
+ - `transformers`
92
+ - `numpy`
93
+ - `scipy`
94
+ - `scikit-image`
95
+
96
+ DICOM/NIfTI helpers:
97
+
98
+ - `SimpleITK`
99
+ - `nibabel`
100
+
101
+ ## Validation
102
+
103
+ A 40-case validation pass was run on the pixel-level annotated dataset used for
104
+ testing portability. Dice scores are computed after resampling predictions back
105
+ to the original DICOM grid.
106
+
107
+ | Group | n | Mean Dice | Median Dice | Min | Max | Dice >= 0.5 | Dice >= 0.7 |
108
+ |---|---:|---:|---:|---:|---:|---:|---:|
109
+ | GE | 20 | 0.5934 | 0.6918 | 0.0016 | 0.8512 | 15 | 10 |
110
+ | Toshiba | 20 | 0.4780 | 0.7103 | 0.0000 | 0.8020 | 11 | 11 |
111
+ | All | 40 | 0.5357 | 0.7035 | 0.0000 | 0.8512 | 26 | 21 |
112
+
113
+ Low-scoring cases should be reviewed for possible annotation, DICOM series, or
114
+ orientation/metadata issues before drawing model-performance conclusions.
115
+
116
+ Additional input-axis permutation checks were run on low-performing cases
117
+ (`04TS`, `20GE`, `01TS`, `17TS`). The original `(z, y, x)` input order gave the
118
+ best Dice in each tested case, suggesting these failures are not explained by a
119
+ simple array transpose mismatch.
120
+
121
+ ## Intended Use
122
+
123
+ This model is intended for research use in pulmonary embolism segmentation from
124
+ CT pulmonary angiography. It is not a medical device and should not be used for
125
+ clinical decision-making without appropriate validation.
126
+
127
+ ## Citation
128
+
129
+ If you use this model, please cite the associated paper:
130
+
131
+ ```bibtex
132
+ @article{pulmonary_embolism_segmentation_2026,
133
+ title = {Pulmonary Embolism Segmentation},
134
+ author = {Zhang, Yixin},
135
+ journal = {Journal of Imaging Informatics in Medicine},
136
+ year = {2026},
137
+ doi = {10.1007/s10278-026-01958-4},
138
+ url = {https://link.springer.com/article/10.1007/s10278-026-01958-4}
139
+ }
140
+ ```
config.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": ["PulmonaryEmbolismSegmentationModel"],
3
+ "auto_map": {
4
+ "AutoConfig": "configuration_pe_segmentation.PulmonaryEmbolismSegmentationConfig",
5
+ "AutoModel": "modeling_pe_segmentation.PulmonaryEmbolismSegmentationModel"
6
+ },
7
+ "model_type": "pulmonary-embolism-segmentation",
8
+ "input_channels": 1,
9
+ "num_labels": 2,
10
+ "patch_size": [224, 320, 320],
11
+ "spacing": [1.0, 0.7373045682907104, 0.7373045682907104],
12
+ "features_per_stage": [32, 64, 128, 256, 320, 320, 320],
13
+ "kernel_sizes": [[3, 3, 3], [3, 3, 3], [3, 3, 3], [3, 3, 3], [3, 3, 3], [3, 3, 3], [3, 3, 3]],
14
+ "strides": [[1, 1, 1], [2, 2, 2], [2, 2, 2], [2, 2, 2], [2, 2, 2], [2, 2, 2], [1, 2, 2]],
15
+ "n_blocks_per_stage": [1, 3, 4, 6, 6, 6, 6],
16
+ "n_conv_per_stage_decoder": [1, 1, 1, 1, 1, 1],
17
+ "conv_bias": true,
18
+ "norm_eps": 1e-05,
19
+ "norm_affine": true,
20
+ "leaky_relu_inplace": true,
21
+ "deep_supervision": false,
22
+ "label2id": {
23
+ "background": 0,
24
+ "pulmonary_embolism": 1
25
+ },
26
+ "id2label": {
27
+ "0": "background",
28
+ "1": "pulmonary_embolism"
29
+ },
30
+ "ct_clip_min": -195.0,
31
+ "ct_clip_max": 305.0,
32
+ "ct_mean": 37.060203552246094,
33
+ "ct_std": 92.34374237060547
34
+ }
configuration_pe_segmentation.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PretrainedConfig
2
+
3
+
4
+ class PulmonaryEmbolismSegmentationConfig(PretrainedConfig):
5
+ model_type = "pulmonary-embolism-segmentation"
6
+
7
+ def __init__(
8
+ self,
9
+ input_channels=1,
10
+ num_labels=2,
11
+ patch_size=None,
12
+ spacing=None,
13
+ features_per_stage=None,
14
+ kernel_sizes=None,
15
+ strides=None,
16
+ n_blocks_per_stage=None,
17
+ n_conv_per_stage_decoder=None,
18
+ conv_bias=True,
19
+ norm_eps=1e-5,
20
+ norm_affine=True,
21
+ leaky_relu_inplace=True,
22
+ deep_supervision=False,
23
+ label2id=None,
24
+ id2label=None,
25
+ ct_clip_min=-195.0,
26
+ ct_clip_max=305.0,
27
+ ct_mean=37.060203552246094,
28
+ ct_std=92.34374237060547,
29
+ **kwargs,
30
+ ):
31
+ label2id = label2id or {"background": 0, "pulmonary_embolism": 1}
32
+ id2label = id2label or {str(v): k for k, v in label2id.items()}
33
+ super().__init__(label2id=label2id, id2label=id2label, **kwargs)
34
+
35
+ self.input_channels = input_channels
36
+ self.num_labels = num_labels
37
+ self.patch_size = patch_size or [224, 320, 320]
38
+ self.spacing = spacing or [1.0, 0.7373045682907104, 0.7373045682907104]
39
+ self.features_per_stage = features_per_stage or [32, 64, 128, 256, 320, 320, 320]
40
+ self.kernel_sizes = kernel_sizes or [[3, 3, 3]] * 7
41
+ self.strides = strides or [
42
+ [1, 1, 1],
43
+ [2, 2, 2],
44
+ [2, 2, 2],
45
+ [2, 2, 2],
46
+ [2, 2, 2],
47
+ [2, 2, 2],
48
+ [1, 2, 2],
49
+ ]
50
+ self.n_blocks_per_stage = n_blocks_per_stage or [1, 3, 4, 6, 6, 6, 6]
51
+ self.n_conv_per_stage_decoder = n_conv_per_stage_decoder or [1, 1, 1, 1, 1, 1]
52
+ self.conv_bias = conv_bias
53
+ self.norm_eps = norm_eps
54
+ self.norm_affine = norm_affine
55
+ self.leaky_relu_inplace = leaky_relu_inplace
56
+ self.deep_supervision = deep_supervision
57
+
58
+ self.ct_clip_min = ct_clip_min
59
+ self.ct_clip_max = ct_clip_max
60
+ self.ct_mean = ct_mean
61
+ self.ct_std = ct_std
local_architecture.py ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Sequence
2
+
3
+ import torch
4
+ from torch import nn
5
+
6
+
7
+ def _as_tuple(value):
8
+ if isinstance(value, int):
9
+ return (value, value, value)
10
+ return tuple(value)
11
+
12
+
13
+ class ConvDropoutNormReLU(nn.Module):
14
+ def __init__(
15
+ self,
16
+ input_channels,
17
+ output_channels,
18
+ kernel_size,
19
+ stride,
20
+ conv_bias=True,
21
+ norm_eps=1e-5,
22
+ norm_affine=True,
23
+ with_nonlin=True,
24
+ ):
25
+ super().__init__()
26
+ kernel_size = _as_tuple(kernel_size)
27
+ stride = _as_tuple(stride)
28
+ padding = tuple((k - 1) // 2 for k in kernel_size)
29
+ self.conv = nn.Conv3d(input_channels, output_channels, kernel_size, stride, padding, bias=conv_bias)
30
+ self.norm = nn.InstanceNorm3d(output_channels, eps=norm_eps, affine=norm_affine)
31
+ modules = [self.conv, self.norm]
32
+ if with_nonlin:
33
+ self.nonlin = nn.LeakyReLU(negative_slope=0.01, inplace=True)
34
+ modules.append(self.nonlin)
35
+ self.all_modules = nn.Sequential(*modules)
36
+
37
+ def forward(self, x):
38
+ return self.all_modules(x)
39
+
40
+
41
+ class StackedConvBlocks(nn.Module):
42
+ def __init__(
43
+ self,
44
+ num_convs,
45
+ input_channels,
46
+ output_channels,
47
+ kernel_size,
48
+ initial_stride,
49
+ conv_bias=True,
50
+ norm_eps=1e-5,
51
+ norm_affine=True,
52
+ ):
53
+ super().__init__()
54
+ if not isinstance(output_channels, (tuple, list)):
55
+ output_channels = [output_channels] * num_convs
56
+ blocks = [
57
+ ConvDropoutNormReLU(
58
+ input_channels,
59
+ output_channels[0],
60
+ kernel_size,
61
+ initial_stride,
62
+ conv_bias=conv_bias,
63
+ norm_eps=norm_eps,
64
+ norm_affine=norm_affine,
65
+ with_nonlin=True,
66
+ )
67
+ ]
68
+ for idx in range(1, num_convs):
69
+ blocks.append(
70
+ ConvDropoutNormReLU(
71
+ output_channels[idx - 1],
72
+ output_channels[idx],
73
+ kernel_size,
74
+ 1,
75
+ conv_bias=conv_bias,
76
+ norm_eps=norm_eps,
77
+ norm_affine=norm_affine,
78
+ with_nonlin=True,
79
+ )
80
+ )
81
+ self.convs = nn.Sequential(*blocks)
82
+
83
+ def forward(self, x):
84
+ return self.convs(x)
85
+
86
+
87
+ class BasicBlockD(nn.Module):
88
+ def __init__(
89
+ self,
90
+ input_channels,
91
+ output_channels,
92
+ kernel_size,
93
+ stride,
94
+ conv_bias=True,
95
+ norm_eps=1e-5,
96
+ norm_affine=True,
97
+ ):
98
+ super().__init__()
99
+ stride = _as_tuple(stride)
100
+ self.conv1 = ConvDropoutNormReLU(
101
+ input_channels,
102
+ output_channels,
103
+ kernel_size,
104
+ stride,
105
+ conv_bias=conv_bias,
106
+ norm_eps=norm_eps,
107
+ norm_affine=norm_affine,
108
+ with_nonlin=True,
109
+ )
110
+ self.conv2 = ConvDropoutNormReLU(
111
+ output_channels,
112
+ output_channels,
113
+ kernel_size,
114
+ 1,
115
+ conv_bias=conv_bias,
116
+ norm_eps=norm_eps,
117
+ norm_affine=norm_affine,
118
+ with_nonlin=False,
119
+ )
120
+ self.nonlin2 = nn.LeakyReLU(negative_slope=0.01, inplace=True)
121
+
122
+ has_stride = any(s != 1 for s in stride)
123
+ requires_projection = input_channels != output_channels
124
+ if has_stride or requires_projection:
125
+ ops = []
126
+ if has_stride:
127
+ ops.append(nn.AvgPool3d(kernel_size=stride, stride=stride))
128
+ if requires_projection:
129
+ ops.append(
130
+ ConvDropoutNormReLU(
131
+ input_channels,
132
+ output_channels,
133
+ 1,
134
+ 1,
135
+ conv_bias=False,
136
+ norm_eps=norm_eps,
137
+ norm_affine=norm_affine,
138
+ with_nonlin=False,
139
+ )
140
+ )
141
+ self.skip = nn.Sequential(*ops)
142
+ else:
143
+ self.skip = nn.Identity()
144
+
145
+ def forward(self, x):
146
+ return self.nonlin2(self.conv2(self.conv1(x)) + self.skip(x))
147
+
148
+
149
+ class StackedResidualBlocks(nn.Module):
150
+ def __init__(
151
+ self,
152
+ n_blocks,
153
+ input_channels,
154
+ output_channels,
155
+ kernel_size,
156
+ initial_stride,
157
+ conv_bias=True,
158
+ norm_eps=1e-5,
159
+ norm_affine=True,
160
+ ):
161
+ super().__init__()
162
+ blocks = [
163
+ BasicBlockD(
164
+ input_channels,
165
+ output_channels,
166
+ kernel_size,
167
+ initial_stride,
168
+ conv_bias=conv_bias,
169
+ norm_eps=norm_eps,
170
+ norm_affine=norm_affine,
171
+ )
172
+ ]
173
+ for _ in range(1, n_blocks):
174
+ blocks.append(
175
+ BasicBlockD(
176
+ output_channels,
177
+ output_channels,
178
+ kernel_size,
179
+ 1,
180
+ conv_bias=conv_bias,
181
+ norm_eps=norm_eps,
182
+ norm_affine=norm_affine,
183
+ )
184
+ )
185
+ self.blocks = nn.Sequential(*blocks)
186
+
187
+ def forward(self, x):
188
+ return self.blocks(x)
189
+
190
+
191
+ class ResidualEncoder(nn.Module):
192
+ def __init__(
193
+ self,
194
+ input_channels,
195
+ features_per_stage,
196
+ kernel_sizes,
197
+ strides,
198
+ n_blocks_per_stage,
199
+ conv_bias=True,
200
+ norm_eps=1e-5,
201
+ norm_affine=True,
202
+ ):
203
+ super().__init__()
204
+ self.stem = StackedConvBlocks(
205
+ 1,
206
+ input_channels,
207
+ features_per_stage[0],
208
+ kernel_sizes[0],
209
+ 1,
210
+ conv_bias=conv_bias,
211
+ norm_eps=norm_eps,
212
+ norm_affine=norm_affine,
213
+ )
214
+ input_channels = features_per_stage[0]
215
+ stages = []
216
+ for idx, output_channels in enumerate(features_per_stage):
217
+ stages.append(
218
+ StackedResidualBlocks(
219
+ n_blocks_per_stage[idx],
220
+ input_channels,
221
+ output_channels,
222
+ kernel_sizes[idx],
223
+ strides[idx],
224
+ conv_bias=conv_bias,
225
+ norm_eps=norm_eps,
226
+ norm_affine=norm_affine,
227
+ )
228
+ )
229
+ input_channels = output_channels
230
+ self.stages = nn.Sequential(*stages)
231
+
232
+ def forward(self, x):
233
+ x = self.stem(x)
234
+ skips = []
235
+ for stage in self.stages:
236
+ x = stage(x)
237
+ skips.append(x)
238
+ return skips
239
+
240
+
241
+ class UNetDecoder(nn.Module):
242
+ def __init__(
243
+ self,
244
+ features_per_stage: Sequence[int],
245
+ strides: Sequence[Sequence[int]],
246
+ num_classes: int,
247
+ n_conv_per_stage_decoder: Sequence[int],
248
+ conv_bias=True,
249
+ norm_eps=1e-5,
250
+ norm_affine=True,
251
+ deep_supervision=False,
252
+ ):
253
+ super().__init__()
254
+ self.deep_supervision = deep_supervision
255
+ self.encoder = nn.Identity()
256
+ encoder_channels = list(features_per_stage)
257
+ decoder_channels = list(features_per_stage[:-1][::-1])
258
+ bottleneck_channels = features_per_stage[-1]
259
+ transpose_strides = list(strides[1:][::-1])
260
+
261
+ self.stages = nn.ModuleList()
262
+ self.transpconvs = nn.ModuleList()
263
+ self.seg_layers = nn.ModuleList()
264
+
265
+ input_channels = bottleneck_channels
266
+ for idx, output_channels in enumerate(decoder_channels):
267
+ self.transpconvs.append(
268
+ nn.ConvTranspose3d(
269
+ input_channels,
270
+ output_channels,
271
+ kernel_size=_as_tuple(transpose_strides[idx]),
272
+ stride=_as_tuple(transpose_strides[idx]),
273
+ )
274
+ )
275
+ self.stages.append(
276
+ StackedConvBlocks(
277
+ n_conv_per_stage_decoder[idx],
278
+ output_channels + encoder_channels[-(idx + 2)],
279
+ output_channels,
280
+ 3,
281
+ 1,
282
+ conv_bias=conv_bias,
283
+ norm_eps=norm_eps,
284
+ norm_affine=norm_affine,
285
+ )
286
+ )
287
+ self.seg_layers.append(nn.Conv3d(output_channels, num_classes, 1, 1, 0))
288
+ input_channels = output_channels
289
+
290
+ def forward(self, skips):
291
+ x = skips[-1]
292
+ seg_outputs = []
293
+ for idx, stage in enumerate(self.stages):
294
+ x = self.transpconvs[idx](x)
295
+ x = torch.cat((x, skips[-(idx + 2)]), dim=1)
296
+ x = stage(x)
297
+ if self.deep_supervision:
298
+ seg_outputs.append(self.seg_layers[idx](x))
299
+ elif idx == len(self.stages) - 1:
300
+ seg_outputs.append(self.seg_layers[-1](x))
301
+ seg_outputs = seg_outputs[::-1]
302
+ return seg_outputs if self.deep_supervision else seg_outputs[0]
303
+
304
+
305
+ class ResidualEncoderUNet(nn.Module):
306
+ def __init__(
307
+ self,
308
+ input_channels,
309
+ features_per_stage,
310
+ kernel_sizes,
311
+ strides,
312
+ n_blocks_per_stage,
313
+ num_classes,
314
+ n_conv_per_stage_decoder,
315
+ conv_bias=True,
316
+ norm_eps=1e-5,
317
+ norm_affine=True,
318
+ deep_supervision=False,
319
+ ):
320
+ super().__init__()
321
+ self.encoder = ResidualEncoder(
322
+ input_channels,
323
+ features_per_stage,
324
+ kernel_sizes,
325
+ strides,
326
+ n_blocks_per_stage,
327
+ conv_bias=conv_bias,
328
+ norm_eps=norm_eps,
329
+ norm_affine=norm_affine,
330
+ )
331
+ self.decoder = UNetDecoder(
332
+ features_per_stage,
333
+ strides,
334
+ num_classes,
335
+ n_conv_per_stage_decoder,
336
+ conv_bias=conv_bias,
337
+ norm_eps=norm_eps,
338
+ norm_affine=norm_affine,
339
+ deep_supervision=deep_supervision,
340
+ )
341
+ self.decoder.encoder = self.encoder
342
+
343
+ def forward(self, x):
344
+ return self.decoder(self.encoder(x))
modeling_pe_segmentation.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Optional, Tuple
3
+
4
+ import torch
5
+ from torch import nn
6
+ from transformers import PreTrainedModel
7
+ from transformers.utils import ModelOutput
8
+
9
+ try:
10
+ from .configuration_pe_segmentation import PulmonaryEmbolismSegmentationConfig
11
+ except ImportError:
12
+ from configuration_pe_segmentation import PulmonaryEmbolismSegmentationConfig
13
+
14
+
15
+ @dataclass
16
+ class SegmentationModelOutput(ModelOutput):
17
+ loss: Optional[torch.Tensor] = None
18
+ logits: torch.Tensor = None
19
+ deep_supervision_logits: Optional[Tuple[torch.Tensor, ...]] = None
20
+
21
+
22
+ class PulmonaryEmbolismSegmentationModel(PreTrainedModel):
23
+ config_class = PulmonaryEmbolismSegmentationConfig
24
+ base_model_prefix = "segmentation_model"
25
+ main_input_name = "pixel_values"
26
+ supports_gradient_checkpointing = False
27
+ _tied_weights_keys = []
28
+ all_tied_weights_keys = {}
29
+ _keys_to_ignore_on_load_missing = [
30
+ r"segmentation_model\..*\.all_modules\..*",
31
+ r"segmentation_model\.decoder\.encoder\..*",
32
+ ]
33
+
34
+ def __init__(self, config: PulmonaryEmbolismSegmentationConfig):
35
+ super().__init__(config)
36
+ self.segmentation_model = self._build_network(config)
37
+
38
+ @staticmethod
39
+ def _build_network(config: PulmonaryEmbolismSegmentationConfig) -> nn.Module:
40
+ try:
41
+ from .local_architecture import ResidualEncoderUNet
42
+ except ImportError:
43
+ from pulmonary_embolism_segmentation.local_architecture import ResidualEncoderUNet
44
+
45
+ return ResidualEncoderUNet(
46
+ input_channels=config.input_channels,
47
+ features_per_stage=config.features_per_stage,
48
+ kernel_sizes=config.kernel_sizes,
49
+ strides=config.strides,
50
+ n_blocks_per_stage=config.n_blocks_per_stage,
51
+ num_classes=config.num_labels,
52
+ n_conv_per_stage_decoder=config.n_conv_per_stage_decoder,
53
+ conv_bias=config.conv_bias,
54
+ norm_eps=config.norm_eps,
55
+ norm_affine=config.norm_affine,
56
+ deep_supervision=config.deep_supervision,
57
+ )
58
+
59
+ def forward(self, pixel_values: torch.Tensor, labels: Optional[torch.Tensor] = None):
60
+ outputs = self.segmentation_model(pixel_values)
61
+ if isinstance(outputs, (tuple, list)):
62
+ logits = outputs[0]
63
+ deep_supervision_logits = tuple(outputs[1:])
64
+ else:
65
+ logits = outputs
66
+ deep_supervision_logits = None
67
+
68
+ loss = None
69
+ if labels is not None:
70
+ loss = nn.functional.cross_entropy(logits, labels.long())
71
+
72
+ return SegmentationModelOutput(
73
+ loss=loss,
74
+ logits=logits,
75
+ deep_supervision_logits=deep_supervision_logits,
76
+ )
pulmonary_embolism_segmentation/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .configuration_pe_segmentation import PulmonaryEmbolismSegmentationConfig
2
+ from .inference import SlidingWindowInferenceConfig, predict_volume, predict_volume_resampled
3
+ from .modeling_pe_segmentation import PulmonaryEmbolismSegmentationModel
4
+
5
+ __all__ = [
6
+ "PulmonaryEmbolismSegmentationConfig",
7
+ "PulmonaryEmbolismSegmentationModel",
8
+ "SlidingWindowInferenceConfig",
9
+ "predict_volume",
10
+ "predict_volume_resampled",
11
+ ]
pulmonary_embolism_segmentation/configuration_pe_segmentation.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PretrainedConfig
2
+
3
+
4
+ class PulmonaryEmbolismSegmentationConfig(PretrainedConfig):
5
+ model_type = "pulmonary-embolism-segmentation"
6
+
7
+ def __init__(
8
+ self,
9
+ input_channels=1,
10
+ num_labels=2,
11
+ patch_size=None,
12
+ spacing=None,
13
+ features_per_stage=None,
14
+ kernel_sizes=None,
15
+ strides=None,
16
+ n_blocks_per_stage=None,
17
+ n_conv_per_stage_decoder=None,
18
+ conv_bias=True,
19
+ norm_eps=1e-5,
20
+ norm_affine=True,
21
+ leaky_relu_inplace=True,
22
+ deep_supervision=False,
23
+ label2id=None,
24
+ id2label=None,
25
+ ct_clip_min=-195.0,
26
+ ct_clip_max=305.0,
27
+ ct_mean=37.060203552246094,
28
+ ct_std=92.34374237060547,
29
+ **kwargs,
30
+ ):
31
+ label2id = label2id or {"background": 0, "pulmonary_embolism": 1}
32
+ id2label = id2label or {str(v): k for k, v in label2id.items()}
33
+ super().__init__(label2id=label2id, id2label=id2label, **kwargs)
34
+
35
+ self.input_channels = input_channels
36
+ self.num_labels = num_labels
37
+ self.patch_size = patch_size or [224, 320, 320]
38
+ self.spacing = spacing or [1.0, 0.7373045682907104, 0.7373045682907104]
39
+ self.features_per_stage = features_per_stage or [32, 64, 128, 256, 320, 320, 320]
40
+ self.kernel_sizes = kernel_sizes or [[3, 3, 3]] * 7
41
+ self.strides = strides or [
42
+ [1, 1, 1],
43
+ [2, 2, 2],
44
+ [2, 2, 2],
45
+ [2, 2, 2],
46
+ [2, 2, 2],
47
+ [2, 2, 2],
48
+ [1, 2, 2],
49
+ ]
50
+ self.n_blocks_per_stage = n_blocks_per_stage or [1, 3, 4, 6, 6, 6, 6]
51
+ self.n_conv_per_stage_decoder = n_conv_per_stage_decoder or [1, 1, 1, 1, 1, 1]
52
+ self.conv_bias = conv_bias
53
+ self.norm_eps = norm_eps
54
+ self.norm_affine = norm_affine
55
+ self.leaky_relu_inplace = leaky_relu_inplace
56
+ self.deep_supervision = deep_supervision
57
+
58
+ self.ct_clip_min = ct_clip_min
59
+ self.ct_clip_max = ct_clip_max
60
+ self.ct_mean = ct_mean
61
+ self.ct_std = ct_std
pulmonary_embolism_segmentation/inference.py ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from itertools import product
3
+ from math import ceil
4
+ from typing import Iterable, Optional, Sequence, Tuple
5
+
6
+ import numpy as np
7
+ import torch
8
+ import torch.nn.functional as F
9
+ from scipy.ndimage import binary_fill_holes, gaussian_filter, map_coordinates
10
+ from skimage.transform import resize
11
+
12
+
13
+ ANISO_THRESHOLD = 3.0
14
+
15
+
16
+ def normalize_ct(volume: np.ndarray, config) -> np.ndarray:
17
+ volume = volume.astype(np.float32, copy=False)
18
+ volume = np.clip(volume, config.ct_clip_min, config.ct_clip_max)
19
+ return (volume - config.ct_mean) / config.ct_std
20
+
21
+
22
+ def compute_new_shape(old_shape: Sequence[int], old_spacing: Sequence[float], new_spacing: Sequence[float]) -> Tuple[int, ...]:
23
+ return tuple(int(round(spacing / target_spacing * size)) for size, spacing, target_spacing in zip(old_shape, old_spacing, new_spacing))
24
+
25
+
26
+ def get_lowres_axis(spacing: Sequence[float]) -> Optional[int]:
27
+ spacing = np.asarray(spacing)
28
+ axes = np.where(np.max(spacing) / spacing == 1)[0]
29
+ return int(axes[0]) if len(axes) == 1 else None
30
+
31
+
32
+ def determine_separate_z_axis(current_spacing: Sequence[float], new_spacing: Sequence[float]) -> Optional[int]:
33
+ current_spacing = np.asarray(current_spacing)
34
+ new_spacing = np.asarray(new_spacing)
35
+ if np.max(current_spacing) / np.min(current_spacing) > ANISO_THRESHOLD:
36
+ return get_lowres_axis(current_spacing)
37
+ if np.max(new_spacing) / np.min(new_spacing) > ANISO_THRESHOLD:
38
+ return get_lowres_axis(new_spacing)
39
+ return None
40
+
41
+
42
+ def create_nonzero_mask(data: np.ndarray) -> np.ndarray:
43
+ mask = data[0] != 0
44
+ for channel in range(1, data.shape[0]):
45
+ mask |= data[channel] != 0
46
+ return binary_fill_holes(mask)
47
+
48
+
49
+ def crop_to_nonzero(data: np.ndarray) -> Tuple[np.ndarray, Tuple[Tuple[int, int], ...]]:
50
+ mask = create_nonzero_mask(data)
51
+ coords = np.array(np.where(mask))
52
+ if coords.size == 0:
53
+ bbox = tuple((0, dim) for dim in data.shape[1:])
54
+ else:
55
+ mins = coords.min(axis=1)
56
+ maxs = coords.max(axis=1) + 1
57
+ bbox = tuple((int(lo), int(hi)) for lo, hi in zip(mins, maxs))
58
+ slicer = (slice(None),) + tuple(slice(lo, hi) for lo, hi in bbox)
59
+ return data[slicer], bbox
60
+
61
+
62
+ def insert_crop(segmentation: np.ndarray, bbox: Sequence[Tuple[int, int]], original_shape: Sequence[int]) -> np.ndarray:
63
+ result = np.zeros(original_shape, dtype=segmentation.dtype)
64
+ slicer = tuple(slice(lo, hi) for lo, hi in bbox)
65
+ result[slicer] = segmentation
66
+ return result
67
+
68
+
69
+ def _resize_segmentation(segmentation: np.ndarray, new_shape: Sequence[int], order: int) -> np.ndarray:
70
+ if order == 0:
71
+ return resize(segmentation, new_shape, order=0, mode="edge", anti_aliasing=False, preserve_range=True)
72
+ result = np.zeros(new_shape, dtype=segmentation.dtype)
73
+ for label in np.unique(segmentation):
74
+ resized = resize(
75
+ (segmentation == label).astype(float),
76
+ new_shape,
77
+ order=order,
78
+ mode="edge",
79
+ anti_aliasing=False,
80
+ preserve_range=True,
81
+ )
82
+ result[resized >= 0.5] = label
83
+ return result
84
+
85
+
86
+ def _resize_channel(channel: np.ndarray, new_shape: Sequence[int], is_seg: bool, order: int) -> np.ndarray:
87
+ if is_seg:
88
+ return _resize_segmentation(channel, new_shape, order)
89
+ return resize(channel, new_shape, order=order, mode="edge", anti_aliasing=False, preserve_range=True)
90
+
91
+
92
+ def resample_nnunet(
93
+ data: np.ndarray,
94
+ new_shape: Sequence[int],
95
+ current_spacing: Sequence[float],
96
+ new_spacing: Sequence[float],
97
+ is_seg: bool = False,
98
+ order: int = 3,
99
+ order_z: int = 0,
100
+ ) -> np.ndarray:
101
+ if data is None:
102
+ return None
103
+ if isinstance(data, torch.Tensor):
104
+ data = data.cpu().numpy()
105
+ assert data.ndim == 4, "data must be channel-first: (c, z, y, x)"
106
+
107
+ old_shape = tuple(data.shape[1:])
108
+ new_shape = tuple(int(i) for i in new_shape)
109
+ if old_shape == new_shape:
110
+ return data
111
+
112
+ axis = determine_separate_z_axis(current_spacing, new_spacing)
113
+ output = np.zeros((data.shape[0], *new_shape), dtype=data.dtype)
114
+ data_float = data.astype(float, copy=False)
115
+
116
+ if axis is None:
117
+ for c in range(data.shape[0]):
118
+ output[c] = _resize_channel(data_float[c], new_shape, is_seg, order)
119
+ return output
120
+
121
+ shape = np.array(old_shape)
122
+ new_shape_array = np.array(new_shape)
123
+ if axis == 0:
124
+ plane_shape = new_shape_array[1:]
125
+ elif axis == 1:
126
+ plane_shape = new_shape_array[[0, 2]]
127
+ else:
128
+ plane_shape = new_shape_array[:-1]
129
+
130
+ for c in range(data.shape[0]):
131
+ intermediate_shape = new_shape_array.copy()
132
+ intermediate_shape[axis] = shape[axis]
133
+ intermediate = np.zeros(tuple(intermediate_shape), dtype=float)
134
+ for idx in range(shape[axis]):
135
+ if axis == 0:
136
+ intermediate[idx] = _resize_channel(data_float[c, idx], plane_shape, is_seg, order)
137
+ elif axis == 1:
138
+ intermediate[:, idx] = _resize_channel(data_float[c, :, idx], plane_shape, is_seg, order)
139
+ else:
140
+ intermediate[:, :, idx] = _resize_channel(data_float[c, :, :, idx], plane_shape, is_seg, order)
141
+
142
+ if shape[axis] == new_shape_array[axis]:
143
+ output[c] = intermediate
144
+ continue
145
+
146
+ rows, cols, depth = new_shape
147
+ orig_rows, orig_cols, orig_depth = intermediate.shape
148
+ row_scale = orig_rows / rows
149
+ col_scale = orig_cols / cols
150
+ depth_scale = orig_depth / depth
151
+ map_rows, map_cols, map_depth = np.mgrid[:rows, :cols, :depth]
152
+ coord_map = np.array(
153
+ [
154
+ row_scale * (map_rows + 0.5) - 0.5,
155
+ col_scale * (map_cols + 0.5) - 0.5,
156
+ depth_scale * (map_depth + 0.5) - 0.5,
157
+ ]
158
+ )
159
+ if not is_seg or order_z == 0:
160
+ output[c] = map_coordinates(intermediate, coord_map, order=order_z, mode="nearest")
161
+ else:
162
+ for label in np.unique(intermediate):
163
+ output[c][
164
+ np.round(map_coordinates((intermediate == label).astype(float), coord_map, order=order_z, mode="nearest")) > 0.5
165
+ ] = label
166
+ return output
167
+
168
+
169
+ def compute_steps(image_size: Sequence[int], tile_size: Sequence[int], overlap: float) -> Tuple[Tuple[int, ...], ...]:
170
+ steps = []
171
+ for image_dim, tile_dim in zip(image_size, tile_size):
172
+ if image_dim <= tile_dim:
173
+ steps.append((0,))
174
+ continue
175
+ target_step = max(1, int(tile_dim * (1.0 - overlap)))
176
+ num_steps = int(ceil((image_dim - tile_dim) / target_step)) + 1
177
+ actual_step = (image_dim - tile_dim) / max(1, num_steps - 1)
178
+ steps.append(tuple(int(round(actual_step * i)) for i in range(num_steps)))
179
+ return tuple(steps)
180
+
181
+
182
+ def gaussian_importance_map(tile_size: Sequence[int], sigma_scale: float = 1.0 / 8.0) -> torch.Tensor:
183
+ tmp = np.zeros(tuple(tile_size), dtype=np.float32)
184
+ tmp[tuple(size // 2 for size in tile_size)] = 1
185
+ sigmas = [size * sigma_scale for size in tile_size]
186
+ weight = gaussian_filter(tmp, sigmas, 0, mode="constant", cval=0)
187
+ weight /= np.max(weight)
188
+ tensor = torch.from_numpy(weight.astype(np.float32, copy=False))
189
+ tensor[tensor == 0] = torch.min(tensor[tensor != 0])
190
+ return tensor
191
+
192
+
193
+ def pad_to_tile_size(volume: torch.Tensor, tile_size: Sequence[int]) -> Tuple[torch.Tensor, Tuple[slice, slice, slice]]:
194
+ spatial = volume.shape[-3:]
195
+ pad_after = [max(tile - dim, 0) for dim, tile in zip(spatial, tile_size)]
196
+ if any(pad_after):
197
+ volume = F.pad(volume, (0, pad_after[2], 0, pad_after[1], 0, pad_after[0]))
198
+ crop = tuple(slice(0, dim) for dim in spatial)
199
+ return volume, crop
200
+
201
+
202
+ def iter_tiles(image_size: Sequence[int], tile_size: Sequence[int], overlap: float) -> Iterable[Tuple[slice, slice, slice]]:
203
+ for z, y, x in product(*compute_steps(image_size, tile_size, overlap)):
204
+ yield (
205
+ slice(z, z + tile_size[0]),
206
+ slice(y, y + tile_size[1]),
207
+ slice(x, x + tile_size[2]),
208
+ )
209
+
210
+
211
+ def shrink_tile(tile_size: Sequence[int], min_tile_size: Sequence[int]) -> Optional[Tuple[int, int, int]]:
212
+ divisibility = (32, 64, 64)
213
+ candidates = []
214
+ for axis, value in enumerate(tile_size):
215
+ if value > min_tile_size[axis]:
216
+ next_tile = list(tile_size)
217
+ step = divisibility[axis]
218
+ next_value = max(min_tile_size[axis], int(value * 0.75) // step * step)
219
+ next_tile[axis] = next_value
220
+ candidates.append(tuple(next_tile))
221
+ if not candidates:
222
+ return None
223
+ return min(candidates, key=np.prod)
224
+
225
+
226
+ def normalize_tile_size(
227
+ image_size: Sequence[int],
228
+ requested_tile_size: Sequence[int],
229
+ divisibility: Sequence[int] = (32, 64, 64),
230
+ ) -> Tuple[int, int, int]:
231
+ tile_size = []
232
+ for image_dim, requested_dim, divisor in zip(image_size, requested_tile_size, divisibility):
233
+ if requested_dim % divisor != 0:
234
+ raise ValueError(
235
+ f"Tile size {tuple(requested_tile_size)} is incompatible with network strides. "
236
+ f"Expected divisibility by {tuple(divisibility)} for z,y,x."
237
+ )
238
+ if image_dim >= requested_dim:
239
+ tile_size.append(requested_dim)
240
+ else:
241
+ tile_size.append(int(ceil(image_dim / divisor) * divisor))
242
+ return tuple(tile_size)
243
+
244
+
245
+ @dataclass
246
+ class SlidingWindowInferenceConfig:
247
+ tile_size: Tuple[int, int, int] = (128, 256, 256)
248
+ min_tile_size: Tuple[int, int, int] = (64, 128, 128)
249
+ overlap: float = 0.5
250
+ use_amp: bool = True
251
+ device: Optional[str] = None
252
+ empty_cache_between_tiles: bool = False
253
+
254
+
255
+ def _predict_with_tile_size(
256
+ model,
257
+ volume: torch.Tensor,
258
+ tile_size: Sequence[int],
259
+ overlap: float,
260
+ device: torch.device,
261
+ use_amp: bool,
262
+ empty_cache_between_tiles: bool,
263
+ ) -> torch.Tensor:
264
+ volume, crop = pad_to_tile_size(volume, tile_size)
265
+ image_size = volume.shape[-3:]
266
+ num_classes = model.config.num_labels
267
+
268
+ logits_sum = torch.zeros((num_classes, *image_size), dtype=torch.float32, device="cpu")
269
+ weight_sum = torch.zeros(image_size, dtype=torch.float32, device="cpu")
270
+ importance = gaussian_importance_map(tile_size).to(device)
271
+
272
+ model.eval()
273
+ with torch.inference_mode():
274
+ for tile in iter_tiles(image_size, tile_size, overlap):
275
+ patch = volume[(slice(None), slice(None), *tile)].to(device, non_blocking=True)
276
+ amp_enabled = use_amp and device.type == "cuda"
277
+ with torch.autocast(device_type=device.type, enabled=amp_enabled):
278
+ logits = model(patch).logits[0]
279
+ weighted_logits = (logits.float() * importance).cpu()
280
+ logits_sum[(slice(None), *tile)] += weighted_logits
281
+ weight_sum[tile] += importance.cpu()
282
+ del patch, logits, weighted_logits
283
+ if empty_cache_between_tiles and device.type == "cuda":
284
+ torch.cuda.empty_cache()
285
+
286
+ logits_sum /= torch.clamp(weight_sum.unsqueeze(0), min=1e-6)
287
+ return logits_sum[(slice(None), *crop)]
288
+
289
+
290
+ def predict_logits(model, volume: np.ndarray, inference_config: Optional[SlidingWindowInferenceConfig] = None) -> torch.Tensor:
291
+ inference_config = inference_config or SlidingWindowInferenceConfig()
292
+ device = torch.device(inference_config.device or ("cuda" if torch.cuda.is_available() else "cpu"))
293
+ model.to(device)
294
+
295
+ tensor = torch.from_numpy(volume.astype(np.float32, copy=False)[None, None])
296
+ tile_size = normalize_tile_size(tensor.shape[-3:], inference_config.tile_size)
297
+
298
+ while True:
299
+ try:
300
+ return _predict_with_tile_size(
301
+ model=model,
302
+ volume=tensor,
303
+ tile_size=tile_size,
304
+ overlap=inference_config.overlap,
305
+ device=device,
306
+ use_amp=inference_config.use_amp,
307
+ empty_cache_between_tiles=inference_config.empty_cache_between_tiles,
308
+ )
309
+ except torch.cuda.OutOfMemoryError:
310
+ if device.type != "cuda":
311
+ raise
312
+ torch.cuda.empty_cache()
313
+ next_tile_size = shrink_tile(tile_size, inference_config.min_tile_size)
314
+ if next_tile_size is None:
315
+ raise RuntimeError(
316
+ f"CUDA out of memory at minimum tile size {tile_size}. "
317
+ "Use CPU inference or lower min_tile_size."
318
+ )
319
+ tile_size = next_tile_size
320
+
321
+
322
+ def predict_volume(model, volume: np.ndarray, inference_config: Optional[SlidingWindowInferenceConfig] = None) -> np.ndarray:
323
+ normalized = normalize_ct(volume, model.config)
324
+ logits = predict_logits(model, normalized, inference_config)
325
+ return torch.argmax(logits, dim=0).numpy().astype(np.uint8)
326
+
327
+
328
+ def predict_volume_resampled(
329
+ model,
330
+ volume: np.ndarray,
331
+ source_spacing_zyx: Sequence[float],
332
+ inference_config: Optional[SlidingWindowInferenceConfig] = None,
333
+ ) -> np.ndarray:
334
+ target_spacing_zyx = tuple(model.config.spacing)
335
+ original_shape = volume.shape
336
+ data = volume.astype(np.float32, copy=False)[None]
337
+ cropped, bbox = crop_to_nonzero(data)
338
+ cropped_shape = cropped.shape[1:]
339
+ normalized = cropped.copy()
340
+ normalized[0] = normalize_ct(normalized[0], model.config)
341
+ resampled_shape = compute_new_shape(cropped_shape, source_spacing_zyx, target_spacing_zyx)
342
+ resampled = resample_nnunet(
343
+ normalized,
344
+ new_shape=resampled_shape,
345
+ current_spacing=source_spacing_zyx,
346
+ new_spacing=target_spacing_zyx,
347
+ is_seg=False,
348
+ order=3,
349
+ order_z=0,
350
+ )
351
+ logits = predict_logits(model, resampled[0], inference_config)
352
+ logits_cropped = resample_nnunet(
353
+ logits,
354
+ new_shape=cropped_shape,
355
+ current_spacing=target_spacing_zyx,
356
+ new_spacing=source_spacing_zyx,
357
+ is_seg=False,
358
+ order=1,
359
+ order_z=0,
360
+ )
361
+ segmentation_cropped = np.argmax(logits_cropped, axis=0).astype(np.uint8, copy=False)
362
+ return insert_crop(segmentation_cropped, bbox, original_shape)
pulmonary_embolism_segmentation/local_architecture.py ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Sequence
2
+
3
+ import torch
4
+ from torch import nn
5
+
6
+
7
+ def _as_tuple(value):
8
+ if isinstance(value, int):
9
+ return (value, value, value)
10
+ return tuple(value)
11
+
12
+
13
+ class ConvDropoutNormReLU(nn.Module):
14
+ def __init__(
15
+ self,
16
+ input_channels,
17
+ output_channels,
18
+ kernel_size,
19
+ stride,
20
+ conv_bias=True,
21
+ norm_eps=1e-5,
22
+ norm_affine=True,
23
+ with_nonlin=True,
24
+ ):
25
+ super().__init__()
26
+ kernel_size = _as_tuple(kernel_size)
27
+ stride = _as_tuple(stride)
28
+ padding = tuple((k - 1) // 2 for k in kernel_size)
29
+ self.conv = nn.Conv3d(input_channels, output_channels, kernel_size, stride, padding, bias=conv_bias)
30
+ self.norm = nn.InstanceNorm3d(output_channels, eps=norm_eps, affine=norm_affine)
31
+ modules = [self.conv, self.norm]
32
+ if with_nonlin:
33
+ self.nonlin = nn.LeakyReLU(negative_slope=0.01, inplace=True)
34
+ modules.append(self.nonlin)
35
+ self.all_modules = nn.Sequential(*modules)
36
+
37
+ def forward(self, x):
38
+ return self.all_modules(x)
39
+
40
+
41
+ class StackedConvBlocks(nn.Module):
42
+ def __init__(
43
+ self,
44
+ num_convs,
45
+ input_channels,
46
+ output_channels,
47
+ kernel_size,
48
+ initial_stride,
49
+ conv_bias=True,
50
+ norm_eps=1e-5,
51
+ norm_affine=True,
52
+ ):
53
+ super().__init__()
54
+ if not isinstance(output_channels, (tuple, list)):
55
+ output_channels = [output_channels] * num_convs
56
+ blocks = [
57
+ ConvDropoutNormReLU(
58
+ input_channels,
59
+ output_channels[0],
60
+ kernel_size,
61
+ initial_stride,
62
+ conv_bias=conv_bias,
63
+ norm_eps=norm_eps,
64
+ norm_affine=norm_affine,
65
+ with_nonlin=True,
66
+ )
67
+ ]
68
+ for idx in range(1, num_convs):
69
+ blocks.append(
70
+ ConvDropoutNormReLU(
71
+ output_channels[idx - 1],
72
+ output_channels[idx],
73
+ kernel_size,
74
+ 1,
75
+ conv_bias=conv_bias,
76
+ norm_eps=norm_eps,
77
+ norm_affine=norm_affine,
78
+ with_nonlin=True,
79
+ )
80
+ )
81
+ self.convs = nn.Sequential(*blocks)
82
+
83
+ def forward(self, x):
84
+ return self.convs(x)
85
+
86
+
87
+ class BasicBlockD(nn.Module):
88
+ def __init__(
89
+ self,
90
+ input_channels,
91
+ output_channels,
92
+ kernel_size,
93
+ stride,
94
+ conv_bias=True,
95
+ norm_eps=1e-5,
96
+ norm_affine=True,
97
+ ):
98
+ super().__init__()
99
+ stride = _as_tuple(stride)
100
+ self.conv1 = ConvDropoutNormReLU(
101
+ input_channels,
102
+ output_channels,
103
+ kernel_size,
104
+ stride,
105
+ conv_bias=conv_bias,
106
+ norm_eps=norm_eps,
107
+ norm_affine=norm_affine,
108
+ with_nonlin=True,
109
+ )
110
+ self.conv2 = ConvDropoutNormReLU(
111
+ output_channels,
112
+ output_channels,
113
+ kernel_size,
114
+ 1,
115
+ conv_bias=conv_bias,
116
+ norm_eps=norm_eps,
117
+ norm_affine=norm_affine,
118
+ with_nonlin=False,
119
+ )
120
+ self.nonlin2 = nn.LeakyReLU(negative_slope=0.01, inplace=True)
121
+
122
+ has_stride = any(s != 1 for s in stride)
123
+ requires_projection = input_channels != output_channels
124
+ if has_stride or requires_projection:
125
+ ops = []
126
+ if has_stride:
127
+ ops.append(nn.AvgPool3d(kernel_size=stride, stride=stride))
128
+ if requires_projection:
129
+ ops.append(
130
+ ConvDropoutNormReLU(
131
+ input_channels,
132
+ output_channels,
133
+ 1,
134
+ 1,
135
+ conv_bias=False,
136
+ norm_eps=norm_eps,
137
+ norm_affine=norm_affine,
138
+ with_nonlin=False,
139
+ )
140
+ )
141
+ self.skip = nn.Sequential(*ops)
142
+ else:
143
+ self.skip = nn.Identity()
144
+
145
+ def forward(self, x):
146
+ return self.nonlin2(self.conv2(self.conv1(x)) + self.skip(x))
147
+
148
+
149
+ class StackedResidualBlocks(nn.Module):
150
+ def __init__(
151
+ self,
152
+ n_blocks,
153
+ input_channels,
154
+ output_channels,
155
+ kernel_size,
156
+ initial_stride,
157
+ conv_bias=True,
158
+ norm_eps=1e-5,
159
+ norm_affine=True,
160
+ ):
161
+ super().__init__()
162
+ blocks = [
163
+ BasicBlockD(
164
+ input_channels,
165
+ output_channels,
166
+ kernel_size,
167
+ initial_stride,
168
+ conv_bias=conv_bias,
169
+ norm_eps=norm_eps,
170
+ norm_affine=norm_affine,
171
+ )
172
+ ]
173
+ for _ in range(1, n_blocks):
174
+ blocks.append(
175
+ BasicBlockD(
176
+ output_channels,
177
+ output_channels,
178
+ kernel_size,
179
+ 1,
180
+ conv_bias=conv_bias,
181
+ norm_eps=norm_eps,
182
+ norm_affine=norm_affine,
183
+ )
184
+ )
185
+ self.blocks = nn.Sequential(*blocks)
186
+
187
+ def forward(self, x):
188
+ return self.blocks(x)
189
+
190
+
191
+ class ResidualEncoder(nn.Module):
192
+ def __init__(
193
+ self,
194
+ input_channels,
195
+ features_per_stage,
196
+ kernel_sizes,
197
+ strides,
198
+ n_blocks_per_stage,
199
+ conv_bias=True,
200
+ norm_eps=1e-5,
201
+ norm_affine=True,
202
+ ):
203
+ super().__init__()
204
+ self.stem = StackedConvBlocks(
205
+ 1,
206
+ input_channels,
207
+ features_per_stage[0],
208
+ kernel_sizes[0],
209
+ 1,
210
+ conv_bias=conv_bias,
211
+ norm_eps=norm_eps,
212
+ norm_affine=norm_affine,
213
+ )
214
+ input_channels = features_per_stage[0]
215
+ stages = []
216
+ for idx, output_channels in enumerate(features_per_stage):
217
+ stages.append(
218
+ StackedResidualBlocks(
219
+ n_blocks_per_stage[idx],
220
+ input_channels,
221
+ output_channels,
222
+ kernel_sizes[idx],
223
+ strides[idx],
224
+ conv_bias=conv_bias,
225
+ norm_eps=norm_eps,
226
+ norm_affine=norm_affine,
227
+ )
228
+ )
229
+ input_channels = output_channels
230
+ self.stages = nn.Sequential(*stages)
231
+
232
+ def forward(self, x):
233
+ x = self.stem(x)
234
+ skips = []
235
+ for stage in self.stages:
236
+ x = stage(x)
237
+ skips.append(x)
238
+ return skips
239
+
240
+
241
+ class UNetDecoder(nn.Module):
242
+ def __init__(
243
+ self,
244
+ features_per_stage: Sequence[int],
245
+ strides: Sequence[Sequence[int]],
246
+ num_classes: int,
247
+ n_conv_per_stage_decoder: Sequence[int],
248
+ conv_bias=True,
249
+ norm_eps=1e-5,
250
+ norm_affine=True,
251
+ deep_supervision=False,
252
+ ):
253
+ super().__init__()
254
+ self.deep_supervision = deep_supervision
255
+ self.encoder = nn.Identity()
256
+ encoder_channels = list(features_per_stage)
257
+ decoder_channels = list(features_per_stage[:-1][::-1])
258
+ bottleneck_channels = features_per_stage[-1]
259
+ transpose_strides = list(strides[1:][::-1])
260
+
261
+ self.stages = nn.ModuleList()
262
+ self.transpconvs = nn.ModuleList()
263
+ self.seg_layers = nn.ModuleList()
264
+
265
+ input_channels = bottleneck_channels
266
+ for idx, output_channels in enumerate(decoder_channels):
267
+ self.transpconvs.append(
268
+ nn.ConvTranspose3d(
269
+ input_channels,
270
+ output_channels,
271
+ kernel_size=_as_tuple(transpose_strides[idx]),
272
+ stride=_as_tuple(transpose_strides[idx]),
273
+ )
274
+ )
275
+ self.stages.append(
276
+ StackedConvBlocks(
277
+ n_conv_per_stage_decoder[idx],
278
+ output_channels + encoder_channels[-(idx + 2)],
279
+ output_channels,
280
+ 3,
281
+ 1,
282
+ conv_bias=conv_bias,
283
+ norm_eps=norm_eps,
284
+ norm_affine=norm_affine,
285
+ )
286
+ )
287
+ self.seg_layers.append(nn.Conv3d(output_channels, num_classes, 1, 1, 0))
288
+ input_channels = output_channels
289
+
290
+ def forward(self, skips):
291
+ x = skips[-1]
292
+ seg_outputs = []
293
+ for idx, stage in enumerate(self.stages):
294
+ x = self.transpconvs[idx](x)
295
+ x = torch.cat((x, skips[-(idx + 2)]), dim=1)
296
+ x = stage(x)
297
+ if self.deep_supervision:
298
+ seg_outputs.append(self.seg_layers[idx](x))
299
+ elif idx == len(self.stages) - 1:
300
+ seg_outputs.append(self.seg_layers[-1](x))
301
+ seg_outputs = seg_outputs[::-1]
302
+ return seg_outputs if self.deep_supervision else seg_outputs[0]
303
+
304
+
305
+ class ResidualEncoderUNet(nn.Module):
306
+ def __init__(
307
+ self,
308
+ input_channels,
309
+ features_per_stage,
310
+ kernel_sizes,
311
+ strides,
312
+ n_blocks_per_stage,
313
+ num_classes,
314
+ n_conv_per_stage_decoder,
315
+ conv_bias=True,
316
+ norm_eps=1e-5,
317
+ norm_affine=True,
318
+ deep_supervision=False,
319
+ ):
320
+ super().__init__()
321
+ self.encoder = ResidualEncoder(
322
+ input_channels,
323
+ features_per_stage,
324
+ kernel_sizes,
325
+ strides,
326
+ n_blocks_per_stage,
327
+ conv_bias=conv_bias,
328
+ norm_eps=norm_eps,
329
+ norm_affine=norm_affine,
330
+ )
331
+ self.decoder = UNetDecoder(
332
+ features_per_stage,
333
+ strides,
334
+ num_classes,
335
+ n_conv_per_stage_decoder,
336
+ conv_bias=conv_bias,
337
+ norm_eps=norm_eps,
338
+ norm_affine=norm_affine,
339
+ deep_supervision=deep_supervision,
340
+ )
341
+ self.decoder.encoder = self.encoder
342
+
343
+ def forward(self, x):
344
+ return self.decoder(self.encoder(x))
pulmonary_embolism_segmentation/modeling_pe_segmentation.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Optional, Tuple
3
+
4
+ import torch
5
+ from torch import nn
6
+ from transformers import PreTrainedModel
7
+ from transformers.utils import ModelOutput
8
+
9
+ from .configuration_pe_segmentation import PulmonaryEmbolismSegmentationConfig
10
+
11
+
12
+ @dataclass
13
+ class SegmentationModelOutput(ModelOutput):
14
+ loss: Optional[torch.Tensor] = None
15
+ logits: torch.Tensor = None
16
+ deep_supervision_logits: Optional[Tuple[torch.Tensor, ...]] = None
17
+
18
+
19
+ class PulmonaryEmbolismSegmentationModel(PreTrainedModel):
20
+ config_class = PulmonaryEmbolismSegmentationConfig
21
+ base_model_prefix = "segmentation_model"
22
+ main_input_name = "pixel_values"
23
+ supports_gradient_checkpointing = False
24
+ _tied_weights_keys = []
25
+ all_tied_weights_keys = {}
26
+ _keys_to_ignore_on_load_missing = [
27
+ r"segmentation_model\..*\.all_modules\..*",
28
+ r"segmentation_model\.decoder\.encoder\..*",
29
+ ]
30
+
31
+ def __init__(self, config: PulmonaryEmbolismSegmentationConfig):
32
+ super().__init__(config)
33
+ self.segmentation_model = self._build_network(config)
34
+
35
+ @staticmethod
36
+ def _build_network(config: PulmonaryEmbolismSegmentationConfig) -> nn.Module:
37
+ from .local_architecture import ResidualEncoderUNet
38
+
39
+ return ResidualEncoderUNet(
40
+ input_channels=config.input_channels,
41
+ features_per_stage=config.features_per_stage,
42
+ kernel_sizes=config.kernel_sizes,
43
+ strides=config.strides,
44
+ n_blocks_per_stage=config.n_blocks_per_stage,
45
+ num_classes=config.num_labels,
46
+ n_conv_per_stage_decoder=config.n_conv_per_stage_decoder,
47
+ conv_bias=config.conv_bias,
48
+ norm_eps=config.norm_eps,
49
+ norm_affine=config.norm_affine,
50
+ deep_supervision=config.deep_supervision,
51
+ )
52
+
53
+ def forward(self, pixel_values: torch.Tensor, labels: Optional[torch.Tensor] = None):
54
+ outputs = self.segmentation_model(pixel_values)
55
+ if isinstance(outputs, (tuple, list)):
56
+ logits = outputs[0]
57
+ deep_supervision_logits = tuple(outputs[1:])
58
+ else:
59
+ logits = outputs
60
+ deep_supervision_logits = None
61
+
62
+ loss = None
63
+ if labels is not None:
64
+ loss = nn.functional.cross_entropy(logits, labels.long())
65
+
66
+ return SegmentationModelOutput(
67
+ loss=loss,
68
+ logits=logits,
69
+ deep_supervision_logits=deep_supervision_logits,
70
+ )
pytorch_model.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2abdbd407070c70df1b83271c25d2812770bb1cc233c6d72f1df1c5769e42188
3
+ size 566265379