PaddleOCR车牌识别实战:微调通用OCR模型实现高精度车牌号输出
2026/9/12 22:14:52 网站建设 项目流程

简介:基于PaddleOCR的车牌号识别精简源码包,面向具备基础Python知识、想快速上手OCR车牌识别的开发者与学习者。它在PaddleOCR官方源码基础上做了针对性裁剪,保留检测、识别、分类等核心模块,配合作者文章中提供的模型权重即可直接调用,免去从零搭建复杂环境的成本。压缩包共33个文件,以15个Python脚本为主,涵盖推理预测、后处理与工具函数;另有14个pyc缓存文件、2个文本字典/参数文件及2张JPG样例图,整体体积仅273KB,结构紧凑,便于下载后快速查看与调试。资源内含det、rec、cls三类预测入口及结果可视化样例,目录组织方式贴近PaddleOCR原生工程,方便理解OCR推理流程或在此基础上扩展自己的车牌数据。当前已有417人学习使用,适合用于课程设计、毕业设计或工业车牌识别前的效果验证,是一份轻量实用的参考代码包。

1. 车牌识别这件事,PaddleOCR 解决了一半

用 PP-OCR 通用模型直接跑车牌图片,检测基本能框住车牌区域,但识别结果往往败在中文省份简称上——"鲁A"识别成"兽A"、"京N"变成"示N",这类错误不是模型不够强,而是通用 OCR 的字符集是全量中文字符,车牌只用了 31 个省级简称,两者概率分布天然不匹配。车牌识别本质上是一个约束极强的问题:字符位置固定、长度固定或接近固定、字体单一、字符集小,但对比度差、大角度、反白字和金属反光又让图像质量远不如文档扫描件。本文要讲的是,怎么在 PaddleOCR 的检测+识别双阶段架构上,用一套可复现的源码方案,把通用 OCR 改造成能输出合法车牌号的专用识别链路,覆盖数据准备、微调、后处理到部署排错的完整路径。

2. 从 PaddleOCR 检测+识别结构拆车牌识别需求

2.1 检测、识别分离结构对车牌场景的三个关键影响

PaddleOCR 的默认流水线是 DB 文本检测 + PP-OCRv4 识别(也可以是 v3 或更早的 v2),两个模型独立训练、独立推理。这个结构对车牌场景有三个直接影响。

第一,检测模型输出的四边形框是"文本行"级别的,不是"车牌"级别的。车牌虽然有明确边框,但检测器只认字符纹理,如果车牌上有污渍、拖车钩、保险杠横条,框会略大或略歪。框歪了,识别模型拿到的就是带背景的裁剪图,字符切分起点就偏了。第二,识别模型按整行文本的逻辑做序列预测,对字符间粘连非常敏感。"京A·12345"里的小圆点、间隔符,模型有时会吞掉,有时会输出成标点。第三,中文省份简称是低频字符,通用训练集里"京""沪""浙"这类字出现频率远低于"的""了""一",模型对它们的特征记忆不深,迁移到车牌这种强反白、低分辨率的场景时,特征响应会漂移。理解了这三点,后续所有改造动作都是围绕"缩小输入多样性、补足字符分布"展开的。

一个常见的误区是直接用 PaddleOCR 官方提供的车牌识别模型或者第三方权重。这些模型在标准蓝牌上表现尚可,但遇到新能源绿牌、警车白牌、使馆黑牌,或者摄像头俯拍的大角度图像时,字符集外字符和几何畸变会同时触发错误。自己微调不是可选项,是必经步骤。

2.2 在本地把 PaddleOCR 车牌识别源码环境跑起来

先搭环境。PaddleOCR 的源码仓和 Python 包是分开的,源码仓用于训练和二次开发,paddleocr 包用于快速推理。两者最好都装上。

