# Synchronization of Axial, Coronal, and Sagittal slices by clicking on a pixel from one of the views

**URL:** <https://discourse.slicer.org/t/synchronization-of-axial-coronal-and-sagittal-slices-by-clicking-on-a-pixel-from-one-of-the-views/43943>\
**Category:** Development\
**Created:** [August 3, 2025, 1:00pm UTC](https://discourse.slicer.org/t/synchronization-of-axial-coronal-and-sagittal-slices-by-clicking-on-a-pixel-from-one-of-the-views/43943 "2025-08-03T13:00:53Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![shahrokh](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.slicer.org/shahrokh/32/1499_2.png) [@shahrokh](https://discourse.slicer.org/u/shahrokh)\
**Post date:** [August 3, 2025, 1:00pm UTC](https://discourse.slicer.org/t/synchronization-of-axial-coronal-and-sagittal-slices-by-clicking-on-a-pixel-from-one-of-the-views/43943/1 "2025-08-03T13:00:53Z")

</div>

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.

> **[ClipSynchronizationAxialCoronalSagittalSlices.mp4](https://drive.google.com/file/d/1B55jKzVH7my0RiSgLSaC_UrpYqFtcc50/view?usp=sharing)**
>
> Google Drive file.

‌Best regards.

Shahrokh

---

<div class="post-metadata">

**Author:** ![mikebind](https://avatars.discourse-cdn.com/v4/letter/m/71e660/32.png) [@mikebind](https://discourse.slicer.org/u/mikebind)\
**Post date:** [August 4, 2025, 6:24pm UTC](https://discourse.slicer.org/t/synchronization-of-axial-coronal-and-sagittal-slices-by-clicking-on-a-pixel-from-one-of-the-views/43943/2 "2025-08-04T18:24:40Z")

</div>

If you hold down the Shift key while moving the mouse over a slice view, the other slice views will change to follow the point in such a way that each view contains the point under the mouse. [https://slicer.readthedocs.io/en/latest/user\_guide/user\_interface.html](https://slicer.readthedocs.io/en/latest/user_guide/user_interface.html)

You can control whether the other slice views center on this point or simply scroll to contain that point without centering in the dropdown menu next to the crosshair icon in the toolbar (choose “Jump slices - centered” or “Jump slices -offset”).
