Image Segmentation
Transformers
PyTorch
pulmonary-embolism-segmentation
feature-extraction
ct-pulmonary-angiography
medical-imaging
ct
pulmonary-embolism
segmentation
nnunet
custom_code
Instructions to use yzluka/PulmonaryEmbolismSegmentation with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use yzluka/PulmonaryEmbolismSegmentation with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-segmentation", model="yzluka/PulmonaryEmbolismSegmentation", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("yzluka/PulmonaryEmbolismSegmentation", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Add portable PE segmentation model
Browse files- .gitattributes +1 -35
- README.md +140 -0
- config.json +34 -0
- configuration_pe_segmentation.py +61 -0
- local_architecture.py +344 -0
- modeling_pe_segmentation.py +76 -0
- pulmonary_embolism_segmentation/__init__.py +11 -0
- pulmonary_embolism_segmentation/configuration_pe_segmentation.py +61 -0
- pulmonary_embolism_segmentation/inference.py +362 -0
- pulmonary_embolism_segmentation/local_architecture.py +344 -0
- pulmonary_embolism_segmentation/modeling_pe_segmentation.py +70 -0
- pytorch_model.bin +3 -0
.gitattributes
CHANGED
|
@@ -1,35 +1 @@
|
|
| 1 |
-
|
| 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
|