Commit 7eb92910 authored by Duggan, John's avatar Duggan, John
Browse files

Add clim/opacity settings to fix full turbine rendering

parent d0ddb5b0
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
[tool.poetry]
name = "ctscan-viz"
version = "0.2.4"
version = "0.3.0"
description = "Template application"
authors = []
readme = "README.md"
+7 −0
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ class MainModel:
    def __init__(self) -> None:
        self.data_directory = ""
        self.file_list: list[str] = []
        self.needs_update = True
        self.volume: Optional[ImageData] = None

    def get_file_count(self) -> int:
@@ -28,6 +29,7 @@ class MainModel:

    def update_data_directory(self, value: str) -> None:
        self.data_directory = value
        self.needs_update = True
        self.update_file_list()

    def update_file_list(self) -> None:
@@ -43,6 +45,9 @@ class MainModel:
            pass

    def update_volume(self) -> None:
        if not self.needs_update:
            return

        # Read the slices and validate their dimensions
        start = time()
        x_range = 0
@@ -76,3 +81,5 @@ class MainModel:
        # Define the 3D volume's scalars for the color/opacity transfer functions
        self.volume["TIFF Scalars"] = scalars
        print(f"PyVista volume creation: {time() - start:.2f}s", flush=True)

        self.needs_update = False
+40 −0
Original line number Diff line number Diff line
@@ -17,17 +17,35 @@ class MainViewModel:
        self.model = model
        self.binding = binding

        self.clim_max: Optional[float] = None
        self.clim_min: Optional[float] = None
        self.loading = False
        self.opacity = "linear"

        self.clim_max_bind = self.binding.new_bind()
        self.clim_min_bind = self.binding.new_bind()
        self.data_directory_bind = self.binding.new_bind()
        self.file_count_bind = self.binding.new_bind()
        self.loading_bind = self.binding.new_bind()
        self.opacity_bind = self.binding.new_bind()
        self.render_bind = self.binding.new_bind()
        self.update_clim_bind = self.binding.new_bind()
        self.update_opacity_bind = self.binding.new_bind()

    def get_clim(self) -> Optional[list[float]]:
        if self.clim_max is None or self.clim_min is None:
            return None

        return [self.clim_min, self.clim_max]

    def get_opacity(self) -> str:
        return self.opacity

    def get_volume(self) -> Optional[ImageData]:
        return self.model.get_volume()

    async def monitor_loading(self) -> None:
        await sleep(0.1)
        while self.loading:
            await sleep(0.1)

@@ -47,10 +65,32 @@ class MainViewModel:
        self.loading_thread.start()
        create_task(self.monitor_loading())

    def update_clim_max(self, value: str) -> None:
        try:
            self.clim_max = float(value)
        except ValueError:
            self.clim_max = None

    def update_clim_min(self, value: str) -> None:
        try:
            self.clim_min = float(value)
        except ValueError:
            self.clim_min = None

    def update_data_directory(self, value: str) -> None:
        self.model.update_data_directory(value)
        self.update_view()

    def update_loading(self, value: bool) -> None:
        self.loading = value
        self.update_view()

    def update_opacity(self, value: str) -> None:
        self.opacity = value

    def update_view(self) -> None:
        self.clim_max_bind.update_in_view(self.clim_max)
        self.clim_min_bind.update_in_view(self.clim_min)
        self.file_count_bind.update_in_view(self.model.get_file_count())
        self.loading_bind.update_in_view(self.loading)
        self.opacity_bind.update_in_view(self.opacity)
+45 −3
Original line number Diff line number Diff line
@@ -2,7 +2,10 @@

from typing import Any

from trame_facade.view.components import RemoteFileInput
from trame.widgets import html
from trame.widgets import vuetify3 as vuetify
from trame_facade.view.components import InputField, RemoteFileInput
from trame_facade.view.layouts import GridLayout
from trame_server.core import Server

