Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Copy input image header to the output by default #4397

Merged
merged 37 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
47028b9
copy header by default in crop_img
man-shu Apr 24, 2024
906a74b
mean_img copy header by default
man-shu Apr 24, 2024
2fcb3fa
threshold_img copy header by default
man-shu Apr 24, 2024
da04af3
copy header by default in binarize_img
man-shu Apr 24, 2024
82b13f3
copy header by default in resample_img and reorder_img
man-shu Apr 24, 2024
510ae96
Merge branch 'main' into fix/4393
man-shu Apr 24, 2024
a3a17ae
test crop_img header
man-shu Apr 24, 2024
7c9e364
test for equality of header fields
man-shu Apr 24, 2024
6993030
remove unecessary print statement
man-shu Apr 25, 2024
ad1277b
move testing function out of conftest
man-shu May 2, 2024
340b168
remove from conftest
man-shu May 2, 2024
59911d5
Merge branch 'main' into fix/4393
man-shu May 2, 2024
71c13f8
fix utility import
man-shu May 2, 2024
b350f58
fix import in resampling
man-shu May 2, 2024
9b4350f
add to changelog
man-shu May 2, 2024
7dc9be1
Merge branch 'main' into fix/4393
man-shu May 15, 2024
88fbc46
remove extra space
man-shu May 15, 2024
ed0b253
Add future warning about copying header by default
man-shu May 29, 2024
b0f7573
changelog entry
man-shu May 30, 2024
afccca1
Merge branch 'warning_copy_header' into fix/4393
man-shu May 30, 2024
59c34c0
add parameter to control header copying
man-shu May 30, 2024
d2f0044
update changelog
man-shu May 30, 2024
20a58f3
use a helper function to throw the warning
man-shu May 31, 2024
d7a487f
set copy header to True in tests
man-shu May 31, 2024
bdb9173
forgot updating
man-shu May 31, 2024
c6cb66c
CI blackify _utils.helpers.py
man-shu May 31, 2024
e0b24c4
forgot changing docstrings and defaults
man-shu May 31, 2024
743aecb
fix failing test
man-shu May 31, 2024
b7ae2e3
test for warning when copy_header=False
man-shu May 31, 2024
a44c21b
Merge branch 'main' into fix/4393
man-shu May 31, 2024
2869ad5
switch to copy_header=True in codebase (tests, examples)
man-shu Jun 3, 2024
ac31d33
Merge branch 'main' into fix/4393
man-shu Jun 3, 2024
9068cbd
roll back blackifying surface module
man-shu Jun 3, 2024
5fe7360
missed a updating in a few tests
man-shu Jun 3, 2024
9965c49
missed a couple of instances
man-shu Jun 3, 2024
6b9665e
update binarize_img api ref example
man-shu Jun 3, 2024
9132c6e
Merge branch 'main' into fix/4393
man-shu Jun 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 6 additions & 3 deletions nilearn/image/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ def _crop_img_to(img, slices, copy=True):
new_affine[:3, :3] = linear_part
new_affine[:3, 3] = new_origin

return new_img_like(img, cropped_data, new_affine)
return new_img_like(img, cropped_data, new_affine, copy_header=True)


def crop_img(img, rtol=1e-8, copy=True, pad=True, return_offset=False):
Expand Down Expand Up @@ -570,7 +570,9 @@ def mean_img(imgs, target_affine=None, target_shape=None, verbose=0, n_jobs=1):
running_mean += this_mean

running_mean = running_mean / float(n_imgs)
return new_img_like(first_img, running_mean, target_affine)
return new_img_like(
first_img, running_mean, target_affine, copy_header=True
)


def swap_img_hemispheres(img):
Expand Down Expand Up @@ -984,7 +986,7 @@ def threshold_img(
img_data = img_data[:, :, :, 0]

# Reconstitute img object
thresholded_img = new_img_like(img, img_data, affine)
thresholded_img = new_img_like(img, img_data, affine, copy_header=True)
man-shu marked this conversation as resolved.
Show resolved Hide resolved

return thresholded_img

Expand Down Expand Up @@ -1172,6 +1174,7 @@ def binarize_img(img, threshold=0, mask_img=None, two_sided=True):
img=threshold_img(
img, threshold, mask_img=mask_img, two_sided=two_sided
),
copy_header_from="img",
)


