Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions bdpy/mri/fmriprep.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from collections import OrderedDict

import nibabel
import nipy
import numpy as np
import pandas as pd

Expand Down Expand Up @@ -95,13 +94,13 @@
# File name patterns
# FIXME
if self.__fmriprep_version == '1.2':
file_pattern = {'volume_native' : '.*_space-T1w_desc-preproc_bold\.nii\.gz$',

Check warning on line 97 in bdpy/mri/fmriprep.py

View workflow job for this annotation

GitHub Actions / test (3.10)

invalid escape sequence '\.'

Check warning on line 97 in bdpy/mri/fmriprep.py

View workflow job for this annotation

GitHub Actions / test (3.11)

invalid escape sequence '\.'
'volume_standard' : '.*_space-MNI152NLin2009cAsym_desc-preproc_bold\.nii\.gz$',

Check warning on line 98 in bdpy/mri/fmriprep.py

View workflow job for this annotation

GitHub Actions / test (3.10)

invalid escape sequence '\.'

Check warning on line 98 in bdpy/mri/fmriprep.py

View workflow job for this annotation

GitHub Actions / test (3.11)

invalid escape sequence '\.'
'surf_native_left' : '.*_space-fsnative_hemi-L\.func\.gii$',

Check warning on line 99 in bdpy/mri/fmriprep.py

View workflow job for this annotation

GitHub Actions / test (3.10)

invalid escape sequence '\.'

Check warning on line 99 in bdpy/mri/fmriprep.py

View workflow job for this annotation

GitHub Actions / test (3.11)

invalid escape sequence '\.'
'surf_native_right' : '.*_space-fsnative_hemi-R\.func\.gii$',

Check warning on line 100 in bdpy/mri/fmriprep.py

View workflow job for this annotation

GitHub Actions / test (3.10)

invalid escape sequence '\.'

Check warning on line 100 in bdpy/mri/fmriprep.py

View workflow job for this annotation

GitHub Actions / test (3.11)

invalid escape sequence '\.'
'surf_standard_left' : '.*_space-fsaverage_hemi-L\.func\.gii$',

Check warning on line 101 in bdpy/mri/fmriprep.py

View workflow job for this annotation

GitHub Actions / test (3.10)

invalid escape sequence '\.'

Check warning on line 101 in bdpy/mri/fmriprep.py

View workflow job for this annotation

GitHub Actions / test (3.11)

invalid escape sequence '\.'
'surf_standard_right' : '.*_space-fsaverage_hemi-R\.func\.gii$',

Check warning on line 102 in bdpy/mri/fmriprep.py

View workflow job for this annotation

GitHub Actions / test (3.10)

invalid escape sequence '\.'

Check warning on line 102 in bdpy/mri/fmriprep.py

View workflow job for this annotation

GitHub Actions / test (3.11)

invalid escape sequence '\.'
'surf_standard_41k_left' : '.*_space-fsaverage6_hemi-L\.func\.gii$',

Check warning on line 103 in bdpy/mri/fmriprep.py

View workflow job for this annotation

GitHub Actions / test (3.10)

invalid escape sequence '\.'

Check warning on line 103 in bdpy/mri/fmriprep.py

View workflow job for this annotation

GitHub Actions / test (3.11)

invalid escape sequence '\.'
'surf_standard_41k_right' : '.*_space-fsaverage6_hemi-R\.func\.gii$',
'surf_standard_10k_left' : '.*_space-fsaverage5_hemi-L\.func\.gii$',
'surf_standard_10k_right' : '.*_space-fsaverage5_hemi-R\.func\.gii$',
Expand Down Expand Up @@ -406,17 +405,17 @@
- Returns voxel ijk indexes (3 x voxel)
- Data, xyz, and ijk are flattened by Fortran-like index order
"""
img = nipy.load_image(self.__dpath)
img = nibabel.load(self.__dpath)

data = img.get_fdata()
if data.ndim == 4:
data = data.reshape(-1, data.shape[-1], order='F').T
i_len, j_len, k_len, t = img.shape
affine = np.delete(np.delete(img.coordmap.affine, 3, axis=0), 3, axis=1)
affine = img.affine
elif data.ndim == 3:
data = data.flatten(order='F')
i_len, j_len, k_len = img.shape
affine = img.coordmap.affine
affine = img.affine
else:
raise ValueError('Invalid shape.')

Expand Down Expand Up @@ -838,17 +837,17 @@
- Returns voxel ijk indexes (3 x voxel)
- Data, xyz, and ijk are flattened by Fortran-like index order
"""
img = nipy.load_image(fpath)
img = nibabel.load(fpath)

data = img.get_fdata()
if data.ndim == 4:
data = data.reshape(-1, data.shape[-1], order='F').T
i_len, j_len, k_len, t = img.shape
affine = np.delete(np.delete(img.coordmap.affine, 3, axis=0), 3, axis=1)
affine = np.delete(np.delete(img.affine, 3, axis=0), 3, axis=1)
elif data.ndim == 3:
Comment on lines 843 to 847

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this looks similar to the affine handling changed in the active loading paths.

However, fmriprep.__load_mri and __get_xyz appear to be unused code paths with no caller in the current codebase. Since there is no concrete use case to verify against, I could not determine whether the current behavior is actually incorrect or intentional for these helpers.

For that reason, I intentionally left them unchanged in this PR to avoid introducing an unverified behavior change. I think these helpers should be revisited separately if a real use case is identified, or removed if they are confirmed to be dead code.

data = data.flatten(order='F')
i_len, j_len, k_len = img.shape
affine = img.coordmap.affine
affine = img.affine
else:
raise ValueError('Invalid shape.')

Expand Down
9 changes: 9 additions & 0 deletions bdpy/mri/glm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


import csv
import warnings

import numpy as np
from nipy.modalities.fmri.experimental_paradigm import (
Expand Down Expand Up @@ -45,6 +46,14 @@ def make_paradigm(event_files, num_vols, tr=2., cond_col=2, label_col=None, regr
run_regressors : nuisance regressors for runs
run_regressors_label : labels for the run regressors
"""
warnings.warn(
"make_paradigm depends on nipy, which is unmaintained. nipy support "
"will be removed in a future release of bdpy. Consider migrating to "
"nilearn (e.g. nilearn.glm.first_level.make_first_level_design_matrix).",
DeprecationWarning,
stacklevel=2,
)

onset = []
duration = []
conds = []
Expand Down
6 changes: 3 additions & 3 deletions bdpy/mri/load_epi.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import itertools as itr

import nipy
import nibabel
import numpy as np


Expand Down Expand Up @@ -35,7 +35,7 @@ def load_epi(datafiles):
print("Loading %s" % df)

# Load an EPI image
img = nipy.load_image(df)
img = nibabel.load(df)

xyz = _check_xyz(xyz, img)
data_list.append(np.array(img.get_fdata().flatten(), dtype=np.float64))
Expand All @@ -49,7 +49,7 @@ def _check_xyz(xyz, img):
"""Check voxel xyz consistency."""
# Use the image shape metadata; avoid get_fdata() here, which would read the
# whole volume just to check consistency (load_epi reads the data later).
xyz_current = _get_xyz(img.coordmap.affine, img.shape[:3])
xyz_current = _get_xyz(img.affine, img.shape[:3])

if xyz.size == 0:
xyz = xyz_current
Expand Down
8 changes: 4 additions & 4 deletions bdpy/mri/load_mri.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""load_mri"""


import nipy
import nibabel
import numpy as np


Expand All @@ -13,17 +13,17 @@ def load_mri(fpath):
- Returns voxel ijk indexes (3 x voxel)
- Data, xyz, and ijk are flattened by Fortran-like index order
"""
img = nipy.load_image(fpath)
img = nibabel.load(fpath)

data = img.get_fdata()
if data.ndim == 4:
data = data.reshape(-1, data.shape[-1], order='F').T
i_len, j_len, k_len, t = img.shape
affine = np.delete(np.delete(img.coordmap.affine, 3, axis=0), 3, axis=1)
affine = img.affine
elif data.ndim == 3:
data = data.flatten(order='F')
i_len, j_len, k_len = img.shape
affine = img.coordmap.affine
affine = img.affine
else:
raise ValueError('Invalid shape.')

Expand Down
88 changes: 88 additions & 0 deletions tests/test_mri.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,94 @@ def test_load_mri_3d(self) -> None:
self.assertEqual(xyz.shape, (3, arr.size))
self.assertEqual(ijk.shape, (3, arr.size))

def test_load_mri_4d(self) -> None:
"""Test load_mri on a 4D volume.

Guards a bug in the 4D path where the affine was reduced to 3x3 before
being applied to homogeneous (4 x N) voxel indices, raising a shape
mismatch. The 4D path must use the full 4x4 affine, just like the 3D
path, and return one row per time point.
"""
import os
import tempfile

import nibabel

i_len, j_len, k_len, t_len = 2, 3, 4, 5
n_vox = i_len * j_len * k_len
arr = np.arange(n_vox * t_len, dtype=np.float32).reshape(
i_len, j_len, k_len, t_len)
affine = np.array([[2., 0., 0., -10.],
[0., 2., 0., -12.],
[0., 0., 2., -8.],
[0., 0., 0., 1.]])

with tempfile.TemporaryDirectory() as tmpdir:
fpath = os.path.join(tmpdir, 'test4d.nii.gz')
nibabel.save(nibabel.Nifti1Image(arr, affine=affine), fpath)
data, xyz, ijk = bmr.load_mri(fpath) # type: ignore

# Data is (sample x voxel): one sample per time point, voxels flattened
# in Fortran order.
self.assertEqual(data.shape, (t_len, n_vox))
np.testing.assert_array_equal(
data, arr.reshape(-1, t_len, order='F').T)

# ijk are the voxel indices in Fortran order.
expected_ijk = np.array(np.unravel_index(
np.arange(n_vox), (i_len, j_len, k_len), order='F'))
np.testing.assert_array_equal(ijk, expected_ijk)

# xyz are the world coordinates from the full 4x4 affine.
ijk_b = np.vstack([expected_ijk, np.ones((1, n_vox))])
expected_xyz = np.dot(affine, ijk_b)[:3]
np.testing.assert_allclose(xyz, expected_xyz)
self.assertEqual(xyz.shape, (3, n_vox))

def test_braindata_load_volume_4d(self) -> None:
"""Test BrainData volume loading on a 4D volume.

Guards the same 4D affine bug as test_load_mri_4d in the duplicated
loader inside fmriprep.BrainData.__load_volume. The surrounding
create_bdata_fmriprep code treats ``data.shape[0]`` as the number of
volumes (time points), so the 4D path must return ``(T, N)`` data and
spatial xyz from the full 4x4 affine.
"""
import os
import tempfile

import nibabel

from bdpy.mri.fmriprep import BrainData

i_len, j_len, k_len, t_len = 2, 3, 4, 5
n_vox = i_len * j_len * k_len
arr = np.arange(n_vox * t_len, dtype=np.float32).reshape(
i_len, j_len, k_len, t_len)
affine = np.array([[2., 0., 0., -10.],
[0., 2., 0., -12.],
[0., 0., 2., -8.],
[0., 0., 0., 1.]])

with tempfile.TemporaryDirectory() as tmpdir:
fpath = os.path.join(tmpdir, 'bold4d.nii.gz')
nibabel.save(nibabel.Nifti1Image(arr, affine=affine), fpath)
brain = BrainData(fpath, dtype='volume')

# data.shape[0] must be the number of time points (volumes).
self.assertEqual(brain.data.shape, (t_len, n_vox))
np.testing.assert_array_equal(
brain.data, arr.reshape(-1, t_len, order='F').T)

expected_ijk = np.array(np.unravel_index(
np.arange(n_vox), (i_len, j_len, k_len), order='F'))
np.testing.assert_array_equal(brain.index, expected_ijk)

ijk_b = np.vstack([expected_ijk, np.ones((1, n_vox))])
expected_xyz = np.dot(affine, ijk_b)[:3]
np.testing.assert_allclose(brain.xyz, expected_xyz)
self.assertEqual(brain.xyz.shape, (3, n_vox))

def test_get_roiflag_pass0002(self) -> None:
"""Test for get_roiflag (pass case 0002)."""
roi_xyz = [np.array([[1, 2, 3],
Expand Down
Loading