From 396c10a97f76fb242156f9cc326ef197059b2a4b Mon Sep 17 00:00:00 2001 From: Gregory Lee Date: Wed, 22 Sep 2021 23:38:53 -0400 Subject: [PATCH 1/8] Handle affine, reorienting as close to RAS+ as possible reorder data axes to RAS before passing onto Napari. This avoids potential issues in affine handling: https://github.com/napari/napari/issues/3410 We also now drop the non-diagonal (rotation, shear) components from the affine, since napari does not currently handle these. oi --- napari_nibabel/nibabel.py | 162 +++++++++++++++++++++++++++++--------- 1 file changed, 125 insertions(+), 37 deletions(-) diff --git a/napari_nibabel/nibabel.py b/napari_nibabel/nibabel.py index 10dd78d..b3cb91a 100644 --- a/napari_nibabel/nibabel.py +++ b/napari_nibabel/nibabel.py @@ -19,10 +19,96 @@ from nibabel.imageclasses import all_image_classes from nibabel.filename_parser import splitext_addext +from nibabel.orientations import (io_orientation, inv_ornt_aff, + apply_orientation) +valid_volume_exts = {klass.valid_exts for klass in all_image_classes} +valid_volume_exts = set(functools.reduce(operator.add, valid_volume_exts)) + + +def reorder_axes_to_ras(affine, data): + """Permutes data dimensions and updates the affine accordingly. + + Reorders and/or flips data axes to get the spatial axes in RAS+ order. + For oblique scans the axes closest to RAS (Right-Anterior-Superior) as + determined by ``nibabel.affine.io_orientation``. + + Parameters + ---------- + affine : (4, 4) ndarray + Affine matrix + data : ndarray + Data array. Spatial dimensions must be first. + + Returns + ------- + affine_ras : (4, 4) ndarray + The affine matrix for the RAS-space data. + data_ras : (4, 4) ndarray + Data with axes reordered and/or flipped to RAS+ order. + + Notes + ----- + Adapted from code converting to LAS+ in nibabel's parrec2nii.py + """ + + # Reorient data block to RAS+ if necessary + ornt = io_orientation(affine) + if np.all(ornt == [[0, 1], + [1, 1], + [2, 1]]): + # already in desired orientation + return affine, data + + # Reorient to RAS+ + t_aff = inv_ornt_aff(ornt, data.shape) + affine_ras = np.dot(affine, t_aff) + + ornt = np.asarray(ornt) + data_ras = apply_orientation(data, ornt) + return affine_ras, data_ras + + +def adjust_translation(affine, affine_plumb, data_shape): + """Adjust translation vector of affine_plumb. + + The goal is to have affine_plumb result in the same data center + point in world coordinates as the original affine. + + Parameters + ---------- + affine : ndarray + The shape (4, 4) affine matrix read in by nibabel. + affine_plumb: ndarray + The affine after permutation to RAS+ space followed by discarding + of any rotation/shear elements. + data_shape : tuple of int + The shape of the data array + + Returns + ------- + affine_plumb : ndarray + A copy of affine_plumb with the 3 translation elements updated. + """ + data_shape = data_shape[-3:] + if len(data_shape) < 3: + # TODO: prepend or append? + data_shape = data_shape + (1,) * (3 - data.ndim) + + # get center in world coordinates for the original RAS+ affine + center_ijk = (np.array(data_shape) - 1) / 2 + center_world = np.dot(affine[:3, :3], center_ijk) + affine[:3, 3] + + # make a copy to avoid in-place modification of affine_plumb + affine_plumb = affine_plumb.copy() + + # center in world coordinates with the current affine_plumb + center_world_plumb = np.dot(affine_plumb[:3, :3], center_ijk) + + # adjust the translation elements + affine_plumb[:3, 3] = center_world - center_world_plumb + return affine_plumb -all_valid_exts = {klass.valid_exts for klass in all_image_classes} -all_valid_exts = set(functools.reduce(operator.add, all_valid_exts)) @napari_hook_implementation def napari_get_reader(path): @@ -48,7 +134,7 @@ def napari_get_reader(path): froot, ext, addext = splitext_addext(path) # if we know we cannot read the file, we immediately return None. - if not ext.lower() in all_valid_exts: + if not ext.lower() in valid_volume_exts: return None # otherwise we return the *function* that can read ``path``. @@ -82,18 +168,29 @@ def reader_function(path): paths = [path] if isinstance(path, str) else path n_spatial = 3 + # note: we don't squeeze the data below, so 2D data will be 3D with 1 slice if len(paths) > 1: # load all files into a single array objects = [nib.load(_path) for _path in paths] header = objects[0].header affine = objects[0].affine - if not all([_obj.shape == _obj[0].shape for _obj in objects]): + if not all([_obj.shape == objects[0].shape for _obj in objects]): raise ValueError( "all selected files must contain data of the same shape") + if not all(np.allclose(affine, _obj.affine) for _obj in objects): + raise ValueError( + "all selected files must share a common affine") + arrays = [_obj.get_fdata() for _obj in objects] + # apply same transform to all volumes in the stack + affine_orig = affine.copy() + for i, arr in enumerate(arrays): + affine, arr_ras = reorder_axes_to_ras(affine_orig, arr) + arrays[i] = arr_ras + # stack arrays into single array data = np.stack(arrays) else: @@ -102,6 +199,8 @@ def reader_function(path): affine = img.affine data = img.get_fdata() # keep this as dataobj or use get_fdata()? + affine, data = reorder_axes_to_ras(affine, data) + spatial_axis_order = tuple(range(n_spatial)) if data.ndim > 3: # nibabel formats have spatial axes in the first 3 positions, but @@ -114,43 +213,32 @@ def reader_function(path): if spatial_axis_order != (0, 1, 2): data = data.transpose(spatial_axis_order[:data.ndim]) - try: - # only get zooms for the spatial axes - zooms = np.asarray(header.get_zooms())[:n_spatial] - if np.any(zooms == 0): - raise ValueError("invalid zoom = 0 found in header") - # normalize so values are all >= 1.0 (not strictly necessary) - # zooms = zooms / zooms.min() - zooms = tuple(zooms) - if data.ndim > 3: - zooms = (1.0, ) * (data.ndim - n_spatial) + zooms - except (AttributeError, ValueError): - zooms = (1.0, ) * data.ndim - - apply_translation = False - if apply_translation: - translate = tuple(affine[:n_spatial, 3]) - if data.ndim > 3: - # set translate = 0.0 on non-spatial dimensions - translate = (0.0,) * (data.ndim - n_spatial) + translate + if np.all(affine[:3, :3] == (np.eye(3) * affine[:3, :3])): + # no rotation or shear components + affine_plumb = affine else: - translate = (0.0,) * data.ndim + # Set any remaining non-diagonal elements of the affine to 0 + # (napari currently cannot display with rotate/shear) + affine_plumb = np.diag(np.diag(affine)) + + # Set translation elements of affine_plumb to get the center of the + # data cube in the same position in world coordinates + affine_plumb = adjust_translation(affine, affine_plumb, data.shape) + + # Note: The translate, scale, rotate, shear kwargs correspond to the + # 'data2physical' component of a composite affine transform. + # https://github.com/napari/napari/blob/v0.4.11/napari/layers/base/base.py#L254-L268 #noqa + # However, the affine kwarg corresponds instead to the 'physical2world' + # affine. Here, we will extract the scale and translate components from + # affine_plumb so that we are specifying 'data2physical' to napari. - # optional kwargs for the corresponding viewer.add_* method - # https://napari.org/docs/api/napari.components.html#module-napari.components.add_layers_mixin - # see also: https://napari.org/tutorials/fundamentals/image add_kwargs = dict( metadata=dict(affine=affine, header=header), rgb=False, - scale=zooms, - translate=translate, - # contrast_limits=, + scale=np.diag(affine_plumb[:3, :3]), + translate=affine_plumb[:3, 3], + affine=None, + channel_axis=None, ) - # TODO: potential kwargs to set for viewer.add_image - # contrast_limits kwarg based on info in image header? - # e.g. for NIFTI: nii.header._structarr['cal_min'] - # nii.header._structarr['cal_max'] - - layer_type = "image" # optional, default is "image" - return [(data, add_kwargs, layer_type)] + return [(data, add_kwargs, "image")] From 7e4ee6309048704c01b6a9482a0aa80e2ee5f641 Mon Sep 17 00:00:00 2001 From: Gregory Lee Date: Wed, 6 Oct 2021 14:31:19 -0400 Subject: [PATCH 2/8] TST: test intended behavior when reading a list of paths --- napari_nibabel/_tests/test_nibabel.py | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/napari_nibabel/_tests/test_nibabel.py b/napari_nibabel/_tests/test_nibabel.py index 688c724..36ea73a 100644 --- a/napari_nibabel/_tests/test_nibabel.py +++ b/napari_nibabel/_tests/test_nibabel.py @@ -1,5 +1,7 @@ +import atexit import os import shutil +import tempfile import nibabel as nib import numpy as np @@ -168,3 +170,38 @@ def test_analyze_hdr_only(): filename = os.path.join(data_path, 'analyze.hdr') with pytest.raises(FileNotFoundError): _test_basic_read(filename) + + +def test_read_filelist(): + filename = os.path.join(data_path, 'example4d.nii.gz') + n_files = 3 + data = _test_basic_read([filename,] * n_files) + assert data.ndim == 5 + assert data.shape[0] == n_files + + +def test_read_filelist_mismatched_shape(): + # cannot stack multiple files when the shapes are different + filename = os.path.join(data_path, 'example_nifti2.nii.gz') + filename2 = os.path.join(data_path, 'example4d.nii.gz') + with pytest.raises(ValueError): + _test_basic_read([filename, filename2]) + + +def test_read_filelist_mismatched_affine(): + # cannot stack multiple files when the shapes are different + tmp_dir = tempfile.mkdtemp() + atexit.register(shutil.rmtree, tmp_dir) + + filename = os.path.join(data_path, 'anatomical.nii') + nii1 = nib.load(filename) + data = nii1.get_fdata() + affine2 = nii1.affine.copy() + affine2[0, 0] *= 2 + affine2[1, 1] *= -1 + nii2 = nib.Nifti1Image(data, affine=affine2, header=nii1.header) + filename2 = os.path.join(tmp_dir, 'anatomical_affine2.nii') + nii2.to_filename(filename2) + + with pytest.raises(ValueError): + _test_basic_read([filename, filename2]) From 20a35cf6664f2db3dde49571a58b1d582afe25a9 Mon Sep 17 00:00:00 2001 From: Gregory Lee Date: Wed, 6 Oct 2021 16:45:19 -0400 Subject: [PATCH 3/8] use SPL ordering so initial view is axial with the brain upright patient left will be to the right side of the screen (radiological orientation) verified that for each dimensions, moving the slider to the right moves to S, P or L as expected, depending on the visible dimensions --- napari_nibabel/nibabel.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/napari_nibabel/nibabel.py b/napari_nibabel/nibabel.py index b3cb91a..4f250d8 100644 --- a/napari_nibabel/nibabel.py +++ b/napari_nibabel/nibabel.py @@ -225,6 +225,18 @@ def reader_function(path): # data cube in the same position in world coordinates affine_plumb = adjust_translation(affine, affine_plumb, data.shape) + # convert from RAS to SPL + affine_plumb_spl = affine_plumb.copy() + # update scales for RAS -> SPL + affine_plumb_spl[0, 0] = affine_plumb[2, 2] # move S to first + affine_plumb_spl[1, 1] = -affine_plumb[1, 1] # flip A->P + affine_plumb_spl[2, 2] = -affine_plumb[0, 0] # flip R->L, move L last + # make the same order and sign flips to the translations + affine_plumb_spl[:3, 3] = affine_plumb[2::-1, 3] + affine_plumb_spl[2:4, 3] *= -1 # flip R->L, A->P + # reverse order of the last 3 data dimensions correspondingly + data = data.transpose(tuple(range(0, data.ndim - 3)) + (-1, -2, -3)) + # Note: The translate, scale, rotate, shear kwargs correspond to the # 'data2physical' component of a composite affine transform. # https://github.com/napari/napari/blob/v0.4.11/napari/layers/base/base.py#L254-L268 #noqa @@ -235,8 +247,8 @@ def reader_function(path): add_kwargs = dict( metadata=dict(affine=affine, header=header), rgb=False, - scale=np.diag(affine_plumb[:3, :3]), - translate=affine_plumb[:3, 3], + scale=np.diag(affine_plumb_spl[:3, :3]), + translate=affine_plumb_spl[:3, 3], affine=None, channel_axis=None, ) From 26f9b4fd6d86683f753bbdb0316a9eea1e84a06b Mon Sep 17 00:00:00 2001 From: Gregory Lee Date: Wed, 6 Oct 2021 16:57:31 -0400 Subject: [PATCH 4/8] fix error in translation sign --- napari_nibabel/nibabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napari_nibabel/nibabel.py b/napari_nibabel/nibabel.py index 4f250d8..a6a241e 100644 --- a/napari_nibabel/nibabel.py +++ b/napari_nibabel/nibabel.py @@ -233,7 +233,7 @@ def reader_function(path): affine_plumb_spl[2, 2] = -affine_plumb[0, 0] # flip R->L, move L last # make the same order and sign flips to the translations affine_plumb_spl[:3, 3] = affine_plumb[2::-1, 3] - affine_plumb_spl[2:4, 3] *= -1 # flip R->L, A->P + affine_plumb_spl[1:3, 3] *= -1 # flip R->L, A->P # reverse order of the last 3 data dimensions correspondingly data = data.transpose(tuple(range(0, data.ndim - 3)) + (-1, -2, -3)) From 72d6dbb9948a71bcd234860c81cce92f17521047 Mon Sep 17 00:00:00 2001 From: Gregory Lee Date: Fri, 15 Oct 2021 15:34:46 -0400 Subject: [PATCH 5/8] Remove erroneous RAS->SPL transform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit update reorder_axes to take a specified target orientation instead of assuming RAS Use LPS rather than SPL for now so that affine_plumb[:3, :3] will be diagonal. Once appropriate fixes for perfmuted axes have been made to napari, we can switch to SPL Co-authored-by: Christopher Nauroth-Kreß <56394171+ch-n@users.noreply.github.com> --- napari_nibabel/nibabel.py | 70 +++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/napari_nibabel/nibabel.py b/napari_nibabel/nibabel.py index a6a241e..863ff7d 100644 --- a/napari_nibabel/nibabel.py +++ b/napari_nibabel/nibabel.py @@ -17,20 +17,20 @@ from napari_plugin_engine import napari_hook_implementation +from nibabel import orientations from nibabel.imageclasses import all_image_classes from nibabel.filename_parser import splitext_addext -from nibabel.orientations import (io_orientation, inv_ornt_aff, - apply_orientation) valid_volume_exts = {klass.valid_exts for klass in all_image_classes} valid_volume_exts = set(functools.reduce(operator.add, valid_volume_exts)) -def reorder_axes_to_ras(affine, data): +def reorder_axes(affine, data, target=('L', 'P', 'S')): """Permutes data dimensions and updates the affine accordingly. - Reorders and/or flips data axes to get the spatial axes in RAS+ order. - For oblique scans the axes closest to RAS (Right-Anterior-Superior) as + Reorders and/or flips data axes to get the spatial axes in SPL order. + + For oblique scans the axes closest the desired orientation are determined by ``nibabel.affine.io_orientation``. Parameters @@ -39,34 +39,36 @@ def reorder_axes_to_ras(affine, data): Affine matrix data : ndarray Data array. Spatial dimensions must be first. + target : 3-tuple of str + Tuple containing the 'S' or 'I', 'P' or 'A' and 'R' or 'L' in the + desired order. The default of ('S', 'P', 'L') means that we will order + the data array so the first axis runs from I->S, the second from + A->P and the third from L->R. Returns ------- - affine_ras : (4, 4) ndarray - The affine matrix for the RAS-space data. + affine_target : (4, 4) ndarray + The affine matrix for the data in the target space. data_ras : (4, 4) ndarray - Data with axes reordered and/or flipped to RAS+ order. - - Notes - ----- - Adapted from code converting to LAS+ in nibabel's parrec2nii.py + Data with axes reordered and/or flipped to the target space. """ - - # Reorient data block to RAS+ if necessary - ornt = io_orientation(affine) - if np.all(ornt == [[0, 1], - [1, 1], - [2, 1]]): + current_ornt = orientations.io_orientation(affine) + target_ornt = orientations.axcodes2ornt(target) + if np.array_equal(current_ornt, target_ornt): # already in desired orientation return affine, data - # Reorient to RAS+ - t_aff = inv_ornt_aff(ornt, data.shape) - affine_ras = np.dot(affine, t_aff) + # determine the transform needed to get to the target orientation + transform_ornt = orientations.ornt_transform(current_ornt, target_ornt) - ornt = np.asarray(ornt) - data_ras = apply_orientation(data, ornt) - return affine_ras, data_ras + # update the data array to the desired orientation + data_reoriented = orientations.apply_orientation(data, transform_ornt) + + # Update the affine to the target orientation + t_aff = orientations.inv_ornt_aff(transform_ornt, data.shape) + affine_reoriented = affine.dot(t_aff) + + return affine_reoriented, data_reoriented def adjust_translation(affine, affine_plumb, data_shape): @@ -188,7 +190,7 @@ def reader_function(path): # apply same transform to all volumes in the stack affine_orig = affine.copy() for i, arr in enumerate(arrays): - affine, arr_ras = reorder_axes_to_ras(affine_orig, arr) + affine, arr_ras = reorder_axes(affine_orig, arr, target=('L', 'P', 'S')) arrays[i] = arr_ras # stack arrays into single array @@ -199,7 +201,7 @@ def reader_function(path): affine = img.affine data = img.get_fdata() # keep this as dataobj or use get_fdata()? - affine, data = reorder_axes_to_ras(affine, data) + affine, data = reorder_axes(affine, data, target=('L', 'P', 'S')) spatial_axis_order = tuple(range(n_spatial)) if data.ndim > 3: @@ -225,18 +227,6 @@ def reader_function(path): # data cube in the same position in world coordinates affine_plumb = adjust_translation(affine, affine_plumb, data.shape) - # convert from RAS to SPL - affine_plumb_spl = affine_plumb.copy() - # update scales for RAS -> SPL - affine_plumb_spl[0, 0] = affine_plumb[2, 2] # move S to first - affine_plumb_spl[1, 1] = -affine_plumb[1, 1] # flip A->P - affine_plumb_spl[2, 2] = -affine_plumb[0, 0] # flip R->L, move L last - # make the same order and sign flips to the translations - affine_plumb_spl[:3, 3] = affine_plumb[2::-1, 3] - affine_plumb_spl[1:3, 3] *= -1 # flip R->L, A->P - # reverse order of the last 3 data dimensions correspondingly - data = data.transpose(tuple(range(0, data.ndim - 3)) + (-1, -2, -3)) - # Note: The translate, scale, rotate, shear kwargs correspond to the # 'data2physical' component of a composite affine transform. # https://github.com/napari/napari/blob/v0.4.11/napari/layers/base/base.py#L254-L268 #noqa @@ -247,8 +237,8 @@ def reader_function(path): add_kwargs = dict( metadata=dict(affine=affine, header=header), rgb=False, - scale=np.diag(affine_plumb_spl[:3, :3]), - translate=affine_plumb_spl[:3, 3], + scale=np.diag(affine_plumb[:3, :3]), + translate=affine_plumb[:3, 3], affine=None, channel_axis=None, ) From f26dd62f75b335eb55e1e6a0cb058e3690390eee Mon Sep 17 00:00:00 2001 From: Gregory Lee Date: Fri, 15 Oct 2021 16:00:58 -0400 Subject: [PATCH 6/8] simplify the reorientation code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit use as_reoriented method as it is more concise and will also update the affine we pass on in the metadata Co-authored-by: Christopher Nauroth-Kreß <56394171+ch-n@users.noreply.github.com> --- napari_nibabel/nibabel.py | 66 +++++++-------------------------------- 1 file changed, 12 insertions(+), 54 deletions(-) diff --git a/napari_nibabel/nibabel.py b/napari_nibabel/nibabel.py index 863ff7d..334c82f 100644 --- a/napari_nibabel/nibabel.py +++ b/napari_nibabel/nibabel.py @@ -25,50 +25,10 @@ valid_volume_exts = set(functools.reduce(operator.add, valid_volume_exts)) -def reorder_axes(affine, data, target=('L', 'P', 'S')): - """Permutes data dimensions and updates the affine accordingly. - - Reorders and/or flips data axes to get the spatial axes in SPL order. - - For oblique scans the axes closest the desired orientation are - determined by ``nibabel.affine.io_orientation``. - - Parameters - ---------- - affine : (4, 4) ndarray - Affine matrix - data : ndarray - Data array. Spatial dimensions must be first. - target : 3-tuple of str - Tuple containing the 'S' or 'I', 'P' or 'A' and 'R' or 'L' in the - desired order. The default of ('S', 'P', 'L') means that we will order - the data array so the first axis runs from I->S, the second from - A->P and the third from L->R. - - Returns - ------- - affine_target : (4, 4) ndarray - The affine matrix for the data in the target space. - data_ras : (4, 4) ndarray - Data with axes reordered and/or flipped to the target space. - """ +def get_transform_ornt(affine, target=('L', 'P', 'S')): current_ornt = orientations.io_orientation(affine) - target_ornt = orientations.axcodes2ornt(target) - if np.array_equal(current_ornt, target_ornt): - # already in desired orientation - return affine, data - - # determine the transform needed to get to the target orientation - transform_ornt = orientations.ornt_transform(current_ornt, target_ornt) - - # update the data array to the desired orientation - data_reoriented = orientations.apply_orientation(data, transform_ornt) - - # Update the affine to the target orientation - t_aff = orientations.inv_ornt_aff(transform_ornt, data.shape) - affine_reoriented = affine.dot(t_aff) - - return affine_reoriented, data_reoriented + target_ornt = orientations.axcodes2ornt(('L', 'P', 'S')) + return orientations.ornt_transform(current_ornt, target_ornt) def adjust_translation(affine, affine_plumb, data_shape): @@ -175,34 +135,32 @@ def reader_function(path): if len(paths) > 1: # load all files into a single array objects = [nib.load(_path) for _path in paths] - header = objects[0].header affine = objects[0].affine + header = objects[0].header if not all([_obj.shape == objects[0].shape for _obj in objects]): raise ValueError( "all selected files must contain data of the same shape") - if not all(np.allclose(affine, _obj.affine) for _obj in objects): raise ValueError( "all selected files must share a common affine") - + # reorient volumes to the desired orientation + transform_ornt = get_transform_ornt(affine, target=('L', 'P', 'S')) + objects = [_obj.as_reoriented(transform_ornt)] arrays = [_obj.get_fdata() for _obj in objects] - - # apply same transform to all volumes in the stack - affine_orig = affine.copy() - for i, arr in enumerate(arrays): - affine, arr_ras = reorder_axes(affine_orig, arr, target=('L', 'P', 'S')) - arrays[i] = arr_ras + affine = objects[0].affine + header = objects[0].header # stack arrays into single array data = np.stack(arrays) else: img = nib.load(paths[0]) + # reorient volume to the desired orientation + transform_ornt = get_transform_ornt(img.affine, target=('L', 'P', 'S')) + img = img.as_reoriented(transform_ornt) header = img.header affine = img.affine data = img.get_fdata() # keep this as dataobj or use get_fdata()? - affine, data = reorder_axes(affine, data, target=('L', 'P', 'S')) - spatial_axis_order = tuple(range(n_spatial)) if data.ndim > 3: # nibabel formats have spatial axes in the first 3 positions, but From acb5186ea5d03e2cfe3ab996d83a4e310e51090a Mon Sep 17 00:00:00 2001 From: Gregory Lee Date: Fri, 15 Oct 2021 16:03:09 -0400 Subject: [PATCH 7/8] fix bug in applying orientation in multi-input case --- napari_nibabel/nibabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napari_nibabel/nibabel.py b/napari_nibabel/nibabel.py index 334c82f..f944a0a 100644 --- a/napari_nibabel/nibabel.py +++ b/napari_nibabel/nibabel.py @@ -145,7 +145,7 @@ def reader_function(path): "all selected files must share a common affine") # reorient volumes to the desired orientation transform_ornt = get_transform_ornt(affine, target=('L', 'P', 'S')) - objects = [_obj.as_reoriented(transform_ornt)] + objects = [_obj.as_reoriented(transform_ornt) for _obj in objects] arrays = [_obj.get_fdata() for _obj in objects] affine = objects[0].affine header = objects[0].header From a3fee0f61d72df9222576b5d2227eebda85e4081 Mon Sep 17 00:00:00 2001 From: Gregory Lee Date: Fri, 15 Oct 2021 16:12:28 -0400 Subject: [PATCH 8/8] Fix test case that does not account for the reorientation --- napari_nibabel/_tests/test_nibabel.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/napari_nibabel/_tests/test_nibabel.py b/napari_nibabel/_tests/test_nibabel.py index 36ea73a..30ba190 100644 --- a/napari_nibabel/_tests/test_nibabel.py +++ b/napari_nibabel/_tests/test_nibabel.py @@ -17,8 +17,11 @@ def test_reader(tmp_path): # write some fake data in NIFTI-1 format my_test_file = str(tmp_path / "myfile.nii") - original_data = np.random.rand(20, 20) - nii = nib.Nifti1Image(original_data, affine=np.eye(4)) + original_data = np.random.rand(20, 20, 1) + + # Set affine to an LPS affine here so internal reorientation will not be + # needed. + nii = nib.Nifti1Image(original_data, affine=np.diag((-1, -1, 1, 1))) nii.to_filename(my_test_file) np.save(my_test_file, original_data)