三维显示与二维共用深度色标,配置名改为 Finger

箭头阈值写入设置;去掉网格告警。三维色标改为立刻采用二维量程(不再 P5/P95 缓变)。选择器显示 Finger。已用 pane UI、shared-scale 与 catalog 单测验证。
This commit is contained in:
ykcheungab
2026-10-08 16:10:58 +08:00
parent f2d7b3e83c
commit 19ae1f4968
9 changed files with 85 additions and 22 deletions
@@ -1,7 +1,7 @@
{ {
"schema_version": 1, "schema_version": 1,
"profile_id": "orisys.finger.r01", "profile_id": "orisys.finger.r01",
"display_name": "Finger R01", "display_name": "Finger",
"product_family": "finger", "product_family": "finger",
"processing_backend": "uv_unroll", "processing_backend": "uv_unroll",
"sensor_json": "sensor.json", "sensor_json": "sensor.json",
+1 -1
View File
@@ -31,7 +31,7 @@ same rows.
|---|---|---| |---|---|---|
| `schema_version` | `1` | The SDK's current descriptor schema (`CURRENT_DESCRIPTOR_SCHEMA`). | | `schema_version` | `1` | The SDK's current descriptor schema (`CURRENT_DESCRIPTOR_SCHEMA`). |
| `profile_id` | `orisys.finger.r01` | The processing profile this folder is: finger design revision `r01`, whose unroll map and camera scale the configuration pins. Never a product-family name. | | `profile_id` | `orisys.finger.r01` | The processing profile this folder is: finger design revision `r01`, whose unroll map and camera scale the configuration pins. Never a product-family name. |
| `display_name` | `Finger R01` | Shown in the picker. Excluded from the processing fingerprint. | | `display_name` | `Finger` | Shown in the picker. Excluded from the processing fingerprint. |
| `product_family` | `finger` | What the operator calls it. `finger-mems` is a different family on a different pipeline. | | `product_family` | `finger` | What the operator calls it. `finger-mems` is a different family on a different pipeline. |
| `processing_backend` | `uv_unroll` | The pipeline the finger actually runs: `FingerRuntime` builds a UV domain from the export's unroll map, then flow and force. | | `processing_backend` | `uv_unroll` | The pipeline the finger actually runs: `FingerRuntime` builds a UV domain from the export's unroll map, then flow and force. |
| `sensor_json` | `sensor.json` | The configuration, still the only place camera, tracker and contact settings live. | | `sensor_json` | `sensor.json` | The configuration, still the only place camera, tracker and contact settings live. |
@@ -263,6 +263,24 @@ def shader_source(profile: Profile, program: str) -> tuple[str, str]:
return header + vertex, header + body return header + vertex, header + body
def _finite_minmax(scalar, valid) -> tuple[float, float] | None:
"""Finite minimum and maximum — the same interval ``FrameView.frame_depth_span`` uses."""
import numpy as np
values = np.asarray(scalar, dtype=np.float64).reshape(-1)
mask = np.asarray(valid, dtype=bool).reshape(-1)
if values.size != mask.size:
return None
usable = values[mask & np.isfinite(values)]
if usable.size == 0:
return None
lo = float(np.min(usable))
hi = float(np.max(usable))
if hi <= lo:
hi = lo + 1.0
return lo, hi
def _valid_scalar_range( def _valid_scalar_range(
scalar, valid, *, signed: bool = False scalar, valid, *, signed: bool = False
) -> tuple[float, float] | None: ) -> tuple[float, float] | None:
@@ -379,6 +397,10 @@ def _field_scalar_range(
hi = float(shared_depth_scale[1]) hi = float(shared_depth_scale[1])
if hi > lo and not (math.isnan(lo) or math.isnan(hi)): if hi > lo and not (math.isnan(lo) or math.isnan(hi)):
return lo, hi return lo, hi
if quantity == "depth_response":
# Same interval as the 2D canvas (finite min/max). A P5/P95 blend here
# made the 3D colour bar crawl toward a different number than the 2D label.
return _finite_minmax(field.scalar, field.valid)
measured = _valid_scalar_range( measured = _valid_scalar_range(
field.scalar, field.scalar,
field.valid, field.valid,
+37 -11
View File
@@ -159,6 +159,9 @@ class ThreeDPanel(QFrame):
#: prefs (or the dataclass defaults) so a colour-scale click cannot reset them. #: prefs (or the dataclass defaults) so a colour-scale click cannot reset them.
self._flow_grid_spacing = 10 self._flow_grid_spacing = 10
self._max_arrows = 400 self._max_arrows = 400
#: Depth colour span published by the 2D canvas. Shown here immediately so the
#: two legends do not wait on the next GL paint (which is rate-limited).
self._shared_depth_span: tuple[float, float] | None = None
root = QVBoxLayout(self) root = QVBoxLayout(self)
root.setContentsMargins(8, 6, 8, 6) root.setContentsMargins(8, 6, 8, 6)
@@ -490,7 +493,16 @@ class ThreeDPanel(QFrame):
if quantity.startswith("scene3d."): if quantity.startswith("scene3d."):
quantity = descriptor.quantity quantity = descriptor.quantity
quality = t(f"scene3d.quality_{descriptor.quality}") quality = t(f"scene3d.quality_{descriptor.quality}")
if effective is None or effective.scale_mode == "": shared = (
self._shared_depth_span
if str(descriptor.quantity) == "depth_response"
else None
)
if shared is not None:
lo, hi = shared
elif effective is not None and effective.scale_mode:
lo, hi = effective.scale_lower, effective.scale_upper
else:
self._set_legend( self._set_legend(
t( t(
"scene3d.legend", "scene3d.legend",
@@ -503,7 +515,7 @@ class ThreeDPanel(QFrame):
return return
mode = t( mode = t(
"scene3d.legend_auto" "scene3d.legend_auto"
if effective.scale_mode == "automatic" if (effective is None or effective.scale_mode != "fixed")
else "scene3d.legend_fixed" else "scene3d.legend_fixed"
) )
self._set_legend( self._set_legend(
@@ -512,15 +524,25 @@ class ThreeDPanel(QFrame):
quantity=quantity, quantity=quantity,
unit=descriptor.unit, unit=descriptor.unit,
mode=mode, mode=mode,
# Three significant digits: the footer is one line, and "0.033 – 0.90" lower=self._format_bound(lo),
# says as much as "0.03337 – 0.8993" while leaving room for the rest. upper=self._format_bound(hi),
lower=f"{effective.scale_lower:.3g}",
upper=f"{effective.scale_upper:.3g}",
quality=quality, quality=quality,
) )
) )
self.legend_label.setVisible(self._show_diagnostics) self.legend_label.setVisible(self._show_diagnostics)
def set_shared_depth_scale(self, span: tuple[float, float] | None) -> None:
"""Adopt the 2D canvas's depth colour span and refresh the legend now."""
if span is None:
self._shared_depth_span = None
else:
self._shared_depth_span = (float(span[0]), float(span[1]))
self._render_scale_bounds()
self._render_legend()
def _format_bound(self, value: float) -> str:
return f"{float(value):.3f}"
def _render_scale_bounds(self) -> None: def _render_scale_bounds(self) -> None:
"""Show only the numeric bounds needed to read the color ramp.""" """Show only the numeric bounds needed to read the color ramp."""
descriptor = self._descriptor descriptor = self._descriptor
@@ -531,16 +553,20 @@ class ThreeDPanel(QFrame):
upper = "" upper = ""
mode = str(self.scale_combo.currentData() or "fixed") mode = str(self.scale_combo.currentData() or "fixed")
effective = self._effective_scale effective = self._effective_scale
if visible and mode == "fixed": quantity = "" if descriptor is None else str(descriptor.quantity)
lower = f"{self.lower_spin.value():.3g}" if visible and quantity == "depth_response" and self._shared_depth_span is not None:
upper = f"{self.upper_spin.value():.3g}" lower = self._format_bound(self._shared_depth_span[0])
upper = self._format_bound(self._shared_depth_span[1])
elif visible and mode == "fixed":
lower = self._format_bound(self.lower_spin.value())
upper = self._format_bound(self.upper_spin.value())
elif ( elif (
visible visible
and effective is not None and effective is not None
and effective.scale_mode == "automatic" and effective.scale_mode == "automatic"
): ):
lower = f"{effective.scale_lower:.3g}" lower = self._format_bound(effective.scale_lower)
upper = f"{effective.scale_upper:.3g}" upper = self._format_bound(effective.scale_upper)
self.scale_lower_label.setText(lower) self.scale_lower_label.setText(lower)
self.scale_upper_label.setText(upper) self.scale_upper_label.setText(upper)
self.scale_lower_label.setVisible(visible) self.scale_lower_label.setVisible(visible)
@@ -317,6 +317,8 @@ class Scene3DView(QWidget):
"""Colour depth with the 2D canvas's latched ``(lo, hi)``.""" """Colour depth with the 2D canvas's latched ``(lo, hi)``."""
if self._widget is not None: if self._widget is not None:
self._widget.set_shared_depth_scale(span) self._widget.set_shared_depth_scale(span)
if self._panel is not None:
self._panel.set_shared_depth_scale(span)
def set_theme(self, theme: str) -> None: def set_theme(self, theme: str) -> None:
"""Adopt the application theme's colour ramp. """Adopt the application theme's colour ramp.
+17 -2
View File
@@ -129,6 +129,21 @@ def test_arrow_threshold_round_trips_and_feeds_the_shared_policy(panel):
assert policy_for_settings(again).flow_on_px == pytest.approx(0.9) assert policy_for_settings(again).flow_on_px == pytest.approx(0.9)
def test_depth_legend_follows_the_shared_2d_span_immediately(panel):
"""The 3D colour-bar numbers are the 2D span, not a lagged GL automatic range."""
from orisys_app_shared.scene3d.contracts import FieldDescriptor, RenderStats
panel.set_descriptor(
FieldDescriptor("depth_response", "1", "approximate", False)
)
panel.set_render_stats(
RenderStats(scale_lower=0.0, scale_upper=0.0489, scale_mode="automatic")
)
panel.set_shared_depth_scale((0.0, 0.029))
assert panel.scale_lower_label.text() == "0.000"
assert panel.scale_upper_label.text() == "0.029"
def test_source_kind_analysis_uses_its_own_label(panel): def test_source_kind_analysis_uses_its_own_label(panel):
"""A stored result is not labelled as live acquisition.""" """A stored result is not labelled as live acquisition."""
panel.set_diagnostics_visible(True) panel.set_diagnostics_visible(True)
@@ -241,8 +256,8 @@ def test_the_diagnostics_switch_governs_what_the_pane_explains(panel):
) )
panel.set_diagnostics_visible(False) panel.set_diagnostics_visible(False)
assert not panel.scale_bar.isHidden() assert not panel.scale_bar.isHidden()
assert panel.scale_lower_label.text() == "0.25" assert panel.scale_lower_label.text() == "0.250"
assert panel.scale_upper_label.text() == "2.5" assert panel.scale_upper_label.text() == "2.500"
assert not panel.scale_lower_label.isHidden() assert not panel.scale_lower_label.isHidden()
assert not panel.scale_upper_label.isHidden() assert not panel.scale_upper_label.isHidden()
assert panel.legend_label.isHidden() assert panel.legend_label.isHidden()
+1 -1
View File
@@ -1018,7 +1018,7 @@ def test_the_legend_shows_the_range_the_shader_actually_used(qt_app):
) )
) )
legend = panel.legend_label.text() legend = panel.legend_label.text()
assert "0.25" in legend and "7.5" in legend, legend assert "0.250" in legend and "7.500" in legend, legend
assert t("scene3d.legend_auto") in legend, legend assert t("scene3d.legend_auto") in legend, legend
assert panel.scale_bar.isVisible() or panel.scale_bar.parent() is not None assert panel.scale_bar.isVisible() or panel.scale_bar.parent() is not None
assert "flow_magnitude" not in legend assert "flow_magnitude" not in legend
@@ -51,13 +51,13 @@ EXPECTED_SHIPPED: dict[str, tuple[str, str, str, bool, str]] = {
} }
#: What an operator reads in the picker, one row per shipped bundle: the bundle's name, and #: What an operator reads in the picker, one row per shipped bundle: the bundle's name, and
#: nothing else. ``finger`` declares ``Finger R01`` in its descriptor; the other three have no #: nothing else. ``finger`` declares ``Finger`` in its descriptor; the other three have no
#: descriptor and are named by the folder they live in, as the SDK's built-in table words it. #: descriptor and are named by the folder they live in, as the SDK's built-in table words it.
#: The origin, the calibration selection and the SDK's ``degraded`` verdict are not rows — they #: The origin, the calibration selection and the SDK's ``degraded`` verdict are not rows — they
#: are read in the dialog's details surface, which sits under the selector. #: are read in the dialog's details surface, which sits under the selector.
EXPECTED_ROWS: dict[str, str] = { EXPECTED_ROWS: dict[str, str] = {
"box": "Box", "box": "Box",
"finger": "Finger R01", "finger": "Finger",
"finger-mems": "Finger (MEMS)", "finger-mems": "Finger (MEMS)",
"mini": "Mini", "mini": "Mini",
} }
+2 -4
View File
@@ -43,12 +43,10 @@ def test_depth_without_a_shared_span_still_measures_the_field() -> None:
field = _field("depth_response", np.linspace(0.0, 9.0, 16).reshape(4, 4)) field = _field("depth_response", np.linspace(0.0, 9.0, 16).reshape(4, 4))
span = _field_scalar_range( span = _field_scalar_range(
field, field,
previous=None, previous=(0.0, 2.0),
shared_depth_scale=None, shared_depth_scale=None,
) )
assert span is not None assert span == (0.0, 9.0)
assert span[0] == 0.0
assert span[1] > 1.0
def test_a_degenerate_shared_span_falls_back_to_the_measurement() -> None: def test_a_degenerate_shared_span_falls_back_to_the_measurement() -> None: