☰
车牌识别系统实战:从YOLO检测到OCR识别全流程解析
2026/10/22 10:12:49 网站建设 项目流程

手写一套车牌识别系统的设计,这个题目前段时间刚帮一位同行做完技术选型评审,他自己在校园里训练了一版模型,识别率卡在93%左右上不去,让我帮忙看问题出在哪。当时一句话点醒他:不是模型不够深,是你的训练集里“蓝牌”太多了。这个细节后面我再展开说。

车牌识别这个方向,在计算机视觉领域里属于一个特别典型的“跨界”项目:说它是目标检测,也对;说它是OCR文字识别,也对;真正落到工程上,它又是一整套从图像采集到结果输出的Pipeline。很多初学者以为把YOLO的训练流程跑通就完事了,实际上从模型训练到真正能用的系统,中间还隔着好几道坎。

这篇文章我就用自己实际做过的完整项目来讲讲,从任务拆解、数据集准备、模型选型、训练调优,到最后的服务化部署,把每一步的决策逻辑和踩坑记录都摊开说。适合正在做毕业设计、课程项目,或者想在简历上放一个“完整落地案例”的开发者参考。我会尽量把代码和细节都给出来,但不建议直接照抄,一定要结合自己的实际场景去调整。

1. 车牌识别系统的整体架构拆解:先定位问题,再动手写代码

很多人拿到这种题目,第一反应就是“找个目标检测模型,把车牌框出来,再找个OCR模型把字读出来”。思路没错,但如果真的这么直接开干,后面会遇到一堆很现实的问题:比如蓝牌和黄牌的处理逻辑不一样,新能源绿牌是8位字符而传统蓝牌是7位,还有汉字识别错误率奇高(省份简称里的“赣”“鄂”“皖”很容易混淆),单行牌和双行牌怎么统一处理,这些都是在拆解需求时就要想清楚的。

我做这个项目时,第一步做的事不是下载数据集,而是把整个系统切成四个环节,每个环节单独验收:

  • 车牌区域检测:从整张图中定位车牌所在的位置,输出四边形的框坐标。
  • 车牌角度校正:处理倾斜、透视形变的问题,把车牌统一矫正成水平方向。
  • 字符分割或者序列识别:把车牌区域内的字符逐一切出来,或者直接用序列模型端到端识别。
  • 字符分类识别:最终输出省份汉字、字母、数字的组合结果。

实际项目中,后面两步通常会被合并成一个OCR模块。现在主流的做法是直接用LSTM+CTC或者Transformer-based的OCR模型去做端到端识别,不走字符分割的老路,字符分割的阈值不稳定,遇到铆钉、边框干扰时很容易崩。但是如果你用的是传统方法或者是学习目的,字符分割仍然是一个值得掌握的方案,毕竟理解分割逻辑有助于你调试端到端模型的问题。

我当时的设计是一套两阶段的方案:检测用YOLOv5s,识别用基于ResNet18的轻量CNN,在推理阶段把检测和识别串成一个Pipeline。选这个组合的原因很直接:YOLOv5s在车牌这种小目标检测任务上精度和速度平衡得不错,ResNet18纯CNN做序列识别,结构简单、调试容易,在嵌入式设备上也能跑起来。

说到设备约束,这也是需求拆解时容易被忽略的一点。如果你的系统最终要跑在服务器的GPU上,那模型随便选;但如果是跑在Jetson Nano、树莓派这类边缘设备上,模型的参数量和推理时延就必须从第一天就纳入考量。我当时的目标是能在普通CPU上单帧推理时间低于200ms,GPU上低于50ms,这直接淘汰掉了一批重型模型。

2. 数据集准备工作的四个要点:来源、清洗、增强、校验

说实话,车牌识别这个方向最不缺数据,但又最缺高质量数据。网上开源的车牌数据集有不少,标准的开源数据集(比如CCPD)质量很高,大概有20万张以上来自真实场景的图片,覆盖了不同光照、角度、天气。不过在实际使用时一定要注意:公开数据集和你的实际应用场景大概率存在分布偏差。

我当时踩的第一个坑就是“蓝牌比例过高”。某公开数据集里蓝牌(传统燃油车)占比超过95%,而我们的应用场景里混着不少黄牌(大货车)、绿牌(新能源)和白牌(特种车辆)。如果直接拿公开数据集训练,模型对黄牌和绿牌的召回率会非常难看,边缘场景能掉到70%以下。解决方式就是去补数据,黄牌找停车场和物流园门口拍,绿牌跑了一圈商场充电桩,再加上从网上搜集的合规开源图片,最后把各类车型配比调到蓝牌70%、黄牌15%、绿牌10%、其他5%,模型的泛化能力就上来了。

数据清洗是个体力活,但绝对不能省略。公开数据集里总有那么一些图片,标注框画歪的、车牌挡了一半的、压根不是车牌的。我把这类问题分成三类处理:清晰度太低的直接删除,标注框偏移超过15%的重新标注,场景完全不相关的(比如把广告牌上的假车牌当目标的)直接剔除。

清洗完成之后,数据增强这个环节我建议不要用那种一口气全开的做法,图片旋转、平移、缩放、色彩抖动、曝光度调整全部拉满,这样很容易把模型训飘。我试过多次,车牌识别项目中最好的增强策略是“轻度几何变换+中度色彩扰动”:水平平移不超过10%、旋转不超过10度、缩放不超过15%,色彩上做HSV随机抖动,亮度因子保持在0.8到1.2之间。别做翻转增强,车牌字符是方向敏感的,水平翻转会把“鄂A”变成“A鄂”,这是一个新手很容易踩的坑。

数据校验这一步是我后来才补上的,但收益很大:训练集里字符类别是22个省直辖市简称+24个字母+10个数字,实际训练时发现“数字0和字母O”在模型预测时经常混,原因很简单,中文车牌里字母O根本不用,数字0却有,但公开数据集里混入了少量含字母O的标注。我在后处理里加了字符映射表,把“O”强制映射成“0”,“I”映射成“1”,识别准确率直接涨了0.8个百分点。

还有一个经常被忽略的环节:标注格式的转换。很多开源数据集给的是xml或json格式,装进YOLO需要转成txt格式,每行是“类别id 中心点归一化坐标 宽高归一化值”。我处理过的某个数据集,坐标是从左上角开始算的,转成YOLO格式时要先除以图片宽高,经常有人忘记归一化,导致训练时loss直接Nan,排查半天才发现是坐标没转对。

这里给出我最终使用的训练集/验证集/测试集组合比例供参考:

数据来源图片数量增强系数最终使用数用途
公开数据集A48000张1.257600张训练+验证
自采停车场图片2600张3.59100张训练(补充黄/绿牌)
夜间/雨天场景补拍900张4.03600张训练(提升鲁棒性)
公开测试集8000张1.08000张最终测试

这个组合跑出来的测试mAP@0.5到了0.96,mAP@0.5:0.95到了0.82左右,比只用公开数据集高了差不多6个点。

3. 车牌区域检测模型:YOLO系选型、训练细节与调参心得

检测模块是整套系统里最吃数据也最吃算力的部分。当前YOLO系列已经出到v8、v9甚至更高版本,但对于这种固定场景下的中小目标检测,YOLOv5s依然是一个非常稳妥的起点:技术文档多、部署方案全、遇到的坑都能搜到解决方案。我在项目中一开始用的就是YOLOv5s,后来换到v8s做过对比实验,精度提升在0.5个点左右,但推理速度快了不少,这让它在如Jetson这样的边缘设备上更有优势。

如果只看mAP,YOLOv8s确实更好,但选择YOLOv5s还有一个原因:它的输出特征图和Anchor机制我太熟悉了,出了问题我可以快速定位。车牌定位任务中,车牌这种小目标的Anchor匹配策略很关键,YOLOv5的默认Anchor是基于COCO数据集聚类出来的,对车牌这种长宽比固定的目标并不完全适配。做法是用K-Means在你的车牌数据集上重新聚类Anchor,聚出来通常会有两组典型比例:蓝牌接近3.14:1的长条形状,绿牌接近2.5:1的形状。替换掉默认Anchor之后,小目标的召回率会有一个肉眼可见的提升,这个是我实测过的。

训练参数我用了一套比较保守但实际的方案:

