Controlling brush diameter increment when using Shift+Scroll

segmentation
python

(Matt) #1

When using paint tool in segment editor, the brush diameter can be controlled by holding shift key and scrolling (in addition to using the arrow keys on the UI). I would like the jump in diameter when scrolling to be much smaller than the default. Could this feature be implemented by using a python script that listened for scroll event and overrides the default behavior? Or maybe there is a more direct way?

I am using the paint tool to segment abdominal aorta on many slices. Using the arrows on the interface to adjust diameter takes too much time and having this feature would greatly speed up my workflow.

Thanks for your help!


(Andras Lasso) #2

Shift+mouse wheel is for gross adjustment of the brush size. For fine adjustments, zoom in/out the image (by default, brush size is defined in screen size, so brush size relative to the image depends on image zoom factor).


(Andras Lasso) #3

There are several semi-automatic tools, which should be able to segment the aorta within a minute (depending on image quality). For example, try these options:

  • Fast marching effect: available after you install SegmentEditorExtraEffects extension; you just need to paint a few seed regions inside the aorta, and choose how large region you want to grow
  • Grow from seeds effect: you need to paint inside with one segment and paint outside non-vessel background with another segment
  • If you prefer full more manual segmentation, then you can use paint tool (with non-sphere brush) on every 5th-10th slice and create a full 3D segmentation from these isolated slices using Fill between slices effect.

(Matt) #4

Thanks for your reply! That is very helpful. I think I will use one of those automated methods you listed. However, I would like to also implement an override to the default resizing of brush size. The idea is to have two keys that when pressed will increase/decrease the brush diameter by a pixel size I specify. To do so, I need the ability to programatically change the diameter. Using other posts on this forum, I figured out how to change BrushSphere parameter with below:

segmentEditorWidget = slicer.qMRMLSegmentEditorWidget()
segmentEditorWidget.setMRMLScene(slicer.mrmlScene)
segmentEditorNode = slicer.mrmlScene.GetSingletonNode(“SegmentEditor”, “vtkMRMLSegmentEditorNode”)
slicer.mrmlScene.AddNode(segmentEditorNode)
segmentEditorWidget.setMRMLSegmentEditorNode(segmentEditorNode)
segmentEditorWidget.setActiveEffectByName(“Paint”)
effect = segmentEditorWidget.activeEffect()
effect.setParameter(“BrushSphere”,“1”)

However, I have been unable to find the parameter name for brush diameter. Do you know what this is and is there documentation somewhere with a list of parameter names?

Also, is it possible to listen for keyboard press events using a python script within 3D-slicer? How would I go about doing this?

Thanks!


(Matt) #5

Actually, just found your comment on this post. And have figured out how to handle keyboard press events. An example that decreases brush diameter when “M” is pressed is shown below:

import qt
import slicer

PARAM_DIAM = "BrushAbsoluteDiameter"

def decreaseDiameter():
	segmentEditorWidget = slicer.qMRMLSegmentEditorWidget()
	segmentEditorWidget.setMRMLScene(slicer.mrmlScene)
	segmentEditorNode = slicer.mrmlScene.GetSingletonNode("SegmentEditor", "vtkMRMLSegmentEditorNode")
	slicer.mrmlScene.AddNode(segmentEditorNode)
	segmentEditorWidget.setMRMLSegmentEditorNode(segmentEditorNode)

	segmentEditorWidget.setActiveEffectByName("Paint")
	effect = segmentEditorWidget.activeEffect()

	currentDiam = float(effect.parameter(PARAM_DIAM))
	newDiam = currentDiam - 0.5
	print("decreasing to new diam of %0.2f" % newDiam)
	effect.setParameter(PARAM_DIAM, newDiam)
	effect.forceRender

shortcutDecrease = qt.QShortcut(slicer.util.mainWindow())
shortcutDecrease.setKey(qt.QKeySequence("M"))
shortcutDecrease.connect("activated()", decreaseDiameter)

However, I am having a problem where the brush diameter does not refresh until I move the mouse. Is there a way to force this to repaint?


(Andras Lasso) #6

Nice, you’ve almost done it! The brush size is not updating because effect.forceRender without () does not call anything, just returns the address of the method and rendering does not force update of the brush model anyway. Replace effect.forceRender by this:

# get current slice node
crosshairNode=slicer.mrmlScene.GetSingletonNode('default','vtkMRMLCrosshairNode')
pos_unused = [0,0,0]
sliceNode = crosshairNode.GetCursorPositionXYZ(pos_unused)
if sliceNode:
    # force updating displayed brush size
    sliceNode.Modified()

Still, I would recommend to zoom in the image if you want to segment smaller details: it not just zooms in/out the image but also decreases/increases absolute brush size, and it is done very smoothly (no “jumps” in zoom factor) if you use right-click+mouse move up/down for zooming.

We introduced brush size defined in screen size exactly for this purpose: we realized that every time we decreased brush size we always had to zoom in as well. So, if you use brush size relative to screen size option (default) then you just need to set an approximate brush size once (dictated by how accurately you can move your mouse pointer) and you do the fine brush sizing by zooming the image in/out.


(Matt) #7

The smooth zooming is a very nice feature. For more customization, I am going to try and complete this python feature to give more control. Below is my updated code. Buttons “D” and “F” decrease and increase the diameter, and buttons “E” and “R” decrease/increase the increment by which the diameter is changed. One additional problem I had to deal with is that when there were multiple segments and a segment other than the first one was selected, after running my code the selection would change back to the first segment. I fixed this by getting the selected-segment-id from the segment-editor-node and then setting the ID later in the segment-editor widget.

import qt
import slicer
from functools import partial
from qt import QCursor
from time import sleep

PARAM_DIAM = "BrushAbsoluteDiameter"
increment = 0.5

def repaint():
	crosshairNode=slicer.mrmlScene.GetSingletonNode('default','vtkMRMLCrosshairNode')
	pos_unused = [0,0,0]
	sliceNode = crosshairNode.GetCursorPositionXYZ(pos_unused)
	if sliceNode:
	    # force updating displayed brush size
	    sliceNode.Modified()

def changeDiameter(direction):
	segmentEditorWidget = slicer.qMRMLSegmentEditorWidget()

	segmentEditorWidget.setMRMLScene(slicer.mrmlScene)
	segmentEditorNode = slicer.mrmlScene.GetSingletonNode("SegmentEditor", "vtkMRMLSegmentEditorNode")
	slicer.mrmlScene.AddNode(segmentEditorNode)

	# Once node passed to widget the selected segment will get set back to first
	selectedSegmentID = segmentEditorNode.GetSelectedSegmentID()
	
	segmentEditorWidget.setMRMLSegmentEditorNode(segmentEditorNode)

	# Set selected segment back to what it was before
	segmentEditorWidget.setCurrentSegmentID(selectedSegmentID)
	
	segmentEditorWidget.setActiveEffectByName("Paint")
	effect = segmentEditorWidget.activeEffect()

	currentDiam = float(effect.parameter(PARAM_DIAM))
	newDiam = currentDiam + (increment*direction)

	if newDiam <= 0:
		return

	print("decreasing to new diam of %0.2f" % newDiam)
	effect.setParameter(PARAM_DIAM, newDiam)
	repaint()

def changeIncrement(direction):
	global increment
	newIncrement = increment + (0.1*direction)
	if newIncrement <= 0:
		return
	increment = newIncrement
	print("Increment: %0.2f" % increment)
	repaint()

shortcutDecrease = qt.QShortcut(slicer.util.mainWindow())
shortcutDecrease.setKey(qt.QKeySequence("D"))
shortcutDecrease.connect("activated()", partial(changeDiameter, -1))

shortcutIncrease = qt.QShortcut(slicer.util.mainWindow())
shortcutIncrease.setKey(qt.QKeySequence("F"))
shortcutIncrease.connect("activated()", partial(changeDiameter, 1))

shortcutDecreaseFactor = qt.QShortcut(slicer.util.mainWindow())
shortcutDecreaseFactor.setKey(qt.QKeySequence("E"))
shortcutDecreaseFactor.connect("activated()", partial(changeIncrement, -1))

shortcutIncreaseFactor = qt.QShortcut(slicer.util.mainWindow())
shortcutIncreaseFactor.setKey(qt.QKeySequence("R"))
shortcutIncreaseFactor.connect("activated()", partial(changeIncrement, 1))

While the code serves its functional purpose, the last remaining bug is that after running this code you can no longer use the UI of the segment editor widget to change the diameter. The diameter is held to whatever is programatically set and neither using Shift+Scroll nor the UI can change it. I figure there must be some way to release the effect back to the default but not sure how to do this. If you have the time would appreciate how I can eliminate this last bug.

Thanks for your continued help!


(Andras Lasso) #8

One big issue (that might cause the not updating brush size) is that you call slicer.qMRMLSegmentEditorWidget() each time you change the brush size, which creates a completely new segment editor widget!! (you never show this widget, so you don’t see it, but probably after a while you would notice slowdowns)

If you want to interact with the existing segment editor widget then you can get it using:

segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor

(Matt) #9

Ah, I see. Updated changeDiameter method to following:

def changeDiameter(direction):
	segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor

	segmentEditorWidget.setActiveEffectByName("Paint")
	effect = segmentEditorWidget.activeEffect()

	currentDiam = float(effect.parameter(PARAM_DIAM))
	newDiam = currentDiam + (increment*direction)

	if newDiam <= 0:
		return

	print("decreasing to new diam of %0.2f" % newDiam)
	effect.setParameter("BrushDiameterIsRelative","0")
	effect.setParameter(PARAM_DIAM, newDiam)
	repaint()

Still have the same bug as before (cannot change brush diameter through default means), but definitely much cleaner code


(Andras Lasso) #10

Good. I guess not re-creating the segment editor widget fixed the selection reset issue.

Brush parameters are shared between paint and erase effects, so it is a common parameter, which need to be set by using setCommonParameter method:

effect.setCommonParameter("BrushDiameterIsRelative", "0")
effect.setCommonParameter("BrushAbsoluteDiameter", newDiam)

(Matt) #11

After that last change, it works perfectly! Thank you for all your help!!