界面显示每帧耗时与摄像头自身帧率,让低帧率可归因 - What: SdkPacket 新增 capture_fps(采集源自报的发布速率,与 fps 同一窗口)与 capture_dropped(SDK 后台读取线程累计的"发布了但没人取走"的帧数);sdk_worker 的循环记录 (t0, 已发布序号) 并在每帧算出摄像头自身的发布速率,同时读 dropped_frames;新增 _capture_source()/_capture_dropped() 两个取值助手(原 _capture_sequence 里的取值逻辑抽出来复用)。app_window 在 视图▸显示帧率 打开时把这一段一起显示在轨道摘要上:16.7 FPS · 12 ms · 摄像头 30.0 (-13)——速率是我们消费的、耗时是每帧全部开销、摄像头速率是采集源自己发布的;两个速率不一致时后面的 (-N) 就是"摄像头有、我们没接住"的帧数。新增 locale 键 status.rail_camera_fps(中英)。tests/test_worker_frame_rate.py:假设备的采集源现在也像 orisys.capture.source 一样统计"发布时上一帧还没被取走"的丢帧,并支持给 get_img 加一个可配置的每帧耗时(PROCESS_COST_S);新增 test_the_camera_s_own_rate_is_reported_next_to_ours:循环跟得上时摄像头速率≈30、丢帧 0;把流水线放到 60 ms/帧后我们的速率掉到 22 以下、**摄像头速率仍报 30**、丢帧 >5。 - Why: 用户报"上方显示 16.7 fps 而不是实际 30",并且给出了关键数据:16.7 fps 但每帧只耗 12 ms。12 ms 的工作量塞进 60 ms 的周期里,说明循环大部分时间在等——而"等"有两种完全不同的原因:(a) 摄像头/驱动只发布 16.7(USB 带宽、MJPG/YUYV、格式协商),(b) 摄像头发布 30 而我们只取到一半(节拍与轮询问题)。原来的读数只有"我们消费了多少",两种情形长得一模一样,无法判断,所以这不是显示 bug 而是**读数缺一半**。SDK 的 status.last_seq 与 dropped_frames 正好提供另一半(last_seq 每发布一帧就自增,与消费者无关;dropped_frames 在发布时若上一帧仍在缓存里就 +1,即消费者的亏欠),因此把它读出来即可判定。 - Verification: shared 全量 160 passed(新增 2 项,含上面的归因测试);studio 90 passed;ui_check 554 项检查 0 失败;locale_check 287 字面键全部命中、中英键集一致。过程中自查出一个我自己引入的问题:最初把 (t0, 序号) 的记录与按时间裁剪放在**每次轮询**里,而等待期间该分支每秒会进几万次,列表推导把 GIL 抢光,假摄像头的发布速率从 30 掉到 8(test_the_same_frame_is_not_counted_twice 立刻变红)——改为"仅在该序号发生变化时记录",即为每发布帧 O(1) 而非每轮询 O(n)。 - Not in this change: 未改节拍/等待逻辑(假设备上 30 fps 与 60 ms 慢流水线两种情形的消费速率都符合预期,未发现节拍 bug);frames.csv 仍未记录 process_ms/capture_fps(下次改记录格式时一并加)。
This commit is contained in:
@@ -38,7 +38,9 @@ class _CaptureSource:
|
||||
|
||||
The real one is a thread too: the sequence advances whether or not the
|
||||
consumer asks for a frame, which is exactly why an unchanged sequence means
|
||||
"no new frame" rather than "the consumer has not called yet".
|
||||
"no new frame" rather than "the consumer has not called yet". It also counts
|
||||
a drop whenever it replaces a frame nobody took, like
|
||||
``orisys.capture.source.CaptureSource._background_loop`` does.
|
||||
"""
|
||||
|
||||
def __init__(self, period_s: float = FRAME_PERIOD_S) -> None:
|
||||
@@ -46,6 +48,7 @@ class _CaptureSource:
|
||||
self.running = True
|
||||
self._period = float(period_s)
|
||||
self._stop = threading.Event()
|
||||
self._taken_seq = 0
|
||||
self._thread = threading.Thread(target=self._publish, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
@@ -53,14 +56,25 @@ class _CaptureSource:
|
||||
while not self._stop.is_set():
|
||||
if self._stop.wait(timeout=self._period):
|
||||
return
|
||||
# Replacing a frame that was never consumed is a drop on our side.
|
||||
if self.status.last_seq > self._taken_seq:
|
||||
self.status.dropped_frames += 1
|
||||
self.status.last_seq += 1
|
||||
|
||||
def take(self) -> None:
|
||||
self._taken_seq = self.status.last_seq
|
||||
|
||||
def close(self) -> None:
|
||||
self.running = False
|
||||
self._stop.set()
|
||||
self._thread.join(timeout=1.0)
|
||||
|
||||
|
||||
#: Per-frame processing cost the fake device charges, so a test can make the
|
||||
#: pipeline slower than the camera (the real box chain costs ~10 ms at 480p).
|
||||
PROCESS_COST_S = 0.0
|
||||
|
||||
|
||||
class _Info:
|
||||
"""The channel names the worker reads off ``sensor.info``."""
|
||||
|
||||
@@ -105,7 +119,11 @@ class _Sensor:
|
||||
|
||||
def get_img(self):
|
||||
# ``read_latest``: hand back the newest frame immediately, the same one
|
||||
# again when nothing new has arrived.
|
||||
# again when nothing new has arrived. A test can make this cost time, so
|
||||
# the loop can be put under a pipeline slower than the camera.
|
||||
if PROCESS_COST_S > 0:
|
||||
time.sleep(PROCESS_COST_S)
|
||||
self.capture_source.take()
|
||||
return self.frame
|
||||
|
||||
def disconnect(self):
|
||||
@@ -211,6 +229,42 @@ def test_the_reported_rate_is_the_delivered_rate():
|
||||
assert abs(settled[-1] - measured) < 0.25 * measured, (settled[-1], measured)
|
||||
|
||||
|
||||
def test_the_camera_s_own_rate_is_reported_next_to_ours(monkeypatch):
|
||||
"""A low frame rate must be attributable, not just visible.
|
||||
|
||||
With a pipeline that fits the frame period the two rates agree. With one that
|
||||
does not, the *camera's* rate stays at 30 while ours falls and the source
|
||||
counts the frames nobody took — which is the whole difference between "the
|
||||
camera is slow" and "we are slow".
|
||||
"""
|
||||
packets, _ = _collect(duration_s=1.5)
|
||||
assert packets, "the loop emitted nothing at all"
|
||||
camera = [float(packet.capture_fps) for packet in packets if packet.capture_fps]
|
||||
assert camera, "no packet carried the capture source's own rate"
|
||||
assert 28.0 <= camera[-1] <= 31.0, f"the fake camera publishes at 30 fps, got {camera[-1]}"
|
||||
assert packets[-1].capture_dropped == 0, (
|
||||
f"the loop dropped {packets[-1].capture_dropped} frames while keeping up"
|
||||
)
|
||||
|
||||
# Now make the pipeline three times slower than the camera's own period.
|
||||
monkeypatch.setattr(sys.modules[__name__], "PROCESS_COST_S", 0.06)
|
||||
slow, _ = _collect(duration_s=2.0)
|
||||
try:
|
||||
assert slow, "the loop emitted nothing at all"
|
||||
ours = _rate(slow)
|
||||
assert ours < 22.0, f"a 60 ms pipeline still kept up at {ours:.1f} fps"
|
||||
assert 28.0 <= float(slow[-1].capture_fps) <= 31.0, (
|
||||
"the camera's rate was reported as slow too: "
|
||||
f"{slow[-1].capture_fps}"
|
||||
)
|
||||
assert slow[-1].capture_dropped > 5, (
|
||||
"the loop fell behind without recording a single dropped frame: "
|
||||
f"{slow[-1].capture_dropped}"
|
||||
)
|
||||
finally:
|
||||
monkeypatch.setattr(sys.modules[__name__], "PROCESS_COST_S", 0.0)
|
||||
|
||||
|
||||
def test_a_file_source_runs_on_the_video_s_own_timeline():
|
||||
"""A pass over a video is stamped with the video's time, not the wall clock."""
|
||||
from orisys_app_shared.acquisition.sdk_worker import SdkAcquisitionWorker
|
||||
|
||||
Reference in New Issue
Block a user