
简介这是一份基于 OpenCV 与 Python 的车牌识别系统完整代码包面向毕业设计、期末大作业和课程设计的学生也适合刚接触计算机视觉的开发者。项目围绕数字图像处理中的车牌定位、字符分割与识别完整流程展开包含图像预处理、车牌区域检测、字符切分、SVM 分类识别等核心模块整体采用模块化设计代码注释详细、步骤清晰下载后简单配置即可运行。资源包共 18 个文件含 5 个 Python 源文件、多张测试与结果图片、可直接调用的中文车牌 SVM 模型数据dat、演示 PPT 和说明文档整体约 4.73MB由于内置了训练好的模型与样例图片可省去重新采集数据和训练的环节目录结构清晰便于按模块阅读与复用。已有 1588 人学习下载除主程序外还配套图像处理函数、识别脚本和 README 说明适合作为完整项目参照、答辩讲解或二次开发基础可帮助快速落地一套可演示、可扩展的车牌识别方案。1. 是时候用 OpenCVPython 跑通一个车牌识别系统了这套代码能解决什么停车场出入口的识别灯、老小区闸机的离线识别、还有大学毕设里最常见的演示项目背后大多是一套基于 OpenCVPython 的车牌识别系统。它的思路和深度学习方案不一样不依赖 GPU、不训练模型靠图像处理把车牌从画面里抠出来再拿去和模板比对几行代码就能在普通笔记本上跑出结果。这套方案最大的价值在于“下载即用”代码包结构清晰、依赖少从一张带车牌的图片到打印识别结果通常只需要改一个图片路径。适合正在做图像处理课程设计的学生、刚接触计算机视觉的开发者以及想理解传统视觉方案边界的技术人。2. 跑通环境与拿到可运行代码OpenCVPython 的安装与项目结构拆解操作系统里 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 的颜色范围去做过滤比如“蓝色是 B255, G0, R0”然后写死这几个值。真实图像里有光照、阴影、车漆反光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、VH 下限设为 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(axis0) # 找出像素数为 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(axis0)是对二值图每一列求和白色像素越多值越大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 里按CtrlShiftP执行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), dtypenp.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。记录比记忆可靠得多。希望今天的这些细节能帮你在自己的项目里少走几段弯路。本文还有配套的精品资源点击获取