Skip to content

Commit

Permalink
Merge pull request #747 from EmmaRenauld/volume_module
Browse files Browse the repository at this point in the history
Volumes modules
  • Loading branch information
arnaudbore authored Oct 16, 2023
2 parents 2ff56cf + 8d18f33 commit bd62f0c
Show file tree
Hide file tree
Showing 23 changed files with 230 additions and 197 deletions.
119 changes: 0 additions & 119 deletions scilpy/image/resample_volume.py

This file was deleted.

28 changes: 0 additions & 28 deletions scilpy/image/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,9 @@

import nibabel as nib
import numpy as np
import six
from sklearn.cluster import KMeans


def count_non_zero_voxels(image):
"""
Count number of non zero voxels
Parameters:
-----------
image: string
Path to the image
"""
if isinstance(image, six.string_types):
nb_object = nib.load(image)
else:
nb_object = image

data = nb_object.get_fdata(dtype=np.float32, caching='unchanged')

# Count the number of non-zero voxels.
if len(data.shape) >= 4:
axes_to_sum = np.arange(3, len(data.shape))
nb_voxels = np.count_nonzero(np.sum(np.absolute(data),
axis=tuple(axes_to_sum)))
else:
nb_voxels = np.count_nonzero(data)

return nb_voxels


def volume_iterator(img, blocksize=1, start=0, end=0):
"""Generator that iterates on volumes of data.
Expand Down
File renamed without changes.
185 changes: 182 additions & 3 deletions scilpy/utils/image.py → scilpy/image/volume_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,84 @@
RigidTransform3D)
from dipy.io.gradients import read_bvals_bvecs
from dipy.io.utils import get_reference_info
from dipy.segment.mask import median_otsu
from dipy.segment.mask import crop, median_otsu
import nibabel as nib
import numpy as np

from scilpy.image.reslice import reslice # Don't use Dipy's reslice. Buggy.
from scipy.ndimage import binary_dilation

from scilpy.io.image import get_data_as_mask
from scilpy.utils.bvec_bval_tools import identify_shells
from scilpy.utils.util import voxel_to_world, world_to_voxel


def count_non_zero_voxels(image):
"""
Count number of non-zero voxels
Parameters:
-----------
image: string
Path to the image
"""
# Count the number of non-zero voxels.
if len(image.shape) >= 4:
axes_to_sum = np.arange(3, len(image.shape))
nb_voxels = np.count_nonzero(np.sum(np.absolute(image),
axis=tuple(axes_to_sum)))
else:
nb_voxels = np.count_nonzero(image)

return nb_voxels


def transform_anatomy(transfo, reference, moving, filename_to_save,
interp='linear', keep_dtype=False):
def flip_volume(data, axes):
"""
data: np.ndarray
axes: a list containing any number of values amongst ['x', 'y', 'z'].
"""
if 'x' in axes:
data = data[::-1, ...]

if 'y' in axes:
data = data[:, ::-1, ...]

if 'z' in axes:
data = data[:, :, ::-1, ...]

return data


def crop_volume(img: nib.Nifti1Image, wbbox):
"""Applies cropping from a world space defined bounding box and fixes the
affine to keep data aligned.
wbbox: WorldBoundingBox from the scrip scil_crop_volume. ToDo. Update this.
"""
data = img.get_fdata(dtype=np.float32, caching='unchanged')
affine = img.affine

voxel_bb_mins = world_to_voxel(wbbox.minimums, affine)
voxel_bb_maxs = world_to_voxel(wbbox.maximums, affine)

# Prevent from trying to crop outside data boundaries by clipping bbox
extent = list(data.shape[:3])
for i in range(3):
voxel_bb_mins[i] = max(0, voxel_bb_mins[i])
voxel_bb_maxs[i] = min(extent[i], voxel_bb_maxs[i])
translation = voxel_to_world(voxel_bb_mins, affine)

data_crop = np.copy(crop(data, voxel_bb_mins, voxel_bb_maxs))

new_affine = np.copy(affine)
new_affine[0:3, 3] = translation[0:3]

return nib.Nifti1Image(data_crop, new_affine)


def apply_transform(transfo, reference, moving, filename_to_save,
interp='linear', keep_dtype=False):
"""
Apply transformation to an image using Dipy's tool
Expand Down Expand Up @@ -243,3 +310,115 @@ def compute_snr(dwi, bval, bvec, b0_thr, mask,
val[idx]['snr'] = val[idx]['mean'] / val[idx]['std']

return val


def _interp_code_to_order(interp_code):
orders = {'nn': 0, 'lin': 1, 'quad': 2, 'cubic': 3}
return orders[interp_code]


def resample_volume(img, ref=None, res=None, iso_min=False, zoom=None,
interp='lin', enforce_dimensions=False):
"""
Function to resample a dataset to match the resolution of another
reference dataset or to the resolution specified as in argument.
One of the following options must be chosen: ref, res or iso_min.
Parameters
----------
img: nib.Nifti1Image
Image to resample.
ref: nib.Nifti1Image
Reference volume to resample to. This method is used only if ref is not
None. (default: None)
res: tuple, shape (3,) or int, optional
Resolution to resample to. If the value it is set to is Y, it will
resample to an isotropic resolution of Y x Y x Y. This method is used
only if res is not None. (default: None)
iso_min: bool, optional
If true, resample the volume to R x R x R with R being the smallest
current voxel dimension. If false, this method is not used.
zoom: tuple, shape (3,) or float, optional
Set the zoom property of the image at the value specified.
interp: str, optional
Interpolation mode. 'nn' = nearest neighbour, 'lin' = linear,
'quad' = quadratic, 'cubic' = cubic. (Default: linear)
enforce_dimensions: bool, optional
If True, enforce the reference volume dimension (only if res is not
None). (Default = False)
Returns
-------
resampled_image: nib.Nifti1Image
Resampled image.
"""
data = np.asanyarray(img.dataobj)
original_res = data.shape
affine = img.affine
original_zooms = img.header.get_zooms()[:3]

if ref is not None:
if iso_min or zoom or res:
raise ValueError('Please only provide one option amongst ref, res '
', zoom or iso_min.')
ref_img = nib.load(ref)
new_zooms = ref_img.header.get_zooms()[:3]
elif res is not None:
if iso_min or zoom:
raise ValueError('Please only provide one option amongst ref, res '
', zoom or iso_min.')
if len(res) == 1:
res = res * 3
new_zooms = tuple((o / r) * z for o, r,
z in zip(original_res, res, original_zooms))

elif iso_min:
if zoom:
raise ValueError('Please only provide one option amongst ref, res '
', zoom or iso_min.')
min_zoom = min(original_zooms)
new_zooms = (min_zoom, min_zoom, min_zoom)
elif zoom:
new_zooms = zoom
if len(zoom) == 1:
new_zooms = zoom * 3
else:
raise ValueError("You must choose the resampling method. Either with"
"a reference volume, or a chosen isometric resolution"
", or an isometric resampling to the smallest current"
" voxel dimension!")

interp_choices = ['nn', 'lin', 'quad', 'cubic']
if interp not in interp_choices:
raise ValueError("interp must be one of 'nn', 'lin', 'quad', 'cubic'.")

logging.debug('Data shape: %s', data.shape)
logging.debug('Data affine: %s', affine)
logging.debug('Data affine setup: %s', nib.aff2axcodes(affine))
logging.debug('Resampling data to %s with mode %s', new_zooms, interp)

data2, affine2 = reslice(data, affine, original_zooms, new_zooms,
_interp_code_to_order(interp))

logging.debug('Resampled data shape: %s', data2.shape)
logging.debug('Resampled data affine: %s', affine2)
logging.debug('Resampled data affine setup: %s', nib.aff2axcodes(affine2))

if enforce_dimensions:
if ref is None:
raise ValueError('enforce_dimensions can only be used with the ref'
'method.')
else:
computed_dims = data2.shape
ref_dims = ref_img.shape[:3]
if computed_dims != ref_dims:
fix_dim_volume = np.zeros(ref_dims)
x_dim = min(computed_dims[0], ref_dims[0])
y_dim = min(computed_dims[1], ref_dims[1])
z_dim = min(computed_dims[2], ref_dims[2])

fix_dim_volume[:x_dim, :y_dim, :z_dim] = \
data2[:x_dim, :y_dim, :z_dim]
data2 = fix_dim_volume

return nib.Nifti1Image(data2.astype(data.dtype), affine2)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
from dipy.core.interpolation import trilinear_interpolate4d, \
nearestneighbor_interpolate
from dipy.io.stateful_tractogram import Origin, Space
from dipy.segment.mask import bounding_box


from scilpy.utils.util import voxel_to_world


class DataVolume(object):
Expand Down
Loading

0 comments on commit bd62f0c

Please sign in to comment.