from ctscan_viz.view_models.main import MainViewModel
@@ -12,13 +15,18 @@ class DataSelector:
    """Selection widget for folder containing CT scans."""

    def __init__(self, server: Server, vm: MainViewModel) -> None:
        self.server = server
        self.ctrl = server.controller
        self.vm = vm
        self.vm.clim_max_bind.connect("clim_max")
        self.vm.clim_min_bind.connect("clim_min")
        self.vm.data_directory_bind.connect("data_directory")
        self.vm.opacity_bind.connect("opacity")

        # TODO: why is this needed?
        self.last_dir = ""

        @server.change("data_directory")
        @self.server.change("data_directory")
        def _on_data_directory_change(*args: Any, **kwargs: Any) -> None:
            if server.state.data_directory != self.last_dir:
                self.last_dir = server.state.data_directory
@@ -27,11 +35,45 @@ class DataSelector:
        self.create_ui()

    def create_ui(self) -> None:
        @self.ctrl.trigger("update_clim_max")
        def _update_clim_max(value: str) -> None:
            self.vm.update_clim_max(value)

        @self.ctrl.trigger("update_clim_min")
        def _update_clim_min(value: str) -> None:
            self.vm.update_clim_min(value)

        @self.ctrl.trigger("update_opacity")
        def _update_opacity(value: str) -> None:
            self.vm.update_opacity(value)

        RemoteFileInput(
            v_model="data_directory",
            allow_files=False,
            allow_folders=True,
            base_paths=["/HFIR", "/SNS"],
            input_props={"classes": "mb-4"},
            input_props={"classes": "mb-2"},
            label="Select Data Directory",
        )

        with GridLayout(classes="mb-2", columns=4, valign="center"):
            InputField(
                v_model="clim_min",
                label="Minimum Value",
                update_modelValue="trigger('update_clim_min', [clim_min])",
            )
            InputField(
                v_model="clim_max",
                label="Maximum Value",
                update_modelValue="trigger('update_clim_max', [clim_max])",
            )
            InputField(
                v_model="opacity",
                items=("['linear', 'geom', 'sigmoid']",),
                label="Opacity",
                type="select",
                update_modelValue="trigger('update_opacity', [opacity])",
            )
            with vuetify.VBtn(disabled=("file_count < 1 || loading",), click="trigger('start_render');"):
                vuetify.VProgressCircular(indeterminate=True, v_if="loading", size=24)
                html.Span("Render {{ file_count }} files", v_else=True)
+8 −12
Original line number Diff line number Diff line
@@ -6,9 +6,8 @@ from typing import Any, Optional

from pyvista import Plotter, start_xvfb, themes
from pyvista.trame.ui import get_viewer
from trame.widgets import html
from trame.widgets import vuetify3 as vuetify
from trame_server.core import Server
from trame_server.state import State

from ctscan_viz.view_models.main import MainViewModel

@@ -20,6 +19,7 @@ class VisualizationPanel:

    def __init__(self, server: Server, vm: MainViewModel) -> None:
        self.server = server
        self.ctrl = server.controller
        self.vm = vm
        self.vm.file_count_bind.connect("file_count")
        self.vm.loading_bind.connect("loading")
@@ -28,6 +28,10 @@ class VisualizationPanel:
        self.plotter = self.create_plotter()
        self.create_ui()

    @property
    def state(self) -> State:
        return self.server.state

    def create_plotter(self) -> Plotter:
        plotter = Plotter(off_screen=True)
        plotter.background_color = "black"
@@ -36,25 +40,17 @@ class VisualizationPanel:
        return plotter

    def create_ui(self) -> None:
        @self.server.controller.trigger("start_render")
        @self.ctrl.trigger("start_render")
        def _start_render() -> None:
            self.plotter.clear()
            self.vm.render_files()

        view = get_viewer(self.plotter)

        with vuetify.VBtn(
            classes="mb-2",
            disabled=("file_count < 1 || loading",),
            click="trigger('start_render');",
        ):
            vuetify.VProgressCircular(indeterminate=True, v_if="loading", size=24)
            html.Span("Render {{ file_count }} files", v_else=True)
        view.ui(mode="server", style="height: 66vh;")

    async def render_in_background(self) -> None:
        start = time()
        self.plotter.add_volume(self.vm.get_volume(), opacity="sigmoid")
        self.plotter.add_volume(self.vm.get_volume(), clim=self.vm.get_clim(), opacity=self.vm.get_opacity())
        self.plotter.view_isometric(self.plotter)
        print(f"PyVista volume rendering: {time() - start:.2f}s", flush=True)