Improve markups label visibility

Sometimes markups labels are difficult to read over complex background. Should we enable shadow and bold style by default to improve visibility?

Examples:

Current default (no bold or shadow):
image

Shadow:
image

Bold:
image

Shadow+bold:
image

Shadow+bold (with smaller font size):
image

A shadow plus bold, slightly smaller font looks good to me. A screen-space halo would be even better but I don’t think that’s available in VTK.

OHIF defaults to a yellow which is more readable sometimes but not always.

image

They use a convention I do like though, which is that the markup is green while it’s being actively defined (e.g. when only one point of a line has been placed) the then it gets it’s final color when completed.

Shadow and bold look great!

1 Like

In anatomical drawings/labelling, it is common to pair these lines with a color that is much darker in contrast (e.g., white/black, yellow/blue etc), so that part of the line remains visible regardless both in areas of high/low contrast. On option like this would be great actually!

Yes, that’s what the drop shadow does, but our default markup color is too dark. I’ve never really cared for that red color actually. It carries over from the days when you needed to use bland colors so they could be recorded to VHS tape (clearly no longer a requirement).

+1 on drop shadow on by default. Incidentally, sometimes enabling shadow doesn’t seem to work - any idea why that might happen?

We would need more information to investigate. Preferably step-by-step instructions to reproduce.

Yeah, sorry, that was a pretty useless observation. I’ll try to get something more concrete… I just have a few scenes where drop shadows don’t seem to show. I’ll work on steps to reproduce.

I meant the line/curve itself as well, not just the markups label. Even in your example above, part of the the yellow line is hard to see.

Also in the stable version, the two unselected and active color is either identical or too close to differentiate (I really can’t tell). I set them to be different, and then hit Save as Defaults, but this is not retained after Slicer is restarted. Is this a bug or a color defaults are not meant to be saved ? In comparison glyph/text size changes are saved and retained correctly.

image

Currently, only “basic” display properties can be saved as default. This made sense when there were not too many display properties, but now there are dozens, and it is very hard to select a meaningful subset. So, I’ll change the implementation to save/restore all display properties.

I agree that we would need high-contrast outline for lines and markers as well. One option is to use additional rendering passes, such as Gaussian or glow pass, but that would require some work (adding two additional rendering layers and improve image quality of these rendering passes, because glow pass is optimized for adding bright glow and not dark shadow and the outline is not continuous; Gaussian step is hardcoded to 5 pixels, which is not sufficient as a border).

Example of glow pass:

Code to test custom rendering passes
newRenderer=False
gaussian=False

if newRenderer:
    iren = vtk.vtkRenderWindowInteractor()
    renWin = vtk.vtkRenderWindow()
    renWin.SetMultiSamples(0)
    iren.SetRenderWindow(renWin)
    renderer = vtk.vtkRenderer()
    rendererOutline = vtk.vtkRenderer()
    rendererOutline.SetLayer(1)
    renWin.SetNumberOfLayers(2)
    renWin.AddRenderer(rendererOutline)
    renWin.AddRenderer(renderer)
else:
    layoutManager = slicer.app.layoutManager()
    view = layoutManager.sliceWidget('Red').sliceView()
    renderWindow = view.renderWindow()
    renderers = renderWindow.GetRenderers()
    renderer = renderers.GetItemAsObject(0)
    renderWindow.SetNumberOfLayers(renderWindow.GetNumberOfLayers()+2)
    renderer = vtk.vtkRenderer()
    rendererOutline = vtk.vtkRenderer()
    if gaussian:
        renderer.SetLayer(renderWindow.GetNumberOfLayers()-1)
        rendererOutline.SetLayer(renderWindow.GetNumberOfLayers()-2)
    else:
        renderer.SetLayer(renderWindow.GetNumberOfLayers()-2)
        rendererOutline.SetLayer(renderWindow.GetNumberOfLayers()-1)
    renderWindow.AddRenderer(renderer)
    renderWindow.AddRenderer(rendererOutline)

