采集循环只认新帧,选区统计移到数值卡,状态栏改为会过期的通知行 - 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 里)。

This commit is contained in:
ykcheungab
2026-09-13 21:19:23 +08:00
parent 824544fc7a
commit e6a1e77a5b
13 changed files with 783 additions and 66 deletions
+22 -3
View File
@@ -52,6 +52,25 @@ UI 线程 _on_run_tick ──> 曲线显示(仍可降帧)
控件更新必须留在 UI 线程。
- **sink 是普通 callable 列表**,某个 sink 抛异常会被记录并只影响该 packet,不会杀掉采集。
### 2.1 采集循环只认「新帧」,并据此定义帧率
`CaptureSource.read_latest` 返回的是**后台读取线程的最新一帧**:如果那一刻还没有新帧,
它把**同一帧**再给一次。采集循环以前不做判断,于是同一帧被反复处理 —— 30 fps 的设备
录出了 63 行/秒(且 `frame_id` 各不相同,看起来像真的),顶栏的 `packet.fps` 显示的是
**我们自己的轮询速率**。
- `_capture_sequence(sensor)` 读 `capture_source.status.last_seq`:计数没变就是"还没有新
帧",睡 `CAPTURE_REPEAT_POLL_S`(2 ms)后重试,**不进处理链、不发 packet**。
只有带后台读取线程的源才有这个标记;视频文件是同步 `read_raw()`,每次调用都解码下一
帧,因此它的计数与"重复"无关,直接忽略(返回 `None`,行为不变)。
- `packet.fps` 改为**速率**:`(窗口内帧数 - 1) / (首末到达时刻之差)`,窗口 `FPS_WINDOW_S`
= 1 s。按"1 秒内到达了多少帧"数数会系统性地偏大(窗口里正好装一秒,抖动就多算一帧)。
- 这一层是"哪些帧算一帧"的唯一判据,记录、曲线、标定与 CSV 都跟着它走;它不改动
`packet.fps` 的语义之外的东西 —— **容器速率**仍然是传感器配置值(见 §6.1)。
`shared/tests/test_worker_frame_rate.py` 用一个假 SDK 驱动真实循环验证:同一帧返回 3 次
时,packet 数 ≈ 交付帧数(去掉判据后是 11317 : 39),且 `packet.fps` 与交付速率一致。
### 关键文件
| 文件 | 职责 |
@@ -161,9 +180,9 @@ outputs/recordings/<UTC 时间戳>_session<NNNN>/
`session.json` 的 `video.fps` 是**容器速率**,必须等于传感器的采集速率,取自传感器配置
(`config/sensors/<type>/sensor.json` 的 `fps`,box 为 30),**不是** packet 里那个
`fps` 字段 —— 后者是"管线每秒完成多少帧"的计数器,开流瞬间的缓冲突发或视频文件源会让
它读到 48,用它写 30 fps 的容器就等于把回放播成了快进。这是"传感器 30 fps、录制结果
48 fps"的直接原因。
`fps` 字段 —— 后者是"本机每秒处理了多少帧"的实时速率(见 §2.1),开流瞬间的缓冲突发会
让它高于配置值,用它写 30 fps 的容器就等于把回放播成了快进。这是"传感器 30 fps、录制
结果 48 fps"的直接原因。
- `StudioMainWindow._video_fps()`:实时相机 → 传感器配置的 `fps`;视频文件源 → 实测值;
实时相机的实测值另存 `video.measured_fps`,两者都进 `session.json`,对不上时看得见。
+20
View File
@@ -118,11 +118,31 @@
- **符号不替代语义**:每个按钮都带 tooltip 与 `accessibleName`,`ui_check.py` 的
`chart header and unit chip` 段逐项断言 `text()==""` 且这三样齐全——只有符号没有说明
等于把动作藏起来。全程总览**区域名**(`window_caption`)仍是文字,它标注的是一块区域。
- **选区统计在数值卡上,不在下方**:框选/缩放出一段窗口后,每张卡的大号数字变成该窗口的
**峰值**、下方小字是该窗口的**均值 · σ**(`chart.selection_detail`),卡片的 tooltip 说明
大数字是峰值;曲线下方只剩「选区 4.13 s / 183 点」(完整的一行仍在它的 tooltip 里)。
以前把三通道的峰/均/σ 塞在一行 `QLabel` 里,中文长句直接把面板顶出右边界。
窗口等于整个会话(「全程总览」)不算选区——那是默认适配,此时卡片回到"游标处样本"。
- **单位芯片是可点的按钮**(`QPushButton#unitBadge`,不再是 `QLabel`):点一下就切换
`N` ⇄ `raw`,同一条命令(`_set_display_unit`)同时改曲线、数值卡与设置区下拉,三处永远
一致。旧写法只有「raw/N」两个字,看起来像只读状态。
- 箭头图页脚的放大倍率写成「箭头按位移放大 ×2」,不再只有一个「×2」。
### 状态栏是一条通知行,不是第二条状态行
底部 `QStatusBar` 只承载**一次性通知**(录制结束/失败、标定已应用、连接慢、错误原因):
- **没有常驻控件**。以前右下角有一条 `配置 sensor.json`(`status.device_label`),而状态轨
的传感器摘要按钮已经写着 `MINI · sensor.json`,右下角是同一件事实的第二遍。
- **消息会过期**(`_NoticeBar.DEFAULT_TIMEOUT_MS` = 6 s)。Qt 的 `showMessage(text)` 默认
`msecs=0`,意思是"永不消失",于是会话刚开始那句「运行中(0)」一直挂到退出;只读状态
的读法应该在顶栏(阶段芯片 + 来源槽),所以子类把"没有超时"一律换成 6 秒。
- **与状态轨重复的两条提示被删掉**:`status.run_started`(「运行中(0)」)与
`status.connecting`(「正在连接…」)在轨上分别有「采集中」芯片与相机编号、以及
「连接中」芯片。
- **逐帧重复的通告只发一次**:`status.units_raw_fallback`(无标定 → 显示原始单位)以前每
一帧都发一次,等于永久占住通知行;现在每次运行只提示一次。
### 单位切换是"开关",不是"重新解释"
曲线缓冲区里的每个数都带着它被测量时的单位。设备在头几帧还没确认标定时,显示会退到
+5
View File
@@ -220,6 +220,11 @@ flow_ramp = 画布色, #6E4AD0, #FA02F3, #FF8A6B, #D8FF00
按钮,所以不参与 §6.5 的"同一时刻只有一个实心按钮"计数(`QLabel#unitBadge` 仍是通道
徽标与传感器缩略图的样式,两者靠控件类型区分)。
8. **数值卡的小字用等宽体,且不会把面板顶宽**。选区统计(均值 · σ)画在数值卡内的 9pt
等宽小字上,与 16pt 的大号数字同一列;底部状态栏不再出现长句,因此曲线面板的宽度由
三个数值卡决定,而不是由一行统计文字决定。`ui_check.py` 的 `session browsing` 段断言
该小字 `width() >= sizeHint().width()`(中文与英文都不裁切)。
---
## 7. 三个顶层视图 = 同一窗口的三套布局