简介:本资源为阿里车牌识别技术的学习与实战项目包,面向计算机视觉初学者、Android开发者及智能交通应用爱好者,帮助理解车牌识别从图像预处理、特征提取、车牌定位到字符识别的完整流程,并掌握自定义相机与仿二维码扫描拍照的实现思路。包内共97个文件,以java源码、class编译文件、xml布局与配置、jar依赖库及png图片资源为主,另含apk安装包与dex文件,压缩包约4.43MB,结构清晰便于对照学习。项目重点展示了摄像头参数调整、实时预览、自动对焦曝光控制、固定尺寸图像裁剪保存等自定义相机功能,可应用于交通管理、停车场无人值守、车辆追踪与租赁还车等场景。目前已有422人学习下载,适合希望快速上手车牌识别与Android相机开发的读者参考借鉴。
1. 车牌识别落地前,先把这几个问题想清楚
车牌识别这四个字,放在项目里往往不是“识别一张图”这么简单。它是一条从图像采集、车牌定位、字符分割到字符识别的完整链路,任何一个环节掉链子,最终输出就是错的。很多团队第一次做,以为调个开源模型就能跑通,结果发现夜间逆光、车牌倾斜、新能源绿牌、双层黄牌这些场景一上来,准确率直接掉到没法看。这篇文章面向的是准备把车牌识别真正落到业务里的开发者,不管你是做停车场出入管理、道路卡口,还是做车辆档案系统,核心诉求都一样:怎么用可复现的方案把识别率做到能上线,参数怎么调,坑在哪。我会按“先讲清链路和选型,再给可抄的代码和参数,最后说避坑和进阶”的顺序展开,尽量让新手能跟着跑,熟手能看到边界。
2. 车牌识别链路拆解与检测方案选型
2.1 从一张图到车牌号,中间到底经过了几步
车牌识别的标准链路通常拆成四步:车牌检测、车牌矫正、字符分割、字符识别。检测负责在整张图里框出车牌位置,输出一个矩形框或四角点;矫正负责把倾斜、透视变形的车牌拉正,这一步在卡口场景里特别关键,因为相机角度往往不是正对车牌;字符分割负责把车牌里的每个字符切出来,或者直接跳过分割做序列识别;字符识别负责把字符图或序列映射成具体文字和数字。
早期方案喜欢用颜色和边缘特征做检测,比如在 HSV 空间里找蓝色或绿色区域,再用 Sobel 边缘和形态学操作拼出候选框。这种做法在光照稳定、背景干净时够用,但一到夜间、雨天、车牌脏污就崩。现在主流做法是用目标检测模型直接回归车牌框,常见的是 YOLO 系列或 SSD 系列,把车牌当成一个类别来训。检测出来之后,矫正可以用透视变换,基于检测框的四个角点做映射;如果检测模型输出的是水平矩形框,那矫正就只能做旋转,透视变形处理不了,所以检测阶段最好直接回归四角点,或者用分割模型输出车牌掩码再拟合角点。
字符识别这一端,早期是“分割 + CNN 分类”,先把每个字符切出来,再送进一个小的分类网络。这种方案对分割精度依赖极大,字符粘连、铆钉遮挡、边框干扰都会导致切错。现在更稳的是 CTC 或 Attention 的序列识别,把矫正后的车牌图直接送进 CRNN 或 Transformer 类模型,输出字符序列,省掉分割步骤。序列识别对车牌长度变化、字符间距不均更鲁棒,但训练数据要求更高,需要带标注的整牌文本。
选型时先看你的场景:如果相机固定、车牌基本正对、光照可控,传统检测加序列识别就够;如果相机角度多变、车牌类型杂,检测阶段必须上四角点回归,矫正用透视变换,识别用序列模型。不要一上来就堆大模型,先跑通最小链路,再针对badcase补数据。
2.2 检测模型选型:YOLO 还是分割,看你的角点需求
检测模型的选择直接决定后续矫正能做到什么程度。如果你只需要知道车牌大概在哪,不关心倾斜,那 YOLO 输出水平框就够了,训练简单,推理快。但实际业务里,车牌倾斜是常态,水平框矫正后字符还是歪的,识别率上不去。这时候有两种做法:一是让 YOLO 直接回归四角点,把输出从 4 个值改成 8 个值;二是用分割模型输出车牌掩码,再从掩码里拟合最小外接矩形或透视变换的四个角点。
YOLO 回归四角点的好处是端到端,推理一次出框和角点,速度快。但训练时角点标注要准,标注误差会直接传导到矫正。分割方案的好处是对遮挡、模糊更鲁棒,掩码能保留车牌形状信息,但推理速度慢一些,后处理也复杂。我一般会先试 YOLO 四角点,如果badcase里角点回归不准,再换分割。
训练数据方面,车牌检测不需要海量数据,几千张覆盖各种光照、角度、车牌类型的图就能起步。关键是标注质量,角点要贴着车牌边缘,不要框到背景。数据增强重点做透视变换、亮度对比度扰动、运动模糊,这些和实际场景最接近。不要只做翻转和裁剪,那对车牌检测帮助有限。
推理阶段,输入尺寸建议 640 或 736,太小会丢小目标,太大速度掉。置信度阈值先设 0.3 跑一批,看漏检和误检的平衡,再微调。NMS 的 IoU 阈值设 0.5 左右,车牌之间一般不会重叠,但相邻车辆的车牌可能靠得近,阈值太低会误删。
2.3 用 Python 跑通检测加矫正的最小链路
下面这段代码用 ONNX Runtime 加载一个车牌检测模型,输出四角点,然后做透视变换矫正。模型文件假设已经导出为 ONNX,输入尺寸 640x640,输出格式为 [1, N, 9],其中 9 是 [x1,y1,x2,y2,x3,y3,x4,y4,conf]。实际导出格式可能不同,按你的模型调整解析逻辑。
import cv2 import numpy as np import onnxruntime as ort # 加载 ONNX 模型,providers 按可用性排序 session = ort.InferenceSession("plate_det.onnx", providers=["CPUExecutionProvider"]) def preprocess(img, input_size=640): """等比缩放并填充到正方形,保持长宽比""" h, w = img.shape[:2] scale = min(input_size / w, input_size / h) new_w, new_h = int(w * scale), int(h * scale) resized = cv2.resize(img, (new_w, new_h)) canvas = np.zeros((input_size, input_size, 3), dtype=np.uint8) # 填充到左上角,后续还原时按同样偏移 canvas[:new_h, :new_w] = resized blob = canvas.astype(np.float32) / 255.0 blob = blob.transpose(2, 0, 1)[None, ...] return blob, scale, new_w, new_h def perspective_crop(img, corners): """按四角点做透视变换,输出矫正后的车牌图""" pts = np.array(corners, dtype=np.float32).reshape(4, 2) # 按左上、右上、右下、左下排序 s = pts.sum(axis=1) diff = np.diff(pts, axis=1).ravel() ordered = np.zeros((4, 2), dtype=np.float32) ordered[0] = pts[np.argmin(s)] ordered[2] = pts[np.argmax(s)] ordered[1] = pts[np.argmin(diff)] ordered[3] = pts[np.argmax(diff)] # 目标宽高取对边最大值,避免拉伸过度 w = int(max(np.linalg.norm(ordered[0] - ordered[1]), np.linalg.norm(ordered[2] - ordered[3]))) h = int(max(np.linalg.norm(ordered[0] - ordered[3]), np.linalg.norm(ordered[1] - ordered[2]))) dst = np.array([[0, 0], [w - 1, 0], [w - 1, h - 1], [0, h - 1]], dtype=np.float32) M = cv2.getPerspectiveTransform(ordered, dst) return cv2.warpPerspective(img, M, (w, h)) def detect_and_crop(img_path, conf_thres=0.3): img = cv2.imread(img_path) blob, scale, new_w, new_h = preprocess(img) outputs = session.run(None, {session.get_inputs()[0].name: blob})[0] # outputs 形状 [1, N, 9],按置信度过滤 results = [] for det in outputs[0]: conf = det[8] if conf < conf_thres: continue corners = det[:8].reshape(4, 2) # 还原到原图坐标:先减填充偏移,再除以缩放比 corners[:, 0] = (corners[:, 0] - 0) / scale corners[:, 1] = (corners[:, 1] - 0) / scale plate = perspective_crop(img, corners) results.append((conf, plate)) return results if __name__ == "__main__": for conf, plate in detect_and_crop("test.jpg"): cv2.imwrite(f"plate_{conf:.2f}.jpg", plate)这段代码的关键点有三个。第一,预处理做了等比缩放加填充,还原坐标时要注意填充偏移,上面代码假设填充在左上角,如果填充在中心,偏移量要相应调整。第二,四角点排序用 sum 和 diff 的方法,这是常见做法,但对极端旋转可能失效,更稳的是按角度排序。第三,透视变换的目标宽高取对边最大值,避免把车牌拉变形。参数方面,conf_thres 先设 0.3,漏检多就降到 0.2,误检多就升到 0.4。输入尺寸 640 是速度和精度的折中,如果车牌在图中占比很小,可以提到 736 或 800。
矫正后的车牌图建议保存下来,人工过一遍,看看有没有拉歪、裁掉边缘的情况。这些badcase就是后续调模型和调后处理的依据。
3. 字符识别:从分割分类到序列模型
3.1 为什么现在更推荐 CTC 序列识别
字符识别这一端,早期方案是“分割 + 分类”,先把车牌里的每个字符切出来,再送进一个 CNN 分类器。这种做法在车牌干净、字符标准时能跑,但实际场景里字符粘连、铆钉遮挡、边框干扰、新能源车牌多一位,都会让分割出错。一旦切错,后面分类再准也没用。序列识别把整牌当成一个序列来预测,输入是矫正后的车牌图,输出是字符序列,中间不需要显式分割。CTC 是其中最常见的一种,它允许输入和输出长度不一致,通过引入空白符和对齐机制,把不定长的特征序列映射到字符序列。
CTC 的优点是训练相对简单,不需要字符级标注,只要整牌文本就行。推理时用贪心解码或束搜索解码,速度也快。缺点是 CTC 假设字符之间条件独立,对上下文利用不够,遇到相似字符比如“0”和“O”、“1”和“I”容易混。Attention 类序列模型能利用上下文,但训练更慢,推理也更重。实际项目里,如果车牌类型固定、字符集小,CTC 够用;如果混淆严重,可以在 CTC 后面加一个小的语言模型做后处理,或者直接上 Attention。
训练数据方面,序列识别需要大量带整牌文本标注的图。数据来源可以是检测阶段裁出来的车牌图,人工标注文本。标注时注意统一字符集,比如中文、英文、数字分开定义,新能源车牌多一位,字符集要覆盖。数据增强重点做透视变换、亮度扰动、模糊、遮挡模拟,这些和实际badcase对应。
3.2 用 CRNN 加 CTC 训练一个车牌识别模型
下面是一个简化的 CRNN 加 CTC 训练代码,用 PyTorch 实现。输入是矫正后的车牌图,统一缩放到 32x100,输出是字符序列。字符集假设为数字加英文加常见中文,实际按你的数据定义。
import torch import torch.nn as nn import torch.nn.functional as F class CRNN(nn.Module): def __init__(self, num_classes): super().__init__() # 卷积层提取特征,输入 1x32x100 self.cnn = nn.Sequential( nn.Conv2d(1, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(128, 256, 3, padding=1), nn.BatchNorm2d(256), nn.ReLU(), nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(), nn.MaxPool2d((2, 1)), nn.Conv2d(256, 512, 3, padding=1), nn.BatchNorm2d(512), nn.ReLU(), nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(), nn.MaxPool2d((2, 1)), ) # 循环层,双向 LSTM self.rnn = nn.LSTM(512, 256, bidirectional=True, batch_first=True) self.fc = nn.Linear(512, num_classes) def forward(self, x): # x: [B, 1, 32, 100] feat = self.cnn(x) # [B, 512, 2, 25] 左右 b, c, h, w = feat.shape feat = feat.permute(0, 3, 1, 2).reshape(b, w, c * h) out, _ = self.rnn(feat) logits = self.fc(out) # [B, T, num_classes] return F.log_softmax(logits, dim=2) # CTC 损失,blank 设为 0 ctc_loss = nn.CTCLoss(blank=0, zero_infinity=True) def train_step(model, optimizer, images, targets, target_lengths): model.train() logits = model(images) # [B, T, C] log_probs = logits.permute(1, 0, 2) # CTC 要求 [T, B, C] input_lengths = torch.full((images.size(0),), log_probs.size(0), dtype=torch.long) loss = ctc_loss(log_probs, targets, input_lengths, target_lengths) optimizer.zero_grad() loss.backward() optimizer.step() return loss.item()这段代码里,CNN 把输入 32x100 逐步下采样到高度 2、宽度 25,然后 reshape 成序列送进双向 LSTM。CTC 损失要求输入是 log_softmax 后的结果,形状 [T, B, C],blank 标签设为 0。训练时 target 是拼接后的字符索引序列,target_lengths 是每个样本的字符数。注意 CTC 的输入长度 T 要大于等于 target 长度,否则会报错,所以输入宽度不能太小,100 是常见值,如果车牌字符多,可以加到 120 或 160。
解码时用贪心策略,取每个时间步最大概率的字符,去掉重复和 blank,就得到最终文本。束搜索能提升一点准确率,但速度慢,实际项目里贪心够用。评估时按整牌准确率算,不要按字符准确率,因为业务只关心整牌对不对。
3.3 识别阶段的参数怎么调
识别模型的输入尺寸、字符集、解码方式都会影响最终准确率。输入尺寸方面,高度 32 是常见值,宽度按车牌长宽比来,蓝牌 440x140,比例约 3.14,所以宽度 100 左右合适;新能源绿牌 480x140,比例约 3.43,宽度可以到 110。如果统一缩放到固定尺寸,注意保持长宽比,不要硬拉伸,否则字符变形影响识别。
字符集定义要覆盖你的业务场景。蓝牌是 7 位,新能源是 8 位,黄牌有单层和双层,字符集里中文、英文、数字都要有。如果业务里只出现某几个省份的车牌,可以缩小字符集,减少混淆。但不要为了省事只留数字和英文,中文省份简称是必须的。
解码时,贪心解码速度快,束搜索准确率略高。如果业务对准确率要求极高,可以用束搜索加语言模型,但推理延迟会增加。实际项目里,先跑贪心,看badcase里有没有因为解码导致的错误,再决定要不要换。
后处理也很重要。识别出来的文本可以做规则校验,比如长度是否符合车牌类型,字符是否在合法字符集内,省份简称是否合理。校验不过的可以打低置信度,转人工复核。这一步能过滤掉一部分明显错误,提升上线后的体验。
4. 避坑与常见问题排查
4.1 检测框抖动导致矫正后字符变形
现象:同一辆车连续几帧,检测框位置轻微抖动,矫正后的车牌图有时正有时歪,识别结果跟着跳。原因:检测模型对边缘回归不稳定,或者 NMS 阈值设置不当,导致框在相邻位置之间跳。解决:在检测后加跟踪,用卡尔曼滤波或简单的 IoU 匹配,把连续帧的框平滑;或者提高检测模型精度,增加角点回归的监督信号。如果业务允许,取连续几帧里置信度最高的那一帧做识别,不要每帧都识别。
4.2 夜间逆光车牌过曝或欠曝
现象:夜间卡口图里,车牌要么被车灯照得过曝,要么在阴影里欠曝,检测模型漏检,识别模型也认不出。原因:训练数据里夜间样本不足,模型没见过这种极端光照。解决:数据增强里加亮度对比度的强扰动,模拟过曝和欠曝;收集夜间badcase,人工标注后加入训练集;检测阶段可以先用图像增强,比如 CLAHE 做局部对比度均衡,再送模型。注意 CLAHE 参数不要调太猛,否则噪声也会被放大。
4.3 新能源绿牌和双层黄牌识别率低
现象:蓝牌识别率不错,但新能源绿牌和双层黄牌准确率明显低。原因:训练数据里这两类车牌占比少,模型对它们的特征学习不足;绿牌底色和某些背景颜色接近,检测容易混;双层黄牌上下两行字符,序列模型如果按单行处理会漏掉一行。解决:针对这两类车牌补充数据,至少各几千张;检测阶段可以把车牌类型作为额外类别,让模型区分;识别阶段对双层黄牌,可以先检测上下两行,分别识别再拼接,或者用支持多行的序列模型。
4.4 字符粘连和铆钉遮挡导致识别错
现象:车牌脏污、铆钉遮挡、字符磨损时,识别结果出现错字或漏字。原因:序列模型对局部遮挡鲁棒性有限,CTC 在遮挡处容易输出 blank 或错字。解决:数据增强里加随机遮挡,模拟铆钉和脏污;识别模型可以加注意力机制,让模型关注未遮挡区域;后处理里做规则校验,比如长度不对的打低置信度。如果业务允许,可以训练一个专门的修复模型,对遮挡车牌做补全,但成本较高。
4.5 推理速度跟不上业务并发
现象:单张图推理没问题,但并发上来后延迟飙升,吞吐不够。原因:模型太大,或者预处理后处理耗时多,或者没有做批处理。解决:检测和识别模型都可以导出 ONNX 或 TensorRT,用 GPU 推理;预处理用 OpenCV 的 GPU 版本或自己写 CUDA kernel;推理时做批处理,把多张图拼成一个 batch 送模型。如果业务对延迟敏感,可以先用小模型快速筛,再用大模型精修,但链路会复杂。实际项目里,先测单张延迟,再算并发需求,不要一上来就优化。
5. 进阶:用跟踪加多帧投票把准确率再提一档
单帧识别总有波动,卡口场景里同一辆车会连续出现在多帧里,把这些帧的识别结果聚合起来,能显著提升最终准确率。常见做法是检测加跟踪,给每辆车分配一个 ID,把同一个 ID 下的多帧车牌图都送识别,然后对识别结果做投票。投票可以按置信度加权,也可以按帧质量加权,比如清晰度高的帧权重更大。
跟踪算法可以用简单的 IoU 匹配,也可以用卡尔曼滤波加匈牙利算法。IoU 匹配适合相机固定、车辆运动连续的场景,实现简单。卡尔曼滤波能预测下一帧位置,对遮挡和漏检更鲁棒,但调参麻烦。实际项目里,先上 IoU 匹配,看跟踪 ID 切换的频率,如果切换多,再换卡尔曼。
多帧投票的具体做法:维护一个字典,key 是跟踪 ID,value 是该 ID 下所有识别结果和对应置信度。当车辆离开画面或跟踪丢失时,对 value 里的结果做投票。投票时先按置信度过滤,低于阈值的丢弃;然后对剩余结果做多数投票,票数相同的取置信度之和最高的。如果业务允许,还可以把多帧的车牌图拼在一起,送一个专门的融合模型,但成本高,一般投票就够了。
验证方法:拿一段连续视频,跑单帧识别和多帧投票,对比整牌准确率。通常多帧投票能把准确率提升几个百分点,尤其是夜间和模糊场景。注意投票会引入延迟,因为要等车辆离开才能出结果,如果业务要求实时出结果,可以设一个时间窗口,比如 500ms 内的帧做投票,超时就用当前结果。
我自己的习惯是,上线前一定拿真实场景的视频跑一遍,不要只看单张图的准确率。视频里连续帧的波动、跟踪 ID 的切换、投票窗口的设置,这些细节只有跑过才知道。参数方面,IoU 匹配阈值先设 0.3,投票置信度阈值先设 0.5,然后根据badcase调。不要一次调太多参数,每次只动一个,看效果变化。
这套链路从检测到识别再到多帧投票,每个环节都有坑,但每个坑都有对应的解法。先把最小链路跑通,再针对badcase补数据、调参数、加后处理。不要一上来就追求端到端大模型,那只会让你在调试时失去抓手。希望帮到你。
本文还有配套的精品资源,点击获取