Skip to content

Commit 90f1b69

Browse files
add smoothed label rendering modes (#129)
1 parent c30e5c8 commit 90f1b69

27 files changed

Lines changed: 1083 additions & 211 deletions

‎src/cellier/convenience/gui/_controls_config.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ class DatasetInfoControl(Control):
344344
"visible": "visible",
345345
"opacity": "opacity",
346346
"render_mode": "labels_render_mode",
347+
"smooth_geometry_level": "smooth_geometry_level",
347348
"salt": "salt",
348349
"background_label": "background_label",
349350
}

‎src/cellier/gui/_appearance_fields.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ def as_rgba(value: Any) -> tuple[float, float, float, float]:
308308
# kind rather than the kind being the field (design section 6.5.2
309309
# decision 6).
310310
"labels_render_mode": ("LabelsRenderModeCombo", "Render mode"),
311+
"smooth_geometry_level": ("SmoothGeometryLevelSlider", "Smooth geometry level"),
311312
"salt": ("SaltSpin", "Salt"),
312313
"background_label": ("BackgroundLabelSpin", "Background label"),
313314
# Mesh, points and lines all spell the uniform colour ``color``.

‎src/cellier/gui/anywidget/visuals/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
AnywidgetBackgroundLabelSpin,
3333
AnywidgetLabelsRenderModeCombo,
3434
AnywidgetSaltSpin,
35+
AnywidgetSmoothGeometryLevelSlider,
3536
)
3637
from cellier.gui.anywidget.visuals._level_of_detail import (
3738
AnywidgetLevelOfDetailControls,
@@ -95,6 +96,7 @@
9596
"AnywidgetSideCombo",
9697
"AnywidgetSizeSpaceCombo",
9798
"AnywidgetSizeSpin",
99+
"AnywidgetSmoothGeometryLevelSlider",
98100
"AnywidgetThicknessSpaceCombo",
99101
"AnywidgetThicknessSpin",
100102
"AnywidgetToggle",

‎src/cellier/gui/anywidget/visuals/_labels.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111

1212
from typing import ClassVar
1313

14+
from cellier.gui._appearance_fields import field_bounds
1415
from cellier.gui.anywidget.visuals._base import (
16+
AnywidgetBoundedSlider,
1517
AnywidgetChoice,
1618
AnywidgetIntSpin,
1719
)
20+
from cellier.visuals._label_memory import BaseLabelsAppearance
1821

1922

2023
class AnywidgetLabelsRenderModeCombo(AnywidgetChoice):
@@ -88,3 +91,32 @@ class AnywidgetBackgroundLabelSpin(AnywidgetIntSpin):
8891
_label: ClassVar[str] = "Background label"
8992
_default_value: ClassVar[int] = 0
9093
_default_range: ClassVar[tuple[int, int]] = (-(2**31), 2**31 - 1)
94+
95+
96+
_SMOOTH_GEOMETRY_LEVEL_RANGE = field_bounds(
97+
BaseLabelsAppearance, "smooth_geometry_level"
98+
) or (0.05, 0.95)
99+
"""Read off the model, so the slider cannot offer a value pydantic rejects."""
100+
101+
102+
class AnywidgetSmoothGeometryLevelSlider(AnywidgetBoundedSlider):
103+
"""Where the ``smooth_geometry`` surface sits.
104+
105+
The share of the blended weight a label must carry. Lower grows every
106+
label, higher shrinks it; 0.5 puts flat sides on the voxel faces. No
107+
effect in the other render modes.
108+
109+
Parameters
110+
----------
111+
visual_id :
112+
UUID of the visual, or a sequence of UUIDs to drive as one
113+
group.
114+
initial_value :
115+
Starting value -- typically
116+
``visual.appearance.smooth_geometry_level``.
117+
"""
118+
119+
_field: ClassVar[str] = "smooth_geometry_level"
120+
_label: ClassVar[str] = "Smooth geometry level"
121+
_default_value: ClassVar[float] = 0.5
122+
_default_range: ClassVar[tuple[float, float]] = _SMOOTH_GEOMETRY_LEVEL_RANGE

‎src/cellier/gui/qt/visuals/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
QtBackgroundLabelSpin,
3434
QtLabelsRenderModeCombo,
3535
QtSaltSpin,
36+
QtSmoothGeometryLevelSlider,
3637
)
3738
from cellier.gui.qt.visuals._level_of_detail import QtLevelOfDetailControls
3839
from cellier.gui.qt.visuals._lines import (
@@ -92,6 +93,7 @@
9293
"QtSideCombo",
9394
"QtSizeSpaceCombo",
9495
"QtSizeSpin",
96+
"QtSmoothGeometryLevelSlider",
9597
"QtThicknessSpaceCombo",
9698
"QtThicknessSpin",
9799
"QtToggle",

‎src/cellier/gui/qt/visuals/_labels.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111

1212
from typing import ClassVar
1313

14+
from cellier.gui._appearance_fields import field_bounds
1415
from cellier.gui.qt.visuals._base import (
16+
QtBoundedSlider,
1517
QtChoice,
1618
QtIntSpin,
1719
)
20+
from cellier.visuals._label_memory import BaseLabelsAppearance
1821

1922

2023
class QtLabelsRenderModeCombo(QtChoice):
@@ -88,3 +91,32 @@ class QtBackgroundLabelSpin(QtIntSpin):
8891
_label: ClassVar[str] = "Background label"
8992
_default_value: ClassVar[int] = 0
9093
_default_range: ClassVar[tuple[int, int]] = (-(2**31), 2**31 - 1)
94+
95+
96+
_SMOOTH_GEOMETRY_LEVEL_RANGE = field_bounds(
97+
BaseLabelsAppearance, "smooth_geometry_level"
98+
) or (0.05, 0.95)
99+
"""Read off the model, so the slider cannot offer a value pydantic rejects."""
100+
101+
102+
class QtSmoothGeometryLevelSlider(QtBoundedSlider):
103+
"""Where the ``smooth_geometry`` surface sits.
104+
105+
The share of the blended weight a label must carry. Lower grows every
106+
label, higher shrinks it; 0.5 puts flat sides on the voxel faces. No
107+
effect in the other render modes.
108+
109+
Parameters
110+
----------
111+
visual_id :
112+
UUID of the visual, or a sequence of UUIDs to drive as one
113+
group.
114+
initial_value :
115+
Starting value -- typically
116+
``visual.appearance.smooth_geometry_level``.
117+
"""
118+
119+
_field: ClassVar[str] = "smooth_geometry_level"
120+
_label: ClassVar[str] = "Smooth geometry level"
121+
_default_value: ClassVar[float] = 0.5
122+
_default_range: ClassVar[tuple[float, float]] = _SMOOTH_GEOMETRY_LEVEL_RANGE

‎src/cellier/render/shaders/_label_multiscale.py‎

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,24 +79,29 @@ class LabelVolumeBrickMaterial(gfx.VolumeBasicMaterial):
7979
Colormap mode (baked into shader; frozen after construction).
8080
salt : int
8181
Hash seed for random mode.
82-
render_mode : "iso_categorical" | "flat_categorical" | "gradient_debug" \
83-
| "smooth_iso"
84-
3D render mode. ``gradient_debug`` renders abs(normalize(gradient))
85-
as RGB (R=|X|, G=|Y|, B=|Z|); flat regions with no gradient show as
86-
solid blue. Use to diagnose brick-boundary normal discontinuities.
87-
``smooth_iso`` uses a soft trilinear density field for bisection and
88-
a 3x3x3 Sobel normal kernel for smooth curved shading.
82+
render_mode : str
83+
3D render mode (``MultiscaleLabelsAppearance.render_mode``).
84+
``gradient_debug`` renders abs(normalize(gradient)) as RGB (R=|X|,
85+
G=|Y|, B=|Z|); flat regions with no gradient show as solid blue.
86+
Use to diagnose brick-boundary normal discontinuities.
87+
``smooth_iso`` shades the voxel-face surface with a smooth normal;
88+
``smooth_geometry`` draws a smooth surface per label (both in
89+
``cellier.label_smooth.wgsl``).
8990
ray_steps_per_voxel : float
9091
Ray-march samples per voxel of the drawn level, measured along the
9192
ray. Default is ``1.0``.
9293
render_planes_buffer : Buffer or None
9394
The visual's ``u_render_planes`` buffer, read in ``"plane"`` render
9495
mode. ``None`` gives the material one of its own with no plane.
96+
smooth_level : float
97+
``MultiscaleLabelsAppearance.smooth_geometry_level``: where the
98+
``smooth_geometry`` surface sits. Default ``0.5``.
9599
"""
96100

97101
uniform_type: ClassVar[dict] = dict(
98102
gfx.VolumeBasicMaterial.uniform_type,
99103
ray_steps_per_voxel="f4",
104+
smooth_level="f4",
100105
)
101106

102107
def __init__(
@@ -117,10 +122,12 @@ def __init__(
117122
outline_selection_texture: gfx.Texture | None = None,
118123
ray_steps_per_voxel: float = 1.0,
119124
render_planes_buffer: Buffer | None = None,
125+
smooth_level: float = 0.5,
120126
**kwargs,
121127
) -> None:
122128
super().__init__(**kwargs)
123129
self.uniform_buffer.data["ray_steps_per_voxel"] = float(ray_steps_per_voxel)
130+
self.uniform_buffer.data["smooth_level"] = float(smooth_level)
124131
self.uniform_buffer.update_full()
125132
self.render_planes_buffer = (
126133
render_planes_buffer
@@ -169,6 +176,19 @@ def ray_steps_per_voxel(self, value: float) -> None:
169176
self.uniform_buffer.data["ray_steps_per_voxel"] = float(value)
170177
self.uniform_buffer.update_full()
171178

179+
@property
180+
def smooth_level(self) -> float:
181+
"""The field level of the ``smooth_geometry`` surface.
182+
183+
A uniform, so a change does not rebuild the shader.
184+
"""
185+
return float(self.uniform_buffer.data["smooth_level"])
186+
187+
@smooth_level.setter
188+
def smooth_level(self, value: float) -> None:
189+
self.uniform_buffer.data["smooth_level"] = float(value)
190+
self.uniform_buffer.update_full()
191+
172192

173193
# ---------------------------------------------------------------------------
174194
# 3D shader

‎src/cellier/render/shaders/_label_volume.py‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
from pathlib import Path
6+
from typing import ClassVar
67

78
import pygfx as gfx
89
import wgpu
@@ -41,9 +42,10 @@ class LabelVolumeMaterial(gfx.VolumeBasicMaterial):
4142
Frozen after construction.
4243
salt : int
4344
Hash seed for random mode.
44-
render_mode : "iso_categorical", "flat_categorical" or "plane"
45-
Whether to apply Lambertian shading; ``"plane"`` draws the labels
46-
on the render planes instead. A change rebuilds the shader.
45+
render_mode : str
46+
``InMemoryLabelsAppearance.render_mode``: ``"iso_categorical"``,
47+
``"flat_categorical"``, ``"smooth_iso"``, ``"smooth_geometry"`` or
48+
``"plane"``. A change rebuilds the shader.
4749
label_keys_texture : gfx.Texture or None
4850
Sorted int32 label-ID texture for direct mode.
4951
label_colors_texture : gfx.Texture or None
@@ -55,8 +57,16 @@ class LabelVolumeMaterial(gfx.VolumeBasicMaterial):
5557
render_planes_buffer : gfx.Buffer or None
5658
The visual's ``u_render_planes`` buffer, read in ``"plane"`` mode.
5759
``None`` gives the material one of its own with no plane.
60+
smooth_level : float
61+
``InMemoryLabelsAppearance.smooth_geometry_level``: where the
62+
``smooth_geometry`` surface sits. Default ``0.5``.
5863
"""
5964

65+
uniform_type: ClassVar[dict] = dict(
66+
gfx.VolumeBasicMaterial.uniform_type,
67+
smooth_level="f4",
68+
)
69+
6070
def __init__(
6171
self,
6272
background_label: int = 0,
@@ -69,9 +79,11 @@ def __init__(
6979
label_params_buffer=None,
7080
outline_selection_texture=None,
7181
render_planes_buffer=None,
82+
smooth_level: float = 0.5,
7283
**kwargs,
7384
) -> None:
7485
super().__init__(**kwargs)
86+
self.smooth_level = smooth_level
7587
self.background_label = background_label
7688
self.colormap_mode = colormap_mode
7789
self.salt = salt
@@ -104,6 +116,19 @@ def render_mode(self) -> str:
104116
def render_mode(self, value: str) -> None:
105117
self._store.render_mode = value
106118

119+
@property
120+
def smooth_level(self) -> float:
121+
"""The field level of the ``smooth_geometry`` surface.
122+
123+
A uniform, so a change does not rebuild the shader.
124+
"""
125+
return float(self.uniform_buffer.data["smooth_level"])
126+
127+
@smooth_level.setter
128+
def smooth_level(self, value: float) -> None:
129+
self.uniform_buffer.data["smooth_level"] = float(value)
130+
self.uniform_buffer.update_full()
131+
107132

108133
@register_wgpu_render_function(Volume, LabelVolumeMaterial)
109134
class LabelVolumeShader(BaseVolumeShader):

0 commit comments

Comments
 (0)