简介这份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-python4.8.1.78 pip install numpy1.24.3 pip install pytesseract0.3.10 pip install Pillow10.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 rC:\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, ksize3) 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(keylambda c: c[2] * c[3], reverseTrue) # 在原图上画出候选框 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, fCandidate {i1}, (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, axis0) / 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_whitelist0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ # 转成 Tesseract 需要的格式 # Tesseract 对黑底白字识别更好所以反转一下 char_inv cv2.bitwise_not(char_img) # 识别 text pytesseract.image_to_string(char_inv, configconfig) # 清理结果 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, configconfig) 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_classes65): super(PlateCharCNN, self).__init__() # 输入 20x40 灰度图 self.conv1 nn.Conv2d(1, 32, kernel_size3, padding1) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.conv3 nn.Conv2d(64, 128, kernel_size3, padding1) 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, epochs20): model PlateCharCNN(num_classes65) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.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(fEpoch {epoch1}, Loss: {running_loss/len(train_loader):.4f}, fVal Acc: {100*correct/total:.2f}%) return model网络结构的设计思路三层卷积逐步提取特征每层后接最大池化降维最后用全连接层分类。输入 20×40 的灰度图经过三次池化变成 2×5展平后是 128×2×51280 维。num_classes65对应 10 个数字、24 个字母去掉 I 和 O、31 个汉字各省简称。训练时用 Adam 优化器学习率 0.001Dropout 0.5 防止过拟合。数据增强可以用随机旋转 ±5 度、随机亮度调整模拟不同拍摄条件。4.3 主流程串联与结果保存把各个模块串起来形成完整的识别流程。def main(image_path, output_dirdata/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:yh, x:xw] # 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_okTrue) 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, dtypenp.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, connectivity8) # 根据面积和位置合并连通域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), borderModecv2.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_classes65) model.load_state_dict(torch.load(models/plate_cnn.pth, map_locationcpu)) model.eval() # 动态量化 quantized_model torch.quantization.quantize_dynamic( model, {torch.nn.Linear}, dtypetorch.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:yh, x:xw] # 统计蓝色像素比例 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 范围即可。我做完几个课程设计后最大的习惯是每写一个模块就先拿一张图单独测试确认输出符合预期再往下写。这样出问题时能快速定位是哪个环节的锅不用在几百行代码里大海捞针。希望帮到你。本文还有配套的精品资源点击获取