☰
车牌识别课程设计实战:OpenCV与CNN从零实现完整项目
2026/10/12 17:03:31 网站建设 项目流程

简介:这份Python课程设计车牌识别项目代码包,面向高校计算机相关专业学生及需要完成课程设计、毕业设计的开发者,帮助解决车牌定位、字符分割与识别等典型图像处理任务。包内共18个文件,以5个py源码文件为核心,配合5个png与3个jpg示例图片、2个dat训练数据文件,以及1个pptx答辩演示文稿和1个md说明文档,压缩包整体约4.73MB,结构紧凑便于直接运行与二次修改。项目围绕车牌识别完整流程展开,涵盖图像预处理、车牌定位、字符分割与SVM识别等模块,并附带演示文稿与说明文档,方便理解算法思路与整理答辩材料。目前已有652人学习下载,适合作为课程设计参考模板,也可用于学习OpenCV图像处理与机器学习分类的实战案例。

1. 车牌识别课程设计:从一张图片到可交付代码的完整路径

很多同学做课程设计时,选题定了车牌识别,打开编辑器却不知道第一步该干什么。这个标题背后其实是一条完整的工程链路:图像预处理、车牌定位、字符分割、字符识别,最后封装成可运行的 Python 项目。它解决的核心问题是——给定一张车辆照片,自动输出车牌号码字符串。适合正在做课程设计的学生,也适合想快速跑通一个计算机视觉小项目的开发者。我见过太多人卡在环境配置和数据集准备上,其实只要把流程拆清楚,两三天就能跑出一个能演示的版本。下面按实际动手顺序,把每个环节的参数、代码和踩坑点讲透。

2. 车牌识别项目的技术选型:为什么用 OpenCV 加轻量级方案

2.1 课程设计场景下的技术栈取舍

课程设计不是工业级产品,选型的第一原则是「能跑通、好演示、代码量可控」。常见做法是用 OpenCV 做图像处理,配合简单的形态学操作定位车牌,再用模板匹配或轻量级 CNN 做字符识别。为什么不直接上 YOLO 检测加 CRNN 识别?因为那套方案需要标注数据集、训练模型、调参,周期至少一周起步,而且对显卡有要求。课程设计通常只有一到两周,还要写报告,所以轻量方案更实际。

我一般会推荐这样的组合:OpenCV 负责图像预处理和车牌定位,Tesseract OCR 或者自己训练一个小型 CNN 做字符识别。Tesseract 的好处是安装即用,支持中文车牌字符集;缺点是默认模型对车牌这种特定场景识别率一般,需要做字符分割后再识别。如果时间充裕,用 PyTorch 训练一个 10 类左右的字符分类器,准确率能到 95% 以上。

提示:如果课程设计允许使用预训练模型,优先考虑用 OpenCV 的 DNN 模块加载一个轻量级检测模型,比从头训练省事得多。

2.2 环境搭建与依赖安装

先把环境搭好,避免后面写代码时被各种报错打断。Python 版本建议 3.8 到 3.10,太新的版本有些库还没适配。创建一个虚拟环境,然后安装核心依赖。

# 创建虚拟环境 python -m venv plate_env # 激活环境(Windows) plate_env\Scripts\activate # 激活环境(macOS/Linux) source plate_env/bin/activate # 安装核心依赖 pip install opencv-python==4.8.1.78 pip install numpy==1.24.3 pip install pytesseract==0.3.10 pip install Pillow==10.0.1

安装完成后验证一下 OpenCV 是否能正常导入:

import cv2 import numpy as np import pytesseract print("OpenCV 版本:", cv2.__version__) print("NumPy 版本:", np.__version__) # 测试读取一张图片 img = cv2.imread("test_car.jpg") if img is not None: print("图片尺寸:", img.shape) else: print("图片读取失败,检查路径")

