
简介这份资源是面向计算机相关专业毕业设计、课程设计与期末大作业场景的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, ksize3) # 二值化把边缘变成黑白 _, 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 自动找阈值省去手动调参闭运算的核形状决定了字符能不能连成一块。参数上ksize3是 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:yh, x:xw] 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:yh, x:xw] 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(axis1) diff np.diff(box_pts, axis1) ordered np.zeros((4, 2), dtypenp.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]], dtypenp.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, axis0) # 找投影为零的列作为切分点 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, axis0)把每一列的像素值加起来字符区域值大空隙值小遍历找连续非零区间就是字符宽度小于 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, threshold0.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, dtypenp.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 图像处理的完整链路串了一遍。你跟着做一遍以后遇到类似的检测分割任务心里就有谱了。希望帮到你。本文还有配套的精品资源点击获取