简介这份共享单车数据集面向计算机视觉学习者与目标检测开发者采用VOC格式组织可直接用于训练和验证单车识别模型适合课程设计、毕业项目或算法练手等场景。压缩包共272个文件包含136张jpg实拍图像与136个xml标注文件两者一一对应xml由labelImg工具生成记录了单车目标的边界框与类别信息整体约90.08MB规模适中便于快速加载与调试。目前已有518人学习下载说明该数据集在实际项目中有一定参考价值。图像覆盖多种城市街景与拍摄角度标注规范统一读者拿到后可直接接入YOLO、Faster R-CNN等主流检测框架省去自行采集与标注的时间成本也能用于对比不同模型在共享单车场景下的精度表现或作为数据增强与迁移学习的实验素材。1. 共享单车数据集VOC格式从压缩包到可训练模型的真实路径你拿到一个名为“共享单车数据集VOC格式.rar”的压缩包解压后看到的是 JPEGImages、Annotations、ImageSets 三个文件夹外加可能存在的 SegmentationClass 和 SegmentationObject。这不是一个“随便跑跑”的玩具数据集而是一份已经按 PASCAL VOC 标准组织好的目标检测数据。共享单车场景的难点在于车辆密集堆叠、遮挡严重、光照变化剧烈、拍摄角度多样而且“单车”这个类别在不同标注者眼里边界并不一致——有的把车筐算进去有的只框车轮和车架。这份数据集的价值在于它省去了你从零标注的几百个小时但代价是你必须理解 VOC 格式的每一个字段否则训练时 loss 不降、mAP 卡在 0.3 上不去你还以为是模型不行。适合谁用做城市管理、园区车辆调度、违停检测的算法工程师以及想拿真实场景练手目标检测的学生和开发者。下面我从格式拆解、转换脚本、训练配置到避坑把这条路走一遍。2. VOC格式拆解三个文件夹里到底装了什么2.1 Annotations里的XML不是给你看的是给解析器吃的解压后第一件事不是急着写 DataLoader而是打开 Annotations 里任意一个 XML 文件。你会看到size里有 width、height、depthobject里有 name、pose、truncated、difficult 和 bndbox。共享单车数据集里 name 通常是 “bicycle” 或 “bike”但不同来源可能混用这是第一个要统一的地方。bndbox 的 xmin、ymin、xmax、ymax 是 1-based 像素坐标不是 0-based很多自己写解析器的人在这里翻车——用 PIL 读图后直接拿坐标去裁剪结果整体偏移一个像素小目标上就是致命的。import xml.etree.ElementTree as ET from pathlib import Path def parse_voc_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) objects [] for obj in root.findall(object): name obj.find(name).text.strip().lower() # 统一类别名共享单车场景常见混用 if name in (bike, bicycle, shared_bike): name bicycle bbox obj.find(bndbox) xmin int(float(bbox.find(xmin).text)) - 1 # 转 0-based ymin int(float(bbox.find(ymin).text)) - 1 xmax int(float(bbox.find(xmax).text)) ymax int(float(bbox.find(ymax).text)) # 过滤越界框 xmin, ymin max(0, xmin), max(0, ymin) xmax, ymax min(w, xmax), min(h, ymax) if xmax xmin or ymax ymin: continue objects.append({name: name, bbox: [xmin, ymin, xmax, ymax]}) return {width: w, height: h, objects: objects}这段代码做了三件事坐标转 0-based、类别名归一化、越界框过滤。参数上唯一需要你根据自己数据集调整的是类别名映射表。如果 XML 里还有difficult标记为 1 的框建议在训练时保留但在评估时忽略VOC 官方评测就是这么做的。2.2 ImageSets/Main里的txt文件决定你的实验可复现性ImageSets/Main 下通常有 train.txt、val.txt、trainval.txt、test.txt每行是一个不含扩展名的图片 ID。共享单车数据集如果是从视频抽帧来的相邻帧高度相似随机划分会导致验证集泄漏——模型在验证集上 mAP 虚高上线就崩。正确做法是按拍摄片段或时间戳划分而不是按帧随机打散。如果你拿到的压缩包里只有 trainval.txt 和 test.txt那就自己从 trainval 里切 10% 做 val但切之前先按图片文件名里的时间前缀排序每隔 10 张取 1 张进 val。# 假设图片文件名格式为 siteA_20240101_120000_frame001.jpg ls JPEGImages/ | sort all.txt awk NR%100 all.txt val.txt grep -v -f val.txt all.txt train.txt这个划分策略保证同一时间段、同一地点的图片不会同时出现在训练和验证里。参数说明NR%100 表示每 10 张取 1 张你可以根据数据集大小调整但 val 至少要有 200 张图否则 mAP 波动太大没有参考意义。2.3 JPEGImages的命名和尺寸藏着训练稳定性的线索共享单车数据集的图片尺寸往往不统一有的 1920x1080有的 1280x720甚至混入手机竖拍图。VOC 格式本身不要求统一尺寸但你的训练框架要求。常见做法是统一 resize 到 640x640 或 416x416但共享单车场景里小目标多——远处一排车可能只占 20x20 像素直接 resize 到 416 会让它们变成 5x5特征几乎消失。我一般会先统计 bndbox 的宽高分布如果 80% 的框小于 32x32就把输入尺寸定在 800 以上或者用 mosaic 增强时保留小图不缩放。import numpy as np from pathlib import Path from parse_voc_xml import parse_voc_xml # 前面定义的函数 areas [] for xml_file in Path(Annotations).glob(*.xml): ann parse_voc_xml(xml_file) for obj in ann[objects]: w obj[bbox][2] - obj[bbox][0] h obj[bbox][3] - obj[bbox][1] areas.append(w * h) areas np.array(areas) print(f框面积中位数: {np.median(areas):.0f}) print(f小于32x32的框占比: {(areas 1024).mean()*100:.1f}%)如果小于 32x32 的框占比超过 40%你的数据增强策略就要保守随机缩放范围控制在 0.8 到 1.2 之间不要用 0.5 到 1.5 这种激进设置否则小目标在训练中频繁被缩到不可见模型会学会忽略它们。3. 从VOC到YOLO/COCO转换脚本与四个边界坑3.1 转YOLO格式归一化坐标和类别索引的映射YOLO 格式要求每张图对应一个 txt每行是class_id x_center y_center width height全部归一化到 0 到 1。共享单车数据集通常只有 bicycle 一个类class_id 就是 0。但如果你后续要加“电动车”“摩托车”类别就要提前规划好索引表不要等训练到一半再改否则所有标签都要重做。import os from pathlib import Path from parse_voc_xml import parse_voc_xml CLASS_MAP {bicycle: 0} # 扩展时在这里加 ebike: 1 def voc_to_yolo(xml_dir, img_dir, out_dir): os.makedirs(out_dir, exist_okTrue) for xml_file in Path(xml_dir).glob(*.xml): ann parse_voc_xml(xml_file) w, h ann[width], ann[height] lines [] for obj in ann[objects]: if obj[name] not in CLASS_MAP: continue xmin, ymin, xmax, ymax obj[bbox] xc (xmin xmax) / 2.0 / w yc (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h # 裁剪到 [0,1]防止浮点误差越界 xc, yc min(max(xc, 0), 1), min(max(yc, 0), 1) bw, bh min(max(bw, 0), 1), min(max(bh, 0), 1) lines.append(f{CLASS_MAP[obj[name]]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) out_file Path(out_dir) / (xml_file.stem .txt) out_file.write_text(\n.join(lines))逻辑说明归一化用图片实际宽高不是网络输入尺寸。参数上.6f保留 6 位小数足够YOLO 官方解析器对精度不敏感。注意空标签文件也要生成否则训练时该图会被当成负样本而实际上它可能只是没有 bicycle 类别不代表没有目标。3.2 转COCO格式json结构里的id连续性陷阱COCO 格式用单个 json 文件images、annotations、categories 三个数组。共享单车数据集转 COCO 时最容易翻车的是 id 不连续——你删了几张坏图image_id 就断了pycocotools 评估时直接报错。正确做法是重新分配连续 id并建立旧 id 到新 id 的映射。import json from pathlib import Path from parse_voc_xml import parse_voc_xml def voc_to_coco(xml_dir, img_dir, out_json): images, annotations [], [] ann_id 1 for idx, xml_file in enumerate(sorted(Path(xml_dir).glob(*.xml))): ann parse_voc_xml(xml_file) img_name xml_file.stem .jpg images.append({ id: idx, file_name: img_name, width: ann[width], height: ann[height] }) for obj in ann[objects]: if obj[name] ! bicycle: continue xmin, ymin, xmax, ymax obj[bbox] annotations.append({ id: ann_id, image_id: idx, category_id: 1, bbox: [xmin, ymin, xmax - xmin, ymax - ymin], # COCO 是 xywh area: (xmax - xmin) * (ymax - ymin), iscrowd: 0 }) ann_id 1 coco { images: images, annotations: annotations, categories: [{id: 1, name: bicycle, supercategory: vehicle}] } Path(out_json).write_text(json.dumps(coco))参数说明COCO 的 bbox 是[x, y, width, height]不是[xmin, ymin, xmax, ymax]这是最常见的转换错误。area 用于评估时区分小/中/大目标必须准确计算。3.3 四个边界坑空标签、截断框、difficult标记、类别混用第一个坑空标签文件。VOC 里有些图没有任何 object转 YOLO 后生成空 txt训练时被当作背景。但如果这张图实际上有单车只是没标模型会学到“这种纹理背景”降低召回。解决方法是人工抽查空标签图确认是真负样本还是漏标。第二个坑截断框。XML 里truncated1表示目标被图像边缘截断bndbox 可能超出图像边界。转 YOLO 时如果不裁剪归一化坐标会小于 0 或大于 1训练时 loss 直接 NaN。前面代码里的min(max(...))就是干这个的。第三个坑difficult 标记。VOC 评估时 difficult1 的框不计入 mAP但训练时保留。如果你用 COCO 评估器需要把 difficult 映射为 iscrowd1 或直接忽略否则指标不可比。第四个坑类别混用。共享单车数据集里可能同时出现 “bicycle” 和 “bike”甚至 “shared_bike”。不统一的话模型会学出两个类但实际是同一个东西mAP 被稀释。统一成 “bicycle” 是最安全的。3.4 用五十行代码验证转换正确性转换完不要直接开训先做可视化验证。随机抽 20 张图把转换后的框画上去人眼过一遍。import cv2 import random from pathlib import Path def visualize_yolo(img_dir, label_dir, num20): imgs list(Path(img_dir).glob(*.jpg)) for img_path in random.sample(imgs, min(num, len(imgs))): img cv2.imread(str(img_path)) h, w img.shape[:2] label_path Path(label_dir) / (img_path.stem .txt) if not label_path.exists(): continue for line in label_path.read_text().strip().split(\n): if not line: continue _, 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.imshow(check, img) cv2.waitKey(0)如果框的位置明显偏移、大小不对回去检查归一化时用的宽高是不是图片实际宽高而不是网络输入尺寸。这个验证步骤花 10 分钟能省你 10 小时的 debug。4. 训练配置共享单车场景的anchor和增强参数怎么设4.1 anchor聚类用k-means跑一遍你的bndboxYOLO 的默认 anchor 是在 COCO 上聚类的共享单车场景的框宽高比差异很大——正面骑行的车框接近正方形侧面停放的框宽高比可能到 3:1。直接套默认 anchor召回率会掉 10 个点以上。用 k-means 在自己的数据上跑一遍k 取 9。import numpy as np from pathlib import Path from parse_voc_xml import parse_voc_xml def load_boxes(xml_dir): boxes [] for xml_file in Path(xml_dir).glob(*.xml): ann parse_voc_xml(xml_file) for obj in ann[objects]: if obj[name] ! bicycle: continue xmin, ymin, xmax, ymax obj[bbox] boxes.append([xmax - xmin, ymax - ymin]) return np.array(boxes) def kmeans_anchors(boxes, k9, iters100): np.random.seed(42) centers boxes[np.random.choice(len(boxes), k, replaceFalse)] for _ in range(iters): distances np.array([np.abs(boxes - c).sum(axis1) for c in centers]) labels distances.argmin(axis0) for i in range(k): if (labels i).any(): centers[i] boxes[labels i].mean(axis0) return centers boxes load_boxes(Annotations) anchors kmeans_anchors(boxes) print(推荐anchor宽高:) for a in sorted(anchors.tolist(), keylambda x: x[0]*x[1]): print(f {a[0]:.0f}x{a[1]:.0f})参数说明距离用 L1 而不是 L2因为 L2 对大框惩罚过重聚类结果偏向大目标。迭代 100 次足够收敛。拿到 anchor 后按面积从小到大排序填入模型配置。4.2 数据增强mosaic和mixup在密集场景下的取舍共享单车场景车辆密集mosaic 增强把 4 张图拼成 1 张能显著提升小目标检测。但 mosaic 会让框的分布更密集如果原始数据里车与车之间遮挡就严重mosaic 后可能一格里挤了 20 辆车模型学到的特征过于混乱。我的经验是mosaic 概率设 0.5 而不是默认的 1.0mixup 直接关掉因为 mixup 把两张图线性叠加单车的轮廓变得模糊对遮挡场景没有帮助反而引入噪声。# 训练配置片段 augment: mosaic: 0.5 mixup: 0.0 hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.4 degrees: 0.0 # 共享单车通常水平拍摄不旋转 translate: 0.1 scale: 0.3 # 小目标多缩放范围收窄 shear: 0.0 flipud: 0.0 fliplr: 0.5参数说明degrees 设 0 是因为旋转后单车变成倾斜框而 VOC 标注是水平框旋转会引入大量背景。scale 设 0.3 而不是默认 0.5防止小目标被缩到消失。hsv_v 设 0.4 模拟不同光照共享单车在树荫和阳光下外观差异大。4.3 学习率和batch size小数据集不要用大学习率共享单车数据集通常几千张图属于小数据集。学习率设 0.01 起步用 cosine 衰减到 0.0001。batch size 根据显存来但不要小于 8否则 BN 层统计量不准。如果显存不够用梯度累积模拟大 batch。# 优化器配置 optimizer dict( typeSGD, lr0.01, momentum0.937, weight_decay0.0005, nesterovTrue ) lr_scheduler dict( typeCosineAnnealingLR, T_max300, # 总 epoch 数 eta_min0.0001 )参数说明momentum 0.937 是 YOLO 系列的惯用值weight_decay 0.0005 防止过拟合。T_max 等于总 epoch 数不要设成一半否则学习率提前降到最低模型还没收敛。4.4 验证指标mAP0.5够用但要看小目标AP训练日志里 mAP0.5 到 0.7 以上算及格但共享单车场景更关键的是小目标 AP。如果小目标 AP 只有 0.2说明模型对远处车辆漏检严重。用 COCO 评估器输出 AP_small、AP_medium、AP_large针对性调整 anchor 和输入尺寸。# 用 pycocotools 评估 python -c from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval coco_gt COCO(annotations/instances_val.json) coco_dt coco_gt.loadRes(predictions.json) e COCOeval(coco_gt, coco_dt, bbox) e.evaluate() e.accumulate() e.summarize() 输出里AP_small低于 0.15 就要警惕要么增大输入尺寸要么在 anchor 里增加小尺寸 anchor。5. 避坑与排查训练不收敛、mAP虚高、漏检严重5.1 loss震荡不降检查标签里的零宽高框现象训练前 10 个 epoch loss 从 10 降到 2然后开始震荡始终不降。原因标签里有 width 或 height 为 0 的框归一化后 bw 或 bh 为 0计算 IoU 时除零。解决在转换脚本里加过滤if xmax xmin or ymax ymin: continue。另外检查是否有框的坐标是浮点数带小数VOC 标准是整数但有些标注工具输出浮点转 YOLO 时归一化没问题但某些框架解析整数时截断导致零宽。5.2 验证集mAP虚高相邻帧泄漏现象验证集 mAP 0.85测试集只有 0.5。原因train 和 val 里有同一段视频的相邻帧模型记住了背景而不是目标。解决按视频片段划分同一片段的帧只出现在 train 或 val 之一。如果文件名没有片段信息用感知哈希去重把相似度高于 0.95 的图分到同一组。5.3 漏检远处小车输入尺寸和anchor不匹配现象近处单车检测正常远处一排车全部漏检。原因输入尺寸 416远处车只有 10x10 像素经过 5 次下采样后特征图上是 0.3 个像素anchor 最小尺寸 8x8 也匹配不上。解决输入尺寸提到 800anchor 里加 4x4 和 8x8 的小 anchor或者在特征金字塔的 P2 层加检测头。5.4 误检栏杆和树影负样本不足现象模型把栏杆、树影、地面反光误检为单车。原因训练集里这些负样本太少模型没学会区分。解决从空标签图里挖负样本或者用 hard negative mining——先用模型跑一遍训练集把误检置信度高于 0.5 的图挑出来人工确认后加入训练。5.5 类别不平衡单车和电动车混标现象数据集里 90% 是 bicycle10% 是 ebike模型对 ebike 几乎全漏。原因类别不平衡导致 loss 被 bicycle 主导。解决用 focal loss 或者对 ebike 类别加权权重设为 bicycle 数量的倒数比。如果 ebike 样本太少考虑合并为一个“两轮车”类别先保证召回。6. 进阶技巧用半自动标注把数据集扩三倍共享单车场景的数据集永远不够——新的停车区域、新的车型、新的光照条件。手动标 1000 张图要两天但用训练好的模型做半自动标注一天能标 5000 张。具体流程先用现有数据集训一个 baseline 模型mAP 到 0.6 以上即可用这个模型对未标注图片推理置信度阈值设 0.3输出预测框人工在标注工具里修正漏检和误检而不是从零画框。修正一张图的平均时间从 3 分钟降到 40 秒。# 半自动标注模型推理输出 VOC XML import cv2 from pathlib import Path from xml.etree.ElementTree import Element, SubElement, ElementTree def auto_annotate(model, img_dir, out_xml_dir, conf_thres0.3): Path(out_xml_dir).mkdir(exist_okTrue) for img_path in Path(img_dir).glob(*.jpg): img cv2.imread(str(img_path)) h, w img.shape[:2] results model(img)[0] # 假设用 ultralytics 接口 root Element(annotation) SubElement(root, folder).text VOC2007 SubElement(root, filename).text img_path.name size SubElement(root, size) SubElement(size, width).text str(w) SubElement(size, height).text str(h) SubElement(size, depth).text 3 for box in results.boxes: if box.conf conf_thres: continue x1, y1, x2, y2 box.xyxy[0].tolist() obj SubElement(root, object) SubElement(obj, name).text bicycle SubElement(obj, pose).text Unspecified SubElement(obj, truncated).text 0 SubElement(obj, difficult).text 0 bndbox SubElement(obj, bndbox) SubElement(bndbox, xmin).text str(int(x1)) SubElement(bndbox, ymin).text str(int(y1)) SubElement(bndbox, xmax).text str(int(x2)) SubElement(bndbox, ymax).text str(int(y2)) tree ElementTree(root) tree.write(Path(out_xml_dir) / (img_path.stem .xml))参数说明conf_thres 设 0.3 而不是 0.5是为了多召回一些候选框宁可让标注员删掉误检也不要漏掉真目标。推理时用 TTA多尺度测试能再提升 3 到 5 个点的召回但速度慢一倍根据你的标注预算权衡。半自动标注的迭代节奏第一轮用 1000 张标好的训 baseline标 3000 张新图第二轮用 4000 张训新模型再标 5000 张。三轮下来数据集到 1 万张mAP 通常能从 0.6 推到 0.75 以上。注意每轮都要保留 10% 的人工验证集防止模型在自动标注的噪声上过拟合。我自己的习惯是每次拿到新数据集先花 30 分钟写解析和可视化脚本把 20 张图的框画出来看一遍。这 30 分钟能发现 80% 的格式问题——坐标偏移、类别混用、空标签、截断框。很多人跳过这一步直接开训结果 loss 不降就换模型、调参数折腾两天才发现是标签里有个框的 xmax 小于 xmin。希望帮到你。本文还有配套的精品资源点击获取