Skip to content

Commit f9ead2e

Browse files
authored
Core function to extract a timepoint from a 4D file (Inria-Empenn#239)
* Core function to extract a timepoint from a 4D file * Codespell * New versions for actions * New versions for actions * Extract timepoints range * Use core function instead of FSL's ExtractROI for SPM pipelines
1 parent 63fc9c6 commit f9ead2e

File tree

11 files changed

+134
-20
lines changed

11 files changed

+134
-20
lines changed

.github/workflows/code_quality.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
with:
3333
python-version: 3.9
3434

35-
- uses: actions/cache@v3
35+
- uses: actions/cache@v4
3636
with:
3737
path: ~/.cache/pip
3838
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}

.github/workflows/pipeline_status.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
with:
3838
python-version: 3.9
3939

40-
- uses: actions/cache@v3
40+
- uses: actions/cache@v4
4141
with:
4242
path: ~/.cache/pip
4343
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}

docs/core.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,17 @@ This module contains a set of functions dedicated to computations on images.
124124
# Get dimensions of voxels along x, y, and z in mm (returns e.g.: [1.0, 1.0, 1.0]).
125125
get_voxel_dimensions('/path/to/the/image.nii.gz')
126126
```
127+
128+
* `get_image_timepoint` : extracts the 3D volume in a given time point range from a 4D Nifti image
129+
130+
```python
131+
# Create a nifti 3D file containing time point 5 of the input image (returns a path to the generated 3D image)
132+
get_image_timepoints('/path/to/the/image.nii.gz', 5, 5):
133+
134+
# Create a nifti 3D file containing time points 5 to 10 of the input image (returns a path to the generated 3D image)
135+
get_image_timepoints('/path/to/the/image.nii.gz', 5, 10):
136+
```
137+
127138
## narps_open.core.interfaces
128139

129140
This module contains a set of interface creators inheriting form the `narps_open.core.interfaces.InterfaceCreator` abstract class.

narps_open/core/image.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,32 @@ def get_voxel_dimensions(image: str) -> list:
2323
float(voxel_dimensions[1]),
2424
float(voxel_dimensions[2])
2525
]
26+
27+
def get_image_timepoints(in_file: str, start_time_point: int, end_time_point: int) -> str:
28+
"""
29+
Extract the 3D volume at a given time point from a 4D Nifti image.
30+
Create a Nifti file containing the 3D volume.
31+
Return the filename of the created 3D output file.
32+
33+
Arguments:
34+
in_file: str, string that represent an absolute path to a Nifti 4D image.
35+
start_time_point: int, zero-based index of the first volume to extract from the 4D image.
36+
end_time_point: int, zero-based index of the last volume to extract from the 4D image.
37+
38+
Returns:
39+
str, path to the created file
40+
"""
41+
# These imports must stay inside the function, as required by Nipype
42+
from os import extsep
43+
from os.path import abspath, basename
44+
import nibabel as nib
45+
46+
# The output filename is based on the base filename of the input file.
47+
# Note that we use abspath to write the file in the base_directory of a nipype Node.
48+
out_file = abspath(
49+
basename(in_file).split(extsep)[0]+f'_timepoint-{start_time_point}-{end_time_point}.nii')
50+
51+
# Perform timepoints extraction
52+
nib.save(nib.load(in_file).slicer[..., start_time_point:end_time_point+1], out_file)
53+
54+
return out_file

narps_open/pipelines/team_0H5E.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,14 @@
1717
EstimateModel, EstimateContrast, Threshold
1818
)
1919
from nipype.interfaces.spm.base import Info as SPMInfo
20-
from nipype.interfaces.fsl import (
21-
ExtractROI
22-
)
2320

2421
from narps_open.pipelines import Pipeline
2522
from narps_open.data.task import TaskInformation
2623
from narps_open.data.participants import get_group
2724
from narps_open.core.common import (
2825
remove_parent_directory, list_intersection, elements_in_string, clean_list
2926
)
27+
from narps_open.core.image import get_image_timepoints
3028
from narps_open.utils.configuration import Configuration
3129

3230
class PipelineTeam0H5E(Pipeline):
@@ -93,10 +91,13 @@ def get_preprocessing(self):
9391

9492
# EXTRACTROI - remove first image of func
9593
# > Removal of "dummy" scans (deleting the first four volumes from each run)
96-
remove_first_image = Node(ExtractROI(), name = 'remove_first_image')
97-
remove_first_image.inputs.t_min = 4
98-
remove_first_image.inputs.t_size = 449 # number of time points - 4
99-
remove_first_image.inputs.output_type='NIFTI'
94+
remove_first_image = Node(Function(
95+
function = get_image_timepoints,
96+
input_names = ['in_file', 'start_time_point', 'end_time_point'],
97+
output_names = ['roi_file']
98+
), name = 'remove_first_image')
99+
remove_first_image.inputs.start_time_point = 4
100+
remove_first_image.inputs.end_time_point = 453 # last image
100101
preprocessing.connect(gunzip_func, 'out_file', remove_first_image, 'in_file')
101102

102103
# REALIGN - motion correction

narps_open/pipelines/team_98BT.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
DARTELNorm2MNI, FieldMap, Threshold
1717
)
1818
from nipype.interfaces.spm.base import Info as SPMInfo
19-
from nipype.interfaces.fsl import ExtractROI
2019
from nipype.algorithms.modelgen import SpecifySPMModel
2120
from niflow.nipype1.workflows.fmri.spm import create_DARTEL_template
2221

@@ -26,6 +25,7 @@
2625
from narps_open.core.common import (
2726
remove_parent_directory, list_intersection, elements_in_string, clean_list
2827
)
28+
from narps_open.core.image import get_image_timepoints
2929
from narps_open.utils.configuration import Configuration
3030

3131
class PipelineTeam98BT(Pipeline):
@@ -264,10 +264,13 @@ def get_preprocessing_sub_workflow(self):
264264
preprocessing.connect(slice_timing, 'timecorrected_files', motion_correction, 'in_files')
265265

266266
# Intrasubject coregistration
267-
extract_first = Node(ExtractROI(), name = 'extract_first')
268-
extract_first.inputs.t_min = 1
269-
extract_first.inputs.t_size = 1
270-
extract_first.inputs.output_type = 'NIFTI'
267+
extract_first = Node(Function(
268+
function = get_image_timepoints,
269+
input_names = ['in_file', 'start_time_point', 'end_time_point'],
270+
output_names = ['roi_file']
271+
), name = 'extract_first')
272+
extract_first.inputs.start_time_point = 1
273+
extract_first.inputs.end_time_point = 1
271274
preprocessing.connect(
272275
motion_correction, 'realigned_unwarped_files', extract_first, 'in_file')
273276

narps_open/pipelines/team_V55J.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
Coregister, Smooth, OneSampleTTestDesign, EstimateModel, EstimateContrast,
1515
Level1Design, TwoSampleTTestDesign, RealignUnwarp,
1616
Normalize12, NewSegment, FieldMap, Threshold)
17-
from nipype.interfaces.fsl import ExtractROI
1817
from nipype.algorithms.modelgen import SpecifySPMModel
1918
from nipype.interfaces.spm.base import Info as SPMInfo
2019

@@ -24,6 +23,7 @@
2423
from narps_open.core.common import (
2524
remove_parent_directory, list_intersection, elements_in_string, clean_list
2625
)
26+
from narps_open.core.image import get_image_timepoints
2727
from narps_open.utils.configuration import Configuration
2828

2929
class PipelineTeamV55J(Pipeline):
@@ -100,10 +100,13 @@ def get_preprocessing(self):
100100

101101
# EXTRACTROI - get the image 10 in func file
102102
# "For each run, we selected image 10 to distortion correct and asked to match the VDM file"
103-
extract_tenth_image = Node(ExtractROI(), name = 'extract_tenth_image')
104-
extract_tenth_image.inputs.t_min = 10
105-
extract_tenth_image.inputs.t_size = 1
106-
extract_tenth_image.inputs.output_type='NIFTI'
103+
extract_tenth_image = Node(Function(
104+
function = get_image_timepoints,
105+
input_names = ['in_file', 'start_time_point', 'end_time_point'],
106+
output_names = ['roi_file']
107+
), name = 'extract_tenth_image')
108+
extract_tenth_image.inputs.start_time_point = 9 # 0-based 10th image
109+
extract_tenth_image.inputs.end_time_point = 9
107110
preprocessing.connect(gunzip_func, 'out_file', extract_tenth_image, 'in_file')
108111

109112
# FIELDMAP - Calculate VDM routine of the FieldMap tool in SPM12

tests/core/test_image.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
pytest -q test_image.py -k <selected_test>
1212
"""
1313

14-
from os.path import abspath, join
14+
from os.path import abspath, join, basename, exists
15+
from filecmp import cmp
16+
from shutil import copyfile
17+
1518
from numpy import isclose
1619

1720
from pytest import mark
@@ -46,3 +49,67 @@ def test_get_voxel_dimensions():
4649

4750
# Check voxel sizes
4851
assert isclose(outputs.voxel_dimensions, [8.0, 8.0, 9.6]).all()
52+
53+
@staticmethod
54+
@mark.unit_test
55+
def test_get_image_timepoints(temporary_data_dir):
56+
""" Test the get_image_timepoints function """
57+
58+
# Path to the test image
59+
test_file_path = abspath(join(
60+
Configuration()['directories']['test_data'],
61+
'core',
62+
'image',
63+
'test_timepoint_input.nii.gz'))
64+
65+
# Create a Nipype Node using get_image_timepoint
66+
test_get_image_timepoint = Node(Function(
67+
function = im.get_image_timepoints,
68+
input_names = ['in_file', 'start_time_point', 'end_time_point'],
69+
output_names = ['out_file']
70+
), name = 'test_get_image_timepoint')
71+
test_get_image_timepoint.inputs.in_file = test_file_path
72+
test_get_image_timepoint.inputs.start_time_point = 10
73+
test_get_image_timepoint.inputs.end_time_point = 10
74+
test_get_image_timepoint.base_dir = temporary_data_dir
75+
outputs = test_get_image_timepoint.run().outputs
76+
77+
# Check output file name
78+
assert basename(outputs.out_file) == 'test_timepoint_input_timepoint-10-10.nii'
79+
80+
# Check if file exists
81+
assert exists(outputs.out_file)
82+
83+
# Compare with expected
84+
out_test_file_path = abspath(join(
85+
Configuration()['directories']['test_data'],
86+
'core',
87+
'image',
88+
'test_timepoint_output.nii'))
89+
assert cmp(outputs.out_file, out_test_file_path)
90+
91+
# Try a new time range
92+
test_get_image_timepoint = Node(Function(
93+
function = im.get_image_timepoints,
94+
input_names = ['in_file', 'start_time_point', 'end_time_point'],
95+
output_names = ['out_file']
96+
), name = 'test_get_image_timepoint')
97+
test_get_image_timepoint.inputs.in_file = test_file_path
98+
test_get_image_timepoint.inputs.start_time_point = 2
99+
test_get_image_timepoint.inputs.end_time_point = 4
100+
test_get_image_timepoint.base_dir = temporary_data_dir
101+
outputs = test_get_image_timepoint.run().outputs
102+
103+
# Check output file name
104+
assert basename(outputs.out_file) == 'test_timepoint_input_timepoint-2-4.nii'
105+
106+
# Check if file exists
107+
assert exists(outputs.out_file)
108+
109+
# Compare with expected
110+
out_test_file_path = abspath(join(
111+
Configuration()['directories']['test_data'],
112+
'core',
113+
'image',
114+
'test_timepoint_output_2.nii'))
115+
assert cmp(outputs.out_file, out_test_file_path)
3.27 KB
Binary file not shown.
1.16 KB
Binary file not shown.

0 commit comments

Comments
 (0)