ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

树莓派+OpenCV DNN实现目标检测与单目测距:电赛C题实战方案

树莓派+OpenCV DNN实现目标检测与单目测距:电赛C题实战方案 简介这套代码是二零二五年电赛C题的树莓派视觉方案面向电子设计竞赛参赛者与嵌入式视觉初学者。它基于OpenCV库实现了目标检测与测距其中一个脚本用于解决透视n点定位问题通过多个特征点估算相机与目标的相对位姿另一个脚本则负责形状检测识别矩形、圆形等几何目标轮廓。两个脚本配合可在树莓派上完成从读取图像、识别目标到计算距离的完整流程适用于自动化监控、机器人导航、增强现实等场景。资源压缩包内共2个Python源码文件整体仅6KB体量非常精简便于直接研读和修改也方便迁移到其他摄像头模块上目前已有563人学习下载适合想在计算资源有限的树莓派上快速部署视觉算法、为电赛搭建原型的开发者。通过学习这份代码既能理解PnP测距和形状检测的具体工程实现也能掌握在低算力环境下优化OpenCV算法的实用思路是一份轻量但完整的学习参考。 2025电赛C题一出来很多队伍就把主控盯上了树莓派。赛题要求的目标检测与测距放到STM32这类单片机上会非常吃力而树莓派上套OpenCV配合轻量级目标检测模型和单目测距标定能在短时间内打出一套能上场的方案。这篇内容把我带队参赛时整理的代码思路完整拿出来包括OpenCV DNN怎么加载模型、检测结果怎么换算距离、树莓派环境怎么配以及我们踩过的几个坑给准备走这条路线的同学做参考。1. 整体思路与方案选型1.1 为什么这套方案能贴合电赛C题电赛C题通常属于控制或测量类核心是“看到目标、知道在哪、给出反馈”。树莓派在这里的优势是完整Linux系统、Python/C都能跑、摄像头接口现成代码调试和算法替换都比单片机舒服太多。以2025年赛题来看目标检测与测距属于典型视觉任务树莓派4B/5完全能撑住。如果走传统单片机路线比如用STM32配合OpenMV或者MaixCam也不是不行但瓶颈很明显OpenMV算力有限目标检测只能靠颜色阈值或模板匹配场地光线一变就容易翻车而深度学习模型在单片机上的部署门槛又高大多数队伍在备赛期搞不定。树莓派价格合适、资料多、社区成熟在比赛场景下属于“性价比最高的平衡点”。如果上Jetson Nano或Orin性能确实强但价格贵、功耗高不少赛区还限制板卡功耗。树莓派5相比4B性能又提升了一大截跑YOLO级别的小模型基本能达到实时。说实话C题这个计算量树莓派是刚好够用的一个台阶选它当主控并不只是图省事而是综合算力、成本、开发效率以后最合理的选择。1.2 检测和测距两条技术线的选型目标检测的部分我们最终选择了OpenCV DNN YOLOv4-tiny。为什么不直接上YOLOv8那套因为它在树莓派上推理要依赖PyTorch环境重、内存占用大树莓派跑起来很吃力。而用OpenCV DNN读取darknet权重或onnx模型部署非常轻只需要一份OpenCV就能跑C和Python接口都有也完全符合题目“使用OpenCV”的要求。赛前我们还对比过传统视觉方案比如用HSV颜色提取加轮廓找目标。这套在实验室里很稳但到了比赛现场灯光、反光、背景颜色都会干扰一点点参数变化就会导致目标丢失。深度学习模型的泛化能力明显强很多哪怕目标角度变一点、光线暗一点置信度可能下降但不会直接检测不到。所以只要条件允许优先上模型。测距方面我们用单目测距公式法没有上双目摄像头。双目的精度确实更高但标定流程复杂两个摄像头装在车模上稍微震动一下参数就偏了比赛现场根本没时间重新标。深度相机又要额外硬件成本高且供电压力大。反而是单目测距只要知道目标物体的物理高度/宽度加一次简单的标定精度能做到5%以内对C题测量任务来说完全够用。选型对照表我放这儿了备赛时可以快速做个决策。方案成本开发量推理速度部署难度适合场景OpenMV STM32低中中中颜色识别、简单检测树莓派 OpenCV DNN中低中低目标检测、单目测距Jetson PyTorch高中高高复杂模型、多路视觉树莓派 深度相机高中中中高精度测距需求2. 目标检测模块实现2.1 模型准备YOLOv4-tiny的下载与放置要跑OpenCV DNN首先得有模型文件和配置文件。YOLOv4-tiny的cfg和weights在darknet的GitHub仓库都有yolov4-tiny.weights大约23MB完整版YOLOv4有244MB树莓派跑完整版基本是幻灯片tiny版本是性能和精度的折中。把yolov4-tiny.cfg和yolov4-tiny.weights放进项目目录然后用OpenCV加载。如果你用的不是YOLO系而是MobileNet-SSD那对应的就是deploy.prototxt和.caffemodel加载方式稍有区别但逻辑一模一样。这里我们以YOLOv4-tiny为主讲。还需要一个类别名称列表。官方权重对应COCO数据集的80类从person、car、bicycle等开始。注意类别顺序必须和模型训练时一致否则检测结果会张冠李戴。常见错误就是自己手敲的类别顺序和coco.names对不上导致明明检测到的是杯子输出却显示“人”。2.2 基于OpenCV DNN的推理代码解析推理代码的核心步骤是加载模型、构建blob、前向推理、解析输出。下面这段是完整可跑的框架代码import cv2 import numpy as np # COCO类别列表这里只截了几行实际要放完整80类 coco_names [person, bicycle, car, motorcycle, airplane, bus, train, truck, boat, traffic light] # 加载模型 net cv2.dnn.readNet(yolov4-tiny.cfg, yolov4-tiny.weights) net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) # 打开摄像头树莓派上建议用CAP_V4L2 cap cv2.VideoCapture(0, cv2.CAP_V4L2) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) CONF_THRESH 0.4 NMS_THRESH 0.4 def letterbox(img, size416): h, w img.shape[:2] ratio min(size / w, size / h) new_w, new_h int(w * ratio), int(h * ratio) resized cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_LINEAR) canvas np.full((size, size, 3), 128, dtypenp.uint8) x_offset (size - new_w) // 2 y_offset (size - new_h) // 2 canvas[y_offset:y_offset new_h, x_offset:x_offset new_w] resized return canvas, ratio, x_offset, y_offset while True: ok, frame cap.read() if not ok: break h, w frame.shape[:2] input_blob, ratio, x_off, y_off letterbox(frame, 416) # 构建blob归一化到0~1BGR转RGB blob cv2.dnn.blobFromImage(input_blob, 1/255.0, (416, 416), swapRBTrue, cropFalse) net.setInput(blob) outs net.forward(net.getUnconnectedOutLayersNames()) boxes [] confs [] class_ids [] for out in outs: for detection in out: scores detection[5:] class_id np.argmax(scores) confidence scores[class_id] if confidence CONF_THRESH: cx, cy, bw, bh detection[:4] # 还原到416输入图上的像素坐标 cx, cy, bw, bh cx * 416, cy * 416, bw * 416, bh * 416 x1 int((cx - bw / 2 - x_off) / ratio) y1 int((cy - bh / 2 - y_off) / ratio) x2 int((cx bw / 2 - x_off) / ratio) y2 int((cy bh / 2 - y_off) / ratio) boxes.append([x1, y1, x2 - x1, y2 - y1]) confs.append(float(confidence)) class_ids.append(class_id) idxs cv2.dnn.NMSBoxes(boxes, confs, CONF_THRESH, NMS_THRESH) for i in idxs: x, y, w_box, h_box boxes[i] label coco_names[class_ids[i]] cv2.rectangle(frame, (x, y), (x w_box, y h_box), (0, 255, 0), 2) cv2.putText(frame, f{label} {confs[i]:.2f}, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) cv2.imshow(detect, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这里有一个特别容易踩坑的地方YOLO输出的坐标是相对网络输入尺寸416x416的归一化坐标先乘回416得到输入图上的像素坐标再通过letterbox的偏移和缩放还原到原始画面。如果你跳过letterbox直接把416坐标映射到640x480框就会整体偏移。很多同学的检测框“看起来框住了但位置差一点”十有八九是这一步处理错了。2.3 后处理参数怎么调代码里的CONF_THRESH和NMS_THRESH是直接影响结果的两个参数。置信度阈值设高了误检少但容易漏检设低了检测框会乱蹦。我们在树莓派上的经验值置信度0.3到0.4之间NMS阈值0.4到0.5之间。比赛场地目标单一、背景干净可以大胆把置信度调高到0.5减少误报对后续测距的干扰。另外建议把FPS打印出来看。在树莓派4B上YOLOv4-tiny输入416大约3到5帧输入320能到5到8帧树莓派5会快很多。如果帧率低于2就是模型对于这个平台来说偏重了可以考虑降分辨率或者换更轻的模型。调试时还可以把检测框的坐标和宽高直接打印出来。如果某个目标框的宽度偶尔变成正常值的两倍多半是NMS没压住同一个目标的多个候选框这时候先检查NMS阈值不要急着加滤波。3. 单目测距算法实现3.1 原理小孔成像与像素焦距单目测距的核心是相似三角形。目标在画面里的像素尺寸和它的实际物理尺寸、以及到相机的距离存在一个固定比例关系。简单写出来就是D (W_real * f) / w_pixel其中D是目标到相机的距离W_real是目标某个方向上的真实物理长度w_pixel是目标在图像里对应的像素长度f是像素焦距单位是像素。这个f不需要查相机参数表可以直接标定出来。把目标放在已知距离处测出它的像素长度反推f (w_pixel * D) / W_real。标定一次以后只要摄像头焦距不变这个值就一直有效。3.2 实操标定流程我在备赛时的做法是拿一个已知高度的小车模型当目标。假设车模真实高度是0.25m先放在距离相机1m处测出检测框像素高度约208px放在2m处像素高度约104px。两次计算出来的f都在520左右取平均即可。具体步骤固定摄像头确保比赛过程中位置不再移动。在1m、2m、3m处分别放置目标记录对应的框高。代入公式f (h_pixel * D) / H_real得到多组f取平均值。将f和H_real写进代码常量。这里有个小坑测像素高度时不要用人眼在截图里量而是直接用YOLO检测框的h_box。这样标定的是一整套管线模型检测框测距的联合参数而不是单纯的相机内参实际使用时的误差更小。3.3 地面投影法更稳的高阶测距如果比赛要求对地面上的目标测距而且目标底部能稳定检测出来可以换一种更稳的公式叫地面投影法D (H_cam * f) / (y_bottom - y_horizon)其中H_cam是摄像头离地面的高度f是像素焦距y_bottom是目标检测框底边的像素纵坐标y_horizon是画面里地平线的像素纵坐标。注意这个地平线不是真的地平面线而是“无穷远处目标的底边位置”。这个公式的好处是不需要知道目标的物理高度只要目标底部贴地就能测。缺点是地平线位置必须标定准确。标定方法放一个目标在很远很远的地方比如10m以上记录检测框底边的纵坐标近似当作地平线。我们当时用这个方法之后测距稳定性明显提升。因为检测框高度的波动往往比较大但底边y坐标只要目标没有遮挡波动就很小。如果你用的是垂直固定的摄像头这个方法可以优先考虑。3.4 距离跳变用中值滤波压掉无论用什么公式单帧测出来的距离都会有波动。我们实测波动幅度大概在3%到8%左右如果检测框偶尔跳一下距离能瞬间偏出20%。处理办法很简单维护一个最近N帧的距离缓冲取中位数输出dist_history [] def smooth_distance(d, window5): dist_history.append(d) if len(dist_history) window: dist_history.pop(0) return sorted(dist_history)[len(dist_history) // 2]为什么用中值而不是平均值因为平均值会被离群值拉偏中值可以直接丢掉极端值。如果一帧误检导致距离偏到10m平均值会很难看但中位数完全不受影响。窗口大小取5到7比较合适窗口太大响应慢窗口太小滤波效果弱。4. 树莓派环境配置与部署4.1 OpenCV安装别折腾源码编译最早找教程的时候网上很多帖子教你从源码编译OpenCV还要配CUDA。在树莓派上这么做完全是浪费时间一个晚上就搭进去了编译完还不一定比pip装在性能上强多少。我的建议是直接pip安装sudo apt update sudo apt upgrade -y sudo apt install -y python3-pip pip3 install opencv-python --break-system-packages新版树莓派OS的pip有外部管理限制需要加--break-system-packages。如果你用apt的python3-opencv也行版本会旧一点但胜在省事。我个人推荐先把apt源换成国内镜像然后直接sudo apt install python3-opencv比赛现场网络不稳定时也能装完稳定可靠。装完一定要验证一下python3 -c import cv2; print(cv2.__version__)如果输出4.x版本号就说明OpenCV装好了。4.2 摄像头启用与测试树莓派配OV5647摄像头模块安装时注意排线插法金属触点朝向网口方向老版本树莓派或者主板边缘新版树莓派插反了会损坏摄像头。在Raspberry Pi OS Bookworm里摄像头默认是开启的不需要像老教程那样去raspi-config里翻找。直接跑libcamera-hello能看到实时画面说明硬件没问题。然后再跑OpenCV代码用cv2.VideoCapture(0, cv2.CAP_V4L2)去读。如果OpenCV打不开摄像头先排除设备占用。比赛现场可能插了多个USB摄像头设备节点不一定是video0可以用ls /dev/video*看看有几个再尝试用1、2的编号打开。注意一个摄像头只能被一个进程占用调试时如果程序崩溃后有残留进程摄像头会被锁住。4.3 代码组织与比赛现场运行备赛时把检测和测距两个模块拆成函数def detect(frame): # 返回boxes, class_ids, confs def measure_distance(box, class_id, focal, real_h): # 返回距离值主循环里调用这两个函数检测框和距离一起画出来。现场比赛时建议在代码开头加一段“标定参数确认”的调试输出把f、H_real、置信度阈值打印出来一旦距离不对能立刻发现问题。运行程序用下面这条命令退出终端也不会中断nohup python3 main.py log.txt 21 平时调试还是老老实实在前台跑看实时输出。比赛前如果要求开机自启再写systemd服务不建议一开始就搞自动化不然日志都看不到。4.4 功耗与散热别让绿灯闪树莓派4B/5的电源最好用官方5V 3A或者至少5V 3A以上的合格电源。比赛现场如果用普通手机充电器电流不够的时候树莓派会开始绿灯闪甚至直接重启这是供电不足的典型症状。树莓派5额外的功耗更高建议加个散热风扇和UPS扩展板。我们队伍之前用的不间断电源模块能扛住电压波动比赛现场非常加分不然场地电源不稳检测程序跑着跑着自己关机心态直接崩。5. 常见问题与排查技巧实录5.1 摄像头绿灯闪程序打不开树莓派红灯常亮是供电正常绿灯闪则是系统在访问存储或者供电异常。如果绿灯闪并且摄像头无画面优先检查供电电源是否达标、摄像头排线是否插好。再用libcamera-hello单独测试摄像头如果测试工具能出画面而OpenCV打不开那就是软件层问题看下一节。5.2 ModuleNotFoundError: No module named cv2这类问题绝大多数是Python环境和pip不匹配。比如系统默认python3用的是/usr/bin/python而pip3装的包却装到了 /usr/local/lib/python3.x/site-packages有些只对特定用户生效。统一用下面的检查命令python3 -m pip --version python3 -c import cv2用python3 -m pip而不是裸的pip3可以确保pip属于当前python3解释器。如果还不解决问题直接which python3看看路径实在不行就新建一个虚拟环境干净省心。5.3 检测框一直错位检测框能出来但位置明显偏离目标这个几乎可以肯定是letterbox坐标还原没做对。检查方法非常简单把x, y, w_box, h_box直接画到输入给网络的416图上看看框是否对准了目标。如果416图上是对的、原图不对说明映射逻辑有bug如果416图上就不对那就是模型输入和cfg配置不一致或者YOLO输出解析格式没匹配上。5.4 推理速度慢帧率个位数树莓派跑深度学习模型性能瓶颈就在CPU。能做的优化按性价比排序输入尺寸从416降到320速度提升明显精度损失可接受。换更轻的模型比如MobileNet-SSD或者YOLOv3-tiny。在代码里设置net.setNumThreads(4)让OpenCV尽量用满CPU线程。关闭桌面环境通过SSH跑程序省下图形界面资源。如果真到了极限用C重写一遍推理循环性能能翻一倍。5.5 测距结果忽远忽近先确认检测框是否稳定框稳了距离一般就稳。检测框抖动就先调低NMS阈值再加中值滤波。还有一种是摄像头没固定好树莓派主板上摄像头排线松动或者摄像头支架被风扇震动影响这些硬件层面的问题比算法问题更难查建议第一时间检查物理固定。5.6 检测类别总是错乱类别错乱很多不是模型识别错误而是coco_names列表顺序和模型输出不匹配。COCO 80类的顺序是固定的直接从官方仓库下载对应的coco.names不要自己手动整理。如果自己训练了模型那类别顺序就是你训练时生成的classes.txt的顺序前后必须一致。下面把排查思路汇总成一张速查表方便现场快速定位。现象优先检查项处理办法摄像头打不开供电、排线、设备占用换电源、重插排线、用libcamera测试import cv2报错环境、pip路径用python3 -m pip安装检测框错位letterbox坐标还原在416图上画框验证推理慢模型大小、输入尺寸降分辨率、换轻量模型距离跳变检测框稳定性调NMS、加中值滤波类别错乱类别列表顺序用官方coco.names6. 一些比赛现场的经验补充代码能跑通只是第一步。备赛最后几天一定要在接近比赛条件的光照环境下把标定重新做一遍。实验室灯管和赛场的顶灯、侧灯差别很大强光下目标的反光会让检测置信度掉一个档测距自然跟着飘。另外摄像头镜头要经常擦。赛前我们用普通眼镜布一擦检测率直接恢复这种小细节平时根本想不到。还有目标物体的背景如果比赛现场允许布置场地尽量把目标放在颜色单纯、无明显反光的区域模型的稳定性会强很多。树莓派跑视觉任务的容错空间不大但正因为资源有限反而逼着你把代码写得干净、参数调得明确。备赛到比赛这套方案我们前前后后调了一周左右最后赛场上跑下来的检测成功率在九成以上测距最大误差不超过6%对于电赛C题来说完全够用。本文还有配套的精品资源点击获取
返回列表