ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RK3588+摄像头跑通YOLOv5s:从抓帧到推理全流程指南

RK3588+摄像头跑通YOLOv5s:从抓帧到推理全流程指南 直接把上次那套跑通的YOLOv5s从“看图说话”改成“看摄像头说话”是很多玩香橙派RK3588的朋友都会卡一下的坎。离线推理demo大家都跑得很顺一提摄像头就懵摄像头节点在哪儿、示例代码要不要大改、抓回来的帧能不能直接喂给模型全是坑。这篇就从香橙派RK3588的Ubuntu环境出发手把手把摄像头接上YOLOv5s示例抓一帧、推理、画框、存图把整条链路走通。1. 先把思路捋清楚这一步到底要做什么1.1 从离线到在线差的不是“摄像头”这么简单你手上应该已经有一个能跑的YOLOv5s示例也就是那套“加载rknn模型 - 读取一张图片 - 推理 - 画框”的流程。离线版本里图片路径写死在代码里推理完拿张带框的图片交差。换成摄像头后唯一的变化好像是“把图片路径换成/dev/video0”但实际还要解决三件事第一摄像头能不能被系统识别分辨率、像素格式是不是模型输入想要的第二用OpenCV抓帧时帧率、颜色通道、对齐方式必须稳定可控第三推理引擎的输入接口只认特定排列的原始数据OpenCV默认的BGR图像不能直接塞进去中间要加转换和resize。这三件事没理顺哪怕示例代码只改一行跑起来也会出现“摄像头打开了但全是黑屏”“模型推理结果全零”“检测框画在错误位置”这类问题。所以我建议先不急着改代码按下面的顺序排查环境。1.2 动手之前先回答三个问题你的摄像头的接口是UVC还是MIPI香橙派RK3588两个都支持但调试方式完全不同。你的YOLOv5s模型输入是640x640还是其他尺寸推理前必须按模型输入的letterbox方式缩放。你的模型输入通道顺序是RGB还是BGRRockchip官方转出来的模型一般要求RGB而OpenCV抓出来的是BGR不做转置识别率会暴跌。这三个问题答案清楚了后面基本就是照着流水线做。如果还不清楚也不急我会在下一部分把“查摄像头”和“查模型”的命令都列出来。2. 摄像头准备与环境检查2.1 摄像头选型UVC还是MIPI香橙派RK3588上最常见的是两类摄像头。USB摄像头也就是UVC设备插上就能用内核自带uvcvideo驱动设备节点通常是/dev/video0或者/dev/video1。优点是真省事缺点是可用的分辨率和帧率受USB带宽限制而且多个UVC摄像头同时接入时节点号容易乱。MIPI CSI摄像头比如树莓派那类OV5647模块优点是延迟低、数据直接走ISP缺点是驱动要跟板级设备树匹配不是随便插一根排线就能识别。香橙派官方提供的OV5647摄像头一般需要在系统里启用对应的设备树overlay或者确认dmesg里有没有ov5647的注册日志。如果你只想快速验证“摄像头接上YOLOv5s”这件事我建议直接用USB摄像头。MIPI那套更适合后续做低延迟视频流产品调试成本高不少。2.2 确认板子“看见”摄像头系统起来后打开终端敲三条命令。ls /dev/video* v4l2-ctl --list-devices dmesg | grep -i uvc第一条看节点第二条看设备名和对应节点的映射关系。比如一条常见的输出是USB Camera: USB Camera (18ec:6688) /dev/video0 /dev/video1UVC摄像头出现两个video节点很正常一般video0是图像采集节点video1是metadata或者扩展单元。我们只在代码里用采集节点。第三条命令看内核日志如果你看到类似“uvcvideo: Found UVC 1.00 device”这种输出说明驱动挂上了。确认节点后再用v4l2-ctl看一下摄像头支持的格式和分辨率。v4l2-ctl --list-formats-ext -d /dev/video0重点关注有没有YUYV、MJPEG格式以及你需要的640x640、1280x720分辨率是否在列表里。如果摄像头最大只有640x480模型输入又是640x640也不是不能用但letterbox时会多出黑边实际检测区域变小。2.3 顺手把权限和分辨率帧率设置好很多朋友代码里打不开摄像头是因为用户不在video组里。Ubuntu下执行sudo usermod -a -G video $USER然后重新登录一次。注意别省这一步否则后面每次跑程序都要sudoOpenCV在这种组合下还容易出环境变量问题非常不划算。权限解决后可以用v4l2-ctl提前设置好帧率和控制项。v4l2-ctl -d /dev/video0 --set-parm30 v4l2-ctl -d /dev/video0 --set-ctrl exposure_auto1第一行把帧率固定在30fps第二行把自动曝光打开。很多人摄像头画质偏暗偏亮根本原因是自动曝光的控制项被OpenCV改坏了手动用v4l2-ctl恢复一下最省事。3. 改造YOLOv5示例代码从图片到摄像头3.1 原示例代码的骨架以Rockchip RKNN官方的yolov5 C示例为例核心流程通常是这样// 1. 加载rknn模型 rknn_app_context_t rknn_app_ctx; init_post_process(); ret init_rknn(rknn_app_ctx, model_path); // 2. 读取图片 cv::Mat img cv::imread(input_image_path); // 3. 前处理 letterbox(img, resized_img, model_input_width, model_input_height); // ... 把resized_img拷进rknn_input // 4. 推理 rknn_run(rknn_app_ctx.rknn_handle, rknn_inputs); // 5. 后处理 post_process(...); // 6. 画框 cv::rectangle(img, ...);改动其实就集中在第2步。把cv::imread换成cv::VideoCapture然后从摄像头里读一帧cv::Mat即可后面的流程完全不用动。但如果你只改这一行通常会遇到两个小问题。一是摄像头默认分辨率可能不是640宽前处理letterbox会把图像缩得很厉害画框时坐标映射没问题但图像细节丢失。二是视频设备打开时的像素格式和OpenCV默认的BGR期望不一致。所以抓帧部分建议单独封装别直接在主流程里塞一行read。3.2 抓帧逻辑VideoCapture怎么用才稳推荐的做法是写一个函数专门负责“从摄像头取一帧干净图像”。cv::Mat grab_frame(cv::VideoCapture cap) { cv::Mat frame; if (!cap.grab()) { std::cerr grab failed std::endl; return cv::Mat(); } if (!cap.retrieve(frame)) { std::cerr retrieve failed std::endl; return cv::Mat(); } return frame; }很多人直接用cap.read(frame)它等价于grab加retrieve但问题在于read失败时不区分“摄像头掉线”和“帧还没准备好”导致后续拿到空Mat直接崩。用grab和retrieve分开写至少可以定位到具体是哪一步出问题。打开摄像头的部分也建议显式设置参数。cv::VideoCapture cap(0); cap.set(cv::CAP_PROP_FOURCC, cv::VideoWriter::fourcc(M, J, P, G)); cap.set(cv::CAP_PROP_FRAME_WIDTH, 640); cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480); cap.set(cv::CAP_PROP_FPS, 30);如果摄像头支持MJPEG格式优先用MJPEG带宽占用更低帧更稳定。如果摄像头只有YUYV就把fourcc那行注释掉避免设置失败后一肚子火。打开后加一个判断if (!cap.isOpened()) { std::cerr failed to open /dev/video0 std::endl; return -1; }同时打印一下实际生效的分辨率。std::cout w cap.get(cv::CAP_PROP_FRAME_WIDTH) h cap.get(cv::CAP_PROP_FRAME_HEIGHT) std::endl;这一步能帮你发现“我以为设置成640x640结果摄像头只给320x240”这类尴尬情况。3.3 把一帧图像送进推理引擎拿到摄像头帧之后别忘了摄像头帧默认是BGR顺序。YOLOv5模型训练时常用RGB图像送入RKNN前要做一次颜色转换。cv::Mat bgr grab_frame(cap); cv::Mat rgb; cv::cvtColor(bgr, rgb, cv::COLOR_BGR2RGB);接下来按模型输入尺寸做letterbox。所谓letterbox就是图像等比缩放后不足的部分用灰色填充得到一个完整的正方形输入。YOLOv5训练时默认是这种预处理方式推理时保持一致检测框坐标映射才准确。cv::Mat resized; float scale std::min(640.0f / rgb.cols, 640.0f / rgb.rows); int new_w static_castint(rgb.cols * scale); int new_h static_castint(rgb.rows * scale); cv::resize(rgb, resized, cv::Size(new_w, new_h)); cv::Mat letterboxed(640, 640, CV_8UC3, cv::Scalar(114, 114, 114)); resized.copyTo(letterboxed(cv::Rect(0, 0, new_w, new_h)));这里有个细节填充像素值用114是YOLOv5官方代码的默认值如果你模型用的是自己训练的数据且改了填充色这里也要跟着改。然后把letterboxed的数据拷进rknn_input。rknn_input inputs[1]; inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; inputs[0].fmt RKNN_TENSOR_NHWC; inputs[0].size 640 * 640 * 3; inputs[0].buf letterboxed.data; inputs[0].pass_through 0;这里RKNN_TENSOR_NHWC对应HWM(Hight,Wight,Channel)的内存排列OpenCV的Mat.data正好是HWC连续排列能直接拷效率高。如果你模型导出时指定的是NCHW就得先把数据转成CHW排列否则结果全乱。3.4 后处理别只看“推理成功”四个字推理调用很简单。rknn_run(rknn_app_ctx.rknn_handle, nullptr); rknn_output outputs[8]; memset(outputs, 0, sizeof(outputs)); outputs[0].want_float 1; outputs[1].want_float 1; outputs[2].want_float 1; rknn_outputs_get(rknn_app_ctx.rknn_handle, 3, outputs, nullptr);YOLOv5s在RK3588上输出三个特征图分别为80x80、40x40、20x20大小每个网格点对应85个数x、y、w、h再加一个物体置信度和80个类别概率。RKNN拿到的是三个一维或三维的float数组需要自己解出检测框。这一部分如果从零写会很长最简单省事的办法是直接用rknn_model_zoo里yolov5的post_process它对三路输出的解码和NMS都处理好了。如果你的示例代码里没有可以按“置信度阈值0.25、NMS阈值0.45”这套标准参数逐网格解出目标框。注意坐标还原后处理得到的cx、cy、w、h是针对640x640输入图的要映射回原始摄像头图像必须除以letterbox的scale并且减去填充偏移。这步做错了框会整体往左下或右上飘。3.5 编译和运行的完整命令代码改完后编译命令大概是g yolov5_cam.cpp -o yolov5_cam \ -I/usr/local/include/opencv4 \ -L/usr/local/lib -lopencv_core -lopencv_imgproc -lopencv_highgui -lopencv_videoio \ -L/usr/local/lib/rockchip -lrknnrt \ -lpthread注意这里我加的是rockchip的RKNN库路径如果你的SDK装在其他位置换成实际路径。如果你用的是cmake就把这些依赖写进CMakeLists.txt。运行./yolov5_cam model/yolov5s.rknn 0 result.jpg第一个参数是模型路径第二个参数是摄像头编号0对应/dev/video0第三个参数是输出图片名。看到程序打印出推理耗时并生成result.jpg这一步就算成了。4. 一帧推理的耗时构成与结果校验4.1 用时间戳拆开三段耗时接完摄像头后很多朋友会问“这个YOLOv5s能跑多少帧”我先给一个香橙派RK3588上的参考数据。阶段耗时参考影响因素摄像头抓帧5~15ms像素格式、分辨率、USB带宽前处理(letterbox颜色转换)3~8ms是否用NEON优化、图像尺寸NPU推理30~45ms模型输入大小、int8量化、NPU核心数后处理(NMS)5~15ms检测目标数量、NMS实现方式加起来单帧大概50~80ms也就是12~20帧每秒。如果我只抓一帧就退出实际耗时会比连续跑低一些因为模型和摄像头都只初始化一次。想精确知道瓶颈在哪可以在代码里加时间戳。auto t0 std::chrono::steady_clock::now(); cv::Mat frame grab_frame(cap); auto t1 std::chrono::steady_clock::now(); // 前处理... auto t2 std::chrono::steady_clock::now(); // rknn_run... auto t3 std::chrono::steady_clock::now(); // 后处理... auto t4 std::chrono::steady_clock::now();然后打印每个阶段的耗时。我实际测过如果摄像头用YUYV格式、640x480分辨率抓帧时间会明显偏高换成MJPEG后能低不少。如果你觉得单帧太慢最直接的办法是换轻量模型或者把输入尺寸从640降到416但注意检测小目标的能力会下降。4.2 到底怎么判断这次推理“对了”接摄像头后不要只盯着控制台有没有打印检测结果。强烈建议把画完框的结果保存成图片肉眼确认。cv::imwrite(output_path, bgr); std::cout saved output_path std::endl;如果你用的桌面环境有显示器也可以直接imshow但香橙派很多时候是无头模式imshow会报错或者弹不出窗口。判断标准有三个第一能否正确识别你放在摄像头前的东西比如水杯、手机、人。第二检测框是否紧贴目标如果边框明显偏移检查letterbox坐标还原。第三推理结果里的置信度分数是否正常如果所有类别分数都很低大概率是颜色顺序或缩放方式不对。4.3 为什么我不让你只打印结果控制台打印出一堆“person 0.85 123 456 789 321”很容易让人误以为成功了。我接摄像头调试时最喜欢做的验证就是让模型识别一个我特定摆好的目标然后把保存的图片放大看。有时候打印的结果确实有框但框和实际物体完全对不上这种问题只有在图像上才能看出来。5. 摄像头接入常见坑与排查实录5.1 摄像头不出图先查设备节点和驱动我把遇到的典型情况整理成了一张快速定位表。现象可能原因处理方式打开摄像头失败报“cant open camera by index”节点号不对先ls /dev/video*把index改成实际节点报“permission denied”用户不在video组sudo usermod -a -G video $USER后重新登录报“device or resource busy”摄像头被其他进程占用fuser -v /dev/video0杀掉占用进程能打开但画面全黑曝光控制异常或镜头没盖v4l2-ctl --set-ctrl exposure_auto1画面花屏/颜色不对摄像头只支持MJPEG但OpenCV用了YUYV设置CAP_PROP_FOURCC为MJPG最容易被忽略的是fuser那条。很多朋友之前用v4l2-ctl或者OpenCV测过摄像头进程没退干净第二次打开就报busy。写代码时记得在程序开头检测一下设备是否已被占用。5.2 推理结果全零、置信度低、检测框偏移这几个问题看起来不同但根源几乎都在输入数据上。推理结果全零先检查rknn_input的size和model输入是否一致。如果模型输入是640x640你传了1280x720的连续内存后续数据读取会乱结果全零一点也不奇怪。置信度低很可能因为颜色顺序。OpenCV是BGR模型要RGB忘了cvtColor的话模型的检测能力会明显退化很多目标被当背景滤掉。检测框偏移十有八九是letterbox的缩放坐标没有还原。把后处理里每个检测框的坐标除以scale再减去padding值这一步别省。5.3 长时间跑摄像头掉线怎么办如果只抓一帧就退出这个问题体现不出来。但你想循环读摄像头跑几分钟后偶发掉线就要注意USB供电和驱动稳定性了。遇到掉线最简单粗暴的恢复方法是重启程序。但更优雅的办法是循环里加一个断线重连逻辑一旦grab或retrieve失败释放摄像头sleep一秒重新VideoCapture。这个方法在大多数USB摄像头上都能避免手动重启程序。另外建议给USB摄像头用独立供电或者带供电的HUB。香橙派RK3588的USB口供电能力有限摄像头加上各种转接线后容易出现供电不足表现就是间歇性丢帧或设备直接断开。5.4 热插拔时节点号变化的坑你插着两个UVC设备时/dev/video0和/dev/video1的分配顺序可能会在重启后互换。代码里写死0下次跑就可能采错摄像头。解决方式有两个一是用视频设备的v4l2-ctl --list-devices输出里找到确定的名字然后直接用/etc/udev/rules.d写一条udev规则固定节点二是在程序启动时扫描/dev/video*逐个测试能否打开并读取一帧用四字符序列号或分辨率特征来筛选。第二种不用翻系统文件对新手更友好。6. 从“抓一帧”到“跑视频流”还差这最后一步6.1 代码结构上的一个建议你可能会想既然能抓一帧那就写个while循环不断抓帧推理不就成了实时检测吗对但问题在于单线程循环的帧率不稳定而且后处理耗时会让摄像头帧缓冲堆积。如果是做验证demo就一帧一帧来跑完打印耗时不要急着上满帧率推理。如果后续要做视频流检测建议把“采集线程”和“推理线程”分开摄像头只负责往队列里放帧推理线程从队列取最新帧丢掉来不及处理的旧帧这样才能保证画面延迟可控。6.2 我个人踩过的最蠢的坑我最初接摄像头调试时摄像头画面看起来正常模型也正常但检测框总是“领先”目标半拍。后来发现是因为我没有清空OpenCV的内部帧缓冲视频流模式下读到的其实是滞后好几帧的旧画面而打印的时间戳却是当前时间让我误判了处理延迟。所以如果你后续做实时检测一定记得用cap.grab()丢弃旧帧或者主动清空缓冲。这个坑在抓单帧时不会出现但一旦进入视频流模式坑得非常隐蔽。把这一帧接上之后整个香橙派RK3588上的YOLOv5s才算是从“模型能跑”迈向了“摄像头能用”。后面的路无论是接RTSP、做多路摄像头还是切换成YOLOv8本质都是在这一帧的链路里做文章。
返回列表