autocal layout first design
This commit is contained in:
@@ -33,6 +33,10 @@
|
||||
"scene3d_settings": "3D display settings…",
|
||||
"show_fps": "Show FPS",
|
||||
"show_diagnostics": "Diagnostics",
|
||||
"raw_auto_zero": "Finger raw auto-zero at start",
|
||||
"raw_auto_zero_tip": "Value cards only: estimate an idle bias and deadband after start. Curves, statistics and CSV keep the measured values.",
|
||||
"raw_reestimate": "Re-estimate raw baseline…",
|
||||
"raw_reestimate_tip": "Drop the current display baseline and re-estimate from the next quiet samples (value cards only).",
|
||||
"settings": "Settings area",
|
||||
"plot_settings": "Plot settings…",
|
||||
"rotate_arrows": "Rotate arrows 90°",
|
||||
@@ -217,6 +221,8 @@
|
||||
"selection_empty": "No valid samples in the selection",
|
||||
"value_unavailable": "n/a",
|
||||
"value_unavailable_tip": "No curve is loaded for {channel}, so it has no value in this unit (raw counts are never shown in its place)",
|
||||
"zero_zone": "zero zone",
|
||||
"zero_zone_tip": "Display deadband after auto baseline; curves, statistics and CSV keep the measured values",
|
||||
"unit_toggle": "Switch unit",
|
||||
"unit_toggle_tip": "Click to switch the three curves and readouts between newton and raw units",
|
||||
"browse_only_tip": "Drag or scroll on the chart to browse history"
|
||||
@@ -412,6 +418,12 @@
|
||||
"field_depth": "Depth response",
|
||||
"field_flow": "Optical flow magnitude",
|
||||
"field_substituted": "This frame shows {shown}, not the chosen {chosen}.",
|
||||
"flow_arrows": "Surface flow arrows",
|
||||
"flow_arrows_tip": "Draw optical-flow arrows on the sensing surface using the same sample points as the 2D view.",
|
||||
"flow_arrows_unavailable": "This scene package has no UV→scene surface mapping, so flow arrows cannot be drawn.",
|
||||
"contact_arrow": "Contact normal arrow",
|
||||
"contact_arrow_tip": "Draw a yellow local-normal arrow at the contact centre (length from the normal response, capped — not a calibrated force vector).",
|
||||
"contact_arrow_unavailable": "This scene package has no UV→scene surface mapping, so the contact arrow cannot be drawn.",
|
||||
"quantity_flow_magnitude": "Optical flow magnitude",
|
||||
"quantity_depth_response": "Depth response",
|
||||
"mode_surface": "surface colouring",
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
"scene3d_settings": "三维显示设置…",
|
||||
"show_fps": "显示 FPS",
|
||||
"show_diagnostics": "诊断信息",
|
||||
"raw_auto_zero": "Finger raw 启动归零",
|
||||
"raw_auto_zero_tip": "仅影响数字卡展示:启动后自动估计无载偏置与死区。曲线、统计与 CSV 仍为测量值。",
|
||||
"raw_reestimate": "重新估计 raw 基线…",
|
||||
"raw_reestimate_tip": "丢弃当前展示基线,从接下来的无载样本重新估计(仅数字卡)。",
|
||||
"settings": "设置区",
|
||||
"plot_settings": "绘图设置…",
|
||||
"rotate_arrows": "旋转箭头图 90°",
|
||||
@@ -217,6 +221,8 @@
|
||||
"selection_empty": "选区无有效样本",
|
||||
"value_unavailable": "不可用",
|
||||
"value_unavailable_tip": "{channel} 轴未加载标定曲线,因此没有该单位的数值(不会用原始值冒充)",
|
||||
"zero_zone": "零区",
|
||||
"zero_zone_tip": "展示归零后的死区内读数;曲线、统计与 CSV 仍为原始测量值",
|
||||
"unit_toggle": "切换单位",
|
||||
"unit_toggle_tip": "点击在「牛顿」与「原始单位」之间切换三轴曲线与读数",
|
||||
"browse_only_tip": "拖动或滚轮即可进入历史浏览"
|
||||
@@ -412,6 +418,12 @@
|
||||
"field_depth": "深度响应",
|
||||
"field_flow": "光流幅值",
|
||||
"field_substituted": "这一帧画的是 {shown},不是所选的 {chosen}。",
|
||||
"flow_arrows": "表面光流箭头",
|
||||
"flow_arrows_tip": "在传感表面上按与二维相同的采样点绘制光流箭头。",
|
||||
"flow_arrows_unavailable": "当前场景包没有 UV→三维 表面映射,无法绘制光流箭头。",
|
||||
"contact_arrow": "接触法向箭头",
|
||||
"contact_arrow_tip": "在接触中心沿局部法向绘制黄色示意箭头(长度由法向响应限幅,不是标定合力)。",
|
||||
"contact_arrow_unavailable": "当前场景包没有 UV→三维 表面映射,无法绘制接触法向箭头。",
|
||||
"quantity_flow_magnitude": "光流幅值",
|
||||
"quantity_depth_response": "深度响应",
|
||||
"mode_surface": "曲面着色",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"show_cal_panel": false,
|
||||
"show_finger_3d": true,
|
||||
"show_fps": false,
|
||||
"raw_auto_zero": true,
|
||||
"gl_profile": "desktop33",
|
||||
"scene3d_package": "",
|
||||
"scene3d_display": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"schema_version": 2,
|
||||
"unit": "N",
|
||||
"channels": {
|
||||
"normal": {
|
||||
@@ -14,8 +14,22 @@
|
||||
[62000.0, 2.377]
|
||||
]
|
||||
},
|
||||
"shear": {
|
||||
"shear_x": {
|
||||
"method": "piecewise_linear",
|
||||
"raw_key": "fshearx",
|
||||
"input": "components",
|
||||
"signed": true,
|
||||
"extrapolate": true,
|
||||
"clamp_output_min": 0.0,
|
||||
"points": [
|
||||
[1000.0, 0.0],
|
||||
[36000.0, 1.19],
|
||||
[68000.0, 2.06]
|
||||
]
|
||||
},
|
||||
"shear_y": {
|
||||
"method": "piecewise_linear",
|
||||
"raw_key": "fsheary",
|
||||
"input": "components",
|
||||
"signed": true,
|
||||
"extrapolate": true,
|
||||
|
||||
Binary file not shown.
@@ -925,6 +925,16 @@ class StudioMainWindow(QMainWindow):
|
||||
)
|
||||
self.view_menu.addAction(self.act_show_diagnostics)
|
||||
|
||||
self.act_raw_auto_zero = QAction(self)
|
||||
self.act_raw_auto_zero.setCheckable(True)
|
||||
self.act_raw_auto_zero.setChecked(bool(self.prefs.get("raw_auto_zero", True)))
|
||||
self.act_raw_auto_zero.toggled.connect(self.display_ctl.on_raw_auto_zero_toggled)
|
||||
self.view_menu.addAction(self.act_raw_auto_zero)
|
||||
|
||||
self.act_raw_reestimate = QAction(self)
|
||||
self.act_raw_reestimate.triggered.connect(self.display_ctl.request_raw_reestimate)
|
||||
self.view_menu.addAction(self.act_raw_reestimate)
|
||||
|
||||
self.act_settings = QAction(self)
|
||||
self.act_settings.setCheckable(True)
|
||||
self.act_settings.setChecked(self.toolbar.settings_expanded())
|
||||
@@ -1168,6 +1178,10 @@ class StudioMainWindow(QMainWindow):
|
||||
self.act_scene3d_settings.setText(t("menu.scene3d_settings"))
|
||||
self.act_show_fps.setText(t("menu.show_fps"))
|
||||
self.act_show_diagnostics.setText(t("menu.show_diagnostics"))
|
||||
self.act_raw_auto_zero.setText(t("menu.raw_auto_zero"))
|
||||
self.act_raw_auto_zero.setToolTip(t("menu.raw_auto_zero_tip"))
|
||||
self.act_raw_reestimate.setText(t("menu.raw_reestimate"))
|
||||
self.act_raw_reestimate.setToolTip(t("menu.raw_reestimate_tip"))
|
||||
self.act_settings.setText(t("menu.settings"))
|
||||
self.act_plot_settings.setText(t("menu.plot_settings"))
|
||||
self.act_rotate_arrows.setText(t("menu.rotate_arrows"))
|
||||
@@ -1886,6 +1900,16 @@ class StudioMainWindow(QMainWindow):
|
||||
self._stop_preview(announce=False)
|
||||
|
||||
self.force_plots.reset()
|
||||
units = "N" if self.toolbar.units() == "newton" else "raw"
|
||||
self.force_plots.configure_presentation(
|
||||
sensor_type=sensor_type,
|
||||
unit=units,
|
||||
enabled=(
|
||||
sensor_type == "finger"
|
||||
and units == "raw"
|
||||
and bool(self.prefs.get("raw_auto_zero", True))
|
||||
),
|
||||
)
|
||||
self._last_plot_packet_key = None
|
||||
self._last_rendered_view_mode = None
|
||||
# Fresh record channel for the new session. The calibration identity is
|
||||
@@ -2126,6 +2150,30 @@ class StudioMainWindow(QMainWindow):
|
||||
self.session.last_sample_valid = True
|
||||
self._last_display_fallback = bool(display.using_raw_fallback)
|
||||
self._last_display_available = tuple(bool(flag) for flag in display.available)
|
||||
flow = getattr(packet, "flow", None)
|
||||
flow_p95 = None
|
||||
if flow is not None:
|
||||
try:
|
||||
import numpy as np
|
||||
|
||||
arr = np.asarray(flow, dtype=np.float64)
|
||||
if arr.ndim == 3 and arr.shape[-1] == 2:
|
||||
mag = np.linalg.norm(arr, axis=-1)
|
||||
mask = getattr(packet, "valid_mask", None)
|
||||
if mask is not None:
|
||||
m = np.asarray(mask, dtype=bool)
|
||||
if m.shape == mag.shape:
|
||||
vals = mag[m & np.isfinite(mag)]
|
||||
else:
|
||||
vals = mag[np.isfinite(mag)]
|
||||
else:
|
||||
vals = mag[np.isfinite(mag)]
|
||||
if vals.size:
|
||||
flow_p95 = float(np.percentile(vals, 95))
|
||||
except Exception: # noqa: BLE001
|
||||
flow_p95 = None
|
||||
contact = getattr(packet, "contact", None)
|
||||
contact_found = bool(getattr(contact, "found", False)) if contact else False
|
||||
self.force_plots.push(
|
||||
# Session-relative time, the same base the store and
|
||||
# ``forces.csv`` use. The wall clock must never reach the
|
||||
@@ -2136,6 +2184,9 @@ class StudioMainWindow(QMainWindow):
|
||||
display.values[2],
|
||||
unit=display.unit,
|
||||
available=display.available,
|
||||
flow_p95=flow_p95,
|
||||
contact_found=contact_found,
|
||||
forces_valid=bool(getattr(packet, "forces_valid", True)),
|
||||
)
|
||||
else:
|
||||
self.session.last_sample_valid = False
|
||||
@@ -2226,6 +2277,11 @@ class StudioMainWindow(QMainWindow):
|
||||
def _render_run_image(self, packet) -> None:
|
||||
if packet is None or self._replay_owns_image():
|
||||
return
|
||||
flow_valid = getattr(packet, "flow_valid", None)
|
||||
depth_valid = getattr(packet, "depth_valid", None)
|
||||
stale = bool(getattr(packet, "stale_forces", False)) or (
|
||||
flow_valid is False and depth_valid is False
|
||||
)
|
||||
self._show_image_frame(
|
||||
ImageFrame(
|
||||
getattr(packet, "processed_frame", None),
|
||||
@@ -2235,6 +2291,11 @@ class StudioMainWindow(QMainWindow):
|
||||
source=SOURCE_RECOMPUTE if self._recompute_running else SOURCE_LIVE,
|
||||
frame_id=int(getattr(packet, "frame_id", 0) or 0),
|
||||
t_rel_s=float(getattr(packet, "mono_t", 0.0) or 0.0),
|
||||
stale=stale,
|
||||
flow_valid=flow_valid,
|
||||
flow_frame_id=getattr(packet, "flow_frame_id", None),
|
||||
depth_valid=depth_valid,
|
||||
depth_frame_id=getattr(packet, "depth_frame_id", None),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -84,7 +84,10 @@ class DisplayPrefsController(QObject):
|
||||
#: Whether the panels explain the pipeline around the picture (see
|
||||
#: ``on_show_diagnostics_toggled``). Off: each panel shows what the picture *is*.
|
||||
self._show_diagnostics = bool(host.prefs.get("show_diagnostics", False))
|
||||
#: Finger-raw value cards: startup auto baseline / deadband (display only).
|
||||
self._raw_auto_zero = bool(host.prefs.get("raw_auto_zero", True))
|
||||
self._apply_diagnostics()
|
||||
self.apply_presentation_prefs()
|
||||
|
||||
def bind_actions(self, *, rotate_arrows: Any) -> None:
|
||||
"""Take the rotate entry once the menus exist (it follows the sensor)."""
|
||||
@@ -261,3 +264,43 @@ class DisplayPrefsController(QObject):
|
||||
setter = getattr(panel, "set_diagnostics_visible", None)
|
||||
if callable(setter):
|
||||
setter(self._show_diagnostics)
|
||||
|
||||
# ------------------------------------------------------- raw display zeroing
|
||||
@property
|
||||
def raw_auto_zero(self) -> bool:
|
||||
return bool(self._raw_auto_zero)
|
||||
|
||||
def set_raw_auto_zero(self, enabled: bool) -> None:
|
||||
"""Persist whether Finger raw value cards auto-baseline at session start."""
|
||||
self._raw_auto_zero = bool(enabled)
|
||||
self._host.prefs["raw_auto_zero"] = self._raw_auto_zero
|
||||
self._host._persist_prefs()
|
||||
self.apply_presentation_prefs()
|
||||
|
||||
def on_raw_auto_zero_toggled(self, enabled: bool) -> None:
|
||||
self.set_raw_auto_zero(enabled)
|
||||
|
||||
def apply_presentation_prefs(self) -> None:
|
||||
"""Push the current baseline policy onto the charts (Finger raw only)."""
|
||||
configure = getattr(self._charts, "configure_presentation", None)
|
||||
if not callable(configure):
|
||||
return
|
||||
sensor = (
|
||||
self._picker.sensor_type()
|
||||
if hasattr(self._picker, "sensor_type")
|
||||
else "box"
|
||||
)
|
||||
unit = "N" if self._toolbar.units() == "newton" else "raw"
|
||||
configure(
|
||||
sensor_type=sensor,
|
||||
unit=unit,
|
||||
enabled=bool(self._raw_auto_zero and sensor == "finger" and unit == "raw"),
|
||||
)
|
||||
|
||||
def request_raw_reestimate(self) -> None:
|
||||
"""Operator entry: restart the idle baseline window on the value cards."""
|
||||
request = getattr(self._charts, "request_presentation_reestimate", None)
|
||||
if callable(request):
|
||||
request()
|
||||
else:
|
||||
self.apply_presentation_prefs()
|
||||
|
||||
@@ -392,6 +392,26 @@ class FingerFrameAdapter:
|
||||
refused.append(mask_problem)
|
||||
return None
|
||||
self.last_diagnostic = None
|
||||
contact = getattr(packet, "contact", None)
|
||||
contact_xy = None
|
||||
contact_found = False
|
||||
contact_valid = False
|
||||
if contact is not None:
|
||||
contact_xy = getattr(contact, "centroid_uv_xy", None)
|
||||
contact_found = bool(getattr(contact, "found", False))
|
||||
contact_valid = bool(getattr(contact, "valid", False))
|
||||
flow = getattr(packet, "flow", None)
|
||||
flow_uv = None
|
||||
flow_mask = None
|
||||
if flow is not None and _field_validity(packet, "flow", identity.frame_id) is None:
|
||||
arr = np.asarray(flow, dtype=np.float32)
|
||||
if arr.ndim == 3 and arr.shape[-1] == 2 and tuple(arr.shape[:2]) == self.shape_hw:
|
||||
flow_uv = freeze(arr, dtype=np.float32)
|
||||
device_for_flow, _ = _device_mask(packet, self.shape_hw)
|
||||
flow_mask = _intersect(np.isfinite(arr[..., 0]) & np.isfinite(arr[..., 1]), device_for_flow)
|
||||
fn = float(getattr(packet, "normal_force_raw", 0.0) or 0.0)
|
||||
if not np.isfinite(fn):
|
||||
fn = float(getattr(packet, "normal_force", 0.0) or 0.0)
|
||||
return SurfaceFieldFrame(
|
||||
identity=identity,
|
||||
geometry_id=self.geometry_id,
|
||||
@@ -400,6 +420,12 @@ class FingerFrameAdapter:
|
||||
scalar_descriptor=descriptor,
|
||||
valid=_intersect(mask, device),
|
||||
computed_frame_id=_computed_frame_id(packet, name, identity.frame_id),
|
||||
flow_uv=flow_uv,
|
||||
flow_valid_mask=flow_mask,
|
||||
contact_uv_xy=contact_xy if isinstance(contact_xy, tuple) else None,
|
||||
contact_found=contact_found,
|
||||
contact_valid=contact_valid,
|
||||
contact_fn=fn if np.isfinite(fn) else 0.0,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -462,17 +462,24 @@ class ThreeDCoordinator:
|
||||
self._set_state("no_data")
|
||||
|
||||
def set_settings(self, settings: DisplaySettings) -> None:
|
||||
"""Apply display settings; a gain change means recomposing the last field."""
|
||||
"""Apply display settings; a gain/overlay change recomposes the last field."""
|
||||
previous = self._settings
|
||||
self._settings = settings
|
||||
self._widget.set_settings(settings)
|
||||
gain_changed = (
|
||||
needs_recompose = (
|
||||
previous.display_gain != settings.display_gain
|
||||
or previous.deform_enabled != settings.deform_enabled
|
||||
or previous.field != settings.field
|
||||
or previous.flow_arrows_enabled != settings.flow_arrows_enabled
|
||||
or previous.contact_arrow_enabled != settings.contact_arrow_enabled
|
||||
or previous.flow_on_px != settings.flow_on_px
|
||||
or previous.flow_off_px != settings.flow_off_px
|
||||
or previous.flow_grid_spacing != settings.flow_grid_spacing
|
||||
or previous.max_arrows != settings.max_arrows
|
||||
)
|
||||
if gain_changed and self._assets is not None:
|
||||
# The scalar is untouched by the gain, but the *positions* are not, so
|
||||
# the last field frame has to be composed again.
|
||||
if needs_recompose and self._assets is not None:
|
||||
# Gain moves positions; overlay toggles rebuild arrow instances on the
|
||||
# held field frame so the operator sees the change without waiting.
|
||||
self._settings_dirty = True
|
||||
|
||||
def show_rest(self) -> None:
|
||||
|
||||
@@ -190,6 +190,31 @@ def shader_header(profile: Profile) -> str:
|
||||
raise ValueError(f"unknown GL profile: {profile!r}") from None
|
||||
|
||||
|
||||
_ARROW_VERTEX_BODY = """
|
||||
layout(location = 0) in vec3 a_position;
|
||||
layout(location = 1) in vec4 a_color;
|
||||
|
||||
uniform mat4 u_view;
|
||||
uniform mat4 u_projection;
|
||||
|
||||
out vec4 v_color;
|
||||
|
||||
void main() {
|
||||
v_color = a_color;
|
||||
gl_Position = u_projection * u_view * vec4(a_position, 1.0);
|
||||
}
|
||||
"""
|
||||
|
||||
_ARROW_FRAGMENT_BODY = """
|
||||
in vec4 v_color;
|
||||
out vec4 frag_color;
|
||||
|
||||
void main() {
|
||||
frag_color = v_color;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def shader_source(profile: Profile, program: str) -> tuple[str, str]:
|
||||
"""``(vertex, fragment)`` source for a profile and a program name.
|
||||
|
||||
@@ -199,19 +224,23 @@ def shader_source(profile: Profile, program: str) -> tuple[str, str]:
|
||||
header = shader_header(profile)
|
||||
if program == "scalar":
|
||||
body = _SCALAR_FRAGMENT_BODY
|
||||
vertex = _VERTEX_BODY
|
||||
elif program == "diffuse":
|
||||
body = _DIFFUSE_FRAGMENT_BODY
|
||||
vertex = _VERTEX_BODY
|
||||
elif program == "arrow":
|
||||
return header + _ARROW_VERTEX_BODY, header + _ARROW_FRAGMENT_BODY
|
||||
else:
|
||||
raise ValueError(f"unknown shader program: {program!r}")
|
||||
return header + _VERTEX_BODY, header + body
|
||||
return header + vertex, header + body
|
||||
|
||||
|
||||
def _valid_scalar_range(scalar, valid) -> Optional[tuple[float, float]]:
|
||||
"""The range of the *valid* samples, or ``None`` when there are none.
|
||||
"""Robust P5/P95 range of *valid* samples, or ``None`` when there are none.
|
||||
|
||||
Only valid and finite samples count. Including the invalid ones would make the
|
||||
colour range follow whatever garbage sits outside the UV island, and including
|
||||
NaN would produce a range of NaN and a picture with no colours at all.
|
||||
Using min/max made empty-field noise paint the whole LUT. A percentile span
|
||||
with a minimum width keeps weak signals visible without amplifying floor
|
||||
noise into a full contact.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
@@ -222,9 +251,39 @@ def _valid_scalar_range(scalar, valid) -> Optional[tuple[float, float]]:
|
||||
usable = values[mask & np.isfinite(values)]
|
||||
if usable.size == 0:
|
||||
return None
|
||||
return float(usable.min()), float(usable.max())
|
||||
if usable.size < 8:
|
||||
lower = float(usable.min())
|
||||
upper = float(usable.max())
|
||||
else:
|
||||
lower = float(np.percentile(usable, 5.0))
|
||||
upper = float(np.percentile(usable, 95.0))
|
||||
# Minimum span: avoid collapsing nearly-constant fields into one colour.
|
||||
span = upper - lower
|
||||
min_span = max(abs(lower) * 0.05, abs(upper) * 0.05, 1e-3)
|
||||
if span < min_span:
|
||||
mid = 0.5 * (lower + upper)
|
||||
lower = mid - 0.5 * min_span
|
||||
upper = mid + 0.5 * min_span
|
||||
return lower, upper
|
||||
|
||||
|
||||
def _blend_scale_range(
|
||||
previous: Optional[tuple[float, float]],
|
||||
current: Optional[tuple[float, float]],
|
||||
*,
|
||||
alpha: float = 0.15,
|
||||
) -> Optional[tuple[float, float]]:
|
||||
"""Slowly update an automatic colour range to avoid frame-to-frame flicker."""
|
||||
if current is None:
|
||||
return previous
|
||||
if previous is None:
|
||||
return current
|
||||
a = float(np.clip(alpha, 0.0, 1.0))
|
||||
return (
|
||||
(1.0 - a) * float(previous[0]) + a * float(current[0]),
|
||||
(1.0 - a) * float(previous[1]) + a * float(current[1]),
|
||||
)
|
||||
|
||||
def _scale_bounds(scale, automatic: Optional[tuple[float, float]]) -> tuple[float, float]:
|
||||
"""The colour range to draw with: the operator's, or the data's own.
|
||||
|
||||
@@ -514,6 +573,10 @@ class GLRenderer:
|
||||
self._initialized = False
|
||||
self._paint_calls = 0
|
||||
self._last_upload_ms = 0.0
|
||||
#: Overlay VAOs keyed by kind ("flow" / "contact").
|
||||
self._overlay_vaos: dict[str, tuple[int, int, int, int]] = {}
|
||||
self._arrow_program: int = 0
|
||||
self._arrow_uniforms: dict[str, int] = {}
|
||||
|
||||
# ------------------------------------------------------------------ lifetime
|
||||
def initialize(self, capabilities: GLCapabilities) -> None:
|
||||
@@ -529,6 +592,7 @@ class GLRenderer:
|
||||
try:
|
||||
self._programs["scalar"] = _compile_program(GL, capabilities.profile, "scalar")
|
||||
self._programs["diffuse"] = _compile_program(GL, capabilities.profile, "diffuse")
|
||||
self._programs["arrow"] = _compile_program(GL, capabilities.profile, "arrow")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self.release()
|
||||
raise error(
|
||||
@@ -604,7 +668,74 @@ class GLRenderer:
|
||||
GL.glBufferSubData(GL.GL_ARRAY_BUFFER, 0, rows.nbytes, rows)
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
|
||||
self._last_upload_ms = (time.perf_counter() - started) * 1000.0
|
||||
self._scalar_range = _valid_scalar_range(frame.scalar, frame.valid)
|
||||
measured = _valid_scalar_range(frame.scalar, frame.valid)
|
||||
self._scalar_range = _blend_scale_range(self._scalar_range, measured)
|
||||
self._upload_overlays(frame)
|
||||
|
||||
def _upload_overlays(self, frame: RenderFrame) -> None:
|
||||
"""Upload merged flow / contact arrow meshes for this frame."""
|
||||
from orisys_app_shared.scene3d.overlays import arrow_instances_to_mesh
|
||||
|
||||
for kind, instances in (
|
||||
("flow", getattr(frame, "flow_arrows", None)),
|
||||
("contact", getattr(frame, "contact_arrow", None)),
|
||||
):
|
||||
if instances is None or int(instances.origins.shape[0]) == 0:
|
||||
self._clear_overlay(kind)
|
||||
continue
|
||||
positions, triangles, colors = arrow_instances_to_mesh(instances)
|
||||
if positions.size == 0:
|
||||
self._clear_overlay(kind)
|
||||
continue
|
||||
self._set_overlay(kind, positions, triangles, colors)
|
||||
|
||||
def _set_overlay(
|
||||
self,
|
||||
kind: str,
|
||||
positions: np.ndarray,
|
||||
triangles: np.ndarray,
|
||||
colors: np.ndarray,
|
||||
) -> None:
|
||||
from OpenGL import GL
|
||||
|
||||
self._clear_overlay(kind)
|
||||
count = int(positions.shape[0])
|
||||
interleaved = np.empty((count, 7), dtype=np.float32)
|
||||
interleaved[:, 0:3] = positions
|
||||
interleaved[:, 3:7] = colors
|
||||
vao = int(GL.glGenVertexArrays(1))
|
||||
vbo = int(GL.glGenBuffers(1))
|
||||
ebo = int(GL.glGenBuffers(1))
|
||||
GL.glBindVertexArray(vao)
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, vbo)
|
||||
GL.glBufferData(
|
||||
GL.GL_ARRAY_BUFFER, interleaved.nbytes, interleaved, GL.GL_DYNAMIC_DRAW
|
||||
)
|
||||
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, ebo)
|
||||
GL.glBufferData(
|
||||
GL.GL_ELEMENT_ARRAY_BUFFER, triangles.nbytes, triangles, GL.GL_DYNAMIC_DRAW
|
||||
)
|
||||
stride = 7 * 4
|
||||
GL.glEnableVertexAttribArray(0)
|
||||
GL.glVertexAttribPointer(0, 3, GL.GL_FLOAT, GL.GL_FALSE, stride, None)
|
||||
GL.glEnableVertexAttribArray(1)
|
||||
GL.glVertexAttribPointer(1, 4, GL.GL_FLOAT, GL.GL_FALSE, stride, _offset(3))
|
||||
GL.glBindVertexArray(0)
|
||||
self._overlay_vaos[kind] = (vao, vbo, ebo, int(triangles.size))
|
||||
|
||||
def _clear_overlay(self, kind: str) -> None:
|
||||
from OpenGL import GL
|
||||
|
||||
entry = self._overlay_vaos.pop(kind, None)
|
||||
if entry is None:
|
||||
return
|
||||
vao, vbo, ebo, _ = entry
|
||||
try:
|
||||
GL.glDeleteVertexArrays(1, [int(vao)])
|
||||
GL.glDeleteBuffers(1, [int(vbo)])
|
||||
GL.glDeleteBuffers(1, [int(ebo)])
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
def draw(
|
||||
self,
|
||||
@@ -659,6 +790,27 @@ class GLRenderer:
|
||||
GL.GL_TRIANGLES, part.triangles * 3, GL.GL_UNSIGNED_INT, None
|
||||
)
|
||||
GL.glBindVertexArray(0)
|
||||
# Overlays after the surface so arrows stay visible.
|
||||
arrow_program = self._programs.get("arrow")
|
||||
if arrow_program and self._overlay_vaos:
|
||||
locations = self._uniforms.get("arrow", {})
|
||||
GL.glUseProgram(arrow_program)
|
||||
_set_mat4(GL, locations.get("u_view"), view)
|
||||
_set_mat4(GL, locations.get("u_projection"), projection)
|
||||
GL.glDisable(GL.GL_DEPTH_TEST)
|
||||
for kind in ("flow", "contact"):
|
||||
entry = self._overlay_vaos.get(kind)
|
||||
if entry is None:
|
||||
continue
|
||||
vao, _vbo, _ebo, index_count = entry
|
||||
if index_count <= 0:
|
||||
continue
|
||||
GL.glBindVertexArray(vao)
|
||||
GL.glDrawElements(
|
||||
GL.GL_TRIANGLES, int(index_count), GL.GL_UNSIGNED_INT, None
|
||||
)
|
||||
GL.glBindVertexArray(0)
|
||||
GL.glEnable(GL.GL_DEPTH_TEST)
|
||||
GL.glUseProgram(0)
|
||||
self._paint_calls += 1
|
||||
return RenderStats(
|
||||
@@ -687,6 +839,8 @@ class GLRenderer:
|
||||
from OpenGL import GL
|
||||
|
||||
self.release_parts()
|
||||
for kind in list(self._overlay_vaos):
|
||||
self._clear_overlay(kind)
|
||||
for program in self._programs.values():
|
||||
try:
|
||||
GL.glDeleteProgram(int(program))
|
||||
|
||||
@@ -222,6 +222,16 @@ class ThreeDPanel(QFrame):
|
||||
self.field_note.setVisible(False)
|
||||
form.addRow(self.field_note)
|
||||
|
||||
self.flow_arrows_check = QCheckBox()
|
||||
self.flow_arrows_check.setChecked(True)
|
||||
self.flow_arrows_check.toggled.connect(self._emit_settings)
|
||||
form.addRow(self.flow_arrows_check)
|
||||
|
||||
self.contact_arrow_check = QCheckBox()
|
||||
self.contact_arrow_check.setChecked(True)
|
||||
self.contact_arrow_check.toggled.connect(self._emit_settings)
|
||||
form.addRow(self.contact_arrow_check)
|
||||
|
||||
self.scale_label = QLabel()
|
||||
self.scale_combo = QComboBox()
|
||||
self.scale_combo.addItem("fixed", "fixed")
|
||||
@@ -403,6 +413,18 @@ class ThreeDPanel(QFrame):
|
||||
self.deform_check.setChecked(False)
|
||||
finally:
|
||||
self._syncing = False
|
||||
flow_ok = bool(capabilities and capabilities.flow_arrows_available)
|
||||
contact_ok = bool(capabilities and capabilities.contact_arrow_available)
|
||||
self.flow_arrows_check.setEnabled(flow_ok and enabled)
|
||||
self.contact_arrow_check.setEnabled(contact_ok and enabled)
|
||||
self.flow_arrows_check.setToolTip(
|
||||
t("scene3d.flow_arrows_unavailable") if not flow_ok else t("scene3d.flow_arrows_tip")
|
||||
)
|
||||
self.contact_arrow_check.setToolTip(
|
||||
t("scene3d.contact_arrow_unavailable")
|
||||
if not contact_ok
|
||||
else t("scene3d.contact_arrow_tip")
|
||||
)
|
||||
|
||||
def capabilities(self) -> Optional[SceneCapabilities]:
|
||||
return self._capabilities
|
||||
@@ -604,6 +626,8 @@ class ThreeDPanel(QFrame):
|
||||
self.housing_check,
|
||||
self.deform_check,
|
||||
self.gain_spin,
|
||||
self.flow_arrows_check,
|
||||
self.contact_arrow_check,
|
||||
self.scale_combo,
|
||||
self.fps_combo,
|
||||
self.reset_button,
|
||||
@@ -635,6 +659,8 @@ class ThreeDPanel(QFrame):
|
||||
index = self.field_combo.findData(str(settings.field))
|
||||
if index >= 0:
|
||||
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))
|
||||
scale = settings.color_scale
|
||||
if scale is not None:
|
||||
index = self.scale_combo.findData(str(scale.mode))
|
||||
@@ -678,6 +704,8 @@ class ThreeDPanel(QFrame):
|
||||
housing_visible=self.housing_check.isChecked(),
|
||||
max_fps=int(self.fps_combo.currentData()),
|
||||
field=self.field(),
|
||||
flow_arrows_enabled=bool(self.flow_arrows_check.isChecked()),
|
||||
contact_arrow_enabled=bool(self.contact_arrow_check.isChecked()),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ signals
|
||||
@@ -730,6 +758,8 @@ class ThreeDPanel(QFrame):
|
||||
self.field_combo.setItemText(0, t("scene3d.field_depth"))
|
||||
self.field_combo.setItemText(1, t("scene3d.field_flow"))
|
||||
self._render_field_note()
|
||||
self.flow_arrows_check.setText(t("scene3d.flow_arrows"))
|
||||
self.contact_arrow_check.setText(t("scene3d.contact_arrow"))
|
||||
self.scale_label.setText(t("scene3d.scale"))
|
||||
self.range_label.setText(t("scene3d.scale_range"))
|
||||
self.range_label.setToolTip(t("scene3d.scale_range_tip"))
|
||||
|
||||
@@ -80,6 +80,12 @@ def settings_to_prefs(settings: DisplaySettings) -> dict[str, Any]:
|
||||
"housing_visible": bool(settings.housing_visible),
|
||||
"max_fps": int(settings.max_fps),
|
||||
"field": str(settings.field),
|
||||
"flow_arrows_enabled": bool(settings.flow_arrows_enabled),
|
||||
"contact_arrow_enabled": bool(settings.contact_arrow_enabled),
|
||||
"flow_on_px": float(settings.flow_on_px),
|
||||
"flow_off_px": float(settings.flow_off_px),
|
||||
"flow_grid_spacing": int(settings.flow_grid_spacing),
|
||||
"max_arrows": int(settings.max_arrows),
|
||||
}
|
||||
if scale is not None:
|
||||
block["color_scale"] = {
|
||||
@@ -138,6 +144,16 @@ def settings_from_prefs(
|
||||
lut_id=str(saved.get("lut_id") or "sequential"),
|
||||
mode=mode, # type: ignore[arg-type]
|
||||
)
|
||||
flow_on = max(0.0, _number(raw.get("flow_on_px"), 0.4))
|
||||
flow_off = max(0.0, _number(raw.get("flow_off_px"), 0.3))
|
||||
if flow_on < flow_off:
|
||||
flow_on, flow_off = flow_off, flow_on
|
||||
spacing = int(_number(raw.get("flow_grid_spacing"), 10))
|
||||
if spacing < 1:
|
||||
spacing = 10
|
||||
max_arrows = int(_number(raw.get("max_arrows"), 400))
|
||||
if max_arrows < 1:
|
||||
max_arrows = 400
|
||||
return DisplaySettings(
|
||||
display_gain=gain,
|
||||
deform_enabled=_flag(raw.get("deform_enabled"), False),
|
||||
@@ -145,4 +161,10 @@ def settings_from_prefs(
|
||||
housing_visible=_flag(raw.get("housing_visible"), True),
|
||||
max_fps=max_fps,
|
||||
field=field,
|
||||
flow_arrows_enabled=_flag(raw.get("flow_arrows_enabled"), True),
|
||||
contact_arrow_enabled=_flag(raw.get("contact_arrow_enabled"), True),
|
||||
flow_on_px=flow_on,
|
||||
flow_off_px=flow_off,
|
||||
flow_grid_spacing=spacing,
|
||||
max_arrows=max_arrows,
|
||||
)
|
||||
|
||||
@@ -415,8 +415,10 @@ class Scene3DView(QWidget):
|
||||
"""
|
||||
adapter = self._adapter
|
||||
descriptor = self._last_descriptor
|
||||
if adapter is None and descriptor is None:
|
||||
if adapter is None and descriptor is None and self._assets is None:
|
||||
return None
|
||||
mapping = getattr(self._assets, "surface_mapping", None) if self._assets else None
|
||||
overlays_ok = mapping is not None
|
||||
return SceneCapabilities(
|
||||
quantity=str(
|
||||
getattr(adapter, "quantity", "") or getattr(descriptor, "quantity", "")
|
||||
@@ -427,6 +429,8 @@ class Scene3DView(QWidget):
|
||||
),
|
||||
displacement_quality=None,
|
||||
source_kind=str(self._source_kind),
|
||||
flow_arrows_available=overlays_ok,
|
||||
contact_arrow_available=overlays_ok,
|
||||
)
|
||||
|
||||
def _publish_capabilities(self) -> None:
|
||||
|
||||
@@ -18,6 +18,7 @@ from PyQt5.QtWidgets import (
|
||||
from orisys_app_shared.acquisition.constants import FORCE_SAMPLE_RATE_HZ
|
||||
from orisys_app_shared.force import MetricHistory
|
||||
from orisys_app_shared.force.history import DEFAULT_MAX_FPS
|
||||
from orisys_app_shared.force.presentation import ForcePresentation
|
||||
from orisys_app_shared.force.source import StoreCurveSource
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.theme import color
|
||||
@@ -70,8 +71,12 @@ def _nice_ticks(lo: float, hi: float, count: int = 5) -> list[tuple[float, str]]
|
||||
return [(lo + i * step, f"{(lo + i * step):g}") for i in range(count)]
|
||||
|
||||
|
||||
def format_force(value: float, unit: str) -> str:
|
||||
def format_force(
|
||||
value: float, unit: str, *, presentation: ForcePresentation | None = None
|
||||
) -> str:
|
||||
"""Render one force reading for the value cards."""
|
||||
if presentation is not None:
|
||||
return presentation.format_force(value, unit=unit)
|
||||
if unit == "N":
|
||||
return f"{value:.3f}"
|
||||
return f"{value:.1f}"
|
||||
@@ -137,6 +142,11 @@ class ForcePlotsPanel(QFrame):
|
||||
self._last_values: tuple[float, float, float] = (0.0, 0.0, 0.0)
|
||||
#: Per axis: does ``_last_values`` hold a real value in the active unit?
|
||||
self._available: tuple[bool, bool, bool] = (True, True, True)
|
||||
#: Display-only baseline / deadband for Finger raw value cards.
|
||||
self.presentation = ForcePresentation(enabled=False)
|
||||
self._sensor_type = "box"
|
||||
self._display_values: tuple[float, float, float] = (0.0, 0.0, 0.0)
|
||||
self._display_suppressed: tuple[bool, bool, bool] = (False, False, False)
|
||||
# -- browsing state -------------------------------------------------
|
||||
#: Where the curves read from: the in-memory session store here, a
|
||||
#: recording on disk in the replay. One interface either way.
|
||||
@@ -484,7 +494,9 @@ class ForcePlotsPanel(QFrame):
|
||||
self.history.reset()
|
||||
self._clear_curves()
|
||||
self._last_values = (0.0, 0.0, 0.0)
|
||||
self._display_values = (0.0, 0.0, 0.0)
|
||||
self._available = (True, True, True)
|
||||
self.presentation.reset_session(unit=self._unit)
|
||||
self._unit = "raw"
|
||||
self._mode = "follow"
|
||||
self._window = (0.0, 0.0)
|
||||
@@ -500,6 +512,49 @@ class ForcePlotsPanel(QFrame):
|
||||
self._apply_axis_mode()
|
||||
self._refresh_mode_label()
|
||||
|
||||
def configure_presentation(
|
||||
self,
|
||||
*,
|
||||
sensor_type: str,
|
||||
unit: str,
|
||||
enabled: bool | None = None,
|
||||
) -> None:
|
||||
"""Enable Finger-raw auto baseline for value cards only."""
|
||||
self._sensor_type = str(sensor_type or "box")
|
||||
use = (
|
||||
bool(enabled)
|
||||
if enabled is not None
|
||||
else (self._sensor_type == "finger" and unit != "N")
|
||||
)
|
||||
self.presentation.enabled = use
|
||||
self.presentation.reset_session(unit=unit)
|
||||
|
||||
def request_presentation_reestimate(self) -> None:
|
||||
"""Restart the idle baseline window (display cards only)."""
|
||||
self.presentation.request_reestimate()
|
||||
self._display_values = self._last_values
|
||||
self._render_value_cards()
|
||||
|
||||
def observe_for_baseline(
|
||||
self,
|
||||
values: tuple[float, float, float],
|
||||
*,
|
||||
t_rel_s: float,
|
||||
valid: bool = True,
|
||||
flow_p95: float | None = None,
|
||||
contact_found: bool = False,
|
||||
) -> None:
|
||||
"""Feed the startup estimator; curves keep the raw measurement."""
|
||||
if not self.presentation.enabled:
|
||||
return
|
||||
self.presentation.observe(
|
||||
values,
|
||||
t_rel_s=t_rel_s,
|
||||
valid=valid,
|
||||
flow_p95=flow_p95,
|
||||
contact_found=contact_found,
|
||||
)
|
||||
|
||||
def push(
|
||||
self,
|
||||
t_rel_s: float,
|
||||
@@ -509,6 +564,9 @@ class ForcePlotsPanel(QFrame):
|
||||
*,
|
||||
unit: str = "raw",
|
||||
available: tuple[bool, bool, bool] | None = None,
|
||||
flow_p95: float | None = None,
|
||||
contact_found: bool = False,
|
||||
forces_valid: bool = True,
|
||||
) -> None:
|
||||
"""Append one live sample.
|
||||
|
||||
@@ -532,6 +590,18 @@ class ForcePlotsPanel(QFrame):
|
||||
self._last_values = (float(fn), float(fx), float(fy))
|
||||
if available is not None:
|
||||
self._available = tuple(bool(flag) for flag in available)
|
||||
self.observe_for_baseline(
|
||||
self._last_values,
|
||||
t_rel_s=float(t_rel_s),
|
||||
valid=bool(forces_valid),
|
||||
flow_p95=flow_p95,
|
||||
contact_found=contact_found,
|
||||
)
|
||||
displayed = self.presentation.accept(
|
||||
self._last_values, unit=unit, available=self._available
|
||||
)
|
||||
self._display_values = displayed.values
|
||||
self._display_suppressed = displayed.suppressed
|
||||
if unit != self._unit:
|
||||
# A unit change mid-stream is a *switch*, never a reinterpretation:
|
||||
# the buffered values were measured in the old unit and must not be
|
||||
@@ -1182,6 +1252,8 @@ class ForcePlotsPanel(QFrame):
|
||||
peak = float(selecting.peaks[index])
|
||||
available = bool(np.isfinite(peak))
|
||||
value = peak
|
||||
elif self.presentation.enabled and self.presentation.ready:
|
||||
value = self._display_values[index]
|
||||
if not available:
|
||||
# An axis with no curve in this unit: a dash, never a raw count
|
||||
# presented as a newton value.
|
||||
@@ -1192,14 +1264,30 @@ class ForcePlotsPanel(QFrame):
|
||||
t("chart.value_unavailable_tip", channel=CHANNEL_LABELS[index])
|
||||
)
|
||||
continue
|
||||
value_label.setText(fmt.format(value=value))
|
||||
value_label.setToolTip(
|
||||
t("chart.selection_peak_tip") if selecting is not None else ""
|
||||
value_label.setText(
|
||||
format_force(value, self._unit, presentation=self.presentation)
|
||||
if selecting is None
|
||||
else fmt.format(value=value)
|
||||
)
|
||||
tip = t("chart.selection_peak_tip") if selecting is not None else ""
|
||||
zero_zone = (
|
||||
selecting is None
|
||||
and self.presentation.enabled
|
||||
and self.presentation.ready
|
||||
and abs(float(value)) == 0.0
|
||||
and index < len(self._display_suppressed)
|
||||
and self._display_suppressed[index]
|
||||
)
|
||||
if zero_zone:
|
||||
tip = t("chart.zero_zone_tip")
|
||||
value_label.setToolTip(tip)
|
||||
unit_label.setText(unit_display(self._unit))
|
||||
detail_label.setText(
|
||||
self._selection_detail(index) if selecting is not None else ""
|
||||
)
|
||||
if selecting is not None:
|
||||
detail_label.setText(self._selection_detail(index))
|
||||
elif zero_zone:
|
||||
detail_label.setText(t("chart.zero_zone"))
|
||||
else:
|
||||
detail_label.setText("")
|
||||
self.unit_badge.setText(unit_display(self._unit))
|
||||
|
||||
def _selection_detail(self, index: int) -> str:
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Composer overlays and 2D/3D sample-point parity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.acquisition.arrows import _policy_from_style
|
||||
from orisys_app_shared.scene3d.composer import SceneComposer
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
DisplaySettings,
|
||||
FieldDescriptor,
|
||||
SurfaceFieldFrame,
|
||||
freeze,
|
||||
)
|
||||
from orisys_app_shared.scene3d.synthetic import poc_identity, poc_scene
|
||||
from orisys_app_shared.scene3d.visual_policy import VisualPolicy, select_flow_samples
|
||||
|
||||
|
||||
def _depth_frame(assets, *, flow=None, contact_xy=None, fn=0.0):
|
||||
h, w = assets.shape_hw
|
||||
scalar = freeze(np.zeros((h, w), dtype=np.float32), dtype=np.float32)
|
||||
valid = freeze(np.ones((h, w), dtype=bool), dtype=np.bool_)
|
||||
flow_uv = None if flow is None else freeze(np.asarray(flow, dtype=np.float32), dtype=np.float32)
|
||||
flow_mask = None if flow is None else freeze(np.asarray(valid, dtype=bool).copy(), dtype=np.bool_)
|
||||
found = contact_xy is not None
|
||||
return SurfaceFieldFrame(
|
||||
identity=poc_identity(),
|
||||
geometry_id=assets.geometry_id,
|
||||
field_domain_id=assets.field_domain_id,
|
||||
scalar=scalar,
|
||||
valid=valid,
|
||||
scalar_descriptor=FieldDescriptor(
|
||||
"depth_response", "1", "approximate", False
|
||||
),
|
||||
computed_frame_id=0,
|
||||
flow_uv=flow_uv,
|
||||
flow_valid_mask=flow_mask,
|
||||
contact_uv_xy=None if contact_xy is None else tuple(contact_xy),
|
||||
contact_found=found,
|
||||
contact_valid=found,
|
||||
contact_fn=float(fn),
|
||||
)
|
||||
|
||||
|
||||
def test_composer_builds_overlays_when_mapping_present() -> None:
|
||||
assets = poc_scene()
|
||||
h, w = assets.shape_hw
|
||||
flow = np.zeros((h, w, 2), dtype=np.float32)
|
||||
flow[1, 1] = (0.9, 0.0)
|
||||
frame = _depth_frame(assets, flow=flow, contact_xy=(1.0, 1.0), fn=800.0)
|
||||
composed = SceneComposer().compose(
|
||||
assets,
|
||||
frame,
|
||||
DisplaySettings(
|
||||
flow_arrows_enabled=True,
|
||||
contact_arrow_enabled=True,
|
||||
flow_grid_spacing=1,
|
||||
flow_on_px=0.4,
|
||||
flow_off_px=0.3,
|
||||
max_arrows=50,
|
||||
),
|
||||
)
|
||||
assert composed.flow_arrows is not None
|
||||
assert composed.flow_arrows.origins.shape[0] >= 1
|
||||
assert composed.contact_arrow is not None
|
||||
assert composed.contact_arrow.origins.shape[0] == 1
|
||||
|
||||
|
||||
def test_composer_hides_overlays_when_disabled() -> None:
|
||||
assets = poc_scene()
|
||||
h, w = assets.shape_hw
|
||||
flow = np.zeros((h, w, 2), dtype=np.float32)
|
||||
flow[1, 1] = (0.9, 0.0)
|
||||
frame = _depth_frame(assets, flow=flow, contact_xy=(1.0, 1.0), fn=800.0)
|
||||
composed = SceneComposer().compose(
|
||||
assets,
|
||||
frame,
|
||||
DisplaySettings(flow_arrows_enabled=False, contact_arrow_enabled=False),
|
||||
)
|
||||
assert composed.flow_arrows is None
|
||||
assert composed.contact_arrow is None
|
||||
|
||||
|
||||
def test_2d_3d_flow_sample_points_match() -> None:
|
||||
assets = poc_scene()
|
||||
h, w = assets.shape_hw
|
||||
flow = np.zeros((h, w, 2), dtype=np.float64)
|
||||
flow[1, 1] = (0.7, 0.2)
|
||||
mask = np.ones((h, w), dtype=bool)
|
||||
settings = DisplaySettings(
|
||||
flow_on_px=0.4,
|
||||
flow_off_px=0.3,
|
||||
flow_grid_spacing=1,
|
||||
max_arrows=50,
|
||||
)
|
||||
policy_3d = _policy_from_style(
|
||||
"finger",
|
||||
flow_on_px=settings.flow_on_px,
|
||||
flow_off_px=settings.flow_off_px,
|
||||
flow_grid_spacing=settings.flow_grid_spacing,
|
||||
max_arrows=settings.max_arrows,
|
||||
)
|
||||
policy_shared = VisualPolicy(
|
||||
flow_on_px=settings.flow_on_px,
|
||||
flow_off_px=settings.flow_off_px,
|
||||
flow_grid_spacing=settings.flow_grid_spacing,
|
||||
max_arrows=settings.max_arrows,
|
||||
flow_edge_margin=0,
|
||||
).normalised()
|
||||
samples_2d = select_flow_samples(flow, mask, policy_shared)
|
||||
samples_3d = select_flow_samples(flow, mask, policy_shared)
|
||||
assert list(zip(samples_2d.rows, samples_2d.cols)) == list(
|
||||
zip(samples_3d.rows, samples_3d.cols)
|
||||
)
|
||||
assert policy_3d.flow_on_px == settings.flow_on_px
|
||||
@@ -433,6 +433,12 @@ def test_the_display_settings_round_trip():
|
||||
deform_enabled=True,
|
||||
housing_visible=False,
|
||||
max_fps=60,
|
||||
flow_arrows_enabled=False,
|
||||
contact_arrow_enabled=True,
|
||||
flow_on_px=0.55,
|
||||
flow_off_px=0.35,
|
||||
flow_grid_spacing=12,
|
||||
max_arrows=250,
|
||||
color_scale=ColorScale(
|
||||
descriptor=FieldDescriptor("flow_magnitude", "px", "approximate", False),
|
||||
lower=0.25,
|
||||
@@ -446,6 +452,12 @@ def test_the_display_settings_round_trip():
|
||||
assert restored.deform_enabled is True
|
||||
assert restored.housing_visible is False
|
||||
assert restored.max_fps == 60
|
||||
assert restored.flow_arrows_enabled is False
|
||||
assert restored.contact_arrow_enabled is True
|
||||
assert restored.flow_on_px == 0.55
|
||||
assert restored.flow_off_px == 0.35
|
||||
assert restored.flow_grid_spacing == 12
|
||||
assert restored.max_arrows == 250
|
||||
assert restored.color_scale is not None
|
||||
assert (restored.color_scale.lower, restored.color_scale.upper) == (0.25, 4.5)
|
||||
assert restored.color_scale.mode == "fixed"
|
||||
|
||||
Reference in New Issue
Block a user