做车牌识别这个需求找我的人一直不少,从停车场道闸到园区安防,说的直白一点就是两件事:先把车牌区域从画面里框出来,再把框里的字符读出来。以前我常用的是 YOLOv8 + PaddleOCR 那套组合,整体能用,但部署时总被 PaddlePaddle 的依赖折腾得够呛,换设备、配环境经常要浪费大半天。最近我把整条管线换成了 YOLO11 + RapidOCR,实测下来无论检测精度、识别速度还是部署便利性都明显提升,尤其是 RapidOCR 用 ONNX Runtime 跑起来非常轻,CPU 环境也能实时处理。这篇文章就把这套 AI 车牌识别方案的完整细节拆开讲清楚,包含模型选型、训练调参、OCR 接入、后处理规则以及我踩过的坑,适合刚接触目标检测和 OCR 的开发者,也适合想把手头车牌识别项目做扎实的工程师参考。
1. 项目背景与整体方案设计
1.1 两阶段方案为什么比端到端更靠谱
车牌识别在技术路线上基本分成两种:一种是端到端模型,输入图片直接输出车牌字符串,比如 LPRNet、HyperLPR 这类方案;另一种是两阶段方案,先用目标检测模型把车牌定位出来,再对裁剪出来的小图做字符识别。我一开始也试过端到端模型,在公开数据集上表现不错,到了实际场景就开始露馅:车牌稍微倾斜一点、光照暗一点,识别结果就彻底乱了。而且端到端模型一旦出问题,你很难判断是定位错了还是识别错了,排查起来非常痛苦。
两阶段方案最大的好处是每一步都可控。检测阶段只负责回答“车牌在哪”,识别阶段只负责回答“这串字符是什么”,任何一个环节出了问题都可以单独验证、单独优化。比如检测框偏了,我就先去调检测模型;字符读错了,我只需要针对 OCR 环节做后处理或者重新训练。这种模块化的设计思路在实际工程项目里非常实用,尤其是当你需要对接不同品牌的摄像头、处理各种复杂场景时,灵活性决定了你能省多少精力。所以我的最终选型很明确:YOLO11 负责检测车牌位置,RapidOCR 负责识别车牌字符,中间加一个预处理和后处理模块作为粘合剂。
1.2 完整流程拆解:从图像输入到结构化输出
整条推理链路看起来不复杂,真正做起来每个环节都有文章。先用 YOLO11 对原始图像做目标检测,拿到车牌检测框的坐标、置信度和类别信息;然后根据检测框把车牌区域从原图里裁剪出来,如果车牌有明显倾斜角度,还需要做透视校正;接着把处理好的车牌区域图片交给 RapidOCR,OCR 会输出文本内容和置信度;最后用车牌规则对 OCR 原始输出做清洗,比如过滤非法字符、纠正易混淆字符、补全缺失格式,最终输出规范的车牌字符串。
这个流程最关键的思维转换是:OCR 不是直接对整张图做识别,而是把检测出来的车牌小图喂给它。这么做有几个实际好处。第一,省去了 OCR 在整张大图上找文字区域的计算量,CPU 占用和延迟都会明显下降;第二,输入图像内容更纯净,OCR 不需要面对车身上的广告文字、背景招牌等干扰,识别准确率自然上升;第三,车牌检测和字符识别两个模型可以独立升级迭代,比如检测模型换了更强的版本,OCR 完全不用动,整体维护成本低不少。这也是我在项目里一直坚持两阶段流水线的原因。
2. 环境准备与依赖安装
2.1 开发环境基础配置
先说开发环境。我这里建议使用 Python 3.9 到 3.11 之间的版本,太老的新版本依赖装不上,太新的某些包还没做好兼容。我自己用的是 Python 3.10 + CUDA 11.8 + PyTorch 2.x 这套组合,训练和推理都跑得很稳。如果只是做 CPU 推理,那 PyTorch 直接装 CPU 版本就行,不需要装 CUDA 全家桶,能少踩很多环境坑。
推荐用 conda 管理环境,隔离性比 pip 全局安装好太多。我建环境的命令是:
conda create -n plate python=3.10 conda activate plate pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118注意 PyTorch 的安装要看你的显卡驱动版本。如果机器上没有 NVIDIA GPU,或者只是测试用,直接pip install torch torchvision装 CPU 版即可,后面的代码完全一样,只是训练速度会慢几个数量级。我平时会在台式机上训练,到现场部署时用一台只装了 CPU 版本依赖的迷你主机,模型文件完全通用,这也是这套方案让我舒服的地方。
2.2 YOLO11 与 RapidOCR 安装细节
安装 YOLO11 非常简单,Ultralytics 官方把推理、训练、导出整套能力都封装在同一个包里:
pip install ultralytics装完之后可以顺手验证一下版本,至少在 8.3.0 以上才算包含 YOLO11:
python -c "import ultralytics; print(ultralytics.__version__)"RapidOCR 的安装同样走 pip,我推荐装 onnxruntime 版本:
pip install rapidocr_onnxruntime这个包会自动把 OCR 需要的三个模型文件(文本检测、方向分类、字符识别)下载到本地缓存,不需要手动下载模型,也不需要安装 PaddlePaddle 框架,依赖非常干净。有些老教程会让你直接装 RapidOCR 的源码包,那个版本还需要自己拉模型库,没必要,用 PyPI 上的现成包就好。
这里有一个我踩过的坑:rapidocr_onnxruntime对 NumPy 版本有要求。如果你之前装的是 NumPy 2.0 以上版本,onnxruntime 可能会报找不到某个符号的错误。解决方法是把 NumPy 降到 1.24 或者 1.26:
pip install "numpy<2"这个问题在 Windows 上尤其常见,Linux 上稍微好一点,但还是建议装完依赖后先用一段极小的图片跑一次 OCR,确认环境没问题再往下走。
3. 车牌检测:YOLO11 的实战配置
3.1 数据集准备与标注要点
车牌检测模型的训练效果很大程度上取决于数据集,而不是模型结构。我用的数据主要来自两个部分:公开数据集 CCPD(中国车牌数据集)和自己拍摄补充的现场图片。CCPD 覆盖了多种天气、角度和光照条件,但是图片分辨率普遍不高,而且车牌相对画面占比偏大,直接拿它训练出来的模型在远距离小目标场景下容易漏检。
自采数据不需要太多,三百到五百张足够,关键是覆盖你自己的真实部署场景。标注工具我用的是 LabelImg 或者 X-AnyLabeling,导出成 YOLO 格式的 txt 文件就行。每张图的标注内容是一行:类别编号 + 归一化后的中心点坐标和宽高。这里有个细节,车牌标注框不要太紧贴字符边缘,稍微往外扩 2 到 3 个像素反而有助于后续 OCR 识别,因为 OCR 需要一定的边界上下文来正确切分字符。
关于类别设置,我建议只设一个“plate”类别,不要按车牌颜色或类型分多个类别。原因很简单:后续 OCR 环节本身就能区分字符内容,没必要在检测阶段做细分类;而且多类别会分散模型的拟合能力,在车辆类型复杂的场景下反而容易互相干扰。黄牌、绿牌、蓝牌之间的差异主要体现在颜色和尺寸上,检测模型只需要找到车牌这个区域,颜色信息丢给后处理去做判定更合理。
3.2 训练参数选择与调优经验
训练参数我直接给一套自己反复验证过的配置。以 YOLO11s 为例,输入图像尺寸设为 640x640,初始学习率 0.01,批次大小看显存来定,80G 显存可以开到 64 或 128,16G 显存开到 16 到 32 比较稳妥。训练轮数不用硬性追求 300 轮,我通常设 100 轮加早停,观察验证集 mAP 曲线,基本在 60 到 80 轮之间就收敛了。
# plate.yaml path: ./dataset train: images/train val: images/val nc: 1 names: ["plate"]训练命令:
yolo detect train \ model=yolo11s.pt \ data=plate.yaml \ imgsz=640 \ epochs=100 \ batch=32 \ patience=15 \ optimizer=AdamW \ lr0=0.01 \ device=0这个配置里patience=15的意思是连续 15 轮验证集指标没有提升就自动停止,能省不少训练时间。yolo11s.pt是官方预训练权重,加载它能大幅缩短收敛时间,尤其是你的数据集只有几百张图片时,一定不要从头训练,不划算。
我在实际调参过程中发现几个可以复用的判断标准。如果模型在验证集上的查全率低、漏检多,优先考虑增加数据增强强度、采集更多远距离样本;如果查准率低、误检多(比如把车标、车灯当成了车牌),优先检查标注框是否准确,以及是否在增强过程中把非车牌区域切了进来。另外,用小模型 yolo11n 做快速迭代实验非常高效,先确定数据方案和参数方向,再切到 yolo11s 或 yolo11m 做最终训练。
3.3 推理后处理:置信度阈值与 NMS 设置
模型训练好之后,推理阶段有两个参数直接影响效果:置信度阈值 conf 和 NMS 的 IoU 阈值 iou。我自己常用的设置是conf=0.25, iou=0.7。这个组合在多数场景下表现均衡,检测框不会太碎,误检也不多。
置信度阈值不能拍脑袋乱调。阈值设太高,比如 0.8,会把一些光照不足、部分遮挡的车牌框漏掉,因为它们天然置信度偏低;阈值设太低,比如 0.05,又会出现大量误检框,后处理阶段处理起来很麻烦。有一种调参经验是:先跑一批真实场景图片,统计检测框的置信度分布,看看正确的检测框集中在哪个区间,再把阈值卡在分布低谷处。这个办法比凭感觉调要科学得多。
NMS 的 IoU 阈值主要管重叠框的合并。如果一张图上同一个车牌被模型输出了三四个框,说明 iou 阈值偏高,合并不够狠;如果两个相邻车牌的框被合并成了一个,说明 iou 阈值偏低。一般 0.6 到 0.7 是个合理区间,我通常不动它,除非出现特殊的密集停车场景。
3.4 车牌裁剪与透视校正细节
拿到检测框后,第一件事是从原图里把车牌区域裁出来。这里要留意一个细节:检测框是轴对齐矩形,而车牌本身在画面里往往带有透视角度。直接按矩形裁剪会带入大块背景,OCR 看到背景后容易产生误识别。
我的做法是先判断车牌区域是否需要校正。简单场景,比如车牌水平角度偏差在 5 度以内,不影响 OCR,就不用校正;角度稍大时,可以用 OpenCV 的minAreaRect或者通过检测框四点和目标矩形做透视变换。代码层面大概是这样:
import cv2 import numpy as np def crop_plate(image, box): # box: [x1, y1, x2, y2] x1, y1, x2, y2 = map(int, box) margin = 5 x1 = max(0, x1 - margin) y1 = max(0, y1 - margin) x2 = min(image.shape[1], x2 + margin) y2 = min(image.shape[0], y2 + margin) return image[y1:y2, x1:x2]如果要做透视校正,流程会多两步:先拿到车牌区域的四个顶点,然后计算变换矩阵,最后warpPerspective。我之前尝试过用检测框的四边形顶点做校正,但 YOLO 默认输出的是轴对齐框,没有顶点信息。实际工程中我一般用轮廓分析来找车牌的四个角点:对裁剪图做边缘检测、二值化、轮廓查找、多边形逼近,通常能找到车牌的矩形轮廓。这一套操作听起来复杂,但跑起来很快,单张图也就是几毫秒的事。不过要提醒的是,轮廓校正并非万能,车牌被挡了一角或者出现反光时,轮廓会找歪,这时反而比不校正更糟。所以我的建议是:批量测试时先不加校正,等数据证明加了有稳定提升再配置上。
4. 车牌字符识别:RapidOCR 的接入与调优
4.1 为什么选择 RapidOCR 而不是 PaddleOCR
RapidOCR 的本质是把百度 PaddleOCR 训练出来的模型转换成了 ONNX 格式,然后用 ONNX Runtime 做推理。这意味着不需要安装 PaddlePaddle 这个重框架,也能获得接近 PaddleOCR 的识别精度。在我实际的部署场景里,这一点价值太大了:现场服务器经常是未联网、无 GPU 的 Windows 机器,PaddlePaddle 装上之后动不动缺 DLL、版本冲突,RapidOCR 只要一个 onnxruntime 就能跑。
对比 Tesseract 的话,RapidOCR 在中文和数字混合场景下的优势更明显。Tesseract 对印刷体英文识别不错,但车牌里的中文字符、特殊字体、低分辨率场景下,识别率差强人意。RapidOCR 自带的识别模型是在大规模中文语料上训练过的,通用性更强。而且 RapidOCR 绑定的是深度学习模型,对低光照、模糊图像的鲁棒性比传统 OCR 引擎好不少。
还有一个实际优势是 RapidOCR 支持三个子任务:文本检测(det)、方向分类(cls)、文本识别(rec)。虽然我们只识别车牌小图,文本检测依然会先运行,把图片里的文字区域圈出来再识别。对车牌这种文字排列规整的图片,这个检测步骤不会拖后腿,反而能自动跳过无文字区域。
4.2 关键参数与识别质量提升
RapidOCR 的初始化非常简单:
from rapidocr_onnxruntime import RapidOCR ocr = RapidOCR()默认参数下它已经能跑,但有几个参数我建议按需调整。use_angle_cls控制是否启用方向分类模型,默认是 True。方向分类对倾斜文本有用,但会额外增加一点推理时间。对车牌来说,前面如果已经做了透视校正,车牌文字基本是水平的,这时候可以关掉方向分类来提速。如果没能做校正,建议保持打开。
处理速度上还要留意输入图片的尺寸。RapidOCR 内部会把长边缩放到一个合适范围,但如果你的车牌裁剪图本身就特别大,比如上千像素宽,会让 OCR 的处理时间翻倍。我通常会在送入 OCR 之前把裁剪图的宽度缩放到 320 像素左右,高度等比例缩放,这样识别速度会快很多,而且精度几乎不受影响。车牌字符本身就比较大,320 像素宽度足够识别。
CPU 占用率是另一个容易被忽视的问题。RapidOCR 的 onnxruntime 版本默认会使用多个线程,在无 GPU 的机器上并行推理时,CPU 会直接被拉满,导致整机卡顿,尤其是车牌识别和视频解码同时跑的时候。可以通过intra_op_num_threads限制线程数:
from rapidocr_onnxruntime import RapidOCR ocr = RapidOCR(intra_op_num_threads=2)或者直接修改config.ini中的线程配置。我在现场部署时一般限制为 2 到 4 个线程,识别速度受影响不大,但整机稳定性提升非常明显。
4.3 车牌字符后处理与规则约束
OCR 输出的原始文本不能直接当结果用,必须过一个规则校验层。车牌有严格的格式约束,比如蓝牌是“省份汉字 + 字母 + 5位字符”,新能源绿牌是“省份汉字 + 字母 + 6位字符”。OCR 经常会混淆形态相近的字符,比如“O”和“0”、“I”和“1”、“B”和“8”,这时候就需要靠规则来兜底。
我的后处理逻辑分三步。第一步,从 OCR 输出的所有文本候选中选长度匹配的那一条,通常车牌识别结果为 7 位或 8 位,筛选时按这个长度过滤。第二步,对每个位置的字符做合法性校验,第一位必须是汉字,第二位必须是字母,其余位置必须是字母或数字,不符合的直接剔除或替换。第三步,建立混淆字符映射表,把常见的误识别结果纠正过来。比如 OCR 把数字“0”识别成字母“O”时,在车牌规则下“O”不是合法字符,就把它改回“0”。
为了提升中文字符的识别准确率,我还会额外做一个省份汉字白名单。车牌第一位只可能是 34 个省级行政区简称,比如京、津、冀、晋、蒙、辽、吉、黑、沪、苏、浙、皖、闽、赣、鲁、豫、鄂、湘、粤、桂、琼、渝、川、贵、云、藏、陕、甘、青、宁、新、港、澳、台。OCR 识别出的第一个中文字符如果不在白名单里,我就在白名单里找一个字形最接近的候选做替换。这个技巧看着简单,但对实际准确率的提升非常显著,有些原本只有 80% 准确率的场景能拉到 95% 以上。
5. 完整推理流程与代码实现
5.1 端到端推理代码解析
把前面所有环节串起来,核心推理代码其实不长。我先给一版完整的:
import cv2 from ultralytics import YOLO from rapidocr_onnxruntime import RapidOCR # 初始化模型 det_model = YOLO("best.pt") ocr = RapidOCR(intra_op_num_threads=4) # 省份汉字白名单 PROVINCE_SET = "京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼" # 混淆字符映射 CHAR_MAP = {"O": "0", "I": "1", "Z": "2", "S": "5", "B": "8"} def recognize_plate(image): results = det_model(image, conf=0.25, iou=0.7) if len(results[0].boxes) == 0: return None # 取置信度最高的检测框 box = results[0].boxes.xyxy[0].cpu().numpy() crop = crop_plate(image, box) # 缩放车牌区域,控制 OCR 处理时长 h, w = crop.shape[:2] target_w = 320 scale = target_w / w resized = cv2.resize(crop, (target_w, int(h * scale))) # OCR 识别 ocr_result, _ = ocr(resized) if not ocr_result: return None raw_text = ocr_result[0][1] return postprocess(raw_text)postprocess函数做的是规则清洗:
def postprocess(text): text = text.replace(" ", "").replace("·", "") text = "".join([CHAR_MAP.get(c, c) for c in text]) if len(text) < 7: return None first, rest = text[0], text[1:] if first not in PROVINCE_SET: # 尝试把常见误识别的汉字映射回正确省份 first = fix_province_char(first) if first not in PROVINCE_SET: return None plate = first + rest[:6] return plate这套代码直接扔进一个摄像头循环里就能用,单帧检测加识别的总耗时在 CPU 上大约 150 到 250 毫秒,在 GPU 上能压到 50 毫秒以内。当然这是不加队列优化的裸跑速度,工程部署时还需要加异步队列和结果缓存。
5.2 性能优化与部署建议
实际项目里没人会一张一张地同步推理,那样太浪费算力。我常用的是多线程加队列的方案:主线程读视频帧,做检测,检测到车牌后把裁剪图丢进队列;识别线程从队列取图,跑 OCR,返回结果。这样即使 OCR 偶尔慢一点,也不会阻塞主流程的画面采集和设备响应。
另一个实用技巧是检测框追踪。连续视频帧中同一辆车的车牌位置变化很小,不需要每帧都跑检测和识别。我维护了一个简单的车牌结果缓存,记录最近 20 帧内识别过的车牌位置和结果,新帧的检测框和缓存框的 IoU 超过 0.8 就直接复用之前的识别结果,只在检测框变化较大时才重新触发 OCR。这个方法能把 CPU 占用再降一半,对实时性场景帮助很大。
如果是边缘设备部署,比如 RK3588、Jetson Orin 之类的板子,YOLO11 可以导出为 ONNX 或 TensorRT 格式,配合 INT8 量化,推理速度还能提升 3 到 5 倍。RapidOCR 本身也是 ONNX 模型,同样能走量化通道。我通常的做法是先用原模型在开发机上验证逻辑,确认没问题后导出量化版本部署到设备上,两边的输出结果要批量对比一遍,确保量化带来的精度损失在可接受范围内。
6. 常见问题与排查技巧实录
6.1 检测框偏移和漏检怎么解决
检测框偏移是车牌识别里最常见的坑,框的位置差一点点,裁出来的图就会混入大量背景,OCR 识别率直线下降。我排查这类问题有一个固定套路:先把检测框可视化到原图上,逐张看框和真实车牌的贴合情况。如果所有框都系统性偏左上,那大概率是训练数据里标注框中心点有偏差,检查一下标注坐标是否正确;如果只是部分图片偏移多,看看是不是那些图片中车牌本身被车辆其他部件遮挡。
漏检的常见原因有三个:车牌太小、光照太强(反光导致车牌区域发白)、检测目标密度太高。车牌太小时,把输入分辨率从 640 提升到 960 或 1280 是最直接的解法,代价是推理时间变长;反光问题可以在训练数据里多加入带强光斑的样本,或者在前处理阶段做直方图均衡化;密集场景则优先调 NMS 参数。
如果这些都不奏效,我会检查训练数据中车牌尺寸的分布。YOLO 对目标尺寸的分布比较敏感,如果训练集里车牌普遍占画面 10% 以上,而实际部署时车牌只占 2%,小目标漏检几乎是必然。解决办法有两个方向:一是采集更多远距离样本,让训练分布逼近真实分布;二是引入 Mosaic 增强和 Copy-Paste 增强,模拟不同尺寸的车牌。YOLO11 内置了多种增强方式,开箱即用,不用自己写增强代码。
6.2 RapidOCR 推理卡顿和 CPU 占用过高
很早之前我给客户现场部署时遇到过一个问题:软件一跑起来,整个工控机界面都卡住,鼠标都挪不动。查了一圈发现就是 RapidOCR 的 onnxruntime 线程数失控导致的。默认情况下 onnxruntime 会把线程数拉满到 CPU 核心数,如果你在视频流里每帧都调用 OCR,CPU 资源就被瞬间吃光。
解决方法是控制intra_op_num_threads参数,同时限制 OCR 调用频率。如果场景不是实时监控而是车辆通行抓拍,那 OCR 的触发本来就低频,不需要太担心;如果是连续视频流识别,强烈建议加上检测框追踪和结果缓存,没有显著变化就不重新 OCR。在线程数上,我一般设置为核心数的一半,比如 8 核 CPU 就写 4。
还遇到过一个小坑:RapidOCR 在 Windows 下首次执行时模型加载很慢,大概需要 1 到 2 秒的初始化时间。这个不是报错,是正常的模型加载过程,不要在代码热路径里反复初始化 OCR 实例。我的做法是在程序启动时初始化一个全局 OCR 对象,后续所有识别调用都复用这个实例,线程安全问题则用threading.Lock或者独立队列来规避。
6.3 OCR 识别结果乱码和字符误判
OCR 把“鲁”识别成“鱼”、“粤”识别成“奥”这类错误,基本每个做车牌识别的人都会遇到。原因不复杂:通用 OCR 模型虽然认识汉字,但在车牌这种小字号、高压缩比的图像上,省字的笔画细节丢失严重。我的应对策略就是前面说的省份白名单替换法。具体做法是从 OCR 的返回结果里拿到每个字符的置信度,如果第一个字符置信度低于 0.7 且不在白名单里,就强制走映射表。
字符误判方面,“0”和“O”的混淆是最经典的。这需要结合车牌位置的规则来判断:第二位本来是字母,如果 OCR 输出“0”,这时候应该优先换成“O”;而后面数字位如果输出“O”,就应该换成“0”。这就是所谓的“位置感知替换”。我代码里的CHAR_MAP是一个全局映射,但实际更好的做法是根据字符位置分别设置映射表,第二位用字母优先映射,其余位用数字优先映射。
另外我建议对所有识别结果做一次概率评分。RapidOCR 会返回每个识别文本的置信度,如果整体置信度低于 0.5,基本可以判定这张图质量太差,可以选择重试或者直接丢弃,不要让低质量结果混入最终数据。这个置信度阈值在工程上是个非常好的兜底条件,能避免下游业务被奇怪的车牌数据污染。
写在最后的一点体会
这套 YOLO11 + RapidOCR 的方案我已经在三个不同的车牌识别项目里跑通了,从停车场出入口到高速公路匝道场景都有覆盖。整套流程里最难的不是训练模型,也不是写代码,而是把检测、OCR、后处理、性能优化这些环节串成一个稳定运转的闭环。RapidOCR 给我留下的印象尤其深,它在轻量部署和识别效果之间做得确实不错,省掉了 PaddlePaddle 那套重依赖后,现场交付的压力小了很多。如果你也在做类似的车牌识别场景,建议先按我给的流程搭一个最小可用版本,跑通之后再慢慢优化数据、调参数、加规则。最后忍不住提醒一句:车牌数据的采集合规性很重要,训练和测试图片一定要来自你确实有权限使用的场景,别随便抓第三方数据就往模型里喂,这既是技术习惯也是职业底线。