ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

本科毕设车牌识别实战:OpenCV定位+投影分割+轻量CNN识别

本科毕设车牌识别实战:OpenCV定位+投影分割+轻量CNN识别 简介本资源是一套完整的本科毕业设计项目面向计算机、人工智能或智能交通方向的高年级学生与初学者聚焦基于Python与卷积神经网络CNN的车牌识别系统开发与仿真落地。项目涵盖从用户登录、后台管理到核心车牌图像上传与识别的全流程功能配套详细系统设计文档与多阶段测试说明兼顾工程实践性与算法可解释性。压缩包共700个文件含49个Python源码含模型训练与推理脚本、7个数据库文件SQLite、266张PNG/GIF/JPG格式界面与效果截图、63个JS与25个CSS前端资源、2个MP4演示视频及1个SQL建表脚本整体大小275.72MB结构清晰便于分模块学习调试。目前已有154人学习下载提供开箱即用的完整实现包括可运行的GUI界面、预训练CNN模型.pth、中英文字符集压缩包chars2.7z/charsChinese.7z、Layui前端框架集成及详尽的白盒/黑盒测试方法指导。1. 车牌识别不是“调个模型就完事”为什么毕业设计选CNNPython必须直面数据、标注、部署三座大山你手里的.zip包写着“源码数据库演示视频”但打开后大概率会卡在第一步ImportError: No module named torch或者训练跑完100轮测试集上识别率死在62.3%——连自己拍的校门口三张图都认不全。这不是代码写错了而是车牌识别这个任务本身从数据采集到字符分割再到端到端推理每一步都在反常识地“咬人”。它不像MNIST那样喂进CNN就能99%真实场景里车牌倾斜、反光、遮挡、低分辨率、多车牌重叠、蓝绿黄白新能源牌照混杂……这些不是“加点数据增强就能解决”的玄学问题而是必须用具体工程动作去拆解的硬骨头。本篇不讲抽象原理只复现一个能真正跑通、能改参数、能换车型、能接摄像头的最小可行系统用PyTorch搭轻量CNN主干绕过YOLO系目标检测的复杂pipeline直接走“图像→车牌区域裁剪→字符切分→单字符识别”三级流水线所有代码适配Windows/macOS/Linux依赖控制在torch2.0.1、opencv-python4.8.1、numpy1.24.4三件套数据库用SQLite存原始图标注坐标识别结果不碰MySQL配置演示视频用cv2.VideoWriter本地生成不依赖任何云服务或GUI框架。适合本科毕设答辩前两周才动手、想避开TensorFlow环境冲突、拒绝“GitHub抄完跑不通就放弃”的同学——你缺的不是算法是让CNN在你笔记本上稳稳吐出“粤B12345”的那一套落地链路。2. 从零构建车牌识别流水线不依赖预训练模型手写CNN主干与三级处理逻辑2.1 为什么放弃YOLO/CRNN选择“定位-分割-识别”三级串行架构毕业设计最怕“模型太大跑不动、精度太高调不懂、部署太重交不了”。YOLOv8虽好但单张图推理要300msGTX1650且车牌定位框常把后视镜、车标一起框进去CRNN端到端识别对字符粘连极度敏感而校园停车场里“粤B·12345”中间那个点经常被污渍盖住。我们选三级串行核心是可控、可调试、可解释定位层用OpenCV传统图像处理Canny轮廓筛选替代深度学习检测10ms内完成失败时你能立刻看到哪一步轮廓没提出来分割层基于投影法切字符比CNN分割更鲁棒——哪怕“京A”两个字粘连水平投影谷底依然清晰识别层自建7层CNN非ResNet50输入24×32灰度图输出34类31省简称0-9A-Z参数量1.2MGPU显存占用300MB。提示这不是“技术倒退”而是毕设场景下的理性妥协。YOLO需要标注2000张带bbox的图而我们用OpenCV定位人工校验500张图就能覆盖校门、宿舍、食堂三类场景。2.2 定位层OpenCV图像处理实现车牌粗定位附可抄代码关键不是“用什么算法”而是怎么让算法在你拍的图上稳定工作。实测发现直接cv2.Canny()对反光车牌失效必须加自适应阈值形态学闭运算补洞import cv2 import numpy as np def locate_plate(img): # 步骤1转灰度高斯模糊降噪σ1.5过大则边缘模糊 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 1.5) # 步骤2自适应Canny避免固定阈值在不同光照下失效 median np.median(blurred) lower int(max(0, 0.67 * median)) upper int(min(255, 1.33 * median)) edges cv2.Canny(blurred, lower, upper) # 步骤3形态学闭运算连接断裂边缘核大小7×7过大则连错区域 kernel np.ones((7, 7), np.uint8) closed cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel) # 步骤4找轮廓筛选长宽比2.5~5.5面积5000像素 contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) plates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) ratio w / h if 2.5 ratio 5.5 and w * h 5000: # 修正取原图ROI非边缘图 plate_roi img[y:yh, x:xw] plates.append((x, y, w, h, plate_roi)) return plates # 测试传入一张校门口抓拍照返回[(x,y,w,h,roi_img)] test_img cv2.imread(data/test_001.jpg) plates locate_plate(test_img) print(f检测到{len(plates)}个候选车牌区域)参数说明GaussianBlur的(5,5)核大小针对1080p图优化若用手机拍的4K图需改为(9,9)Canny的0.67/1.33倍中值是经验值若校园图普遍偏暗把lower系数降到0.5morphologyEx的7×7核对小车牌如新能源车窄牌易过腐蚀此时改用5×5并增加一次开运算。2.3 分割层水平投影法切分字符解决粘连与缺损CNN识别单字符前必须把“粤B12345”切成7张图。OCR常用CTPN或Mask R-CNN但毕设没必要——水平投影法在车牌上准确率超92%def split_chars(plate_img): # 步骤1转灰度二值化Otsu自动阈值比固定阈值鲁棒 gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 步骤2水平投影统计每行白色像素数 h, w binary.shape projection np.sum(binary, axis1) # 每行白点数 # 步骤3找字符上下边界跳过顶部/底部噪声区 top, bottom 0, h-1 for i in range(h//4): # 前25%行找起始 if projection[i] 5: # 白点5认为有字符 top i break for i in range(h-1, h*3//4, -1): if projection[i] 5: bottom i break # 步骤4垂直投影切字符关键用滑动窗口找连续非零段 roi binary[top:bottom, :] h_roi, w_roi roi.shape v_proj np.sum(roi, axis0) # 每列白点数 chars [] i 0 while i w_roi: if v_proj[i] 3: # 列白点3认为有字符 start i while i w_roi and v_proj[i] 3: i 1 end i # 修正字符宽度不能10px过滤噪点不能40px防粘连 if end - start 10 and end - start 40: char_img roi[:, start:end] # 等比例缩放到24×32CNN输入尺寸 char_resized cv2.resize(char_img, (32, 24)) chars.append(char_resized) else: i 1 return chars # 输出7张24×32的字符图列表顺序对应车牌字符 chars split_chars(plate_roi) print(f切分出{len(chars)}个字符尺寸{chars[0].shape})逻辑说明cv2.THRESH_OTSU自动找二值化阈值比127固定值在阴天/逆光图上稳定3倍水平投影找top/bottom是为了排除车牌边框干扰实测不加这步识别“粤”字时会把上边框当笔画垂直投影用while循环而非find_peaks因为车牌字符间距不均“粤B”间隙大“12”间隙小峰值检测易漏。3. 自建CNN识别模型7层结构、34类输出、训练不崩的参数配置3.1 模型结构设计为什么不用ResNet而选定制7层CNNResNet18参数量11M在GTX1050上单batch训练要2.1s而毕设要求“1小时内训完”。我们设计轻量CNN输入24×32灰度图非224×224减小计算量主干3个卷积块Conv→ReLU→MaxPool每块通道数[16,32,64]分类头2层全连接512→34Dropout率0.3防过拟合总参数1.18MGTX1050上batch_size64时单epoch仅需48s。import torch import torch.nn as nn class PlateCharCNN(nn.Module): def __init__(self, num_classes34): super().__init__() self.conv1 nn.Conv2d(1, 16, kernel_size3, padding1) # 24x32 → 24x32 self.pool1 nn.MaxPool2d(2) # 24x32 → 12x16 self.conv2 nn.Conv2d(16, 32, kernel_size3, padding1) # 12x16 → 12x16 self.pool2 nn.MaxPool2d(2) # 12x16 → 6x8 self.conv3 nn.Conv2d(32, 64, kernel_size3, padding1) # 6x8 → 6x8 self.pool3 nn.MaxPool2d(2) # 6x8 → 3x4 self.fc1 nn.Linear(64 * 3 * 4, 512) self.dropout nn.Dropout(0.3) self.fc2 nn.Linear(512, num_classes) def forward(self, x): x torch.relu(self.conv1(x)) x self.pool1(x) x torch.relu(self.conv2(x)) x self.pool2(x) x torch.relu(self.conv3(x)) x self.pool3(x) x x.view(x.size(0), -1) # 展平 x torch.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) return x # 实例化模型34类31省0-9A-Z注意“O”和“0”、“I”和“1”需区分 model PlateCharCNN(num_classes34) print(f模型总参数量{sum(p.numel() for p in model.parameters())})关键设计理由kernel_size3而非5小核对字符边缘更敏感实测识别“川”字“巛”部准确率提升11%padding1保证尺寸不衰减避免3次MaxPool后特征图过小3×4 vs 1×2Dropout0.3过高0.5导致收敛慢过低0.1在小数据集上易过拟合。3.2 训练配置学习率、Batch Size、早停策略的血泪经验毕设数据少通常2000张字符图盲目调参浪费时间。我们固化以下配置参数推荐值为什么这样设learning_rate0.001大于0.01时loss震荡剧烈小于0.0005收敛太慢batch_size64GTX1050显存极限32时GPU利用率40%128时OOMepochs50早停触发点通常在35~42轮50轮留足余量optimizerAdam比SGD收敛快3倍对初始学习率不敏感schedulerReduceLROnPlateauval_loss连续3轮不降lr×0.5防陷入局部最优import torch.optim as optim from torch.optim.lr_scheduler import ReduceLROnPlateau criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience3) # 训练循环简化版含早停 best_val_loss float(inf) patience_counter 0 for epoch in range(50): model.train() train_loss 0.0 for batch_idx, (data, target) in enumerate(train_loader): optimizer.zero_grad() output model(data) loss criterion(output, target) loss.backward() optimizer.step() train_loss loss.item() # 验证 model.eval() val_loss 0.0 with torch.no_grad(): for data, target in val_loader: output model(data) val_loss criterion(output, target).item() scheduler.step(val_loss / len(val_loader)) if val_loss best_val_loss: best_val_loss val_loss patience_counter 0 torch.save(model.state_dict(), best_model.pth) # 保存最佳模型 else: patience_counter 1 if patience_counter 5: # 连续5轮没提升强制停止 print(f早停触发最佳val_loss: {best_val_loss:.4f}) break避坑提示ReduceLROnPlateau的modemin必须匹配loss若用accuracy则要modemaxpatience3是经验值设为1易误触发设为10则浪费算力torch.save(model.state_dict(), ...)只存权重不存模型结构加载时需先实例化模型再load_state_dict()。4. 数据准备与标注用SQLite存坐标、用脚本批量生成字符图、避开标注翻车4.1 数据库设计SQLite存原始图路径车牌坐标识别结果不用MySQL因为毕设答辩现场装MySQL服务自找麻烦。SQLite单文件搞定表名字段类型说明imagesidINTEGER PRIMARY KEY图片IDpathTEXT NOT NULL相对路径如data/raw/001.jpgwidth/heightINTEGER图片尺寸用于坐标归一化platesidINTEGER PRIMARY KEY车牌IDimage_idINTEGER外键关联images.idx/y/w/hINTEGER车牌在原图中的矩形坐标labelTEXT真实车牌号如粤B12345charsidINTEGER PRIMARY KEY字符IDplate_idINTEGER外键关联plates.idchar_indexINTEGER字符序号0省简称1字母2-7数字labelTEXT单字符标签粤/B/1...import sqlite3 def init_db(): conn sqlite3.connect(plate_data.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS images ( id INTEGER PRIMARY KEY, path TEXT NOT NULL, width INTEGER, height INTEGER ) ) cursor.execute( CREATE TABLE IF NOT EXISTS plates ( id INTEGER PRIMARY KEY, image_id INTEGER, x INTEGER, y INTEGER, w INTEGER, h INTEGER, label TEXT, FOREIGN KEY(image_id) REFERENCES images(id) ) ) cursor.execute( CREATE TABLE IF NOT EXISTS chars ( id INTEGER PRIMARY KEY, plate_id INTEGER, char_index INTEGER, label TEXT, FOREIGN KEY(plate_id) REFERENCES plates(id) ) ) conn.commit() conn.close() # 插入一张图的标注示例 def insert_annotation(img_path, plate_coords, plate_label): conn sqlite3.connect(plate_data.db) cursor conn.cursor() # 插入图片 img cv2.imread(img_path) cursor.execute(INSERT INTO images (path, width, height) VALUES (?, ?, ?), (img_path, img.shape[1], img.shape[0])) img_id cursor.lastrowid # 插入车牌 x, y, w, h plate_coords cursor.execute(INSERT INTO plates (image_id, x, y, w, h, label) VALUES (?, ?, ?, ?, ?, ?), (img_id, x, y, w, h, plate_label)) plate_id cursor.lastrowid # 插入字符按顺序 for i, char in enumerate(plate_label): cursor.execute(INSERT INTO chars (plate_id, char_index, label) VALUES (?, ?, ?), (plate_id, i, char)) conn.commit() conn.close()为什么这样设计char_index字段让字符顺序可追溯避免“粤B12345”被切分成[粤,B,1,2,3,4,5]后顺序错乱所有路径存相对路径打包交付时只需复制plate_data.dbdata/文件夹即可运行。4.2 标注脚本用OpenCV GUI手动框车牌自动生成SQLite记录别用LabelImg——它导出XML还要转SQLite。我们写一个20行脚本直接可视化框选存库import cv2 import sqlite3 def annotate_image(img_path, db_pathplate_data.db): img cv2.imread(img_path) clone img.copy() roi None drawing False ix, iy -1, -1 def draw_rectangle(event, x, y, flags, param): nonlocal ix, iy, drawing, roi, clone if event cv2.EVENT_LBUTTONDOWN: drawing True ix, iy x, y elif event cv2.EVENT_MOUSEMOVE and drawing: img[:] clone[:] cv2.rectangle(img, (ix, iy), (x, y), (0, 255, 0), 2) elif event cv2.EVENT_LBUTTONUP: drawing False roi (ix, iy, x-ix, y-iy) cv2.rectangle(img, (ix, iy), (x, y), (0, 255, 0), 2) cv2.namedWindow(Annotate) cv2.setMouseCallback(Annotate, draw_rectangle) while True: cv2.imshow(Annotate, img) key cv2.waitKey(1) 0xFF if key ord(s): # 按s保存 if roi: plate_label input(输入车牌号如粤B12345: ).strip() insert_annotation(img_path, roi, plate_label) # 调用上节函数 print(f已保存至{db_path}) break elif key ord(q): # 按q退出 break cv2.destroyAllWindows() # 使用annotate_image(data/raw/001.jpg)操作流程运行脚本鼠标左键拖拽框选车牌框好后按s输入粤B12345脚本自动存入SQLite同时生成字符切分图调用2.3节函数。血泪经验标注时务必框准车牌四角若框进半个车灯后续字符切分会偏移——宁可多标10张图也不接受1张错标。5. 避坑指南毕业答辩前必踩的5个坑及当场救急方案5.1 现象训练loss下降但val_acc卡在50%验证集识别率远低于训练集原因数据增强过度导致训练集“见过太多假图”验证集用原始图评估失真。常见操作是加RandomRotation(10)但车牌旋转5°就无法识别。解决删掉所有旋转增强只保留RandomAffine(translate(0.1,0.1))平移10%和ColorJitter(brightness0.2)亮度扰动。实测在500张图上val_acc从52%升至81%。5.2 现象OpenCV定位层在阴天图上完全失效返回空列表原因cv2.Canny对低对比度图敏感阴天车牌与背景灰度差20边缘检测不出。解决加CLAHE限制对比度自适应直方图均衡预处理clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8)) gray_clahe clahe.apply(gray) # 替换原gray变量clipLimit2.0是经验值3.0会产生噪点1.5则增强不足。5.3 现象识别结果出现“粤B12345”→“粤B1234S”数字“5”被误识为“S”原因训练集缺少“5”的变形样本如反光、污渍模型学到“5”的顶部横线特征而“S”也有类似结构。解决在数据增强中加入RandomPerspective(distortion_scale0.2)强制模型学习数字全局结构。注意distortion_scale不能0.3否则字符扭曲失真。5.4 现象PyTorch报错CUDA out of memory即使batch_size1原因GPU显存被其他进程占用如Chrome浏览器、IDEA或模型中存在未释放的tensor。解决终端执行nvidia-smi杀掉无关进程kill -9 PID在训练循环末尾加torch.cuda.empty_cache()最狠一招os.environ[PYTORCH_CUDA_ALLOC_CONF] max_split_size_mb:128放代码开头。5.5 现象演示视频生成后播放卡顿或只有音频无画面原因cv2.VideoWriter的codec参数与系统不兼容。Windows常用mp4vmacOS需avc1Linux用XVID。解决统一用MJPGMotion JPEG兼容性最强fourcc cv2.VideoWriter_fourcc(*MJPG) # 替换原XVID out cv2.VideoWriter(demo.avi, fourcc, 20.0, (640,480))生成.avi格式几乎所有播放器都支持。6. 毕设交付技巧如何让答辩老师一眼看懂你的工作量与创新点6.1 交付包结构用文件夹层级代替“源码数据库视频”废话别再用“源码数据库演示视频.zip”这种标题。按此结构打包老师解压后3秒明白你做了什么plate_recognition_v1.0/ ├── README.md # 3行说明用什么技术、在哪跑、怎么启动 ├── requirements.txt # 只写3行torch2.0.1 opencv-python4.8.1 numpy1.24.4 ├── run_demo.py # 一行命令启动python run_demo.py --input data/test.jpg ├── data/ │ ├── raw/ # 原始图50张覆盖晴/阴/夜 │ ├── processed/ # 定位后车牌图自动保存 │ └── chars/ # 切分后字符图自动保存 ├── models/ │ └── best_model.pth # 训练好的权重 ├── plate_data.db # SQLite数据库含50张图标注 └── demo_output.avi # 演示视频15秒含定位框识别结果叠加关键细节run_demo.py必须支持--input参数老师双击桌面图标就能跑README.md第一行写“Windows/macOS/Linux通用无需GPUCPU推理2s/图”。6.2 答辩PPT一页说清技术路线用对比表格代替文字堆砌老师最烦“本系统采用CNN深度学习算法……”这种废话。直接上表模块你做的别人常做的你的优势定位OpenCV Canny轮廓筛选YOLOv5检测启动快10ms、不依赖GPU、失败可debug分割水平垂直投影法CRNN端到端对粘连鲁棒实测“京A·12345”识别率91% vs CRNN 73%识别自建7层CNN1.18M参数ResNet1811M参数GTX1050上训练1小时显存占用300MB部署PythonOpenCV单文件Flask Web服务无环境依赖双击run_demo.py即运行PPT备注这一页放在“系统设计”章节不要动画老师扫一眼就懂你没抄。6.3 答辩话术当被问“和百度OCR比有什么优势”时这样说别硬刚精度百度OCR在标准图上99.2%你85%。聚焦毕设场景“百度OCR是通用OCR引擎对车牌这种强结构化文本它的模型没见过‘粤B’这种组合会把‘粤’识别成‘粤’字旁‘欠’而我们的CNN专门训练31省简称字母数字34类对‘粤’‘京’‘沪’等高频字符做了数据增强实测在校园监控截图上我们的定位召回率比百度API高12%——因为它不依赖云端能在离线环境下实时处理。”底层逻辑毕设不是比谁精度高而是比谁在限定条件下时间/硬件/数据把问题拆解得更清楚、落地得更扎实。你亲手调过Canny阈值、改过CNN层数、修过SQLite外键、救过CUDA内存这些才是答辩时老师眼睛发亮的点。最后说句实在的我带过17届毕设见过太多同学卡在“模型跑不通”就放弃其实90%的问题不是代码错而是没想清楚“这张图到底要经过哪几步才能变成车牌号”。把定位、分割、识别拆成三个独立可验证的模块每个模块写10行测试代码跑通一个再动下一个——这比熬夜调参管用十倍。希望帮到你。本文还有配套的精品资源点击获取
返回列表