简介:这份资源是面向高校学生与深度学习初学者的车牌识别系统完整项目包,适用于毕业设计、课程设计及期末大作业等场景,帮助读者将卷积神经网络与OCR技术落地到真实图像识别任务中。包内共1177个文件,以Python脚本、Markdown文档、JPG示例图、TXT说明、YAML配置为主,另含少量C++、Rust、Jupyter笔记本及模型权重文件,压缩包约39.43MB,覆盖数据准备、模型训练、验证评估与预测推理全流程。系统以yolov11n完成车牌区域定位,再借助paddleocr实现文字识别,训练、验证与预测脚本分工明确,示例图片可辅助理解运行流程。目前已有85人学习下载,读者可据此掌握从数据预处理到模型部署的完整开发链路,积累目标检测与OCR结合的工程经验,并作为进一步优化识别精度与响应速度的实验平台。
1. 车牌识别系统选型:为什么是 yolov11n 加 paddleocr 这套组合
车牌识别这件事,单靠一个模型很难端到端搞定。真实场景里,一张卡口图里车牌可能只占几十个像素,还伴随逆光、雨雾、倾斜、遮挡,直接上 OCR 大模型去读整图,准确率和速度都撑不住。所以工程上更稳的做法是拆成两段:先用目标检测把车牌框出来,再对裁剪出来的小图做文字识别。这套「检测 + 识别」的流水线,就是 yolov11n_paddleocr 车牌识别系统的核心思路。
yolov11n 是 YOLO 系列里偏轻量的 n 版本,参数量小、推理快,适合部署在边缘设备或者单卡服务器上跑实时视频流;paddleocr 负责把检测框里的车牌字符读出来,它对中文车牌、双层黄牌、新能源绿牌的字符集支持比较完整,而且自带方向分类器,能处理 180 度倒置的车牌。两者拼起来,一个管「在哪」,一个管「是什么」,职责清晰,替换和调优都方便。
这套方案适合谁?做智慧停车、园区卡口、交通违章抓拍、车辆出入管理的团队,尤其是预算有限、要在 Jetson 或普通 GPU 上跑实时推理的场景。如果你手上只有几百到几千张标注图,又不想从零训一个端到端大模型,这条路线落地周期短、可控性强。下面按数据准备、检测训练、识别对接、避坑、进阶的顺序,把每一步的参数和坑讲清楚。
2. 数据准备与标注:让 yolov11n 学得动的车牌数据集怎么造
2.1 车牌检测数据集的组织格式
yolov11n 用的是 YOLO 格式标注,每张图对应一个同名 txt,每行是class x_center y_center width height,坐标都归一化到 0 到 1。车牌检测通常只有一个类别,class 写 0 就行。目录结构按 ultralytics 的约定来:
dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml里写清楚路径和类别名:
path: /home/user/dataset train: images/train val: images/val nc: 1 names: ['plate']这里nc是类别数,车牌检测只检「车牌」这一类,所以是 1。names的顺序必须和标注里的 class 编号一致,写反了模型会把车牌当背景学。路径建议用绝对路径,相对路径在不同工作目录下跑训练容易翻车。
2.2 标注的四个实操要点
标注工具用 labelImg 或 X-AnyLabeling 都行,导出 YOLO 格式。标注时注意几点:框要贴紧车牌四角,不要留太多背景,否则 OCR 阶段裁出来的图会带干扰;双层车牌(黄牌大车)要把上下两层整体框住,不要只框一层;倾斜车牌按最小外接矩形框,别硬拉成水平框,YOLO 的框是轴对齐的,倾斜太厉害时框会包含大量背景,这种情况要么增加倾斜样本,要么在预处理阶段做矫正。
数据量上,单类别车牌检测,train 集 2000 到 5000 张基本能出一个可用的模型,val 集留 10% 到 15%。如果场景固定(比如就一个停车场出口),500 张左右也能收敛,但泛化会差。样本要覆盖白天、夜间、逆光、雨雾、不同角度,尤其是夜间和逆光,这两类不专门补样本,模型上线后大概率在这些场景集体翻车。
2.3 用脚本检查标注是否合法
标注完别急着训,先跑个校验脚本,把越界坐标、空标注、图片和标签不匹配的问题揪出来:
import os from pathlib import Path img_dir = Path("dataset/images/train") lbl_dir = Path("dataset/labels/train") for lbl in lbl_dir.glob("*.txt"): img = img_dir / (lbl.stem + ".jpg") if not img.exists(): print(f"缺图: {lbl.name}") continue with open(lbl) as f: for i, line in enumerate(f): parts = line.strip().split() if len(parts) != 5: print(f"{lbl.name} 第{i}行字段数不对: {line}") continue cls, x, y, w, h = parts vals = list(map(float, [x, y, w, h])) if any(v < 0 or v > 1 for v in vals): print(f"{lbl.name} 第{i}行坐标越界: {line}") if float(w) <= 0 or float(h) <= 0: print(f"{lbl.name} 第{i}行宽高非正: {line}")这段脚本做三件事:检查每张标签有没有对应图片、每行是不是 5 个字段、归一化坐标是否在 0 到 1 之间且宽高为正。越界坐标在训练时会被 ultralytics 静默裁剪,导致框变形,模型学到的位置就偏了,所以必须在训练前清掉。宽高为 0 的框会让 loss 计算出 NaN,训练直接崩。
3. 训练 yolov11n 检测模型:参数怎么设、指标怎么看
3.1 从预训练权重起步的最小训练命令
不要从随机初始化训,用 COCO 预训练的 yolov11n 权重做迁移学习,收敛快很多。ultralytics 的命令行方式最省事:
yolo detect train \ model=yolo11n.pt \ data=dataset/data.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ device=0 \ project=runs/plate \ name=yolo11n_plate逐项说下参数。model=yolo11n.pt是加载预训练权重,第一次跑会自动下载。imgsz=640是输入分辨率,车牌在整图里偏小的话可以提到 960 或 1280,但显存和速度会涨。batch=16按显存调,8G 显存跑 640 大概能到 16,跑 1280 就得降到 4 或 8。lr0=0.01是初始学习率,迁移学习常用 0.01,如果 loss 震荡厉害降到 0.001。patience=20是早停,20 轮指标不涨就停,省时间。device=0指定第一块 GPU,CPU 训练把 device 设成 cpu,但速度会慢到没法接受。
3.2 训练过程要盯的三个指标
训练日志里重点看box_loss、cls_loss和验证集的mAP50。box_loss 管框的位置回归,cls_loss 管分类,车牌单类别时 cls_loss 会降得很快。mAP50 是 IoU 阈值 0.5 下的平均精度,车牌检测一般能到 0.95 以上算不错。如果 mAP50 卡在 0.7 左右上不去,先查标注质量,再看是不是小目标太多——车牌在 640 分辨率下小于 16 像素的话,模型很难学好,这时候要么提高 imgsz,要么在数据增强里加 mosaic 和 copy_paste。
验证时用命令行跑一批图看效果:
yolo detect predict \ model=runs/plate/yolo11n_plate/weights/best.pt \ source=test_images/ \ conf=0.25 \ save=Trueconf=0.25是置信度阈值,低于这个值的框不输出。车牌检测可以适当调低到 0.2,宁可多检几个框让 OCR 去过滤,也别漏检。save=True把画了框的结果存下来,肉眼过一遍,重点看夜间和逆光样本有没有漏。
3.3 导出推理格式
训练完的 best.pt 是 PyTorch 权重,部署时按目标平台导出。GPU 服务器上跑 TensorRT 最快,边缘设备用 ONNX 或 OpenVINO:
yolo export model=best.pt format=onnx opset=12 simplify=True yolo export model=best.pt format=engine half=True device=0opset=12是 ONNX 算子集版本,兼容性比较好。simplify=True会做图优化,去掉冗余算子。TensorRT 导出加half=True用 FP16,速度能再提一截,精度掉得很少。注意导出 engine 时的 GPU 型号要和部署机器一致,换卡要重新导出,这个坑很多人踩过。
4. 对接 paddleocr:从检测框到车牌字符串的完整链路
4.1 裁剪、矫正、识别的三步流程
yolov11n 输出的是车牌框坐标,paddleocr 吃的是裁剪后的小图。中间要做裁剪和可选的透视矫正。先装依赖:
pip install paddlepaddle-gpu paddleocr opencv-python如果只用 CPU 推理,装paddlepaddle就行。然后写识别主流程:
import cv2 from paddleocr import PaddleOCR from ultralytics import YOLO det_model = YOLO("best.pt") ocr = PaddleOCR(use_angle_cls=True, lang='ch', show_log=False) def recognize_plate(img_path): img = cv2.imread(img_path) results = det_model(img, conf=0.25, verbose=False) plates = [] for box in results[0].boxes.xyxy.cpu().numpy(): x1, y1, x2, y2 = map(int, box) crop = img[y1:y2, x1:x2] if crop.size == 0: continue ocr_res = ocr.ocr(crop, cls=True) if not ocr_res or not ocr_res[0]: continue text = "".join([line[1][0] for line in ocr_res[0]]) score = min([line[1][1] for line in ocr_res[0]]) plates.append({"text": text, "score": score, "box": [x1, y1, x2, y2]}) return plates逻辑上分三步:YOLO 出框、按框裁图、裁图送 OCR。use_angle_cls=True开启方向分类,处理倒置车牌。lang='ch'用中文模型,能识别汉字省份简称。ocr.ocr返回的结构是「行」的列表,每行含文本和置信度,车牌一般只有一行,多行的情况(双层牌)用 join 拼起来。score取所有行里最低的置信度,作为整块车牌的可信度,低于阈值就丢弃。
4.2 透视矫正:倾斜车牌的后悔药
YOLO 的框是轴对齐的,车牌倾斜时框里会混入大量背景,OCR 容易读错。如果检测阶段能拿到四个角点,可以做透视变换把车牌拉正。ultralytics 默认不输出角点,需要自己加一个关键点分支或者用传统方法找轮廓。简单场景下用最小外接矩形旋转矫正也能救一部分:
import numpy as np def deskew(crop): gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) coords = np.column_stack(np.where(thresh > 0)) if len(coords) < 10: return crop angle = cv2.minAreaRect(coords)[-1] if angle < -45: angle = 90 + angle h, w = crop.shape[:2] M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) return cv2.warpAffine(crop, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)这段用 Otsu 二值化后找前景点,算最小外接矩形的角度,再旋转回正。borderMode=cv2.BORDER_REPLICATE用边缘像素填充旋转后的空白,避免出现黑边干扰 OCR。注意这个方法对背景复杂的图会失效,前景点里混入背景时角度算不准,所以只适合车牌区域比较干净的裁剪图。
4.3 识别结果的清洗规则
OCR 出来的字符串不能直接用,车牌有固定格式,得做规则校验。普通蓝牌是「省份简称 + 字母 + 5 位字母数字」,新能源绿牌是 8 位。写个简单的校验:
import re PROVINCES = "京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新" def clean_plate(text): text = re.sub(r'[^A-Z0-9\u4e00-\u9fa5]', '', text.upper()) if len(text) < 7: return None if text[0] not in PROVINCES: return None if not re.match(r'^[A-Z][A-Z0-9]{5,6}$', text[1:]): return None return text先去掉非字母数字汉字的字符,再检查首位是不是合法省份简称,后面是不是字母开头加 5 到 6 位。\u4e00-\u9fa5是汉字 Unicode 范围。这个规则能过滤掉大部分 OCR 误读,比如把「京」读成「凉」的情况。规则不是万能的,新能源牌和使馆牌格式特殊,按实际业务调整。
5. 避坑与排查:车牌识别上线后最常见的五个问题
5.1 夜间车牌漏检严重
现象是白天检测正常,夜间 mAP 掉到 0.5 以下,大量车牌框不出来。原因是训练集里夜间样本太少,模型没见过低照度下的车牌纹理。解决分两步:一是补夜间样本,至少占训练集 20%;二是在预处理阶段做自适应直方图均衡,提升暗部对比度。用 OpenCV 的 CLAHE:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) lab[:, :, 0] = clahe.apply(lab[:, :, 0]) img = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)在 LAB 空间的 L 通道做均衡,不影响颜色。clipLimit=2.0控制对比度增强幅度,太大噪声会被放大,车牌边缘反而糊。
5.2 OCR 把相似字符读混
现象是「0」和「O」、「1」和「I」、「8」和「B」经常读错。原因是车牌字体和 OCR 训练用的通用字体有差异,加上裁剪图分辨率低。解决办法:一是把裁剪图放大到 OCR 推荐的输入尺寸(一般高度 32 或 48 像素),二是用规则后处理强制替换。车牌里字母 I 和 O 基本不出现(避免和 1、0 混淆),可以在清洗时把 I 换成 1、O 换成 0:
text = text.replace('I', '1').replace('O', '0')这个替换要在省份简称校验之后做,避免把汉字误伤。
5.3 检测框偏大导致 OCR 读入背景
现象是车牌能检出,但 OCR 结果里混入广告字、车身贴纸。原因是标注时框留了太多背景,或者模型回归不准。解决:训练时把框贴紧车牌,推理后对框做内缩,按框宽高的 5% 到 10% 往里收:
pad_x = int((x2 - x1) * 0.05) pad_y = int((y2 - y1) * 0.05) crop = img[y1 + pad_y:y2 - pad_y, x1 + pad_x:x2 - pad_x]内缩能去掉框边缘的背景,但缩太多会切掉车牌字符,5% 到 10% 是经验值,按实际效果调。
5.4 推理速度达不到实时
现象是单帧处理超过 100ms,视频流卡顿。排查顺序:先看 YOLO 推理耗时,再看 OCR 耗时。YOLO 用 TensorRT FP16 一般能到 5ms 以内,OCR 是瓶颈,paddleocr 的检测加识别在 GPU 上大概 20 到 40ms。优化手段:OCR 只跑识别不跑检测(因为车牌已经裁好了),关掉不需要的模块;把 OCR 的det设为 False,直接用识别模型:
ocr = PaddleOCR(use_angle_cls=True, lang='ch', det=False, rec=True)det=False跳过 OCR 自带的文本检测,省一大半时间。另外批处理也能提吞吐,把多块车牌拼成一个 batch 送 OCR。
5.5 换部署机器后精度暴跌
现象是开发机上 mAP 0.95,换到另一台机器掉到 0.6。原因是导出的 TensorRT engine 绑定了特定 GPU 架构和 TensorRT 版本,换卡后不兼容,或者 FP16 在旧卡上精度损失大。解决:部署机器上重新导出 engine,别跨机器拷贝;旧卡(算力低于 7.0)用 FP32 导出,别开 half。ONNX 格式跨平台兼容性好,实在搞不定就用 ONNX Runtime 跑。
6. 进阶技巧:用关键点回归把车牌矫正做到端到端
前面用最小外接矩形矫正倾斜车牌,背景复杂时会失效。更稳的做法是让 yolov11n 在检测的同时回归车牌的四个角点,拿到角点后直接做透视变换,一步到位。ultralytics 支持关键点任务,把数据集标注从框扩展成框加四个角点,格式是class x y w h px1 py1 px2 py2 px3 py3 px4 py4,角点也归一化。训练时把任务设成 pose:
yolo pose train model=yolo11n-pose.pt data=plate_pose.yaml epochs=100 imgsz=640推理时拿到角点,用cv2.getPerspectiveTransform把车牌拉成标准矩形:
def perspective_crop(img, pts): pts = np.array(pts, dtype=np.float32).reshape(4, 2) rect = np.zeros((4, 2), dtype=np.float32) s = pts.sum(axis=1) rect[0] = pts[np.argmin(s)] rect[2] = pts[np.argmax(s)] d = np.diff(pts, axis=1) rect[1] = pts[np.argmin(d)] rect[3] = pts[np.argmax(d)] w = int(max(np.linalg.norm(rect[0] - rect[1]), np.linalg.norm(rect[2] - rect[3]))) h = int(max(np.linalg.norm(rect[0] - rect[3]), np.linalg.norm(rect[1] - rect[2]))) dst = np.array([[0, 0], [w - 1, 0], [w - 1, h - 1], [0, h - 1]], dtype=np.float32) M = cv2.getPerspectiveTransform(rect, dst) return cv2.warpPerspective(img, M, (w, h))角点排序的逻辑是:左上角坐标和最小,右下角和最大,右上角坐标差最小,左下角差最大。透视变换后车牌变成水平矩形,OCR 准确率能明显提升,尤其是大角度倾斜的场景。代价是标注成本高,每张图要标四个角点,数据量需求也更大,几千张起步。如果业务场景里车牌基本都是正对摄像头,用前面的旋转矫正就够了,不必上关键点。
验证整套系统时,我习惯准备一个包含 200 张图的回归测试集,覆盖白天、夜间、逆光、倾斜、双层牌,每次改完模型或参数都跑一遍,记录端到端准确率(检测框 IoU 大于 0.5 且 OCR 字符串完全正确)。这个数字比单看 mAP 更能反映上线效果。踩过的坑里,最亏的一次是没做回归测试就换了 OCR 版本,结果新能源牌识别率掉了 15%,上线后第二天才发现。现在我改任何一环都先跑回归集,宁可多花十分钟,也不赌线上不出事。希望帮到你。
本文还有配套的精品资源,点击获取