☰
黄牌车牌检测数据集JSON标签解析与YOLO格式转换实战
2026/10/30 11:30:34 网站建设 项目流程

简介:面向车牌识别与检测任务的数据集,内容为已标注的黄牌车辆图像及配套标签文件,覆盖不同角度的实际拍摄场景,适合计算机视觉方向的研究者、算法工程师和高校学生用于模型训练、效果评估与调优。压缩包共含九千三百二十四个文件,大小约两百五十七兆字节,其中标签文件与图像文件基本对半,图像以JPG为主,另有少量JPEG;标注采用多边形目标框,四个顶点分别落在车牌四角,能够贴合倾斜、透视等不同角度。目前已有八百八十人浏览学习。数据经过人工逐张筛选,剔除模糊图片并处理有歧义的区域,可直接用于车牌检测与字符识别,减少清洗和标注时间;如果后续需要矩形框或其他标签格式,还可联系作者转换,使用上更加灵活。整体目录结构清晰,便于自行划分训练集和验证集,是开展车牌识别实验或项目开发时较为实用的数据资源。 做车牌检测的朋友应该都有过这种感觉,公开能下载的车牌数据集一搜一大把,但真正拿来训练 YOLOv8、能跑出像样 mAP 的,十个里能挑出两个就算不错。尤其是“黄牌”这种特定车型车牌,和满大街的蓝牌比起来,公开资源少得可怜。我去年接了一个大型车辆出入口识别的小项目,甲方要求对黄牌货车、挂车做到全天候检测,找了一圈现成数据集,要么只有几百张图,要么标注格式完全是自造的,最后干脆基于“车牌检测数据集(黄牌)+ json 标签”这套结构自己整理和改造了一版。今天就把这套数据集的整理思路、json 标签的解析方式、转成 YOLO 训练格式的完整流程,以及我踩过的那些坑一次性说清楚。

这套事情适合谁?如果你正准备训练自己的车牌检测模型,或者已经从某个渠道下载到了车牌图片和 json 标签,但不知道如何下手处理,那么这篇文章基本能帮你省掉一周的试错时间。整篇文章不会讲太多虚的,主要围绕“黄牌数据集长什么样、json 怎么解析、怎么转 YOLO 格式、训练前要检查什么”这几件事展开。

1. 数据集构成与黄牌检测的独特难点

1.1 黄牌和蓝牌的检测差异

黄牌在国内主要挂在大中型货车、客车、挂车上,和蓝牌小汽车相比,有几个直接影响检测模型训练的特点。首先是车牌悬挂位置高,而且车身平面大,摄像头从低处拍过去会产生明显的仰角透视,车牌在画面里经常是一个倾斜的梯形而不是一个规整的矩形。其次是黄底黑字的颜色对比度在夜间偏低,尤其是黄色在钠灯路灯下的色偏非常严重,有时候拍出来接近橙色甚至土黄色。第三个问题是污损率极高,货车常年跑长途,车牌被泥浆、灰尘遮挡是常态,而且挂车尾部的车牌经常被货箱挡掉一半。

这些特点决定了我们不能直接拿蓝牌数据集的权重去微调,而是需要一批有代表性的黄牌样本。以我手头这套数据为例,图片分辨率以 1080p 为主,来源覆盖了白天、傍晚、夜间、逆光、雨雾天,每张图包含 0 到 3 个车牌目标不等。标注时我没有采用字符级标注,而是统一采用车牌外接框检测——也就是每个目标只标一个四边形或者矩形框,标签统一叫 plate。

1.2 下载到的数据往往长什么样

如果你是从网上下载到这套“车牌检测数据集(黄牌)+ json 标签”,大概率会看到这样的目录结构:

plate_dataset/ ├── images/ │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... ├── labels/ │ ├── 000001.json │ ├── 000002.json │ └── ... └── README.txt

images 目录放原图,labels 目录放和图片同名的 json 文件,这种组织方式对接 LabelMe 或者 CVAT 导出格式很常见。我在实际处理中遇到过两种 json 结构:一种是 LabelMe 风格,每个 json 里有一个 shapes 数组,包含了标注点坐标和标签名;另一种是 COCO 风格,把多张图的所有标注汇总到一个大 json 里。下面这套整理方案以 LabelMe 风格为主,因为单张图一个 json 在排查问题时最方便,标注错了改一个文件就行,不用动整个数据库。COCO 这种大 json 如果抽其中一部分做训练集,还得额外写过滤脚本,做数据清洗时真的很费事。

2. JSON 标签结构解析与关键字段

2.1 LabelMe 风格 JSON 长什么样

拿我整理后的一个典型 json 文件来说,核心结构是这样:

{ "version": "5.2.1", "flags": {}, "shapes": [ { "label": "plate", "points": [[856, 512], [1074, 584]], "group_id": null, "shape_type": "rectangle", "flags": {} } ], "imagePath": "000001.jpg", "imageData": null, "imageHeight": 1080, "imageWidth": 1920, "imageDepth": 3 }

很多新手拿到 json 后第一反应是去读 imageData 字段,因为里面看起来有很长一串内容。但请注意,LabelMe 导出的 json 里 imageData 经常是 null,因为图片是单独存放的,base64 编码只在某些标注工具的嵌入式快照模式里才会出现。如果你的 json 里 imageData 不为空而且很大,说明这个 json 自带了图片数据,转换时可以直接从它里面解码出图,不一定非要到 images 目录去找同名文件。

shapes 数组里每个元素对应一个标注目标。label 是类别名,points 是一个二维数组,存放多边形顶点坐标。shape_type 如果是 rectangle,points 里只有两个点,分别是矩形的左上角和右下角;如果 shape_type 是 polygon,points 里会有四个甚至更多点,这种情况常见于标注人员用多边形精细框出车牌外轮廓。两种类型在转换时处理逻辑不一样,我后面会专门说。

2.2 黄牌目标在 json 中的常见标注问题

整理这套数据集的过程中,我发现黄牌目标在 json 里最容易出现的标注问题有两个。第一个是坐标越界,标注时不小心把矩形框拉到了图片外面,导致 points 里的 x 或 y 大于 imageWidth 或 imageHeight。这个问题在画面边缘的车牌上特别容易出现,尤其是车牌只剩一半在画面内时,标注人员往往习惯性把框拉出去。第二个问题是全天候样本的标签一致性,同一个车牌在白天是清晰的矩形框,在夜间被灯光打亮后可能变成了多边形,因为标注人员想更贴合高光边界。但如果训练时目标格式不统一,模型学到的框形状就会乱。

我最后采取的做法是全量统一:所有 shape_type 都转换成 rectangle,也就是不管 polygon 还是 rectangle,最终都取最小外接矩形。有人可能会担心这样丢失精度,但从检测任务的角度来说,车辆出入口、停车场这些场景只需要定位车牌大概位置,矩形框完全够用,而且和 YOLO 系的 anchor 机制契合度更高。如果你的后续任务需要做车牌矫正、字符识别,那建议保留四角点标注,那是另一套处理方案。

3. JSON 标签转 YOLO TXT 格式的完整脚本

3.1 为什么必须转换

YOLOv5 和 YOLOv8 训练时,标签文件不是 json,而是每个图片对应一个同名的 txt 文件,每行代表一个目标,格式为:

class_id x_center y_center width height

注意这里的 x_center、y_center、width、height 都是相对于图片宽度和高度的归一化数值,值域在 0 到 1 之间。归一化的原因是模型输入尺寸会缩放,不同分辨率的图片如果直接使用绝对坐标,训练时就乱了套。这是目标检测训练里的基础操作,但也正因如此,很多人会忽略一个小问题——json 里存的是左上角 xy 和右下角 xy,转换时需要先算出中心点和宽高,再分别除以 imageWidth 和 imageHeight。顺序错了,框就全偏了。

3.2 转换脚本与参数选择过程

我的转换脚本用 Python 写,核心逻辑分三步:遍历 labels 目录下所有 json 文件,解析出每个目标框,写成同名 txt 并放到指定输出目录。先放完整代码:

import json import os import glob def convert_labelme_json_to_yolo(json_path, output_dir, class_names=None): if class_names is None: class_names = {"plate": 0} with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) img_w = data.get("imageWidth", 0) img_h = data.get("imageHeight", 0) image_path = data.get("imagePath", "") if not img_w or not img_h: print(f"Warning: missing imageSize in {json_path}") return base_name = os.path.splitext(os.path.basename(image_path))[0] if not base_name: base_name = os.path.splitext(os.path.basename(json_path))[0] txt_path = os.path.join(output_dir, base_name + ".txt") lines = [] for shape in data.get("shapes", []): label = shape.get("label", "") if label not in class_names: continue shape_type = shape.get("shape_type", "rectangle") points = shape.get("points", []) if shape_type == "rectangle" and len(points) >= 2: x1, y1 = points[0] x2, y2 = points[1] else: xs = [p[0] for p in points] ys = [p[1] for p in points] x1, y1, x2, y2 = min(xs), min(ys), max(xs), max(ys) # 坐标越界裁剪 x1 = max(0, min(x1, img_w)) y1 = max(0, min(y1, img_h)) x2 = max(0, min(x2, img_w)) y2 = max(0, min(y2, img_h)) if x2 <= x1 or y2 <= y1: continue x_center = (x1 + x2) / 2.0 / img_w y_center = (y1 + y2) / 2.0 / img_h width = (x2 - x1) / img_w height = (y2 - y1) / img_h lines.append(f"{class_names[label]} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") with open(txt_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) def batch_convert(json_dir, output_dir): os.makedirs(output_dir, exist_ok=True) json_files = glob.glob(os.path.join(json_dir, "*.json")) for jf in json_files: convert_labelme_json_to_yolo(jf, output_dir) if __name__ == "__main__": batch_convert("labels", "yolo_labels")

这段代码里我特意加了几个容易被忽略的处理。第一是 polygon 取最小外接矩形时用 min/max 遍历所有顶点,而不是只取前两个点。第二是坐标越界裁剪,把超出图片范围的数值强行拉回边界内,这一步如果不做,训练时 YOLO 会直接报错或者 loss 出现 nan。第三是过滤掉 x2<=x1 或 y2<=y1 的无效框,这些框通常是在标注时手滑创建了两点重合的目标。

3.3 做一个可视化检查脚本

转换完成后,我强烈建议不要急着开训练,先画一遍框看看转换结果对不对。我自己写了一个极简的可视化脚本,用 OpenCV 读 txt 标签,在图上把矩形画出来,然后一张一张肉眼扫过去。这一步能发现大量机械化转换发现不了的问题,比如标注框是不是比实际车牌大了一圈、夜间反光把框撑大了、某些图片的标签和文件名对不上等等。

import cv2 import os def draw_yolo_labels(image_dir, label_dir, output_dir, class_names=None): if class_names is None: class_names = {0: "plate"} os.makedirs(output_dir, exist_ok=True) for img_name in os.listdir(image_dir): if not img_name.lower().endswith((".jpg", ".jpeg", ".png")): continue img_path = os.path.join(image_dir, img_name) base = os.path.splitext(img_name)[0] txt_path = os.path.join(label_dir, base + ".txt") img = cv2.imread(img_path) h, w = img.shape[:2] if os.path.exists(txt_path): with open(txt_path, "r") as f: for line in f: parts = line.strip().split() if len(parts) < 5: continue cls_id = int(parts[0]) x_center = float(parts[1]) * w y_center = float(parts[2]) * h box_w = float(parts[3]) * w box_h = float(parts[4]) * h x1 = int(x_center - box_w / 2) y1 = int(y_center - box_h / 2) x2 = int(x_center + box_w / 2) y2 = int(y_center + box_h / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, class_names.get(cls_id, str(cls_id)), (x1, max(0, y1 - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) out_path = os.path.join(output_dir, img_name) cv2.imwrite(out_path, img) print(f"processed {img_name}")

跑完可视化,重点看三类图:车牌位于画面边缘的图、夜间图、雨雾天图。边缘图看框是否被裁剪得不完整,夜间图看反光区域是否导致框比实际车牌大了很多,雨雾天图看标注是否漏检。一套数据如果这三类图都经得住看,那训练起来心里就有底了。

4. 训练前的数据划分与黄牌增强策略

4.1 划分训练集和验证集时的坑

YOLO 训练要求的数据划分很简单,但偏偏是这里最容易出问题。很多人脚本写完直接按 9:1 随机切分,结果训练集和验证集里出现了同一辆车的图片。对于车牌检测来说,同一辆车的不同帧画面极度相似,如果同时出现在训练集和验证集里,验证集精度看起来会虚高,一上线就现出原形。

我在划分时采用的策略是:先按图片的文件名前缀分组,同一个采集批次或者同一辆车的前后帧尽量归到同一边。实际操作中是先把所有图片按拍摄时间排序,再每隔 10 帧抽一帧进验证集,这样能最大程度避免数据泄漏。黄牌这块尤其严重,因为货车经常停在路边不动,连续几分钟拍出来的几十帧几乎一模一样,你不做去重或者分组切分,训练集和验证集相似度极高,mAP 能虚高五六个点。

4.2 黄牌场景的增强策略

数据增强上,对黄牌数据不能照搬通用目标检测那套路数。YOLOv8 自带的 mosaic 和 mixup 对提升鲁棒性确实有帮助,但对车牌字符型目标来说,过度的几何扭曲会让字符变形到失真的程度。我训练时把 hsv_h、hsv_s、hsv_v 这三个颜色增强参数调高了,因为黄牌在不同色温光源下的色偏本来就大,模型必须见过足够多不同色调的黄才能泛化;但 translate 和 shear 我调得比较保守,因为车牌是平面物体,现实中不太可能出现夸张的斜切视角。一个经验值供参考,我最终用的配置是 hsv_h=0.02、hsv_s=0.6、hsv_v=0.5,translate=0.1,scale=0.4,mosaic=1.0,大家可以根据自己数据的光照分布再微调。

还有一个值得单独说的点,就是不要把“蓝牌”归类为背景。有些项目里不需要检测蓝牌,但数据集里混入了大量蓝牌照片,如果不加任何处理,蓝牌会被当成困难负样本,导致模型对蓝色区域的响应被抑制。但蓝牌和黄牌在颜色特征上差异明显,蓝牌样本过多会让模型倾向忽略饱和度高的小目标。我处理时把蓝牌图片单独过滤出来,按适当比例保留一部分作为负样本,既不会让模型看到蓝牌就“懵”,也不会因为负样本太多而压低了黄牌的召回率。

4.3 训练参数和超参的一点经验

当你把 json 标签转成 txt、划分好数据集之后,训练命令基本是标准的 YOLOv8 流程。我自己用的命令是这样的:

yolo detect train data=dataset.yaml model=yolov8n.pt epochs=150 imgsz=640 batch=16 device=0

数据集不大时,从头训练很容易欠拟合,我建议用 yolov8n.pt 或者 yolov8s.pt 做迁移学习。哪怕预训练权重里没有车牌类别,但模型底层的纹理、边缘、颜色特征提取能力已经很强了,微调一下就能收敛。我手头这套黄牌数据总共只有 3200 多张图,用 yolov8s 从头训的话 mAP@0.5 能到 0.91 左右,从 COCO 预训练权重迁移上来能到 0.96,差别还是很明显的。另外,epoch 数不是越多越好,我跑过 300 轮的实验,发现 150 轮以后 val loss 基本不再下降,反而有轻微过拟合的趋势,早停法在这个场景下很管用。

5. 常见问题与排查技巧实录

这一部分整理一下我在处理这套“车牌检测数据集(黄牌)+ json 标签”时实际遇到的问题,基本都能对应到你们可能踩到的坑。

问题现象可能原因解决办法
转换后 txt 全是空文件json 里 shapes 数组中 label 不叫 plate,可能叫 license_plate 或 0检查 README 或打开一个 json 确认 label 值,在 class_names 里做好映射
训练报错“image not found”json 里的 imagePath 不含扩展名,或扩展名大小写不一致转换时使用 json 文件名作为基准,而不是 imagePath 字段
框整体偏左上/右下json 坐标使用中心点格式,而脚本按左上右下解析对照标注工具导出格式,确认 points 语义;不知道就打印一个 json 自己判断
夜间图片车牌漏检严重增强参数里 hsv 变换不够,或训练集夜间样本占比过低从原数据中补充夜间帧,提高 hsv_s、hsv_v 的随机范围
验证集 mAP 很高但实际效果差训练验证拆分时有同源数据泄漏按时间或拍摄场景分组切分,增强样本多样性
训练 loss 先降后升、振荡学习率过大或 batch size 太小把初始 lr 降到 0.001 以下,batch size 提到 16 以上
一个 json 里出现了两个重叠框标注时重复标了同一块车牌可视化检查时发现重叠框,手动删掉后重新生成 txt

除了表里的问题,还想补充一点标注质量层面的体会。数据传输过程中偶尔会遇到 json 文件损坏,最常见的是编码格式问题——Windows 下标注工具导出的 json 用 GBK 编码保存了中文字段,导致 Python 读入时报 UnicodeDecodeError。我转换脚本里加了 encoding="utf-8" 参数,但如果你在 Windows 上拿到数据,可能需要试一下 gbk 编码或者用 chardet 自动检测,否则程序就会中断在第一个文件上,后面基本没法继续跑。

再有一个知识点容易被忽略,就是转换前最好先跑一下数据完整性检查。我把检查规则写得很简单:遍历所有标注过的图片,确认每张 json 都存在且能被 json.load 正确解析;再遍历所有 json,确认对应的图片文件存在。两轮遍历下来,基本上能过滤掉 90% 的脏数据。这个检查往往只花几分钟,但能为后续训练节省好几个小时的排查时间,强烈建议大家在开始转换前先做这一步。

6. 从数据集到模型落地的整体流程

现在把整套流程串起来看,你会发现“车牌检测数据集(黄牌)+ json 标签”这个题目听起来是个数据集的下载和使用问题,但真正落地时涉及的工作链条其实不短。第一步是确认数据规模和标注类型,第二步是解析 json、检查脏数据,第三步是转换成 YOLO txt 格式并做可视化验证,第四步是合理划分训练集和验证集,第五步才是开始训练调参。每一步之间都有依赖关系,前一步没做好,后面跑出来的结果都会受到影响。

以我个人的经验来说,这套黄牌数据集在做完上述处理后,用 YOLOv8s 训练出来的模型在实际出入口场景的表现相当稳定,白天检测基本一台设备能做到实时流畅,夜间配合补光灯也能维持不错的召回率。而且因为我有意加入了大量极端角度和污损样本,模型对车牌缺角、泥浆覆盖这些情况的容忍度明显好于之前用蓝牌数据微调的版本,这说明样本选择和标注质量在目标检测项目里的重要性,其实比模型结构的选择还要高。

最后再分享一个小技巧,如果你后续计划把检测结果接进车牌识别流程,建议你在检测阶段就保留置信度输出和坐标信息,不要只保存一个类别标签。我实际开发中经常遇到检测框每帧抖动十几像素的情况,最后在输出端做了一次 IoU 跟踪匹配,用连续三帧的检测结果取中值来平滑坐标,识别准确率提升非常明显。这个经验后续有机会再展开,但有一点可以确定,检测这步做扎实了,后面一切都顺。

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

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

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

立即咨询