简介:面向计算机相关专业学生的数字图像处理课程设计资料,是一套基于Python与OpenCV的车牌识别项目源码,适合用于期末大作业、课程答辩和实战练习。项目覆盖车牌定位、字符分割、字符识别等核心环节,展示了颜色分割、边缘检测、形态学操作以及模板匹配/机器学习等常用方法的工程化写法,能够帮助读者贯通图像处理与模式识别的主要流程。压缩包共18个文件,包含5个Python脚本、若干JPG/PNG测试图像、2个DAT模型数据文件、1份PPT汇报以及README说明文档;其中脚本分别承担图像预处理、车牌定位、字符分割、识别与主流程控制,测试图片用于效果验证,DAT文件保存匹配模型参数,PPT与说明文档便于汇报和快速上手,整体仅4.73MB,目录结构清晰。目前已有74人学习下载;源码经调试可直接运行,既便于对照代码理解车牌识别实现细节,也可作为答辩展示和二次开发的基础。
1. 车牌识别是数字图像处理课设里最不该绕开的一个题
车牌识别(License Plate Recognition,LPR)在数字图像处理课程设计里一直处于选题排行的前列,原因是它的知识覆盖面太契合大纲了:灰度变换、边缘检测、阈值分割、形态学操作、轮廓分析、几何矫正、字符匹配,几乎每个章节都能在同一个项目里落地。使用 Python 和 OpenCV 实现时,整条链路不需要引入任何第三方机器学习框架,传统图像处理手段已经足够完成一次完整的识别闭环,这一点对课程设计来说非常关键。
另一个实际原因在答辩环节。车牌识别项目的中间产物几乎都能可视化,原图带候选框、边缘图、闭运算结果、矫正后的车牌、切分后的字符,每一张都能单独讲一段处理动机。相比之下,纯滤波或纯直方图题目输出单薄,深度学习方案又容易陷入“黑盒调参”,都很难支撑起一场完整的原理讲解。这套看得见、讲得清、改得动的特性,正是评委老师愿意给高分的直接理由。
需要说明的是,网上能搜到的“车牌识别项目源码”大多存在三类问题:预处理参数完全写死,换一张图就找不到车牌;只适配蓝底白字,遇到黄牌、绿牌或倾斜图像直接失效;以及第三步识别环节直接调用现成 OCR 库,把课程设计硬生生做成库调用展示。本文不提供所谓源码包,也不诱导下载,只把一套不依赖商用 SDK 的完整做法从头到尾展开,从预处理、定位、分割到识别与答辩准备,每一步都能直接搬到自己的工程里。
2. OpenCV 预处理链路:灰度化、边缘提取与形态学闭运算
车牌识别的第一步不是找轮廓,而是把原始图像处理成适合提取连通域的形式。多数课设把这一步写成了三行固定代码,换成实际拍摄图就失效,原因在于没有理解每个算子究竟在为什么目标服务。这里的目标非常明确:让车牌字符区域的纹理变成一块“能粘连成整体”的前景,同时把背景中的树枝、车灯、路面标线压下去。
2.1 读图后的尺寸统一与通道检查
OpenCV 的imread对路径中的中文字符支持并不友好,同时不同课程图片尺寸差异极大,一张 2000px 和一张 300px 的图直接进同一套参数,结果一定南辕北辙。我会在管道入口统一做一次输入检查:
import cv2 def load_image(path, max_side=800): img = cv2.imread(path) if img is None: raise FileNotFoundError(f"无法读取图像,请检查路径:{path}") h, w = img.shape[:2] scale = max_side / max(h, w) if max(h, w) > max_side else 1.0 if scale != 1.0: img = cv2.resize(img, (int(w * scale), int(h * scale))) if len(img.shape) == 2: img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) return img这段代码做了三件事:确认图片能读出来;把最长边压到 800 以内,让后续高斯模糊、Sobel 算子的核尺寸有一个统一的尺度参考;灰度图被强制转回三通道,避免后面 HSV 转换时出现维度错误。max_side=800是个经验值,既能保留车牌字符的笔画细节,又不至于让形态学运算在大图上消耗过多时间。
2.2 灰度化与高斯模糊:顺序不能随意调换
车牌定位看起来可以在彩色图上直接做,但彩色图每个像素有三个通道,边缘算子和形态学算子都要逐通道运算,噪声也会按通道各自累积。统一转成灰度图之后,后续所有算子在单通道上执行,参数简化一个量级。转换时必须使用cv2.COLOR_BGR2GRAY,不能直接用 RGB 读取的数组,因为 OpenCV 默认通道顺序是 BGR,直接取数组会得到反色结果。
高斯模糊放在灰度化之后,核选择(5, 5)、标准差传 0。标准差为 0 表示由核尺寸自动计算,这个写法在换图时更稳健。模糊的作用不是“让图好看”,而是抑制高频噪声,避免 Sobel 边缘响应被路面颗粒和 CCD 噪点占据。核太大会把字符边缘连同背景一起抹平,后续边缘检测输出稀疏,核太小则闭运算前噪声先膨胀,候选轮廓数量爆炸。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5, 5), 0)2.3 边缘提取:用 Sobel-x 而不是 Canny 的场合
边缘检测算子选 Sobel 还是 Canny,是预处理链路里最容易被忽略的分歧点。Canny 输出的是完整闭合边缘,对车灯、散热格栅这类高反差物体也会产生大量响应;而 Sobel 在 x 方向求导,只突出垂直走向的边缘。车牌字符的笔画主要由竖直线段构成,比如“京”“A”“B”“0”,它们在强度上表现为垂直边缘,Sobel-x 能同时抓住字符纹理并抑制车头横向的线条。
sobel_x = cv2.Sobel(blurred, cv2.CV_64F, 1, 0, ksize=3) sobel_x_abs = cv2.convertScaleAbs(sobel_x)这里ddepth必须写cv2.CV_64F。原因是车牌区域灰度跨越较大,Sobel 计算结果会超过 8 位有符号数范围,直接存到uint8中会出现截断,边缘变成两条平行虚线。convertScaleAbs再把结果安全映射回 0 到 255。若用 Canny 替代,则要额外过滤水平边缘,下述对比更直观:
| 算子 | 提取方向 | 车牌区域表现 | 复杂背景表现 | 后续处理难度 |
|---|---|---|---|---|
| Sobel-x | 垂直边缘 | 字符竖笔画响应强,区域纹理密集 | 路面横向标线响应弱 | 低,直接二值化即可 |
| Sobel-y | 水平边缘 | 字符横笔画响应弱 | 车盖横向棱线响应强 | 较高,需额外过滤 |
| Canny 全向 | 含全部方向 | 轮廓完整但内部填充不足 | 任意高反差物都响应 | 高,候选框杂而多 |
课设场景中固定使用 Sobel-x 是合理的,因为它天然把车道线、地面阴影这类水平纹理排除在外。表格里的“后续处理难度”是指候选轮廓数量与筛选复杂度,Sobel-x 的初筛结果最少,这是它成为默认选择的核心原因。
2.4 阈值化与闭运算:把字符边缘“焊”成连通块
Sobel-x 输出的是梯度强度图,直接用固定阈值 127 切分会遇到光照不均问题:车头反光区域整体灰度偏高,固定阈值会把背景切成大片白色。常见做法是换adaptiveThreshold,按局部邻域动态决定阈值。
binary = cv2.adaptiveThreshold( sobel_x_abs, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, blockSize=15, C=5 )blockSize必须为奇数,表示参与计算阈值的邻域边长,15 对应约 800px 宽的图像;C=5是阈值偏移量,用于抑制平坦区域的细小波动。随后用闭运算将断裂笔画连接成一个整体外框。核尺寸用getStructuringElement生成:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)核的宽高比 17:5 是刻意设计的。水平方向需要足够长,才能把字符与字符之间的空隙桥接起来;垂直方向不能太长,否则会合并上下边缘的干扰纹理。闭运算是膨胀接腐蚀,膨胀先填充字符间隙,腐蚀恢复原尺寸,此时车牌区域会被“焊”成一个实心长条矩形,而车灯、文字等区域仍保持细碎轮廓,交给第 3 章的轮廓筛选去处理。
3. 车牌区域定位:轮廓几何筛选与 HSV 颜色双路方案
预处理完成后,图像里依旧存在很多前景块,需要从这些连通域中找出“真正的车牌”。定位策略分两路:第一路依据轮廓的几何特征做粗筛,第二路以颜色先验做二次确认。两路结合在课程设计里既展示了对轮廓分析的理解,又能显著提高换图后的鲁棒性。
3.1 用 findContours 拿到候选矩形
OpenCV 3 和 4 的findContours返回值不同,3.x 返回(image, contours, hierarchy),4.x 只返回(contours, hierarchy),几乎所有从旧博客复制来的课设代码都会在这行报错。统一写成以下形式即可兼容主流版本:
contours, _ = cv2.findContours( closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)RETR_EXTERNAL只取最外层轮廓,车牌区域经过闭运算后内部已经填实,外部轮廓足够表达完整位置;CHAIN_APPROX_SIMPLE会压缩水平、垂直方向的冗余点,减少后续boundingRect的计算量。注意输入参数是二值图closed,不是灰度图,否则轮廓会沿灰度渐变产生大量锯齿。
3.2 宽高比、面积占比与轮廓外接框
拿到轮廓后,逐条计算外接矩形的宽、高,并用三项指标筛掉干扰候选区。车牌在规定尺寸下宽高比落在 2.5 到 5.5 之间,面积占整图比例一般在 0.5% 到 15% 之间。这三个阈值是根据车牌实际比例和拍摄距离推算的,换用不同分辨率时依然成立:
candidates = [] img_area = img.shape[0] * img.shape[1] for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) area = w * h aspect = w / max(h, 1) area_ratio = area / img_area if area_ratio < 0.005 or area_ratio > 0.15: continue if aspect < 2.0 or aspect > 5.5: continue candidates.append((x, y, w, h))参数取自车牌的真实长宽规格,而不是猜出来的。我国蓝底白字车牌统一长度 440mm、宽度 140mm,比值约 3.14,加上车牌边缘留白后接近 3.3。考虑到拍摄角度和透视压缩,阈值上下限放宽到 2.0 到 5.5 是合理的。面积下限 0.5% 能筛掉小字、反光点;上限 15% 能挡住远处停着的货车车头。若候选项仍然多于两个,说明预处理阶段噪声过重,应回头调整闭运算核宽度。
以下表格列出滤波项的推荐区间与实际作用:
| 筛选项 | 推荐区间 | 主要排除对象 |
|---|---|---|
| 外接框宽高比 w/h | 2.0 ~ 5.5 | 车灯、车轮、文字块 |
| 面积占比 area/img_area | 0.005 ~ 0.15 | 远处小物、近处大面积色块 |
| 填充率 contour_area/area | 0.3 ~ 1.0 | 镂空边框、细长线条 |
填充率计算方式为cv2.contourArea(cnt) / area,能进一步排除空心圆环类轮廓。放进同一个循环即可,代价为额外约 3 行代码,收益是候选框数量平均减少一半以上。
3.3 HSV 颜色辅助:蓝牌和黄牌的 inRange 参数
几何筛选在背景干净的场景下已经够用,但校园内常见的红砖墙、深色绿植会把候选框数量推高。此时配合颜色信息做第二路验证。RGB 空间受光照影响大,因此转 HSV 后直接用inRange按色相切割:
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) blue_mask = cv2.inRange(hsv, (100, 90, 80), (124, 255, 255)) yellow_mask = cv2.inRange(hsv, (20, 105, 100), (32, 255, 255)) mask = cv2.bitwise_or(blue_mask, yellow_mask)蓝色范围的 H 值开在 100 到 124,避开青色(85 附近)和深紫(130 附近);S 下限取 90 是为了过滤灰暗的蓝色阴影;V 下限 80 把夜间暗部排除。黄色范围 H 取 20 到 32,对应橙黄到黄绿的过渡带。如果课设只做蓝牌,可以只保留blue_mask,但加上黄色分支后,黄牌货车、驾校车也能识别,算法覆盖面明显提升。用得到的mask与候选框做交集计算,当候选框内蓝色或黄色像素占比超过 15% 时,保留该候选框。
3.4 透视矫正:从任意四边形到水平矩形
候选框直接裁剪会得到倾斜图像,字符分割阶段会因此切出歪斜的笔画。常规做法是用minAreaRect求轮廓的最小外接矩形,再执行透视变换:
rect = cv2.minAreaRect(cnt) box = cv2.boxPoints(rect) box = np.int0(box) width, height = int(rect[1][0]), int(rect[1][1]) src = box.astype("float32") dst = np.array([[0, 0], [width, 0], [width, height], [0, height]], dtype="float32") M = cv2.getPerspectiveTransform(src, dst) warped = cv2.warpPerspective(img, M, (width, height))box是四个角点,顺序由boxPoints返回;getPerspectiveTransform要求源点与目标点按相同顺序对应。这里容易踩坑的是rect[1]可能返回负角度,导致宽高互换,可打印rect[1]观察后统一让width = max(rect[1][0], rect[1][1])。透视变换后的warped应是一条水平条形车牌,字符随角度被拉正,此时进入第 4 章的分割环节才有稳定结果。
4. 字符分割与识别:垂直投影、归一化与模板匹配
定位到车牌区域是“找到车牌”,接下来的任务是把车牌拆成一排独立字符,并判断每个字符是什么。数字图像处理课设的考核重点在这一步:字符分割用的是什么策略、识别用的是模板匹配还是分类器、中文汉字怎么处理。这些问题的答案直接决定源码的答辩质量。识别部分不使用任何 OCR 库,是因为课程设计的核心目的是展示图像处理原理,而不是展示库调用。
4.1 车牌区域的二值化与极性判断
warped是彩色图,先转灰度再二值化。车牌底色为蓝或黄,字符为白或黑,不同底色的极性相反。为保证分割代码对两种极性都有效,用 OTSU 自动求阈值,再通过统计前景像素量决定是否反转:
roi_gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) _, roi_bin = cv2.threshold(roi_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) if np.mean(roi_bin == 255) > np.mean(roi_bin == 0): roi_bin = cv2.bitwise_not(roi_bin)THRESH_BINARY + THRESH_OTSU组合写法让 OTSU 自动替代固定阈值。np.mean(roi_bin)的比较判断白色像素是否占主导:当车牌是黄底黑字或白底黑字时,背景占大面积,白色占比高,此时反转为黑底白字。翻转后规则就统一了——字符为白色、背景为黑色,后续投影统计统一只统计白色像素。
4.2 垂直投影法切割字符
车牌上字符之间有空隙,空隙列的前景像素数量显著低于字符列。对二值图沿垂直方向统计每一列的白点数量,形成一条投影曲线,再从曲线中找连续起伏区间即可完成切割:
def split_chars(bin_img, min_width=8): col_sum = np.sum(bin_img > 0, axis=0) chars = [] in_char = False start = 0 for col, val in enumerate(col_sum): if val > 3 and not in_char: in_char = True start = col elif val <= 3 and in_char: in_char = False end = col if end - start >= min_width: chars.append((start, end)) return chars代码的思路是按列扫描,前景像素数大于 3 视为进入字符区,小于等于 3 视为离开字符区。阈值选 3 而不是 0,是为了抗噪:单个噪点列不至于让字符区被切碎。min_width=8过滤掉过窄的干扰列。需要注意的点是汉字中间可能存在笔画断开,例如“京”字的下部横画与上部口字之间有间隙,投影法可能把一个字切成两段。面对这种情况,可以在切割后按高度合并相邻区间:若两段高度都接近整体高度的 80% 以上且中间空隙小于字符平均宽度的 1/3,则合并为同一个字符。
4.3 尺寸归一化与模板匹配识别
字符切割完,将每个小图缩放到统一 40x40 尺寸,然后与模板库逐张做归一化相关匹配。模板库字符包括省份简称汉字、24 个英文字母和 10 个数字,由自己手工收集并预先归一化,数量不多,但足够覆盖课设场景:
char_resized = cv2.resize(char, (40, 40), interpolation=cv2.INTER_AREA) result = cv2.matchTemplate(char_resized, template, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ = cv2.minMaxLoc(result)TM_CCOEFF_NORMED是相关系数归一化,输出范围在 -1 到 1 之间,越接近 1 表示模板与字符越相似。minMaxLoc取出最大值作为该模板的得分。遍历所有模板后取最高分对应的字符作为识别结果。模板必须与输入同为单通道uint8,尺寸一致,否则matchTemplate会报尺寸不匹配或者输出无效结果。
匹配方法的选择直接影响识别稳定性,下面这张表列出三种匹配模式的适用情况:
| 匹配模式 | 对光照敏感性 | 对局部噪声敏感性 | 课设场景推荐度 |
|---|---|---|---|
| TM_SQDIFF_NORMED | 较高 | 较高 | 不推荐 |
| TM_CCORR_NORMED | 高 | 高 | 不推荐 |
| TM_CCOEFF_NORMED | 低 | 中 | 推荐 |
TM_CCOEFF_NORMED在匹配前会减去均值,相当于做了直流分量清除,对整体亮度的变化不敏感。车牌拍摄图像会受到车头反光和阴影干扰,这个特性比另外两种更合适。为增强识别稳定性,可以在模板匹配前加一步cv2.normalize(char_resized, None, 0, 255, cv2.NORM_MINMAX),将对比度拉伸到统一范围。
4.4 加一条先验规则:省份简称与第二位字母强制约束
模板匹配容易把形状相近的字符搞混,比如 0 与 O、1 与 I、8 与 B。课设中最有效的补救不是改进模板,而是利用车牌编号规则做后处理纠错。中国车牌第一位是省份简称汉字,第二位固定是英文字母,后五位由数字和字母混合组成。这串规则可以用约十行代码约束输出:
def refine_result(chars, scores): province = "京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼" if chars[0] not in province: chars[0], scores[0] = correct_best_in_province(chars[0], scores[0]) if not chars[1].isalpha(): chars[1], scores[1] = correct_second_pos(chars[1], scores[1]) return chars其中correct_best_in_province在省份简称模板集合内重新取最高分,correct_second_pos在字母模板集合内重赛。这个规则让第三个位置的“D”不会被误判成数字“0”,因为第二位强制字母;也让第一个汉字只与汉字模板比较,避免“京”被识别成“家”这类低概率错误。评委看到这里会明确感知到你在识别环节做过系统设计,而不是只堆了模板图片。
5. 从“能跑”到“高分”:参数集中管理、失败归因与答辩提分点
前四章把识别管道搭建完整,但课程设计拿高分的关键往往在管道之外。代码能不能换图后稳定运行、参数能不能讲出理由、答辩时能不能主动说明失败案例,这些才是分数拉开差距的地方。最后一章写给准备提交作品的人,内容围绕参数组织、可视化验证和答辩演示三个可执行操作展开。
5.1 用一个配置字典集中管理全部阈值
预处理和定位阶段零散散落了十几个常量,直接写死在函数里,换图时改起来十分痛苦。常见做法是建一个config字典集中管理:
config = { "max_side": 800, "gaussian_ksize": 5, "sobel_ksize": 3, "adaptive_block": 15, "adaptive_c": 5, "morph_kernel": (17, 5), "min_area_ratio": 0.005, "max_area_ratio": 0.15, "min_aspect": 2.0, "max_aspect": 5.5, "blue_hsv_low": (100, 90, 80), "blue_hsv_high": (124, 255, 255), "char_min_width": 8, }再写一个run_pipeline(image_path, config)函数统一接收配置。这样调试时只需要改一张配置表,还能用循环遍历不同核宽度做实验,比较哪组参数在多个测试图上表现最好。代码可维护性在答辩中会被直接评价,“每个参数都标注了调整依据”是评委最愿意看到的注释。
5.2 用失败样本反向定位是哪一层出了问题
识别失败时不要整条链路一起调。先保存预处理输出图closed.png,看车牌区域是否已经连成白色实心块;再保存定位阶段的候选框图,确认车牌是否被boundingRect框住。若候选框偏移,问题在形态学核尺寸和轮廓筛选阈值;若字符切割错位,问题在二值化极性和投影阈值;若只有个别字符匹配错误,则问题在模板库覆盖率。按层定位可以省掉大部分无效调参时间。
5.3 答辩演示中的三种验证方式
第一,准备 8 到 10 张难度递进的测试图,包含清晰蓝牌、倾斜 20 度的车牌、黄牌货车、有绿植遮挡背景的图,逐张运行并记录识别结果。第二,在代码中增加--debug参数,运行时把灰度图、Sobel 图、闭运算图、候选框图逐张弹出窗口,演示时用这组图串讲整个处理流程。第三,写一个简易准确率统计:
correct = sum(1 for pred, gt in zip(predictions, groundtruth) if "".join(pred) == "".join(gt)) accuracy = correct / len(groundtruth)三组动作都不复杂,但能把答辩节奏从“讲源码”引导到“讲实验设计”。调参时优先操作adaptive_block、morph_kernel和min_area_ratio这三项,它们对最终定位结果的影响最明显,也是评委追问概率最高的参数。逐层保存中间图像的调试习惯,比任何模板代码都更能保证你在答辩现场拿到主动。
本文还有配套的精品资源,点击获取