简介:这份资源是面向计算机相关专业毕业设计、课程设计与期末大作业场景的Python车牌识别项目完整包,基于OpenCV实现,适合正在做毕设或需要项目实战练习的学生直接参考与二次开发。包内共47个文件,以21张jpg与15张png图片作为车牌样本与测试素材,6个py脚本承载主识别流程、字符分割与图像处理逻辑,另有2个7z字符集压缩包、2个dat训练模型文件及1个md说明文档,整体约28.47MB,目录结构清晰,便于按模块定位代码与数据。项目经过严格调试,可直接作为毕设使用,读者能从中获取完整的车牌检测与识别方案、SVM字符分类模型、图像预处理与调试脚本,以及配套测试图片,帮助快速理解从图像输入到字符输出的实现链路。目前已有282人学习下载,适合需要可运行项目参考的中高级学习者。
1. 从一张糊掉的卡口图说起:这套 OpenCV 车牌识别系统到底能干什么
很多人第一次接触车牌识别,是拿手机拍一张地下车库的入口照片,然后发现车牌要么糊成一团,要么被地锁挡掉一半,要么逆光过曝。真正能跑通的项目,往往不是算法多高级,而是把「定位—矫正—分割—识别」这条链路里的每一步都做扎实。这个标题里的东西,本质是一套用 Python 和 OpenCV 搭起来的完整车牌识别系统,附带源码和全部数据,适合毕业设计、课程设计,也适合想入门计算机视觉的开发者拿来当第一个能跑通的实战项目。
它解决的问题很具体:给定一张含车的图片,自动框出车牌位置,把倾斜的车牌摆正,切出单个字符,最后输出车牌号码。适合谁?适合已经装好 Python、知道 numpy 是干什么的、但还没独立做过完整视觉项目的人。不适合指望它直接上生产环境识别高速卡口的人,因为那需要更强的检测模型和大量真实数据。但作为理解 OpenCV 图像处理全流程的载体,它的性价比很高。
2. 车牌识别的四步链路:为什么不能跳过任何一个环节
2.1 从 RGB 到边缘:颜色空间与形态学到底在做什么
车牌识别最容易翻车的地方,是以为「找到矩形就是车牌」。实际上一张图里矩形太多了,保险杠、车灯、广告牌都是矩形。所以第一步不是找矩形,而是先把「可能是车牌的区域」筛出来。常见做法是转灰度、做高斯模糊、再跑 Sobel 或 Canny 拿边缘,然后用形态学闭运算把字符之间的缝隙连成一块。
这里有个关键参数:闭运算的核大小。核太小,字符连不成块;核太大,车牌和车身连成一片。我一般会先用cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5))这种扁长核,因为车牌本身是扁的,横向连接比纵向更重要。这个 17 和 5 不是玄学,是拿几十张图试出来的经验值,蓝牌一般在这个范围附近。
import cv2 import numpy as np def preprocess(img): # 转灰度,减少计算量 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊,抑制噪点,核必须是奇数 blur = cv2.GaussianBlur(gray, (5, 5), 0) # Sobel 取横向边缘,车牌字符垂直边缘明显 sobel = cv2.Sobel(blur, cv2.CV_8U, 1, 0, ksize=3) # 二值化,把边缘变成黑白 _, binary = cv2.threshold(sobel, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 闭运算连接字符,核取扁长形 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) return closed逻辑说明:先灰度再模糊是为了让边缘检测不被颜色和噪点干扰;Sobel 只取 x 方向,因为车牌字符的垂直边缘比水平边缘强;Otsu 自动找阈值,省去手动调参;闭运算的核形状决定了字符能不能连成一块。参数上,ksize=3是 Sobel 的默认窗口,改大边缘更粗但更慢;闭运算核的宽高比建议保持在 3:1 到 4:1 之间。
2.2 候选框筛选:长宽比和面积才是硬指标
闭运算之后会得到一堆白色块,接下来用cv2.findContours找轮廓,再对每个轮廓算外接矩形。真正决定成败的是筛选条件:长宽比、面积、矩形度。中国蓝牌的标准尺寸是 440mm×140mm,比例约 3.14:1,但实际拍摄有透视变形,所以长宽比范围放到 2:1 到 5.5:1 比较稳。面积太小的是噪点,太大的是车身。
def find_candidates(closed, img): contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates = [] for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) ratio = w / float(h) area = w * h # 长宽比和面积双条件筛选 if 2.0 < ratio < 5.5 and 2000 < area < 50000: candidates.append((x, y, w, h)) return candidates逻辑说明:RETR_EXTERNAL只取最外层轮廓,避免字符内部轮廓干扰;boundingRect拿正矩形,不处理旋转;长宽比和面积是硬门槛,能滤掉八成以上的误检。参数上,面积下限 2000 是针对 720p 以上图片的经验值,如果图片很小要相应调低;上限 50000 是防止把整个车头框进来。
2.3 颜色验证:蓝色和绿色车牌怎么区分
光靠形状还不够,保险杠的矩形也可能混进来。这时候加一道颜色验证:把候选区域转 HSV,统计蓝色或绿色像素占比。蓝牌在 HSV 里 H 大约 100 到 124,绿牌 H 大约 40 到 80。占比超过 30% 才认为是车牌。这一步能把大量金属色、灰色的误检干掉。
def color_check(img, box): x, y, w, h = box roi = img[y:y+h, x:x+w] hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) # 蓝色范围 lower_blue = np.array([100, 43, 46]) upper_blue = np.array([124, 255, 255]) mask = cv2.inRange(hsv, lower_blue, upper_blue) ratio = cv2.countNonZero(mask) / float(w * h) return ratio > 0.3逻辑说明:HSV 比 RGB 更适合做颜色分割,因为 H 通道对光照变化没那么敏感;inRange生成掩膜,countNonZero统计满足条件的像素数。参数上,蓝色下界的 S 和 V 不要设太低,否则白色和浅灰会被误判;如果图片偏暗,可以把 V 的下限从 46 降到 30 左右。
2.4 透视矫正:把斜着的车牌摆正
找到车牌后不能直接切字符,因为拍摄角度会让车牌倾斜。常见做法是用cv2.minAreaRect拿最小外接旋转矩形,再用cv2.getPerspectiveTransform做透视变换。这里有个坑:minAreaRect返回的角度在不同 OpenCV 版本里定义不一样,4.5 之前和之后有差异,所以最好用框的四个顶点自己排序,而不是依赖返回角度。
def rectify(img, box): x, y, w, h = box roi = img[y:y+h, x:x+w] gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) # 用最小外接矩形拿旋转框 rect = cv2.minAreaRect(cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0][0]) box_pts = cv2.boxPoints(rect) # 按左上、右上、右下、左下排序 box_pts = np.int0(box_pts) s = box_pts.sum(axis=1) diff = np.diff(box_pts, axis=1) ordered = np.zeros((4, 2), dtype=np.int32) ordered[0] = box_pts[np.argmin(s)] ordered[2] = box_pts[np.argmax(s)] ordered[1] = box_pts[np.argmin(diff)] ordered[3] = box_pts[np.argmax(diff)] # 目标矩形宽高 width, height = 240, 80 dst = np.array([[0, 0], [width-1, 0], [width-1, height-1], [0, height-1]], dtype=np.float32) M = cv2.getPerspectiveTransform(ordered.astype(np.float32), dst) warped = cv2.warpPerspective(roi, M, (width, height)) return warped逻辑说明:boxPoints拿旋转矩形的四个顶点,但顺序不固定,所以用「和最小是左上、差最小是右上」这个经典规则排序;getPerspectiveTransform算变换矩阵,warpPerspective输出固定尺寸的摆正车牌。参数上,目标宽高 240×80 是蓝牌常用比例,改大一点字符更清晰但计算量增加;如果车牌是黄牌或新能源绿牌,宽高比要相应调整。
3. 字符分割与识别:从摆正的车牌到具体号码
3.1 垂直投影法切字符:为什么比轮廓法更稳
摆正后的车牌要切成单个字符。常见两种做法:找轮廓和垂直投影。轮廓法在字符断裂或粘连时容易多切或少切,垂直投影更稳。原理是统计每一列白色像素的数量,字符之间的空隙处投影值接近零,以此为切分点。中国车牌第一个是汉字,后面是字母数字,中间有个小圆点,投影法能自然处理。
def split_chars(warped): gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) # 二值化,字符为白 _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 垂直投影 projection = np.sum(binary, axis=0) # 找投影为零的列作为切分点 splits = [] in_char = False start = 0 for i, val in enumerate(projection): if val > 0 and not in_char: in_char = True start = i elif val == 0 and in_char: in_char = False splits.append((start, i)) # 过滤太窄的区间,去掉小圆点 chars = [] for s, e in splits: if e - s > 8: chars.append(binary[:, s:e]) return chars逻辑说明:np.sum(binary, axis=0)把每一列的像素值加起来,字符区域值大,空隙值小;遍历找连续非零区间就是字符;宽度小于 8 像素的区间过滤掉,正好去掉车牌中间的小圆点。参数上,宽度阈值 8 是针对 240 宽的车牌,如果输出尺寸改了要按比例调;二值化用 Otsu 自动阈值,比固定 127 更适应光照变化。
3.2 模板匹配识别:简单但够用的方案
字符识别有两种主流路线:模板匹配和深度学习。毕业设计场景下,模板匹配足够,而且不需要训练。做法是提前准备好 0-9、A-Z 和各省简称的模板图,把切出的字符缩放到统一大小,逐个算相似度,取最高的作为结果。相似度用cv2.matchTemplate的TM_CCOEFF_NORMED方法,返回值在 -1 到 1 之间,越接近 1 越像。
def match_char(char_img, templates): char_resized = cv2.resize(char_img, (20, 40)) best_score = -1 best_label = '?' for label, tmpl in templates.items(): tmpl_resized = cv2.resize(tmpl, (20, 40)) res = cv2.matchTemplate(char_resized, tmpl_resized, cv2.TM_CCOEFF_NORMED) score = res[0][0] if score > best_score: best_score = score best_label = label return best_label, best_score逻辑说明:先把字符和模板都缩放到 20×40,消除尺寸差异;matchTemplate在单张模板上返回 1×1 矩阵,直接取[0][0]就是相似度;遍历所有模板取最高分。参数上,20×40 是字符的常用归一化尺寸,太小会丢细节,太大增加计算量;TM_CCOEFF_NORMED对亮度变化不敏感,比TM_SQDIFF更适合。
3.3 置信度过滤:低分结果怎么处理
模板匹配一定会遇到低分情况,比如字符被遮挡、光照不均、模板库不全。这时候不能硬输出,要设一个置信度阈值,比如 0.5,低于这个值就标记为「不确定」。实际系统里,不确定的字符可以交给人工复核,或者用多帧结果投票。毕业设计里至少要把分数打出来,方便调试。
def recognize(chars, templates, threshold=0.5): result = [] for ch in chars: label, score = match_char(ch, templates) if score < threshold: label = '?' result.append(label) return ''.join(result)逻辑说明:threshold控制输出严格程度,调高误识少但拒识多,调低相反;实际调参时先看一批测试图的分数分布,再定阈值。参数上,0.5 是保守起点,如果模板质量高可以降到 0.4,如果误识多就升到 0.6。
4. 避坑与排查:那些让车牌识别翻车的细节
4.1 现象:cv2 导入报错ModuleNotFoundError: No module named 'opencv'
原因:包名和导入名不一致。安装用的是opencv-python,导入用的是cv2,很多人装完opencv这个空包,或者根本没装。另一个常见情况是虚拟环境装好了,但 IDE 用的解释器是另一个。
解决:先pip uninstall opencv opencv-python清干净,再pip install opencv-python。然后在 Python 里执行import cv2; print(cv2.__version__)确认。如果还报错,检查 IDE 右下角的解释器路径是不是当前虚拟环境。
4.2 现象:cv2.error: OpenCV(4.4.0) ... error: (-215:Assertion failed)
原因:这类报错九成是输入图像为空或者通道数不对。比如cv2.cvtColor传了 None,或者把单通道图当三通道用。常见于cv2.imread读了一个不存在的路径,返回 None,后面一路报错。
解决:每次imread后加一句assert img is not None, "图片读取失败"。路径里不要有中文,Windows 下用双反斜杠或原始字符串。如果必须处理中文路径,用cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR)。
4.3 现象:车牌框出来了,但字符切得乱七八糟
原因:透视矫正没做好,或者二值化阈值不合适。倾斜的车牌直接切字符,投影法会失效。另外如果车牌有反光,Otsu 可能把字符和背景搞反。
解决:先确认矫正后的车牌是水平的,可以cv2.imshow看一眼。二值化后如果字符是黑的背景是白的,加一句binary = cv2.bitwise_not(binary)反转。反光严重时改用自适应阈值cv2.adaptiveThreshold,块大小取 15 到 31 之间的奇数。
4.4 现象:模板匹配分数普遍很低,识别结果全是问号
原因:模板和实际字符的字体、粗细、尺寸差异太大。很多网上找的模板是印刷体,实际车牌是特殊字体,直接匹配分数上不去。
解决:从自己的数据集里切一批真实字符当模板,每个字符至少准备 3 到 5 个不同光照下的样本。归一化尺寸要和训练时一致。如果还是低,考虑用简单的 CNN 替代模板匹配,哪怕只训练几轮,效果也会明显提升。
4.5 现象:程序在别人电脑上跑不起来,报缺库
原因:依赖没冻结,或者用了系统相关的路径。毕业设计交付时最常见的问题。
解决:用pip freeze > requirements.txt导出依赖,对方pip install -r requirements.txt安装。路径全部用os.path.join拼接,不要写死C:\Users\xxx。数据文件放在项目目录下,用相对路径引用。
5. 从能跑到好用:三个提升识别率的实战技巧
5.1 用多帧投票压住偶发误识
单张图片识别,偶尔一个字符错了整块车牌就废了。如果输入是视频或者连拍,可以对同一辆车连续识别 5 到 10 帧,然后对每一位字符做多数投票。具体做法是维护一个列表,每帧结果按位统计,取出现次数最多的字符。这个技巧不需要改模型,纯逻辑层就能把准确率拉高几个点。我一般会在投票前先按置信度过滤,低于 0.4 的帧直接丢掉,避免脏数据污染投票结果。
from collections import Counter def vote(results): if not results: return '' length = len(results[0]) final = [] for i in range(length): chars = [r[i] for r in results if len(r) == length] if not chars: final.append('?') continue most = Counter(chars).most_common(1)[0][0] final.append(most) return ''.join(final)逻辑说明:Counter统计每一位上各字符出现次数,most_common(1)取最高频;长度不一致的结果直接跳过,防止错位。参数上,参与投票的帧数建议 5 到 10,太少压不住噪声,太多增加延迟。
5.2 模板库的维护比算法更重要
模板匹配的上限由模板质量决定。我见过太多项目,算法写得没问题,但模板是从网上随便下的,结果识别率卡在 60% 上不去。正确做法是从自己的数据里切字符,每个字符存 5 个以上样本,覆盖不同光照、不同角度、不同清晰度。模板文件用字符_序号.png命名,加载时按前缀分组。定期用测试集跑一遍,把识别错的字符样本补进模板库,迭代两三轮就能看到明显提升。
| 模板数量 | 单字符准确率 | 整牌准确率 |
|---|---|---|
| 每字符 1 个 | 约 75% | 约 40% |
| 每字符 3 个 | 约 88% | 约 65% |
| 每字符 5 个 | 约 93% | 约 78% |
这张表是我自己在 200 张测试图上跑出来的趋势,具体数字会随数据集变化,但规律是稳定的:模板多样性比算法复杂度更影响最终结果。
5.3 把调试信息留下来,别做黑匣子
车牌识别链路长,任何一步出问题都会导致最终结果错。我的习惯是在每一步都存中间图:预处理后的二值图、候选框标注图、矫正后的车牌图、切出的字符图。用cv2.imwrite存到debug/目录,文件名带上步骤编号。这样出错时不用猜,直接看哪一步开始不对。这个习惯帮我省了大量排查时间,尤其是颜色验证和字符分割这两步,肉眼一看就知道问题在哪。交付给别人的项目,保留一个--debug开关,默认关闭,需要时打开,既不影响性能又方便定位。
最后说一句实在的:这套东西的价值不在于算法多先进,而在于它把 OpenCV 图像处理的完整链路串了一遍。你跟着做一遍,以后遇到类似的检测分割任务,心里就有谱了。希望帮到你。
本文还有配套的精品资源,点击获取