# 创建虚拟环境,Python 3.8 - 3.10 均可 conda create -n paddleocr python=3.10 -y conda activate paddleocr # 安装 PaddlePaddle GPU 版(以 CUDA 11.8 为例),CPU 版去掉 cu118 后缀即可 python -m pip install paddlepaddle-gpu==2.6.1.post118 -i https://www.paddlepaddle.org.cn/packages/stable/cu118/ # 克隆源码仓并安装依赖 git clone https://github.com/PaddlePaddle/PaddleOCR.git cd PaddleOCR pip install -r requirements.txt pip install -e .
# 用自带模型快速推理一张车牌图 paddleocr --image_dir test_license.jpg --use_angle_cls false --use_gpu true

上面命令里--use_angle_cls false是关掉方向分类器。方向分类器用于区分 0/180 度旋转文本,车牌这种强先验目标用不到,关掉能省一次推理开销。输出会以 JSON 形式打印检测框、识别文本和置信度。先跑通这一步,确认环境无异常,再进入微调阶段。

组件版本建议作用
Python3.10Paddle 官方 wheel 覆盖最稳
paddlepaddle-gpu2.6.x训练和推理后端
PaddleOCR 源码release/2.7含训练脚本和 PPOCRLabel
opencv-python4.8+图像预处理依赖

2.3 用现成模型先看基线结果

环境通之后,别急着训练,先拿 20 张不同类型的车牌照跑一轮基线,把 JSON 结果落盘,逐张看失败模式。

from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=False, lang="ch", show_log=False) def run_baseline(image_paths): for path in image_paths: result = ocr.predict(path) for res in result: texts = [line["text"] for line in res["rec_texts"]] scores = res["rec_scores"] boxes = res["det_polys"] print(f"{path}: {texts} | scores: {scores} | boxes: {boxes}") run_baseline(["car1.jpg", "car2.jpg", "car3.jpg"])

这段代码把检测框坐标、识别文本、置信度一次打出来。重点观察两类问题:如果boxes坐标明显偏出车牌边界,是检测问题,后续要靠数据增强或者检测微调解决;如果boxes正常但texts里的中文是错的,是识别问题,这占大多数。统计一下 20 张图里"检测框正常但识别错"的比例,如果超过 30%,说明当前模型对这张风格的车牌完全不敏感,微调识别模型是最高优先级;如果检测框本身就飘,那要先处理检测。

提示:车牌识别场景下,检测框的质量直接决定识别上限。框歪 2 个像素,识别准确率就可能掉 5 个点,后处理补不回来。

3. 车牌数据是识别精度的分水岭,微调前先做两类数据

3.1 真实车牌数据和合成车牌的取舍

车牌识别微调的数据来源有三类:现场采集、网络爬取、程序合成。三类的成本和效果差异巨大。

现场采集最理想,但涉及个人隐私和数据合规问题,普通开发者拿不到大批量真实车牌,能拿到几十张做验证已经算不错。网络爬取的车牌图质量参差不齐,而且很多是文章配图,分辨率低、角度单一,实际帮助有限。程序合成是性价比最高的方式——用 PIL 或 OpenCV 把字符渲染到背景图上,加透视变换、噪声、模糊、光照变化。合成数据的核心技巧是"字符字体要接近真实车牌字体"。

真实车牌用的是特殊字体,中文是等线体变种,数字和字母是 DIN 类的无衬线字体。常见的免费替代方案是用开源的FS_MA_字体或汽车牌照专用字体,找不到就用思源黑体加粗,再配合一定程度的形态学腐蚀膨胀模拟喷墨打印效果。合成时必做的增强项包括:随机饱和度偏移营造不同新旧程度、随机多边形遮挡模拟泥污、随机透视角度模拟摄像头安装高度。

import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont def synthesize_plate(plate_text, font_path, output_size=(440, 140)): # 蓝牌底色,新增度随机 bg = np.full((output_size[1], output_size[0], 3), (220, 130, 30), dtype=np.uint8) # 转换为 PIL 绘制字符 img = Image.fromarray(bg) draw = ImageDraw.Draw(img) font = ImageFont.truetype(font_path, 80) # 字符间距按真实车牌比例设定 draw.text((20, 20), plate_text, font=font, fill=(255, 255, 255)) # 随机透视变换 pts1 = np.float32([[0, 0], [output_size[0], 0], [0, output_size[1]], [output_size[0], output_size[1]]]) pts2 = np.float32([[5, 3], [output_size[0]-8, 2], [2, output_size[1]-6], [output_size[0]-5, output_size[1]-9]]) matrix = cv2.getPerspectiveTransform(pts1, pts2) result = cv2.warpPerspective(np.array(img), matrix, output_size) # 加高斯模糊模拟摄像头对焦不准 result = cv2.GaussianBlur(result, (3, 3), 0) return result

合成代码里的关键参数是pts2的四个坐标偏移量,偏移从 2 到 10 像素,模拟不同角度的轻微透视。GaussianBlur的核大小固定为 3×3,不要用大核,否则字符边缘会糊成一团,模型学到的特征是模糊而不是字符结构。合成数据生成量建议 5000 张起步,覆盖 31 个省份简称、字母 I/O 的混淆场景、数字 0/O、1/I 的形近组合。

3.2 用 PPOCRLabel 产出 PaddleOCR 可训的标注格式

合成数据可以自动生成标注文件,但真实采集的图片需要人工标注。PaddleOCR 官方提供了 PPOCRLabel 工具,它位于源码仓的PPOCRLabel目录。启动它会打开一个 GUI 界面,可以画框、写转写文本。

cd PaddleOCR/PPOCRLabel python PPOCRLabel.py --lang ch --save_to_txt

标注界面里按住鼠标左键画四边形框,框住整个车牌区域,然后在弹出的对话框里输入车牌内容,例如"京A12345"。保存后生成两个重要文件:Label.txt包含检测框坐标和文本,crop_img/目录下存放裁剪后的车牌小图。注意 PPOCRLabel 的多边形点序是顺时针还是逆时针,PaddleOCR 训练脚本能自动处理,但如果你写脚本读标注文件,必须检查坐标点序,点序错了检测 loss 会不收敛。

标注规范上要统一几点:蓝牌和绿牌分开标注,绿牌的字符比蓝牌多一位,新能源牌是 8 位(省份简称 + 字母 + 6 位数字/字母),如果标注时把位数标错,识别模型的序列长度分布会被污染。遮挡严重的车牌该删就删,不要因为"这张图难"强行标进去,噪声样本会把模型带偏。

3.3 识别模型微调的 yaml 改法与训练命令

数据齐了进入微调。PaddleOCR 的识别模型训练入口是tools/train.py,配置文件在configs/rec/PP-OCRv4/下。微调不是从头训,而是加载官方在公开数据集上训好的权重做 finetune,这样可以大幅减少数据和训练时间。

# 下载官方 PP-OCRv4 识别模型权重 wget -P ./pretrain_models/ https://paddleocr.bj.bcebos.com/PP-OCRv4/chinese/ch_PP-OCRv4_rec_train.tar tar -xf ./pretrain_models/ch_PP-OCRv4_rec_train.tar -C ./pretrain_models/ # 修改配置文件 ch_PP-OCRv4_rec_hgnet.yml 中的关键项后启动训练 python tools/train.py -c configs/rec/PP-OCRv4/ch_PP-OCRv4_rec_hgnet.yml \ -o Global.pretrained_model=./pretrain_models/ch_PP-OCRv4_rec_train/best_accuracy \ Global.epoch_num=50 \ Train.dataset.data_dir=./train_data \ Train.dataset.label_file_list=["./train_data/rec_gt.txt"] \ Train.dataset.data_transforms[0].RecAug.prob=0.5

配置参数拆开看:pretrained_model指向官方预训练权重的路径,不包含.pdparams后缀;epoch_num这里设 50 轮,因为车牌字符集小、数据量不大,轮数太多会过拟合,在验证集上看到字符准确率不再提升时就该停;Train.dataset.data_transforms[0].RecAug.prob是数据增强概率,车牌场景建议降低到 0.3-0.5,增强太强会把字符的几何结构改变太多,反而破坏车牌字符的硬先验。

