Hello Dear Developers and Users
To draw ROI, it is needed that three views axial, coronal, and sagittal be synchronize together. This means, for example, by clicking on one certain pixel in sagittal slice, the slices shown in axial and coronal display windows also pass through that pixel in orthogonal way.
To activate this synchronization, the commands below must be copy and paste in Python Interactor.
import slicer
from slicer.util import VTKObservationMixin
import vtk
import numpy as np
class SliceClickUpdater(VTKObservationMixin):
def _init_(self):
VTKObservationMixin._init_(self)
# Define slice nodes with their expected orientations
self.sliceNodes = {
“Red”: {“node”: slicer.mrmlScene.GetNodeByID(“vtkMRMLSliceNodeRed”), “orientation”: “Axial”},
“Yellow”: {“node”: slicer.mrmlScene.GetNodeByID(“vtkMRMLSliceNodeYellow”), “orientation”: “Coronal”},
“Green”: {“node”: slicer.mrmlScene.GetNodeByID(“vtkMRMLSliceNodeGreen”), “orientation”: “Sagittal”}
}
# Validate slice nodes
for color, info in self.sliceNodes.items():
if info[“node”] is None:
print(f"Error: Slice node for {color} not found in the scene.")
continue
# Ensure correct orientation
current_orientation = info[“node”].GetOrientation()
expected_orientation = info[“orientation”]
if current_orientation != expected_orientation:
print(f"Setting {color} slice node orientation to {expected_orientation} (was {current_orientation})")
info[“node”].SetOrientation(expected_orientation)
# Ensure slice is visible
info[“node”].SetSliceVisible(1)
info[“node”].SetWidgetVisible(1)
# Validate and link volume to slice nodes via composite nodes
self.volumeNode = slicer.mrmlScene.GetFirstNodeByClass(“vtkMRMLScalarVolumeNode”)
if self.volumeNode is None:
print(“Error: No volume node found in the scene. Please load a volume.”)
return
if self.volumeNode.GetImageData() is None:
print(f"Error: Volume node {self.volumeNode.GetName()} has no image data.")
return
# Get volume bounds in RAS coordinates
self.volumeBounds = self.getVolumeBounds()
# Find composite nodes for each slice view
compositeNodes = slicer.mrmlScene.GetNodesByClass(“vtkMRMLSliceCompositeNode”)
self.compositeNodeMap = {}
for color, info in self.sliceNodes.items():
if info[“node”] is None:
continue
sliceNodeID = info[“node”].GetID()
compositeNode = None
for i in range(compositeNodes.GetNumberOfItems()):
node = compositeNodes.GetItemAsObject(i)
if node.GetLayoutName() == color:
compositeNode = node
break
if compositeNode is None:
print(f"Error: Composite node for {color} slice view not found.")
continue
# Link volume to the slice view
print(f"Linking {color} slice node to volume {self.volumeNode.GetName()} via composite node")
compositeNode.SetBackgroundVolumeID(self.volumeNode.GetID())
compositeNode.SetForegroundVolumeID(None) # Ensure no foreground volume interferes
compositeNode.SetLinkedControl(0) # Disable linked control to prevent automatic updates
self.compositeNodeMap[color] = compositeNode
# Reset field of view for all slice views
slicer.util.resetSliceViews()
# Get the crosshair node
self.crosshairNode = slicer.mrmlScene.GetFirstNodeByClass(“vtkMRMLCrosshairNode”)
if self.crosshairNode is None:
print(“Error: Crosshair node not found in the scene.”)
return
# Add observers for interaction events on each slice view
for color, info in self.sliceNodes.items():
if info[“node”] is None or color not in self.compositeNodeMap:
continue
# Access the render window via the layout manager
layoutManager = slicer.app.layoutManager()
sliceWidget = layoutManager.sliceWidget(color)
if sliceWidget is None:
print(f"Error: Slice widget for {color} not found.")
continue
renderWindow = sliceWidget.sliceView().renderWindow()
interactor = renderWindow.GetInteractor()
if interactor:
self.addObserver(interactor, vtk.vtkCommand.LeftButtonPressEvent, lambda caller, event, c=color: self.onSliceViewClicked(caller, event, c))
print(f"Added click observer for {color} slice view")
print(“SliceClickUpdater initialized successfully.”)
def getVolumeBounds(self):
“”“Get the RAS bounds of the volume.”“”
imageData = self.volumeNode.GetImageData()
if not imageData:
return None
# Get volume dimensions and spacing
dimensions = imageData.GetDimensions()
spacing = imageData.GetSpacing()
# Get IJK-to-RAS transformation matrix
ijkToRas = vtk.vtkMatrix4x4()
self.volumeNode.GetIJKToRASMatrix(ijkToRas)
# Compute bounds in RAS coordinates
rasBounds = [0, 0, 0, 0, 0, 0]
corners = [
[0, 0, 0], [dimensions[0] - 1, 0, 0], [0, dimensions[1] - 1, 0], [dimensions[0] - 1, dimensions[1] - 1, 0],
[0, 0, dimensions[2] - 1], [dimensions[0] - 1, 0, dimensions[2] - 1], [0, dimensions[1] - 1, dimensions[2] - 1], [dimensions[0] - 1, dimensions[1] - 1, dimensions[2] - 1]
]
for corner in corners:
rasPoint = [0, 0, 0, 1]
ijkToRas.MultiplyPoint([corner[0], corner[1], corner[2], 1], rasPoint)
for i in range(3):
rasBounds[2*i] = min(rasBounds[2*i], rasPoint[i])
rasBounds[2*i+1] = max(rasBounds[2*i+1], rasPoint[i])
print(f"Volume RAS bounds: R=[{rasBounds[0]}, {rasBounds[1]}], A=[{rasBounds[2]}, {rasBounds[3]}], S=[{rasBounds[4]}, {rasBounds[5]}]")
return rasBounds
def clampOffset(self, offset, orientation):
“”“Clamp the slice offset to the volume’s bounds.”“”
if not self.volumeBounds:
return offset
rMin, rMax, aMin, aMax, sMin, sMax = self.volumeBounds
if orientation == “Axial”:
return max(sMin, min(sMax, offset)) # S axis for Axial
elif orientation == “Coronal”:
return max(aMin, min(aMax, offset)) # A axis for Coronal
elif orientation == “Sagittal”:
# Use -offset to convert R to L (Left = -R in RAS)
lOffset = -offset
# Clamp to the negative of R bounds (L = -R)
clamped_offset = max(-rMax, min(-rMin, lOffset))
if lOffset < -rMax or lOffset > -rMin:
center = (-rMax - rMin) / 2
print(f"Invalid L offset {lOffset} (R={offset}) for Sagittal, using center {center}")
return center
return clamped_offset
return offset
def onSliceViewClicked(self, caller, event, color):
# Handle left-button click in the specified slice view
print(f"Left-button click detected in {color} slice view")
# Get the coordinates of the clicked point in the RAS system
ras = [0, 0, 0]
self.crosshairNode.GetCursorPositionRAS(ras)
r, a, s = ras
print(f"Click at RAS: R={r}, A={a}, S={s}")
# Update the other two slice views
for other_color, info in self.sliceNodes.items():
if other_color == color or info[“node”] is None:
continue # Skip the clicked view and any missing nodes
sliceNode = info[“node”]
sliceToRas = sliceNode.GetSliceToRAS()
orientation = sliceNode.GetOrientation()
# Set the slice based on RAS coordinates, clamped to volume bounds
if orientation == “Axial”:
clamped_offset = self.clampOffset(s, orientation)
sliceNode.SetSliceOffset(clamped_offset) # S axis for Axial view
print(f"Updated {other_color} (Axial) slice offset to {clamped_offset}")
elif orientation == “Coronal”:
clamped_offset = self.clampOffset(a, orientation)
sliceNode.SetSliceOffset(clamped_offset) # A axis for Coronal view
print(f"Updated {other_color} (Coronal) slice offset to {clamped_offset}")
elif orientation == “Sagittal”:
clamped_offset = self.clampOffset(r, orientation) # Use -R for Sagittal (L = -R)
sliceNode.SetSliceOffset(clamped_offset) # R axis for Sagittal view
print(f"Updated {other_color} (Sagittal) slice offset to {clamped_offset} (L={clamped_offset})")
else:
print(f"Unknown orientation {orientation} for {other_color} slice node")
sliceNode.UpdateMatrices()
sliceNode.SetSliceVisible(1)
sliceNode.SetWidgetVisible(1)
def removeObservers(self):
# Remove observers to deactivate the script
for color, info in self.sliceNodes.items():
if info[“node”] is None or color not in self.compositeNodeMap:
continue
layoutManager = slicer.app.layoutManager()
sliceWidget = layoutManager.sliceWidget(color)
if sliceWidget is None:
continue
renderWindow = sliceWidget.sliceView().renderWindow()
interactor = renderWindow.GetInteractor()
if interactor:
self.removeObserver(interactor, vtk.vtkCommand.LeftButtonPressEvent, lambda caller, event, c=color: self.onSliceViewClicked(caller, event, c))
print(“Observers removed.”)
# Activate the script
updater = SliceClickUpdater()
# To deactivate the script (if needed):
# updater.removeObservers()
After executing these commands, this synchronization of slices will occur.
Also, a clip has been prepared for showing this feature.
‌Best regards.
Shahrokh