ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

RK3588 NPU视频流推理:服务层架构与RTSP断流重连实战

RK3588 NPU视频流推理:服务层架构与RTSP断流重连实战 系列写到第七篇终于轮到大家最关心的服务层和摄像头了。RK3588 的 NPU 跑 YOLOv5s 真不算难模型转换、rknn 推理接口都有现成文档。真正磨人的是怎么把一路或多路视频流稳定地喂进模型再把推理结果稳定地吐给上层业务用。这一篇我把自己在服务层架构上的设计思路、摄像头接入的三种典型方式以及一个让我折腾了整整一个周末、最后查到底才发现是连环坑的问题完整写出来希望对正在做 RK3588 边缘 AI 盒子的朋友有帮助。先交代一下背景我这边的环境是 RK3588 开发板8GB 内存系统是 Ubuntu 20.04模型是 YOLOv5s 转成的 RKNN 格式输入尺寸 640x640用 rknn-toolkit2 做了 INT8 量化。前面的文章已经讲了环境搭建、模型转换和单张图片推理这一篇的重点是从单张图片推理跨到连续视频流推理以及让这个推理过程变成可以被外部业务调用的服务。1. 服务层设计先理清数据流向再动手写代码1.1 服务层到底要解决什么问题很多人把 YOLOv5s 部署到 RK3588 上之后第一件事就是写一个 main 函数打开摄像头、循环读帧、调 rknn.run、画框、显示。这样 demo 确实能跑但距离“能用”还差得远。实际项目中检测结果要么需要推送给 web 前端要么需要按照某种协议上报给平台要么需要联动其他硬件动作。这些都不是一个简单 while 循环能搞定的。服务层的核心价值在于把摄像头采集、视频解码、模型推理、结果分发这四个环节解耦让每一块都能独立替换、独立调试。比如换了摄像头型号只改采集层模型从 YOLOv5s 换成 YOLOv8s只改推理层要新增一个告警推送只加一个消费者。我见过太多项目所有逻辑写在一个线程里摄像头偶尔断一下流整个进程就卡死原因就是没有分层一个环节出错直接拖垮全部。1.2 我的模块划分与线程模型我的服务层分成了四个模块采集模块CameraWorker、缓冲模块FrameQueue、推理模块InferenceWorker、输出模块ResultHandler。每个模块跑在独立线程里模块之间通过有界队列通信。采集模块只负责从摄像头拿帧拿到的是解码后的原始帧比如 BGR 或 NV12压进队列就走人。推理模块从队列里取帧做缩放、格式转换、rknn 推理、后处理把检测结果丢给输出模块。缓冲模块很关键它用的是一个容量很小的有界队列我设置了最大长度 2 帧。为什么这么小因为如果队列太大推理速度跟不上采集速度帧会积压画面延迟会越来越大。用有界队列加上“队满丢最旧帧”的策略保证推理永远处理最新一帧这在实时检测场景里比“一帧不丢”重要得多。输出模块我用了两个消费者一个是 WebSocket 推送把检测结果实时推到前端页面叠加显示另一个是 HTTP 回调把结构化结果 POST 到业务平台。FastAPI 在这里很合适它天然支持异步用来做 HTTP 接口和 WebSocket 都很顺手。选 FastAPI 而不是 Flask 的原因很直接异步能力和自动生成的 OpenAPI 文档调试接口时少写不少代码。2. 摄像头接入USB、MIPI、RTSP 三个方向逐个说2.1 USB 摄像头V4L2 下的参数选择陷阱USB 摄像头是最容易上手的插上就能在 /dev/video0 看到设备。但这里有个容易被忽略的点分辨率、帧率、像素格式这三者要匹配好否则要么取不到帧要么格式不对导致 CPU 软转开销爆炸。我调试时第一步永远是先用 v4l2-ctl 看设备能力v4l2-ctl -d /dev/video0 --list-formats-ext这一步能列出所有支持的像素格式和分辨率组合。我之前遇到过一个摄像头标称支持 1080p但实际在 1080p 下只支持 YUYV 格式30fps 时 YUYV 的数据量相当于 30 帧 x 1920x1080x2 字节已经不小了。如果直接把它当 RGB 用转换耗时非常可观。后来我果断调到 720p MJPEG 格式用硬件解码减少数据量CPU 占用立刻降下来。用 OpenCV 的 VideoCapture 虽然方便但底层对 V4L2 的参数控制能力有限。我更推荐直接用 FFmpeg 的 libavdevice 接口或者 GStreamer 的 v4l2src 来采 USB 摄像头因为这两个库在 RK3588 上能方便地和后面的硬件解码、RGA 缩放衔接。如果你只是测试OpenCV 没问题但生产环境建议直接从 FFmpeg 起。2.2 MIPI 摄像头设备树配置是绕不开的一道坎RK3588 上接 MIPI CSI 摄像头难度比 USB 高一个量级。核心问题是设备树DTS的配置。不同的 sensor 模组对应不同的驱动比如 IMX415、OV5647、GC2093都需要在 dts 里正确配置 I2C 地址、MIPI lane 数、时钟频率。我手上有一块 OV5647 模组照着芯片手册确认了默认 I2C 地址和 lane 配置。设备树改完重新编译并烧录后/dev/video0 才出现。这里提醒一句RK3588 的 MIPI CSI 接口有多个别插错检查 dmesg 里有没有 sensor 上电失败的报错是关键。MIPI 摄像头的好处是延迟低、体积小适合做嵌入式一体机。但调试成本确实高如果你不是做量产产品只是先验证算法我建议优先用 USB 摄像头把精力花在推理和服务层上。2.3 RTSP 网络摄像头海康大华小米都能接关键在取流参数IPC 网络摄像头接入走 RTSP 协议是通用方案。海康的 RTSP 地址格式一般是rtsp://用户名:密码IP:554/Streaming/Channels/101末尾的 101 表示通道 1 主码流102 表示通道 1 子码流。大华的地址格式略有不同一般是rtsp://用户名:密码IP:554/cam/realmonitor?channel1subtype0subtype0 主码流subtype1 子码流。小米摄像头开启 RTSP 需要在 App 里找到“局域网协议”开关打开后会多出 RTSP 地址。主码流和子码流怎么选我的经验是如果做精细检测比如车牌识别、小目标检测用主码流一般是 1080p 或更高H.265 编码如果只是做区域内的人体/车辆粗检测或者要同时接很多路摄像头用子码流通常是 720p 或更低H.264 编码会更划算。我之前测试时用主码流接了两路 1080pRK3588 的硬件解码器 VDPU 能轻松应付但网络带宽和存储压力会大一些。取流我推荐直接用 FFmpeg 库设置 RTSP over TCP能有效避免 UDP 模式下的丢包花屏问题AVDictionary* opts NULL; av_dict_set(opts, rtsp_transport, tcp, 0); av_dict_set(opts, stimeout, 5000000, 0); // 5秒超时 av_dict_set(opts, max_delay, 500000, 0);rtsp_transporttcp 是强烈建议的UDP 虽然延迟略低但网络稍一波动就满脸马赛克。stimeout 设置 5 秒表示 socket 5 秒没有收到数据就报超时这是为后面那场事故埋下的引子别急后面细说。3. 折腾最久的坑一场由 RTSP 断流引发的连环事故3.1 现象进程没死画面死了这个故事必须单独写一节因为它是我在这整个项目里排查时间最长、最挠头的问题。现象是这样的系统上线运行前面几个小时一切正常检测框稳定输出。但运行到某个时间点后Web 端看到的画面突然不动了服务进程还在跑CPU 占用率不高内存也不涨但就是没有新的检测结果出来。我一开始怀疑是线程死锁。检查了所有锁没有发现明显的错误。又怀疑是队列满了打印了队列深度显示确实是满的但消费线程没有在消费。这就很奇怪了推理线程明明没退出为什么不取帧3.2 排查从日志到抓包逐层剥开我先给每个模块加了详细日志重点看采集线程的状态。结果发现采集线程卡在 av_read_frame 这个函数里没有返回。这说明 FFmpeg 在等网络数据而摄像头那边已经不推流过来了。用 tcpdump 在 RK3588 上抓包确认了摄像头确实在某个时间点停止了发送 RTP 数据包。这就是网络摄像头的一个“老毛病”RTSP 会话空闲超时后服务端主动断开推流。有的摄像头超时时间是 60 秒有的是 120 秒取决于厂商固件。我们这边如果中途有几十秒没有新帧比如画面几乎没有变化时某些摄像头会自动降低帧率就可能触发服务端的超时断开。好了那问题就变成了为什么 av_read_frame 超时后我的重连逻辑没有生效3.3 根因三个粗糙设计叠加出来的灾难排查到这里我翻了重连逻辑的代码发现自己埋了三个雷。第一我设置 stimeout50000005 秒理论上 av_read_frame 会在 5 秒后返回错误。但实际测试发现在某些情况下这个超时并不会精确触发或者说触发了但我没有正确处理返回值。我原来的代码是int ret av_read_frame(ic, pkt); if (ret 0) { // 重连 }表面上没问题但我没看错误码具体是什么。有的是 AVERROR_EOF有的是 AVERROR(ETIMEDOUT)有的是 -1。而我重连时只重新调用了 avformat_open_input重新打开了 RTSP 连接但没有调用 avcodec_flush_buffers 清理解码器内部缓冲。这就导致重连成功后解码器还残留着上一段流的状态输出视频的第一帧会出现花屏或 PTS 错乱。第二重连后我没有检查新的视频流参数是否发生了变化。有些摄像头在重连后会因为配置状态不同输出不同分辨率的码流。我之前假设无论怎么重连分辨率都是 1080p。没想到有一台测试摄像头在事件触发后比如移动侦测主码流会短暂切换到子码流的分辨率。解码器一旦输出 720p 的帧我的图像预处理模块按照 1080p 宽高分配的内存和 letterbox 参数全部错乱轻则花屏重则内存越界。第三也是最隐蔽的我在采集线程里直接调用了 av_read_frame 和解码而重连逻辑放在同一个线程里。当 av_read_frame 因为某种原因阻塞超过 5 秒比如 DNS 解析卡住、TCP 重传超时采集线程就卡死了。采集线程卡死 → 队列没人推帧 → 推理线程空等 → 服务层整体假死。3.4 修复重连要彻底动态分辨率要适配修复方案分三步走。第一步把重连逻辑独立出来不要和采集解码放同一个路径。我专门写了一个 ReconnectHelper重连时执行完整流程关闭旧的 AVFormatContext → 关闭解码器 → 重新打开 RTSP → 重新打开解码器 → avcodec_flush_buffers → 检查流参数。每步都加了超时保护确保不会卡死。第二步在每次解码出一帧后检查 frame-width 和 frame-height 是否和当前预处理配置一致。如果不一致立即触发预处理模块的重新初始化并丢弃缓冲区内所有旧帧。对于模型输入我加了一层固定 letterbox不管摄像头出什么分辨率模型输入永远是 640x640坐标映射关系动态计算。第三步在采集线程和推理线程之间加了一个 watchdog。每 2 秒检查一次如果发现队列在持续超时没有新帧进来就主动触发一次完整重连而不是等 av_read_frame 返回错误。这一步非常管用相当于把被动防御变成了主动巡检。修复之后系统连续跑了 72 小时没有再出现假死。4. 避坑清单与性能调优实录4.1 常见问题速查表把这段时间踩过的坑整理成表格遇到类似问题可以直接对照排查。现象常见原因解决方案摄像头取流一段时间后画面卡死RTSP 服务端空闲超时断开连接设置 stimeout独立重连线程watchdog 巡检重连后画面花屏解码器缓冲未清空重连后调用 avcodec_flush_buffers检测框位置偏移输入图像做 letterbox 后坐标映射错误所有坐标计算统一基于 letterbox 变换矩阵推理延迟越来越大帧队列无界导致积压队列改用有界队列队满丢最旧帧多路视频推理时 CPU 占用暴涨颜色转换和缩放用了 CPU改用 RGA 硬件加速或 RKNN 直接输入 NV12单帧推理耗时 60ms 以上量化精度不够或模型输入格式不规范确认输入是 uint8 RGB检查 rknn 版本和驱动补充一句关于 RKNN 输入的细节rknn 推理接口对输入格式有要求尽量直接喂 NV12 或 RGB888。如果你从 FFmpeg 解码拿到的是 YUV420P不要自己写循环转换RK3588 的 RGA 硬件模块能一行代码搞定rga_set_buffer_info(src, dst); rga_blit(...); // 缩放 格式转换用 RGA 之后1080p 帧缩放到 640x640 并转 RGB 的耗时从 CPU 软转的 20ms 降到 2ms 左右效果立竿见影。这是我强烈推荐的一步优化。4.2 性能优化与资源控制我在实际调优中发现RK3588 的 NPU 跑 YOLOv5s INT8 单帧推理大概在 20-40ms 之间加上预处理和后处理单路视频能做到 30fps 左右实时。如果同时接 4 路 720p 子码流建议把推理线程改成独立进程通过共享内存或本地 socket 通信避免其中一个模型实例卡死导致整个服务层一起崩。内存方面每路摄像头解码后的原始帧占用的内存不小如果你是 4 路 1080p光是帧缓冲就要几百 MB。合理做法是只保留当前帧和上一帧不要保留深队列。我给每路摄像头只分配两个 AVFrame 大小的缓冲循环覆盖使用实测内存占用稳定在 300MB 以内。4.3 上线前自检清单最后给一份我自己复盘的检查清单照着走一遍能省很多现场调试时间摄像头超时机制是否独立重连是否执行完整清理和参数检查队列是否为有界队列是否配置了丢帧策略图像预处理是否为动态兼容分辨率letterbox 参数是否随帧尺寸变化是否有 watchdog 巡检采集线程和推理线程的健康状态日志是否足够完整至少能定位到具体模块的卡点和耗时分布我后来在实际项目里把重连逻辑抽成了一个公共组件接其他品牌的摄像头时只需要改 RTSP 地址和帧率参数剩下的断流重连、参数自适应、watchdog 全部复用。这也算是在那个“折腾最久的坑”里捞到的最大的收获。如果你在集成某个摄像头厂商的设备时发现它的 RTSP 行为特别不规范别头铁去猜先抓包看它到底是怎么断流的再针对性地调重连策略这远比反复改超时参数高效。
返回列表