多相机同步采集、推流与 host 侧可视化工具集,支持 RealSense 与 Hikrobot USB3 Vision 相机。
当前这套流程已经收敛为:
- 容器内自动探测当前在线相机(RealSense / Hikrobot)
- 自动生成运行时配置
- 对齐多路时间戳后推流到 host
- host 侧可用 OpenCV 实时查看
- host 侧代码可直接拿到同步后的
frames, timestamp
下一阶段的数据面迁移约束已经冻结在 docs/zmq-transport-contract.md:
- HTTP 继续承担 control-plane 和 preview
- ZMQ multipart 作为后续 aligned-set data-plane
- 迁移期间
/api/latest-set仅保留兼容语义,不再作为长期目标
进入仓库:
cd /home/corenetic/Code/sensor_proto启动推流服务:
make stream-up打开实时多路预览:
make stream-viewer用 Rerun 可视化一个已录好的 episode:
make episode-rerun EPISODE=artifacts/lerobot/hw-10s-episode-20260312T134201抓取当前最新一组同步帧:
make stream-shot录制一条目标为 300 个对齐帧的 LeRobot v3 episode(约等于 10 秒 @ 30fps):
make stream-record-10s查看推流日志:
make stream-logs停止推流服务:
make stream-downmake stream-up 会自动:
- 启动
sensor-stream容器 - 探测当前有效连接的 RealSense
- 提取序列号、型号、USB 端口等关键信息
- 自动生成运行时配置
- 在 host 暴露
http://127.0.0.1:8787
默认会按当前在线相机数自适应启动,不要求必须满 8 台。
如果要显式验证 ZMQ data-plane,可直接把运行时模板切到
configs/realsense-8cam-zmq-session.json;
这份模板会保留 HTTP control-plane / preview,同时开启 transport.enabled=true。
主入口在 stream_client.py。
最常用接口:
from sensor_proto.stream_client import AlignedStreamClient
client = AlignedStreamClient("http://127.0.0.1:8787")
frames, timestamp = client.get_latest_aligned_frames()
frame_rs00 = frames["rs-00"] # numpy.ndarray, BGR
print(timestamp, frame_rs00.shape)这里的语义是:
frames:dict[camera_id, numpy.ndarray]timestamp: 该同步帧集的统一参考时间
注意:
- 这是同步后的统一
timestamp - 不是每路各自一份
timestamps - 当前是软同步,所以各路仍可能存在小的
offsets_ms
如果你还需要诊断信息,用详细接口:
from sensor_proto.stream_client import AlignedStreamClient
client = AlignedStreamClient("http://127.0.0.1:8787")
aligned = client.get_latest_aligned_set()
print(aligned.set_id)
print(aligned.timestamp)
print(aligned.skew_ms)
print(aligned.offsets_ms)
print(aligned.device_timestamps_ms)详细接口返回:
set_idtimestampframesoffsets_msdevice_timestamps_msskew_mscamera_orderraw_payload
如果启用了 ZMQ data-plane,host 侧还可以使用最小阻塞客户端:
from sensor_proto.stream_client import ZmqAlignedStreamClient
client = ZmqAlignedStreamClient("tcp://127.0.0.1:5555")
aligned = client.recv_aligned_set(timeout_ms=1000)
print(aligned.set_id, aligned.camera_order)当前决策是:
make stream-up继续启动单个sensor-stream进程- 该进程同时承载 HTTP control-plane / preview 与 ZMQ data-plane
- 是否启用 ZMQ 由运行时
transport.enabled控制 /api/latest-set和逐帧 BMP endpoint 现阶段只保留 debug fallback,不再作为长期数据面目标
实时 viewer 入口在 stream_viewer.py。
直接使用:
make stream-viewer行为:
- 自动按网格布局显示多路相机
- 自动 resize,尽量完整铺进屏幕
- 顶部显示
set_id / timestamp / skew_ms / camera_count - 每个子画面显示
camera_id / offset - 按
q或ESC退出
viewer 当前的数据面选择规则:
- 默认
auto - 如果
/api/health广播了启用中的 ZMQ transport,则 viewer 直接消费 ZMQ aligned-set data-plane 并在 host 侧渲染网格 - 否则回退到现有 HTTP preview mosaic
命令:
make episode-rerun EPISODE=artifacts/lerobot/hw-10s-episode-20260312T134201底层脚本是:
bash scripts/run_episode_rerun.sh artifacts/lerobot/hw-10s-episode-20260312T134201行为:
- 脚本运行在 host
- 输入是一个已经落盘完成的 LeRobot v3 episode 目录
- 自动从
meta/info.json和videos/发现相机和视频文件 - 如果 episode 带有
meta/aligned_timestamps.json,则优先按真实 aligned-set 时间轴重播 - 否则回退到 MP4 自带的名义帧时间轴
- 默认会自动拉起本地 Rerun viewer
命令:
make stream-shot底层使用 stream_client_cli.py,会把当前最新同步帧集保存到:
CLI 当前的数据面选择规则也默认为 auto:
- 若健康检查显示 ZMQ data-plane 已启用,则优先从 ZMQ 接收 aligned set
- 否则回退到 HTTP
/api/latest-set - 也可以显式传
--transport http|zmq
推流服务启动后,运行时配置会写到:
这里能看到:
- 当前参与推流的相机列表
- 每台相机的
serial - 自动识别到的
model device_inventory
如果同事想确认“这次到底连了哪几台”,优先看这个文件。
- Docker
docker compose- RealSense 设备访问权限
host 侧 viewer 和 Python client 依赖:
numpycv2pyzmq(仅在使用 ZMQ data-plane 时需要)uvffmpeg可执行文件,版本需>= 5.1
基准工具入口在 transport_benchmark.py:
PYTHONPATH=src python -m sensor_proto.transport_benchmark --base-url http://127.0.0.1:8787 --transport http
PYTHONPATH=src python -m sensor_proto.transport_benchmark --base-url http://127.0.0.1:8787 --transport zmq真实双机部署和 LAN benchmark 步骤见 docs/dual-machine-zmq-benchmark.md。
当前已经跑过的 mock / 硬件 / 正式镜像验证快照见 docs/zmq-transport-validation-snapshot-2026-03-13.md。
如果要用 make episode-rerun,host 上需要先有 ffmpeg >= 5.1。
注意:Ubuntu 22.04 系统仓库自带的 ffmpeg 4.4.x 不够新,Rerun 仍会拒绝解码。
脚本会在启动前检查版本;如果你的新版 ffmpeg 不在默认 PATH 里,可以显式指定:
SENSOR_PROTO_RERUN_FFMPEG_PATH=/path/to/ffmpeg make episode-rerun EPISODE=artifacts/lerobot/<episode_dir>Ubuntu 22.04 若临时只想装系统包,可以先装上旧版用于其他工具:
sudo apt-get install -y ffmpegPython 侧的 replay 依赖已经写在项目 extras 里:.[replay]。
make episode-rerun 启动前还会把 Rerun 的本地持久化配置同步到当前 host 的 ffmpeg 路径,避免 viewer 自己的 spawn/config 链拿不到系统 PATH 时仍然报 “Couldn't find an installation of the FFmpeg executable”。
仓库已经通过脚本和 make 目标固化了必要环境变量,不需要手写长串命令。
如果同事用不起来,按这个顺序查:
make stream-logs- 看 realsense-8cam-stream-runtime.json
- 运行
make stream-shot - 最后再看 GUI 显示问题
常见判断:
make stream-shot成功,make stream-viewer失败 说明同步流和 host client 是通的,问题在 GUI 显示链路127.0.0.1:8787连不上 先看make stream-logs- 相机数量不是
8默认允许按当前在线相机数自适应启动
最短入口:
make stream-record-10s底层实际调用的是:
bash scripts/record_stream_episode.sh 10这个脚本会:
- 以 realsense-8cam-stream.json 作为模板
- 在启动前临时注入
recording配置,但不写死真实相机列表 - 启动时自动探测当前在线 RealSense,并生成运行时配置
- 以
300个对齐帧作为默认停止条件(约等于10秒 @30fps) - 通过独立 recording worker + bounded queue 落盘,避免 LeRobot 写盘直接阻塞主同步消费链
- LeRobot video 路径优先启用流式编码,避免默认的 per-frame PNG 临时文件落盘
- 默认在 recording 队列打满时将 recording 标记为 failed,但保持 stream 服务继续运行
- 如果本次批量录制过程中 recording 已失败,命令会在 shutdown 后以非零状态退出,避免误报成功
- 给 LeRobot v3 的视频编码和
finalize()预留足够的收尾时间 - 另外保留一个独立 watchdog,防止硬件异常时无限挂住
默认产物:
- 数据集输出到
artifacts/lerobot/hw-10s-episode-<timestamp> - 运行时配置输出到 realsense-8cam-stream-recording-runtime.json
- 真实对齐时间轴输出到
meta/aligned_timestamps.json,供 host 侧 replay 保持实际采集节奏
默认 recording 队列配置:
recording.queue_maxsize = 32recording.overflow_policy = fail_recording_keep_stream
可选环境变量:
SENSOR_PROTO_RECORD_OUTPUT_DIRSENSOR_PROTO_RECORD_FINALIZE_GRACE_SSENSOR_PROTO_RECORD_MAX_RUNTIME_SSENSOR_PROTO_RECORD_TARGET_ALIGNED_SETSSENSOR_PROTO_RECORD_FPSSENSOR_PROTO_RECORD_QUEUE_MAXSIZESENSOR_PROTO_RECORD_OVERFLOW_POLICYSENSOR_PROTO_RECORD_VIDEO_CODECSENSOR_PROTO_RECORD_ENCODER_QUEUE_MAXSIZESENSOR_PROTO_RECORD_ENCODER_THREADSSENSOR_PROTO_RECORD_TASKSENSOR_PROTO_RECORD_REPO_IDSENSOR_PROTO_RECORD_ROBOT_TYPE
运行测试:
make test运行 mock:
make mock-run当前支持把同步后的多相机观测直接录制为本地 LeRobot v3 数据集。
前提:
- 运行环境已安装官方
lerobot包 - 参与录制的相机都启用了
capture_image_data - 如果多路相机
fps不一致,需要在配置里显式设置recording.fps
最小 mock 示例配置见 mock-lerobot-recording.json。
启动录制:
source $HOME/.local/bin/env
UV_CACHE_DIR=/tmp/uv-cache PYTHONPATH=src uv run --no-project --python "$(command -v python3)" python -m sensor_proto.stream_main --config configs/mock-lerobot-recording.json说明:
- 录制入口复用同步流服务,不走 host 侧 HTTP 轮询
- 一次进程生命周期默认保存为一个 episode
- 使用
Ctrl-C退出时会执行save_episode()和finalize() - 默认输出目录由配置里的
recording.root_dir控制,例如artifacts/lerobot/mock-session
make hikrobot-stream-up # 启动流服务
make hikrobot-stream-logs # 查看容器日志
make hikrobot-stream-shot # 抓取最新同步帧
make hikrobot-stream-down # 停止流服务
make hikrobot-stream-record-10s # 录制 10s LeRobot v3 episode两台相机都应连在 USB 3.x 口(5000Mbps),否则带宽不够:
for dev in $(find /sys/bus/usb/devices -maxdepth 1 -name "[0-9]*" -not -name "*:*" 2>/dev/null); do
vendor=$(cat "$dev/idVendor" 2>/dev/null)
if [[ "$vendor" == "2bdf" ]]; then
serial=$(cat "$dev/serial" 2>/dev/null)
speed=$(cat "$dev/speed" 2>/dev/null)
busnum=$(cat "$dev/busnum" 2>/dev/null)
devpath=$(cat "$dev/devpath" 2>/dev/null)
echo "Serial=$serial Bus=$busnum Path=$devpath Speed=${speed}Mbps"
fi
done期望输出(两台均为 5000Mbps,且在同一 Bus):
Serial=DA5404760 Bus=2 Path=2.4 Speed=5000Mbps
Serial=DA5404769 Bus=2 Path=2.3 Speed=5000Mbps
若某台显示 480Mbps,则该相机插在 USB 2.0 口,需换插 USB 3.0 蓝色口。
make hikrobot-stream-logs— 确认无OpenDevice failed错误curl -s http://localhost:8787/api/health | python3 -m json.tool— 查看published_sets是否在增长make hikrobot-stream-shot— 抓帧,确认两台相机都有图像
| 现象 | 原因 | 处理 |
|---|---|---|
OpenDevice failed: 0x80000301 |
两相机并发打开,MVS SDK 不线程安全 | 已修复(全局锁序列化打开) |
| 流 stall,无 aligned sets | 两相机时钟漂移超出 45ms 同步窗口 | 已修复(15s watchdog 自动重启相机会话) |
| 视频快进 | exposure_us 超过帧周期(30fps 最大 ~33000µs) |
检查 configs/hikrobot-stream.json 中 exposure_us |
| 某相机 480Mbps | 接在 USB 2.0 口,带宽不足 | 换插 USB 3.0 口 |