basicPasses = vtk.vtkRenderStepsPass()
if gaussian:
    glowPass = vtk.vtkGaussianBlurPass()
else:
    glowPass = vtk.vtkOutlineGlowPass()
    glowPass.SetOutlineIntensity(1.0)

glowPass.SetDelegatePass(basicPasses)

# Apply the render pass to the highlight renderer
rendererOutline.SetPass(glowPass)

outlineColor = [1.0, 1.0, 1.0]

# Create mapper and actor for the main renderer
coneSource = vtk.vtkConeSource()
coneMapperMain = vtk.vtkPolyDataMapper()
coneMapperMain.SetInputConnection(coneSource.GetOutputPort())
coneActorMain = vtk.vtkActor()
coneActorMain.SetMapper(coneMapperMain)
renderer.AddActor(coneActorMain)

# Create mapper and actor for the outline
coneMapperOutline = vtk.vtkPolyDataMapper()
coneMapperOutline.SetInputConnection(coneSource.GetOutputPort())
coneActorOutline = vtk.vtkActor()
coneActorOutline.SetMapper(coneMapperOutline)
coneActorOutline.GetProperty().SetColor(outlineColor)
coneActorOutline.GetProperty().LightingOff()
rendererOutline.AddActor(coneActorOutline)

fontSize=20
textActor = vtk.vtkTextActor()
textActor.SetDisplayPosition(100, 100);
textActor.SetVisibility(True)
textActor.GetTextProperty().SetFontSize(fontSize)
textActor.GetTextProperty().SetColor(0,0,0)
textActor.SetInput("This is a test")
renderer.AddActor(textActor)

textActorOutline = vtk.vtkTextActor()
textActorOutline.SetDisplayPosition(100, 100);
textActorOutline.SetVisibility(True)
textActorOutline.GetTextProperty().SetColor(outlineColor)
textActorOutline.GetTextProperty().SetFontSize(fontSize)
textActorOutline.SetInput("This is a test")
rendererOutline.AddActor(textActorOutline)

if newRenderer:
    renWin.SetSize(400, 400)
    renderer.ResetCamera()
    camera = renderer.GetActiveCamera()
    camera.Azimuth(-40.0)
    camera.Elevation(20.0)
    renderer.ResetCamera()
    rendererOutline.SetActiveCamera(camera)
    renWin.Render()
    iren.Start()

Another option is to render thicker dark line in the same renderer behind the normal line:

image

With enabling FXAA anti-aliasing (to make the line edges smoother):

image

Code snippet to enable FXAA anti-aliasing
view = slicer.app.layoutManager().sliceWidget('Red').sliceView()
#view = slicer.app.layoutManager().threeDWidget(0).threeDView()
renderWindow = view.renderWindow()
renderers = renderWindow.GetRenderers()
for renderer in renderers:
    renderer.UseFXAAOn()

Probably we should expose FXAA anti-aliasing option to users, too. It performs smart smoothing in the rendered image (only smoothing at sharp edges), which may impact anything in the rendering layer, but if we render markups in a separate layer (which is easy to do in slice views) then we can ensure that only markups edges are smoothed.

2 Likes

I’ve submitted a pull request to enable bold and shadow by default for markup labels, a potential fix for the issue when shadow cannot be enabled, and saving/restoring all markups display properties as default:

I’ve added an issue to track the remaining enhancement ideas:

I think I was able to find out what was happening with shadows not showing up but not why. In a scene where I didn’t see any drop shadows, the scene MRML had the textProperty setting for the MarkupsFiducialDisplay node set with text-shadow:0px 0px 2px rgba(0,0,0,1.0);. All I had to do was manually change it to text-shadow:2px 2px 2px rgba(0,0,0,1.0); and drop shadows showed up.

1 Like

These corrupted values were saved in the scene due to a bug (that I fixed in April).