简介:这是一套基于 Python 与 OpenCV 实现的车牌识别系统源码资源,面向计算机视觉入门者、课程设计或毕业设计需求者,以及希望把传统图像处理与摄像头实时识别结合的开发者。项目在图片车牌号识别、GUI 界面设计、识别数据导出 Excel 的基础上,新增了 OpenCV 摄像头实时识别功能,并对原识别模块函数做了优化,提升了识别速度与准确率。压缩包共 16468 个文件,约 24.91MB,其中 16415 个 jpg 为车牌样本与测试图像,另有 6 个 py 脚本承载核心识别与界面逻辑,9 个 xml 与 svm.dat、svmchinese.dat 等模型文件用于字符与车牌分类,其余为配置与工程辅助文件。目前已有 1843 人学习下载。读者可据此获得一套可运行的车牌识别工程,理解样本组织、模型加载、GUI 交互与 Excel 导出的完整链路,并参考摄像头实时识别与函数优化思路,用于二次开发或课程实践。
1. 车牌识别系统:从一张图片到结构化结果的完整链路
很多开发者第一次接触计算机视觉,都是从车牌识别这个场景切入的。原因很直接:它既有明确的输入输出边界,又能把图像预处理、边缘检测、字符分割、模板匹配这几个核心环节串起来。我手上这份基于 Python + OpenCV 的车牌识别系统,就是这样一个适合拿来拆解和二次开发的项目。它不依赖深度学习框架,纯靠传统图像处理链路完成识别,对刚入门计算机视觉的开发者来说,代码可读性和可调试性都远高于直接上神经网络。你拿到的是一套能跑通的完整工程,而不是零散的算法片段。下面我从项目定位、目录结构、核心代码到改造思路,按实际拆项目的顺序讲一遍。
2. 项目定位与技术选型:为什么用传统图像处理而不是深度学习
2.1 这个项目解决什么问题
车牌识别在工程上通常分两个阶段:车牌定位和字符识别。这份项目的处理链路是:读取车辆图片 → 灰度化 → 高斯模糊去噪 → Sobel 边缘检测 → 形态学闭运算 → 轮廓筛选定位车牌区域 → 透视校正 → 字符分割 → 模板匹配输出结果。整条链路不涉及模型训练,所有参数都在代码里显式定义,改一个阈值就能看到中间结果的变化。
这种设计的好处是调试成本低。你不需要 GPU,不需要标注数据,一台普通笔记本就能跑。对于想理解计算机视觉基础流程的开发者来说,每一步的输入输出都是可见的,不会出现“模型输出不对但不知道哪一层出了问题”的黑匣子情况。
2.2 技术栈与依赖
项目依赖非常轻量,核心就三个库:
| 依赖 | 版本建议 | 用途 |
|---|---|---|
| Python | 3.8+ | 运行环境 |
| opencv-python | 4.5+ | 图像处理与轮廓检测 |
| numpy | 1.21+ | 矩阵运算与数组操作 |
安装命令一行搞定:
pip install opencv-python numpy如果你用的是 Anaconda 环境,opencv 通常已经预装,直接确认版本即可。常见做法是建一个独立虚拟环境,避免和系统里其他版本的 opencv 冲突。
2.3 目录结构
项目目录组织得很直白,适合二次开发时快速定位:
license-plate-recognition/ ├── main.py # 主入口,串联完整识别流程 ├── config.py # 参数配置,阈值、尺寸、路径集中管理 ├── core/ │ ├── preprocess.py # 灰度化、滤波、边缘检测 │ ├── locate.py # 车牌定位与轮廓筛选 │ ├── segment.py # 字符分割 │ └── recognize.py # 模板匹配识别 ├── templates/ # 字符模板库 │ ├── 0.png ~ 9.png │ └── A.png ~ Z.png ├── images/ # 测试图片 └── output/ # 识别结果输出config.py单独抽出来是个好习惯。车牌定位的阈值、形态学核大小、字符分割的投影阈值这些参数,在不同拍摄条件下需要微调,集中管理比散落在各个模块里强得多。
3. 核心模块拆解:从图像预处理到字符识别的代码实现
3.1 图像预处理:灰度化与边缘检测
预处理的目标是把彩色图像变成只保留结构信息的二值边缘图,方便后续定位车牌。核心代码如下:
import cv2 import numpy as np def preprocess(image_path): # 读取图片,统一缩放到固定宽度,避免尺寸差异影响阈值 img = cv2.imread(image_path) img = cv2.resize(img, (800, int(img.shape[0] * 800 / img.shape[1]))) # 灰度化:三通道转单通道,减少计算量 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊:核大小 5x5,去除高频噪声,避免边缘检测产生碎点 blur = cv2.GaussianBlur(gray, (5, 5), 0) # Sobel 水平方向边缘检测:车牌区域垂直边缘密集,水平梯度响应强 sobel_x = cv2.Sobel(blur, cv2.CV_16S, 1, 0, ksize=3) abs_x = cv2.convertScaleAbs(sobel_x) # 二值化:阈值 120,高于阈值的保留为边缘 _, binary = cv2.threshold(abs_x, 120, 255, cv2.THRESH_BINARY) return img, binary这里有几个参数值得注意。高斯核(5,5)是常见起点,图片噪声大可以加到(7,7),但太大会把车牌边缘也模糊掉。Sobel 的ksize=3是标准选择,CV_16S是为了保留负梯度,后面用convertScaleAbs转回 uint8。二值化阈值 120 不是固定的,光照偏暗的图片可能要降到 80 左右。
3.2 车牌定位:形态学运算与轮廓筛选
边缘图里有很多干扰轮廓,需要用形态学闭运算把车牌区域的边缘连成块,再按宽高比和面积筛选。
def locate_plate(binary): # 形态学闭运算:先膨胀后腐蚀,把断开的边缘连成矩形区域 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(binary, 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 / float(h) area = w * h # 车牌宽高比通常在 2.5 到 5.5 之间,面积不能太小 if 2.5 < ratio < 5.5 and area > 2000: candidates.append((x, y, w, h)) # 按面积排序,取最大的作为车牌区域 candidates.sort(key=lambda c: c[2] * c[3], reverse=True) return candidates[0] if candidates else None形态学核(17,5)是横向长条,因为车牌是扁长的,横向连接能力要强于纵向。宽高比范围 2.5 到 5.5 覆盖了国内蓝牌和新能源绿牌的常见比例。面积阈值 2000 是经验值,图片分辨率高的话要相应调大。
3.3 字符分割:垂直投影法
定位到车牌后,需要把七个字符逐个切开。常用做法是垂直投影:统计每一列白色像素的数量,字符之间的间隙处投影值接近零。
def segment_characters(plate_img): # 转灰度并二值化 gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 垂直投影:统计每列白色像素数 projection = np.sum(binary, axis=0) / 255 # 找到投影值从低到高的跳变点,作为字符边界 chars = [] in_char = False start = 0 for i, val in enumerate(projection): if val > 2 and not in_char: in_char = True start = i elif val <= 2 and in_char: in_char = False if i - start > 5: # 过滤太窄的噪声 chars.append(binary[:, start:i]) return chars投影阈值val > 2表示该列至少有两个白色像素才算字符区域,这个值可以根据图片清晰度调整。i - start > 5过滤掉宽度小于 5 像素的噪声块。分割出来的字符数量应该是 7 个,如果多了或少了,说明二值化或投影阈值需要调。
3.4 模板匹配识别
字符识别用的是最基础的模板匹配:把分割出的字符缩放到统一尺寸,和模板库里的字符逐一比对,取相似度最高的作为结果。
def recognize_char(char_img, templates): # 统一缩放到 20x40 char_resized = cv2.resize(char_img, (20, 40)) best_score = -1 best_char = '?' for char, template in templates.items(): # 模板匹配:TM_CCOEFF_NORMED 返回归一化相关系数 result = cv2.matchTemplate(char_resized, template, cv2.TM_CCOEFF_NORMED) score = result[0][0] if score > best_score: best_score = score best_char = char return best_char, best_scoreTM_CCOEFF_NORMED的输出范围是 -1 到 1,越接近 1 越相似。模板库需要提前准备好,每个字符一张标准图,尺寸统一。常见做法是用同一字体渲染生成模板,或者从标注好的车牌图片里裁剪。
4. 运行与调试:怎么把项目跑起来并看到结果
4.1 环境准备与首次运行
拿到项目后,第一步是确认 Python 和依赖版本:
python --version pip show opencv-python如果 opencv 没装,直接pip install opencv-python。然后进入项目根目录,运行主入口:
python main.py --image images/test_01.jpgmain.py里通常会解析命令行参数,指定输入图片路径。如果没有参数解析,直接在代码里改image_path变量也行。运行后会在output/目录生成标注了车牌位置和识别结果的图片。
4.2 中间结果可视化
调试时最有用的手段是把每一步的中间结果存下来看。我一般会在预处理和定位模块里加临时保存:
cv2.imwrite('output/debug_binary.jpg', binary) cv2.imwrite('output/debug_closed.jpg', closed)这样能快速判断问题出在哪一步。如果二值图里车牌边缘就不清晰,后面怎么调定位参数都没用,得回头改预处理。如果二值图没问题但定位框偏了,就调形态学核大小或宽高比范围。
4.3 参数调整的优先级
遇到识别失败时,按这个顺序排查效率最高:
- 先看二值化图,确认车牌区域边缘是否完整
- 再看闭运算后的图,确认车牌是否连成了一个块
- 然后看定位框是否准确覆盖车牌
- 最后看字符分割是否切出了 7 个字符
大部分问题出在前两步。光照不均导致二值化断裂,或者形态学核大小不合适导致车牌没连起来,是最常见的翻车点。
5. 避坑与常见问题:调试车牌识别时容易踩的坑
5.1 二值化后车牌区域断裂
现象:边缘检测后的二值图里,车牌边框断成几段,闭运算也连不起来。
原因:Sobel 阈值设太高,弱边缘被过滤掉了。或者高斯模糊核太大,把边缘细节模糊没了。
解决:把二值化阈值从 120 降到 80 到 100 之间试,高斯核从(5,5)降到(3,3)。如果图片本身对比度低,可以先做一次直方图均衡化再边缘检测。
5.2 定位框选到了非车牌区域
现象:定位框覆盖了车灯、保险杠或背景里的矩形物体。
原因:宽高比和面积筛选不够严格,或者图片里有多个符合筛选条件的区域。
解决:在候选区域里加颜色筛选。蓝牌在 HSV 空间里 H 通道集中在 100 到 124 之间,绿牌在 40 到 80 之间。先按颜色过滤一遍再按形状筛选,准确率会明显提升。
5.3 字符分割多切或少切
现象:分割出 6 个或 8 个字符块,或者某个字符被切成两半。
原因:投影阈值太敏感,字符内部笔画间隙被误判为字符边界。或者车牌有倾斜,垂直投影不再准确。
解决:先做透视校正把车牌摆正,再分割。投影阈值从 2 调到 3 或 4,过滤掉笔画间隙。如果某个字符确实被切开了,可以在分割后加一步合并逻辑:相邻两个块宽度都小于正常字符宽度的一半时,合并成一个。
5.4 模板匹配把 0 认成 O、1 认成 I
现象:数字和字母混淆,识别结果里出现不该出现的字符。
原因:模板库里 0 和 O、1 和 I 的模板太相似,匹配分数接近。
解决:按车牌规则做后处理。国内车牌第一位是汉字,第二位是字母,后面五位是数字和字母组合,但不会出现 I 和 O。识别完后按规则过滤,把 O 替换成 0,I 替换成 1。
5.5 换一张图片就完全识别不出来
现象:在测试图上跑得好好的,换一张实拍图就定位失败。
原因:参数过拟合到测试图了。阈值、核大小、面积范围都是针对特定图片调的。
解决:把参数抽到config.py里,针对不同场景做几套预设。白天、夜间、近距离、远距离各一套,运行时根据图片亮度或尺寸自动选择。常见做法是用图片的平均亮度做判断,暗图用低阈值,亮图用高阈值。
6. 二次开发方向:从模板匹配升级到深度学习识别
6.1 替换识别模块
模板匹配的局限很明显:字体变化、模糊、倾斜都会导致识别率下降。如果你想把项目升级到实用级别,最直接的改造是把recognize.py里的模板匹配换成 CNN 推理。保留前面的定位和分割链路,只替换最后一步。
import cv2 import numpy as np # 假设你有一个训练好的字符分类模型,输入 20x40 灰度图,输出 65 类 def recognize_with_cnn(char_img, model): char_resized = cv2.resize(char_img, (20, 40)) char_normalized = char_resized.astype('float32') / 255.0 char_input = char_normalized.reshape(1, 40, 20, 1) prediction = model.predict(char_input, verbose=0) char_index = np.argmax(prediction) confidence = prediction[0][char_index] return char_index, confidence模型可以用简单的三层 CNN 训练,数据集用公开的字符数据集或者自己合成。常见做法是用 OpenCV 的putText渲染不同字体、不同模糊程度的字符图,生成几万张训练样本,训练一个轻量模型。这样识别率能从模板匹配的 70% 左右提升到 95% 以上。
6.2 端到端车牌识别
如果你想进一步简化流程,可以跳过字符分割,直接用一个检测模型输出车牌号码。这类方案通常用 CRNN 或 CTC 损失,输入整张车牌图,输出字符序列。改造时把locate.py保留,后面的分割和识别合并成一个模型推理。
6.3 验证改造效果的方法
改完之后怎么确认效果?我一般会准备一个测试集,至少 50 张不同场景的车辆图片,每张标注真实车牌号。跑完统计两个指标:定位准确率(定位框 IoU 大于 0.5 算对)和识别准确率(七个字符全对才算对)。模板匹配方案在测试集上的识别准确率通常在 60% 到 75% 之间,换成 CNN 后能到 90% 以上。如果定位准确率低于 80%,优先调定位模块,别急着换识别模型。
6.4 一个实用技巧:用颜色先验缩小搜索范围
不管用什么识别方案,定位都是最耗时的一步。我习惯在定位前加一步颜色筛选,把蓝色和绿色区域先提取出来,再在掩膜范围内做边缘检测和轮廓查找。这样能减少 70% 以上的无效轮廓,速度提升明显,误定位也少很多。
def color_mask(img): hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝色范围 blue_mask = cv2.inRange(hsv, (100, 80, 80), (124, 255, 255)) # 绿色范围 green_mask = cv2.inRange(hsv, (40, 80, 80), (80, 255, 255)) return cv2.bitwise_or(blue_mask, green_mask)这个掩膜可以直接和边缘检测结果做与运算,只保留车牌颜色区域内的边缘。参数里的 S 和 V 下限可以按实际图片调,光照暗就降低下限。
从那以后我每次改识别链路,都强制先跑一遍中间结果可视化,确认每一步的输出符合预期再往下走。希望帮到你。
本文还有配套的精品资源,点击获取