mrid-python 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mrid_python-0.1.3/PKG-INFO +140 -0
- mrid_python-0.1.3/README.md +126 -0
- mrid_python-0.1.3/mrid/__init__.py +5 -0
- mrid_python-0.1.3/mrid/atlas/MNI152/__init__.py +80 -0
- mrid_python-0.1.3/mrid/atlas/SRI24/__init__.py +77 -0
- mrid_python-0.1.3/mrid/atlas/__init__.py +7 -0
- mrid_python-0.1.3/mrid/loading/__init__.py +1 -0
- mrid_python-0.1.3/mrid/loading/convert.py +68 -0
- mrid_python-0.1.3/mrid/preprocessing/__init__.py +12 -0
- mrid_python-0.1.3/mrid/preprocessing/bias_field_correction.py +28 -0
- mrid_python-0.1.3/mrid/preprocessing/cropping.py +36 -0
- mrid_python-0.1.3/mrid/preprocessing/registration.py +251 -0
- mrid_python-0.1.3/mrid/preprocessing/skullstripping.py +185 -0
- mrid_python-0.1.3/mrid/study.py +442 -0
- mrid_python-0.1.3/mrid/utils/__init__.py +3 -0
- mrid_python-0.1.3/mrid/utils/dcm2niix.py +90 -0
- mrid_python-0.1.3/mrid/utils/dicom_uid_fixer.py +86 -0
- mrid_python-0.1.3/mrid/utils/plotting.py +102 -0
- mrid_python-0.1.3/mrid/utils/python_utils.py +48 -0
- mrid_python-0.1.3/mrid/utils/stl_utils.py +70 -0
- mrid_python-0.1.3/mrid/utils/torch_utils.py +16 -0
- mrid_python-0.1.3/mrid_python.egg-info/PKG-INFO +140 -0
- mrid_python-0.1.3/mrid_python.egg-info/SOURCES.txt +30 -0
- mrid_python-0.1.3/mrid_python.egg-info/dependency_links.txt +1 -0
- mrid_python-0.1.3/mrid_python.egg-info/requires.txt +2 -0
- mrid_python-0.1.3/mrid_python.egg-info/top_level.txt +3 -0
- mrid_python-0.1.3/pyproject.toml +45 -0
- mrid_python-0.1.3/setup.cfg +4 -0
- mrid_python-0.1.3/tests/test_loading.py +82 -0
- mrid_python-0.1.3/tests/test_preprocessing.py +43 -0
- mrid_python-0.1.3/tests/test_study.py +136 -0
- mrid_python-0.1.3/tests/test_utils.py +16 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mrid-python
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Tools for working with 3D medical images and segmentations - registration, brain skull-stripping, etc.
|
|
5
|
+
Author-email: Ivan Nikishev <nkshv2@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/inikishev/mrid
|
|
7
|
+
Project-URL: Repository, https://github.com/inikishev/mrid
|
|
8
|
+
Project-URL: Issues, https://github.com/inikishev/mrid/isses
|
|
9
|
+
Keywords: MRI,medical imaging
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: SimpleITK
|
|
14
|
+
|
|
15
|
+
<h1 align='center'>mrid</h1>
|
|
16
|
+
|
|
17
|
+
mrid is a library for preprocessing of 3D images, particularly medical images.
|
|
18
|
+
|
|
19
|
+
`mrid.Study` provides a convenient way to work with scans and provides all the tools (skull-stripping, registration, etc). See [this notebook](https://nbviewer.org/github/inikishev/mrid/blob/main/mrid_tutorial.ipynb) for an example of how to use it ([Google Colab](https://colab.research.google.com/drive/1d1MWPaGAqfK6q_5879LpwwxPCB-tbExe?usp=sharing)).
|
|
20
|
+
|
|
21
|
+
All methods are also available as separate functions, see below.
|
|
22
|
+
|
|
23
|
+
## Registering a scan
|
|
24
|
+
|
|
25
|
+
All methods accept scans that can be a path to NIfTI, DICOM dir, sitk Image, numpy array or torch tensor, and return sitk.Image format.
|
|
26
|
+
|
|
27
|
+
For registering you need to have SimpleITK-SimpleElastix installed (<https://pypi.org/project/SimpleITK-SimpleElastix/>)
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
registered = mrid.register(moving, fixed) # returns sitk.Image
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
mrid includes downloaders for commonly used templates - MNI152 and SRI24: `mrid.get_mni152`, and `mrid.get_sri24`.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
t1_sri = mrid.register(t1, mrid.get_sri24("T1")) # returns sitk.Image
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
note you can always convert anything to numpy array using `mrid.tonumpy(src)`:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
t1_sri_np = mrid.tonumpy(t1_sri) # np.array
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Registering multiple scans
|
|
46
|
+
|
|
47
|
+
If you have multiple scans that are already aligned, you can register one of them and use the same transformation to transform the rest. This can also be used to register segmentations.
|
|
48
|
+
|
|
49
|
+
In this example we register T1 to SRI24 T1 template, and transform the rest
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
data = {"t1": "t1c.nii.gz", "t2": "t2w.nii.gz", "flair": "t2f.nii.gz"}
|
|
53
|
+
registered_data = mrid.register_D(data, key="t1", to=mrid.get_sri24("T1"))
|
|
54
|
+
# dict of sitk.Image registered to SRI24
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If you have multiple scans that are not aligned (e.g. have different shapes, orientations), you can use `register_each`. This registers one scan to the target, and then registers all other scans to the first scan. In the example it registers T1 to SRI24, and then registers T2 and FLAIR to T1.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
data = {"t1": "t1c.nii.gz", "t2": "t2w.nii.gz", "flair": "t2f.nii.gz"}
|
|
61
|
+
registered_data = mrid.register_each(data, key="t1", to=mrid.get_sri24("T1"))
|
|
62
|
+
# dict of sitk.Image registered to SRI24
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Inverse transform
|
|
66
|
+
|
|
67
|
+
mrid provides a simple way to compute an inverse of registration, this is also another way to perform registration in general.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
reg = mrid.Registration()
|
|
71
|
+
|
|
72
|
+
# first we need to fit a transform
|
|
73
|
+
reg.find_transform(t1, mrid.get_sri24("T1"))
|
|
74
|
+
|
|
75
|
+
# now we can use `reg.apply_transform` to transform any scan (assuming it is aligned to `t1`)
|
|
76
|
+
t2_sri24 = reg.apply_transform(t2) # returns sitk.Image
|
|
77
|
+
|
|
78
|
+
# use `apply_inverse_transform` to transform back to original space
|
|
79
|
+
t2_recovered = reg.apply_inverse_transform(t2_sri24) # returns sitk.Image
|
|
80
|
+
|
|
81
|
+
# when transforming segmentations, use nearest
|
|
82
|
+
# neigbour interpolation to preserve edges
|
|
83
|
+
seg = (mrid.tonumpy(t2_sri24) > 100).astype(np.uint8)
|
|
84
|
+
seg_orig = reg.apply_inverse_transform(seg, use_nearest_interpolation=True) # returns sitk.Image
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Skullstripping
|
|
88
|
+
|
|
89
|
+
For skullstripping mrid uses HD-BET model (<https://github.com/MIC-DKFZ/HD-BET>), and you need to have it installed (it uses torch). It works with pre/post contrast T1, T2 and FLAIR MRIs, but it expects them to be in MNI152 space (while many new datasets like BraTS are in SRI24). If your scans aren't in MNI152, the `skullstrip` function can register specified scan to MNI152, pass it to HD-BET, and then register back to original space, if `register_to_mni152` argument is specified.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
t1_skullstripped = mrid.skullstrip(t1) # if t1 is in MNI152
|
|
93
|
+
t1_skullstripped = mrid.skullstrip(t1, register_to_mni152="T1") # if t1 is not in MNI152
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
HD-BET is generally very robust but it can remove parts of tumors adjascent to the skull, but this is true for all skullstripping tools.
|
|
97
|
+
|
|
98
|
+
## Skullstripping multiple scans
|
|
99
|
+
|
|
100
|
+
If you have multiple scans that are already aligned, you can compute brain mask on one of them (ideally post-contrast T1) and apply it to all scans.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
data = {"t1": "t1c.nii.gz", "t2": "t2w.nii.gz", "flair": "t2f.nii.gz"}
|
|
104
|
+
skullstripped_data = mrid.skullstrip_D(data, key="t1", to=mrid.get_sri24("T1"))
|
|
105
|
+
# dict of skullstripped sitk.Image
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## dcm2niix
|
|
109
|
+
|
|
110
|
+
mrid provides a simple wrapper around [dcm2niix](https://github.com/rordenlab/dcm2niix), which is a very robust tool for converting DICOMs to NIfTI.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# this saves output.nii.gz to output_path. It can save multiple files if there are multiple cases in "path/to/dicoms".
|
|
114
|
+
mrid.utils.run_dcm2niix("path/to/dicoms", outfolder="output_path", outfname="output")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
If you want to use it convert DICOMs directly to SimpleITK, use `dcm2sitk`:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
t1 = mrid.utils.dcm2sitk("path/to/dicoms") # returns sitk.Image with correct spatial information
|
|
121
|
+
```
|
|
122
|
+
dcm2niix is much slower compared to SimpleITK's series reader which mrid uses by default, but it's more careful about splitting different studies to different files, although it might fail with malformed DICOMs which are surprisingly common (where it would think each slice is a separate study).
|
|
123
|
+
|
|
124
|
+
## Loading STL segmentations
|
|
125
|
+
|
|
126
|
+
`stl2sitk` converts segmentations in STL format to `sitk.Image` given a reference `sitk.Image` that the STL should be aligned to. Basically something I encountered so I had to make this.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
seg = mrid.utils.stl2sitk("path/to/file.stl", reference) # returns sitk.Image (hopefully) aligned with `reference`
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## How to install
|
|
133
|
+
Either run
|
|
134
|
+
```
|
|
135
|
+
pip install mrid-python
|
|
136
|
+
```
|
|
137
|
+
or
|
|
138
|
+
```
|
|
139
|
+
pip install git+https://github.com/inikishev/mrid
|
|
140
|
+
```
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<h1 align='center'>mrid</h1>
|
|
2
|
+
|
|
3
|
+
mrid is a library for preprocessing of 3D images, particularly medical images.
|
|
4
|
+
|
|
5
|
+
`mrid.Study` provides a convenient way to work with scans and provides all the tools (skull-stripping, registration, etc). See [this notebook](https://nbviewer.org/github/inikishev/mrid/blob/main/mrid_tutorial.ipynb) for an example of how to use it ([Google Colab](https://colab.research.google.com/drive/1d1MWPaGAqfK6q_5879LpwwxPCB-tbExe?usp=sharing)).
|
|
6
|
+
|
|
7
|
+
All methods are also available as separate functions, see below.
|
|
8
|
+
|
|
9
|
+
## Registering a scan
|
|
10
|
+
|
|
11
|
+
All methods accept scans that can be a path to NIfTI, DICOM dir, sitk Image, numpy array or torch tensor, and return sitk.Image format.
|
|
12
|
+
|
|
13
|
+
For registering you need to have SimpleITK-SimpleElastix installed (<https://pypi.org/project/SimpleITK-SimpleElastix/>)
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
registered = mrid.register(moving, fixed) # returns sitk.Image
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
mrid includes downloaders for commonly used templates - MNI152 and SRI24: `mrid.get_mni152`, and `mrid.get_sri24`.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
t1_sri = mrid.register(t1, mrid.get_sri24("T1")) # returns sitk.Image
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
note you can always convert anything to numpy array using `mrid.tonumpy(src)`:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
t1_sri_np = mrid.tonumpy(t1_sri) # np.array
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Registering multiple scans
|
|
32
|
+
|
|
33
|
+
If you have multiple scans that are already aligned, you can register one of them and use the same transformation to transform the rest. This can also be used to register segmentations.
|
|
34
|
+
|
|
35
|
+
In this example we register T1 to SRI24 T1 template, and transform the rest
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
data = {"t1": "t1c.nii.gz", "t2": "t2w.nii.gz", "flair": "t2f.nii.gz"}
|
|
39
|
+
registered_data = mrid.register_D(data, key="t1", to=mrid.get_sri24("T1"))
|
|
40
|
+
# dict of sitk.Image registered to SRI24
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If you have multiple scans that are not aligned (e.g. have different shapes, orientations), you can use `register_each`. This registers one scan to the target, and then registers all other scans to the first scan. In the example it registers T1 to SRI24, and then registers T2 and FLAIR to T1.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
data = {"t1": "t1c.nii.gz", "t2": "t2w.nii.gz", "flair": "t2f.nii.gz"}
|
|
47
|
+
registered_data = mrid.register_each(data, key="t1", to=mrid.get_sri24("T1"))
|
|
48
|
+
# dict of sitk.Image registered to SRI24
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Inverse transform
|
|
52
|
+
|
|
53
|
+
mrid provides a simple way to compute an inverse of registration, this is also another way to perform registration in general.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
reg = mrid.Registration()
|
|
57
|
+
|
|
58
|
+
# first we need to fit a transform
|
|
59
|
+
reg.find_transform(t1, mrid.get_sri24("T1"))
|
|
60
|
+
|
|
61
|
+
# now we can use `reg.apply_transform` to transform any scan (assuming it is aligned to `t1`)
|
|
62
|
+
t2_sri24 = reg.apply_transform(t2) # returns sitk.Image
|
|
63
|
+
|
|
64
|
+
# use `apply_inverse_transform` to transform back to original space
|
|
65
|
+
t2_recovered = reg.apply_inverse_transform(t2_sri24) # returns sitk.Image
|
|
66
|
+
|
|
67
|
+
# when transforming segmentations, use nearest
|
|
68
|
+
# neigbour interpolation to preserve edges
|
|
69
|
+
seg = (mrid.tonumpy(t2_sri24) > 100).astype(np.uint8)
|
|
70
|
+
seg_orig = reg.apply_inverse_transform(seg, use_nearest_interpolation=True) # returns sitk.Image
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Skullstripping
|
|
74
|
+
|
|
75
|
+
For skullstripping mrid uses HD-BET model (<https://github.com/MIC-DKFZ/HD-BET>), and you need to have it installed (it uses torch). It works with pre/post contrast T1, T2 and FLAIR MRIs, but it expects them to be in MNI152 space (while many new datasets like BraTS are in SRI24). If your scans aren't in MNI152, the `skullstrip` function can register specified scan to MNI152, pass it to HD-BET, and then register back to original space, if `register_to_mni152` argument is specified.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
t1_skullstripped = mrid.skullstrip(t1) # if t1 is in MNI152
|
|
79
|
+
t1_skullstripped = mrid.skullstrip(t1, register_to_mni152="T1") # if t1 is not in MNI152
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
HD-BET is generally very robust but it can remove parts of tumors adjascent to the skull, but this is true for all skullstripping tools.
|
|
83
|
+
|
|
84
|
+
## Skullstripping multiple scans
|
|
85
|
+
|
|
86
|
+
If you have multiple scans that are already aligned, you can compute brain mask on one of them (ideally post-contrast T1) and apply it to all scans.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
data = {"t1": "t1c.nii.gz", "t2": "t2w.nii.gz", "flair": "t2f.nii.gz"}
|
|
90
|
+
skullstripped_data = mrid.skullstrip_D(data, key="t1", to=mrid.get_sri24("T1"))
|
|
91
|
+
# dict of skullstripped sitk.Image
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## dcm2niix
|
|
95
|
+
|
|
96
|
+
mrid provides a simple wrapper around [dcm2niix](https://github.com/rordenlab/dcm2niix), which is a very robust tool for converting DICOMs to NIfTI.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# this saves output.nii.gz to output_path. It can save multiple files if there are multiple cases in "path/to/dicoms".
|
|
100
|
+
mrid.utils.run_dcm2niix("path/to/dicoms", outfolder="output_path", outfname="output")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
If you want to use it convert DICOMs directly to SimpleITK, use `dcm2sitk`:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
t1 = mrid.utils.dcm2sitk("path/to/dicoms") # returns sitk.Image with correct spatial information
|
|
107
|
+
```
|
|
108
|
+
dcm2niix is much slower compared to SimpleITK's series reader which mrid uses by default, but it's more careful about splitting different studies to different files, although it might fail with malformed DICOMs which are surprisingly common (where it would think each slice is a separate study).
|
|
109
|
+
|
|
110
|
+
## Loading STL segmentations
|
|
111
|
+
|
|
112
|
+
`stl2sitk` converts segmentations in STL format to `sitk.Image` given a reference `sitk.Image` that the STL should be aligned to. Basically something I encountered so I had to make this.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
seg = mrid.utils.stl2sitk("path/to/file.stl", reference) # returns sitk.Image (hopefully) aligned with `reference`
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## How to install
|
|
119
|
+
Either run
|
|
120
|
+
```
|
|
121
|
+
pip install mrid-python
|
|
122
|
+
```
|
|
123
|
+
or
|
|
124
|
+
```
|
|
125
|
+
pip install git+https://github.com/inikishev/mrid
|
|
126
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# https://zenodo.org/api/records/15470657/files-archive
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"get_mni152",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
_ROOT = Path(os.path.dirname(__file__))
|
|
14
|
+
|
|
15
|
+
_URLS = {
|
|
16
|
+
"2006 T1w symmetric": (
|
|
17
|
+
"https://zenodo.org/records/15470657/files/icbm_mni152_t1_06_sym.nii.gz?download=1",
|
|
18
|
+
"https://zenodo.org/records/15470657/files/icbm_mni152_t1_06_sym_bet.nii.gz?download=1",
|
|
19
|
+
),
|
|
20
|
+
"2009a T1w symmetric": (
|
|
21
|
+
"https://zenodo.org/records/15470657/files/icbm_mni152_t1_09a_sym.nii.gz?download=1",
|
|
22
|
+
"https://zenodo.org/records/15470657/files/icbm_mni152_t1_09a_sym_bet.nii.gz?download=1",
|
|
23
|
+
),
|
|
24
|
+
"2009a T2w symmetric": (
|
|
25
|
+
"https://zenodo.org/records/15470657/files/icbm_mni152_t2_09a_sym.nii.gz?download=1",
|
|
26
|
+
"https://zenodo.org/records/15470657/files/icbm_mni152_t2_09a_sym_bet.nii.gz?download=1",
|
|
27
|
+
),
|
|
28
|
+
"2009a T1w asymmetric": (
|
|
29
|
+
"https://zenodo.org/records/15470657/files/icbm_mni152_t1_09a_asym.nii.gz?download=1",
|
|
30
|
+
"https://zenodo.org/records/15470657/files/icbm_mni152_t1_09a_asym_bet.nii.gz?download=1",
|
|
31
|
+
),
|
|
32
|
+
"2009a T2w asymmetric": (
|
|
33
|
+
"https://zenodo.org/records/15470657/files/icbm_mni152_t2_09a_asym.nii.gz?download=1",
|
|
34
|
+
"https://zenodo.org/records/15470657/files/icbm_mni152_t2_09a_asym_bet.nii.gz?download=1",
|
|
35
|
+
),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_mni152_url = "https://zenodo.org/records/15470657/files/icbm_mni152_t1_06_sym_bet.nii.gz?download=1"
|
|
39
|
+
|
|
40
|
+
def _download_template(type: str, bet:bool):
|
|
41
|
+
filename = f"{type} {bool(bet)}.nii.gz"
|
|
42
|
+
if filename in os.listdir(_ROOT):
|
|
43
|
+
raise RuntimeError(f"Template {type} is already downloaded")
|
|
44
|
+
|
|
45
|
+
import requests
|
|
46
|
+
|
|
47
|
+
response = requests.get(_URLS[type][bet], stream=True, timeout=30)
|
|
48
|
+
response.raise_for_status()
|
|
49
|
+
|
|
50
|
+
with open(_ROOT / f"{filename}", 'wb') as file:
|
|
51
|
+
shutil.copyfileobj(response.raw, file) # type:ignore
|
|
52
|
+
|
|
53
|
+
def get_mni152(
|
|
54
|
+
type: Literal[
|
|
55
|
+
"2006 T1w symmetric",
|
|
56
|
+
"2009a T1w symmetric",
|
|
57
|
+
"2009a T2w symmetric",
|
|
58
|
+
"2009a T1w asymmetric",
|
|
59
|
+
"2009a T2w asymmetric",
|
|
60
|
+
],
|
|
61
|
+
skullstripped: bool = False,
|
|
62
|
+
):
|
|
63
|
+
"""Returns path to .nii.gz file of specified MNI-152 template.
|
|
64
|
+
|
|
65
|
+
Descriptions of templates are available here https://zenodo.org/records/15470657
|
|
66
|
+
"""
|
|
67
|
+
filename = f"{type} {bool(skullstripped)}.nii.gz"
|
|
68
|
+
|
|
69
|
+
if filename in os.listdir(_ROOT):
|
|
70
|
+
return str(_ROOT / filename)
|
|
71
|
+
|
|
72
|
+
print(f"{filename} will be downloaded from https://zenodo.org/records/15470657, this may take a few minutes.")
|
|
73
|
+
_download_template(type, skullstripped)
|
|
74
|
+
|
|
75
|
+
if filename not in os.listdir(_ROOT):
|
|
76
|
+
raise RuntimeError(
|
|
77
|
+
f"Failed to download {filename}; try downloading it manually from https://zenodo.org/records/15470657"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return str(_ROOT / filename)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""This subpackage allows one to download SRI-24 brain atlas files from https://www.nitrc.org/projects/sri24/.
|
|
2
|
+
|
|
3
|
+
The SRI24 atlas is licensed under the terms of the
|
|
4
|
+
|
|
5
|
+
Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)
|
|
6
|
+
|
|
7
|
+
license (https://creativecommons.org/licenses/by-...).
|
|
8
|
+
|
|
9
|
+
In publications using the SRI24 atlas, please cite the following paper:
|
|
10
|
+
|
|
11
|
+
T. Rohlfing, N.M. Zahr, E.V. Sullivan, A. Pfefferbaum, "The SRI24
|
|
12
|
+
Multichannel Atlas of Normal Adult Human Brain Structure," Human
|
|
13
|
+
Brain Mapping, vol. 31, no. 5, pp. 798-819, 2010.
|
|
14
|
+
|
|
15
|
+
http://dx.doi.org/10.1002/hbm.20906
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import tempfile
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Literal
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"get_sri24",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
_ROOT = Path(os.path.dirname(__file__))
|
|
29
|
+
|
|
30
|
+
_sri24_url = "https://www.nitrc.org/frs/download.php/4841/sri24_spm8.zip//?i_agree=1&download_now=1"
|
|
31
|
+
|
|
32
|
+
def _download_sri24() -> None:
|
|
33
|
+
import requests
|
|
34
|
+
|
|
35
|
+
response = requests.get(_sri24_url, stream=True, timeout=30)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
|
|
38
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
39
|
+
tmpdir = Path(tmpdir)
|
|
40
|
+
with open(tmpdir / "sri24_spm8.zip", 'wb') as file:
|
|
41
|
+
shutil.copyfileobj(response.raw, file) # type:ignore
|
|
42
|
+
|
|
43
|
+
shutil.unpack_archive(tmpdir / "sri24_spm8.zip", tmpdir / "sri24_spm8")
|
|
44
|
+
|
|
45
|
+
for file in os.listdir(tmpdir / "sri24_spm8" / "templates"):
|
|
46
|
+
shutil.copyfile(tmpdir / "sri24_spm8" / "templates" / file, _ROOT / file)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_sri24(type: Literal["EPI", "EPI_brain", "PD", "PD_brain", "T1", "T1_brain", "T2", "T2_brain"]) -> str:
|
|
50
|
+
"""Returns path to .nii file of specified SRI-24 template. Templates are downloaded if they haven't been downloaded already.
|
|
51
|
+
|
|
52
|
+
The following templates are available:
|
|
53
|
+
- `"T1"`: post-contrast T1-weighted MRI with skull;
|
|
54
|
+
- `"T1_brain"`: post-contrast T1-weighted MRI without skull;
|
|
55
|
+
- `"T2"`: T2-weighted MRI with skull;
|
|
56
|
+
- `"T2_brain"`: T2-weighted MRI without skull;
|
|
57
|
+
- `"EPI"`: echo-planar imaging MRI with skull;
|
|
58
|
+
- `"EPI_brain"`: echo-planar imaging MRU without skull;
|
|
59
|
+
- `"PD"`: proton density weighted spin-echo imaging MRI with skull;
|
|
60
|
+
- `"PD_brain"`: proton density weighted spin-echo imaging MRI without skull;
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
filename = f"{type}.nii"
|
|
64
|
+
if filename in os.listdir(_ROOT):
|
|
65
|
+
return str(_ROOT / filename)
|
|
66
|
+
|
|
67
|
+
print("SRI24 will be downloaded from https://www.nitrc.org/projects/sri24/, this may take a few minutes.")
|
|
68
|
+
_download_sri24()
|
|
69
|
+
|
|
70
|
+
if filename not in os.listdir(_ROOT):
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
f"Failed to download {filename}; try downloading it manually from https://www.nitrc.org/projects/sri24/, "
|
|
73
|
+
"then unpack the zip file, open it, open `templates` folder, you will see files such as `EPI.nii`. "
|
|
74
|
+
f"Copy all of those files to {_ROOT}."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return str(_ROOT / filename)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .convert import tonumpy, tositk, totensor, ImageLike
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import os
|
|
3
|
+
from typing import TYPE_CHECKING, TypeAlias
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import SimpleITK as sitk
|
|
7
|
+
|
|
8
|
+
from ..utils.torch_utils import TORCH_INSTALLED
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
PREFER_DCM2NIIX = False
|
|
14
|
+
ImageLike: TypeAlias = "np.ndarray | sitk.Image | torch.Tensor | str | os.PathLike"
|
|
15
|
+
|
|
16
|
+
def read_dicoms(dir: str | os.PathLike) -> sitk.Image:
|
|
17
|
+
"""reads a directory of DICOM files and returns a ``sitk.Image``"""
|
|
18
|
+
# load with dcm2niix
|
|
19
|
+
if PREFER_DCM2NIIX and importlib.util.find_spec("dcm2niix") is not None:
|
|
20
|
+
from ..utils.dcm2niix import dcm2sitk
|
|
21
|
+
return dcm2sitk(dir)
|
|
22
|
+
|
|
23
|
+
# load with SimpleITK
|
|
24
|
+
reader = sitk.ImageSeriesReader()
|
|
25
|
+
dicom_names = reader.GetGDCMSeriesFileNames(str(dir))
|
|
26
|
+
|
|
27
|
+
if not dicom_names:
|
|
28
|
+
raise FileNotFoundError(f"No DICOM series found in directory: {dir}")
|
|
29
|
+
|
|
30
|
+
reader.SetFileNames(dicom_names)
|
|
31
|
+
return reader.Execute()
|
|
32
|
+
|
|
33
|
+
def _read_sitk(path: str | os.PathLike) -> sitk.Image:
|
|
34
|
+
if os.path.isfile(path): return sitk.ReadImage(str(path))
|
|
35
|
+
if os.path.isdir(path): return read_dicoms(str(path))
|
|
36
|
+
raise FileNotFoundError(f"{path} doesn't exist")
|
|
37
|
+
|
|
38
|
+
def tositk(x: ImageLike) -> sitk.Image:
|
|
39
|
+
"""Load an image into an ``sitk.Image`` object.
|
|
40
|
+
``x`` can be a numpy array, a ``sitk.Image``, a ``torch.Tensor`` or a string (path to an image file)."""
|
|
41
|
+
if isinstance(x, np.ndarray): return sitk.GetImageFromArray(x)
|
|
42
|
+
if isinstance(x, sitk.Image): return x
|
|
43
|
+
if isinstance(x, (str, os.PathLike)): return _read_sitk(x)
|
|
44
|
+
if TORCH_INSTALLED:
|
|
45
|
+
import torch
|
|
46
|
+
if isinstance(x, torch.Tensor): return sitk.GetImageFromArray(x.numpy())
|
|
47
|
+
raise TypeError(f"Unsupported type {type(x)}")
|
|
48
|
+
|
|
49
|
+
def tonumpy(x: ImageLike) -> np.ndarray:
|
|
50
|
+
"""Load an image into a numpy.ndarray.
|
|
51
|
+
``x`` can be a numpy array, a ``sitk.Image``, a ``torch.Tensor`` or a string (path to an image file)."""
|
|
52
|
+
if isinstance(x, np.ndarray): return x
|
|
53
|
+
if isinstance(x, sitk.Image): return sitk.GetArrayFromImage(x)
|
|
54
|
+
if isinstance(x, (str, os.PathLike)): return sitk.GetArrayFromImage(_read_sitk(x))
|
|
55
|
+
if TORCH_INSTALLED:
|
|
56
|
+
import torch
|
|
57
|
+
if isinstance(x, torch.Tensor): return x.numpy()
|
|
58
|
+
raise TypeError(f"Unsupported type {type(x)}")
|
|
59
|
+
|
|
60
|
+
def totensor(x: ImageLike) -> "torch.Tensor":
|
|
61
|
+
"""Load an image into a torch.Tensor.
|
|
62
|
+
``x`` can be a numpy array, a ``sitk.Image``, a ``torch.Tensor`` or a string (path to an image file)."""
|
|
63
|
+
import torch
|
|
64
|
+
if isinstance(x, np.ndarray): return torch.from_numpy(x)
|
|
65
|
+
if isinstance(x, sitk.Image): return torch.from_numpy(sitk.GetArrayFromImage(x))
|
|
66
|
+
if isinstance(x, (str, os.PathLike)): return torch.from_numpy(sitk.GetArrayFromImage(_read_sitk(x)))
|
|
67
|
+
if isinstance(x, torch.Tensor): return x
|
|
68
|
+
raise TypeError(f"Unsupported type {type(x)}")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .bias_field_correction import n4_bias_field_correction
|
|
2
|
+
from .cropping import crop_bg, crop_bg_D
|
|
3
|
+
from .registration import resample_to, register, register_D, register_each, resize, downsample, Registration
|
|
4
|
+
from .skullstripping import skullstrip, skullstrip_D, run_hd_bet, predict_brain_mask
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"n4_bias_field_correction",
|
|
8
|
+
"crop_bg", "crop_bg_D",
|
|
9
|
+
"resample_to", "register", "register_D", "register_each", "resize", "downsample",
|
|
10
|
+
"skullstrip", "skullstrip_D", "run_hd_bet", "predict_brain_mask"
|
|
11
|
+
|
|
12
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import SimpleITK as sitk
|
|
2
|
+
from ..loading.convert import tositk, ImageLike
|
|
3
|
+
|
|
4
|
+
def n4_bias_field_correction(image: ImageLike, shrink: int = 4) -> sitk.Image:
|
|
5
|
+
"""Perform N4 Bias Field Correction to correct low frequency intensity non-uniformity present in MRI image.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
image (ImageLike): Input MRI image to be corrected. Can be any format supported by tositk conversion.
|
|
9
|
+
shrink (int, optional): Shrink factor for reducing image size before correction to speed up computation.
|
|
10
|
+
Default is 4. If set to 1 or less, no shrinking is performed.
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
image = tositk(image)
|
|
14
|
+
|
|
15
|
+
norm_image = sitk.RescaleIntensity(image, 0, 255)
|
|
16
|
+
mask = sitk.OtsuThreshold(norm_image, 0, 1)
|
|
17
|
+
|
|
18
|
+
if shrink > 1:
|
|
19
|
+
reduced = sitk.Shrink(image, [shrink] * image.GetDimension())
|
|
20
|
+
mask = sitk.Shrink(mask, [shrink] * mask.GetDimension())
|
|
21
|
+
|
|
22
|
+
else: reduced = image
|
|
23
|
+
|
|
24
|
+
corrector = sitk.N4BiasFieldCorrectionImageFilter()
|
|
25
|
+
corrector.Execute(reduced, mask)
|
|
26
|
+
log_bias_field = corrector.GetLogBiasFieldAsImage(image)
|
|
27
|
+
|
|
28
|
+
return image / sitk.Cast(sitk.Exp(log_bias_field), image.GetPixelID())
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any
|
|
3
|
+
import SimpleITK as sitk
|
|
4
|
+
|
|
5
|
+
from ..loading.convert import tositk, ImageLike
|
|
6
|
+
|
|
7
|
+
def _get_bbox(image: sitk.Image):
|
|
8
|
+
rescaled = sitk.RescaleIntensity(image, 0, 255)
|
|
9
|
+
filt = sitk.LabelShapeStatisticsImageFilter()
|
|
10
|
+
filt.Execute(sitk.OtsuThreshold(rescaled, 0, 255))
|
|
11
|
+
return filt.GetBoundingBox(255)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def crop_bg(image: ImageLike) -> sitk.Image:
|
|
15
|
+
"""Crops black background of a single 3D image via Otsu's thresholding.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
image (ImageLike): Input 3D image to be cropped. Can be any format supported by tositk conversion.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
sitk.Image: Cropped image with black background removed, maintaining the same pixel type as input.
|
|
22
|
+
"""
|
|
23
|
+
image = tositk(image)
|
|
24
|
+
bbox = _get_bbox(image)
|
|
25
|
+
return sitk.RegionOfInterest( image, bbox[int(len(bbox) / 2) :], bbox[0 : int(len(bbox) / 2)],)
|
|
26
|
+
|
|
27
|
+
def crop_bg_D(images: Mapping[str, ImageLike], key: str) -> dict[str, sitk.Image]:
|
|
28
|
+
"""Finds the bounding box of ``images[key]`` and crops all images in ``images`` to that bounding box."""
|
|
29
|
+
images = {k: tositk(v) for k,v in images.items()}
|
|
30
|
+
reference = images[key]
|
|
31
|
+
|
|
32
|
+
bbox = _get_bbox(reference)
|
|
33
|
+
|
|
34
|
+
ret = {k: sitk.RegionOfInterest(v, bbox[int(len(bbox) / 2) :], bbox[0 : int(len(bbox) / 2)]) for k,v in images.items()}
|
|
35
|
+
return ret
|
|
36
|
+
|