☰
7张标注图冷启动:YOLOv5违规停放识别模型构建实战
2026/10/14 13:23:24 网站建设 项目流程

简介:压缩包内含三轮车tricycle7类别的已标注图像集,共2009个文件,包括1021张jpg原始图片与988个xml标注文件,压缩后大小约87.52MB。该子集取自三轮车八分类数据集的第七类,图片清晰、场景多样,覆盖淮海、金彭、宗申等常见三轮车品牌,并配有YOLO训练常用的VOC格式XML标注,可直接用于训练非机动车违规停放检测模型。面向机器视觉初学者与YOLOv5实战开发者,尤其适合需要补充三轮车样本、完成违规停放识别或目标检测课程设计的用户。数据包按图片与标注文件组织,便于快速划分训练集与验证集;结合YOLOv5官方代码即可启动模型训练。目前已有258人学习下载,轻量级体积适合反复调试与迭代实验。

1. 只有 7 张标注图,怎么建出能用的违规停放识别模型

接手一个物业安防类的小需求:摄像头拍到非机动车违规停放,系统要自动识别并上报,重点盯三轮车这类占道大户。我拿到的标的物就是标题里那个典型组合——yolov5 + 非机动车违规停放识别 + 已标注的 VOC 格式数据集,三轮车类别文件里就 7 张图和 7 个 xml。第一反应是这数据量不够看,但实际做下来,靠预训练权重 + 迁移学习 + 数据增强,两周时间就能跑出一个白天场景能上手的初级模型。这个项目最有意思的点在于:它依赖的不是数据量,而是把“机器视觉识别”和“违规判定”两件事拆干净的工程思路。本文把这套从 7 张图冷启动到能部署判定的完整路径写给你,新手照着做能复现,熟手可以直接看参数边界和踩坑记录。

2. 先想清楚:检测不等于违规判定,YOLOv5 在这里的边界在哪

2.1 为什么是 YOLOv5:小数据量、单类目标、边缘部署的组合

机器视觉识别这件事,选模型第一个要考虑的不是谁精度高,而是谁能在你的约束条件下真正落地。YOLOv5 是单阶段检测器,一次前向直接输出类别、置信度和边界框,端到端训练、部署生态成熟。对比两阶段检测器如 Faster R-CNN,精度上确实还有优势,但推理速度慢一个量级,在摄像头实时帧分析或边缘盒子场景下不划算。对比更新的 YOLOv8/YOLO11,训练和部署体验更好,但对于这种只有几十到几百张切片的小数据集,YOLOv5 的社区资料和踩坑案例最全,出问题能查得到答案。标题里既然指定了 yolov5,照做就完事。

再说检测任务形态。违规停放识别的核心是“目标存在性检测”,不是姿态估计,也不是细粒度分类。三轮车、电动车、自行车停放在禁停区域,它们在画面里的特征就是一个静止的车辆轮廓。YOLOv5 的 anchor-based 设计对这类中低长宽比、尺度相对固定的目标非常友好。配合 COCO 预训练权重,模型已经具备了车辆、自行车、行人等 80 类基础物体的特征先验,我们只是在它的基础上做小样本微调,把注意力转向“非机动车”和“三轮车”这两个业务类别。

提示:预训练权重是整个 7 张图方案能成立的根基。别想从零训练一个 YOLOv5,那才是真正的黑匣子,损失函数都不收敛。迁移学习在你这种数据量下不是可选项,是唯一选项。

2.2 违规判定的两层逻辑:检测框 + 禁停区域几何关系

如果把模型输出直接当成“违规”结果,你会在上线第一天就被误报淹没。YOLOv5 能告诉你的是“画面里某个位置有一个非机动车,置信度多少”,但它完全没有“这里能不能停”的概念。违规停放识别必须拆成两层:

第一层是目标检测,模型输出 (x1, y1, x2, y2) 边界框、类别、置信度;第二层是规则判定,把检测框和预先标注的禁停区域做几何计算。判定规则一般有三条,按优先级排列:

判定维度常见阈值说明
框与禁停区重叠面积比检测框面积 ∩ 禁停区面积 ÷ 检测框面积 ≥ 0.5车辆主体大部分压线才算违规
置信度过滤conf < 0.25 直接丢弃过滤模型犹豫的检测结果
驻留时长同一位置连续 N 帧命中才判定车辆经过禁停区不能算停放