这段代码的作用是确认三个核心库都能正常工作。cv2.imread返回的是 BGR 格式的 NumPy 数组,shape 是 (高度, 宽度, 通道数)。如果返回 None,说明路径不对或者文件损坏。参数方面,cv2.imread第二个参数默认是cv2.IMREAD_COLOR,如果需要灰度图可以传cv2.IMREAD_GRAYSCALE。

Tesseract OCR 需要单独安装引擎,不是 pip 装完就能用。Windows 下下载安装包后,要记住安装路径,然后在代码里指定:

# Windows 下指定 tesseract 路径 pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" # macOS/Linux 通常不需要指定,如果报错可以用 which tesseract 查看路径

这一步是血泪经验,很多人 pip 装完 pytesseract 后直接调用,结果报TesseractNotFoundError,就是因为没装引擎或者没指定路径。

2.3 项目目录结构设计

一个清晰的目录结构能让代码好维护,也方便答辩时展示。我一般会这样组织:

plate_recognition/ ├── data/ │ ├── input/ # 待识别的原始图片 │ └── output/ # 识别结果保存 ├── models/ # 存放训练好的模型文件 ├── src/ │ ├── preprocess.py # 图像预处理 │ ├── locate.py # 车牌定位 │ ├── segment.py # 字符分割 │ ├── recognize.py # 字符识别 │ └── main.py # 主流程入口 ├── requirements.txt └── README.md

这种拆分方式的好处是每个模块可以单独测试。比如定位效果不好时,只需要调locate.py,不用动识别部分的代码。data/input放测试图片,data/output存中间结果和最终结果,方便对比调试。

3. 车牌定位与字符分割:从整张图里把车牌抠出来

3.1 图像预处理:灰度化、高斯模糊、边缘检测

车牌定位的第一步是把彩色图变成灰度图,减少计算量。然后做高斯模糊去噪,再用 Sobel 或 Canny 做边缘检测。这里的关键参数是高斯核大小和 Canny 的双阈值。

import cv2 import numpy as np def preprocess_image(image_path): """图像预处理:灰度化、去噪、边缘检测""" # 读取图片 img = cv2.imread(image_path) if img is None: raise ValueError(f"无法读取图片: {image_path}") # 转灰度 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊,核大小 (5,5) 是经验值 # 核越大越模糊,但太小去噪效果差 blurred = cv2.GaussianBlur(gray, (5, 5), 0) # Sobel 算子检测垂直边缘(车牌字符垂直边缘丰富) sobel_x = cv2.Sobel(blurred, cv2.CV_16S, 1, 0, ksize=3) abs_x = cv2.convertScaleAbs(sobel_x) # 二值化,阈值 120 是调出来的 _, binary = cv2.threshold(abs_x, 120, 255, cv2.THRESH_BINARY) # 形态学闭操作,把断开的边缘连起来 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) return img, gray, closed

这段代码的逻辑是:灰度化减少通道数,高斯模糊平滑噪声,Sobel 检测垂直边缘(因为车牌字符排列方向是垂直的),二值化把边缘变成黑白图,最后用闭操作把相邻的边缘连成块。参数方面,高斯核 (5,5) 适合大多数场景,如果图片分辨率很高可以调到 (7,7)。Sobel 的 ksize 用 3 就够了。二值化阈值 120 不是固定的,光线暗的图片可能要降到 80,光线强的可以升到 150。形态学核 (17,5) 是横向长条,因为车牌是扁长的矩形。

注意:如果图片里车牌倾斜角度很大,Sobel 检测垂直边缘效果会变差,需要先做倾斜校正。

3.2 轮廓筛选:用长宽比和面积锁定车牌区域

边缘检测后会得到很多轮廓,需要根据车牌的几何特征筛选。中国蓝牌的标准尺寸是 440mm×140mm,长宽比约 3.14:1。筛选时用这个比例做约束。

def locate_plate(closed_img, original_img): """从边缘图中定位车牌区域""" # 找轮廓 contours, _ = cv2.findContours( closed_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) candidates = [] for cnt in contours: # 计算外接矩形 x, y, w, h = cv2.boundingRect(cnt) # 长宽比筛选,蓝牌约 3:1 到 4:1 aspect_ratio = w / float(h) if aspect_ratio < 2.5 or aspect_ratio > 5.0: continue # 面积筛选,太小或太大都排除 area = w * h if area < 1000 or area > 50000: continue # 矩形度筛选,轮廓面积与外接矩形面积比 rect_area = w * h extent = cv2.contourArea(cnt) / float(rect_area) if extent < 0.5: continue candidates.append((x, y, w, h)) # 按面积从大到小排序,取最大的几个 candidates.sort(key=lambda c: c[2] * c[3], reverse=True) # 在原图上画出候选框 result = original_img.copy() for i, (x, y, w, h) in enumerate(candidates[:3]): cv2.rectangle(result, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(result, f"Candidate {i+1}", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return candidates, result

筛选逻辑分三层:长宽比排除明显不是车牌的矩形,面积排除太小或太大的区域,矩形度排除形状不规则的轮廓。长宽比范围 2.5 到 5.0 是留了余量,因为拍摄角度会导致透视变形。面积阈值 1000 到 50000 是针对 640×480 左右的图片,如果图片分辨率是 1920×1080,阈值要相应放大。矩形度 0.5 是经验值,太低说明轮廓很碎,太高可能把非车牌区域也选进来。

实际跑的时候,候选区域可能有多个,需要进一步筛选。常见做法是取面积最大的那个,或者用颜色特征再过滤一遍——蓝牌在 HSV 空间的色调范围大概是 100 到 124。

3.3 字符分割:投影法切出单个字符

定位到车牌后,需要把车牌区域裁剪出来,然后做字符分割。常用方法是垂直投影法:统计每一列的白色像素数,找到波谷作为分割点。

def segment_characters(plate_img): """垂直投影法分割字符""" # 转灰度并二值化 gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) # 自适应阈值,应对光照不均 binary = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 19, 5 ) # 垂直投影:统计每列白色像素数 height, width = binary.shape projection = np.sum(binary, axis=0) / 255 # 找波谷(投影值很小的列) threshold = height * 0.1 # 低于高度 10% 认为是波谷 is_valley = projection < threshold # 找字符边界 boundaries = [] in_char = False start = 0 for i in range(width): if not is_valley[i] and not in_char: # 进入字符区域 in_char = True start = i elif is_valley[i] and in_char: # 离开字符区域 in_char = False if i - start > 5: # 宽度太小的忽略 boundaries.append((start, i)) # 处理最后一个字符 if in_char and width - start > 5: boundaries.append((start, width)) # 裁剪出每个字符 chars = [] for (start, end) in boundaries: char_img = binary[:, start:end] # 统一尺寸到 20x40 char_resized = cv2.resize(char_img, (20, 40)) chars.append(char_resized) return chars, binary, projection

投影法的核心思想是:字符之间的间隙处,垂直方向的白色像素很少,形成波谷。阈值设为高度的 10% 是经验值,如果字符粘连严重可以调低到 5%,如果字符断裂可以调高到 15%。cv2.adaptiveThreshold的自适应阈值比全局阈值更适合光照不均的场景,blockSize 19 和 C 值 5 是调出来的参数。分割后的字符统一缩放到 20×40,方便后续识别模型处理。

提示:中国车牌第一个字符是汉字,后面是字母和数字。汉字笔画复杂,投影法可能把一个汉字切成多块,需要做合并处理。

4. 字符识别与结果输出:让程序读出车牌号

4.1 用 Tesseract 做快速识别

Tesseract 是最省事的方案,安装后直接调用。但默认模型对车牌字符识别率不高,需要配置参数。