# 关键训练参数配置 # YOLOv5s + 输入尺寸640x640 + BatchSize 16 # 优化器:SGD, lr0=0.01, momentum=0.937, weight_decay=0.0005 # 关于数据增强超参数可以参考具体YOLO版本的默认配置进行调整 # epochs=150, 前3个epoch启用warmup # 学习率调度:cosine annealing

这里有个非常反直觉的点:很多人以为训练轮数越多越好,我的实验结果是100轮到150轮在mAP上几乎没有增益,150轮之后反而因为过度拟合某些训练集噪声,验证集mAP略有下滑。所以到了120轮左右,如果loss曲线已经平稳了,就不要再烧算力了,直接提前停止,用保留的最佳权重做测试。

关于超参数,有一点我想强调:不要盲从别人的最佳参数。不同数据集的分布不一样,哪怕都是车牌数据,你的训练集里夜间图片比例高,那么曝光度增强的幅度就要调小,否则模型会学到错误的亮度偏移。最忌讳的就是直接复制网上教程的一整段超参数,然后祈祷训练能出好结果。模型训练不是玄学,任何一项改动都应该能说出原因。

在训练过程中我养成了一个习惯,每个epoch结束的时候手动输出一批验证集预测结果的可视化图,看看漏检和误检到底长什么样。很多跑深度学习训练的人只看loss曲线,这是远远不够的。loss下降不代表模型学到了你想要的特征,有时它会学到一种偷懒的解法。我记得有一次模型mAP很高,但可视化图一看,检测框全压在图片里所有蓝色物体的边缘上,包括蓝色的路牌。原因就是训练集里蓝色元素太多,数据分布有偏。可视化验证能让你一眼发现这种错误,比看任何指标都直观。

4. 字符识别模块:从整图OCR到CNN字符分类器的落地调优

车牌字符识别是整个系统里准确率提升最困难的部分。我最早用的是PaddleOCR里的一些通用识别模型,识别率不错,但有两个问题:模型太大,部署到边缘设备上推理延迟高;通用OCR对车牌这种固定字体、严格排版的目标反而不如专门设计的模型来得稳。

后来我换了一个方案:先用检测模型把车牌区域crop出来,做透视矫正,然后用字符分割把7个/8个字符切出来,最后用CNN分类器逐字符识别。听起来比端到端的序列识别多了几个步骤,但每一块都是可解释、可调试的,这在实际工程里非常重要。OCR引入CTC这种序列建模后,有一个很头疼的问题,就是车牌字符边界不清晰时,模型的attention会飘,输出结果莫名其妙,你还没有一个好的办法去排查。

字符分割这里有一个细节值得单独拿来说:车牌图像二值化的阈值选择。传统方法固定阈值,Otsu自适应虽然好一些,但对光照不均的情况还是很敏感。我最终采用了“自适应局部阈值+形态学处理”的组合路线:先转灰度,用cv2.adaptiveThreshold做局部二值化,然后用开运算去掉细小噪点和铆钉干扰,最后在垂直方向做投影,根据字符之间的波谷位置切分字符。这个方案在蓝牌上几乎不会切错,但在双行黄牌上要另做一套逻辑,因为双行车牌的第二行没有汉字只有数字字母,而且字符更粗,算法参数得微调。

分割完成之后就是字符分类器了。我用了一个结构很简单的CNN,就是ResNet18去掉最后的全连接层,换成一个7分类输出。这个模型非常好训,收敛快、参数量小,单字符识别准确率能做到99.2%以上。你可能会问,为什么不用更深的ResNet50?完全没必要,车牌字符类别一共不到70个(各省简称+字母+数字),数据集又很充足,ResNet18已经严重够用了,用更大的模型反而容易过拟合。

分类器的细节优化主要在训练数据层面。单字符数据集是从开源数据集的标注框里crop出来的,不同来源的图片清晰度差距很大。我在剪裁数据集时做了一次“困难样本挖掘”:把模型预测错误和置信度较低的字符全部捞出来,人工确认之后加入训练集重训一轮,效果非常显著。怀疑样本挖掘本质上就是在教模型去关注它容易忽略的那种模糊边界、极端光照下的字符特征。

如果我重新做一次这个项目,我会认真考虑直接用一个轻量级的端到端OCR方案来替换“分割+单字符识别”的流水线。目前很多端到端方法在车牌场景的准确率已经超过了分割法,而且省去了字符分割的调参痛苦。但注意,如果部署目标是边缘设备,端到端模型的加载和推理复杂度还是会高一些,这道选择题没有绝对正确的答案,还是要结合项目资源和实际需求来权衡。

5. 工程落地的关键步骤:预处理、推理Pipeline和服务化部署

模型训练好了只是第一步,甚至可以说只是很小的一部分。我见过太多人训练出了不错的模型,却在部署阶段翻车,因为图片从摄像头里进来的时候,和训练集里干净整齐的图片完全是两回事。

首先是预处理环节。前端摄像头拍摄的画面通常不是直接输入模型的,需要先做两步:一是车牌区域可能很小,直接缩放进模型会丢失细节,我处理的办法是先做ROI放大,把车牌附近区域截出来再送入检测模型;二是过曝和欠曝问题,简单的一个直方图均衡化就能有效缓解。预处理里面最容易踩的坑是图像通道顺序搞错,OpenCV读进来是BGR,PyTorch模型预训练时用的是RGB,如果不转换,模型精度会莫名其妙掉一截,而且这个错了通常不会报错,只会性能下降,很难排查。

推理Pipeline我用Python写了一个标准的过程,伪代码如下:

# 以YOLOv8检测+CNN识别为例的简化流程 def process_frame(frame): # 1. 预处理 frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 2. 目标检测,获取车牌框 results = detect_model(frame_rgb) # 返回boxes, scores, class_ids plates = [] for box in results: x1, y1, x2, y2, score = box if score < conf_threshold: continue # 3. 裁切车牌区域 plate_img = frame_rgb[y1:y2, x1:x2] # 4. 透视矫正 plate_img = perspective_correct(plate_img) # 5. 字符识别 plate_text = recognize_plate_chars(plate_img) plates.append({ "bbox": [x1, y1, x2, y2], "confidence": round(float(score), 4), "plate_number": plate_text, "plate_color": "blue" }) return plates

这里边有一个值得关注的性能问题:如果视频流每一帧都跑完整Pipeline,很快会感受到CPU算力吃紧。最简单的优化手段是做帧间隔推理,对30fps的视频每隔两三帧处理一次,同时用跟踪算法(比如简单的IoU匹配)在中间帧之间保持检测框的连续性。我实测这样处理能把CPU占用率降低60%,而且用户体验几乎不受影响。

服务化部署方面,我当时用FastAPI起了一个HTTP服务,接收图片base64编码的JSON请求,返回识别结果。上传图片经过Nginx转发到后端,静态资源和API分离,响应时间国内服务器实测大概100ms到200ms不等,看图片分辨率。如果要在生产环境更进一步提升并发能力,可以对模型做ONNX导出并用ONNXRuntime推理,速度比PyTorch的eager模式快不少。我这里给出一个ONNX导出的典型代码:

# 以YOLOv8s为例的onnx导出做法 from ultralytics import YOLO model = YOLO("best.pt") model.export(format="onnx", dynamic=False, imgsz=640, simplify=True)

导出后建议再用onnxruntime测一遍输出是否和PyTorch一致,我当时在两阶段Pipeline里就因为检测模型输出的xywh坐标归一化方式在ONNX中发生了变化,导致下游裁切全部偏移,排查了一个下午。

6. 误检与异常识别:鲁棒性提升的实战记录

车牌识别系统上线后,最大的挑战往往不是识别不出车牌,而是把不是车牌的东西当成车牌,也就是误检。这个在停车场收费、出入口门禁等场景中尤其致命,一道栏杆如果因为误检而频繁误触发,那运营方一定接受不了。

我的项目里就出现过这么一次事故:系统上线第一天,蓝色广告牌、蓝色垃圾桶、甚至安保人员的深蓝色制服都被当成了车牌输出。我心里清楚,这是训练集里蓝牌比例太高导致的模型偏见,不是算法本身的Bug。针对这个问题我做了两套措施:一是数据层面,加入大量负样本(场景里没有车牌的图片、类似蓝色矩形但不是车牌的物体),二是逻辑层面,在后处理里加入车牌的“物理约束”校验,比如几何长宽比要接近各类型车牌的标准范围、检测置信度阈值提高、识别结果的字符组合必须符合车牌规则(省份简称必须是合法汉字、位置固定等)。