我一般写一个简单脚本做第二层判定:把 detect 输出和禁停区 polygon 做 Intersection over Union 或面积比计算。这层规则逻辑独立于模型,用纯几何库就能实现。为什么强调拆分?因为客户真正关心的是“罚单”,不是“检测结果”。模型漏检可以通过补数据解决,但判定逻辑混乱会导致同一辆车一会儿违规一会儿不违规,这在真实项目中是大事故。

2.3 VOC 标注格式里藏着的三个关键点

标题里的tricycle7_images_xmls已经明示:数据集是 VOC 风格标注,每张图片对应一个同名的 xml 标注文件。拿到 xml 后先别急着转格式,先检查三个地方:

第一个是object节点数量。一个 xml 里可能有多个 object,每个 object 对应一个目标。第二个是name节点,这个决定了类别映射表怎么写。第三个是bndbox节点,里面是xmin/ymin/xmax/ymax四个整数,这是归一化坐标转换的唯一输入。

还有个容易被忽略的细节:VOC 里有个difficult节点,标注为 1 的目标在训练时通常被忽略。如果是别人标注的数据,difficult=1的目标要单独挑出来看看,它很可能包含“人被车遮挡只露出一半”这类难例,后期做难例挖掘时这些反而是金矿。另外 xml 的坐标是像素绝对值,而 YOLO 训练需要归一化到 0-1,这个转换环节最容易出问题,下一章细讲。

3. 把 7 张 VOC 标注变成能训练的数据:格式转换与数据增强

3.1 XML 转 YOLO 标签:归一化坐标与四步转换脚本

YOLOv5 训练读的不是 xml,而是每张图片对应一个同名的 txt,每行格式是class_id x_center y_center width height,所有值都是归一化到 0-1 的浮点数。转换脚本核心就是把xmin/ymin/xmax/ymax换算成中心点和宽高,再除以图片宽高。我建议写一个批量处理脚本,一次性处理整个标注目录:

import os import xml.etree.ElementTree as ET def convert_xml_to_yolo(xml_path, img_width, img_height, class_map): """ 将单个 VOC 格式 xml 转为 YOLO 格式 txt class_map: {'tricycle': 0, 'non_motor': 1} 业务类别映射表 """ tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter('object'): # 跳过 difficult=1 的目标,训练时容易引入噪声 if obj.findtext('difficult') == '1': continue name = obj.findtext('name') if name not in class_map: continue bndbox = obj.find('bndbox') xmin = float(bndbox.findtext('xmin')) ymin = float(bndbox.findtext('ymin')) xmax = float(bndbox.findtext('xmax')) ymax = float(bndbox.findtext('ymax')) # 边界框坐标裁剪,防止标注出现极端越界值 xmin = max(0, min(xmin, img_width)) xmax = max(0, min(xmax, img_width)) ymin = max(0, min(ymin, img_height)) ymax = max(0, min(ymax, img_height)) # 归一化:中心点坐标和宽高都除以图片宽高 x_center = (xmin + xmax) / 2.0 / img_width y_center = (ymin + ymax) / 2.0 / img_height width = (xmax - xmin) / img_width height = (ymax - ymin) / img_height lines.append(f"{class_map[name]} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") return lines # 使用示例:读取同名图片尺寸,转换后写入同名 txt for xml_file in os.listdir('annotations'): if not xml_file.endswith('.xml'): continue stem = xml_file[:-4] img = cv2.imread(f'images/{stem}.jpg') h, w = img.shape[:2] yolo_lines = convert_xml_to_yolo(f'annotations/{xml_file}', w, h, class_map) with open(f'labels/{stem}.txt', 'w') as f: f.write('\n'.join(yolo_lines))

这个脚本有三个关键处理点。第一个:difficult==1直接跳过,避免把遮挡严重的难例在冷启动阶段灌进训练集。第二个:边界框坐标做了裁剪,有些标注工具的 bndbox 会略微超出图片边界,不裁剪的话归一化值会大于 1,训练时 loss 直接飙升。第三个:类别映射表class_map必须和后续data.yaml里的类别顺序完全一致,顺序错了模型学出来的语义就全乱了。建议转换完成后随机抽三张图,把 txt 里的坐标反算回像素值画框,和原 xml 人工比对一遍,这一步 10 分钟,能省掉后面一整天排查时间。

3.2 数据增强:小样本项目翻车最多的地方,albumentations 配置参考

