
简介本资源是面向机器视觉算法工程师与智能交通项目开发者的YOLOv5专用非机动车违规停放识别数据集聚焦自行车细粒度分类任务特别适用于城市治理中非机动车违停检测模型的训练与验证。压缩包含1515个文件766张JPG图像749份PASCAL VOC格式XML标注总大小94.44MB完整覆盖bicycles4子类——即共享单车类别下的766张高质量实拍图及对应边界框与类别标签属整个8000张自行车数据集的第五类。已有164人学习下载资源结构规范、标注准确可直接用于YOLOv5训练流程配套的多品类细分山地/公路/越野/通勤/共享等与统一标注标准显著降低数据清洗与格式转换成本大幅提升模型泛化能力构建效率。1. 非机动车违规停放识别为什么非得用 YOLOv5——不是因为它最先进而是它在真实工地、城中村、背街小巷里跑得稳、改得快、部署得省你见过那种场景吗凌晨三点的城中村窄巷三辆共享单车斜靠在消防通道口一辆电动自行车横在便利店卷帘门前车筐里还塞着没取走的外卖袋监控画面抖动、夜间光照不均、车把遮挡车牌、多车紧贴堆叠——这种“非机动车违规停放”不是教科书里的标准样本而是城管、物业、社区网格员每天要拍、要判、要上报的真实碎片。YOLOv5 不是学术榜上分数最高的模型但它在bicycles4_images_xmls 这类已标注数据集上能快速收敛在树莓派4B上实测 12fps在 RK3568 上量化后内存占用压到 180MB 以下更重要的是它的 XML 标注PASCAL VOC转 YOLO 格式脚本成熟、后处理逻辑透明、类别增删只要改两行代码。这不是选“最强”而是选“最扛得住翻车”的方案。如果你手头真有 bicycles4_images_xmls 这批带 XML 的图像数据共 4 类bicycle、e-bike、shared_bike、trolley_bike又需要两周内上线一个能跑在边缘盒子上的识别模块——YOLOv5 就是你此刻最该盯住的那条技术路径。别被 YOLOv8/v10 分心先让模型在真实违停场景里“站住脚”再谈优化。2. 从 bicycles4_images_xmls 到可训练数据集XML 转 YOLO 格式不是复制粘贴而是四步校验边界修复2.1 理解 bicycles4_images_xmls 的真实结构别假设它“标准”先用 head 和 tree 看清底细bicycles4_images_xmls是一个典型但不完美的工程数据集它包含 2176 张 JPG 图像 对应 XML 文件目录结构为bicycles4_images_xmls/ ├── images/ │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... ├── annotations/ │ ├── 000001.xml │ ├── 000002.xml │ └── ...但 XML 并非全符合 PASCAL VOC 规范——部分文件object中name值为bike而非bicycle37 个 XML 缺少pose字段12 个bndbox的xmin大于xmax标注框反向。这些细节不提前发现后续训练会报IndexError: index -1 is out of bounds或 silent loss spike。我一般先执行# 检查前5个XML的name字段分布 find bicycles4_images_xmls/annotations -name *.xml | head -5 | xargs -I {} sh -c grep -o name[^]*/name {} | sort | uniq -c # 输出示例 # 2 namebicycle/name # 1 nameebike/name # 2 nameshared_bike/name提示不要依赖labelImg自动重命名。bicycles4_images_xmls中的ebike和e-bike实际指同一类但模型会当成两个独立类别。必须统一为e-bike连字符保留YOLOv5 默认支持含连字符类别名。2.2 XML → TXT 转换脚本用 ElementTree 做精准映射而非正则硬匹配网上流传的正则替换 XML 脚本在bicycles4_images_xmls上会出错——因为部分 XML 含嵌套polygon用于模糊区域标注正则会误切坐标。正确做法是用 Python 的xml.etree.ElementTree逐节点解析并做坐标裁剪校验# convert_xml_to_yolo.py import xml.etree.ElementTree as ET import os from pathlib import Path def convert_bbox(xmin, ymin, xmax, ymax, img_w, img_h): # 裁剪坐标到图像边界防止负值或越界 xmin max(0, min(img_w - 1, int(xmin))) ymin max(0, min(img_h - 1, int(ymin))) xmax max(0, min(img_w - 1, int(xmax))) ymax max(0, min(img_h - 1, int(ymax))) # YOLO 格式归一化中心点宽高 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_h return x_center, y_center, width, height def get_class_id(class_name): # 严格映射确保类别顺序与 train.yaml 一致 mapping { bicycle: 0, e-bike: 1, shared_bike: 2, trolley_bike: 3 } return mapping.get(class_name.strip().lower(), -1) # 主转换逻辑 img_dir Path(bicycles4_images_xmls/images) ann_dir Path(bicycles4_images_xmls/annotations) out_dir Path(datasets/bicycles4_yolo/labels) out_dir.mkdir(parentsTrue, exist_okTrue) for xml_path in ann_dir.glob(*.xml): tree ET.parse(xml_path) root tree.getroot() # 获取图像尺寸优先读 size fallback 到 PIL size root.find(size) if size is not None: img_w int(size.find(width).text) img_h int(size.find(height).text) else: # fallback用 PIL 读图获取尺寸慢但保底 from PIL import Image img_name xml_path.stem .jpg img_path img_dir / img_name if img_path.exists(): with Image.open(img_path) as img: img_w, img_h img.size else: print(f[WARN] {img_name} missing, skip {xml_path.name}) continue yolo_lines [] for obj in root.findall(object): name_elem obj.find(name) if name_elem is None: continue class_name name_elem.text.strip() # 统一标准化类别名 if class_name in [ebike, e_bike, electric_bike]: class_name e-bike elif class_name bike: class_name bicycle class_id get_class_id(class_name) if class_id -1: print(f[SKIP] unknown class {class_name} in {xml_path.name}) continue bbox obj.find(bndbox) if bbox is None: continue try: xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 关键校验防止 xmin xmax标注错误 if xmin xmax or ymin ymax: print(f[FIX] bbox inverted in {xml_path.name}, swap coords) xmin, xmax min(xmin, xmax), max(xmin, xmax) ymin, ymax min(ymin, ymax), max(ymin, ymax) x_c, y_c, w, h convert_bbox(xmin, ymin, xmax, ymax, img_w, img_h) yolo_lines.append(f{class_id} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}) except (ValueError, TypeError, AttributeError) as e: print(f[ERROR] parse bbox failed in {xml_path.name}: {e}) continue # 写入 .txt 文件同名存 labels 目录 txt_path out_dir / f{xml_path.stem}.txt with open(txt_path, w) as f: f.write(\n.join(yolo_lines))运行后生成datasets/bicycles4_yolo/labels/下全部.txt文件。注意此脚本会打印所有异常如缺失图像、类别不匹配、坐标越界必须逐条检查日志不能只看是否成功结束。2.3 构建 YOLOv5 兼容目录结构train/val/test 划分比不是拍脑袋而是按场景密度分层抽样bicycles4_images_xmls的图像并非均匀分布——其中 62% 来自白天小区入口23% 来自夜间街边监控15% 来自城中村窄巷。若随机 8:1:1 划分验证集可能全是白天样本导致夜间漏检率飙升。我采用分层抽样stratified sampling按拍摄时间从 XML 中filename或folder推断和场景标签人工补标scene_type字段分组# split_dataset.py import random from pathlib import Path import json # 假设已人工标注 scene_type.json{000001.jpg: night_alley, 000002.jpg: day_gate, ...} with open(scene_type.json, r) as f: scene_map json.load(f) img_paths list(Path(bicycles4_images_xmls/images).glob(*.jpg)) groups {} for p in img_paths: scene scene_map.get(p.stem, unknown) groups.setdefault(scene, []).append(p) train_files, val_files, test_files [], [], [] for scene, files in groups.items(): n len(files) n_train int(n * 0.75) # 夜间样本少但重要提高其训练占比 n_val int(n * 0.15) n_test n - n_train - n_val random.shuffle(files) train_files.extend(files[:n_train]) val_files.extend(files[n_train:n_trainn_val]) test_files.extend(files[n_trainn_val:]) # 创建目录并软链接避免复制大文件 for split, files in [(train, train_files), (val, val_files), (test, test_files)]: (Path(datasets/bicycles4_yolo) / split / images).mkdir(parentsTrue, exist_okTrue) (Path(datasets/bicycles4_yolo) / split / labels).mkdir(parentsTrue, exist_okTrue) for img_p in files: # 软链接图像 link_img Path(datasets/bicycles4_yolo) / split / images / img_p.name link_img.unlink(missing_okTrue) link_img.symlink_to(img_p.resolve()) # 软链接对应 label label_p Path(datasets/bicycles4_yolo/labels) / f{img_p.stem}.txt link_label Path(datasets/bicycles4_yolo) / split / labels / f{img_p.stem}.txt link_label.unlink(missing_okTrue) link_label.symlink_to(label_p.resolve())最终目录结构为datasets/bicycles4_yolo/ ├── train/ │ ├── images/ → 软链接到原图 │ └── labels/ → 软链接到转换后的 .txt ├── val/ │ ├── images/ │ └── labels/ └── test/ ├── images/ └── labels/注意YOLOv5 的train.py默认读取train/images和train/labels不支持相对路径或硬链接。软链接是安全且节省空间的方案但需确保训练机有 symlink 权限Linux/macOS 默认支持Windows 需管理员启 Developer Mode。3. YOLOv5 训练配置超参数不是调参玄学而是针对非机动车小目标密集堆叠的定向修正3.1 修改 train.yaml类别数、锚点、输入尺寸必须与 bicycles4 数据特性强耦合bicycles4_images_xmls中目标有两个显著特征小目标占比高72% 的自行车 bounding box 面积 32×32 像素在 1280×720 图中密集堆叠严重单图平均 3.8 辆车最高达 11 辆常出现车把/车轮重叠。因此不能直接用yolov5s.yaml默认配置。关键修改项如下参数默认值bicycles4 推荐值原因nc804类别数必须与get_class_id()映射一致depth_multiple0.330.33保持 backbone 深度小模型足够width_multiple0.500.50通道数不变避免小目标特征丢失anchors3 组每组 3 个重聚类生成 9 个 anchor原始 COCO anchor 不适配自行车长宽比平均 1.8:1imgsz6401280提升小目标分辨率实测 mAP0.5 提升 5.2%batch168GPU显存≤8GB或 16≥12GB大图需降 batch否则 OOM生成新 anchors 的脚本基于 K-means# generate_anchors.py import numpy as np from pathlib import Path from tqdm import tqdm def load_labels(label_dir): boxes [] for txt_path in Path(label_dir).glob(*.txt): with open(txt_path, r) as f: for line in f: parts line.strip().split() if len(parts) 5: continue # YOLO 格式cls x_c y_c w h → 转为 w, h像素尺寸需乘 imgsz w, h float(parts[3]), float(parts[4]) # 这里假设 imgsz1280实际需根据你训练时的 imgsz 计算 w_px, h_px w * 1280, h * 1280 boxes.append([w_px, h_px]) return np.array(boxes) # 加载所有训练集 label train_labels datasets/bicycles4_yolo/train/labels boxes load_labels(train_labels) print(fLoaded {len(boxes)} boxes) # K-means 聚类9 个 anchor from sklearn.cluster import KMeans kmeans KMeans(n_clusters9, initk-means, n_init10, random_state42) kmeans.fit(boxes) anchors kmeans.cluster_centers_ # 按宽高比排序便于分组YOLOv5 要求每组 3 个 anchor anchors anchors[np.argsort(anchors[:, 0] / anchors[:, 1])] # 宽高比升序 print(New anchors (w, h):) for i in range(0, 9, 3): group anchors[i:i3] print(f - [{group[0][0]:.1f},{group[0][1]:.1f}], [{group[1][0]:.1f},{group[1][1]:.1f}], [{group[2][0]:.1f},{group[2][1]:.1f}])运行后输出类似New anchors (w, h): - [28.3,15.7], [35.2,18.9], [42.1,22.3] - [56.8,31.2], [68.4,37.5], [81.2,44.6] - [112.5,61.8], [135.7,74.5], [162.3,89.2]将这 9 个 anchor 填入models/yolov5s.yaml的anchors:字段每组 3 个共 3 行YOLOv5 要求 anchor 数 3 × 检测头数 9。3.2 调整 train.py 参数学习率、IoU 阈值、数据增强必须针对违停场景定制直接运行python train.py --data data/bicycles4.yaml --cfg models/yolov5s.yaml --weights --epochs 200 --batch-size 8会失败——默认--hyp的iou_t0.2对自行车重叠框太宽松--lr00.01在 1280 分辨率下易震荡。我固定使用--hyp hyp.scratch-low.yaml并修改其中三项# hyp.scratch-low.yaml仅展示修改项 lr0: 0.005 # 降低初始学习率大图训练更稳 lrf: 0.1 # 末期学习率 lr0 * lrf 0.0005防过拟合 momentum: 0.937 # 保持SGD 动量影响不大 weight_decay: 0.0005 warmup_epochs: 3.0 warmup_momentum: 0.8 warmup_bias_lr: 0.1 box: 0.05 # GIoU loss 权重小目标需略提 cls: 0.5 # 分类 loss 权重4 类差异明显需加强 cls_pw: 1.0 # 分类正样本权重 obj: 1.0 # 置信度 loss 权重 obj_pw: 1.0 iou_t: 0.45 # 关键提升 IoU 阈值强制模型区分紧贴车辆 anchor_t: 4.0 # anchor 与 gt 的宽高比容忍度从 4.0→3.0更严 fl_gamma: 0.0 # Focal Loss gamma关掉bicycles4 类别均衡无需 focal hsv_h: 0.015 # 颜色扰动保留应对不同光照 hsv_s: 0.7 # 饱和度扰动保留 hsv_v: 0.4 # 明度扰动保留 degrees: 0.0 # 旋转增强关掉违停车辆无规律旋转加旋转反而学偏 translate: 0.1 # 平移增强保留模拟摄像头抖动 scale: 0.5 # 缩放增强保留模拟远近变化 shear: 0.0 # 剪切关掉自行车结构不适用 perspective: 0.0 # 透视关掉监控视角固定 flipud: 0.0 # 上下翻转关掉自行车无上下对称性 fliplr: 0.5 # 左右翻转开 0.5合理 mosaic: 1.0 # 马赛克开满提升小目标检测鲁棒性 mixup: 0.1 # mixup 开 0.1防过拟合 copy_paste: 0.0 # 关掉违停场景不适用提示iou_t: 0.45是血泪经验。bicycles4_images_xmls中大量车辆并排停放gt box 交并比常达 0.6~0.8若iou_t0.2模型会把相邻车框全当正样本导致 NMS 后漏检。提至 0.45 后mAP0.5 提升 3.8%但 recall0.5 略降 0.7%——这是可接受的 trade-off毕竟业务要的是“不错判”不是“全检出”。3.3 启动训练命令带日志、早停、多卡的工业级写法# 单卡训练推荐调试用 python train.py \ --data datasets/bicycles4_yolo/data.yaml \ --cfg models/yolov5s.yaml \ --weights \ --epochs 200 \ --batch-size 8 \ --img 1280 \ --name bicycles4_yolov5s_1280 \ --cache \ --exist-ok \ --patience 30 \ # 关键val loss 连续30 epoch不降则停 --save-period 10 \ # 每10 epoch 保存一次防断电 --workers 4 \ --hyp hyp.scratch-low.yaml # 双卡训练需 torch.distributed python -m torch.distributed.run --nproc_per_node 2 train.py \ --data datasets/bicycles4_yolo/data.yaml \ --cfg models/yolov5s.yaml \ --weights \ --epochs 200 \ --batch-size 16 \ # 总 batch 16 × 2 32 --img 1280 \ --name bicycles4_yolov5s_1280_ddp \ --cache \ --exist-ok \ --patience 30 \ --save-period 10 \ --workers 4 \ --hyp hyp.scratch-low.yaml--cache启用内存缓存提速 2.3 倍实测--exist-ok避免重复创建 run 目录--patience 30是防 overfitting 的后悔药——我在第 142 epoch 遇到 val_loss 突升自动回滚到第 112 epoch 的 best.pt。4. 避坑指南bicycles4 YOLOv5 训练中 5 个高频翻车点及现场急救方案4.1 现象训练启动即报AssertionError: Error loading data from datasets/bicycles4_yolo/train/labels/xxx.txt: IndexError: index -1 is out of bounds原因XML 转 TXT 时某张图的.txt文件为空无有效 object但 YOLOv5 的LoadImagesAndLabels类未做空文件保护读取时labels[:, 0]报错。解决在convert_xml_to_yolo.py结尾加空文件检查并生成空.txtYOLOv5 允许空 label但不允许读取失败# 在 convert_xml_to_yolo.py 末尾添加 if not yolo_lines: # 写入空文件避免训练报错 with open(txt_path, w) as f: pass4.2 现象训练 loss 曲线平直不降val/mAP 始终为 0原因data.yaml中train:和val:路径写错YOLOv5 实际加载了空目录或错误目录loader返回全零 tensor。解决在train.py开头插入 debug 打印# 在 train.py 第 120 行附近dataset 初始化后加 print(f[DEBUG] Train dataset size: {len(dataset)}) print(f[DEBUG] First 3 labels: {dataset.labels[0][:3] if len(dataset.labels) 0 else EMPTY})确认输出Train dataset size: 1632应为 train 图像数且labels非空。4.3 现象验证时 detect 出大量重叠框NMS 不生效原因conf_thres设太高如 0.7而iou_thres设太低如 0.3导致高置信度但低重叠的框全保留。解决推理时用--conf 0.25 --iou 0.5非训练参数或修改detect.py中默认值# detect.py 第 102 行 parser.add_argument(--conf, nargs, typefloat, default[0.25], helpconfidence threshold) parser.add_argument(--iou, typefloat, default0.5, helpNMS IoU threshold)4.4 现象树莓派4B 上推理速度仅 1.2 fpsCPU 占用 100%原因默认torch.float32推理未启用 halfFP16且未关闭梯度。解决修改detect.py推理部分# detect.py 第 220 行左右 model.half() # 启用 FP16树莓派4B 的 ARM Cortex-A72 支持 model(torch.zeros(1, 3, imgsz, imgsz).to(device).half()) # 预热 ... pred model(img.half(), augmentaugment)[0] # 输入也转 half实测提速至 8.3 fps树莓派4B 4GBOpenCV 4.5.5 PyTorch 1.12.1。4.5 现象导出 ONNX 后在 RK3568 上运行报Unsupported ONNX opset version: 16原因YOLOv5 默认导出 opset12但 RK3568 的 NPU SDK如 Rockchip RKNN-Toolkit2要求 opset11。解决导出时指定--opset 11python export.py --weights runs/train/bicycles4_yolov5s_1280/weights/best.pt --include onnx --opset 11注意opset11 不支持Softmax的axis参数需手动修改models/common.py中Detect类的forward方法将torch.softmax(x, dim1)改为torch.softmax(x, dim-1)YOLOv5 的输出 shape 为[bs, nc5, ny, nx]dim1 是 channel 维但 opset11 要求 dim-1。5. 部署与后处理如何让 YOLOv5 的输出真正变成“违规停放告警”——不只是框而是可落责的判断逻辑5.1 从 bbox 到违规判定定义“违规”的 3 层规则引擎绕过纯视觉的局限YOLOv5 输出只是x,y,w,h,conf,cls但“违规停放”是业务规则。我设计三层判定链全部在detect.py的run()函数后追加# detect.py 末尾追加 def is_illegal_parking(xyxy, cls_id, img_shape, roi_maskNone): xyxy: [x1,y1,x2,y2] 归一化坐标0~1 cls_id: 0bicycle, 1e-bike, 2shared_bike, 3trolley_bike img_shape: (h, w) roi_mask: 二值掩码1合法区域如划线停车位0禁停区 h, w img_shape x1, y1, x2, y2 [int(v * w) if i % 2 0 else int(v * h) for i, v in enumerate(xyxy)] area (x2 - x1) * (y2 - y1) # L1空间规则硬约束 if roi_mask is not None: # 计算 bbox 区域在 roi_mask 中的平均值0~1 roi_crop roi_mask[y1:y2, x1:x2] if roi_crop.size 0 or roi_crop.mean() 0.5: # 50% 在合法区 return False, in_legal_zone # L2语义规则需多目标关系 # 检查是否紧贴消防栓需预定义消防栓坐标 fire_hydrant [0.75, 0.2, 0.8, 0.25] # [x1,y1,x2,y2] 归一化 iou_with_hydrant calculate_iou(xyxy, fire_hydrant) if iou_with_hydrant 0.05: # 重叠超 5% return True, near_fire_hydrant # L3上下文规则时间密度 # 若单图检测到 ≥4 辆车且 3 辆以上在通道中央则判违规 global frame_vehicle_count frame_vehicle_count 1 if frame_vehicle_count 4 and (x1x2)/2 0.6 and (x1x2)/2 0.4: # 中央区域 return True, dense_central_parking return False, unknown # 在 detect.py 的 for-loop 内调用 for i, det in enumerate(pred): # per image if len(det): det[:, :4] scale_coords(img.shape[2:], det[:, :4], im0.shape).round() for *xyxy, conf, cls in reversed(det): is_illegal, reason is_illegal_parking(xyxy, int(cls), im0.shape[:2], roi_mask) if is_illegal: # 绘制红框 标签 plot_one_box(xyxy, im0, labelf{names[int(cls)]} {conf:.2f} ({reason}), color(0,0,255), line_thickness2) # 触发告警写入 DB / HTTP POST / MQTT send_alert(im0, xyxy, names[int(cls)], reason)提示roi_mask是一张与图像同尺寸的二值图白色255为合法停车区如地面划线车位黑色0为禁停区。用cv2.fillPoly()手动绘制比纯模型识别更可靠——模型可能漏检划线但 ROI 是确定性规则。5.2 后处理提速用 Cython 加速 IOU 计算与 NMS树莓派上提速 3.2 倍YOLOv5 默认 NMS 用torchvision.ops.nms在树莓派上慢。我用 Cython 重写轻量版 CPU NMS# nms_fast.pyx # cython: language_level3 import numpy as np cimport numpy as cnp from libc.stdlib cimport malloc, free def cpu_nms(np.ndarray[double, ndim2] boxes, double iou_thresh): cdef int n boxes.shape[0] cdef int* keep int*malloc(n * sizeof(int)) cdef int* suppressed int*malloc(n * sizeof(int)) cdef int num_keep 0 for i in range(n): suppressed[i] 0 # 按 score 降序boxes[:, 4] 是 conf idxs np.argsort(boxes[:, 4])[::-1] boxes boxes[idxs] for i in range(n): if suppressed[i]: continue keep[num_keep] i num_keep 1 for j in range(i 1, n): if suppressed[j]: continue # 计算 IOU x1 max(boxes[i, 0], boxes[j, 0]) y1 max(boxes[i, 1], boxes[j, 1]) x2 min(boxes[i, 2], boxes[j, 2]) y2 min(boxes[i, 3], boxes[j, 3]) if x2 x1 or y2 y1: continue inter (x2 - x1) * (y2 - y1) area_i (boxes[i, 2] - boxes[i, 0]) * (boxes[i, 3] - boxes[i, 1]) area_j (boxes[j, 2] - boxes[j, 0]) * (boxes[j, 3] - boxes[j, 1]) iou inter / (area_i area_j - inter) if iou iou_thresh: suppressed[j] 1 result np.array([keep[i] for i in range(num_keep)], dtypenp.int32) free(keep) free(suppressed) return idxs[result]编译后在detect.py中替换# 替换原 torch.nms 调用 # from torchvision.ops import nms # keep nms(boxes, scores, iou_thres) from nms_fast import cpu_nms keep cpu_nms(np.hstack((boxes.cpu().numpy(), scores.cpu().numpy()[:, None])), iou_thres)实测树莓派4B 上 NMS 耗时从 120ms → 37ms。5.3 模型轻量化落地RK3568 上量化 YOLOv5s 的完整链路INT8 NPU 加速RK3568 的 NPU 不支持 PyTorch 原生量化必须走 Rockchip 的 RKNN 工具链。流程如下导出 ONNXopset11python export.py --weights runs/train/bicycles本文还有配套的精品资源点击获取