网络相机图传与多相机集中管理系统
网络相机图传与多相机集中管理系统
相机采集端
封装工业相机发现、连接、取流及参数读写流程,按设备编号维护采集上下文;处理连续采集、外部触发和软件触发模式,将相机回调输出的图像交由处理线程。
- 发现是枚举设备;连接是取得设备句柄;取流是启动采集并接收图像;参数读写是通过设备接口读取或修改真实参数。
- 每台设备一份
CameraContext:设备编号、SDK 句柄、状态、采集模式、实际参数、图像格式、帧号、原图队列和停止状态。以设备序列号建立稳定的deviceId映射。设备状态固定为Closed → Opened → Grabbing → Stopping → Opened,失败进入Failed。同设备的 SDK 开停与参数命令交给采集控制执行线程串行处理;SDK 图像回调只做数据交接,不与网络线程并发修改句柄。
为什么不能把重活都放在回调里?模式 谁决定出图 例子 连续采集 相机按设置节奏持续出图 调焦时持续看画面 外部触发 外部信号满足触发条件 光电传感器检测到零件 软件触发 软件发触发命令 点击“拍一张”
回调像相机交货的窗口。转换、编码、网络发送耗时不稳定,如果占着回调不返回,会延迟 SDK 调度或缓冲归还,后续图像可能堆积或丢失。耗时编码与不可预测的网络等待会占住 SDK 的交付线程、延迟缓冲归还。
回调只做:检查结果 → 记录元数据 → 取得可靠内存所有权 → 放入有界队列 → 返回。
图像传输协议
设计控制报文与图像报文格式,定义消息类型、设备编号、负载长度、帧序号和时间戳;通过接收缓冲区与解析状态机处理半包、粘包及连续报文,使用请求编号关联控制应答。
TCP 字节流+固定 34 字节公共头+变长包体。整数使用大端序逐字段序列化。
图像包体固定为“1 字节关键入口标记(IDR)+一张完整 H.264 图像的访问单元(Access Unit)”
| 字段 | 类型/字节数 | 作用 | 使用约定 |
|---|---|---|---|
消息类型 type |
uint16/2 | 区分操作与数据 | 设曝光、应答、流重置、编码参数、图像等 |
设备编号 deviceId |
uint32/4 | 标明目标相机 | 设备稳定标识 |
负载长度 payloadLength |
uint32/4 | 包体长度,不含公共头 | 总长度=34+负载长度 |
帧序号 frameSeq |
uint64/8 | 标识图像 | 控制消息不用时置 0 |
时间戳 timestamp |
uint64/8 | 图像媒体时间 | 本次采集开始后的微秒数;控制不用时置 0 |
请求编号 requestId |
uint64/8 | 关联控制请求与应答 | 请求从 1 递增,普通图像置 0 |
半包就是没收全,粘包就是多条业务报文同时在缓冲中;连续报文指完整一条后循环看下一条,最后不完整的留到下次。
损坏恢复:检测到非法类型、异常长度或无法解析的包体,立即停止处理,断开并清空状态。重连后重新订阅、收到参数并等 IDR。若需原连接内重同步时,需扩展协议(魔数)再进行校验。
状态机两个:
WaitHeader → WaitBody → WaitHeader,确认完整头,检查类型和长度,再等完整体;处理完消费整条,继续解析,数据不够就退出本轮回调。
图像编解码
基于 FFmpeg 完成相机图像格式转换、编码压缩、码流解析和客户端解码;按照实时预览场景配置帧间预测与编码缓存,并随图像报文传递编码参数、关键帧标记及显示时间戳。
相机格式转 BGRA 转 YUV420P, H264 编解码,
低延迟参数
1
2
3
4
5// 源码节选:可复用的低延迟配置意图,不能当作实测性能。
codecContext_->gop_size = 30; // 当前关键帧间隔1秒一次 IDR
codecContext_->max_b_frames = 0; // 当前关闭 B 帧
av_opt_set(codecContext_->priv_data, "tune", "zerolatency", 0); // 请求低延迟调优
av_opt_set(codecContext_->priv_data, "preset", "ultrafast", 0); // 选择快速预设码流解析和客户端解码:收到压缩数据后,自定义协议负责解析,FFmpeg 负责解码
实时预览希望图像早点出来,因此我们的方案是:关闭 B 帧,保留 P 帧预测。这样减少等待,同时仍然能利用相邻画面的重复信息压缩。并配置 IDR 帧间隔。
配置编码缓存:尽量减少编码器为了做决策而等待后续图片。
编码器为了压缩得更好,可以先攒几张后续图片,看完再决定怎么编码。这叫“前瞻”。但预览时,攒图就会增加等待。例如相机每秒出 30 张图,每张间隔约 33 毫秒;如果必须等待后面 3 张图,单是等它们到达就约需 100 毫秒。
所以我们采用低延迟调优,减少前瞻和重排序等待。代价是压缩效率承压:相同画质下通常需要更多码率。参数 FFmpeg 代码 作用 B 帧缓存 max_b_frames = 0 不使用 B 帧,减少重排序等待 前瞻缓存 rc-lookahead = 0 减少编码器提前缓存未来帧进行分析 同步前瞻 sync-lookahead = 0 关闭 x264 的同步前瞻缓冲 编码器延迟策略 tune = zerolatency 启用 x264 的一组低延迟配置 “随图像报文传递编码参数”:先把解码所需的说明交给客户端。SPS、PPS 中,是“这一路压缩图像的解码说明”。
同一条 TCP 连接按下面的顺序发送:编码参数消息 → IDR 图像 → 后续图像……
把“编码参数消息”命名为 CodecConfig。它通过已有的消息类型字段区分,包体里放 SPS/PPS 等内容。客户端先用参数初始化解码器,再解码图像。
“采集到的 BGRA 转为 YUV420P 后交给 FFmpeg 编码 H.264,采用无 B 帧、闭合 GOP 和低延迟调优。发送端把 SPS/PPS 装进编码参数消息,把完整 Annex B 图像装进图像消息;两类消息与控制请求共用六字段协议和一条 TCP 连接,控制、媒体分别排队。客户端先按长度拆包,控制应答匹配 requestId,媒体按 deviceId 路由;用参数初始化解码器,等到 IDR 后将图像负载构造成 AVPacket,用 send_packet/receive_frame 解码为 YUV,再交给 GUI 的最新帧槽显示。”
图像处理流水线
将相机取流、格式转换、编码和网络发送拆分为独立线程,通过环形缓冲传递图像对象;使用智能指针管理帧内存,缓冲区积压时清理旧帧并保留当前图像。
- 原图→转换、转换→编码都用
vector<shared_ptr<Frame>>固定槽数组,加mutex与condition_variable;容量各为 2,关闭状态和索引受同一把锁保护。编码完成对象也经过有界环形队列交给网络线程,每路初始上限 8 个任务、16 MiB,不能绕开它另建无界任务列表。
图像通道分发
服务端按照设备和通道维护发布订阅关系,客户端接入后先获取编码参数,再从关键帧开始接收码流。
① 按 deviceId 找到设备的预览通道,建立订阅;
② 参数就绪后不投递普通 P 帧,向发布端请求下一张 IDR。
③ 遇到真实 IDR 后,按“参数(SPS/PPS) → IDR → 后续帧”顺序入队,进入 Streaming
同一相机图像仅执行一次采集与编码,再根据订阅关系分发至管理端。
画面预览
集成 OpenGL 渲染组件,将解码后的 YUV 图像按平面上传至纹理,通过着色器完成颜色空间转换;处理图像行距、纹理尺寸、等比缩放及解码线程与界面线程间的帧投递。
固定输出 8 位、偶数宽高的 YUV420P。Y 平面存亮度,宽高为 W×H;U、V 存色度,各为 W/2×H/2。总像素字节数约 1.5WH,实际分配还要计算行尾填充。三张单通道纹理分别放 Y、U、V,片段着色器在每个显示位置采样并还原 RGB
图像行距:在内存中,从这一行的开头,到下一行的开头,要跨过多少字节。FFmpeg 中叫
linesizesetRowLength(frame->linesize[n]);应逐个平面设置自己的行距,不能 Y/U/V 都使用第一平面的值。平面 有效宽高 每行有效字节 实际行距(示例) Y 640×480 640 linesize[0] = 672U 320×240 320 linesize[1] = 352V 320×240 320 linesize[2] = 3521
2
3// 处理正行距的 8 位单通道 Y 平面。
upload.setAlignment(1); // 不让默认四字节对齐改变源行跨度。
upload.setRowLength(frame->linesize[0]); // 当前单通道格式一像素一字节。rowLength:告诉 OpenGL,源数据一行占多少个像素位置,包括填充。alignment=1:计算行跨度时,不再额外向上凑成 4 字节的倍数。等比缩放取
s=min(窗口宽/图宽, 窗口高/图高),显示尺寸为sW×sH,放在窗口正中央,其余区域清黑。
相机管理功能
基于 Qt 开发设备列表、通道预览、曝光增益配置、触发控制和图像抓拍功能;通过数据模型维护设备在线状态和采集参数,将界面操作封装为控制请求,并根据设备应答更新显示状态。
- 数据模型就是管理端内存里的一份“设备状态表”。例如:
Qt 中选择设备编号 在线状态 采集状态 实际曝光 实际增益 操作状态 7 在线 正在采集 1000 μs 2 空闲 8 离线 已停止 上次确认值 上次确认值 禁止操作 QAbstractTableModel管理这些记录,QTableView从模型读取数据负责展示。设备上线、离线或参数变化时,更新对应记录,再通知界面刷新。网络模块将解析后的设备状态或控制应答通过信号发出,使用Qt::QueuedConnection将更新投递到 GUI 线程。模型槽函数修改对应设备记录,并发送信号通知视图刷新,整体使用的 MVC 架构。1
2
3
4
5
6
7
8struct CameraState { // 每台设备保存一份状态。
uint32_t deviceId; // 设备编号,确定这份记录属于哪台相机。
bool online; // 服务端报告的设备在线状态。
bool grabbing; // 设备确认的采集状态。
double exposureUs; // 设备回读确认的曝光值。
double gain; // 设备回读确认的增益,单位由适配层约定。
bool setting; // 是否正在等待参数设置结果。
};
优化处理
将编码后的图像数据封装为只读共享对象,各观看端仅维护发送位置,控制分发时重复复制码流;按连接状态清理过期帧,并将控制报文与图像报文分队列处理,改进大帧发送阻塞相机参数指令问题。

