简介:这是一套基于Python与OpenCV实现的车牌识别系统源码资源,面向计算机视觉入门者、课程设计或毕业设计需求的学生,以及希望了解车牌检测与识别完整流程的开发者。资源在原项目基础上扩展了摄像头实时识别功能,并优化了识别模块函数,兼顾图片识别、GUI界面交互与识别结果导出Excel等实用能力。压缩包共16468个文件,以16415张jpg图像样本为主,辅以少量xml标注、py源码、dat模型文件及配置文件,整体约24.91MB,目录结构便于按模块查阅与二次开发。目前已有1843人学习下载。读者可从中获取车牌定位、字符分割与识别的完整代码实现,理解OpenCV图像处理与SVM分类在真实场景中的配合方式,并参考摄像头实时识别与数据导出的工程化思路,适合作为视觉项目练手与功能扩展的起点。
1. 车牌识别系统:从一张图片到结构化结果的完整链路
很多开发者第一次接触计算机视觉,都是从车牌识别这个场景切入的。原因很直接:它既有明确的输入输出,又能把图像预处理、边缘检测、字符分割、模式识别这几个核心环节串起来。这份基于 python-opencv 的车牌识别系统源码,做的就是这件事——输入一张车辆照片,输出车牌上的字符内容。它适合两类人:一类是刚学完 OpenCV 基础、想找一个完整项目练手的新手;另一类是需要快速搭建车牌识别原型、验证业务可行性的开发者。整个项目不依赖深度学习框架,纯靠传统图像处理算法实现,这意味着你不需要 GPU,也不需要训练数据集,拿到源码配好环境就能跑。但传统算法有它的脾气,光照、角度、车牌颜色稍有变化,识别率就会波动,这也是后面要重点聊的踩坑点。
2. 技术选型与架构拆解:为什么用传统图像处理而不是深度学习
2.1 核心依赖与技术栈
这个项目的技术栈非常克制,核心依赖只有三个:
| 依赖 | 作用 | 版本建议 |
|---|---|---|
| opencv-python | 图像读取、灰度化、边缘检测、形态学操作、轮廓查找 | 4.x |
| numpy | 矩阵运算、数组切片、像素值统计 | 1.21+ |
| matplotlib | 调试阶段可视化中间结果 | 3.x |
没有 PyTorch,没有 TensorFlow,也没有训练好的权重文件。整个识别流程靠的是 OpenCV 提供的图像处理函数和一套基于规则的分割逻辑。这种选型的好处是部署轻量,一个脚本就能跑;坏处是对场景的泛化能力有限,后面会具体说。
2.2 整体处理流程
项目的处理链路可以拆成五个阶段,每个阶段对应一个函数或一组操作:
# 伪代码流程示意 def recognize_plate(image_path): img = cv2.imread(image_path) # 1. 读取原图 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 2. 灰度化 blurred = cv2.GaussianBlur(gray, (5, 5), 0) # 3. 高斯模糊去噪 edged = cv2.Canny(blurred, 50, 150) # 4. Canny边缘检测 contours = cv2.findContours(edged, ...) # 5. 查找轮廓 plate_region = locate_plate(contours, img) # 6. 定位车牌区域 chars = split_characters(plate_region) # 7. 字符分割 result = match_characters(chars) # 8. 字符识别 return result这段流程里,第 6 步和第 8 步是变数最大的地方。定位车牌靠的是轮廓的宽高比和面积筛选,字符识别靠的是模板匹配或简单的特征比对。没有神经网络兜底,每一步的参数都需要根据实际图片调整。
2.3 目录结构推测
虽然项目正文没有给出完整的目录树,但按这类项目的常见组织方式,结构大致如下:
plate-recognition/ ├── main.py # 入口脚本,串联整个识别流程 ├── preprocess.py # 图像预处理:灰度、模糊、边缘检测 ├── locate.py # 车牌定位:轮廓筛选、透视变换 ├── segment.py # 字符分割:垂直投影、连通域分析 ├── recognize.py # 字符识别:模板匹配或特征分类 ├── templates/ # 字符模板库(如果采用模板匹配) │ ├── 0.png ~ 9.png │ └── A.png ~ Z.png ├── test_images/ # 测试图片 └── requirements.txt # 依赖清单这种拆分方式的好处是每个环节可以独立调试。比如定位不准,你只需要改locate.py,不用动识别部分的代码。我一般会建议在main.py里加一个debug开关,打开后把每一步的中间结果都存成图片,方便肉眼排查问题出在哪个环节。
2.4 车牌定位的核心逻辑
定位是整个流程里最关键的环节,定位错了后面全白搭。项目里常用的做法是基于颜色和边缘双重筛选:
def locate_plate(contours, img): candidates = [] for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) aspect_ratio = w / float(h) area = w * h # 中国蓝牌宽高比约 3:1 到 4:1 之间 if 2.5 < aspect_ratio < 5.0 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这里的参数2.5 < aspect_ratio < 5.0和area > 2000是经验值。宽高比范围太窄会漏掉倾斜拍摄的车牌,太宽又会把保险杠、车灯区域误判进来。面积阈值同理,图片分辨率越高,这个值要相应调大。常见做法是先跑几张测试图,把中间结果可视化出来,再根据实际轮廓的分布来定这两个参数。
2.5 字符分割与识别
定位到车牌区域后,下一步是把车牌里的字符一个个切出来。传统做法是垂直投影法:对车牌区域做二值化,然后统计每一列的白色像素数量,字符之间的间隔处投影值会接近零,以此作为切分点。
def split_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 # 找到投影值低于阈值的列作为分割点 split_points = [] for i in range(1, len(projection)): if projection[i] < 5 and projection[i-1] >= 5: split_points.append(i) # 根据分割点切出字符区域 chars = [] for i in range(len(split_points) - 1): char = binary[:, split_points[i]:split_points[i+1]] chars.append(char) return chars分割完成后的字符识别,项目里大概率用的是模板匹配。就是把切出来的字符缩放到统一尺寸,然后和模板库里的字符逐个比对像素差异,取差异最小的作为识别结果。这种方法的识别率高度依赖模板质量和字符归一化的精度。如果模板库覆盖不全,或者字符有轻微倾斜,识别就会出错。
3. 环境搭建与运行:从零把项目跑起来
3.1 环境准备
这个项目对环境的依赖很轻,Python 3.7 以上都能跑。建议用虚拟环境隔离依赖,避免和系统里的其他包冲突:
# 创建虚拟环境 python -m venv venv # 激活虚拟环境(Windows) venv\Scripts\activate # 激活虚拟环境(macOS/Linux) source venv/bin/activate # 安装依赖 pip install opencv-python numpy matplotlib如果下载速度慢,可以换国内镜像源,比如清华的-i https://pypi.tuna.tsinghua.edu.cn/simple。安装完成后,在 Python 里执行import cv2; print(cv2.__version__)确认 OpenCV 能正常导入。
3.2 运行入口与参数说明
项目的入口通常是main.py,调用方式一般有两种:命令行传参或直接改脚本里的图片路径。
# 方式一:命令行传参 python main.py --image test_images/car1.jpg # 方式二:直接运行,图片路径写在脚本里 python main.py如果项目用的是argparse解析参数,--image后面跟的就是待识别的图片路径。有些版本还会提供--debug参数,打开后会把中间处理结果保存到output/目录下。我建议第一次运行时把 debug 打开,这样能直观看到每一步的处理效果。
3.3 验证运行结果
跑通之后,终端会输出识别到的车牌号码,同时可能弹出一个窗口显示标注了车牌区域的图片。如果窗口没弹出来,检查一下是不是在无头环境(比如服务器)里运行,这种情况下需要把cv2.imshow相关的代码注释掉,改成cv2.imwrite保存结果。
一个正常的输出大概长这样:
检测到车牌区域: (320, 180, 200, 60) 分割出 7 个字符 识别结果: 京A12345如果输出是空的或者乱码,先别急着改代码,按下一章的排查思路一步步定位。
4. 避坑与排查:识别率上不去的五个常见原因
4.1 现象:车牌区域定位不到,返回空结果
原因:Canny 边缘检测的双阈值设置不合理,导致车牌边缘没被检测出来,或者轮廓筛选的宽高比范围太窄,把正确的轮廓过滤掉了。
解决:先把 Canny 的低阈值降到 30,高阈值降到 100,看看边缘图里车牌轮廓是否清晰。如果边缘没问题但轮廓还是筛不出来,把宽高比范围放宽到2.0 < ratio < 6.0,面积阈值降到 1000,先确保能定位到,再逐步收紧参数。
4.2 现象:字符分割多切或少切
原因:垂直投影的阈值设得太高或太低。阈值太高,字符内部的空白列被当成分割点,导致一个字符被切成两半;阈值太低,字符之间的间隔没被识别出来,两个字符粘在一起。
解决:把二值化后的车牌区域和投影曲线都存成图片,肉眼观察投影值的分布。字符间隔处的投影值通常接近零,字符内部的投影值在几十到几百之间。根据实际分布把阈值定在两者之间,一般取 5 到 15 之间的值。
4.3 现象:识别结果张冠李戴,把 0 认成 O,把 1 认成 I
原因:模板匹配对相似字符的区分能力弱,尤其是数字和字母混排的车牌。模板库里的字符如果字体和实际车牌不一致,匹配分数会很接近,导致选错。
解决:在模板匹配的基础上加一层规则约束。比如中国车牌的第一个字符是汉字,第二个是字母,后面五位是数字和字母的组合。利用这个位置规则,可以在匹配结果里排除掉不符合位置类型的候选字符。
4.4 现象:换一张图片就完全识别不出来
原因:参数过拟合到测试图片上了。宽高比、面积阈值、投影阈值这些参数都是针对某一张或某几张图片调的,换一张光照、角度、分辨率不同的图片,参数就不适用了。
解决:收集至少 20 张不同场景的车辆图片,包括白天、傍晚、正面、侧面、蓝牌、黄牌,然后写一个批量测试脚本,统计每张图片的识别结果。参数调整的目标是让整体识别率最高,而不是让某一张图片识别对。
4.5 现象:程序运行报错error: (-215:Assertion failed)
原因:最常见的是图片路径写错了,cv2.imread返回None,后续操作就崩了。另一种可能是图片格式不支持,或者 OpenCV 安装不完整。
解决:在cv2.imread后面加一行判断:
img = cv2.imread(image_path) if img is None: print(f"图片读取失败,请检查路径: {image_path}") exit(1)这样能快速定位是路径问题还是图片本身的问题。
5. 二次开发与改造:从能跑到好用
5.1 替换字符识别方案
模板匹配的识别率天花板很低,如果你想提升效果,最直接的改造方向是把字符识别换成轻量级的 CNN 模型。不需要上 ResNet 这种大网络,一个三层的卷积网络就够用。改造步骤:
- 用 OpenCV 的
cv2.dnn模块加载 ONNX 格式的模型,这样不用额外装深度学习框架。 - 把分割出来的字符缩放到 32x32,归一化后输入模型。
- 模型输出 65 个类别(10 个数字 + 26 个大写字母 + 汉字)的概率分布,取最大概率对应的字符。
# 用 OpenCV DNN 加载 ONNX 模型做字符识别 net = cv2.dnn.readNetFromONNX("char_recognition.onnx") def recognize_char(char_img): blob = cv2.dnn.blobFromImage(char_img, 1/255.0, (32, 32)) net.setInput(blob) output = net.forward() char_index = np.argmax(output) return index_to_char[char_index]这种改造的好处是识别率能提升到 95% 以上,代价是需要准备训练数据。如果不想自己训练,可以在网上找开源的字符数据集,用迁移学习微调一下。
5.2 增加车牌颜色识别
现在的项目大概率只处理蓝牌,但实际场景里还有黄牌(大型车)、绿牌(新能源车)。改造思路是在定位到车牌区域后,加一步颜色判断:
def detect_plate_color(plate_img): hsv = cv2.cvtColor(plate_img, cv2.COLOR_BGR2HSV) # 蓝色范围 blue_mask = cv2.inRange(hsv, (100, 80, 80), (130, 255, 255)) # 绿色范围 green_mask = cv2.inRange(hsv, (40, 80, 80), (80, 255, 255)) # 黄色范围 yellow_mask = cv2.inRange(hsv, (20, 80, 80), (35, 255, 255)) blue_ratio = np.sum(blue_mask) / blue_mask.size green_ratio = np.sum(green_mask) / green_mask.size yellow_ratio = np.sum(yellow_mask) / yellow_mask.size if green_ratio > 0.3: return "新能源绿牌" elif yellow_ratio > 0.3: return "大型车黄牌" else: return "普通蓝牌"颜色识别能帮你过滤掉一些误检区域,比如把蓝色车身误判成车牌的情况。
5.3 批量处理与结果导出
单张图片识别只能用来演示,实际用的时候肯定要批量处理。改造方向是加一个batch_process函数,遍历指定目录下的所有图片,把识别结果写进 CSV 文件:
import csv import os def batch_process(image_dir, output_csv): results = [] for filename in os.listdir(image_dir): if filename.endswith((".jpg", ".png", ".jpeg")): path = os.path.join(image_dir, filename) plate = recognize_plate(path) results.append({"filename": filename, "plate": plate}) with open(output_csv, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=["filename", "plate"]) writer.writeheader() writer.writerows(results)这个改造看起来简单,但实际用的时候能省很多事。我一般还会在 CSV 里加一列confidence,记录识别时的匹配分数,方便后续筛选出低置信度的结果人工复核。
5.4 一个容易忽略的细节
改造过程中最容易翻车的地方是字符分割和识别的衔接。模板匹配对字符的尺寸很敏感,如果你在分割阶段没有把字符统一缩放到模板的尺寸,匹配分数会整体偏低,导致识别结果随机。我的习惯是在split_characters返回之前,就把每个字符resize到固定尺寸,比如(20, 40),这样识别模块拿到的输入就是规整的。
从那以后我每次改分割逻辑,都会先把中间结果存图看一眼,确认字符切得干净、尺寸一致,再往下走识别。希望帮到你。
本文还有配套的精品资源,点击获取