基于 RK3588 的多输入视频 AI 推理流水线:YOLO 车牌检测、OCR、WebRTC 推流与边缘联动
本文介绍一个面向 Rockchip RK3588 的 C++ 视频智能分析项目。项目将 MP4、USB 摄像头和 RTSP 视频统一接入同一条边缘推理流水线,完成目标检测、车牌 OCR、跟踪、OSD、硬件编码和网络分发,并可选接入 MQTT 事件上报、OLED 显示和 Modbus 继电器。
采用 NPU Core Affinity 机制,各推理线程持有独立rknn_context,通过 core_mask 绑定不同 NPU 硬件核心,实现多路检测任务在多 NPU 上并行执行,降低单 NPU 资源争抢,提升多路 RTSP 流推理吞吐。
GitHub:https://github.com/shaddockpeel2/Plate_detection_recognition(点⭐哦)
一、项目主要做什么
这个项目解决的不是“加载一个 YOLO 模型并输出检测框”这么单一的问题,而是一个完整的边缘视频分析链路:
MP4 / USB 摄像头 / RTSP │ ▼ 解封装、采集与硬件解码 │ ▼ RGA 图像预处理 │ ▼ RKNN YOLO 推理 │ ▼ 后处理、ByteTrack、车牌 OCR │ ▼ OSD 叠加结果 │ ▼ MPP H.264/H.265 编码 │ ┌─────┴──────────────┐ ▼ ▼ 保存 MP4 RTMP 推送到 ZLMediaKit │ WebRTC / HTTP-FLV / HLS项目的典型应用场景包括:
- RK3588 边缘盒子上的车辆和车牌识别;
- 停车场、园区或出入口的视频分析;
- 需要低延迟预览的智能摄像头网关;
- 识别结果需要上传到平台,或者需要联动继电器、门禁等设备的场景;
- 在一块 RK3588 上运行三路独立推理任务的性能验证。
核心程序是rk_mp4_yolo_stage5,它负责单路视频从输入到输出的完整处理。rk_yolo_supervisor负责多路 worker 的配置校验、启动、健康检查和重启,不直接参与图像推理。
二、项目的核心架构
1. 多输入统一为DecodedFrame
项目将输入分支和后续推理链路解耦。
对于 MP4 和 RTSP 输入,程序使用 FFmpeg 完成解封装,再使用 Rockchip MPP 硬件解码 H.264/H.265。RTSP 采用 TCP 传输,并配置读流超时。
对于 USB 摄像头,程序使用 V4L2 的 MMAP 方式采集YUYV数据,在摄像头输入线程内转换为NV12,随后也进入DecodedFrameQueue。这样,摄像头不需要复制一套后处理、推理和编码逻辑。
统一后的DecodedFrame不只是一个图像指针,还包含:
frame_id:应用层帧编号;pts:源视频时间戳;source_fps:源帧率;- 宽高和 stride;
- MPP 帧对象和 DMA-BUF 文件描述符。
后续模块通过 DMA fd 直接访问硬件缓冲区,减少不必要的 CPU 拷贝。
2. RGA 负责预处理,RKNN 负责推理
预处理线程从解码队列取帧,使用 RGA 完成:
- NV12 到 RGB/BGR 的格式转换;
- 缩放;
- Letterbox 填充;
- 写入 RKNN 输入张量;
- 对量化模型执行必要的输入转换。
项目通过RknnInputRuntime在启动时查询模型输入输出属性,包括输入尺寸、布局、数据类型和量化信息,并创建输入内存池。默认输入池包含多个 DMA 内存槽位,使预处理和推理之间可以形成有界流水线,而不是每帧重复申请和释放内存。
RGA 预处理和 RKNN 输入内存之间通过 DMA fd 连接,适合 RK3588 这类需要充分利用硬件加速和共享缓冲区的环境。
3. RKNN 推理支持 NPU 核绑定
YOLO 模型通过 RKNN Runtime 初始化,程序可以使用rknn_set_core_mask将推理上下文绑定到指定 NPU 核:
--npu-core 0 使用 NPU core 0 --npu-core 1 使用 NPU core 1 --npu-core 2 使用 NPU core 2 --npu-core auto 交给 RKNN 自动调度绑定只作用于 RKNN 推理上下文,并不意味着 RGA、MPP、CPU、内存带宽和网络资源也被隔离。三路 worker 即使分别使用 core 0、1、2,仍然会共享其他系统资源,因此实际部署仍需要根据输入分辨率、帧率、OCR 和编码码率进行容量评估。
4. 后处理兼容不同 YOLO 输出布局
后处理模块会读取 RKNN 输出张量,并根据张量布局解析检测框和类别分数,当前代码中包含两类主要路径:
- YOLO26 风格的检测输出;
- YOLOv8 DFL 风格的 box、score 输出。
完成输出解码后,模块依次执行:
- 置信度过滤;
- 坐标还原,将模型坐标映射回原始图像;
- NMS 去除重复框;
- ByteTrack 关联连续帧中的同一目标;
- 为检测结果写入
track_id。
Detection数据结构同时保存检测框、检测分数、跟踪 ID、车牌文本和 OCR 分数,后面的 OCR、OSD、上传和继电器逻辑都基于这份结构继续处理。
三、车牌 OCR 是如何接入的
车牌 OCR 不是单独的离线工具,而是后处理阶段的一部分。
当检测到车辆目标并且 ByteTrack 已经分配了track_id后,PlateOcrStage会执行以下操作:
车辆检测框 │ ▼ 扩大并裁剪车牌区域 │ ▼ RGA 缩放到 OCR 模型输入尺寸 │ ├─ 量化输入:直接使用 RKNN DMA 输入内存 └─ 浮点输入:转为 OpenCV Mat 后归一化 │ ▼ PP-OCRv5 车牌识别模型 │ ▼ CTC 解码 + 字典映射 │ ▼ plate_text / plate_scoreOCR 阶段包含几个适合实时视频的优化:
- 同一帧限制最大 OCR 目标数量;
- 同一跟踪目标按帧间隔重新识别;
- 识别成功后按
track_id缓存结果; - 识别失败时使用较短的重试间隔;
- 缓存过期后自动清理,避免目标长期消失仍保留旧车牌。
需要注意,当前 YOLO 和 OCR 默认绑定到同一个 NPU core。启用 OCR 后,实际吞吐会下降,需要结合目标数量和 OCR 触发频率重新评估,而不能只看 YOLO 单模型的速度。
四、OSD、编码和播放
1. OSD 直接叠加到视频帧
后处理线程会在原始 NV12 帧上绘制:
- 检测框;
- 跟踪 ID;
- 车牌识别文字;
- 中文或 ASCII 文字。
中文文字使用 FreeType 加载系统字体并生成位图缓存,重复出现的车牌不会每帧重复生成字形。这样编码输出的视频本身就已经包含检测框和车牌文字,浏览器端不需要再次执行绘制。
2. MPP 编码支持本地文件和网络输出
编码线程使用 MPP 硬件编码器输出 H.264/H.265 码流,并根据输出模式选择不同 sink:
mp4 -> FFmpeg libavformat 封装为 MP4 annexb -> 保存裸 H.264/H.265 码流 rtmp -> H.264 Annex-B 写入 FFmpeg 子进程,再转封装为 FLV/RTMPRTMP 推流使用FfmpegPushSink,主程序通过管道把 MPP 输出的 H.264 码流交给 FFmpeg,FFmpeg 使用视频复制方式转封装并推送到 ZLMediaKit。推流进程异常时支持有限次数重启。
ZLMediaKit 负责将 RTMP 转换为浏览器可消费的协议,页面可以使用:
- WebRTC:低延迟预览;
- HTTP-FLV:便于 VLC、ffplay 或接口排障;
- HLS:兼容部分原生 HLS 播放器,但延迟通常更高。
3. 实时帧率和时间戳处理
RTSP 实时场景最容易出现的问题不是模型本身,而是输入速度、编码声明和媒体时间戳不一致。项目对此做了专门处理:
--output-fps 25时,在 RGA 和 RKNN 之前均匀抽帧,避免只修改编码器声明而仍然处理全部输入帧;--output-fps auto时,跟随 RTSP 建连时探测到的源帧率,不主动抽帧;- 输出队列有界,系统过载时丢弃最旧帧,优先保持实时性;
- RTMP 路径不使用墙钟时间戳,也不使用 FFmpeg
-re强制限速; - FFmpeg 根据输入帧率生成连续 PTS,避免推理耗时抖动直接变成媒体时间轴抖动;
- MPP 编码输出按 partition 收齐,只有完整帧结束后才交给下游,避免半帧 H.264 造成推流断开。
项目现有技术报告记录了三路1024×576 @ 30 FPS测试流的验证结果:每路约 5 秒输出 150 个包,PTS 间隔约为0.033~0.034秒,三个 worker 的重启次数为 0。这个结果说明帧率选择和 RTMP 时间戳链路已经能够稳定工作,但不代表任意分辨率、码率和 OCR 配置下都能获得相同吞吐。
五、三路独立推理与 supervisor
项目提供了一个轻量级的rk_yolo_supervisor。它将三路推理拆成三个独立进程:
worker.core0 -> --npu-core 0 -> rtmp://127.0.0.1:1936/live/rk3588-core0 worker.core1 -> --npu-core 1 -> rtmp://127.0.0.1:1936/live/rk3588-core1 worker.core2 -> --npu-core 2 -> rtmp://127.0.0.1:1936/live/rk3588-core2supervisor 的职责包括:
- 读取
config/inference_fleet.ini; - 检查 NPU core、流名、摄像头设备、截图目录、MQTT 标识和外设资源是否冲突;
- 为每个 worker 构造命令行并脱敏打印;
- 创建独立日志和 health 文件;
- 监测 worker 进程和健康心跳;
- 对 RTSP、摄像头 worker 进行有限次数的指数退避重启;
- 原子生成网页读取的
rk-yolo-status.json。
浏览器页面web/rk_yolo.html根据状态文件显示三个视频卡片,每一路可以独立重试、停止或切换到排障播放地址。因此一路摄像头或 RTSP 断开时,不会直接阻塞另外两路。
三路模式适合以下两类任务:
- 三路独立摄像头分别占用一个 NPU core;
- 使用同一个输入源对三份 worker 做性能压测。
生产环境不建议让三个 worker 同时打开同一个 USB 摄像头,应使用三路独立 RTSP、不同的/dev/video*,或混合 MP4 测试源。
六、远程事件上报与硬件联动
1. HTTP 图片 + MQTT 事件
远程上报采用“图片和事件分离”的设计:
检测 / 跟踪 / OCR │ ▼ 阈值判断与去重 │ ▼ 有界 UploadEventQueue │ ▼ 独立上传线程 ├─ 裁剪车辆或车牌截图并编码 JPEG ├─ HTTP multipart 上传图片 └─ MQTT 发布事件 JSON 和 image_urlMQTT 只传事件元数据,不传图片二进制。事件通常包含设备 ID、帧号、跟踪 ID、车牌文本、检测分数、OCR 分数、检测框和图片 URL。
网络请求全部发生在上传线程中,主链路不会因为 HTTP 或 MQTT 服务器不可达而阻塞。上传队列满时丢弃新事件,不让网络反压传递到检测、OCR、OSD 和编码线程。当前版本不做失败事件的本地持久化缓存,断网期间失败的事件会丢失,但视频主链路仍可继续运行。
2. 白名单继电器和 OLED
继电器功能默认关闭,打开后需要同时满足:
- 车牌位于白名单文件;
- 检测分数达到阈值;
- OCR 分数达到阈值;
- 车牌不在冷却时间内。
控制器使用 Modbus RTU 通过串口发送线圈开关命令,默认脉冲结束后强制关闭。OLED 则通过 I2C 显示近期识别到的车牌。
这两个功能都通过独立队列和线程接入,不改变主视频处理链路。部署时必须确认串口、I2C 设备和继电器通道的实际硬件参数,不能直接照搬示例路径。
七、性能监控和背压设计
项目没有使用无限队列。解码、预处理、推理和 OSD 之间都使用固定容量队列:
DecodedFrameQueue 容纳解码帧 PreprocessedFrameQueue 容纳 RGA 处理后的输入 InferenceResultQueue 容纳 RKNN 输出 OsdFrameQueue 容纳待编码帧 UploadEventQueue 容纳待上传事件这样做的取舍很明确:
- MP4 文件处理可以通过阻塞队列保持完整性;
- RTSP 实时处理优先降低延迟,队列满时丢弃旧帧;
- 上传事件队列满时丢弃新事件,不影响视频输出;
- 所有队列都支持关闭和唤醒,便于程序退出时按顺序回收线程。
打开--metrics-interval-ms 1000后,程序会输出:
- 解封装、MPP 提交、RGA、预处理、RKNN、后处理、编码和 RTMP 写入耗时;
- 队列深度、峰值、入队等待和出队等待;
- MPP buffer full、FFmpeg pipe 阻塞、推流重启等事件;
- 实际输出 FPS 与声明输出 FPS。
这比只查看“平均 FPS”更适合定位实时视频卡顿:平均值正常并不代表不存在 P99 延迟或队列积压。
八、如何构建和运行
1. 环境要求
项目主要面向:
硬件:Rockchip RK3588 系统:aarch64 Linux 语言:C++11 构建:CMake + g++ 推理:RKNN Runtime 图像处理:RGA、OpenCV 视频:FFmpeg、Rockchip MPP 字体:FreeType仓库提供了项目内的 Rockchip 依赖,也支持通过RKNPU2_DIR指向外部 SDK。x86 Linux 可以阅读和修改代码,但没有对应的 MPP、RGA、RKNN 驱动和库时不能直接完成板端运行。
2. 编译
cmake-S.-Bbuild cmake--buildbuild\--targetrk_mp4_yolo_stage5 rk_yolo_supervisor\-j$(nproc)如果 CMake 找不到 FFmpeg、OpenCV 或 FreeType 开发库,应先检查pkg-config和系统开发包;如果运行时找不到 Rockchip 动态库,应检查构建出的 RUNPATH 是否指向third_party/rockchip。
3. MP4 测试
关闭 OCR 进行基础链路验证:
./build/rk_mp4_yolo_stage5\--inputmp4\--video./video/test-video/5s.mp4\--model./models/car-v8/v8-car-relu-3588.rknn\--ocr-model off\--output./video/output-video/result.mp4启用车牌 OCR 时,需要同时指定识别模型和字典:
./build/rk_mp4_yolo_stage5\--inputmp4\--video./video/test-video/9s.mp4\--model./models/car-v8/v8-car-relu-3588.rknn\--ocr-model ./ppocrv5/PP-OCRv5_mobile_rec_license_plate.rknn\--ocr-vocab ./ppocrv5/model/license_plate_dict.txt\--output./video/output-video/plate-result.mp44. 摄像头测试
第一版摄像头输入主要验证YUYV采集链路:
./build/rk_mp4_yolo_stage5\--inputcamera\--device/dev/video0\--width640\--height480\--fps25\--frame-limit300\--model./models/car-v8/v8-car-relu-3588.rknn\--ocr-model off\--output./video/output-video/camera-result.mp4运行前可以使用v4l2-ctl --list-formats-ext查看摄像头是否支持目标分辨率和像素格式。
5. RTSP 推流测试
先启动 ZLMediaKit,再启动推理程序:
./build/rk_mp4_yolo_stage5\--inputrtsp\--urlrtsp://<camera-ip>:8554/live\--model./models/car-v8/v8-car-relu-3588.rknn\--ocr-model off\--output-mode rtmp\--push-url rtmp://127.0.0.1:1936/live/rk3588\--output-fps autoZLMediaKit 的具体端口和页面路径以部署配置为准。通常可以用 WebRTC 查看低延迟画面,用 HTTP-FLV 或 HLS 排查推流是否真正进入媒体服务器。
6. 三路 supervisor
cpconfig/inference_fleet.ini.example config/inference_fleet.ini ./build/rk_yolo_supervisor--configconfig/inference_fleet.ini--check./build/rk_yolo_supervisor--configconfig/inference_fleet.ini --dry-run ./build/rk_yolo_supervisor--configconfig/inference_fleet.ini正式部署前应修改:
- 三路输入地址或摄像头设备;
npu_core,确保正好覆盖 0、1、2;- RTMP 应用名和流名;
- 模型和 OCR 资源路径;
- 日志、健康文件和状态页面路径;
- MQTT、继电器和 OLED 配置。
公开配置或发布文章时,不要提交真实 RTSP 凭据、MQTT 密码、服务器地址和设备私钥。
九、项目目录说明
include/ 模块接口和数据结构 src/main.cpp 单路流水线编排 src/decoder_thread.cpp FFmpeg 解封装与 MPP 解码 src/camera_source_thread.cpp V4L2 摄像头采集与 YUYV/NV12 输入转换 src/preprocess_thread.cpp RGA 预处理和 Letterbox src/inference_thread.cpp RKNN 推理与输出张量复制 src/postprocess_osd_thread.cpp YOLO 后处理、NMS、OSD 和功能分发 src/plate_ocr_stage.cpp 车牌裁剪、OCR 调度和缓存 src/encoder_writer_thread.cpp MPP 编码和 MP4/Annex-B 输出 src/ffmpeg_push_sink.cpp RTMP 推流子进程管理 src/worker_supervisor_main.cpp 多 worker 健康管理和重启 src/worker_fleet_config.cpp INI 配置解析、校验和参数展开 models/ YOLO 模型和标签 ppocrv5/ OCR 模型、字典和测试资源 config/ 多路推理配置模板 web/ ZLMediaKit 播放页面 third_party/rockchip/ MPP、RGA、RKNN 项目依赖 docs/ RTSP、ZLMediaKit、MQTT 技术记录 doc/ 面向公开发布的项目介绍文章十、当前边界和后续方向
项目已经形成可运行的端到端框架,但仍有明确边界:
- 摄像头第一版主要支持 V4L2
YUYV,如果设备直接输出 NV12、MJPEG 或 H.264,需要在摄像头输入模块增加格式分支。 - NPU core 绑定只隔离 RKNN 上下文,三路并发仍需关注 RGA、MPP、CPU 和内存带宽竞争。
- OCR、上传和外设功能会增加处理开销,建议先关闭附加功能完成基础链路验证,再逐项打开。
- 单独运行推理程序时,RTSP 断流不会自动恢复;使用 supervisor 时可以通过 worker 退出或 health 过期触发重启。
- 上传失败事件当前不做本地失败缓存,断网期间应由平台侧接受事件丢失,或者后续增加持久化队列。
- WebRTC 依赖 ZLMediaKit 的 WebRTC 构建和端口配置,浏览器播放问题应先用 HTTP-FLV 或
ffprobe验证上游媒体流。
后续可以继续完善:
- 增加更多 V4L2 像素格式和硬件零拷贝路径;
- 将多路配置、健康状态和性能指标接入统一运维平台;
- 对 RGA、RKNN 和编码阶段采集 P95/P99 延迟;
- 增加上传事件的本地持久化和断点补传;
- 根据不同模型输出定义独立的后处理插件;
- 3路扩大至6路;
- 引入更完整的测试视频、模型和设备能力探测。
总结
这个项目的价值在于把 RK3588 上分散的硬件能力组织成了一条可部署的视频 AI 产品链路:输入侧兼容文件、摄像头和 RTSP,计算侧使用 MPP、RGA 和 RKNN,识别侧组合 YOLO、ByteTrack 和车牌 OCR,输出侧支持 MP4、RTMP 和 Web 播放,业务侧还可以继续连接 MQTT、OLED 和继电器。
从代码结构看,项目采用“输入分支统一、阶段线程解耦、队列有界、网络异步、worker 进程隔离”的设计。它更适合作为 RK3588 车牌识别、边缘视频网关和多路实时推理系统的工程基础,而不仅是一个模型推理样例。
关键词:RK3588、Rockchip MPP、RGA、RKNN、YOLO、车牌识别、OCR、ByteTrack、RTSP、ZLMediaKit、WebRTC、MQTT、边缘 AI、C++ 视频处理