refactor(mri): replace nipy image I/O with nibabel and deprecate nipy GLM paradigm helper#138
Conversation
…d GLM paradigm Work toward dropping the unmaintained nipy dependency (KamitaniLab#68). - Replace nipy.load_image with nibabel.load and img.coordmap.affine with img.affine in load_mri, load_epi, and fmriprep volume loading. These paths no longer use nipy; behavior is unchanged (nibabel's affine matches nipy's coordmap.affine for the supported images). - Emit a DeprecationWarning from make_paradigm, which still relies on nipy's paradigm objects, pointing users toward nilearn. nipy is retained as a dependency so the GLM path keeps working until a later release removes it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 4D code path reduced the affine to a 3x3 matrix before applying it to homogeneous (4 x N) voxel indices, raising a shape-mismatch ValueError whenever a 4D image was loaded. This bug predates the nipy->nibabel swap (nipy's coordmap.affine and nibabel's affine are both 4x4, so both backends hit it). Use the full 4x4 affine for the 4D path, matching the 3D path. Fixed in the active loaders: load_mri.load_mri and fmriprep.BrainData.__load_volume. The latter is the real volume path: create_bdata_fmriprep treats BrainData.data.shape[0] as the number of volumes (time points), which only holds for the (T, N) shape the 4D branch produces. The unused, name-private fmriprep.__load_mri and __get_xyz are left untouched to keep the change minimal. Adds test_load_mri_4d and test_braindata_load_volume_4d covering the data/xyz/ijk shapes and values for a 4D volume with a non-trivial affine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR reduces bdpy[mri]’s reliance on nipy by migrating MRI/EPI image I/O to nibabel, while also fixing (and regression-testing) a long-standing 4D affine handling bug in the main 4D loading paths.
Changes:
- Replace
nipy.load_image+coordmap.affineusage withnibabel.load+img.affinein MRI/EPI/fmriprep loaders. - Add a
DeprecationWarningtoglm.make_paradigmto signal planned future removal ofnipy-dependent functionality. - Fix the 4D affine bug in the primary volume-loading paths and add regression tests covering 4D loading behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
tests/test_mri.py |
Adds regression tests for correct 4D data/xyz/index outputs for load_mri and fmriprep.BrainData volume loading. |
bdpy/mri/load_mri.py |
Switches MRI loading to nibabel and keeps full 4×4 affine for 4D loads. |
bdpy/mri/load_epi.py |
Switches EPI loading/xyz consistency checking to use nibabel and img.affine. |
bdpy/mri/glm.py |
Adds a deprecation warning to the nipy-dependent paradigm helper. |
bdpy/mri/fmriprep.py |
Migrates volume loading to nibabel and fixes the 4D affine handling in BrainData.__load_volume (but leaves a remaining 4D affine bug in __load_mri). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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: |
There was a problem hiding this comment.
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.
Summary
This PR mainly aims to gradually reduce the dependency on
nipy, while also including a small fix for a 4D image-loading affine bug found during the refactoring. The changes can be grouped into two parts:nipyimage I/O withnibabel, and deprecate the GLM paradigm helper — the main purpose of this PR.Background / Why
bdpy[mri]currently depends onnipy, which appears to be effectively unmaintained. This blocks support for newer Python versions, especially Python 3.14, becausenipydoes not provide acp314wheel. See the related discussions in #68 and #120.Rather than removing
nipyall at once, this PR takes a staged approach:nipytonibabelwhile preserving behavior.nipy-specific paradigm object, so it is kept for now but marked as deprecated.What changed
1.
refactor:migrate image I/O tonibabeland deprecate the GLM helpernipy.load_imagewithnibabel.loadinload_mri.py,load_epi.py, andfmriprep.py.img.coordmap.affinewithimg.affine.Rationale for behavioral equivalence:
nibabel’simg.affinematchesnipy’simg.coordmap.affine.image.py::export_brain_imagealready relies on an exact float-level comparison between the two affine representations, which supports their equivalence in the current use case.nipyandnibabelcode paths.glm.make_paradigmis not migrated in this PR because it returns anipy-specific paradigm object intended to be passed to thenipyGLM machinery. Instead, it now emits aDeprecationWarningand recommends migrating tonilearnin the future.2.
fix:fix the 4D affine bug and add regression testsThe 4D loading path previously reduced the affine matrix from 4×4 to 3×3, but the downstream code multiplies it by homogeneous coordinates of shape 4×N. As a result, loading a 4D image always raised a
ValueError. This was an existing bug independent of whether the backend wasnipyornibabel.This PR fixes the bug by keeping the full 4×4 affine matrix.
The fix is applied only to the two actually used code paths:
load_mri.load_mrifmriprep.BrainData.__load_volumeFor the latter, I confirmed that this is the real volume-loading path:
create_bdata_fmripreptreatsBrainData.data.shape[0]as the number of volumes / time points, so the 4D branch returning data of shape(T, N)is the intended behavior.Regression tests added:
test_load_mri_4dtest_braindata_load_volume_4dIntentionally not changed
fmriprep.__load_mri/__get_xyzThese appear to be unused code paths with no caller in the current codebase. Although their affine-handling logic looks similar to the code changed in the active loading paths, I could not determine whether the current behavior is actually incorrect or intentional, because there is no concrete use case to verify against.
I therefore left them unchanged in this PR to avoid introducing an unverified behavior change. They can be revisited separately if needed.
__get_xyzdoes not directly referencenipy, so it is left completely unchanged. Whether these unused functions should be removed can be discussed separately.pyproject.toml/ thenipydependencyThese are intentionally unchanged. Since
make_paradigmstill depends onnipy, the dependency remains in themri/allextras for now. Fully removingnipyshould be handled in a future release.Testing
pytest tests/test_mri.py→ 5 passedruff check bdpy/mri/→ no new errors; the result is unchanged before and after this PRnibabelpaths produce results matching the previousnipypaths