☰
基于yolov5与CRNN的中文车牌识别系统:检测矫正到部署
2026/10/26 10:42:12 网站建设 项目流程

简介:本资源是一套完整的中文车牌识别高分毕设项目,面向人工智能、自动化、电子信息等专业学生及初学者,解决车牌检测与OCR识别两大核心任务。系统基于YOLOv5实现车牌定位,结合CRNN网络完成端到端中文字符识别,支持单双层车牌、复杂光照与倾斜场景,附带GUI可视化界面及测试视频演示。压缩包共92个文件,含53个Python源码(涵盖训练、推理、GUI、模型导出等模块)、15个YAML配置文件(含YOLOv5多版本模型参数与训练超参)、9张效果展示图及7张测试样例图,另有2个预训练权重.pth/.pt文件和完整README文档,整体大小仅7.65MB,结构清晰、模块解耦度高,便于学习理解与二次开发。项目已通过导师评审,答辩得分95分,代码全部实测可运行,配套详细技术文档与使用说明,覆盖数据准备、模型训练、推理部署全流程,适合课程设计、毕业设计及AI视觉入门实践。 看到这个标题,我猜大多数人第一反应是:又一套yolov5+CRNN中文车牌识别源码,还带“高分项目”四个字。这套基于yolov5和CRNN的中文车牌识别系统,我完整跑通之后最直接的感受是——它确实是典型的教学级工程包,代码结构清楚,文档也全,适合拿来学习、做毕业设计、或者在某个小型安防/道闸场景里快速起步。但“能跑通”和“跑得稳”之间隔着不少坑,很多细节标题里看不出来,真正动手才会暴露。这篇就把整条链路拆开讲清楚:yolov5负责什么、CRNN负责什么、中间那块最容易翻车的环节是什么、以及怎么评估和改进。

1. 先拆清楚:yolov5和CRNN各自在这套系统里干什么

1.1 为什么车牌识别要拆成“检测+识别”两段

车牌识别在工程上从来不是一个模型干完的事,而是两个性质完全不同的任务。第一件事是在一张大图里找到车牌在哪里,这是目标检测问题;第二件事是把车牌区域里的字符读出来,这是一个序列识别问题。把两件事混在一起,会让模型既要做空间定位又要做字符分类,训练数据、网络结构、评价指标都很难兼顾。这也是为什么绝大多数工业级车牌识别系统都是两阶段架构。

yolov5在这个项目里负责第一阶段,输出的是车牌区域的边界框,通常是一个矩形框加上置信度。CRNN负责第二阶段,输入是裁剪好的车牌图片,输出是一串字符,比如“粤A12345”。两个模型各自独立训练、独立调优,整套系统的灵活性高很多。如果你采集了一批新的夜间数据,发现检测漏了,只需要优化yolov5;如果发现字符老是认错,只需要优化CRNN,不需要动另一头。

1.2 为什么选yolov5,为什么选CRNN

yolov5在车牌检测里的地位,有点像“万能钥匙”。它最大的优势不是精度绝对领先,而是生态太成熟了。从v6.0到v7.0,网上能搜到的教程、踩坑记录、预训练权重多到看不过来;训练、验证、导出onnx、转TensorRT的链路官方都给你铺好了。对于这种需要快速落地的项目,你不需要折腾太多工程问题,把精力留给数据更现实。

CRNN则是因为它天生适合车牌这种“有限字符集、变长序列”的场景。它不像目标检测那样要把每个字符框出来,只需要把整张车牌图片映射成一个字符串,训练的时候只需要给整行文本的标签,不需要字符级位置标注。中文字符、英文字母、数字混排的序列识别,它就是正经路子。

当然也有其他选择。有人用LPRNet做轻量端到端识别,也有人直接套PaddleOCR的PP-OCR系列,但各自有取舍。

方案优点缺点适合场景
yolov5 + CRNN两阶段分工清晰,检测和识别可独立优化中间多了裁剪和矫正步骤,流程稍长教学项目、中小型工程落地
LPRNet模型轻量,端到端,部署简单精度和鲁棒性一般,中文字符细节弱资源受限的嵌入式设备
PaddleOCR PP-OCR中文识别能力强,工具链完整更偏通用文本行,车牌这种强结构场景要调通用OCR场景,车牌需要二次适配
单模型端到端车牌识别一个网络直接输出字符串架构复杂,数据要求高,训练难度大大厂自研场景,不太适合个人项目

1.3 一条完整车牌识别的数据流

把这套系统的数据流完整画出来,大概是这样:

摄像头或图片 → yolov5检测车牌 → 得到bbox和置信度 → 按置信度过滤 → 裁剪车牌区域 → 角度矫正/方向归一化 → 归一化尺寸 → 送入CRNN → 输出车牌字符串和置信度 → 后处理纠错 → 交给业务系统。

中间“角度矫正”那一步,很多人忽略,但这往往是识别率差的最大原因。yolov5给出的框不一定水平,尤其是路侧停车、斜向摄像头、车身摆角大的时候,裁剪出来的图片带着倾斜。CRNN对倾斜文字非常敏感,不矫正直接喂进去,识别率能掉十几个百分点。这一点后面专门展开说。

2. yolov5车牌检测:从基础配置到中文车牌特化

2.1 用官方预训练权重还是从零训练

很多初学者拿到车牌标注数据后第一件事就是yolov5s.pt直接开训,但这里有个基本选择要做:用COCO预训练权重做迁移学习,还是从零开始训练。我建议直接用预训练权重,原因很朴素——车牌检测本质上是一个“中大型物体检测”问题,COCO预训练模型已经学会了边缘、纹理、物体边界等通用视觉特征,你只需要在它的基础上微调,让它把注意力聚焦到“车牌”这个类别上。从零训练需要的数据量和训练轮数都要大得多,个人项目根本划不来。

实际操作时,yolov5的--weights参数填预训练权重路径,训练代码默认会冻结前几层还是全部微调,这取决于版本和配置。通常默认配置下模型会把所有层都参与训练,对于车牌这种与自然图像差异比较大的目标,全量微调反而效果更好,不必担心过拟合,只要数据别太少。

2.2 数据标注与车牌长宽比陷阱

车牌检测标注看着简单,但有个隐藏陷阱:车牌的长宽比和COCO里常见的目标很不一样。普通蓝牌长宽比大约3:1,黄牌和新能源绿牌更长一些。如果你标注的时候框得不够紧,模型会学到很多多余的背景干扰,后续裁剪出来的区域就会带上大面积车漆或车灯,直接影响识别。

标注建议用labelme或CVAT,最终转成YOLO格式的txt文件,每行一个目标:类别序号、中心点x、中心点y、宽度w、高度h,全部归一化到0~1。注意框要尽量贴合车牌边缘,四边不要留太多余量,但也不要切掉字符。

yolov5默认训练输入是640×640,但对于车牌这种细长目标,直接把整张图缩放到640×640会把车牌压得非常小。我的做法是先把默认输入尺寸调大,比如用--img 960,或者用640×384这种更接近横屏的长方形输入,减少非等比压缩带来的形变。如果你的数据里车牌本来就比较大,640也能用;如果视频流里车离得远,一定要上高分辨率。

2.3 锚框重聚类、超参数和训练自己的数据集

yolov5默认锚框是COCO数据集上聚出来的,对车牌这种长条形目标并不友好。训练时yolov5会自动调用autoanchor逻辑,根据你的标注重新聚类锚框。但有时候自动计算出的结果不一定最优,你可以手动跑一下python utils/autoanchor.py --data plate.yaml看看聚类的Anchor和默认差异有多大。通常在车牌数据集里,会聚出几个长宽比接近3:1甚至4:1的锚框,这说明默认锚框确实不合适。

训练命令大致是这样:

python train.py --data plate.yaml --cfg models/yolov5s.yaml --weights yolov5s.pt --img 640 --batch-size 16 --epochs 100

plate.yaml里主要配置训练集和验证集路径、类别数nc: 1、类别名称names: ['plate']。

超参数文件我一般会在hyp.scratch-low.yaml基础上调两处:一是mosaic增强概率,如果你用960输入尺寸来补偿小目标,mosaic可以保留为默认1.0,但如果显存不够只能用640,建议把mosaic概率降到0.8左右,避免车牌被缩到过小;二是HSV颜色扰动,车牌颜色在不同光照下变化大,适当增大hsv_h、hsv_s、hsv_v的值,让模型对颜色变化更鲁棒。

有个容易被忽略的点和“yolov5训练单通道”这个热词相关:检测阶段一般用三通道彩色图,但如果你后续识别阶段用灰度图,两者不要混着来。检测模型始终喂彩色图更稳,因为蓝色车牌和绿色车牌的色相信息对检测也有帮助;灰度化降的是识别端的计算量,不是检测端。

2.4 检测阶段容易翻车的地方:小目标、模糊、夜间反光

车牌检测在实际场景里最常见的问题不是模型结构不够好,而是目标太小。监控画面里一辆车在50米开外,车牌在整幅画面里可能只有二三十像素宽,yolov5的neck层对这种小目标召回率明显不足。我的处理规则是:全图先做一次低置信度检测,比如阈值0.15,把可能的候选框都捞出来;对于那些置信度中等、尺寸很小的框,再按原图位置放大裁出来,单独用更高分辨率重检一轮。这种方法在停车场道闸这种机位固定的场景里非常管用。

夜间是另一个重灾区。红外补光下字符对比度足够,yolov5检测一般还有不错的效果;怕的是强光反射和灯下眩光,车漆反光会在画面里形成高亮斑块,模型容易把高亮区域误判成车牌。治本的办法是收集负样本:把没有车牌的车头、车尾、地面反光图片放进数据目录,不加标注文件。yolov5训练时,没有标注文件的图片会被当作背景样本,帮助模型学会区分“像车牌但不是车牌”的干扰区域。

3. 车牌矫正:很多人忽略但直接影响识别率的一步

3.1 为什么识别前必须矫正

CRNN这类序列识别模型,底层卷积核是按水平方向提取特征的。它对输入图片有一个隐含假设:文字基本是水平排列的。一旦车牌旋转了20度、30度,或者因为透视产生了梯形形变,卷积特征图里的字符形状会发生明显改变,LSTM建模出来的序列也会乱套。实测下来,一张倾斜15度的车牌,直接送进CRNN,识别率可能从95%掉到70%左右。这不是模型不行,而是输入分布变了。

所以yolov5检测得到bbox之后,绝不能直接交给CRNN,必须先做方向归一化。这也是这套项目里决定“能看”和“好用”的关键一步。

3.2 minAreaRect加仿射变换,几行代码解决大部分倾斜问题

最实用的矫正方法是用cv2.minAreaRect找到车牌区域的最小外接矩形,再用旋转矩阵把图像掰正。代码大概长这样:

import cv2 import numpy as np def rotate_plate(plate_img, angle): h, w = plate_img.shape[:2] center = (w // 2, h // 2) matrix = cv2.getRotationMatrix2D(center, angle, 1.0) cos = np.abs(matrix[0, 0]) sin = np.abs(matrix[0, 1]) new_w = int((h * sin) + (w * cos)) new_h = int((h * cos) + (w * sin)) matrix[0, 2] += (new_w / 2) - center[0] matrix[1, 2] += (new_h / 2) - center[1] rotated = cv2.warpAffine(plate_img, matrix, (new_w, new_h)) return rotated # 假设pts是yolov5输出的四个角点或检测框的四个顶点 rect = cv2.minAreaRect(pts.astype(np.float32)) angle = rect[2] if rect[1][0] < rect[1][1]: # 高度大于宽度,需要转90度 angle -= 90 plate_corrected = rotate_plate(crop_img, angle)

这里有个细节很多人会踩坑:OpenCV的minAreaRect返回的角度范围是[-90, 0),它到底代表长边还是短边,不同OpenCV版本可能有差异。判断标准很简单,看rect[1]的宽高,如果宽度小于高度,说明矩形站起来了,需要再转90度。矫正完之后,用cv2.threshold或亮度归一化增强一下,再resize到CRNN的输入尺寸。

3.3 透视畸变什么时候要处理

minAreaRect处理的是平面旋转,遇到斜向摄像头产生的透视变形就不太够了。比如停车位侧方位摄像头,车牌在画面里是一个梯形,用旋转掰正只能把整体转到水平,但左右两侧字符大小还是不一致。这种情况需要用透视变换:

src_pts = np.float32([top_left, top_right, bottom_right, bottom_left]) dst_pts = np.float32([[0, 0], [width, 0], [width, height], [0, height]]) matrix = cv2.getPerspectiveTransform(src_pts, dst_pts) result = cv2.warpPerspective(crop_img, matrix, (width, height))

难点在于四个角点怎么来。如果项目里专门加了车牌角点检测网络,那直接用;如果只有yolov5的矩形框,可以先用边缘检测找到车牌区域的四边形轮廓,再取四个顶点。这套源码里多数场景用的是旋转矫正,已经够用。我的建议是不要一开始就上透视变换,先跑通旋转矫正版本,在真实测试集上统计识别错误,如果大量错误集中在斜侧视角,再考虑加角点检测。

还有一个反直觉的经验:矫正不要“矫枉过正”。如果物体本身只有轻微透视,强行拉成正规矩形反而会造成字符横向拉伸变形,CRNN未必能适应。轻微倾斜交给数据增强去覆盖,重度畸变再走透视变换。

4. CRNN识别:中文字符是这套系统的真正难点

4.1 用一句话讲清楚CRNN怎么把图像变成车牌字符串

CRNN的结构可以理解为三段:CNN负责从图片里提取视觉特征,RNN(通常用双向LSTM)负责建模字符之间的序列关系,CTC负责把特征序列和最终字符串对齐。

具体来说,一张车牌图片经过CNN骨干网络后,会输出一个特征图。这个特征图的高度被压缩到1行,宽度被保留,于是整张图变成了一串特征列。每一列就相当于“打字机扫过车牌的一个竖条”,LSTM对这些特征列逐列建模,最后在每个时间步输出一个字符概率分布。CTC Loss的巧妙之处在于,它不需要我们标出每个字符在第几列,只需要把预测序列和整行字符串做对齐,训练时自动处理重复字符和空白位置。

类比一下:把CRNN想象成一个打字员,它从左到右扫过车牌,每个瞬间只看到一小条区域,但它能把看到的片段串成一句话。这就是序列识别相对单字符分类的优势——它天然能处理字符之间的距离不均匀问题。

4.2 中文字符类别表的坑

中文车牌识别最难的不是技术架构,而是字符集本身。车牌由省份简称汉字、发牌机关代号字母、序号数字/字母、以及少量特殊字符(如警、学、挂、港、澳、使、领)组成。汉字数量虽然只有31个省份简称,但对模型来说,汉字和字母数字混排会把分类空间的难度拉高不少。

设计字符类别表时,我的建议是只包含车牌上真实出现的字符,不要贪全。省级简称就是那么几十个汉字,字母这块有一个关键处理:车牌上通常不用字母“I”和“O”,以免和数字1、0混淆。所以类别表里可以直接去掉这两个字母,减少分类压力。

字符类别数量大致范围例子
省份简称汉字约31个京、津、冀、晋、蒙、辽、吉、黑、沪、苏、浙、皖、闽、赣、鲁、豫、鄂、湘、粤、桂、琼、渝、川、贵、云、藏、陕、甘、青、宁、新
英文字母24个(去掉I/O)A-Z中除I、O
数字10个0-9
特殊字8~10个警、学、挂、港、澳、使、领等
CTC blank1个用于CTC对齐的空位

总类别数一般在70个上下。这个规模不大,但样本分布极度不均衡,省份简称里人口大省出现的车牌远多于其他省份,直接训练会让模型对低频省份简称过拟合风险低但召回差。训练时可以对样本做类别加权,或者按省份简称做重采样。

4.3 训练CRNN的数据来源与数据增强

车牌真实数据是稀缺资源,尤其是带各种复杂背景、光照变化的中文车牌。公开数据集里有一个常见的方案是用CCPD(中国城市停车场数据集)做预训练,它包含几十万张真实中文车牌图片,带字符级标注,很适合做CRNN的初始训练。但CCPD的图片背景比较单一,直接拿去部署到各种摄像头视角会掉点,后期还是要用你自己的场景数据做微调。

另一个绕不开的路子是合成数据。用字体渲染把字符画在背景上,再加透视变换、模糊、噪声、光照遮盖,生成大量带标签的车牌图片。合成数据的最大问题是“假”——字符边缘太干净,背景太干净。我的经验是合成数据用来做预训练可以,但最终必须用真实数据微调,否则识别率看着虚高,一上真实场景就崩。

CRNN的输入规格通常是固定高度32像素,宽度可以动态变化。蓝牌7个字符、新能源绿牌8个字符,宽度差不少,训练时batch内会做宽度等宽补齐。推理阶段需要注意动态shape对框架部署的约束,如果导出onnx时固定了输入宽高,长车牌有被截断的风险。

4.4 CTC解码、置信度与易混淆字符

CRNN解码最常用的贪心策略是:每个时间步取概率最高的字符,然后合并相邻重复字符、去掉blank。对于车牌这种长度确定(蓝牌7位、绿牌8位)的场景,贪心解码基本够用。如果解码出来的长度和预期不符,可以用beam search找更合理的候选序列,再按长度过滤。

置信度不能用最后一个softmax最大值,更好的做法是计算所有字符概率的几何平均,这样能反映整行字符的稳定度。实际应用中,我会把置信度低的识别结果标记为“不确定”,不直接参与开闸或报警,避免误识别造成更大问题。

易混淆字符主要集中在汉字之间。比如“浙”和“湘”、“赣”和“费”之间笔画细节相似,模糊图片下非常容易错。另一个常见错误是字符有遮挡或脏污时,模型把“8”认成“6”、把“B”认成“8”。这些只能靠数据增强和真实验证集迭代慢慢压下去,没有什么一劳永逸的trick。

5. 串起来跑通:源码结构、训练流程与端到端评估

5.1 拿到源码后的第一件事:看目录与README

“高分项目”这套源码,文件夹里通常会有detect/和recognize/两个模块,分别对应yolov5和CRNN的训练与推理代码,外加若干工具脚本、权重文件和文档。我的建议是别急着跑train,先把README从头到尾读一遍。重点看四件事:环境依赖版本、目录说明、训练命令、推理命令。

安装依赖时最容易翻车的是PyTorch和CUDA的版本不匹配。yolov5官方requirements会拉一堆包,其中有些不是必须的。我的习惯是新建一个虚拟环境,把torch版本锁定到和官方文档一致的版本,例如Python 3.8、PyTorch 1.12左右,openCV就用pip默认版本。先把detect.py跑一张测试图,确认检测正常,再跑CRNN的demo,最后才动训练。

5.2 从训练到推理的完整命令流

整个项目从零跑通的命令流类似这样:

# 1. 训练yolov5检测模型 python train.py --data data/plate.yaml --weights yolov5s.pt --img 640 --batch-size 16 --epochs 100 # 2. 测试检测模型 python detect.py --weights weights/best.pt --source test_images/ --save-txt --save-conf # 3. 准备CRNN训练数据 # train.txt中每行格式:图片路径 车牌号 cat data/train.txt | head -5 # 4. 训练CRNN识别模型 python train_crnn.py --train data/train.txt --val data/val.txt --batch-size 64 --lr 1e-3 --epochs 60 # 5. 端到端推理 python run.py --image demo.jpg --detect_weights weights/detect_best.pt --recognize_weights weights/crnn_best.pth

CRNN训练脚本里的字符表、输入高度、序列长度都要和你的数据集对齐。如果你用的是别人改过的CRNN源码,注意看它config里的字符集合是否覆盖了你需要的汉字,否则训练时会因为字符越界报错或者静默识别错误。

真正部署时要注意,不要每次都把裁剪的车牌图写到磁盘再读取,内存直接传数组给CRNN推理,速度能快不少。视频处理时,可以先把一帧检测完的所有车牌区域一次性送入CRNN,利用batch推理,比逐张处理快几倍。

5.3 如何科学地评估“识别率”

很多项目文档里只写一个总体准确率,比如“识别率95%”,但这是一个容易被数字骗的游戏。车牌识别至少要拆成三个指标看:检测召回率(车牌有没有被找出来)、整牌准确率(整个车牌字符串对不对)、字符准确率(每个字符对不对)。整牌准确率要求最高,一个字符错就是整牌错,所以它通常比字符准确率低几个点。

评估集还应该按场景分组,不能混在一起只看一个数。

场景样本数检测召回率字符准确率整牌准确率
白天近距离200099.3%98.5%95.2%
白天远距离80094.1%94.6%88.3%
夜间100096.2%92.1%84.6%
斜侧角度60093.8%90.4%80.1%

按场景区分之后,你才能知道模型短板在哪里。如果夜间整牌准确率只有85%,那优化重点应该放在夜间数据增强、曝光补偿、CRNN输入归一化上,而不是继续堆白天数据。

还有一个容易被忽略的问题:评估集不能从视频连续帧里密集抽帧。同一个场景同一辆车连续几十帧,模型只要能推断这辆车,准确率就会被刷得很高,但这不能代表真实应用。采样时至少要隔几帧抽一帧,或者按车辆ID去重。

6. 部署优化与后续扩展

6.1 模型压缩与边缘端落地

这套源码如果只是跑在PC上,yolov5s加标准CRNN已经够用。但真实场景经常是边缘盒子部署,比如rk3568、rv1106这类国产嵌入式平台。这时候模型要跟着换:yolov5s换成yolov5n,CRNN的骨干网络换成轻量的CycleMLP或者缩减通道数。最近热词里“yolov5在rk3568上”“rv1106搭建yolov5模型”出现的频率很高,说明很多人都在做这个方向的移植。

移植流程一般是:PyTorch模型导出ONNX(注意固定输入尺寸或支持动态shape),再用ONNX转成NCNN或RKNN,最后做量化。这个过程中最容易出问题的是某些算子在目标平台上不支持,比如yolov5里的Focus层在转ONNX时会有兼容问题。好在yolov5官方已经把这些都处理好,v7.0版本导出onnx基本无痛。

如果实在想省事,检测和识别都统一输入尺寸,用静态shape导出,部署端会稳定很多。代价是输入图片的长宽比会被拉伸,需要靠检测阶段letterbox和识别阶段等宽padding来缓解。

6.2 实际项目中遇到的几个“玄学”问题

跑得久了你会发现,最影响体验的往往不是模型精度,而是工程细节。视频流长时间运行后越跑越慢,大概率是每帧创建了大量numpy数组没有释放,或者PyTorch在GPU上累积了显存碎片。我的处理方式是每处理完一帧就del掉大数组,torch.cuda.empty_cache()只在每500帧显存明显增长时调用一次,否则频繁调用反而拖慢速度。

另一个容易踩的坑是相机自动曝光导致同一辆车在不同帧的亮度差异巨大。CRNN对亮度的敏感度比检测模型高,同一张车牌白天识别对、晚上识别错的情况很常见。一个低成本解法是在送进CRNN之前做一次简单归一化,比如把整块车牌区域转灰度后做对比度拉伸。这比加数据增强更直接有效。

车牌表面有泥、贴装饰边框、前车挡住一半,这些corner case不会出现在教学数据里,但真实数据里总会撞见。这类case模型的识别置信度会明显下降,你需要在后处理里设计兜底策略:置信度低于阈值的车牌不做最终判定,而是记录现场图,供人工复核或下一帧再试。

6.3 后续可以怎么扩展

这套项目跑通后,往上加东西很方便。最简单的扩展是加车牌颜色识别,从yolov5检测到的crop区域直接用颜色直方图判断蓝牌、绿牌、黄牌,零成本且非常准,能和字符识别结果互相校验。再进一步可以加车辆检测和跟踪,把“识别到一张车牌”升级成“识别到一辆车在某个时间点进入/离开”,这才是道闸和园区管理真正需要的语义。

如果想让识别率再往上走,可以考虑用视频多帧投票策略:同一辆车连续几帧识别结果相同且置信度较高,才作为最终结果。这样能把单帧偶发错误大量过滤掉。更重的做法是对CRNN输出做语言模型纠错,利用车牌字符的合法组合规则(比如省份简称后一位必须是字母),从beam search候选里挑出最合理的序列。

我的体会是,这套基于yolov5和CRNN的中文车牌识别系统,最大的价值不是“能识别车牌”这个结果,而是它把你带进了从检测到序列识别再到工程部署的完整链路。把评估集建好、把错误case翻清楚,这套系统会越用越顺手。

本文还有配套的精品资源,点击获取

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

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

立即咨询