Skip to content

Commit

Permalink
Merge pull request #365 from paskino/viewer_slider
Browse files Browse the repository at this point in the history
Closes #358 

- Add slider widget
  • Loading branch information
DanicaSTFC authored Apr 25, 2024
2 parents c33ace9 + ecad4f0 commit 6d6baa9
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog
## vx.x.x
- Add slider widget #365
- Removed VTK 8 variants from conda recipe.
- Change Python variants: removed 3.6 and 3.7, added 3.11 and 3.12.
- Bugfix on resample reader #359. Standalone viewer app defaults to downsample in all dimensions.
Expand Down
80 changes: 78 additions & 2 deletions Wrappers/Python/ccpi/viewer/CILViewer2D.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from ccpi.viewer.CILViewerBase import CILViewerBase
from ccpi.viewer.utils import Converter

from ccpi.viewer.widgets import cilviewerBoxWidget
from ccpi.viewer.widgets import cilviewerBoxWidget, SliceSliderRepresentation, SliderCallback


class CILInteractorStyle(vtk.vtkInteractorStyle):
Expand Down Expand Up @@ -991,7 +991,7 @@ class CILViewer2D(CILViewerBase):
IMAGE_WITH_OVERLAY = 0
RECTILINEAR_WIPE = 1

def __init__(self, dimx=600, dimy=600, ren=None, renWin=None, iren=None, debug=True):
def __init__(self, dimx=600, dimy=600, ren=None, renWin=None, iren=None, debug=False, enableSliderWidget=True):
CILViewerBase.__init__(self, dimx=dimx, dimy=dimy, ren=ren, renWin=renWin, iren=iren, debug=debug)

