为采集帧绑定主机时间与来源信息

采集时间随图像穿过处理队列,并修正未读帧的丢帧计数。通过采集与流水线针对性测试。
This commit is contained in:
ykcheungab
2026-10-11 12:48:34 +08:00
parent 0784eacf51
commit 120b8c2cf6
7 changed files with 256 additions and 40 deletions
+37 -10
View File
@@ -74,7 +74,13 @@ from typing import Any
import numpy as np
__all__ = ["FrameSnapshot", "PipelineStats", "PauseLevel", "StreamPipeline"]
__all__ = [
"FrameSnapshot",
"ProducedFrame",
"PipelineStats",
"PauseLevel",
"StreamPipeline",
]
#: Array fields a snapshot can carry. ``image`` is the working grey frame; the
#: rest come from ``Sensor.read_info`` (already copied by the SDK).
@@ -105,6 +111,14 @@ class PauseLevel(str, Enum):
CAPTURE_ONLY = "capture_only"
@dataclass(frozen=True)
class ProducedFrame:
"""A producer image with opaque metadata carried to its finished snapshot."""
image: np.ndarray
metadata: Any = None
@dataclass(frozen=True)
class FrameSnapshot:
"""An immutable view of one finished frame.
@@ -139,6 +153,7 @@ class FrameSnapshot:
#: when the producer did not report one. Read from ``Sensor.img_size_raw`` because callers
#: compare it with ``image.shape`` to see how much the frame was reduced.
image_size_raw: tuple[int, int] | None = None
source_metadata: Any = None
@dataclass
@@ -171,8 +186,9 @@ class PipelineStats:
class StreamPipeline:
"""Run ``produce -> flow -> hhd`` as three concurrent stages.
``produce`` is a zero-argument callable returning either the working frame
(``(H, W)`` grey or ``(H, W, 3)`` BGR) or ``None`` to signal end of input.
``produce`` is a zero-argument callable returning the working frame
(``(H, W)`` grey or ``(H, W, 3)`` BGR), a ``ProducedFrame`` carrying source
metadata, or ``None`` to signal end of input.
For a ``Sensor`` that is ``sensor.get_img``; for a ``FingerRuntime`` use
:meth:`for_finger_runtime`.
"""
@@ -180,7 +196,7 @@ class StreamPipeline:
def __init__(
self,
sensor: Any,
produce: Callable[[], np.ndarray | None],
produce: Callable[[], np.ndarray | ProducedFrame | None],
*,
contact: bool = False,
depth: int = 2,
@@ -519,10 +535,14 @@ class StreamPipeline:
frame = self._produce()
if frame is None:
break
source_metadata = None
if isinstance(frame, ProducedFrame):
source_metadata = frame.metadata
frame = frame.image
t1 = time.perf_counter()
# Private copy: the SDK buffer is rewritten in place by the next
# produce() call while the flow stage is still reading this frame.
payload = (frame.copy(), t0, t1 - t0)
payload = (frame.copy(), t0, t1 - t0, source_metadata)
if not self._put(self._q1, payload):
break
self.stats.produced += 1
@@ -547,7 +567,7 @@ class StreamPipeline:
if self._reset_pending.is_set():
self._reset_pending.clear()
self.sensor.reset()
frame, t_cap, produce_s = item
frame, t_cap, produce_s, source_metadata = item
t0 = time.perf_counter()
# The frame is passed explicitly: the SDK's self.img slot is shared
# with the producer, so it must not be the transport.
@@ -557,7 +577,10 @@ class StreamPipeline:
# Private copy: with the flow EMA enabled Sensor.flow is a buffer the
# next frame rewrites.
flow_copy = flow.copy() if isinstance(flow, np.ndarray) else None
self._put(self._q2, (frame, flow_copy, t_cap, produce_s, flow_s))
self._put(
self._q2,
(frame, flow_copy, t_cap, produce_s, flow_s, source_metadata),
)
self._put(self._q2, None)
def _hhd_loop(self) -> None:
@@ -572,7 +595,7 @@ class StreamPipeline:
if not self._wait_out_pause():
break
continue
frame, flow, t_cap, produce_s, flow_s = item
frame, flow, t_cap, produce_s, flow_s, source_metadata = item
t0 = time.perf_counter()
# Explicit flow: compute_hhd reads self.flow, which the flow stage
# overwrites for the next frame, so it cannot be the transport.
@@ -580,7 +603,9 @@ class StreamPipeline:
if self._contact:
self.sensor.compute_contact()
hhd_s = time.perf_counter() - t0
snap = self._build(frame, flow, produce_s, flow_s, hhd_s, t_cap)
snap = self._build(
frame, flow, produce_s, flow_s, hhd_s, t_cap, source_metadata
)
self._publish(snap)
if self._on_frame is not None:
# Runs on this stage's thread: a slow consumer exerts real
@@ -600,7 +625,8 @@ class StreamPipeline:
return value.copy()
def _build(self, frame: np.ndarray, flow: np.ndarray | None, produce_s: float,
flow_s: float, hhd_s: float, t_cap: float) -> FrameSnapshot:
flow_s: float, hhd_s: float, t_cap: float,
source_metadata: Any = None) -> FrameSnapshot:
sensor = self.sensor
info = sensor.info
include = self._include
@@ -641,6 +667,7 @@ class StreamPipeline:
flow_ms=flow_s * 1000.0,
hhd_ms=hhd_s * 1000.0,
image_size_raw=image_size_raw,
source_metadata=source_metadata,
)
def _fps(self) -> float: