☰
1000张泊车位图片带你跑通YOLO目标检测全流程:格式转换、划分与训练避坑
2026/10/5 14:16:59 网站建设 项目流程

简介:面向目标检测初学者与课程实践者,这份数据集可直接用于泊车位检测模型训练。内含一千张真实场景图片,使用LabelImg标注软件标注,提供VOC、COCO、YOLO三种格式标签并分文件夹存放,省去格式转换环节,适合快速上手YOLO系列算法。该压缩包共两千个文件,除图片外还含XML标注文件、TXT标签文件、YAML配置、Python划分脚本以及环境搭建与训练教程的网页文档,整体大小约二百三十八兆。配套的三个划分脚本能将图片和标签按比例切分为训练集、验证集、测试集;教程覆盖Windows和Linux下的YOLO环境配置、自定义数据集训练流程,并配有Ubuntu安装说明,对新手较为友好。目前已有五百零七人学习使用,可用于课程设计、毕业设计或泊车视觉项目开发,能节省数据准备与入门调试时间。

1. 泊车位目标检测:一张1000张图片的数据集,能带新手跑通整个YOLO流程吗

拿到一个包含1000张泊车位图片、同时附带VOC、COCO、YOLO三种格式标签和划分脚本的数据集包,我的第一反应是:这足够让一个零基础的人把目标检测的完整链路跑通。停车位检测本质上是目标检测里的一个细分场景,检测目标从“人、车、猫狗”换成“泊车位”,但标注格式转换、数据集划分、YOLO训练这些核心步骤完全一样。很多人卡在第一步:手里有数据,却不知道VOC的xml文件怎么变成YOLO训练能直接读的txt文件,或者以为划分数据集就是随便把文件名打乱一扔。这篇笔记就按标题里的内容一步步讲清楚三种标签格式的区别、划分脚本怎么用、以及拿这1000张图训练一个可用的泊车位检测模型需要调哪些参数。适合刚装好YOLO环境、准备拿真实数据练手的从业者,也适合想把自己标注的数据集转成统一格式的老手。

2. 一张泊车位图片和它的三种标签格式:VOC、COCO、YOLO到底有什么区别

2.1 VOC是xml、COCO是json、YOLO是txt:三种格式的存储和坐标差异

标题里的数据集之所以同时给了三种格式,是因为不同训练框架的入口数据格式不一样。VOC格式源自Pascal VOC挑战赛,每张图片对应一个同名的xml文件,里面用<object>节点描述每个目标,包含类别名称和<bndbox>下的xmin、ymin、xmax、ymax四个绝对像素坐标。COCO格式则把所有图片的标注汇总到一个json文件里,标注信息分成images、annotations、categories三大段,每个目标的坐标用bbox字段记录,格式是[x, y, width, height]。YOLO格式最简单粗暴,每张图片对应一个txt文件,每行一个目标,格式是class_id x_center y_center width height,注意这里的四个数值全部是相对于图片宽高的归一化值,范围在0到1之间。

三种格式的核心差异不在“存储结构”,而在“坐标表述方式”。VOC和COCO用的是绝对像素值或基于绝对像素的宽高,YOLO用归一化比例;COCO的bbox原点在框的左上角,YOLO的中心点坐标在框的正中心。如果直接把VOC的xml内容原样拷进txt,训练出来的模型会完全学不到位置信息。我自己处理数据集时,习惯先建一个转换脚本把原始格式统一成YOLO格式,因为ultralytics的YOLO训练接口默认读的就是txt标签。转换前先看一眼类别列表,泊车位检测里通常只有两类:“占用occupied”和“空闲empty”,有的数据集合并成单类“泊车位”,这决定了输出层的类别数。

2.2 VOC转YOLO格式:一个脚本看懂坐标归一化与类别映射

把VOC的xml转换成YOLO的txt是最常见的操作,我一般用下面这段Python脚本处理。它读取标注文件夹里的所有xml文件,解析出每个<object>的类别名和边界框坐标,再归一化后写入同名txt。

