按"硬件问题、配置不变"收尾:记录里写下实测帧率,探针加逐颗验收模式 - 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 列、记录格式升版时一起)。

This commit is contained in:
ykcheungab
2026-09-14 13:48:08 +08:00
parent 30ef50814f
commit 4a482e579f
7 changed files with 115 additions and 11 deletions
+1 -1
View File
@@ -357,7 +357,7 @@
"run_failed": "Run failed: {error}",
"filters_saved": "Filter settings saved: {describe} (applies to the next session)",
"rail_camera_fps": "camera {fps}",
"capture_slow": "The camera cannot deliver the configured frame rate: {detail}. Software cannot change that — the driver only offers this format; the recording keeps the real rate. Use a smaller resolution/another format, or another camera.",
"capture_slow": "The camera delivers less than the configured frame rate: {detail}. The recording keeps the real rate. This is a capture-side (camera/driver) matter that software cannot change: use a camera that really does deliver 30 fps — after checking that the cable or port is not slowing this one down — and measure again.",
"video_ended": "Video ended",
"camera_disconnected": "Camera disconnected",
"reset_ok": "Tracker reset",
+1 -1
View File
@@ -357,7 +357,7 @@
"run_failed": "启动失败: {error}",
"filters_saved": "滤波设置已保存:{describe}(下次采集生效)",
"rail_camera_fps": "摄像头 {fps}",
"capture_slow": "摄像头达不到配置的帧率:{detail}。这不是软件能改的——驱动只提供这个格式;记录会按实际帧率保存,请降低分辨率/换格式或换摄像头。",
"capture_slow": "摄像头交付的帧率低于配置值:{detail}。记录会按实际帧率保存。这是采集端(摄像头/驱动)的问题,软件改不了:换一颗真能交付 30 fps 的摄像头,或先确认线材/端口没有把它降速,然后再测。",
"video_ended": "视频已结束",
"camera_disconnected": "摄像机已断开",
"reset_ok": "追踪器已重置",
+4
View File
@@ -2139,6 +2139,10 @@ def test_the_camera_probe_is_usable_without_a_camera(capsys):
# The fourcc round trip is what makes the table readable.
assert camera_probe._decode(camera_probe._fourcc_code("MJPG")) == "MJPG"
assert camera_probe._decode(0) == "-"
# Acceptance mode: one line, one exit code, no camera means failure.
assert camera_probe.main(["--check", "--index", "9", "--samples", "2"]) == 1
check_out = capsys.readouterr().out
assert "No camera answered on index 9" in check_out
# ------------------------------------------------------- plot settings dialog
+43
View File
@@ -175,13 +175,56 @@ def size_sweep(index: int, backend_name: str, backend: int, fps: float = 30.0,
return rows
def check_format(
index: int,
*,
request: tuple[str, int, int, float] = REQUESTS[0],
samples: int = 120,
) -> int:
"""Acceptance mode: does this camera deliver the bundle's format?
One line, one exit code, for going through units in turn. The request is the
one the sensor bundles make (MJPG 1920x1080@30); the verdict is the *measured*
rate, because the reported one is an echo.
"""
fourcc, width, height, fps = request
for backend_name, backend in BACKENDS:
result = probe(index, backend_name, backend, request, samples=samples)
if result is None:
continue
measured = result["measured_fps"]
asked = f"{fourcc} {width}x{height}@{fps:g}"
if measured >= fps * TOLERANCE:
print(f"PASS camera {index}: {asked} -> {measured:.1f} fps "
f"[{backend_name}, {result['fourcc']}]")
return 0
print(
f"FAIL camera {index}: {asked} -> {measured:.1f} fps "
f"(reported {result['reported_fps']:.1f}, {result['fourcc']}, "
f"{backend_name})"
)
return 1
print(f"No camera answered on index {index} (is the app closed?).")
return 1
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--index", type=int, default=None, help="camera index to probe")
parser.add_argument("--max-index", type=int, default=4, help="highest index to try")
parser.add_argument("--samples", type=int, default=SAMPLES, help="frames per row")
parser.add_argument(
"--check",
action="store_true",
help="acceptance mode: measure the bundle's format once, exit 0 when it "
"delivers it (use this to go through units)",
)
args = parser.parse_args(argv)
if args.check:
return check_format(args.index if args.index is not None else 0,
samples=args.samples)
indices = [args.index] if args.index is not None else list(range(args.max_index + 1))
found = 0
for index in indices: