界面显示每帧耗时与摄像头自身帧率,让低帧率可归因 - What: SdkPacket 新增 capture_fps(采集源自报的发布速率,与 fps 同一窗口)与 capture_dropped(SDK 后台读取线程累计的"发布了但没人取走"的帧数);sdk_worker 的循环记录 (t0, 已发布序号) 并在每帧算出摄像头自身的发布速率,同时读 dropped_frames;新增 _capture_source()/_capture_dropped() 两个取值助手(原 _capture_sequence 里的取值逻辑抽出来复用)。app_window 在 视图▸显示帧率 打开时把这一段一起显示在轨道摘要上:16.7 FPS · 12 ms · 摄像头 30.0 (-13)——速率是我们消费的、耗时是每帧全部开销、摄像头速率是采集源自己发布的;两个速率不一致时后面的 (-N) 就是"摄像头有、我们没接住"的帧数。新增 locale 键 status.rail_camera_fps(中英)。tests/test_worker_frame_rate.py:假设备的采集源现在也像 orisys.capture.source 一样统计"发布时上一帧还没被取走"的丢帧,并支持给 get_img 加一个可配置的每帧耗时(PROCESS_COST_S);新增 test_the_camera_s_own_rate_is_reported_next_to_ours:循环跟得上时摄像头速率≈30、丢帧 0;把流水线放到 60 ms/帧后我们的速率掉到 22 以下、**摄像头速率仍报 30**、丢帧 >5。 - Why: 用户报"上方显示 16.7 fps 而不是实际 30",并且给出了关键数据:16.7 fps 但每帧只耗 12 ms。12 ms 的工作量塞进 60 ms 的周期里,说明循环大部分时间在等——而"等"有两种完全不同的原因:(a) 摄像头/驱动只发布 16.7(USB 带宽、MJPG/YUYV、格式协商),(b) 摄像头发布 30 而我们只取到一半(节拍与轮询问题)。原来的读数只有"我们消费了多少",两种情形长得一模一样,无法判断,所以这不是显示 bug 而是**读数缺一半**。SDK 的 status.last_seq 与 dropped_frames 正好提供另一半(last_seq 每发布一帧就自增,与消费者无关;dropped_frames 在发布时若上一帧仍在缓存里就 +1,即消费者的亏欠),因此把它读出来即可判定。 - Verification: shared 全量 160 passed(新增 2 项,含上面的归因测试);studio 90 passed;ui_check 554 项检查 0 失败;locale_check 287 字面键全部命中、中英键集一致。过程中自查出一个我自己引入的问题:最初把 (t0, 序号) 的记录与按时间裁剪放在**每次轮询**里,而等待期间该分支每秒会进几万次,列表推导把 GIL 抢光,假摄像头的发布速率从 30 掉到 8(test_the_same_frame_is_not_counted_twice 立刻变红)——改为"仅在该序号发生变化时记录",即为每发布帧 O(1) 而非每轮询 O(n)。 - Not in this change: 未改节拍/等待逻辑(假设备上 30 fps 与 60 ms 慢流水线两种情形的消费速率都符合预期,未发现节拍 bug);frames.csv 仍未记录 process_ms/capture_fps(下次改记录格式时一并加)。
This commit is contained in:
@@ -356,6 +356,7 @@
|
||||
"samples_dropped": "Record buffer is full; the oldest {count} samples were released (the full session is no longer complete)",
|
||||
"run_failed": "Run failed: {error}",
|
||||
"filters_saved": "Filter settings saved: {describe} (applies to the next session)",
|
||||
"rail_camera_fps": "camera {fps}",
|
||||
"video_ended": "Video ended",
|
||||
"camera_disconnected": "Camera disconnected",
|
||||
"reset_ok": "Tracker reset",
|
||||
|
||||
@@ -356,6 +356,7 @@
|
||||
"samples_dropped": "记录缓冲已达上限,最早的 {count} 个样本已被释放(全程记录不再完整)",
|
||||
"run_failed": "启动失败: {error}",
|
||||
"filters_saved": "滤波设置已保存:{describe}(下次采集生效)",
|
||||
"rail_camera_fps": "摄像头 {fps}",
|
||||
"video_ended": "视频已结束",
|
||||
"camera_disconnected": "摄像机已断开",
|
||||
"reset_ok": "追踪器已重置",
|
||||
|
||||
@@ -200,6 +200,10 @@ class StudioMainWindow(QMainWindow):
|
||||
#: What the last frame cost to acquire and process, in milliseconds — the
|
||||
#: other half of the frame-rate reading.
|
||||
self._latest_process_ms: float | None = None
|
||||
#: The capture source's own published rate and shortfall, over the same
|
||||
#: 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
|
||||
#: Last coefficients file written by the calibration panel (not applied).
|
||||
self._last_calibration_export = ""
|
||||
self._depth_range_value: tuple[float, float] | None = None
|
||||
@@ -1365,6 +1369,13 @@ class StudioMainWindow(QMainWindow):
|
||||
# cannot keep up, and only this number tells the two apart.
|
||||
if self._latest_process_ms:
|
||||
fps = f"{fps} · {self._latest_process_ms:.0f} ms"
|
||||
# ...and the camera's own rate settles it: a capture rate well above
|
||||
# what we consumed means the frames were there and we did not take
|
||||
# them (the drop counter says how many).
|
||||
if self._latest_capture_fps:
|
||||
fps = f"{fps} · {t('status.rail_camera_fps', fps=f'{self._latest_capture_fps:.1f}')}"
|
||||
if self._latest_capture_dropped:
|
||||
fps = f"{fps} (-{self._latest_capture_dropped})"
|
||||
detail = f"{detail} · {fps}" if detail else fps
|
||||
self.toolbar.set_source_summary(detail)
|
||||
# The full path and the replay's notes do not fit on a 30 px rail, so
|
||||
@@ -2120,6 +2131,8 @@ class StudioMainWindow(QMainWindow):
|
||||
self._last_sample_valid = None
|
||||
self._latest_fps = None
|
||||
self._latest_process_ms = None
|
||||
self._latest_capture_fps = 0.0
|
||||
self._latest_capture_dropped = 0
|
||||
self._depth_range_value = None
|
||||
self._last_depth_frame = None
|
||||
# The new session's store is cleared *before* the charts are attached to
|
||||
@@ -2207,6 +2220,8 @@ class StudioMainWindow(QMainWindow):
|
||||
self.arrow_panel.clear_preview(t("panels.arrow_empty"))
|
||||
self._latest_fps = None
|
||||
self._latest_process_ms = None
|
||||
self._latest_capture_fps = 0.0
|
||||
self._latest_capture_dropped = 0
|
||||
self._last_sample_valid = None
|
||||
self._last_display_fallback = None
|
||||
self._last_display_available = None
|
||||
@@ -2376,6 +2391,10 @@ class StudioMainWindow(QMainWindow):
|
||||
if is_new:
|
||||
self._latest_fps = getattr(packet, "fps", None)
|
||||
self._latest_process_ms = getattr(packet, "process_ms", None)
|
||||
self._latest_capture_fps = float(getattr(packet, "capture_fps", 0.0) or 0.0)
|
||||
self._latest_capture_dropped = int(
|
||||
getattr(packet, "capture_dropped", 0) or 0
|
||||
)
|
||||
# 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.
|
||||
|
||||
Reference in New Issue
Block a user