达不到帧率的判据改用实测速率(DSHOW 回读值会说谎),探针加帧率扫描 - What: short_capture_fallacy() 改为以**实测**速率为准(新增 measured_fps 关键字参数),消息改成"it delivers about 15 fps while the config asks for 30 (1920x1080 reported 30, MJPG/DSHOW)"——把驱动自报值降级为"矛盾的一部分"而不是判据;app_window 的 _check_capture_negotiation() 相应改为用 packet 的 capture_fps(实测)判据、实测为 0 时不下结论(等窗口填满)。tools/camera_probe.py 重写:probe() 返回结构体、format_row() 把 reported 与 measured 分列并在达不到 90% 时标 "<-- misses the request";新增 --samples 与**帧率扫描**(同一格式依次申请 60/30/20/15/10 fps,逐行给实测值与"跟随申请 / 只有申请的 x 倍"的判断,最后给"本次看到的ceiling")。tests/test_worker_frame_rate.py 的归因测试同步更新,并显式覆盖"驱动自报 30 而实测 15 必须判为不达标"与"没有实测值时不说话"。 - Why: 用户跑探针的结果推翻了我上一轮的判据:MJPG 1920x1080@30 -> MJPG 1920x1080 @ 30.0 measured 15.0。DSHOW 后端下 CAP_PROP_FPS 只是回读你设的值,所以 SDK 的 status.fps 同样不可信 —— 我原来比较"申请 vs 协商"等于拿申请跟自己比,那个提示永远不会出现(用户实际看到的只有轨道上的实测 16.7)。这也解释了为什么这件事必须靠实测:真实速率只能测出来。另外探针显示 1280x720 MJPG 实测也是 15、YUYV 只有 1 fps,说明换分辨率不解决问题,需要区分"摄像头本身的上限"与"我们申请多少就被砍半"。 - Verification: shared 全量 161 passed(含新增/更新的 3 项断言);studio 91 passed;ui_check 554 项检查 0 失败;探针在本机(无摄像头)仍返回 1 并给出解释,format_row/_decode 单测通过("reported 30.0 measured 15.0 ... <-- misses the request")。 - Not in this change: 未改 SDK 采集申请顺序/后端;未做自动降级;frames.csv 仍未记录 process_ms/capture_fps。
This commit is contained in:
@@ -195,24 +195,33 @@ def capture_status_of(sensor) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def short_capture_fallacy(status: Mapping[str, Any] | None) -> str:
|
||||
"""A one-line description of a capture negotiation that fell short.
|
||||
def short_capture_fallacy(
|
||||
status: Mapping[str, Any] | None, *, measured_fps: float = 0.0
|
||||
) -> str:
|
||||
"""A one-line description of a capture that falls short of the request.
|
||||
|
||||
Empty when the driver gave what was asked for (or when either half is
|
||||
unknown): this exists to be *said*, not to fill a field.
|
||||
The rate to compare against is the **measured** one. OpenCV's DSHOW backend
|
||||
reports ``CAP_PROP_FPS`` back as whatever was set — a camera that advertises
|
||||
30 and delivers 15 says "30.0" — so the reported figure cannot be the evidence
|
||||
(it is included only to show the contradiction).
|
||||
|
||||
Empty when nothing measurable fell short, or when either half is unknown: this
|
||||
exists to be *said*, not to fill a field.
|
||||
"""
|
||||
if not isinstance(status, Mapping):
|
||||
return ""
|
||||
requested = dict(status.get("requested") or {})
|
||||
negotiated = dict(status.get("negotiated") or {})
|
||||
wanted = requested.get("fps")
|
||||
got = negotiated.get("fps")
|
||||
try:
|
||||
wanted_value = float(wanted)
|
||||
got_value = float(got)
|
||||
wanted_value = float(requested.get("fps"))
|
||||
except (TypeError, ValueError):
|
||||
return ""
|
||||
if wanted_value <= 0 or got_value <= 0 or got_value >= wanted_value * 0.9:
|
||||
if wanted_value <= 0:
|
||||
return ""
|
||||
measured = float(measured_fps or 0.0)
|
||||
if measured <= 0.0:
|
||||
return ""
|
||||
if measured >= wanted_value * 0.9:
|
||||
return ""
|
||||
size = ""
|
||||
width, height = negotiated.get("width"), negotiated.get("height")
|
||||
@@ -220,11 +229,13 @@ def short_capture_fallacy(status: Mapping[str, Any] | None) -> str:
|
||||
size = f"{int(width)}x{int(height)} "
|
||||
fourcc = str(negotiated.get("fourcc") or "").strip()
|
||||
backend = str(negotiated.get("backend") or "").strip()
|
||||
reported = negotiated.get("fps")
|
||||
detail = "/".join(part for part in (fourcc, backend) if part)
|
||||
return (
|
||||
f"the camera negotiated {size}@{got_value:g} fps "
|
||||
f"(the config asks for {wanted_value:g})"
|
||||
+ (f" [{detail}]" if detail else "")
|
||||
f"it delivers about {measured:g} fps while the config asks for "
|
||||
f"{wanted_value:g}"
|
||||
+ (f" ({size}reported {float(reported):g}" if reported else f" ({size}")
|
||||
+ (f", {detail})" if detail else ")")
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -312,11 +312,19 @@ def test_a_low_camera_rate_is_recorded_as_a_negotiation(monkeypatch):
|
||||
assert status["negotiated"]["fps"] == 16.67, status
|
||||
assert status["negotiated"]["fourcc"] == "MJPG"
|
||||
assert status["negotiated"]["backend"] == "MSMF"
|
||||
detail = short_capture_fallacy(status)
|
||||
assert "1920x1080" in detail and "16.67" in detail and "30" in detail, detail
|
||||
# A driver that gives what was asked for says nothing at all.
|
||||
# The *measured* rate is the evidence: DSHOW echoes the requested fps back
|
||||
# as the "negotiated" one, so trusting that number would hide a camera
|
||||
# that advertises 30 and delivers 15 (measured on this project's camera).
|
||||
detail = short_capture_fallacy(status, measured_fps=16.67)
|
||||
assert "16.67" in detail and "30" in detail and "1920x1080" in detail, detail
|
||||
assert short_capture_fallacy(status, measured_fps=29.5) == ""
|
||||
# A driver reporting 30 while delivering half of it is the case that
|
||||
# matters, and it must not slip through.
|
||||
source.status.fps = 30.0
|
||||
assert short_capture_fallacy(worker.capture_status()) == ""
|
||||
echoing = worker.capture_status()
|
||||
assert short_capture_fallacy(echoing, measured_fps=15.0) != ""
|
||||
# No measured rate yet: nothing to say.
|
||||
assert short_capture_fallacy(echoing) == ""
|
||||
finally:
|
||||
worker.stop(wait=True)
|
||||
_restore(saved)
|
||||
|
||||
Reference in New Issue
Block a user