简介:这是一个基于Python与OpenCV实现的车牌识别系统毕业设计工程包,面向计算机视觉、人工智能方向的高校毕业生,也适合用作期末大作业或课程设计参考。项目完整覆盖车牌图像采集、预处理、定位、字符分割与识别等关键流程,对RGB/HSV色彩空间转换、边缘检测、形态学处理和SVM分类均有工程实现,已通过指导教师验收并获高分,零基础学习者也能顺利运行。包内共81个文件,以Python源码、OpenCV模型配置(xml与dat)、车牌样本图片(jpg/png)及可直接启动的exe程序为主,整体大小约67.93MB,目录结构清晰,便于按模块阅读和二次开发。目前已有590人学习使用,除了可直接运行的完整工程外,还包含训练好的模型参数与测试图片,既能用于毕业设计答辩演示,也可作为智能交通场景的算法实践基础。
1. 基于python+OpenCV的车牌识别系统:这套毕设源码能做什么
拿到“基于python+OpenCV的车牌识别系统”这个毕设项目,先别急着跑 main.py。我拆过不少同类源码,这套项目的结构是典型的“图像预处理 → 车牌定位 → 字符分割 → SVM 识别”四段式,文件里既有 Python 源码(main.py、img_recognition.py、RBG_HSV.py),也带了一整套可直接运行的 exe 和两个关键模型文件 svm.dat、svmchinese.dat,属于那种能直接交差、也能拿来二次开发的完整度比较高的毕设资源。它解决的痛点很明确:给你一条从普通图片到输出车牌字符串的完整链路,省掉你自己去调 OpenCV 算子和训练 SVM 的时间。适合正在做计算机视觉方向课程设计、毕业设计的人,也适合想快速上手 OpenCV 车牌识别的初学者。下面我从定位、分割、识别这三段逐个拆。
2. 车牌定位:先想清楚靠边缘还是靠颜色
车牌定位是整个识别系统里最影响成败的一步,定位框画偏了,后面分割和识别做得再漂亮都没用。这个项目里同时出现了边缘检测类和颜色转换类(img_function.py、RBG_HSV.py),其实就是给了两套定位思路:一种靠车牌区域的纹理边缘特征,一种靠车牌底色(蓝牌为主)的 HSV 颜色范围。实际跑的时候你会发现,两套方法各有各的适用前提。
2.1 图像预处理:灰度、滤波、二值化的参数怎么定
不管是走边缘还是走颜色,第一步都是把原始图片转成适合 OpenCV 后续处理的格式。项目里 chuli.py 承担的就是这部分脏活:读图、转灰度、降噪、二值化。常见的处理链路是这样的:
import cv2 import numpy as np def preprocess(src_path): # 读取 BGR 原图 img = cv2.imread(src_path) # BGR 转灰度,车牌识别不需要颜色信息时先用灰度降低计算量 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯滤波去噪,ksize 越大越模糊,5x5 是千万级像素下的折中值 blur = cv2.GaussianBlur(gray, (5, 5), 0) # 用自适应阈值代替固定阈值,能扛住光照不均 binary = cv2.adaptiveThreshold( blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 19, # 邻域块大小,奇数,太大会丢失字符细节 9 # 常数 C,越大越容易剔除噪点 ) return img, gray, binary这段代码里最值得关注的是adaptiveThreshold而不是threshold。固定阈值 127 这种写法在实验室灯光下没问题,一旦图片里有阴影、反光,固定阈值会直接把车牌区域切成黑白花斑,后面定位就跟着崩。自适应阈值会根据每个像素邻域算出一个局部阈值,抗干扰能力强得多。
参数上,块大小 19 和常数 C=9 是针对 500~800 像素宽度的图片调出来的。如果你的测试图片分辨率更高,块大小要跟着加大;图片更小,块大小要减小。否则要么字符糊成一团,要么背景噪点全被当成前景。
2.2 边缘检测与形态学操作:候选车牌区域是怎么框出来的
车牌区域有一个很稳定的纹理特征:字符密集、边缘丰富、背景相对单调。所以用 Sobel 或者 Canny 先找边缘,再用形态学闭运算把边缘连成一块连续的连通域,最后按轮廓外接矩形的宽高比筛掉非车牌区域,这是经典做法。项目里的 img_function.py 走的差不多就是这条路线。
def locate_by_edge(binary): # 边缘检测,Canny 双阈值:低阈值用于连接,高阈值用于抑制噪声 edges = cv2.Canny(binary, 100, 200) # 闭运算:先膨胀后腐蚀,把断裂的字符边缘粘连成完整区域 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel) # 找外轮廓 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 / h # 标准蓝牌宽高比约 3.14,实际允许 2.5~4.0 的浮动 if 2.5 < ratio < 4.0 and 200 < w * h < 20000: candidates.append((x, y, w, h)) return candidates注意Canny(binary, 100, 200)的两个阈值,一个是低阈值 100,一个是高阈值 200。低于 100 的梯度直接丢弃,高于 200 的肯定是边缘,介于两者之间的看是否与高阈值边缘相连。这个比例一般是 1:2 到 1:3 之间。你在项目里如果发现车牌边缘断得太碎,就把低阈值降到 60;如果噪声太多,就把高阈值拉到 250。
形态学闭运算的 kernel 设成(17, 5)是有讲究的。车牌是横向的矩形,字符横向排列,所以你希望把同一行里的字符边缘横向连起来,而不是把上下相邻的区域也粘到一起。17 的宽度对 500px 宽的图来说大约能覆盖 1.5~2 个字符间距,5 的高度刚好容下字符高度。
2.3 HSV 颜色筛选:RBG_HSV.py 里到底做了什么
边缘定位的弱点是背景里如果存在栅栏、树叶、车窗缝隙这类纹理丰富的区域,会产生大量假候选框。这时候颜色信息就是更硬的条件。中国蓝牌的主色调非常稳定,在 HSV 色彩空间里可以用一个窄范围把它圈出来。这也是 RBG_HSV.py 这个模块的核心职责。
def locate_by_color(img): # 原图是 BGR,先转成 HSV 再筛颜色 hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝色车牌典型范围:H 100~124,S 90~255,V 90~255 lower_blue = np.array([100, 90, 90]) upper_blue = np.array([124, 255, 255]) mask = cv2.inRange(hsv, lower_blue, upper_blue) # 蓝色区域可能因为反光而断成几块,做一次闭运算连通 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates = [] for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) # 如果检测到的蓝色区域接近整张图,说明阈值太宽,直接跳过 if 2.0 < w / h < 4.5 and w > img.shape[1] * 0.15: candidates.append((x, y, w, h)) return candidates这里有个最容易翻车的地方:HSV 的下界和上界不是随便写的。OpenCV 里 H 通道范围是 0~179,不是 0~360,所以蓝牌的 H 值区间是 100~124,不是 200~248。很多新手直接把别的语言例程里的 HSV 范围搬过来,结果一张图都识别不出来,就是因为没换算 OpenCV 的 H 范围。
另外w > img.shape[1] * 0.15这个条件是过滤“全屏变蓝”的情况。如果输入的图片里天空占比很大,而且 HSV 阈值过宽,整个天空都会被划成候选区域,这个宽度比例条件能把这种明显不合理的候选过滤掉。颜色定位和边缘定位不冲突,正常项目里是两者结合:先用颜色粗定位,再在候选区域里做边缘验证,能同时过滤掉纹理噪声和颜色噪声。
3. 字符分割与 SVM 识别:svm.dat 和 svmchinese.dat 的分工
定位到车牌区域之后,下一步是把它统一校正成标准矩形,然后切成单个字符,逐个丢给识别模型。字符分割质量决定了识别准确率的上限,而 SVM 模型的成熟度决定了这个上限能兑现多少。这个项目里最值钱的其实不是定位代码,而是那两个训练好的模型文件:svm.dat 和 svmchinese.dat。
3.1 字符分割:投影法是这套项目里最稳的方案
分割的输入是一张已经裁出来的车牌图。第一步要把车牌转正,因为摄像头拍摄角度不可能完全正面。项目里通常会用透视变换把任意四边形映射到固定尺寸,比如 440x140 的标准车牌宽高比,然后再做二值化和分割。
def split_chars(binary_plate): # 去掉车牌上下边框:先做一次水平投影,统计每行白点数量 row_sum = np.sum(binary_plate, axis=1) # 找到字符区域的起始和结束行 row_idx = np.where(row_sum > 0)[0] if len(row_idx) == 0: return [] top, bottom = row_idx[0], row_idx[-1] plate_clean = binary_plate[top:bottom + 1, :] # 去掉左右边框后进行垂直投影 col_sum = np.sum(plate_clean, axis=0) col_idx = np.where(col_sum > 0)[0] left, right = col_idx[0], col_idx[-1] plate_clean = plate_clean[:, left:right + 1] # 垂直投影:每一列求和,字符之间有明显的零值列 col_sum_clean = np.sum(plate_clean, axis=0) chars = [] in_char = False start = 0 for i, val in enumerate(col_sum_clean): if val > 0 and not in_char: # 进入一个字符 in_char = True start = i elif val == 0 and in_char: # 字符结束,切出这一段 w = i - start if 10 < w < 80: # 过滤掉噪声点造成的过窄片段 char = plate_clean[:, start:i] chars.append(char) in_char = False return chars垂直投影的原理是统计每一列上的白点数量。字符笔画密集,所以字符所在列的投影值很高;字符之间的缝隙没有笔画,投影值为 0。通过扫描投影值的“高→零→高”变化,就能把每个字符切出来。这套代码在白色背景、黑色字符的干净车牌上效果很好,但有两个前置条件:一是二值化必须保证字符是白色、背景是黑色,二是车牌边框必须提前去掉,否则边框的投影会把所有字符连成一片。
分割的时候10 < w < 80这个宽度过滤很关键。在 440 宽的车牌图上,单个字符宽度差不多在 40~60 像素,如果出现一个宽度只有十几像素的片段,那基本是噪点或者螺丝钉。但也要小心过滤条件设得太死,把掉漆严重的字符直接丢掉了。我一般会先打印出所有切片的宽度,看看分布再调阈值。
3.2 SVM 模型文件:svm.dat 和 svmchinese.dat 各管哪一部分
国内车牌由 7 个字符组成:第 1 位是省份简称的汉字,第 2 位是发牌机关字母,后 5 位是字母和数字混合。新能源车牌是 8 位,多了第 6 位和最后一位的区分规则。汉字和英文字母数字的形态差异很大,所以项目里拆成了两个模型:
| 模型文件 | 识别对象 | 类别数 | 加载方式 |
|---|---|---|---|
| svm.dat | 0~9 数字和 A~Z 字母(不含 I、O) | 34 | cv2.ml.SVM_load('svm.dat') |
| svmchinese.dat | 省份简称汉字,如 京、沪、苏、粤 | 31 | cv2.ml.SVM_load('svmchinese.dat') |
这里要理解一个核心概念:OpenCV 的 SVM 不能直接吃图片,它只吃固定长度的特征向量。项目里字符图片一般是统一缩放成固定尺寸(常见的是 20x20 或 32x32),然后把像素值拉成一维数组作为特征。灰度图 20x20 拉平就是 400 维的特征向量,32x32 就是 1024 维。SVM 本质上是在高维空间里找分类超平面,特征向量越长,能区分的细节越多,但计算量也越大,而且对小样本更容易过拟合。
import cv2 import numpy as np def load_models(): # 路径尽量用绝对路径,exe 场景下相对路径极易踩坑 svm_eng = cv2.ml.SVM_load('svm.dat') svm_cn = cv2.ml.SVM_load('svmchinese.dat') return svm_eng, svm_cn def predict_char(model, char_img, size=(20, 20)): # 字符图统一缩放,保持长宽比会引入黑边,直接拉伸更常见 resized = cv2.resize(char_img, size, interpolation=cv2.INTER_AREA) # 归一化到 [0,1],SVM 对特征尺度敏感 resized = resized.astype(np.float32) / 255.0 # 拉伸成一维特征向量,reshape(-1) 自动计算长度 feature = resized.reshape(1, -1) # OpenCV SVM 的 predict 返回 (结果, 置信度) _, result = model.predict(feature) return int(result[0][0])注意resize用的INTER_AREA,这是缩小图片时效果最好的插值方式;放大时才用INTER_LINEAR或INTER_CUBIC。归一化到 0~1 而不是直接用 0~255 的像素值,是 SVM 的一个常见调优手段。因为像素值范围越大,某些特征的权重就越容易被大数值主导,归一化能让所有维度的尺度一致。
3.3 识别主流程:img_recognition.py 里的调用链
把定位、分割、识别串起来,就是 img_recognition.py 的完整职责。这个模块的输入是一张完整图片,输出是车牌字符串。它的调用顺序如下:
def recognize_plate(image_path): # 第一步:读图和预处理 img, gray, binary = preprocess(image_path) # 第二步:定位候选车牌区域(颜色 + 边缘双重验证) candidates = locate_by_color(img) if not candidates: candidates = locate_by_edge(binary) # 第三步:对每个候选区域做透视校正和字符分割 svm_eng, svm_cn = load_models() for x, y, w, h in candidates: plate = img[y:y + h, x:x + w] # 透视校正省略,这里直接假设正面拍摄 binary_plate = binarize_plate(plate) chars = split_chars(binary_plate) if len(chars) != 7: # 不是 7 个字符,跳过这个候选 continue # 第四步:逐个字符用对应模型预测 result = [] for i, char_img in enumerate(chars): if i == 0: # 第一个字符一定是汉字 result.append(predict_char(svm_cn, char_img)) else: # 其余是字母或数字 result.append(predict_char(svm_eng, char_img)) return ''.join(result) return None这里面有一个很使用的小技巧:利用车牌格式的固定结构来补强模型。第 1 位字符固定是省份汉字,所以你直接用汉字模型;第 2 位固定是字母,不是数字;后 5 位才是字母数字混合。哪怕后 5 位里混进了和字母相似的数字(比如 0 和 O),这种结构约束也能在拼接结果时帮你纠错。
另一个值得注意的地方是len(chars) != 7的过滤。现实中很多图片定位出来的候选框根本不是车牌,或者车牌掉漆导致字符切不全,这个判断能快速跳过一批假候选。但你也要意识到,这个硬性条件对新能源 8 位车牌不友好,后面我再说怎么改。
4. 从源码到可运行:环境准备与 main.py 运行流程
源码结构清楚了,接下来就是把它跑起来。这一步看似简单,实际上环境问题比代码问题更折腾人。这个项目里既有纯 Python 源码,又有打包好的 exe,两条路的踩坑点完全不同。
4.1 环境准备:python、OpenCV 和 numpy 的版本组合
先从 Python 环境说起。这个项目的代码风格是典型的毕业设计写法,对 Python 版本不挑,3.6 到 3.10 基本都能跑。但 OpenCV 版本很挑:OpenCV 4.x 以后cv2.ml.SVM_load的调用方式基本没变,但底层的 SVM 模型文件格式有兼容性风险。如果 svm.dat 是用旧版 OpenCV 训练出来的,拿新版直接加载可能会报错。
我一般建议先建一个虚拟环境,不要直接往系统 Python 里装包,毕设项目依赖经常互相打架。
# 创建虚拟环境,python 3.8 是比较稳的选择 python -m venv vlpr_env # Windows 下激活 vlpr_env\Scripts\activate # 安装依赖,注意版本号 pip install opencv-python==4.5.5.64 pip install numpy==1.23.5opencv-python这个包名很奇怪,但装上之后 Python 里的导入名是cv2,不是opencv。很多新手报ModuleNotFoundError: No module named 'cv2',就是因为用pip install opencv去装,结果装了个不相关的包。至于版本,我选 4.5.5 而不是最新版,是因为它同时兼容旧的 SVM 模型文件格式和新的 Python API,是这类毕设项目里最不容易出事的版本。
numpy 的版本也值得注意。OpenCV 4.5.5 搭配 numpy 1.23.5 是比较经典的组合,numpy 2.x 出来后有些 API 变了,会导致cv2.findContours返回值的解析方式出问题。
4.2 运行 main.py:从加载图片到输出车牌的完整过程
环境装好后,先看看 main.py 的入口逻辑。这类项目的 main.py 通常做四件事:解析命令行参数、读取图片、调用识别函数、打印结果。代码结构大概是这样的:
def main(): # 项目里 img.png 是自带的测试图片 image_path = 'img.png' # 调用识别主流程 plate = recognize_plate(image_path) if plate: print('识别结果:', plate) else: print('未检测到车牌') if __name__ == '__main__': main()运行方式很简单:
cd 项目目录 python main.py第一次跑的时候,我建议你把img.png换成自己拍的清晰车牌照片,不要急着换复杂场景。先用一张干净的、正面拍摄、光线均匀的蓝牌图片验证整条链路能通。如果这张图都识别不出来,优先检查定位环节,而不是模型。
main.py 里还有一个细节:它可能会调用chuli.py里的函数做图像变换,也可能会直接调img_recognition.py。这两个模块的边界在项目里不一定分得很清楚,你要在 IDE 里用断点或者print调试的话,先从recognize_plate这个函数往里看就行,没必要一开始就纠结每个小函数是干什么的。
4.3 直接跑 exe:黑盒运行的边界在哪
项目带的 exe 目录里有main.exe、svm.dat、svmchinese.dat和一个 pic 子目录,这说明它打包时是把整个运行环境都塞进去了。好处是省去装 Python 的麻烦,坏处是黑盒。
exe 的运行有几个硬性条件:exe 和两个 svm 模型文件必须放在同一个目录下。因为打包后的程序找模型文件用的是相对路径,你在桌面双击这个 exe,它会在当前工作目录找 svm.dat,找不到就直接闪退。正确做法是打开 exe 所在的文件夹,在里面双击运行,或者用命令行:
cd exe .\main.exe用命令行跑的好处是即使闪退,你能在终端里看到具体的报错信息,而双击运行时窗口一关就什么都没了。
另外,exe 里集成的 Python 环境是打包那一刻的快照,你没法给它升级 OpenCV,也没法换模型文件。如果你想换一个自己训练的 svm.dat,直接覆盖同名文件是能起作用的,因为程序按相对路径加载这两个文件。换完如果效果变差,再把原文件改回去,这就是黑盒运行里保留的“后悔药”。
5. 常见问题与避坑:五个必踩的坑和对应解法
跑完一轮之后,你会发现这个项目 70% 的问题不出在算法,而出在环境和路径。我把实际运行中最容易遇到的五个问题按“现象 → 原因 → 解决”列出来,照着排查能省半天时间。
5.1 ModuleNotFoundError: No module named 'cv2'
现象:执行python main.py,控制台直接报ModuleNotFoundError: No module named 'cv2'。
原因:当前 Python 环境里没装 OpenCV,或者装了不正确的包。还有人用pip install opencv,这个包不是 OpenCV 本体,装上后依然找不到cv2。
解决:用下方命令安装正确的包。安装完成后用python -c "import cv2; print(cv2.__version__)"验证当前环境能导入。
pip uninstall opencv -y pip install opencv-python==4.5.5.64 python -c "import cv2; print(cv2.__version__)"这里要特别提醒:如果你开了一个 IDE(比如 PyCharm),它可能默认使用虚拟环境,而你在系统终端里 pip 安装的包进不到 IDE 的解释器里。检查 PyCharm 右下角解释器路径,确保和你装包的环境是同一个。
5.2 加载 svm.dat 时报错或者返回空模型
现象:程序执行到cv2.ml.SVM_load('svm.dat')时没有异常,但后面调用predict报错,或者svm_eng直接是空对象。
原因:路径不对,或者 OpenCV 版本与模型序列化版本不匹配。SVM 模型文件里有 OpenCV 的版本信息,新版 OpenCV 加载旧格式文件时可能静默失败,不报错但返回空模型。
解决:先确认当前工作目录,再用绝对路径加载。程序里直接写绝对路径是最省事的方式:
import os # 把模型路径指向项目根目录,避免 cmd 工作目录不同导致找不到文件 base_dir = os.path.dirname(os.path.abspath(__file__)) svm_eng = cv2.ml.SVM_load(os.path.join(base_dir, 'svm.dat')) svm_cn = cv2.ml.SVM_load(os.path.join(base_dir, 'svmchinese.dat'))如果确认路径没问题还是加载失败,就换个 OpenCV 版本试试。优先试 4.5.x,不行再降到 4.1.x,总有一个能兼容。
5.3 数字字母都能识别,但汉字识别出来是乱码或空
现象:定位、分割都正常,前 6 个字符识别正确,第 7 位之前的位置(也就是首位汉字)识别结果明显不对,或者输出一堆乱码。
原因:车牌字符串拼接时,没有区分汉字模型和字母数字模型。如果代码里对整整 7 个字符都用svm.dat预测,汉字就会被硬塞进字母数字的类别空间里,因为svm.dat训练的时候根本没见过汉字样本。
解决:首位汉字强制走svmchinese.dat。如果你的分割结果总是 7 个字符,第一位固定是汉字,补齐这个判定逻辑:
# i == 0 时必须用汉字模型;字符只有 7 位,第一位不能当字母预测 if i == 0: label = predict_char(svm_cn, char_img) else: label = predict_char(svm_eng, char_img)还有一种情况是分割时第一个字符没切出来,整个车牌只剩 6 个字符,被前面的len(chars) != 7过滤掉了。这种问题不在模型,在分割参数的宽度阈值。
5.4 自带测试图能识别,换一张图就识别不出来
现象:跑项目自带的img.png一切正常,换成自己用手机拍的图,要么定位不到车牌,要么定位到了但分割乱掉。
原因:这套项目的定位参数是围绕“正面、光照均匀、蓝牌”调出来的。手机照片可能有透视畸变、大面积反光、车牌占画面比例太小或者偏色严重,这些都会让 HSV 阈值和形态学 kernel 失灵。
解决:按顺序调整三个地方。先把 HSV 蓝牌阈值放宽(比如 H 下界从 100 降到 95),再看定位框是否框住了整个车牌,最后在分割前加一道透视校正。如果透视畸变大,投影法会直接把倾斜的字符切成斜条:
# 用最小外接矩形得到倾斜角度,再做仿射变换扶正 rect = cv2.minAreaRect(cnt) angle = rect[-1] if angle < -45: angle = 90 + angle M = cv2.getRotationMatrix2D(rect[0], angle, 1.0) rotated = cv2.warpAffine(plate, M, (plate.shape[1], plate.shape[0]))这个旋转修正对手机拍出来的斜车牌效果非常明显,是提升换图识别率的最有效手段。
5.5 双击 exe 闪退,命令行下能跑但看不到输出
现象:双击main.exe后窗口一闪而过,啥也没看到。
原因:程序异常退出。常见原因有两类:一类是 exe 的工作目录不对,找不到svm.dat;另一类是识别函数里某个 OpenCV 调用在特定图片上抛异常,没有被捕获。
解决:先用命令行运行cd exe && .\main.exe,这样至少能保留异常信息。如果是模型路径问题,把 exe、svm.dat、svmchinese.dat 放同一目录再试。如果是识别异常,就要回到 Python 源码里给关键函数加 try-except:
try: plate = recognize_plate(test_img) except Exception as e: print(f'识别异常: {e}')这种兜底代码在毕设答辩演示时特别有用,至少不会在评委面前弹一个难看的崩溃窗口。
6. 调试验证与二次开发:把识别率从“偶尔对”调到“稳定对”
6.1 准备一套自己的验证集
项目自带的单张测试图只能证明链路通,不能证明系统稳定。我的习惯是从网上下载或者自己拍摄 20~30 张不同场景的车牌图,按难度分成三档:清晰正面照、轻微倾斜照、逆光或反光照。每次改完参数,就把这组图全跑一遍,统计正确识别率。这个动作能让你知道当前改动到底是在改进还是在倒退,而不是靠一张图“感觉这次好像行”。
跑批量测试时别一张张肉眼盯,写个简单的脚本把结果汇总起来:
import os test_dir = 'test_imgs' for name in os.listdir(test_dir): path = os.path.join(test_dir, name) plate = recognize_plate(path) print(f'{name}: {plate}')输出结果里如果某张图返回None,单独拿去调试定位环节;如果返回了但字符串里有错字,单独调试分割和识别环节。定位问题、分割问题、识别问题,三者的现象是完全不一样的。
6.2 可视化调试:把中间结果写到图片上
不看中间结果就去改参数,基本是玄学调试。我把每步中间结果都画到图上,一眼就能看出是哪一环断了。在调试代码里加入可视化输出:
def debug_show(img, candidates, result_text): # 把候选框画到原图上,确认定位是否准确 for x, y, w, h in candidates: cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2) # 在图片顶部叠加识别结果 cv2.putText(img, result_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2) cv2.imwrite('debug_result.jpg', img)打开debug_result.jpg后,你会快速形成判断:绿色框没框住车牌是定位参数的问题,框住了但识别错字是分割或模型的问题,识别全错则要考虑是不是模型特征匹配不上。定位、分割、识别这三个模块完全可以独立调试,各自调好了,整体准确率自然上去。
6.3 扩展成新能源车牌支持
新能源车牌是多一位的 8 位字符,而且第二位和最后一位之间有一条分割点。如果你想把这套毕设拿去做课程设计升级版,改动点是:分割时不要硬性要求len(chars) == 7,改成放行 8 个字符;识别时前 7 位规则不变,第 8 位加一个分类标记即可。核心的 SVM 模型不用重新训练,因为新能源车牌第 8 位的字符集没有超出原来的字母数字范围。
我的习惯是每拿到一套开源毕设源码,先不追求跑通,而是先把定位框和分割结果可视化出来,确认这两步质量达标后,再谈识别率。从那以后我每次跑同类 OpenCV 识别项目,都强制先做一遍视觉化调试,已经替我省掉了好几晚排查问题的时间。这套基于 python 和 OpenCV 的车牌识别系统,思路清晰,文件也不乱,照着上面的流程走一遍,基本能在一个晚上把整条链路跑通,希望帮到你。
本文还有配套的精品资源,点击获取