7 张原始图直接训练,过拟合到怀疑人生。常见做法是用数据增强扩充到 100-200 张级别,增强必须在转换 txt 之后做,因为增强库要同时变换图片和对应的边界框坐标。我用的是 albumentations,它对 bbox 坐标的同步变换封装得很好,不需要自己写几何映射逻辑。

import albumentations as A from albumentations import bbox_utils # 关键:bbox_params 必须指定 format='yolo',和上一节输出的 txt 格式对齐 augment_pipeline = A.Compose([ A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.15, p=0.8), A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=15, val_shift_limit=10, p=0.6), A.Rotate(limit=10, border_mode=0, p=0.5), # 小角度旋转,border_mode=0 填充黑色 A.RandomScale(scale_limit=0.15, p=0.5), A.GaussNoise(var_limit=(10.0, 30.0), p=0.3), A.HorizontalFlip(p=0.5), ], bbox_params=A.BboxParams(format='yolo', min_visibility=0.3)) # 每张原图生成 10 个增强版本,7 张变成 70 张 for img_path, label_path in sample_pairs: image = cv2.imread(img_path) with open(label_path) as f: bboxes = [parse_yolo_line(line) for line in f.readlines()] for i in range(10): augmented = augment_pipeline(image=image, bboxes=bboxes) aug_img, aug_bboxes = augmented['image'], augmented['bboxes'] # 保存增强图和对应的 txt 标签文件

注意几个容易翻车的地方。Rotate旋转后图片四个角会变成黑色区域,用border_mode=0是黑色填充,模型会学到“角上有黑块”这种伪特征,后期如果能保证部署画面干净可以接受,否则建议换border_mode=1用白边或者直接裁剪。min_visibility=0.3很关键,它表示旋转或缩放后如果框被切掉超过 70%,这个标注框直接丢弃,避免给模型喂一堆“半个车轮也算三轮车”的残缺标签。另外增强样本不要过度,一张原图生成 10-15 个版本就够,太多同源增强会让验证集失去意义,模型很快把增强参数背下来。

3.3 训练集与验证集划分:小数据量下的比例陷阱

7 张图如果按常规 8:2 切分,训练集 5 张、验证集 2 张,验证集根本看不出模型真实水平。我的做法是两阶段:第一次冷启动训练,5 张训练 + 2 张验证,目标是确认 loss 能下降、验证集不崩;第二次回灌数据后,按 7:3 重新切分。小数据集下划分的随机性影响非常大,一定要固定随机种子,不然每次训练结果都像抽奖。

这个阶段还有两个目录结构细节要注意。第一,YOLOv5 训练不直接读 xml,目录要整理成images/和labels/两个平铺目录,文件名一一对应。第二,增强后的图片命名要带来源标识,比如orig1_aug03.jpg,这样后期如果发现某个增强版本标注错了,能顺着文件名回溯到原图和原始 xml。冷启动阶段不追求严谨的抽样统计,追求的是快速验证全链路通不通。

4. 用 YOLOv5 正式训练:小数据集的参数设计与命令行实操

4.1 先定三个参数:预训练权重、img 分辨率、epochs

3.2 节做完,手头大概有 70-100 张增强后的训练图片。训练前要写好data.yaml,内容就是指定训练和验证集路径、类别数和类别名称:

# nonmotor.yaml train: ./datasets/nonmotor/images/train val: ./datasets/nonmotor/images/val nc: 2 names: ['tricycle', 'non_motor']

然后开始选参数。预训练权重选yolov5s.pt,s 版本最均衡,速度快、显存占用小,对于这种单类别小目标项目足够。img 分辨率设 640 还是 416,取决于你的部署设备。如果跑边缘盒子或摄像头实时流,416 能显著提升推理速度;如果跑 GPU 服务器离线分析,640 对小目标的召回率更友好。epochs 设 200,配合 early stop 机制,loss 连续 30 轮不降就自动停。batch-size 看显存,8G 显存跑 8,4G 跑 4。

提示:不要因为数据少就把 epochs 调小。小数据集训练的特点就是收敛慢、波动大,200 轮里可能前 80 轮看起来什么都没学到,后面才开始明显下降。中途觉得没戏就 Ctrl-C 是我见过最多的翻车操作。

4.2 训练命令:关掉 Mosaic,打开增强,其他参数默认

YOLOv5 官方的train.py参数体系很成熟,小数据集下大部分参数保持默认即可,真正需要手动改的只有四个:

