本文介绍了使用Yolo26和LPRNet来完成车牌识别的项目,并提供了完整的基于CCPD数据集训练的模型和可视化GUI程序,在本文末尾可直接下载使用。
CCPD数据集介绍可以参考下面这个文章,介绍的非常详细,我就不赘叙了,大概的含义是CCPD2019是油牌车的数据集,CCPD2020是绿牌新能源的车牌,这次训练是把这两者融为了一体进行训练的。
CCPD的github地址是https://github.com/detectRecog/CCPD
2019数据集下载网址: https://pan.baidu.com/s/1i5AOjAbtkwb17Zy-NQGqkw 提取密码: hm0u
2020数据集下载网址: https://pan.baidu.com/s/1JSpc9BZXFlPkXxRK4qUCyw 提取密码: ol3jYolo模型的训练是用了yolo的格式,所以需要把原来的CCPD的数据整理一下。原始数据的格式是直接把标注信息写成了文件名称,类似于0136360677083-95_103-255&434_432&512-432&512_267&494_255&434_424&449-0_0_3_25_30_24_24_32-98-218.jpg这种,记录的非常详细,需要把这个文件名提取的信息存储成txt格式的labels文件。
yolo的存储方式是: <class_id> <x_center> <y_center>
为了模型的训练,需要将原始图像和标注文件放在相应的文件中,格式如下
dataset/
├── images/
│ ├── train/
│ └── val/
└── labels/
├── train/
└── val/所以需要转换一下,下面是我的完整的转换代码:
# -*- coding: utf-8 -*-
"""
Created on Wed Jun 9 18:24:10 2021
@author: luohenyueji
"""
from PIL import Image, ImageDraw, ImageFont
import os
import cv2
import shutil
import random
from tqdm import tqdm
provincelist = [
"皖", "沪", "津", "渝", "冀",
"晋", "蒙", "辽", "吉", "黑",
"苏", "浙", "京", "闽", "赣",
"鲁", "豫", "鄂", "湘", "粤",
"桂", "琼", "川", "贵", "云",
"西", "陕", "甘", "青", "宁",
"新"]
wordlist = [
"A", "B", "C", "D", "E",
"F", "G", "H", "J", "K",
"L", "M", "N", "P", "Q",
"R", "S", "T", "U", "V",
"W", "X", "Y", "Z", "0",
"1", "2", "3", "4", "5",
"6", "7", "8", "9"]
# --- 绘制边界框
def DrawBox(im, box):
draw = ImageDraw.Draw(im)
draw.rectangle([tuple(box[0]), tuple(box[1])], outline="#FFFFFF", width=3)
# --- 绘制四个关键点
def DrawPoint(im, points):
draw = ImageDraw.Draw(im)
for p in points:
center = (p[0], p[1])
radius = 5
right = (center[0]+radius, center[1]+radius)
left = (center[0]-radius, center[1]-radius)
draw.ellipse((left, right), fill="#FF0000")
# --- 绘制车牌
def DrawLabel(im, label):
draw = ImageDraw.Draw(im)
font = ImageFont.truetype('simsun.ttc', 64)
draw.text((30, 30), label, font=font)
# --- 图片可视化
def ImgShow(imgpath, box, points, label):
# 打开图片
im = Image.open(imgpath)
DrawBox(im, box)
DrawPoint(im, points)
DrawLabel(im, label)
# 显示图片
im.show()
im.save('result.jpg')
def create_dataset_structure(base_path="dataset"):
"""创建YOLO数据集目录结构"""
dirs = [
os.path.join(base_path, "images", "train"),
os.path.join(base_path, "images", "val"),
os.path.join(base_path, "labels", "train"),
os.path.join(base_path, "labels", "val")
]
for dir_path in dirs:
os.makedirs(dir_path, exist_ok=True)
print(f"Created directory: {dir_path}")
return base_path
def process_image(imgpath, imgbasepath, output_base="dataset", mode="train"):
"""
处理单张图片并生成YOLO格式的标注文件
Args:
imgpath: 图片文件名
imgbasepath: 原始图片路径
output_base: 输出数据集根目录
mode: 数据集类型 ('train' 或 'val')
"""
try:
# 图像名(不含扩展名)
imgname = os.path.basename(imgpath).split('.')[0]
# 根据图像名分割标注
_, _, box, points, label, brightness, blurriness = imgname.split('-')
# --- 边界框信息
# 对应边界框左上角和右下角坐标
box = box.split('_')
box = [list(map(int, i.split('&'))) for i in box]
# --- 关键点信息
points = points.split('_')
points = [list(map(int, i.split('&'))) for i in points]
# 将关键点的顺序变为从左上顺时针开始
points = points[-2:]+points[:2]
# --- 读取车牌号
label = label.split('_')
# 省份缩写
province = provincelist[int(label[0])]
# 车牌信息
words = [wordlist[int(i)] for i in label[1:]]
# 车牌号
plate_text = province+''.join(words)
# 读取图片获取尺寸
img = cv2.imread(os.path.join(imgbasepath, imgpath))
if img is None:
print(f"Warning: Could not read image {imgpath}")
return None
height, width = img.shape[:2]
# 计算YOLO格式的标注(归一化坐标)
x_center = (box[0][0] + box[1][0]) / 2.0 / width
y_center = (box[0][1] + box[1][1]) / 2.0 / height
bbox_width = (box[1][0] - box[0][0]) / width
bbox_height = (box[1][1] - box[0][1]) / height
# 确保坐标在0-1范围内
x_center = max(0.0, min(1.0, x_center))
y_center = max(0.0, min(1.0, y_center))
bbox_width = max(0.0, min(1.0, bbox_width))
bbox_height = max(0.0, min(1.0, bbox_height))
# YOLO格式:<class_id> <x_center> <y_center> <width> <height>
# 这里假设所有都是车牌,class_id=0
yolo_line = f"0 {x_center:.6f} {y_center:.6f} {bbox_width:.6f} {bbox_height:.6f}"
# 复制图片到目标目录
output_img_path = os.path.join(output_base, "images", mode, imgpath)
shutil.copy(os.path.join(imgbasepath, imgpath), output_img_path)
# 保存标签文件
label_filename = os.path.join(output_base, "labels", mode, f"{imgname}.txt")
with open(label_filename, 'w') as f:
f.write(yolo_line)
# 返回车牌文本信息(可用于后续验证)
return plate_text
except Exception as e:
print(f"Error processing {imgpath}: {e}")
return None
def create_dataset_yaml(output_base="dataset", num_classes=1):
"""创建YOLO数据集配置文件"""
yaml_content = f"""# YOLO数据集配置文件
path: {os.path.abspath(output_base)} # 数据集根目录
train: images/train # 训练集图片路径(相对于path)
val: images/val # 验证集图片路径(相对于path)
# 类别数
nc: {num_classes}
# 类别名称
names: ['license_plate']
# 下载脚本/指令(可选)
# download: bash download.sh
"""
yaml_path = os.path.join(output_base, "dataset.yaml")
with open(yaml_path, 'w') as f:
f.write(yaml_content)
print(f"Created dataset configuration: {yaml_path}")
return yaml_path
def main():
# 设置参数
dataset_base = "dataset" # 输出数据集根目录
imgbasepath = 'CCPD2019/ccpd_base/' # 原始图片路径
# 创建目录结构
print("Creating dataset directory structure...")
create_dataset_structure(dataset_base)
# 获取所有图片文件
image_list = [f for f in os.listdir(imgbasepath) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]
print(f"Found {len(image_list)} images in source directory")
# 随机打乱数据集
random.seed(42) # 设置随机种子确保可重复性
random.shuffle(image_list)
# 划分训练集和验证集(默认80%训练,20%验证)
split_ratio = 0.8
split_index = int(len(image_list) * split_ratio)
train_images = image_list[:split_index]
val_images = image_list[split_index:]
print(f"Split dataset: {len(train_images)} training, {len(val_images)} validation")
# 处理训练集
print("\nProcessing training set...")
train_plates = []
for imgpath in tqdm(train_images, desc="Training images"):
plate_text = process_image(imgpath, imgbasepath, dataset_base, "train")
if plate_text:
train_plates.append(plate_text)
# 处理验证集
print("\nProcessing validation set...")
val_plates = []
for imgpath in tqdm(val_images, desc="Validation images"):
plate_text = process_image(imgpath, imgbasepath, dataset_base, "val")
if plate_text:
val_plates.append(plate_text)
# 统计信息
print("\n" + "="*50)
print("Dataset Statistics:")
print(f"Total processed: {len(train_plates) + len(val_plates)} images")
print(f"Training set: {len(train_plates)} images")
print(f"Validation set: {len(val_plates)} images")
# 显示示例车牌(如果有处理成功的)
if train_plates:
print(f"\nSample license plates in training set:")
for i, plate in enumerate(train_plates[:5]):
print(f" {i+1}. {plate}")
# 创建数据集配置文件
dataset_yaml = create_dataset_yaml(dataset_base)
print(f"\nDataset preparation complete!")
print(f"Dataset saved to: {os.path.abspath(dataset_base)}")
print(f"Dataset config: {dataset_yaml}")
print("\nFolder structure:")
print(f" {dataset_base}/")
print(f" ├── images/")
print(f" │ ├── train/ ({len(train_images)} images)")
print(f" │ └── val/ ({len(val_images)} images)")
print(f" ├── labels/")
print(f" │ ├── train/ ({len(train_plates)} label files)")
print(f" │ └── val/ ({len(val_plates)} label files)")
print(f" └── dataset.yaml")
if __name__ == "__main__":
main()准备好训练集之后,就可以开始模型的训练了,在根目录中编辑如下代码,命名为run.py, 训练代码如下所示。本次训练使用的是yolo26n模型,也就是26系列最小的那个模型,如果要更改模型的话可以在代码中更改。
import warnings
warnings.filterwarnings('ignore')
from ultralytics import YOLO
if __name__ == '__main__':
# 初始化模型结构
#model = YOLO(model='/root/workspace/ultralytics-8.4.2/ultralytics/cfg/models/26/yolo26.yaml')
# 加载预训练权重(可选)
#model.load('yolo26n.pt') # 若从头训练可注释此行
model = YOLO("yolo26n.pt")
# 开始训练
model.train(
data=r'dataset/dataset.yaml', # 数据配置文件路径
imgsz=640, # 输入图像尺寸
epochs=5, # 训练轮数
batch=16, # 批次大小
workers=8, # 数据加载线程数
device='0', # 使用GPU编号(多卡可用 '0,1')
optimizer='SGD', # 优化器选择
close_mosaic=1, # 最后10轮关闭Mosaic增强
resume=False, # 是否断点续训
name='base', # 实验名称
single_cls=False, # 是否单类训练
cache=False, # 是否缓存数据集到内存
)训练结果如下,大概的mAP50能做到99%左右了,mAP50-95能做到97%左右:
| epoch | time | train/box_loss | train/cls_loss | metrics/recall(B) | metrics/mAP50(B) | metrics/mAP50-95(B) |
|---|---|---|---|---|---|---|
| 1 | 1378.93 | 1.05814 | 0.63146 | 0.97924 | 0.99404 | 0.76039 |
| 2 | 2700.88 | 1.1743 | 0.38363 | 0.99443 | 0.99495 | 0.77516 |
| 3 | 4002.42 | 1.11833 | 0.33517 | 0.99597 | 0.99497 | 0.77598 |
| 4 | 5297.61 | 1.00391 | 0.29686 | 0.99706 | 0.99498 | 0.77394 |
| 5 | 6586.38 | 0.92852 | 0.24604 | 0.99791 | 0.99496 | 0.7751 |
按照目前的训练结果来看,还是训练的很不错了,但是我注意到一个前提是数据集中的车牌一般都比较大,都是属于近距离拍摄的结果,如果是小目标,或者远景照片的话还是挺难识别到的。
在模型训练完成后,会形成一个文件夹,存储了所有的训练数据,过程数据和结果展示。在runs/detect/base文件夹中。
然后车牌的识别模型使用的是经典的LPRNet,这个我并没有做过多的改动,直接调用即可。
有了模型文件,就可以使用一个GUI的页面来做展示了,使用基于pyside6的方式来搭建,借鉴了一些别人已经写好的框架,参考的代码是YOLOSHOW,代码仓库如下:https://github.com/YOLOSHOW/YOLOSHOW
我看网上很多的项目是基于这个GUI项目来构建的。因为yoloshow貌似很久没有更新了,所以使用的是另一个开源的项目,是https://blog.csdn.net/weixin_43694096/article/details/138080654。最终展示的页面效果如下:
原始项目需要做一些debug的工作,我确实对pyqt不是很熟悉,所以就用大模型帮我改了一些内容,适配的还不错。但是目前的代码只能是画出目标框,也就是检测出车牌的位置,车牌的内容并没有识别出来,所以还需要增加LPR的代码模块。
在代码中增加LPRNet的模型加载和识别,然后把模型的输出结果打印在结果展示的图片上。所以要获取到yolo26识别到的车牌的图像,把截取的图像输入到LPRNet中进行识别,并保存返回的结果,因为yolo识别的图像长宽是不固定的,所以需要对截取的图像做一个变换处理,例如下面这样,原始的img是一个numpy格式的矩阵。
def transform(img):
img = cv2.resize(img,(94,24))
img = img.astype('float32')
img -= 127.5
img *= 0.0078125
img = np.transpose(img, (2, 0, 1))
return img一切的代码准备好之后就可以运行了,首先是创建一个虚拟的python环境,因为要兼顾yoloshow的代码和LPRNet的代码,所以我用的python版本是3.9,使用anaconda 创建一个虚拟环境。
conda create -n yolo_plate python=3.9然后是安装必要的安装包,主要是ultralytics、pyside、torch,这个用cpu跑就很快了,目前不需要上GPU来运行,所以torch使用的也是CPU的版本
conda activate yolo_plate
# cd到项目的根目录中
pip install -r requirements.txt按下面的步骤操作,先按1,选择要识别的图片,然后按2,开始模型推理,最后在3的位置,会显示识别的结果。如下图所示:
完整模型和代码文件:
参考文档:
CCPD数据集讲解:
https://blog.csdn.net/LuohenYJ/article/details/117752120
yolo训练代码
https://blog.csdn.net/qq_40716944/article/details/128648001
pyside6代码
https://blog.csdn.net/weixin_43694096/article/details/138080654



