fix(studio): 消除 Finger 实时采集的帧排队
实时处理相机最新帧,并修复重复采样、数组快照和暂停恢复。通过共享模块及相关 Studio 测试。
This commit is contained in:
@@ -294,9 +294,8 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
use_stream_pipeline: bool = True,
|
||||
):
|
||||
self._init_sinks()
|
||||
#: ``runtime.pipeline()`` is the acquisition path; the serial chain is kept
|
||||
#: so a change can be checked against it frame-for-frame on the same input
|
||||
#: (``False`` selects the reference).
|
||||
#: File analysis can pipeline every frame; live capture uses the serial
|
||||
#: chain so old inputs and completed results cannot accumulate in FIFOs.
|
||||
self._use_stream_pipeline = bool(use_stream_pipeline)
|
||||
#: The running session's ``StreamPipeline``, when there is one.
|
||||
self._pipe = None
|
||||
@@ -500,8 +499,11 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
never computed: keeping the pre-pause reference frame would report the hold
|
||||
itself as a large deformation.
|
||||
"""
|
||||
self._pause.clear()
|
||||
pipe = self._pipe
|
||||
if pipe is None and self._pause.is_set():
|
||||
with self._lock:
|
||||
self._pending_reset = True
|
||||
self._pause.clear()
|
||||
if pipe is not None:
|
||||
try:
|
||||
pipe.resume(reset=True)
|
||||
@@ -642,12 +644,7 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
if opened is None:
|
||||
return
|
||||
mode, source, runtime, is_file = opened
|
||||
if self._use_stream_pipeline:
|
||||
# The recommended path: the SDK runs produce (capture + UV unroll) ->
|
||||
# flow -> HHD on three threads, so throughput is bounded by the slowest
|
||||
# stage rather than by their sum. The app's own layer — session and
|
||||
# frame bookkeeping, the record sink, calibration reload, contact
|
||||
# provenance — stays on top of the frames the pipeline finishes.
|
||||
if self._use_stream_pipeline and is_file:
|
||||
self._run_stream_pipeline(session, mode, source, runtime, is_file)
|
||||
return
|
||||
self._run_serial_loop(session, mode, source, runtime, is_file)
|
||||
@@ -1033,19 +1030,15 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
def _run_serial_loop(
|
||||
self, session: int, mode: FixedCameraMode, source, runtime, is_file: bool
|
||||
) -> None:
|
||||
"""The single-threaded chain, kept whole for the packet-diff check.
|
||||
|
||||
``use_stream_pipeline=False`` selects it: read, unroll, decompose and read
|
||||
the forces back to back on this one thread, which bounds throughput by the
|
||||
sum of the stages. It is the reference the concurrent path is measured
|
||||
against, and it is not the path a session uses.
|
||||
"""
|
||||
"""Run whole frames sequentially; live capture always takes the newest input."""
|
||||
frame_times: list[float] = []
|
||||
#: (collection time, published sequence), so the camera's own rate
|
||||
#: can be told apart from the rate this loop consumes (see the box
|
||||
#: worker: the two are the same measurement and must mean the same).
|
||||
source_times: list[tuple[float, int]] = []
|
||||
last_source_seq = None
|
||||
last_processed_seq = None
|
||||
capture = _RuntimeCapture(runtime)
|
||||
capture_fps = 0.0
|
||||
saw_frame = False
|
||||
last_frame_mono = 0.0
|
||||
@@ -1107,6 +1100,12 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
time.sleep(PAUSE_POLL_S)
|
||||
continue
|
||||
|
||||
if not is_file and source_seq is not None:
|
||||
if source_seq == last_processed_seq:
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
last_processed_seq = source_seq
|
||||
|
||||
unrolled = runtime.process_frame(source_frame, color=False)
|
||||
if unrolled is None:
|
||||
if saw_frame and is_file and none_streak_start is None:
|
||||
@@ -1116,6 +1115,7 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
|
||||
none_streak_start = None
|
||||
tactile = runtime.tactile
|
||||
sensor = getattr(tactile, "raw_sensor", tactile)
|
||||
compute_error = ""
|
||||
# The frame's acquisition time, so an event that happens on this
|
||||
# frame is dated when it happened rather than when it was queued.
|
||||
@@ -1135,9 +1135,11 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
if self._on_error is not None and session == self._session_id:
|
||||
self._on_error(message)
|
||||
# The same chain the offline depth replay runs on the tactile.
|
||||
chain_depth, chain_error = process_chain(tactile)
|
||||
chain_depth, chain_error = process_chain(sensor)
|
||||
compute_error = compute_error or chain_error
|
||||
t1 = time.perf_counter()
|
||||
if self._pause.is_set() or self._stop.is_set():
|
||||
continue
|
||||
frame_times.append(t0)
|
||||
cutoff = t1 - FPS_WINDOW_S
|
||||
frame_times = [t for t in frame_times if t >= cutoff]
|
||||
@@ -1147,7 +1149,7 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
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
|
||||
# The camera's own rate, so this view can attribute a low rate too.
|
||||
delivered = _capture_sequence(tactile)
|
||||
delivered = source_seq if not is_file else None
|
||||
if delivered is not None and delivered != last_source_seq:
|
||||
last_source_seq = delivered
|
||||
source_times.append((t0, delivered))
|
||||
@@ -1163,11 +1165,11 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
with self._lock:
|
||||
self._measured_capture_fps = capture_fps
|
||||
forces = read_forces_from_sensor(
|
||||
tactile,
|
||||
sensor,
|
||||
config_ref=mode.config_ref,
|
||||
)
|
||||
invalid_reason = forces.get("invalid_reason") or compute_error
|
||||
raw_frame = _ensure_bgr(source_frame)
|
||||
raw_frame = _ensure_bgr(source_frame).copy()
|
||||
# The frame the tactile chain consumed, kept for depth replay.
|
||||
self.note_preroll(raw_frame)
|
||||
# Provenance for anything that colours a derived array (the 3D pane):
|
||||
@@ -1187,13 +1189,13 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
if depth_valid:
|
||||
last_depth_frame_id = frame_id
|
||||
contact = _contact_from_sensor(
|
||||
tactile,
|
||||
sensor,
|
||||
frame_id=frame_id,
|
||||
compute_error=compute_error,
|
||||
)
|
||||
packet = SdkPacket(
|
||||
raw_frame=raw_frame,
|
||||
processed_frame=_ensure_bgr(unrolled),
|
||||
processed_frame=_ensure_bgr(unrolled).copy(),
|
||||
timestamp=utc_ns / 1e9,
|
||||
wall_t=utc_ns / 1e9,
|
||||
mono_t=frame_mono,
|
||||
@@ -1206,6 +1208,8 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
media_time_ms=media_ms,
|
||||
fps=fps,
|
||||
process_ms=(t1 - t0) * 1000.0,
|
||||
capture_fps=capture_fps,
|
||||
capture_dropped=int(_capture_dropped(capture) or 0),
|
||||
normal_force=forces["normal_force"],
|
||||
shear_x=forces["shear_x"],
|
||||
shear_y=forces["shear_y"],
|
||||
@@ -1224,9 +1228,12 @@ class FingerAcquisitionWorker(RecordSinkMixin):
|
||||
# place; the app must not record them as this frame's data.
|
||||
stale_forces=bool(compute_error),
|
||||
raw_available=bool(forces.get("raw_available", True)),
|
||||
flow=flow,
|
||||
depth=depth,
|
||||
valid_mask=runtime.valid_mask,
|
||||
flow=None if flow is None else np.array(flow, copy=True),
|
||||
depth=None if depth is None else np.array(depth, copy=True),
|
||||
valid_mask=(
|
||||
np.array(runtime.valid_mask, copy=True)
|
||||
if runtime.valid_mask is not None else None
|
||||
),
|
||||
flow_valid=flow_valid,
|
||||
flow_frame_id=frame_id if flow_valid else last_flow_frame_id,
|
||||
depth_valid=depth_valid,
|
||||
|
||||
Reference in New Issue
Block a user