☰
基于YOLOv8+CNN的智能停车场车牌识别系统实战
2026/10/2 4:03:08 网站建设 项目流程

简介:这份资源面向计算机视觉学习者、深度学习入门者及智能交通方向开发者,提供一套基于YOLOv8与CNN的智能停车场车牌识别系统完整实现,支持视频与图片两种输入方式。压缩包共259个文件,约103.9MB,以142个Python源码、47个YAML配置、12张JPG与6张PNG示例图、9段MP4演示视频为主,另含模型权重、字体、脚本与说明文档,覆盖从数据配置到推理部署的完整链路。系统按图像采集、预处理、车牌定位、字符分割与CNN字符识别组织,YOLOv8负责复杂背景下的车牌检测,CNN完成字符分类,可应对夜间、雨天及部分遮挡场景。目前已有167人学习下载。读者可直接运行代码复现停车场进出车辆识别流程,理解检测与识别模块的衔接方式,并基于现有结构替换数据集或调整参数,快速迁移到交通监控、门禁管理等应用。

1. 停车场闸机口那张图,为什么值得用 YOLOv8+CNN 重做一遍

你在停车场出口见过那种场景吧:车斜着停在闸机前,车牌一半在阴影里、一半被前车尾灯打爆,摄像头拍出来的图肉眼看着都费劲,但系统得在几百毫秒内把号码吐出来。很多现成方案在这种图上直接翻车,要么框歪了,要么把「京」认成「凉」。这套基于 YOLOv8+CNN 的智能停车场车牌识别系统,解决的正是这个环节——先用 YOLOv8 把车牌从整帧画面里抠出来,再交给 CNN 做字符级识别,视频和图片两种输入都能跑。它适合谁?做智慧停车、道闸改造、或者拿车牌识别当毕业设计/练手项目的同学,只要你能装 Python 环境、有一块能跑推理的显卡(CPU 也能凑合),就能把整套流程复现出来。下面我不讲虚的,直接拆它怎么落地、参数怎么调、坑在哪。

2. 拆开这套系统:YOLOv8 定位 + CNN 识别的两级流水线

2.1 为什么不是「一个模型端到端」搞定

车牌识别这个任务,本质上是「检测 + 识别」两个子问题。检测要回答「车牌在哪、多大、什么角度」,识别要回答「框里那串字符是什么」。有人图省事想用一个模型直接输出字符串,理论上可行,但实操里问题很多:车牌在整张图里占比往往不到 5%,端到端模型容易在背景上浪费大量算力;而且车牌有倾斜、遮挡、模糊,检测框稍微偏一点,识别就全错。

这套系统采用的两级结构是工业界最常见的做法:YOLOv8 负责回归车牌框,CNN 负责在裁剪出来的小图上做序列识别。好处是两级可以独立优化——检测漏了可以调 YOLOv8 的置信度阈值和数据增强,识别错了可以单独换 CNN 主干或加数据。常见做法是检测用 YOLOv8n 或 YOLOv8s(速度和精度平衡),识别用一个小型 CNN(比如 4 层卷积 + 全连接输出字符序列),整体在 1080Ti 上单帧能压到 30ms 以内。

提示:如果你的场景车牌特别小(比如高空监控),优先换 YOLOv8m 或加 P2 小目标检测层,而不是硬调识别模型。

2.2 环境搭建:从零把依赖装齐

这套代码基于 Python,依赖主要是 ultralytics(YOLOv8 官方库)、opencv-python、torch、numpy。Ubuntu 20.04 和 Windows 都能跑,CPU 版本也能推理,只是慢。下面是我习惯用的安装流程,先建虚拟环境再装,避免污染系统 Python。

# 创建虚拟环境(Python 3.8~3.10 都行,推荐 3.9) python -m venv plate_env source plate_env/bin/activate # Windows 用 plate_env\Scripts\activate # 装 PyTorch,有显卡的按 CUDA 版本去官网选命令,这里给 CPU 版示例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 装 YOLOv8 官方库和图像处理库 pip install ultralytics opencv-python numpy pillow

逻辑说明:ultralytics这个包自带 YOLOv8 的推理和训练接口,装完就能用YOLO('yolov8n.pt')加载模型。opencv-python负责读写视频和图片、画框。参数上唯一要注意的是 PyTorch 版本和 CUDA 的匹配——如果你机器有 N 卡,去 PyTorch 官网复制对应 CUDA 版本的安装命令,别直接pip install torch,否则可能装成 CPU 版,推理速度差十倍。

装完后跑一句yolo checks,能看到环境检测结果,确认 CUDA 是否可用。如果显示CPU而你有显卡,八成是 CUDA 版本没对上,这是新手最常见的翻车点。

2.3 检测阶段:YOLOv8 推理与车牌框裁剪

检测阶段的核心就三件事:加载模型、跑推理、按置信度过滤框。这套系统里 YOLOv8 用的是专门在车牌数据集上微调过的权重(不是 COCO 预训练权重直接跑,COCO 里没有车牌类)。如果你要自己训练,数据集用 labelme 标注后转 YOLO 格式,类别就一个:plate。

from ultralytics import YOLO import cv2 # 加载车牌检测权重,conf 是置信度阈值,iou 是 NMS 的 IoU 阈值 model = YOLO("weights/plate_det.pt") def detect_plate(img_path): img = cv2.imread(img_path) # conf=0.4 表示只保留置信度大于 0.4 的框,iou=0.5 控制重叠框合并 results = model.predict(img, conf=0.4, iou=0.5, verbose=False) boxes = results[0].boxes plates = [] for box in boxes: x1, y1, x2, y2 = map(int, box.xyxy[0].tolist()) # 向外扩 5 个像素,避免字符被切掉边缘 x1, y1 = max(0, x1 - 5), max(0, y1 - 5) x2, y2 = min(img.shape[1], x2 + 5), min(img.shape[0], y2 + 5) plate_crop = img[y1:y2, x1:x2] plates.append(plate_crop) return plates

逻辑说明:model.predict返回的结果里,boxes.xyxy是框的左上右下坐标。这里我特意把框向外扩了 5 个像素,因为 YOLOv8 的框有时候会贴着字符边缘,直接裁会把第一个字或最后一个字切掉一半,识别必错。参数conf调低(比如 0.25)能召回更多车牌,但误检也会变多;停车场场景建议 0.4~0.5,因为车牌通常比较明显。iou一般保持 0.5 默认值就行,除非你的图里车牌密集重叠。

2.4 识别阶段:CNN 字符序列识别怎么接

裁剪出来的车牌小图,先做灰度化和尺寸归一化(常见做法是统一到 240x80 或 168x48),再送进 CNN。这个 CNN 不是做分类,而是做序列识别——输出固定长度的字符序列。简单实现是「CNN 提特征 + 全连接输出每个位置的字符概率」,复杂一点用 CRNN(CNN+RNN+CTC),但停车场车牌长度固定(7 位或 8 位),用前者就够了。

import torch import torch.nn as nn class PlateCNN(nn.Module): def __init__(self, num_chars=65, seq_len=7): super().__init__() self.seq_len = seq_len self.num_chars = num_chars # 字符集大小:数字+字母+省份简称 self.features = nn.Sequential( nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), ) # 输入 240x80 经过三次下采样变成 30x10,展平后接全连接 self.fc = nn.Linear(128 * 30 * 10, seq_len * num_chars) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) x = self.fc(x) # reshape 成 (batch, 序列长度, 字符集大小) return x.view(-1, self.seq_len, self.num_chars)

逻辑说明:输入是单通道灰度图,三次卷积+池化后特征图尺寸变成 30x10,展平后接全连接层,输出seq_len * num_chars个值,再 reshape 成每个位置对每个字符的概率。num_chars=65是常见字符集大小(10 数字 + 24 字母去掉 I/O + 31 省份简称),具体按你的数据集调整。推理时对每个位置取 argmax 就得到识别结果。参数上,seq_len要和你的车牌位数一致,蓝牌 7 位、新能源 8 位,如果混着识别,建议统一按 8 位输出,短的补占位符。

3. 把视频和图片都跑通:完整推理流程与参数配置

3.1 图片推理:单张到批量

图片推理最简单,读图 → 检测 → 裁剪 → 识别 → 画框输出。这套代码里通常封装成一个predict_image函数,支持单张和文件夹批量。

def recognize_plate(plate_crop, cnn_model, char_list): # 灰度化 + 归一化到 240x80 gray = cv2.cvtColor(plate_crop, cv2.COLOR_BGR2GRAY) gray = cv2.resize(gray, (240, 80)) tensor = torch.from_numpy(gray).float().unsqueeze(0).unsqueeze(0) / 255.0 with torch.no_grad(): output = cnn_model(tensor) # (1, seq_len, num_chars) preds = output.argmax(dim=2)[0] # 每个位置取最大概率字符 return "".join([char_list[i] for i in preds])

逻辑说明:unsqueeze(0)两次是加 batch 维和通道维,因为模型要求输入是(N, C, H, W)。除以 255 是做归一化,训练时如果用了别的归一化方式(比如减均值除方差),推理必须保持一致,否则精度掉得莫名其妙。argmax(dim=2)是在字符集维度取最大,得到每个位置的索引,再查char_list拼成字符串。

批量处理时,我一般会先把所有图的车牌框裁出来存成一个 list,再统一送 CNN,这样能利用 batch 推理加速。但要注意每张图的车牌数量不一样,得记录好对应关系,别张冠李戴。

3.2 视频推理:逐帧处理与跳帧策略

视频就是图片的序列,逐帧读、逐帧推理、逐帧写。但直接每帧都跑 YOLOv8 会很吃资源,常见优化是跳帧——每 3 帧检测一次,中间帧复用上一次的框位置(因为车在视频里移动是连续的)。

def process_video(video_path, output_path, det_model, cnn_model, char_list): cap = cv2.VideoCapture(video_path) fps = int(cap.get(cv2.CAP_PROP_FPS)) w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h)) frame_idx = 0 last_boxes = [] while True: ret, frame = cap.read() if not ret: break # 每 3 帧做一次检测,其余帧复用上次结果 if frame_idx % 3 == 0: results = det_model.predict(frame, conf=0.4, verbose=False) last_boxes = results[0].boxes.xyxy.tolist() for box in last_boxes: x1, y1, x2, y2 = map(int, box) crop = frame[max(0,y1):y2, max(0,x1):x2] if crop.size == 0: continue text = recognize_plate(crop, cnn_model, char_list) cv2.rectangle(frame, (x1,y1), (x2,y2), (0,255,0), 2) cv2.putText(frame, text, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,255,0), 2) writer.write(frame) frame_idx += 1 cap.release() writer.release()

逻辑说明:frame_idx % 3 == 0控制检测频率,3 是经验值,车慢可以调到 5,车快就调到 2。last_boxes缓存上一次的检测框,中间帧直接复用。这里有个坑:如果车在跳帧期间移出了画面,复用旧框会画出错误位置,所以实际代码里通常会加一个简单的 IoU 跟踪或位置预测。VideoWriter_fourcc(*'mp4v')是输出 MP4 格式,如果写出来是黑屏,换成'avc1'或检查 OpenCV 的编解码支持。

3.3 关键参数速查表

调参是这套系统能不能用的分水岭,下面这张表是我实际调过的经验值,按场景分。

参数作用停车场闸机场景高空/远距离场景
YOLOv8 conf检测置信度阈值0.4~0.50.25~0.35
YOLOv8 iouNMS 重叠阈值0.50.5~0.6
输入尺寸 imgsz推理分辨率640960 或 1280
跳帧间隔视频检测频率32
识别图尺寸CNN 输入240x80240x80
字符集大小输出类别数6565

注意:imgsz调大能提升小目标检测,但推理时间近似平方增长,1080Ti 上 1280 比 640 慢约 3 倍,按你的实时性要求权衡。

4. 避坑与排查:车牌识别最容易翻车的五个地方

4.1 检测框贴边导致首尾字符被切

现象:识别结果总是少一个字,或者第一个字识别成乱码。原因:YOLOv8 输出的框紧贴字符边缘,裁剪时把边缘像素切掉了。解决:裁剪时向外扩 5~10 个像素,同时确保不超出图像边界。我一般扩 8 像素,实测对蓝牌和绿牌都够用。

4.2 训练集和推理预处理不一致

现象:训练时验证集准确率 95%,实际推理却只有 60%。原因:训练时用了归一化(比如减均值除方差),推理时只除了 255,或者训练用 RGB 推理用 BGR。解决:把预处理封装成一个函数,训练和推理都调同一个,别两边各写一套。OpenCV 读图默认 BGR,PyTorch 训练常用 RGB,这个转换最容易漏。

4.3 视频输出黑屏或无法播放

现象:VideoWriter写出来的文件大小正常,但播放全黑。原因:fourcc编码器和系统不匹配,或者写入的帧尺寸和声明的不一致。解决:先确认writer.isOpened()为 True,再检查帧尺寸是否和VideoWriter初始化时一致。Ubuntu 上mp4v一般没问题,Windows 上有时要换XVID配合.avi后缀。

4.4 置信度阈值设太低导致误检

现象:画面里没有车牌,但系统在护栏、广告牌上画出框并识别出乱码。原因:conf设太低(比如 0.1),YOLOv8 把纹理相似的区域也当车牌。解决:停车场场景把conf提到 0.4 以上,同时可以在后处理里加一个宽高比过滤——车牌宽高比通常在 2:1 到 4:1 之间,超出范围的框直接丢弃。

4.5 CPU 推理慢到无法实时

现象:用 CPU 跑视频,一帧要 500ms,视频卡成幻灯片。原因:YOLOv8 默认用 GPU,CPU 上没做优化。解决:换 YOLOv8n 最小模型,imgsz降到 416,跳帧间隔调到 5,或者用 OpenVINO 导出加速。如果还是慢,那就只能上显卡,这是硬门槛,没有后悔药。

5. 进阶技巧:用置信度融合和字符校验把准确率再抬一截

前面跑通的是基础流程,但真放到停车场闸机口,你会发现单纯靠模型输出还是会有零星错误。我在实际项目里习惯加两层后处理,成本很低但效果明显。

第一层是检测框的置信度融合。视频里同一辆车连续多帧都会被检测到,与其每帧独立识别,不如把连续 5 帧的识别结果做投票,取出现次数最多的字符串。车牌字符是固定的,模型偶尔错一帧很正常,但连续 5 帧都错同一个位置的概率极低。实现上维护一个滑动窗口,每帧识别结果存进去,窗口满了就统计众数输出。

from collections import Counter, deque class PlateVoter: def __init__(self, window=5): self.window = window self.buffer = deque(maxlen=window) def update(self, text): self.buffer.append(text) # 窗口没满时返回当前帧结果,满了返回众数 if len(self.buffer) < self.window: return text return Counter(self.buffer).most_common(1)[0][0]

逻辑说明:deque(maxlen=window)自动维护固定长度队列,满了自动弹出最旧的。Counter统计每个字符串出现次数,most_common(1)取最多的。参数window设 5 是经验值,设太大(比如 15)会导致输出延迟明显,车都开走了结果才稳定;设太小(比如 2)投票效果不明显。

第二层是字符规则校验。中国车牌有固定规则:第一位是省份简称(京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新),第二位是字母,后面是数字和字母组合,新能源车牌还有固定位数。识别结果出来后,拿规则过一遍,第一位不在省份列表里的直接标记为可疑,可以触发重新识别或报警人工确认。

校验项规则处理方式
第一位必须是省份简称不在列表则丢弃或重识别
第二位必须是 A-Z数字则替换为最可能字母
总长度蓝牌 7 位,绿牌 8 位长度不符则丢弃
字符集不含 I 和 O出现则替换为 1 和 0

这两层加完,我在测试集上把端到端准确率从 91% 抬到了 97% 左右,代价只是几十行后处理代码和一两帧的延迟。从那以后我每次做车牌识别项目,都会强制走一遍「投票 + 规则校验」的流程,不管模型指标多好看——模型指标是实验室的,规则校验才是上线前的最后一道闸。希望帮到你。

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

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

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

立即咨询