
简介这份共享单车数据集采用VOC格式整理面向从事目标检测算法练习、课程设计或毕业设计的学生与开发者可用于训练和验证单车识别模型解决真实街景中共享单车标注样本不足的问题。压缩包共272个文件包含136张jpg实拍图像与136个xml标注文件两者一一对应xml由labelImg工具生成记录单车目标的边界框与类别信息可直接接入YOLO、Faster R-CNN等主流检测框架进行训练与评估。资源包整体约90.08MB体积适中便于本地存储与快速加载。目前已有518人学习下载具备一定的参考与复用价值。图像覆盖多角度、多距离的城市街景单车场景标注规范统一读者可据此完成数据预处理、模型训练、精度对比与可视化分析也可作为扩充自有数据集的补充样本适合入门目标检测或需要快速搭建实验基线的人群使用。1. 共享单车数据集VOC格式从标注文件到检测模型的落地路径手里有一份共享单车场景的 VOC 格式数据集压缩包名字很直白叫「共享单车数据集VOC格式.rar」。这类资源在目标检测任务里属于典型的「拿来就能训」的素材——图片、XML 标注、类别定义都按 PASCAL VOC 那套目录规范摆好了省掉自己从零标注的功夫。它解决的核心问题不是「有没有数据」而是「数据能不能直接喂给 YOLO、Faster R-CNN 这类框架」。适合谁正在做城市管理、车辆调度、违停识别相关 Demo 的开发者或者想拿真实街景数据跑通训练流程的学生。但 VOC 格式本身有它的脾气目录结构、坐标越界、类别不均衡这几个坑不提前摸清楚训练脚本跑起来就是一堆报错。2. VOC 目录结构与标注文件解析先看懂再动手2.1 标准 VOC 目录长什么样PASCAL VOC 的目录规范不是随便定的它把图片、标注、划分文件分得很清楚。拿到压缩包解压后常见做法是先确认根目录下有没有这几个文件夹# 解压后先看目录层级别急着写训练脚本 unzip 共享单车数据集VOC格式.rar -d bike_voc cd bike_voc find . -maxdepth 2 -type d | sort典型输出应该是这样的结构. ├── Annotations # 存放 XML 标注文件每张图对应一个 ├── ImageSets │ └── Main # train.txt / val.txt / trainval.txt 划分列表 ├── JPEGImages # 原始图片jpg 格式为主 └── SegmentationClass # 语义分割掩码检测任务用不到可忽略如果解压后发现Annotations和JPEGImages平级但缺少ImageSets/Main说明这份数据只给了原始标注没做训练集验证集划分。这时候得自己写脚本按比例切分常见做法是 8:2 或 7:3别小看这一步划分随机种子没固定下次跑结果就对不上。2.2 XML 标注文件里到底存了什么VOC 的标注是 XML 格式每张图一个文件文件名和图片名一一对应。用 Python 读一个看看import xml.etree.ElementTree as ET def parse_voc_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() # 图片尺寸后面做坐标归一化要用 size root.find(size) width int(size.find(width).text) height int(size.find(height).text) # 遍历所有目标框 objects [] for obj in root.findall(object): name obj.find(name).text # 类别名比如 bicycle bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) objects.append((name, xmin, ymin, xmax, ymax)) return width, height, objects # 试读一个文件 w, h, objs parse_voc_xml(Annotations/000001.xml) print(f尺寸: {w}x{h}, 目标数: {len(objs)}) for o in objs: print(o)这段代码的逻辑很直接size节点拿图片宽高object节点拿每个框的类别和坐标。参数上要注意xmin/ymin/xmax/ymax都是整数但有些标注工具导出时会带小数点直接int()会截断稳妥点用float()再转。类别名name是字符串训练前必须映射成数字 ID映射表要固定下来别每次跑都重新生成否则类别顺序变了模型学的东西就乱了。2.3 图片与标注的对应关系检查数据集中最怕的是图片有、标注没有或者标注有、图片丢了。写个脚本对一遍import os img_dir JPEGImages xml_dir Annotations # 去掉扩展名做集合比对 img_names {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith(.jpg)} xml_names {os.path.splitext(f)[0] for f in os.listdir(xml_dir) if f.endswith(.xml)} only_img img_names - xml_names # 有图无标注 only_xml xml_names - img_names # 有标注无图 print(f有图无标注: {len(only_img)} 个) print(f有标注无图: {len(only_xml)} 个)这个检查跑一遍心里就有底了。有图无标注的图片要么补标要么直接从训练列表里剔除有标注无图的 XML 直接删掉留着只会让数据加载器报FileNotFoundError。常见做法是把这些异常文件名输出到日志文件处理完再重新生成train.txt和val.txt。3. 从 VOC 到 YOLO 格式转换脚本与参数映射3.1 为什么需要转格式VOC 的坐标是绝对像素值(xmin, ymin, xmax, ymax)而 YOLO 系列要的是归一化后的中心点加宽高(x_center, y_center, w, h)且每个值都在 0 到 1 之间。不转格式直接硬塞训练脚本第一步就崩。转换的核心公式不复杂x_center (xmin xmax) / 2 / width y_center (ymin ymax) / 2 / height w (xmax - xmin) / width h (ymax - ymin) / height但公式简单不代表没坑下面这个脚本把转换和划分一起做了import os import random import xml.etree.ElementTree as ET # 类别映射表根据数据集实际类别改 CLASS_MAP {bicycle: 0, motorbike: 1, person: 2} def voc_to_yolo(xml_path, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_MAP: continue # 跳过不在映射表里的类别 cls_id CLASS_MAP[name] 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) # 坐标裁剪防止越界 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) # 归一化计算 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return lines def convert_dataset(img_dir, xml_dir, out_label_dir, out_txt_dir): os.makedirs(out_label_dir, exist_okTrue) os.makedirs(out_txt_dir, exist_okTrue) all_names [] for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue base os.path.splitext(xml_file)[0] img_path os.path.join(img_dir, base .jpg) if not os.path.exists(img_path): continue # 图片不存在就跳过 # 从 XML 里读尺寸比重新读图快 tree ET.parse(os.path.join(xml_dir, xml_file)) size tree.getroot().find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines voc_to_yolo(os.path.join(xml_dir, xml_file), img_w, img_h) if not lines: continue # 没有有效目标也跳过 with open(os.path.join(out_label_dir, base .txt), w) as f: f.write(\n.join(lines)) all_names.append(base) # 按 8:2 划分训练验证集 random.seed(42) # 固定种子保证可复现 random.shuffle(all_names) split int(len(all_names) * 0.8) with open(os.path.join(out_txt_dir, train.txt), w) as f: f.write(\n.join(all_names[:split])) with open(os.path.join(out_txt_dir, val.txt), w) as f: f.write(\n.join(all_names[split:])) print(f转换完成训练集 {split} 张验证集 {len(all_names)-split} 张) convert_dataset(JPEGImages, Annotations, labels, ImageSets)逻辑说明先遍历 XML读尺寸调转换函数写 YOLO 格式的 txt。参数上CLASS_MAP必须和数据集实际类别对齐多一个少一个都会导致训练时类别错位。random.seed(42)是固定划分结果团队协作时每个人跑出来的train.txt都一样避免「我这边能训你那边不能」的玄学问题。坐标裁剪那两行别省有些标注框会超出图片边界不裁的话归一化后出现负数或大于 1 的值YOLO 的损失函数直接给你 NaN。3.2 转换后的验证方法转完不能直接开训先抽查几个标注可视化一下import cv2 def draw_yolo_label(img_path, label_path): img cv2.imread(img_path) h, w img.shape[:2] with open(label_path) as f: for line in f: cls_id, 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.imwrite(check_vis.jpg, img) draw_yolo_label(JPEGImages/000001.jpg, labels/000001.txt)打开check_vis.jpg看一眼框要是歪的、偏的、大小不对说明转换公式或尺寸读取有问题。这一步花两分钟比训到一半发现 mAP 只有 0.1 再回头查强得多。4. 训练配置与数据加载把数据集接进 YOLO 流程4.1 data.yaml 怎么写YOLO 系列训练靠一个 yaml 文件告诉它数据在哪、类别有几个# bike_data.yaml path: /home/user/bike_voc # 数据集根目录 train: ImageSets/train.txt # 训练列表相对 path val: ImageSets/val.txt # 验证列表 nc: 3 # 类别数和 CLASS_MAP 长度一致 names: [bicycle, motorbike, person] # 类别名顺序和 ID 对应nc和names必须严格对应names的顺序就是类别 ID 的顺序。常见错误是nc写了 3 但names只列了 2 个训练脚本加载时直接抛异常。path用绝对路径最稳相对路径在不同工作目录下跑容易找不到文件。4.2 启动训练与关键参数# 以 YOLOv8 为例具体版本按自己环境来 yolo detect train \ databike_data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectbike_train \ nameexp1参数说明imgsz640是输入分辨率共享单车场景里目标普遍偏小分辨率太低小目标直接糊掉640 是底线显存够就上 1280。batch16看显存调爆显存就减半。patience20是早停轮数20 轮验证集指标不涨就停省时间。lr00.01是初始学习率小数据集别设太大容易震荡。4.3 数据增强的取舍共享单车数据集通常存在类别不均衡——自行车多、人少、摩托车更少。YOLO 默认开启 mosaic 和 mixup对小目标检测有帮助但如果验证集 mAP 波动大可以试着关掉 mixupyolo detect train databike_data.yaml modelyolov8n.pt epochs100 mixup0.0 mosaic1.0mosaic1.0保留四图拼接对密集场景有效mixup0.0关掉图像混合减少训练初期的噪声。这个组合是我在几个小数据集上试出来的不一定通用但值得作为起点。5. 避坑与排查标注越界、类别不均衡、路径报错5.1 坐标越界导致 loss 变 NaN现象训练第一个 epoch 还没跑完loss 直接显示nan终端刷一堆警告。 原因XML 里某些框的xmax大于图片宽度或者xmin是负数归一化后出现大于 1 或小于 0 的值YOLO 的 CIoU 损失计算出 NaN。 解决在转换脚本里强制裁剪坐标就是第 3 章代码里max(0, min(xmin, img_w))那几行。已经转完的可以写个脚本扫一遍 label 文件发现值不在 [0,1] 范围就重新生成。5.2 类别不均衡导致模型只认自行车现象训练完 mAP 看着还行但一推理发现所有框都标成 bicycleperson 和 motorbike 几乎检不出来。 原因数据集中 bicycle 样本占 80% 以上模型学会了「全猜 bicycle 也能拿高分」。 解决两个方向。一是数据层面对少样本类别做过采样或者用增强手段复制少类图片二是损失层面YOLO 支持cls权重调整但更直接的是在data.yaml里确认nc和names没错的前提下用focal_loss或类别权重。常见做法是先把每类样本数统计出来差 10 倍以上就得处理。5.3 train.txt 路径写错导致加载为空现象训练启动后提示No images found或者Dataset is empty。 原因train.txt里写的是文件名但data.yaml的path和train拼接后路径不对。比如path是/home/user/bike_voctrain是ImageSets/train.txt但train.txt里写的是JPEGImages/000001.jpg实际拼接后变成/home/user/bike_voc/JPEGImages/000001.jpg如果图片不在这个位置就找不到。 解决统一约定train.txt里只写文件名不带路径YOLO 会自动去path下找。或者写全相对路径但要和path配合好。最稳的办法是训练前用ls拼一个路径确认文件存在。5.4 XML 解析报错encoding 问题现象xml.etree.ElementTree.ParseError: encoding specified in XML declaration is incorrect。 原因有些标注工具导出的 XML 声明是encodingutf-8但实际文件是 GBK 编码或者反过来。 解决读文件时指定编码或者用lxml库容错性更好。批量处理时加个 try-except把报错的文件名记下来单独处理。try: tree ET.parse(xml_path) except ET.ParseError: with open(xml_path, r, encodingutf-8, errorsignore) as f: content f.read() root ET.fromstring(content)5.5 图片格式不统一现象数据加载器报UnidentifiedImageError或cannot identify image file。 原因JPEGImages里混了 png、bmp 甚至 webp 格式的图片但文件名还是.jpg后缀。 解决用 PIL 批量检查图片真实格式不是 jpg 的统一转换from PIL import Image import os for f in os.listdir(JPEGImages): path os.path.join(JPEGImages, f) try: img Image.open(path) if img.format ! JPEG: img.convert(RGB).save(path, JPEG) except Exception as e: print(f损坏文件: {f}, {e})6. 进阶技巧用 FiftyOne 做数据集体检与可视化验证训练前花十分钟做一次数据集体检比训完再回头查省事得多。FiftyOne 是个专门用来探查数据集的工具装好之后几行代码就能把 VOC 数据加载进来看类别分布、框的大小分布、有没有异常样本。import fiftyone as fo import fiftyone.utils.voc as fouvoc # 加载 VOC 格式数据集 dataset fouvoc.load_voc_detection_dataset( data_pathJPEGImages, labels_pathAnnotations, dataset_dir./fiftyone_bike, max_samples500 # 先看 500 张全量加载慢 ) # 启动可视化界面 session fo.launch_app(dataset) session.wait()加载后在浏览器里能看到每张图的标注框还能按类别筛选、按框面积排序。我一般会重点看三样东西一是框面积分布如果大量框小于 32x32 像素说明小目标多训练时imgsz得往上调二是类别直方图哪类少一目了然三是随机抽 20 张看有没有漏标、错标。体检完再跑一个统计脚本把每类样本数和平均框大小打出来import os import xml.etree.ElementTree as ET from collections import defaultdict stats defaultdict(lambda: {count: 0, areas: []}) for xml_file in os.listdir(Annotations): tree ET.parse(os.path.join(Annotations, xml_file)) for obj in tree.getroot().findall(object): name obj.find(name).text bndbox obj.find(bndbox) w float(bndbox.find(xmax).text) - float(bndbox.find(xmin).text) h float(bndbox.find(ymax).text) - float(bndbox.find(ymin).text) stats[name][count] 1 stats[name][areas].append(w * h) for name, s in stats.items(): avg_area sum(s[areas]) / len(s[areas]) if s[areas] else 0 print(f{name}: {s[count]} 个, 平均面积 {avg_area:.0f} 像素)这个统计结果直接决定训练策略如果 bicycle 有 5000 个框而 motorbike 只有 200 个那要么补数据要么在损失函数里给 motorbike 加权。平均面积小于 1024 像素的类别imgsz至少设 1024否则特征图上一格就覆盖没了。从那以后我每次拿到新数据集都强制走一遍「目录检查 → 标注解析 → 格式转换 → 可视化抽查 → 类别统计」这五步一步不落。看起来费时间但比训到一半发现数据有问题再返工省下的时间够跑好几轮实验了。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。