import pytesseract import cv2 import numpy as np def recognize_with_tesseract(char_img): """用 Tesseract 识别单个字符""" # 配置:只识别数字和大写字母 config = "--psm 10 -c tessedit_char_whitelist=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" # 转成 Tesseract 需要的格式 # Tesseract 对黑底白字识别更好,所以反转一下 char_inv = cv2.bitwise_not(char_img) # 识别 text = pytesseract.image_to_string(char_inv, config=config) # 清理结果 text = text.strip().replace(" ", "").replace("\n", "") return text if text else "?" def recognize_plate(chars): """识别整个车牌""" result = "" for i, char_img in enumerate(chars): # 第一个字符可能是汉字,用不同配置 if i == 0: config = "--psm 10 -l chi_sim" char_inv = cv2.bitwise_not(char_img) text = pytesseract.image_to_string(char_inv, config=config) else: text = recognize_with_tesseract(char_img) result += text.strip() if text.strip() else "?" return result

--psm 10表示把图片当作单个字符处理,tessedit_char_whitelist限制识别范围,避免出现奇怪符号。第一个字符用-l chi_sim加载中文模型,但 Tesseract 的中文识别对车牌汉字效果一般,常见做法是单独训练一个汉字分类器。实际测试中,数字和字母的识别率能到 80% 左右,汉字可能只有 50%。

4.2 训练一个轻量级 CNN 做字符分类

如果对准确率有要求,自己训练一个小 CNN 更靠谱。网络结构不用太复杂,三层卷积加两层全连接就够了。

import torch import torch.nn as nn import torch.nn.functional as F class PlateCharCNN(nn.Module): """车牌字符分类网络""" def __init__(self, num_classes=65): super(PlateCharCNN, self).__init__() # 输入 20x40 灰度图 self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1) self.pool = nn.MaxPool2d(2, 2) self.dropout = nn.Dropout(0.5) # 经过三次池化,20x40 -> 2x5 self.fc1 = nn.Linear(128 * 2 * 5, 256) self.fc2 = nn.Linear(256, num_classes) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) # 10x20 x = self.pool(F.relu(self.conv2(x))) # 5x10 x = self.pool(F.relu(self.conv3(x))) # 2x5 x = x.view(-1, 128 * 2 * 5) x = self.dropout(F.relu(self.fc1(x))) x = self.fc2(x) return x # 训练配置 def train_model(train_loader, val_loader, epochs=20): model = PlateCharCNN(num_classes=65) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) for epoch in range(epochs): model.train() running_loss = 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() # 验证 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in val_loader: outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print(f"Epoch {epoch+1}, Loss: {running_loss/len(train_loader):.4f}, " f"Val Acc: {100*correct/total:.2f}%") return model

网络结构的设计思路:三层卷积逐步提取特征,每层后接最大池化降维,最后用全连接层分类。输入 20×40 的灰度图,经过三次池化变成 2×5,展平后是 128×2×5=1280 维。num_classes=65对应 10 个数字、24 个字母(去掉 I 和 O)、31 个汉字(各省简称)。训练时用 Adam 优化器,学习率 0.001,Dropout 0.5 防止过拟合。数据增强可以用随机旋转 ±5 度、随机亮度调整,模拟不同拍摄条件。

4.3 主流程串联与结果保存

把各个模块串起来,形成完整的识别流程。

def main(image_path, output_dir="data/output"): """主流程:预处理 -> 定位 -> 分割 -> 识别""" import os # 1. 预处理 original, gray, edges = preprocess_image(image_path) # 2. 定位车牌 candidates, marked = locate_plate(edges, original) if not candidates: print("未检测到车牌区域") return None # 取面积最大的候选区域 x, y, w, h = candidates[0] plate_img = original[y:y+h, x:x+w] # 3. 字符分割 chars, binary, projection = segment_characters(plate_img) if len(chars) < 5: print(f"字符分割结果异常,只得到 {len(chars)} 个字符") return None # 4. 字符识别 plate_number = recognize_plate(chars) # 5. 保存结果 os.makedirs(output_dir, exist_ok=True) cv2.imwrite(os.path.join(output_dir, "plate_region.jpg"), plate_img) cv2.imwrite(os.path.join(output_dir, "binary.jpg"), binary) cv2.imwrite(os.path.join(output_dir, "marked.jpg"), marked) print(f"识别结果: {plate_number}") return plate_number if __name__ == "__main__": result = main("data/input/car1.jpg")