python train.py \ --weights yolov5s.pt \ --data nonmotor.yaml \ --epochs 200 \ --batch-size 8 \ --img 640 \ --mosaic 0 \ --augment \ --project ./runs \ --name nonmotor_v1

--mosaic 0是很多人会忽略的一个关键开关。Mosaic 增强会把四张图拼成一张,在正常数据集上能大幅提升泛化能力,但小数据量下存在两个问题:一是拼接后每个目标只占原图的 1/4,本来就不够的三轮车特征被进一步缩小;二是 7 张源图拼接来拼接去,生成的组合几乎都是重复样本,等于变相过拟合。小数据冷启动阶段建议关掉,等数据量扩充到 200 张以上再打开。--augment会启用 YOLOv5 内置的 HSV 色域变换和随机平移缩放,与我在 3.2 节做的离线增强叠加,能让模型看到更丰富的光线变化。

训练时要盯两个指标:box_loss和obj_loss是否持续下降,以及val/obj_loss有没有和训练 loss 拉开差距。小数据集通病是 val loss 波动剧烈,这很正常,只要整体趋势向下就可以继续。每个 epoch 结束时输出的 PR 曲线和混淆矩阵图片,保存在runs/train/nonmotor_v1/里,第一轮训练后直接看这两张图比看 loss 数字更直观。

4.3 训练完先做一次侦探式推理:detect.py 与置信度调优

训练完成后不要急着聊部署。第一步是用官方detect.py在新图片上做一次带置信度的推理,看看模型在真实摄像头帧上的表现:

python detect.py \ --weights runs/train/nonmotor_v1/weights/best.pt \ --source ./test_frames/ \ --conf-thres 0.25 \ --iou-thres 0.45 \ --save-txt \ --save-conf

--save-txt会把检测结果保存为同名 txt,每行是class_id conf x1 y1 x2 y2;--save-conf在图上直接标出置信度数值。我习惯在第一次推理时把--conf-thres设到 0.25,然后逐张看图,统计漏检和误报。漏检多说明模型没学会,误报多说明类别区分度不够。v1 模型通常会出现“三轮车漏检但置信度很高”或“行人被识别成非机动车”两种情况,这些都是下一批数据应该采集的方向。这一轮检测的意义不是上线,而是明确告诉你:模型现在缺什么,下一批训练数据应该补什么。

如果验证集 mAP 已经到 0.7 以上,但实际摄像头画面上漏检严重,基本上就是数据分布和训练集差异太大,通道偏移、视角不同、光线暗,都会让小模型崩盘。此时调参没有意义,直接进入数据回灌,也就是后面第 6 章讲的半自动标注流程。

5. 避坑与排查:小数据集训练最常见的五个现场

5.1 现象一:loss 不降反升,训练曲线像心电图

第一次训练时遇到 loss 持续震荡甚至上升,第一个排查点不是网络结构,而是标签文件。打开 labels 目录下的 txt,看有没有坐标值大于 1 或小于 0 的行。这一类都是 XML 转 YOLO 脚本的锅,常见原因是 xml 里xmax比图片宽度还大,或者类别 id 映射表顺序写反了。另一个高频原因是增强后 bbox 没跟着图片一起变换,增强图保存了,txt 标签还是旧的,模型看到的标注和图上目标位置完全对不上。

解决步骤是:先看训练日志里每个 epoch 的obj_loss,如果是 NaN 或持续大于 1.0,100% 是标签异常;把增强前后的labels/里任意一张 txt 和原图画框对比,找出哪一类样本出了问题。

5.2 现象二:三轮车近景能识别,远景小目标漏检

这个现象在小数据集项目里几乎是必然出现。7 张原图多数是近景,三轮车占了画面 1/3,增强后也都是在近景上做变换,模型对“小尺度三轮车”完全没有概念。我踩过的坑是把 img 从 640 调到 320,想靠缩小分辨率让目标相对变大,结果近景目标反而糊了,整体效果更差。

正确处理是补数据,不是调参数:单独从视频里抽远景帧,把画面里车只占 5%-10% 的情况标注 20-30 张,回灌后模型立刻改善。置信度阈值也可以从 0.25 降到 0.15,代价是误报变多,适合用在事后录像分析而不是实时告警。

5.3 现象三:白天识别正常,傍晚开始误报飙升