import xml.etree.ElementTree as ET import os # 类名列表,顺序就是YOLO标签里的class_id classes = ["occupied", "empty"] def voc_to_yolo(xml_dir, txt_dir): os.makedirs(txt_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() # 图片宽高从xml的<size>节点读取 img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) base_name = os.path.splitext(xml_file)[0] with open(os.path.join(txt_dir, base_name + ".txt"), "w") as f_out: for obj in root.findall("object"): cls_name = obj.find("name").text if cls_name not in classes: continue cls_id = classes.index(cls_name) bndbox = obj.find("bndbox") xmin = int(float(bndbox.find("xmin").text)) ymin = int(float(bndbox.find("ymin").text)) xmax = int(float(bndbox.find("xmax").text)) ymax = int(float(bndbox.find("ymax").text)) # 转成YOLO需要的中心点x、中心点y、宽、高,全部除以图片宽高归一化 x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h # 防止边界溢出,夹紧到0~1之间 x_center = min(max(x_center, 0), 1) y_center = min(max(y_center, 0), 1) box_w = min(max(box_w, 0), 1) box_h = min(max(box_h, 0), 1) f_out.write(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}\n") voc_to_yolo("annotations/voc", "labels/yolo")

这段代码里的关键点是classes列表的顺序。YOLO标签文件里第一列的数字不是类别名称,而是类别在列表里的索引,classes = ["occupied", "empty"]意味着occupied对应class_id 0,empty对应class_id 1。如果xml里出现了列表之外的类别名,脚本会直接跳过,避免训练时类别数对不上。归一化的位置计算是先求绝对像素中心点再除以图片宽高,顺序不能反过来,否则数值会整体偏移。

COCO转YOLO的原理也一样,只是解析json里的bbox字段后要把[x, y, w, h]转换成中心点格式:x_center = x + w/2,y_center = y + h/2,再分别除以图片宽高。这里容易犯的一个错误是忘记json里的bbox坐标是int类型,直接做除法会得到整数结果,归一化后全是0,训练直接崩。转完后随手打印前几个txt文件的内容看看数值范围,如果出现大于1的数,说明某张图的标注框越界了。

2.3 YOLO训练时为什么要用txt而不是json或xml:加载效率和anchor匹配逻辑

很多人会问,既然VOC和COCO格式更通用,为什么YOLO偏偏要一种txt格式?核心原因有两个。第一是加载效率,ultralytics在训练时会用torch.load配合数据集索引文件逐张读取图片,再根据txt标签快速构建训练张量,txt文件每行定长、结构简单,不需要像json那样先反序列化整个大字典,也不需要像xml那样做DOM解析,数据流水线的预处理时间能省下一大截。对于1000张图片这种规模可能感知不强,但换成几万张的工业数据集,差别会直接反映在GPU利用率上。

第二个原因是YOLO系列的anchor匹配机制。YOLO把输入图片划分成网格,每个网格负责预测中心点落在自己范围内的目标,标签在进入损失函数前要被编码成“网格偏移量”和“anchor宽高比例”。txt里直接存储归一化的中心点和宽高,省去了训练时再对绝对坐标做归一化的步骤,也避免了因图片尺寸不一致导致标签失效的问题。VOC的xml里记录的是原图绝对像素坐标,如果训练时做了letterbox缩放或Mosaic增强,坐标必须重新映射;YOLO的归一化坐标天然免疫这些几何变换,模型在imgsz=640下读到的标签和原图640x640时完全一致。

3. 划分脚本怎么用:从1000张图中切割出train/val/test的正确姿势

3.1 按7:2:1划分脚本:随机打乱、种子固定、防止数据集泄漏

标题里提到“划分脚本”,它解决的是训练集、验证集、测试集怎么切的问题。常见做法是按7:2:1的比例把1000张图分成700张训练、200张验证、100张测试。如果数据集包只给了train和val两个目录,测试集可以临时从val里匀或者不管。核心是脚本里必须做三件事:随机打乱、固定随机种子、同时移动图片和对应的标签文件。

import os import random import shutil random.seed(42) # 固定种子,保证每次运行划分结果一致 image_dir = "images" label_dir = "labels/yolo" train_dir = "dataset/images/train" val_dir = "dataset/images/val" test_dir = "dataset/images/test" images = [f for f in os.listdir(image_dir) if f.endswith(".jpg")] random.shuffle(images) # 按 7:2:1 切分 total = len(images) train_end = int(total * 0.7) val_end = int(total * 0.9) splits = { "train": images[:train_end], "val": images[train_end:val_end], "test": images[val_end:], } for split_name, split_images in splits.items(): os.makedirs(os.path.join(train_dir.replace("train", split_name), "..", split_name, "images"), exist_ok=True) # 简化的目录创建逻辑,实际建议用pathlib统一管理 os.makedirs(f"dataset/{split_name}/labels", exist_ok=True) for img in split_images: base = os.path.splitext(img)[0] shutil.copy(os.path.join(image_dir, img), f"dataset/{split_name}/images/{img}") label_src = os.path.join(label_dir, base + ".txt") if os.path.exists(label_src): shutil.copy(label_src, f"dataset/{split_name}/labels/{base}.txt") else: print(f"警告: 图片 {img} 没有对应的标签文件")

这里有个隐藏要求:图片和标签必须同进同退。很多人只移动了图片,训练时YOLO会在labels目录里找不到对应txt,直接跳过这张图,最后出现“训练了700张但实际只用了500张”的情况。划分前的随机种子固定也很重要,random.seed(42)保证每次运行脚本得到相同的划分结果,否则你上午训练和下午训练用的验证集不一样,模型的指标没法对比。如果后续要调参,一定不要换种子。

划分比例不是拍脑袋定的。1000张图片的数据量偏小,val取20%也就是200张,已经足够观察模型是否过拟合;test取10%是为了最后做一次“盲测”。如果你的数据集本身存在严重类别不平衡,比如“占用”类有800个框而“空闲”类只有200个框,建议先按类别比例做分层划分,而不是简单随机,否则验证集里可能一张空闲车位的图都没有。

3.2 泊车位场景的特殊划分:同一停车场不同帧的相似度会骗过验证集

泊车位检测这个任务有个独特问题:数据集中同一个停车场的不同图片可能是在不同时间、不同角度下拍摄的,画面高度相似。如果划分脚本只做全局随机,训练集和验证集里很可能出现“同一批车位、同一个摄像头视角”的图片,验证集loss降得很低,看起来模型泛化得很好,实际换一个停车场就废掉。这个坑我踩过,当时用全局随机划分跑出来的mAP有0.85,结果拿到另一栋楼的地下车库测试,几乎全漏检。

解决办法是按“场景分组”划分。先把文件名的前缀提取出来,比如parking_lot_a_001.jpg、parking_lot_a_002.jpg按parking_lot_a分组,再对组进行随机划分,保证同一个场景的所有帧只出现在一个集合里。这样验证集的场景分布和训练集不重叠,才能真实反映模型的泛化能力。如果原始文件名没有规律,就手动看一眼图片内容,把同一地点拍摄的图分到一组。对泊车位数据来说,宁可牺牲一点训练集数量,也要让验证集“硬”一点。

3.3 划分后检查文件数量与标签一致性:推荐用这三行命令行

划分完不要急着训练,先做一致性检查。我通常用以下三个命令确认图片和标签没有错位:

# 统计各集合图片数量 find dataset/train/images -name "*.jpg" | wc -l # 统计各集合标签数量 find dataset/train/labels -name "*.txt" | wc -l # 找出有图片但没有标签的文件名 comm -23 <(ls dataset/train/images | sed 's/.jpg//') <(ls dataset/train/labels | sed 's/.txt//')

第一个命令确认图片总数,第二个命令确认标签总数,正常情况两者应该完全相等。第三个命令使用comm比较两个文件名列表,打印出“有图无标签”的文件名。如果输出非空,说明有图片被单独挪进了训练集,需要补齐或移除。另一个验证思路是检查txt文件内容是否为空,0字节的标签文件会让训练器报“Corrupt JPEG”或者“empty labels”错误,用find dataset -name "*.txt" -size 0就能扫出来。

4. 把训练跑起来:用YOLO训练泊车位检测的设置、参数和时长预估

4.1 先装环境配data.yaml:缺一步都会让训练直接崩掉

标题里的“训练教程”环节,最常见的方式是基于ultralytics的YOLO仓库来跑。环境配置的关键是Python版本、PyTorch版本和ultralytics包三者匹配,我一般用Python 3.10配合PyTorch 2.x的CUDA版本,然后一条命令装完依赖:

# 创建虚拟环境,避免系统Python环境被搞乱 conda create -n yolo python=3.10 -y conda activate yolo pip install ultralytics

装完后先跑一次yolo detect predict拉取预训练权重,确认CUDA可用。如果只用CPU跑,1000张图训练会慢得让人失去耐心,建议至少用一块6GB显存的显卡。环境装好后,最重要的文件是data.yaml,YOLO靠它找到数据集路径和类别信息:

# data.yaml train: dataset/train/images val: dataset/val/images test: dataset/test/images nc: 2 names: ["occupied", "empty"]

