采集循环只认新帧,选区统计移到数值卡,状态栏改为会过期的通知行 - 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:
@@ -36,6 +36,11 @@ STOP_JOIN_TIMEOUT_S = 5.0
|
||||
#: How long a new session waits (in its own thread, never on the GUI) for a
|
||||
#: previous session that is still winding down before opening the device.
|
||||
HANDOVER_TIMEOUT_S = 60.0
|
||||
#: Window over which the delivered frame rate is measured.
|
||||
FPS_WINDOW_S = 1.0
|
||||
#: How long to wait before asking again when the capture source has not produced
|
||||
#: a new frame yet. Long enough not to spin, short enough not to add latency.
|
||||
CAPTURE_REPEAT_POLL_S = 0.002
|
||||
|
||||
# Re-exported so ``from ...sdk_worker import SdkPacket`` keeps working; the
|
||||
# definitions live in ``models`` so neither worker depends on the other.
|
||||
@@ -72,6 +77,29 @@ def _sensor_force_calibrated(sensor, *, config_ref: str | Path | None = None) ->
|
||||
return False
|
||||
|
||||
|
||||
def _capture_sequence(sensor) -> Optional[int]:
|
||||
"""The capture source's delivered-frame counter, when it can mark a repeat.
|
||||
|
||||
``CaptureSource.read_latest`` returns the newest frame from its background
|
||||
reader; if that reader has not produced another frame yet, the *same* one
|
||||
comes back. Its ``status.last_seq`` advances once per delivered frame, so an
|
||||
unchanged counter means "no new frame". Only a source with a running
|
||||
background reader has that marker: a video file is read synchronously and
|
||||
every call decodes the next frame, so its counter says nothing about repeats
|
||||
and is ignored (``None``).
|
||||
"""
|
||||
source = getattr(sensor, "capture_source", None)
|
||||
if source is None:
|
||||
source = getattr(sensor, "_capture_source", None)
|
||||
if source is None or not bool(getattr(source, "running", False)):
|
||||
return None
|
||||
status = getattr(source, "status", None)
|
||||
try:
|
||||
return int(getattr(status, "last_seq"))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def process_chain(sensor) -> tuple[Optional[np.ndarray], str]:
|
||||
"""Run the live processing chain for one already-read frame.
|
||||
|
||||
@@ -595,6 +623,9 @@ class SdkAcquisitionWorker(RecordSinkMixin):
|
||||
saw_frame = False
|
||||
last_frame_mono = 0.0
|
||||
none_streak_start: Optional[float] = None
|
||||
#: The capture sequence of the last frame that was actually *processed*:
|
||||
#: an unchanged counter means the same frame came back again.
|
||||
last_delivered: Optional[int] = None
|
||||
# Per-session monotonic frame index: the join key shared by the video,
|
||||
# the CSV and the calibration records.
|
||||
frame_id = 0
|
||||
@@ -615,6 +646,18 @@ class SdkAcquisitionWorker(RecordSinkMixin):
|
||||
break
|
||||
|
||||
t0 = time.perf_counter()
|
||||
# ``CaptureSource.read_latest`` hands back the newest frame and,
|
||||
# until the pipeline produces another one, that **same** frame
|
||||
# again. Treating the repeat as a new sample doubled every
|
||||
# measurement (a 30 fps device recorded 63 rows per second) and
|
||||
# made the reported rate this loop's own polling rate. A source
|
||||
# without a background reader (a video file) decodes a new frame
|
||||
# per call, so it has no repeat marker and is left alone.
|
||||
delivered = _capture_sequence(sensor)
|
||||
if delivered is not None and delivered == last_delivered:
|
||||
time.sleep(CAPTURE_REPEAT_POLL_S)
|
||||
continue
|
||||
last_delivered = delivered
|
||||
processed = sensor.get_img()
|
||||
if processed is None:
|
||||
if saw_frame and is_file and none_streak_start is None:
|
||||
@@ -658,9 +701,14 @@ class SdkAcquisitionWorker(RecordSinkMixin):
|
||||
compute_error = compute_error or chain_error
|
||||
t1 = time.perf_counter()
|
||||
frame_times.append(t1)
|
||||
cutoff = t1 - 1.0
|
||||
cutoff = t1 - FPS_WINDOW_S
|
||||
frame_times = [t for t in frame_times if t >= cutoff]
|
||||
fps = float(len(frame_times)) if frame_times else 0.0
|
||||
# A *rate*, not a count: counting arrivals in a one-second window
|
||||
# reads one frame high whenever the window holds a full second of
|
||||
# a jittery stream. The span between the first and last arrival is
|
||||
# exactly what "frames per second" means.
|
||||
span = frame_times[-1] - frame_times[0] if len(frame_times) > 1 else 0.0
|
||||
fps = (len(frame_times) - 1) / span if span > 0.0 else 0.0
|
||||
forces = read_forces_from_sensor(sensor, config_ref=mode.config_ref)
|
||||
invalid_reason = forces.get("invalid_reason") or compute_error
|
||||
raw_frame = _ensure_bgr(raw)
|
||||
|
||||
Reference in New Issue
Block a user