3D View interactive ROI Cropping

Only administrators can move posts between topics. If you give a list of posts that you want to move out to a separate topic then I can move them for you, but it is probably easier to just create a new topic and include links to relevant posts.

Sorry, Iā€™m referring to the SlicerQREADS Development topic. I will post about the extraction to a new topic thereā€¦

Hello @lassoan, what do you mean by ā€œenter interactive rendering mode?ā€

Thanks

The renderer (or the interactor) has a flag that temporarily changes the desired update rate while interacting with the view (typically zooming/rotating the view). This results in faster, lower-quality rendering while the view is being rotated and when you release the mouse button then a normal-quality rendering is done. This interactive mode is enabled by the volume rendering offset slider, too. Note that this makes the most difference for CPU volume rendering, which should not be used on modern hardware.

OK, new terms; Volume rendering offset slider. Is this a type of widget? Also, shouldnā€™t the lower-quality rendering mode be enabled automatically for CPU volume rendering? How do you set this flag? Whatā€™s the name of it.

Thanks

I meant the slider labeled as ā€œShiftā€ (sorry for the confusion with mentioning ā€œoffsetā€):

image

You want to have high-quality volume rendering. You just want to temporarily lower the resolution while you are interactively adjusting parameters. Nowadays average laptops have enough graphics capability that GPU volume rendering is a few times faster than CPU volume rendering (and discrete GPUs are 10-100x faster), so I would recommend using GPU volume rendering by default. Most likely you would run into issues with GPU rendering only if you are trying to render a large, high-resolution CT on a low-performance computer, but there I would recommend to downsample the volume (lower the resolution by a factor of 2x along each axis would results in 8x smaller memory requirement and barely visible image quality loss for volume rendering of a high-resolution volume).

If there are many users with old computers in your organization that would like to volume-render high-resolution volumes then it could worth adding an option to automatically downsample large volumes after loading.

You can find source code for a function on the GUI by searching for text that you see in the GUI and then following the references, as shown in this example. I give an example for this ā€œShiftā€ slider, too:

  • search for shift ui in the Slicer github repository (we add ui to the search terms because most user interface elements are specified in .ui files)
  • the first search hit has volume rendering in its name and it is a .ui file (qSlicerVolumeRenderingPresetComboBox.ui), so it looks like this is the file we need, click on that link
  • Search for offset in the file and you can find a slider with the name PresetOffsetSlider. This is the name how the slider is referred to in source code. Now you just need to search for PresetOffsetSlider in the source code.
  • Again, the first hit is the correct one. Click on the link to see the file.
  • Search for PresetOffsetSlider in the file and you can see how interaction events (SliderPressed, SliderReleased, etc. map to methods). You can search for the associated method names startInteraction, endInteraction to see how interactive rendering mode is activated/deactivated:
1 Like

Thank you and yes @lassoan. I tried this search method after @jamesobutler instructed me on how to use the Slicer Code Repo to search for the code. I totally did not know how to do the final translation to Python code. Do I need to create new code on the C++ level, or is there a way to reuse this code? Thatā€™s where I got stuck.

Thanks

You can use the same classes from C++ and Python. There are just a number of trivial syntax differences between Python and C++ that we listed here.

@lassoan,

OK, just to complete my understanding of how to translate to Python from the C++ I find. I appreciate your patience here and please forgive me for my gap in understandingā€¦

In qSlicerVolumeRenderingPresetComboBox.cxx I found the followingā€¦

  QObject::connect(this->PresetOffsetSlider, SIGNAL(valueChanged(double)),
    q, SLOT(offsetPreset(double)));
  QObject::connect(this->PresetOffsetSlider, SIGNAL(sliderPressed()),
    q, SLOT(startInteraction()));
  QObject::connect(this->PresetOffsetSlider, SIGNAL(valueChanged(double)),
    q, SLOT(interaction()));
  QObject::connect(this->PresetOffsetSlider, SIGNAL(sliderReleased()),
    q, SLOT(endInteraction()));

From here, do I set up 4 corresponding signal/slot connections in Python? I.e,ā€¦

