Skip to content
Closed
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
3 changes: 1 addition & 2 deletions .github/workflows/compat_minimal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ jobs:
run:
shell: bash
env:
# TODO: Revert nibabel here pending https://github.com/mne-tools/mne-python/issues/11564
CONDA_DEPENDENCIES: 'numpy scipy matplotlib nibabel'
CONDA_DEPENDENCIES: 'numpy scipy matplotlib'
DEPS: 'minimal'
DISPLAY: ':99.0'
MNE_DONTWRITE_HOME: true
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ The minimum required dependencies to run MNE-Python are:
- NumPy >= 1.20.2
- SciPy >= 1.6.3
- Matplotlib >= 3.4.0
- NiBabel >= 3.2.1
Copy link
Member

Choose a reason for hiding this comment

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

we discovered this in HNN. Looks like nibabel is a core dependency now but it's not reflected in the setup.py: https://github.com/mne-tools/mne-python/blob/main/requirements_base.txt

Copy link
Member Author

Choose a reason for hiding this comment

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

Can you give a traceback? We ended up not making it a core dependency

Copy link
Member

Choose a reason for hiding this comment

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

here you go: jonescompneurolab/hnn-core#649 (comment)

should we be installing nibabel in our CIs separately?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, nibabel is an optional dependency not a core one, so you have to install it separately. PR welcome to change the wording of the error message to say something about it being optional and to consider installing it

raise ImportError(f"The {lib} package{extra} is required to {what}, " f"{why}")

- pooch >= 1.5
- tqdm
- Jinja2
Expand All @@ -104,7 +105,6 @@ For full functionality, some functions require:
- PySide2 >= 5.12

- Numba >= 0.53.1
- NiBabel >= 3.2.1
- OpenMEEG >= 2.5.5
- Pandas >= 1.2.4
- Picard >= 0.3
Expand Down
84 changes: 10 additions & 74 deletions mne/_freesurfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import os.path as op
import numpy as np
from gzip import GzipFile
from pathlib import Path

from .bem import _bem_find_surface, read_bem_surfaces
Expand All @@ -17,7 +16,7 @@
_ensure_trans, read_ras_mni_t, Transform)
from .surface import read_surface, _read_mri_surface
from .utils import (verbose, _validate_type, _check_fname, _check_option,
get_subjects_dir, _require_version, logger)
get_subjects_dir, logger)


def _check_subject_dir(subject, subjects_dir):
Expand All @@ -33,7 +32,6 @@ def _check_subject_dir(subject, subjects_dir):

def _get_aseg(aseg, subject, subjects_dir):
"""Check that the anatomical segmentation file exists and load it."""
_require_version('nibabel', 'load aseg', '2.1.0')
import nibabel as nib

subjects_dir = Path(get_subjects_dir(subjects_dir, raise_error=True))
Expand All @@ -50,18 +48,6 @@ def _get_aseg(aseg, subject, subjects_dir):
return aseg, aseg_data


def _import_nibabel(why='use MRI files'):
try:
import nibabel as nib
except ImportError as exp:
msg = 'nibabel is required to %s, got:\n%s' % (why, exp)
else:
msg = ''
if msg:
raise ImportError(msg)
return nib


