
简介面向停车位检测任务的数据集资源基于YOLOv8格式构建适用于目标检测模型训练、验证与测试也可作为智慧停车、自动驾驶场景中车位识别算法的训练样本。数据集包含1520张图像划分为训练集1380张、验证集119张、测试集21张每张图像均配套txt格式标注文件并附有yaml配置文件可直接接入YOLOv8训练流程。图像统一处理为640x640分辨率并已做EXIF方向矫正同时通过水平/垂直翻转、随机旋转、亮度调整、高斯模糊及椒盐噪声等增强方式将每个源图像扩展为3个版本有效提升模型泛化能力。全部数据打包为zip格式共2000个文件含1520个txt标注、479张jpg图像及1个yaml配置压缩包大小164.36MB。该资源已有167人学习下载适合开展停车位识别、车位占用检测等项目的开发者或研究人员直接取用。1. 停车位检测为什么常被当成目标检测里的“翻车重灾区”停车场俯视画面里一个空位和一辆白色轿车顶部在视觉上常常只差几条边缘线夜间灯光下地锁阴影和相邻车位的黑色路面几乎无法区分。这就是停车位检测和通用目标检测最大的不同——它不是找“有轮子的物体”而是找“没有物体的空缺区域”分类边界天然模糊。很多团队拿COCO预训练权重直接训结果map50看着还行一上实景视频就发现空位误报率飙到30%以上。这篇文章围绕“目标检测-停车位检测数据集yolov8”这个组合做一套完整落地路径先讲清停车位检测为什么不能照搬通用检测方案然后给出数据集从采集、标注到格式整理的完整步骤再把YOLOv8训练参数、损失曲线分析和常见训练事故一次讲透最后落到部署验证和推理速度优化上。适合正在做智慧停车、园区安防或者只是想用YOLOv8练手自动驾驶感知的工程师目标是让你拿到一个真实车位数据集后能独立完成从数据到可部署模型的全流程而不是只跑通一个demo。2. YOLOv8做停车位检测之前先想清楚要检测的到底是什么2.1 车位检测的本质是“空位判定”不是“物体识别”通用目标检测的数据集里每个目标都有明确的物理边界比如人、车、猫、狗模型学习的核心是“纹理形状”的类别特征。停车位没有实体边界一个空车位在图像里表现为一块“符合车位尺寸的地面区域”。它周围可能有白线、黄线、地锁、车轮挡块也可能因为磨损而完全没有标线只有地面颜色和相邻车辆的相对位置能暗示这里可以停车。这就带来一个模型选型层面的关键问题YOLOv8这类单阶段检测器到底适不适合直接做车位检测我的结论是适合但必须配合恰当的标注策略。如果按“有车/无车”去标相当于让模型同时学会两个任务——找车位区域和判断占用状态这在数据不充分时极易过拟合。更稳的做法是把“空车位”作为唯一正类有车占据的车位不标注模型只回答“这个位置当前是否空缺”。这样训练目标单一正负样本边界也清晰得多。2.2 车位检测数据集的特殊性长宽比和视角分布普通目标检测数据集中目标的宽高比集中在1:1到2:1之间而停车位常见的是2.5:1到4:1的横向长条或者当摄像头装在通道尽头时车位变成纵向2:1到3:1的矩形。这对YOLOv8的锚框初始化影响很大默认的anchor比例在训练初期会产生大量低IoU匹配拉慢收敛速度。视角问题是另一个坑。停车场摄像头高度通常在3米到6米俯仰角30度到60度之间车位在画面中的形状受透视影响严重。同一个车位车头朝里和车头朝外时它的标注框长宽比能相差一倍。所以数据集的视角多样性直接决定模型的泛化能力如果只采集单一摄像头角度的数据模型换个停车场基本就废了。2.3 选YOLOv8的哪个变体n/s/m/l的取舍要看部署端YOLOv8提供n、s、m、l、x五个尺寸很多人一上来就选yolov8m甚至yolov8l理由是精度更高。但如果你的部署目标是RK3588、Jetson Orin Nano这类边缘设备yolov8m在1080p输入下单帧推理已经要到20到30毫秒加上前后处理实际帧率可能在15fps左右这在道闸场景勉强够用在园区连续监控里就不太行了。我更推荐的做法是先在yolov8s上跑通整个流程确认数据质量和训练配置正确再用yolov8m做精度对比。如果s和m在验证集上的map50差距小于3个百分点直接部署s版本因为车位检测的精度瓶颈通常在数据本身而不是模型容量。3. 构建停车位检测数据集从视频抽帧到YOLO格式标注的完整脚本3.1 数据采集不要只拍晴天白天的画面停车位数据集的采集策略直接决定模型上线后的体验。我是按“3个时段、2种天气、2个视角”的矩阵来采集的早中晚三个时段各拍一段晴天和阴天各拍一段俯视和斜视各拍一段。每段视频时长控制在10到15分钟帧率25fps这样一段视频大约能抽到500到800帧有效画面。抽帧不能均匀抽因为停车场的车辆进出是稀疏事件均匀抽帧会有大量重复画面。建议用“运动检测定时抽帧”的组合策略画面中有车辆移动时每5帧抽一帧静止时每30帧抽一帧。用OpenCV实现这个逻辑很简单import cv2 import os def extract_frames(video_path, output_dir, motion_interval5, static_interval30): cap cv2.VideoCapture(video_path) if not cap.isOpened(): raise ValueError(f无法打开视频: {video_path}) os.makedirs(output_dir, exist_okTrue) frame_count 0 saved_count 0 prev_gray None while True: ret, frame cap.read() if not ret: break frame_count 1 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 计算当前帧与前一帧的差异判断是否有运动 if prev_gray is not None: diff cv2.absdiff(gray, prev_gray) motion_score diff.mean() interval motion_interval if motion_score 2.0 else static_interval else: interval static_interval motion_score 0.0 # 按动态间隔抽帧 if frame_count % interval 0: filename fframe_{saved_count:06d}_m{motion_score:.1f}.jpg cv2.imwrite(os.path.join(output_dir, filename), frame) saved_count 1 prev_gray gray cap.release() print(f抽帧完成共保存 {saved_count} 帧到 {output_dir})这里的motion_score阈值2.0是我调试下来的经验值它表示前后帧灰度差均值超过2时认为有车辆移动。实际使用中你需要根据自己视频的分辨率和压缩质量微调这个值画面越清晰运动帧的motion_score越高。抽出来的帧建议保留原始分辨率不要提前缩放标注和训练时的imgsz参数可以后期再控制。3.2 标注策略用LabelMe还是LabelImg以及“该标什么不该标什么”车位检测的标注工具我在LabelImg和LabelMe之间最终选了LabelMe。原因是车位边界常常是不规则的四边形普通矩形框会把相邻车位的边缘也框进来产生严重干扰。LabelMe支持多边形标注能精确贴合车位线的内边界。标注时有一条核心原则只标注“当前时刻处于空缺状态的车位”并且只标可用的标准车位。具体来说有几类区域不要标已被车辆占用的车位、残疾人专用车位如果业务不需要识别它、宽度不足2.2米的畸形车位、画面边缘处被严重截断的车位。这些“不要标”的区域如果硬标进去会让模型学到错误的车位宽度和位置先验。每帧画面建议至少标注5到8个空车位标注框要紧贴车位线内侧。如果车位没有标线水泥地停车场常见就以相邻车辆的中线为边界标注间距比标准车位略微收紧。一个1000帧的数据集单人标注大约需要12到15小时这个时间成本要在项目排期里预留。3.3 从LabelMe JSON转成YOLOv8格式转换脚本与坐标归一化LabelMe导出的每个标注帧是一个JSON文件里面记录的是多边形的绝对坐标。YOLOv8训练需要的是每张图对应一个txt文件每行格式为“class_id x_center y_center width height”坐标全部归一化到0到1之间并且用矩形框表示。关于多边形转矩形框我经历过几次翻车。最稳妥的方案是用多边形的最小外接矩形而不是简单取x和y的极值。因为标线斜着时直接取极值会把框撑得过大混入邻车位的背景。最小外接矩形可以用OpenCV的cv2.minAreaRect实现但要注意它返回的角度需要把旋转矩形转换回轴对齐矩形再做归一化。import json import os import cv2 import numpy as np def labelme_json_to_yolo(json_path, output_dir, class_nameempty_spot): with open(json_path, r, encodingutf-8) as f: data json.load(f) image_path data[imagePath] image cv2.imread(os.path.join(os.path.dirname(json_path), image_path)) if image is None: raise FileNotFoundError(f无法读取图片: {image_path}) h, w image.shape[:2] txt_name os.path.splitext(os.path.basename(image_path))[0] .txt output_path os.path.join(output_dir, txt_name) yolo_lines [] for shape in data[shapes]: if shape[label] ! class_name: continue points np.array(shape[points], dtypenp.float32) # 计算最小外接矩形避免斜车位导致标注框过大 rect cv2.minAreaRect(points) box cv2.boxPoints(rect) box np.int0(box) x_min box[:, 0].min() x_max box[:, 0].max() y_min box[:, 1].min() y_max box[:, 1].max() # 归一化到0-1并转换为YOLO格式的cx,cy,width,height x_center ((x_min x_max) / 2) / w y_center ((y_min y_max) / 2) / h box_width (x_max - x_min) / w box_height (y_max - y_min) / h # 过滤掉面积过小的标注可能是误标注 if box_width 0.01 or box_height 0.01: continue yolo_lines.append(f0 {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}) with open(output_path, w, encodingutf-8) as f: f.write(\n.join(yolo_lines)) return len(yolo_lines) # 批量处理一个目录下的所有JSON def batch_convert(json_dir, output_dir): os.makedirs(output_dir, exist_okTrue) total_boxes 0 for filename in os.listdir(json_dir): if filename.endswith(.json): count labelme_json_to_yolo( os.path.join(json_dir, filename), output_dir ) total_boxes count print(f转换完成共标注 {total_boxes} 个车位框)这段脚本中cv2.minAreaRect返回的旋转矩形在后续取极值时仍然是轴对齐的但因为这个矩形已经是最小外接比直接取原多边形坐标极值要紧凑得多。过滤条件box_width 0.01是用来剔除那些标注时手滑画出的极窄框这种框通常是误操作留着会干扰训练。3.4 数据集划分与目录结构YOLOv8训练前的“后悔药”数据集划分看起来是个小步骤但划分不合理时后面所有训练结果都是白费。我见过不少人直接用train_test_split随机划分结果同一个停车场同一个摄像头角度下的帧同时出现在训练集和验证集里验证指标虚高换场景就现原形。正确做法是按“场景”划分而不是按“帧”划分。如果你采集了3个停车场的数据那验证集应该完整包含其中1个停车场训练集包含另外2个停车场。这样验证集和训练集在场景上完全隔离指标才有参考价值。目录结构按YOLOv8的标准来parking_spot_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamldata.yaml是YOLOv8读取数据配置的入口内容如下# 数据集配置path是数据集根目录的绝对路径 path: /home/user/parking_spot_dataset train: images/train val: images/val test: images/test # 类别定义单类检测 nc: 1 names: 0: empty_spot注意path字段必须写绝对路径但train、val、test相对path来写就行。这样YOLOv8的train.py在解析时会自动拼接成images/train的完整路径不易出错。如果多台机器同步训练可以用相对路径加软链接的方式处理但新手不建议折腾这个。4. 用YOLOv8训练停车位检测模型关键参数设置与损失曲线判读4.1 训练命令与核心参数imgsz、batch、epochs的最优组合训练启动命令本身很简单但参数组合的合理性直接决定收敛速度和最终精度。我一般用下面的命令作为基准配置yolo detect train \ model/home/user/parking_spot_dataset/weights/yolov8s.pt \ data/home/user/parking_spot_dataset/data.yaml \ epochs100 \ imgsz640 \ batch16 \ lr00.005 \ lrf0.01 \ warmup_epochs3 \ patience15 \ device0 \ project/home/user/parking_spot_train \ nameexp_parking_spot \ optimizerSGD \ cos_lrTrue \ close_mosaic5这里几个参数是专门针对停车位场景调过的。imgsz640是精度和速度的平衡点车位检测不需要看到太小的纹理细节640足够batch16在单卡12GB显存下能稳定运行如果显存不足可以降到8lr00.005比YOLOv8默认的0.01低一半因为车位数据集通常只有几千张学习率太大极易震荡close_mosaic5表示最后5个epoch关闭mosaic增强让模型在接近真实分布的数据上微调这个对车位检测帮助很大因为mosaic拼接会产生大量跨车位的人造边界训练后期会影响定位精度。4.2 YOLOv8的训练增强与车位小目标问题YOLOv8默认开启的增强有一大串但对于车位检测有一部分需要谨慎处理。hsv_augment色彩增强可以保留停车场不同时段色温差异大这有助于提升光照泛化。但fliplr水平翻转我不建议开因为车位标线在翻转后方向箭头和宽度关系会发生语义错误模型可能学到错误的左右不对称特征实测开着fliplr训练验证集精度反而下降1到2个点。车位在画面中属于中等尺寸目标一般占图像的2%到8%不算小目标。所以不需要启用SAHI这类切片推理方案。但如果你用的是高位全景摄像头车位宽度可能只有30到40个像素这时候应该把imgsz提升到960或者1280而不是依赖SAHI——前者训练和推理是一体的后者在部署时要额外挂一个预处理模块复杂度不值得。4.3 损失曲线怎么看不只看loss数值要看三个信号训练跑起来之后YOLOv8会在训练目录下生成results.png里面有box_loss、cls_loss、dfl_loss和精度曲线的趋势。很多人只看box_loss降没降但车位检测更要关注的是cls_loss——因为空车位和地面的分类边界是最容易出错的点。我判断训练是否正常的三个信号按优先级排列。第一box_loss在前10个epoch应该从初始值快速下降下降曲线应该陡峭且光滑若出现锯齿状波动通常是batch太小或学习率太高。第二验证集的map50曲线应该随着训练稳步上升如果训练集map50已经到0.9以上而验证集长期徘徊在0.6以下这是过拟合信号需要增加数据或增强正则化。第三在训练的后20个epochmap50-95曲线应该仍然有缓慢上升的趋势如果提前进入平台期大概率是数据多样性不足而不是模型容量不够。4.4 训练常见事故排查loss为NaN和验证集精度为0训练过程中最常见的翻车是loss变成NaN。我第一次训车位数据时也遇到了当时第一反应是调低学习率但问题不在学习率而是数据里有异常的标注框——某个JSON转换时出现了宽度为0的归一化框导致损失函数中除以宽高时产生无穷值。解决方法是训练前对标签文件做一次全量校验import os def validate_labels(label_dir): problems [] for filename in os.listdir(label_dir): if not filename.endswith(.txt): continue with open(os.path.join(label_dir, filename), r) as f: lines f.readlines() for line in lines: parts line.strip().split() if len(parts) ! 5: problems.append(f{filename}: 格式错误) continue _, cx, cy, w, h parts cx, cy, w, h float(cx), float(cy), float(w), float(h) # 检查坐标是否在有效范围内 if not (0 cx 1 and 0 cy 1): problems.append(f{filename}: 中心坐标越界) if w 0 or h 0 or w 1 or h 1: problems.append(f{filename}: 宽高非法) return problems另一个经典问题是验证集精度为0。这不一定是模型没学好很可能是数据集划分时验证集目录和标签目录对不上或者data.yaml里的val路径写错指向了空目录。先检查验证集目录下图片数量和标签文件数量是否一致再检查验证集标签里有没有和训练集重复的图片文件名——如果复制数据时把有的图片覆盖了YOLOv8会报datasets error但不会中断训练只是验证时全部跳过。5. 停车位检测的五个致命坑从数据采集到模型部署的血泪经验5.1 坑一把“有车占用”的车位也标注了现象训练时loss曲线正常收敛map50能到0.85以上但推理时模型频繁把有车的车位也识别为空位。原因标注阶段没有严格执行“只标空车位”的原则部分有车车位的框被标了进去。模型学到的是“矩形区域内有停车线就是空位”而没有真正学到“区域内没有车辆才是空位”。解决用LabelMe重新检查所有标注删除有车车位的框。如果数据量太大可以先训练一个粗糙模型用模型预测结果人工抽检的方式筛选可疑标注这样比纯人工复查效率高不少。5.2 坑二多段视频抽出来的数据高度相似验证集虚高现象训练时map50一路飙升到0.95以上但换个停车场摄像头测试精度直接掉到0.4以下。原因采集视频时在同一时段同一角度拍了多段抽帧后大量画面只有车辆位置有细微变化背景几乎完全相同。随机按帧划分训练集和验证集时验证集里的帧和训练集里的帧在背景上几乎一模一样模型记住了背景纹理指标虚高。解决按视频文件为单位划分数据每个视频的帧只能出现在同一个集合中并且确保训练集和验证集来自不同停车场或不同摄像头角度。5.3 坑三mosaic增强在车位数据上帮倒忙现象开了mosaic训练loss下降更快但推理时对单块地面的误检率升高。原因mosaic把4张图拼在一起车位标线被切断模型学到了“有碎片化线条的地方就是车位”的错误先验。推理时真实画面里地面的一条裂缝或水渍被放大成了类似特征。解决在训练后期关闭mosaic配置close_mosaic10到15个epoch。如果数据量本身不够大也可以直接把mosaic关掉用基本的平移、缩放、色彩抖动增强代替。5.4 坑四夜间数据不足模型在低光照下全部“瞎猜”现象白天测试效果不错晚上指示灯一亮检测框开始乱跳空位识别率降到30%以下。原因停车位检测高度依赖车位线的边缘对比度夜间标线在车灯照射下呈现完全不同的纹理。模型在训练时没有见过这种光照分布特征提取全部失效。解决采集夜间数据时不能简单调高ISO那样会产生大量噪点模型会把噪点当作特征。正确做法是保留原始画面同时可以加入适度伽马校正的变体让模型学到不同亮度下的车位形态。夜间数据至少要占总数据量的30%。5.5 坑五导出部署模型时忽略输入尺寸导致精度突然下降现象onnx转换后在PC上用OpenCV推理精度和PyTorch验证时基本一致但部署到板端后map50掉了5到8个点。原因板端推理框架如RKNN、TensorRT在输入尺寸和PyTorch不一致时会自动做resize但resize算法不同双线性vs最近邻车位标线的边缘会被拉糊小尺寸车位的定位精度急剧下降。解决导出onnx时固定输入尺寸为训练时的imgsz并用opencv-python里的dnn模块做推理验证确认输入预处理逻辑和训练时完全一致包括归一化方式。不要省掉这一步板端debug的成本远高于本地验证的时间。6. 部署验证的一小时排障法从PyTorch模型到实际推理的快速检查项当你训练完模型准备部署时不要急着往板端烧先用一个脚本在本地把推理全链路验证一遍。我习惯用下面的流程一小时能排查掉80%的部署问题。先验证PyTorch模型本身的输出确认类别和置信度阈值设置合理。车位检测是单类问题置信度阈值建议设在0.35到0.45之间比通用检测的0.25要高因为空车位的误报代价远高于漏报——把空位识别成占用只会少收一笔停车费把占用识别成空位会导致车主开进去才发现有车这是安全事故级别的错误。NMS的IoU阈值保持默认的0.45即可但要注意每个车位的候选框可能多达5到10个NMS计算量在边缘设备上不可忽视。from ultralytics import YOLO # 加载训练好的模型验证保存路径 model YOLO(/home/user/parking_spot_train/exp_parking_spot/weights/best.pt) # 单张图片推理测试耗时和输出结构 results model.predict( source/home/user/parking_spot_test/night_01.jpg, conf0.4, iou0.45, imgsz640, devicecpu, # 先用CPU验证排除GPU引入的差异 verboseTrue ) # 解析推理结果检查框的数量和坐标范围 boxes results[0].boxes if boxes is not None: xyxy boxes.xyxy.numpy() # 左上角和右下角坐标 scores boxes.conf.numpy() print(f检测到 {len(xyxy)} 个车位最高置信度 {scores.max():.3f}) # 检查是否有重复框同一位置出现多个高置信度框 for i, score in enumerate(scores): if score 0.8: print(f框 {i}: {xyxy[i]}, conf {score:.3f})这段脚本的核心作用是做“输出口径检查”。部署到板端之前你需要在本地确认几个事实模型输出的检测框数量是否符合场景预期高置信度框是否均匀分布在不同车位而不是扎堆在某一区域CPU推理单帧耗时是多少如果CPU已经需要300毫秒板端即使有NPU加速帧率也可能达不到实时要求。确认这些之后再切到GPU或NPU做精度对比通常不会出现大的意外。我踩过最深的一次坑是模型量化后定位框整体偏移了半个车位宽度排查了两天最后发现是量化校准集里没有包含夜间图像导致模型在低光照条件下的输出分布和浮点模型偏差过大。所以做INT8量化时校准集一定要按训练集的比例混合白昼和夜晚图像而不是随便找几十张白天画面凑数。这是我从那个项目之后一直保留的习惯也希望帮到你。本文还有配套的精品资源点击获取