简介:面向计算机视觉入门与进阶开发者,这套车牌检测识别方案基于YOLOv5高效目标检测框架,解决交通场景下车辆牌照自动定位与字符识别的实际需求。内容涵盖多尺度特征提取、边界框预测、字符分割与光学字符识别等关键环节,适用于智能交通、安防监控等实时检测场景。资源共84个文件,压缩包约78.16MB,以Python脚本、JPG/JPEG样例图片、模型权重、YAML配置及XML标注文件为主,并附带Dockerfile、环境依赖说明和训练推理脚本,便于快速搭建环境并复现实验。目前已有2057人学习下载。除完整项目代码外,还提供教程式笔记、不同尺度模型配置及测试结果样例,方便对照理解模型优化与部署思路,同时可结合数据增强、非极大值抑制和模型轻量化等实现细节开展二次开发。整体结构清晰,适合希望掌握该技术在车牌场景下应用细节的学习者直接运行与进阶优化。
1. YOLOv5车牌检测和识别:为什么这个任务值得认真做一篇落地笔记
车牌检测和识别,听起来是老话题,但绝大多数照着网上的 demo 复现的人,都卡在黑匣子里:模型训练完看不出好坏、蓝牌能识别但新能源绿牌频繁翻车、识别部分要么慢要么精度差。这个标题背后其实是一条清晰的任务流水线:先用 YOLOv5 把车牌从复杂场景里框出来,再把框内的字符交给另一个识别模型或算法去读出车牌号。很多人以为这是一个模型能搞定的,实际是两段任务,每一段都有独立的模型选型、数据清洗和调参逻辑。这篇笔记按一条完整可复现的路径走一遍:技术栈为什么这么选、数据怎么转格式、训练参数怎么调、推理部署怎么接,以及那些不跑一遍根本碰不到的坑。适合手里有车牌图片、想自己训练并落地成品工具的开发者,也适合要把车牌识别能力集成到已有视觉平台里的团队。
2. 车牌检测和识别任务的最小技术栈:检测用 YOLOv5,识别用轻量OCR
2.1 为什么检测选 YOLOv5,而不是一上来就上大模型
车牌检测这个环节,工作的对象是一个很小的目标。在 1080p 的监控画面里,车牌通常只占图像面积的 1% 到 3%。检测模型要做的不是把整张图分类,而是在大图上准确定位出车牌框。YOLOv5 是这里最稳的起点:单阶段检测器,推理速度快,显存需求低,训练生态成熟,换硬件迁移也方便。
对比另一个常见方案,用检测加分割的 Mask R-CNN,精度确实可能略高,但推理速度和显存开销翻倍,部署起来要额外处理轮廓转外接框的逻辑。对于车牌这种矩形刚性目标,分割属于杀鸡用牛刀。YOLOv5 的回归头直接输出四边形框,配合旋转框处理能在大多数场景下覆盖车牌角度问题。在工程化视角下,团队协作、模型可解释性和迭代速度都很重要,YOLOv5 的学习资料最全,遇到问题最容易找到答案。
2.2 识别环节的三条路线:车牌字符识别不是 OCR 通用模型的简单迁移
框出车牌后,第二步是把框内图像变成一串字符,这就是车牌识别。这个环节有常见三条路线。
第一条是端到端模型,例如 LPRNet、CRNN 这类 CNN+RNN+CTC 结构,直接输入车牌图片输出字符序列。这条路的好处是字符切分和识别一步到位,端到端训练,工程链路短。第二条是字符分割后单字分类器,把车牌区域按字符宽度切分成一个个字,再用小分类器逐个识别。这在过去很常见,但切分受字符粘连、倾斜和铆钉干扰影响很大,现在被用得越来越少。第三条是直接复用通用 OCR,例如 PaddleOCR 这种,输出整图文本。实际效果在标准印刷体文档上不错,但车牌字符底色和字体特殊,通用模型不过滤蓝底白字、黄底黑字之外的合约牌、教练车牌等样式时,误识别率很高。
我在实际项目里用的是第一条路线:检测用 YOLOv5,识别用轻量 CNN+CTC 模型。版本选择上,YOLOv5s 或 YOLOv5n 作为骨干就够用。字符识别模型我用过 LPRNet 一个公开实现,也能用 PaddleOCR 的车牌识别模块。如果你只想搭建一个能跑通的 demo,YOLOv5s 加一个训练好的车牌 OCR 模型就是最清晰的路径。
2.3 两个模型的输入输出怎么衔接:坐标框是几何信息,字符是语义信息
整个流水线的衔接逻辑是这样:第一段 YOLOv5 的输入是整张原始图像,输出是若干带类别置信度的矩形框,形式是
class_id, x_center, y_center, width, height, confidence
第二段识别模型的输入是从原图上裁剪出来的车牌区域图像,输出是一个变长字符串。两段模型各自独立训练、独立推理。实际部署时,识别模型拿到的就是检测框裁剪出来的那一小块图。所以检测框如果偏了,识别结果一定错。很多项目最后精度上不去,根因就在检测框不稳,识别模型再努力也没有用。
用 YOLOv5 做检测、轻量 OCR 做识别这个组合,训练方式是把字符识别模型的输入规范为固定高度、动态宽度的灰度图像。车牌字符数是固定的(通常省份简称加字母数字共 7 位,新能源车牌 8 位),但只需要按最大长度留出 CTC 输出空洞即可。
2.4 模型性能边界:什么场景会扛不住
这套组合不是万能的。我复现过后发现几个硬边界,说明白这些,能省掉很多无效试错。极端角度下,车牌在画面里倾斜超过 45 度,检测框贴合不稳,识别精度掉得厉害;夜晚时段,车牌反光让字符区域变成一片白,识别模型几乎输出随机字符;运动模糊、车速过快导致的拖影,会让框架和字符同时糊掉;双层挂车车牌高度方向字符太密集,轻量识别模型经常读漏一行。
如果要硬啃这些场景,常规动作是扩充数据、增加旋转增强、引入图像去模糊预处理,或者换更大模型。但这些都属于进阶优化,不在基础路径里。先把标准场景跑通,再考虑对抗极端情况。A 同学之前就是在模糊场景较真了很久,后来先把白天车辆进出闸口的场景做稳,再接夜间,节奏明显更顺。
3. 把公开车牌数据集转成 YOLO 格式:一个转换脚本与四个边界坑
3.1 车牌数据集的来源与格式差异
训练车牌检测模型,首先要解决数据格式问题。公开的车牌数据集常见格式是 VOC 风格的 XML 标注,标注信息里保留原始图像尺寸和每个目标的矩形框坐标。YOLOv5 训练需要的是每个图像对应一个同名 txt 文件,每行存储归一化的类别、中心点坐标和宽高。拿到的数据集不可能天然就是 YOLO 格式,所以必须做一轮转换。
转换关键在于坐标系的换算。VOC 的标注是xmin, ymin, xmax, ymax,YOLO 用的是归一化的x_center, y_center, width, height。公式是:
x_center = (xmin + xmax) / 2 / img_width y_center = (ymin + ymax) / 2 / img_height box_width = (xmax - xmin) / img_width box_height = (ymax - ymin) / img_height看起来不复杂,但实操中边界情况一堆。我常看到新手直接照搬网上脚本,跑完才发现训练集里一堆坏框。
3.2 完整转换脚本:从 XML 到 YOLO txt
下面这段脚本是实际可用的版本,我基于一个常见基础脚本改过,加了对边界情况的检查和过滤。运行环境是 Python 3.8 以上,依赖xml.etree.ElementTree和PIL。
import os import xml.etree.ElementTree as ET from PIL import Image # 配置三个路径:XML标注目录、图像目录、输出txt目录 xml_dir = "annotations_xml" img_dir = "images" out_dir = "labels_yolo" os.makedirs(out_dir, exist_ok=True) # 类别映射,0对应车牌 class_map = {"plate": 0} def convert_annotation(xml_path, img_path, out_path): tree = ET.parse(xml_path) root = tree.getroot() # 读取图像实际宽高,防止XML里写的尺寸和真实图像不一致 with Image.open(img_path) as img: img_w, img_h = img.size lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_map: continue bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) # 框合法性检查:右边界必须大于左边界,且不超过图像宽高 if xmin >= xmax or ymin >= ymax: print(f"跳过非法框: {xml_path} ({xmin},{ymin},{xmax},{ymax})") continue xmin = max(0.0, min(xmin, img_w - 1)) ymin = max(0.0, min(ymin, img_h - 1)) xmax = max(0.0, min(xmax, img_w - 1)) ymax = max(0.0, min(ymax, img_h - 1)) if xmax - xmin <= 1 or ymax - ymin <= 1: continue # 中心点坐标和宽高换算,全部归一化到 [0,1] x_center = ((xmin + xmax) / 2.0) / img_w y_center = ((ymin + ymax) / 2.0) / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h line = f"{class_map[name]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}" lines.append(line) with open(out_path, "w") as f: f.write("\n".join(lines)) # 遍历所有XML文件,找到同名图像并转换 for xml_file in os.listdir(xml_dir): if not xml_file.endswith(".xml"): continue stem = xml_file.replace(".xml", "") xml_path = os.path.join(xml_dir, xml_file) img_path = os.path.join(img_dir, stem + ".jpg") if not os.path.exists(img_path): img_path = os.path.join(img_dir, stem + ".png") if not os.path.exists(img_path): print(f"找不到图像: {img_path}") continue out_path = os.path.join(out_dir, stem + ".txt") convert_annotation(xml_path, img_path, out_path) print(f"转换完成: {stem}")逻辑说明:脚本先读取图像真实尺寸,而非信任 XML 里的 width 和 height 标签。原因是有不少数据集导出时分辨率被重采样过,XML 的尺寸字段没跟着更新,用错尺寸会导致所有框偏移,训练出来检测框永远差一截。再做一次框范围裁剪,是为了防止标注坐标刚好越界一个像素导致加载时报错。最后判断宽高差大于 1 才写入,过滤掉最小目标噪声框。
参数说明:class_map按实际类别名调整。如果你的数据集里类别叫license_plate或者car,就改成对应映射,否则会被整段过滤掉。img_w-1这种写法是防止坐标等于图像宽度时归一化结果为 1,YOLOv5 读取时容易出 NaN 梯度。
3.3 四个边界坑:坐标越界、大小写后缀、标签名不统一、空标注文件
第一个坑是坐标越界。有些标注框超出图像边界,比如xmax等于图像宽度甚至更大。直接换算后目标框有一部分落在图外,训练时模型收到的正样本信息是错的。解决方式就是脚本里的 clamp 操作,同时把完全无效的框丢弃。
第二个坑是图像后缀大小写。数据集中.jpg和.JPG并存,脚本只查小写后缀会漏掉一批图,直接导致对应标签文件缺失,YOLOv5 训练时会跳过该图。稳妥做法是用os.listdir全量比对文件名,不假设后缀一致。
第三个坑是标注标签名不统一。同一个数据集里,plate、license_plate、car_plate混着写,或者在类别字段里多了空格。映射表里没写到的类会被直接过滤,训练集目标数量悄无声息地变少。处理办法是打印类别统计,转换后检查每个类的目标数量,确认没有丢类。
第四个坑是空 txt 文件。某些原图确实没有车牌,转换后 txt 文件是 0 字节。YOLOv5 训练时读不到目标会报错,或者在跑评估时把该图当异常跳掉。批量删除空标签文件,同时把对应的空图像清理掉。文件数量要人工过一遍,漏掉一个,后面训练日志里全是警告。
3.4 标注数据集的结构分配:验证集别偷懒
YOLOv5 训练需要images和labels两个目录,各自按train和val子目录分配。划分数据时不能随机乱切,要按场景切分:同一个停车场不同时间段、不同天气的图尽量均匀分到训练集和验证集,否则验证集里全是没见过的场景,评估指标波动会很大。我一般保持train : val 为 8 : 2 或 9 : 1,车牌数据量够大就 9:1,让验证集覆盖更多分布。
划分脚本本身不用写多复杂,用random.shuffle按比例切就行,但记得设固定随机种子,保证每次跑出来的数据划分一致。跑车检项目时常改逻辑,固定种子能省大量对账时间。
4. 训练 YOLOv5 车牌检测模型:参数怎么设、日志怎么判
4.1 预训练权重怎么选:从 COCO 权重迁移,不是从零开训
车牌检测模型不需要从零训练。YOLOv5 官方提供了在 COCO 上预训练过的权重,这些权重学会了边缘、纹理、对比度这些通用特征,对车牌这种强边缘矩形目标有天然优势。迁移学习能明显缩短收敛时间。YOLOv5s 是精度与速度的平衡点,显存 6G 以上的卡都能跑;YOLOv5n 更轻,适合嵌入式;追求更高精度可以换 YOLOv5m,但显存和推理延迟都会上来。
训练命令写在下面,改动核心参数即可用:
python train.py \ --data dataset.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --workers 4 \ --device 0 \ --project runs/plate_train参数说明:--data指向自己写的数据集配置文件,里面写明训练和验证图片路径、类别数量和类别名。--weights填预训练权重路径,YOLOv5 会自动下载对应版本。--img输入分辨率,640 是通用值。车牌是小目标,有时调大辨识度更高但显存会涨。--batch看显存,16 以上需要 8G 显存。--epochs先用 100 看收敛曲线,再决定是否加。--device 0指用第一张 GPU。--project是输出目录名。
4.2 dataset.yaml 怎么写:路径别用相对路径,类别名要和标签一致
dataset.yaml是训练配置的入口,写错一点训练直接报错。一个可用配置如下:
# dataset.yaml train: /data/plate_dataset/images/train val: /data/plate_dataset/images/val nc: 1 names: 0: plate逻辑说明:train和val指向图片目录,YOLOv5 会自动在同级目录下找labels里的 txt 标签。路径建议写绝对路径,相对路径在切换工作目录或跑多卡训练时容易丢。nc是类别数量,车牌检测就 1 类。names的索引要和标签文件第一列的数字对应,标签里写0,配置里0必须是plate。
4.3 训练日志怎么判:val mAP 才是主角,train loss 别太当真
训练过程中终端打出一堆指标,我一般只看三个:val mAP@0.5、val mAP@0.5:0.95和val Precision。train loss 下降是模型在拟合训练集,不能说明泛化能力;val mAP 才是真章。车牌检测任务目标是精确度高、漏检率低,mAP@0.5 能到 0.95 以上、mAP@0.5:0.95 能过 0.8,这个模型就足够干净场景使用了。
训练到 100 epoch,如果 val mAP 曲线还在上升,就加大--epochs到 200 接着跑。曲线进入平台期后不再变化,就可以停。有人在 200 epoch 后 mAP 还涨但涨得很慢,这个时候要看训练时间成本,考虑换更大的输入尺寸更划算,比如--img 960,小目标检测的收益比单纯加 epoch 明显。
4.4 常用数据增强的取舍:翻转增强应谨慎
YOLOv5 自带一系列数据增强,但对车牌任务,默认的马赛克增强保留即可,关键要关掉垂直翻转增强。垂直翻转后,字符上下颠倒,在车牌识别阶段的语义不可用。检测模型本身不受影响,但因为整个流水线要对接识别,检测框即使识别正确,识别模型对倒置字符完全无效。
水平翻转增强可以保留,对蓝牌效果影响不大,但要注意新能源车牌中字母和数字的位置固定,水平翻转后模型会学到错误的字符排列先验。稳妥做法是把水平翻转概率调低,比如--fliplr 0.0直接关闭。增不增强看数据量,数据量低于 5000 张时,关闭翻转增强反而更稳。
4.5 识别模型的训练:定长识别还是变长识别
车牌字符识别模型我建议走 CTC 变长识别路线,用 CNN 提特征,LSTM 或 GRU 建模序列,CTC 解码输出变长字符序列。字符集包含省份汉字、数字和字母,一共大约 70 个类别。汉字部分“京、津、冀、晋、蒙、辽、吉、黑、沪、苏、浙、皖、闽、赣、鲁、豫、鄂、湘、粤、桂、琼、川、贵、云、藏、陕、甘、青、宁、新”等,一个省份前缀加字母数字组合,新能源车牌还有额外位数。字符集要覆盖全,任何一个漏掉的汉字在测试时都会变成 ?。
如果只用公开的普通车牌识别模型,识别新能源车牌的位数上限要对齐。普通蓝牌 7 位,新能源车牌 8 位,用 CTC 模型的输出序列要支持最长 8 位解码。在字符识别训练脚本里,把序列长度设成 12 以上,多出来的尾部空位 CTC 会自己折叠掉,不碍事。
5. 对接部署:检测框裁剪、OCR 推理与格式规范化
5.1 检测到裁图:从全图到车牌区域,尺寸归一化
模型训练完之后进入推理阶段。推理脚本的流程是:图片输入检测模型,得到所有车牌框坐标,按置信度阈值过滤一次,然后对每个框做裁剪并送入识别模型。裁剪时不能直接把矩形框原图送进去,识别模型有固定的输入尺寸,常见的是高 32、宽 96,宽高比接近蓝牌的 2:3 或新能源牌的 3:2。直接把矩形框拉伸形变会影响识别精度,需要按宽度等比缩放、保持宽高比,再统一到目标尺寸。
我在推理里加了一个操作:先把裁剪区域保留原始像素比,灰度化后等比缩放到目标高度,宽度不够的部分用白色填充。白色填充比黑色填充好,因为车牌底色通常较深,字符是亮色,白色填充后字符和背景的对比度仍然是正的。这部分代码在推理脚本里不起眼,但对精度的影响很大。
5.2 识别模型的推理代码:输出字符串时注意空格与?的处理
识别模型推理输出是序列概率分布。用 CTC 解码时,要处理空白符。下面是一段实际可用的推理代码,基于 PyTorch:
import torch import numpy as np # 字符集,0 保留为 CTC blank char_list = ["_", "京", "津", "冀", "晋", "蒙", "辽", "吉", "黑", "沪", "苏", "浙", "皖", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "桂", "琼", "川", "贵", "云", "藏", "陕", "甘", "青", "宁", "新", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"] def decode_ctc(probs, char_list): # probs: (seq_len, num_classes) # 取每个时间步最高概率的类别索引 argmax = np.argmax(probs, axis=1) # 合并连续重复,并去掉 blank last_char = None res = [] for idx in argmax: if idx == 0: last_char = None elif idx != last_char: res.append(char_list[idx]) last_char = idx else: # 重复字符跳过 pass return "".join(res) # 使用示例:probs 来自模型输出的 softmax 结果 # plate_number = decode_ctc(probs_np, char_list)逻辑说明:CTC 解码的核心是合并规则。同一字符连续出现多次会被合并为一个。比如模型输出的连续帧都是“苏”,解码结果只保留一个“苏”。字符重复问题在车牌场景很少出现,但合并规则必须保留。代码里last_char记录上一个非空字符,遇到重复直接跳过。
参数说明:char_list第 0 位是空白符,训练时索引 0 固定为 CTC blank。推理时遇到索引 0 跳过。省份简称出现顺序要和训练集保持一致,训练标签是按蓝牌、黄牌的省份顺序编码的,推理端顺序对不上,全部识别结果都会错位。
5.3 置信度阈值怎么定:有没有漏检比误检更严重
推理时 YOLOv5 默认的置信度阈值是 0.25。实际部署中,闸机场景漏一把车牌很麻烦,停车场收费丢一个车牌直接少一笔计费。我一般调低到 0.15,宁可多识别几个假正例,再由识别模型去过滤那些不是车牌的框。识别模型接到非车牌图会输出乱码,就把乱码结果再过滤掉,代价很小。反过来,阈值设太高,漏检一个车牌,整条流水线输出没有记录,这个问题更难排查。
NMS 的 IoU 阈值保持默认 0.45 就行。车牌场景重叠框很少,不用调。调整置信度阈值之后,重新做一遍验证集评估,看哪些图是被阈值砍掉的,远比拍脑袋调参靠谱。
5.4 端侧部署与接口设计:模型格式转换和调用方式
训练完的 PyTorch 权重不能直接部署到生产环境,需要导出。常见做法是导出成 ONNX,再用推理引擎做后续转换。导出命令是修改 YOLOv5 仓库里的export.py推理入口。YOLOv5 自带支持,按模型文件路径和格式命令行导出即可。识别模型单独导出 ONNX,两个模型串成一条推理流水线。
接口层在项目里我一般拆成三个函数:detect_plates(image) -> boxes,crop_plate(image,box) -> image_crop,recognize_plate(image_crop) -> string。三个函数各自独立,改模型不影响接口调用方。输入输出类型统一成 NumPy 数组或字节流,避免框架耦合。
6. 避坑:车牌识别跑不起来时的 5 个典型问题与排查方法
6.1 现象:训练 100 个 epoch,val mAP 一直为 0
这是个常见的翻车场景:训练日志正常,loss 在下降,但 val mAP 永远是 0。原因基本是标签文件不对,要么 txt 全空,要么坐标换算错导致框落到图外,要么类别索引和dataset.yaml里的names对不上。还有可能是验证集图片路径里用了软链接,YOLOv5 读标签时找不到对应的 label 目录。
解决方法是训练前先单独跑一遍标签检查脚本,随机挑 20 张训练图,用cv2.rectangle把标签框画到图上,人工确认位置和大小对不对。这一步花 10 分钟,比训练完再排查省两小时。A 同学之前就是怀疑模型结构有问题,最终定位是类别映射表少了一个plate的别名,三分之一的数据被静默过滤了。
6.2 现象:训练正常,但识别时检测框总比车牌大一圈
检测框比实际车牌大,通常问题出在标注质量上。公开数据集里标注框本身就比车牌边缘松一圈,模型学会了带框。另一种原因是输入分辨率过低,小目标的框回归精度受到限制。排查方法是取几张 val 图跑推理,把框画出来看偏移方向。如果四周都有余量,就是标注松了,用标注工具重新收紧边界,或者加细微的 margin 裁剪。如果只在上下方向偏,检查数据增强里是否有随机的平移裁剪覆盖了目标。
处理这种问题一般是对标注做个后处理:按标注框向内收缩 5% 作为新的真值。针对车牌这类大矩形目标,收紧边界后检测框贴合度明显提升。
6.3 现象:识别模型对汉字总是识别错
汉字识别错,常见于两个原因。一是字符集不完整,模型训练时就没见过某个汉字,推理时只会输出概率分布中相近的其他汉字。二是训练数据里各省份简称数量极不均衡,某省车牌占比超过一半,模型对低频省份汉字欠拟合。
处理方式是先统计训练集字符频次,确认低频汉字有没有进入训练。数据不够的省份简称,可以单独收集该省份车牌图或做人工合成数据补足。一个实际效果不错的方法是,将已有车牌图做字符级替换增强:把“粤”字区域扣掉,贴上“云”字。这需要字符切分的辅助工具,但能有效补足省份分布。如果只是 demo 不需要全省份,直接在字符集里把未覆盖的省份删除,宁可输出空串也不要输出错误结果。
6.4 现象:部署后识别速度慢,来不及处理视频流
速度瓶颈几乎都在识别模型。YOLOv5s 在 GPU 上单帧检测约 10 到 20 毫秒,识别模型如果用偏大的 CRNN,加上字符词典大,单帧很容易超过 30 毫秒。再加上 Python 接口和多张图重采样,整体延迟容易冲到 80 毫秒以上。
解决方式是把识别模型的骨干换得更轻。LPRNet 本身就比 CRNN 轻不少,逐帧速度能到 5 毫秒以内。另一个容易忽略的优化点是批量推理:多张检测框一次性送入识别模型,用 batch 方式替代循环推理,吞吐量提升非常明显。视频流里一帧往往只有一辆车,一个车牌框,批量收益一般。但在停车场出口同时出现三四辆车时,批量推理一次处理四个裁剪图,整体延迟提升一截。
6.5 现象:图片里有多个车牌时,输出结果张冠李戴
结果对应错车牌,不是识别模型的问题,是检测结果和车牌目标对应关系没维护好。检测模型输出框的顺序在每次推理中不稳定,如果代码里用顺序索引去关联识别结果,一旦某一帧少检出一个车牌,后面所有结果全部错位。
解决方式是给每个检测框一个唯一标识,比如按框的中心点坐标去排列并作为 key,识别结果按键值对应存储。在多目标追踪场景里,建议按车辆在画面中的位置或 ID 做关联,而不是依赖检测框输出顺序。这个坑在单目标 demo 里暴露不出来,但一接多目标监控就会翻车。
7. 进阶验证:自定义车牌样本的批量回归测试与模型迭代习惯
模型迭代想要真正落地,不能只盯着 mAP 曲线。我最后的建议是,在训练之外建立一组固定的回归测试集,每次调整模型后都跑同样的验证流程,用统一指标对比历史结果。这组回归集不要太难,覆盖每个类型车牌各几十张,包含白天、夜晚、不同角度,并且严格与训练集无交集。测试集一旦固定,后面每个版本迭代都能直接对比好坏。
回归测试不需要写很复杂的工具。一个简单的脚本,跑检测加识别,输出每张图的实际车牌号和预测车牌号,保存成 CSV 对比表。逐行人工扫一遍,重点关注汉字是否识别对、车牌类型是否混、浓雾或夜间场景有没有系统性失败。跑完后再算整体准确率,记录一个基线数字,就能指导下一次迭代方向。
# 简单回归脚本骨架 import csv import cv2 from detector import detect_plates from recognizer import recognize_plate image_list = [ "test_set/day/blue_001.jpg", "test_set/day/green_001.jpg", "test_set/night/blue_002.jpg", ] results = [] for img_path in image_list: img = cv2.imread(img_path) boxes = detect_plates(img) if len(boxes) == 0: results.append([img_path, "none", "none"]) continue for box in boxes: crop = crop_plate(img, box) pred = recognize_plate(crop) results.append([img_path, "unknown_gt", pred]) with open("regression_result.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["image", "ground_truth", "prediction"]) writer.writerows(results)验证脚本要保留成固定文件,每次训练完新权重直接改模型路径再跑一遍,不另写新工具。这样可以保持长期统一口径。正确率没有提升时,先回头查回归集里的失败样本是检测框偏了还是字符识别错,用一次区分步骤定问题归属,也减少无用功。
我的一个深刻教训是,有段时间为了追求 mAP 指标反复调增强参数,跑了几十个夜晚训练版本,回归精度几乎没有变化。后来静下心分析失败样本,发现大多数是夜间反光导致的字符过曝。把问题定位到数据采样层面后,新增了一批过曝样本做针对性训练,精度一次性提升了近两个百分点。从那以后,我建立了固定回归集优先的习惯——先用最小代价确认问题在哪一段模型,再开下一轮训练迭代。这个习惯能帮你把时间花在真正有效的地方,也让每个迭代有据可查。希望帮到你。
本文还有配套的精品资源,点击获取