trame icon indicating copy to clipboard operation
trame copied to clipboard

VTK local rendering with scalar bar

Open jourdain opened this issue 3 years ago • 15 comments

Need to better support scalarbar with and without widget along NaN and other settings.

jourdain avatar Aug 08 '22 16:08 jourdain

Is there an estimate to when this issue might be addressed? Need to decide whether to use Trame for a project and rendering scalar bars is a crucial feature.

pazars avatar Sep 23 '22 13:09 pazars

Which limitation are you currently running into? It should mostly work already if you don't use it within a widget. That issue is to make sure we fully support all the details rather than just the basic (which is currently supported).

jourdain avatar Sep 23 '22 13:09 jourdain

Also having an example highlighting the short comings could help speedup that process.

jourdain avatar Sep 23 '22 13:09 jourdain

Here is a demo of the issue colorbar_issue.zip It contains the Python script, source file, and an image of the expected output. Tested with Trame 2.2.0.

pazars avatar Sep 29 '22 13:09 pazars

Found that in <python_lib>\site-packages\trame_vtk\modules\common\serve\trame-vtk.js there is a parameter drawNanAnnotation: !0 in function Cs that trickles down to instances of vtkScalarBarActor. Setting drawNanAnnotation: !1 fixed the issue.

Not sure though what for and how exactly this trame-vtk.js is used.

pazars avatar Oct 05 '22 08:10 pazars

So I finally managed to look at your example and from what I see, the scalar bar is there but horizontal rather than vertical? The drawNanAnnotation is to have or remove the red color for NaN values?

So I'm not sure what is the issue you would like us to fix? The positioning or the fact that you don't want to see the NaN color?

jourdain avatar Oct 05 '22 21:10 jourdain

For me it is working like you wanted. I just had to fix your UI code so the bottom does not get cropped.

My code with edit is below

"""Basic example demonstrating an issue with colorbars."""

from trame.app import get_server

from trame.ui.vuetify import SinglePageWithDrawerLayout
from trame.ui.router import RouterViewLayout
from trame.widgets import vtk, vuetify, trame, html, router

from vtkmodules.vtkRenderingCore import (
    vtkActor,
    vtkColorTransferFunction,
    vtkDataSetMapper,
    vtkRenderer,
    vtkRenderWindow,
    vtkRenderWindowInteractor,
)

from vtkmodules.vtkIOXML import vtkXMLUnstructuredGridReader
from vtkmodules.vtkCommonDataModel import vtkDataObject
from vtkmodules.vtkCommonCore import vtkLookupTable
from vtkmodules.vtkCommonColor import vtkNamedColors
from vtkmodules.vtkRenderingAnnotation import vtkScalarBarActor
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera


# Required for interactor initialization
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleSwitch  # noqa

# Required for rendering initialization, not necessary for
# local rendering, but doesn't hurt to include it
import vtkmodules.vtkRenderingOpenGL2  # noqa

import pathlib

# -----------------------------------------------------------------------------
# Globals
# -----------------------------------------------------------------------------


class Representation:
    Points = 0
    Wireframe = 1
    Surface = 2
    SurfaceWithEdges = 3


class LookupTable:
    Rainbow = 0
    Inverted_Rainbow = 1
    Greyscale = 2
    Inverted_Greyscale = 3


# -----------------------------------------------------------------------------
# VTK pipeline
# -----------------------------------------------------------------------------

# Source/Reader
source_path = pathlib.Path(__file__).with_name("patch_antenna.vtu")
if not source_path.exists():
    raise FileNotFoundError(source_path.as_posix())


vtk_source = vtkXMLUnstructuredGridReader()
vtk_source.SetFileName(source_path.as_posix())
vtk_source.Update()
show_array = "ElectricField"


# Misc stuff
colors = vtkNamedColors()

# Lookup table & color transfer function
num_colors = 256

ctf = vtkColorTransferFunction()
ctf.SetColorSpaceToDiverging()
ctf.AddRGBPoint(0.0, 0.230, 0.299, 0.754)
ctf.AddRGBPoint(1.0, 0.706, 0.016, 0.150)

lut = vtkLookupTable()
lut.SetNumberOfTableValues(num_colors)
lut.Build()

for i in range(0, num_colors):
    rgb = list(ctf.GetColor(float(i) / num_colors))
    rgb.append(1.0)
    lut.SetTableValue(i, *rgb)

scalar_range = vtk_source.GetOutput().GetScalarRange()

mapper = vtkDataSetMapper()
mapper.SetInputConnection(vtk_source.GetOutputPort())
mapper.ScalarVisibilityOn()
mapper.SetScalarRange(scalar_range)
mapper.SetLookupTable(lut)

