Python+OpenCV车牌识别系统:从Sobel定位到SVM分类

发布时间:2026/10/10 16:56:09
Python+OpenCV车牌识别系统:从Sobel定位到SVM分类
简介这是一份基于Python和OpenCV的车牌识别系统完整源码适用于计算机视觉课程设计、期末大作业或入门实战项目。该项目源自实际期末考核任务代码结构清晰注释完整经过严格调试并获导师评审得分达九十六分以上确保可以直接运行。资源压缩包内共包含二十九个文件整体大小约十三点八七兆字节主要涵盖Python源码、车牌与字符图片样本、SVM模型数据、XML配置文件以及若干辅助资源同时还包含大量真实车牌图片便于测试不同场景。目前已有七百二十一人下载学习。通过研读源码与对应数据能够直观理解车牌定位、字符分割与识别流程掌握SVM分类器在车牌字符识别中的实际用法并方便在此基础上继续扩展与改造。1. 基于 PythonOpenCV 的车牌识别系统为什么这份源码能直接复现车牌识别系统在课程设计里算得上经典选题但网上能找到的源码大多分两种要么是深度学习方案环境装到崩溃要么是半成品关键模块只有 TODO。这次拆的这份基于 PythonOpenCV 的车牌识别源码属于第三种——传统计算机视觉路线svm.dat 和 svmchinese.dat 两个模型文件已经训练好predict.py 直接跑就能出结果。整套流程分三步定位车牌、分割字符、SVM 分类每一步都可以在答辩时用一两句话讲清楚原理这也是它能拿到高分的原因。适合正在做期末大作业、又不想在环境上耗两天的从业者也适合想理解传统 OCR 思路的初学者。压缩包里的测试图片覆盖了不同光线和角度先跑通再改比自己从零写要快得多。2. 车牌定位Sobel 边缘检测与形态学闭运算的配合2.1 为什么定位不走颜色筛选拿到一份车牌识别源码我一般先看定位策略这是整套系统的地基。很多新手做车牌定位会优先想到颜色筛选先把图像转到 HSV 空间提取蓝色区域再找轮廓。听起来很直接但实际效果容易被环境光、车身颜色和路面反光干扰。这份源码走的不是颜色筛选而是边缘检测加形态学处理原因在于车牌本身的几何特征足够明显矩形边框、固定宽高比、字符密集排列。只要边缘检测能把这些纹理找出来再用闭运算把断裂的字符边缘连成一个整体车牌的候选框就出来了。颜色信息在后续字符分割时再介入定位阶段不依赖它鲁棒性会好很多。2.2 定位主流程代码与参数说明以 predict.py 里的处理思路为参照定位部分的核心逻辑可以精简成下面这样。实际源码里还包了透视校正和边界扩展这里先把主干拆出来看参数是怎么起作用的。import cv2 import numpy as np def locate_plate(img): # 1. 灰度化后先做高斯模糊把路面噪点压下去 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) # 2. 只取 x 方向梯度车牌字符竖边密集x 方向变化最剧烈 sobel_x cv2.Sobel(blurred, cv2.CV_64F, 1, 0, ksize3) abs_sobel np.uint8(np.absolute(sobel_x)) # 3. 梯度图二值化后做闭运算让字符边缘连成矩形块 _, edge cv2.threshold(abs_sobel, 120, 255, cv2.THRESH_BINARY) kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(edge, cv2.MORPH_CLOSE, kernel) # 4. 找外轮廓按宽高比和面积筛出车牌区域 cnts, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for c in cnts: x, y, w, h cv2.boundingRect(c) if w 80 and 2.5 w / h 5.5: return x, y, w, h return None这段代码里几个参数值得说明一下。高斯核(5, 5)是常规取值核太大容易把字符边缘一起模糊掉Sobel 的ksize3表示使用 3x3 的 Sobel 核CV_64F是因为梯度计算会产生负值需要浮点型承接后面再用np.absolute转回 8 位图。闭运算核(17, 5)的设计有讲究宽度 17 是为了把相邻字符的竖向边缘横向连起来高度 5 则是避免把上下两行车牌边框也粘连。阈值 120 是经验值如果测试图里车体反光严重或者车牌在画面上占比很小这个阈值可能要降到 80 到 100 之间。2.3 候选框过滤宽高比、面积与透视校正找到轮廓之后过滤条件决定了定位精度。标准蓝牌尺寸是 440x140宽高比约 3.14但实际拍摄时因为角度和裁剪这个比值会在 2.5 到 5.5 之间波动。代码里的2.5 w / h 5.5就是给这个波动留的余量。w 80是最小宽度过滤防止路面上一些纹理杂乱的小区域被误判成车牌。只用一个候选框时可以直接 return但如果画面里同时出现多辆车光靠宽高比会选出多个矩形这时候常见做法是再按面积排序取最大的一块。还有一类情况需要特别处理车牌在画面里是倾斜的boundingRect返回的是正矩形会把倾斜车牌的背景也包进来。predict.py 里对候选区域做了进一步的旋转矩形拟合用cv2.minAreaRect拿到中心点、宽高和旋转角度再做透视变换把车牌扶正。这一步直接影响后续字符分割因为倾斜状态下字符的垂直投影会变形切出来的字符会带着多余边缘。如果只是大作业验收正矩形已经够用如果测试图里有明显倾斜的车牌建议把透视校正加上。3. 字符分割连通域分析与 7 位车牌结构约束3.1 二值化方向蓝牌字符是亮的还是暗的定位到车牌区域后下一步是把字符从背景里抠出来。蓝底白字车牌在灰度图里字符比背景亮但这只是理想情况。实际拍摄时车牌的白色字符可能因为反光变成暗色或者黄色车牌的黑色字符本身就是暗色。所以做二值化时不能写死一个方向要先看均值再决定是否反色。常见做法是先用大津法Otsu自动算阈值然后统计二值图里白色像素占比如果白像素超过一半说明当前位图把背景当成了前景直接把整张图取反。这一步写错后面所有字符都会变成黑底白字或白底黑字SVM 输入的特征向量就会完全反掉。def binarize_plate(plate_gray): # 大津法自动阈值避免手动调亮度 _, th cv2.threshold(plate_gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 如果白色像素占多数说明字符是暗色需要反色 if np.mean(th) 127: th cv2.bitwise_not(th) return thcv2.THRESH_BINARY cv2.THRESH_OTSU这种组合写法的含义是忽略第二个参数里填入的固定阈值由大津法根据图像灰度分布自动计算。np.mean(th) 127的判断是基于 8 位图像均值 127 作为中间值来推测当前前景/背景方向。这个判断在字符占比正常的车牌区域里是可靠的因为字符区域大约占车牌面积的 40% 左右白像素占比不会超过一半。如果分割后字符依然模糊可以再叠加一次中值滤波把铆钉和边框残留去掉。3.2 垂直投影分割从列统计到字符区间字符分割最实用的手段是垂直投影统计每一列里前景像素的数量有字符的列数值大字符间隙的列接近 0投影曲线上的谷底就是切分点。但直接找谷底会遇到一个典型问题汉字结构复杂比如“苏”“湘”这类左右结构明显的字中间会出现很窄的断开如果按零值点切一个汉字会被切成两块而数字“1”这类窄字符又可能因为宽度太小被当成噪点吞掉。所以做分割时不能只做一次投影就完事还要结合宽度阈值和车牌总字符数做约束。def split_chars(th): h, w th.shape col_sum np.count_nonzero(th, axis0) segments [] start None for i, v in enumerate(col_sum): if v 0 and start is None: start i elif v 0 and start is not None: if i - start 8: # 太窄的区间视为噪点 segments.append((start, i)) start None # 相邻区间太近就合并防止汉字左右结构被拆开 merged [] for seg in segments: if merged and seg[0] - merged[-1][1] 12: merged[-1] (merged[-1][0], seg[1]) else: merged.append(seg) return mergedi - start 8这个宽度下限是经验值对应归一化后车牌图像里一个字符的最小像素宽度。如果测试图片分辨率较高这个值可以按比例放大到 12 到 16。合并阈值 12 则是为了把“京”“苏”这类左右结构的汉字重新拼起来。实际操作中合并阈值调太大会把两个独立的字符粘在一起调太小汉字还是会断。一个辅助判断方法是看合并后的字符数量蓝牌标准是 7 个字符如果分割结果超过 7 个说明有字符被切开调大合并阈值如果少于 7 个说明有字符被吞掉检查宽度下限是否过高。3.3 汉字切碎问题与合并策略这套源码里训练好的 svmchinese.dat 是针对汉字字符的它期望输入是一个完整的汉字而不是半个偏旁。所以在 3.2 的合并策略上我更推荐再加一道兜底校验在拿到合并后的区间列表后统计区间个数。正常蓝牌结构是 1 个汉字加 1 个字母加 5 位数字/字母共 7 段。如果区间数大于 7就把最窄的相邻两段合并后再统计如果小于 7说明某一段宽度异常可能是把两个字符粘连了此时按车牌字符大致等宽的特点把异常宽的段拆成两段。这个过程写成一个 while 循环直到区间数稳定在 7。这套逻辑不是源码里必须有的部分但我在重训模型时加上它之后字符分割的准确率提升很明显。另外要注意字符切完之后的外扩边距。SVM 训练时输入的字符图如果带边框或者被切掉一部分都会影响分类精度。常见做法是每个字符外扩 2 到 3 个像素把字符放在图像正中附近而不是紧贴边缘。预测时再做一次和训练时完全相同的归一化——缩放尺寸、转灰度、二值化——这一步不统一模型效果会大打折扣。4. SVM 字符识别svm.dat 与 svmchinese.dat 的分工4.1 两个模型的职责边界字符识别是这套系统里相对“黑匣子”的部分但只要搞清楚两个文件的职责就不难。svm.dat 负责的是字母和数字的区分训练样本是 34 类24 个字母去掉 I 和 O因为和数字 1、0 容易混淆加 10 个数字svmchinese.dat 负责的是省份汉字训练样本是各省简称比如京、苏、鲁、粤这类。两个模型分开训练的原因很实际汉字笔画复杂和字母数字混在一起训练特征空间会互相干扰分类器容易偏向字体差异更大的类别。推理时先用车牌第一位字符去查 svmchinese.dat再用后面 6 位去查 svm.dat。模型文件识别对象典型类别数训练数据来源svm.dat字母 A-Z去 I/O 数字 0-934 类train chars2.7zsvmchinese.dat省份汉字简称31 类左右charsChinese.7z这里要特别提醒一点两个模型的特征维度必须和 predict.py 里预处理后的输入维度一致。如果训练时用的是 20x20 灰度图展开成 400 维向量预测时就不能改成 32x32。源码能直接跑通说明模型文件里的维度和代码是对应的但如果你自己重训一定要把训练脚本里的 resize 尺寸记下来否则预测时 OpenCV 会直接报错或者给出完全离谱的分类结果。4.2 调用 SVM 预测的代码与特征归一化在 predict.py 里字符识别不是直接调用一个万能函数而是把分割好的字符逐个送进对应模型。调用方式在 OpenCV 4.x 下通常是这样的import cv2 import numpy as np def svm_predict(cell, model_path, label_dict): model cv2.ml.SVM_load(model_path) # 常见做法统一缩放到 20x20再拉平成 1 行 400 维特征 cell cv2.resize(cell, (20, 20)) feature cell.reshape(1, 400).astype(np.float32) _, result model.predict(feature) return label_dict[int(result[0][0])]这段代码有三个容易忽略的点。第一model.predict返回的是一个元组第一个值是错误率相关的返回值第二个值才是预测结果矩阵很多人第一次写会只取一个返回值导致异常。第二result[0][0]是二维矩阵里的第一个元素必须转成 int 才能作为字典索引浮点数的 3.0 和整数 3 在字典查询里不是一回事。第三label_dict里的键顺序必须和训练时的类别顺序一致这份资源里 charsChinese.7z 解压后的目录名就是参考——目录名即标签排序后和模型输出对齐。4.3 如果要重训7z 样本集的解压与标签对应train chars2.7z 和 charsChinese.7z 这两个压缩包是训练素材不是 Python 的 zipfile 能直接解的标准 zip 格式要用 7-Zip 或者 py7zr 来解。很多人在这一步卡住以为是压缩包损坏其实只是工具不对。解压后你会看到一层层目录每个目录名对应一个字符类别目录里是该字符的样本图。如果你想换掉 svm.dat 重新训练就先遍历目录把每张图 resize 到 20x20、转成灰度、拉平成 400 维特征再把目录名作为标签交给 OpenCV 的cv2.ml.SVM_create()训练。训练时核函数选 RBF 核通常比线性核准确率高但要注意参数别过拟合。常见参数组合是C100、gamma0.01这两个值控制着分类器的容错范围和决策边界的平滑程度。C 太大容易把噪声样本记住测试集上看着 100% 准确换新图片立刻翻车gamma 太大会让每个样本只影响自己周围很小的区域泛化能力变差。如果你只是想交大作业直接用源码里的模型就好如果你想把“重训模型”写进答辩内容建议用交叉验证跑一遍 C 和 gamma 的组合把最优参数截图放进去这比口头解释有说服力得多。5. 运行排查从 cv2 报错到识别翻车的 5 个真实问题5.1 依赖安装cv2 装不上、版本对不上现象运行 predict.py 时直接报ModuleNotFoundError: No module named cv2或者 Python 解释器停在ImportError: DLL load failed这类错误上。原因环境里根本没装 OpenCV或装了和系统 Python 位数不匹配的版本。解决在命令行执行pip install opencv-python如果速度慢或失败换国内镜像源pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple。装完后用python -c import cv2; print(cv2.__version__)验证。注意 4.x 和 3.4 的 API 有差异比如cv2.ml.SVM_load在 3.x 里写cv2.SVM_load如果你高版本源码报找不到属性优先检查 OpenCV 大版本。5.2 中文路径读图失败现象代码里写img cv2.imread(测试图片/鲁A12345.jpg)返回None程序后面处理空图像直接报错。原因OpenCV 的imread在 Windows 中文路径下会因编码问题读不到文件。解决不要用imread改成先读字节流再解码img cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_COLOR)np.fromfile读取原始文件字节cv2.imdecode负责解码成图像绕过了imread的编码限制。这个坑在中文 Windows 环境几乎必踩建议把所有图片路径都走这个函数。5.3 汉字被切碎导致整牌识别失败现象蓝牌图片定位没问题字符分割后输出结果里汉字部分缺笔画比如“苏A12345”识别成“A12345”或者“苏”被拆成两个字符。原因垂直投影时汉字左右结构的中间间隙太大被当成字符分界。解决把 3.2 里的合并阈值从 12 调到 18 到 22同时检查是否加了字符数量兜底校验。我处理这类问题时会先打印出分割后的区间数量如果超过 7基本可以断定是切碎问题。值得注意的是每种汉字的结构不一样“冀”这类更宽的字需要的合并阈值也更大所以别指望一个固定值通吃所有测试图。5.4 黄牌和新能源绿牌识别不到现象蓝牌测试图全部正常但放一张黄底黑字的货车牌或者绿底黑字的新能源车牌程序直接定位失败或输出空结果。原因源码的预处理逻辑默认字符比背景亮蓝底白字正好匹配黄牌和新能源车牌是黑字二值化方向反转字符特征完全变样。解决在分割前增加一个方向判断统计二值图中暗色像素占比如果字符像素是暗色就在投影前做一次cv2.bitwise_not反转。另外黄牌的宽高比和蓝牌有细微差异过滤条件里的2.5 w/h 5.5也可以适当放宽到 2.0 到 6.0。这套源码本身不是为多类型车牌设计的如果你想在答辩里展示扩展能力把这两处逻辑加上就是很好的加分点。5.5 0/O、B/8 混淆与后处理现象整牌识别基本正确但偶尔把字母 O 识别成数字 0或者把 B 识别成 8再或者反过来。原因训练样本里 0 和 O 的样本量不均衡SVM 决策边界偏向样本多的一侧部分测试图字体偏圆导致视觉特征接近。解决第一种办法是收集混淆样本补进训练集对应目录重训模型第二种更快的办法是加车牌规则后处理——车牌第二位固定是发牌机关代号字母所以如果第二个字符被判成数字 0可以强制改写为 O但后续 5 位里 0 和 O 都合法不能靠规则硬改只能靠模型。这类问题在传统 SVM 方案里没法完全消除答辩被问到时如实说明训练样本限制比硬吹准确率更稳。6. 批量验证用测试图集给识别准确率打分6.1 批量评估脚本拿到这套源码后建议先别急着单张测试而是把 test 目录下的图片全部跑一遍统计字符级和整牌级准确率。测试图文件名里有部分车牌信息比如wA87271.jpg、cAA662F.jpg但命名规则不统一还有1.jpg、2.jpg这类纯数字文件所以标签不能直接从文件名全量推断。我的做法是建一个字典手动给纯数字命名的图片补标签对带前缀的图片去掉小写前缀后作为后 6 位标签。评估代码可以这样写import os import glob import numpy as np def predict_plate(img_path): # 替换为 predict.py 里你封装好的识别主函数 return A87271 test_dir test total_plate, ok_plate 0, 0 total_char, hit_char 0, 0 manual_labels {1.jpg: A12345, 2.jpg: B67890} # 按实际内容补 for img_path in glob.glob(os.path.join(test_dir, *.jpg)): name os.path.basename(img_path) if name in manual_labels: label manual_labels[name] else: label name.split(.)[0][1:] # 去掉小写文件名前缀 pred predict_plate(img_path) total_plate 1 ok_plate (pred label) for c1, c2 in zip(pred, label): total_char 1 hit_char (c1 c2) print(f整牌准确率: {ok_plate}/{total_plate}) print(f字符准确率: {hit_char}/{total_char})这个脚本的价值不在于代码本身而在于把“好像识别对了”变成可量化的数字。你会发现字符准确率和整牌准确率不是一个概念7 个字符的整牌只要有一位错就整牌错字符准确率 95% 时整牌准确率可能只有 65%。答辩时如果老师问系统好不好用别只说“基本都能识别”直接报这两组数字。6.2 用准确率说话批量跑完之后把识别错误的图片单独拎出来看错误通常集中在三类字符切碎导致汉字错、倾斜车牌定位不准、0/O 混淆。每调一次参数就重跑一遍评估脚本对比字符准确率的变化不要凭感觉判断“好像变好了”。这是我在拆这份源码时最深的体会——刚拿到时我调了半天闭运算核大小感觉输出顺眼了一跑批量测试发现整牌准确率反而降了 2 个百分点。从那以后我每次拿到这类课程设计源码第一件事就是整理测试集标签、写一个批量评估脚本把所有调整都建立在数字对比上不再靠肉眼玄学判断。希望帮到你。本文还有配套的精品资源点击获取