主流程的每一步都有中间结果保存,方便调试。如果最终识别结果不对,可以按顺序检查:定位框是否准确、二值化是否清晰、字符分割是否完整。candidates[0]取面积最大的区域,但实际场景中可能有多个车牌,需要根据位置或颜色进一步筛选。字符数少于 5 个说明分割失败,常见原因是二值化阈值不合适或者车牌倾斜。

5. 避坑指南:车牌识别课程设计中最容易翻车的五个点

5.1 图片读取返回 None 导致后续全部报错

现象:运行代码时抛出ValueError: 无法读取图片,或者cv2.imread返回 None 后继续操作报AttributeError。

原因:图片路径包含中文或空格,OpenCV 在 Windows 下对中文路径支持不好;或者图片格式是 CMYK 模式,OpenCV 读不了。

解决:用cv2.imdecode配合np.fromfile读取中文路径的图片:

def read_image_chinese_path(path): """支持中文路径的图片读取""" img = cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR) return img

另外,读取前先用os.path.exists检查文件是否存在,避免路径拼写错误。

5.2 车牌定位框偏移或漏检

现象:画出的绿色框没有套住车牌,或者根本找不到候选区域。

原因:Sobel 边缘检测对倾斜车牌不敏感;形态学核大小不合适;长宽比筛选范围太窄。

解决:先做倾斜校正再检测边缘。用cv2.minAreaRect找到最小外接矩形,计算倾斜角度,然后用cv2.getRotationMatrix2D旋转校正。形态学核可以尝试 (13,5)、(17,5)、(21,7) 几组值。长宽比范围放宽到 2.0 到 5.5,先保证不漏检,再通过颜色特征过滤误检。

5.3 字符分割时汉字被切碎

现象:第一个汉字被切成两三个部分,导致识别结果多出几个问号。

原因:汉字笔画复杂,垂直投影的波谷不明显,阈值设置过高会把笔画间隙当成字符边界。

解决:对第一个字符区域单独处理,降低投影阈值,或者用连通域分析代替投影法。连通域分析用cv2.connectedComponentsWithStats,把面积相近的连通域合并成一个字符。

# 连通域分析示例 num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary, connectivity=8) # 根据面积和位置合并连通域

5.4 Tesseract 识别结果全是乱码

现象:识别出来的字符串包含大量特殊符号,或者全是问号。

原因:没有指定--psm参数,Tesseract 默认按段落处理;没有设置字符白名单;图片没有反转(Tesseract 对黑底白字识别更好)。

解决:加上--psm 10和tessedit_char_whitelist,识别前用cv2.bitwise_not反转图片。如果还是不行,检查 Tesseract 版本,4.0 以上对中文支持更好。

5.5 训练 CNN 时损失不下降

现象:训练几个 epoch 后,loss 一直在 4.0 左右徘徊,准确率不涨。

原因:学习率太大导致震荡;数据没有归一化;标签编码错误。

解决:学习率从 0.001 降到 0.0001 试试;输入图片除以 255 归一化到 0 到 1;检查标签是否从 0 开始连续编码。另外,batch size 太小也会导致 loss 震荡,建议调到 32 或 64。

6. 进阶技巧:用数据增强和模型量化提升识别率

6.1 针对车牌场景的数据增强策略

课程设计的数据集通常只有几百张图片,直接训练容易过拟合。数据增强是低成本提升泛化能力的手段。针对车牌场景,我一般用这几种增强:

import cv2 import numpy as np import random def augment_char_image(char_img): """字符图片增强""" h, w = char_img.shape[:2] # 1. 随机旋转 -8 到 8 度 angle = random.uniform(-8, 8) M = cv2.getRotationMatrix2D((w/2, h/2), angle, 1.0) rotated = cv2.warpAffine(char_img, M, (w, h), borderMode=cv2.BORDER_REPLICATE) # 2. 随机亮度调整 brightness = random.uniform(0.7, 1.3) adjusted = np.clip(rotated * brightness, 0, 255).astype(np.uint8) # 3. 随机添加高斯噪声 if random.random() > 0.5: noise = np.random.normal(0, 10, adjusted.shape).astype(np.uint8) adjusted = cv2.add(adjusted, noise) # 4. 随机模糊 if random.random() > 0.5: adjusted = cv2.GaussianBlur(adjusted, (3, 3), 0) return adjusted

旋转模拟拍摄角度偏差,亮度调整模拟光照变化,高斯噪声模拟传感器噪声,模糊模拟对焦不准。这四种增强组合起来,能把几百张图片扩充到几千张。注意旋转角度不要超过 10 度,否则字符变形太严重反而影响训练。

6.2 模型量化与推理加速

训练好的 PyTorch 模型如果要在 CPU 上跑,可以做动态量化,模型大小能压缩到原来的四分之一,推理速度提升两到三倍。

import torch # 加载训练好的模型 model = PlateCharCNN(num_classes=65) model.load_state_dict(torch.load("models/plate_cnn.pth", map_location="cpu")) model.eval() # 动态量化 quantized_model = torch.quantization.quantize_dynamic( model, {torch.nn.Linear}, dtype=torch.qint8 ) # 保存量化后的模型 torch.save(quantized_model.state_dict(), "models/plate_cnn_quantized.pth") # 测试推理速度 import time dummy_input = torch.randn(1, 1, 20, 40) start = time.time() for _ in range(100): with torch.no_grad(): _ = quantized_model(dummy_input) end = time.time() print(f"量化后平均推理时间: {(end-start)/100*1000:.2f} ms")

quantize_dynamic只量化全连接层,卷积层保持浮点,这样精度损失很小。实测下来,量化后准确率下降不到 1%,但推理速度明显提升。如果课程设计需要在树莓派或手机上演示,量化是必做的步骤。

6.3 一个容易被忽略的细节:车牌颜色判断

中国车牌有蓝牌、黄牌、绿牌(新能源)几种,颜色判断能辅助定位和识别。蓝牌的 HSV 色调范围是 100 到 124,黄牌是 20 到 35,绿牌是 40 到 80。在定位阶段加入颜色过滤,能大幅减少误检。

def filter_by_color(img, candidates): """根据颜色过滤候选区域""" hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) valid = [] for (x, y, w, h) in candidates: roi = hsv[y:y+h, x:x+w] # 统计蓝色像素比例 blue_mask = cv2.inRange(roi, (100, 43, 46), (124, 255, 255)) blue_ratio = np.sum(blue_mask > 0) / (w * h) # 蓝色像素超过 30% 认为是蓝牌 if blue_ratio > 0.3: valid.append((x, y, w, h)) return valid

这个函数在轮廓筛选之后调用,把颜色不符合的候选区域去掉。蓝牌检测用cv2.inRange生成掩码,统计非零像素比例。阈值 0.3 是经验值,太低会漏掉颜色偏淡的车牌,太高会误删。黄牌和绿牌类似,调整 HSV 范围即可。

我做完几个课程设计后最大的习惯是:每写一个模块就先拿一张图单独测试,确认输出符合预期再往下写。这样出问题时能快速定位是哪个环节的锅,不用在几百行代码里大海捞针。希望帮到你。

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

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

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

立即咨询