简介:这份共享单车数据集采用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_ok=True) os.makedirs(out_txt_dir, exist_ok=True) 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 \ data=bike_data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=bike_train \ name=exp1参数说明:imgsz=640是输入分辨率,共享单车场景里目标普遍偏小,分辨率太低小目标直接糊掉,640 是底线,显存够就上 1280。batch=16看显存调,爆显存就减半。patience=20是早停轮数,20 轮验证集指标不涨就停,省时间。lr0=0.01是初始学习率,小数据集别设太大,容易震荡。
4.3 数据增强的取舍
共享单车数据集通常存在类别不均衡——自行车多、人少、摩托车更少。YOLO 默认开启 mosaic 和 mixup,对小目标检测有帮助,但如果验证集 mAP 波动大,可以试着关掉 mixup:
yolo detect train data=bike_data.yaml model=yolov8n.pt epochs=100 mixup=0.0 mosaic=1.0mosaic=1.0保留四图拼接,对密集场景有效;mixup=0.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 看着还行,但一推理发现所有框都标成 bicycle,person 和 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_voc,train是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 声明是encoding="utf-8",但实际文件是 GBK 编码,或者反过来。 解决:读文件时指定编码,或者用lxml库容错性更好。批量处理时加个 try-except,把报错的文件名记下来单独处理。
try: tree = ET.parse(xml_path) except ET.ParseError: with open(xml_path, 'r', encoding='utf-8', errors='ignore') 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_path='JPEGImages', labels_path='Annotations', dataset_dir='./fiftyone_bike', max_samples=500 # 先看 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,否则特征图上一格就覆盖没了。
从那以后我每次拿到新数据集,都强制走一遍「目录检查 → 标注解析 → 格式转换 → 可视化抽查 → 类别统计」这五步,一步不落。看起来费时间,但比训到一半发现数据有问题再返工,省下的时间够跑好几轮实验了。希望帮到你。
本文还有配套的精品资源,点击获取