简介:一份面向车牌检测与车牌识别任务的高质量图像数据集,素材以黄牌车辆为主,涵盖不同拍摄角度与场景,适合目标检测模型训练、车牌角度适配及后续字符识别等应用。压缩包内共9324个文件,其中包含4662个json标注文件、4627张jpg图片及35张jpeg图片,总体积约257.79MB。标注采用polygon多边形格式,每个车牌用四个顶点精准对应四角,能贴合不同拍摄角度下的车牌形变,相比普通矩形框更具几何适应性。所有图片均经过人工逐张筛选,已剔除模糊、遮挡或存在歧义的样本,数据干净一致,可直接输入检测网络进行训练和字符识别,也可用于目标检测、车牌定位、字符分割等典型任务,显著节省数据清洗与二次标注的时间。当前已有883人学习下载,适合需要规整车牌标注数据的开发者、算法工程师及科研人员。若需转为rectangle矩形框或其他标签格式,可私信作者进行转换。
1. 车牌检测数据集(黄牌)+json标签:先弄清它和蓝牌到底差在哪
做车辆视觉落地的人基本都有同感:车牌检测看着门槛低,真正训起来才发现数据全是脏活。尤其黄牌,货车、拖挂、农用机械上最常见,车牌区域大、字符间距宽、反光严重,网上现成的 json 标签数据集又以蓝牌为主,拿来做黄牌检测几乎等于重新攒一份。这篇不绕弯子,就讲清楚怎么从零搞出一份能直接喂给 YOLO 类模型的“黄牌检测数据集 + json 标签”:原始数据从哪来、json 标签文件怎么拆、如何转成训练格式,以及标注清洗里那些必须提前知道的坑。适合正在用 YOLOv8 或 YOLOv5 训练自己的数据集、但被数据质量卡住的人。
2. 黄牌原始数据从哪来:采集场景、开源现状与视频抽帧命令
2.1 黄牌车的真实分布场景:先定场景再拍,别眉毛胡子一把抓
黄牌车和蓝牌车的分布逻辑完全不同。蓝牌私家车在马路上随处都是,随手截几张图就能开训;黄牌车集中在物流园区、高速收费站、港口闸口、建筑工地、农用车辆检测站这几类场景。如果你打算做通用黄牌检测,我的建议是每个场景至少覆盖三类角度:车头正对、30 度左右斜向、以及上下坡带来的俯仰角度。这直接决定模型能不能在真实项目里扛住。
我一般会在拍摄前先列一个采集清单,把场景分布写清楚,避免数据全堆在同一个角度上。给一个可参考的采集表:
| 场景 | 建议数量(张) | 质量要求 |
|---|---|---|
| 物流园区出入口 | 800 以上 | 车牌宽度占画面 1/8 以上,包含完整边框 |
| 高速收费站 | 500 以上 | 避免前车遮挡,尽量正对或小角度 |
| 港口/工地门口 | 400 以上 | 注意扬尘和泥污导致的局部遮挡 |
| 农用车辆停放区 | 300 以上 | 允许空旷背景,但不要拍成远景过小 |
拍摄时手机、行车记录仪、监控录像都可以,唯一硬指标是车牌最短边不要少于 40 像素。低于这个值,后面标注员自己都看不清楚字符边界,标注质量很难保证。另外,黄牌比蓝牌长一截,标准的 440mmx140mm 尺寸放在蓝牌旁边视觉上就大不少,这会让模型在后续推断时对目标尺寸的预设产生偏差,数据里必须有意识地混入不同距离下的车牌样本。
2.2 开源数据集里黄牌占比很低:别指望拿来就能用
市面上能找到的车牌数据集,比如常见的城市车牌数据集 CCPD,绝大多数样本是蓝牌,黄牌在其中的比例相当低。另一个常被提到的 UA-DETRAC 是交通监控场景,本身定位是车辆目标检测和跟踪,车牌区域太小,直接拿来做车牌检测需要大量裁剪和二次标注,工作量并不比自采小。
- 检查音频、视频、图片、日志、压缩包、文档等各类文件;
- 集成病毒检测引擎,提供扫描报告与威胁级别判定;可对检测文件采取隔离或删除等处置动作;
提示:安全产品若缺少对压缩包内文件的深度检测,常常让攻击载荷借壳潜入,这是文件检测落地时必须补齐的一环。
我的做法是:拿开源数据做预标注的种子,而不是直接作为训练集。比如先用已有的蓝牌检测模型去对黄牌图片做初筛框,再人工修正边界。这样省掉的只有“画框”这一步,标注员仍然得逐张看,黄牌的字符排列、双层结构、污损反光这些细节必须人来确认。指望一个 json 标签从网上下载下来就能匹配自己的场景,基本不现实。
2.3 从视频抽帧:ffmpeg 命令与去重逻辑
如果你能搞到监控视频或行车记录仪片段,抽帧是最高效的来源方式。我常用的命令是:
ffmpeg -i input.mp4 -vf "fps=1,scale=1920:1080" -q:v 2 -fps_mode passthrough frame_%04d.jpg这里fps=1表示每秒抽一帧,对车牌检测来说足够密了,太快反而会让相邻帧高度相似,相当于数据翻倍但信息没翻倍。-q:v 2控制 JPEG 质量,2 是高质量档,车牌上的字符边缘在压缩后仍然清晰。-fps_mode passthrough让输出帧率跟随过滤链,避免 ffmpeg 对帧做重采样引入多余处理。
抽完帧之后必须做相似度去重。连续两帧的画面如果只有车牌位置差几个像素,训练时容易被模型当同一条样本反复记忆,导致验证集指标虚高。我一般先抽完帧用感知哈希算法粗筛,两张图的汉明距离小于 5 就删掉一张,然后再人工扫一遍,把镜头快速切换时产生的花帧、运动模糊帧、车辆只露出一半的帧清掉。这个步骤看着琐碎,但直接影响后面标注环节的返工率。
# 用 ImageMagick 比较相邻帧的结构相似度 # 大于 0.95 的帧删除 ffmpeg -i input.mp4 -vf "select=gt(scene,0.05)" -vsync vfr frame_%04d.jpg用select=gt(scene,0.05)可以直接让 ffmpeg 只保留画面突变超过阈值的帧,配合人工复查比先抽帧再删除省一半时间。注意阈值不要设得太大,黄牌车进出闸口时车速慢,画面变化本来就不大,设 0.05 左右比较稳妥。
3. json 标签文件拆开看:LabelMe 风格、COCO 风格和校验脚本
3.1 车牌检测用 LabelMe 风格就够了:polygon 反而是负担
车牌检测数据集的 json 标签,最常见的两种格式是 LabelMe 风格和 COCO 风格。LabelMe 风格的每个文件对应一张图片,顶层包含imagePath、imageWidth、imageHeight、shapes四个关键字段;COCO 风格则是把所有图片的标注汇总到一个大 json 里,包含images、annotations、categories三个数组。
我的建议是:如果是从标注工具导出,选 LabelMe 风格更省事。车牌是刚性目标,矩形框足够表达,但 LabelMe 默认会让你标 polygon,标注员很容易在车牌四角上多点几下。polygon 看起来精细,训练时反而有害——模型目标检测的锚框回归学习的是矩形框参数,polygon 边界上一点点的抖动都会被放大成 bbox 抖动。所以我统一要求标注员用 rectangle 模式,四个点最多六个点,拖拽一个对角再微调另一边即可。
3.2 一个标准的黄牌 json 文件长什么样
下面这个例子是单张图片、单个车牌框的 LabelMe 风格标签:
{ "version": "5.2.1", "flags": {}, "shapes": [ { "label": "yellow_plate", "points": [ [128, 256], [512, 384] ], "group_id": null, "shape_type": "rectangle", "flags": {} } ], "imagePath": "frame_0001.jpg", "imageData": null, "imageWidth": 1920, "imageHeight": 1080 }这里points只有两个点,分别是矩形左上角和右下角,坐标单位是像素,以图片左上角为原点。label是类别名,建议统一成yellow_plate,不要一会写yellow_plate一会写yellowplate,后面转格式时踩坑还得回头改。imagePath是相对路径,注意它必须和实际文件名完全一致,包括大小写和后缀。
如果你拿到的数据是 COCO 风格,对应的标注字段是bbox: [x, y, width, height],area是面积,category_id是类目编号。COCO 的 bbox 用左上角坐标加宽高表达,和 YOLO 训练需要的中心点坐标加宽高不是一回事,转换时公式很容易搞反,后面第四章会给出具体换算方式。
3.3 训练前先跑一遍标签校验脚本:及时发现问题
不管 json 标签来自标注平台还是网上开源,训练前都必须做一次完整性扫描。常见问题包括:json 文件缺失、图片路径指向不存在的文件、同一张图重复标注、类别名不一致、坐标超出图片边界。下面这个脚本可以一次性扫出这些问题:
import json, os from glob import glob img_dir = "images" json_dir = "labels_json" for json_path in glob(os.path.join(json_dir, "*.json")): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) img_name = data["imagePath"] img_path = os.path.join(img_dir, os.path.basename(img_name)) if not os.path.exists(img_path): print(f"[缺图] {json_path} -> {img_name}") continue w, h = data["imageWidth"], data["imageHeight"] for shape in data["shapes"]: pts = shape["points"] for x, y in pts: if x < 0 or y < 0 or x > w or y > h: print(f"[越界] {json_path}: 点({x},{y})超出 {w}x{h}") break labels = set(s["label"] for s in data["shapes"]) if len(labels) > 1: print(f"[多类别] {json_path}: {labels}")脚本做了三件事:查图片是否存在、查坐标是否越界、查类别是否统一。输出会逐条列出问题文件路径,照着改就行。这里有个容易被忽略的细节——imagePath里如果写的是绝对路径或含子目录,直接用os.path.basename取文件名去匹配图片,避免路径层级不一致导致的误报。坐标越界这个检查尤其重要,很多标注工具允许你拖出画布边缘,标签照样保存,但模型训练时 loss 直接飘到 NaN。
4. 标签转 YOLO 格式:json 转 txt 的归一化细节与目录划分
4.1 坐标归一化:先加后除,方向搞反就全废
YOLO 训练用的 txt 标签文件,每一行是class_id x_center y_center width height,四个坐标值全部归一化到 0 到 1 之间。这里的 x_center、y_center 是 bbox 中心点相对整张图片的比例,width、height 是 bbox 宽高相对图片宽高的比例。
从 LabelMe 的矩形框左上角点(x1, y1)和右下角点(x2, y2)换算公式如下:
- x_center = ((x1 + x2) / 2) / imageWidth
- y_center = ((y1 + y2) / 2) / imageHeight
- width = (x2 - x1) / imageWidth
- height = (y2 - y1) / imageHeight
注意先求中心点像素坐标,再除以图片尺寸,顺序不要颠倒。有人图省事先各自归一化再加,得到的结果差之毫厘谬以千里。另外,YOLO 格式不接受负数坐标,越界的标注必须在转换时就裁掉,或者直接扔掉这张图,不要想着让模型去学习越界目标。
黄牌特有的一个问题是双层车牌。大货车挂的双层牌,上层小下层大,标注策略必须先统一:是上下两层合起来框一个整体,还是各自分开框两个框。我统一建议分开框,因为检测到两层后,后续识别字符时天然知道哪一层是号牌主体,合框会把两层中间的缝隙噪声带入模型。做转换脚本时,遇到同一个车牌区域出现两个分离的框,不要去合并。
4.2 转换脚本:直接可用,附详细参数说明
下面这个脚本把 LabelMe 风格 json 转成 YOLO txt,同时处理了双层牌、路径分隔符和类别映射:
import json, os from glob import glob CLASS_MAP = {"yellow_plate": 0} img_dir = "images" json_dir = "labels_json" out_dir = "labels_txt" os.makedirs(out_dir, exist_ok=True) for json_path in glob(os.path.join(json_dir, "*.json")): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) img_w = data["imageWidth"] img_h = data["imageHeight"] img_name = data["imagePath"].replace("\\", "/") base_name = os.path.splitext(os.path.basename(img_name))[0] txt_path = os.path.join(out_dir, base_name + ".txt") with open(txt_path, "w", encoding="utf-8") as out_f: for shape in data["shapes"]: label = shape["label"] if label not in CLASS_MAP: continue pts = shape["points"] x_coords = [p[0] for p in pts] y_coords = [p[1] for p in pts] x1, x2 = min(x_coords), max(x_coords) y1, y2 = min(y_coords), max(y_coords) # 去掉越界部分,避免归一化出现负数 x1 = max(0, x1) y1 = max(0, y1) x2 = min(img_w, x2) y2 = min(img_h, y2) box_w = x2 - x1 box_h = y2 - y1 if box_w <= 2 or box_h <= 2: continue # 面积太小的框视为废标注 x_center = (x1 + x2) / 2 / img_w y_center = (y1 + y2) / 2 / img_h norm_w = box_w / img_w norm_h = box_h / img_h out_f.write(f"{CLASS_MAP[label]} {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}\n")我写脚本时特别注意了两点。第一是data["imagePath"].replace("\\", "/"),Windows 上标注导出的路径经常带反斜杠,Python 读字符串时\f、\t这类会被转义成特殊字符,导致文件名匹配失败。第二是小型过滤,box_w <= 2 or box_h <= 2把面积小于 2 像素的框丢掉,这类框基本是标注员手误点出来的噪点。如果你的图片里有特殊字符类名,比如新能源绿牌要叫green_plate,直接在CLASS_MAP里加一行即可。
4.3 目录划分和训练文件:固定随机种子,验证集别混入重复帧
转换完 txt 标签后,目录结构我一般组织成:
dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── data.yaml划分时用随机种子固定顺序,否则每次跑训练数据分布都变,实验结果没法对比。下面这段脚本把图片列表打乱后按 9:1 分到 train 和 val:
import os, random from glob import glob random.seed(42) imgs = glob("images/*.jpg") random.shuffle(imgs) val_count = max(1, int(len(imgs) * 0.1)) for i, img in enumerate(imgs): dest = "val" if i < val_count else "train" os.makedirs(f"images/{dest}", exist_ok=True) os.makedirs(f"labels/{dest}", exist_ok=True) base = os.path.basename(img) os.rename(img, f"images/{dest}/{base}") txt = f"labels_txt/{base[:-4]}.txt" if os.path.exists(txt): os.rename(txt, f"labels/{dest}/{base[:-4]}.txt")这里 val 占比 10% 对车牌检测够用,因为黄牌场景单一,不需要像自动驾驶数据集那样留大量验证场景。划分后打开data.yaml写下路径:
path: /absolute/path/to/dataset train: images/train val: images/val names: 0: yellow_platepath建议写绝对路径,YOLO 对相对路径的处理在不同版本之间有差异,写绝对路径少踩一个坑。names 里的编号必须和转换脚本里 CLASS_MAP 定义一致,这是最容易被忽略的对应关系。
5. 黄牌数据集避坑:5 个真实翻车点,每一条我都付出过时间代价
5.1 绿牌被标成黄牌:颜色信息是最后的防线
现象:训练完模型后,在新能源车多的路段连续误检,绿色车牌被识别成黄牌。打开标注数据检查,发现大量绿牌样本的 label 写的是 yellow_plate。
原因:标注员在灰度模式下看图,绿牌和黄牌在灰度图上色调接近,肉眼很难区分,一批次标完就全错了。
解决:在标注规范里强制要求彩色模式看图,同时在转换脚本里加一道颜色校验。这里可以写一个小函数,对标注框内的图像区域统计 HSV 色相均值,绿色的色相在 35 到 85 之间,黄色在 20 到 35 之间,如果框内绿像素比例超过 30% 但标签写的是 yellow,就打印警告。这个校验不能完全替代人工复核,但能过滤掉八成以上的低级错误。
5.2 拖挂车的双层车牌只标了一半
现象:模型在高速收费站对拖挂车漏检,而且漏检的集中在车尾。检查标注文件发现,这些车牌的框高度只有正常车牌的三分之一。
原因:部分标注工具默认区域是单目标,标注员看到双层车牌后习惯性只框下半部分大号牌,上半层小号牌完全没标。模型训练时学到的黄牌特征变成“一块窄条形区域”,对完整双层车牌响应低。
解决:规格定义阶段就写明双层牌的标注规则。如果上下两层间距小于上层高度的两倍,合并成一个整体框;间距再大,分两个框标,类别名保持一致。更重要的是,在转换脚本里统计同一个 json 文件里同类目标的框高度分布,如果一批数据中出现大量高度明显偏小的框,立刻回溯检查原始标注。
5.3 反光导致标注员“脑补”边框
现象:验证集 loss 一直正常,但实际使用中逆光场景车牌检测框偏移严重,框边界贴着字符而不是车牌边缘。
原因:户外逆光下黄牌表面反光,标注员看不到真实的金属边框,只能凭经验猜一个边界。不同人猜的偏差不同,有的贴着字符,有的把旁边车灯也框进来了。模型看到的是边界不一致的目标,回归自然学不好。
解决:采集阶段尽量选顺光时段拍摄,或者用行车记录仪循环录像时注意遮阳板角度。如果必须用逆光素材,标注时打开图像增强辅助工具,把亮度临时拉高看清边框再标。建议在标注界面设置一条规则:边框模糊看不清的一律先标一个粗框,后面专门人复查,而不是让标注员自由发挥。数据集质量烂在标注环节,后面对话模型再优秀也救不回。
5.4 ffmpeg 抽帧产生大量异常帧:花屏帧和运动模糊帧
现象:训练到一半 loss 突然跳高,检查数据集发现里面混进了几百张只有半截车身、车牌像被拉伸过的图片。
原因:用fps=1抽帧时,如果视频编码有 B 帧,某些关键帧之间的插值帧会被 ffmpeg 直接输出,画面呈现撕裂;快速变道时运动模糊帧也混进去了。人工筛片很难注意这种异常帧,它们和正常帧通常只有细微差别。
解决:抽帧后跑一遍清晰度筛选。我一般用 OpenCV 的拉普拉斯方差判断模糊程度,方差低于阈值的帧直接删除。同时把抽帧后相邻两帧做一次结构相似度比较,相似度大于 0.9 的删掉。这样处理后数据集整体质量明显提高,loss 曲线的毛刺也少了很多。
5.5 json 文件名和图片名不同步:导出工具的绝对路径陷阱
现象:转换脚本跑完后统计发现只有六成图片有对应 txt,其余图片的标签文件是空的。
原因:标注平台或本地工具导出的 json 里,imagePath有时记录的是标注当时文件的绝对路径,比如/home/user/2024/05/frame_001.jpg,但实际图片被移动到了images/frame_001.jpg。转换脚本按 basename 匹配时,部分文件名中有重复数字或空格导致匹配失败。
解决:转换之前先做一次全量文件名检查,用脚本把 json 里的 imagePath 和 images 目录下的实际文件名做交集比对,列出差集再人工处理。不要试图在转换脚本里用模糊匹配去猜,猜出来的结果迟早会在训练时翻车。另一个技巧是收集 json 时直接要求标注工具输出相对路径,大多数工具在导出设置里可以勾选,这一步能在源头上省掉后面的对齐工作。
6. 用 YOLOv8 快速验证数据集质量:20 轮训练与混淆矩阵查账
6.1 最小训练命令:数据质量验证不需要大模型
只验证标签质量时,不要直接上 YOLOv8x 或 YOLOv5m,用最小的模型跑 20 轮就够。小模型对数据噪声更敏感,数据有问题它会直接体现在指标上,大模型反而可能在参数里“消化”掉部分错误。命令如下:
yolo detect train \ data=dataset/data.yaml \ model=yolov8n.pt \ epochs=20 \ imgsz=640 \ batch=16 \ patience=10patience=10表示验证集指标连续 10 轮不提升就提前停止,质量差的数据集往往十几轮后就开始震荡,不会出现虚假的“越训越好”。imgsz=640对黄牌这种大目标足够,不要用更高的分辨率测试,否则会把换行符的输入尺寸差异和标签质量问题混在一起。跑完后看两处:results.csv里的验证集 mAP50 和混淆矩阵图。
6.2 训练曲线和混淆矩阵要这么读:指标高不代表数据干净
如果 mAP50 在 0.9 以上,看起来很美,但打开混淆矩阵发现黄牌和蓝牌类别之间有 15% 的互相误判,说明类别之间的区分度有问题。出现这种组合的概率很高——单纯看 mAP 不会暴露具体错在哪里,要真正排查标签噪声,混淆矩阵是最直观的账本。
我还会额外看一眼训练集和验证集的 loss 曲线。训练集 loss 下降而验证集 loss 不降甚至上升,通常不是过拟合,而是验证集里混入了标错的样本,导致验证集本身不可信。这时候做的不是调模型结构,而是回去重新清洗数据。
一天晚上我调了一个模型的参数,怎么都压不住验证集 loss,最后发现是验证集里混了几十张标注框偏了大半的车牌图。从那以后我养成了习惯:每份数据集训完都留一份可视化验证结果,随机抽 100 张验证集图片,把预测框和标签框画出来快速过一遍。这个过程 20 分钟,能省掉后面正式部署时反复排查问题的几小时。数据集的脏东西不会自动消失,只会等你在最不想遇到的时候冒出来,希望这篇梳理能帮你把前面的坑填得平一点。
本文还有配套的精品资源,点击获取