从“图片测试通过”到“摄像头能出画面”,中间其实隔着一条不小的沟。因为跑图片推理的时候,数据是现成的,喂进去就行;但摄像头是外部硬件,要经过驱动识别、节点选择、格式协商、抓帧这一大串环节,任何一个地方卡住,模型再好都白搭。
这篇是在前面教程基础上的第09篇,硬件依旧是香橙派5(RK3588),模型依旧是YOLOv5s,不过目标从“检测一张测试图片”变成了“打开摄像头,抓一帧,推理,把框画出来”。听起来简单,实际推进的时候踩了好几个坑,我把完整的自检流程、核心代码和排查过程都整理在下面,照着做基本能少走一半弯路。想直接看代码的,可以直接跳到第4节;想在动手前搞明白摄像头和推理链路怎么配合的,建议从头看。
1. 为什么前8篇都过来了,这一篇才接摄像头
先说个背景。前面的教程里,我们已经完成了系统烧录、Python环境搭建、YOLOv5源码下载、权重文件准备、用detect.py跑了静态图片这几件事。按理说,YOLOv5这套东西已经能跑了。但这个“能跑”是指:你把一张图给它,它给你吐一张带框的图。真实项目里没有谁会拿张照片来检测,都是要处理摄像头实时画面。
之所以把摄像头放到第9篇才讲,是因为“增加一个摄像头外设”这个动作,会把原来纯软件的事情一下子拉回到硬件层面。
- 内核有没有识别到你的摄像头芯片?这里涉及UVC协议和驱动。
- 设备节点是
/dev/video0还是/dev/video1?多摄像头设备经常在这里翻车。 - OpenCV的VideoCapture能不能跟摄像头完成格式协商?比如MJPG、YUYV,协商失败就直接打不开。
- 抓出来的帧格式是BGR还是RGB?YOLOv5训练时用的是RGB,OpenCV默认读出来的是BGR,这个不一致会让颜色通道错乱。
手机或PC上用QQ视频、会议软件时觉得摄像头“插上就能用”,是因为系统已经把底层驱动和格式协商处理完了。但到了香橙派这种嵌入式板子上,尤其是精简系统、或者自己编译的固件里,很多环节都暴露在你面前,每个都是一道坎。
另外还要说一下,这次我用的是USB免驱摄像头,市面上常见的海康威视、罗技C270、还有那种几十块的USB工业相机,都是UVC协议,插上就能被Linux识别。为什么不选MIPI CSI接口的摄像头,比如树莓派OV5647那种?
| 摄像头类型 | 接口 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| USB UVC摄像头 | USB 2.0/3.0 | 即插即用,免驱或半免驱,OpenCV直接支持 | 延迟稍高,CPU参与传输,线缆容易松动 | 快速起步、原型验证 |
| MIPI CSI摄像头 | MIPI CSI-2排线 | 延迟低,画质稳定,带宽独立 | 香橙派5需要特定转接板,设备树配置复杂,不同板子驱动不通用 | 量产级视觉方案 |
| RTSP网络摄像头 | 以太网/Wi-Fi | 布线方便,距离远,多路复用 | 需要网络环境,推流延迟受带宽影响 | 分布式监控、远程巡检 |
如果你手头只有MIPI摄像头,也不是不能用,但需要提前确认香橙派5的内核和设备树是否支持,还要配置dts节点,这个作为单独话题展开比较合适。基础教程阶段,老老实实上USB摄像头是最稳的路线,一次配好,后面所有例子都能复用。
还有,有些朋友问我“RK3588不是有NPU吗?为什么还不直接上NPU推理”。原因很简单:原生YOLOv5是PyTorch框架,NPU不认识PyTorch模型的算子,需要先用RKNN-Toolkit2做模型转换和量化,再走RKNN Runtime接口。那一步涉及较多工具链适配。这一篇先把PyTorch版本的推理链路跑通,之后单独开一篇讲NPU转换。硬件平台不变,模型不变,先有流程,再优化性能,顺序不会错。
2. 动手前先过一遍自检清单,30秒定位摄像头问题
很多新手接到“打开摄像头”这个任务,第一反应就是直接开写cap = cv2.VideoCapture(0),然后跑,然后报错,然后不知道该干嘛。我自己的习惯是,先花半分钟做系统层面的检查,确认板子真的“看见”了摄像头,再去碰代码。
2.1 用 lsusb 确认硬件枚举
lsusb正常的情况下,会看到一行类似:
Bus 002 Device 003: ID 0c45:636b Microdia USB Camera0c45这类ID是常见的摄像头芯片厂商标识。如果这行都没有,说明物理链路就有问题,先不要纠结代码,试试:
- 换一个USB口,优先USB3.0口(有些USB2.0口供电不够,摄像头初始化失败)。
- 确认线材是不是只供电不通数据。遇到过好几次,充电线当数据线用,灯亮但枚举不上。
- 拔插之后再跑一次
lsusb,排除接触不良。
热词里出现频率很高的“ubuntu连接不到摄像头”,十有八九就是死在物理枚举这一关。
2.2 确认设备节点
接着看节点编号:
ls /dev/video*如果板子上没有别的摄像头,一般是/dev/video0,有时候会系统同时生成/dev/video1、/dev/video2,这实际上是同摄像头的不同元数据节点,通常用video0就可以了。多摄像头场景下,如何区分video0、video1这类的设备编号,是很多开发者的痛点,这部分后面找机会单独展开。这一篇先专注单摄像头单节点。
2.3 用 v4l2-ctl 看摄像头支持的能力
Linux自带的v4l2-ctl工具非常好用,如果系统没装,先补一下:
sudo apt install v4l-utils -y v4l2-ctl --list-devices v4l2-ctl -d /dev/video0 --list-formats-ext这个命令会罗列摄像头支持的像素格式与分辨率。有些摄像头只支持YUYV 640x480,你却在代码里非要设1280x720,OpenCV不会报错,但默默不生效,读出来的帧还是640x480。提前看一眼,能少怀疑一次人生。
2.4 Python 裸测:先不加载模型,只看它能吐帧
python3import cv2 cap = cv2.VideoCapture(0) if not cap.isOpened(): print("摄像头打开失败") exit(1) ret, frame = cap.read() print(ret, frame.shape if ret else "无帧") cap.release()如果上面这段能输出True (480, 640, 3)之类的形状信息,说明摄像头链路完全没问题。这步先跑通,再往下面走。
从这步开始就可以准备把后面所有摄像头相关代码写进一个脚本了。这一篇的核心思路是“抓一帧,推理一帧”,跟那种持续跑视频流的逻辑不太一样,先把这个差异说清楚再动手改代码。
3. 抓一帧和跑视频流,底层逻辑不是一回事
YOLOv5官方仓库里的detect.py其实写得很完整,也支持摄像头输入。你直接运行:
python detect.py --weights yolov5s.pt --source 0它会一直循环读摄像头帧,持续推理并弹窗显示实时画面。你要退出就得按Ctrl+C。看起来好像这一篇的核心需求已经满足了?其实不然。
官方detect.py的设计目标是“持续检测”,它会一帧接一帧地跑推理,中间还包含FPS统计、窗口显示等逻辑。但我们是“抓一帧并推理”的场景,典型的比如:
- 按一次按键触发一次拍照检测;
- 定时巡检测试,来了某一帧才去做一次推理;
- 嵌入式终端通过串口或GPIO收到外部信号才抓拍一张。
这些场景如果直接跑官方detect.py,要么得靠外部进程发信号去kill它,要么就得改它的循环逻辑,非常别扭。更关键的问题是,官方脚本把整个“推理管线”封装得太严密,新手很难看清楚数据在每一层到底发生了什么变化。所以我在这里坚持自己写一个独立脚本,把链路拆开,边写边讲道理。
YOLOv5的单帧推理链路,本质上是五步:
- 抓帧:从摄像头获得一张BGR格式的原始图像。
- 前处理:把输入缩放并填充到模型输入尺寸(通常640x640),同时保持原始宽高比,防止目标变形。
- 张量变换:把HWC布局转成CHW,把BGR转成RGB,归一化到0~1,并且补上batch维度。
- 网络推理:模型输出一个三维张量,维度分别是候选框数量、每个框的坐标、置信度、类别概率。
- 后处理:非极大值抑制去掉重复框,坐标映射回原始图像,绘制边框和标签。
其中第2步的letterbox可能是最容易被忽视的。很多人图省事直接resize成640x640,结果就是用方形变歪的图像喂给网络,detect精度下降明显。letterbox的做法是保持比例缩放图像,剩余区域填充灰色像素,这样目标虽然变小了一点,但形状不变,模型识别的可靠性高得多。这个细节,官方的detect.py是帮你处理好的,自己写代码就必须自己看着办。
我再强调一点:很多嵌入式板子上的教程,告诉你“拿OpenCV读帧,然后直接调模型”,看起来像是一个整体,实际上每一行代码都对应着模型训练时的预处理方式。训练时用letterbox,推理时就必须匹配;训练时在RGB空间,推理时就必须转RGB。链路里任何一个环节和训练时不匹配,推理结果都会大打折扣。自己写脚本的另一个好处,就是能准确感知这种匹配关系。
4. 操刀改造:抓一帧并推理的完整代码
下面这个脚本就是这一篇的核心产物。它做的事情很简单:打开摄像头,抓一帧,跑一次YOLOv5s推理,在原始帧上画出检测框,保存结果图。代码基于YOLOv5官方v6.x/v7.x源码结构,建议放在官方仓库根目录下运行,这样models、utils这些包能直接导入。
# single_shot_camera.py import cv2 import time import torch import numpy as np from pathlib import Path from models.experimental import attempt_load from utils.augmentations import letterbox from utils.general import non_max_suppression, scale_coords from utils.torch_utils import select_device # 基本参数 weights = 'yolov5s.pt' # 换成你自己的best.pt也行 source = 0 # /dev/video0 imgsz = 640 # 输入尺寸 conf_thres = 0.25 # 置信度阈值 iou_thres = 0.45 # NMS IoU阈值 save_path = 'camera_result.jpg' # 设备选择,香橙派上没有NVIDIA GPU,老老实实用CPU device = select_device('cpu') torch.set_num_threads(4) # A76核心留2个给系统 # 加载模型 model = attempt_load(weights, map_location=device) model.eval() # 摄像头初始化 cap = cv2.VideoCapture(source) if not cap.isOpened(): print("摄像头打开失败,检查节点") exit(1) # 先丢几帧,让摄像头完成自动曝光,避免第一帧全黑 for _ in range(5): cap.read() # 抓一帧 ret, frame = cap.read() if not ret: print("抓帧失败") cap.release() exit(1) original_shape = frame.shape[:2] # H, W # letterbox前处理:等比缩放 + 灰边填充 img = letterbox(frame, new_shape=imgsz, stride=32)[0] # BGR -> RGB, HWC -> CHW, 归一化 img = img[:, :, ::-1].transpose(2, 0, 1) img = np.ascontiguousarray(img) img_tensor = torch.from_numpy(img).to(device) img_tensor = img_tensor.float() / 255.0 if img_tensor.ndimension() == 3: img_tensor = img_tensor.unsqueeze(0) # 推理 with torch.no_grad(): pred = model(img_tensor)[0] pred = non_max_suppression(pred, conf_thres=conf_thres, iou_thres=iou_thres) # 后处理:坐标映射回原图 det = pred[0] if det is not None and len(det): det[:, :4] = scale_coords(img_tensor.shape[2:], det[:, :4], original_shape).round() # 绘制结果 for *xyxy, conf, cls in reversed(det): x1, y1, x2, y2 = map(int, xyxy) label = f'{int(cls)} {conf:.2f}' color = (0, 255, 0) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) cap.release() cv2.imwrite(save_path, frame) print(f"推理完成,结果保存在 {save_path},共检测到 {0 if det is None else len(det)} 个目标")代码本身不长,核心逻辑清晰。有几个地方值得多说一句:
4.1 为什么 letterbox 要指定 stride=32
YOLOv5的输出特征图有三个尺度,总下采样倍数正好是32倍。letterbox如果不带stride参数,填充后尺寸可能不是32的整数倍,模型输入尺寸不对会报错。指定了stride=32之后,它会自动把尺寸向上取整到32的倍数,保证网络各层特征图尺寸整除。
4.2 为什么先丢5帧
摄像头刚打开的瞬间,自动曝光和自动白平衡还没稳定,前几帧通常是黑的或一片惨白。直接抓第一帧去推理,很可能得到一张全黑图片。所以我习惯先read几张扔出去,再从第6帧开始正式抓取。这是很多教程不会写的小细节。
4.3 BGR、RGB、HWC、CHW,容易混的四个字母
OpenCV读进来的图是BGR、HWC布局,YOLOv5模型运行时需要的是RGB、CHW布局且归一化到0~1。代码里用img[:, :, ::-1]完成了BGR到RGB的翻转,用transpose(2, 0, 1)完成了HWC到CHW,再除以255归一化。如果这步偷懒,模型的类别预测会错乱,因为颜色通道输入错了。这也是很多“同样代码为什么我效果差”的悬案原因之一。
4.4 为什么要 cap.release() 释放设备
摄像头设备是独占资源,如果不释放,下一次运行脚本就可能报“can not open camera”。特别是在反复调试环节,脚本没走到release就异常退出的话,那个video0节点会被占用一段时间,必须等内核超时释放或重启板子才能恢复。调试期建议尽量用try/finally保证释放。
到这里,本篇文章的核心代码就算给出来了。你可以先跑通,看看输出图片里有没有框。第一次跑,速度和效果可能都不理想,所以下一步我把会遇到的问题集中复盘一遍。
5. 排坑实录:我从这个流程里走出来,踩了不止5个坑
作者按:下面的内容全部来自实测。香橙派5(RK3588)的硬件条件决定了它的坑位跟x86 PC不太一样,CPU架构、USB控制器、供电方案都有差别。一个在PC上正常运行的脚本,拿到板子上可能就是打不开、不出图、巨卡,原因各不相同。
5.1 摄像头“打不开”/“can not open camera by index 0”
按出现的概率排,原因有三个。
一是权限问题。默认情况下/dev/video0只有root和video组能访问,而你的shell用户不一定在video组里。解决办法:
sudo usermod -a -G video $USER然后重新登录,或者直接重启板子。不想重启的话,临时给个权限:
sudo chmod 777 /dev/video0二是系统里其实有多个video节点,但你没有选对。查看一下:
v4l2-ctl --list-devices视频设备名字下面会跟着一串子节点,选一个看起来是“主视频流”的节点。
三是USB控制器供电不足。香橙派这种主板,USB口带大功率外设时,初始化容易失败。测试方法很简单:把摄像头插到另一个口,最好插USB3.0直连的口,避开扩展HUB。遇到过好几次摄像头在PC上正常、插板子就枚举不到,十有八九是供电问题。
5.2 打开成功了,但画面花屏或撕裂
花屏通常不是模型问题,而是帧格式与尺寸不匹配。有些摄像头的出厂默认格式是MJPG,OpenCV打开后如果系统里没有对应的解码器,就会出现马赛克或者半绿半花的情况。这时候可以强制指定期望的格式:
cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)设了之后读一帧打印frame.shape确认是否生效。不生效就换分辨率,摄像头通常只有固定的几组分辨率,比如640x480、1280x720,不能随意指定。这在第2.3节查list-formats-ext的时候就能提前避雷。
5.3 抓到的帧全黑
不是bug,大部分时候就是“第一帧曝光未完成”。前面代码里已经加了丢帧逻辑。如果你是自己写循环,也建议抓帧之前time.sleep(0.2)或者丢弃前几帧。还有两种情况:一是镜头盖没摘(别笑,真遇到几次),二是晚上反光导致自动曝光算法起不来,可以用手遮挡镜头一下再试。
5.4 速度慢得离谱:一帧推理要好几秒
先明确一个预期。香橙派5(RK3588)的CPU是4个A76大核加4个A55小核,在纯PyTorch CPU推理下,YOLOv5s输入640x640,单帧的推理时间大约在0.3~0.8秒之间,具体跟板子的散热、频率、内存配置都有关系。跑出1秒上下都属于正常。如果你发现一帧要3秒以上,先排查下面几个点:
- 确认板子是不是被调度到了小核。可以用
cat /sys/devices/system/cpu/cpu*/cpufreq/policy/scaling_cur_freq看看当前频率,如果一直在1.8GHz以下,可能是系统负载高或过热降频。 - 在脚本里加上
torch.set_num_threads(4),给足并发线程后面再考虑“NPU部署”“RKNN模型转换”的效率提升路径。如果4个线程都满负荷,再往上加线程其实性能收益很小,反而可能因为线程切换变慢。 - 确认用的是
yolov5s.pt,而不是m或l版本。之前试过拿yolov5l.pt(模型里的l版,大约200MB)在板子上跑,单帧推理4秒以上,内存也吃紧。低算力平台选模型要克制。
5.5 内存不足,推理到一半进程被杀
香橙派5有4GB/8GB/16GB内存版本,如果你用的是4GB版本,加载PyTorch(约800MB)+ YOLOv5s权重(约14MB)+ 输入张量 + OpenCV缓冲,加上系统本身,内存确实会比较紧张。如果同时开了桌面环境,非常容易触发OOM。建议:
- 终端命令行启动,不要进桌面环境;
- 推理前用
free -h检查剩余内存; - 如果实在不够,把
imgsz从640降到480,显存占用会减少很多,速度也更快,代价只是小目标检测率下降。
5.6 detect.py 弹窗显示的问题
香橙派如果通过SSH远程连接操作,不带图形环境,cv2.imshow会直接报错或者无输出。所以我在代码里没有用imshow显示,而是把结果imwrite保存到本地,然后用scp拉回来查看。如果是接了HDMI屏幕、在板子上本地操作,改成cv2.imshow也没有问题。远程调试是这个阶段比较省事的做法,不用来回切环境。
这些坑看着零散,但几乎每一个都有明确的排查方向和解决套路。把自检清单跑一遍,再对照上面这几条,大部分问题都能在5分钟内定位。
6. 从单帧到连续帧,只需要加一个while循环
单帧抓取推理跑通之后,很多人第一反应就是:“我要看实时效果”。这时候把代码改造成连续帧循环,其实非常简单。核心就三件事:
- 把
cap.read()放进while True循环; - 每次推理完把绘制结果
imshow出来; - 按
q键退出,并释放资源。
while True: ret, frame = cap.read() if not ret: continue # 前处理 img = letterbox(frame, new_shape=imgsz, stride=32)[0] img = img[:, :, ::-1].transpose(2, 0, 1) img = np.ascontiguousarray(img) img_tensor = torch.from_numpy(img).float() / 255.0 img_tensor = img_tensor.unsqueeze(0) with torch.no_grad(): pred = model(img_tensor)[0] pred = non_max_suppression(pred, conf_thres, iou_thres) det = pred[0] if det is not None and len(det): det[:, :4] = scale_coords(img_tensor.shape[2:], det[:, :4], frame.shape[:2]).round() for *xyxy, conf, cls in reversed(det): x1, y1, x2, y2 = map(int, xyxy) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f'{int(cls)} {conf:.2f}', (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) cv2.imshow('yolov5s', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()日志里如果输出帧率不稳定,多半是受推理耗时本身影响,而不是摄像头帧率低。要算FPS的话,在循环首尾各取一次time.time(),用1 / (end - start)就行。
值得留意的是,连续帧循环里,每帧都跑一次实时推理,CPU占用率会很高。有人会在这里加“跳帧”策略,逻辑也很朴素:摄像头抓帧是实时的,但检测不必每帧都执行,可以每抓3帧只推理1帧,让占用率和响应速度达到一个可接受的平衡。这个策略实际工程中很常见,下次做视频流项目时可以直接套用。
再说一个后续方向。如果要在香橙派5上把YOLOv5s的推理性能真正拉满,纯CPU这次只是“跑通”,下一步就是走RK3588自带的NPU。实现方式不是重新写一套逻辑,而是用RKNN-Toolkit2把.pt权重转换成.rknn格式,再用Rockchip的Python推理库去加载。那段链路跟本篇的代码风格接近,但多了模型预处理、量化、板端验证几个步骤,到时候单独开一篇,工程量不比这一篇小。
我个人建议是:这篇的单帧推理脚本,别看它简单,它是后面所有“触发式检测”功能的地基。你想做按键抓拍、定时巡检、串口/GPIO联动拍照,全部是从“抓一帧,推一帧”这个模式延伸出来的。把今天这条链路吃透,摄像头这块的架子就算搭稳了。