bcab2790bf072e83526779b82f78f0b7d2fd885d
28 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3daee6d407 | fixed display problem | ||
|
|
12781854ca | fixed commetn from songxiao | ||
|
|
ccedb9ddd9 | migrate pyqt5 to pyside6 | ||
|
|
c538dbf074 | fixed label under 3d view | ||
|
|
c2293d4d41 |
界面:三维可选「深度响应 / 光流幅值」上色(默认深度),并加一个「诊断信息」开关收拾面板脚下的文字
- What(颜色来源): `DisplaySettings` 增 `field`(`"depth"` 默认 / `"flow"`),随显示设置持久化 (`scene3d/prefs.py`:新增 `FIELD_CHOICES`/`DEFAULT_FIELD`,手改文件里的非法值修回默认)。 `FingerFrameAdapter` 按所选字段先试、失败再用另一个,并把"换了量"这件事记在 `last_substitution`(新诊断码 `field_substituted`,info 级);两个字段都拿不到时, `last_diagnostic` 取**信息量最大**的那条(域不符/过期/未声明 > 没这个字段),而不是"所选字段"的 那条空话。设置对话框新增「颜色来源」下拉,下面一行 `field_note` 直接写"这一帧画的是 X,不是所选的 Y"; 图例(`legend_label`)永远标注**实际**画出来的量,所以设定与画面不一致时两处都能看出来。 深度响应 `[1]`、光流幅值 `[px]`,两者都是 `approximate`——代码与文案都不把它们说成毫米。 - What(诊断信息开关): 视图菜单新增 `诊断信息`(可勾选、持久化 `show_diagnostics`、**默认关**, 由 `DisplayPrefsController` 持有并推给两个面板)。关掉后:2D 面板只留"画面 + 计划说明 + 深度色标", 三条解释流水线的标签(箭头按位移放大、原始图像 → 算法输出、全部帧)隐藏;3D 面板只留 色条 + 一行图例 + 来源,info 级诊断隐藏。**永不隐藏**:warning/error 级诊断(测量是否过期、 字段是否本帧的、是否与设备核对过——`geometry_unverified` 虽然是 info 也白名单保留)与 深度色标。3D 图例改成 `ShrinkableLabel`(一行、裁切 + tooltip,不再折行),色标数值从 4 位有效数字 收到 3 位("0.0334 – 0.899")。设置对话框控件宽度 88 → 108 px(不再需要迁就 240 px 面板)。 - What(取景脚本): `tools/ui_snapshot.py --scene flow3d` 现在同时给 flow 与 depth(各带 provenance), 所以「颜色来源」开关有东西可切;再加 `.repro/shot_variants.py` 一次出四张图 (深度版 / 光流版 / 诊断开 / 设置对话框)。 - What(测试): `test_scene3d_adapters.py` 增 3 项(默认深度、换量时给出 `field_substituted` 并在恢复后 清掉、depth 不在场景栅格时按 `field_domain_mismatch` 拒绝而不是缩放);`test_scene3d_view.py` 增 1 项(field 的持久化与修复,并把 `field in FIELD_CHOICES` 加进"手改文件会被修复"的参数化); `test_scene3d_pane_ui.py` 增 3 项(两种量可选 + 说明行 + 恢复的偏好会移动下拉;诊断开关只隐藏 info、 保留 warning 与"未核对";2D 脚注三标签的显隐与窗口开关一致);`test_display_prefs.py` 增 1 项 (开关推到两个面板、写偏好)。`test_ui_smoke.py` 与 `tools/ui_check.py` 里"箭头图例可见"的四条断言 改为按新规则检查(默认隐藏、开诊断后可见、关掉再隐藏)。 - Why: ①"深度"比"光流"更贴近操作员口中的"压得多深",但两者都只是近似量,所以做成开关而不是替换—— 并且换量必须说出来(否则画面会被读成所选的量)。②面板脚下的解释文字是"内部管线"而不是"画面是什么", 拍视频与日常看都只需要后者;用一个开关收起,比做一个滚动日志面板诚实(这些是每帧状态,不是事件)。 - Verification: pytest shared/tests apps/studio/tests(offscreen)658 passed / 0 failed / 15 skipped; 真 GL 组(gl_smoke + view + pane_ui)70 passed / 0 failed / 1 skipped; `tools/ui_check.py` 590 项检查 0 失败;`.repro/locale_check.py` 0 缺失(新增 7 个 key 双语齐备)。 目视验收:同一批合成数据渲染的四张图(深色主题)——深度版(默认)、光流版、诊断打开、设置对话框。 - Not in this change: 不改采集/记录/契约之外的三维渲染;不做页签容器与窄窗口回退;曲线量程仍是出厂 ±20 N。 真机待验证:手指路径上 `packet.depth` 的栅格是否就是场景的 300×280(若不是,面板会写明并回退到光流)。 |
||
|
|
fbf808d878 |
拆分 B:回放策略移入 ReplayController(app_window 2837→2499 行)
- What: 新增 apps/studio/src/orisys_studio/replay_controller.py:回放视图的**策略** (哪些入口能打开视频、transport 何时是播放何时是采集、从视频重算对面板与采集 管线做什么)从 app_window 搬进 ReplayController,共 30 个公开方法。replay.py 仍是 **引擎**(reader / 游标 / 异步深度重算),本模块是它外面的状态机。 它直接驱动的对象只有五个:toolbar 的回放控件、曲线图(时间轴)、2D 面板、三维面板的 来源标签、五个菜单项;其余依赖用 ReplayHost Protocol 写明(状态栏、轨道、视图切换、 显示单位、2D 唯一入口 _show_image_frame、采集管线 _start_run/_stop_run)——这些是 窗口级操作,所以留在窗口,并由 Protocol 把边界写清楚,而不是让控制器伸手进"上帝对象"。 - What: 播放时钟与定时器(33 ms)、_recompute_running、入口校验、菜单启用与提示、 重算开关的全部规则都随之上移;窗口侧保留过渡 shim(_load_recording / _load_replay_video / _render_replay_frame / _start_playback / _pause_playback / _on_play_shortcut / _stop_replay_activity / _replay_owns_image / _can_recompute / _play_timer / _recompute_running)供 tests、tools/ui_check.py、tools/ui_snapshot.py 继续驱动窗口。 窗口内部调用点(_apply_view_preset、_update_run_enabled、_sync_menu_checks、_on_run_toggled、 _on_stop_shortcut、_on_reset、菜单与工具栏连线)改为直接调用 self.replay_ctl.*。 删除窗口里已搬走的 _on_browse_video、_on_replay_position、_on_recording_changed、 _wire_replay、_on_recompute_toggled 等,以及随之为空的导入(time、QFileDialog、 SOURCE_REPLAY、PLAYBACK_TICK_MS)。顺带删掉一个从未接线的死方法 _on_replay_unit_toggled(与 _on_chart_unit_toggle 重复,无人调用)。 - What: 新增 apps/studio/tests/test_replay_controller.py:8 个用例,用假的 host/engine/ panel 直接测策略——播放在末尾从头开始、零长录制不播、playhead 跟墙钟并在末尾停下、 重算开关打开时 transport 启动管线而非播放、2D 面板归属、裸视频被接受/在实时视图被拒、 无标定录制回落到 raw、回放入口只在回放视图存在。 - Fix: 重算结束的 flag 由它自己的所有者清除(transport 暂停、开关关闭、stop_activity 三条路径),不再依赖窗口的 _stop_run 顺手清;效果是"重算结束后停止按钮正确变为不可用", test_ui_smoke 里那个"手动补状态"的注释随之删除。 - Why: 回放是 app_window 的第二个独立变更驱动:录制格式变一次是引擎的事,操作员流程 变一次不该动 2500 行窗口。搬完之后回放策略可单独阅读、单独测,窗口只剩装配与转发。 - Verification: pytest shared/tests apps/studio/tests(offscreen)611 passed / 0 failed / 15 skipped(原 603 + 新增 8);apps/studio/tools/ui_check.py 587 项检查 0 失败; .repro/locale_check.py 0 缺失。行为等价(搬运 + 一层转发)。 - Not in this change: C1 视图排列、C2 显示偏好、标定交互仍未拆分;docs 的行数/方法数 指标待全部切片落定后统一刷新。真机 GL 冒烟仍无法复跑(本机创建不出 GL 上下文, eglError 3005),13 例跳过、probe 用例按机器实况失败,与本次改动无关。 |
||
|
|
a35d1877d6 |
拆分 A2:会话状态与生命周期移入 SessionController(app_window 2900→2837 行)
- What: 新增 apps/studio/src/orisys_studio/session_controller.py(423 行 / 41 成员): 它拥有两个 worker 及当前 worker、相位、会话计数、每会话遥测(fps / process_ms / capture_fps / capture_dropped / 读数失败计数 / 丢帧上报语义)和运行定时器(16 ms), 并给出 reset_for_run / mark_running / mark_stopped / note_first_frame / note_connect_slow / note_sample_telemetry / check_capture_negotiation / take_dropped_message / take_session_error / consume_end_reason / detach_sinks。app_window 只剩「看到什么」: 按钮、轨道、状态栏、面板,以及 16 ms 呈现槽 _on_run_tick。 同时删除窗口里已成副本的 _check_capture_negotiation / _session_time / _current_frame_id / _report_invalid_sample / _report_dropped_samples / _on_worker_error / _on_worker_status, 并让 _on_packet_recorded、_on_calibration_applied、_set_phase 只在窗口一层转发。 - What: 窗口保留过渡 shim(_running / _active_worker / _box_worker / _finger_worker / _run_timer / _session_phase / _session_counter / _connect_started_at 等属性与 _ensure_workers / _worker_for / _session_time 等方法),因为这些名字是 apps/studio/tests 与 tools/ui_check.py 驱动窗口的既有接口;它们随后续切片逐个删除。 test_ui_smoke.py 里两处直接写窗口状态改为写 window.session.*(否则会绕开新所有者)。 - Why: app_window 是唯一 fan-in=1 的中心、也是四个独立变更驱动的交汇点。会话/采集/ 记录生命周期是其中第一个:设备与相位规则改一次,这个 2900 行文件就要跟着改一次。 状态与机制搬进控制器后,「谁在跑、跑到哪一阶段、报了什么数」只有一个所有者, 窗口只负责把这些事实画出来。 - Verification: pytest shared/tests apps/studio/tests(QT_QPA_PLATFORM=offscreen) 603 passed / 0 failed / 15 skipped(与改动前基线一致);apps/studio/tools/ui_check.py 587 项检查 0 失败;.repro/locale_check.py 325 个字面 key 0 缺失。 - Not in this change: 不改行为(纯搬运 + 一层转发)。有意的语义收敛只有一处:相位信号 现在每次 set_phase 都发出(状态芯片据此重绘,幂等),窗口不再自留一份相位判据。 B 回放控制器、C1 视图排列、C2 显示偏好、标定交互仍未拆分;docs 里的行数/方法数 指标待全部切片落定后统一刷新。 已知未接线(本次未动、也非本次引入):worker 的 on_reset_applied 从未接到 recording.note_reset,所以真机上按「清零」只会看到「指令已发出」,确认事件不会 进入记录;这是独立缺陷,另行处理。 真机 GL 冒烟本轮无法复跑:本机当前创建不出 GL 上下文(eglError 3005), test_scene3d_gl_smoke 13 例跳过、probe 用例因机器无上下文而失败,与本次改动无关。 |
||
|
|
7e415c4ae9 |
feat(studio-2d): 回放可一键重算;实时界面不再有任何回放入口
- What:
1. **回放里一键重算**:`ui/toolbar.py` 的 replay 组新增 `btn_recompute`(可选中的"由视频重算…",
信号 `recompute_clicked`、`set_recompute_enabled/checked`、tooltip 说明"2D 面板会显示处理后的图像,
录像本身只存了原始相机图像");`app_window` 把它接到 `act_recompute`(同一个开关,所以菜单、rail 与
`_on_recompute_toggled` 不会各走一套),并在 `_refresh_replay_state`/`_sync_replay_actions` 里同步
可用与选中状态。
2. **实时界面禁止回放入口**:新增 `_sync_replay_actions()`——`act_open_video`/`act_playback`/
`act_playback_latest` 只在 `_view_preset == "replay"` 时可用,否则禁用并把原因写进 tooltip
(`toolbar.replay_view_only` = "请在「历史回放」视图里打开录像");`_load_replay_video()` 与
`_open_latest_recording()` 在非 replay 预设下**直接拒绝并提示**,不再改动 `source_mode`/`video_path`
(原来打开一段没有 forces.csv 的视频会走到 `_load_replay_video` 的裸视频分支,把采集源切成文件而 2D
面板什么都没变)。rail 的回放按钮本来就在 replay 分组(`toolbar.py:476/598`),不改动;空格键原本就按
`run_role == "playback"` 收口,加了断言。
3. locale:新增 `toolbar.recompute`/`toolbar.recompute_tip`/`toolbar.replay_view_only`(中英各一套)。
- Why: 用户报的第二个 bug——「实时检测界面还是可以回放视频,建议回放会自动切到历史回放上面」,并在两个
选项里选了"实时界面禁止回放入口(反向做法)"。核对确认:`_on_browse_video`/`_load_replay_video` 在实时
预设下可用且只设 `source_mode="video"` + `video_path`,不切视图预设;随后 `_render_replay_frame()` 因
`_view_preset != "replay"` 直接返回、`_can_recompute()` 同因返回 False,于是界面停在"实时"却已经把采集源
换成了视频——按运行键就"在实时界面跑视频"。配套地,回放里"看不到处理后的图像"这一个用户期待,由
「由视频重算」给出:它现在就在回放视图的 rail 上,点一下即可,2D 面板与实时共用同一个后端(见上一提交)。
注意区分:实时视图用视频**当采集源**(`source_mode == "video"` 显示"回到摄像机")是另一项既有能力,保留
不变,只是不再由回放路径隐式设置。
- Verification:
· `test_ui_smoke.py` +1(`test_the_replay_entries_exist_only_in_the_replay_view`):实时预设下三个文件
菜单动作禁用且 tooltip 带原因、rail 无回放/重算按钮、`_load_replay_video()` 返回 False 且
`replay.has_video()`/`source_mode()`/播放定时器都不变、空格不启动回放;切到历史回放后三者可用、裸视频
仍能打开(`plan_modes() == ("recorded",)`)、rail 的重算按钮可点并同步 `act_recompute`。
· 既有用例继续通过:`test_a_bare_video_plays_even_though_it_is_not_a_recording`(先切到历史回放再打开,
播放与逐帧解码不受影响)、`test_depth_view_...`、三维与记录相关用例。
· 全量:apps/studio 237 项 0 失败 14 跳过;ui_check 587 项 0 失败;locale_check 0 缺失(zh/en 键集合
仍一致)。
- Not in this change: 没有改成"打开视频时自动切视图"(按用户选择,改为禁止 + 说明);`source_mode == "video"`
在实时视图的既有行为未动("由视频当相机"仍可用,只是入口不再是回放路径);阶段 8(界面与有效控制)与
阶段 9(限速/统计/保护包)未开始。
|
||
|
|
8e6f9658b4 |
refactor(studio-2d): 二维图像单一后端 —— 视图模式由帧内容决定,四条路径共用一个渲染入口
- What:
1. 新增 `shared/.../acquisition/image_backend.py`(纯 NumPy,无 Qt):`ImageFrame`(底图 + flow/depth/
valid_mask + source/frame_id/t_rel_s/stale)、`ImagePlan`、`plan_image()`、`render_image()`,以及模式
常量(`arrows_pure`/`arrows_stitched`/`depth`/`recorded`)与来源常量(live/recompute/replay/preview)。
`plan_image()` 分两个问题回答:**可选项**由来源决定(live/recompute = 三种派生视图;录像/摄像头预览
= 只有 `recorded`),**这一帧能不能画**由 `missing` 说明(某帧缺 flow/depth 不会悄悄改掉操作者选的
视图,而是在页脚写一行)。`render_image()` 是唯一渲染入口:内部复用 `draw_arrow_display`/
`depth_map_to_bgr`/`flow_colormap`(公共签名不变),深度图**改用主题色带**(与面板色条同一张表——
原来窗口没传 colormap,深度图走 OpenCV JET 而色条画品牌色带,图例与图片两张表)。
2. `ui/arrow_panel.py`:新增 `bgr_to_qimage()`(全应用唯一一份 BGR→QImage,`app_window._bgr_to_qimage`
成为它的别名、`replay.py` 的重复实现删除);新增 `set_plan(plan)`/`plan_modes()`——模式集来自 plan,
`view_modes()` 在无 plan 时回退旧的预设式列表(`set_source_kind` 保留"命名/文案"职责,并支持
`recompute` 状态与 `panels.recompute_pipeline`);页脚新增一行 `note_label` 说明 plan(如
"录像只保存原始相机图像…"),箭头放大/有效区注解改由"本帧是否支持箭头视图"决定;
`set_view_mode()` 与 `_populate_view_combo()` 按当前模式集校验(不再只认三个常量,否则 `recorded`
会被强行改回 `arrows_pure`)。
3. `app_window.py`:新增唯一入口 `_show_image_frame(ImageFrame)`(plan → 渲染 → 一个 QImage 给面板),
**四条生产者全部改走它**:实时/重算(`_render_run_image`,来源按 `_recompute_running` 区分)、回放
(`_render_replay_frame` 用 `replay.video_frame_bgr()`,不再自己转 QImage)、摄像头预览
(`_on_preview_tick`)。`_render_run_image` 里那套按 `mode` 分支画箭头的代码整体删除。
4. `replay.py`:新增 `video_frame_bgr()`(按光标缓存 BGR),`video_image()` 保留为兼容薄封装;删除
本模块的 `_bgr_to_qimage`。
5. locale:新增 `panels.replay_raw_note`(录像只有原始图像的说明 + 指路「由视频重算」)、
`panels.frame_missing_note`(`{fields}` 参数化)、`panels.field_flow`/`panels.field_depth`、
`panels.recompute_pipeline(_tip)`(中英各一套)。
- Why: 用户报的两个 bug 之一——「历史回放面板的 2 维图只能显示原始采集图像,没有进行任何处理」,并建议
"每一个 2D 图的后端保持一致"。核对后确认根因是架构而非漏了一步渲染:二维有三个(实际四个)互不一致的
生产者——实时/重算走 `draw_arrow_display`/`depth_map_to_bgr`,回放与预览把 BGR 直接交给面板;模式集与
注解由**视图预设**决定(`arrow_panel.set_source_kind("replay")` 才切成 `recorded`),于是同一段视频在
"回放"和"重算"下是两个成像栈,且 `_bgr_to_qimage` 有两份实现。用户已选定"统一后端 + 一键重算(不改记录
格式)",本提交做前半段;"一键重算"的入口在下一提交。
- Verification:
· 新增 `shared/tests/test_image_backend.py` 15 项:plan 的三类来源(live/recompute 三种派生视图、
replay/preview 只有 recorded)、`missing`/`note_key`/`note_fields`、模式回退、**与旧渲染逐像素一致**
(arrows_pure/arrows_stitched 对 `draw_arrow_display`、depth 对 `depth_map_to_bgr` 带主题色带)、
原始帧不做缩放/不替图、缺 depth → None、缺 flow → 底图、灰度帧只在这里加宽一次。
· `apps/studio/tests/test_ui_smoke.py` +2:同一段视频在回放路径只出 `recorded` 且有说明行、在重算/实时
路径出三种派生视图且都经过同一个 `render_image`(用 spy 断言来源与模式)、面板 `note_label` 不是裸键、
combo 项数与模式集一致;以及"全应用只有一处 BGR→QImage"的结构断言。
· 既有 `test_depth_view_reports_missing_data_and_fixes_its_scale`(缺深度必须报"本帧没有深度数据"而不是
回退显示普通形变图)继续通过——新的"缺字段说明 + 不回退替图"语义与它一致。
· 全量:apps/studio 236 项 0 失败 14 跳过;shared 331 项 0 失败 1 跳过;ui_check 587 项 0 失败;
locale_check 0 缺失。
- Not in this change: 「一键重算」按钮与"实时界面禁止回放入口"(7.2/7.3);深度图改用主题色带改变了 depth
视图的颜色(这是修正:色条一直画的就是主题色带);三维(scene3d)未动;录像格式未动(仍只存原始相机图像)。
|
||
|
|
6de6b8b703 |
feat(studio): 回放可以直接播放「一条视频」——不必先按「从视频重算」
- What: 新增 `record/playback.py::VideoOnlySource`(一个视频文件的时间轴:时长来自 `CAP_PROP_FRAME_COUNT/FPS`,按位置解码、顺序前进时只 step 不 seek、越界钳到最后一帧;没有帧计数或帧率就 拒绝而不是猜);`ReplayController` 增加 `open_video()`/`has_video()`/`is_video_only()`/`video_path()`, `duration_s`/`set_position`/`_refresh_video`/`close` 都覆盖这条路径,并在 `warnings_text()` 里说明 "这段视频没有配套记录";`app_window._load_replay_video()` 改为按"这条视频是什么"处理: 同目录有 `forces.csv` → 当记录打开(原行为),否则**当视频播放**(曲线为空、明确提示),只有连解码都不行 才拒绝并给出 `从视频重算` 的出路;transport/`_render_replay_frame`/`_replay_owns_image`/状态栏 tooltip 一律改用 `has_video()`(而查曲线的 `_apply_replay_source` 仍用 `is_loaded()`)。 - Why: 用户要求"只导入视频也能用回放按钮"。此前有录像必须走「从视频重算」才能动,因为一条裸视频被当成 "不是记录"而拒绝(更早是当成空记录"成功"打开→零长度时间轴→按回放没反应)。这两种都不对:视频就是可以看的 东西,只是没有力值;把它按视频播放、把"没有记录"如实写在标签与提示里,才既不骗人也不挡人。 - Verification: `shared/tests/test_playback.py` +3(VideoOnlySource:时长/按位置取帧/顺序前进/越界钳位/关闭后 返回空/不可解码与无时间轴被拒;顺带修回我在插入新类时弄丢的 `list_recordings` 的 `base = Path(root)`); `test_ui_smoke.py` 改 1 增 1(裸视频可播放:`is_video_only`、transport 可用、tooltip 说明曲线为空、 按下后位置推进且解出画面、停止有效;不可解码文件仍被拒并提示);ui_check 587 项 0 失败(回放段改为验证 "裸视频能播");apps/studio 与 shared 全量通过;locale_check 0 缺失。 - Not in this change: 裸视频的曲线(没有 CSV 就没有力值,这是事实而不是待办);裸视频的三维形变(要形变得走 「从视频重算」,面板标签在回放里仍然是"录像不含三维场");多段视频拼接播放。 |
||
|
|
eb1dfbce8a |
fix(studio): 回放视图加一个无条件「停止」(Esc 同效),并让「文件一帧都不出」直接报错而不是一直等
- What: `ToolbarWidget` 新增 `set_reset_role("reset"/"stop")`,回放视图把轨道第二个按钮变成「停止」;
`StudioMainWindow._stop_replay_activity()` 一次停掉回放(play_timer)与「从视频重算」通道(worker),
并清掉三维面板上的重算画面,状态栏说明"已停止了什么";`_on_reset`/`_on_stop_shortcut`(Esc) 按角色路由;
`_start_playback`/`_pause_playback` 结束时调用 `_update_run_enabled()`,让停止按钮在有东西可停时才可用;
两个采集 worker 新增 `FILE_FIRST_FRAME_TIMEOUT_S=10s`:文件源一帧都没出来就报错结束会话
(原先 `saw_frame` 为假时 EOF 分支永不执行,循环空转到天荒地老、窗口一直停在"连接中")。
- Why: 用户报告"视频播放完之后无法再次按回放,卡住了",并建议加手动停止。复现得到两种状态:
① 普通回放播到末尾后再按回放**是可以**重新从头播的(探针实测 0.967→0→0.371,计时器在跑),
不是这条路径的问题;② 「从视频重算」通道跑文件时,工作线程可能既不出帧也不结束
(实测:1.5 s 的合成视频跑 25 s,packets=0 且 is_running=True),于是界面永远停在运行/连接状态,
操作者看到"卡住"。所以既给一个无论什么状态都能按的停止,也让"读不出帧"变成一条明确的错误。
- Verification: 新增/更新用例:`test_worker_frame_rate.py` 的"文件一帧都不出→error+消息"(缩短时限跑,
真实 worker);`test_ui_smoke.py` 三项:回放视图的停止按钮(播放中可用、按下停回放、重算中停下重算、
无事可停时说明)、Esc 同样能停、实时视图仍是「重置」;apps/studio 全量通过;shared 全量通过;
ui_check 583 项 0 失败(新增回放停止的 5 项检查,并给"场景已加载却仍在显示消息"那条加了诊断细节,
它此前偶发失败过一次、这次跑通)。
- Not in this change: 普通回放"播到末尾→再按回放"的路径(实测正常,未改);让回放直接播放"非记录"的视频
(仍走「从视频重算」);`_recompute_running` 与 worker 真实状态的进一步合并(现在停止按钮同时看两者)。
|
||
|
|
a26f77bb67 |
fix(studio): 回放视图里选了不是记录的视频时,明确说明原因并给出可行路径
- What: `_load_replay_video()` 先用与 `list_recordings()` 相同的判据确认"这是一条记录"(视频同目录 必须有 `forces.csv`);不是记录时不再把该目录当成记录打开,而是给出原因(状态栏 + 2D 面板常驻文案)、 在 rail 上保留该视频路径,并把「从视频重算力值」置为可用;`_can_recompute()` 统一"重算有没有东西可读" (记录或已选视频),`_sync_menu_checks`/`_refresh_replay_state`/`_on_recompute_toggled` 共用它; 回放按钮在"已选视频但不是记录"时给出对应 tooltip;`_start_playback()` 遇到零长度时间轴时改为明确提示, 不再静默取消勾选。 - Why: 用户报告"历史回放面板,选择视频后按回放键没有反应"。复现确认:`RecordingReader` 容忍空目录, 所以随便一段视频所在目录都会被"成功"打开成一条空记录,`duration_s()==0`,按回放→`_start_playback` 直接静默 return——按钮点了像没点。一条记录的定义(录像 + forces.csv + session.json)此前只写在 `list_recordings` 里,回放入口没有用同一个判据。 - Verification: 复现脚本三种状态(未选/裸视频/记录内视频)行为符合预期;新增三项 UI 用例 (裸视频被拒且说明原因并能重算、记录内视频可打开且能播、缺视频段的记录给出提示); apps/studio 全量通过;ui_check 576 项 0 失败(新增 6 项覆盖同一行为);locale_check 0 缺失。 - Not in this change: 让回放直接播放"非记录"的视频(那需要一条没有力值的纯视频通道;现在用「从视频重算」 覆盖这个需求);`RecordingReader` 对空目录的容忍行为(它要允许正在写入、尚未写 session.json 的记录)。 |
||
|
|
4a482e579f | 按"硬件问题、配置不变"收尾:记录里写下实测帧率,探针加逐颗验收模式 - What: worker 把本会话实测到的采集速率记进 self._measured_capture_fps(box 与 finger 两条循环都算:finger 的帧率也从"1 秒窗口内计数"改成 (n-1)/span 的速率,两条视图对 fps 的含义必须一致),capture_status() 因此多出 measured_fps —— session.json 的 capture 块现在同时有 requested / negotiated / measured_fps / dropped_frames,一颗摄像头到底交付了多少 fps 由记录自证。status.capture_slow 的中英文案改写:删掉已被实测否证的"降低分辨率/换格式"建议,改成"这是采集端(摄像头/驱动)的问题、软件改不了,换一颗真能交付 30 fps 的(并先确认线材/端口没把它降速)再测"。tools/camera_probe.py 新增 --check 验收模式:只测 bundle 用的那一个格式(MJPG 1920x1080@30,120 帧),按**实测**速率打印一行 PASS/FAIL 并给出退出码,用于逐颗过检;无摄像头时报 "No camera answered on index N (is the app closed?)" 并返回 1。tests:假设备用上 capture_status() 的 measured_fps(0.05 s 粒度等它出现,断言 20–35 fps)、探针 --check 的无摄像头路径。 - Why: 用户判定这是驱动/硬件问题(同型号产品此前能跑 30 fps),决定**配置不变**、以后用能到 30 fps 的产品,并在之后逐颗检测。配置不变意味着应用继续按 30 fps 申请(这是对的:申请 15 只会把问题上移),但由此产生两件必须收尾的事:(1) 提示语里"降低分辨率/换格式"是错的建议——探针已经证明 720p MJPG 一样是 15 fps、YUYV 只有 1 fps;(2) 既然要逐颗检测,就必须有一个能回答"这一颗交付了多少"的权威数字,而驱动自报值不可信(DSHOW 回读),所以把实测值写进记录并给探针一个单行验收模式。 - Verification: shared 全量 161 passed;studio 91 passed;ui_check 554 项检查 0 失败;locale_check 中英键集一致;探针 --check 在无摄像头时打印指定文案并返回 1,--help 列出该选项。 - Not in this change: 未改任何采集配置(resolution_raw/fps/fourcc 都不动);未做自动降级;frames.csv 仍未记录 process_ms/capture_fps(留到加未滤波 17 列、记录格式升版时一起)。 | ||
|
|
3db55b2077 | 新增摄像头能力探针 tools/camera_probe.py,用来把 16.7 fps 归因到格式协商 - What: 新脚本对每个摄像头索引 × 两种后端(DSHOW/MSMF)× 五组请求(MJPG 1920x1080@30、MJPG 1920x1080@auto、MJPG 1280x720@30、YUYV 1920x1080@30、YUYV 1280x720@30)实际打开并连续抓 60 帧,逐行打印"申请 → 驱动协商到的 fourcc/尺寸/帧率 + 实测帧率",这样\"协商到 30\"与\"真的给到 30\"是两个分开的事实;没有摄像头时给出明确说明(含"要关掉 app,两个进程不能同时占用同一个摄像头")并返回 1;--help/--index/--max-index 可用。tests/test_ui_smoke.py 增加 test_the_camera_probe_is_usable_without_a_camera:无摄像头时返回 1 且输出解释、fourcc 编解码往返正确('MJPG' → 'MJPG'、0 → '-')。 - Why: 用户按上一轮读数确认"还是摄像头 16.7",即摄像头自身只发布 16.7。配置申请的是 resolution_raw=1920x1080、fps=30、fourcc 走 CaptureConfig 默认 MJPG、backend=auto;SDK 已在 open 里做了 width/height → fourcc → fps → fourcc 再设一次 + warmup 后校验并在 fourcc 不符时重试一次,但仍存在两种可能:这颗摄像头在 1080p 下就只有 16.67(UVC 常见),或者它在 MJPG 之外落到了别的格式/后端。这两种的处置完全不同(前者要改分辨率/换摄像头,而 1920x1080 是 5x3 瓦片网格的结构参数,改它要重做拼接标定;后者是申请顺序/格式的问题,可以在 SDK 侧修),而在应用里看不出来。因此需要一个不经过应用的独立探针,把驱动真正提供的东西列出来。 - Verification: 本机(无摄像头)运行 --index 9 → 输出 "No camera answered on indices 9..9." 与关闭 app 的提示、退出码 1;--help 正常;_decode(_fourcc_code('MJPG')) == 'MJPG'。studio 全量 91 passed(新增 1 项);ui_check 554 项检查 0 失败。 - Not in this change: 未改 SDK 的采集申请顺序或后端选择(等探针结果:若显示 MJPG+1080p 下驱动只给 16.67,则是硬件边界;若显示申请 MJPG 却协商成 YUYV,再改 SDK);未做分辨率自动降级。 | ||
|
|
5ef4d955fb | 帧率读数改为"采集时刻"的到达率,并在旁边显示每帧耗时 - What: sdk_worker 的采集循环里把帧率与时间戳的取样点从"算完之后"(t1) 改成"取到这一帧的时刻"(t0)——frame_times、SIGPkt.mono_t(实时分支)与 last_frame_mono(断线判定)都用 t0,所以 16.7 FPS 现在明确表示"帧是以这个速率到达的",而不是把流水线耗时当成帧率;process_ms 的终点从 process_chain 之后改到 read_forces_from_sensor 之后(t_done),因此它终于涵盖这一帧的**全部**开销(此前漏掉力值读取);last_frame_at 仍取 t1(compute chain 结束)作为节拍相位参考,行为不变(帧率测试仍然通过)。app_window 在 视图▸显示帧率 打开时把每帧耗时一起显示在轨道摘要里:"16.7 FPS · 58 ms",并新增 _latest_process_ms(随会话/回放清空)。tests/test_ui_smoke.py 增加一项断言:轨道同时给出速率与耗时,且没有耗时数据时仍只显示速率。 - Why: 用户看到 Studio 上方显示 16.67 fps 而不是 30,问是不是显示错了。查证结论:这个数字来自 packet.fps,是采集循环真实的"到达率",记录里的行数也是它——不是界面 bug;但它有两个缺陷让这个数字**无法单独判读**:(1) 取样点在算完之后,于是它把本机流水线耗时混进了"设备帧率";(2) 旁边没有每帧耗时,而"16.7 FPS"既可能是摄像头只给 16.7,也可能是流水线跟不上 30(实测:每帧 ≤30 ms 时循环仍能跑满 29.3 fps;40/50/60/70 ms 分别掉到 24.7/19.8/16.5/14.1 fps——16.67 恰好对应每帧约 60 ms)。本机 app 配置(365x365 拼接、dis_mode=3)在 480p 素材上 process_chain+力值读取只要 ~10 ms(上限约 100 fps),因此本次报的 16.67 不可能是这台机器上这个配置的流水线瓶颈;两种可能必须靠耗时数据区分。 - Verification: pytest shared/tests/test_worker_frame_rate.py 4 项全过(30 fps 设备仍被消费在 29.3 fps);studio 全量 90 passed(新增 1 项);ui_check 554 项检查 0 失败;真实 SDK 复测:文件源 55.4 fps 到达率、每帧 18.2 ms(含力值读取)。 - Not in this change: frames.csv 仍未记录 process_ms(下次改记录格式、加未滤波 17 列时一并加,避免连做两次版本升级);未动节拍逻辑本身。 | ||
|
|
92e0b95ca2 | 曲线图表头加齿轮,绘图设置对话框里可以直接调滤波 - What: ui/icons.py 新增 settings(齿轮)图标(八个齿 + 圆环,颜色取自主题 token,与另外三个图标同一套画法);force_plots.py 表头新增 btn_settings 与 plot_settings_requested 信号(放在缩放按钮之后、单位芯片之前),换主题时一并重绘图标,retranslate 里补 tooltip/可访问名;plot_settings_dialog.py 增加"滤波"分区:预设下拉(off/low_latency/balanced/low_noise + 自定义,显示的就是写进 sensor.json 的那个名字)、光流 EMA α(0.05–1.00,1.00 = 关)、中值窗口(1–31,1 = 关)、低通截止(0–60 Hz,0 = 关)、低通阶数(1–6)、采样率("自动跟随帧率"复选框 + 固定值)、"同时显示未滤波曲线";选预设即写入该预设的数值,手改任一数值自动切到"自定义",选中的预设名会随 filter_values() 一起带出(preset 字段),"_reset_defaults" 回到"什么都不滤";对话框底部按当前数值给提示:光流 EMA 打开时提示"这是测量设置、两次录制不可比"、全关时提示"当前不滤波"、录制中显示"录制中不能更改滤波"并禁用整个分区;截止频率达到或超过奈奎斯特(帧率一半)时用 dialog.filter_nyquist 明确说出"设备不会滤",其中 cutoff/nyquist/rate 三个数字由 FilterSettings.nyquist_issue() 提供(problems() 仍返回英文串供日志与测试用,判断条件只有一处)。filters.py 新增 FALLBACK_PRESETS 与 available_presets()(SDK 能导入就用 SDK 的 FILTER_PRESETS,导入不了才用这份文档副本,测试保证两者逐值一致)。app_window.py:把 plot_settings_requested 接到 _on_plot_settings,打开对话框时传入当前 _filter_settings()、filters_locked(正在跑或正在录)、以及 _device_sample_rate()(实测帧率优先,否则 30 fps,用于奈奎斯特检查);接受后把 filter_values().as_dict() 逐键写成 plot_* 偏好(与 _filter_settings() 读回的是同一组键),未在采集时立刻 set_filter_settings() 交给两个 worker,并在状态栏回报"已保存:{describe}(下次采集生效)"。两个 locale 文件补齐 21+3 个新键,中英各 343 键一一对应。tests/test_ui_smoke.py 新增 4 项:齿轮确实触发设置信号、预设/自定义切换与数值一致、录制中分区被禁用且给出理由、对话框写的键与 _filter_settings 读回的键是同一组。 - Why: 用户要求"建议曲线图增加一个齿轮的配置按键,可以调节曲线图的上限和滤波等"。此前滤波只能靠手改 preferences.json 的 plot_* 键(上一轮留下的口子),而 app 默认已改成"什么都不滤"——没有这个入口,等于把默认变成了唯一选择。 - Verification: studio 全量 89 passed(原 85 + 新增 4);shared 全量 158 passed;ui_check 554 项检查 0 失败;.repro/locale_check.py 扫描 64 个源文件里 287 个 t("…") 字面键,两个 locale 全部命中且键集合完全相同(343/343);离屏实测对话框:预设 balanced→flowEMA(0.5)+median(3)、改中值→自定义、选 low_noise→带低通、30 Hz 采样率下填 30 Hz 截止→弹出奈奎斯特警告、恢复默认→off/不滤波。 - Not in this change: CSV 的 17 列与未滤波第二 buffer(SessionStore/记录格式版本)、回放界面显示滤波前曲线、标定面板显示 filters 块、发布说明里"默认不再滤波"的措辞。 | ||
|
|
da8165cf03 | 删除深度重算引擎;修好回放视频每帧重开文件导致的无法播放,运输控制改到状态轨主按钮 - What: 1) 彻底删除深度重算引擎:shared/.../record/depth.py(DepthRecomputer/DepthResult)与 depth_backends.py(BoxReplay/FingerReplay)、其专属测试 test_depth_replay.py,record/__init__ 不再导出,playback.py::replay_warnings 不再报 depth_recomputed;apps/studio/replay.py 由 515 行瘦身为只做读盘+游标+视频帧+提示,回放视图的 2D 面板只提供「原始图像」;session.json 的 depth.replay 由 recompute 改为 none;删除 31 个只剩深度使用的文案键(depth_*/warn_state_*/warn_reset_*),warn_config_changed 与 warn_sdk_changed 改为描述"由视频重算";ui_check 深度段、studio 两个深度用例、test_playback 三例、drive_shared 的 test_depth_replay 与 reproduce.py 的深度段一并清理(shared 套件 150→134 项)。2) 无法播放的根因是 RecordingReader.video_frame_at 每帧都 cv2.VideoCapture 重开并 seek:小片段已 ~10 ms/帧,1080p H.264 每次 seek 需从关键帧解码(上百 ms),而播放每 33 ms 要一帧且全在 UI 线程上,视频因此看着不动;改为按分段缓存解码器、顺序帧用 read() 步进(实测 10 ms → 0.5 ms/帧)、跳转才 seek,新增 RecordingReader.close() 释放。运输控制按用户建议上移到状态轨主按钮:toolbar.set_run_role("playback") 使回放视图的主按钮变成「▶ 回放」/「⏸ 暂停」(实时视图仍是「▶ 运行」),点按以 1× 推进游标(墙钟计时,漏拍不变慢)、到末尾自动停,拖动游标/框选/滚轮/拖总览条经 ForcePlotsPanel.timeline_taken_over 自动暂停,空格键同效;曲线表头不再放第二个播放按钮;按钮样式在回放角色下不再套用"停止"的红色语义。 - Why: 用户要求先清掉深度重算引擎、并反馈"现在无法播放视频",建议把上方播放按钮在历史回放形式下改为回放。 - Verification: ui_check.py 554 项 0 失败(新增 seek<250ms/步进<60ms 的解码预算、回放按钮文字随播放/暂停切换、手动拖游标即暂停、切回实时视图按钮恢复运行、按深度视图已移除);test_ui_smoke.py 71 项 0 失败;drive_shared.py 134 项 0 失败(深度引擎用例随代码删除);locale_studio.py 262 键齐全、zh==en;reproduce.py 退出 0;.repro/probe_play.py 实测按「回放」后 0.75 s 内游标推进 0.689 s、面板 12/12 帧都在更新;两张复核图(回放视图的「▶ 回放」、实时视图的「▶ 运行」)。 - Not in this change: 录制文件格式与 CSV 列序不变(raw_*.mp4 与 forces.csv 继续一起写);「由视频重算」复核模式保留(它复用实时管线,是有状态、只能顺序前进的一遍)。 | ||
|
|
9b3c5fe46c | 回放与实时统一为一套界面;修复 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 会有差异(这正是它要显式呈现的)。 | ||
|
|
e6a1e77a5b | 采集循环只认新帧,选区统计移到数值卡,状态栏改为会过期的通知行 - What: 1) 选区统计不再挤在曲线下方(中文长句会顶出面板右边界)——框选后三个数值卡的大号数字变成该窗口的峰值、下方小字为该窗口的均值·σ(chart.selection_detail),峰值语义放在卡片 tooltip;曲线下方只保留「选区 4.00s / 121 点」(完整一行留在它的 tooltip 里)。「全程总览」不算选区(那是默认适配),此时卡片回到"游标处样本";切换单位/换数据源/返回最新都会正确切回。2) 上方的 63.0 FPS 是采集循环自己的轮询速率,根因是同一帧被反复当成新样本:CaptureSource.read_latest 返回后台线程的最新帧,没有新帧时把同一帧再给一次,循环未做判断,于是 30 fps 的设备录出约 63 行/秒(frame_id 还各不相同)。新增 _capture_sequence():读 capture_source.status.last_seq,计数未变即"还没有新帧",睡 2 ms 重试、不进处理链、不发 packet;只有带后台读取线程的实时相机有该标记,视频文件是同步解码、每次都取下一帧,因此忽略(行为不变)。packet.fps 改为速率 (窗口帧数-1)/(首末到达差),窗口 1 s——按"1 秒内到达几帧"数数会系统性偏大。容器速率仍取传感器配置值,与此层无关。3) 底部状态栏只留一次性通知:删除右下角常驻「配置 sensor.json」(顶栏摘要按钮已写明),删除与顶栏重复的 status.run_started(运行中(0))与 status.connecting(正在连接…);QStatusBar 换成 _NoticeBar——Qt 的 showMessage(text) 默认 msecs=0 表示永不消失,这正是那句「运行中(0)」一直挂到底部的原因,子类统一补 6 秒超时;status.units_raw_fallback 之前每帧都发(等于永久占住通知行),改为每次运行只提示一次。附带修复:英文下表头模式名被截断(长句移入 tooltip,标签只留模式名 chart.mode_*_tip);数据源切换后曲线下方的选区文案会残留,改为在 _on_source_changed 里清零。 - Why: 用户评审 3 点——选区统计显示不全、顶部 fps 应受硬件限制最大 30、右下角配置文件名与左下角「运行中(0)」和顶栏信息重复。第 2 点查出根因是重复采样(数据缺陷,不只是显示问题),修掉后设备速率与记录行数才真正一致。 - Verification: ui_check.py 546 项 0 失败(新增:选区统计在卡片、卡片小字不裁切、状态栏无常驻标签且消息过期、顶栏 FPS 即 packet 交付速率、表头模式名中英不裁切);test_ui_smoke.py 71 项 0 失败(新增选区读数与通知行过期两例);drive_shared.py 148 项 0 失败(新增 shared/tests/test_worker_frame_rate.py:同一帧返回 3 次时 packet 数≈交付帧数,去掉判据为 11317:39,且 fps 与交付速率一致);locale_studio.py 267 键齐全、zh==en、prefs 对齐;reproduce.py 退出 0;ui_snapshot.py 新增 --select A,B,六张复核图(选区中英、实时、录制中、回放、标定 1280x720)。 - Not in this change: 录制文件格式与 CSV 列序不变;去重只影响"哪些帧算一帧",视频文件源行为不变;曲线下方不再显示三通道峰/均/σ(数值在卡片上,完整一行在 tooltip 里)。 | ||
|
|
824544fc7a | 回放并入主窗口,并修掉开场尖峰 / 录制时长 / 录制帧率 / 回放被摄像头覆盖等 9 项评审问题 - What: 回放不再是独立窗口——删除 PlaybackDialog,拆成 replay.py::ReplayController(读盘、游标、深度重算,只发信号)与 ui/replay_bar.py(录制选择、可拖拽进度条、单位、重算深度、提示行),品牌轨「历史回放」直接换主窗口布局,文件菜单两条入口都切到该视图,曲线改读 RecordingCurveSource(区间按需读盘 + 缓存包络),2D 面板切「原始图像 / 深度图」;传感器与标定合并到 SensorDialog(类型/配置下拉 + 当前生效标定 + 最近导出系数),标定面板加纵向滚动区、九按钮两行、六列表头 ResizeToContents。评审 9 项:1) 开场尖峰两个根因——push() 遇单位变化只记 _unit 而保留旧单位样本(raw 计数被画在牛顿轴上),改为「切换即换单位」:清缓冲按新单位重建、触发切换的那一帧仍刷数值卡;_start_run 先 session_store.reset() 再 set_store(),新会话不再带上次的尾巴(复现脚本 .repro/probe_item1.py:旧 39 样本峰值 9.841 → 新 37 样本峰值 2.390)。2) 箭头页脚改为「箭头按位移放大 ×{factor}」。3) 表头单位芯片由只读 QLabel 改为可点按钮,一次点击同时改曲线、数值卡与设置区下拉。4-6) 全程总览/框选/返回最新 改为 ui/icons.py 按主题令牌运行时绘制的图标,tooltip 与 accessibleName 保留文字,全程总览区域名仍是文字。7) stats() 在录制进行中即返回 _last_t - _t0,记录条开始计时(此前只在 _finalise 写、整场停在 00:00.0)。8) 容器 fps 改用传感器配置的速率(packet.fps 是"每秒完成多少帧"的计数器,突发/文件源会读到 48),视频按 1/fps 节流并把超前的帧计入 resampled_video(frames.csv/forces.csv 一行不少,报告写明原因),session.json 同时记 fps 与 measured_fps。9) _replay_owns_image() 让回放视图独占 2D 面板,实时帧不再覆盖;push() 在录制源挂载时连单位都不切,曲线/数值卡/单位一并归录制。附带:数值卡改读游标处样本(_readout_at_cursor)、回放按录制实际有的单位打开(_replay_unit_for + ReplayController.set_unit,避免 raw-only 录制在 newton 偏好下画三张空图)、视频速率下限。 - Why: 用户评审 9 点,其中 1/7/8/9 是代码缺陷(开场尖峰、录制时长不更新、录制 48fps 与实际 30fps 不符、回放时 2D 面板仍被摄像头刷新),2-6 是可读性问题(×2 含义不明、单位与视图控制应可点/应符号化)。 - Verification: ui_check.py 524 项 0 失败(新增 chart header and unit chip / acquisition correctness / replay owns the 2D pane 三段)、test_ui_smoke.py 69 项 0 失败(新增 7 个回归用例:单位切换不混单位、单位芯片、图标语义、录制中时长、传感器帧率+节流、回放独占面板、回放游标读数与单位回退)、drive_shared.py 146 项 0 失败、locale_studio.py(268 键齐全、zh==en、prefs 对齐)、reproduce.py、ui_snapshot.py 四张复核图(深色实时/回放、浅色 1280x720 实时与标定)。 - Not in this change: 录制文件格式(forces.csv 列序、frames.csv、session.json 路径与分段规则)不变,节流只影响"哪些帧进视频",并显式计数上报;三维面板仍是占位符。 | ||
|
|
35e86ec1f7 |
界面改用「精密仪器工作台」布局:顶栏压缩、视图切布局、标定与回放面板不再裁切
- What: 品牌条 40→28px 并加 实时监测/历史回放/标定 导航;导航改为切换主界面三套布局(实时=2D图像+曲线、回放=自动全程浏览、标定=标定面板 880px+曲线,2D 图像整体缩小);传感器选择(传感器 / 传感器配置 两个带 tooltip 的下拉 + 产品徽标)移到状态轨最左侧,记录模式移入设置区;新增 RecordStatusChip(记录中时长、丢帧、写入异常)与「同一时刻只有一个实心按钮」(新增 dangerOutline 描边危险样式);三轴读数移到曲线上方(16pt 等宽)、只保留最下方时间轴、网格淡化、全程浏览条可拖动;三轴配色统一为蓝/橙/绿并把语义色对比度审计扩展到 toolbar_bg;箭头页脚显示放大倍率与有效区域,深度模式才显示色条与范围;左侧面板改名「2D 图像」且固定显示图像宽高比;标定面板加纵向滚动区、九按钮分两行、六列表头 ResizeToContents;PlaybackDialog 补上从未调用的 retranslate;main() 在事件循环结束后 flush 日志并 os._exit,避免设备 SDK 原生线程把进程挂在解释器收尾。 - Why: 评审提出的 7 点:顶部占高与装饰过多、记录状态不突出、配置切换看不懂且藏得太深、切视图应换主界面布局、标定与回放面板文字被裁、关窗后命令行不退出、2D 图像无固定宽高比且名字不符。 - Verification: ui_check.py(430 项,新增 top area / instrument panels / clean shutdown 段,含中英×1280/1440 的轨不挤压检查)、test_ui_smoke.py(57 项,新增视图切换、标定不裁切、2D 宽高比、回放按钮文字、传感器选择器等用例)、drive_shared.py(146 项)、locale_studio.py(261 键齐全、zh==en、prefs 对齐)、reproduce.py、ui_snapshot.py 四张复核图。 - Not in this change: 录制文件格式、CSV/MP4 写入路径与记录通道数据流不变;三维面板仍是占位符;回放对话框(含离线深度重算)本身只修了按钮文字。 |
||
|
|
73592f085f |
Browse a long recording from disk instead of loading it into memory
What
- record/index.py: RecordingIndex streams forces.csv once and keeps only the
small things — one time, one validity flag, one frame id per row plus a byte
offset every 4096 rows (~6.5 MB per 500k rows). row_range() bisects the times,
read_rows() seeks to the nearest checkpoint and scans forward, and overview()
folds every row into its bucket during the same pass to build a per-axis
min/max envelope.
- read_rows(max_points, unit) decimates **while streaming**: each bucket keeps
the extreme row of every axis in the requested unit, so a long interval never
has to exist in memory in full and a shear spike survives a flat normal channel.
- RecordingReader: info() decimates above max_rows (default 200k) and marks
itself decimated, with total_rows and a duration taken from the frame index
rather than from the view; window()/window_all()/overview()/statistics() read
the interval being asked about. list/window semantics unchanged for small
recordings.
- force/source.py: StoreCurveSource (live memory) and RecordingCurveSource (disk
interval + cached envelope) implement one interface — extent, window,
envelope, statistics, duration_s — so ForcePlotsPanel asks a *source* instead
of knowing where the data lives.
- force_plots: reads through the source (window, envelope, statistics, extent),
which also replaces its last direct store accesses.
- playback_dialog: draws the whole-session view through window_all() in the unit
being shown (bounded by CHART_MAX_POINTS), and says so when the view is
decimated ("显示 N / 共 M 行") instead of letting the operator count points.
- experiment._run: a failure that lands while the files are opening can no longer
be overwritten by the writer's own transition to "recording" (found by a flaky
test; the state check now covers STATE_FAILED).
Why
The store releases old blocks above ~4.6 h and the replay loaded every CSV row
into Python lists before drawing anything, so neither the live browser nor the
replay could show an earlier interval of a long recording. The index makes the
disk the data source and memory a cache of what is on screen.
Verification
- shared/tests/test_history_source.py (new, 8 cases) over a 12 000-row recording:
the index reports its rows and reads a bounded interval; a large recording is
marked decimated with its true duration and total rows; a small one is not; an
interval read sees the exact rows including a single peak; a decimated window
keeps the peak; the envelope fits the whole session in ≤200 points, keeps the
spike and is cached; the disk and memory sources agree on extent, values and
statistics; the whole session can be read without holding it all. Shared
suites: 146 tests, 0 failures (three consecutive runs).
- apps/studio/tools/ui_check.py: the replay section now checks the index row
count, that an interval read spans only the interval, the envelope size and
total_rows. 324 checks, 0 failures.
- apps/studio/tests/test_ui_smoke.py: 43 tests, 36 pass; the 7 failures are the
unchanged sandbox/harness artifacts.
- D:/orisys_ws/_review_phase12/reproduce.py: all ten lines unchanged;
compileall, locale and prefs parity clean.
- apps/studio/docs/record-channel.md §8 documents the index, the reader API and
the unified source, and lists what is still hardware work.
Not in this change
- Maximum RSS / backlog / loss measurements at real resolution on the target ARM
board (needs the hardware), and range CSV export.
|
||
|
|
22cc59c402 |
Fix the phase-1/2 review findings in recording, calibration and depth replay
What - record: one packet->row conversion (record/sample.py). A failed compute chain records no force value at all, a newton value is written only for an axis that really has a calibration curve, and mono_t (not the wall clock) orders history with a per-row calibration hash. - record lifecycle: explicit starting/recording/stopping/stopped/failed states owned by the writer thread; stop() reports completed=False on timeout and does not write ended_at; consecutive write failures stop acceptance immediately. - record capacity: byte budget for the video queue (512 MiB default); a full queue sheds the *frame* and parks the tiny data row, so a video overflow no longer deletes CSV rows. Lost rows leave a dropped=1 index row and are counted as dropped_gaps / unrecorded_gaps. - calibration identity: sha256 content hash instead of a path, minted from the calibration the sensor actually loaded (reported before the first frame) and confirmed by the acquisition thread; events are timestamped when they happen. - calibration diagnostics: the fit summary, residual and validation error now come from the model that export/apply would use (piecewise_linear); validation points are held out of the build; repeated reference loads are merged with an n-weighted mean and pooled std. - calibration record: every point keeps its full three-axis sample series (frame id, time, raw, newtons, valid) plus diagnostics, written to an independent calibration_session_*.json that can be reopened for inspection; save record / export coefficients / apply to device are three separate actions. - depth replay: the cursor's frame is bound to the depth result by request sequence and frame id, so a moved cursor never shows another frame's map; the SDK is stepped from its current position instead of re-reading from the start; the live chain (box deformation+contact, finger unroll+tactile) is replayed with the exact resolved config path; algorithm state is rebuilt from a reset marker or from the recorded pre-roll, and says so when it cannot be. Why The review (D:/orisys_ws/_review_phase12/review.md) showed the app writing raw counts into the newton columns, recording a stale value after a failed compute, losing CSV rows when the video queue filled, reporting success while a writer was still running, diagnosing a model nobody applies, applying a calibration before the device confirmed it, dropping the samples behind a calibration point, showing a stale depth map under a moved cursor and recomputing depth of the wrong frame. Verification - D:/orisys_ws/_review_phase12/reproduce.py: all ten acceptance lines flipped (log saved as _review_phase12/reproduce_after.log). Including 25/25 rows kept with 8 video frames shed, frames [2]->[3], depth 8.0/8.0, fit 2/2 points. - apps/studio/tools/ui_check.py: 249 checks, 0 failures (twice). - shared suites: 91 tests, 0 failures (new: test_record_semantics, test_record_lifecycle, test_calibration_model, test_calibration_record, test_depth_replay). - apps/studio/tests/test_ui_smoke.py: 23/30; the 7 remaining are harness artifacts of the sandbox (4 tmp_path PermissionError, 3 parametrized tests the shim cannot drive), unchanged from before this change. - compileall clean; locale parity zh/en + t() key coverage: 0 failures. Not in this change - P2#10 disk-based browsing of a long recording and range CSV export (next round, per your choice). - Stability-triggered calibration sampling, replay fixed colour scale, PyOpenGL three-dimensional view. - Hardware verification: no camera attached, so the SDK's real processing chain and the pre-roll's effect on a live tracker were exercised with fakes only. |
||
|
|
92da51217c |
add session browsing, calibration diagnostics and the 2D layout preset
Phase 2 of the review: browse the whole session instead of only the rolling window, diagnose a calibration from the recorded points, and provide the reviewed two-panel layout. What - SessionStore gains extent(), visible(t0, t1, max_points) and statistics(t0, t1, unit). visible() walks only the blocks that overlap the window and decimates by taking each bucket's minimum and maximum, so a spike survives decimation and browsing a long session does not copy all of it. - ForcePlotsPanel gains follow/browse modes, a whole-session overview strip with the current window, pan and wheel zoom, a rubber-band zoom toggle, a shared time cursor across all three channels, and a selection readout (duration, valid samples, per-axis peak/mean/std). - CalibrationPoint records a bounded per-axis trace, an excluded flag, a validation flag and its fit residual. CalibrationRecorder gains set_excluded/set_validation/fit; the fit is a least-squares line over the non-excluded, non-validation points with per-point residuals and an independent validation error. - The calibration table becomes the reviewed diagnostic view (reference load | FN | FX | FY | stability | state) with the residual shown next to the loaded axis, plus exclude/restore and mark-validation actions and a fit summary line. Double clicking a row opens the point's three-axis trace. - Add a layout preset: free layout or 2D mode (depth map on the left, force charts on the right, calibration folded away), persisted in preferences. Why - Browsing is a display pause, not a capture pause: the record channel keeps running while the charts show an older window. - Excluding a point removes it from the fit and from the exported calibration but keeps it on record, so the evidence for the decision is not destroyed. - The non-loaded axes are recorded but never given a reference load: their true load is unknown during a single-axis calibration, so it stays empty. Verification - shared/tests/test_record_channel.py grows to 18 cases, covering extent, peak-preserving decimation, windowed reads, inverted/empty windows, unit-aware statistics and the time origin. - tools/ui_check.py gains "session browsing" and "layout preset" sections (223 checks, 0 failures overall). - apps/studio/tests/test_ui_smoke.py covers the same invariants. Not in this change: synchronised playback of video, depth and curves. It needs a decision on how depth is stored for replay (per-frame PNG, reduced-rate PNG, or offline recomputation); the cursor plumbing (cursor_moved / set_cursor) is already in place for it. |
||
|
|
817f830565 |
record the full experiment: video, CSV and metadata
Phase 1 of the review asked for one directory per experiment, a force CSV, video of the pre-processing image, and metadata that ties them together. The record channel from the previous change already kept the whole session; this adds the on-disk format. What - Add record/experiment.py: ExperimentRecorder writes one directory per session containing session.json, forces.csv, frames.csv, events.csv, raw_NNNN.mp4 (segmented) and an optional lossless PNG sequence. - forces.csv uses the reviewed column order (session_id, frame_id, t_rel_s, FN_raw, FX_raw, FY_raw, FN_N, FX_N, FY_N, valid, calibration_id) and leaves a cell empty when a value is unavailable: an uncalibrated newton value or a failed readout is never written as 0. - frames.csv is the frame-index mapping: frame_id, t_rel_s, acquisition wall clock, segment, segment_frame, in_video. forces.csv and frames.csv share the packet frame_id and the same t_rel_s origin, so video and data can be aligned after dropped or segmented frames. - session.json carries format version, session id, start/end time in UTC, device and config (with a sha256 of the config file), app/shared/SDK versions, calibration reference, axis convention, filter parameters, video codec and the lossy-MP4 caveat, plus the event log and end-of-run statistics. It is written once at start so a crash still leaves identifiable metadata. - Add a record control to the toolbar: mode (video+data / data only) and a start/stop toggle that stays enabled while recording. - Log zeroing, calibration application and session end as timestamped events. Why - The recorder is fed from the worker sink list on the acquisition thread and only appends to a bounded queue, so disk I/O can never stall acquisition. - On_packet copies the raw frame instead of holding a reference: the packet frame may alias a buffer the SDK reuses between frames. - A queue overflow is counted and alarmed (rate limited), and a write or encode failure is counted, reported and after 5 consecutive errors stops the recording with failed=true rather than silently losing data. Verification - shared/tests/test_experiment_recorder.py: 12 cases covering the directory layout, the CSV contract, frame-id/time-base agreement, segment rolling, data-only and lossless modes, queue overflow accounting, encoder failure, metadata fields, crash-before-stop, and that on_packet never blocks. - tools/ui_check.py gains an "experiment record" section (169 checks, 0 failures overall), driving a recording through the UI and asserting the layout, the empty-cell rule and the event sequence. - apps/studio/tests/test_ui_smoke.py mirrors those invariants. - docs/record-channel.md documents the format and the remaining phase-2 work. Not in this change: CSV export of a selected time range, full-session curve browsing, calibration diagnostics (outlier exclusion, fit residuals) and PyOpenGL 3D. |
||
|
|
f501fa93f3 | subject: fix calibration cancel, unit mixing, FN clamping, export/apply and depth scale | ||
|
|
85d700bc06 | fix stop button disabled during connection | ||
|
|
a2db648560 | dsh ui improve: add brand bar |