fix(studio): 消除 Finger 实时采集的帧排队
实时处理相机最新帧,并修复重复采样、数组快照和暂停恢复。通过共享模块及相关 Studio 测试。
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
"""Live Finger consumes current frames without a FIFO of pending work."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from orisys_app_shared.acquisition import finger_worker as fw
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_file, expected", [(False, "serial"), (True, "stream")])
|
||||
def test_default_path_keeps_only_offline_processing_queued(
|
||||
monkeypatch, is_file, expected
|
||||
):
|
||||
worker = fw.FingerAcquisitionWorker()
|
||||
paths = []
|
||||
monkeypatch.setattr(worker, "_open_runtime", lambda *_: (None, None, None, is_file))
|
||||
monkeypatch.setattr(worker, "_run_serial_loop", lambda *_: paths.append("serial"))
|
||||
monkeypatch.setattr(
|
||||
worker, "_run_stream_pipeline", lambda *_: paths.append("stream")
|
||||
)
|
||||
worker._run_session(1, None)
|
||||
assert paths == [expected]
|
||||
|
||||
|
||||
def _live_loop(monkeypatch, *, sequences=None, compute_s=0.0, sink_s=0.0, pause=False):
|
||||
clock = [10.0]
|
||||
worker = fw.FingerAcquisitionWorker()
|
||||
worker._session_id = 1
|
||||
worker._mono_t0 = clock[0]
|
||||
packets = []
|
||||
resets = []
|
||||
source = SimpleNamespace(
|
||||
running=True, status=SimpleNamespace(last_seq=0, dropped_frames=7)
|
||||
)
|
||||
flow = np.zeros((8, 8, 2), np.float32)
|
||||
depth = np.zeros((8, 8), np.float32)
|
||||
tactile = SimpleNamespace(reset=lambda: resets.append(clock[0]))
|
||||
runtime = SimpleNamespace(
|
||||
tactile=tactile, capture_source=source, valid_mask=np.ones((8, 8), bool)
|
||||
)
|
||||
reads = [0]
|
||||
paused_once = [False]
|
||||
|
||||
def read():
|
||||
reads[0] += 1
|
||||
if reads[0] > 40:
|
||||
worker._stop.set()
|
||||
return None
|
||||
if sequences is not None:
|
||||
seq = sequences[min(reads[0] - 1, len(sequences) - 1)]
|
||||
else:
|
||||
seq = 1 + int((clock[0] - 10.0) * 30 + 1e-6)
|
||||
source.status.last_seq = seq
|
||||
return SimpleNamespace(
|
||||
image=np.full((8, 8, 3), seq, np.uint8),
|
||||
sequence=seq,
|
||||
host_mono_ns=int(clock[0] * 1e9),
|
||||
host_utc_ns=int(clock[0] * 1e9),
|
||||
media_time_ms=None,
|
||||
)
|
||||
|
||||
def process(frame, **_):
|
||||
flow.fill(frame[0, 0, 0])
|
||||
depth.fill(frame[0, 0, 0])
|
||||
return frame[:, :, 0]
|
||||
|
||||
def chain(_):
|
||||
clock[0] += compute_s
|
||||
if pause and not paused_once[0]:
|
||||
paused_once[0] = True
|
||||
worker.pause()
|
||||
return depth, ""
|
||||
|
||||
def sleep(seconds):
|
||||
clock[0] += max(seconds, 0.001)
|
||||
if worker.paused:
|
||||
worker.resume()
|
||||
|
||||
def sink(packet):
|
||||
packets.append(packet)
|
||||
clock[0] += sink_s
|
||||
if len(packets) >= 3:
|
||||
worker._stop.set()
|
||||
|
||||
runtime.read_source_stamped = read
|
||||
runtime.process_frame = process
|
||||
monkeypatch.setattr(
|
||||
fw, "time", SimpleNamespace(perf_counter=lambda: clock[0], sleep=sleep)
|
||||
)
|
||||
monkeypatch.setattr(fw, "process_chain", chain)
|
||||
monkeypatch.setattr(fw, "_contact_from_sensor", lambda *_, **__: None)
|
||||
monkeypatch.setattr(
|
||||
fw,
|
||||
"read_forces_from_sensor",
|
||||
lambda *_, **__: {
|
||||
"normal_force": 1.0,
|
||||
"shear_x": 2.0,
|
||||
"shear_y": 3.0,
|
||||
"normal_force_raw": 1.0,
|
||||
"shear_x_raw": 2.0,
|
||||
"shear_y_raw": 3.0,
|
||||
"force_unit": "raw",
|
||||
"force_calibrated": False,
|
||||
"flow": flow,
|
||||
"depth": depth,
|
||||
},
|
||||
)
|
||||
worker.add_sink(sink)
|
||||
worker._run_serial_loop(1, SimpleNamespace(config_ref=None), 0, runtime, False)
|
||||
return packets, worker, resets
|
||||
|
||||
|
||||
def test_repeated_camera_frames_are_not_processed_or_recorded(monkeypatch):
|
||||
packets, _, _ = _live_loop(monkeypatch, sequences=[1, 1, 2, 2, 3])
|
||||
assert [p.source_frame_id for p in packets] == [1, 2, 3]
|
||||
assert [p.frame_id for p in packets] == [0, 1, 2]
|
||||
|
||||
|
||||
def test_slow_compute_and_sink_take_current_input_without_a_backlog(monkeypatch):
|
||||
packets, _, _ = _live_loop(monkeypatch, compute_s=0.07, sink_s=0.10)
|
||||
assert [p.source_frame_id for p in packets] == [1, 6, 11]
|
||||
for packet in packets:
|
||||
assert packet.flow_frame_id == packet.depth_frame_id == packet.frame_id
|
||||
assert np.all(packet.flow == packet.source_frame_id)
|
||||
assert np.all(packet.depth == packet.source_frame_id)
|
||||
assert packet.capture_dropped == 7
|
||||
|
||||
|
||||
def test_pause_during_compute_does_not_publish_and_resume_rebases(monkeypatch):
|
||||
packets, _, resets = _live_loop(monkeypatch, compute_s=0.07, pause=True)
|
||||
assert len(packets) == 3
|
||||
assert packets[0].source_frame_id > 1
|
||||
assert packets[0].frame_id == 0
|
||||
assert len(resets) == 1
|
||||
@@ -283,6 +283,8 @@ def test_finger_video_and_camera_use_the_same_packet_path(source, monkeypatch):
|
||||
assert opened == [source]
|
||||
assert packets
|
||||
packet = packets[0]
|
||||
assert packet.valid
|
||||
assert packet.force_calibrated
|
||||
assert packet.raw_frame.shape == (8, 8, 3)
|
||||
assert packet.processed_frame.shape == (8, 8, 3)
|
||||
assert packet.session_id == 1
|
||||
@@ -300,20 +302,25 @@ def test_finger_video_and_camera_use_the_same_packet_path(source, monkeypatch):
|
||||
_restore(saved)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["box", "finger"])
|
||||
def test_a_stopped_session_leaves_no_pipeline_threads(kind):
|
||||
@pytest.mark.parametrize(
|
||||
"kind, source", [("box", "0"), ("finger", "recording.mp4"), ("finger", 0)]
|
||||
)
|
||||
def test_a_stopped_session_leaves_no_pipeline_threads(kind, source):
|
||||
"""``stop`` must join the three stages: they own the sensor's tracker state."""
|
||||
packets: list = []
|
||||
saved = _install()
|
||||
worker = _worker(kind)
|
||||
try:
|
||||
worker.add_sink(packets.append)
|
||||
worker.start("0", config_ref=_config(kind))
|
||||
worker.start(source, config_ref=_config(kind))
|
||||
deadline = time.perf_counter() + 5.0
|
||||
while time.perf_counter() < deadline and not packets:
|
||||
time.sleep(0.02)
|
||||
assert packets, "the session published nothing to stop"
|
||||
assert _pipeline_threads(), "the fake session did not start the pipeline"
|
||||
if kind == "finger" and source == 0:
|
||||
assert _pipeline_threads() == []
|
||||
else:
|
||||
assert _pipeline_threads(), "the fake session did not start the pipeline"
|
||||
worker.stop(wait=True)
|
||||
deadline = time.perf_counter() + 5.0
|
||||
while time.perf_counter() < deadline and _pipeline_threads():
|
||||
|
||||
Reference in New Issue
Block a user