网络相机图传与多相机集中管理系统

相机采集端

封装工业相机发现、连接、取流及参数读写流程,按设备编号维护采集上下文;处理连续采集、外部触发和软件触发模式,将相机回调输出的图像交由处理线程。

  1. 发现是枚举设备;连接是取得设备句柄;取流是启动采集并接收图像;参数读写是通过设备接口读取或修改真实参数。
  2. 每台设备一份 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
  1. 半包就是没收全,粘包就是多条业务报文同时在缓冲中;连续报文指完整一条后循环看下一条,最后不完整的留到下次。

    损坏恢复:检测到非法类型、异常长度或无法解析的包体,立即停止处理,断开并清空状态。重连后重新订阅、收到参数并等 IDR。若需原连接内重同步时,需扩展协议(魔数)再进行校验。

  2. 状态机两个: WaitHeader → WaitBody → WaitHeader,确认完整头,检查类型和长度,再等完整体;处理完消费整条,继续解析,数据不够就退出本轮回调。

图像编解码

基于 FFmpeg 完成相机图像格式转换、编码压缩、码流解析和客户端解码;按照实时预览场景配置帧间预测与编码缓存,并随图像报文传递编码参数、关键帧标记及显示时间戳。

  1. 相机格式转 BGRA 转 YUV420P, H264 编解码,

  2. 低延迟参数

    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); // 选择快速预设
  3. 码流解析和客户端解码:收到压缩数据后,自定义协议负责解析,FFmpeg 负责解码

  4. 实时预览希望图像早点出来,因此我们的方案是:关闭 B 帧,保留 P 帧预测。这样减少等待,同时仍然能利用相邻画面的重复信息压缩。并配置 IDR 帧间隔。

  5. 配置编码缓存:尽量减少编码器为了做决策而等待后续图片。
    编码器为了压缩得更好,可以先攒几张后续图片,看完再决定怎么编码。这叫“前瞻”。但预览时,攒图就会增加等待。例如相机每秒出 30 张图,每张间隔约 33 毫秒;如果必须等待后面 3 张图,单是等它们到达就约需 100 毫秒。
    所以我们采用低延迟调优,减少前瞻和重排序等待。代价是压缩效率承压:相同画质下通常需要更多码率。

    参数 FFmpeg 代码 作用
    B 帧缓存 max_b_frames = 0 不使用 B 帧,减少重排序等待
    前瞻缓存 rc-lookahead = 0 减少编码器提前缓存未来帧进行分析
    同步前瞻 sync-lookahead = 0 关闭 x264 的同步前瞻缓冲
    编码器延迟策略 tune = zerolatency 启用 x264 的一组低延迟配置
  6. “随图像报文传递编码参数”:先把解码所需的说明交给客户端。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 的最新帧槽显示。”

图像处理流水线

将相机取流、格式转换、编码和网络发送拆分为独立线程,通过环形缓冲传递图像对象;使用智能指针管理帧内存,缓冲区积压时清理旧帧并保留当前图像。

  1. 原图→转换、转换→编码都用 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

  1. 图像行距:在内存中,从这一行的开头,到下一行的开头,要跨过多少字节。FFmpeg 中叫 linesize
    setRowLength(frame->linesize[n]);应逐个平面设置自己的行距,不能 Y/U/V 都使用第一平面的值。

    平面 有效宽高 每行有效字节 实际行距(示例)
    Y 640×480 640 linesize[0] = 672
    U 320×240 320 linesize[1] = 352
    V 320×240 320 linesize[2] = 352
    1
    2
    3
    // 处理正行距的 8 位单通道 Y 平面。
    upload.setAlignment(1); // 不让默认四字节对齐改变源行跨度。
    upload.setRowLength(frame->linesize[0]); // 当前单通道格式一像素一字节。

    rowLength:告诉 OpenGL,源数据一行占多少个像素位置,包括填充。
    alignment=1:计算行跨度时,不再额外向上凑成 4 字节的倍数。

  2. 等比缩放取 s=min(窗口宽/图宽, 窗口高/图高),显示尺寸为 sW×sH,放在窗口正中央,其余区域清黑。

相机管理功能

基于 Qt 开发设备列表、通道预览、曝光增益配置、触发控制和图像抓拍功能;通过数据模型维护设备在线状态和采集参数,将界面操作封装为控制请求,并根据设备应答更新显示状态。

  1. 数据模型就是管理端内存里的一份“设备状态表”。例如:
    设备编号 在线状态 采集状态 实际曝光 实际增益 操作状态
    7 在线 正在采集 1000 μs 2 空闲
    8 离线 已停止 上次确认值 上次确认值 禁止操作
    Qt 中选择 QAbstractTableModel 管理这些记录,QTableView 从模型读取数据负责展示。设备上线、离线或参数变化时,更新对应记录,再通知界面刷新。网络模块将解析后的设备状态或控制应答通过信号发出,使用 Qt::QueuedConnection 将更新投递到 GUI 线程。模型槽函数修改对应设备记录,并发送信号通知视图刷新,整体使用的 MVC 架构。
    1
    2
    3
    4
    5
    6
    7
    8
    struct CameraState {    // 每台设备保存一份状态。
    uint32_t deviceId; // 设备编号,确定这份记录属于哪台相机。
    bool online; // 服务端报告的设备在线状态。
    bool grabbing; // 设备确认的采集状态。
    double exposureUs; // 设备回读确认的曝光值。
    double gain; // 设备回读确认的增益,单位由适配层约定。
    bool setting; // 是否正在等待参数设置结果。
    };

优化处理

将编码后的图像数据封装为只读共享对象,各观看端仅维护发送位置,控制分发时重复复制码流;按连接状态清理过期帧,并将控制报文与图像报文分队列处理,改进大帧发送阻塞相机参数指令问题。