注意train和val指向的是图片目录而非标签目录,YOLO会根据图片路径自动把/images/替换成/labels/寻找同名txt标签。如果你的数据集目录结构不是这种约定,比如标签放在别的文件夹,需要在yaml里手动指定标签路径,否则会报“label not found”错误。nc和names必须和前面转换脚本里的classes列表完全一致,occupied是第0类还是第1类,取决于训练时用哪个列表,换顺序就等于换标签,模型会把“占用”学成“空闲”。

4.2 1000张图怎么训练:预训练权重、冻结层和数据增强的三板斧

图片数量只有1000张,直接从头训练YOLO几乎不可能收敛。常见做法是加载COCO预训练权重做迁移学习,让模型继承通用的特征提取能力,只微调最后几层检测头。训练命令如下:

yolo detect train \ data=data.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ patience=20 \ workers=4 \ device=0 \ cache=True

model=yolov8s.pt是最关键的一项,它的作用是加载COCO预训练权重而不是从头初始化。epochs=100对1000张图的小数据集足够,配合patience=20表示连续20轮验证指标不提升就早停。cache=True让数据加载时直接缓存到内存或磁盘,省去每次epoch都做磁盘IO的时间。batch=16要按GPU显存调整,6GB显存跑yolov8s这个批次已经偏大,如果报CUDA Out Of Memory就降到8。

小数据集训练时,我还会在训练命令行里手动打开增强项。ultralytics默认开启Mosaic、翻转、色彩抖动等增强,这本身对小数据集是好事,但Mosaic增强在训练后期可能拖慢收敛。建议前50轮保持默认增强,50轮后关掉Mosaic再微调几轮:

yolo detect train \ data=data.yaml \ model=runs/detect/train/weights/last.pt \ epochs=20 \ imgsz=640 \ batch=16 \ mosaic=0.0 # 关闭马赛克增强,让模型在干净数据上精调

这里用last.pt接续训练而不是从头再来,后面的mosaic=0.0是关闭增强的开关。注意泊车位检测的目标通常是固定的框,长宽比接近真实车位,关闭Mosaic和随机裁剪能减少车位被裁掉一半的情况。

4.3 训练过程的三个观察点:loss曲线、验证mAP和类别混淆矩阵

训练开始后不要干等着跑完,每轮结束要看三个指标。第一个是box_loss,代表边界框回归的损失值,正常趋势是稳步下降;如果训练到第30轮还在0.1以上震荡,说明anchor设置和车位目标的宽高比不匹配。第二个是mAP50,这是判断模型能不能用的主要指标,泊车位检测做到0.85以上的mAP50算合格,0.9以上算优秀。第三个是类别分布,用yolo detect val输出的混淆矩阵看“占用”和“空闲”是否都被正确检出。

训练结束后,runs/detect/train/目录下会生成weights/best.pt和weights/last.pt,前者是验证集指标最高的一轮,后者是最后一轮的权重。迁移学习场景下,我永远优先加载best.pt做后续测试,因为last.pt可能已经过拟合。如果训练日志里mAP50一直很低而loss正常下降,回数据集检查标注框有没有画错——很多人把虚线车位框的框线本身标注进去了,模型当然学不会。

5. 泊车位目标检测避坑:五条最常见的训练翻车记录

5.1 现象:val的mAP很高,换一个停车场场景全部漏检

这是小数据集做目标检测最容易踩的坑。训练时验证集mAP达到0.9,导出模型后跑到别的停车场测试,一张图只能检出零星几个车位。原因是划分数据集时用了全局随机,某个停车场的几十张相似图片被同时分进训练集和验证集,模型实际上“背”下了这个特定视角的车位外观。解决方法是按场景分组重新划分数据,确保同一个停车场的图片只在训练集或只在验证集;如果数据不够,就减少验证集比例,用test集在另一个停车场拍摄的图上做一次盲测。模型评估看的是“没见过的车位”上的表现,而不是“没见过的图片”。

5.2 现象:训练完检测不到空车位,只检测到有车的泊位

检测结果总是漏掉空车位,占用类车位却检得很好。原因通常是类别不平衡:数据集中“占用”类标注框数量远大于“空闲”类,模型学到的是“找车位轮廓”而不是“区分占空”。更隐蔽的原因是两类标注框的形态差异很大,“占用”类框里有车体轮廓,特征丰富;“空闲”类框只有地面标线,特征弱,同样的学习率下模型自然偏向学特征强的类别。解决思路有两个,一是给“空闲”类提高loss权重,在data.yaml里设置cls参数增大分类损失的系数;二是做类别平衡采样,保证每个batch里两类样本数量接近,ultralytics里可以开启class_weight选项。