self.ui.RenderShiftSlider.connect("valueChanged(double)", self.offsetPreset()
self.ui.RenderShiftSlider.connect("sliderPressed", self.startInteraction()
self.ui.RenderShiftSlider.connect("valueChanged(double)", self.interaction()
self.ui.RenderShiftSlider.connect("sliderReleased", self.endInteraction()

Thanks

ā€¦ and I imagine for example, in this function

void qSlicerVolumeRenderingPresetComboBox::startInteraction()
{
  Q_D(qSlicerVolumeRenderingPresetComboBox);
  if (!d->VolumePropertyNode)
    {
    return;
    }

  vtkVolumeProperty* volumeProperty = d->VolumePropertyNode->GetVolumeProperty();
  if (volumeProperty)
    {
    volumeProperty->InvokeEvent(vtkCommand::StartInteractionEvent);
    }
}

I donā€™t know how I would somehow invoke the StartInteractionEvent from Python.

@lassoan,

@jcfr is using a similar Slider control to change the thickness. But, the only Slot (or event handler) is updateParameterNodeFromGUI

self.ui.SlabThicknessSliderWidget.connect("valueChanged(double)", self.updateParameterNodeFromGUI)

In updateParameterNodeFromGUI is this staementā€¦

self._parameterNode.SetParameter("SlabThicknessInMm", str(self.ui.SlabThicknessSliderWidget.value))

ā€¦ and things magically happen from there.

I guess ā€œSlabThicknessInMmā€ is a slicer parameter?

The correct syntax is:

self.ui.RenderShiftSlider.connect("valueChanged(double)", self.offsetPreset

It will look something like this in Python:

def startInteraction(self):
  if not self.VolumePropertyNode:
    return
  volumeProperty = self.VolumePropertyNode.GetVolumeProperty()
  if volumeProperty:
    volumeProperty.InvokeEvent(vtk.vtkCommand.StartInteractionEvent)

There is nothing magical, itā€™s just the GUI and logic are nicely separated. Most likely you have added an observer to the parameter node in your logic class, so if the parameter node changes then a callback function is executed, which updates your view.

here is all of my codeā€¦

   self.OldPresetPosition = 0

   self.ui.RenderShiftSlider.connect("sliderPressed", self.startInteraction)
   self.ui.RenderShiftSlider.connect("valueChanged(double)", self.offsetPreset)
   self.ui.RenderShiftSlider.connect("valueChanged(double)", self.interaction)
   self.ui.RenderShiftSlider.connect("sliderReleased", self.endInteraction)

  def startInteraction(self):
    if not self.displayNode:
      return
    volumeProperty = self.displayNode.GetVolumeProperty()
    if volumeProperty:
      volumeProperty.InvokeEvent(vtk.vtkCommand.StartInteractionEvent)

  def endInteraction(self):
    if not self.displayNode:
      return
    volumeProperty = self.displayNode.GetVolumeProperty()
    if volumeProperty:
      volumeProperty.InvokeEvent(vtk.vtkCommand.EndInteractionEvent)

  def offsetPreset(self,newPosition):
    if not self.displayNode:
      return

    # WRONG SYTAX  emit presetOffsetChanged(newPosition - self.OldPresetPosition, 0., False)

    self.OldPresetPosition = newPosition

  def interaction(self):
    if not self.displayNode:
      return
    volumeProperty = self.displayNode.GetVolumeProperty()
    if volumeProperty:
      volumeProperty.InvokeEvent(vtk.vtkCommand.InteractionEvent)

In the above code, Iā€™m using the node referencing my volume rendering named self.displayNode, instead of self.VolumePropertyNode. self.OldPresetPosition is a local variable that keeps track of the old value.

I hope thatā€™s mostly correct, how do I translate ā€¦

emit presetOffsetChanged(newPosition - self.OldPresetPosition, 0., False)

ā€¦ to Python. What is ā€œemitā€?

ā€œemitā€ is a Qt keyword, it makes a Qt class emit a signal that can be observed or connected to Qt slots. Since you donā€™t need to notify anyone about the offset change, you can simply skip this line.

Do I replace the it with code that change the render opacity? Iā€™m guessing the presetOffsetChanged or
moveAllPoints code does exactly thatā€¦

I found this in the codeā€¦

  QObject::connect(this->PresetComboBox, SIGNAL(presetOffsetChanged(double, double, bool)),
                   this->VolumePropertyNodeWidget, SLOT(moveAllPoints(double, double, bool)));

Shall I implement the moveAllPoints method?

Yes, unfortunately this method is implemented at the wrong level (in GUI instead of widget) therefore it cannot be reused but you need to implement it in your own code.

ā€¦ or shall I use this oneā€¦

def updateRenderShift(self):
    gradientThreshold = self.ui.RenderShiftSlider.value
    maxOpacity = 1.0
    gradientOpacityTransferFunction = self.displayNode.GetVolumePropertyNode().GetVolumeProperty().GetGradientOpacity()
    gradientOpacityTransferFunction.RemoveAllPoints()
    gradientOpacityTransferFunction.AddPoint(0, 0.0)
    gradientOpacityTransferFunction.AddPoint(gradientThreshold-1, 0.0)
    gradientOpacityTransferFunction.AddPoint(gradientThreshold+1, maxOpacity)
    self.displayNode.SetVisibility(True)

Thanks

Something similar. The code snippet above removes all the points, then creates a new gradient opacity transfer function. If you want to shift an existing preset then you need to iterate through its points of the opacity and color transfer functions and change the x coordinate values of the points.

Hello @lassoan @jamesobutler .

For adjusting the x coordinate (gradient threshold), the documentation statesā€¦

  :param gradientThreshold: regions that has gradient value below this threshold will be made transparent
    (minimum value is 0.0, higher values make more tissues transparent, starting with soft tissues)

Question: What do I use for the max gradientThreshold value? The series max pixel value? If not, then what?

Thanks

Note that the code snippet above adjusts the gradient opacity transfer function. The offset slider in volume rendering module does not touch the gradient opacity transfer function, just the scalar opacity and color transfer (because the gradient is not sensitive to absolute image intensity changes, so it should not be adjusted along with the two other transfer functions).