Skip to content

refactor(mri): replace nipy image I/O with nibabel and deprecate nipy GLM paradigm helper#138

Open
KenyaOtsuka wants to merge 2 commits into
KamitaniLab:devfrom
KenyaOtsuka:chore/deprecate-nipy-dependency
Open

refactor(mri): replace nipy image I/O with nibabel and deprecate nipy GLM paradigm helper#138
KenyaOtsuka wants to merge 2 commits into
KamitaniLab:devfrom
KenyaOtsuka:chore/deprecate-nipy-dependency

Conversation

@KenyaOtsuka

@KenyaOtsuka KenyaOtsuka commented Jun 24, 2026

Copy link
Copy Markdown

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:

  1. Replace nipy image I/O with nibabel, and deprecate the GLM paradigm helper — the main purpose of this PR.
  2. Fix the 4D affine bug and add regression tests — a small, related fix included in this PR.

I can split the bug fix into a separate PR if preferred. However, since the affected code overlaps with the image-loading refactor and the actual fix is only a few lines, I included it here. The commits are separated into refactor: and fix: commits to make the scope easier to review.

Background / Why

bdpy[mri] currently depends on nipy, which appears to be effectively unmaintained. This blocks support for newer Python versions, especially Python 3.14, because nipy does not provide a cp314 wheel. See the related discussions in #68 and #120.

Rather than removing nipy all at once, this PR takes a staged approach:

  • Replace what can be replaced: image I/O is migrated from nipy to nibabel while preserving behavior.
  • Deprecate what cannot yet be replaced cleanly: the GLM paradigm helper still returns a nipy-specific paradigm object, so it is kept for now but marked as deprecated.

What changed

1. refactor: migrate image I/O to nibabel and deprecate the GLM helper

  • Replaced nipy.load_image with nibabel.load in load_mri.py, load_epi.py, and fmriprep.py.
  • Replaced img.coordmap.affine with img.affine.

Rationale for behavioral equivalence:

  • For the target images, nibabel’s img.affine matches nipy’s img.coordmap.affine.
  • The existing image.py::export_brain_image already relies on an exact float-level comparison between the two affine representations, which supports their equivalence in the current use case.
  • I also confirmed locally that the loaded data, xyz coordinates, and ijk coordinates match between the nipy and nibabel code paths.

glm.make_paradigm is not migrated in this PR because it returns a nipy-specific paradigm object intended to be passed to the nipy GLM machinery. Instead, it now emits a DeprecationWarning and recommends migrating to nilearn in the future.

2. fix: fix the 4D affine bug and add regression tests

The 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 was nipy or nibabel.

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_mri
  • fmriprep.BrainData.__load_volume

For the latter, I confirmed that this is the real volume-loading path: create_bdata_fmriprep treats BrainData.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_4d
  • test_braindata_load_volume_4d

Intentionally not changed

  • fmriprep.__load_mri / __get_xyz

These 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_xyz does not directly reference nipy, so it is left completely unchanged. Whether these unused functions should be removed can be discussed separately.

  • pyproject.toml / the nipy dependency

    These are intentionally unchanged. Since make_paradigm still depends on nipy, the dependency remains in the mri / all extras for now. Fully removing nipy should be handled in a future release.

Testing

  • pytest tests/test_mri.py → 5 passed
  • ruff check bdpy/mri/ → no new errors; the result is unchanged before and after this PR
  • Locally confirmed that the migrated nibabel paths produce results matching the previous nipy paths

KenyaOtsuka and others added 2 commits June 24, 2026 08:12
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.affine usage with nibabel.load + img.affine in MRI/EPI/fmriprep loaders.
  • Add a DeprecationWarning to glm.make_paradigm to signal planned future removal of nipy-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.

Comment thread bdpy/mri/fmriprep.py
Comment on lines 843 to 847
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:

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.

@KenyaOtsuka KenyaOtsuka marked this pull request as ready for review June 24, 2026 08:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants