Improving qMRMLVolumeWidget range bounds

I’m hoping to improve the qMRMLVolumeWidget based on feedback from several users that I know. There is a common complaint about widgets that inherit functionality from qMRMLVolumeWidget, specifically commonly used qMRMLWindowLevelWidget and qMRMLVolumeThresholdWidget.

There is frequent frustrations regarding the range bounds slider in the popup widget and the values that it chooses to use. Users find it annoying and weird that the primary slider can select values outside of the scalar range of the MRMLVolumeNode that is currently set to the widget. There appears to be some logic that tries to provide some space above and below the min/max of the scalar range. Is there a reason why it was decided to function like this? It appears that setting bounds inside the min max scalar range could help fine tune slider selection, but it doesn’t seem to make sense to support values outside the range of the min max scalar values. When users are changing sliders and nothing is happening due to being outside scalar range, that’s explained as being frustrating. cc: @pieper, @lassoan

The specific code in question is updateRangeForVolumeDisplayNode. See
https://github.com/Slicer/Slicer/blame/daad7c2f41f1706ab8c2d0b144aeea38f9b5c6a3/Libs/MRML/Widgets/qMRMLVolumeWidget.cxx#L120-L163

image

Would it be valid to change the current behavior to pin these sliders so the range could never be set lower than the scalar min or greater than the scalar max? Or does this need be a new parameter for this qMRMLVolumeWidget for choosing between desired functionality?

I have only used these widgets a few times in my life. Maybe partly because they are inconvenient, but mainly because I always set window/level visually (using the window/level mouse mode). Can you describe why and how do you use these widgets? Is there any workflow when you rely on setting these values numerically instead of visually?

The popup makes sense in theory (it saves space and clicks), but I think the widget is quite confusing.

slicer.qMRMLRangeWidget() solves the same task in a much cleaner way. It uses more space (the “…” button), and it requires one more click, but it is still easier to understand its logic.

image

When clicking on the range adjustment slider:

image

Most often you actually select values outside of the scalar range of the volume, so this should not be annoying or weird. If the min/max values are set to values within the scalar range then the display is saturated (=signal is lost), so this should not be the norm.

I agree that it is not useful is to allow the minimum mapped value to be larger than the scalar range maximum, or the maximum mapped value to be smaller than the scalar range minimum. However, these limits don’t help with specifying a finite range for the slider, because the minimum mapped value must still be allowed to be much lower than the scalar range minimum and the maximum mapped value must be allowed to go well above the scalar range maximum.

The logic is that maximum range of the slider is 5x larger than the scalar range of the volume (with some extra heuristics, that if the scalar range is larger than 10 then the mapped range is minimum -1200 to 900). It means that you can reduce the displayed contrast of the volume to 1/5th of the original contrast.

Mapped range = scalar range:

Mapped range = 5x scalar range (1/5th contrast):

To me it looks reasonable that we allow this much contrast reduction. Most details are still visible and such faded out image can be quite useful for example when the image is shown overlaid on another image.

Considering all the above, most likely the frustration is not due to the size of the accessible slider range (which seems reasonable), but how the widget appears and behaves (the popup automatically showing up when the widget is approached, and the main slider and the popup slider being too close and too similar).

What do you think about replacing the automatic popup with a ... button similar to the qMRMLRangeWidget?

+1 for changing the popup to a collapsible area to set the range and maybe other properties. The way the current widget comes and goes makes things seem more complex than they really are.

Yes, I would be a +1 for changing qMRMLVolumeWidget to use a qMRMLRangeWidget rather than the current ctkRangeWidget + ctkPopupWidget usage. Indeed, users didn’t like the popup as often they were just hovering over and they were just trying to change the values of Window/Level or Threshold. Also the popup would appear and cover up widgets below too. They weren’t actually wanting to change bounds. Having the ToolButton extra click option would seem appropriate for the less used action to make it more deliberate. I had previously tried manipulating things such as calling popup_widget.autoShow = False but then changing Window/Level mode would then cause the popup to appear again.
image

