
简介这是一份面向目标检测与机器视觉违规停放场景的三轮车已标注数据集专供使用YOLOv5训练非机动车检测模型的开发者与研究人员使用。资源聚焦三轮车第七类tricycle7从完整非机动车数据集中单独拆出共有1021张JPG图片与988个配套XML标注文件文件总数2009个压缩包整体大小87.52MB。XML标注与图片一一对应可直接接入YOLOv5格式训练流程省去自行转标与整理的耗时。目前已有257人学习下载。数据集取材于真实道路与非机动车停放环境覆盖不同角度、光照和遮挡条件的三轮车样本并包含常见品牌车型可作为非机动车违规停放识别任务的补充训练集或验证样本帮助训练者提升对三轮车目标的检测精度与泛化能力。1. 非机动车违规停放识别为什么选了 yolov5 而不是更“新”的模型把“yolov5非机动车违规停放已标注数据集机器视觉识别三轮车tricycle7_images_xmls”拆开看这其实是一条很典型的落地需求用机器视觉识别街道上的非机动车违停其中三轮车是重点对象手头已有一套标注好的 XML 数据集目录名是 tricycle7_images_xmls。很多第一次接触这个方向的人会纠结“yolov5 是不是过时了”实际做下来会发现在违停检测这种目标尺度固定、场景相对单一、需要快速部署到边缘设备的任务上yolov5 的成熟度和可控性反而是优势。先说结论违停检测的本质不是“认出是什么车”而是“判断车停的位置对不对”。yolov5 负责前半部分——把画面里的非机动车尤其是三轮车框出来后半部分靠业务规则比如检测框是否压线、是否进入禁停区域、停留时间是否超过阈值。所以整套方案里yolov5 只是一个前置感知模块真正决定项目能不能用是数据标注质量和后处理逻辑。这篇文章会从数据集整理、环境配置、训练调参、部署推理一路讲到常见坑照着做能直接跑通一版可用的违停识别流程。先给读者一个预期整条链路大概是这样——把 tricycle7_images_xmls 里的 XML 标注转成 yolov5 需要的 txt 格式配置好数据 YAML用预训练权重微调导出模型后在边缘设备比如树莓派或 Jetson上做实时推理再用“检测框 禁停区”的 IoU 判断是否违停。下面每一章都会给出可复现的命令和参数。2. 从 XML 标注到 yolov5 训练集先搞定数据再谈模型2.1 理解 tricycle7_images_xmls 的数据结构拿到一个名为 tricycle7_images_xmls 的目录通常意味着里面是成对的图片文件和 Pascal VOC 格式的 XML 标注文件。VOC 格式是目标检测领域最通用的标注格式之一XML 里记录了对象类别、边界框坐标xmin, ymin, xmax, ymax以及图片尺寸。这类数据的典型目录结构如下tricycle7_images_xmls/ ├── images/ │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... └── xmls/ ├── 000001.xml ├── 000002.xml └── ...也有可能 images 和 xmls 在同一目录下文件名一一对应。动手之前先做一件事用脚本统计类别分布和标注框尺寸分布。这一步能提前发现标注错误比如类别名字不一致、XML 里有空框或异常坐标。下面这段脚本会遍历所有 XML输出类别名称、每类数量以及边界框宽度和高度的最小值、最大值、平均值。import os import xml.etree.ElementTree as ET import numpy as np xml_dir tricycle7_images_xmls/xmls class_count {} bbox_sizes [] for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() for obj in root.iter(object): cls obj.find(name).text class_count[cls] class_count.get(cls, 0) 1 bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) bbox_sizes.append([xmax - xmin, ymax - ymin]) bbox_sizes np.array(bbox_sizes) print(类别统计:, class_count) print(框数量:, len(bbox_sizes)) print(宽度均值/最小/最大:, bbox_sizes[:, 0].mean(), bbox_sizes[:, 0].min(), bbox_sizes[:, 0].max()) print(高度均值/最小/最大:, bbox_sizes[:, 1].mean(), bbox_sizes[:, 1].min(), bbox_sizes[:, 1].max())这段脚本的逻辑很简单遍历每个 XML 文件解析出对象类别和边界框最后汇总统计。为什么要做这一步因为后续 yolov5 训练时锚框anchor尺寸会根据标注框分布自动调整如果数据里混入坐标异常比如 xmin xmax或超大框会导致训练早期 loss 异常波动。笔者在真实项目里遇到过一张 XML 里标注框坐标写了 0、0、0、0训练出来模型在某个角度总有误检排查半天才发现是这个脏数据在作祟。2.2 把 VOC XML 转换成 yolov5 的 txt 标注yolov5 训练时不读 XML它要求每个图片对应一个同名 txt 文件内容每一行是“class_id x_center y_center width height”后四个值都是相对于图片宽高的比例值范围在 0 到 1 之间。转换脚本是整条链路里最容易出错的环节常见错误包括坐标没归一化、类别 ID 跟类别名映射错误、训练集和验证集划分方式不对。下面给出一个稳健的转换脚本假设图片目录是 imagesXML 目录是 xmls输出目录为 labelsimport os import xml.etree.ElementTree as ET from pathlib import Path IMG_DIR tricycle7_images_xmls/images XML_DIR tricycle7_images_xmls/xmls LABEL_DIR yolodata/labels CLASS_NAMES [tricycle, bicycle, electric_bicycle] # 按数据集实际类别填写 os.makedirs(LABEL_DIR, exist_okTrue) def convert_bbox(size, box): dw 1.0 / size[0] dh 1.0 / size[1] x (box[0] box[1]) / 2.0 y (box[2] box[3]) / 2.0 w box[1] - box[0] h box[3] - box[2] return x * dw, y * dh, w * dw, h * dh for xml_file in os.listdir(XML_DIR): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(XML_DIR, xml_file)) root tree.getroot() size root.find(size) img_width int(size.find(width).text) img_height int(size.find(height).text) lines [] for obj in root.iter(object): cls obj.find(name).text if cls not in CLASS_NAMES: continue cls_id CLASS_NAMES.index(cls) bndbox obj.find(bndbox) box [ float(bndbox.find(xmin).text), float(bndbox.find(xmax).text), float(bndbox.find(ymin).text), float(bndbox.find(ymax).text), ] # 跳过异常框 if box[1] box[0] or box[3] box[2]: print(f跳过异常框: {xml_file} {cls} {box}) continue bbox_norm convert_bbox((img_width, img_height), box) lines.append(f{cls_id} {bbox_norm[0]:.6f} {bbox_norm[1]:.6f} {bbox_norm[2]:.6f} {bbox_norm[3]:.6f}) label_txt os.path.join(LABEL_DIR, xml_file.replace(.xml, .txt)) with open(label_txt, w) as f: f.write(\n.join(lines)) print(转换完成输出目录:, LABEL_DIR)转换之后需要把图片和 txt 放到同一个名为 images 的目录下或者按 yolov5 的约定组织成datasets/images/train、datasets/images/val、datasets/labels/train、datasets/labels/val四层结构。很多新手直接把转换后的 txt 留在原目录忘记调整目录结构导致训练时报image not found或label not found。关于类别 ID 有一点要特别注意CLASS_NAMES 的顺序必须和最终模型输出层顺序一致tricycle排在 0 位训练时的 YAML 里 classes 列表就必须按同样顺序写。如果中途增删类别要在训练前重新生成所有 txt否则错位后模型学到的类别跟实际标注对不上推理时输出乱套。2.3 划分训练集与验证集不要随机乱切划分训练集和验证集看起来只是把文件分到两个文件夹但有一个常见错误按文件名随机 shuffle 后直接切分导致同一监控画面不同时段的图片同时出现在训练集和验证集中验证集指标虚高。实际推流识别场景下同一条街道的图像背景高度相似模型很容易“背下”背景纹理而不是学会检测车本身。常见的可靠做法是按时间或场景分组。如果数据是按时序采集的可以按文件名序号切分比如前 80% 的图片做训练后 20% 做验证不同摄像头采集的数据要保证同一摄像头的画面只出现在一个集合里。下面用脚本实现按文件名的数值序号排序后切分python - EOF import os import shutil from pathlib import Path src_images yolodata/images src_labels yolodata/labels train_img yolodata/images/train val_img yolodata/images/val train_lab yolodata/labels/train val_lab yolodata/labels/val for d in [train_img, val_img, train_lab, val_lab]: os.makedirs(d, exist_okTrue) files sorted([f for f in os.listdir(src_images) if f.endswith(.jpg)]) n len(files) split int(n * 0.8) for f in files[:split]: shutil.move(os.path.join(src_images, f), train_img) shutil.move(os.path.join(src_labels, f.replace(.jpg, .txt)), train_lab) for f in files[split:]: shutil.move(os.path.join(src_images, f), val_img) shutil.move(os.path.join(src_labels, f.replace(.jpg, .txt)), val_lab) print(f训练集图片 {split} 张验证集图片 {n - split} 张) EOF这段脚本直接按文件名排序切分适合文件名能反映采集时间顺序的场景。如果文件名没有规律就要修改排序逻辑比如用图片的 EXIF 时间属性排序。验证集比例通常 10% 到 20%数据量小可以留 20%数据量大留 10%。另外不要动原始 tricycle7_images_xmls 目录而是把转换后的文件复制到新的 yolodata 目录里操作避免误删原始标注。3. 环境配置与数据 YAML跑通一次训练的最小命令3.1 conda 环境与 yolov5 源码依赖yolov5 环境配置常见的问题出在 PyTorch 版本和 CUDA 版本不匹配。最省事的做法是用 conda 建独立环境然后按官方 requirements.txt 安装依赖。以下是在 Linux 或 Windows 上都适用的步骤conda create -n yolo python3.9 -y conda activate yolo # 先安装 PyTorch注意去 pytorch.org 选择匹配本机 CUDA 版本的命令 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 git clone https://github.com/ultralytics/yolov5 # 如果已有源码包则直接进入目录 cd yolov5 pip install -r requirements.txt安装完成后检查一下关键库版本用python -c import torch; print(torch.__version__, torch.cuda.is_available())。如果输出True说明 GPU 可用如果是False需要排查显卡驱动和 CUDA 版本。CPU 机器也能训练但速度慢yolov5s 在 CPU 上训练一轮 COCO 要几个小时实际项目中非机动车违停数据集几百张图片还好但能上 GPU 就上 GPU。3.2 编写 data.yaml 与模型配置文件yolov5 训练前要准备两个 YAML一个是数据配置一个是模型配置。数据配置内容如下# yolodata/data.yaml train: yolodata/images/train val: yolodata/images/val nc: 3 names: [tricycle, bicycle, electric_bicycle]train和val写的是图片目录路径也可以用文件列表路径。路径建议写完整路径或相对 yolov5 目录的路径很多报错都来自路径写错。nc是类别数必须等于 names 列表长度。这里 echo 出一个细节如果标注类别只有三轮车那 names 就只写[tricycle]nc 为 1但实际违停场景往往还包含自行车、电动车建议把出现的类别都标上否则模型会把三轮车以外的目标全部归为背景。模型配置可以用官方现成的 yolov5s.yaml也可以自定义修改。yolov5s.yaml 里的depth_multiple和width_multiple控制网络规模和宽度。不做大改动的话直接用官方配置即可。下面是自定义缩减版模型的配置示例# yolodata/yolov5s_custom.yaml nc: 3 depth_multiple: 0.33 width_multiple: 0.50 anchors: - [10,13, 16,30, 33,23] - [30,61, 62,45, 59,119] - [116,90, 156,198, 373,326] backbone: - [-1, 1, Conv, [64, 6, 2, 2]] - [-1, 1, Conv, [128, 3, 2]] # 后续层与官方一致 head: - [-1, 1, Conv, [256, 1, 1]] # 后续层与官方一致在实际操作中直接复制官方models/yolov5s.yaml只把nc改成自己的类别数就行。anchors 可以先用官方默认值训练时 yolov5 会自动做 k-means 自适应锚框如果希望固定锚框可以加上anchors字段。3.3 训练命令和关键参数含义训练入口是train.py一个最简训练命令如下python train.py \ --data yolodata/data.yaml \ --cfg models/yolov5s.yaml \ --weights yolov5s.pt \ --batch-size 16 \ --epochs 100 \ --img 640 \ --device 0--weights yolov5s.pt会从官方仓库自动下载预训练权重如果网络受限可以手动下载后放在 yolov5 根目录。--img 640指定训练输入分辨率这个参数对非机动车识别很关键因为三轮车相对小而窄太小的输入会丢失细节。--batch-size受显存限制显存 8G 可以跑 164G 建议降到 8 或更小。训练过程会输出每个 epoch 的 loss、mAP 等指标并在runs/train/exp目录下保存权重。关于--epochs实际项目里几百张图的小数据集100 轮足够。更大的自由度可以用--patience做早停比如--patience 2020 轮没有改善就自动停止。还可以用--cache ram把图片缓存到内存以加快读取图片多、内存够的情况下建议打开训练速度提升明显。4. 训练调参与效果验证让三轮车检得准、记得牢4.1 从模型大小出发选择主干yolov5 提供 s、m、l、x 四种规模对应不同的精度和速度权衡。非机动车违停识别属于“目标小、数量多、背景复杂”的任务通常 yolov5s 就够用因为违停检测的重点是“有没有车”而不是“车是什么牌子”。如果场景里三轮车很小比如远距离监控画面可以用 yolov5m 或 yolov5l 提升小目标召回率。规模选择建议树莓派 5 之类边缘设备上跑必须用 s 或更小的 n有 Nvidia Jetson 或者 GPU 服务器做推流可以上 m。实测下来 yolov5s 在 640 分辨率下Jetson Nano 大约能跑 10 到 15 FPS树莓派 5 上能跑到 5 到 8 FPS基本满足“秒级响应”的违停检测需求。如果追求更高帧率可以导出 TensorRT 或 ONNX 格式加速。4.2 超参数含义与调参顺序yolov5 自带一组默认超参数写在data/hyps/hyp.scratch-low.yaml里。新手不建议一上来就改超参数先用默认值跑通再根据训练曲线微调。真正需要优先调整的是这几个lr0初始学习率默认 0.01。训练时 loss 显著震荡可以降到 0.005。mosaic是否启用马赛克数据增强默认 1.0能显著提升小目标检测能力但会拖慢训练。degrees和translate旋转和平移增强幅度适合监控场景微调设置太大容易把三轮车的长宽比扭曲得不像车。iou_thres验证时的 IoU 阈值通常保持默认 0.45 或 0.5。调参顺序的常见做法是先固定 batch size 和输入分辨率跑完一次完整训练观察 P、R、mAP50 三条曲线。如果 P 低误检多增大置信度阈值或提升难负样本挖掘比例如果 R 低漏检多检查是否目标太小、标注框是否完整覆盖目标。超参数是最后手段数据质量对结果影响更大。4.3 验证集结果怎么看训练结束后在runs/train/exp下有results.csv和confusion_matrix.png等文件。其中 PR 曲线和 F1 曲线能直观反映模型状态。下面这段命令用训练好的权重在验证集上做推理并生成指标python val.py \ --data yolodata/data.yaml \ --weights runs/train/exp/weights/best.pt \ --img 640 \ --conf-thres 0.25 \ --iou-thres 0.45输出结果包括各类别的 precision、recall 和 mAP。针对非机动车场景有一个关键观察点如果tricycle类别的 recall召回率低于 0.8最常见的原因不是模型问题而是标注框只框了车身、没框全车轮或者图片里三轮车被其他物体遮挡。此时应该回看训练数据补标漏掉的目标而不是调超参数。4.4 训练中的常见坑现象loss 一开始就接近 0后续不变或下降极慢。原因类别 ID 与标注不一致模型把所有目标当成背景。解决检查第一个 batch 数据可视化结果用python train.py --... --project runs --name debug训练时开启--save-period保存中间权重或者用view_img参数实时查看标注框是否正确。现象训练时显存内存波动大甚至 OOM。原因batch size 过大或 mosaic 增强的图片组合占用内存。解决调小 batch size同时设置--workers 4而不是默认的 8降低数据加载压力。如果仍然 OOM可以把输入分辨率降到 480代价是小目标精度下降。现象模型对某个方向的三轮车识别很好换个摄像头角度完全失灵。原因训练数据视角单一。解决在标注阶段特意保留多角度数据不要只收集正对车尾的图片。违停场景里摄像头通常斜俯视标注时要注意框住完整车辆区域。现象验证集 mAP 高但实际监控画面漏检严重。原因训练图片和实际视频帧差异大比如分辨率、压缩、光照。解决从真实视频流中抽帧补充训练数据。这也是数据工程里最需要耐心的一环。5. 模型部署与违规判断后处理从“识别出车”到“判定违停”5.1 导出 ONNX 或 TensorRT 模型训练完成后工程落地时往往不会直接用 PyTorch 权重因为部署端需要更高推理性能或更小的依赖。常见做法是先导出 ONNX再在目标设备上转成对应平台的推理引擎。导出 ONNX 的命令如下python export.py \ --weights runs/train/exp/weights/best.pt \ --img 640 \ --batch-size 1 \ --include onnx \ --simplify如果目标是树莓派 5 上跑还可以导出--include tflite或--include edgetpu后者需要额外安装 Edge TPU 编译器。导出后建议用自带的detect.py先验证导出模型是否正常python detect.py \ --weights best.onnx \ --source test.jpg \ --img 640 \ --conf-thres 0.25ONNX 模型在 CPU 上也比 PyTorch 原模型快不少。在树莓派 5 上如果还想加速可以再转成 TensorRT需要 Nvidia 设备或使用 Rust 生态的推理框架。对于纯 CPU 环境onnxruntime是常见选择安装命令为pip install onnxruntime。5.2 禁停区检测用 IoU 判断“车是否越线”模型给出的是检测框要判断是否违停需要和禁停区域做几何关系计算。这里用 IoU交并比或检测框中心点与多边形区域的包含关系来判断。对于固定摄像头场景禁停区可以在画面中手动标出一个多边形然后判断检测框中心是否落在多边形内。下面用 Python 的 shapely 库实现一个简单判断逻辑from shapely.geometry import Polygon, Point # 假设检测框为 [x1, y1, x2, y2] def is_violation(box, forbidden_zone): x1, y1, x2, y2 box center Point((x1 x2) / 2, (y1 y2) / 2) return forbidden_zone.contains(center) # 示例禁停区多边形画面坐标 forbidden_zone Polygon([(100, 100), (300, 100), (300, 300), (100, 300)]) box [80, 80, 150, 150] print(违停:, is_violation(box, forbidden_zone))这个方法的局限是只判断中心点如果三轮车车身横跨禁停线中心点在外面但车体已经压线会漏判。更严格的做法是计算检测框与禁停多边形的重合面积占比超过设定阈值比如 20%即判违停def is_violation_by_area(box, forbidden_zone, threshold0.2): x1, y1, x2, y2 box box_poly Polygon([(x1, y1), (x2, y1), (x2, y2), (x1, y2)]) inter_area box_poly.intersection(forbidden_zone).area return inter_area / box_poly.area threshold在视频流场景里不能单帧判定就报警否则行人路过、车流移动都会造成误报。正确的做法是连续 N 帧比如 10 帧中都检测到同一辆车的检测框位置基本不变且处于违停状态才触发告警。这就是“停留时间”逻辑也是违停识别和普通目标检测最大的区别。5.3 树莓派 5 上的部署注意事项树莓派 5 的 CPU 跑 yolov5s 推理单帧大约 200 到 400 毫秒勉强够用。如果要做更快的实时检测可以尝试以下优化路径降低输入分辨率到 416能显著提升帧率但对小目标有影响。使用--half半精度推理ONNX Runtime 在 CPU 上提升有限。把模型换成yolov5n网络更小、速度更快精度略低。开启多线程推理用 OpenCV 的cv2.dnn或 onnxruntime 的 intra-op 线程并行。部署代码建议用 onnxruntime而不是跑完整的 PyTorch 推理。下面给出一个可直接用的推理脚本骨架import cv2 import onnxruntime as ort import numpy as np session ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) def preprocess(img, size640): img cv2.resize(img, (size, size)) img img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, HWC to CHW img np.ascontiguousarray(img, dtypenp.float32) img / 255.0 return img[None] def detect(img): input preprocess(img) outputs session.run(None, {session.get_inputs()[0].name: input}) # outputs 是 1x25200x(c5) 的矩阵需要后处理 return outputs[0]后处理包括置信度过滤和非极大值抑制NMS。yolov5 的 NMS 逻辑在官方utils/general.py里部署时要么复用代码要么用cv2.dnn.NMSBoxes简化。这里提一个常见的坑onnx 模型输出的坐标是相对于 640x640 的推理前要记录原图尺寸后处理时把坐标映射回原图否则画框位置偏差。5.4 后处理参数的实践取值实际部署时置信度阈值conf-thres建议调到 0.3 到 0.45 之间。阈值太低会引入大量误检尤其在监控画面里人行道、树木阴影都可能被误判为三轮车阈值太高会漏检远距离的小目标。IoT 设备或低算力设备上为了降低 CPU 占用可以把阈值调高到 0.5再用“多帧确认”兜底。NMS 的 IoU 阈值通常保持 0.45 或 0.5。违停场景里多辆车挨着停时NMS 阈值太大可能合并两个不同车辆的框导致漏检太小又会在同一辆车上产生重复框。调试时多截几张有重叠车辆的画面反复测试直到检测结果稳定。对于禁停区判断多边形标注也有一个技巧不要画得太贴合路边石沿留出 5 到 10 像素的缓冲带因为检测框本身有抖动。通过计算检测框位置的稳定性比如过往 10 帧中心点坐标的标准差小于某个值可以在多帧确认阶段同时实现“稳停判断”。6. 违停检测项目收尾从单帧检测到持续运行的三个技巧走到这一步你已经有了一个能识别三轮车、能判断是否在禁停区的完整方案。但真实场景远没有训练时那么干净这里有三个提升项目落地度的技巧也是我每次做类似项目时习惯性检查的事项。第一个技巧是对视频流做缓存抽帧。不要对每一帧都推理那样 CPU 一直满载。常见做法是每秒抽 2 到 3 帧做检测其余帧直接放行结合上一章的多帧确认逻辑既不掉帧报警又能大大减少计算量。调整抽帧频率时要保证同一辆车在连续两帧中至少有 30% 以上的面积重叠这样跟踪匹配才可靠。第二个技巧是用检测框的面积变化过滤误检。同一摄像头下三轮车在画面中的面积应该在一个合理范围内。如果模型检出一个面积只有正常三轮车 1/10 的框大概率是误检。在代码里加一个简单过滤能消掉不少远处的噪声框。配合置信度阈值可以让整个系统更沉稳。第三个技巧是定期用真实截图回流训练集。项目初期模型上线后每天收集被误报和漏报的图片每周做一次增量训练在现有权重上继续训练几个 epoch。这项工作比调任何超参数都有用。我见过最有说服力的案例是仅靠两周真实负样本回流把误报率从每百帧 15 次降到 1 次以下。这也是机器视觉项目能否长期稳定运行的关键。最后说一个我自己的习惯每训一版模型都把数据 YAML、训练命令和权重 hash 记录在一个 txt 里。项目跑久了模型迭代十几版没有记录根本不知道线上跑的是哪一版。这个习惯帮我省过很多次返工。希望这篇文章能帮你在非机动车违规停放识别这件事上少走弯路从数据集到合规部署都顺利落地。本文还有配套的精品资源点击获取