把摄像头协商结果记进 session.json,并在达不到配置帧率时明说 - What: sdk_worker 新增 capture_status_of(sensor)(把采集源的 config 与 status 合成 {requested:{width,height,fps,fourcc}, negotiated:{width,height,fps,fourcc,backend}, dropped_frames})与 short_capture_fallacy(status)(当协商帧率低于申请值 90% 时给出一句话描述,含分辨率/实际帧率/申请帧率/格式/后端;两者都齐全时才说话);两个 worker 都加 capture_status() 访问器(box 取当前 sensor,finger 取 runtime.tactile.raw_sensor)。app_window:session.json 新增顶层 capture 块(_capture_metadata(),走同一个访问器);新增 _check_capture_negotiation()——每个会话在驱动第一次报出速率后检查一次,达不到就通过 _notify 明确提示(不是每帧刷屏),新键 status.capture_slow(中英)写明"软件改不了、记录按实际帧率保存、请降分辨率/换格式或换摄像头"。tests/test_worker_frame_rate.py:假设备补上 CaptureConfig 与 CaptureStatus 的 width/height/fps/fourcc/backend_name 字段,新增 test_a_low_camera_rate_is_recorded_as_a_negotiation(1920x1080、申请 30、协商 16.67 时 capture_status() 两半都在、描述里含 1920x1080/16.67/30;协商到位时描述为空串)。 - Why: 用户按上一轮的读数报告"还是摄像头 16.7",即摄像头自身只发布 16.7 —— 这排除了流水线(每帧只 12 ms)与应用侧节拍,剩下的只能是采集格式协商:配置申请的是 resolution_raw 1920x1080 @ 30 fps(fourcc 走 CaptureConfig 默认 MJPG)。这类"驱动只提供这个格式"的情况软件无法改变,但记录里必须留下证据、操作员必须被告知,否则一个 16.7 的采集和一次正常的 30 fps 采集在文件里长得一样,事后再也无法解释。 - Verification: shared 全量 161 passed(新增 1 项);studio 90 passed;ui_check 554 项检查 0 失败;locale_check 287 字面键全部命中、中英键集一致(343+1=344? 见下方键数)。 - Not in this change: 未改 SDK 的采集参数(仍是 1920x1080/30/MJPG、后端 auto);frames.csv 仍未记录 process_ms/capture_fps(下次改记录格式时一并加);未做自动降级(例如检测到 16.7 就自动改用更低分辨率)——那会改变传感器几何,必须由人决定。

