1. 复杂环境下车牌识别到底难在哪:先看清问题再动手
做车牌识别这个方向,很多人一开始都会觉得“不就是先检测车牌,再识别上面的字符吗”,等自己拿真实数据跑一遍才发现完全不是那么回事。我最初也是在干净数据集上把准确率刷到95%以上,结果一到停车场、收费站、路测场景,立刻被打回原形。所以这篇文章我想先从问题本身聊起,把我踩过的坑、试过的方法、最终能稳定跑通的方案都整理出来。
先说结论:复杂环境下的车牌识别,核心难点根本不在“识别”这一步,而在“能不能稳定拿到一张高质量的、正视角的车牌图像”。这里的复杂环境包括但不限于光照剧烈变化、车牌倾斜旋转、运动模糊、遮挡、雨雾天气、夜间低照度、高光反射,甚至还有车牌本身被泥水污染、边框铆钉干扰、字体磨损老化等情况。任何一个因素叠加进来,识别率都会明显下降。
对于计算机视觉方向的初学者,或者正在做大作业、课程设计的学生,我建议不要一上来就追求端到端的黑盒模型。先理解“检测 + 校正 + 识别”这条经典链路,把每个环节的问题拆清楚,再决定哪些环节用深度学习、哪些环节用传统图像处理。实际工程里,传统算法并没有过时,有些环节它比深度模型更稳、更快、更省资源。
一个典型的两阶段车牌识别系统可以拆成下面几个子任务:
- 车牌区域定位:从复杂背景中找到车牌所在的矩形区域,这一步属于目标检测任务,需要排除汽车前脸、行人、路面文字、路牌等干扰。
- 车牌角度校正:实际拍摄中车牌往往不是理想的正视矩形,而是带有透视畸变的四边形,需要做透视变换还原。
- 车牌字符分割与识别:把校正后的车牌图像中的字符逐个(或者整体序列化)识别出来,输出最终的字符串结果。
- 后处理与规则校验:根据车牌编码规则对识别结果进行合法性校验和纠错,比如汉字省份简称、发牌机关代号、编号字符的组合规律。
我先用一个生活化的类比来解释这套流水线:这就像你在路边看一辆飞驰而过的汽车,想记下它的车牌号。你首先要“锁定”车牌位置(注意力机制),接着如果车是歪着开过去的,你脑袋会不自觉地把画面“摆正”再读(空间变换网络),最后你才能逐个字符念出来(序列识别)。计算机视觉做这件事,本质上就是把这个人类感知过程拆成显式的算法步骤。
2. 车牌检测阶段怎么选型:深度学习检测器与传统图像处理的分工
2.1 车牌检测的经典路线:颜色特征 + 边缘特征 + 形态学操作
在深度学习还没有普及的年代,车牌检测主要依靠颜色空间转换和形态学操作。蓝底白字是中国最常见的车牌样式,所以一个很自然的思路是先把图像从RGB转到HSV色彩空间,提取蓝色区域作为候选区域候选。为什么用HSV而不是直接用RGB?因为HSV将颜色信息分解为色相(H)、饱和度(S)和明度(V),对光照变化的鲁棒性远好于RGB。RGB三个通道都随光照强度整体缩放,阈值不好定;而H分量在小幅光照变化下基本稳定。
具体操作上,我用OpenCV实现过的流程大概是:
import cv2 import numpy as np def get_plate_candidates(image): # 转HSV色彩空间 hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) # 蓝色车牌阈值范围(H,S,V) lower_blue = np.array([100, 80, 80]) upper_blue = np.array([124, 255, 255]) mask_blue = cv2.inRange(hsv, lower_blue, upper_blue) # 形态学闭运算,连接相邻区域 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5)) mask_closed = cv2.morphologyEx(mask_blue, cv2.MORPH_CLOSE, kernel) # 查找轮廓 contours, _ = cv2.findContours(mask_closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates = [] for cnt in contours: # 计算外接矩形,用长宽比筛选 x, y, w, h = cv2.boundingRect(cnt) aspect_ratio = w / h area = w * h # 标准车牌长宽比约3:1,适当放宽 if 2.0 < aspect_ratio < 5.0 and area > 2000: candidates.append((x, y, w, h)) return candidates这套方法在光照均匀、背景简单的场景下效果不错,计算速度快到可以在树莓派上实时跑。但它的缺陷也很明显:一旦遇到白色新能源车牌(渐变绿色底)、黄色大车车牌,或者车身本身就是蓝色、场景里有蓝色广告牌,纯颜色阈值就崩了。所以我后来在真实项目里,只用它做“快速初筛”,候选区域会继续交给分类器确认。
2.2 基于深度学习的检测器选择:YOLO系列与开源车牌检测模型
复杂环境下要稳,还是得上深度学习目标检测。我实测过YOLOv5和YOLOv8在车牌检测上的表现,精度差距不算特别大,但YOLOv8在边缘设备上的推理效率更高,部署也更方便。这里我分享一个关键经验:用开源预训练模型做迁移学习,比自己从头训练省太多事。
网上比较成熟的开源车牌检测模型有几个方向:
- 直接用YOLO官方COCO权重,但COCO数据集里没有专门的车牌类别,所以这条路走不通,得自己标注或找车牌数据集微调。
- 用PP-OCR系列里的文本检测模型,它的检测头能框出场景中的文本区域,车牌本质上也是文本,所以效果不错,尤其是中国车牌。
- 找现成的车牌检测开源项目,比如GitHub上有很多基于YOLOv5的车牌检测仓库,通常附带标注好的国内车牌数据集。
我的建议是:先下载一个已经用国内车牌数据微调过的YOLOv5或YOLOv8权重,跑一遍你的测试视频,看看哪些场景下漏检、误检,再针对性补充数据微调。不要一上来就自己标几千张图,标注成本太高,而且新手容易在anchor参数上翻车。
关于anchor这个细节,我用YOLOv5时踩过一次坑。默认anchor是基于COCO数据集聚类出来的,COCO里的大目标尺度和车牌分布差异很大,导致训练初期loss降得很慢。后来我改用自定义数据集重新聚类anchor,训练收敛速度明显加快,最终mAP也高了几个点。具体操作是把标注好的车牌框长宽喂给k-means聚类,生成适合车牌尺寸的先验框,然后在yaml配置里替换默认值。
2.3 候选区域再确认:一个轻量分类器解决误检问题
深度学习检测器偶尔也会把车灯、散热器格栅、前保险杠装饰条当成车牌。这时候引入一个轻量级二分类器(车牌/非车牌)做二次确认非常有效。我常用的做法是从检测结果里裁剪出候选区域,缩放到统一尺寸,然后丢给一个简单的CNN分类器。
这里有一个工程上的省事技巧:如果检测器本身够强(比如mAP 0.5以上达到95%),你可以跳过独立的二分类器,直接用字符识别结果做反向确认。如果OCR出来一串字符,并且符合车牌编码正则表达式,那就接受检测框,否则丢弃。这个策略我把误检率压到了很低,几乎不影响速度。
3. 透视几何在车牌校正中的应用:倾斜车牌的摆正实战
3.1 为什么必须做透视校正
很多初学者直接把检测框里的图像送去识别,结果发现识别率很不稳定。原因在于,车辆在画面里不总是正对着镜头。侧方停车、转弯、跟车距离近、摄像头安装角度高,都会让车牌产生透视畸变——车牌不再是标准矩形,而是变成不规则的四边形。
字符识别模型对输入图像的归一化要求很高。CRNN这类序列模型在稍微倾斜的文字上还能勉强工作,但倾斜角度一大,字符之间会发生重叠、粘连,识别准确率断崖式下跌。所以,在检测和识别之间加一个透视校正模块,是整个系统稳定性的关键所在。
3.2 四点透视变换的原理与实现
四点透视变换的基本思路是:在畸变图像上找到车牌的四个角点,然后计算一个变换矩阵,把四边形映射到一个预设尺寸的矩形上(比如 440x140,对应标准车牌尺寸比例)。变换矩阵的计算在OpenCV里就是cv2.getPerspectiveTransform,一行代码的事。真正的难点在于怎么稳定地找到那四个角点。
我用的方法是基于轮廓的近似多边形拟合。车牌检测框已经缩小了范围,在框内再做一次边缘检测和轮廓查找,然后用cv2.approxPolyDP把轮廓拟合成四边形。如果轮廓点数为4,就把它作为车牌的四条边;如果点数不是4,则退回到检测框的四个角点。
def order_points(pts): # 按左上、右上、右下、左下顺序排列四个角点 rect = np.zeros((4, 2), dtype="float32") s = pts.sum(axis=1) rect[0] = pts[np.argmin(s)] # 左上角,x+y最小 rect[2] = pts[np.argmax(s)] # 右下角,x+y最大 diff = np.diff(pts, axis=1) rect[1] = pts[np.argmin(diff)] # 右上角,x-y最小 rect[3] = pts[np.argmax(diff)] # 左下角,x-y最大 return rect def four_point_transform(image, pts): rect = order_points(pts) (tl, tr, br, bl) = rect # 计算输出矩形的最大宽度和高度 width_top = np.linalg.norm(tr - tl) width_bottom = np.linalg.norm(br - bl) max_width = max(int(width_top), int(width_bottom)) height_left = np.linalg.norm(bl - tl) height_right = np.linalg.norm(br - tr) max_height = max(int(height_left), int(height_right)) dst = np.array([ [0, 0], [max_width - 1, 0], [max_width - 1, max_height - 1], [0, max_height - 1] ], dtype="float32") M = cv2.getPerspectiveTransform(rect, dst) warped = cv2.warpPerspective(image, M, (max_width, max_height)) return warped注意order_points这一步看起来很不起眼,但角点顺序一旦错了,变换结果就是镜像翻转或者对角线拉伸,整个识别全废。四个点的排列逻辑是:左上角的x+y和最小,右下角的x+y和最大,右上角的x-y差值最小,左下角的x-y差值最大。这个技巧我在多个项目里复用过,很可靠。
3.3 透视校正后的尺寸归一化
透视变换输出的尺寸是动态计算的,不同帧之间可能大小不一致。在做字符识别前,我还会把它统一缩放到固定尺寸。缩放比例建议保持车牌宽高比不变,避免字符被横向或纵向拉伸变形。
以中国蓝牌为例,标准尺寸440mm x 140mm,宽高比约3.14。我一般缩放到240x76或者300x96,这个分辨率下的字符细节足够识别模型使用,同时计算开销也不大。新能源车牌是480mm x 140mm,宽高比约3.43,检测到绿色车牌时应按这个比例归一化,否则字符宽度会被压缩,影响识别率。
4. 车牌字符识别:从传统分割到序列识别的演进与选型
4.1 传统字符分割方案的局限
早期的车牌识别系统普遍走“字符分割 + 单个字符分类”的路线:先通过投影法找到字符间的空隙,逐一切出单个字符图像,再用分类器(SVM、CNN等)逐个识别。这个方案在图像质量好的时候表现尚可,但一到复杂环境就非常脆弱。
投影法分割字符的核心是统计每一列的像素值或边缘强度,找到波谷作为字符分隔点。这个思路有个致命弱点:车牌上的铆钉、边框、汉字联通笔画、反光条都会造成投影曲线异常,导致分割位置偏移。而且一旦分割错误,后面每个字符都错位,整个识别结果报废——错误传递是串联系统的通病。
4.2 CRNN + CTC:免分割序列识别的核心逻辑
现在的方案主流是CRNN(Convolutional Recurrent Neural Network)+ CTC(Connectionist Temporal Classification),直接对整张车牌图像做序列识别,不需要显式分割字符。它的原理可以这样理解:CNN负责提取图像的空间特征,RNN(通常是双向LSTM)按时间步处理这些特征序列,CTC负责把预测序列对齐到最终标签,并自动处理字符之间的空白和重复。
CTC的关键作用是解决“对齐问题”。字符识别中,我们不知道每个字符在图像序列里对应哪几个时间步。CTC为每个时间步预测一个字符分布,并允许输出中包含空白符号(blank),然后在解码阶段把所有相邻重复字符和空白合并,得到最终结果。举个简化例子:如果时间步预测序列是"A-A-空白-1-1-2-空白-3-3",合并后就是"A123"。这个机制让模型可以端到端训练,不需要人工标注字符位置。
在车牌识别这个场景里,我推荐直接用LPRNet(Lightweight Plate Recognition Network),它是专为车牌识别设计的轻量级网络,结构上也是CNN + RNN + CTC的思路,但比通用CRNN更精简,模型的参数只有几十万,CPU上跑一张图只要几毫秒。LPRNet还有一个优势是支持不定长字符序列,这对新能源车牌(比蓝牌多一位)特别友好。
4.3 车牌字符集的特殊性:汉字识别与易混淆字符
车牌字符集和普通OCR有本质区别:它包含省份汉字(31个省级行政区简称)、发牌机关字母(24个,不含I和O)、以及数字和字母组合。分类空间大约70类左右,比通用OCR小得多,但难在字符间相似度极高。
汉字里“京”“津”“冀”“鲁”等字形差异相对明显,比较容易区分;但字母和数字之间的混淆是重灾区——O和0、I和1、Z和2、B和8、G和6,在低分辨率、模糊、污损情况下极其容易认错。我处理这类问题有两个经验:
- 在训练数据里做有针对性的数据增强,对易混淆字符对做更多的模糊、噪声、形变扰动,让模型学会在“模糊”状态下区分它们。
- 在后处理阶段用车牌编码规则做约束修正。比如中国车牌第2位是省份简称的字母,取值范围是"A-Z"但排除"I"和"O";编号位如果识别结果是"O",则可以依据上下文修正为"0",识别结果是"I"同理修正为"1"。这类规则用正则表达式实现很简单,但能实打实把整体准确率提升1到2个百分点。
4.4 正则表达式后处理技巧
后处理这块值得专门说说。我的流水线里会先让模型输出原始识别结果,再做一套基于规则的清洗:
import re PROVINCE_CODES = set("京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新") LETTERS = set("ABCDEFGHJKLMNPQRSTUVWXYZ") DIGITS = set("0123456789") def validate_plate(text): # 移除可能混入的干扰字符 text = text.strip().upper() if len(text) < 7 or len(text) > 8: return None # 第一位必须是省份汉字 if text[0] not in PROVINCE_CODES: return None # 第二位必须是发牌机关字母(排除I和O) if text[1] not in LETTERS: return None # 最后一位如果是新能源车牌可能为字母,其他情况多为数字 # 这里做宽松处理,仅保证不含非法字符 for ch in text[2:]: if ch not in LETTERS and ch not in DIGITS: return None return text正则表达式在实际工程里还有一个作用:可以在README里作为验收标准。我给别人交付系统时,都会直接写明“识别结果必须通过车牌字典校验”,这样运维人员拿到系统后自我检测也有据可依。
5. 光照、模糊与恶劣天气:复杂环境下的图像预处理策略
5.1 光照不均与高光反射的处理
停车场出入口常见的场景是:晴天正午,阳光直射车牌,蓝底变成淡蓝甚至泛白,白字变成亮白,对比度极低。这种高光情况下的车牌,字符边缘信息几乎丢失,直接用原图识别,模型会非常挣扎。
我实测有效的预处理组合是:先转灰度图,再做CLAHE(对比度受限的自适应直方图均衡化)。CLAHE和普通直方图均衡化的区别在于,它是分块处理的,避免全图均衡化对局部亮度的过度拉伸。
def preprocess_plate(plate_bgr): # 转灰度 gray = cv2.cvtColor(plate_bgr, cv2.COLOR_BGR2GRAY) # CLAHE:clipLimit控制对比度限制,tileGridSize控制分块大小 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) equalized = clahe.apply(gray) return equalized参数上我调试过一些组合:clipLimit在1.5到3之间对车牌识别效果比较稳定,太大容易放大噪声,太小起不到增强作用。tileGridSize一般8x8就够,车牌本身尺寸不大,分块太多反而会出现块状伪影。
夜间场景的处理思路不同。夜间车牌经常处于车灯直射或路灯暗光下,图像整体噪声大、细节少。这时可以先做去噪(双边滤波或NLM),再适度提高亮度。另外,暗光环境下用红外摄像头是另一个维度的事——很多收费站和停车场道闸直接用红外补光设备,因为牌照表面是反光材料,在特定角度的红外光下字符会异常清晰,这种硬件方案能从源头上简化软件问题。
5.2 运动模糊与图像去模糊
车辆行驶中拍摄的视频帧,多多少少会有运动模糊。运动模糊的成因是曝光时间内物体在传感器上产生了位移,相当于图像在某个方向上被卷积了一个一维核。理论上可以用维纳滤波或Richardson-Lucy算法做去卷积,但实际效果取决于模糊核估计的准确性,而且计算开销大,在实时视频流里很难落地。
工程上我更推荐两个轻量替代方案:
- 从视频流里选帧:如果车牌在连续多帧里都出现,挑清晰度最高的一帧再识别。清晰度的度量可以用Laplacian算子的方差,方差越大代表边缘越锐利,图像越清晰。
- 轻微锐化:对图像做unsharp mask,增强字符边缘对比,让模型更容易提取特征。锐化幅度要控制,过头会出现光晕状伪影。
其实在真实系统中,“选帧 + 多帧投票”是提升识别率性价比最高的方式。单帧准确率95%时,连续抽5帧至少3帧识别一致的概率超过99%,而且计算量没有实质增加。这个思路在道闸、停车场场景里非常实用。
5.3 低分辨率图像的超分处理
如果摄像头安装位置离车道较远,车牌在画面里的像素宽度可能不到100像素,人眼都看得费劲。字符识别模型在这种分辨率下表现很差,可以尝试超分辨率重建。轻量级的超分方案比如ESPCN(Efficient Sub-Pixel Convolutional Neural Network)速度够快,能在边缘设备上实时跑。不过要提醒的是,超分不是万能的,它只是放大图像并“脑补”细节,如果原图本身已经模糊到字符轮廓断裂,超分也难以恢复语义信息。在硬件允许的情况下,优先考虑调整摄像头焦距、安装位置,从源头保证车牌成像像素数,远比后期算法修补更可靠。
6. 端到端系统搭建:代码框架与性能调优笔记
6.1 完整推理流水线的代码结构
把前面所有模块串成一个完整系统,我习惯用类来封装,方便后续维护和测试:
class PlateRecognizer: def __init__(self, detector_weights, recognizer_weights): self.detector = self._load_detector(detector_weights) self.recognizer = self._load_recognizer(recognizer_weights) def detect(self, image_bgr): # 返回候选车牌框列表 [(x1, y1, x2, y2, confidence)] results = self.detector(image_bgr) boxes = [] for det in results.xyxy[0].cpu().numpy(): x1, y1, x2, y2, conf, cls = det if conf > 0.5: boxes.append((int(x1), int(y1), int(x2), int(y2), conf)) return boxes def recognize(self, plate_region_bgr): # 预处理 + 透视校正 + 字符识别 corrected = four_point_transform(plate_region_bgr, self._get_corners(plate_region_bgr)) processed = preprocess_plate(corrected) text = self.recognizer(processed) return validate_plate(text) def run(self, image_bgr): # 完整流程:检测 -> 校正 -> 识别 -> 校验 plates = [] for box in self.detect(image_bgr): x1, y1, x2, y2, conf = box region = image_bgr[y1:y2, x1:x2] text = self.recognize(region) if text: plates.append({"box": (x1, y1, x2, y2), "plate": text, "confidence": conf}) return platesrun方法是核心入口,输入一帧图像,输出检测到的所有车牌及识别结果。多辆车同时出现在画面里时,这个循环会遍历所有检测框,分别做识别——这是停车场入口的刚需场景。
6.2 性能优化:从帧率瓶颈到实时推理
如果目标是在普通PC上跑实时视频流(25FPS以上),性能优化是绕不开的。我实测过几个优化点的性价比排序:
- 图像缩放:把输入图像缩放到合适尺寸再送给检测模型。YOLO系列输入分辨率对推理速度影响非常大,从1280降到640,帧率可能翻倍,mAP损失却只有1到2个点。
- TensorRT加速:NVIDIA GPU环境下,把PyTorch模型导出为TensorRT引擎,推理时间通常能压缩一半以上。导出时注意固定batch size,动态shape会降低优化效果。
- 检测与识别并行:在视频流里,车牌检测和字符识别可以放到不同的线程里,用队列传递候选区域。识别一张车牌也只要几毫秒,但并发处理能避免一些偶发的卡顿。
- 跳帧策略:对于实时视频流,不需要每帧都做检测,可以每隔一帧做一次检测,识别阶段只对置信度高的新候选框执行,减少重复计算。
6.3 模型部署的注意事项
真正把系统部署到现场后,会发现训练时完全没遇到过的问题。比如摄像头视角固定但光照环境全天变化,上午和下午的检测置信度差异巨大。我建议在项目早期就在测试机位采集全天候数据,至少覆盖白天强光、黄昏逆光、夜间暗光三个时段。数据里如果连这种基本变体都没有,模型泛化就是一句空话。
硬件选型上,小算力场景(比如道闸一体机)推荐用推理框架的INT8量化,普通Jetson Nano上跑YOLOv5s + LPRNet能稳定达到实时;大算力场景(如服务器集中处理多路视频流)则直接用TensorRT加多路异步推理,吞吐量优先。
7. 实战踩坑记录:六个最容易翻车的细节
7.1 车牌边框和铆钉对字符识别的影响
国内车牌的白色边框和防拆铆钉经常被一起抠进检测框里。CRNN这类序列模型对整张图做特征提取,边框的竖线在特征图上会形成明显的垂直纹理,严重干扰字符序列的特征分布。处理办法是透视校正后做一次内边框裁剪——车牌的字符区域不会紧贴外边缘,通常向外边框方向收缩3到5个像素就能避开大部分干扰。如果车牌图像本身分辨率够高,还可以用颜色分析去掉白色边框,只保留蓝底区域。
7.2 新能源车牌与蓝牌的尺寸差异
新能源车牌是渐变绿色底、8位字符,比蓝牌多一位且车牌的宽高比也不同。你的系统如果只按蓝牌的数据训练,碰到绿色车牌时检测可能漏掉,识别也可能因为字符长度不匹配而报错。比较好的做法是:检测和识别模型都混入新能源车牌数据,同时在后处理里分别适配8位和7位两种长度。不要偷懒只做7位验证,否则路上跑的绿牌车会直接让你的准确率掉一截。
7.3 夜间反光导致的字符“消失”
车牌材质本身有反光涂层,夜间遇到车灯直射时,字符区域可能白得一片,人眼都看不清边上写了什么。我试过很多图像增强算法,什么gamma变换、MSRCR、暗通道先验去雾,效果都不如直接在硬件上安装偏振片、调整补光灯角度来得立竿见影。如果软件方案受限,至少可以做一个简单的过曝检测——图像中高光像素占比超过阈值时,主动丢弃该帧,等下几帧再识别。
7.4 多车牌场景的排队问题
监控画面里同时出现两辆车时,检测器可能只输出了一个框,尤其当前后车距离很近时,前车车牌和后车车牌可能被框进同一个矩形里。这个问题的本质是NMS(非极大值抑制)的参数设置和特征不够细粒度。我处理方法是:在NMS阶段调低IoU阈值,这样两个重叠的车牌框更不容易被合并成一个;另一个思路是训练时多加入遮挡和相邻车辆的场景数据。
7.5 识别结果的稳定性:多帧投票机制
道闸场景中,车辆是动态驶过的,每帧图像质量波动很大。单帧识别结果可能今天准、明天就不准,因为光照和角度在变。我最终上线用的是多帧投票机制:连续5帧中,如果3帧以上识别结果一致,就作为最终结果输出;否则等待下一轮。这个机制同时也抑制了偶发误检,准确率比单帧要提高不少。代价是延迟增加了几百毫秒,但对道闸放行这种场景完全无感。
7.6 字符分割方案在复杂环境下为何彻底失败
我前面提到了传统“分割+分类”方案在复杂环境下的脆弱性。这里再展开一下,很多课程设计或开源项目仍然用这种方案,因为在实验室环境拍几张正视角、光照均匀的照片,投影法确实能完美分割。可一旦遇到倾斜、逆光、字符磨损,分割线就开始漂移。与其花大量时间调分割参数,不如直接换CRNN/LPRNet这类免分割方案。同样一批数据,免分割方案的准确率通常高出5到10个百分点,我在不止一个数据集上验证过这个结论。
8. 数据、评估与持续迭代:让识别系统越用越准
8.1 数据增强怎么加才不会过拟合
车牌识别场景的数据增强和通用图像分类不太一样。除了常规的旋转、缩放、色彩抖动,更应该关注的是模拟摄像头成像特性的增强:运动模糊、高斯噪声、透视畸变、光照变化、雨天水滴效果。我用imgaug库做增强时,会给每个样本随机组合3到5种增强操作,而不是每次只做单一变换。这样模型见到的“真实”的复杂情况更多,泛化能力明显更好。
8.2 评估指标别只盯着OCR准确率
做系统交付时不要太迷信识别准确率这个单一指标。一个完整的车牌识别系统至少要盯三个指标:检测的召回率(有没有漏掉车牌)、检测的精确率(误检多不多)、识别准确率(检测到的车牌里识别对的占比)。很多人在演示时只报识别准确率95%,但没统计漏检率——如果100辆车里有5辆压根没检测到,那实际效果就打了折扣。工程评估时,建议按“端到端准确率”来算:从整段视频或整张图出发,系统最终给出的所有车牌中,完全正确且没有遗漏的比例。
8.3 从失败样本中持续迭代
系统上线后,把每天识别失败的样本存下来,定期做bad case分析。我统计过真实场景下的失败案例,排名靠前的原因往往是:车辆速度过快导致运动模糊、车牌部分遮挡(比如拖车钩、防护栏)、极端的逆光角度。针对模糊类失败,我尝试过GAN-based去模糊,但推理速度太慢,工程上不划算;针对遮挡类,最终是靠多帧信息融合解决的——前几帧能看到遮挡区域,就用时间维度的信息补全。
8.4 一个实用的数据集组织规范
如果你决定自己收集数据微调模型,数据目录建议这样组织:
plate_dataset/ ├── train/ │ ├── images/ │ ├── labels/ │ └── annotations.json ├── val/ │ ├── images/ │ └── labels/ └── test/ ├── images/ └── labels/标注格式取决于你选的检测框架。YOLO系列用txt文件自带的格式,每个标注是“类别 x_center y_center width height”,坐标都归一化到0到1之间。注意透视畸变的车牌不要标成轴对齐矩形标得过大——框得多一点问题不大,但框得偏了会把旁边背景带进来,拉低识别效果。标完后抽查一遍很有必要,我自己就遇到过标注文件里标签错位导致训练完全乱套的情况。
9. 开源模型与工具链速查:能直接上手的资源清单
如果你不想从零训练模型,这条路最省时间。我整理过一份常用资源选型表,这里直接列出来:
| 环节 | 推荐方案 | 说明 |
|---|---|---|
| 车牌检测 | YOLOv5 / YOLOv8 微调 | 开源权重多,社区成熟,部署资料齐全 |
| 文本检测替代方案 | PP-OCR Det(PP-LCNet) | 对不规则文本鲁棒,车牌可当作文本处理 |
| 车牌识别 | LPRNet | 轻量级、专为车牌设计,支持不定长序列 |
| 综合OCR引擎 | PaddleOCR | 检测+识别一体,可配置文本检测和识别模型做端到端车牌识别 |
| 数据标注 | LabelImg / X-AnyLabeling | LabelImg轻量,X-AnyLabeling支持半自动标注 |
| 数据增强 | imgaug / albumentations | 图像增强库,支持多种变换组合 |
| 推理部署 | ONNX Runtime / TensorRT | ONNX方便跨平台,TensorRT GPU加速明显 |
| 车牌数据集 | CCPD(中国城市停车场数据集) | 脚本丰富,包含多种复杂场景,规模和标注质量比较理想 |
特别解释一下CCPD,它是一个公开的中国车牌数据集,标注包含四角点坐标,可以直接用于训练透视校正模型或者检测模型,数据量有几万张,基本覆盖了各种天气和光照条件。不过在复杂场景的版本多样性上,不同子集的难易程度差异比较大,选型时记得看子集说明。
还有一个细节:用PaddleOCR做车牌识别时,它默认的文本识别模型是针对中英文文本的,字典里包含大量通用中文字符,对省份汉字这种受限字符集会浪费模型容量。建议在车牌数据集上微调它的识别头,或者把输出字典裁剪成车牌专用字符集,推理速度和准确率都能提升。
10. 关于算法选型的一些个人体会
做了这么多车牌识别项目,我最大的感受是:计算机视觉工程问题,方案选型永远比调参更重要。检测阶段,深度学习是必须的,传统颜色方法只配做辅助;校正阶段,透视几何仍然是性价比之王,深度学习在这个环节反而不是最优解;识别阶段,CRNN/LPRNet这类免分割方案完胜传统分割方案;后处理阶段,规则引擎和字典校验是最不可忽视的兜底手段。
如果只让我用一句话概括复杂环境下车牌识别的核心,那就是:让每个环节都做它擅长的事,然后用系统级的策略去抵消单点的误差。单帧不准,就多帧投票;单模型误检,就加规则校验;单图模糊,就多帧优选。这套系统思维,比追各种新鲜模型框架重要得多。
最后再分享一个我在实际项目中的小技巧:方案交付时,把识别结果和中间每一帧的车牌裁剪图一起存下来,留作日志。后面一旦出现问题,回溯分析会非常省事——看到一个失败的框,马上就能判断是检测环节丢了、校正环节歪了,还是识别环节认错了。这个习惯帮我省了很多现场排查的时间。