Skip to content

Commit 09c5cef

Browse files
committed
ENH: Add T1T2Ratio interface
1 parent 46ba744 commit 09c5cef

File tree

2 files changed

+114
-0
lines changed

2 files changed

+114
-0
lines changed

src/smriprep/interfaces/calc.py

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
#
4+
# Copyright 2024 The NiPreps Developers <[email protected]>
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
# We support and encourage derived works from this project, please read
19+
# about our expectations at
20+
#
21+
# https://www.nipreps.org/community/licensing/
22+
#
23+
"""Image calculation interfaces."""
24+
25+
import os
26+
from pathlib import Path
27+
28+
import nibabel as nb
29+
import numpy as np
30+
from nipype.interfaces.base import (
31+
File,
32+
SimpleInterface,
33+
TraitedSpec,
34+
)
35+
36+
37+
class T1T2RatioInputSpec(TraitedSpec):
38+
t1w_file = File(exists=True, mandatory=True, desc='T1-weighted image')
39+
t2w_file = File(exists=True, mandatory=True, desc='T2-weighted image')
40+
mask_file = File(exists=True, desc='Brain mask')
41+
42+
43+
class T1T2RatioOutputSpec(TraitedSpec):
44+
t1t2_file = File(exists=True, desc='T1/T2 ratio image')
45+
46+
47+
class T1T2Ratio(SimpleInterface):
48+
input_spec = T1T2RatioInputSpec
49+
output_spec = T1T2RatioOutputSpec
50+
51+
def _run_interface(self, runtime):
52+
self._results['t1t2_file'] = make_t1t2_ratio(
53+
self.inputs.t1w_file, self.inputs.t2w_file, self.inputs.mask_file, newpath=runtime.cwd
54+
)
55+
return runtime
56+
57+
58+
def make_t1t2_ratio(
59+
t1w_file: str,
60+
t2w_file: str,
61+
mask_file: str | None = None,
62+
newpath: str | None = None,
63+
) -> str:
64+
t1w = nb.load(t1w_file)
65+
t2w = nb.load(t2w_file)
66+
if mask_file is not None:
67+
mask = np.asanyarray(nb.load(mask_file).dataobj) != 0
68+
else:
69+
mask = np.ones(t1w.shape, dtype=bool)
70+
71+
t1w_data = t1w.get_fdata(dtype=np.float32)
72+
t2w_data = t2w.get_fdata(dtype=np.float32)
73+
74+
t1t2_data = np.zeros_like(t1w_data)
75+
76+
ratio = t1w_data[mask] / t2w_data[mask]
77+
ratio[~np.isfinite(ratio)] = 0
78+
minval = ratio.min()
79+
maxval = ratio.max()
80+
81+
t1t2_data[mask] = (ratio - minval) / (maxval - minval) * 100
82+
83+
t1t2 = nb.Nifti1Image(t1t2_data, t1w.affine, t1w.header)
84+
t1t2.header.set_data_dtype(np.float32)
85+
86+
t1t2_path = Path(newpath or os.getcwd()) / 't1t2_ratio.nii.gz'
87+
88+
t1t2.to_filename(t1t2_path)
89+
90+
return str(t1t2_path)
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import nibabel as nb
2+
from nipype.pipeline import engine as pe
3+
from templateflow import api as tf
4+
5+
from ..calc import T1T2Ratio
6+
7+
8+
def test_T1T2Ratio(tmp_path):
9+
t1w = tf.get('MNI152NLin2009cAsym', desc=None, resolution=1, suffix='T1w')
10+
t2w = tf.get('MNI152NLin2009cAsym', desc=None, resolution=1, suffix='T2w')
11+
mask = tf.get('MNI152NLin2009cAsym', desc='brain', resolution=1, suffix='mask')
12+
13+
t1t2 = pe.Node(
14+
T1T2Ratio(t1w_file=t1w, t2w_file=t2w, mask_file=mask),
15+
name='t1t2',
16+
base_dir=tmp_path,
17+
)
18+
19+
result = t1t2.run()
20+
21+
t1t2ratio = nb.load(result.outputs.t1t2_file)
22+
assert t1t2ratio.shape == (193, 229, 193)
23+
assert t1t2ratio.get_fdata().min() == 0.0
24+
assert t1t2ratio.get_fdata().max() == 100.0

0 commit comments

Comments
 (0)