As it relates to setting the bounds based on the scalar range of the current set volume, I think the primary reason for this was specifically as it relates to the qMRMLVolumeThresholdWidget. Having the threshold min value be less than scalar min or the threshold max be greater than the scalar max doesn’t make sense as nothing will change. So currently if you load the MRHead sample dataset, the threshold min/max will be set to -600 to 600 (the full range based on the current bounds). However, the scalar range for the volume is between 0 and 279. It is confusing to the user to change threshold value and nothing change in the image. Moving the threshold min form -600 to 0 does nothing in this case. Only, 279/1200=0.2325, 23.5% of the slider is actually doing anything.
image

I’m going to ask the user more about the qMRMLWindowLevelWidget bounds usage and will report back here. Just wanted to go ahead and provide some more feedback.

100% agree, using the tool button or another explicit solutions seems like a good alternative to the intrusive and unexpected popup.

Ok here are some updates after clarifying behavior from some users.

They are all on board for the change to the qMRMLRangeWidget instead of the ctkRangeWidget + ctkPopupWidget usage. They also appreciate how the bounds works in the qMRMLRangeWidget in that it allows setting pretty much an infinite value. In the current widget, the MRHead volume has bounds of -600 to 600 but only allows the bounds to become -1200 to 900. They want to be able to set their bounds to whatever they want. Some of the images we work with have representations that have upper bounds dependent on various acquisition settings and not the base units of the detector for say like a uint8 of 0 to 255. So providing the flexibility to set upper bound to whatever is desired. The qMRMLRangeWidget will support this and they are excited to see this change.
image

What they are really desiring is being able to set the Window/Level Min/Max to the exact same values across various volumes to then be able to visually compare that this Ultrasound image is brighter than the other Ultrasound image. How the current widget bounds work (that -1200 to 900 range), it can at times prevent two volumes from having the exact same Window/Level Min/Max for comparison.

As it relates to qMRMLVolumeThresholdWidget, they want the values used for the bounds to be set automatically based on the scalar range. So when a new volume node is set to the widget the bounds get automatically set to the scalar min max value. The widget values would continue to reflect the upper and lower threshold values as set in the volume node’s display node. Of course after changing to the qMRMLRangeWidget for this widget as well, the upper bounds could be set to anything and that could still be supported for consistency reasons with the Window Level Widget, although setting bounds above the max scalar value really doesn’t seem to be helpful in any way and same with setting lower bound to anything less than the min scalar value.

This makes complete sense.

Such feature could be added as a right-click menu action to Data module, similarly to the “Show volumes in folder” action (right-click on eye icon of a folder; see implementation here). Or it could be a right-click action on a volume that would copy the volume’s window/level settings to all other sibling volumes (volumes in the same branch of the tree).

Or, it could be added to the “Compare volumes” module (or any equivalent module that you use now for setting up volume comparison).

I agree that setting the threshold widget range to the volume scalar range by default makes sense (if the current threshold is outside the volume’s scalar range then the widget range would need to be set to accommodate that).

It would be great if you could update qMRMLVolumeThresholdWidget and qMRMLWindowLevelWidget so that they used qMRMLRangeWidget internally, while keeping the current API.

Sure I can try to make these edits. I may post a PR to get final help on some C++ related help as that’s not my strong point. Expect something this upcoming week.

Should the min/max spinboxes on either side of the window level mode combobox be replaced by the spinboxes included in the qMRMLRangeWidget? I think the current logic is doing some show/hiding of spinboxes whether window/level or the min/max value. Then the combobox expanding the full width?

I see, the qMRMLRangeWidget does not have the window/level numerical display mode, so it is not a full replacement of the window/level widget. Since the layout of the window/level widget is quite nice and the only issue is the automatic popup, it may be easier to just modify the current widgets by making the popup appear when the newly added ... button is clicked. The ... button should be added to the top row because there is more space there, but it’s probably fine to add it next to the bottom row (on the right side of the range slider), too.

I’ve posted a PR regarding this work:

1 Like

As a final update, the PR has been merged for this work.