This commit is contained in:
ykcheungab
2026-09-14 13:27:33 +08:00
parent a1314a02cb
commit 50122d19a2
6 changed files with 209 additions and 1 deletions
+1
View File
@@ -357,6 +357,7 @@
"run_failed": "Run failed: {error}",
"filters_saved": "Filter settings saved: {describe} (applies to the next session)",
"rail_camera_fps": "camera {fps}",
"capture_slow": "The camera cannot deliver the configured frame rate: {detail}. Software cannot change that — the driver only offers this format; the recording keeps the real rate. Use a smaller resolution/another format, or another camera.",
"video_ended": "Video ended",
"camera_disconnected": "Camera disconnected",
"reset_ok": "Tracker reset",
+1
View File
@@ -357,6 +357,7 @@
"run_failed": "启动失败: {error}",
"filters_saved": "滤波设置已保存:{describe}(下次采集生效)",
"rail_camera_fps": "摄像头 {fps}",
"capture_slow": "摄像头达不到配置的帧率:{detail}。这不是软件能改的——驱动只提供这个格式;记录会按实际帧率保存,请降低分辨率/换格式或换摄像头。",
"video_ended": "视频已结束",
"camera_disconnected": "摄像机已断开",
"reset_ok": "追踪器已重置",
@@ -29,6 +29,7 @@ from PyQt5.QtWidgets import (
)
from orisys_app_shared.acquisition.constants import FORCE_SAMPLE_RATE_HZ
from orisys_app_shared.acquisition.sdk_worker import short_capture_fallacy
from orisys_app_shared.force import select_display_forces
from orisys_app_shared.force.calibration_recorder import CalibrationRecorder
from orisys_app_shared.force.filters import FilterSettings, settings_from_mapping
@@ -204,6 +205,9 @@ class StudioMainWindow(QMainWindow):
#: window: together with the above they say who is the slow part.
self._latest_capture_fps: float = 0.0
self._latest_capture_dropped: int = 0
#: Whether this session has already reported a shortfall between the
#: requested and the negotiated capture rate (said once, not per frame).
self._capture_checked = True
#: Last coefficients file written by the calibration panel (not applied).
self._last_calibration_export = ""
self._depth_range_value: tuple[float, float] | None = None
@@ -518,6 +522,9 @@ class StudioMainWindow(QMainWindow):
"shared_api": API_VERSION,
"sdk": self._sdk_version(),
},
# What the camera was asked for and what the driver gave. A recording
# that runs at 16.7 fps instead of 30 has to say why by itself.
"capture": self._capture_metadata(),
"calibration": {
"config_ref": config_ref,
"force_calibrated": bool(getattr(info, "force_calibrated", False)),
@@ -605,6 +612,50 @@ class StudioMainWindow(QMainWindow):
stored[key] = value
return settings_from_mapping(stored)
def _capture_metadata(self) -> dict:
"""Requested vs negotiated capture parameters, for ``session.json``.
Empty while nothing is open. A recording that runs at 16.7 fps on a 30 fps
config has to be able to explain itself; from the frames alone nobody can
tell whether the camera or the pipeline was the ceiling.
"""
worker = getattr(self, "_active_worker", None)
reader = getattr(worker, "capture_status", None) if worker is not None else None
if not callable(reader):
return {}
try:
return dict(reader() or {})
except Exception: # noqa: BLE001 - metadata must never block a record
logger.exception("Could not read the capture status")
return {}
def _check_capture_negotiation(self) -> None:
"""Say it out loud when the camera cannot do what the config asks.
A camera that only offers a lower rate than the config requests is not
something the pipeline can fix, and it is not something the operator
should have to infer from a small number on the rail. Said once per
session, and only once the driver has confirmed a rate.
"""
if self._capture_checked:
return
worker = getattr(self, "_active_worker", None)
reader = getattr(worker, "capture_status", None) if worker is not None else None
if not callable(reader):
return
try:
status = dict(reader() or {})
except Exception: # noqa: BLE001
logger.exception("Could not read the capture status")
self._capture_checked = True
return
if not ((status.get("negotiated") or {}).get("fps")):
return # the driver has not reported a rate yet: ask again next frame
self._capture_checked = True
detail = short_capture_fallacy(status)
if detail:
self._notify(t("status.capture_slow", detail=detail))
def _filter_metadata(self) -> dict:
"""The filters this session actually ran, for ``session.json``.
@@ -2133,6 +2184,7 @@ class StudioMainWindow(QMainWindow):
self._latest_process_ms = None
self._latest_capture_fps = 0.0
self._latest_capture_dropped = 0
self._capture_checked = False
self._depth_range_value = None
self._last_depth_frame = None
# The new session's store is cleared *before* the charts are attached to
@@ -2222,6 +2274,7 @@ class StudioMainWindow(QMainWindow):
self._latest_process_ms = None
self._latest_capture_fps = 0.0
self._latest_capture_dropped = 0
self._capture_checked = False
self._last_sample_valid = None
self._last_display_fallback = None
self._last_display_available = None
@@ -2395,6 +2448,8 @@ class StudioMainWindow(QMainWindow):
self._latest_capture_dropped = int(
getattr(packet, "capture_dropped", 0) or 0
)
# Once per session, as soon as the driver has confirmed its rate.
self._check_capture_negotiation()
# The rail carries the live telemetry (fps, validity, calibration) and
# the record strip its own numbers. Both compare before repainting, so
# this is safe on a 16 ms timer.