资讯详情

车牌识别落地实战:从检测矫正到CTC序列识别与多帧投票

📅 2026/10/19 4:29:08 | 华诺云谱 👁 阅读
车牌识别落地实战:从检测矫正到CTC序列识别与多帧投票
简介本资源为阿里车牌识别技术的学习与实战项目包面向计算机视觉初学者、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_size640): 等比缩放并填充到正方形保持长宽比 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), dtypenp.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, dtypenp.float32).reshape(4, 2) # 按左上、右上、右下、左下排序 s pts.sum(axis1) diff np.diff(pts, axis1).ravel() ordered np.zeros((4, 2), dtypenp.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]], dtypenp.float32) M cv2.getPerspectiveTransform(ordered, dst) return cv2.warpPerspective(img, M, (w, h)) def detect_and_crop(img_path, conf_thres0.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(fplate_{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, padding1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(128, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(), nn.Conv2d(256, 256, 3, padding1), nn.ReLU(), nn.MaxPool2d((2, 1)), nn.Conv2d(256, 512, 3, padding1), nn.BatchNorm2d(512), nn.ReLU(), nn.Conv2d(512, 512, 3, padding1), nn.ReLU(), nn.MaxPool2d((2, 1)), ) # 循环层双向 LSTM self.rnn nn.LSTM(512, 256, bidirectionalTrue, batch_firstTrue) 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, dim2) # CTC 损失blank 设为 0 ctc_loss nn.CTCLoss(blank0, zero_infinityTrue) 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), dtypetorch.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 是跟踪 IDvalue 是该 ID 下所有识别结果和对应置信度。当车辆离开画面或跟踪丢失时对 value 里的结果做投票。投票时先按置信度过滤低于阈值的丢弃然后对剩余结果做多数投票票数相同的取置信度之和最高的。如果业务允许还可以把多帧的车牌图拼在一起送一个专门的融合模型但成本高一般投票就够了。验证方法拿一段连续视频跑单帧识别和多帧投票对比整牌准确率。通常多帧投票能把准确率提升几个百分点尤其是夜间和模糊场景。注意投票会引入延迟因为要等车辆离开才能出结果如果业务要求实时出结果可以设一个时间窗口比如 500ms 内的帧做投票超时就用当前结果。我自己的习惯是上线前一定拿真实场景的视频跑一遍不要只看单张图的准确率。视频里连续帧的波动、跟踪 ID 的切换、投票窗口的设置这些细节只有跑过才知道。参数方面IoU 匹配阈值先设 0.3投票置信度阈值先设 0.5然后根据badcase调。不要一次调太多参数每次只动一个看效果变化。这套链路从检测到识别再到多帧投票每个环节都有坑但每个坑都有对应的解法。先把最小链路跑通再针对badcase补数据、调参数、加后处理。不要一上来就追求端到端大模型那只会让你在调试时失去抓手。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