简介:这是一份面向目标检测初学者、课程设计与智能停车项目的YOLO泊车位检测数据集,包含1000张真实停车场景图片,覆盖不同光线、角度与遮挡情况,已用LabelImg完成人工标注,框体质量较高。资源共2000个文件,其中标签文件以XML与TXT为主,对应VOC和YOLO格式,分文件夹存放,可直接用于YOLO系列训练;另有6个HTML教程、3个Python划分脚本和1个YAML配置,可一键完成训练集、验证集、测试集划分并生成ImageSets索引文件。压缩包约238.49MB,整体结构清晰,教程覆盖Linux与Windows两套环境搭建、YOLO训练案例及脚本使用说明,从零开始也能按步骤训练自己的车位检测模型。目前已有507人学习下载,无论用于算法验证、课程设计还是实际停车位检测演示,都能有效节省数据采集与标注时间。
1. 泊车位目标检测数据集:1000张图、三套标签和划分脚本到底帮你省了什么
假设你在做停车场出入口的空位识别,或园区车位占用统计,第一反应往往是去搜公开数据集。但翻一圈就会发现,现成的要么是车辆检测数据集,要么是自动驾驶全景分割,真正只识别“泊车位”这一类目标、标注框又齐整的很少。这套资源最实在的地方不在于“1000张图片”这个数字本身,而是它把标注、格式转换、数据划分、起步训练四件事一次性补齐:VOC、COCO、YOLO三种标签可以让你按框架需要随意切换,划分脚本避免了手工拆分导致的标签错位,训练教程则让0基础纯小白也能在本地把YOLO目标检测模型跑起来。对想快速验证方案可行性的工程师来说,这套数据省掉的是最容易翻车的那段脏活。
2. 三种标签格式的底层差异:VOC的XML、COCO的JSON与YOLO的TXT怎么互相转换
2.1 VOC格式:XML里的绝对坐标与目录组织
VOC(PASCAL VOC)格式历史最长,它不用单一文件管理全部标注,而是每张图片对应一个同名XML文件。解压后典型的VOC目录里会有JPEGImages放图片、Annotations放XML、ImageSets/Main里放train.txt和val.txt这类划分清单。XML里最关键的是<size>、<name>和<bndbox>三段信息,下面是一个最小例子:
<annotation> <filename>IMG_20250301_001.jpg</filename> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>parking_space</name> <bndbox> <xmin>102</xmin> <ymin>210</ymin> <xmax>468</xmax> <ymax>401</ymax> </bndbox> </object> </annotation>注意VOC的坐标是“左上角xmin、ymin + 右下角xmax、ymax”这种双角点表示法,不是“左上角+宽高”。<size>里的width和height是图片真实分辨率,做归一化时必须以它们为分母。VOC允许同一个XML里挂多个<object>,一张图里通常停着好几个车位,就会依次展开多个<bndbox>。另外XML里偶尔会出现<difficult>1</difficult>标记,difficult=1的目标在训练时通常会被忽略,转换脚本里最好也跳过它,否则会引入标注质量很差的正样本。
2.2 COCO格式:JSON里的三张表
COCO格式把整个数据集的标注塞进一个JSON文件,内部是“三段式”结构:images数组存每张图的id、文件名、宽高,annotations数组存每个目标的image_id、category_id、bbox,categories数组存类别id与类别名的对应关系。展开后大概长这样:
{ "images": [{"id": 1, "file_name": "IMG_20250301_001.jpg", "width": 1280, "height": 720}], "categories": [{"id": 1, "name": "parking_space"}], "annotations": [ {"id": 1001, "image_id": 1, "category_id": 1, "bbox": [102, 210, 366, 191], "area": 69906, "iscrowd": 0} ] }COCO的bbox是[x, y, width, height],也就是左上角坐标加框宽高,而不是右下角坐标。area字段理论上是多边形面积,目标检测标注里一般直接用宽高乘积。iscrowd=0表示普通目标,如果是1则代表目标重叠密集,YOLO转换时应该过滤掉。另一个容易忽略的点是:COCO的category_id不要求连续,常见数据集里可能是1、3、5这样的编号,而YOLO要求从0开始连续编号,所以转换时不能直接把category_id写进TXT。
2.3 YOLO格式:每行一个归一化目标
YOLO的标签格式是纯文本TXT,每个目标占一行,五列依次是:类别id、归一化中心点x、归一化中心点y、归一化宽w、归一化高h。典型一行:
0 0.222656 0.424306 0.285937 0.265278含义是:类别0的parking_space,中心点位于图片宽度22.27%、高度42.43%处,框的宽度占整张图28.59%,高度占26.53%。所有值都落在0到1之间,与图片实际分辨率解耦,这样YOLO在训练时把任意尺寸的图片resize到统一输入大小时,标签坐标只需要等比缩放,不会有像素级误差。TXT文件名必须和图片名完全一致,且放在同一个对应的labels目录下,YOLO训练时严格靠文件basename配对,差一个字符都会导致这张图的标签被忽略。
2.4 三格式互译:本质是坐标换算而不是格式搬运
三种格式互转,绕不开的只有两件事:坐标换算和类别id映射。坐标方面,VOC(角点)与YOLO(中心点+宽高)之间:
x_center = (xmin + xmax) / 2 / widthy_center = (ymin + ymax) / 2 / heightw = (xmax - xmin) / widthh = (ymax - ymin) / height
COCO(左上角+宽高)与YOLO之间:
x_center = (x + w / 2) / widthy_center = (y + h / 2) / height
拿2.1的XML举例,xmin=102、xmax=468、width=1280,那么中心点x就是(102 + 468) / 2 / 1280 = 0.222656,宽是(468 - 102) / 1280 = 0.285937,和2.3里的TXT对上了。反过来从YOLO还原成VOC时,xmin =(x_center - w/2) * width。另外类别映射必须单独维护一张对照表,VOC用名字符串,COCO用不连续id,YOLO用连续id,三者之间不能直接等价替换。
| 格式 | 文件形态 | 坐标表示 | 类别表示 |
|---|---|---|---|
| VOC | 每图一个XML | xmin,ymin,xmax,ymax(绝对像素) | 名字符串 |
| COCO | 单个JSON | x,y,w,h(绝对像素) | 连续或不连续id |
| YOLO | 每图一个TXT | x_center,y_center,w,h(归一化) | 从0开始的连续id |
3. 动手转换标签:把VOC和COCO写成YOLO格式的Python脚本与四个边界坑
3.1 先看清压缩包解压后的目录结构
拿到这类资源包,第一步别急着写转换代码,先解压看目录。常见做法是:
tree -L 2 .输出里至少有图片目录(JPEGImages或images)、标签目录(Annotations存XML,或annotations里存JSON)、划分脚本和训练教程文档。我的建议是先确认图片扩展名是jpg还是png,再确认标签文件名是否和图片一一对应。如果图有1000张而XML只有980个,说明有20张空图或标注缺失,这类数据要先洗,不要直接进训练流程。
3.2 VOC转YOLO:Python脚本与归一化计算
下面这个VOC转YOLO脚本只依赖Python标准库,不需要额外安装任何包,数据集的标签目录是Annotations、图片目录是JPEGImages时可以直接跑:
import xml.etree.ElementTree as ET from pathlib import Path xml_dir = Path("Annotations") # XML所在目录 img_dir = Path("JPEGImages") # 图片所在目录 out_dir = Path("labels") # 输出TXT目录 out_dir.mkdir(exist_ok=True) CLASS_MAP = {"parking_space": 0} # 类别字符串 -> YOLO id for xml_file in xml_dir.glob("*.xml"): tree = ET.parse(xml_file) root = tree.getroot() # 宽高必须取自XML里的size,不能靠猜 size = root.find("size") W = float(size.find("width").text) H = float(size.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in CLASS_MAP: # 无关类别直接跳过 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.0 / W y_center = (ymin + ymax) / 2.0 / H box_w = (xmax - xmin) / W box_h = (ymax - ymin) / H lines.append(f"{CLASS_MAP[name]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") out_file = out_dir / (xml_file.stem + ".txt") out_file.write_text("\n".join(lines))逻辑上分三步:解析每个object节点,把角点坐标算成归一化中心点和宽高,再按“同名不同扩展名”的原则写出TXT。xml_file.stem取的是XML文件名去掉.xml的部分,保证TXT和图片同名。所有除法都用2.0和浮点宽度高度,避免Python 3里整数除法截断。:.6f保留6位小数,既保证精度又控制文件体积。如果XML里混着car这类非车位目标,CLASS_MAP里没有它,自然被跳过;如果车位类别名不统一,比如同时出现parking_space和ParkingSpace,转换前先把XML里的<name>统一一下,否则会拆成两个类别id。
3.3 COCO转YOLO:单JSON文件如何拆成逐图TXT
COCO转YOLO比VOC多一个步骤:要把JSON里的三张表读进内存,建立“image_id到图片信息”和“category_id到新id”两个映射,再逐条写annotation。一个可复用的版本:
import json from pathlib import Path coco_path = Path("annotations/instances_train.json") img_dir = Path("images") out_dir = Path("labels") out_dir.mkdir(exist_ok=True) with open(coco_path, encoding="utf-8") as f: data = json.load(f) # COCO的category_id不一定从0开始,必须重新映射 cat_map = {cat["id"]: i for i, cat in enumerate(data["categories"])} img_map = {img["id"]: img for img in data["images"]} # 用dict收集:同一张图可能有多条annotation per_img_lines = {} for ann in data["annotations"]: img = img_map.get(ann["image_id"]) if img is None: continue W = float(img["width"]) H = float(img["height"]) x, y, w, h = ann["bbox"] # COCO是左上角+宽高 cat_id = cat_map[ann["category_id"]] # 归一化 x_center = (x + w / 2.0) / W y_center = (y + h / 2.0) / H bw = w / W bh = h / H stem = Path(img["file_name"]).stem per_img_lines.setdefault(stem, []).append( f"{cat_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}" ) for stem, lines in per_img_lines.items(): (out_dir / f"{stem}.txt").write_text("\n".join(lines))per_img_lines这个字典把同一图片的多个目标聚合到一起,最后一次性写出,避免频繁开关文件。cat_map把原始category_id映射到0到N-1,这是YOLO格式的硬性要求。还有一个小细节:ann["bbox"]里的值可能是整数也可能是浮点,w / 2.0已经保证了浮点运算,如果改写成w // 2,中心点会算偏,小目标上偏差尤其明显。
3.4 转换时常见的四个边界坑
第一个是坐标越界。标注工具偶尔会给出xmax略大于图片宽度或ymin小于0的框,归一化后会出现大于1或小于0的值,YOLO训练时不直接报错,但loss曲线会在初期乱跳。解决方法是转换时加裁剪:x_center = min(max(x_center, 0.0), 1.0),宽高同理,并过滤掉裁剪后宽或高小于0.001的框。
第二个是空标签文件。某张图没有车位时,转换脚本生成空TXT,YOLO会静默忽略这张图,但它仍留在数据目录里,白白占一个batch位。更好的做法是:生成TXT时若lines为空就不创建文件,并在后续划分脚本里按“标签文件存在且非空”过滤图片。
第三个是类别名或id错位。VOC里类别名不统一,COCO里原始id不连续,都会被转换脚本错误地映射到不同YOLO id上。转换完成后,我习惯打印一份所有类别及其框数量的统计表核对,确认只有0一个id对应parking_space。
第四个是宽高为0的畸形框。这类框通常来自标注中途操作失误,归一化后width为0,训练时会产生NaN梯度。转换时直接过滤掉w < 2或h < 2像素的框,代价很小,但能避免一次莫名其妙的训练中断。
4. 划分脚本实战:1000张图按70/20/10拆成train/val/test并保持标签同步
4.1 为什么划分是训练前必做的一步
机器学习的目标是让模型在没见过的数据上表现稳定,而不只是记住训练集。划分脚本负责把1000张图拆成三个互不相交的集合:train更新权重,val在每个epoch后评估并挑选最佳模型,test只在最终验收时使用一次。如果省掉test,你基于val反复调参到满意为止,这个“满意”本身就带着过拟合成分。泊车位检测场景相对固定,但还要注意另一个问题:同一个停车场不同时间连拍的图片高度相似,如果随机划分把同一时刻的照片分进train和val,模型会作弊式地拿高分,真到新停车场就翻车。
4.2 一个稳妥的划分脚本:随机打乱、按比例切分、同步移动标签
import random import shutil from pathlib import Path img_dir = Path("images") lab_dir = Path("labels") out_dir = Path("split_dataset") # 只保留有标签且标签非空的图片,避免空图混入训练集 names = [] for p in img_dir.glob("*"): if p.is_file(): lab = lab_dir / f"{p.stem}.txt" if lab.exists() and lab.stat().st_size > 0: names.append(p.stem) names.sort() # 排序让打散前顺序可预期 random.seed(42) random.shuffle(names) n = len(names) n_val = int(n * 0.2) n_test = int(n * 0.1) n_train = n - n_val - n_test splits = { "train": names[:n_train], "val": names[n_train:n_train + n_val], "test": names[n_train + n_val:], } for split, name_list in splits.items(): images_out = out_dir / split / "images" labels_out = out_dir / split / "labels" images_out.mkdir(parents=True, exist_ok=True) labels_out.mkdir(parents=True, exist_ok=True) for name in name_list: shutil.copy(img_dir / f"{name}.jpg", images_out / f"{name}.jpg") shutil.copy(lab_dir / f"{name}.txt", labels_out / f"{name}.txt")这里用lab.exists() and lab.stat().st_size > 0把上一章提到的空标签问题一并挡掉,只有同时存在图片和有效标签的文件才会进入训练集。shutil.copy而不是move是故意为之:copy保留完整原始数据,万一划分比例不合适还能重跑,相当于给自己留了后悔药,磁盘够大就放心用。random.seed(42)固定随机数种子,保证每次运行脚本得到完全相同的划分结果,这对复现实验和对比超参数非常关键;如果想每次划分都不同,把42换成当前时间戳即可,但同一套实验过程里不要反复改种子。
如果图片扩展名不是jpg而是png,把复制那两行的.jpg换成.png;也可以用img_dir / name直接取原始文件名的完整路径来复制,避免扩展名写死。
4.3 比例与随机种子怎么定
对1000张图的数据量,70/20/10是合理的起点,对应train约700张、val约200张、test约100张。泊车位检测是单类别、目标几何形状相对固定的任务,train有700张足够学到稳定的特征,test保留100张也能统计出有意义的mAP和召回率。如果以后数据量涨到3000张以上,可以调整为80/10/10,val不一定要跟着涨,因为val只用来选模型,太多反而拖慢调参节奏。seed选42只是约定俗成,没有魔法,关键是固定下来并在实验记录里写明,不然隔一周再跑一次划分,得到的结果可能完全不同。
4.4 划分后的校验脚本
划分完成不是终点,还要确认集合之间没有交叉、图片与标签一一对应。一个一次性校验脚本:
from pathlib import Path out_root = Path("split_dataset") for split in ["train", "val", "test"]: img_stems = {p.stem for p in (out_root / split / "images").glob("*")} lab_stems = {p.stem for p in (out_root / split / "labels").glob("*")} print(f"[{split}] images={len(img_stems)} labels={len(lab_stems)} " f"only_img={len(img_stems - lab_stems)} only_lab={len(lab_stems - img_stems)}")如果打印结果里only_img或only_lab不为0,说明文件错配,要回头检查划分脚本。另一个值得做的是类别分布检查:统计test集里每个类别出现的框数量,以单类别为例,test集至少有几十个正样本框才有统计意义。如果test里某类框数量为0,测试结果会缺这个类别的召回率,验收时容易被误判。
5. 用YOLO训练泊车位模型:数据yaml、训练命令与必调参数排查
5.1 数据配置文件:yaml里的路径与names怎么写
Ultralytics YOLO(常见如YOLOv8、YOLOv11)训练时需要数据配置文件,它决定了模型去哪找图、有几类、每类叫什么:
path: /home/user/parking/split_dataset # 划分后数据的根目录 train: train/images val: val/images test: test/images names: 0: parking_space三个容易踩的细节。path最好用绝对路径,因为train和val是相对path的值,yaml放在项目根目录而数据在另一个盘时,相对路径会解析错。names的索引序号必须和标签TXT第一列的id一致,单类别就是0,如果写成1: parking_space,训练时会认为有类别0和类别1两个类。test字段在训练时不参与计算,只在你手动执行yolo detect val data=xxx.yaml split=test时才生效,但先写上能避免后面验证时现改文件。
5.2 训练命令:从预训练权重到核心超参数
环境配好、数据就位后,最简单的训练命令:
yolo detect train data=parking.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16 device=0model=yolov8n.pt表示加载一个在COCO上预训练过的nano权重,再用泊车位数据做迁移学习。0基础纯小白先把这条命令跑通,再逐步理解内部机制也来得及。epochs=100对1000张单类别数据是充裕的,通常40到60轮mAP已经收敛,后面几十轮留给学习率衰减和验证集稳定。imgsz=640是YOLO系列最通用的输入分辨率,车位框在画面里占比不小,640足够;如果画面角落有很多远处小车位,再试imgsz=960,代价是训练和推理都更慢。batch=16取决于显存,8GB显存建议降到8,16GB以上用16,报CUDA out of memory时优先减batch而不是减imgsz。device=0指用第0张GPU,单卡机器可以不写这一项。
5.3 训练过程观测:损失函数、mAP50与混淆矩阵
训练日志里会出现box_loss、cls_loss、dfl_loss三项损失,分别对应框回归、分类和分布焦点损失。对车位检测,我主要看两个东西:训练集上box_loss是否稳定下降,验证集上mAP50是否持续上升。如果mAP50连续30轮不再变化,就可以提前停掉,不必跑满100轮。训练结束后,runs/detect/train/weights/best.pt是后续验证要用的权重。
mAP50是IoU阈值0.5下的平均精度,车位这类规则四边形目标大概率能达到0.9以上;mAP50-95把IoU从0.5到0.95每隔0.05算一次再平均,更反映框贴合精度,一般0.7以上算正常。训练生成的混淆矩阵图值得细看:对角线越亮越好,如果真实车位那一列有大量被预测成背景,说明漏检严重,优先检查标注框是否太松导致正样本质量差。
5.4 训练阶段常见问题排查
现象:训练刚开始提示“All labels empty”,进程自动退出。原因:数据yaml里的train路径指向了没有TXT的目录,或TXT文件名与图片名不一致。解决:按第4.4节的校验脚本核对,确认train/images和train/labels一一对应,且每个TXT至少有一行数据。
现象:loss曲线中途出现NaN,之后所有指标归零。原因:标签里有归一化后宽或高为0的畸形框,或学习率过高。解决:用脚本扫描所有TXT,过滤坐标不在0到1范围以及宽高为0的行,同时把学习率调低,在命令后加lr0=0.005重试。
现象:train的loss降得很快但val的mAP停滞。原因:最常见的是train和val数据存在同场景重复,比如连拍帧被分到两边;也可能是标注本身不一致。解决:回到第4章,按拍摄时刻或摄像头编号分组划分,不要纯随机打散,这是数据量不大时的保命操作。
现象:显存不足以OOM中断。原因:batch过大或输入分辨率过大。解决:batch降到8或4,imgsz降到480先跑通,再逐步加大;把workers从默认8降到4也能缓解内存峰值。
现象:mAP50很高但mAP50-95明显偏低。原因:模型框的位置预测不够精细,多半是标注框边缘没贴住车位线。解决:不要急着调超参数,先回看几十张标注图,把松垮的框修正后重新训练。这种问题靠调参基本靠玄学,修标注才是正路。
6. 上线前验证与进阶技巧:从测试集指标到实拍场景的最后一公里
训练结束,best.pt在测试集上的mAP只是及格线,真正决定能不能上线的是它在实际停车场的表现。先跑一条验证命令看测试集整体情况:
yolo detect val data=parking.yaml model=runs/detect/train/weights/best.pt split=test重点看混淆矩阵里漏检和误检的具体分布,而不是只看mAP数字。我还会对一张实拍图做yolo predict,配合输出预测结果的特征图和热力图,观察模型关注了哪些区域:热力图高亮集中在车位边线附近,说明学的特征基本正确;高亮散落在路面纹理上,就要怀疑训练数据里的负样本不足。
实际部署时,固定相机比移动设备简单,但光线和视角差异仍然存在。常见做法是用RTSP拉取停车场监控视频流,把每帧resize到640再交给模型推理。如果手头有d435i这类深度相机,可以额外加一层深度阈值校验:模型检出的车位框里,如果深度值连续且平坦说明是空位,深度值断裂且有凸起则判定为占用。这条规则对“车位线内停着黑色车”的漏检场景特别有效。
把模型装进边缘设备前,先导出ONNX再转TensorRT:
yolo export model=best.pt format=onnx imgsz=640导出后务必对比PyTorch模型和ONNX在同一张测试图上的输出差异。我遇到过很多次ONNX推理精度下降,最后查出来都是部署端预处理忘了归一化,或者把BGR当RGB读入。这些坑和模型本身无关,但排错成本往往比重新训练还高。
最后说一个我自己吃过的亏:第一次做车位检测demo时,为了赶进度直接用随机划分,test集和train集混进了同一监控的同一分钟连拍帧,那套模型测试mAP刷到0.98,现场部署到另一个停车场直接掉到0.72。从那以后,每个新数据集我都先按场景分组,再跑一遍第4.4节的校验,确认没有跨集重复才算数。希望这些步骤能帮你少走这一段弯路,也希望这套数据资源在你手上真正跑成一个可靠的车位检测服务。
本文还有配套的精品资源,点击获取