5.3 现象:txt标签里出现0字节文件,训练时报错

训练到一半打印出类似WARNING: corrupt image或empty labels的提示,然后跳过某张图。原因是VOC转YOLO时,有些xml文件里没有有效的<object>节点,生成的txt是空文件;或者是标注框坐标越界,被脚本过滤后整行没写进去。暴力解决办法是在划分脚本里加一个过滤条件,扫描转换后的txt,把0字节文件对应的图片从数据集里剔除:

find labels -name "*.txt" -size 0 -exec sh -c 'mv "$1" /tmp/empty_labels/' _ {} \;

更稳的办法是用ultralytics自带的yolo check命令验证标签格式。

5.4 现象:loss下降到0.02但验证指标不再上升

训练日志里训练loss每轮都在降,看着很舒服,但mAP50在第60轮后开始波动甚至回调。这是典型的过拟合特征,1000张图训练100轮很容易把标注噪声也背下来。解决方法是提前用早停,或者把patience从20改小到10;另一个有效手段是增加dropout和权重衰减,在训练命令里加上dropout=0.1和weight_decay=0.0005。最重要的是加载best.pt而不是last.pt做推理,best.pt所在的那一轮通常还在正常泛化区间内。

5.5 现象:摄像头视角换一个,检测框整体偏移

同一个停车场,摄像头装在3米高和5米高拍出来的画面里车位框大小差异极大,模型的检测框会出现系统性偏移。这是小样本数据集测试时常见的“视角过拟合”。处理办法是在训练时打开degrees=10做小角度旋转增强,同时用translate=0.1做平移增强,模拟视角变化。如果数据集里不同视角的图片本来就有,优先保证划分时每个视角都在训练集和验证集里都有对应样本。不要寄希望于把模型“调大”,换成yolov8m不会根治视角泛化问题,本质是训练数据覆盖不足。

6. 验证泊车位检测效果:从mAP数字到逐帧画框的最后一公里

6.1 输出验证集预测图与混淆矩阵:用官方脚本一步到位

mAP是一个数字,但部署前必须看可视化结果。我训练完第一件事是跑验证并保存预测图:

yolo detect val \ model=runs/detect/train/weights/best.pt \ data=data.yaml \ save_json=True \ save_conf=True \ plots=True

plots=True会在验证集每张图上画出预测框、类别名和置信度,直接翻看这些图能快速发现系统性问题。save_json=True会输出COCO格式的检测结果文件,方便你用自己的脚本计算各类别的AP。重点看两类图片:一类是“占用”车位被误判成“空闲”的,这类错误通常来自形态非常相似的车位;另一类是漏检的,车位线模糊或光照反光时模型容易看不见。

6.2 单帧推理与视频流检测:确认边界框坐标和置信度阈值

验证完静态图,再拿一段停车场监控视频做实时推理测试。常用命令:

yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=test_video.mp4 \ conf=0.4 \ save=True

conf=0.4是最小置信度阈值。泊车位检测的阈值选择有明显倾向:如果用于车位引导系统,漏检一个空车位比误报一个占用位更影响体验,可以把阈值调低到0.25;如果用于收费闸机统计车辆,误报会导致计费出错,阈值要调高到0.6。4K分辨率的视频建议在推理前把输入尺寸从640提升到960,imgsz=960能明显改善远距离小车位的检出率,代价是推理速度下降约一倍。

6.3 从检测框到可用功能:关键点回归是泊车位检测的进阶方向

到这里,用这个1000张图片的数据集训练YOLO泊车位检测模型的流程已经完整跑通。但说实话,纯边界框检测做泊车引导有个天然缺陷:框不能告诉你车位的四个角点在哪,也就无法判断车辆能否准确停入。从业方案里常见的进阶做法是改用YOLO的关键点检测,把每个车位的四个角点作为关键点输出,再根据角点几何关系推算车位朝向和空位面积。你也可以把“占用”和“空闲”的分类任务升级成实例分割,用YOLO-seg模型分割出车位的精确轮廓,对斜线车位的适应性更强。

我的习惯是每次训练完都把best.pt、data.yaml、划分脚本的随机种子和最终的超参数一起存档,标注数据的迭代一定会再训练,没有记录就等于没有发生过这次实验。数据集1000张不够,我通常先用它确定模型结构和参数范围,再决定是补标注还是做数据增强。技术和流程都是这套东西,真正拉开效果差距的是对数据的理解和反复试错。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询