# Actors
actor = vtkActor()
actor.SetMapper(mapper)
# Mesh: Setup default representation to surface
actor.GetProperty().SetRepresentationToSurface()
actor.GetProperty().SetPointSize(1)
actor.GetProperty().EdgeVisibilityOn()

scalar_bar = vtkScalarBarActor()
scalar_bar.SetLookupTable(mapper.GetLookupTable())
scalar_bar.SetNumberOfLabels(7)
scalar_bar.UnconstrainedFontSizeOn()
scalar_bar.SetMaximumWidthInPixels(100)
scalar_bar.SetMaximumHeightInPixels(800 // 3)
scalar_bar.SetTitle(show_array)


max_scalar = scalar_range[1]
if max_scalar < 1:
    precision = 4
elif max_scalar < 10:
    precision = 3
elif max_scalar < 100:
    precision = 2
else:
    precision = 1
scalar_bar.SetLabelFormat(f"%-#6.{precision}f")

# Render stuff
renderer = vtkRenderer()
renderer.SetBackground(colors.GetColor3d("SlateGray"))  # SlateGray
renderer.AddActor(actor)
renderer.AddActor2D(scalar_bar)

render_window = vtkRenderWindow()
render_window.SetSize(300, 300)
render_window.AddRenderer(renderer)
render_window.SetWindowName("VTK Test")

render_window_interactor = vtkRenderWindowInteractor()
interactor_style = vtkInteractorStyleTrackballCamera()
render_window_interactor.SetInteractorStyle(interactor_style)
render_window_interactor.SetRenderWindow(render_window)
renderer.ResetCamera()

# -----------------------------------------------------------------------------
# Trame setup
# -----------------------------------------------------------------------------

server = get_server()
state, ctrl = server.state, server.controller

state.setdefault("active_ui", "geometry")
state.vtk_bground = "SlateGray"

# -----------------------------------------------------------------------------
# Callbacks
# -----------------------------------------------------------------------------

# Representation Callbacks
def update_representation(actor, mode):
    property = actor.GetProperty()
    if mode == Representation.Points:
        property.SetRepresentationToPoints()
        property.SetPointSize(5)
        property.EdgeVisibilityOff()
    elif mode == Representation.Wireframe:
        property.SetRepresentationToWireframe()
        property.SetPointSize(1)
        property.EdgeVisibilityOff()
    elif mode == Representation.Surface:
        property.SetRepresentationToSurface()
        property.SetPointSize(1)
        property.EdgeVisibilityOff()
    elif mode == Representation.SurfaceWithEdges:
        property.SetRepresentationToSurface()
        property.SetPointSize(1)
        property.EdgeVisibilityOn()


@state.change("mesh_representation")
def update_mesh_representation(mesh_representation, **kwargs):
    update_representation(actor, mesh_representation)
    ctrl.view_update()


# Color By Callbacks
def color_by_array(actor, array):
    _min, _max = array.get("range")
    mapper = actor.GetMapper()
    mapper.SelectColorArray(array.get("text"))
    mapper.GetLookupTable().SetRange(_min, _max)
    if array.get("type") == vtkDataObject.FIELD_ASSOCIATION_POINTS:
        mapper.SetScalarModeToUsePointFieldData()
    else:
        mapper.SetScalarModeToUseCellFieldData()
    mapper.SetScalarVisibility(True)
    mapper.SetUseLookupTableScalarRange(True)


# Opacity Callbacks
@state.change("mesh_opacity")
def update_mesh_opacity(mesh_opacity, **kwargs):
    actor.GetProperty().SetOpacity(mesh_opacity)
    ctrl.view_update()


def toggle_background():
    bgcolor = "SlateGray"
    if state.vtk_bground == "SlateGray":
        bgcolor = "black"

    state.vtk_bground = bgcolor
    renderer.SetBackground(colors.GetColor3d(bgcolor))

    ctrl.view_update()


# -----------------------------------------------------------------------------
# GUI ELEMENTS
# -----------------------------------------------------------------------------


def ui_card(title, ui_name):
    with vuetify.VCard(to="/", v_show=f"active_ui == '{ui_name}'"):
        vuetify.VCardTitle(
            title,
            classes="grey lighten-1 py-1 grey--text text--darken-3",
            style="user-select: none; cursor: pointer",
            hide_details=True,
            dense=True,
        )
        content = vuetify.VCardText(classes="py-2")
    return content


def mesh_card():
    with ui_card(title="Geometry", ui_name="geometry"):

        with vuetify.VRow(classes="pt-2", dense=True):
            vuetify.VSelect(
                # Representation
                v_model=("mesh_representation", Representation.Surface),
                items=(
                    "representations",
                    [
                        {"text": "Points", "value": 0},
                        {"text": "Wireframe", "value": 1},
                        {"text": "Surface", "value": 2},
                        {"text": "Surface With Edges", "value": 3},
                    ],
                ),
                label="Representation",
                hide_details=True,
                dense=True,
                outlined=True,
                classes="pt-1",
            )

        vuetify.VSlider(
            # Opacity
            v_model=("mesh_opacity", 1.0),
            min=0,
            max=1,
            step=0.1,
            label="Opacity",
            classes="mt-1",
            hide_details=True,
            dense=True,
        )


# -----------------------------------------------------------------------------
# GUI
# -----------------------------------------------------------------------------

with RouterViewLayout(server, "/"):
    with html.Div(style="height: 100%; width: 100%;"):
        view = vtk.VtkLocalView(render_window)
        ctrl.view_update.add(view.update)
        ctrl.on_server_ready.add(view.update)

with RouterViewLayout(server, "/foo"):
    with vuetify.VCard():
        vuetify.VCardTitle("This is foo")
        with vuetify.VCardText():
            vuetify.VBtn("Take me back", click="$router.back()")


with SinglePageWithDrawerLayout(server) as layout:
    layout.title.set_text("Colorbar issue example")

    with layout.toolbar as toolbar:
        toolbar.dense = True
        vuetify.VSpacer()
        vuetify.VDivider(vertical=True, classes="mx-2")
        vuetify.VSwitch(
            v_model=("$vuetify.theme.dark"),
            inset=True,
            hide_details=True,
            dense=True,
            change=toggle_background,
        )

    with layout.drawer as drawer:
        drawer.width = 325
        with vuetify.VList(shaped=True, v_model=("selectedRoute", 0)):
            with vuetify.VListGroup(value=("true",), sub_group=True):
                with vuetify.Template(v_slot_activator=True):
                    vuetify.VListItemTitle("3D View")
                mesh_card()

    with layout.content:
        with vuetify.VContainer(fluid=True, classes="pa-0 fill-height"):
            router.RouterView(style="width: 100%; height: 100%")


# -----------------------------------------------------------------------------
# Main
# -----------------------------------------------------------------------------

if __name__ == "__main__":
    server.start()

jourdain avatar Oct 05 '22 22:10 jourdain

Screen Shot 2022-10-05 at 4 09 13 PM

jourdain avatar Oct 05 '22 22:10 jourdain

At the time of writing I had never encountered NaN as part of the color bar. It suggested that there are NaN values in the dataset, which was not true, so I thought it was a bug. Later I found in vtkScalarBar's documentation that it's just a visual prop that can be toggled with DrawNanAnnotationOn and DrawNanAnnotationOff functions, irrespective of whether there actually are NaN values or not. However, I could not get them to work in Trame.

Looking at it now, I'm not sure if this is a bug. It just seems that the default behavior is to display the NaN annotation. However, it's a bit confusing from a user's perspective and not consistent with defaults for other views since this happens only for vtk.VtkLocalView.

pazars avatar Oct 10 '22 12:10 pazars

This has to do with the correct mapping between server object (VTK) properties vs local ones (vtk.js). This is part of that issue. But the part that matter to you is just the NaN visibility.

jourdain avatar Oct 11 '22 15:10 jourdain

The NaN and text color will be managed in https://github.com/Kitware/trame-vtk/pull/5

jourdain avatar Oct 18 '22 20:10 jourdain

I've been trying to configure a scalar bar in my own Trame app, but the included code actually demonstrates the issue. The following call seems to have no effect: scalar_bar.SetNumberOfLabels(7)

How can we increase from the default number of labels? And also, how to include the min and max values in the color bar labels? I've tried the following: scalar_bar.AddRangeLabels = 1 which I found from the Paraview trace, but this also has no effect.

j-hallen avatar Jul 31 '23 03:07 j-hallen

If you want a 1-to-1 mapping, do remote rendering. Otherwise, improvement will have to be made in vtk.js to fully support all of those options.

jourdain avatar Jul 31 '23 15:07 jourdain

Thanks Sebastien. Remote rendering does not really work for my use case, so I'll try the custom annotations as a workaround. Do you know if extending vtk.js to approach 1:1 mapping is planned?

j-hallen avatar Aug 01 '23 23:08 j-hallen

We are exploring a path with wasm, but either way, I don't see it happening in the coming months unless it became a priority of a project with funding.

jourdain avatar Aug 02 '23 04:08 jourdain