ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

YOLOv5+非机动车违停识别:从VOC转YOLO到部署全流程

YOLOv5+非机动车违停识别:从VOC转YOLO到部署全流程 简介面向非机动车违规停放检测任务该压缩包内含已标注三轮车数据集的第一类tricycle1适合使用yolov5进行机器视觉训练的研发人员与学习者。数据来源为完整的非机动车图像库专门聚焦三轮车这一细分类型可直接用于训练检测模型或微调现有权重。包内包含957张实拍三轮车图片及953个配套XML标注文件共1910个文件压缩后约82MB标注格式为通用VOC格式能便捷转换为YOLO所需文本格式。已有295人学习下载属于非机动车系列数据中的首个分类其余七类三轮车数据可按需获取。该子集标注框包含完整三轮车轮廓与常见停放姿态适合验证违规停放识别算法、补充训练样本、降低人工标注成本为城市管理场景的智能化巡检提供数据支撑。1. yolov5 非机动车违停识别这条技术路线到底能不能直接落地路侧摄像头拍到一辆三轮车停在禁停黄线内司机卸货后三分钟没动。这类典型非机动车违规停放场景靠人工盯屏不现实而通用目标检测模型又几乎没有针对性的公开权重。标题里这串名词拼出了一个完整方案用 yolov5 做机器视觉识别用 tricycle1_images_xmls 这类已标注数据集补上“三轮车”类别最终在监控画面上自动判定违停行为。这个方案适合手里有监控画面、需要给城管或物业做违停告警的工程师。你不需要从零标注数据但必须处理好格式转换、训练参数和后处理规则——这篇笔记就是带你把整条链路走通。2. 把 tricycle1_images_xmls 变成 yolov5 能吃的格式VOC 转 YOLO 与数据校验2.1 先盘清 tricycle1_images_xmls 的目录确认 VOC 标签可以读拿到数据集第一步不是急着训练而是搞清楚目录结构和标注格式。像 tricycle1_images_xmls 这种命名通常表示一批图片配同名的 XML 标注文件XML 里是 VOC 格式的object标签。VOC 格式和 yolov5 原生格式不一样VOC 记录的是xmin/ymin/xmax/ymax绝对坐标yolov5 训练时需要的是每个目标一行、类别 ID 加归一化中心坐标和宽高的纯文本 txt 文件。先看目录结构常见组织方式是images/和xmls/并列也有把 XML 直接和图片混放的。先把原始目录列出来确认三类信息图片后缀是 jpg 还是 png、XML 里有哪些类别名、有没有找不到对应图片的脏标注。# 查看目录结构 find tricycle1_images_xmls -maxdepth 2 -type f | head -20 # 统计 xml 里的类别名确认需要映射成几个类 grep -h name tricycle1_images_xmls/xmls/*.xml | sort | uniq -c这一步的价值在动手转换前就暴露问题。很多公开数据集类别名写得很随意tricycle和three_wheel混用e_bike和electric_bicycle并存。如果不先统计转换脚本的映射表就会丢数据而且丢得很安静——训练时发现类别数不对再回头排查 XML 已经不是损失曲线的问题而是整个数据集白标注了一遍。所以建议先跑上面两条命令把类别名和图片数量两个底账摸清楚。提示如果发现一个 XML 里同时出现多个相似类别名先统一类别不要想着“转换完再处理”。转换后 txt 只有类别 ID没有原始字符串信息一旦丢失很难补回来。2.2 用 Python 一次性把 VOC 的 xml 转成 YOLO 的 txt 归一化坐标确认标签内容后写一个 VOC 转 YOLO 的转换脚本。yolov5 训练自己的数据集时每张图片对应一个同名 txttxt 每行格式是class_id x_center y_center width height其中坐标必须归一化到 0~1。这里有一个容易踩坑的细节归一化的宽高要用图片实际像素尺寸计算最好直接用 PIL 读图拿宽高而不是从 XML 的size节点里读——因为有些标注工具的 size 节点写错了分辨率用错尺寸会导致框全部偏移。import os import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image # 按实际目录调整 xml_dir tricycle1_images_xmls/xmls img_dir tricycle1_images_xmls/images out_dir datasets/tricycle/labels os.makedirs(out_dir, exist_okTrue) # 类别映射根据 2.1 统计的真实类别名来填 class_map { bicycle: 0, electric_bicycle: 1, tricycle: 2, motorcycle: 3, } def voc_to_yolo(xml_path, img_w, img_h): 单个 xml 转成 yolo 格式的文本行列表 tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in class_map: print(f[skip] 未映射类别: {name} in {xml_path.name}) continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # VOC: 左上右下 - YOLO: 中心宽高全部归一化 x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 过滤掉标注错误的空框和越界框 if w 0 or h 0 or w 1 or h 1: print(f[warn] 异常框: {xml_path.name}) continue lines.append(f{class_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return lines # 批量处理 for xml_file in sorted(Path(xml_dir).glob(*.xml)): img_name xml_file.stem .jpg img_path os.path.join(img_dir, img_name) if not os.path.exists(img_path): print(f[skip] 图片不存在: {img_name}) continue with Image.open(img_path) as im: img_w, img_h im.size lines voc_to_yolo(xml_file, img_w, img_h) out_txt os.path.join(out_dir, xml_file.stem .txt) with open(out_txt, w, encodingutf-8) as f: f.write(\n.join(lines)) print(转换完成txt 输出到:, out_dir)这段脚本的逻辑说明先遍历所有 XML再用Image.open读图片宽高然后逐 object 解析出 bndbox 坐标转成归一化中心点格式。两个关键设计一是未映射类别直接跳过但打印提示避免后续怀疑“模型漏检”其实是数据丢了类别二是异常框过滤把宽高为 0 或坐标越界的坏样本挡在训练集之外。参数上你可能需要改的是class_map这是唯一和你的数据集强耦合的地方。2.3 生成 data.yaml 并画框复核趁训练前把脏数据揪出来转换完成后在datasets/tricycle/下建tricycle.yaml这是 yolov5 训练时读数据配置的入口。注意train和val指向的路径如果你的数据量不大不建议单独切测试集把验证集留 15%~20% 就够用。# datasets/tricycle/tricycle.yaml path: datasets/tricycle # 数据集根目录相对 yolov5 运行目录 train: images/train val: images/val nc: 4 names: [bicycle, electric_bicycle, tricycle, motorcycle]图片目录也要按训练集/验证集拆分。一个合理的拆分方式是按文件名哈希切分而不是直接用 random 打乱再切——哈希切分能保证同一个场景序列不会同时出现在训练集和验证集里避免验证集“作弊”。切分命令可以简单用 Python 处理也可以直接用sklearn的train_test_split。切完以后做最后一步体检随机挑 10 张训练图把 txt 里的标注框画回原图人工确认坐标没有整体偏移。import cv2 import random label_dir datasets/tricycle/labels img_dir datasets/tricycle/images/train names [bicycle, electric_bicycle, tricycle, motorcycle] for txt_file in random.sample(sorted(Path(label_dir).glob(*.txt)), 10): img_path os.path.join(img_dir, txt_file.stem .jpg) img cv2.imread(img_path) h, w img.shape[:2] with open(txt_file) as f: for line in f: cid, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, names[int(cid)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(check_ txt_file.stem .jpg, img)这段画框脚本本身没有“技术含量”但它是我每次训练前的固定动作。实操里最常见的翻车就是类别 ID 错位比如 XML 里tricycle排在第 0 位转换后 class_id 写成 2画框时显示“motorcycle”的名字——模型根本训不出来。肉眼扫一遍 10 张图比任何日志检查都直接。3. 用已标注数据集训练自己的违停识别模型conda 环境、训练命令与超参数调整3.1 conda 环境配置与 yolov5 源码准备先跑通官方检测yolov5 源码是典型的“环境配置比训练本身更容易劝退”项目。PyTorch 版本、CUDA 版本、torchvision 版本三者必须匹配否则装完import torch直接报错。我的习惯是用 conda 建独立环境避免把系统 Python 搞乱。# 创建独立环境Python 3.9 兼容性最稳 conda create -n yolov5 python3.9 -y conda activate yolov5 # 安装 PyTorch按你的 CUDA 版本选命令这里以 CUDA 11.8 为例 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 拉取 yolov5 官方仓库并安装依赖 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt装完后先不急着训自己的数据用官方权重跑一轮检测确认环境没有“假成功”。这一步看起来多此一举但能帮你在后续排障时快速区分问题归属是数据的问题、超参数的问题还是环境不完整的问题。# 下载官方预训练权重并跑一张测试图 python detect.py --weights yolov5s.pt --source data/images/bus.jpg如果这张图能正常输出检测结果说明环境链路是通的后面出问题都可以往数据和配置上查。我见过不少同行装完环境直接训自己的数据集训练时 loss 正常下降但推理时输出全是空——最后发现是requirements.txt里某个依赖没装全推理时特定函数库没有正确加载。先跑通官方 demo 是成本最低的验证。3.2 训练命令逐参数说明weights、batch、img、epochs 怎么定环境就绪后训练命令的核心参数是weights、batch-size、img-size、epochs这四个。很多人习惯直接抄别人的参数但你的显卡显存和数据量决定这些参数不能照搬。python train.py \ --data datasets/tricycle/tricycle.yaml \ --weights yolov5s.pt \ --batch-size 16 \ --epochs 150 \ --img 640 \ --workers 4 \ --cache参数说明--weights yolov5s.pt表示用官方在 COCO 上预训练的 yolov5s 作为起点而不是从零训练。COCO 里包含 80 类通用目标虽然三轮车不在其中但卷积层提取的纹理、边缘、形状特征可以迁移过来能明显加快收敛。--batch-size 16是比较保守的选择如果你的显卡显存只有 8G建议降到 8如果显存 24G可以开到 32。--img 640是速度和精度的平衡点当画面里三轮车很小比如不到 40 像素时再考虑 960但训练时间会明显变长。--epochs 150对于几千张图的数据集够用不必一上来就 300。--cache这个参数值得单独说。它会把训练图片预加载到内存避免每个 epoch 都从磁盘读图。你的数据集如果不算大开--cache能省下大量时间代价是占用内存。训练时建议加--project runs/tricycle把实验输出分目录管理否则多个实验都写到runs/train/里对比试验时容易搞混。训练过程中最需要盯的是results.png里的损失曲线和验证集 mAP。如果val/box_loss和train/box_loss之间出现明显 gap说明过拟合已经开始如果两个 loss 都高位震荡大概率是学习率太大或数据有脏标注。3.3 超参数只调这 5 个mosaic、copy_paste、scale、lr0、anchoryolov5 超参数文件hyp.scratch-low.yaml里参数很多但真正值得新手动的不超过 5 个。那些fl_gamma、label_smoothing这类参数默认值在多数场景下比你自己拍脑袋调的要稳。这里按对训练结果影响从大到小排序# hyp.scratch-low.yaml 中的关键修改项 lr0: 0.003 # 初始学习率小数据集建议从 0.001~0.003 起步 mosaic: 1.0 # 马赛克拼接增强对中小目标效果明显保持 1.0 copy_paste: 0.2 # 复制粘贴增强适合样本少的类别比如三轮车 scale: 0.5 # 图像缩放范围太小会让模型没见过大尺度变化mosaic是 yolov5 最有代表性的数据增强每轮把 4 张图拼接成一张再训练相当于强制模型学会在复杂背景下识别目标。对于非机动车这种多姿态目标mosaic 带来的收益非常明确只有一种情况需要关掉——你的目标在原始图片里已经很小mosaic 拼接后可能被缩得更小。copy_paste是样本量不平衡时的后悔药把三轮车目标“抠”到别的场景图上等于免费扩充了稀缺类别样本但注意密度太高的图不要用会让模型产生“这里必有车”的偏见。anchor参数值得单列一句。yolov5 会自动基于你的标注框重新聚类 anchor如果你发现训练日志里出现anchors miss之类的提示多半是数据集里目标尺寸变化太大。一个偷懒但有效的做法先把带自动 anchor 的训练跑 50 轮看train/box_loss能否降到 0.05 以下降不下去再手动调整 anchor不要一开始就动它。4. 从“认出三轮车”到“判定违规停放”后处理与业务规则4.1 后处理先干什么置信度、NMS、classes 过滤别把误检带上线训练收敛后模型输出的是一堆候选框每个框带类别、置信度和坐标。直接把这些框上屏会出现大量重叠框和低置信度误检所以后处理管线是上线前必须做的一道工序。yolov5 的detect.py已经有默认的 NMS 和置信度过滤但默认参数是为 COCO 这种通用场景调出来的不适合直接用在违停业务上。# 推理时按业务场景收紧参数 python detect.py \ --weights runs/tricycle/exp/weights/best.pt \ --source test_videos/street_1.mp4 \ --conf-thres 0.45 \ --iou-thres 0.45 \ --classes 2 \ --save-txt \ --save-crop参数说明--conf-thres 0.45表示置信度低于 0.45 的框直接丢弃。这个值需要按你的实际场景调如果误检太多就往上抬到 0.5~0.6如果漏检明显就降到 0.35。--iou-thres 0.45是 NMS 的 IoU 阈值两个重叠框的 IoU 超过 0.45 就合并值越大保留的框越多。--classes 2是本次推理重点违停识别场景通常只关心三轮车直接只让模型输出类别 ID 为 2 的检测框把自行车和摩托车的检测结果挡在门外面这也是减少误报的最简单手段。--save-crop会把每个检测框截图单独保存。这个参数平时没人提但做误报分析时非常有用——你可以快速翻 cropped 图确认哪些背景被当成了三轮车比逐帧看视频高效得多。如果模型已经部署成服务建议在服务里同时保留这个逻辑。4.2 违停判定不能只看单帧结合停留时长与禁区坐标机器视觉识别到这里完成的是“检测到一辆三轮车”但“违规停放”的判定还需要业务规则。摄像头捕捉到一辆三轮车驶过禁停区不能立刻告警它可能只是路过判定违停的核心是三帧以上的“静态存在”加位置判断。这个逻辑用目标追踪或简单状态机都能实现关键在设定“停留多久算违停”的阈值。常见做法是给每个检测框分配一个 track_id记录它的中心坐标和命中次数。如果连续 N 帧根据视频帧率换算比如 15 帧/秒下 30 帧即 2 秒目标都停留在禁停区域内且中心点位移小于某个像素阈值就判为一次违停事件。# 违停判定的核心逻辑目标在禁停区内连续命中足够帧数 class ViolationTracker: def __init__(self, zone_polygon, need_frames30, max_miss5): self.zone zone_polygon # 禁停区多边形坐标 self.need_frames need_frames # 连续多少帧算违停 self.max_miss max_miss # 允许丢帧数缓解短暂遮挡 self.tracks {} def update(self, detections, frame_id): events [] for det in detections: # det: [x1, y1, x2, y2, conf, class] x1, y1, x2, y2 det[:4] cx, cy (x1 x2) / 2, (y1 y2) / 2 if not self.point_in_zone(cx, cy): continue # 用中心点粗略关联上一帧的 track tid self.find_near_track(cx, cy) if tid is None: self.tracks[len(self.tracks)] {hit: 1, last: (cx, cy)} else: self.tracks[tid][hit] 1 if self.tracks[tid][hit] self.need_frames: events.append({track_id: tid, frame: frame_id}) return events这段代码是简化版实际工程里要接一个轻量跟踪器做find_near_track避免摄像头轻微抖动导致 track 断裂。我的建议是别自己硬写跟踪逻辑直接用 SORT 或 ByteTrack 这一类现成方案把检测结果喂进去拿轨迹你再套一个“轨迹是否长时间落入禁停区”的判定即可。注意帧数阈值和像素位移阈值是两个必须联动的参数。帧数只限制“时间维度”如果你不限制“位移维度”一辆从禁停区缓慢驶过的三轮车也会被当成违停。通常中心点位移连续 N 帧都小于 5~8 个像素才认为是真的停住了。4.3 告警事件生成的最小状态机判定违停是一次连续事件不等于每帧告警一次。如果检测模型在 3 秒内判定目标停留并触发告警然后又因为目标被遮挡短暂丢失接着重新检测到又触发一次下游的告警平台会被重复消息淹没。这里需要一个简单的状态机空闲 - 疑似 - 告警 - 冷却。状态机的四个状态含义空闲表示禁停区没有目标疑似表示目标进入禁停区但停留帧数没到阈值告警表示停留帧数满足条件此时发送一条告警冷却表示告警已经发过短时间内即使再检测到也不重复发冷却时间一般设置 60~120 秒。class AlarmStateMachine: def __init__(self, cooldown_sec90): self.state idle self.last_alarm_time -cooldown_sec self.cooldown_sec cooldown_sec def tick(self, vehicle_stopped, now_sec): # vehicle_stopped: 当前帧判定目标处于违停状态 if self.state alarm and now_sec - self.last_alarm_time self.cooldown_sec: self.state idle if vehicle_stopped and self.state idle: self.state alarm self.last_alarm_time now_sec return True # 触发告警 return False这个最小实现只有三十行代码但它决定了整个系统的“吵闹程度”。很多项目算法本身做得很准最后被业务方投诉“你们这个系统一天告警八百次”就是缺少冷却逻辑。另一个实用技巧告警消息里附带上一张检测截图让审核人员不用点开视频就能判断这是真违停还是模型误报这会让投产验收顺利得多。5. 训练与部署避坑5 个让我返工的真实问题5.1 现象mAP 很高现场实拍却什么都检不出来训练日志里mAP0.5显示 0.92看起来很漂亮但换到真实摄像头画面里模型输出几乎是空的。原因验证集和训练集来自同一个场景序列数据分布高度重合模型没有机会接触真实场景里的光线变化、视角畸变和背景噪声mAP 虚高。解决训练集里必须混入至少 20%~30% 的“脏图”——不同角度、不同时段、甚至包含运动模糊的负样本。如果手头没有这些数据宁可把单场景数据按时段切分也不要让训练/验证集来自同一段监控。5.2 现象三轮车被 A 柱/树干挡住一大半直接漏检非机动车违规停放的目标经常停在道路边缘被路灯杆、绿化带或旁边的大车遮掉一半。yolov5 对完整目标效果好对严重遮挡目标召回率明显下降。原因训练数据里标注框都是完整可见的三轮车模型没有学过“半个轮胎 部分车厢”也是三轮车。解决给训练集做随机遮挡增强或者直接把遮挡场景的标注框保留在训练集里不要因为“标注看起来不完整”就删掉。我刚做这个项目时删了一批半遮挡标注后来后悔得很那正是真实场景最需要学习的数据。5.3 现象三轮车类别训练后 AP 只有零点几如果数据集里三轮车只有几百张而电动车有两千张训练完成后tricycle类别的 AP 往往惨不忍睹。原因类别不平衡导致模型把学习资源都给了大类别。解决先调copy_paste增强的概率参数把三轮车样本量补到接近电动车水平如果补完还不够就单独给三同类的高置信度预测结果降低损失权重。这件事别指望靠调.yaml里的cls_pw参数一步到位数据层面补样本优先级最高。5.4 现象训练用大图部署用 640模型表现明显下滑有个同行用--img 1280训练出模型线下验证 mAP 很高但部署到边缘设备后为了保帧率把输入降到 640检测框开始乱跳。原因很直白模型学的是大分辨率下的细节纹理缩到 640 后这些细节全部丢失目标特征分布变了。解决训练分辨率和部署分辨率保持一致没有特殊理由不要训练用大图部署用低分辨率。如果现场确实需要检小目标就老老实实训练 960 且部署时保持 960不要指望“模型能自适应缩放”。这个坑我踩过一次之后换模型时都会先确认推理端实际使用尺寸。5.5 现象标注框有 1~2 像素偏差增强后损失曲线剧烈震荡训练过程中train/box_loss上下跳不收敛最后发现是部分标注框的边界和车身边缘有 1~2 像素偏差。单独看这些偏差不算大问题但 yolov5 的 mosaic 增强会把多张图拼接缩放微小偏差被放大成 3~5 个像素的偏移导致回归任务一直在学习噪声。解决训练前用脚本统计所有标注框尺寸分布把明显异常的框宽高为空或超出图片边界批量清洗。自动化清洗后再做一轮人工抽检比直接开训稳得多。我的习惯是这种清洗脚本单独存一份换数据集时报错立即跑一遍。6. 部署到边缘设备的验证技巧先导出再压测最后固定一个检验流程6.1 导出 ONNX/TorchScript确认部署格式可用训练完成不代表能上线yolov5 训练出的 PyTorch 权重需要转换成部署格式。常见做法是转 ONNX 给 OpenVINO 或 TensorRT 用或者转 TorchScript 给 C 端直接加载。转换命令本身很简单但转换后的输出尺寸和动态轴必须确认否则真到设备上报错。# 导出 ONNX开启简化模式 python export.py \ --weights runs/tricycle/exp/weights/best.pt \ --img 640 \ --batch 1 \ --simplify \ --include onnx # 导出 TorchScript python export.py \ --weights runs/tricycle/exp/weights/best.pt \ --img 640 \ --include torchscript如果目标设备是树莓派 5 这类小算力环境还可以通过--include engine走 TensorRT 或者转成 NCNN 格式这一步取决于底层的推理框架。但不管导出成什么格式都要先在本地把导出的模型跑一遍相同的测试视频对比导出前后检测框数量和坐标是否一致。忽略这一步你会在部署后发现模型“莫名其妙变笨了”实际上只是转换配置用错——比如动态分辨率没有放开。6.2 用 5 分钟真实视频切片做回归测试记录误报和漏报部署前的压测不要用单张图片要找一段 5 分钟的真实监控视频切片最好覆盖白天、黄昏、夜间三个时段。运行一遍完整管线后统计“模型检出三轮车但实际没有的目标数”和“实际有三轮车但模型没检出的目标数”分别对应误报率和漏报率。这两个数字比 mAP 更重要因为直接决定业务方的信任度。测试时要特别关注两种翻车情况镜头抖动造成的框闪烁以及夜间车灯导致的误检。如果夜间误报多最简单的调整是提高conf-thres到 0.55代价是会牺牲一部分遮挡目标的召回。这个取舍没有标准答案只能按实际场景反复试。6.3 一个长期迭代的小习惯新旧模型对比压测我自己的习惯是每次替换模型权重前把旧模型和新模型在同一个视频切片上跑一遍输出检测框到文件里再逐帧对比差异。如果新模型只是 mAP 高了 2 个点但误报率也高了 1 个点我不会轻易替换只有当阈值调整后整体误报漏报都有改善才上设备。这个习惯帮我挡掉过至少三次上线后返工——有一次新模型在夜间把路灯杆认成了三轮车测试 mAP 比旧模型高 3 个点但实际误报率翻了一倍。模型迭代最忌讳只看训练指标不看业务指标。这套方案的投入产出比核心就在数据清洗和后处理规则的打磨上。yolov5 本身是成熟工具难的是让模型在具体场景里稳定工作。先把 VOC 转 YOLO 的转换脚本和数据校验流程跑通模型训练用默认参数起步再根据压测结果逐步收紧置信度和告警阈值这个顺序能帮你把返工成本控制在最低。希望帮到你。本文还有配套的精品资源点击获取
返回列表