Source code for sksurgeryvtk.widgets.vtk_reslice_widget

"""
Module to show slice views of volumetric data.
"""
#pylint:disable=too-many-instance-attributes, no-name-in-module
import os
import vtk
import numpy as np
from PySide6 import QtWidgets
from PySide6.QtCore import QTimer

from vtkmodules.qt.QVTKRenderWindowInteractor \
        import QVTKRenderWindowInteractor


[docs]class VTKResliceWidget(QVTKRenderWindowInteractor): """ Widget to show a single slice of Volumetric Data. :param reader: vtkReader class e.g. DICOM/Niftii/gipl :param axis: x/y/z axis selection :param parent: parent QWidget. """ def __init__(self, reader, axis, parent): if axis not in ['x', 'y', 'z']: raise TypeError('Argument should be x/y/z') super().__init__(parent) self.axis = axis self.position = 0 self.reader = reader # Calculate the center of the volume self.x_min, self.x_max, self.y_min, self.y_max, self.z_min, self.z_max \ = self.reader.GetExecutive().GetWholeExtent( self.reader.GetOutputInformation(0)) self.x_spacing, self.y_spacing, self.z_spacing = \ self.reader.GetOutput().GetSpacing() self.x_0, self.y_0, self.z_0 = self.reader.GetOutput().GetOrigin() self.center =\ [self.x_0 + self.x_spacing * 0.5 * (self.x_min + self.x_max), self.y_0 + self.y_spacing * 0.5 * (self.y_min + self.y_max), self.z_0 + self.z_spacing * 0.5 * (self.z_min + self.z_max)] self.actor = vtk.vtkImageActor() self.set_lookup_table_min_max(-1000, 1000) self.text_actor = vtk.vtkTextActor() self.text_actor.SetInput(self.axis) self.renderer = vtk.vtkRenderer() self.renderer.AddActor(self.actor) self.renderer.AddActor(self.text_actor) # Move camera so that the slice is in view if axis == "x": self.renderer.GetActiveCamera().Azimuth(90) if axis == "y": self.renderer.GetActiveCamera().Elevation(90) self.set_slice_position_mm(0) self.renderer.ResetCamera(self.actor.GetBounds()) self.GetRenderWindow().AddRenderer(self.renderer) # Remove unwanted mouse interaction behaviours actions = ['MouseWheelForwardEvent', 'MouseWheelBackwardEvent', \ 'LeftButtonPressEvent', 'RightButtonPressEvent'] for action in actions: self._Iren.RemoveObservers(action)
[docs] def set_lookup_table_min_max(self, min, max): #pylint:disable=redefined-builtin """ Set the minimum/maximum values for the VTK lookup table i.e. change displayed range of intensity values. """ self.lut = vtk.vtkLookupTable() self.lut.SetTableRange(min, max) self.lut.SetHueRange(0, 0) self.lut.SetSaturationRange(0, 0) self.lut.SetValueRange(0, 1) self.lut.Build() self.colours = vtk.vtkImageMapToColors() self.colours.SetInputConnection(self.reader.GetOutputPort()) self.colours.SetLookupTable(self.lut) self.colours.Update() self.actor.GetMapper().SetInputConnection(self.colours.GetOutputPort())
[docs] def set_slice_position_pixels(self, pos): """ Set the slice position in the volume in pixels """ pos = int(pos) if self.axis == 'x': pos = np.clip(pos, self.x_min, self.x_max) self.actor.SetDisplayExtent( pos, pos, self.y_min, self.y_max, self.z_min, self.z_max) if self.axis == 'y': pos = np.clip(pos, self.y_min, self.y_max) self.actor.SetDisplayExtent( self.x_min, self.x_max, pos, pos, self.z_min, self.z_max) if self.axis == 'z': pos = np.clip(pos, self.z_min, self.z_max) self.actor.SetDisplayExtent( self.x_min, self.x_max, self.y_min, self.y_max, pos, pos) self.position = pos # Fill widget with slice by moving camera self.renderer.ResetCamera(self.actor.GetBounds()) self.GetRenderWindow().Render()
[docs] def set_slice_position_mm(self, pos): """ Set the slice position in the volume in mm """ if self.axis == 'x': self.set_slice_position_pixels(pos / self.x_spacing) if self.axis == 'y': self.set_slice_position_pixels(pos / self.y_spacing) if self.axis == 'z': self.set_slice_position_pixels(pos / self.z_spacing)
[docs] def get_slice_position(self): """ Return the current slice position. """ return self.position
[docs] def reset_position(self): """ Set slice position to the middle of the axis. """ if self.axis == 'x': lower, upper = self.x_min, self.x_max if self.axis == 'y': lower, upper = self.y_min, self.y_max if self.axis == 'z': lower, upper = self.z_min, self.z_max self.set_slice_position_mm(lower + (upper - lower) // 2)
[docs] def on_mouse_wheel_forward(self, obj, event): #pylint:disable=unused-argument """ Callback to change slice position using mouse wheel. """ current_position = self.get_slice_position() self.set_slice_position_pixels(current_position + 1)
[docs] def on_mouse_wheel_backward(self, obj, event): #pylint:disable=unused-argument """ Callback to change slice position using mouse wheel. """ current_position = self.get_slice_position() self.set_slice_position_pixels(current_position - 1)
[docs] def set_mouse_wheel_callbacks(self): """ Add callbacks for scroll events. """ self._Iren.AddObserver('MouseWheelForwardEvent', self.on_mouse_wheel_forward) self._Iren.AddObserver('MouseWheelBackwardEvent', self.on_mouse_wheel_backward)
[docs]class VTKSliceViewer(QtWidgets.QWidget): """ Othrogonal slice viewer showing Axial/Sagittal/Coronal views :param input_data: path to volume data """ def __init__(self, input_data): super().__init__() self.layout = QtWidgets.QGridLayout() self.setLayout(self.layout) # Start by loading some data. if os.path.isdir(input_data): self.reader = vtk.vtkDICOMImageReader() self.reader.SetDirectoryName(input_data) elif input_data.endswith(('.nii', '.nii.gz')): self.reader = vtk.vtkNIFTIImageReader() self.reader.SetFileName(input_data) self.reader.Update() self.frame = QtWidgets.QFrame() self.fourth_panel_renderer = vtk.vtkRenderer() self.fourth_panel_renderer.SetBackground(.1, .2, .1) self.x_view = VTKResliceWidget(self.reader, 'x', self.frame) self.y_view = VTKResliceWidget(self.reader, 'y', self.frame) self.z_view = VTKResliceWidget(self.reader, 'z', self.frame) self.layout.addWidget(self.x_view, 0, 0) self.layout.addWidget(self.y_view, 0, 1) self.layout.addWidget(self.z_view, 1, 0) self.x_view.GetRenderWindow().Render() self.y_view.GetRenderWindow().Render() self.z_view.GetRenderWindow().Render() self.fourth_panel = QVTKRenderWindowInteractor(self.frame) self.fourth_panel.GetRenderWindow().AddRenderer( self.fourth_panel_renderer) for view in [self.x_view, self.y_view, self.z_view]: self.fourth_panel_renderer.AddActor(view.actor) self.layout.addWidget(self.fourth_panel, 1, 1) self.fourth_panel.GetRenderWindow().Render()
[docs] def set_lookup_table_min_max(self, min, max): #pylint:disable=redefined-builtin """ Set lookup table min/max for all slice views """ self.x_view.set_lookup_table_min_max(min, max) self.y_view.set_lookup_table_min_max(min, max) self.z_view.set_lookup_table_min_max(min, max)
[docs] def update_slice_positions_mm(self, x_pos, y_pos, z_pos): """ Set the slice positions for each view. :param x: slice 1 position :param y: slice 2 position :param z: slice 3 position """ self.x_view.set_slice_position_mm(x_pos) self.y_view.set_slice_position_mm(y_pos) self.z_view.set_slice_position_mm(z_pos) self.fourth_panel.GetRenderWindow().Render()
[docs] def update_slice_positions_pixels(self, x_pos, y_pos, z_pos): """ Set the slice positions for each view. :param x: slice 1 position :param y: slice 2 position :param z: slice 3 position """ self.x_view.set_slice_position_pixels(x_pos) self.y_view.set_slice_position_pixels(y_pos) self.z_view.set_slice_position_pixels(z_pos) self.fourth_panel.GetRenderWindow().Render()
[docs] def reset_slice_positions(self): """ Set slcie positions to some default values. """ self.x_view.reset_position() self.y_view.reset_position() self.z_view.reset_position() self.fourth_panel.GetRenderWindow().Render()
[docs]class MouseWheelSliceViewer(VTKSliceViewer): """ Orthogonal slice viewer using mouse wheel to control slice position. Example usage: qApp = QtWidgets.QApplication([]) input_data = 'tests/data/dicom/LegoPhantom_10slices' slice_viewer = MouseWheelSliceViewer(input_data) slice_viewer.start() qApp.exec_() """ def __init__(self, input_data): super().__init__(input_data) self.x_view.set_mouse_wheel_callbacks() self.y_view.set_mouse_wheel_callbacks() self.z_view.set_mouse_wheel_callbacks() self.update_rate = 20
[docs] def update_fourth_panel(self): """ Update 3D view. """ self.fourth_panel.GetRenderWindow().Render()
[docs] def start(self): #pylint:disable=attribute-defined-outside-init, no-member """ Start a timer which will update the 3D view. """ self.timer = QTimer() self.timer.timeout.connect(self.update_fourth_panel) self.timer.start(1000.0 / self.update_rate) self.show() self.reset_slice_positions()
[docs]class TrackedSliceViewer(VTKSliceViewer): #pylint:disable=invalid-name """ Orthogonal slice viewer combined with tracker to control slice position. :param input_data: Path to file/folder containing volume data :param tracker: scikit-surgery tracker object, used to control slice positions. Example usage: qApp = QtWidgets.QApplication([]) input_data = 'tests/data/dicom/LegoPhantom_10slices' tracker = ArUcoTracker() slice_viewer = MouseWheelSliceViewer(input_data, tracker) slice_viewer.start() qApp.exec_() """ def __init__(self, input_data, tracker): super().__init__(input_data) self.tracker = tracker self.update_rate = 20
[docs] def update_position(self): """ Get position from tracker and use this to set slice positions. """ _, _, _, tracking_data, _ = self.tracker.get_frame() if tracking_data is not None: x, y, z = tracking_data[0][0][3], \ tracking_data[0][1][3], \ tracking_data[0][2][3] self.update_slice_positions_mm(x, y, z)
[docs] def start(self): #pylint:disable=attribute-defined-outside-init, no-member """Show the overlay widget and set a timer running""" self.timer = QTimer() self.timer.timeout.connect(self.update_position) self.timer.start(1000.0 / self.update_rate) self.show() self.reset_slice_positions()