车牌字符识别实战:用1458张标记图跑通YOLOv8训练全流程
简介中国车辆车牌号识别数据集面向计算机视觉入门和进阶开发者用于训练车牌数字与字母的检测、识别模型所有标注均按YOLOv8格式输出可无缝接入主流目标检测训练流程适合毕业设计、竞赛项目或安防车牌识别原型开发。压缩包共2000个文件包含542张JPG原始车辆图片、1457个TXT标签文件以及1个YAML配置JPG与标签一一对应TXT记录车牌区域的类别和坐标YAML则便于直接定义数据路径与类别名称整体大小约17.6MB轻量紧凑便于下载和快速训练。目前已有751人学习下载。除基础训练外文件命名保留了原始样本前缀便于按名称筛选、清洗或扩充数据用户拿到后只需划分训练集与验证集即可开始YOLOv8模型训练省去手动标注的时间并可根据自己的场景进一步微调参数。1. 1458张车牌标记图能不能直接拿去做车牌识别很多人拿到「中国车辆车牌号识别数据集可识别车牌数字和字母支持yolov8格式的标记1458张标记图片.zip」之后第一反应是问1458张够不够结论先说够你把 YOLOv8 训练流程完整跑通并且得到一个在小规模测试图上能用的字符检测模型但如果你指望它直接变成生产级车牌识别系统大概率会翻车。原因不是图片数量而是这个数据集的边界——它标注的是数字和字母没有省份汉字也没有配套的帧间关系。它适合用来完成「YOLOv8 目标检测 字符排序」的闭环验证也适合作为你扩展自有车牌数据时的基础底料。下面按实际动手顺序展开。2. 拆开ZIP先看YOLOv8标记标签格式、类别文件和目录关系2.1 先做一次文件体检images 和 labels 是否一一对应解压之后先别急着训练。我一般的习惯是先跑一轮文件体检看图片和标签是不是一一对应。很多标记数据集解压出来会有图片多、标签少或者标签文件名大小写不匹配的情况这些坑到了训练阶段才会爆到时候排错很费时间。先统计文件类型分布find . -maxdepth 3 -type f | awk -F. {print $NF} | sort | uniq -c这行命令把目录里所有文件的扩展名抽出来统计每个扩展名出现了多少次。正常情况下你会看到.jpg和.txt数量接近 1:1。如果.txt数量明显少于图片张数说明标签不完整后面做训练集会白做。接着逐一核对每个图片是否有同名标签for img in images/*.jpg; do base$(basename $img .jpg) if [ ! -f labels/$base.txt ]; then echo 缺少标签: $base fi done这段脚本遍历 images 下的每张 jpg用basename去掉后缀得到文件名主体再去 labels 目录里查同名 txt。如果有输出就先把缺失的文件补上或剔除。这里要注意的是图片可能混合 jpg、png、jpeg所以第一段统计命令比肉眼可靠扩展名大小写也要看清楚。2.2 标签文本的五个数字类别和归一化坐标YOLOv8 的标签格式和 YOLOv5 一脉相承每个 txt 文件里每一行对应一个目标一行五个数字。随便打开一个标签文件cat labels/15012345678.txt看到的内容通常是0 0.462123 0.563118 0.128333 0.072192第一个数字是类别 id后面四个分别是目标中心点的 x 坐标、y 坐标、目标宽度 w、目标高度 h全部是相对于原图宽高的归一化值范围在 0 到 1 之间。也就是说图片宽度是 1920x_center 是 0.462那实际中心像素就是 1920 乘以 0.462。这里有个判断数据集标注思路的小技巧如果这一行的 w 在 0.5 以上说明标的是整个车牌如果 w 在 0.05 左右、h 在 0.05 左右说明标的是单个字符。拿到这个数据集之后先抽几个标签看看确定它是哪一类标注再决定后面怎么做。如果标的是整个车牌那你还需要接一个 OCR 步骤才能识别出数字和字母如果标的是字符框那 YOLOv8 输出的就是字符位置和类别。2.3 类别表没有汉字这个数据集能识别什么标题写得很清楚可识别数字和字母。为了确认类别数量可以快速统计标签里出现过的类别 idcat labels/*.txt | grep -v ^$ | awk {print $1} | sort -n | uniq -c输出结果会给你一个分布比如 0 到 35。中国车牌的数字加字母一共 36 个类别如果你的数据集中所有号码都由字母和数字组成常见映射是 0-9 对应数字10-35 对应 A 到 Z。但要明确一点中国车牌的首位通常是省份汉字简称比如“京”“沪”“粤”这套类别表里没有汉字。也就是说这个数据集能学会“识别字符”但学不会“识别这辆车是哪里的”。这个边界在实际项目里很重要。如果业务只要求识别车牌的后六位或后七位那 1458 张能用如果业务要求输出完整车牌那你必须另外准备省份汉字数据要么用分类模型单独处理首位要么扩展标注数据重训。我倾向的做法是先用这套数据把字母数字识别跑通后续单独补一轮汉字子模型这样两个模型的训练节奏都不互相拖累。如果你要做完整车牌识别可以把最左侧的汉字框单独留给一个专用分类器其余字母数字框交给 YOLOv8 模型。注意汉字识别框和数据集中其他字符框会混在一起后处理时要把最左侧且宽度接近字符宽度的框挑出来先送汉字分类器再处理后面的字母数字序列。2.4 训练集/验证集划分不要按文件名随机切1458 张图看起来不多很多人图省事直接随机切成 85:15。但我见过一个很隐蔽的问题同一个车牌被连续帧拍到随机切分会让同一辆车的两张图分别进入训练集和验证集。训练时模型见过这辆车的纹理验证时又拿这辆车的另一个角度来打分mAP 会虚高上了新图立刻现原形。一个更稳的做法是先按文件名的前缀分组再按组划分。很多采集工具生成的图片名带时间戳或车牌编号前缀比如plate_id_0001.jpg那plate_id就是分组键。以下脚本按组划分并复制到指定目录import os, random, shutil random.seed(42) img_dir images label_dir labels train_img_dir dataset/images/train train_lbl_dir dataset/labels/train val_img_dir dataset/images/val val_lbl_dir dataset/labels/val for d in [train_img_dir, train_lbl_dir, val_img_dir, val_lbl_dir]: os.makedirs(d, exist_okTrue) imgs [f for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .jpeg, .png))] groups {} for f in imgs: prefix f.split(_)[0] groups.setdefault(prefix, []).append(f) group_names list(groups.keys()) random.shuffle(group_names) split int(len(group_names) * 0.85) for idx, g in enumerate(group_names): target_img train_img_dir if idx split else val_img_dir target_lbl train_lbl_dir if idx split else val_lbl_dir for f in groups[g]: shutil.copy(os.path.join(img_dir, f), target_img) label_file os.path.splitext(f)[0] .txt src_label os.path.join(label_dir, label_file) if os.path.exists(src_label): shutil.copy(src_label, target_lbl) else: print(缺少标签, label_file)这段代码先按_之前的内容分组能有效避免同一车牌的多帧画面被拆开。如果你的文件名没有可分组前缀那就退化成直接随机打乱文件列表划分结果也能接受但要意识到验证集分数会乐观一些。复制而不是移动是为了保留原始数据作为后悔药改错划分后还能重来。3. 用YOLOv8训练这套车牌数据最小命令与三个关键参数3.1 环境准备CPU也能跑Ubuntu 20.04 先别急着装CUDA这套数据量不大CPU 版本完全能跑。如果你的机器没有 NVIDIA GPU完全不用先折腾 CUDA直接装 CPU 版的 PyTorch 和 ultralytics 就够了conda create -n plate python3.10 -y conda activate plate pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install ultralytics第一行创建虚拟环境避免把系统 Python 搞乱。第二行激活环境。第三行从 PyTorch 官方 CPU 索引安装 CPU 版这样不会去拉 CUDA 那一大堆依赖。最后安装 ultralytics它会自动带上 YOLOv8 的 CLI 工具和 Python 接口。安装完之后验证一下环境python -c from ultralytics import YOLO; print(ok)如果输出 ok环境就绪。CPU 训练 1458 张图、每轮 50 epoch速度取决于 CPU 核心数几分钟一轮很正常不要和对 GPU 的期望做对比。这个阶段的目标是把流程跑通而不是拼训练速度。3.2 整理数据目录并写 data.yamlYOLOv8 不直接认裸的 images 和 labels 目录它要的是 data.yaml 里定义好的数据组织。常见布局是plate_dataset/ ├── data.yaml ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/如果你在第 2.4 节用脚本划分它已经生成了dataset/images/train这样的结构。接下来写 data.yamlpath: /home/user/plate_dataset train: images/train val: images/val nc: 36 names: 0: 0 1: 1 2: 2 3: 3 4: 4 5: 5 6: 6 7: 7 8: 8 9: 9 10: A 11: B 12: C 13: D 14: E 15: F 16: G 17: H 18: I 19: J 20: K 21: L 22: M 23: N 24: O 25: P 26: Q 27: R 28: S 29: T 30: U 31: V 32: W 33: X 34: Y 35: Zpath 写数据集根目录train 和 val 是相对 path 的两个子目录。nc 是类别总数 36names 的 key 必须从 0 连续排到 35。特别注意YOLOv8 读标签时只看 class id不看 names 字符串所以 names 顺序一定要和标签里的 id 对应。如果你的实际标签里没有某个字母那 id 也不能跳号只能把未出现的类别也列出来。path 字段建议写绝对路径。相对路径在 IDE 里或者从其他目录执行 yolo 命令时经常出现找不到images/train的问题白折腾半天。如果你的盘子最终要拷到别的机器上训练记得改 path这是最容易漏的一处。3.3 训练命令和参数imgsz、epochs、batch 怎么调数据就绪后最小训练命令是这样yolo detect train \ data/home/user/plate_dataset/data.yaml \ modelyolov8n.pt \ imgsz640 \ epochs50 \ batch16 \ devicecpu第一次跑会自动下载 yolov8n.pt 预训练权重这个权重是 COCO 预训练的不是车牌领域但它能让模型学习基础特征比从随机初始化开始收敛快很多。如果你希望完全从头训练把 model 参数改为yolov8n.yaml但 1458 张图不太建议这么做。三个关键参数要盯住。第一是 imgsz输入图片尺寸默认 640。车牌字符是小目标如果你的原始图片分辨率高、车牌区域小可以先裁剪到车牌周围再训练或者把 imgsz 提到 960但 CPU 上训练时间会成倍上涨。第二是 epochs50 轮对这种规模的数据集够用。第三是 batchCPU 上首次训练建议 8 或 16显存不够就减batch 太小会导致梯度震荡batch 太大在 1458 张图的数据量上没有必要。如果你的原始标注是从 Labelme 转过来的转 YOLOv8 格式时注意 Labelme 的 json 坐标是绝对像素的左上角加宽高YOLOv8 需要中心点加归一化别直接把 json 里的值抄进 txt。转换脚本的核心就是x_center (x1 x2) / 2 / width这一点在跑训练前一定要验证。3.4 观察训练日志和损失曲线什么时候停、什么时候改训练结束后所有结果都在runs/detect/train/下。YOLOv8 会写一个 results.csv里面每一行是一个 epoch 的指标。用下面这段脚本画损失曲线和 mAP 曲线import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/train/results.csv) fig, ax plt.subplots(1, 2, figsize(12, 4)) ax[0].plot(df[epoch], df[train/box_loss], labeltrain box_loss) ax[0].plot(df[epoch], df[val/box_loss], labelval box_loss) ax[1].plot(df[epoch], df[metrics/mAP50(B)], labelmAP50) ax[0].legend() ax[1].legend() plt.savefig(train_curve.png)第一列是 epoch之后分别是训练和验证的 box_loss、分类损失以及 precision、recall、mAP50、mAP50-95。看的时候我习惯先看 val/box_loss 是不是还在降如果已经平了说明再训下去收益有限如果 train loss 还在降、val loss 开始反弹那就是过拟合。mAP50 在 0.8 以上只能说明模型在验证集上还行真正能不能用要拿到新图去测。这里有个经验损失曲线是黑匣子它只能告诉你训练有没有出问题不能告诉你数据标注对不对。如果你发现验证损失一直不降先别急着改网络回去看标签有没有错十次里有八次是标签的问题。所谓网络结构图、yolov8 改进这些话题看起来诱人但对 1458 张图的小数据集结构改进的收益远不如把标注质量提上来。4. 数字和字母识别不等于车牌识别检测后的字符排序与拼接4.1 整体车牌检测 vs 字符框检测两种标注思路拿到数据集你先要搞清楚里面标的是整体车牌还是一个字符一个框。两种思路差异很大。标注方式类别数训练后输出后处理难度整体车牌 1 个框1 类车牌位置高必须再接 OCR每个字符 1 个框36 类字符位置 类别中需要排序拼接如果标签里某个 txt 的 w 值很大比如 0.5 以上那就是整体框如果 w 在 0.05 上下同时还输出了类别那就是字符框。这套数据标题说的是“可识别车牌数字和字母”通常意味着字符框标注。即便如此YOLOv8 给出的只是一堆无序的字符框你必须把它们按车牌上的顺序排成字符串。4.2 用YOLO做字符框检测时的类别设计类别设计最常见的坑是尝试把所有汉字省份简称加进去。先不说数据量不够单是汉字类别和字母数字混在一起类别间相似度差异就很大模型容易把「京」和「J」搞混。所以我建议分两阶段这个数据集先只训 36 类字符省份汉字另外做一个小分类模型或者干脆不识别省份。训练结束后检查一下模型的类别表用一段小代码就能确认python -c from ultralytics import YOLO; mYOLO(runs/detect/train/weights/best.pt); print(m.names)输出是一个字典key 是 0 到 35value 是字符。这里要特别留意 names 字典的顺序预测结果的 cls 值拿到后是从这个字典里查字符的如果你在 data.yaml 里把 A 和 0 的位置写反推理出来的车牌就是错的但损失曲线看不出任何问题。所以写完 data.yaml 后先对一个标签文件做一次人工对照别偷懒。如果预测时同一个字符位置出现两个重叠框NMS 已经过滤过一部分但仍可能留下相邻重复框。常见做法是对重叠度高且类别相同的两个框只保留置信度更高的那一个如果两个框类别不同按业务规则选概率高的或者看哪个框更接近正方形。字符框宽高比一般接近 1如果某个框宽高比大于 2.5大概率是两个字符粘连需要丢弃。4.3 按顺序拼接字符一行代码排序为什么不对最常见的拼接方式是按 x 坐标排序from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) result model.predict(sourcetest_plate.jpg, conf0.25, verboseFalse)[0] boxes result.boxes.data.cpu().numpy() boxes [(b[0], b[1], b[2], b[3], b[5]) for b in boxes if b[4] 0.3 and b[2] - b[0] 10 and b[3] - b[1] 15] boxes.sort(keylambda b: b[0]) plate .join(model.names[int(b[4])] for b in boxes) print(plate)先预测再把 boxes 里的 x1、y1、x2、y2、cls 取出来用宽高阈值过滤掉细小的误检最后按 x1 排序。这个代码在正对镜头、角度平整的蓝牌上能跑通但遇到倾斜车牌就会出错。原因很简单车牌旋转后同一行字符的 x 坐标并不是严格递增的字符中心点会落在一条斜线上按 x 排序会把高位字符和低位字符穿插在一起。一个更稳的做法是先用字符框的中心 y 坐标做行聚类再对每一行按 x 排序filtered [] for b in boxes: x1, y1, x2, y2, cls b filtered.append((x1, y1, x2, y2, cls)) rows {} for x1, y1, x2, y2, cls in filtered: cy (y1 y2) / 2 placed False for key in list(rows.keys()): if abs(cy - key) 12: rows[key].append((x1, y1, x2, y2, cls)) placed True break if not placed: rows[cy] [(x1, y1, x2, y2, cls)] ordered [] for cy, row in sorted(rows.items(), keylambda kv: kv[0]): row.sort(keylambda b: b[0]) ordered.extend(row) plate .join(model.names[int(b[4])] for b in ordered) print(plate)这段代码按 y 中心把字符框聚成一行行间再按 y 排序行内按 x 排序。阈值 12 是经验值如果图分辨率高可以按框高的三分之一来定。实际操作里如果车牌是双行的这个逻辑也能处理多行自然各归各位。4.4 蓝牌、黄牌、绿牌的字符提取差异不同车牌类型的字符数量不同这既是先验也是校验规则。蓝牌是 7 位新能源绿牌是 8 位大型车黄牌有可能是双行。拼接完成后最好做一次数量校验if len(plate) 7: print(蓝牌, plate) elif len(plate) 8: print(绿牌, plate) elif len(plate) in (6, 7): print(黄牌/其他, plate) else: print(数量异常过滤后, plate)字符数量可以帮你筛掉明显的漏检但不能矫正错检。比如模型把 O 和 0 搞混数量不变这种只能靠易混淆类别合并或业务规则来兜底。YOLOv8 对字符宽高比也有隐含假设单个字符框通常接近正方形如果某个预测框宽高比大于 2那大概率是半个车牌或者两个字符粘连可以在后处理里直接去掉。这个步骤看上去很小但对最终识别率影响很大。5. 训练车牌数据集的5个避坑记录5.1 标签里混入了空行和越界坐标训练中途报错现象训练刚开始就报RuntimeError: labels empty或者某个 epoch 的 loss 变成 nan。 原因标签 txt 文件里有空行或坐标值不在 0-1 之间YOLOv8 读取后产生了空标签或无效框。 解决训练前统一清洗一遍标签文件。import os label_dir labels for f in os.listdir(label_dir): path os.path.join(label_dir, f) lines open(path).read().strip().splitlines() cleaned [] for line in lines: parts line.split() if len(parts) ! 5: continue cls, x, y, w, h parts if float(w) 0 or float(h) 0: continue if not all(0.0 float(v) 1.0 for v in (x, y, w, h)): continue cleaned.append(line) open(path, w).write(\n.join(cleaned))脚本把每一行拆成五个字段字段数不对的直接跳过宽高小于等于零的跳过任何一个坐标越界的跳过。注意这里的越界检查针对的是归一化坐标如果原始标注本来就是像素坐标那训练前必须转换而不是靠这个脚本过滤。清洗完再看一下空行wc -l和实际行数对不上就说明还有残留。5.2 蓝底白字反光导致漏检严重现象验证集 mAP 有 0.9但实拍阳光下反光时车牌整体检测不到。 原因1458 张图中反光样本占比太少模型学到的蓝底颜色分布过于理想。 解决在训练时加大色彩增强参数。yolo detect train \ data/home/user/plate_dataset/data.yaml \ modelyolov8n.pt \ imgsz640 \ epochs50 \ batch16 \ devicecpu \ hsv_h0.02 hsv_s0.8 hsv_v0.5 rotate10hsv_h 控制色调偏移0.02 已经够用hsv_s 控制饱和度0.8 相当于把蓝色深浅大幅拉伸hsv_v 控制明度0.5 模拟白天不同亮度。rotate10 是让模型看到最多 10 度的旋转角度模拟相机安装歪斜。色彩增强也不是越大越好太大会把蓝底车牌变成红底反而学偏。反光本质是局部高亮单靠 HSV 增强模拟不了如果实拍场景反光多建议单独合成一批带高光斑块的车牌图补进训练集。5.3 数字类别多、字母类别少字母识别容易被带偏现象数字识别得挺好B、S、Z 这类字母经常漏检或认错。 原因车牌字符集中数字出现频率高字母出现频率低类别不平衡。 解决先统计每个类别出现次数找到低频类别。from collections import Counter cnt Counter() for f in os.listdir(labels): for line in open(labels/ f): line line.strip() if line: cls int(line.split()[0]) cnt[cls] 1 names_list [str(i) for i in range(10)] [chr(ord(A) i) for i in range(26)] for i in range(36): print(names_list[i], cnt[i])输出后会看到某些字母只有几十次甚至几次。对高频数字模型样本充足对低频字母样本可能不够学稳。常见做法是把包含低频字母的整张图复制几份放进训练集注意一定复制整张图和对应标签不能只复制标签。另一个做法是换用带类别权重的损失函数但在 YOLOv8 的 CLI 里没有现成权重参数所以我实际操作中更多用复制样本。5.4 车牌倾斜角度大YOLO水平框被拉出一个扁矩形现象倾斜车牌上的字符框之间互相重叠模型置信度低。 原因YOLO 框架输出的是轴对齐的水平框字符倾斜后水平框中心偏移宽高被拉大。 解决训练时加旋转增强并对标签做小角度修正。上面命令里的 rotate10 就是做这件事的。不过 rotate 参数不能开太大。旋转增强会给每个标签框做对应的旋转但框本身还是水平矩形旋转后的字符区域外会包一圈背景。角度太大会让模型学会在噪声框上找特征。如果实际场景里车牌经常倾斜 30 度以上更好的方案是先用角点检测或传统图像处理把车牌摆正再做字符识别而不是指望检测模型硬扛。5.5 验证集和训练集来自同一张图的不同裁剪mAP虚高现象训练完 mAP 0.98换一批完全没见过的照片直接掉到 0.7。 原因数据划分时没有按车牌实例分组同一车牌在不同角度、不同亮度的多张图被分到了训练集和验证集两边。 解决用第 2.4 节的分组脚本按车牌编号或视频采样前缀来切分。宁可验证集张数少一点也要保证同一个车牌只出现在一侧。反过来如果你的文件名有采集时间戳同一秒内的帧要当作一个组。这个坑最阴的地方在于损失曲线和 mAP 都正常你会以为模型真的强直到部署现场才暴露。6. 自定义一张测试车牌验证模型命令行端到端验证与字符排序技巧训练完成不等于方案完成。我建议不要只看验证集分数而是自己挑一张没进过训练集的实拍车牌做一次端到端验证。先用命令行直接预测yolo detect predict \ modelruns/detect/train/weights/best.pt \ sourcetest_plate.jpg \ conf0.25 \ iou0.45conf 是置信度阈值低于 0.25 的框会被丢掉iou 是 NMS 交并比阈值0.45 是默认值字符框之间重叠小可以保持默认。输出会生成一张带标注的图和一个 labels 目录labels 里是排序前的原始框。如果你发现字母 O 和数字 0 经常互换不要急着调模型先在业务层加一条规则车牌第二位必须是字母、后五位必须是数字按这个规则做字符级纠错。这是我对这种 36 类小模型通用的兜底手段。做一个简单的可执行验证把模型加载脚本和排序逻辑放到同一个 Python 文件里输入任意车牌图片输出一行字符串。上面 4.3 节的排序代码可以直接复用但建议把宽度和高度过滤阈值改成相对值例如按图片宽度的 1% 计算而不是写死 10 像素这样不同分辨率下表现更稳定。验证通过后再考虑部署到 RK3588 这类边缘设备小模型 yolov8n 量化后可以跑到实时但那是另一套优化流程。我自己的习惯是任何数据集到手先做标签体检再分组切分然后训练一个最小模型最后一定拿真实场景图做端到端测试。这个顺序帮我避开了很多次返工。1458 张图不是终点但值得用它把你的车牌识别链路先跑通。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →