为二维与三维深度色标增加每帧自适应选项。
默认仍锁定本会话首帧,勾选后两视图按当前帧有效深度共用同一范围。已用深度色标与显示设置测试验证。
This commit is contained in:
@@ -2001,6 +2001,10 @@ class StudioMainWindow(QMainWindow):
|
||||
self.mesh3d_panel.message.text().strip()
|
||||
or t("panels.mesh3d_no_scene")
|
||||
)
|
||||
return
|
||||
# The depth colour-bar switch lives in that dialog and also recolours the 2D
|
||||
# canvas, so the live path has to hear the change as soon as the dialog exists.
|
||||
self._connect_analysis_display(self.mesh3d_panel)
|
||||
|
||||
def _on_sensor_type_changed(self, sensor_type: str) -> None:
|
||||
self.prefs["sensor_type"] = str(sensor_type)
|
||||
@@ -2331,7 +2335,7 @@ class StudioMainWindow(QMainWindow):
|
||||
self._active_calibration_id = ""
|
||||
self._depth_range_value = None
|
||||
self._last_depth_frame = None
|
||||
self._sync_shared_depth_scale(None)
|
||||
self._publish_shared_depth_scale(None)
|
||||
# The new session has not established a domain boundary yet; the previous
|
||||
# session's island must not survive into it (a different sensor is a different
|
||||
# island, and the latch compares against what it holds).
|
||||
@@ -2647,34 +2651,97 @@ class StudioMainWindow(QMainWindow):
|
||||
presenter.tick_3d(display_paused=self._display_paused)
|
||||
|
||||
# ------------------------------------------------------------ depth scale
|
||||
def _depth_range(self) -> tuple[float, float] | None:
|
||||
"""The colour scale, latched to the first frame that carries depth.
|
||||
def _depth_display_settings(self):
|
||||
"""The display settings that decide the shared depth colour span."""
|
||||
panel = getattr(self, "mesh3d_panel", None)
|
||||
if panel is None or not hasattr(panel, "display_settings"):
|
||||
return None
|
||||
try:
|
||||
return panel.display_settings()
|
||||
except Exception: # noqa: BLE001 - a missing pane must not blank the 2D view
|
||||
return None
|
||||
|
||||
Re-fitting the range on every frame (the old behaviour) made colours
|
||||
drift, so two frames could look identical while holding different
|
||||
depths. Latching keeps frames comparable for the whole session; the
|
||||
numeric range is shown in the panel header. The same span is handed to
|
||||
the 3D pane so both legends name one interval.
|
||||
"""
|
||||
if self._depth_range_value is not None:
|
||||
return self._depth_range_value
|
||||
depth = self._last_depth_frame
|
||||
def _fixed_depth_span(self) -> tuple[float, float] | None:
|
||||
"""Typed colour bounds, when the operator chose a fixed scale."""
|
||||
settings = self._depth_display_settings()
|
||||
scale = getattr(settings, "color_scale", None) if settings is not None else None
|
||||
if scale is None or str(getattr(scale, "mode", "")) != "fixed":
|
||||
return None
|
||||
lo = float(scale.lower)
|
||||
hi = float(scale.upper)
|
||||
if hi <= lo:
|
||||
hi = lo + 1.0
|
||||
return lo, hi
|
||||
|
||||
def _depth_scale_adaptive(self) -> bool:
|
||||
settings = self._depth_display_settings()
|
||||
return bool(getattr(settings, "depth_scale_adaptive", False))
|
||||
|
||||
def _depth_scale_mode(self) -> str:
|
||||
"""``fixed``, ``adaptive`` or ``latched`` — the rule both views follow."""
|
||||
if self._fixed_depth_span() is not None:
|
||||
return "fixed"
|
||||
if self._depth_scale_adaptive():
|
||||
return "adaptive"
|
||||
return "latched"
|
||||
|
||||
def _frame_depth_span(self, depth) -> tuple[float, float] | None:
|
||||
"""Finite minimum and maximum of one depth map."""
|
||||
if depth is None:
|
||||
return None
|
||||
arr = np.asarray(depth, dtype=np.float64)
|
||||
finite = np.isfinite(arr)
|
||||
if arr.size == 0 or not np.any(finite):
|
||||
return None
|
||||
lo = float(np.nanmin(arr[finite]))
|
||||
hi = float(np.nanmax(arr[finite]))
|
||||
lo = float(np.min(arr[finite]))
|
||||
hi = float(np.max(arr[finite]))
|
||||
if hi <= lo:
|
||||
hi = lo + 1.0
|
||||
self._depth_range_value = (lo, hi)
|
||||
self._sync_shared_depth_scale(self._depth_range_value)
|
||||
return lo, hi
|
||||
|
||||
def _depth_range(self) -> tuple[float, float] | None:
|
||||
"""The session latch: the first frame that carries finite depth.
|
||||
|
||||
Re-fitting the range on every frame made colours drift, so two frames
|
||||
could look identical while holding different depths. Latching keeps
|
||||
frames comparable for the whole session. The per-frame colour bar is
|
||||
the adaptive setting; this method is only the latch.
|
||||
"""
|
||||
if self._depth_range_value is not None:
|
||||
return self._depth_range_value
|
||||
span = self._frame_depth_span(self._last_depth_frame)
|
||||
if span is None:
|
||||
return None
|
||||
self._depth_range_value = span
|
||||
return self._depth_range_value
|
||||
|
||||
def _sync_shared_depth_scale(self, span: tuple[float, float] | None) -> None:
|
||||
"""Push the latched 2D depth colour span to the 3D surface (or clear it)."""
|
||||
def _active_depth_span(self) -> tuple[float, float] | None:
|
||||
"""The span both views colour with for the depth now on screen.
|
||||
|
||||
Leaving the adaptive or fixed rule re-latches on this frame, so the
|
||||
next comparison does not keep a range from the other mode.
|
||||
"""
|
||||
mode = self._depth_scale_mode()
|
||||
previous = getattr(self, "_depth_scale_mode_applied", "latched")
|
||||
if mode == "latched" and previous != "latched":
|
||||
self._depth_range_value = None
|
||||
self._depth_scale_mode_applied = mode
|
||||
if mode == "fixed":
|
||||
return self._fixed_depth_span()
|
||||
if mode == "adaptive":
|
||||
return self._frame_depth_span(self._last_depth_frame)
|
||||
return self._depth_range()
|
||||
|
||||
def _depth_scale_tip_key(self) -> str:
|
||||
mode = getattr(self, "_depth_scale_mode_applied", "latched")
|
||||
if mode == "fixed":
|
||||
return "panels.depth_scale_tip_fixed"
|
||||
if mode == "adaptive":
|
||||
return "panels.depth_scale_tip_adaptive"
|
||||
return "panels.depth_scale_tip"
|
||||
|
||||
def _publish_shared_depth_scale(self, span: tuple[float, float] | None) -> None:
|
||||
"""Hand the depth colour span to the 3D pane, when one exists."""
|
||||
panel = getattr(self, "mesh3d_panel", None)
|
||||
if panel is not None and hasattr(panel, "set_shared_depth_scale"):
|
||||
panel.set_shared_depth_scale(span)
|
||||
@@ -2688,7 +2755,7 @@ class StudioMainWindow(QMainWindow):
|
||||
instead of leaving the operator to wonder. It is not the same statement as
|
||||
``panels.depth_unavailable``, which is for a frame that carries no depth at all.
|
||||
"""
|
||||
span = self._depth_range()
|
||||
span = self._active_depth_span()
|
||||
text = "" if span is None else t(
|
||||
"panels.depth_scale", low=f"{span[0]:.3f}", high=f"{span[1]:.3f}"
|
||||
)
|
||||
@@ -2804,13 +2871,17 @@ class StudioMainWindow(QMainWindow):
|
||||
boundary = self._latched_domain_mask(frame)
|
||||
self.arrow_panel.set_arrow_scale(arrow_display_scale(draw_style))
|
||||
self.arrow_panel.set_valid_area(frame.valid_mask is not None)
|
||||
# Latch (and sync to 3D) from any depth-carrying frame, not only when the
|
||||
# 2D mode is Depth — the surface may show depth while the canvas shows arrows.
|
||||
if frame.depth is not None:
|
||||
# Remember every depth-bearing frame, whichever 2D mode is showing,
|
||||
# so a later depth view and the 3D surface name the same interval.
|
||||
self._last_depth_frame = frame.depth
|
||||
self._depth_range()
|
||||
span = self._active_depth_span()
|
||||
self._publish_shared_depth_scale(span)
|
||||
if mode == DEPTH:
|
||||
self.arrow_panel.set_depth_scale(self._depth_scale_text(frame))
|
||||
self.arrow_panel.set_depth_scale(
|
||||
self._depth_scale_text(frame),
|
||||
tip_key=self._depth_scale_tip_key(),
|
||||
)
|
||||
else:
|
||||
self.arrow_panel.set_depth_scale(None)
|
||||
rendered = render_image(
|
||||
@@ -2819,7 +2890,7 @@ class StudioMainWindow(QMainWindow):
|
||||
mode=mode,
|
||||
theme=self._theme,
|
||||
draw_style=draw_style,
|
||||
depth_range=self._depth_range(),
|
||||
depth_range=span,
|
||||
canvas_bgr=hex_to_bgr(color(self._theme, "arrow_canvas", "#0B1830")),
|
||||
domain_mask=boundary,
|
||||
# A frame read from a stored analysis result says which policy its *recovered*
|
||||
@@ -3060,9 +3131,16 @@ class StudioMainWindow(QMainWindow):
|
||||
if panel is None or not hasattr(panel, "settingsChanged"):
|
||||
return False
|
||||
panel.settingsChanged.connect(self.replay_ctl.on_display_settings)
|
||||
panel.settingsChanged.connect(self._on_depth_scale_display)
|
||||
self._analysis_display_connected = True
|
||||
return True
|
||||
|
||||
def _on_depth_scale_display(self, _settings) -> None:
|
||||
"""A colour-scale change has to repaint the frame already on screen."""
|
||||
presenter = getattr(self, "acquire_presenter", None)
|
||||
if presenter is not None and hasattr(presenter, "drop_render_marker"):
|
||||
presenter.drop_render_marker()
|
||||
|
||||
def _await_analysis_display(self) -> None:
|
||||
"""Wait, briefly and off the critical path, for the 3D view to exist.
|
||||
|
||||
|
||||
@@ -506,12 +506,6 @@ class ThreeDCoordinator:
|
||||
# held field frame so the operator sees the change without waiting.
|
||||
self._settings_dirty = True
|
||||
|
||||
def set_shared_depth_scale(self, span: tuple[float, float] | None) -> None:
|
||||
"""Adopt the 2D canvas's latched depth colour span for automatic colouring."""
|
||||
setter = getattr(self._widget, "set_shared_depth_scale", None)
|
||||
if callable(setter):
|
||||
setter(span)
|
||||
|
||||
def set_ramp(self, ramp: str) -> None:
|
||||
"""Hand the theme's colour ramp to the composer.
|
||||
|
||||
|
||||
@@ -367,9 +367,10 @@ def _field_scalar_range(
|
||||
) -> tuple[float, float] | None:
|
||||
"""The automatic colour span for the uploaded field.
|
||||
|
||||
Depth shares the 2D canvas's latched ``(lo, hi)`` when the window has one, so the
|
||||
two legends name the same interval. Flow (and depth before that latch exists) keep
|
||||
the measured P5/P95 blend.
|
||||
Depth shares the span the window published for this frame — the session latch, or
|
||||
the current frame's finite minimum and maximum when the adaptive colour bar is on —
|
||||
so the two legends name the same interval. Flow (and depth before that span exists)
|
||||
keep the measured P5/P95 blend.
|
||||
"""
|
||||
descriptor = getattr(field, "descriptor", None)
|
||||
quantity = str(getattr(descriptor, "quantity", "") or "")
|
||||
@@ -1018,8 +1019,8 @@ class GLRenderer:
|
||||
view = np.asarray(camera.view, dtype=np.float32).reshape(4, 4)
|
||||
projection = np.asarray(camera.projection, dtype=np.float32).reshape(4, 4)
|
||||
scale = settings.color_scale
|
||||
# Prefer the 2D latched span at draw time, not only at upload: the latch can
|
||||
# arrive after this field was uploaded, and a repaint must still equalise.
|
||||
# Prefer the shared depth span at draw time, not only at upload: the window can
|
||||
# publish a new latch or a new per-frame span after this field was uploaded.
|
||||
automatic = self._scalar_range
|
||||
descriptor = getattr(scale, "descriptor", None) if scale is not None else None
|
||||
if (
|
||||
|
||||
@@ -257,6 +257,13 @@ class ThreeDPanel(QFrame):
|
||||
self.scale_combo.currentIndexChanged.connect(self._on_scale_mode_changed)
|
||||
form.addRow(self.scale_label, self.scale_combo)
|
||||
|
||||
# Depth only, and shared with the 2D canvas: off keeps the session latch,
|
||||
# on restores the per-frame colour bar. Fixed bounds ignore this switch.
|
||||
self.depth_adaptive_check = QCheckBox()
|
||||
self.depth_adaptive_check.setChecked(False)
|
||||
self.depth_adaptive_check.toggled.connect(self._emit_settings)
|
||||
form.addRow(self.depth_adaptive_check)
|
||||
|
||||
# The two bounds get their own row rather than sharing the mode's: three
|
||||
# controls side by side made this row the widest thing in the pane.
|
||||
self.range_label = QLabel()
|
||||
@@ -389,6 +396,7 @@ class ThreeDPanel(QFrame):
|
||||
self.scale_combo.setEnabled(False)
|
||||
self.lower_spin.setEnabled(False)
|
||||
self.upper_spin.setEnabled(False)
|
||||
self._sync_depth_adaptive_check()
|
||||
self.scale_bar.setVisible(False)
|
||||
self.scale_lower_label.setVisible(False)
|
||||
self.scale_upper_label.setVisible(False)
|
||||
@@ -397,6 +405,7 @@ class ThreeDPanel(QFrame):
|
||||
fixed = self.scale_combo.currentData() == "fixed"
|
||||
self.lower_spin.setEnabled(fixed)
|
||||
self.upper_spin.setEnabled(fixed)
|
||||
self._sync_depth_adaptive_check()
|
||||
self.scale_bar.set_lut(
|
||||
"diverging" if descriptor.signed and not fixed else "sequential"
|
||||
)
|
||||
@@ -685,6 +694,7 @@ class ThreeDPanel(QFrame):
|
||||
self.field_combo.setCurrentIndex(index)
|
||||
self.flow_arrows_check.setChecked(bool(settings.flow_arrows_enabled))
|
||||
self.contact_arrow_check.setChecked(bool(settings.contact_arrow_enabled))
|
||||
self.depth_adaptive_check.setChecked(bool(settings.depth_scale_adaptive))
|
||||
scale = settings.color_scale
|
||||
if scale is not None:
|
||||
index = self.scale_combo.findData(str(scale.mode))
|
||||
@@ -730,6 +740,7 @@ class ThreeDPanel(QFrame):
|
||||
field=self.field(),
|
||||
flow_arrows_enabled=bool(self.flow_arrows_check.isChecked()),
|
||||
contact_arrow_enabled=bool(self.contact_arrow_check.isChecked()),
|
||||
depth_scale_adaptive=bool(self.depth_adaptive_check.isChecked()),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ signals
|
||||
@@ -751,6 +762,21 @@ class ThreeDPanel(QFrame):
|
||||
self.gain_spin.setEnabled(bool(enabled) and available and self.enable_check.isChecked())
|
||||
self._emit_settings()
|
||||
|
||||
def _sync_depth_adaptive_check(self) -> None:
|
||||
"""The per-frame switch applies only while the colour scale is automatic.
|
||||
|
||||
Fixed bounds are typed by the operator and already shared; leaving the box
|
||||
enabled there would look as if both rules were in force.
|
||||
"""
|
||||
automatic = str(self.scale_combo.currentData() or "fixed") != "fixed"
|
||||
measuring = (
|
||||
self._descriptor is not None
|
||||
and self._descriptor.quantity != "unavailable"
|
||||
)
|
||||
self.depth_adaptive_check.setEnabled(
|
||||
automatic and measuring and self.enable_check.isChecked()
|
||||
)
|
||||
|
||||
def _on_scale_mode_changed(self, index: int) -> None:
|
||||
"""Only a fixed scale has bounds to type in; automatic ones come from data."""
|
||||
self.set_descriptor(self._descriptor)
|
||||
@@ -789,6 +815,8 @@ class ThreeDPanel(QFrame):
|
||||
self.range_label.setToolTip(t("scene3d.scale_range_tip"))
|
||||
self.scale_combo.setItemText(0, t("scene3d.scale_fixed"))
|
||||
self.scale_combo.setItemText(1, t("scene3d.scale_auto"))
|
||||
self.depth_adaptive_check.setText(t("scene3d.depth_adaptive"))
|
||||
self.depth_adaptive_check.setToolTip(t("scene3d.depth_adaptive_tip"))
|
||||
self.fps_label.setText(t("scene3d.max_fps"))
|
||||
self.fps_label.setToolTip(t("scene3d.max_fps_tip"))
|
||||
self.reset_button.setText(t("scene3d.reset_view"))
|
||||
|
||||
@@ -88,6 +88,7 @@ def settings_to_prefs(settings: DisplaySettings) -> dict[str, Any]:
|
||||
"flow_off_px": float(settings.flow_off_px),
|
||||
"flow_grid_spacing": int(settings.flow_grid_spacing),
|
||||
"max_arrows": int(settings.max_arrows),
|
||||
"depth_scale_adaptive": bool(settings.depth_scale_adaptive),
|
||||
}
|
||||
if scale is not None:
|
||||
block["color_scale"] = {
|
||||
@@ -169,4 +170,5 @@ def settings_from_prefs(
|
||||
flow_off_px=flow_off,
|
||||
flow_grid_spacing=spacing,
|
||||
max_arrows=max_arrows,
|
||||
depth_scale_adaptive=_flag(raw.get("depth_scale_adaptive"), False),
|
||||
)
|
||||
|
||||
@@ -107,8 +107,6 @@ class Scene3DView(QWidget):
|
||||
#: they cannot drift apart. ``None`` means "no session": the adapter then uses
|
||||
#: each frame's own mask, which is what the tests and the PoC want.
|
||||
self._domain_latch: Any = None
|
||||
#: The 2D canvas's latched depth colour span, held until the coordinator exists.
|
||||
self._shared_depth_scale: tuple[float, float] | None = None
|
||||
self._expected: ExpectedGeometry | None = None
|
||||
self._snapshot: Callable[[Any, FrameIdentity], Any] | None = None
|
||||
#: The adapter behind ``_snapshot``: it owns the reason a packet was refused,
|
||||
@@ -150,8 +148,6 @@ class Scene3DView(QWidget):
|
||||
self._coordinator = ThreeDCoordinator(
|
||||
self._widget, self._composer, self._executor, max_fps=30
|
||||
)
|
||||
if self._shared_depth_scale is not None:
|
||||
self._coordinator.set_shared_depth_scale(self._shared_depth_scale)
|
||||
self._panel = ThreeDPanel(self._coordinator, self._widget)
|
||||
# The picture must not be squeezed out of existence by the controls above it:
|
||||
# a GL widget with zero height draws nothing and looks like a broken view.
|
||||
@@ -318,17 +314,9 @@ class Scene3DView(QWidget):
|
||||
self._adapter.domain_latch = latch
|
||||
|
||||
def set_shared_depth_scale(self, span: tuple[float, float] | None) -> None:
|
||||
"""Share the 2D canvas's latched depth colour span with this pane.
|
||||
|
||||
Automatic depth colouring on the surface uses this interval so the 3D legend
|
||||
names the same ``(lo, hi)`` as the 2D depth scale. Fixed mode still follows the
|
||||
operator's spin boxes.
|
||||
"""
|
||||
self._shared_depth_scale = None if span is None else (float(span[0]), float(span[1]))
|
||||
if self._coordinator is not None and hasattr(
|
||||
self._coordinator, "set_shared_depth_scale"
|
||||
):
|
||||
self._coordinator.set_shared_depth_scale(self._shared_depth_scale)
|
||||
"""Colour depth with the 2D canvas's latched ``(lo, hi)``."""
|
||||
if self._widget is not None:
|
||||
self._widget.set_shared_depth_scale(span)
|
||||
|
||||
def set_theme(self, theme: str) -> None:
|
||||
"""Adopt the application theme's colour ramp.
|
||||
|
||||
@@ -383,11 +383,17 @@ class ArrowPanel(QFrame):
|
||||
self._theme = str(theme)
|
||||
self.colorbar.set_theme(self._theme)
|
||||
|
||||
def set_depth_scale(self, text: str | None) -> None:
|
||||
"""Show the fixed depth colour-scale range (``None`` hides it)."""
|
||||
def set_depth_scale(
|
||||
self, text: str | None, *, tip_key: str = "panels.depth_scale_tip"
|
||||
) -> None:
|
||||
"""Show the depth colour-scale range (``None`` hides it).
|
||||
|
||||
``tip_key`` names which rule produced the numbers: the session latch, the
|
||||
per-frame adaptive bar, or typed fixed bounds.
|
||||
"""
|
||||
self._depth_scale_text = str(text or "")
|
||||
self.depth_scale_label.setText(self._depth_scale_text)
|
||||
self.depth_scale_label.setToolTip(t("panels.depth_scale_tip"))
|
||||
self.depth_scale_label.setToolTip(t(tip_key))
|
||||
self._refresh_footer_visibility()
|
||||
|
||||
def _refresh_scale_label(self) -> None:
|
||||
|
||||
@@ -114,7 +114,7 @@ class Mesh3dPanel(QFrame):
|
||||
#: The session's island latch, shared with the 2D canvas (see
|
||||
#: ``set_domain_latch``). Held until the view exists.
|
||||
self._domain_latch = None
|
||||
#: The 2D canvas's latched depth colour span (see ``set_shared_depth_scale``).
|
||||
#: The 2D canvas's latched depth colour span, held until the view exists.
|
||||
self._shared_depth_scale: tuple[float, float] | None = None
|
||||
|
||||
# Reserve this top-level window's native handle *now*, before the window is
|
||||
@@ -230,10 +230,25 @@ class Mesh3dPanel(QFrame):
|
||||
self._view.set_theme(self._theme)
|
||||
|
||||
def set_shared_depth_scale(self, span: tuple[float, float] | None) -> None:
|
||||
"""Share the 2D depth colour span with the 3D surface (and hold it until built)."""
|
||||
self._shared_depth_scale = None if span is None else (float(span[0]), float(span[1]))
|
||||
"""Share the 2D canvas's latched depth colour span with the 3D surface.
|
||||
|
||||
Held until the view exists, the same way the island latch is: the view is
|
||||
built on first use, and a span decided before then still has to colour the
|
||||
first depth frame.
|
||||
"""
|
||||
normalized: tuple[float, float] | None
|
||||
if span is None:
|
||||
normalized = None
|
||||
else:
|
||||
normalized = (float(span[0]), float(span[1]))
|
||||
if normalized == self._shared_depth_scale:
|
||||
return
|
||||
self._shared_depth_scale = normalized
|
||||
if self._view is not None and hasattr(self._view, "set_shared_depth_scale"):
|
||||
self._view.set_shared_depth_scale(self._shared_depth_scale)
|
||||
self._view.set_shared_depth_scale(normalized)
|
||||
|
||||
def shared_depth_scale(self) -> tuple[float, float] | None:
|
||||
return self._shared_depth_scale
|
||||
|
||||
def set_domain_latch(self, latch) -> None:
|
||||
"""Share the 2D canvas's island latch with the 3D pane (see ``Scene3DView``).
|
||||
@@ -329,7 +344,7 @@ class Mesh3dPanel(QFrame):
|
||||
view.set_theme(self._theme)
|
||||
if self._domain_latch is not None and hasattr(view, "set_domain_latch"):
|
||||
view.set_domain_latch(self._domain_latch)
|
||||
if hasattr(view, "set_shared_depth_scale"):
|
||||
if self._shared_depth_scale is not None and hasattr(view, "set_shared_depth_scale"):
|
||||
view.set_shared_depth_scale(self._shared_depth_scale)
|
||||
if self._pending_settings is not None:
|
||||
view.restore_settings(self._pending_settings)
|
||||
|
||||
Reference in New Issue
Block a user