简介:这份资源是面向数字图像处理初学者与进阶学习者的课程设计参考包,以Python3结合OpenCV实现车牌识别,并用tkinter搭建可视化界面,可作为课程作业、大作业或工程实训的起步项目。压缩包共19个文件,约4.74MB,包含5个py源码文件、5个png与3个jpg界面及车牌图片、2个dat训练数据、1份pptx答辩演示文稿,以及md说明文档等,目录中数字图像处理过程与车牌样本分开存放,便于对照理解识别流程。目前已有219人学习下载。读者可从中获得完整的车牌识别实现思路、图像预处理与字符分割的代码参考、SVM识别模型数据,以及验收答辩用的PPT框架,适合在读懂代码的基础上自行调试、修改功能并逐步提升识别效果。
1. 车牌识别课程作业:从一张糊图到能跑通的 Python+OpenCV 方案
课程作业里做车牌识别,最尴尬的不是算法不会,而是拿一张手机拍的、带反光、车牌还歪着的照片,跑出来的框比车牌大两倍,字符分割出来全是噪点。这个标题——基于 Python+OpenCV 实现的数字图像处理课程作业车牌识别——本质上就是一套用经典图像处理链路把车牌从背景里抠出来、再切出字符、最后识别出号码的完整流程,配套源码和答辩 PPT。它适合两类人:一是正在赶数字图像处理课程设计、需要一份能讲清楚原理又能跑出结果的作业;二是刚学完 OpenCV 图像处理、想找一个综合项目把灰度化、滤波、边缘检测、形态学、轮廓筛选串起来练手的人。整套方案不依赖深度学习框架,纯 CPU 就能跑,代码量可控,答辩时每一步都能对应到课本上的知识点,这是它最大的价值。
2. 车牌识别的图像处理链路:为什么先做边缘和形态学,而不是直接上 OCR
2.1 经典链路的五个阶段与选型理由
一套能跑通的车牌识别,图像处理部分通常拆成五步:预处理、边缘/纹理增强、形态学定位、车牌校正与裁剪、字符分割。每一步都有明确的输入输出,答辩时按这个顺序讲,逻辑最顺。
预处理阶段做灰度化和去噪。彩色图对车牌定位没有额外帮助,反而三通道计算量翻三倍,所以先转灰度。去噪用高斯滤波,核大小一般取 5×5,太大车牌边缘会被抹掉,太小椒盐噪声去不干净。这里有个容易忽略的点:如果原图分辨率很高(比如 4000×3000),直接做形态学会非常慢,通常先缩放到宽 1000 左右再处理,定位到车牌后再按比例映射回原图裁剪,这样速度和精度都能兼顾。
边缘增强阶段,Sobel 算子对水平边缘敏感,适合提取车牌字符的竖向笔画;Canny 边缘检测更完整,但参数敏感。课程作业里常见做法是先用 Sobel 求 x 方向梯度,再做二值化,这样车牌区域的字符边缘会形成密集的竖向纹理,背景区域则比较干净。
形态学定位是核心。车牌字符经过边缘增强后是一堆离散的短竖线,用闭运算(先膨胀后腐蚀)把同一行的字符连成一个矩形块。这里结构元素的形状和尺寸直接决定成败:矩形结构元素比椭圆更适合车牌,宽度一般取 17 左右(对应字符间距),高度取 5 左右。闭运算之后再做一次开运算去掉小噪点,然后找轮廓,按宽高比(国内蓝牌约 440:140,即 3.14 左右)和面积筛选。
车牌校正与裁剪阶段,如果车牌有倾斜,用最小外接矩形(minAreaRect)拿到角度,做仿射变换转正。裁剪时向外扩几个像素,避免字符被切掉。
字符分割阶段,对裁剪后的车牌做二值化,然后垂直投影:统计每一列的白色像素数,字符之间的间隙处投影值接近零,据此切出 7 个字符。新能源车牌 8 位,需要单独处理。
2.2 用 OpenCV 跑通定位的最小代码
下面这段代码是定位部分的最小可运行版本,输入一张车辆图片,输出候选车牌区域的裁剪图。依赖只有 opencv-python 和 numpy。
import cv2 import numpy as np def locate_plate(image_path): # 读取并缩放,控制处理分辨率 img = cv2.imread(image_path) if img is None: raise FileNotFoundError(f"无法读取图片: {image_path}") scale = 1000.0 / img.shape[1] img_resized = cv2.resize(img, (1000, int(img.shape[0] * scale))) # 灰度化 + 高斯去噪 gray = cv2.cvtColor(img_resized, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (5, 5), 0) # Sobel 求 x 方向梯度,突出字符竖向边缘 sobel_x = cv2.Sobel(gray, cv2.CV_16S, 1, 0, ksize=3) abs_x = cv2.convertScaleAbs(sobel_x) # 二值化,阈值用 Otsu 自动选取 _, binary = cv2.threshold(abs_x, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 形态学闭运算连接字符,再开运算去噪 kernel_close = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel_close) kernel_open = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) opened = cv2.morphologyEx(closed, cv2.MORPH_OPEN, kernel_open) # 找轮廓并按宽高比筛选 contours, _ = cv2.findContours(opened, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates = [] for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) if h == 0: continue ratio = w / float(h) area = w * h # 蓝牌宽高比约 2.5~4.5,面积过滤掉过小区域 if 2.5 < ratio < 4.5 and area > 2000: candidates.append((x, y, w, h)) # 按面积从大到小返回候选 candidates.sort(key=lambda b: b[2] * b[3], reverse=True) return img_resized, candidates if __name__ == "__main__": img, boxes = locate_plate("car.jpg") for i, (x, y, w, h) in enumerate(boxes[:3]): crop = img[y:y+h, x:x+w] cv2.imwrite(f"plate_candidate_{i}.jpg", crop) print(f"候选 {i}: 位置=({x},{y}), 尺寸={w}x{h}, 宽高比={w/h:.2f}")逻辑说明:先缩放控制计算量,灰度化和高斯滤波是标准预处理。Sobel 的 ksize=3 是速度和精度的折中,ksize=5 边缘更粗但慢。Otsu 阈值适合光照均匀的场景,如果图片明暗差异大,需要改用自适应阈值。闭运算的 (17,5) 结构元素是经验值,17 对应字符间距,5 对应字符笔画宽度,实际调参时可以先可视化 closed 图再决定。宽高比 2.5~4.5 覆盖了蓝牌和部分黄牌,面积阈值 2000 是在 1000 宽度下的经验值,图片缩放比例变了要同步调整。
参数说明:scale控制处理分辨率,1000 是速度和精度的平衡点;GaussianBlur的 (5,5) 是去噪核,噪声大可以加到 (7,7);Sobel的 ksize 取 3 或 5;threshold用 Otsu 自动阈值,也可以手动指定 100~150 之间的值;kernel_close的 (17,5) 是最关键的参数,车牌定位失败八成是这里不对;ratio范围根据车牌类型调整,新能源绿牌比例接近 3.0,也在范围内。
2.3 字符分割的投影法实现与参数
定位到车牌后,字符分割用垂直投影最稳。先把车牌转灰度、二值化,然后逐列统计白色像素数,找到投影值低于阈值的列作为切分点。
def split_characters(plate_img): # 车牌图转灰度并二值化 gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 垂直投影:统计每列白色像素数 projection = np.sum(binary == 255, axis=0) # 找字符区间:投影值大于阈值的连续列 threshold = max(projection) * 0.15 # 动态阈值,避免固定值失效 chars = [] in_char = False start = 0 for i, val in enumerate(projection): if val > threshold and not in_char: in_char = True start = i elif val <= threshold and in_char: in_char = False if i - start > 5: # 过滤过窄的噪声区间 chars.append((start, i)) # 按宽度排序,取前 7 个(蓝牌)或 8 个(新能源) chars.sort(key=lambda c: c[0]) return binary, chars if __name__ == "__main__": plate = cv2.imread("plate_candidate_0.jpg") binary, chars = split_characters(plate) print(f"分割出 {len(chars)} 个字符区间") for i, (s, e) in enumerate(chars): cv2.imwrite(f"char_{i}.jpg", binary[:, s:e])逻辑说明:动态阈值max(projection) * 0.15比固定阈值更鲁棒,因为不同图片的投影幅值差异很大。i - start > 5过滤掉宽度小于 5 像素的噪声区间,这个值根据车牌图分辨率调整,如果车牌裁剪图很小,可以降到 3。字符区间按左边界排序后,蓝牌取前 7 个,新能源取前 8 个,多余区间通常是车牌边框或螺丝孔。
参数说明:二值化用 Otsu 自动阈值,如果车牌有反光导致二值化不干净,可以先做一次直方图均衡化;threshold系数 0.15 是经验值,字符清晰可以降到 0.1,噪声多可以升到 0.2;i - start > 5的最小宽度过滤,车牌图宽度 200 左右时取 5 合适,宽度 100 左右时取 3。
3. 从源码到答辩:工程结构、依赖安装与 PPT 讲解顺序
3.1 项目目录结构与依赖清单
一份能直接交的课程作业,目录结构要清晰,答辩时老师一眼能看出模块划分。常见做法是:
plate_recognition/ ├── main.py # 主入口,串联定位、分割、识别 ├── locate.py # 车牌定位模块 ├── split.py # 字符分割模块 ├── recognize.py # 字符识别模块(模板匹配或简单分类) ├── utils.py # 绘图、保存、参数配置 ├── data/ │ ├── test_images/ # 测试图片 │ └── templates/ # 字符模板(如果做模板匹配) ├── requirements.txt └── README.md依赖只有三个:opencv-python、numpy、matplotlib(可选,用于答辩展示中间结果)。requirements.txt 写:
opencv-python>=4.5.0 numpy>=1.19.0 matplotlib>=3.3.0安装命令:
pip install -r requirements.txt如果遇到ModuleNotFoundError: No module named 'cv2',说明 opencv-python 没装成功,先确认 pip 版本,再试pip install opencv-python --upgrade。Windows 下如果报 DLL 加载失败,装一下 Visual C++ Redistributable。Linux 下如果报libGL.so.1找不到,装libgl1-mesa-glx。
3.2 答辩 PPT 的讲解顺序与每页要点
答辩 PPT 不需要花哨,按「问题—方法—结果—分析」四段走,10~12 页足够。第一页标题和姓名学号。第二页讲问题背景:车牌识别在停车场、交通监控里的应用,课程作业为什么选这个题。第三页讲整体流程,放一张流程图(用 PPT 自带形状画,不要用 mermaid)。第四页到第八页按五个阶段分别讲:预处理展示灰度化和滤波前后的对比图;边缘增强展示 Sobel 和 Canny 的效果差异;形态学定位展示闭运算前后的二值图,重点讲结构元素尺寸怎么定的;字符分割展示垂直投影曲线和切分结果;识别部分展示模板匹配或简单分类的结果。
第九页放失败案例:倾斜车牌、反光车牌、夜间车牌各一张,说明当前方案的边界。这一页反而是加分项,老师更看重你知不知道自己方法的局限。第十页放准确率统计:在多少张测试图上,定位成功多少张,分割成功多少张,识别成功多少张,用表格呈现。第十一页讲改进方向:倾斜校正用透视变换、识别用 CNN、多车牌场景用聚类。第十二页致谢。
每页文字不超过 6 行,图占 60% 以上。答辩时不要念 PPT,指着图讲每一步在做什么、参数为什么这么设。
3.3 识别环节的两种低成本方案
课程作业里识别环节通常不做深度学习,两种低成本方案:模板匹配和简单特征分类。
模板匹配:提前把 0-9、A-Z、省份简称的字符模板切成小图,识别时把分割出的字符缩放到模板同尺寸,算归一化相关系数,取最高分对应的字符。优点是代码简单、可解释;缺点是字体变化、模糊、倾斜时准确率掉得快。
简单特征分类:统计字符的笔画数、投影特征、孔洞数,用规则判断。比如「0」有一个孔洞,「8」有两个,「1」没有孔洞且投影窄。这种方法对清晰车牌有效,但规则写起来繁琐。
实际课程作业里,模板匹配更常见,因为模板可以自己生成,答辩时能讲清楚匹配过程。如果时间充裕,可以用 sklearn 的 KNN 或 SVM 对字符的 HOG 特征做分类,准确率比模板匹配高,代码量也不大。
4. 车牌识别踩坑记录:定位框偏移、字符粘连、反光失效怎么排查
4.1 定位框比车牌大一圈或小一圈
现象:画出来的矩形框明显偏离车牌,要么把车标也框进去,要么只框到车牌一半。
原因:形态学闭运算的结构元素尺寸不对。结构元素太宽,相邻字符连成一片后和背景粘连,轮廓变大;结构元素太窄,字符连不起来,轮廓碎成多个小块,筛选时被面积阈值过滤掉。
解决:把闭运算后的二值图用cv2.imshow或cv2.imwrite存下来看,调整kernel_close的宽度。蓝牌在 1000 宽度图片下,宽度 15~20 之间试,高度 3~7 之间试。同时检查宽高比筛选范围,如果车牌是黄牌(比例接近 2.0),要把下限从 2.5 降到 1.8。
4.2 字符分割时两个字符粘在一起
现象:分割出的字符区间数量少于 7 个,某个区间特别宽,里面有两个字符。
原因:车牌二值化后字符之间的间隙被填满,垂直投影在间隙处没有降到阈值以下。常见于车牌有污损、反光,或者二值化阈值选得偏低导致笔画变粗。
解决:先对车牌图做一次开运算(结构元素 2×2 或 3×3),把字符之间的细连接断开。如果还不行,改用连通域分析:对二值图做connectedComponentsWithStats,按连通域宽度筛选,宽度接近单个字符宽度的保留,宽度是两倍以上的尝试从中间切开。另外,二值化前做一次直方图均衡化,能改善反光导致的对比度不均。
4.3 夜间或强反光图片定位完全失效
现象:白天图片能定位,夜间或车灯反光的图片跑出来全是噪点,没有候选框。
原因:Sobel 边缘检测对光照变化敏感,夜间图片整体偏暗,Otsu 阈值把大量背景也二值化成白色,形态学闭运算后整张图连成一片。
解决:预处理阶段加一步自适应直方图均衡化(CLAHE),提升局部对比度。代码:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) gray = clahe.apply(gray)clipLimit控制对比度增强幅度,2.0 是常用值,太高会放大噪声;tileGridSize是分块大小,8×8 适合 1000 宽度图片。如果反光严重,可以在灰度化之前先用cv2.cvtColor转到 HSV 空间,对 V 通道做 CLAHE 再转回灰度。另外,夜间图片可以先把整体亮度拉高(cv2.convertScaleAbs(gray, alpha=1.5, beta=30)),再做后续处理。
4.4 倾斜车牌裁剪后字符是斜的,分割失败
现象:车牌本身有 10~20 度倾斜,裁剪出来字符跟着斜,垂直投影找不到清晰的间隙。
原因:定位阶段只用了boundingRect(正矩形),没有做倾斜校正。
解决:改用cv2.minAreaRect拿到最小外接矩形和角度,然后用cv2.getRotationMatrix2D和cv2.warpAffine做仿射变换转正。角度处理要注意:OpenCV 返回的角度范围是 [-90, 0),如果角度小于 -45,实际倾斜方向要加 90 度再旋转。转正后再裁剪,字符分割的投影曲线会清晰很多。
4.5 答辩时被问「为什么不用深度学习」
现象:老师问「现在都用 YOLO 了,你为什么还用传统方法」。
原因:这是课程作业,考察的是数字图像处理的知识点,不是模型调参能力。
解决:回答时先承认深度学习在复杂场景下准确率更高,然后说明课程作业的目的是把灰度化、滤波、边缘检测、形态学、投影分割这些课本知识串起来,传统方法每一步都可解释、可调参,适合理解图像处理的底层逻辑。如果老师追问,可以补一句「如果要做产品化,会在定位阶段用 YOLO 做粗定位,再用传统方法做字符分割,两者结合」。
5. 把识别率从 60% 拉到 85% 的三个调参技巧
第一个技巧是动态调整形态学结构元素。固定 (17,5) 只能覆盖一种分辨率下的蓝牌,实际测试时图片尺寸差异很大。我一般会按车牌候选区域的宽度反推结构元素尺寸:先跑一遍粗定位拿到候选框,如果候选框宽度普遍偏大,把闭运算宽度调小 2~3 像素再跑一遍。更稳的做法是写一个循环,宽度从 13 到 21 步进 2,每个值跑一遍定位,取候选框宽高比最接近 3.14 的那组结果。这个循环在 1000 宽度图片上跑 5 次大约 0.3 秒,完全可接受。
第二个技巧是二值化前做通道选择。蓝牌在 HSV 空间的 H 通道值集中在 100~124 之间,直接对 H 通道做阈值分割,比灰度图二值化更干净。代码:
hsv = cv2.cvtColor(plate_img, cv2.COLOR_BGR2HSV) h_channel = hsv[:, :, 0] _, binary = cv2.threshold(h_channel, 100, 255, cv2.THRESH_BINARY_INV)THRESH_BINARY_INV是因为蓝牌 H 值在 100~124,取反后车牌区域变白,字符变黑,投影时统计白色像素就是统计车牌背景,字符间隙反而变成投影低谷,分割逻辑要对应调整。这个技巧对蓝牌效果明显,黄牌要换 H 范围(20~35)。
第三个技巧是识别阶段加拒识。模板匹配时,如果最高分低于某个阈值(比如 0.6),不要强行输出字符,标记为「?」。这样虽然识别率数字会降,但错误率大幅下降,答辩时统计「正确识别 / 总字符数」反而更高。我一般会在recognize.py里加一个confidence_threshold参数,默认 0.6,低于这个值返回?。测试时先跑一遍看分数分布,再决定阈值。
最后一个习惯:每次调参前先把中间结果存图。定位阶段存binary、closed、opened三张图,分割阶段存投影曲线图。翻车的时候不用重新跑代码,直接看哪一步的图不对,就能定位到是哪个参数的问题。这个习惯帮我省了至少一半的调试时间。希望帮到你。
本文还有配套的精品资源,点击获取