简介:目标检测中,光照变化是影响模型鲁棒性的关键因素。车牌检测作为智能交通和安防场景的核心任务,在夜间强光或逆光环境下常出现漏检、误检。为了提升模型对光照的适应能力,困难样本挖掘成为数据构建的重要思路。CCPD2019数据集中的5000张暗亮光车牌子集,正是针对这一痛点设计,通过筛选低照度和高反光场景,拓宽模型在光照维度上的特征分布。该子集采用LabelMe多边形标注格式,保留了车牌四角点信息,可灵活转换为YOLO水平框、OBB旋转框等训练格式,便于结合数据增强策略进行专项调优。无论是停车场管理、出入口道闸,还是智能交通项目,利用该子集进行针对性训练和分组评估,都能有效提升检测模型在复杂光照下的表现。本文围绕数据集结构、格式转换、训练配置及常见坑点,提供了一套可落地的工程实践方法。
1. 这5000张图到底值在哪:CCPD2019子集的光照筛选逻辑
做车牌检测的人应该都有这种体会:白天大太阳底下车牌反光一片白,夜间弱光下车牌糊成一团,这两种场景下模型表现断崖式下跌。CCPD2019本身是国内公开车牌数据集里覆盖面比较全的一个,但如果直接拿全量数据训练,光照问题容易被淹没在大量正常样本里,模型学不到足够强的光照鲁棒性。所以把“光线较暗或较亮”这部分单独拎出来,凑成5000张子集,本身就是一种很典型的困难样本挖掘思路。
这套子集的设计意图很明显:不是让你拿它替代完整数据集,而是用它做针对性增强、做鲁棒性验证、做光照场景的专项调优。适合三类人:一是正在做智能交通、停车场管理、出入口道闸项目的工程师,二是用YOLO系列训练自定义数据集但总被光照变化困扰的学生和研究者,三是需要快速验证数据增强策略是否有效的算法调优人员。
1.1 为什么光照问题这么难搞
车牌检测本质上是在一张大图上找到车牌的精确位置,后续还要做字符识别。这一步如果定位不准,后面识别正确率再高也白搭。光照对定位的影响主要体现在两方面:对比度下降导致边缘模糊,以及高光/阴影导致的纹理失真。亮光场景下,金属车牌表面反光会让字符区域变成亮斑;暗光场景下,车牌与车身、背景的灰度差异变小,目标几乎融入环境。
我自己的实测经验是,普通场景训练出来的模型,在正常光照下mAP能到95%以上,一遇到逆光或者夜间,mAP直接掉到70%甚至更低。这不是模型结构的问题,而是训练数据里困难样本太少。CCPD2019这个子集正好补上这一块,5000张暗亮光样本足以让模型在光照维度上的特征分布明显拓宽。
1.2 为什么偏偏是LabelMe格式
现在业界常用的标注格式有VOC XML、YOLO TXT、COCO JSON,LabelMe其实相对小众,但LabelMe有一个不可替代的优势——原生支持多边形标注。车牌不是正矩形,尤其是在斜视角和透视畸变下,用外接矩形框会把大量背景包含进来,导致训练时背景干扰大。CCPD系列本身就是用四角点标注车牌四边形,LabelMe的polygon格式可以完美保留这四个关键点,而不是退化成轴对齐矩形框。
另一个考虑是LabelMe格式的通用性。LabelMe的JSON结构非常直白,label字段标类别,points字段存多边形顶点,几乎没有学习成本。拿到手之后,无论是转成YOLO水平框、YOLO OBB旋转框,还是转成COCO的segmentation多边形,代码都很好写。相比之下,如果一开始就用YOLO TXT存四个归一化坐标,虽然训练省事,但扩展到旋转框或语义分割就麻烦了。所以这个数据集选择LabelMe格式,等于把“任意二次加工”的可能性留给了使用者。
2. 数据集结构解析:从文件命名到LabelMe JSON
拿到数据集第一件事不是急着训练,先把文件结构和标注格式吃透。我自己接手过不少公开数据集,很多坑都出在没搞懂格式细节就直接转换,结果训练的时候发现坐标全部飘了。
2.1 文件目录与命名的门道
这套子集文件名带“part01”,说明是分卷发布的,后续大概率有part02甚至更多。序号从1到4999,也就是5000张图片,但这里有个小细节要注意:很多数据集命名从0开始,或者中间有跳号。如果你的文件是1.jpg到4999.jpg,那就是正好5000张;如果看到的是0.jpg到4999.jpg,那实际上有5000张但编号多了一个。我个人建议拿到数据后先写个脚本统计一下图片总数和JSON文件总数,确认数量对得上再往下走。
目录组织方面,一般有两种常见布局:一种是所有图片和同名JSON放在同一个文件夹,另一种是images和labels分开。LabelMe默认是图片和JSON同目录,如果你拿到的是这种结构,建议第一步就按训练习惯重新整理。我的习惯是建一个dataset目录,里面分images和annotations,图片归一化命名,JSON单独存,这样后续无论是转格式还是做数据划分都省事。
2.2 LabelMe JSON字段逐项拆解
一个标准LabelMe JSON文件长这样:
{ "version": "5.2.1", "flags": {}, "shapes": [ { "label": "plate", "points": [ [812, 524], [968, 526], [967, 556], [811, 554] ], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "0001.jpg", "imageData": null, "imageHeight": 1160, "imageWidth": 720 }字段拆开看就清楚了:imagePath是图片文件名,imageHeight和imageWidth是图片尺寸,这两个数值在转换格式时是归一化的分母,一定不能写错。shapes里是一个列表,每辆车对应一个shape。label是类别名称,points是多边形顶点坐标,shape_type一般标成polygon,flags是额外标记位,这个数据集里多半是空。
这里特别提醒一下imageData字段,LabelMe选图保存时会把原图以base64形式塞进去,但如果图比较大,这个字段会变成null,图片单独存。训练时需要的是图片文件和标注文件对应,不需要也没必要去解析imageData里的base64内容。如果你打开JSON发现这个字段很长,直接忽略即可。
2.3 车牌四点的标注顺序与坑
CCPD系的车牌标注是四个角点,这个顺序特别重要。LabelMe的points数组是按你点击时的顺序存的,而不同工具对四点顺序的约定不一样:有的是左上、右上、右下、左下,有的是顺时针存,有的是逆时针。转换格式时如果不统一,训练出的检测框会乱转,尤其在做OBB旋转框时,四点的顺序直接决定旋转角度计算是否正确。
稳妥的做法是转换前先把四个点按几何关系重排。我在代码里会先算四个点的包围盒中心,然后按每个点相对中心的角度排序,统一成“左上、右上、右下、左下”的顺序。这个过程看起来多余,但能避免很多莫名其妙的问题。尤其当你拿到手的标注有可能是人工点击顺序随意记录的,你根本无法保证每个JSON里的顺序一致。
3. 数据处理与格式转换:从LabelMe到训练所需格式
拿到LabelMe标注后,正常情况下不会直接拿来训练。YOLOv8、YOLOv5这些主流框架默认读的是TXT格式,MMDetection默认读COCO JSON。所以转换这一步绕不开。好消息是LabelMe转其他格式都很简单,核心就是把points坐标按规范重新写一遍。
3.1 先做一轮数据清洗
转换之前我强烈建议先做一次数据完整性检查。我自己踩过这个坑,训练到一半发现loss异常,排查了半天才发现是一张图的标注文件里imagePath对应的图片根本不存在。
检查项主要有四个:
- 图片文件是否有对应JSON,JSON是否有对应图片,数量是否一致;
- 每个JSON里
imageWidth和imageHeight是否与图片实际分辨率一致; shapes是否为空,多边形点是否至少4个;- 类别标签是否统一,比如有的文件写
plate,有的写car_plate,这种不一致要提前改掉。
写个Python脚本跑一遍,几秒钟就能筛出全部问题文件。对于异常文件,我的处理原则是:宁可删掉也不要勉强保留。5000张数据里少几张不影响大局,但一条脏数据可能让模型多花几百轮去修复。
3.2 LabelMe转YOLO格式实操代码
YOLO格式的标注是每行一个目标,五个数值:class_id x_center y_center width height,全部是相对于图片宽高的归一化值。对于车牌这种四边形标注,最简单的转换方式是取四个点的外接矩形。代码如下:
import json import os def labelme_to_yolo(json_path, img_width, img_height, class_id=0): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) lines = [] for shape in data['shapes']: points = shape['points'] xs = [p[0] for p in points] ys = [p[1] for p in points] min_x, max_x = min(xs), max(xs) min_y, max_y = min(ys), max(ys) x_center = (min_x + max_x) / 2.0 y_center = (min_y + max_y) / 2.0 width = max_x - min_x height = max_y - min_y x_center_norm = x_center / img_width y_center_norm = y_center / img_height width_norm = width / img_width height_norm = height / img_height lines.append(f"{class_id} {x_center_norm:.6f} {y_center_norm:.6f} {width_norm:.6f} {height_norm:.6f}") return lines这份代码的逻辑不复杂,但有个点要提醒:归一化分母必须用图片原始宽高,而不能用标注里的imageWidth和imageHeight去推。虽然这两个值理论上应该和图片一致,但在手工标注或数据集下载过程中,遇到过分辨率被修改但JSON没同步更新的情况。最保险的做法是用PIL或者OpenCV读取图片实际尺寸,以此为准。
另外,如果你的标注里有多辆车,class_id都填0就可以,因为车牌只有一个类别。如果后续你打算区分蓝牌、黄牌、新能源绿牌,那就要按label字段做映射,不同label对应不同类别ID。
3.3 需要旋转框?再转一步OBB
车牌检测里有个场景很常见:车辆斜停、照片侧拍,车牌在画面里是一个倾斜四边形。这时候水平框会把一大片无关背景包进来,尤其是当车牌小、周围环境复杂时,水平框的IoU虚高但实际定位不准。解决方式是训练旋转框模型,比如YOLOv8-OBB或者MMRotate里的旋转检测算法。
LabelMe转OBB格式,本质上是在外接矩形基础上增加一个角度信息。旋转框格式一般是class_id x_center y_center width height angle,其中angle是相对于图像x轴的角度。转换时可以直接从四个点里拟合最小外接旋转矩形。OpenCV里已经有现成函数cv2.minAreaRect,拿四个点输入,输出的就是(center_x, center_y), (width, height), angle。不过要注意OpenCV的angle定义和YOLOv8-OBB的angle定义不完全一致,坐标轴方向、角度正负都可能相反,转出来之后最好可视化验证一遍。
我第一次转OBB的时候没做可视化检查,结果训练出来的预测框全部长了“反骨”,角度全偏了90度。后来学乖了,每次转格式都随机抽几十张图,把标注框画回去看看位置对不对再继续。这一步看起来多余,其实省的是后面debug的功夫。
4. 基于这5000张图训练车牌检测模型
数据准备好了,接下来就是训练环节。我用这套数据在YOLOv8上做过完整训练,下面把整个过程和关键参数梳理一遍,直接照着做就能跑通。
4.1 环境与数据准备
环境建议直接用YOLOv8官方镜像或者基于Python 3.9+的虚拟环境。安装就两行命令:
pip install ultralytics pip install opencv-python数据目录按这个结构组织:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ └── val/图片和标注文件要一一对应,同名只是后缀不同。划分比例我建议按8:1:1或者直接用train/val二分的9:1。5000张数据不算多,验证集留500张左右足够看出模型效果。
4.2 data.yaml与训练配置
YOLOv8通过一个YAML文件描述数据集信息,文件内容如下:
path: /path/to/dataset train: images/train val: images/val names: 0: plate这里一个容易忽略的细节是path字段。如果这个路径写错了,ultralytics会去默认路径找数据,然后报错找不到文件。建议用绝对路径,不要用相对路径,避免换机器后路径失效。
训练命令如下:
yolo detect train \ data=/path/to/data.yaml \ model=yolov8n.pt \ epochs=300 \ imgsz=640 \ batch=16 \ lr0=0.01 \ augment=True模型选择上,如果追求速度和部署轻便,用yolov8n或者yolov8s;如果精度优先而且GPU显存够大,用yolov8m或者yolov8l。我的实测是,n模型在验证集上mAP50大概能到90%左右,l模型能到96%左右,但推理速度差了好几倍。工业场景里,我通常选s或m,平衡点比较合适。
4.3 针对暗亮光的增强策略
这个数据集的特点就是暗光和亮光,所以训练时数据增强要特别关注亮度扰动。YOLOv8的增强参数里有hsv_h、hsv_s、hsv_v,分别对应色调、饱和度、明度的随机扰动。默认的hsv_v是0.4,但对于这套暗亮光数据,我建议加大到0.6左右,让模型在明暗变化上有更强的泛化能力。
另外还可以手动加一些额外的图像处理:随机gamma校正、随机对比度调整、模拟夜间低照度的噪声。我写过一个简单的预处理函数,在训练前对部分图片做gamma变换,实验下来确实能提升夜间场景的检测效果。这些操作本质上是把数据集里的光照分布进一步拓宽,让模型见过更多“极端情况”。
4.4 指标怎么看到底准不准
训练结束后不要只盯着mAP看,要结合使用场景看具体指标。车牌检测有两个重点关注项:一是小目标的召回率,二是不同光照条件下的稳定性。
这里有一个经验:把验证集按图片亮度分成三组(暗、正常、亮),分别计算每组mAP。只报一个总mAP会掩盖光照差异的问题。我在实际调优中就发现过一个模型总mAP 93%,但暗光组只有82%,亮的组95%。如果不分组看,你根本不知道模型的短板在夜间。这个分组评估的小工具不复杂,用OpenCV算每张图的平均亮度,按阈值归类,然后分开跑验证就行,但信息密度比单看一个总指标高很多。
5. 我在这个数据集上踩过的坑与排查技巧
这部分是纯经验分享。我在处理车牌检测数据集时,尤其是这种带光照专项的场景,遇到过不少让人抓狂的问题,写出来给大家做个参考。
5.1 标注阶段的坑
LabelMe标注最麻烦的是类别名不统一。如果有多个标注员或多次标注,同样的车牌可能被标成plate、car_plate、license,看起来是小事,但转类别ID时容易出错。处理办法是在清洗脚本里做一次类别映射,把所有非预期类别统一替换掉。
另一个坑是标注点顺序。前面提过一遍,这里再强调一下:转换前一定要把四个点按统一规则排序。尤其当一张图里有多个车牌时,不同标注员点击的顺序可能完全不同,不排序就直接转换的话,旋转框的angle会乱。
5.2 转换与训练阶段的bug
常见的是转YOLO后发现归一化坐标出现大于1或小于0的值。原因一般有两种:一是标注点有超出图片边界的,二是坐标除以了错误的宽高。解决办法是在转换脚本里加边界裁剪,点坐标限制在[0, width]和[0, height]范围内。
训练阶段如果loss不稳定或收敛慢,先检查标注文件和图片是否严格对齐。我遇到过一个情况是图片顺序按文件名排列,但标注文件是按修改时间排列的,错位之后模型相当于在随机学一堆错误映射,效果自然烂。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 训练后预测框整体偏移 | 归一化分母用了JSON里存的宽高,但图片被resize过 | 用OpenCV重新读取图片尺寸做分母 |
| 预测框是歪的或角度反了 | 四点顺序不统一,OBB角度定义不一致 | 统一重排四点为左上、右上、右下、左下,并可视化验证 |
| 暗光图完全检测不到 | 数据增强不够,暗光样本占比不足 | 加大hsv_v扰动,额外加gamma校正和低照度模拟 |
| 同一个车牌输出多个框 | NMS阈值太低,或类别置信度分布宽 | 调高NMS的iou阈值到0.5以上,检查标签是否有重复目标 |
| 训练时loss跳变 | 存在异常标注,如坐标越界或空标注 | 清洗脚本逐文件检查,删除异常数据 |
| 验证集mAP高但实际场景差 | 训练集和实际场景分布不一致 | 按光照分组评估,针对短板补充数据 |
最后再分享一个我自己的习惯:每次拿到这种专项数据集,不要急着全量灌进模型。先挑几十张特征明显的图看一眼标注质量,再拿一个小模型快速跑个10轮验证数据可用性,然后再开正式训练。这套流程能让整个实验过程少走很多弯路。这个5000张的CCPD2019子集,如果利用好光照信息,做成一个专项增强样本池,复用到其他车牌检测项目里也能明显提升模型的鲁棒性。
本文还有配套的精品资源,点击获取