ARTICLE DETAIL

资讯详情

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

YOLOv5实战:三轮车违规停放检测从训练到树莓派部署全流程

YOLOv5实战:三轮车违规停放检测从训练到树莓派部署全流程 简介这是一份面向目标检测与机器视觉学习者的三轮车识别数据集资源适用于基于yolov5的非机动车违规停放检测项目。压缩包内为tricycle7分类的1021张三轮车图片及对应的988个XML标注文件图片涵盖凤凰、飞鸽等品牌车型场景角度多样标注内容完整可直接用于模型训练与验证。数据整体包含自行车、电动车、三轮车共两万余张已标注图片本包为其中三轮车第七类便于按类别分步训练或与其他类目组合。资源共2009个文件以JPEG图像与VOC格式XML标注为主压缩包大小87.52MB目录结构清晰图片与标注一一对应方便做数据集划分与增强。目前已有257人学习下载适合需要特定类别非机动车数据用于yolov5训练、违规停放识别、目标检测算法验证等场景的研发人员。1. 非机动车违规停放检测为什么yolov5只解决了一半问题做违规停放检测的都知道这个需求不能光靠一个目标检测模型收工。yolov5能告诉你画面里有没有三轮车、车在哪个位置但“停在这算不算违规”是另一套逻辑。城市管理项目里网格员拍回一张三轮车乱停的照片后台先做机器视觉识别把车框出来再判断它是不是落在禁停区、停了多久。tricycle7_images_xmls 这类已标注数据集图片配 VOC 格式的 XML 标注正好用来训练自己的检测模型。这篇笔记按落地顺序拆环境配置、XML 转 YOLO 格式、训练调参、踩坑记录最后落到树莓派这种边缘设备上的实时判定。适合正在做智慧城管、园区安防的算法工程师或者接政企项目的个人开发者。2. 跑通yolov5环境与tricycle7_images_xmlsconda配置、标注检查和数据划分2.1 conda搭建yolov5训练环境Python版本与依赖一次配齐训练自己的数据集之前先把环境固定下来。我一般用 conda 单独建一个环境避免把系统 Python 搞乱也方便后面多个项目切换。yolov5 源码对 Python 版本不挑3.8 到 3.10 都能跑但依赖版本之间有点讲究个人经验是 Python 3.8 搭配 CUDA 11.x 最稳老项目多、踩坑案例也多搜问题容易搜到答案。# 创建独立环境, 指定 Python 3.8, -y 跳过确认 conda create -n yolov5 python3.8 -y # 激活环境, 后面所有操作都在这个环境里做 conda activate yolov5 # 在项目根目录安装依赖, requirements.txt 在 yolov5 源码仓库里 pip install -r requirements.txt逻辑说明yolov5 的训练链路依赖 torch、opencv、matplotlib、pyyaml 这些包requirements.txt 已经把版本组合写好了直接装比手动一个个装少踩很多坑。如果机器没有 NVIDIA 显卡需要把 torch 换成 CPU 版本再装否则会因 CUDA 版本不匹配报错。装完可以用python -c import torch; print(torch.__version__)验证一下。参数说明conda 环境名yolov5可以随意改但保持和项目名一致比较好认Python 3.8 不是必须的但如果你后面要跑旧版 ultralytics 仓库里的脚本3.8 的兼容性最省心。显存低于 6G 的机器训练时把 batch size 调小就行不影响环境搭建。克隆源码时注意别用 master 分支的最新代码新提交偶尔会引入问题。我习惯 clone 下来后用git tag看稳定版本选一个时间久、issue 少的 tag 切过去训练行为可预期。2.2 检查tricycle7_images_xmls标注质量先看XML再决定要不要清洗已标注数据集不等于干净数据集。tricycle7_images_xmls 从命名看是三轮车图片加 XML 标注的集合这类 XML 通常是 LabelImg 导出的 VOC 格式。拿到手先别急着转格式花十分钟把标注体检一遍能省后面几天的排查时间。import xml.etree.ElementTree as ET from pathlib import Path xml_dir Path(tricycle7_images_xmls/xmls) cls_counter {} # 遍历所有 XML 标注文件 for xml_file in xml_dir.glob(*.xml): tree ET.parse(xml_file) root tree.getroot() # 检查图片尺寸信息是否完整 size root.find(size) img_w int(size.findtext(width)) img_h int(size.findtext(height)) if img_w 0 or img_h 0: print(f尺寸异常: {xml_file.name}) # 遍历每个标注对象 for obj in root.iter(object): name obj.findtext(name).strip() difficult obj.findtext(difficult, 0) bndbox obj.find(bndbox) x1 float(bndbox.findtext(xmin)) y1 float(bndbox.findtext(ymin)) x2 float(bndbox.findtext(xmax)) y2 float(bndbox.findtext(ymax)) # 统计类别分布 cls_counter[name] cls_counter.get(name, 0) 1 # 坐标合法性检查: 左上角必须在右下角之前, 且不超图像边界 if not (0 x1 x2 img_w and 0 y1 y2 img_h): print(f坐标异常: {xml_file.name} - {name} [{x1}, {y1}, {x2}, {y2}]) print(类别分布:, cls_counter)逻辑说明这个脚本不修改任何文件只做体检。三个检查点分别对应三类问题尺寸异常说明图片可能被压缩过但标注没跟着更新坐标异常说明标注时画框画出了边界类别分布直接决定你要不要重新组织数据。跑完一遍心里对这批数据的质量就有数了。参数说明findtext(difficult, 0)的第二个参数是默认值当 XML 里没有 difficult 字段时返回 0避免属性缺失报错。坐标检查用的是开区间判断x1 x2严格成立因为 x1 等于 x2 的标注是无效框训练时会让模型学到错误的位置信息。如果统计出来类别名不统一比如既有tricycle又有Tricycle或者有多余的空格趁现在一次性清洗掉。这种问题越早处理代价越小等数据划分完再改就要重新生成一遍标签。2.3 训练/验证/测试集划分8比1比1固定随机种子数据集划分必须在格式转换之前做。原因很简单转换后是 txt 标签跟图片的对应关系靠文件名维系如果先转换再划分一旦文件复制出错图片和标签就错位了。按图片维度划分保证同一个场景的视频帧不会同时出现在训练集和验证集里。import random import shutil from pathlib import Path image_dir Path(tricycle7_images_xmls/images) train_dir Path(images/train) val_dir Path(images/val) test_dir Path(images/test) for d in [train_dir, val_dir, test_dir]: d.mkdir(parentsTrue, exist_okTrue) images sorted(image_dir.glob(*.jpg)) random.seed(42) # 固定种子, 保证每次划分结果一致 random.shuffle(images) # 打乱顺序, 避免同类图片扎堆 n len(images) train_end int(n * 0.8) val_end int(n * 0.9) # 按 8:1:1 切分训练/验证/测试 for i, img in enumerate(images): if i train_end: target train_dir elif i val_end: target val_dir else: target test_dir shutil.copy2(img, target) # copy2 保留文件元数据逻辑说明这里只复制图片XML 标注先不动。下一步做格式转换时按图片目录反查 XML保证标签和图片一一对应。随机种子固定为 42 是习惯这样你调整数据后重新划分训练集和验证集的边界不会变化模型对比才有意义。参数说明8:1:1 是检测任务最常见的划分比例。如果总图片数只有几百张可以改 9:1:0把测试集省下来因为验证集每天训练完都在用测试集只在最后评估一次。shutil.copy2比copy多复制元数据对图片无所谓但对后面排查文件来源有帮助。注意划分完成后数一下三个目录的图片数量加总必须等于总数。目录里混入非图片文件是常见问题glob 只匹配.jpg如果数据集里混有.png、.bmp需要先统一格式再划分。3. 把VOC XML转成YOLO标签转换脚本与四个边界坑3.1 转换脚本像素坐标bndbox到归一化cx,cy,w,hyolov5 训练时不读 XML它要求每个标注对象一行类别ID 归一化中心点坐标 归一化宽高。所以转换脚本是整个流程里最不能出错的一环坐标错一个像素训练出来的模型框就会偏。import xml.etree.ElementTree as ET from pathlib import Path # names 的顺序必须和后续 tricycle.yaml 里的 names 完全一致 names [tricycle] name2id {name: i for i, name in enumerate(names)} xml_dir Path(tricycle7_images_xmls/xmls) label_dir Path(labels) label_dir.mkdir(exist_okTrue) def voc_to_yolo(size, box): VOC像素坐标转YOLO归一化坐标, 返回 cx, cy, w, h dw, dh 1.0 / size[0], 1.0 / size[1] x1, y1, x2, y2 box # 归一化到 0~1 cx (x1 x2) / 2.0 * dw cy (y1 y2) / 2.0 * dh w abs(x2 - x1) * dw h abs(y2 - y1) * dh # 标注工具偶尔会画出图像边界, 越界值直接截断 cx min(max(cx, 0.0), 1.0) cy min(max(cy, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) return cx, cy, w, h for xml_file in sorted(xml_dir.glob(*.xml)): root ET.parse(xml_file).getroot() size_el root.find(size) img_w int(size_el.findtext(width)) img_h int(size_el.findtext(height)) lines [] for obj in root.iter(object): name obj.findtext(name).strip() cls_id name2id.get(name) # 类别不在映射表里就跳过并打印, 不要静默放过 if cls_id is None: print(f未知类别: {name} in {xml_file.name}) continue # difficult1 的样本直接跳过, 这些通常是遮挡严重的难例 if obj.findtext(difficult, 0) 1: continue bndbox obj.find(bndbox) box ( float(bndbox.findtext(xmin)), float(bndbox.findtext(ymin)), float(bndbox.findtext(xmax)), float(bndbox.findtext(ymax)), ) cx, cy, w, h voc_to_yolo((img_w, img_h), box) lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) if lines: out label_dir / f{xml_file.stem}.txt out.write_text(\n.join(lines) \n)逻辑说明脚本遍历所有 XML把每个object的 bndbox 像素坐标转成归一化的中心点坐标和宽高。核心是两个公式中心点取左上角和右下角的平均值再除以图片宽高宽高取差值再除以图片宽高。name2id映射表保证类别名到数字 ID 的转换是显式的哪里错了打印哪里。参数说明cx:.6f保留六位小数足够yolov5 训练时读取的精度就是 float32difficult1的样本跳过是保守做法这些样本通常遮挡严重、边界框不准确混进训练集反而让模型困惑。如果你样本总量太少可以把 difficult 样本单独筛出来做验证集不要直接删掉。输出文件用 XML 的文件名stem保证和图片同名后面训练才能对上。3.2 四个边界坑越界坐标、大小写、空XML、文件对不上第一个坑是坐标越界。LabelImg 画框时鼠标稍微拖出图片边界xmax 就会大于图片宽度。转换脚本里必须做 clamp否则训练时模型输出的框也会学着超出边界。判断标注是否越界最直接的方法是看 box 的 x2 减 x1 是不是负数负数说明画反了这种框不能 clamp 了事要回到原图重新标注。第二个坑是类别名大小写不一致。VOC 里写Tricycleyaml 里写tricyclename2id.get(name)返回 None脚本打印一行提示然后跳过。最怕的是你不打印直接跳过训练时发现某张图没有标签文件排查半天才意识到是类别名大小写问题。所以转换脚本里的打印语句不是可有可无的装饰它是你数据清洗的最后一道防线。第三个坑是空 XML。有些图片标注到一半没保存完整XML 里没有任何object。转换后生成一个空的 txt 文件yolov5 训练时不会报错但那张图相当于没参与训练白白浪费一个样本。体检脚本里加一个计数XML 有、但 object 为空的文件列出来要么补标要么剔除。第四个坑是文件名对不上。图片叫IMG_0421.jpgXML 叫IMG_0421.xml是正常的但有人标注完以后批量重命名了图片XML 没跟着改转换后标签和图片错位。这个必须靠代码检查遍历 images 目录对每张图查对应 XML 是否存在反过来也查一遍找出对方缺失的文件清单。from pathlib import Path image_files {p.stem: p for p in Path(images).glob(*.jpg)} xml_files {p.stem: p for p in Path(xmls).glob(*.xml)} # 图片有但标注缺失 orphan_images set(image_files) - set(xml_files) # 标注有但图片缺失 orphan_xmls set(xml_files) - set(image_files) print(缺少标注的图片:, orphan_images) print(缺少图片的标注:, orphan_xmls)逻辑说明两个集合相减把失配的文件一次性找出来。这一步要在转换之前跑因为转换脚本是按 XML 生成的某个 XML 对应的图片不存在它的标签文件就会成为一个孤儿标签训练时起不到任何作用。3.3 可视化验证把txt标签反算回像素坐标画框检查转换完不能直接开训练先抽样几步看一眼效果是值得的。把归一化的 txt 标签反算成像素坐标画到图上一眼就能看出转换有没有问题。import cv2 img_path images/train/IMG_0421.jpg label_path labels/IMG_0421.txt img cv2.imread(img_path) h, w img.shape[:2] with open(label_path) as f: for line in f: parts line.split() cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) # 归一化坐标反算回像素坐标 x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) # 越界画框会画到图像外面, 用 clip 限制显示范围 x1, y1 max(x1, 0), max(y1, 0) x2, y2 min(x2, w), min(y2, h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, fcls{cls_id}, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(check.jpg, img) print(已生成 check.jpg, 请打开检查框是否贴合车身)逻辑说明反算公式是转换公式的逆运算中心点加减宽高的一半得到左上角和右下角。检查重点是两个框是不是紧贴三轮车车身边缘以及类别 ID 是不是正确。抽一两张正面、一张侧面、一张远处小目标的图看基本就能确认转换质量。参数说明这里为了检查方便直接把框画到原图上。实际数据集里三轮车会出现在不同距离远处的框偏小是正常的但如果所有框都比车身大一圈大概率是 XML 里存的本来就是外接矩形需要回到标注源头处理不要靠代码硬调。4. 用yolov5训练自己的三轮车检测模型超参数设置与训练不收敛排查4.1 tricycle.yaml与训练命令先小图跑通再上大图数据转换完接下来就是 yolov5 训练自己的数据集的常规流程写一个数据配置文件然后跑 train.py。如果你只想把 yolov5 基础笔记里的命令抄一遍跑通流程这段可以跳过但如果要训练一个能落地的三轮车检测模型yaml 里的细节值得抠一下。# tricycle.yaml # 训练数据根目录, 相对路径以 train.py 所在位置为基准 path: ./ train: images/train val: images/val test: images/test # 类别数量与类别名, 顺序必须和转换脚本的 names 一致 nc: 1 names: 0: tricycle# 先用 320 输入尺寸跑通流程, 确认数据没配错, 再上 640 python train.py \ --img 320 \ --batch 16 \ --epochs 50 \ --data tricycle.yaml \ --weights yolov5s.pt \ --hyp hyp.scratch-low.yaml \ --cache逻辑说明--weights yolov5s.pt用的是 COCO 预训练权重迁移学习能大幅缩短收敛时间。单类检测任务用 s 模型足够n 模型太小容易欠拟合m 模型对边缘部署不友好。第一次跑用 320 输入不是为了省时间是为了快速验证数据链路如果数据配置有错小图几十个 epoch 就能看到 loss 不降、mAP 为 0比跑 640 省一半时间。参数说明--batch 16在 8G 显存下比较安全显存不足时先降 batch 到 8再不行才降--img到 320。--cache把图片缓存到内存加速训练前提是机器内存不能太小。hyp.scratch-low.yaml是 yolov5 自带的低增强超参组合适合数据量不大、不想让增强把标注搞歪的场景。4.2 真正影响检测效果的超参数lr0、mosaic与类别权重超参数不是玄学但很多人直接改数值而不理解为什么改。对于三轮车这种单类、大目标、样本量几百到几千的任务真正需要手动调的就三个地方。超参数默认值建议值调整理由lr0 初始学习率0.010.005数据量小学习率太大容易震荡loss 曲线像锯齿mosaic 马赛克增强1.00.5~1.0三轮车在画面里通常占比较大mosaic 把目标缩小反而失真cls_pw 类别权重1.00.8~1.0单类检测不需要刻意加大正样本权重保持默认# hyp.scratch-low.yaml 里改这三个位置 lr0: 0.005 # 初始学习率, 数据少就调小一半 mosaic: 0.8 # 马赛克增强概率, 0.8 保留部分真实场景 cls_pw: 0.9 # 类别权重, 单类任务保持接近 1.0逻辑说明lr0是最值得动的一个参数。默认 0.01 在 COCO 这种百万级数据上没问题换成几百张三轮车图片梯度更新方向不稳定loss 容易下不去。降到 0.005 之后训练曲线会明显平滑。mosaic是 yolov5 的招牌增强把四张图拼成一张但对于目标本身占比就大的场景拼图后目标被缩小小目标训练不足。三轮车检测的典型场景是近景mosaic 开太高反而有害。参数说明cls_pw只对多类不平衡有意义。你只有一个tricycle类把它调大反而会让模型对背景的抑制变弱所以保持接近 1.0 就好。真正影响单类检测的是fliplr默认 0.5三轮车左右对称水平翻转增强是安全的flipud保持默认 0车辆上下翻转在语义上说不通不要开。4.3 训练不收敛与过拟合排查loss曲线和mAP怎么读训练开始后yolov5 会把 loss 曲线画在runs/train/exp目录下。常见翻车有两种loss 一直降但 val 的 mAP 不涨这是过拟合loss 直接变 NaN这是数据问题。过拟合的判断标准不是 loss 本身是 train 和 val 的差距。训练集 loss 降到 0.02验证集 mAP 只有 0.5说明模型把训练集背下来了。三轮车样本量少增强又不强很容易走到这一步。解决办法按优先级排补数据 增强增强 换小模型。先看训练集里有没有大量重复背景的图片比如同一个路口拍了几十张去掉重复再训一轮。loss 变成 NaN 的排查顺序先看是不是学习率太大把 lr0 调到 0.001 再试如果还 NaN回去跑第 2 章的标注体检脚本确认没有异常坐标最后检查图片本身有没有损坏用 OpenCV 读一遍返回 None 的就是坏图删掉重训。# 训练结束后, 结果都在 runs/train/exp 目录下 # 重点关注两个指标: # mAP0.5: 0.5 IoU 下的平均精度, 单类检测 0.85 以上算可用 # mAP0.5:0.95: 严格指标, 0.5 到 0.95 的积分平均, 能到 0.5 说明框很准逻辑说明mAP0.5 对单类检测来说0.85 以上才能上生产。低于 0.7 先别调参此数据质量的问题大概率比模型问题大。mAP0.5:0.95 偏低但 mAP0.5 很高说明框不够精确——三轮车检测对框的精度要求没那么苛刻后续违规停放判定用的是中心点所以 mAP0.5 优先看。注意训练过程中不要频繁中断去改超参数。一个训练任务跑完再改半途改参会让对比失去意义。5. 避坑非机动车检测从训练到部署最常踩的5个翻车现场5.1 lossNaN先查标签坐标别动网络结构现象训练跑了不到 10 个 epochloss 突然变成 NaN后面直接雪崩训练进程没法继续。原因90% 的情况出在标签数据上。XML 转 YOLO 格式时某个框的坐标算出了 0 或者负数比如 xmax 小于 xmin归一化后 w 为 0模型回传梯度时除零导致数值爆炸。剩下 10% 是图片损坏某张图读出来是全黑的或者文件不完整。解决先跑第 2 章的体检脚本检查坐标再跑第 3 章的转换脚本把越界坐标 clamp 掉。如果你已经把有问题的标签删了确认 labels 目录下没有空 txt 文件对应的图片还在训练列表里。检查顺序是数据先行网络结构一般不用动。5.2 把自行车也当三轮车类别错位看confusion matrix现象训练完的模型在测试图上把自行车、电动车全都框成 tricycle置信度还很高。原因最常见的是数据集划分时的污染——标注 XML 里把bicycle和tricycle混在一起或者转换脚本的name2id映射表漏了某个类别名模型没见过真正的 tricycle 负样本。另一个隐蔽原因是 data.yaml 的 names 顺序和转换脚本不一致类别 ID 错位。解决训练结束后去看runs/train/exp下的 confusion matrix 图。如果你的类别和背景混在一起说明标签有误如果 categories 之间互混说明类间特征太像。确认 names 顺序重点看第 3 章转换脚本里的names [tricycle]和 yaml 里的names是否完全一致。类别错位这个问题代码不会报错只能靠混淆矩阵暴露。5.3 现场漏检侧后方车辆训练集角度太单一现象训练集和验证集 mAP 都到 0.9但拿到现场素材一测三轮车侧后方角度大量漏检置信度普遍低于 0.3。原因标注数据大多来自同一个方向的拍摄比如都是正面或正侧面。模型学到的其实是“这个角度的三轮车长这样”换一个角度特征分布偏移直接失效。这是机器视觉识别项目里最常见的“数据集分布偏差”。解决调参数没用唯一的解法是补数据。去现场拍一段三轮车绕行的视频抽帧后按第 2 章的流程补进训练集。如果暂时拍不了用 yolov5 自带的增强加大旋转和透视强度但这是临时补救真实场景数据永远比增强生成的数据可靠。我一般会先在 hyp 文件里把degrees从 0 调到 30perspective从 0 调到 0.0005运行一轮看是否缓解。5.4 树荫下和夜间的黑色三轮车漏检对比度不足现象白天光线好的时候检测一切正常一到树荫、傍晚、夜间黑色的三轮车直接消失框不出来。原因标注数据里大多是白天光照充足的图片模型没学过低对比度场景下三轮车的特征。黑色车身和深色背景融合后卷积特征响应变弱置信度跌破阈值。解决数据集里补入低光照样本这是最直接的办法。如果样本短期内补不到训练时在 hyp 里开hsv_h、hsv_s增强把饱和度扰动调大一点同时把推理端的置信度阈值从 0.25 降到 0.15配合后面要讲的停留计时逻辑来滤掉误检。这个组合拳能撑到收集到足够夜间数据为止但不要指望它彻底解决。5.5 违停判定反复横跳检测框做平滑停留做计时现象三轮车停在禁停区边缘检测框轻微抖动判定结果一会违停一会正常截图工单都没法出。原因直接用单帧检测框的中心点判断是否在禁停区内而检测框本身有随机抖动中心点在地理上可能来回跨越 ROI 边界。另一个问题是把“短暂停留”和“缓慢行驶”混为一谈——电动车在禁停区前减速等红灯也被误判成违停。解决两个手段配合。第一对检测框中心点做指数移动平均EMA平滑消掉单帧抖动第二加停留计时逻辑连续 N 帧对应实际时间 5~10 秒检测到目标中心点在禁停区内才触发违停。这套逻辑的具体实现在第 6 章给出这里想强调的是判定“违规”必须有时间维度单帧判定是血泪经验换来的教训。6. 树莓派5上部署自训练的yolov5模型onnx导出、违规停车判定与回放验证6.1 导出onnx输入尺寸、动态轴与半精度的取舍训练完的best.pt不能直接给树莓派用PyTorch 在 ARM CPU 上推理太慢。常见做法是先用 official 仓库自带的 export 脚本转成 onnx再在边缘设备上用 onnxruntime 或 ncnn 推理。# 输入尺寸降到 416, 和训练尺寸尽量接近, 不要直接上 640 python export.py \ --weights runs/train/exp/weights/best.pt \ --img 416 \ --include onnx \ --simplify逻辑说明--img 416是部署时的常用折中原图 640 输入树莓派5 CPU 跑不动降到 416 能保证准确率不损失太多。--simplify用 onnx-simplifier 去掉冗余算子模型体积和推理时间都有改善。这里不推荐用--half转 FP16树莓派 CPU 对 FP16 支持不好反而比 FP32 慢。参数说明导出时如果提示动态轴问题检查 onnx 输出是不是把 batch 维度固定成了 1。固定 batch 1 对部署是好事省掉动态 shape 的额外开销。导出的 onnx 文件可以用 onnxruntime 在电脑上先跑一遍确认输出张量和 PyTorch 原模型一致再拷到树莓派上。6.2 推理与违规停车判定letterbox还原、ROI射线法与停留计时树莓派5 上部署自己训练的 yolov5 模型推理脚本的核心就三件事letterbox 预处理、ONNX 推理加 NMS 后处理、违停判定逻辑。yolov5 后处理看着简单部署到边缘设备后才知道 NMS 和坐标还原才是影响帧率的关键。import cv2 import numpy as np import onnxruntime as ort session ort.InferenceSession(tricycle.onnx, providers[CPUExecutionProvider]) def letterbox(img, new_size416): 等比缩放 灰色填充, 记录缩放比和填充偏移用于反算坐标 h, w img.shape[:2] scale min(new_size / h, new_size / w) nh, nw int(round(h * scale)), int(round(w * scale)) resized cv2.resize(img, (nw, nh)) canvas np.full((new_size, new_size, 3), 114, dtypenp.uint8) top (new_size - nh) // 2 left (new_size - nw) // 2 canvas[top:top nh, left:left nw] resized return canvas, scale, top, left def point_in_polygon(x, y, roi): 射线法判断坐标点是否在多边形禁停区内 inside False n len(roi) for i in range(n): x1, y1 roi[i] x2, y2 roi[(i 1) % n] if (y1 y) ! (y2 y) and x (x2 - x1) * (y - y1) / (y2 - y1) x1: inside not inside return inside # 禁停区多边形, 像素坐标, 需要根据实际监控画面标定 roi [(100, 200), (500, 200), (500, 600), (100, 600)] track {} # 目标ID - 连续帧计数 cap cv2.VideoCapture(test.mp4) fps cap.get(cv2.CAP_PROP_FPS) while True: ret, frame cap.read() if not ret: break # 预处理: letterbox 后归一化, 转 NCHW input_img, scale, top, left letterbox(frame, 416) blob input_img[:, :, ::-1].transpose(2, 0, 1)[None].astype(np.float32) / 255.0 # ONNX 推理, 输出形状为 (1, 25200, 5类别数) outputs session.run(None, {session.get_inputs()[0].name: blob})[0][0] boxes, scores [], [] for det in outputs: score det[4] if score 0.25: # 置信度阈值, 树荫场景可降到 0.15 continue # 按 letterbox 的逆变换还原到原图坐标 cx, cy, w, h det[:4] x1 int((cx - w / 2 - left) / scale) y1 int((cy - h / 2 - top) / scale) x2 int((cx w / 2 - left) / scale) y2 int((cy h / 2 - top) / scale) boxes.append([x1, y1, x2, y2, score]) # 省略 NMS 实现, 用最简单的按置信度排序后抑制重叠框即可 for box in boxes: x1, y1, x2, y2, score box center_x, center_y (x1 x2) / 2, (y1 y2) / 2 if point_in_polygon(center_x, center_y, roi): track_id f{(center_x // 20) * 20}-{(center_y // 20) * 20} track[track_id] track.get(track_id, 0) 1 # 连续 fps*10 帧(约10秒)都在禁停区, 判定为违规停放 if track[track_id] fps * 10: cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(frame, ILLEGAL PARKING, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) else: cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imshow(test, frame) if cv2.waitKey(1) 0xFF ord(q): break逻辑说明这段代码把整个链路串起来了。letterbox 保持宽高比避免拉伸变形导致检测精度下降推理结果反算回原图坐标才能在原图上画框和做 ROI 判断。point_in_polygon用射线法判断中心点是否落在禁停区内这个方法在凸多边形和凹多边形上都能用比简单的矩形判断通用。停留计时用track字典按中心点网格做简易 ID 匹配没有做真正的多目标跟踪但对“一个路口盯一辆三轮车”的场景够用。参数说明fps * 10表示连续 10 秒都在禁停区内才触发这个值按业务调整。网格 20 像素的宽容度是给中心点抖动留的余量配合 EMA 平滑基本不会误判。置信度阈值 0.25 是通用值如果你的模型在特定场景置信度普遍偏低降到 0.15 配合计时逻辑效果反而更好。6.3 上生产前的验证技巧拿现场录像回放跑一遍模型在测试集上 mAP 再高也不代表现场能用。我的习惯是训练完先不着急接摄像头找一段现场真实录像回放跑一遍。录像比照片多一个时间维度能验证停留计时逻辑也能暴露照片测试发现不了的问题——比如树荫下的推车、临时停靠的货车、画面里飘过的塑料袋被误检成目标。回放验证时重点记录两类现象误报把不是三轮车的东西框成三轮车和漏报现场有三轮车但没框出来。误报多就看置信度阈值和 ROI 范围漏报多就回到训练数据补样本。调完一轮再跑一遍直到连续十分钟录像没有明显误报才算过。我现在接到类似的违规停放检测项目第一件事一定是去现场拍一段录像而不是先把模型跑起来。这个习惯帮我省了太多返工的时间——数据集分布、光线条件、摄像头角度这些在办公室里想是想不出来的只有录像回放能暴露问题。训练模型只是第一步把模型放进真实场景里还站得住这个项目才算了结了一半。希望帮到你。本文还有配套的精品资源点击获取
返回列表