摄像头监控应用程序的优化要点
1、背景
本文是对前段时间一个摄像头项目的应用层作一些思路和方向上的整理归纳, 提炼出若干要点(尤其是性能优化方面)以作备忘之用,也供后续同类项目借鉴、复用。
文本以自然语言描述为主,并且由于是商业项目,源码不能公开, 但有部分与具体业务无关的逻辑会陆续封装成通用接口并开源。
2、采集接口
2.1 OpenCV
-
其实是基于后面几种方案的封装,而且速度可能较慢。
- 若项目满足以下一个或多个特点,则可考虑此方案:
- 项目初期的快速开发和功能验证。
- 对速度没有太高要求。
- 兼容多个平台(
Linux
、Mac
、Windows
等)。
- 用到的接口类:
cv::VideoCapture
2.2 V4L2
-
Linux
系统提供的原生接口。若项目仅运行于Linux
,且对速度有极高要求, 以及需要使用底层接口进行某些精细控制,则可采用此方案。 - 提供
3
种流式输入/输出(Streaming I/O
)接口:- 内存映射(
Memory Mapping
,mmap
):最常用,适用性最好, 且效率也很高(除dmabuf
外),在绝大多数项目中推荐使用。 - 直接内存访问缓冲区(
Direct Memory Access Buffer
,dmabuf
): 速度最快,零拷贝,数据直接在硬件设备间(例如GPU
与摄像头ISP
之间)传递 (通过文件描述符,即fd
),无需CPU
干预,显著降低CPU
负载和内存带宽占用, 但缺点是通用性较差,依赖硬件DMA
支持,且缓冲区管理复杂度较高。 - 用户指针(
User Pointer
,userptr
):由用户来管理缓冲区, 此缓冲区可能是共享内存也可能不是,所以运行效率不能一概而论。 这种接口一般很少用。
- 内存映射(
-
在用法上,是基于一系列的
ioctl
接口,步骤有点繁琐。 - 可手动安装
libv4l-dev
库,但此库只是薄封装,仍然不够方便, 感兴趣的可自行到以下头文件去查阅接口:/usr/include/libv4l2.h
/usr/include/libv4lconvert.h
- 根据实际项目的常见需求,另行封装了一个通用接口,
详见“懒编程秘笈”项目的
c_and_cpp/native/camera_v4l2.{c,h}
文件。
2.3 GStreamer
待补充。
2.4 FFmpeg
待补充。
3、数据解码
-
首先是指将摄像头常用的采集格式(例如
NV12
、MJPG
、Bayer Raw
)转换成 便于进行后续处理的内存表示格式(一般是红绿蓝三原色表示法,若有需要再加一个透明通道)。 -
其次要考虑内存表示格式中每个像素点内部数据的次序, 例如对于红绿蓝三原色表示法是是采用
BGR
还是RGB
次序。 - 接下来选择合适的库。常见的选择有:
libv4l-dev
:功能较弱,使用不便,不推荐。OpenCV
:计算机视觉领域的著名工具库,接近事实标准,能处理大多数情况, 且接口易用、资料丰富,在特定条件下还能使用硬件加速,推荐使用。FreeImage
:也是一个很强大的图像格式转换库,可补充OpenCV
的不足, 不一定在这个环节使用,仅在此列举作为备忘之用。- 特定硬件平台的加速接口:例如瑞芯微系列芯片有
RGA
库,具体要查阅官方文档, 并且要进行实测对比。
-
最后一个考虑是缓冲区的预分配。由于解码前后的数据量通常是可预估的(
MJPG
除外), 所以最好先明确分配好缓冲区,后续反复使用,消除每次解码不必要的内存分配耗时。 顺便一提,OpenCV
的cv::Mat
类对象的构造接口支持传入用户提前分配好的缓冲区指针, 在实际编程中注意选用即可。 - 作为备忘,这里顺便给出显示
NV12
图像的命令:$ # 示例图像的尺寸是640x480。注意若是多帧则-fps选项需按实际帧速率指定,单帧则不关心或不必指定。 $ mplayer test_image.nv12 -loop 0 -demuxer rawvideo -fps 30 \ -rawvideo w=640:h=480:size=$(echo "" | awk '{ print 640 * 480 * 1.5 }'):format=NV12
4、数据搬移
4.1 图像数据量的估算
以1280
x960
大小(若无特别说明,后文的所有说明都使用此值)的图像为例:
图像格式 | 单帧数据量 | 每秒数据量(假设15fps的速率) |
---|---|---|
NV12 (摄像头原始格式) |
1.75MB | 26.37MB |
RGB (内存格式) |
3.52MB | 52.73MB |
JPEG (储存或传输格式) |
48KB | 700KB |
对于前2
种格式,即使降低对尺寸的要求,将宽高均缩减一半变为640
x480
,则以上数据除以4
,
得出的数据量依然很可观,处理器和内存的压力仍然很大,所以在各个环节的处理中,
如无必要,不要随便复制数据。
如果用于储存或传输的格式不采用JPEG
,其他格式
很难同时满足高压缩率
和低耗时
的要求,
最后的结果要么是转换超时,要么是数据量太大致使网卡压力爆棚而频繁丢包,详见后文专门章节的分析。
4.2 内存速率的估算(适用于内存内部的复制操作)
-
查询时钟速率:例如
DDR4
内存的时钟速率范围通常在2133MHz
至3200MHz
区间,LPDDR4
的则为2666MHz
至4266MHz
。注意此处所用的单位是MHz
,现在慢慢被MT/s
代替, 后者的T
表示Transfer
或Transmission
,更强调实际的传输频率而非原始时钟频率 ——之所以产生这样的区别,是因为早期SDRAM
(Synchronous Dynamic Random Access Memory
, 同步动态随机访问储存器)的数据传输仅发生在时钟周期的上升沿,而本世纪初出现的DDR SDRAM
(其中DDR
表示Double Data Rate
)则在时钟上升沿和下降沿均可传输, 因此MHz
已不能等同于实际的传输速率,改用MT/s
会更合适。显然,1MHz
=2MT/s
。 不过,无论是MHz
还是MT/s
,都不是最终的速率。 -
查询位宽(即总线宽度):不同代的内存(例如
DDR3
与DDR4
)、标压内存与低压内存 (例如DDR4
与LPDDR4
)的位宽可能也会有不同。例如DDR4
的位宽通常为64
位, 部分可达到72
位,而LPDDR4
则一般为16
或32
位。 -
查询内存控制器的通道数:单通道为
1
,双通道为2
,四通道为4
,八通道为8
。 前两个多用于消费级产品,后两个多用于企业级服务器。 -
了解行地址切换延迟(如
tRCD
、tRP
):这对于随机访问速率影响较大。 对于DDR4
,延迟通常在10ns
至16ns
之间;对于LPDDR4
,通常是15ns
至20ns
。 -
顺序访问与随机访问的速率有很大的差异:前者由于(邻近数据)
预取
机制, 且行地址切换较少,速率可达到理论带宽的70%
到90%
;后者则由于频繁切换行地址, 速率通常低于理论带宽的30%
,极端情况下甚至只有10%
。 由于图像数据通常存放在大范围连续内存,所以顺序访问操作占大多数, 在实际中可取顺序访问速率作近似估算。 -
计算理论带宽:
- 公式:
带宽
=时钟频率
x2
x位宽
x通道数
/8
- 举例:频率为
1600MHz
的双通道64
位宽DDR4
内存理论带宽 =1600MHz
x2
x64
x2
/8
=51.2GB/s
- 单位说明:
- 在半导体领域,
K
、M
、G
的倍数是1000
而不是1024
, 但在估算时只关心数量级而非精确数值,所以影响不大,后文同理。 - 这里的
B
是字节(Byte
),1
字节等于8
个二进制位(Bit
,又叫比特), 与计算机领域的换算关系一样。
- 在半导体领域,
- 测试命令:待补充。
- 公式:
作为小结,这里给出各代内存的(单通道)速率范围(数据由某度DeepSeek
提供,
不保证绝对准确,但可作为参考):
内存类型 | 传输速率范围(MT/s ) |
理论带宽范围(GB/s ) |
顺序访问速率范围 | 随机访问速率范围 |
---|---|---|---|---|
DDR1 |
200 ~400 |
1.6 ~3.2 |
1.1 ~2.8 |
0.05 ~0.3 |
DDR2 |
400 ~1200 |
3.2 ~9.6 |
2.2 ~8.5 |
0.1 ~0.8 |
DDR3 |
800 ~2133 |
6.4 ~17.0 |
4.5 ~15 |
0.2 ~1.5 |
DDR4 |
1600 ~3200 |
12.8 ~25.6 |
9 ~25 |
0.4 ~3 |
注意:上表的随机访问速率范围有争议,因为从DDR2
开始加入预取
(Prefetch
)机制,
即DDR2
是4
位预取,DDR3
是8
位预取,以此类推,在特定的场景下确实能实现速率的翻倍,
但预取的数据在CPU
分支预测失败或内存地址非连续的情况下会用不上而需要重新获取,
因此在不考虑核心频率的提升以及其他方面的改进的情况下,各代DDR
的随机访问速率理应差别不大,
后续若找到更详细的文献再进行更新。
最后还要分清是标压
(DDRx
)内存还是低压内存
(LPDDRx
),因为工艺会影响部分参数指标,
进而影响最终的带宽值,例如DDR4
带宽范围一般是12.8GB/s
~25.6GB/s
,
LPDDR4
则为10.6GB/s
~17GB/s
。
4.3 内存与显存(或其他专用硬件的储存空间)之间的复制
-
显存:待补充。
-
RGA
硬件储存空间(以下内容摘自瑞芯微官方文档):- 可用的内存接口形式及复制效率从高到低为:
物理地址
>dmabuf_fd
>虚拟地址
。 考虑到易用性和性能,通常建议使用dmabuf_fd
。 实际
耗时与理论
耗时的差距:使用物理地址时,实际耗时大概是理论耗时的1.1
~1.2
倍,dmabuf_fd
则是1.3
~1.5
倍,虚拟地址则是1.8
~2.1
倍。- 以
RK3566
为例,以上3
种接口(在系统空载时)复制1280x720
大小的图像的耗时分别为:物理地址
需要2.0ms
,dmabuf_fd
需要2.2ms
,虚拟地址
需要2.6ms
。 - 总线优先级的影响:目前
RGA
在RK
平台的总线优先级为最低档, 当带宽资源较为紧张时(例如运行多路ISP
的场景),RGA
没法及时地读写DDR
内存数据, 就会产生了较大的延迟,最终产生较大的性能抖动。 - 以上只是数据复制的耗时,实际上对于数据本身的操作(例如缩放、裁剪、旋转等)也不是零耗时,
所以在很多场景中
RGA
硬件“加速”甚至不如使用CPU
来得快,因此建议通过实际的测试对比, 再决定是否使用RGA
接口。如果要用,也建议尽量使用improcess()
一次性执行多种操作。
- 可用的内存接口形式及复制效率从高到低为:
5、业务处理
- 图像缩放时要注意对长条状图片的处理:
- 此处的“长条状”只是相对而言,准确地说则是指,源图像的宽高均按照相同的比例进行缩放后,
宽等于目标图像的宽但高却过高(高瘦的竖直长条),或宽高情况对调(矮肥的水平长条)。
一个典型的场景是:摄像头采集到的图像是长方形,但
AI
模型窗口却是正方形。 此时若直接缩放,图像会变形失真,导致AI
推理结果不准确。 - 对于竖直长条:假设使用
OpenCV
的接口类cv::Mat
,则对于同一个目标缓冲区(uchar *data
), 需要为其构造两个cv::Mat
对象,一个使用目标图像的宽高作为参数,另一个则使用目标图像的高, 但宽要小于目标图像的宽(具体值可按两图像之高比例去换算),还需要明确指定步幅
(Step
, 以RGB
格式为例,则此值应为目标图像宽乘以3
)。这样操作下来, 最后得到一幅左侧是缩放后的内容、右侧是纯色(一般使用黑色,即一系列0
值)填充的图像。 - 对于水平长条:也需要两个
cv::Mat
对象,不过情况就简单一些,因为高要小于目标图像的高, 相当于缩放操作提前结束,最后得到的图像的填充区就位于下侧。 - 需要注意的是,水平长条还存在一种特殊情况,就是源图像的宽等于目标图像的宽, 那么就没必要缩放,直接复制甚至直接使用即可,能极大节省时间,不过这种情况非常少。
- 此处的“长条状”只是相对而言,准确地说则是指,源图像的宽高均按照相同的比例进行缩放后,
宽等于目标图像的宽但高却过高(高瘦的竖直长条),或宽高情况对调(矮肥的水平长条)。
一个典型的场景是:摄像头采集到的图像是长方形,但
-
尽量利用特殊比例缩放带来的优化:例如目标图像的宽高均为源图像的
1/2
、1/4
或1/8
时, 会触发特定的优化路径,cv::resize()
内部算法会按需启用SIMD
指令集(如SSE/AVX
)、 多线程并行处理、像素坐标权重预简化等措施,大大提升处理速度, 甚至比部分(注意不是全部)颜色空间转换操作(cv::cvtColor()
)还快。 -
仔细评估是否每一帧图像都需要处理:对于监控而言,每秒的帧数往往在
15
帧以上, 而且场景变化并不快,所以没必要每一帧都处理(指缩放、AI
推理等), 否则CPU
、NPU
、内存等部件的压力都比较大,收益也不大, 因此可以考虑每隔四五帧(帧数可在配置文件指定)处理一次,节省算力、带宽和能耗。 - 其他的图像操作未能在此一一列举,但有一个重要的参考原则就是尽量针对特定关注区域(
ROI
,Region of interest
)而非全图范围,以减少运算量,但常见的库已考虑到这一点, 所以实际问题不大。
6、数据压缩及编码
这一章节的压缩及编码针对的是独立帧,亦即每一帧独立处理,缺失任一帧都不会对其他帧造成影响,
与后面的视频保存
章节所用的编码方式不同。
由于压缩及编码极其消耗CPU
资源,所以使用不同的芯片,耗时也会不一样。
下面列举几种常见的压缩或编码方式在RK3588
中处理尺寸为1280
x960
的图像得出的结果,
其他芯片可根据其与RK3588
的性能差异作出大致估算:
压缩或编码方式 | 质量参数或压缩率 | 处理后的尺寸 | 耗时 | 编程接口 | 备注 |
---|---|---|---|---|---|
JPEG |
30 | 44 KB |
16 ms |
cv::imencode() |
数据量及耗时均比较理想,可用 |
JPEG |
90 | 142 KB |
19 ms |
cv::imencode() |
数据量及耗时偏大但仍可接受 |
PNG |
6 | 1.05 MB |
220 ms |
cv::imencode() |
数据量及耗时超大,不予考虑 |
WebP |
30 | 24 KB |
70 ms |
cv::imencode() |
数据量非常理想,但耗时不可接受 |
WebP |
75 | 34 KB |
80 ms |
cv::imencode() |
同上 |
ZLIB |
6 | 600 KB |
90 ms |
compress2() |
数据量较大,耗时也很大,不可用 |
LZ4 |
1 | 800 KB |
17 ms |
LZ4_compress_fast() |
耗时较理想,但数据量很大,不可用 |
LZ4 |
20 | 1.01 MB |
9 ms |
LZ4_compress_fast() |
耗时非常理想,但数据量超大,不可用 |
由上表可知,通用压缩算法用在图像上的效果并不理想,必须使用专门针对图像的编码算法。
而在这些图像编码算法当中,老牌的JPEG
毫无悬念胜出,很好地兼顾了数据量和处理耗时;
较新的WebP
虽然能产出更少的数据量和更好的图像质量,但目前多数芯片的算力仍未足够强大,
无法进一步压缩耗时,所以暂时无法应用于实时类任务,不过以后应该能慢慢实现。
另外,上表中当JPEG
质量参数为30%
时,会出现轻微的块状伪影
(Blocking Artifacts),
但对于室内图像(较暗、色彩不丰富、区分度不高)的视觉效果影响不大,可以使用。
若有更高要求,可提高到75%
,此值在数据量、耗时和图像质量方面均有很好的效果。
7、数据加密
可选项(并非所有场合都需要加密),待补充。
8、网络传输
对于需要在公网传输(链路质量不佳且多变)或者有跨品牌互联需求(需标准化或认证)的产品,
必须支持RTMP
或RTP
协议族,若有安全需求的还要支持SRTP
或相关的国标,
因此需要慎重选择成熟的库。
若仅在内网甚至端到端直连使用,且不需与其他品牌互操作,则开发难度和要求大大简化, 就算自定义通信协议、手动组包解包进行收发,都问题不大。以下内容正是重点针对这种场景, 但仍对前一种场景有参考意义:
-
首先决定传输层协议:由于本文不是详细设计文档,此处直接说结论: 控制面使用
TCP
(推荐)或UDP
来实现均可,数据面通常使用UDP
,不仅因为快, 还因为可以使用多播
(又叫组播
)机制,不过要注意多播
传输速率要比单播
慢一些。 -
其次决定数据包的大小:由于数据面的数据量是流量传输的大头,且其使用
UDP
, 因此对于单次传输的数据包大小的选取就很重要——既不能过小而导致发包数过多, 也不能过大而导致IP
层产生分片影响效率。由于链路层MTU
通常为1500
, 所以将应用层负载最大大小定为1400
。 - 最后是估算发送耗时:
- 假设使用的是千兆网卡,实际能达到的最高速度约为理论上限的
90%
多一点, 长期稳定运行的速度按70%
来取,约70MB/s
,则1us
能发70
字节。 - 按
15fps
的帧速率来算,每帧的发送时限最多是66ms
,但要扣除采集、解码、 转格式等耗时并且考虑性能抖动而留一点余量,则缩减到20ms
会比较保险。 - 在前面的图像数据量估算章节中提到,
JPEG
格式每帧约占48KB
, 为方便计算且留有余量,向上取70KB
,那么要发完一帧大概需要1ms
(理想情况下), 对前述的时限来说完全没有压力,但实际上这样估算仍不足够。 因为,这70KB
不是一次性发完,而是分成多个包,每个包之间还要有停顿, 需要估算得更精细一点。 - 首先计算分包数,用
700KB
除以1400
,得到50
个包(意味着不至于要发太多次)。 - 然后计算每个包的发送时间,
1400
/70
*1us
=20us
, 照例是留有余量,取100us
,落实到实际的操作可简化为: 调用send()
将数据包从用户空间复制到内核之后,还要调用sleep()
等待100us
(或者先硬性等待50us
,后50us
调用select()
或poll()
来轮询, 缓冲区空闲则尽快发送,策略可以根据实际测试来灵活制定)。 - 最后是计算发送一帧的总耗时。若不计数据复制的耗时,发完
50
个包需要5ms
; 再考虑数据复制耗时,一般也不会超过10ms
(内存毕竟比网络快得多, 可参考前面章节对于内存速率的估算),完全在20ms
的范围之内。 - 注意
UDP
是不可靠传输,所以就算send()
返回成功,对端也不一定收到, 所以发送端(即服务端)和接收端(即客户端)都要做好丢包统计, 前述的100us
等待时间也不能硬编码,而是可配置, 这样才能根据不同网络环境来修改发送间隔。
- 假设使用的是千兆网卡,实际能达到的最高速度约为理论上限的
- 以上的传输逻辑主要针对的是
实时图像
,对于视频上传云平台和客户端回放的场景, 不能使用这种策略,并且也不宜与实时图像传输混在同一个处理程序, 而是最好分开到不同程序。对于视频上传,最好改用TCP
传输。对于客户端回放, 既可以使用RTSP
及相关协议(要借助成熟的库或框架),也可以使用NFS
(无鉴权认证)、Samba
(有鉴权认证)等网络文件系统(可省去服务端的开发), 可根据不同的项目场景进行选用,本文不展开叙述。
9、图像显示
-
服务端在正常运行时通常不需要图形界面,省去这部分逻辑会减小资源消耗。 当然,可以在测试模式下开启图形界面,有助于排查问题,在正常运行时再关闭就行了。
-
客户端需要图形界面,但客户端不一定常开,而且可能运行在电脑上,所以系统资源限制不大, 对消耗也不敏感,只需注意保障好断开重连的逻辑,以及关闭时通知服务端, 让其关闭图像流的传输(尤其是在
多播
场景下)。 -
如果支持,尽量使用
GPU
来渲染,不过这会涉及到内存与显存之间的数据复制(见前面章节)、 需要编写OpenGL
着色器(Shader
)代码(或Vulkan
)等问题。 如果客户端也是嵌入式设备,还要搞清其支持的是全功能的OpenGL
还是OpenGL ES
。 -
通用接口待开源。
10、视频保存
-
非常消耗
CPU
,主要影响因素有:采集接口模式(例如用的是dmabuf
还是mmap
)、 是否用上硬件加速(例如瑞芯微系列芯片有MPP
功能)、视频格式(保存为MJPG
、H.264
还是其他格式)、视频分辨率、编码质量参数等。 -
也很考验磁碟写入耗时,需考虑机械磁碟(或
Flash
)写速率、数据缓冲的影响、 长期写的稳定性、每个视频文件在收尾时产生的延时抖动等。 -
观察实际的编程接口,看能否用上
sendfile()
之类的接口减少用户空间与内核的数据复制。 -
如果不是长期写磁碟,而是按需写(例如检测到有人行过再保存此期间的片段), 则需要注意保证对于何时开始和何时结束的判断的准确性,因为这两个时间节点会有额外的操作, 可能会产生多余的耗时,若错误地将一个片段拆成时间间隔极短的两个片段, 则可能因为这部分耗时而致使中间有一些帧丢失。
11、其他建议
-
对于一些优先级很高且非常消耗资源的环节、线程,可尝试使用
sched_setaffinity()
或类似接口将其绑定到特定的处理器核心(如果有大小核,则推荐绑定到大核), 降低资源缓存失效的可能性,从而提高运行性能。但由于处理器任务调度非常复杂, 而且缩窄选择范围也易与其他线程争夺资源,所以此法未必能获得预期收益, 需要进行实测对比,因此建议将这种绑定操作设置成可配置,以便按需启用或禁用。 -
OpenCV
、Qt
等库可能需要重新编译,以便打开一些优化选项, 这需要深入了解硬件平台有哪些专有的加速特性,以及查阅相关库的编译脚本和官方文档。 与上一条相似,也要经过实测才能确定优化是否有效。 -
CPU
频率的调节。默认的CPU
频率控制策略是按需动态调节频率,以便在高负载时高频率运行、 空闲时降低频率达到节能降温的目的。若某类任务的实时性要求高且对失败容忍度低, 则可考虑让CPU
维持着较高频率运行,再结合前面的核心绑定操作, 还能让一些核心保持原来的默认策略,另一些则全力输出。NPU
等同理。 -
做好散热。过热会导致降频影响性能甚至死机,长期过热还会加速硬件老化, 所以应该从一开始就做好散热设计。
-
网卡多队列(
Multi-Queue
)的配置。多播
要比单播
慢一些, 且两者流量默认都会归类到单一接收队列(通常是0
号队列), 此时可手动设置将多播
流量放到另一个队列,同时再将网卡中断服务绑定到特定的CPU
核心, 即可提升多播
吞吐量。设置命令及测试数据待补充。