Skip to content

Commit 3ddfcb8

Browse files
committed
Merge remote-tracking branch 'origin/main' into test-looklocker
2 parents 7ffc774 + c0769ba commit 3ddfcb8

File tree

8 files changed

+152
-18
lines changed

8 files changed

+152
-18
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ repos:
1414

1515
- repo: https://github.com/astral-sh/ruff-pre-commit
1616
# Ruff version.
17-
rev: 'v0.15.9'
17+
rev: 'v0.15.10'
1818
hooks:
1919
# Run the linter.
2020
- id: ruff-check
@@ -23,7 +23,7 @@ repos:
2323
- id: ruff-format
2424

2525
- repo: https://github.com/pre-commit/mirrors-mypy
26-
rev: v1.20.0
26+
rev: v1.20.1
2727
hooks:
2828
- id: mypy
2929
files: ^src/|^tests/

_toc.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ parts:
1717
- file: "docs/hybrid.md" # About the mritk hybrid command
1818
- file: "docs/concentration.md" # About the mritk concentration command
1919

20+
- caption: Examples
21+
chapters:
22+
- file: "docs/roi.md" # Example of how to extract a region of interest (ROI) from an image using mritk
23+
2024
- caption: Community
2125
chapters:
2226
- file: "CONTRIBUTING.md" # Contributing guidelines

docs/api.rst

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ hybrid
7979
statistics
8080
----------
8181

82-
.. automodule:: mritk.stats
82+
.. automodule:: mritk.statistics
8383
:members:
8484
:inherited-members:
8585

@@ -97,11 +97,3 @@ segmentation
9797
.. automodule:: mritk.segmentation
9898
:members:
9999
:inherited-members:
100-
101-
.. automodule:: mritk.segmentation.groups
102-
:members:
103-
:inherited-members:
104-
105-
.. automodule:: mritk.segmentation.lookup_table
106-
:members:
107-
:inherited-members:

docs/roi.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Selecting a region of interest (ROI)
2+
3+
```python
4+
from pathlib import Path
5+
import logging
6+
import numpy as np
7+
import scipy.ndimage as ndi
8+
import matplotlib.pyplot as plt
9+
import nibabel as nib
10+
import mritk
11+
```
12+
13+
First we make sure that the necessary data is downloaded and present in a folder called "gonzo" in the same directory as this script
14+
15+
```python
16+
mri_data_dir = Path("gonzo")
17+
```
18+
19+
If you haven't downloaded he gonzo dataset, you can do so with the following command:
20+
mritk datasets download gonzo
21+
```shell
22+
mritk datasets download gonzo -o gonzo
23+
```
24+
25+
If you want to learn more about the gonzo dataset, you can check the command
26+
```shell
27+
mritk datasets info gonzo
28+
```
29+
which will point you do the url https://doi.org/10.5281/zenodo.14266867.
30+
31+
32+
```python
33+
# We now load the full T1-weighted image
34+
t1_path = mri_data_dir / "mri-dataset/mri_dataset/sub-01/ses-01/anat/sub-01_ses-01_T1w.nii.gz"
35+
t1_data = mritk.MRIData.from_file(t1_path)
36+
```
37+
38+
We define a new grid of points in physical space that we want to extract from the original image.
39+
This grid is defined by the ranges of x, y, and z coordinates and the number of points along each axis.
40+
41+
```python
42+
xs = np.linspace(0, 70, 80)
43+
ys = np.linspace(0, 20, 20)
44+
zs = np.linspace(-40, 90, 110)
45+
```
46+
47+
```python
48+
# Create a 3D grid of points as one long vector
49+
grid_x, grid_y, grid_z = np.meshgrid(xs, ys, zs, indexing="ij")
50+
grid_points = np.vstack([grid_x.ravel(), grid_y.ravel(), grid_z.ravel()]).T
51+
```
52+
53+
```python
54+
# We also define a new affine transformation for the extracted piece, which in this case is just the identity matrix. This means that the extracted piece will be in the same physical space as the original image.
55+
new_affine = np.eye(4)
56+
new_affine[0, 0] = xs[1] - xs[0]
57+
new_affine[1, 1] = ys[1] - ys[0]
58+
new_affine[2, 2] = zs[1] - zs[0]
59+
new_affine[0, 3] = xs[0]
60+
new_affine[1, 3] = ys[0]
61+
new_affine[2, 3] = zs[0]
62+
```
63+
64+
We now compute the corresponding voxel indices in the original image for each point in our grid using the affine transformation of the original image.
65+
66+
```python
67+
vi = mritk.data.physical_to_voxel_indices(grid_points, affine=t1_data.affine) # Shape: (N, 3)
68+
```
69+
70+
Finally, we extract the values at the specified voxel indices and reshape them back to the original grid shape.
71+
72+
```python
73+
v = t1_data.data[tuple(vi.T)]
74+
v = v.reshape(grid_x.shape)
75+
```
76+
77+
```python
78+
# We save the extracted values as a new NIfTI file with the same affine as the original image.
79+
piece_t1_data = mritk.MRIData(data=v, affine=new_affine)
80+
piece_t1_data.save("piece_T1.nii.gz")
81+
```
82+
83+
We can now visualize the extracted piece of the T1 image using napari or any other NIfTI viewer. The extracted piece will correspond to the specified grid of points in physical space, allowing us to focus on a specific region of interest (ROI) within the original image.
84+
We can do this using the command:
85+
```shell
86+
mritk napari piece_T1.nii.gz gonzo/mri-dataset/mri_dataset/sub-01/ses-01/anat/sub-01_ses-01_T1w.nii.gz
87+
```
88+
![piece_t1](https://github.com/user-attachments/assets/242db608-e9f8-4e4d-8d9e-4c6acc5da94f)
89+
90+
We can now try to do the same thing for the concentration files, which are stored in a different folder and have a different naming convention. We will loop through all the concentration files, extract the values at the specified voxel indices,
91+
and save them as new NIfTI files with the same affine as the original image.
92+
93+
```python
94+
concentration_files = list(
95+
sorted(
96+
(mri_data_dir / "mri-processed/mri_processed_data/sub-01/concentrations").glob("sub-01_ses-0*_concentration.nii.gz")
97+
)
98+
)
99+
```
100+
101+
```python
102+
vs = []
103+
```
104+
105+
```python
106+
for i, path in enumerate(concentration_files):
107+
print(path)
108+
conc_data = mritk.MRIData.from_file(path)
109+
vi = mritk.data.physical_to_voxel_indices(grid_points, affine=conc_data.affine) # Shape: (N, 3)
110+
v = conc_data.data[tuple(vi.T)] # Extract values at the specified voxel indices
111+
v = v.reshape(grid_x.shape)
112+
vs.append(v)
113+
```
114+
115+
```python
116+
piece_data = mritk.MRIData(data=np.stack(vs), affine=new_affine)
117+
piece_data.save(f"piece_conc.nii.gz")
118+
```
119+
120+
We can visualize the extracted concentration files in napari as well, using the command:
121+
```shell
122+
mritk napari piece_conc.nii.gz piece_T1.nii.gz "gonzo/mri-dataset/mri_dataset/sub-01/ses-01/anat/sub-01_ses-01_T1w.nii.gz"
123+
```
124+
![conc_t1](https://github.com/user-attachments/assets/f306e30a-8b12-480c-9bb4-94a4089696a0)

src/mritk/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
statistics,
1717
utils,
1818
)
19+
from .data import MRIData
1920

2021
meta = metadata("mritk")
2122
__version__ = meta["Version"]
@@ -35,4 +36,5 @@
3536
"hybrid",
3637
"r1",
3738
"statistics",
39+
"MRIData",
3840
]

src/mritk/looklocker.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def read_dicom_trigger_times(dicomfile: Path) -> np.ndarray:
4141
all_frame_times = [
4242
f.CardiacSynchronizationSequence[0].NominalCardiacTriggerDelayTime for f in dcm.PerFrameFunctionalGroupsSequence
4343
]
44+
4445
return np.unique(all_frame_times)
4546

4647

@@ -327,6 +328,12 @@ def add_arguments(
327328
dicom_parser.add_argument("-i", "--input", type=Path, help="Path to the input Look-Locker DICOM file")
328329
dicom_parser.add_argument("-o", "--output", type=Path, help="Desired output path for the converted .nii.gz file")
329330

331+
ll_timestamps = subparser.add_parser(
332+
"timestamps", help="Read timestamps from DICOM data", formatter_class=parser.formatter_class
333+
)
334+
ll_timestamps.add_argument("-i", "--input", type=Path, help="Path to the input Look-Locker DICOM file")
335+
ll_timestamps.add_argument("-o", "--output", type=Path, help="Desired output path for the generated file")
336+
330337
ll_t1 = subparser.add_parser("t1", help="Generate a T1 map from Look-Locker data", formatter_class=parser.formatter_class)
331338
ll_t1.add_argument("-i", "--input", type=Path, help="Path to the 4D Look-Locker NIfTI file")
332339
ll_t1.add_argument("-t", "--timestamps", type=Path, help="Path to the text file containing trigger delay times (in ms)")
@@ -353,12 +360,17 @@ def add_arguments(
353360
extra_args_cb(dicom_parser)
354361
extra_args_cb(ll_t1)
355362
extra_args_cb(ll_post)
363+
extra_args_cb(ll_timestamps)
356364

357365

358366
def dispatch(args):
359367
command = args.pop("looklocker-command")
360368
if command == "dcm2ll":
361369
dicom_to_looklocker(args.pop("input"), args.pop("output"))
370+
elif command == "timestamps":
371+
timestamps = read_dicom_trigger_times(args.pop("input"))
372+
if args.pop("output") is not None:
373+
np.savetxt(args.pop("output"), timestamps)
362374
elif command == "t1":
363375
ll = LookLocker.from_file(args.pop("input"), args.pop("timestamps"))
364376

src/mritk/napari.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,6 @@ def dispatch(args):
5252

5353
mri_resource = MRIData.from_file(file_path)
5454
data = mri_resource.data
55-
viewer.add_image(data, name=file_path.stem)
55+
viewer.add_image(data, name=file_path.stem, affine=mri_resource.affine)
5656

5757
napari.run()

src/mritk/segmentation.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,12 @@ def roi_labels(self) -> np.ndarray:
124124
"""An array containing the unique numerical labels of all present ROIs."""
125125
return self.rois
126126

127-
def get_roi_labels(self, rois: npt.NDArray[np.int_] | None = None) -> pd.DataFrame:
127+
def get_roi_labels(self, rois: npt.NDArray[np.int32] | None = None) -> pd.DataFrame:
128128
"""
129129
Retrieves a descriptive mapping for a specified set of ROIs.
130130
131131
Args:
132-
rois (Optional[npt.NDArray[np.int_]], optional): Array of numerical ROIs to look up.
132+
rois (Optional[npt.NDArray[np.int32]], optional): Array of numerical ROIs to look up.
133133
If None, retrieves labels for all ROIs currently present in the volume. Defaults to None.
134134
135135
Returns:
@@ -192,12 +192,12 @@ class ExtendedFreeSurferSegmentation(FreeSurferSegmentation):
192192
the base FreeSurfer anatomical label (modulus 10000).
193193
"""
194194

195-
def get_roi_labels(self, rois: npt.NDArray[np.int_] | None = None) -> pd.DataFrame:
195+
def get_roi_labels(self, rois: npt.NDArray[np.int32] | None = None) -> pd.DataFrame:
196196
"""
197197
Retrieves descriptive mappings including the augmented tissue type classifications.
198198
199199
Args:
200-
rois (Optional[npt.NDArray[np.int_]], optional): Array of numerical ROIs to look up.
200+
rois (Optional[npt.NDArray[np.int32]], optional): Array of numerical ROIs to look up.
201201
If None, retrieves labels for all ROIs currently present. Defaults to None.
202202
203203
Returns:
@@ -220,7 +220,7 @@ def get_roi_labels(self, rois: npt.NDArray[np.int_] | None = None) -> pd.DataFra
220220
how="outer",
221221
).drop(columns=["FreeSurfer_ROI"])[["ROI", self._label_name, "tissue_type"]]
222222

223-
def get_tissue_type(self, rois: npt.NDArray[np.int_] | None = None) -> pd.DataFrame:
223+
def get_tissue_type(self, rois: npt.NDArray[np.int32] | None = None) -> pd.DataFrame:
224224
"""
225225
Determines the tissue type based on the numerical ranges of the ROI labels.
226226
@@ -229,7 +229,7 @@ def get_tissue_type(self, rois: npt.NDArray[np.int_] | None = None) -> pd.DataFr
229229
Labels >= 20000 are classified as "Dura".
230230
231231
Args:
232-
rois (Optional[npt.NDArray[np.int_]], optional): Array of numerical ROIs to evaluate.
232+
rois (Optional[npt.NDArray[np.int32]], optional): Array of numerical ROIs to evaluate.
233233
If None, evaluates all ROIs currently present. Defaults to None.
234234
235235
Returns:

0 commit comments

Comments
 (0)