def _reorient_image(img, axcodes='RAS'):
"""Reorient an image to a given orientation.

Expand Down Expand Up @@ -128,8 +114,6 @@ def _mri_orientation(orientation):

def _get_mri_info_data(mri, data):
# Read the segmentation data using nibabel
if data:
_import_nibabel('load MRI atlas data')
out = dict()
_, out['vox_mri_t'], out['mri_ras_t'], dims, _, mgz = _read_mri_info(
mri, return_img=True)
Expand All @@ -143,45 +127,6 @@ def _get_mri_info_data(mri, data):
return out


def _get_mgz_header(fname):
"""Adapted from nibabel to quickly extract header info."""
fname = _check_fname(fname, overwrite='read', must_exist=True,
name='MRI image')
if fname.suffix != ".mgz":
raise IOError('Filename must end with .mgz')
header_dtd = [('version', '>i4'), ('dims', '>i4', (4,)),
('type', '>i4'), ('dof', '>i4'), ('goodRASFlag', '>i2'),
('delta', '>f4', (3,)), ('Mdc', '>f4', (3, 3)),
('Pxyz_c', '>f4', (3,))]
header_dtype = np.dtype(header_dtd)
with GzipFile(fname, 'rb') as fid:
hdr_str = fid.read(header_dtype.itemsize)
header = np.ndarray(shape=(), dtype=header_dtype,
buffer=hdr_str)
# dims
dims = header['dims'].astype(int)
dims = dims[:3] if len(dims) == 4 else dims
# vox2ras_tkr
delta = header['delta']
ds = np.array(delta, float)
ns = np.array(dims * ds) / 2.0
v2rtkr = np.array([[-ds[0], 0, 0, ns[0]],
[0, 0, ds[2], -ns[2]],
[0, -ds[1], 0, ns[1]],
[0, 0, 0, 1]], dtype=np.float32)
# ras2vox
d = np.diag(delta)
pcrs_c = dims / 2.0
Mdc = header['Mdc'].T
pxyz_0 = header['Pxyz_c'] - np.dot(Mdc, np.dot(d, pcrs_c))
M = np.eye(4, 4)
M[0:3, 0:3] = np.dot(Mdc, d)
M[0:3, 3] = pxyz_0.T
header = dict(dims=dims, vox2ras_tkr=v2rtkr, vox2ras=M,
zooms=header['delta'])
return header


def _get_atlas_values(vol_info, rr):
# Transform MRI coordinates (where our surfaces live) to voxels
rr_vox = apply_trans(vol_info['mri_vox_t'], rr)
Expand Down Expand Up @@ -610,22 +555,13 @@ def _check_mri(mri, subject, subjects_dir):
return mri


def _read_mri_info(path, units='m', return_img=False, use_nibabel=False):
# This is equivalent but 100x slower, so only use nibabel if we need to
# (later):
if use_nibabel:
import nibabel
hdr = nibabel.load(path).header
n_orig = hdr.get_vox2ras()
t_orig = hdr.get_vox2ras_tkr()
dims = hdr.get_data_shape()
zooms = hdr.get_zooms()[:3]
else:
hdr = _get_mgz_header(path)
n_orig = hdr['vox2ras']
t_orig = hdr['vox2ras_tkr']
dims = hdr['dims']
zooms = hdr['zooms']
def _read_mri_info(path, units='m', return_img=False):
import nibabel as nib
hdr = nib.load(path).header
n_orig = hdr.get_vox2ras()
t_orig = hdr.get_vox2ras_tkr()
dims = hdr.get_data_shape()
zooms = hdr.get_zooms()[:3]

# extract the MRI_VOXEL to RAS (non-zero origin) transform
vox_ras_t = Transform('mri_voxel', 'ras', n_orig)
Expand All @@ -648,8 +584,8 @@ def _read_mri_info(path, units='m', return_img=False, use_nibabel=False):

out = (vox_ras_t, vox_mri_t, mri_ras_t, dims, zooms)
if return_img:
nibabel = _import_nibabel()
out += (nibabel.load(path),)
import nibabel as nib
out += (nib.load(path),)
return out


Expand Down
4 changes: 2 additions & 2 deletions mne/bem.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from .transforms import _ensure_trans, apply_trans, Transform
from .utils import (verbose, logger, run_subprocess, get_subjects_dir, warn,
_pl, _validate_type, _TempDir, _check_freesurfer_home,
_check_fname, has_nibabel, _check_option, path_like,
_check_fname, _check_option, path_like,
_on_missing, _import_h5io_funcs, _ensure_int,
_path_like, _verbose_safe_false, _check_head_radius)

Expand Down Expand Up @@ -1283,7 +1283,7 @@ def make_watershed_bem(subject, subjects_dir=None, overwrite=False,
run_subprocess_env(cmd)
del tempdir # clean up directory
if op.isfile(T1_mgz):
new_info = _extract_volume_info(T1_mgz) if has_nibabel() else dict()
new_info = _extract_volume_info(T1_mgz)
if not new_info:
warn('nibabel is not available or the volume info is invalid.'
'Volume info not updated in the written surface.')
Expand Down
6 changes: 1 addition & 5 deletions mne/commands/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
mne_prepare_bem_model, mne_sys_info)
from mne.datasets import testing
from mne.io import read_raw_fif, read_info
from mne.utils import (requires_mne, requires_freesurfer,
requires_nibabel, ArgvSetter,
from mne.utils import (requires_mne, requires_freesurfer, ArgvSetter,
_stamp_to_dt, _record_warnings)

base_dir = op.join(op.dirname(__file__), '..', '..', 'io', 'tests', 'data')
Expand Down Expand Up @@ -138,7 +137,6 @@ def test_kit2fiff():
@testing.requires_testing_data
def test_make_scalp_surfaces(tmp_path, monkeypatch):
"""Test mne make_scalp_surfaces."""
pytest.importorskip('nibabel')
pytest.importorskip('pyvista')
check_usage(mne_make_scalp_surfaces)
has = 'SUBJECTS_DIR' in os.environ
Expand Down Expand Up @@ -199,7 +197,6 @@ def test_maxfilter():
@testing.requires_testing_data
def test_report(tmp_path):
"""Test mne report."""
pytest.importorskip('nibabel')
check_usage(mne_report)
tempdir = str(tmp_path)
use_fname = op.join(tempdir, op.basename(raw_fname))
Expand All @@ -220,7 +217,6 @@ def test_surf2bem():
@pytest.mark.timeout(900) # took ~400 s on a local test
@pytest.mark.slowtest
@pytest.mark.ultraslowtest
@requires_nibabel()
@requires_freesurfer('mri_watershed')
@testing.requires_testing_data
def test_watershed_bem(tmp_path):
Expand Down
2 changes: 0 additions & 2 deletions mne/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,6 @@ def _fwd_surf(_evoked_cov_sphere):
@pytest.fixture(scope='session')
def _fwd_subvolume(_evoked_cov_sphere):
"""Compute a forward for a surface source space."""
pytest.importorskip('nibabel')
evoked, cov, sphere = _evoked_cov_sphere
volume_labels = ['Left-Cerebellum-Cortex', 'right-Cerebellum-Cortex']
with pytest.raises(ValueError,
Expand Down Expand Up @@ -701,7 +700,6 @@ def mixed_fwd_cov_evoked(_evoked_cov_sphere, _all_src_types_fwd):
@pytest.mark.parametrize(params=[testing._pytest_param()])
def src_volume_labels():
"""Create a 7mm source space with labels."""
pytest.importorskip('nibabel')
volume_labels = mne.get_volume_labels_from_aseg(fname_aseg)
with pytest.warns(RuntimeWarning, match='Found no usable.*Left-vessel.*'):
src = mne.setup_volume_source_space(
Expand Down
16 changes: 4 additions & 12 deletions mne/coreg.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
# namespace, too)
from ._freesurfer import (_read_mri_info, get_mni_fiducials, # noqa: F401
estimate_head_mri_t) # noqa: F401
from ._freesurfer import _import_nibabel
from .label import read_label, Label
from .source_space import (add_source_space_distances, read_source_spaces, # noqa: E501,F401
write_source_spaces)
Expand All @@ -41,7 +40,7 @@
rot_to_quat, _angle_between_quats)
from .channels import make_dig_montage
from .utils import (get_config, get_subjects_dir, logger, pformat, verbose,
warn, has_nibabel, fill_doc, _validate_type,
warn, fill_doc, _validate_type,
_check_subject, _check_option)
from .viz._3d import _fiducial_coords

Expand Down Expand Up @@ -1168,35 +1167,28 @@ def scale_source_space(subject_to, src_name, subject_from=None, scale=None,

def _scale_mri(subject_to, mri_fname, subject_from, scale, subjects_dir):
"""Scale an MRI by setting its affine."""
import nibabel as nib
subjects_dir, subject_from, scale, _ = _scale_params(
subject_to, subject_from, scale, subjects_dir)
nibabel = _import_nibabel('scale an MRI')
fname_from = op.join(mri_dirname.format(
subjects_dir=subjects_dir, subject=subject_from), mri_fname)
fname_to = op.join(mri_dirname.format(
subjects_dir=subjects_dir, subject=subject_to), mri_fname)
img = nibabel.load(fname_from)
img = nib.load(fname_from)
zooms = np.array(img.header.get_zooms())
zooms[[0, 2, 1]] *= scale
img.header.set_zooms(zooms)
# Hack to fix nibabel problems, see
# https://github.com/nipy/nibabel/issues/619
img._affine = img.header.get_affine() # or could use None
nibabel.save(img, fname_to)
nib.save(img, fname_to)


def _scale_xfm(subject_to, xfm_fname, mri_name, subject_from, scale,
subjects_dir):
"""Scale a transform."""
subjects_dir, subject_from, scale, _ = _scale_params(
subject_to, subject_from, scale, subjects_dir)

# The nibabel warning should already be there in MRI step, if applicable,
# as we only get here if T1.mgz is present (and thus a scaling was
# attempted) so we can silently return here.
if not has_nibabel():
return

fname_from = os.path.join(
mri_transforms_dirname.format(
subjects_dir=subjects_dir, subject=subject_from), xfm_fname)
Expand Down
6 changes: 2 additions & 4 deletions mne/forward/tests/test_make_forward.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@
get_volume_labels_from_aseg)
from mne.surface import _get_ico_surface
from mne.transforms import Transform
from mne.utils import (requires_mne, requires_nibabel, run_subprocess,
catch_logging, requires_mne_mark,
requires_openmeeg_mark)
from mne.utils import (requires_mne, run_subprocess, catch_logging,
requires_mne_mark, requires_openmeeg_mark)
from mne.forward._make_forward import _create_meg_coils, make_forward_dipole
from mne.forward._compute_forward import _magnetic_dipole_field_vec
from mne.forward import Forward, _do_forward_solution, use_coil_def
Expand Down Expand Up @@ -433,7 +432,6 @@ def test_make_forward_solution_sphere(tmp_path, fname_src_small):

@pytest.mark.slowtest
@testing.requires_testing_data
@requires_nibabel()
def test_forward_mixed_source_space(tmp_path):
"""Test making the forward solution for a mixed source space."""
# get the surface source space
Expand Down
3 changes: 1 addition & 2 deletions mne/gui/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from matplotlib.figure import Figure
from matplotlib.patches import Rectangle

from .._freesurfer import _import_nibabel
from ..viz.backends.renderer import _get_renderer
from ..viz.utils import safe_event
from ..surface import _read_mri_surface, _marching_cubes
Expand All @@ -35,7 +34,7 @@
@verbose
def _load_image(img, verbose=None):
"""Load data from a 3D image file (e.g. CT, MR)."""
nib = _import_nibabel('use GUI')
import nibabel as nib
if not isinstance(img, nib.spatialimages.SpatialImage):
logger.debug(f'Loading {img}')
_check_fname(img, overwrite='read', must_exist=True)
Expand Down
5 changes: 2 additions & 3 deletions mne/gui/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,21 @@
import numpy as np
from numpy.testing import assert_allclose

import nibabel as nib
import pytest

from mne.datasets import testing
from mne.utils import requires_nibabel, catch_logging, use_log_level
from mne.utils import catch_logging, use_log_level
from mne.viz.utils import _fake_click

data_path = testing.data_path(download=False)
subject = "sample"
subjects_dir = data_path / "subjects"


@requires_nibabel()
@testing.requires_testing_data
def test_slice_browser_io(renderer_interactive_pyvistaqt):
"""Test the input/output of the slice browser GUI."""
import nibabel as nib
from mne.gui._core import SliceBrowser
with pytest.raises(ValueError, match='Base image is not aligned to MRI'):
SliceBrowser(nib.MGHImage(
Expand Down
7 changes: 2 additions & 5 deletions mne/gui/tests/test_ieeg_locate.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
import numpy as np
from numpy.testing import assert_allclose

import nibabel as nib
import pytest

import mne
from mne.datasets import testing
from mne.transforms import apply_trans
from mne.utils import requires_nibabel, requires_version, use_log_level
from mne.utils import requires_version, use_log_level
from mne.viz.utils import _fake_click

data_path = testing.data_path(download=False)
Expand All @@ -22,11 +23,9 @@
fname_trans = sample_dir / "sample_audvis_trunc-trans.fif"


@requires_nibabel()
@pytest.fixture
def _fake_CT_coords(skull_size=5, contact_size=2):
"""Make somewhat realistic CT data with contacts."""
import nibabel as nib
brain = nib.load(subjects_dir / subject / "mri" / "brain.mgz")
verts = mne.read_surface(
subjects_dir / subject / "bem" / "outer_skull.surf"
Expand Down Expand Up @@ -59,10 +58,8 @@ def _fake_CT_coords(skull_size=5, contact_size=2):
return ct, coords


@requires_nibabel()
def test_ieeg_elec_locate_io(renderer_interactive_pyvistaqt):
"""Test the input/output of the intracranial location GUI."""
import nibabel as nib
import mne.gui
info = mne.create_info([], 1000)

Expand Down
Loading