简介:这是一份基于 OpenCV 与 Python 的车牌识别系统完整代码包,面向毕业设计、期末大作业和课程设计的学生,也适合刚接触计算机视觉的开发者。项目围绕数字图像处理中的车牌定位、字符分割与识别完整流程展开,包含图像预处理、车牌区域检测、字符切分、SVM 分类识别等核心模块,整体采用模块化设计,代码注释详细、步骤清晰,下载后简单配置即可运行。资源包共 18 个文件,含 5 个 Python 源文件、多张测试与结果图片、可直接调用的中文车牌 SVM 模型数据(dat)、演示 PPT 和说明文档,整体约 4.73MB;由于内置了训练好的模型与样例图片,可省去重新采集数据和训练的环节,目录结构清晰,便于按模块阅读与复用。已有 1588 人学习下载;除主程序外,还配套图像处理函数、识别脚本和 README 说明,适合作为完整项目参照、答辩讲解或二次开发基础,可帮助快速落地一套可演示、可扩展的车牌识别方案。
1. 是时候用 OpenCV+Python 跑通一个车牌识别系统了:这套代码能解决什么
停车场出入口的识别灯、老小区闸机的离线识别、还有大学毕设里最常见的演示项目,背后大多是一套基于 OpenCV+Python 的车牌识别系统。它的思路和深度学习方案不一样:不依赖 GPU、不训练模型,靠图像处理把车牌从画面里抠出来,再拿去和模板比对,几行代码就能在普通笔记本上跑出结果。这套方案最大的价值在于“下载即用”:代码包结构清晰、依赖少,从一张带车牌的图片到打印识别结果,通常只需要改一个图片路径。适合正在做图像处理课程设计的学生、刚接触计算机视觉的开发者,以及想理解传统视觉方案边界的技术人。
2. 跑通环境与拿到可运行代码:OpenCV+Python 的安装与项目结构拆解
操作系统里 Python 解释器版本“能用但又不完全能用”,是这类项目最容易在第一步卡住人的原因。先明确一个原则:尽量避开 Python 3.13 这类刚发布的新版本,因为 OpenCV 的轮子包未必跟得上;也别用系统自带的老旧 Python 2.x,语法都对不上。常见做法是装 Python 3.8 到 3.11 之间的稳定版本,然后为这个独立项目单独建一个虚拟环境,避免和全局环境里的包互相污染。这样后面无论是 pip 安装还是排错,都在一个干净环境里进行,代码包里的依赖不会因为别的问题报错。
2.1 用 venv 创建隔离环境:不用 conda 也能把依赖装干净
虚拟环境不是必选项,但强烈建议做。很多“下载即用”的车牌识别代码报错,源头不是代码本身,而是机器里 OpenCV 版本和系统 Python 里的其他包冲突。先建一个目录,在里面创建虚拟环境,然后激活它。
mkdir plate_recognition && cd plate_recognition python -m venv venv # Windows 下激活: venv\Scripts\activate # Linux / macOS 下激活: source venv/bin/activate激活成功后,命令行前面会出现(venv)前缀,这之后安装的所有包都只存在于这个目录里,不会污染系统环境。如果电脑里原本装了多个 Python 版本,建议先执行python --version确认当前版本是 3.8 到 3.11 之间的某一个,版本不对就换一个解释器路径再创建。
接着安装依赖。车牌识别项目常用的包就三个:OpenCV、NumPy 和 imutils。imutils 是一个很小但实用的工具库,里面封装的resize和grab_contours函数能少写不少兼容样板代码,后面找轮廓时会用到。
pip install opencv-python imutils numpy python -c "import cv2; print(cv2.__version__)"关键参数解释:opencv-python是官方预编译的轮子包,里面包含cv2模块,日常图像处理功能都在里面;imutils版本保持最新即可;numpy是 OpenCV 底层的数组运算库,必须安装。最后那条命令如果打印出 4.x 版本的号,就说明 OpenCV 已经能用。这里有个容易踩的小坑:安装时不要用pip install opencv-contrib-python来代替,因为两个包在某些平台上会互相覆盖文件,导致引用时出现莫名的报错。
注意:如果打印版本号时提示
ModuleNotFoundError: No module named 'cv2',说明当前终端里的解释器不是刚才那个虚拟环境里的 Python。在 VSCode 里尤其常见——右下角选择解释器时要手动选到venv目录下的那个,而不是系统自带的版本。
2.2 下载即用代码包的目录结构与调用入口
标准车牌识别代码包一般不会把所有逻辑塞进一个文件里,拆成几个模块反而更好定位问题。常见的结构大致如下:
| 文件/目录 | 职责 |
|---|---|
| main.py | 程序入口,读入图片后依次调用定位、分割、识别流程 |
| segment.py | 字符分割模块,负责把车牌区域切成单个字符 |
| recognize.py | 字符识别模块,通过模板匹配或 OCR 识别字符 |
| config.py | 集中管理 HSV 颜色范围、形态学核大小、轮廓过滤参数 |
| utils.py | 辅助函数,比如图像缩放、结果绘制、调试显示 |
| test_images/ | 存放测试图片,通常包含蓝牌、黄牌、绿牌样本 |
入口main.py的执行流程是固定的:读取图片、预处理缩放、颜色过滤定位车牌、透视校正、字符分割、模板匹配识别、串联结果输出。下面这段代码帮助你理解入口文件的大致调用顺序,实际使用时直接运行python main.py --image test_images/1.jpg即可。
# main.py 核心流程示意 import cv2 import config from segment import split_plate_chars from recognize import match_char_template def recognize_plate(image_path): image = cv2.imread(image_path) image = cv2.resize(image, (600, int(image.shape[0] * 600 / image.shape[1]))) plate_region = locate_plate(image) # 定位车牌区域 if plate_region is None: return "未检测到车牌" plate_chars = split_plate_chars(plate_region) # 字符分割 result = "" for char_img in plate_chars: char = match_char_template(char_img) # 模板匹配 result += char return result if __name__ == "__main__": print(recognize_plate("test_images/1.jpg"))把参数集中放config.py是这套代码最好用的设计。原因是车牌识别对参数极其敏感:HSV 范围、形态学核的宽度、轮廓最小面积、匹配阈值,这些值散落在各个函数里的话,调起来就是一场灾难。集中在同一个文件里,后面换场景、换图片分辨率时,只开一个文件就能完成所有微调。如果你打算把代码接到摄像头视频流里,也只需要在main.py里加一个cv2.VideoCapture循环,然后每隔几帧调用一次recognize_plate即可“复现方案”。
3. 车牌定位:HSV 颜色空间与形态学操作锁定车牌区域的完整流程
车牌定位是整个识别系统里优先级最高的一步。字符分割和识别做得再精细,前面定位偏移一点,后面都白搭。传统视觉方案里定位车牌区域不靠深度学习的目标检测,而是利用车牌本身的颜色和形状特征:蓝色、黄绿色、矩形长条。这套逻辑在单一背景下很稳,但遇到复杂场景就会暴露问题,所以每一步的参数选择都有讲究。
3.1 先把 RGB 转成 HSV:为什么颜色过滤不能直接用 BGR
初学者最容易犯的错误是直接用 BGR 的颜色范围去做过滤,比如“蓝色是 B=255, G=0, R=0”,然后写死这几个值。真实图像里有光照、阴影、车漆反光,BGR 值波动极大,同一个蓝色车牌在不同光线下能差出几百个数值。HSV 把“色相、饱和度、明度”分开以后,蓝色就是色相的一段区间,光照变化主要影响 V 通道,对 H 通道的影响小得多。
import cv2 import numpy as np def create_blue_mask(image): hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) # 蓝色车牌的标准 HSV 范围,H 在 100~124 之间是纯蓝色区域 lower_blue = np.array([100, 90, 70]) upper_blue = np.array([124, 255, 255]) mask = cv2.inRange(hsv, lower_blue, upper_blue) return mask这段代码的关键在inRange函数:它会逐像素判断 HSV 值是否在两个数组之间,是则置为 255,否则置为 0,最终输出一张单通道二值图。lower_blue里的三个值分别对应 H、S、V:H 下限设为 100 是因为低于这个值就进入了青色调区间,容易把蓝绿车标一起过滤出来;S 下限设为 90 是为了避开灰白色物体在低饱和度下被误判为蓝色;V 下限设为 70 则是为了扔掉深黑环境下的蓝色噪声。
注意:HSV 范围不能照抄网上任何一个项目就完事。你的摄像头成像偏鲜艳还是偏灰暗,要拿着调试图到现场微调。一般做法是把
mask用cv2.imshow显示出来,一边调 H 上下限一边看白色区域是否正好盖住车牌。
3.2 形态学闭运算把零散笔画连成一块完整车牌区域
颜色过滤之后得到的二值图里,车牌区域并不是一整块白色。车牌上的汉字、字母和数字是有间隙的,字符笔画之间会有黑色空隙;同时背景里的其他蓝色物体也会形成小白色碎片。这时候需要形态学操作把属于同一个车牌的字符笔画“焊接”成一块。
形态学操作里,闭运算是先膨胀再腐蚀,它能把相邻的白色区域连起来,同时保留整体形状的大小。关键是核对核的形状和尺寸:车牌是一个横向矩形条带,所以核必须是横向长条形。这里选用 17 像素宽、3 像素高的矩形核,它在横向能让字符笔画之间的距离被填平,纵向又不会把上下两排物体粘连。
def close_plate_region(mask): # 横向长条形核,宽度可依图像分辨率调整 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 3)) closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) return closed参数说明:MORPH_RECT表示矩形结构元素;(17, 3)中宽度 17 是经验值,对应约 600 像素宽度的输入图像。如果图像缩放到了 1200 像素,这个宽度就要相应放大到 30~35;如果车牌在画面里占比特别小,比如远距离拍摄,核的宽度还要再加大。原图分辨率对参数影响很大,这也是为什么把核大小配置到config.py而不是直接写死在函数里。闭运算做完以后,再用一次cv2.dilate膨胀,可以让内外轮廓线中间的小缝隙也闭合,这一步不是必须,但能有效减少车牌边框断裂导致的漏检。
3.3 轮廓筛选:宽高比、面积和边框完整度决定定位成败
二值图里闭运算结束之后,画面里通常还有不止一块白色区域,比如蓝色车身、蓝色路牌、远处车辆的车牌。接下来要用几何特征把真正的车牌轮廓挑出来。车牌的标准宽高比在 3:1 左右,加上边框一般在 2.5:1 到 4.5:1 之间;同时轮廓区域面积要足够大,太小的轮廓是远处噪声,太大的轮廓可能是车身。
def find_plate_contour(closed, image_area): contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for contour in contours: x, y, w, h = cv2.boundingRect(contour) aspect_ratio = w / h # 车牌宽高比过滤 + 面积过滤:车牌区域占整图比例有一个合理范围 if 2.5 < aspect_ratio < 4.5: area = w * h if area > image_area * 0.001 and area < image_area * 0.2: return contour return None这段代码里的两个过滤条件缺一不可。只按宽高比过滤,会把竖长的蓝色广告牌漏进来;只按面积过滤,画面里任意一块蓝色色块都可能成为候选。RETR_EXTERNAL只取最外层轮廓,避免车牌边框内层字符轮廓干扰判断;CHAIN_APPROX_SIMPLE压缩轮廓点数量,减少计算量。
到这里定位流程就闭环了:彩色图转 HSV、按颜色范围生成掩码、闭运算连通区域、轮廓几何过滤。如果车牌在画面里有明显倾斜,还可以在拿到轮廓后使用cv2.minAreaRect获取最小外接矩形和旋转角度,再逆时针旋转图像把车牌摆正。倾斜校正是影响字符分割质量的最大变量,角度超过 10 度还硬切,投影法容易把字符切断。
4. 字符分割与识别:垂直投影切字 + 模板匹配与 OCR 的落地选型
车牌区域抠出来只是第一步,接下来需要把它切成七个独立字符,再逐一识别。字符分割的质量直接决定识别准确率,因为模板匹配对“输入图像是否干净”极其敏感。一个字符被切断、两个字符粘连、边缘多一圈白边,都会让匹配分数明显下降。
4.1 垂直投影法切分字符:按列统计像素变化的直观方案
垂直投影的基本思想非常简单:对二值化后的车牌图像按列统计非零像素的数量,字符之间有间隙的地方像素数会急剧下降甚至归零,间隙所在列就是切分点。汉字宽度不等、数字和字母宽窄不一,如果只用固定宽度一刀切,很容易把“京”字后面留白多切一列。用投影法动态找到列间隙,则能适配任意字符组合。
def split_by_projection(binary): height, width = binary.shape # 按列统计非零像素数量 col_sum = binary.sum(axis=0) # 找出像素数为 0 的空隙列 gaps = [] in_gap = False for col in range(width): if col_sum[col] == 0: if not in_gap: start_gap = col in_gap = True else: if in_gap: gaps.append((start_gap, col - 1)) in_gap = False # gap 列表里就是字符之间的空隙区间 return gaps参数说明:binary.sum(axis=0)是对二值图每一列求和,白色像素越多值越大;col_sum[col] == 0表示这一列完全没有白色像素,即字符间隙;循环里记录每个连续空隙的起止列,最后返回的gaps列表就是所有切分位置。
实际切分时还有一个隐藏问题:车牌边框的铆钉和左侧蓝色区域没有被完全清理干净,投影时会出现额外空隙,把“京”字和“A”切开。解决办法是在切分前对车牌区域再做一次横向裁剪:先对整块区域做水平投影,找出字符所在的行范围,把上下边框裁掉;切分完成后检查字符数量,标准车牌是 7 位,如果分割结果超过 7 个,就把过窄的碎片丢弃;少于 7 个则说明有粘连,需要降低二值化阈值或改用连通域法。
4.2 模板匹配识别字符:用归一化相关系数找最像的模板
分割完成后的字符识别,在“下载即用”这类离线项目里最常见的方案是模板匹配。做法是把字符图缩放到和模板一样大小,然后计算字符图和模板库中每个模板的相似度,相似度最高的模板对应的字符就是识别结果。模板匹配不依赖外部模型,不需要联网注册,几十张模板图就能覆盖中国大陆常见车牌字符。
import cv2 def match_char(char_img, template_dict): best_char = "?" best_score = -1.0 for char, template in template_dict.items(): # 统一缩放字符图和模板到同样尺寸 char_resized = cv2.resize(char_img, (template.shape[1], template.shape[0])) # 归一化相关系数匹配,值越接近 1 越相似 result = cv2.matchTemplate( char_resized, template, cv2.TM_CCOEFF_NORMED ) _, max_val, _, _ = cv2.minMaxLoc(result) if max_val > best_score: best_score = max_val best_char = char return best_char参数说明:TM_CCOEFF_NORMED是归一化相关系数法,它对光照变化不敏感,可以在模板和字符图亮度不一致时保持稳定的匹配效果;cv2.minMaxLoc从匹配结果矩阵里取出最大相似度值;匹配循环结束后,如果best_score低于设定阈值比如 0.7,就返回None或直接返回?,避免把模糊字符强行识别成某个确定字符。
模板匹配最关键的坑是模板尺寸和字符图必须匹配。字符分割出来的图像宽度从 15 像素到 35 像素不等,如果不统一缩放,匹配分数会被尺寸差异严重拉低。缩放时要注意保持字符本身的长宽比不扭曲,否则“1”和“I”这种本身就相似的字符更难区分。
那要不要上 OCR?如果你的应用场景是停车场出入口那种环境较干净的图像,模板匹配完全够用,而且推理速度极快。场景里出现倾斜严重、逆光导致字符断裂、和新能源绿牌混跑等情况时,OCR 方案(比如 PaddleOCR 的字符识别分支)会显现优势。常见做法是先用模板匹配做主流程,代码里留一个 OCR 识别的接口,把两张图分别跑模板匹配和 OCR,取置信度高的一方作为输出。这样既有传统方案的速度,又能通过 OCR 兜底处理模板库里没有的字体。
5. 车牌识别避坑指南:蓝牌、黄牌、新能源牌与图像噪点的 5 个踩坑记录
方案能不能落地,看的不是理想情况下的流程,而是异常情况下的表现。以下五条是车牌识别项目里出现频率最高的故障,每条都按“现象 → 原因 → 解决”来记录,基本覆盖了传统视觉方案的主要翻车点。
5.1 ModuleNotFoundError: No module named 'cv2'
现象:代码一运行就报错,提示找不到cv2模块。
原因:这个问题百分之九十不是 OpenCV 没装,而是当前环境里根本没有cv2,或者 VSCode 选择的解释器不是当初安装依赖的那个虚拟环境。另一个常见原因是同时安装了opencv-python和opencv-contrib-python,两个包的动态库文件互相覆盖,导致import cv2时报出奇怪的 DLL 加载错误。
解决:先执行pip list | grep opencv,查看当前环境里的 OpenCV 相关包,如果有多个就全部卸载后重装一个纯opencv-python。然后在 VSCode 里按Ctrl+Shift+P,执行Python: Select Interpreter,选中项目目录下venv里的 Python,再重新运行。如果仍然报错,打开当前终端的 Python 路径确认:python -c "import sys; print(sys.executable)",输出结果必须指向venv目录下的解释器才算环境切换成功。
5.2 定位到了车身贴膜,而不是车牌
现象:main.py跑出来的结果把蓝色车身或蓝色车膜当成了车牌区域,显示的轮廓框比车牌大好几倍。
原因:HSV 颜色过滤把整块蓝色都保留了,闭运算又把大片蓝色连成一体,宽高比和面积过滤没有拦住它。面积过滤上限设得太宽、宽高比正好落在 2.5-4.5 区间时,这个问题就会不期而至。还有一种情况是车牌目标只占画面很小一部分,面积下限设太高导致车牌被过滤掉,正好和这个错误相反。
解决:增加车牌区域的颜色占比校验。具体做法是拿到候选轮廓之后,裁剪出该区域对应的原图,再统计这块区域内蓝色像素占整体区域的比例。真实车牌的颜色占比通常超过 40%;车身贴膜虽然也是蓝色,但颜色分布更均匀且伴随大量高光点,蓝色占比会明显偏低。把这个比例加到过滤条件里,误检率立刻会降一个数量级。
5.3 强光下蓝牌被识别成白牌
现象:晴天尾随拍摄时,车牌区域反光严重,定位环节完全找不到蓝色掩码,直接报“未检测到车牌”。
原因:太阳斜射时车牌表面变成大片高光,HSV 空间里这些区域的饱和度急剧下降。S 通道低于之前设定的下限 90 时,蓝色像素被完全过滤掉,车牌区域在二值图上变成黑色空洞。
解决:下调 S 通道下限到 40~60,让低饱和度的浅蓝灰区域也能被保留。这会导致背景里的白色墙体、白色车辆进入候选,但后续的颜色占比和宽高比过滤可以筛掉它们。更稳的做法是同时生成两张掩码:一张用高饱和度下限找正常蓝牌,一张用低饱和度下限找高光蓝牌,两张掩码做或运算后统一处理。这个“双掩码”思路是传统视觉方案处理光照变化的常见手段,能有效避免单一阈值在极端光照下翻车。
5.4 汉字被分割成三块,识别结果多出两个乱码
现象:投影法切分后,字符列表里“京”字变成了两到三个碎片,导致车牌结果变成 9 位或 10 位。
原因:“京”字字形复杂,笔画内部有多处不连通区域,垂直投影在字符内部找到了“零像素列”,于是把它错当成字符间隙切开。这种问题在轮廓字体的汉字上特别突出,数字和字母反而很少出现。
解决:切分后检查字符数量和每个字符片段的宽度。正常车牌的单个字符宽度有一定比例范围,比如整张车牌宽度除以 7 再乘以 0.5~1.5。碎片宽度明显小于这个比例的,优先考虑合并到相邻片段中。简单做法是切分完成后做一个宽度约束:如果算出的字符数量大于 7 个,就遍历片段,把宽度小于正常值 60% 的窄条合并到左右邻居里,合并到正好 7 个为止。这种做法不需要重新训练任何模型,只靠后处理规则就能挽回一大半分割错误。
5.5 新能源绿牌被当成蓝牌,黄牌大货车直接漏检
现象:绿牌照片识别结果为蓝色车牌的字符组合,或者黄牌车辆在颜色过滤时漏掉整个区域。
原因:过滤逻辑只写了蓝色通道,绿色车牌颜色投不进[100, 124]这个 H 区间;黄颜色则落在 H 值 20~35 区间,同样被排除在外。
解决:在config.py里增加绿色和黄色的 HSV 过滤分支。绿牌 H 区间在 35~77,注意和蓝色阈值不要重叠;黄牌的 H 区间在 20~35 附近。代码里分别生成三张掩码,取并集作为候选区域。识别侧也要跟进:模板匹配的结果里,绿色车牌第一位字符和后面字符的处理逻辑和蓝牌一致,不需要特殊分支;顶多多加两个模板字符,把新能源汽车专用标志当成一个额外符号处理,面积和宽高比过滤的适用性和蓝牌相同。
注意:多颜色通道会让画面里其他同色物体的误检概率升高。尤其是黄色车牌,工程车辆、警示牌都有相近颜色。解决办法是收紧宽高比过滤区间,黄牌长宽比通常更接近 2:1,而不是蓝牌的 3:1,可以单独为每种颜色设置不同面积范围。
这些坑背后的共性规律是:传统视觉方案的能力边界在颜色和形状的可控范围内,光照、颜色种类和设备差异是关键变量。留好参数入口、增加后处理校验,比试图用一个“完美阈值”解决所有情况更实际。
6. 进阶与验证:用合成车牌数据测定位和识别,调参数不再靠玄学
识别率上不去的时候,第一反应别是换算法。先用一张自己生成的合成车牌图片走一遍完整流程,能精准判断问题出在定位、分割还是匹配哪个环节。下面这段代码生成一张带噪声的蓝色车牌,模拟真实拍摄条件。
import cv2 import numpy as np def generate_test_plate(): plate = np.zeros((60, 240, 3), dtype=np.uint8) plate[:] = (255, 0, 0) # BGR 蓝色背景 # 在固定位置写上白色字符 cv2.putText(plate, "JING-A12345", (5, 45), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), 2) # 添加高斯噪声模拟图像噪点 noise = np.random.normal(0, 10, plate.shape) noisy = np.clip(plate + noise, 0, 255).astype(np.uint8) # 模拟轻微透视变形 src_pts = np.float32([[0, 0], [240, 0], [240, 60], [0, 60]]) dst_pts = np.float32([[10, 5], [230, 0], [235, 55], [0, 50]]) matrix = cv2.getPerspectiveTransform(src_pts, dst_pts) warped = cv2.warpPerspective(noisy, matrix, (240, 60)) return warped参数说明:plate[:] = (255, 0, 0)填充的是 BGR 顺序的蓝色;putText里的字体大小和坐标需要根据生成的画布尺寸调整;高斯噪声的均值 0、标准差 10 表示给每个像素添加轻微随机波动;getPerspectiveTransform里两组点坐标的差异控制了透视变形的程度,差异越大,形变越强烈。
有了这张合成图,你可以分三步定位问题。第一步,运行定位代码,观察轮廓框是否准确框住车牌区域;如果定位框偏了,问题在 HSV 范围或闭运算核大小,和识别无关。第二步,在定位成功的前提下运行分割代码,把每个切出来的字符单独保存并拼接成图片查看;如果字符边缘有黑色边框或者字符断裂,问题在二值化或投影参数。第三步才是调模板匹配。
批量场景也建议用同样思路做:准备几十张包含不同颜色、不同角度的车牌图,代码里循环调用识别流程,打印每张图的识别结果和置信度。把识别失败的图片单独放到一个文件夹里,逐个查看是哪个环节出的问题。这套方法运转下来,参数调整就不再是黑匣子,每次改参数都有明确的目的和反馈。
我的习惯是把每次调整的参数记录在config.py的注释里,标注“哪张测试图改了什么、效果如何”,下次遇到类似场景直接查记录。这个习惯救过我很多次,因为车牌识别这类项目里的参数太容易遗忘,隔两周再看一张新图,根本想不起当时为什么把闭运算核宽度设成 21。记录比记忆可靠得多。希望今天的这些细节能帮你在自己的项目里少走几段弯路。
本文还有配套的精品资源,点击获取