ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

泊车位检测数据集:VOC/COCO/YOLO格式转换与训练实战

2026/10/5 11:53:50 拓冰建站 浏览量
泊车位检测数据集:VOC/COCO/YOLO格式转换与训练实战 简介这是一份面向目标检测初学者、课程设计与智能停车项目的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里的绝对坐标与目录组织VOCPASCAL VOC格式历史最长它不用单一文件管理全部标注而是每张图片对应一个同名XML文件。解压后典型的VOC目录里会有JPEGImages放图片、Annotations放XML、ImageSets/Main里放train.txt和val.txt这类划分清单。XML里最关键的是size、name和bndbox三段信息下面是一个最小例子annotation filenameIMG_20250301_001.jpg/filename size width1280/width height720/height depth3/depth /size object nameparking_space/name bndbox xmin102/xmin ymin210/ymin xmax468/xmax ymax401/ymax /bndbox /object /annotation注意VOC的坐标是“左上角xmin、ymin 右下角xmax、ymax”这种双角点表示法不是“左上角宽高”。size里的width和height是图片真实分辨率做归一化时必须以它们为分母。VOC允许同一个XML里挂多个object一张图里通常停着好几个车位就会依次展开多个bndbox。另外XML里偶尔会出现difficult1/difficult标记difficult1的目标在训练时通常会被忽略转换脚本里最好也跳过它否则会引入标注质量很差的正样本。2.2 COCO格式JSON里的三张表COCO格式把整个数据集的标注塞进一个JSON文件内部是“三段式”结构images数组存每张图的id、文件名、宽高annotations数组存每个目标的image_id、category_id、bboxcategories数组存类别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字段理论上是多边形面积目标检测标注里一般直接用宽高乘积。iscrowd0表示普通目标如果是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) / heightCOCO左上角宽高与YOLO之间x_center (x w / 2) / widthy_center (y h / 2) / height拿2.1的XML举例xmin102、xmax468、width1280那么中心点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用不连续idYOLO用连续id三者之间不能直接等价替换。格式文件形态坐标表示类别表示VOC每图一个XMLxmin,ymin,xmax,ymax绝对像素名字符串COCO单个JSONx,y,w,h绝对像素连续或不连续idYOLO每图一个TXTx_center,y_center,w,h归一化从0开始的连续id3. 动手转换标签把VOC和COCO写成YOLO格式的Python脚本与四个边界坑3.1 先看清压缩包解压后的目录结构拿到这类资源包第一步别急着写转换代码先解压看目录。常见做法是tree -L 2 .输出里至少有图片目录JPEGImages或images、标签目录Annotations存XML或annotations里存JSON、划分脚本和训练教程文档。我的建议是先确认图片扩展名是jpg还是png再确认标签文件名是否和图片一一对应。如果图有1000张而XML只有980个说明有20张空图或标注缺失这类数据要先洗不要直接进训练流程。3.2 VOC转YOLOPython脚本与归一化计算下面这个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_okTrue) 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文件如何拆成逐图TXTCOCO转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_okTrue) with open(coco_path, encodingutf-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的框。第二个是空标签文件。某张图没有车位时转换脚本生成空TXTYOLO会静默忽略这张图但它仍留在数据目录里白白占一个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(parentsTrue, exist_okTrue) labels_out.mkdir(parentsTrue, exist_okTrue) 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/10val不一定要跟着涨因为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)} fonly_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 dataxxx.yaml splittest时才生效但先写上能避免后面验证时现改文件。5.2 训练命令从预训练权重到核心超参数环境配好、数据就位后最简单的训练命令yolo detect train dataparking.yaml modelyolov8n.pt epochs100 imgsz640 batch16 device0modelyolov8n.pt表示加载一个在COCO上预训练过的nano权重再用泊车位数据做迁移学习。0基础纯小白先把这条命令跑通再逐步理解内部机制也来得及。epochs100对1000张单类别数据是充裕的通常40到60轮mAP已经收敛后面几十轮留给学习率衰减和验证集稳定。imgsz640是YOLO系列最通用的输入分辨率车位框在画面里占比不小640足够如果画面角落有很多远处小车位再试imgsz960代价是训练和推理都更慢。batch16取决于显存8GB显存建议降到816GB以上用16报CUDA out of memory时优先减batch而不是减imgsz。device0指用第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的行同时把学习率调低在命令后加lr00.005重试。现象train的loss降得很快但val的mAP停滞。原因最常见的是train和val数据存在同场景重复比如连拍帧被分到两边也可能是标注本身不一致。解决回到第4章按拍摄时刻或摄像头编号分组划分不要纯随机打散这是数据量不大时的保命操作。现象显存不足以OOM中断。原因batch过大或输入分辨率过大。解决batch降到8或4imgsz降到480先跑通再逐步加大把workers从默认8降到4也能缓解内存峰值。现象mAP50很高但mAP50-95明显偏低。原因模型框的位置预测不够精细多半是标注框边缘没贴住车位线。解决不要急着调超参数先回看几十张标注图把松垮的框修正后重新训练。这种问题靠调参基本靠玄学修标注才是正路。6. 上线前验证与进阶技巧从测试集指标到实拍场景的最后一公里训练结束best.pt在测试集上的mAP只是及格线真正决定能不能上线的是它在实际停车场的表现。先跑一条验证命令看测试集整体情况yolo detect val dataparking.yaml modelruns/detect/train/weights/best.pt splittest重点看混淆矩阵里漏检和误检的具体分布而不是只看mAP数字。我还会对一张实拍图做yolo predict配合输出预测结果的特征图和热力图观察模型关注了哪些区域热力图高亮集中在车位边线附近说明学的特征基本正确高亮散落在路面纹理上就要怀疑训练数据里的负样本不足。实际部署时固定相机比移动设备简单但光线和视角差异仍然存在。常见做法是用RTSP拉取停车场监控视频流把每帧resize到640再交给模型推理。如果手头有d435i这类深度相机可以额外加一层深度阈值校验模型检出的车位框里如果深度值连续且平坦说明是空位深度值断裂且有凸起则判定为占用。这条规则对“车位线内停着黑色车”的漏检场景特别有效。把模型装进边缘设备前先导出ONNX再转TensorRTyolo export modelbest.pt formatonnx imgsz640导出后务必对比PyTorch模型和ONNX在同一张测试图上的输出差异。我遇到过很多次ONNX推理精度下降最后查出来都是部署端预处理忘了归一化或者把BGR当RGB读入。这些坑和模型本身无关但排错成本往往比重新训练还高。最后说一个我自己吃过的亏第一次做车位检测demo时为了赶进度直接用随机划分test集和train集混进了同一监控的同一分钟连拍帧那套模型测试mAP刷到0.98现场部署到另一个停车场直接掉到0.72。从那以后每个新数据集我都先按场景分组再跑一遍第4.4节的校验确认没有跨集重复才算数。希望这些步骤能帮你少走这一段弯路也希望这套数据资源在你手上真正跑成一个可靠的车位检测服务。本文还有配套的精品资源点击获取