新闻详情

新闻详情

首页 / 资讯中心 / 详情

香橙派RK3588双路视觉实战:线程池调度与YOLOv5s推理优化

发布时间:2026/9/29 15:50:27来源:尧图网络
香橙派RK3588双路视觉实战:线程池调度与YOLOv5s推理优化
1. 双路视觉方案的整体设计思路1.1 为什么要在香橙派RK3588上做双路视觉单路摄像头跑yolov5s在RK3588上其实已经能跑得比较舒服了。RK3588自带NPU算力标称6TOPSyolov5s量化成RKNN之后单路1080p输入做到30fps以上并不难。但实际项目里单路往往不够用——比如做立体视觉、做前后双摄的盲区覆盖、做多工位同时检测这时候就得上双路。双路视觉的核心矛盾不在于NPU算力而在于CPU侧的图像采集、预处理和调度。两路MIPI或者USB摄像头同时出流如果都塞进一个线程里串行处理帧率直接腰斩如果无脑开一堆线程上下文切换和锁竞争又会把CPU吃满。所以这一节要解决的核心问题就是怎么用线程池把两路视觉的采集、推理、后处理拆开让它们互不阻塞同时又不把CPU搞爆。我这次用的硬件是香橙派5RK3588系统是Ubuntu 20.04摄像头一路是MIPI接口的IMX415另一路是USB3.0的工业相机。软件栈是OpenCV RKNN Runtime 自写的线程池。整套方案的目标是两路各跑各的yolov5s互不干扰单路不低于25fpsCPU占用控制在70%以内。1.2 线程池方案选型为什么不用每路一个独立线程最直觉的做法是每路摄像头开一个线程采集、推理、后处理全在里面串行跑。两路就是两个线程简单粗暴。我一开始也是这么写的实测下来问题很明显第一路推理的时候第二路的采集会被阻塞因为GIL或者OpenCV的全局锁会导致读帧变慢两路帧率不一致的时候慢的那路会拖累快的那路后处理画框、编码、推流如果也在同一个线程里一旦某帧后处理耗时抖动整个流水线就卡一下。所以正确的做法是把流水线拆成阶段每个阶段用线程池来调度。具体来说每一路视觉拆成三个逻辑阶段采集阶段从摄像头读一帧原始图像推理阶段预处理 RKNN推理 后处理输出阶段画框、编码、显示或推流。每一路各自拥有一个独立的线程池池子里至少有两个工作线程一个负责采集一个负责推理。这样两路之间完全隔离一路卡了不影响另一路。线程池的好处是线程可以复用不用每帧都创建销毁减少了系统调用开销。注意这里说的“两路各一个线程池”指的是每一路视觉拥有自己独立的线程池实例而不是两路共用一个池子。共用一个池子的话一路的任务堆积会把另一路的任务饿死隔离性就没了。1.3 线程池的核心参数怎么定线程池不是开得越大越好。RK3588是8核CPU4个A76 4个A55NPU是独立单元。我的经验是每一路的线程池核心线程数设为2最大线程数设为3阻塞队列用有界队列容量设为4拒绝策略用DiscardOldest丢掉最老的帧保证实时性。为什么核心线程是2因为采集和推理是两个必须并行的阶段少于2就会串行。为什么最大是3留一个余量给后处理或者突发任务。队列容量4是因为视觉任务对实时性要求高堆积太多帧没有意义不如丢掉旧的。这里有个坑很多人喜欢用无界队列LinkedBlockingQueue不传容量结果任务堆积到内存爆掉。视觉场景下帧是有时效性的旧帧处理完也没用所以一定要用有界队列。2. 线程池的核心细节与实操要点2.1 线程池的基本结构我用的是C11的std::thread自己封装的线程池没有直接用Java那套ThreadPoolExecutor因为嵌入式环境里Java太重了。核心结构包括一个任务队列std::queuestd::functionvoid()一个互斥锁保护队列一个条件变量通知工作线程一组工作线程一个停止标志。工作线程的主循环就是加锁 - 等条件变量 - 取任务 - 解锁 - 执行任务。这个模式很经典但有几个细节要注意。第一条件变量的等待必须用while循环判断队列是否为空不能用if否则会有虚假唤醒的问题。第二停止的时候要先设置停止标志再notify_all让所有线程退出。第三任务执行的时候不要持锁否则整个池子就串行了。void worker() { while (true) { std::functionvoid() task; { std::unique_lockstd::mutex lock(mtx_); cv_.wait(lock, [this] { return stop_ || !tasks_.empty(); }); if (stop_ tasks_.empty()) return; task std::move(tasks_.front()); tasks_.pop(); } task(); } }这段代码的关键点在于锁的作用域只包住取任务的部分task()是在锁外面执行的。这样多个工作线程可以真正并行执行任务。2.2 采集线程与推理线程的配合每一路视觉里采集线程负责从摄像头读帧读到的帧放进一个帧缓冲区然后通知推理线程。推理线程从缓冲区取帧做预处理和推理结果再交给输出阶段。这里有个设计选择是用线程池的任务队列来传递帧还是单独搞一个帧缓冲区我试过两种方案。方案A采集线程把“推理任务”直接提交到线程池队列。优点是简单缺点是帧和任务耦合在一起队列里堆积的是任务不好做丢帧策略。方案B单独搞一个环形帧缓冲区采集线程写推理线程读缓冲区满了就覆盖最旧的帧。优点是丢帧策略清晰缺点是多了个缓冲区管理。我最后选了方案B因为视觉场景下丢帧是常态用环形缓冲区更容易控制延迟。缓冲区大小设为3写指针追上读指针就覆盖保证推理线程拿到的永远是最新的帧。2.3 两路之间的资源隔离两路各一个线程池听起来隔离得很好但实际上它们共享CPU和NPU。如果两路同时提交推理任务NPU会排队这时候如果线程池的最大线程数设得太大反而会导致大量线程在NPU上排队上下文切换开销剧增。我的做法是在NPU推理这一层加一个全局信号量限制同时只有一路能占用NPU。这样虽然两路不能真正并行推理但避免了NPU排队导致的抖动。实测下来两路交替推理每路的有效帧率反而比无限制并发更高。提示RK3588的NPU是单核的虽然标称6TOPS但只有一个NPU核心所以推理任务本质上是串行的。与其让两路抢不如主动串行化减少调度开销。2.4 线程池的阻塞队列选择阻塞队列的选择直接影响到丢帧行为和延迟。常见的几种队列类型特点适用场景无界队列任务不丢但内存会涨离线批处理有界队列阻塞队列满时提交线程阻塞任务不能丢的场景有界队列丢弃最旧队列满时丢最旧的实时视觉有界队列丢弃最新队列满时拒绝新任务保护系统不被打爆视觉场景我选的是“有界队列丢弃最旧”。因为最新的帧才有价值旧的帧丢了不可惜。实现上就是在提交任务时如果队列满了就pop掉队首再push新任务。这里有个细节丢弃最旧的时候如果那个任务已经持有某些资源比如帧的引用要确保资源能正确释放。我用的是shared_ptr管理帧pop的时候引用计数减一不会泄漏。3. 双路视觉的完整实操流程3.1 环境准备与依赖安装先把基础环境搭好。香橙派5刷的是Ubuntu 20.04官方镜像里已经带了RKNN的驱动但Runtime库需要自己装。# 更新源 sudo apt update sudo apt upgrade -y # 安装OpenCV依赖 sudo apt install -y libopencv-dev cmake build-essential # 确认NPU驱动 ls /dev/rknpu* # 应该能看到 /dev/rknpu0 # 确认RKNN Runtime版本 cat /usr/lib/librknnrt.so | strings | grep versionRKNN Runtime的版本要和转换模型时用的toolkit版本匹配否则会报错。我用的toolkit是1.5.2Runtime也是1.5.2。版本不匹配的典型报错是“rknn_init fail”这时候先查版本。3.2 摄像头初始化与双路配置MIPI摄像头和USB摄像头的初始化方式不一样。MIPI的IMX415在香橙派上通常走V4L2设备节点是/dev/video0USB相机是/dev/video1或video2。# 查看摄像头设备 v4l2-ctl --list-devices # 查看支持的格式 v4l2-ctl -d /dev/video0 --list-formats-extMIPI摄像头我设的是1920x108030fpsNV12格式因为RK3588的ISP对NV12支持最好后面转RGB也快。USB相机设的是1280x72030fpsMJPEG格式因为USB带宽有限用MJPEG能省带宽。OpenCV读帧的时候MIPI那路用CAP_V4L2USB那路也用CAP_V4L2但USB的要设置FOURCC为MJPGcap.set(cv::CAP_PROP_FOURCC, cv::VideoWriter::fourcc(M,J,P,G)); cap.set(cv::CAP_PROP_FRAME_WIDTH, 1280); cap.set(cv::CAP_PROP_FRAME_HEIGHT, 720); cap.set(cv::CAP_PROP_FPS, 30);注意OpenCV读MIPI摄像头的时候如果格式设成NV12读出来的Mat是单通道的需要自己转RGB。我一般直接用cv::cvtColor(mat, rgb, cv::COLOR_YUV2BGR_NV12)。这一步在采集线程里做不要放到推理线程否则推理线程负担太重。3.3 线程池的初始化与任务提交每一路的线程池在初始化的时候创建生命周期和摄像头一致。初始化代码大概长这样class VisionPipeline { public: VisionPipeline(int cam_id, int width, int height) : pool_(2, 3, 4) { // 核心2最大3队列4 cap_.open(cam_id, cv::CAP_V4L2); cap_.set(cv::CAP_PROP_FRAME_WIDTH, width); cap_.set(cv::CAP_PROP_FRAME_HEIGHT, height); // ... 其他配置 } void start() { // 提交采集任务 pool_.submit([this] { captureLoop(); }); // 提交推理任务 pool_.submit([this] { inferLoop(); }); } private: void captureLoop() { while (running_) { cv::Mat frame; cap_ frame; if (frame.empty()) continue; // 写入环形缓冲区 frame_buffer_.write(frame); } } void inferLoop() { while (running_) { cv::Mat frame frame_buffer_.read(); if (frame.empty()) continue; // 预处理 推理 后处理 auto result infer(frame); // 提交输出任务 pool_.submit([this, result] { output(result); }); } } ThreadPool pool_; cv::VideoCapture cap_; RingBuffercv::Mat frame_buffer_; std::atomicbool running_{true}; };这里有个关键点inferLoop里提交输出任务的时候如果线程池队列满了会触发丢弃策略。输出任务被丢掉没关系因为下一帧马上就来。但采集和推理这两个核心任务不能被丢所以它们是在start()里直接提交的不经过队列的丢弃逻辑。3.4 推理阶段的参数配置yolov5s转RKNN的时候输入尺寸我设的是640x640。预处理包括resize、letterbox、归一化、NHWC转NCHW。这些操作在CPU上做耗时大概5-8ms。RKNN推理本身在NPU上耗时大概15-20ms。后处理NMS在CPU上耗时3-5ms。所以单帧的总耗时大概是25-35ms理论帧率28-40fps。两路交替的话每路的有效帧率大概20-25fps。这个数据是在CPU占用70%的情况下测的。推理的时候要注意RKNN的输入buffer要复用不要每帧都malloc。我是在初始化的时候申请好每帧memcpy进去。这样能省不少时间。// 初始化时 rknn_input inputs[1]; inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; inputs[0].size 640 * 640 * 3; inputs[0].fmt RKNN_TENSOR_NHWC; inputs[0].buf input_buffer_; // 预分配 // 每帧 memcpy(input_buffer_, preprocessed.data, 640 * 640 * 3); rknn_inputs_set(ctx, 1, inputs); rknn_run(ctx, nullptr); rknn_outputs_get(ctx, 1, outputs, nullptr);3.5 输出阶段的处理输出阶段包括画框、编码、显示或推流。如果只是本地显示用OpenCV的imshow就行但imshow必须在主线程调用不能在工作线程里调。所以我的做法是工作线程把结果放进一个结果队列主线程从队列取结果并显示。如果要推流可以用FFmpeg编码成H264然后通过RTSP或者RTMP推出去。RK3588有硬件编码器用FFmpeg的h264_rkmpp编码器能走硬件CPU占用很低。ffmpeg -f rawvideo -pix_fmt bgr24 -s 640x640 -r 25 -i - \ -c:v h264_rkmpp -b:v 2M -f rtsp rtsp://localhost:8554/live推流的时候要注意两路要推两个流或者拼成一路。拼成一路的话可以用hstack滤镜。4. 常见问题与排查技巧实录4.1 帧率上不去怎么办帧率上不去是最常见的问题。排查顺序是先看采集帧率再看推理耗时最后看输出耗时。采集帧率用v4l2-ctl查v4l2-ctl -d /dev/video0 --get-parm如果采集帧率就不够那是摄像头配置问题检查分辨率和格式。MIPI摄像头在1080p下如果设成RGB格式带宽可能不够要改成NV12。推理耗时用clock()打点auto t1 std::chrono::steady_clock::now(); rknn_run(ctx, nullptr); auto t2 std::chrono::steady_clock::now(); auto ms std::chrono::duration_caststd::chrono::milliseconds(t2 - t1).count();如果推理超过30ms检查是不是模型没量化或者NPU频率没拉满。NPU频率可以用以下命令查cat /sys/class/devfreq/fdab0000.npu/cur_freq如果频率低可以手动设成高性能模式echo performance | sudo tee /sys/class/devfreq/fdab0000.npu/governor4.2 两路互相干扰怎么排查两路互相干扰的典型表现是单独跑一路很流畅两路一起跑就都卡。这时候先确认是不是NPU排队导致的。可以在推理前后加日志看两路的推理时间是否重叠。如果确认是NPU竞争就加全局信号量串行化。如果加了信号量还是卡那可能是CPU竞争。用top看CPU占用如果某个核跑满100%说明有线程在自旋。还有一种可能是内存带宽不够。两路1080p的帧同时读写DDR带宽可能成为瓶颈。这时候可以降低分辨率或者用NV12格式减少带宽。4.3 线程池任务堆积怎么处理任务堆积的表现是延迟越来越大画面越来越滞后。用队列长度监控就能发现size_t queueSize() { std::unique_lockstd::mutex lock(mtx_); return tasks_.size(); }如果队列长度持续大于2说明消费速度跟不上生产速度。这时候要么降低采集帧率要么加快推理速度要么加大丢弃力度。我的做法是动态调整如果队列长度大于3就主动丢帧采集线程sleep 10ms再读下一帧。这样能快速把延迟降下来。4.4 常见问题速查表问题现象可能原因排查方法解决方案帧率低于预期采集格式不对v4l2-ctl查格式改成NV12或MJPEG推理耗时高NPU频率低查cur_freq设performance模式两路互相卡NPU竞争加日志看重叠加全局信号量延迟越来越大任务堆积查队列长度动态丢帧内存持续上涨帧引用未释放valgrind查泄漏用shared_ptr管理画面撕裂缓冲区竞争加锁检查用环形缓冲区摄像头打不开设备被占用lsof查占用杀掉占用进程模型加载失败版本不匹配查Runtime版本统一toolkit版本4.5 几个踩过的坑第一个坑OpenCV的VideoCapture在多个线程里同时读同一个摄像头会崩。我一开始想两路共用一个摄像头结果直接段错误。后来改成每路独立打开就没问题了。第二个坑线程池的析构顺序。如果线程池先析构工作线程还在跑会访问已经释放的资源。正确的顺序是先设置running_false等所有线程退出再析构线程池。第三个坑RKNN的context不是线程安全的。两路如果共用一个context推理结果会错乱。必须每路一个独立的context。第四个坑MIPI摄像头的曝光时间。默认曝光在室内光线下会过暗需要手动设置v4l2-ctl -d /dev/video0 --set-ctrlexposure1000这个值要根据实际光照调太大会过曝太小会太暗。5. 性能优化与扩展思路5.1 从双路扩展到多路这套架构其实很容易扩展到多路。每一路就是一个VisionPipeline实例各自拥有独立的线程池和RKNN context。扩展到4路的时候主要瓶颈在NPU和内存带宽。NPU串行化之后4路交替推理每路的有效帧率大概10-15fps。如果要求更高就得考虑模型轻量化比如用yolov5n或者剪枝后的模型。5.2 模型轻量化的几个方向yolov5s在RK3588上跑双路已经有点吃力如果要做4路建议换更小的模型。几个方向换yolov5n参数量从7.2M降到1.9M推理耗时能降一半用RKNN的混合量化把部分层量化成int8部分保持fp16剪枝去掉冗余的通道再用RKNN重新转换。我试过yolov5n双路能跑到35fps以上CPU占用也降到50%左右。5.3 线程池的进一步优化现在的线程池是固定大小的其实可以做成动态的。比如根据队列长度动态增减线程。但嵌入式环境里线程创建销毁的开销不小我一般还是用固定大小靠丢帧策略来保证实时性。另一个优化点是任务优先级。采集任务优先级最高推理次之输出最低。可以用两个队列来实现高优先级队列先消费。但这样会增加复杂度看项目需求决定要不要做。5.4 实际部署中的注意事项部署到现场的时候有几个点要注意摄像头要固定好震动会导致MIPI接触不良散热要做好RK3588满载的时候发热不小NPU降频会直接影响帧率电源要稳USB相机和MIPI相机同时工作电流需求不小劣质电源会导致摄像头掉线系统要关掉不必要的服务减少CPU占用。我在实际部署的时候遇到过USB相机在系统负载高的时候掉线的问题后来换了个带独立供电的USB Hub就好了。这种问题在实验室里不容易发现到现场才会暴露。最后再分享一个小技巧如果两路视觉的场景有重叠可以考虑把两路的推理结果做融合比如用卡尔曼滤波做目标跟踪。这样即使某一路偶尔丢帧跟踪也不会断。这个思路在安防和自动驾驶场景里很常用有兴趣的可以试试。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

深度学习图像处理实战:从CNN选型到模型部署全解析 2026/9/29 16:52:28

深度学习图像处理实战:从CNN选型到模型部署全解析

1. 图像处理为什么开始依赖深度学习1.1 传统算法做了几十年,哪些场景仍然吃力我经常被问到一个问题:传统图像处理是不是要被深度学习淘汰了?我的回答通常是:不是淘汰,而是分工变了。入行十年,我从OpenCV的阈…

阅读更多 →
个人微信API二次开发:群控管理与私域社群运营系统设计 2026/9/29 16:52:28

个人微信API二次开发:群控管理与私域社群运营系统设计

官方文档:GeWe API - GeWe API|微信 API 开发文档 一、业务痛点与技术背景 社群运营高频能力:建群、邀人、踢人、公告、关键词回复、违规治理、活跃统计。痛点在于: 群事件与消息回调混杂,规则引擎易误伤 多群广播无…

阅读更多 →
OpenClaw 本地 AI 智能体新范式:TaoToken 统一 Key 接入与技能插件化配置实战 2026/9/29 16:52:21

OpenClaw 本地 AI 智能体新范式:TaoToken 统一 Key 接入与技能插件化配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
AutoLabelImg实战:自动预标注到YOLO训练,效率提升指南 2026/9/29 16:52:08

AutoLabelImg实战:自动预标注到YOLO训练,效率提升指南

简介:这是一款面向深度学习图像识别场景的自动标注工具 AutoLabelImg,支持 YOLOv8/YOLOv9/YOLOv10 与 RT-DETR 等主流检测模型,适合需要快速构建训练数据集的算法工程师与科研人员。资源包共 532 个文件,压缩后约 83MB&#xff0c…

阅读更多 →
WX1860AL4千兆网卡iperf3性能不达标?五大根因排查实战 2026/9/29 16:51:55

WX1860AL4千兆网卡iperf3性能不达标?五大根因排查实战

上周刚把一张网讯WX1860AL4四口千兆网卡装进服务器,兴冲冲地拿iperf3打流,结果傻眼了:服务端到客户端怎么跑都只有480Mbps左右,离千兆Line Rate差了整整一半。第一反应是网卡有问题,退换货申请都写到一半了。但在折腾了…

阅读更多 →
从零构建AI工程能力:模型部署、推理优化与监控实战 2026/9/29 16:51:55

从零构建AI工程能力:模型部署、推理优化与监控实战

从零构建AI工程能力这件事,我前前后后折腾了差不多两年。最开始的时候,我和大多数人一样,觉得搞AI就是调包、跑模型、看准确率,直到真正要把一个模型塞进生产环境,才发现自己连最基本的工程化思维都没有。模型在notebo…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