ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenCV车牌识别课程设计全解析:从图像预处理到SVM识别

OpenCV车牌识别课程设计全解析:从图像预处理到SVM识别 简介这是一份基于Python与OpenCV实现的数字图像处理课程作业面向高校相关专业学生及OpenCV入门者。项目包含完整车牌识别源码、tkinter界面程序、数字图像处理中间过程图片与答辩PPT适合作为课程设计、毕业设计或工程实训的参考。资源共19个文件涵盖5个Python脚本、若干图片与数据文件、1份PPT及说明文档压缩包大小4.74MB。脚本实现了车牌定位、字符分割与识别等核心流程并配有svm.dat等模型文件图片文件夹清晰展示灰度化、边缘检测、二值化等处理过程方便对照学习。现有219人学习下载可作为理解数字图像处理原理与OpenCV实战结合的入门范例。需注意项目识别率不高适合在读懂代码基础上自行调试扩展而非直接商用。1. 车牌识别课程作业为什么值得拆开看一遍车牌识别是数字图像处理课程里出现频率最高的综合作业没有之一。一个完整的流程要覆盖图像预处理、边缘检测、形态学操作、轮廓提取、字符分割和模式识别恰好把冈萨雷斯《数字图像处理》里最核心的几章全部串起来。这份基于 Python3 OpenCV 的课设源码自带 tkinter 界面、答辩 PPT以及 svm.dat、svmchinese.dat 两个训练好的分类模型属于典型的「从网上学习后自己组装」的工程代码不算精致识别率也很诚实——不高但整个链路是通的。我推荐两类人拿它当切入点一是刚学完数字图像处理、正愁课程设计选题的在校生源码里的 tmp 文件夹保留了每一步中间结果图做答辩 PPT 时直接可以当流程展示素材二是想理解传统视觉方案非深度学习在车牌识别场景下能力边界的人看这份代码比看论文来得更直观。它不完美正因为不完美才值得把每一块短板的原因挖出来。2. 环境搭建与源码结构先把 OpenCV 工程跑起来2.1 版本选型与安装命令源码的运行环境是 Python3 OpenCV界面层用 tkinter额外依赖三个库。在 Python 3.8–3.10 的虚拟环境下安装命令为pip install numpy pip install pillow pip install opencv-python三个依赖各司其职numpy 提供矩阵运算底座OpenCV 的所有图像数据在底层都以 numpy 数组形式存储车牌区域的裁剪、像素遍历、二值化都依赖它pillow 负责在 tkinter 界面里把 OpenCV 的 BGR 图像转换成 UI 能渲染的 RGB 图像opencv-python 是核心灰度化、边缘检测、形态学、轮廓搜索都在它里面。这里有两个关键的版本坑。第一opencv-python 从 4.5.x 开始SIFT 和 SURF 算法被移到了 opencv-contrib-python 包里如果后续想换特征点匹配方案需要额外安装。第二OpenCV 4.x 的findContours返回值从两个变成了三个image, contours, hierarchy网上大量老教程还是按 OpenCV 3 的写法直接运行会报not enough values to unpack源码里所有涉及轮廓提取的位置都要按新版写法处理。提示如果 import cv2 报ModuleNotFoundError: No module named cv2优先检查当前 Python 环境是不是虚拟环境以及 pip 是否指向了正确的解释器。用which python确认环境再装依赖比反复重装 opencv-python 有效得多。2.2 源码文件功能映射把 zip 解压后项目结构分成了几个清晰的模块。我拆开看了每个.py文件的职责先建立整体认知再逐个深入文件职责关键依赖main.py程序入口tkinter 界面搭建与事件绑定tkinter, PILimg_recognition.py车牌定位、字符分割、识别主流程cv2, numpyimg_math.py图像灰度化、二值化、边缘检测等基础运算cv2, numpyimg_function.py预处理封装与图像显示辅助函数cv2, PILchuli.py图像处理过程可视化输出中间结果到 tmp/cv2, numpysvm.dat / svmchinese.dat预训练的 SVM 分类模型字符识别用cv2.ml这种拆分方式本身就有教学价值main.py 不碰算法细节只负责界面交互img_recognition.py 做业务编排img_math.py 和 img_function.py 把 OpenCV 操作包了一层。课程设计答辩时老师问「每个文件在做什么」按这张表答就非常清楚。缺点同样明显文件之间的函数调用没有统一的工具类封装全局变量用得多后续想扩展识别省份或者加颜色识别会比较难受。2.3 从 main.py 理解界面启动逻辑启动程序的入口是 main.py它的核心结构如下import tkinter as tk from tkinter import filedialog, Label, Button from PIL import Image, ImageTk import cv2 from img_recognition import recognize_license_plate class PlateApp: def __init__(self, root): self.root root self.root.title(车牌识别系统) self.panel Label(root) self.panel.pack() self.btn_open Button(root, text选择图片, commandself.open_image) self.btn_open.pack() def open_image(self): path filedialog.askopenfilename(filetypes[(Images, *.jpg *.png)]) if not path: return img cv2.imread(path) result_text recognize_license_plate(path) self.show_result(img, result_text) def show_result(self, img, text): img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_pil Image.fromarray(img_rgb) img_tk ImageTk.PhotoImage(img_pil) self.panel.config(imageimg_tk) self.panel.image img_tk # 识别结果用 Label 显示此处省略后续控件代码这段代码有两点值得注意。一是recognize_license_plate(path)接受的是图片路径而不是 numpy 数组因为识别流程内部要多次cv2.imread读取不同处理阶段的图传路径比传数组对象更省内存。二是self.panel.image img_tk这一行不能省略——tkinter 的垃圾回收机制会回收未被引用的 PhotoImage 对象不赋值给 self 会导致图片闪一下就消失。这在 tkinter 图像应用里是高频踩坑点。3. 车牌定位核心预处理与形态学筛选的配合3.1 车牌定位为什么不能只靠轮廓车牌识别里最难的不是字符识别而是定位。自然场景下图像里有大量干扰车身线条、路面文字、树荫、路灯杆边缘检测会把它们全部激活。如果直接findContours再按面积筛得到的候选框会有几百个计算量大且误检率极高。常见的做法是「先肤色再边缘再形态学再用轮廓几何特征过滤」四步走每步都在降低候选区域的数量级。HSV 颜色空间过滤是第一步。蓝色车牌在 HSV 下的 H 通道集中在 100–124利用cv2.inRange可以快速把非蓝色区域压掉。但纯颜色方案对白色车身上的蓝色贴纸也会误判所以源码的做法是让颜色和边缘信息互补颜色过滤拿到候选掩膜Sobel 边缘拿到结构信息两者做bitwise_and后候选区域会大幅收敛。3.2 Sobel 边缘检测为何比 Canny 更合适车牌上有一串字符字符的垂直笔画会产生密集的竖直边缘响应。Canny 是双阈值完整边缘提取会把字符轮廓、车牌边框、铆钉全变成闭合边缘反而让后续形态学运算难以形成统一的矩形连通域。Sobel 的 x 方向梯度则只强调竖直边缘适合车牌这种「竖笔画密集」的目标。import cv2 import numpy as np def locate_plate(image): # 1. 缩放长边限制在800以内降低计算量同时保留定位精度 h, w image.shape[:2] scale 800.0 / max(h, w) if scale 1.0: image cv2.resize(image, (int(w * scale), int(h * scale))) # 2. 颜色过滤蓝牌 HSV 范围V 通道适当放宽应对光照不均 hsv cv2.cvtColor(image, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, (100, 80, 60), (124, 255, 255)) blue cv2.bitwise_and(image, image, maskmask) # 3. 灰度 高斯模糊ksize 取 (5,5)过小保留噪声过大会抹掉笔画 gray cv2.cvtColor(blue, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) # 4. Sobel 检测竖直边缘ksize3 是梯度计算的常规窗口 sobel_x cv2.Sobel(blurred, cv2.CV_64F, 1, 0, ksize3) abs_sobel np.absolute(sobel_x) sobel_8u np.uint8(abs_sobel / 255.0 * 255.0) # 5. 大津二值化自适应阈值免去手动调阈值的麻烦 _, binary cv2.threshold(sobel_8u, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 6. 形态学闭运算先膨胀再腐蚀把分散的字符笔画连接成块 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 7. 轮廓筛选按面积和宽高比锁定车牌区域 contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) plate_area None for cnt in contours: x, y, w, h cv2.boundingRect(cnt) area w * h ratio w / h # 标准车牌宽高比约 3:1允许 2.2~4.5 的浮动 if area 2000 and 2.2 ratio 4.5: plate_area (x, y, w, h) break return plate_area, closed这段代码里几个参数值得细说。高斯模糊核取(5,5)是实践中性价比最高的选择(3,3)对噪声的抑制不足(7,7)虽然更平滑但会削弱 Sobel 的梯度响应。形态学闭运算的核(17,5)是一个横向长条因为车牌是横向矩形字符笔画之间的空隙主要沿 x 方向分布横向长的核才能在闭运算时把相邻字符「粘合」成一个连通域竖向上 5 像素足够覆盖笔画内部的小断裂。3.3 定位失败的常见原因对照表失败现象直接原因解决方向候选框包含整块车身HSV 蓝色阈值过宽深蓝色车身未被过滤收窄 V 通道范围或加 Cb/Cr 色彩空间辅助判断车牌区域断裂成多块形态学核宽 17 不够字符笔画未能连通增大 x 方向核尺寸到 21–25注意不要连到车身边缘图整体偏亮Sobel 阈值化前未归一化用cv2.normalize替代直接np.uint8截断定位框偏移半个字符闭运算后车牌与背景粘连在二值图上先做一次腐蚀再重新取外轮廓源码里tmp/文件夹恰好保留了这每步的中间结果图遇到定位失败时拿失败图跟 tmp 下的正常过程图一张一张对比很容易定位到哪一步开始发散。这是我调试这类代码最快的方法。4. 字符分割与识别从连通域到 SVM 推理4.1 投影法与连通域法怎么选车牌定位得到区域后下一步是切出 7 个字符。这块有两种经典方案垂直投影法和连通域分析法。垂直投影法把二值化的车牌按列累加白色像素投影值为零的列就是字符间隙根据间隙位置切分连通域法直接找每个字符的轮廓。课程设计场景下推荐投影法因为车牌字符间没有粘连且间距相对均匀投影法实现简单排错直观。拿到车牌区域后第一步是透视校正。牌照在画面中往往有倾斜直接用矩形裁剪会导致字符被拦腰切断。源码里通过cv2.findContours找车牌四边形的四个角点再做透视变换。如果输入图里车牌基本水平可以跳过透视校正直接进字符分割这会省掉不少调试时间答辩时再补上校正逻辑即可。4.2 垂直投影分割 SVM 识别代码以下代码是字符分割与识别的核心流程基于 OpenCV 内置的 SVM 模块import cv2 import numpy as np def segment_and_recognize(plate_img): # plate_img 是从原图裁剪出的车牌彩色图 gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) # 车牌区域背景较亮字符较暗需反转使字符为白色 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 水平投影先去掉车牌上下边框避免干扰垂直投影 h_proj np.sum(binary, axis1) // 255 rows_white np.where(h_proj 5)[0] if len(rows_white) 0: return None top, bottom rows_white[0], rows_white[-1] binary binary[top:bottom, :] # 垂直投影统计每列白色像素数找字符边界 v_proj np.sum(binary, axis0) // 255 col_white np.where(v_proj 3)[0] if len(col_white) 0: return None # 连续白色区域的起止位置即为字符左右边界 char_rects [] start col_white[0] prev col_white[0] for c in col_white[1:]: if c - prev 3: # 列间断裂超过3像素视为字符间隙 if prev - start 8: # 宽度过小的区域是噪声点丢弃 char_rects.append((start, prev)) start c prev c char_rects.append((start, prev)) # 限制仅取前7个有效字符区域按宽度中位数筛除铆钉等杂点 if len(char_rects) 7: return None widths [r[1] - r[0] for r in char_rects] median_w np.median(widths) valid [r for r in char_rects if r[1] - r[0] median_w * 0.5] # 加载预训练 SVM 模型做字符分类 svm cv2.ml.SVM_load(svm.dat) svm_chinese cv2.ml.SVM_load(svmchinese.dat) result [] for i, (x1, x2) in enumerate(valid[:7]): char_img binary[:, x1:x2] # 字符图像缩放到 20x40保持宽高比填充 char_resized cv2.resize(char_img, (20, 40)) char_flat char_resized.flatten().astype(np.float32) / 255.0 char_flat char_flat.reshape(1, -1) # 第一个字符是省份汉字用 svmchinese 模型其余用 svm 模型 if i 0: _, pred svm_chinese.predict(char_flat) else: _, pred svm.predict(char_flat) result.append(int(pred[0][0])) return result这段代码的工程考量集中在三个地方。垂直投影时先做水平投影裁剪是因为车牌上下边缘会在二值化后形成两条连续白线如果不先裁掉水平方向上的字符边界信息会被边框淹没。间隙判定用c - prev 3而不是简单的 0是为了容忍字符边缘的不规则毛刺。字符宽度筛选用中位数而不是均值因为均值容易被个别特大噪点区域拉偏中位数对离群值不敏感。4.3 SVM 训练数据特征说明svm.dat 和 svmchinese.dat 是训练好的模型特征向量是 20x40 的像素展开。这意味着分类器只用了原始的灰度特征没接 HOG。对课程设计来说这个选择可以接受但识别率上不去的主因也在这里——20x40 的原始像素对字体形变、笔画粗细变化非常敏感。改进方向是在cv2.resize之前先对字符做一次形态学细化把笔画统一到固定宽度再用 HOG 提取 9 bin 方向直方图拼接特征这样在考试场景下能让识别率提高十个百分点。另外注意两个 SVM 的分工第一个字符是省份简称汉字结构复杂且笔画密集单独训练一个 svmchinese.dat 是合理的第 2 到第 7 位是字母和数字用 svm.dat。如果你要跑通代码但手头没有训练好的模型OpenCV 的cv2.ml.SVM_create()配合svm.train(features, cv2.ml.ROW_SAMPLE, labels)大概需要 300 张字符样本才能训出一个可用的汉字分类器建议直接用源码自带的模型文件。5. 从课程设计到答辩tkinter 界面细节与 PPT 演示脚本5.1 tkinter 界面中 OpenCV 图像的正确显示方式源码的pic/文件夹里保留了程序界面截图界面结构是典型的上下布局顶部放原始图像下方放识别结果文字。核心难点在上文代码里已经提到即ImageTk.PhotoImage的引用问题。补充一个实用细节如果想在界面上同时显示处理过程图灰度图、边缘图、定位框标记图不要直接创建多个 Label而是用一个Label不断config(image...)切换配合一个「下一步」按钮遍历 tmp 文件夹里的中间结果图答辩演示效果非常好。import os import cv2 from PIL import Image, ImageTk def show_intermediate_steps(self, img_dirtmp): # 按文件名排序保证展示顺序是处理流程顺序 files sorted(os.listdir(img_dir)) for f in files: img cv2.imread(os.path.join(img_dir, f)) if img is None: continue img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_pil Image.fromarray(img_rgb) img_tk ImageTk.PhotoImage(img_pil) self.panel.config(imageimg_tk) self.panel.image img_tk self.root.update() # 强制刷新界面让每一步可见 time.sleep(0.8) # 留出观察时间答辩时可调到 1.5 秒这里self.root.update()是让 tkinter 立即处理待绘制的 UI 变更如果没有这行所有图片会积压到循环结束后一次性显示中间过程完全看不到。time.sleep的时长建议答辩时手动调整——老师看得快就调到 0.5 秒想看细节就调到 1.5 秒。源码中 tmp/ 文件夹里恰好已经保存了 01.png、02.jpg 这类逐步结果图这个演示脚本可以直接复用。5.2 答辩 PPT 的有效组织方式源码自带的车牌识别.pptx 是答辩时要现场讲的版本。结合这个项目「识别率不高」的现状PPT 的叙述逻辑比页面美观更重要。建议按四页递进第一页放一张原始图和一张最终识别结果的对比用一个成功案例立住「基本功能已完成」的印象第二页放车牌定位的中间结果图重点讲 HSV 过滤与 Sobel 边缘检测的结合直接回应「你最擅长什么」第三页放字符分割的投影图说明字符间隙算法与噪声筛除策略第四页放失败案例和原因分析这是最加分的部分——主动说「这张曝光过度的图定位失败因为 HSV 阈值在 V 通道上适应性不足」比掩盖短板显得专业得多。在答辩问答环节老师大概率会问「如果车牌是黄底黑字你的代码要怎么改」。答案是只在 HSV 阈值处加一组黄色范围(15, 80, 60)到(35, 255, 255)再用cv2.bitwise_or把蓝色掩膜和黄色掩膜合并。这个改动可以提前写在 PPT 备注里现场改代码演示时直接定位到那一行会给你加不少印象分。5.3 识别失败的样本回收技巧程序识别率不高的一个主要原因是字符模板库覆盖面不足。写一个简单回灌脚本把识别错的图片按字符切好存进对应类别目录积累到每个字符 50 张以上后重新训一次 SVM。注意数据集要用真实的车牌图片裁剪不能用程序生成的字符图片否则泛化能力会因为字体差异变得更差。svmchinese.dat是对识别率提升最敏感的文件——汉字样本每类多收集 20 张省份字准确率会有可感知的提升。这是在这个项目基础上继续改进投入产出比最高的动作把车开到不同光线环境拍 500 张照片比调算法参数更有效。本文还有配套的精品资源点击获取
返回列表