從零實作中文車牌識別:YOLOv8 + LPRNet 訓練、ONNX 部署及雲端 API 接入實踐
从零实现中文车牌识别:YOLOv8 + LPRNet 训练、ONNX 部署与云端 API 接入实践
文章以開源 YOLOv8 和 LPRNet 從零實作中文車牌檢測與識別流程,涵蓋 CCPD 數據集處理、模型訓練及 ONNX 推理部署。流程以 YOLOv8n 檢測車牌、LPRNet 配合 CTC 貪心解碼識別字符,並以 HSV 統計粗判車牌底色。文章亦比較自建模型與雲市場 API 的落地方式及適用場景。
简介: 本文基于开源的 YOLOv8 和 LPRNet,完整实现一套中文车牌检测与识别流程,内容包括 CCPD 数据集处理、检测模型训练、CTC 字符识别原理、ONNX 推理部署和车牌颜色判断,并对比了自建模型与调用云市场 API 两种落地方式,给出各自适用的场景。文中代码均可直接运行。
一、背景
车牌识别(LPR,License Plate Recognition)是计算机视觉中落地最成熟的方向之一,常见于以下场景:
- 智慧停车:道闸抬杆、无感支付、车位管理;
- 交通安防:卡口抓拍、违停识别、车辆轨迹分析;
- 车险与车务:投保录单、事故查勘、年检办理,需要从现场照片或 PDF 资料中提取车牌号。
笔者之前在车险录单项目里接入过蓝兴AI的车牌号识别接口,这次想自己从头实现一遍,弄清楚背后的原理,也顺便对比一下自建模型和成熟服务的差距。
从技术上看,一个完整的车牌识别系统通常拆成检测和识别两个阶段。本文选用业界最常见、资料最丰富的组合:
| 阶段 | 模型 | 选型理由 |
|---|---|---|
| 车牌检测 | YOLOv8n | 轻量、速度快,目标单一时精度足够,生态成熟 |
| 字符识别 | LPRNet | 无需字符分割,CTC 端到端训练,模型仅几百 KB |
| 颜色判断 | HSV 统计 | 实现简单,作为基线方案够用 |
二、整体架构

处理流程如下:
- 输入图片经 letterbox 缩放到 640×640,送入 YOLOv8n 得到车牌外接框;
- 按外接框裁剪车牌区域,缩放到 LPRNet 的输入尺寸 94×24;
- LPRNet 输出字符概率序列,经 CTC 贪心解码得到车牌号;
- 后处理:格式校验,并用 HSV 统计粗判车牌底色。
各阶段的中间结果示意如下:

三、环境准备
| 依赖 | 版本 |
|---|---|
| Python | 3.10 |
| torch | 2.1.0 |
| ultralytics | 8.2.x |
| onnxruntime | 1.17.0 |
| opencv-python | 4.9.0 |
pip install ultralytics onnxruntime opencv-python numpy
训练建议使用 GPU 环境(如阿里云 GPU 云服务器或 PAI-DSW 交互式建模),推理阶段 CPU 即可满足需求。
四、数据集处理
4.1 CCPD 数据集简介
CCPD(Chinese City Parking Dataset)是目前使用最广泛的中文车牌开源数据集:
- CCPD2019:约 30 万张蓝牌图片,包含正常、倾斜、模糊、光照变化等子集;
- CCPD2020:约 1.1 万张新能源绿牌图片。
它的特点是标注信息直接写在文件名里,例如:
025-95_113-154&383_386&473-386&473_177&454_154&383_363&402-0_0_22_27_27_33_16-37-15.jpg
按 - 分隔后,第 3 段为外接框坐标(左上 & 右下),第 5 段为车牌字符编码。
4.2 转换为 YOLO 检测格式
import os
import cv2
def ccpd_to_yolo(img_dir, label_dir):
"""CCPD 文件名 -> YOLO txt:cls cx cy w h(归一化)"""
os.makedirs(label_dir, exist_ok=True)
for name in os.listdir(img_dir):
if not name.endswith(".jpg"):
continue
box = name.split("-")[2]
(x1, y1), (x2, y2) = [map(int, p.split("&")) for p in box.split("_")]
img = cv2.imread(os.path.join(img_dir, name))
if img is None:
continue
h, w = img.shape[:2]
cx, cy = (x1 + x2) / 2 / w, (y1 + y2) / 2 / h
bw, bh = (x2 - x1) / w, (y2 - y1) / h
with open(os.path.join(label_dir, name[:-4] + ".txt"), "w") as f:
f.write(f"0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n")
4.3 解析车牌号(用于训练识别模型)
PROVINCES = ["皖", "沪", "津", "渝", "冀", "晋", "蒙", "辽", "吉", "黑", "苏", "浙", "京", "闽", "赣", "鲁",
"豫", "鄂", "湘", "粤", "桂", "琼", "川", "贵", "云", "藏", "陕", "甘", "青", "宁", "新"]
ALPHABETS = list("ABCDEFGHJKLMNPQRSTUVWXYZ")
ADS = list("ABCDEFGHJKLMNPQRSTUVWXYZ0123456789")
def ccpd_plate_text(name):
idx = list(map(int, name.split("-")[4].split("_")))
return PROVINCES[idx[0]] + ALPHABETS[idx[1]] + "".join(ADS[i] for i in idx[2:])
4.4 数据集的局限
CCPD 适合入门,但直接用于生产会遇到两个问题:
- 省份分布极不均衡:绝大多数样本为"皖"牌,训练出的模型对其他省份简称识别能力弱;
- 场景单一:以停车场正面拍摄为主,缺少黄牌双层、白牌、港澳牌,以及手机翻拍、扫描件等图片。
建议补充自有场景数据,或通过字体渲染合成其他省份车牌进行增强。
五、模型训练
5.1 检测模型
数据配置 plate.yaml:
path: datasets/plate
train: images/train
val: images/val
names:
0: plate
训练与导出:
yolo detect train data=plate.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=32
yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640
训练完成后,runs/detect/train/ 目录下会生成 results.png、PR_curve.png 和混淆矩阵等评估图表。
5.2 识别模型
LPRNet 的论文与 PyTorch 实现均已开源,核心要点有三个:
- 输入为固定尺寸的车牌图片(常见实现为 94×24);
- 网络输出长度为 T 的序列,每个位置是对全部字符(含空白符)的打分;
- 使用
nn.CTCLoss训练,推理时采用贪心解码,即"去掉空白、合并相邻重复"。
def ctc_greedy_decode(indices, charset, blank):
out, prev = [], blank
for i in indices:
if i != blank and i != prev:
out.append(charset[i])
prev = i
return "".join(out)
识别字符集如下,注意国标车牌不使用字母 I 和 O:
京沪津渝冀晋蒙辽吉黑苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新
0123456789
ABCDEFGHJKLMNPQRSTUVWXYZ
学警港澳挂使领
数据增强建议:随机亮度对比度、高斯模糊、轻微旋转和透视、随机裁剪边缘。对真实场景而言,增强带来的收益通常大于加深网络。
六、ONNX 推理部署
以下脚本仅依赖 onnxruntime 和 opencv,可直接部署在 ECS 或函数计算 FC 等环境中。检测部分为 YOLOv8 导出的标准 ONNX(输出 [1, 5, 8400],需自行 NMS);识别部分以常见的 LPRNet 实现为例,字符集和预处理需与训练时保持一致。
import cv2
import numpy as np
import onnxruntime as ort
CHARS = list("京沪津渝冀晋蒙辽吉黑苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新"
"0123456789ABCDEFGHJKLMNPQRSTUVWXYZ学警港澳挂使领") + ["-"] # 最后一个为空白符
BLANK = len(CHARS) - 1
class SimpleLPR:
def __init__(self, det_path, rec_path, conf=0.4, iou=0.5):
self.det = ort.InferenceSession(det_path, providers=["CPUExecutionProvider"])
self.rec = ort.InferenceSession(rec_path, providers=["CPUExecutionProvider"])
self.conf, self.iou = conf, iou
def detect(self, img, size=640):
h, w = img.shape[:2]
r = min(size / h, size / w)
nh, nw = int(h * r), int(w * r)
canvas = np.full((size, size, 3), 114, np.uint8)
canvas[:nh, :nw] = cv2.resize(img, (nw, nh))
x = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB).transpose(2, 0, 1)[None].astype(np.float32) / 255
pred = self.det.run(None, {
self.det.get_inputs()[0].name: x})[0][0].T # [8400, 5]
pred = pred[pred[:, 4] > self.conf]
if len(pred) == 0:
return []
boxes = np.stack([pred[:, 0] - pred[:, 2] / 2, pred[:, 1] - pred[:, 3] / 2,
pred[:, 2], pred[:, 3]], 1) / r
keep = cv2.dnn.NMSBoxes(boxes.tolist(), pred[:, 4].tolist(), self.conf, self.iou)
res = []
for i in np.array(keep).flatten():
x0, y0, bw, bh = boxes[i]
res.append((max(int(x0), 0), max(int(y0), 0), int(x0 + bw), int(y0 + bh), float(pred[i, 4])))
return res
def recognize(self, crop):
x = cv2.resize(crop, (94, 24)).astype(np.float32)
x = ((x - 127.5) * 0.0078125).transpose(2, 0, 1)[None]
logits = self.rec.run(None, {
self.rec.get_inputs()[0].name: x})[0][0] # [num_chars, T]
idx = logits.argmax(0)
text, prev = "", BLANK
for i in idx:
if i != BLANK and i != prev:
text += CHARS[i]
prev = i
return text
@staticmethod
def guess_color(crop):
"""HSV 粗判车牌底色"""
hsv = cv2.cvtColor(crop, cv2.COLOR_BGR2HSV)
h, s, v = [c.mean() for c in cv2.split(hsv)]
if v < 60:
return "black"
if s < 50:
return "white"
if 100 <= h <= 130:
return "blue"
if 15 <= h <= 35:
return "yellow"
if 35 < h < 100:
return "green"
return "unknown"
def __call__(self, img):
out = []
for x1, y1, x2, y2, score in self.detect(img):
crop = img[y1:y2, x1:x2]
if crop.size == 0:
continue
out.append({
"plateNumber": self.recognize(crop),
"plateColor": self.guess_color(crop),
"box": [x1, y1, x2, y2], "detScore": round(score, 3)})
return out
if __name__ == "__main__":
lpr = SimpleLPR("models/plate_det_yolov8n.onnx", "models/lprnet.onnx")
image = cv2.imdecode(np.fromfile("test.jpg", dtype=np.uint8), cv2.IMREAD_COLOR) # 兼容中文路径
for r in lpr(image):
print(r)
输出示例:
{'plateNumber': '皖AD12345', 'plateColor': 'green', 'box': [154, 383, 386, 473], 'detScore': 0.93}
七、效果评估
7.1 检测指标
| 指标 | 含义 |
|---|---|
| P(Precision) | 预测为车牌的框中,真正是车牌的比例 |
| R(Recall) | 真实车牌中被检出的比例,车牌场景下漏检代价更高,需重点关注 |
| mAP50 | IoU 阈值 0.5 下的平均精度 |
| mAP50-95 | IoU 从 0.5 到 0.95 的平均精度,反映定位精细程度 |
7.2 识别指标
- 整牌准确率:整串号码完全一致才算正确;
- 字符准确率:按字符统计,便于定位是省份、字母还是数字错误较多;
- 分组统计:建议按省份、车牌颜色分别统计,避免总体准确率掩盖长尾类别的问题。
7.3 真实场景对比
在 CCPD 测试集上,检测 mAP50 很容易达到 0.99,但换成手机拍摄的真实图片后效果明显下降。笔者选取了一批夜间、逆光、斜拍、翻拍屏幕的样张,与蓝兴AI车牌号识别的结果做对比(其官网提供网页在线体验,每天可免费识别 10 次,支持图片和 PDF),以此定位自建模型的薄弱场景。对比下来,自建模型在 CCPD 风格的停车场图片上差距不大,但在翻拍屏幕、PDF 截图和双层黄牌上差距明显,这也印证了前面说的:真正的难点在数据覆盖。
八、常见问题
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 其他省份简称识别错误 | 训练数据以"皖"牌为主 | 补充其他省份数据或合成数据 |
| 斜拍车牌丢字 | 外接框裁剪带入背景、字符变形 | 增加旋转/透视增强,适当外扩裁剪框 |
| 双层黄牌识别乱码 | LPRNet 默认按单行设计 | 对双层牌单独处理或单独训练 |
| 夜间、反光图片漏检 | 训练集缺少此类场景 | 补充数据,增加亮度和模糊增强 |
| 颜色判断不准 | HSV 阈值受光照影响较大 | 改用小型分类网络判断颜色 |
九、落地方式选择:自建模型 vs 云市场 API
| 对比项 | 自建模型 | 调用云市场 API |
|---|---|---|
| 前期投入 | 数据收集、标注、训练、部署,周期较长 | 开通即用,当天可接入 |
| 场景覆盖 | 取决于自有数据 | 通常已覆盖多种车牌类型 |
| 成本结构 | GPU 训练 + 服务器运维 | 按次计费 |
| 数据合规 | 数据完全自主可控 | 需评估,部分服务支持私有化部署 |
| 适合场景 | 有算法团队、调用量大、场景特殊 | 业务快速上线、调用量中等 |
对于希望快速上线的业务,可以直接在阿里云云市场搜索"车牌号识别",选择按次计费的 API 服务。以笔者用过的蓝兴AI车牌号识别为例,它在阿里云云市场上架,支持蓝、绿、黄、白和港澳黑牌,输入支持图片 URL、Base64 和 PDF,返回车牌号码、颜色和单双层信息,调用方式与云市场其他 API 一致:
import base64
import os
import requests
API_URL = os.environ["PLATE_API_URL"] # 接口地址见云市场商品页
auth = os.environ["PLATE_API_AUTH"] # 鉴权方式见云市场商品页,请勿写入代码
with open("car.jpg", "rb") as f:
img_b64 = base64.b64encode(f.read()).decode()
resp = requests.post(
API_URL,
headers={
"Authorization": auth},
data={
"image": img_b64},
timeout=10,
)
print(resp.json())
本文自建模型的输出字段也按 plateNumber / plateColor 命名,与此类接口保持一致,后续在两种方式之间切换时只需替换调用层。
十、总结
本文基于 YOLOv8 + LPRNet 完成了中文车牌识别的完整链路,主要内容包括:
- 使用 CCPD 数据集,从文件名解析检测框和车牌号;
- YOLOv8n 训练车牌检测模型并导出 ONNX;
- LPRNet + CTC 实现免分割的字符识别;
- 基于 onnxruntime 的纯 CPU 推理脚本,便于部署到 ECS 或函数计算;
- 自建模型与云市场 API 两种落地方式的对比。
从"开源数据集上 99%"到"真实业务可用",主要差距在于数据覆盖和边角场景处理。建议先用开源方案理解原理,再根据业务量和合规要求选择自建,或者接入蓝兴AI这类已经覆盖多种车牌类型、支持私有化部署的云服务。
参考资料
- CCPD 数据集(GitHub:detectRecog/CCPD)
- Ultralytics YOLOv8 官方文档
- LPRNet 论文:LPRNet: License Plate Recognition via Deep Neural Networks(arXiv:1806.10447)
- ONNX Runtime 官方文档
- 蓝兴AI 车牌号识别产品说明
來源:阿里雲開發者社區 AI · developer.aliyun.com