还有一个必改项是字典文件。PaddleOCR 默认的中文字典有 6623 个字符,车牌识别只需要其中 31 个省份简称加数字字母。在配置文件的character_dict_path里指定一个只含相关字符的字典文件,能显著减小模型输出头的维度,加快训练收敛,同时避免模型在推理时输出"鲁"以外的奇怪汉字。

# 生成车牌专用字典 python -c " chars = '京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新' digits = '0123456789' letters = 'ABCDEFGHJKLMNPQRSTUVWXYZ' unique = list(dict.fromkeys(chars + digits + letters)) with open('plate_dict.txt', 'w') as f: f.write('\n'.join(unique) + '\n') "

注意:dict 文件里不能有重复字符,每行一个字符,文件末尾保留换行。字母表里我刻意去掉了IO,因为国内车牌号不使用这两个字母,留着只会让模型在遇到相似字形时多一个容易混淆的输出。

训练完的模型在output/目录下,best_accuracy.pdparams是验证集表现最好的权重。评估命令单独跑,不要用训练时输出的 loss 当作识别精度。

python tools/eval.py -c configs/rec/PP-OCRv4/ch_PP-OCRv4_rec_hgnet.yml \ -o Global.pretrained_model=./output/best_accuracy \ Global.eval_batch_step=[0, 100] \ Eval.dataset.data_dir=./eval_data \ Eval.dataset.label_file_list=["./eval_data/rec_gt.txt"]

4. 把源码推理改造成能输出合法车牌号

4.1 后处理管线:置信度阈值 + 省简称表 + 定长补全

模型微调完,推理侧不能直接把rec_texts当成车牌号输出。模型输出的是原始字符串,里面可能有中间空格、置信度低的字符、甚至模型幻觉出的非法字符。需要在后处理里做三层过滤。

第一层是字符置信度过滤,PaddleOCR 的结果里有每个字符的置信度吗?严格来说没有,rec_scores是整行文本的平均置信度,不是逐字符的。所以要用 PaddleOCR 暴露的rec_textsrec_scores做一个粗筛,低于阈值的整行直接丢弃。第二层是合法性校验,用正则把字符串里的非法字符剔除、把字母转大写、把中文限制在省简称表内。第三层是定长逻辑,蓝牌 7 位(不含点号),绿牌 8 位,如果识别结果长度和期望不一致,做补全或截断。

import re PROVINCE_SHORT = "京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼" PLATE_PATTERN = re.compile(r"^([%s])([A-Z])([A-Z0-9]{5,6})$" % PROVINCE_SHORT) def normalize_plate(text, score, expected_len=7): # 去空格和分隔符 text = text.replace(" ", "").replace("·", "").replace("-", "") # 字母统一大写,排除 I/O text = text.upper().replace("I", "1").replace("O", "0") if score < 0.75: return None, score m = PLATE_PATTERN.match(text) if m: plate = m.group(1) + m.group(2) + m.group(3) if len(plate) == expected_len: return plate, score # 长度不对时尝试截断尾部 if len(text) > expected_len: candidate = text[:expected_len] if PLATE_PATTERN.match(candidate): return candidate, score return None, score

normalize_plate里的几个处理逻辑说明:I转成1O转成0是因为车牌不包含这两个字母,识别模型容易把数字 1 认成 I、把 0 认成 O,这个映射对蓝牌准确率提升明显;expected_len蓝牌传 7,绿牌传 8,在调用方根据检测结果的颜色先做一次判断,或者传两个候选值分别尝试匹配。score阈值 0.75 只是初值,如果实际场景漏检多,往下调到 0.65,如果误检多,往上调到 0.85,阈值在验证集上扫一遍取最优点。

4.2 一批图片的平均字符准确率和整牌准确率怎么算

模型调完,要用客观指标评估,不能在几张测试图上"目测还行"就结束。车牌识别领域有两个常用指标:整牌准确率(严格匹配)和字符准确率(宽容匹配)。整牌准确率是指整个车牌号完全正确才算对,字符准确率是逐字符比对,结果适合定位到底是哪个位置的字符容易错。

def evaluate_predictions(ground_truth, predictions): total_chars = 0 correct_chars = 0 correct_plates = 0 per_position = {i: [0, 0] for i in range(8)} for gt, pred in zip(ground_truth, predictions): if gt == pred: correct_plates += 1 for i, (g, p) in enumerate(zip(gt, pred)): per_position[i][1] += 1 if g == p: correct_chars += 1 else: per_position[i][0] += 1 total_chars += len(gt) char_acc = correct_chars / total_chars plate_acc = correct_plates / len(ground_truth) return char_acc, plate_acc, per_position

per_position统计的是第几位字符的出错次数,这个信息非常值钱。常见规律是第 2 位(省份简称后的字母)和第 3、4 位(数字区域)错误率最高,第 2 位是因为字母区和数字区交界处字符间隔小,第 3、4 位是因为车牌中间的点号或防伪标识会在字符边缘制造噪点。如果某个位置错误率明显偏高,针对这个位置做卡阈值或者在合成数据里加大该位置的字体形变,是最直接的优化手段。

5. 剩下的精度坑,用预处理和部署细节填平

车牌识别做到 95% 整牌准确率后,再往上走的每一步都是抠细节。第一个坑是大角度车牌。摄像头俯拍或侧面抓拍时,车牌是梯形甚至不规则的四边形,直接裁剪送入识别模型,字符会被压缩变形。处理方法是先做透视矫正,把检测出的四个角点映射到标准矩形。

def perspective_fix(image, quad): # quad 是检测模型输出的四个角点,顺序为左上、右上、右下、左下 dst_pts = np.float32([[0, 0], [440, 0], [440, 140], [0, 140]]) quad = np.float32(quad).reshape(4, 2) # 保证点序正确:按左上、右上、右下、左下排列 mat = cv2.getPerspectiveTransform(quad, dst_pts) return cv2.warpPerspective(image, mat, (440, 140))

点序错了矫正会变成翻转或错位映射,所以从检测结果拿角点时先按 y 坐标排序分成上下两组,各组内按 x 坐标排序,这是最常见的四角点排序方式。第二个坑是双层黄牌和新能源绿牌的交替字符排列,这类车牌的字符不是单行的,检测模型可能把两行字共用一个框,识别模型只能识别单行文本,输出必然乱。常见做法是对检测框的高度做判断,如果宽高比小于 2.5,就按上下两半拆开分别识别,再把结果拼接。第三个坑是夜间低照度,蓝牌在低光下呈深灰色,字符对比度骤降,可以在预处理里先做 CLAHE 局部直方图均衡再送入识别。

部署侧的常见诉求是把模型导出成 ONNX 格式,绕开 Paddle 的 Python 运行时,在 C++ 或者 Java 端做推理,这对摄像头抓拍场景的延迟和内存占用都有好处。

# 导出 ONNX,输入尺寸 3x48x320,按权重实际尺寸填 paddle2onnx --model_dir ./output/inference \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ./output/plate_rec.onnx \ --opset_version 11
部署方式单张推理延迟(CPU, i5-1240P)依赖体积适用场景
Paddle Python35-55ms原型验证、离线批处理
Paddle Inference C++18-25ms后端服务
ONNX Runtime C++15-22ms边缘盒子、嵌入式

ONNX 导出后配合 OpenCV 做图像预处理,在 C++ 里集成时格外注意输入张量的归一化参数,PaddleOCR 用的是[0.5, 0.5, 0.5]均值和[0.5, 0.5, 0.5]方差,直接用其他模型的[0.485, 0.456, 0.406]那一套会导致识别精度骤降。归一化参数错了,模型输出全部跑偏,但报错还查不出来,是最容易埋坑的地方。

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

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

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

立即咨询