Expand Down
4 changes: 2 additions & 2 deletions nilearn/image/resampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ def resample_img(
vmax = max(np.nanmax(data), 0)
resampled_data.clip(vmin, vmax, out=resampled_data)

return new_img_like(img, resampled_data, target_affine)
return new_img_like(img, resampled_data, target_affine, copy_header=True)


def resample_to_img(
Expand Down Expand Up @@ -847,4 +847,4 @@ def reorder_img(img, resample=None):
data = data[slice1, slice2, slice3]
affine = from_matrix_vector(np.diag(pixdim), b)

return new_img_like(img, data, affine)
return new_img_like(img, data, affine, copy_header=True)
27 changes: 27 additions & 0 deletions nilearn/image/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Pytest fixtures for testing copied headers in nilearn.image functions."""

import numpy as np
import pytest
from nibabel.nifti1 import Nifti1Image
from numpy.testing import assert_array_equal

from nilearn import image

Expand Down Expand Up @@ -53,3 +55,28 @@ def img_4d_mni_tr2(img_4d_mni):
header = img.header.copy()
header["pixdim"][4] = 2.0
return Nifti1Image(img.get_fdata(), img.affine, header=header)


def match_headers_keys(source, target, except_keys):
man-shu marked this conversation as resolved.
Show resolved Hide resolved
"""Check if header fields of two Nifti images match, except for some keys.

Parameters
----------
source : Nifti1Image
Source image to compare headers with.
target : Nifti1Image
Target image to compare headers from.
except_keys : list of str
List of keys that should from comparison.
"""
for key in source.header.keys():
if key in except_keys:
assert (target.header[key] != source.header[key]).any()
else:
if isinstance(target.header[key], np.ndarray):
assert_array_equal(
target.header[key],
source.header[key],
)
else:
assert target.header[key] == source.header[key]
74 changes: 74 additions & 0 deletions nilearn/image/tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
swap_img_hemispheres,
threshold_img,
)
from nilearn.image.tests.conftest import match_headers_keys

X64 = platform.architecture()[0] == "64bit"

Expand Down Expand Up @@ -386,13 +387,48 @@ def test_crop_img():

cropped_img = crop_img(img)

print(img.get_fdata())

# correction for padding with "-1"
# check that correct part was extracted:
# This also corrects for padding
assert (get_data(cropped_img)[1:-1, 1:-1, 1:-1] == 1).all()
assert cropped_img.shape == (2 + 2, 4 + 2, 3 + 2)


def test_crop_img_copied_header(img_4d_mni_tr2):
# Test equality of header fields between input and output
# create zero padded data
data = np.zeros((10, 10, 10, 10))
data[0:4, 0:4, 0:4, :] = 1
# replace the img_4d_mni_tr2 values with data
img_4d_mni_tr2_zero_padded = new_img_like(
img_4d_mni_tr2,
data=data,
affine=img_4d_mni_tr2.affine,
copy_header=True,
)
cropped_img = crop_img(img_4d_mni_tr2_zero_padded)
# only dim[1:4] should be different
assert (
cropped_img.header["dim"][1:4]
!= img_4d_mni_tr2_zero_padded.header["dim"][1:4]
).all()
# other dim indices should be the same
assert (
cropped_img.header["dim"][0]
== img_4d_mni_tr2_zero_padded.header["dim"][0]
)
assert (
cropped_img.header["dim"][4:]
== img_4d_mni_tr2_zero_padded.header["dim"][4:]
).all()
# other header fields should also be same
match_headers_keys(
cropped_img, img_4d_mni_tr2_zero_padded, except_keys=["dim"]
)


def test_crop_threshold_tolerance(affine_eye):
"""Check if crop can skip values that are extremely close to zero.

Expand Down Expand Up @@ -466,6 +502,16 @@ def test_mean_img_resample(rng):
assert_array_equal(mean_img_with_resampling.affine, target_affine)


def test_mean_img_copied_header(img_4d_mni_tr2):
# Test equality of header fields between input and output
result = image.mean_img(img_4d_mni_tr2)
match_headers_keys(
result,
img_4d_mni_tr2,
except_keys=["dim", "pixdim", "cal_max", "cal_min"],
)


def test_swap_img_hemispheres(affine_eye, shape_3d_default, rng):
# make sure input image data is not overwritten inside function
data = rng.standard_normal(size=shape_3d_default)
Expand Down Expand Up @@ -797,6 +843,19 @@ def test_isnan_threshold_img_data(affine_eye, shape_3d_default):
threshold_img(maps_img, threshold=0.8)


def test_threshold_img_copied_header(img_4d_mni_tr2):
# Test equality of header fields between input and output
result = threshold_img(img_4d_mni_tr2, threshold=0.5)
# only the min value should be different
match_headers_keys(
result,
img_4d_mni_tr2,
except_keys=["cal_min"],
)
# min value should be 0 in the result
assert result.header["cal_min"] == 0


def test_math_img_exceptions(affine_eye, img_4d_ones_eye):
img1 = img_4d_ones_eye
img2 = Nifti1Image(np.zeros((10, 20, 10, 10)), affine_eye)
Expand Down Expand Up @@ -938,6 +997,21 @@ def test_binarize_negative_img(img_4d_rand_eye):
assert_array_equal(np.unique(img_original.dataobj), np.array([0, 1]))


def test_binarize_img_copied_header(img_4d_mni_tr2):
# Test equality of header fields between input and output
result = binarize_img(img_4d_mni_tr2, threshold=0.5)
# only the min value should be different
match_headers_keys(
result,
img_4d_mni_tr2,
except_keys=["cal_min", "cal_max"],
)
# min value should be 0 in the result
assert result.header["cal_min"] == 0
# max value should be 1 in the result
assert result.header["cal_max"] == 1


def test_clean_img(affine_eye, shape_3d_default, rng):
data = rng.standard_normal(size=(10, 10, 10, 100)) + 0.5
data_flat = data.T.reshape(100, -1)
Expand Down
38 changes: 38 additions & 0 deletions nilearn/image/tests/test_resampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
resample_img,
resample_to_img,
)
from nilearn.image.tests.conftest import match_headers_keys

ANGLES_TO_TEST = (0, np.pi, np.pi / 2.0, np.pi / 4.0, np.pi / 3.0)

Expand Down Expand Up @@ -351,6 +352,32 @@ def test_resampling_warning_binary_image(affine_eye, rng):
resample_img(img_binary, target_affine=rot, interpolation="linear")


def test_resample_img_copied_header(img_4d_mni_tr2):
# Test that the header is copied when resampling
result = resample_img(img_4d_mni_tr2, target_affine=np.diag((6, 6, 6)))
# pixdim[1:4] should change to [6, 6, 6]
assert (result.header["pixdim"][1:4] == np.array([6, 6, 6])).all()
# pixdim at other indices should remain the same
assert (
result.header["pixdim"][4:] == img_4d_mni_tr2.header["pixdim"][4:]
).all()
assert result.header["pixdim"][0] == img_4d_mni_tr2.header["pixdim"][0]
# dim, srow_* and min/max should also change
match_headers_keys(
img_4d_mni_tr2,
result,
except_keys=[
"pixdim",
"dim",
"cal_max",
"cal_min",
"srow_x",
"srow_y",
"srow_z",
],
)


def test_4d_affine_bounding_box_error(affine_eye):
bigger_data = np.zeros([10, 10, 10])
bigger_img = Nifti1Image(bigger_data, affine_eye)
Expand Down Expand Up @@ -840,6 +867,17 @@ def test_reorder_img_mirror():
)


def test_reorder_img_copied_header(img_4d_mni_tr2):
# Test that the header is copied when reordering
result = reorder_img(img_4d_mni_tr2)
# all header fields should stay the same
match_headers_keys(
img_4d_mni_tr2,
result,
except_keys=[],
)


def test_coord_transform_trivial(affine_eye, rng):
sform = affine_eye
x = rng.random((10,))
Expand Down