Source code for mphy0026.ui.mphy0026_overlay_app

# -*- coding: utf-8 -*-

""" Harness to run overlay application. """

import sys
import random
import vtk
import numpy as np
from PySide6 import QtWidgets, QtGui, QtCore
import sksurgeryvtk.widgets.vtk_overlay_window as ow
import sksurgeryvtk.models.vtk_surface_model as sm

# pylint:disable=no-member, too-many-instance-attributes, invalid-name
[docs] class OverlaywMainWindow(QtWidgets.QMainWindow): """ OverlayMainWindow. """ def __init__(self): """ Constructor, puts OverlayMainWidget in window. """ super().__init__() self.mode = "circle" self.model = None self.layout = QtWidgets.QVBoxLayout() self.frame = QtWidgets.QFrame() self.frame.setLayout(self.layout) self.setCentralWidget(self.frame) self.h_box = QtWidgets.QHBoxLayout() self.vtk_overlay_window = ow.VTKOverlayWindow() self.controls_box = QtWidgets.QHBoxLayout() self.h_box.addWidget(self.vtk_overlay_window) self.layout.addLayout(self.h_box) self.layout.addLayout(self.controls_box) self.add_group_box() self.add_opacity_slider() # Reset button self.button = QtWidgets.QPushButton('Move Target', self) self.button.clicked.connect(self.reset_models) self.layout.addWidget(self.button) self.target_actor = None self.window_width = 800 self.window_height = 600 self.setFixedSize(self.window_width, self.window_height) style = vtk.vtkInteractorStyleTrackballActor() self.vtk_overlay_window.SetInteractorStyle(style) # Setup text labels self.txtPosition = vtk.vtkTextActor() self.txtPosition.SetDisplayPosition(30, 60) self.txtPosition.GetTextProperty().SetFontSize(25) self.txtScale = vtk.vtkTextActor() self.txtScale.SetDisplayPosition(30, 30) self.txtScale.GetTextProperty().SetFontSize(25) self.reset_text_labels() self.vtk_overlay_window.get_renderer(layer=2).AddActor(self.txtScale) self.vtk_overlay_window.get_renderer(layer=2).AddActor(self.txtPosition) self.setup_target() self.setup_overlay() self.update() self.setContentsMargins(0, 0, 0, 0) def interactionChange(_, event): """ Mouse callbacks """ if event == "EndInteractionEvent": # position pos_error = self.check_position() self.txtPosition.SetInput(f"Alignment Error: {pos_error:.2f}") # scale scale_error = self.check_scale() self.txtScale.SetInput(f"Size Error: {scale_error:.2f}") # pylint:disable=protected-access self.vtk_overlay_window._Iren.AddObserver("EndInteractionEvent", interactionChange) # pylint:disable=no-self-use
[docs] def show_controls_dialog(self): """.""" dialog = QtWidgets.QMessageBox() dialog.setText("Mouse Controls \n" \ "Left button: Rotate (x/y)\n" \ "Ctrl + Left button: Rotate (z)\n"\ "Right Button: Scale\n" \ "Middle Button: Move\n") dialog.exec_()
[docs] def add_group_box(self): """.""" self.group_box = QtWidgets.QGroupBox("Select model") self.radio_btn_circle = QtWidgets.QRadioButton("Circle") self.radio_btn_liver = QtWidgets.QRadioButton("Liver") self.radio_layout = QtWidgets.QHBoxLayout() self.radio_layout.addWidget(self.radio_btn_circle) self.radio_layout.addWidget(self.radio_btn_liver) self.group_box.setLayout(self.radio_layout) self.layout.addWidget(self.group_box) self.radio_btn_circle.clicked.connect(self.circle_selected) self.radio_btn_liver.clicked.connect(self.liver_selected) self.radio_btn_circle.setChecked(True)
[docs] def circle_selected(self): """Callback""" self.mode = "circle" self.reset_models()
[docs] def liver_selected(self): """Callback""" self.mode = "liver" self.reset_models()
[docs] def reset_models(self): """ Reset models to default""" self.setup_overlay() self.setup_target() self.vtk_overlay_window.background_renderer.SetBackground(0, 0, 0) self.update()
[docs] def setup_overlay(self): """Setup overlays""" if self.model: self.vtk_overlay_window.get_renderer(layer=1).RemoveActor( self.model.actor) if self.mode == "circle": self.setup_circle_overlay() elif self.mode == "liver": self.setup_liver_overlay() self.update()
[docs] def setup_circle_overlay(self): """Position overlay sphere """ sphere_model = 'tests/data/overlay/sphere.vtk' self.model = sm.VTKSurfaceModel(sphere_model, [0.5, 0.5, 0.5]) self.vtk_overlay_window.add_vtk_models([self.model])
[docs] def setup_liver_overlay(self): """ Positio overlay liver""" liver_model = 'tests/data/overlay/liver.vtk' self.model = sm.VTKSurfaceModel(liver_model, [1.0, 0.0, 0.0]) self.vtk_overlay_window.add_vtk_models([self.model])
[docs] def update(self): """ Re render the window.""" #pylint:disable=protected-access self.vtk_overlay_window._RenderWindow.Render()
[docs] def add_opacity_slider(self): """Create a QSlider that controls VTK model opactity""" self.opacity_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) self.opacity_slider.setMinimum(0) self.opacity_slider.setMaximum(100) self.opacity_slider.setSliderPosition(100) self.opacity_slider.valueChanged.connect(self.set_opacity) label = QtWidgets.QLabel('Opacity') self.controls_box.addWidget(label) self.controls_box.addWidget(self.opacity_slider) self.layout.addLayout(self.controls_box)
[docs] def set_opacity(self, opacity_percent): """Set the opacity for all VTK models :param opacity_percent: Target opacity value, in % :type opacity_percent: int """ opacity = opacity_percent / 100 self.model.set_opacity(opacity) self.update()
[docs] def setup_target(self): """ Do some setup for the targets. """ # Clear previously drawn circles if self.target_actor: self.vtk_overlay_window.get_renderer(layer=1).RemoveActor( self.target_actor) if self.mode == "circle": self.draw_circle() elif self.mode == "liver": self.set_target_liver() print(f'Circle: {self.target_actor.GetCenter()}') self.vtk_overlay_window.get_renderer(layer=1).AddActor(self.target_actor) self.reset_text_labels() self.update()
[docs] def set_target_liver(self): #pylint:disable=attribute-defined-outside-init, unexpected-keyword-arg """ Set position of target liver. """ liver_model = 'tests/data/overlay/liver.vtk' self.target_model = sm.VTKSurfaceModel(liver_model, [0.5, 0.5, 0.5], pickable=False) self.target_actor = self.target_model.actor self.target_x = -300 + 600 * random.random() self.target_y = -200 + 400 * random.random() self.target_actor.SetOrigin(0, 0, 0) scale = 0.5 + random.random() * 1.0 self.target_actor.SetScale(scale, scale, scale) self.target_actor.RotateX(180*random.random()) self.target_actor.RotateY(90*random.random()) self.target_actor.SetPosition(self.target_x, self.target_y, 0)
[docs] def draw_circle(self): """ Draw a circle at random coordinates in the window. """ #pylint:disable=attribute-defined-outside-init # Generate random position and size self.circle_radius = random.random() * 500 + 25 self.target_x = -400 + self.circle_radius \ + (800 - 2*self.circle_radius) * random.random() self.target_y = -260 + self.circle_radius\ + (520 - 2*self.circle_radius) * random.random() # Create a circle polygon_source = vtk.vtkRegularPolygonSource() # Comment this line to generate a disk instead of a circle. polygon_source.GeneratePolygonOff() polygon_source.SetNumberOfSides(50) polygon_source.SetRadius(self.circle_radius) polygon_source.SetCenter(self.target_x, self.target_y, 0.0) # mapper mapper = vtk.vtkPolyDataMapper() mapper.SetInputConnection(polygon_source.GetOutputPort()) # actor self.target_actor = vtk.vtkActor() self.target_actor.SetMapper(mapper)
[docs] def check_position(self): """ Check if overlay model is correctly aligned in x/y with the background. """ if self.mode == "circle": position_model = self.model.actor.GetCenter() elif self.mode == "liver": position_model = self.model.actor.GetPosition() #pylint:disable=invalid-name x, y = position_model[0], position_model[1] x_error = abs(x - self.target_x) y_error = abs(y - self.target_y) mse = np.sqrt(x_error**2 + y_error**2) return mse
[docs] def check_scale(self): """ Check if the overlay is the correct size for the background image. """ model_scale = self.model.actor.GetScale()[0] if self.mode == "circle": model_in_taget_space = 100 * model_scale * 3 / 2 diff = model_in_taget_space - self.circle_radius elif self.mode == "liver": target_scale = self.target_actor.GetScale()[0] diff = 100 * (model_scale - target_scale) return diff
[docs] def reset_text_labels(self): """ Reset to default""" self.txtPosition.SetInput("Alignment Error: N/A") self.txtScale.SetInput("Size Error: N/A")
[docs] def run_overlay(): """ Run app """ # Need this for all the Qt magic. app = QtWidgets.QApplication([]) # This is a simple way of increasing the font size of all buttons globally. font = QtGui.QFont() font.setPointSize(12) app.setFont(font) # App is just one window, containing one widget, defined above. window = OverlaywMainWindow() #widget.setContentsMargins(0, 0, 0, 0) window.show() window.show_controls_dialog() # Start event loop. return sys.exit(app.exec_())