这是典型的“训练数据分布单一”问题。7 张标注图大概率是白天光线充足的场景,模型学到的是“亮色背景上的三轮车轮廓”。傍晚光线变暗后,车辆阴影拉长、色彩饱和度下降,模型开始把垃圾桶、树影、行人误判为非机动车。

解决的办法是在增强 pipeline 里增加一个“模拟暗光”通道:把图片整体亮度降 30%-40%,并把 HSV 的色调偏移加大。但这种模拟只能缓解,真实夜间数据还是要靠补拍补标。如果业务上夜间是重点时段,建议冷启动阶段就直接加夜帧,不要等白天跑通了再补。

5.4 现象四:几辆车紧挨着停,检测框粘连成一个

非机动车停放场景里,电动车和三轮车往往挤成一片,车辆之间遮挡严重。此时 YOLOv5 的 NMS 会把多个检测框合并成一个,或者只保留置信度最高的那一个。这不是模型坏了,是 NMS 去重逻辑在密集场景下天然会这样做。

解决:一是降低--iou-thres,把去重阈值从 0.45 调到 0.3,让更多低重叠框保留下来;二是对“目标个数”有硬性要求时,改用密度估计类模型做辅助,但这就是另一个项目方向了。常规做法是接受漏检,把业务规则限定在“该区域是否有违规停放”而不是“精确到有多少辆”。

5.5 现象五:行人频繁被当成非机动车违规停放

最后这个现象是最让人头疼的,根源在于类别语义本身。一个小数据集下,模型分不清“人推着三轮车走”和“三轮车停着”——人推车时画面里人和车紧挨着,模型可能把行人的矩形框也算进三轮车检测结果里。加上违规停放判定的第二层逻辑用的是矩形框重叠面积比,人站在禁停区内推车也会触发判定。

解决思路分模型侧和规则侧。模型侧:把“人推车”这类组合样本单独打一个负样本集,回灌训练,让模型学会“人车一体时置信度下调”。规则侧:增加帧间驻留时长判断,人推车是动态目标,连续 N 帧位置变化大,直接过滤掉。这也是为什么我一直强调检测和判定必须拆成两层,只有检测层的话这类误报完全没有干预空间。

6. 从 7 张到持续进化:半自动标注回流与难例挖掘

项目上线只是开始,真正让小数据集方案稳定持久的是数据回流机制。我的做法是搭建一个最简半自动标注流程:用当前模型对新增摄像头帧做推理,把置信度高于 0.8 的检测结果直接当成粗标签写入 txt;置信度在 0.4-0.8 之间的目标单独导出成图片,人工快速判断补标;置信度低于 0.4 但模型有“疑似反应”的区域(比如检测到但被 NMS 压制的框)全部保留为候选难例。这样每一轮采集都能形成自己的难例库,而不是反复在原始 7 张图上做增强。

这个环节有一个核心代码片段,作用是挑出那些“被 NMS 抑制但可能存在目标”的区域。做法是在 detect 的推理循环里,不直接取 NMS 后的结果,而是保留 NMS 前置信度大于阈值的所有原始框:

import torch from models.experimental import attempt_load # 加载训练好的模型,half 精度在推理时能省一半显存 model = attempt_load('runs/train/nonmotor_v1/weights/best.pt', map_location='cuda') model.half().eval() def extract_candidates(img_tensor, conf_thres=0.1): """ 输出 NMS 前的所有候选框,用于难例回流 """ with torch.no_grad(): pred = model(img_tensor)[0] # 原始预测层输出 candidates = [] for det in pred: # 每个检测头分别处理,conf 过滤设在很低的 0.1 keep = det[..., 4] > conf_thres det = det[keep] # 记录 class_id, conf, 原始边界框 candidates.append(det.cpu().numpy()) return candidates

这条回流链路一旦打通,7 张图就会像滚雪球一样越滚越大。我第一次跑通这个逻辑,是在一个停车场项目上,当时诊断模型在侧视角总漏检,我就用这个脚本从三天监控录像里自动抽出了 40 多个疑似难例,人工花一下午补完标注,二次训练的漏检率直接降了一半。后来我养成了一个习惯:每次调参前先做一次难例统计——如果误报集中在某类特定的光线或姿态上,那这一轮就专门补这类数据;等比重的调参只会在真实场景里反复翻车。这个习惯一直沿用手头各个视觉项目,希望你也能用上这套从少量标注起步的冷启动打法。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询