Skip to content

Commit 32842d1

Browse files
improve startup (#69)
* unify 2d/3d visual * add aabb to image visuals * update startup camera fitting
1 parent d5962e6 commit 32842d1

19 files changed

Lines changed: 889 additions & 480 deletions

‎scripts/v2/debug_multiscale_volume_brick_ome.py‎

Lines changed: 96 additions & 157 deletions
Large diffs are not rendered by default.

‎scripts/v2/image_memory_demo.py‎

Lines changed: 85 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
"""Demo: in-memory 3D image viewed as 2D slice and 3D volume.
2+
"""Demo: in-memory 3D image with toggleable 2D/3D view in a single scene.
33
44
Run with:
55
uv run python scripts/v2/image_memory_demo.py
@@ -14,8 +14,10 @@
1414
from PySide6.QtCore import QTimer
1515
from PySide6.QtWidgets import (
1616
QApplication,
17+
QCheckBox,
1718
QHBoxLayout,
1819
QLabel,
20+
QPushButton,
1921
QSpinBox,
2022
QVBoxLayout,
2123
QWidget,
@@ -32,120 +34,123 @@ def main() -> None:
3234
app = QApplication(sys.argv)
3335

3436
# ── 1. Create data ──────────────────────────────────────────────────
35-
volume = binary_blobs(length=128, volume_fraction=0.1, n_dim=3).astype(np.float32)
37+
volume = binary_blobs(length=128, volume_fraction=0.03, n_dim=3).astype(np.float32)
3638
store = ImageMemoryStore(data=volume, name="demo_volume")
3739

3840
# ── 2. Set up the controller ────────────────────────────────────────
3941
controller = CellierController()
4042

41-
# Coordinate system: 3 spatial axes
4243
cs = CoordinateSystem(name="world", axis_labels=("z", "y", "x"))
4344

44-
# 2D scene: displayed_axes=(1,2), slice_indices={0:0}
45-
scene_2d = controller.add_scene(dim="2d", coordinate_system=cs, name="slice")
46-
# 3D scene: displayed_axes=(0,1,2), slice_indices={}
47-
scene_3d = controller.add_scene(dim="3d", coordinate_system=cs, name="volume")
48-
49-
appearance = ImageMemoryAppearance(
50-
color_map="viridis",
51-
clim=(0.0, 1.0),
45+
# Single scene that supports both 2D and 3D rendering.
46+
# Start in 3D; displayed_axes=(0,1,2).
47+
scene = controller.add_scene(
48+
dim="3d",
49+
coordinate_system=cs,
50+
name="volume",
51+
render_modes={"2d", "3d"},
5252
)
5353

54-
# Add the same store to both scenes (store is registered only once)
55-
visual_2d = controller.add_image(
56-
data=store,
57-
scene_id=scene_2d.id,
58-
appearance=appearance,
59-
name="slice_view",
60-
)
61-
visual_3d = controller.add_image(
54+
appearance = ImageMemoryAppearance(color_map="viridis", clim=(0.0, 1.0))
55+
visual = controller.add_image(
6256
data=store,
63-
scene_id=scene_3d.id,
57+
scene_id=scene.id,
6458
appearance=appearance,
6559
name="volume_view",
6660
)
61+
visual.aabb.color = "#ff00ff"
6762

68-
# ── 3. Create canvases and z-slice control ─────────────────────────
63+
# ── 3. Build UI ─────────────────────────────────────────────────────
6964
root = QWidget()
7065
outer_layout = QVBoxLayout(root)
7166
controller.set_widget_parent(root)
7267

73-
# Canvas row
74-
canvas_row = QWidget()
75-
canvas_layout = QHBoxLayout(canvas_row)
76-
canvas_layout.setContentsMargins(0, 0, 0, 0)
68+
canvas_widget = controller.add_canvas(scene_id=scene.id)
69+
outer_layout.addWidget(canvas_widget)
7770

78-
widget_2d = controller.add_canvas(scene_id=scene_2d.id)
79-
widget_3d = controller.add_canvas(scene_id=scene_3d.id)
80-
81-
canvas_layout.addWidget(widget_2d)
82-
canvas_layout.addWidget(widget_3d)
83-
outer_layout.addWidget(canvas_row)
84-
85-
# Z-slice spinbox
86-
z_depth = store.shape[0] # axis 0 = z
71+
# Controls row
8772
controls = QWidget()
8873
ctrl_layout = QHBoxLayout(controls)
8974
ctrl_layout.setContentsMargins(0, 0, 0, 0)
90-
ctrl_layout.addWidget(QLabel("Z slice:"))
9175

76+
z_depth = store.shape[0]
77+
ctrl_layout.addWidget(QLabel("Z slice:"))
9278
z_spin = QSpinBox()
9379
z_spin.setRange(0, z_depth - 1)
94-
z_spin.setValue(0)
80+
z_spin.setValue(z_depth // 2)
9581
ctrl_layout.addWidget(z_spin)
9682
ctrl_layout.addStretch()
83+
84+
aabb_check = QCheckBox("Show bounding box")
85+
aabb_check.setChecked(False)
86+
ctrl_layout.addWidget(aabb_check)
87+
88+
toggle_btn = QPushButton("Switch to 2D")
89+
ctrl_layout.addWidget(toggle_btn)
9790
outer_layout.addWidget(controls)
9891

92+
root.resize(900, 700)
93+
root.show()
94+
95+
# ── 4. Toggle logic ─────────────────────────────────────────────────
96+
_current_dim = ["3d"]
97+
98+
def _toggle() -> None:
99+
if _current_dim[0] == "3d":
100+
_current_dim[0] = "2d"
101+
toggle_btn.setText("Switch to 3D")
102+
scene.dims.selection.slice_indices = {0: z_spin.value()}
103+
scene.dims.selection.displayed_axes = (1, 2)
104+
else:
105+
_current_dim[0] = "3d"
106+
toggle_btn.setText("Switch to 2D")
107+
scene.dims.selection.displayed_axes = (0, 1, 2)
108+
109+
controller.reslice_scene(scene.id)
110+
111+
toggle_btn.clicked.connect(_toggle)
112+
99113
def _on_z_changed(z: int) -> None:
100-
scene_2d.dims.selection.slice_indices = {0: z}
101-
controller.reslice_scene(scene_2d.id)
114+
scene.dims.selection.slice_indices = {0: z}
115+
if _current_dim[0] == "2d":
116+
controller.reslice_scene(scene.id)
102117

103118
z_spin.valueChanged.connect(_on_z_changed)
104119

105-
root.resize(1200, 700)
106-
root.show()
120+
def _on_aabb_toggled(checked: bool) -> None:
121+
visual.aabb.enabled = checked
107122

108-
# ── 4. Initial reslice + camera fit ────────────────────────────────
109-
# The async slicer needs a running event loop, so reslice is deferred
110-
# with QTimer.singleShot(0, ...) until QtAsyncio.run() has started.
111-
#
112-
# look_at_visual computes the bounding box from the current node
113-
# geometry, so it must run *after* data has been committed to the GPU
114-
# (otherwise the placeholder texture gives a near-zero bounding box).
115-
# We wrap the on_data_ready callbacks to trigger camera fit on the
116-
# first delivery — event-driven, not time-based.
117-
118-
visuals_loaded: set[str] = set()
119-
120-
def _wrap_callback(original, visual_id, label, view_dir, up):
121-
def _wrapped(batch):
122-
original(batch)
123-
if label not in visuals_loaded:
124-
visuals_loaded.add(label)
125-
controller.look_at_visual(visual_id, view_direction=view_dir, up=up)
126-
127-
return _wrapped
128-
129-
# Patch the render-layer visuals to fire camera fit on first data.
130-
scene_mgr_2d = controller._render_manager._scenes[scene_2d.id]
131-
gfx_vis_2d = scene_mgr_2d.get_visual(visual_2d.id)
132-
gfx_vis_2d.on_data_ready_2d = _wrap_callback(
133-
gfx_vis_2d.on_data_ready_2d,
134-
visual_2d.id,
135-
"2d",
136-
view_dir=(0, 0, -1),
137-
up=(0, 1, 0),
138-
)
123+
aabb_check.toggled.connect(_on_aabb_toggled)
139124

140-
scene_mgr_3d = controller._render_manager._scenes[scene_3d.id]
141-
gfx_vis_3d = scene_mgr_3d.get_visual(visual_3d.id)
142-
gfx_vis_3d.on_data_ready = _wrap_callback(
143-
gfx_vis_3d.on_data_ready,
144-
visual_3d.id,
145-
"3d",
146-
view_dir=(-1, -1, -1),
147-
up=(0, 0, 1),
148-
)
125+
# ── 5. Initial reslice + camera fit ────────────────────────────────
126+
# Patch on_data_ready to fit the camera on the first delivery.
127+
scene_mgr = controller._render_manager._scenes[scene.id]
128+
gfx_vis = scene_mgr.get_visual(visual.id)
129+
_camera_fitted: set[str] = set()
130+
131+
_orig_3d = gfx_vis.on_data_ready
132+
133+
def _patched_3d(batch):
134+
_orig_3d(batch)
135+
if "3d" not in _camera_fitted:
136+
_camera_fitted.add("3d")
137+
controller.look_at_visual(
138+
visual.id, view_direction=(-1, -1, -1), up=(0, 0, 1)
139+
)
140+
141+
gfx_vis.on_data_ready = _patched_3d
142+
143+
_orig_2d = gfx_vis.on_data_ready_2d
144+
145+
def _patched_2d(batch):
146+
_orig_2d(batch)
147+
if "2d" not in _camera_fitted:
148+
_camera_fitted.add("2d")
149+
controller.look_at_visual(
150+
visual.id, view_direction=(0, 0, -1), up=(0, 1, 0)
151+
)
152+
153+
gfx_vis.on_data_ready_2d = _patched_2d
149154

150155
QTimer.singleShot(0, controller.reslice_all)
151156

‎scripts/v2/integration_2d_3d/example_combined_camera_redraw.py‎

Lines changed: 30 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
QCheckBox,
2525
QDoubleSpinBox,
2626
QFrame,
27+
QGroupBox,
2728
QHBoxLayout,
2829
QLabel,
2930
QMainWindow,
@@ -126,47 +127,6 @@ def _setup_debug_logging(spec: str) -> None:
126127
# ---------------------------------------------------------------------------
127128

128129

129-
def _make_box_wireframe(
130-
box_min: np.ndarray, box_max: np.ndarray, color: str
131-
) -> gfx.Line:
132-
"""Build an AABB wireframe as disconnected edge pairs."""
133-
x0, y0, z0 = box_min
134-
x1, y1, z1 = box_max
135-
positions = np.array(
136-
[
137-
[x0, y0, z0],
138-
[x1, y0, z0],
139-
[x1, y0, z0],
140-
[x1, y1, z0],
141-
[x1, y1, z0],
142-
[x0, y1, z0],
143-
[x0, y1, z0],
144-
[x0, y0, z0],
145-
[x0, y0, z1],
146-
[x1, y0, z1],
147-
[x1, y0, z1],
148-
[x1, y1, z1],
149-
[x1, y1, z1],
150-
[x0, y1, z1],
151-
[x0, y1, z1],
152-
[x0, y0, z1],
153-
[x0, y0, z0],
154-
[x0, y0, z1],
155-
[x1, y0, z0],
156-
[x1, y0, z1],
157-
[x1, y1, z0],
158-
[x1, y1, z1],
159-
[x0, y1, z0],
160-
[x0, y1, z1],
161-
],
162-
dtype=np.float32,
163-
)
164-
return gfx.Line(
165-
gfx.Geometry(positions=positions),
166-
gfx.LineSegmentMaterial(color=color, thickness=1.0),
167-
)
168-
169-
170130
def _make_frustum_wireframe(corners: np.ndarray, color: str = "#00cc44") -> gfx.Line:
171131
"""Build a wireframe from 8 frustum corners (shape 2x4x3)."""
172132
edge_indices = [
@@ -269,21 +229,10 @@ def __init__(self, data_store: MultiscaleZarrDataStore) -> None:
269229
gpu_budget_bytes=GPU_BUDGET_3D,
270230
threshold=0.2,
271231
)
232+
self._visual_2d.aabb.color = AABB_COLOR
233+
self._visual_3d.aabb.color = AABB_COLOR
272234
self._canvas_widget_3d = self._controller.add_canvas(self._scene_3d.id)
273235

274-
# Add AABB wireframe to the 3D scene.
275-
gfx_visual_3d = self._get_gfx_visual_3d()
276-
d, h, w = gfx_visual_3d._volume_geometry.base_layout.volume_shape
277-
pad = 1.0
278-
gfx_scene_3d = self._controller._render_manager.get_scene(self._scene_3d.id)
279-
gfx_scene_3d.add(
280-
_make_box_wireframe(
281-
np.array([-0.5 - pad, -0.5 - pad, -0.5 - pad]),
282-
np.array([w - 0.5 + pad, h - 0.5 + pad, d - 0.5 + pad]),
283-
AABB_COLOR,
284-
)
285-
)
286-
287236
self._setup_ui()
288237

289238
# ── Render-layer visual accessors ─────────────────────────────────
@@ -417,6 +366,25 @@ def _setup_ui(self) -> None:
417366
self._lod_bias_sb.setValue(LOD_BIAS)
418367
pl.addWidget(self._lod_bias_sb)
419368

369+
pl.addWidget(_make_separator())
370+
371+
# ── Shared: bounding box ─────────────────────────────────────
372+
aabb_group = QGroupBox("Bounding box")
373+
aabb_layout = QVBoxLayout(aabb_group)
374+
self._show_aabb_cb = QCheckBox("Show bounding box")
375+
self._show_aabb_cb.setChecked(False)
376+
self._show_aabb_cb.toggled.connect(self._on_show_aabb_toggled)
377+
aabb_layout.addWidget(self._show_aabb_cb)
378+
lw_row = QHBoxLayout()
379+
lw_row.addWidget(QLabel("Line width (px):"))
380+
self._aabb_lw_sb = QSpinBox()
381+
self._aabb_lw_sb.setRange(1, 20)
382+
self._aabb_lw_sb.setValue(int(self._visual_3d.aabb.line_width))
383+
self._aabb_lw_sb.valueChanged.connect(self._on_aabb_line_width_changed)
384+
lw_row.addWidget(self._aabb_lw_sb)
385+
aabb_layout.addLayout(lw_row)
386+
pl.addWidget(aabb_group)
387+
420388
pl.addStretch()
421389

422390
self._status_label = QLabel("Auto-redraw active — move camera to update")
@@ -502,6 +470,14 @@ def _on_show_frustum_toggled(self, checked: bool) -> None:
502470
if self._frustum_line is not None:
503471
self._frustum_line.visible = checked
504472

473+
def _on_show_aabb_toggled(self, checked: bool) -> None:
474+
self._visual_2d.aabb.enabled = checked
475+
self._visual_3d.aabb.enabled = checked
476+
477+
def _on_aabb_line_width_changed(self, value: int) -> None:
478+
self._visual_2d.aabb.line_width = float(value)
479+
self._visual_3d.aabb.line_width = float(value)
480+
505481
def _on_far_plane_changed(self, value: float) -> None:
506482
self._controller.set_camera_depth_range(
507483
scene_id=self._scene_3d.id,

0 commit comments

Comments
 (0)