回放与实时统一为一套界面;修复 30 fps 设备被以 16.7 fps 消费的计时器缺陷 - What: 1) 删除 ui/replay_bar.py(进度条、录制目录选择、重算深度按钮、提示行)与回放条的全部接线:历史回放现在与实时监测同布局(分割比例同为 0,360,1080,0)、同一条状态轨、同一套曲线手势,唯一差别是来源槽——实时是摄像机,回放是「选择录制视频…」(toolbar.btn_replay_video,与文件菜单「打开视频…」同一条 _load_replay_video 路径:视频所在目录即录制目录,曲线自动配对同目录 forces.csv)。时间轴改为曲线游标本身:表头新增播放/暂停图标按钮(icons.play/pause,空格键同效)以 1× 推进(按墙钟计时,漏拍不变慢),到末尾自动停;拖动游标、框选、滚轮平移或拖总览条一律自动暂停(ForcePlotsPanel.pause_playback)。回放的降采样/跨版本提示移入来源摘要 tooltip,打开录制时另发一条 6 秒通知。修复视频文件名残留在实时视图的问题:_refresh_status_rail 只在 _view_preset == "replay" 时写视频名。2) 录制格式不变:raw_*.mp4 与 forces.csv/frames.csv/session.json 继续一起写。按「两者都要」新增 视图 → 由视频重算力值(act_recompute):默认曲线读录制的 CSV,打开后把录制视频重新跑一遍视觉管线(进 SessionStore、2D 面板交给管线绘制、只能顺序前进,播放按钮即开始/暂停,关闭即挂回 CSV),启动路径唯一(set_playing → _on_chart_play_toggled → _start_run),并让文件源的时间轴改为视频自身时间(mono_t = 帧号 × 1/fps),否则重算结果会落在与 CSV 不同的横轴上。回放 2D 面板不再提供深度视图(REPLAY_VIEW_MODES=("recorded",)),离线深度重算引擎保留但无界面入口。3) 采集只有 16.67 fps 的根因是上一轮引入的等待:Windows 默认计时器分辨率 15.6 ms,time.sleep(0.002) 实测睡 15.5 ms,重复帧等两拍即跨过 30 fps 的 33 ms 周期,30 fps 设备被以 ~16.7 fps 消费并隔帧丢弃(配置本身正确:mini/sensor.json 写 camera.fps=30;离线实测整套管线 15.2 ms/帧≈66 fps,余量充足)。新增 _high_resolution_timer() 在会话期间把分辨率提到 1 ms(结束还原),并把重复帧等待改为 min(距下一帧时间, 半个周期)、到期只 sleep(0)。 - Why: 用户评审 3 点——回放界面与实时不一致且拖拽条效果一般、不想为回放录制深度信息、想同时保留视频与 CSV 记录、以及采集帧率只有 16.67 fps(应为 30)。 - Verification: ui_check.py 562 项 0 失败(新增两视图同布局/无 bar、来源槽随视图切换且无残留、播放暂停与手动手势暂停、重算模式切换与视频源、回放提示在 tooltip);test_ui_smoke.py 73 项 0 失败(新增视图统一、传输控制、重算模式);drive_shared.py 150 项 0 失败(新增 shared/tests/test_worker_frame_rate.py::test_a_file_source_runs_on_the_video_s_own_timeline,并把 30 fps 用例改成必须落在 27–31.5 fps;同文件实测无 1 ms 计时器 21.6 fps、有则 30.0 fps,同一帧返回 3 次时 11317:39);locale_studio.py 266 键齐全、zh==en;reproduce.py 退出 0;ui_snapshot.py 三张复核图(统一后的回放、实时、选区中英)。 - Not in this change: 录制文件格式与 CSV 列序不变;离线深度重算引擎(ReplayController.request_depth / DepthRecomputer)保留并有独立测试,但界面已无入口,可下一步删除;重算模式用有损 mp4,数值与原 CSV 会有差异(这正是它要显式呈现的)。

This commit is contained in:
ykcheungab
2026-09-13 22:04:01 +08:00
parent e6a1e77a5b
commit 9b3c5fe46c
17 changed files with 896 additions and 387 deletions
+24 -12
View File
@@ -65,11 +65,22 @@ UI 线程 _on_run_tick ──> 曲线显示(仍可降帧)
帧,因此它的计数与"重复"无关,直接忽略(返回 `None`,行为不变)。
- `packet.fps` 改为**速率**:`(窗口内帧数 - 1) / (首末到达时刻之差)`,窗口 `FPS_WINDOW_S`
= 1 s。按"1 秒内到达了多少帧"数数会系统性地偏大(窗口里正好装一秒,抖动就多算一帧)。
- 这一层是"哪些帧算一帧"的唯一判据,记录、曲线、标定与 CSV 都跟着它走;它不改动
`packet.fps` 的语义之外的东西 —— **容器速率**仍然是传感器配置值(见 §6.1)。
- **等待新帧的间隔必须真的睡得着**:Windows 默认计时器分辨率 15.6 ms,`time.sleep(0.002)`
实际睡 15.6 ms;两拍就跨过 30 fps 的 33 ms 周期,于是 30 fps 的设备被以 16.7 fps 消费
(隔帧丢)。`_high_resolution_timer()` 在会话期间把分辨率提到 1 ms(结束即还原),
且重复帧的等待取 `min(距下一帧的时间, 半个周期)`,已经到期时只 `sleep(0)` 让出而不是
再睡一拍。实测:同样是 30 fps 的假设备,不带 1 ms 计时器时只能消费 21.6 fps,带上即 30 fps
(`shared/tests/test_worker_frame_rate.py::test_a_30_fps_device_is_consumed_at_30_fps`)。
- 这一层是"哪些帧算一帧"与"每秒多少帧"的唯一判据,记录、曲线、标定与 CSV 都跟着它走;
它不改动 `packet.fps` 的语义之外的东西 —— **容器速率**仍然是传感器配置值(见 §6.1)。
`shared/tests/test_worker_frame_rate.py` 用一个假 SDK 驱动真实循环验证:同一帧返回 3 次
时,packet 数 ≈ 交付帧数(去掉判据后是 11317 : 39),且 `packet.fps` 与交付速率一致。
时,packet 数 ≈ 交付帧数(去掉判据后是 11317 : 39),30 fps 的假设备必须被以 27–31.5 fps
消费,且 `packet.fps` 与交付速率一致。
**文件源的时间轴是视频自己的**:`source` 是字符串(文件)时,`mono_t = frame_id × 1/fps`
(用配置的容器速率),而不是墙钟。否则把 5 s 的录制以 60 fps 跑一遍会落在 0–2 s 的轴上,
无法与被复核的 `forces.csv` 对齐。实时相机仍用单调时钟。
### 关键文件
@@ -313,24 +324,25 @@ outputs/recordings/<UTC 时间戳>_session<NNNN>/
### 回放界面
回放**不再是一个独立窗口**:`ui/playback_dialog.py` 已删除,拆成
`replay.py::ReplayController`(读盘 + 游标 + 深度重算)与 `ui/replay_bar.py::ReplayBar`
(录制选择 + 进度条 + 单位 + 提示),入口在「文件 → 打开录制目录…/回放最近一次录制」
与品牌轨的「历史回放」。布局与关闭流程见 `docs/shell-lifecycle.md` §2。
回放**与实时是同一套界面**:`ui/playback_dialog.py` 与 `ui/replay_bar.py` 都已删除。
`replay.py::ReplayController` 只读录制并发信号,主窗口把它接到同一套面板上——曲线表头的
播放按钮就是播放头,来源槽在回放视图里换成「选择录制视频…」。布局、单位、看点与关闭流程
见 `docs/shell-lifecycle.md` §2(含「由视频重算」复核模式)。
- **一个游标驱动三处**:移动位置同时更新视频帧、深度面板与三张曲线的游标线;
并且通过 `cursor_moved` 信号同步到主界面的曲线面板。
- **一个游标驱动三处**:移动位置同时更新视频帧与三张曲线的游标线;并且通过 `cursor_moved`
信号同步到主界面的曲线面板。
- 单位可切换(raw/N),数据直接来自录制目录的 `forces.csv`。
- **回放按录制实际有的单位打开**:录制里 `FN_N` 等列全空(没有标定)时退回 raw,
否则选 N 会画出三张空图;`raw` 一定存在(那是设备测到的值)。
单位由 `ReplayController.set_unit` 一处决定,回放条、曲线与设置区下拉同时改。
- **数值卡读的是游标处的那一帧**(`ForcePlotsPanel._readout_at_cursor`):浏览历史或回放
时,三个大数字是播放头所指的样本,而不是实时采集的最后一个样本 —— 否则一张 7.5e5
原始计数的曲线旁边会写着实时的 `0.0`,看起来就像"回放根本没打开"。
实时视图仍是"最新样本就是游标",所以跟随状态下不额外读盘。
- 帧未被写入视频时(记录队列曾溢出)明确提示,而不是显示上一帧冒充当前帧。
- **深度重算在守护线程上执行**,UI 线程立即返回。实测 `_on_recompute_depth` 返回耗时
0.001 s,而 SDK 调用仍在后台跑。
- **深度不随录制保存,界面也不再提供深度视图**(用户明确不需要这条回放路径);需要复核
算法时用「由视频重算」,它把录制视频重新跑一遍管线(含深度、形变与接触)。离线深度重算
引擎(`ReplayController.request_depth` / `DepthRecomputer`)保留在 `replay.py` 里并有
独立测试,但已没有界面入口。
两个必须知晓的边界:
+47 -18
View File
@@ -54,30 +54,32 @@
---
## 2. 回放并入主窗口(含进度条)
## 2. 回放与实时是同一套界面(只换来源)
回放**不再是一个独立窗口**。`PlaybackDialog` 已删除,它被拆成两半:
「历史回放」不另开窗口,**也不另起一套控件**:它与「实时监测」是同一套布局、同一条状态轨、
同一套图表手势。**唯一的差别是左上的来源槽**——实时是摄像机,回放是一段录制视频。
`ui/replay_bar.py`(进度条 + 录制目录选择)与 `PlaybackDialog` 都已删除。
- `apps/studio/src/orisys_studio/replay.py::ReplayController(QObject)`:录制读取、游标、
深度重算(daemon 线程 + 120 ms 轮询、请求绑定 `(_record_serial, seq, frame)`、旧结果一律丢弃)、
深度色标(按录制锁存,与实时同一规则)、回放提示(降采样 / 跨版本 / 深度状态)。
离线深度重算引擎(daemon 线程 + 120 ms 轮询、请求绑定 `(_record_serial, seq, frame)`、
旧结果一律丢弃;**界面已不提供深度视图**)、深度色标、回放提示(降采样 / 跨版本)。
它不画任何东西,只发信号:`position_changed` / `frames_changed` /
`status_changed` / `recording_changed`。
- `apps/studio/src/orisys_studio/ui/replay_bar.py::ReplayBar`:录制选择、录制信息、
**可拖拽的 QSlider 进度条**、`pos / dur` 读数、单位切换、重算深度、以及第二行的提示。
主窗口把两者接起来:
- **选视频即选录制**:状态轨的「选择录制视频…」(`toolbar.btn_replay_video`)与
「文件 → 打开视频…」都走 `_load_replay_video(path)`——视频所在目录就是录制目录,
于是曲线自动配对同目录的 `forces.csv`。视频与 CSV 仍然同时记录(录制格式不变)。
- 曲线:`ForcePlotsPanel.set_recording_source(reader)` 用 `RecordingCurveSource` 直接读
录制目录(区间按需读盘 + 缓存包络),所以全程总览、拖动、缩放、框选都在同一套曲线上;
表头显示「回放录制(浏览)」,`返回最新` 在录制模式下禁用。
- **回放按录制实际有的单位打开**(`_replay_unit_for`):`FN_N` 等列全空的录制退回 raw,
否则在 newton 偏好下会画出三张空图、每张数值卡一个横杠——看起来就是"回放没加载"。
单位由 `ReplayController.set_unit` 一处决定,回放条、曲线、数值卡与设置区下拉一起改。
- 2D 面板:`ArrowPanel.set_source_kind("replay")` 把视图下拉换成「原始图像 / 深度图」
(录制里没有光流,不提供箭头),页脚显示「录制视频 → 深度重算」并隐藏放大倍率与有效区域;
深度模式显示按录制锁存的色条与范围。
- 游标是同一个:拖进度条 → 视频帧 + 曲线游标;拖曲线游标 → 回放位置。
表头的单位芯片是唯一的单位开关(`_set_display_unit` 同时告诉曲线、工具栏与控制器)。
- 2D 面板:`ArrowPanel.set_source_kind("replay")` 把视图下拉换成「原始图像」一项
(`REPLAY_VIEW_MODES`;录制里没有光流,也没有存储深度),页脚显示「录制视频(原始相机
图像)」并隐藏放大倍率与有效区域。
- **游标就是播放头**:曲线表头的播放按钮(`chart.play` / `chart.pause`,编辑器习惯:
空格键同效)以 1× 推进游标,到末尾自动停;拖动游标、框选、滚轮平移或拖总览条都会
**自动暂停**(`ForcePlotsPanel.pause_playback`)。没有独立进度条。
- **数值卡读游标处的那一帧**(`ForcePlotsPanel._readout_at_cursor`)。浏览历史或回放时,
三个大数字是播放头所指的样本;跟随实时时"最新样本就是游标",不额外读盘。旧写法在回放
时显示实时采集的最后一个样本,于是一张 7.5e5 原始计数的曲线旁边写着实时的 `0.0`。
@@ -85,12 +87,39 @@
仍然在跑,但 `_on_run_tick` / `_on_preview_tick` 不再把实时帧画进 2D 面板,
`_render_run_image` 直接返回。否则回放画面会被摄像头帧一帧一帧盖掉("回放时好像用的是
摄像头")。切回实时视图才把面板交还给实时帧。
- 离开回放视图:曲线挂回实时 store 并恢复跟随,回放条隐藏,**已打开的录制保留**。
- 文件菜单两条入口(`打开录制目录…` / `回放最近一次录制`)都只是切到回放视图并加载录制,
不再弹第二个窗口。
- **回放提示不占位置**:降采样 / 跨版本等说明放在来源摘要的 tooltip 里(状态栏只留一次性
通知,见 §2.2),打开录制时另发一条 6 秒通知。
- 离开回放视图:曲线挂回实时 store 并恢复跟随,来源槽换回摄像机,**已打开的录制保留**,
摘要里的视频名**不再出现**(这正是"视频文件名残留在实时检测里"的修法)。
- 文件菜单两条入口(`打开录制视频…` / `回放最近一次录制`)都只是切到回放视图并加载录制。
### 2.1 「由视频重算」:算法复核模式
`视图 → 由视频重算力值`(`act_recompute`)把曲线的来源从**录制的 CSV** 换成**把录制的视频
重新跑一遍视觉管线**:
| | 默认(关闭) | 打开 |
|---|---|---|
| 曲线数据 | 录制目录的 `forces.csv`(当时测得的值) | 管线在视频上重算出的样本(进 `SessionStore`) |
| 2D 面板 | 录制视频在游标处的那一帧 | 管线正在处理的帧 |
| 时间轴 | 任意定位(游标、框选、总览条、播放) | 只能顺序前进(管线是有状态的),播放按钮即开始/暂停这一遍 |
| 用途 | 看当时的记录 | 复核算法版本(mp4 有损,数值与原记录会有差异) |
打开它时会立即开始这一遍(`_on_recompute_toggled` → `force_plots.set_playing(True)` →
`_on_chart_play_toggled` → `_start_run`,只有一条启动路径,避免按钮与菜单各启动一次);
关闭时停止该遍并挂回 CSV。`_replay_owns_image()` 在这一遍运行期间为 False,2D 面板交给
管线自己画。
`apps/studio/tools/ui_snapshot.py --scene replay` 会合成一段短录制并打开它,用于人工复核
进度条与回放布局(不写业务数据,也不需要硬件)。
统一后的回放布局(不写业务数据,也不需要硬件)。
### 2.2 状态栏与来源摘要的分工
- 状态轨的来源槽按视图切换:实时 = 摄像机编号 + 搜索;回放 = 「选择录制视频…」。
`source_summary` 在回放视图里写「视频文件: xxx.mp4」,在实时视图里**只**写帧率——
视频名出现在实时视图里就是残留。
- 底部状态栏只放一次性通知(见 §1 表格与 `_NoticeBar`),回放提示与完整路径放在来源摘要的
tooltip 里。
### 曲线缩放与默认模式
+6 -1
View File
@@ -225,6 +225,11 @@ flow_ramp = 画布色, #6E4AD0, #FA02F3, #FF8A6B, #D8FF00
三个数值卡决定,而不是由一行统计文字决定。`ui_check.py` 的 `session browsing` 段断言
该小字 `width() >= sizeHint().width()`(中文与英文都不裁切)。
9. **两个视图共用一条状态轨**:来源槽是唯一的差别(实时=摄像机、回放=「选择录制视频…」),
因此轨上的文字宽度预算对两个视图是同一个。回放的视频名写在来源摘要里,**只在回放视图**
(`_refresh_status_rail` 按 `_view_preset` 决定),中英与 1280/1440 都由
`ui_check.rail_squeezed` 覆盖。
---
## 7. 三个顶层视图 = 同一窗口的三套布局
@@ -234,7 +239,7 @@ flow_ramp = 画布色, #6E4AD0, #FA02F3, #FF8A6B, #D8FF00
| 视图 | 面板 | 曲线模式 | 分割比例(3D, 2D, 曲线, 标定) |
|---|---|---|---|
| 实时监测 | 2D 图像 + 力曲线(+ 手指三维) | 跟随实时 | `0, 360, 1080, 0` |
| 历史回放 | 2D 图像 + 力曲线 | 进入全程浏览 | `0, 320, 1120, 0` |
| 历史回放 | 2D 图像 + 力曲线 | 读打开的录制(全程浏览) | `0, 360, 1080, 0`(与实时相同) |
| 标定 | 标定面板 + 力曲线 | 不变 | `0, 0, 560, 880` |
| 自由(`custom`,启动默认) | 由视图菜单决定 | 不变 | 用户在分割条上摆的位置 |