From 82cebc5439e1e4f3937ada9ff0837e7fc6e8c6a2 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sat, 20 Jun 2020 08:10:38 -0700 Subject: [PATCH 1/3] FIX: Segmentation plots aligned with cardinal axes While nilearn/nilearn#25 is considered, this PR advances the generation of mosaics after aligning the affine matrix of the data with the cardinal axes. This should overcome the glitches when nilearn is requested to plot contours and the mask for the ROI has voxels that only partially intersect with the visualization plane. Resolves: #542. References: #281, #304. Depends: #543. --- niworkflows/utils/images.py | 10 ++++++++++ niworkflows/viz/utils.py | 15 ++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/niworkflows/utils/images.py b/niworkflows/utils/images.py index b67747dcd08..07225bcbc8d 100644 --- a/niworkflows/utils/images.py +++ b/niworkflows/utils/images.py @@ -3,6 +3,16 @@ import numpy as np +def as_canonical(img): + """Drop rotation w.r.t. cardinal axes of input image.""" + img = nb.as_closest_canonical(img) + zooms = list(img.header.get_zooms()[:3]) + newaff = np.diag(zooms + [1]) + rot = newaff[:3, :3].dot(np.linalg.inv(img.affine[:3, :3])) + newaff[:3, 3] = rot.dot(img.affine[:3, 3]) + return nb.Nifti1Image(img.dataobj, newaff, img.header) + + def unsafe_write_nifti_header_and_data(fname, header, data): """Write header and data without any consistency checks or data munging diff --git a/niworkflows/viz/utils.py b/niworkflows/viz/utils.py index 040a61803ac..83af2ce4926 100644 --- a/niworkflows/viz/utils.py +++ b/niworkflows/viz/utils.py @@ -20,6 +20,7 @@ from nipype.utils import filemanip from .. import NIWORKFLOWS_LOG +from ..utils.images import as_canonical SVGNS = "http://www.w3.org/2000/svg" @@ -216,19 +217,27 @@ def plot_segs( compress="auto", **plot_params ): - """ plot segmentation as contours over the image (e.g. anatomical). + """ + Generate a static mosaic with ROIs represented by their delimiting contour. + + Plot segmentation as contours over the image (e.g. anatomical). seg_niis should be a list of files. mask_nii helps determine the cut coordinates. plot_params will be passed on to nilearn plot_* functions. If seg_niis is a list of size one, it behaves as if it was plotting the mask. """ plot_params = {} if plot_params is None else plot_params - image_nii = _3d_in_file(image_nii) + image_nii = as_canonical(_3d_in_file(image_nii)) + seg_niis = [as_canonical(_3d_in_file(f)) for f in seg_niis] data = image_nii.get_fdata() plot_params = robust_set_limits(data, plot_params) - bbox_nii = nb.load(image_nii if bbox_nii is None else bbox_nii) + bbox_nii = ( + image_nii if bbox_nii is None + else as_canonical(_3d_in_file(bbox_nii)) + ) + if masked: bbox_nii = nlimage.threshold_img(bbox_nii, 1e-3) From f11d73c19a03b100d1e052f03e8b6f10ba6be930 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sun, 21 Jun 2020 09:52:45 -0700 Subject: [PATCH 2/3] enh: minor refactor for a more consistent rotation of inputs --- niworkflows/utils/images.py | 26 +++++++++++++++++++------- niworkflows/viz/utils.py | 10 ++++++---- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/niworkflows/utils/images.py b/niworkflows/utils/images.py index 07225bcbc8d..686c01d9e63 100644 --- a/niworkflows/utils/images.py +++ b/niworkflows/utils/images.py @@ -3,14 +3,26 @@ import numpy as np -def as_canonical(img): - """Drop rotation w.r.t. cardinal axes of input image.""" +def rotation2canonical(img): + """Calculate the rotation w.r.t. cardinal axes of input image.""" img = nb.as_closest_canonical(img) - zooms = list(img.header.get_zooms()[:3]) - newaff = np.diag(zooms + [1]) - rot = newaff[:3, :3].dot(np.linalg.inv(img.affine[:3, :3])) - newaff[:3, 3] = rot.dot(img.affine[:3, 3]) - return nb.Nifti1Image(img.dataobj, newaff, img.header) + newaff = np.diag(img.header.get_zooms()[:3]) + r = newaff @ np.linalg.inv(img.affine[:3, :3]) + if np.allclose(r, np.eye(3)): + return None + return r + + +def rotate_affine(img, rot=None): + """Rewrite the affine of a spatial image.""" + if rot is None: + return img + + img = nb.as_closest_canonical(img) + affine = np.eye(4) + affine[:3, :3] = rot @ img.affine[:3, :3] + affine[:3, 3] = rot @ img.affine[:3, 3] + return img.__class__(img.dataobj, affine, img.header) def unsafe_write_nifti_header_and_data(fname, header, data): diff --git a/niworkflows/viz/utils.py b/niworkflows/viz/utils.py index 83af2ce4926..2b62ed50416 100644 --- a/niworkflows/viz/utils.py +++ b/niworkflows/viz/utils.py @@ -20,7 +20,7 @@ from nipype.utils import filemanip from .. import NIWORKFLOWS_LOG -from ..utils.images import as_canonical +from ..utils.images import rotation2canonical, rotate_affine SVGNS = "http://www.w3.org/2000/svg" @@ -227,15 +227,17 @@ def plot_segs( """ plot_params = {} if plot_params is None else plot_params - image_nii = as_canonical(_3d_in_file(image_nii)) - seg_niis = [as_canonical(_3d_in_file(f)) for f in seg_niis] + image_nii = _3d_in_file(image_nii) + canonical_r = rotation2canonical(image_nii) + image_nii = rotate_affine(image_nii, rot=canonical_r) + seg_niis = [rotate_affine(_3d_in_file(f), rot=canonical_r) for f in seg_niis] data = image_nii.get_fdata() plot_params = robust_set_limits(data, plot_params) bbox_nii = ( image_nii if bbox_nii is None - else as_canonical(_3d_in_file(bbox_nii)) + else rotate_affine(_3d_in_file(bbox_nii), rot=canonical_r) ) if masked: From 8a6f088fcb6ba207b347a7a981b052044f9fae48 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Tue, 11 Aug 2020 14:20:33 +0200 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Chris Markiewicz --- niworkflows/utils/images.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/niworkflows/utils/images.py b/niworkflows/utils/images.py index 686c01d9e63..2639fda874a 100644 --- a/niworkflows/utils/images.py +++ b/niworkflows/utils/images.py @@ -7,7 +7,7 @@ def rotation2canonical(img): """Calculate the rotation w.r.t. cardinal axes of input image.""" img = nb.as_closest_canonical(img) newaff = np.diag(img.header.get_zooms()[:3]) - r = newaff @ np.linalg.inv(img.affine[:3, :3]) + r = newaff @ np.linalg.pinv(img.affine[:3, :3]) if np.allclose(r, np.eye(3)): return None return r @@ -20,8 +20,7 @@ def rotate_affine(img, rot=None): img = nb.as_closest_canonical(img) affine = np.eye(4) - affine[:3, :3] = rot @ img.affine[:3, :3] - affine[:3, 3] = rot @ img.affine[:3, 3] + affine[:3] = rot @ img.affine[:3] return img.__class__(img.dataobj, affine, img.header)