diff --git a/scilpy/viz/scene_utils.py b/scilpy/viz/scene_utils.py index a54f2ac58..3a1984218 100644 --- a/scilpy/viz/scene_utils.py +++ b/scilpy/viz/scene_utils.py @@ -4,7 +4,7 @@ import numpy as np import vtk -from dipy.reconst.shm import sh_to_sf_matrix +from dipy.reconst.shm import sh_to_sf_matrix, sh_to_sf from fury import window, actor from fury.colormap import distinguishable_colormap from PIL import Image @@ -122,7 +122,8 @@ def set_display_extent(slicer_actor, orientation, volume_shape, slice_index): def create_odf_slicer(sh_fodf, orientation, slice_index, mask, sphere, nb_subdivide, sh_order, sh_basis, full_basis, - scale, radial_scale, norm, colormap): + scale, radial_scale, norm, colormap, sh_variance=None, + variance_k=1, variance_color=(255, 255, 255)): """ Create a ODF slicer actor displaying a fODF slice. The input volume is a 3-dimensional grid containing the SH coefficients of the fODF for each @@ -158,6 +159,12 @@ def create_odf_slicer(sh_fodf, orientation, slice_index, mask, sphere, If True, enables normalization of ODF slicer. colormap : str Colormap for the ODF slicer. If None, a RGB colormap is used. + sh_variance : np.ndarray, optional + Spherical harmonics of the variance fODF data. + variance_k : float, optional + Factor that multiplies sqrt(variance). + variance_color : tuple, optional + Color of the variance fODF data, in RGB. Returns ------- @@ -172,14 +179,46 @@ def create_odf_slicer(sh_fodf, orientation, slice_index, mask, sphere, B_mat = sh_to_sf_matrix(sphere, sh_order, sh_basis, full_basis, return_inv=False) - odf_actor = actor.odf_slicer(sh_fodf, mask=mask, norm=norm, - radial_scale=radial_scale, - sphere=sphere, - colormap=colormap, - scale=scale, B_matrix=B_mat) + var_actor = None + + if sh_variance is not None: + fodf = sh_to_sf(sh_fodf, sphere, sh_order, sh_basis, + full_basis=full_basis) + fodf_var = sh_to_sf(sh_variance, sphere, sh_order, sh_basis, + full_basis=full_basis) + fodf_uncertainty = fodf + variance_k * np.sqrt(np.clip(fodf_var, 0, + None)) + # normalise fodf and variance + if norm: + maximums = np.abs(np.append(fodf, fodf_uncertainty, axis=-1))\ + .max(axis=-1) + fodf[maximums > 0] /= maximums[maximums > 0][..., None] + fodf_uncertainty[maximums > 0] /= maximums[maximums > 0][..., None] + + odf_actor = actor.odf_slicer(fodf, mask=mask, norm=False, + radial_scale=radial_scale, + sphere=sphere, scale=scale, + colormap=colormap) + + var_actor = actor.odf_slicer(fodf_uncertainty, mask=mask, norm=False, + radial_scale=radial_scale, + sphere=sphere, scale=scale, + colormap=variance_color) + var_actor.GetProperty().SetDiffuse(0.0) + var_actor.GetProperty().SetAmbient(1.0) + var_actor.GetProperty().SetFrontfaceCulling(True) + else: + odf_actor = actor.odf_slicer(sh_fodf, mask=mask, norm=norm, + radial_scale=radial_scale, + sphere=sphere, + colormap=colormap, + scale=scale, B_matrix=B_mat) set_display_extent(odf_actor, orientation, sh_fodf.shape[:3], slice_index) + if var_actor is not None: + set_display_extent(var_actor, orientation, + fodf_uncertainty.shape[:3], slice_index) - return odf_actor + return odf_actor, var_actor def _get_affine_for_texture(orientation, offset): diff --git a/scripts/scil_visualize_fodf.py b/scripts/scil_visualize_fodf.py index 2314932e6..78f9f9870 100755 --- a/scripts/scil_visualize_fodf.py +++ b/scripts/scil_visualize_fodf.py @@ -100,45 +100,50 @@ def _build_arg_parser(): help='Disable normalization of ODF slicer.') # Background image options - p.add_argument('--background', + bg = p.add_argument_group('Background arguments') + bg.add_argument('--background', help='Background image file. If RGB, values must ' 'be between 0 and 255.') - p.add_argument('--bg_range', nargs=2, metavar=('MIN', 'MAX'), type=float, + bg.add_argument('--bg_range', nargs=2, metavar=('MIN', 'MAX'), type=float, help='The range of values mapped to range [0, 1] ' 'for background image. [(bg.min(), bg.max())]') - p.add_argument('--bg_opacity', type=float, default=1.0, + bg.add_argument('--bg_opacity', type=float, default=1.0, help='The opacity of the background image. Opacity of 0.0 ' 'means transparent and 1.0 is completely visible. ' '[%(default)s]') - p.add_argument('--bg_offset', type=float, default=0.5, + bg.add_argument('--bg_offset', type=float, default=0.5, help='The offset of the background image. [%(default)s]') - p.add_argument('--bg_interpolation', + bg.add_argument('--bg_interpolation', default='nearest', choices={'linear', 'nearest'}, help='Interpolation mode for the background image. ' '[%(default)s]') - p.add_argument('--bg_color', nargs=3, type=float, default=(0, 0, 0), + bg.add_argument('--bg_color', nargs=3, type=float, default=(0, 0, 0), help='The color of the overall background, behind ' 'everything. Must be RGB values scaled between 0 and ' '1. [%(default)s]') # Peaks input file options - p.add_argument('--peaks', + peaks = p.add_argument_group('Peaks arguments') + peaks.add_argument('--peaks', help='Peaks image file.') - p.add_argument('--peaks_color', nargs=3, type=float, + peaks.add_argument('--peaks_color', nargs=3, type=float, help='Color used for peaks, as RGB values scaled between 0 ' 'and 1. If None, then a RGB colormap is used. ' '[%(default)s]') - p.add_argument('--peaks_width', default=1.0, type=float, + peaks.add_argument('--peaks_width', default=1.0, type=float, help='Width of peaks segments. [%(default)s]') - peaks_scale_group = p.add_mutually_exclusive_group() + peaks_scale = p.add_argument_group('Peaks scaling arguments', 'Choose ' + 'between peaks values and arbitrary ' + 'length.') + peaks_scale_group = peaks_scale.add_mutually_exclusive_group() peaks_scale_group.add_argument('--peaks_values', help='Peaks values file.') @@ -146,6 +151,21 @@ def _build_arg_parser(): help='Length of the peaks segments. ' '[%(default)s]') + # fODF variance options + var = p.add_argument_group('Variance arguments', 'For the visualization ' + 'of fodf uncertainty, the variance is used ' + 'as follow: mean + k * sqrt(variance), where ' + 'mean is the input fodf (in_fodf) and k is the ' + 'scaling factor (variance_k).') + var.add_argument('--variance', + help='FODF variance file.') + var.add_argument('--variance_k', default=1, type=float, + help='Scaling factor (k) for the computation of the fodf ' + 'uncertainty. [%(default)s]') + var.add_argument('--var_color', nargs=3, type=float, default=(1, 1, 1), + help='Color of variance outline. Must be RGB values scaled ' + 'between 0 and 1. [%(default)s]') + return p @@ -222,6 +242,16 @@ def _get_data_from_inputs(args): peak_vals =\ nib.nifti1.load(args.peaks_values).get_fdata(dtype=np.float32) data['peaks_values'] = peak_vals + if args.variance: + assert_same_resolution([args.variance, args.in_fodf]) + variance = nib.nifti1.load(args.variance).get_fdata(dtype=np.float32) + if len(variance.shape) == 3: + variance = np.reshape(variance, variance.shape + (1,)) + if variance.shape != fodf.shape: + raise ValueError('Dimensions mismatch between fODF {0} and ' + 'variance {1}.' + .format(fodf.shape, variance.shape)) + data['variance'] = variance return data @@ -246,17 +276,26 @@ def main(): else: color_rgb = None + variance = data['variance'] if args.variance else None + var_color = np.asarray(args.var_color) * 255 # Instantiate the ODF slicer actor - odf_actor = create_odf_slicer(data['fodf'], args.axis_name, - args.slice_index, mask, sph, - args.sph_subdivide, sh_order, - args.sh_basis, full_basis, - args.scale, - not args.radial_scale_off, - not args.norm_off, - args.colormap or color_rgb) + odf_actor, var_actor = create_odf_slicer(data['fodf'], args.axis_name, + args.slice_index, mask, sph, + args.sph_subdivide, sh_order, + args.sh_basis, full_basis, + args.scale, + not args.radial_scale_off, + not args.norm_off, + args.colormap or color_rgb, + sh_variance=variance, + variance_k=args.variance_k, + variance_color=var_color) actors.append(odf_actor) + # Instantiate a variance slicer actor if a variance image is supplied + if 'variance' in data: + actors.append(var_actor) + # Instantiate a texture slicer actor if a background image is supplied if 'bg' in data: bg_actor = create_texture_slicer(data['bg'],