import warnings
import numpy as np
from scipy import stats
from dipy.testing.decorators import warning_for_keywords
from dipy.utils.optpkg import optional_package
fury, has_fury, setup_module = optional_package("fury", min_version="0.10.0")
if has_fury:
from fury import actor
from fury.utils import apply_affine
[docs]
class SlicesVisualizer:
@warning_for_keywords()
def __init__(
self,
interactor,
scene,
data,
*,
affine=None,
world_coords=False,
percentiles=(0, 100),
rgb=False,
):
self._interactor = interactor
self._scene = scene
self._data = data
self._affine = affine
if not world_coords:
self._affine = np.eye(4)
self._slice_actors = [None] * 3
self._data_ndim = data.ndim
self._data_shape = data.shape
self._rgb = False
self._percentiles = percentiles
vol_data = self._data
if (
self._data_ndim == 4 and rgb and self._data_shape[-1] == 3
) or self._data_ndim == 3:
self._rgb = True and not self._data_ndim == 3
self._int_range = np.percentile(vol_data, self._percentiles)
_evaluate_intensities_range(self._int_range)
else:
if self._data_ndim == 4 and rgb and self._data_shape[-1] != 3:
warnings.warn(
"The rgb flag is enabled but the color "
+ "channel information is not provided",
stacklevel=2,
)
vol_data = self._volume_calculations(self._percentiles)
self._vol_max = np.max(vol_data)
self._vol_min = np.min(vol_data)
self._resliced_vol = None
print(f"Original shape: {self._data_shape}")
self._create_and_resize_actors(vol_data, self._int_range)
print(f"Resized to RAS shape: {self._data_shape}")
self._sel_slices = np.rint(np.asarray(self._data_shape[:3]) / 2).astype(int)
self._add_slice_actors_to_scene(self._sel_slices)
self._picker_callback = None
self._picked_voxel_actor = None
def _volume_calculations(self, percentiles):
for i in range(self._data.shape[-1]):
vol_data = self._data[..., i]
self._int_range = np.percentile(vol_data, percentiles)
if np.sum(np.diff(self._int_range)) != 0:
break
else:
if i < self._data_shape[-1] - 1:
warnings.warn(
f"Volume N°{i} does not have any contrast. "
"Please, check the value ranges of your data. "
"Moving to the next volume.",
stacklevel=2,
)
else:
_evaluate_intensities_range(self._int_range)
return vol_data
def _add_slice_actors_to_scene(self, visible_slices):
self._slice_actors[0].display_extent(
visible_slices[0],
visible_slices[0],
0,
self._data_shape[1] - 1,
0,
self._data_shape[2] - 1,
)
self._slice_actors[1].display_extent(
0,
self._data_shape[0] - 1,
visible_slices[1],
visible_slices[1],
0,
self._data_shape[2] - 1,
)
self._slice_actors[2].display_extent(
0,
self._data_shape[0] - 1,
0,
self._data_shape[1] - 1,
visible_slices[2],
visible_slices[2],
)
for act in self._slice_actors:
self._scene.add(act)
def _create_and_resize_actors(self, vol_data, value_range):
self._slice_actors[0] = actor.slicer(
vol_data,
affine=self._affine,
value_range=value_range,
interpolation="nearest",
)
self._resliced_vol = self._slice_actors[0].resliced_array()
self._slice_actors[1] = self._slice_actors[0].copy()
self._slice_actors[2] = self._slice_actors[0].copy()
for slice_actor in self._slice_actors:
slice_actor.AddObserver(
"LeftButtonPressEvent", self._left_click_picker_callback, 1.0
)
if self._data_ndim == 4 and not self._rgb:
self._data_shape = self._resliced_vol.shape + (self._data.shape[-1],)
else:
self._data_shape = self._resliced_vol.shape
def _left_click_picker_callback(self, obj, event):
# TODO: Find out why this is not triggered when opacity < 1
event_pos = self._interactor.GetEventPosition()
obj.picker.Pick(event_pos[0], event_pos[1], 0, self._scene)
i, j, k = obj.picker.GetPointIJK()
res = self._resliced_vol[i, j, k]
try:
message = f"{res:.2f}"
except TypeError:
message = f"{res[0]:.2f} {res[1]:.2f} {res[2]:.2f}"
message = f"({i}, {j}, {k}) = {message}"
self._picker_callback(message)
# TODO: Fix this
# self._replace_picked_voxel_actor(i, j, k)
def _replace_picked_voxel_actor(self, x, y, z):
if self._picked_voxel_actor:
self._scene.rm(self._picked_voxel_actor)
pnt = np.asarray([[x, y, z]])
pnt = apply_affine(self._affine, pnt)
self._picked_voxel_actor = actor.dot(pnt, colors=(0.9, 0.4, 0.0), dot_size=10)
self._scene.add(self._picked_voxel_actor)
[docs]
def change_volume(self, prev_idx, next_idx, intensities, visible_slices):
vol_data = self._data[..., prev_idx]
# NOTE: Supported only in latests versions of scipy
# percs = stats.percentileofscore(np.ravel(vol_data), intensities)
perc_0 = stats.percentileofscore(np.ravel(vol_data), intensities[0])
perc_1 = stats.percentileofscore(np.ravel(vol_data), intensities[1])
vol_data = self._data[..., next_idx]
value_range = np.percentile(vol_data, [perc_0, perc_1])
if np.sum(np.diff(self._int_range)) == 0:
return False
self._int_range = value_range
self._vol_max = np.max(vol_data)
self._vol_min = np.min(vol_data)
for slice_actor in self._slice_actors:
self._scene.rm(slice_actor)
self._create_and_resize_actors(vol_data, self._int_range)
self._add_slice_actors_to_scene(visible_slices)
return True
[docs]
def register_picker_callback(self, callback):
self._picker_callback = callback
@property
def data_shape(self):
return self._data_shape
@property
def intensities_range(self):
return self._int_range
@property
def selected_slices(self):
return self._sel_slices
@property
def slice_actors(self):
return self._slice_actors
@property
def volume_max(self):
return self._vol_max
@property
def volume_min(self):
return self._vol_min
@property
def rgb(self):
return self._rgb
def _evaluate_intensities_range(intensities_range):
if np.sum(np.diff(intensities_range)) == 0:
raise ValueError(
"Your data does not have any contrast. Please, check the "
"value range of your data."
)