这个校验规则写成代码并不复杂,但非常实用:

def validate_plate_text(text, plate_color="blue"): # 简易车牌类型格式校验 if plate_color == "blue" and len(text) != 7: return False if plate_color == "green" and len(text) != 8: return False if text[0] not in PROVINCE_ABBREVIATIONS: return False if not text[1].isalpha(): return False return True

做完这两步之后,误检率从每千帧14次降到了每千帧不到2次,已经达到了实际可用的水准。不过在实际项目中也要想清楚误检和漏检的取舍:如果想提高召回率,可以适当调低置信度阈值,但要接受误检率上升的代价;如果业务场景对误检零容忍(比如自动扣费),那阈值必须拉高。没有绝对正确的一套参数,只有适配具体业务场景的参数。

在边缘设备(我用的是Jetson Nano作为测试平台)上实测,整个Pipeline的处理速度大约是每帧150ms到200ms,基本满足停车场杆起落的实时性要求。如果是树莓派这类的设备,建议把输入尺寸从640降到416,识别速度会快很多,但mAP可能会有三个点左右的下降,需要你来权衡。

还有一件事值得提醒:不要在部署环境里继续输出训练阶段的调试日志。当时我在代码里忘了关掉verbose输出,结果每次识别都会在控制台打印一大串日志,堆满日志盘不说,还在高并发情况下拖慢了响应速度。小细节很影响系统稳定性。

7. 项目复盘:哪些技术决策做对了,哪些下次会改

这个项目整体做下来,从数据准备到部署完成,前后大概花了三周。周末和我一个在某公司做CV算法岗的朋友聊了一次,他说这个项目的技术栈比他们组里一些正式任务的技术栈还完整。我不敢完全认同,但这个项目确实帮我建立了一套“以实际落地为导向”的计算机视觉思维,这是课堂和纯理论书籍很难给我的。

如果让我总结哪些技术决策做对了,我会选这几条:

  • 把项目拆成检测和识别两阶段,而不是端到端一锅端。每一阶段都可以单独调优、单独测试,出了问题不至于整个系统瘫痪。
  • 训练过程中坚持做可视化输出,而不是只盯loss曲线。这帮助我发现了两次“假收敛”问题。
  • 数据配比上刻意增加黄牌、绿牌和夜间样本,让模型在长尾场景下没有明显短板。
  • 用物理校验规则做了逻辑层的兜底,有效拦截了大量离谱的模型输出。

下次如果再做一个类似的项目,我会改变的做法至少有三件。第一,我不会再用固定字长分割识别的方案来服务所有场景,双行黄牌的字符分割确实麻烦,大概率会直接上端到端的序列识别模型,或者至少做一个分割法和序列法的融合决策。第二,我会更早引入ONNX Runtime部署,而不是在PyTorch的eager模式下撑太久。第三,我会把数据版本管理和实验记录做得更加规范,虽然现在的数据文件和训练脚本都在同一个目录里存着,但当时每改一次数据增强或者训练参数都重新生成一套目录,后期要回溯某个实验结果时有种“陷入一团乱麻”的感觉。

说回开头那位同行的问题:识别率卡在93%上不去,我把他的数据集拉了一个分布直方图,蓝牌比例高达97%,其余场景全部是短板。我让他用各种方法把训练集调成70%左右蓝牌、其余是黄牌绿牌白牌,再重新训练三天,识别率直接从93%跳到了96.5%。很多时候模型的上限不是由网络结构决定的,而是由数据分布决定的,这个结论在我自己的项目里再次得到了验证。

车牌识别系统这类“目标检测+OCR”的组合,算是计算机视觉入门到进阶之间一个非常适合的承接项目。它不像人脸识别那样有大量的现成商业API可以白嫖,也没有目标检测基础任务那么纯粹,但恰恰是这种混合属性,逼着你去理解整个视觉系统的全链路。如果你正在考虑做一个能完整展示你工程能力的项目,我建议你可以在这个方向投入时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询