self.setInteractorStyle(CILInteractorStyle(self))
Expand Down Expand Up @@ -1140,6 +1140,10 @@ def __init__(self, dimx=600, dimy=600, ren=None, renWin=None, iren=None, debug=T
self.imageTracer.AutoCloseOn()
self.imageTracer.AddObserver(vtk.vtkWidgetEvent.Select, self.style.OnTracerModifiedEvent, 1.0)

# Slider widget
self.sliderWidget = None
self._sliderWidgetEnabled = enableSliderWidget

self.__vis_mode = CILViewer2D.IMAGE_WITH_OVERLAY
self.setVisualisationToImageWithOverlay()

Expand All @@ -1153,6 +1157,7 @@ def setInput3DData(self, imageData):

def setInputData(self, imageData):
self.log("setInputData")
self.reset()
self.img3D = imageData
self.installPipeline()
self.axes_initialised = True
Expand Down Expand Up @@ -1310,6 +1315,9 @@ def installPipeline(self):
elif self.vis_mode == CILViewer2D.RECTILINEAR_WIPE:
self.installRectilinearWipePipeline()

if self.getSliderWidgetEnabled():
self.installSliceSliderWidgetPipeline()

self.ren.ResetCamera()
self.ren.Render()

Expand Down Expand Up @@ -1460,6 +1468,59 @@ def installRectilinearWipePipeline(self):

self.AddActor(wipeSlice, WIPE_ACTOR)

def installSliceSliderWidgetPipeline(self):
'''Create the pipeline for the slice slider widget
The slider widget and representation are created if not already present.
Currently the slider widget enabled flag is not used.
'''
if self.sliderWidget is not None:
# reset the values to the appropriate ones of the new loaded image
self.sliderCallback.update_orientation(self.style, 'reset')
return

sr = SliceSliderRepresentation()
sr.SetValue(self.getActiveSlice())
sr.SetMaximumValue(self.img3D.GetDimensions()[2] - 1)
sr.SetMinimumValue(0)

sw = vtk.vtkSliderWidget()
sw.SetInteractor(self.getInteractor())
sw.SetRepresentation(sr)
sw.SetAnimationModeToAnimate()
sw.EnabledOn()

cb = SliderCallback(self, sw)

# Add interaction observers
# propagate events from the slider to the viewer
sw.AddObserver(vtk.vtkCommand.InteractionEvent, cb)

# propagate events from the viewer to the slider
self.style.AddObserver("MouseWheelForwardEvent", cb.update_from_viewer, 0.9 )
self.style.AddObserver("MouseWheelBackwardEvent", cb.update_from_viewer, 0.9 )
self.style.AddObserver("CharEvent", cb.update_orientation, 0.9 )

# reset the slider
cb.update_from_viewer(self.style, 'reset')

# save references
self.sliderWidget = sw
self.sliderCallback = cb

def uninstallSliderWidget(self):
'''remove the slider widget from the viewer'''
if self.sliderWidget is not None:
sr = self.sliderWidget.GetRepresentation()
if sr is not None:
sr.RemoveAllObservers()
coll = vtk.vtkPropCollection()
sr.GetActors(coll)
print ("coll", coll)
for actor in coll:
print ("actor", actor)
self.ren.RemoveActor(actor)

def AdjustCamera(self, resetcamera=False):
self.ren.ResetCameraClippingRange()

Expand Down Expand Up @@ -1801,3 +1862,18 @@ def uninstallPipeline2(self):
elif self.vis_mode == CILViewer2D.RECTILINEAR_WIPE:
# rectilinear wipe visualises 2 images in the same pipeline
pass

def reset(self):
self.uninstallPipeline()
if self.image2 is not None:
self.uninstallPipeline2()
if self.getSliderWidgetEnabled():
self.uninstallSliderWidget()

def getSliderWidgetEnabled(self):
return self._sliderWidgetEnabled
def setSliderWidgetEnabled(self, enable):
if enable:
self._sliderWidgetEnabled = enable


2 changes: 1 addition & 1 deletion Wrappers/Python/ccpi/viewer/utils/conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -933,7 +933,7 @@ def calculate_target_downsample_magnification(max_size, total_size, acq=False):


def calculate_target_downsample_shape(max_size, total_size, shape, acq=False):
'''calculate the magnification of each axis and the number of slices per chunk
'''calculate the shape of the resampled image
Parameters
----------
Expand Down
1 change: 1 addition & 0 deletions Wrappers/Python/ccpi/viewer/widgets/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .box_widgets import cilviewerBoxWidget, cilviewerLineWidget
from .slider import SliderCallback, SliceSliderRepresentation
139 changes: 139 additions & 0 deletions Wrappers/Python/ccpi/viewer/widgets/slider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import vtk
import logging

logger = logging.getLogger(__name__)

class SliceSliderRepresentation(vtk.vtkSliderRepresentation2D):
"""A slider representation for the slice selector slider on a 2D CILViewer
Parameters
-----------
orientation: str, optional
The orientation of the slider. Can be 'horizontal' or 'vertical'
offset: float, optional
The offset of the slider from the edge of the window. Default is 0.12
"""

def __init__(self, orientation='horizontal', offset=0.12):
self.tube_width = 0.004
self.slider_length = 0.015
self.slider_width = 0.015
self.end_cap_length = 0.008
self.end_cap_width = 0.02
self.title_height = 0.02
self.label_height = 0.02
self.bar_color = 'Gray'
cil_pink = [ [ el / 0xff for el in [0xe5, 0x06, 0x95] ],
[ el / 0xff for el in [0xc9, 0x2c, 0x99] ],
[ el / 0xff for el in [0x99, 0x3d, 0xbb] ],
[ el / 0xff for el in [0x51, 0x0c, 0x76] ]
]

self.orientation = 'horizontal'
self.offset = 0.12

self.p1 = [self.offset, self.end_cap_width * 1.1]
self.p2 = [1 - self.offset, self.end_cap_width * 1.1]

self.title = None

if orientation == 'vertical':
self.offset = offset
self.p1 = [self.end_cap_width * 1.1, self.offset]
self.p2 = [self.end_cap_width * 1.1, 1 - self.offset]

self.SetTitleText(self.title)

self.GetPoint1Coordinate().SetCoordinateSystemToNormalizedDisplay()
self.GetPoint1Coordinate().SetValue(self.p1[0], self.p1[1])
self.GetPoint2Coordinate().SetCoordinateSystemToNormalizedDisplay()
self.GetPoint2Coordinate().SetValue(self.p2[0], self.p2[1])

self.SetTubeWidth(self.tube_width)
self.SetSliderLength(self.slider_length)
# slider_width = self.tube_width
# slider.SetSliderWidth(slider_width)
self.SetEndCapLength(self.end_cap_length)
self.SetEndCapWidth(self.end_cap_width)
self.SetTitleHeight(self.title_height)
self.SetLabelHeight(self.label_height)

# Set the colors of the slider components.
# Change the color of the bar.
self.GetTubeProperty().SetColor(vtk.vtkNamedColors().GetColor3d(self.bar_color))
# Change the color of the ends of the bar.
self.GetCapProperty().SetColor(cil_pink[2])
# Change the color of the knob that slides.
self.GetSliderProperty().SetColor(cil_pink[1])
# Change the color of the knob when the mouse is held on it.
self.GetSelectedProperty().SetColor(cil_pink[0])

class SliderCallback:
'''
Class to propagate the effects of interaction between the slider widget and the viewer
the slider is embedded into, and viceversa.
Parameters
-----------
viewer : CILViewer2D the slider is embedded into
slider_widget : the vtkSliderWidget that is embedded in the viewer
'''
def __init__(self, viewer, slider_widget):
self.viewer = viewer
self.slider_widget = slider_widget

def __call__(self, caller, ev):
'''Update the slice displayed by the viewer when the slider is moved
Parameters
-----------
caller : the slider widget
ev : the event that triggered the update
'''
slider_widget = caller
value = slider_widget.GetRepresentation().GetValue()
self.viewer.displaySlice(int(value))
self.update_label(value)

def update_label(self, value):
'''Update the text label on the slider. This is called by update_from_viewer
Parameters
-----------
value : the value to be displayed on text label the slider
'''
rep = self.slider_widget.GetRepresentation()
maxval = rep.GetMaximumValue()
txt = "{}/{}".format(int(value), int(maxval))
rep.SetLabelFormat(txt)

def update_from_viewer(self, caller, ev):
'''Update the slider widget from the viewer. This is called when the viewer changes the slice
Parameters
-----------
caller : the interactor style
ev : the event that triggered the update
'''
# The caller is the interactor style
logger.info(f"Updating for event {ev}")
value = caller.GetActiveSlice()
self.slider_widget.GetRepresentation().SetValue(value)
self.update_label(value)
caller.GetRenderWindow().Render()

def update_orientation(self, caller, ev):
'''Update the slider widget when the orientation is changed
Parameters
-----------
caller : the interactor style
ev : the event that triggered the update
'''
logger.info(f"Updating orientation {ev}")
value = caller.GetActiveSlice()
dims = caller._viewer.img3D.GetDimensions()
maxslice = dims[caller.GetSliceOrientation()] -1
self.slider_widget.GetRepresentation().SetMaximumValue(maxslice)
self.update_from_viewer(caller, ev)

0 comments on commit 6d6baa9

Please sign in to comment.