
简介本资源是一套基于Python、PyCharm与OpenCV的车牌识别完整项目源码面向具备一定Python基础、希望入门计算机视觉与深度学习实战的开发者可用于交通监控、智能停车等场景的学习与二次开发。压缩包共69个文件约35.89MB以21个py脚本和23个pyc编译文件为核心辅以7个h5模型权重、7张jpg示例图、3个gif演示、2个xml配置、1个ttf字体及mp4录屏等覆盖从图像预处理、车牌定位、文本分割到字符识别的完整链路。项目内含HyperLPR相关模块与模型配置可直接运行detect_test等脚本查看检测效果。目前已有876人学习下载适合作为课程设计、毕业设计或CV入门练手参考帮助读者理解边缘检测、轮廓筛选、连通组件分析与CNN字符识别等关键环节并借助PyCharm高效调试与组织代码。1. 从一堆糊图到能跑的车牌识别这套 PyCharmOpenCV 资源到底值不值得拆手头有一批停车场出入口的抓拍图分辨率参差不齐角度还带点偏想用 Python 把车牌区域框出来、再把字符切出来做识别结果搜到的教程要么只讲理论、要么代码跑不通——这是我见过太多从业者的真实处境。这套「PyCharm - Python - OpenCV - 车牌识别」资源核心就是一条从环境搭建到字符分割的完整链路用 OpenCV 做图像预处理、边缘检测、形态学操作定位车牌再配合轮廓筛选和透视校正把车牌摆正最后切出单个字符供后续识别模型使用。它适合两类人一是刚接触计算机视觉、想找一个能跑通的完整小项目练手的开发者二是手里有实际抓拍数据、需要快速搭一套车牌定位预处理管线的工程师。整套东西不依赖深度学习框架纯 OpenCV NumPy 就能跑对机器要求低调试过程透明每一步的中间结果都能可视化出来这对排查问题非常关键。2. 环境搭建与图像预处理把 PyCharm 工程跑起来的第一道坎2.1 工程结构与依赖安装拿到资源后第一件事不是急着跑主程序而是先把工程结构理清楚。常见的组织方式是这样license_plate_recognition/ ├── main.py # 主流程入口 ├── preprocess.py # 图像预处理模块 ├── locate.py # 车牌定位模块 ├── segment.py # 字符分割模块 ├── utils.py # 绘图、保存等工具函数 ├── images/ # 测试图片目录 └── requirements.txt # 依赖清单在 PyCharm 里新建项目后把上述文件放进去然后配置解释器。我一般会单独建一个虚拟环境避免和系统 Python 里的包打架。依赖清单通常只有三个核心包pip install opencv-python numpy matplotlib这里有个细节opencv-python和opencv-contrib-python的区别。前者只包含主模块后者多了 SIFT、SURF 等专利算法和部分扩展模块。车牌识别这个场景用主模块就够了不需要装 contrib 版本装了反而可能因为版本冲突导致cv2.imshow报错。版本上OpenCV 4.x 和 3.x 在 API 上有几处不兼容比如cv2.findContours的返回值数量从 3 个变成了 2 个如果资源里的代码是按 3.x 写的在 4.x 环境下会直接抛ValueError。常见做法是锁定opencv-python4.5.x这个区间兼容性最稳。提示PyCharm 里如果出现ModuleNotFoundError: No module named cv2先检查解释器是不是选到了虚拟环境再看pip list里有没有装成功。Windows 上偶尔会因为权限问题装到用户目录导致 PyCharm 找不到。2.2 灰度化、高斯模糊与边缘检测的参数逻辑预处理是整个链路里最容易被忽视、但最影响后续定位效果的一步。原始抓拍图是 BGR 三通道直接做边缘检测会引入大量颜色噪声所以第一步是灰度化import cv2 import numpy as np def preprocess_image(image_path): # 读取原始图像 img cv2.imread(image_path) if img is None: raise FileNotFoundError(f无法读取图片: {image_path}) # 灰度化BGR - GRAY gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊核大小(5,5)标准差自动计算 blurred cv2.GaussianBlur(gray, (5, 5), 0) # Sobel 水平边缘检测车牌字符垂直边缘丰富水平方向梯度明显 sobel_x cv2.Sobel(blurred, cv2.CV_16S, 1, 0, ksize3) abs_x cv2.convertScaleAbs(sobel_x) # 自适应阈值二值化 binary cv2.adaptiveThreshold( abs_x, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 17, -5 ) return img, gray, blurred, binary逐段说逻辑。cv2.GaussianBlur的核大小选(5,5)是个经验值太小去不掉噪声太大把车牌边缘也糊掉了。标准差传 0 表示让 OpenCV 根据核大小自动算一般不用手动调。Sobel 这里只取 x 方向是因为车牌区域的字符排列产生密集的垂直边缘水平梯度响应强如果取 y 方向反而会把车身线条和背景纹理带进来。ksize3是最小的 Sobel 核对细边缘敏感适合车牌这种字符间距小的场景。自适应阈值那块blockSize17表示每个像素参考周围 17×17 的区域来算阈值C-5是从计算出的均值里减掉 5用来抑制背景噪声。这两个参数是调参重点如果二值化后车牌区域断断续续把blockSize调大如果背景噪点太多把C往负方向调。注意cv2.convertScaleAbs不能省。Sobel 输出是 16 位有符号数直接拿去二值化会因为负值被截断成 0导致边缘信息丢失。这一步是很多新手翻车的地方。3. 车牌定位与形态学处理从边缘图里把车牌框出来3.1 形态学闭运算与轮廓筛选策略拿到二值边缘图后车牌区域表现为一堆密集的短竖线。直接找轮廓会得到几百个碎块所以要用形态学闭运算把相邻的字符边缘连成一块def locate_plate(binary): # 定义形态学核宽 17 高 5横向连接字符边缘 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) # 闭运算先膨胀后腐蚀填充字符间空隙 morph cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 再做一次膨胀让车牌区域更完整 morph cv2.dilate(morph, kernel, iterations1) # 查找轮廓 contours, _ cv2.findContours( morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) candidates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) # 宽高比筛选车牌一般在 2.5 到 5.5 之间 aspect_ratio w / float(h) # 面积筛选去掉太小的噪点 area w * h if 2.5 aspect_ratio 5.5 and area 2000: candidates.append((x, y, w, h)) # 按面积从大到小排序取最大的作为车牌 candidates.sort(keylambda b: b[2] * b[3], reverseTrue) return candidates形态学核的尺寸(17,5)是这套方案的核心参数。宽度 17 是为了把相邻字符之间的间隙填上——国内蓝牌字符间距大概在 10 到 15 像素取决于图像分辨率核宽必须大于这个间隙才能连成一片。高度 5 是为了不把上下方向的无关区域也连进来。如果换成(5,17)那就变成纵向连接完全跑偏。宽高比筛选范围2.5 ~ 5.5覆盖了常见蓝牌和新能源绿牌的比例。蓝牌是 440mm×140mm比例约 3.14新能源绿牌是 480mm×140mm比例约 3.43。留出上下浮动空间是为了容忍拍摄角度带来的透视变形。面积阈值2000是个粗略过滤实际用的时候要根据图像分辨率调整如果原图是 1920×1080这个值可以提到 5000 以上如果是 640×480 的小图2000 可能偏大。3.2 透视校正与 ROI 裁剪定位到的车牌往往带角度直接切字符会切歪。常见做法是用cv2.minAreaRect拿到最小外接旋转矩形再做仿射变换摆正def correct_plate(img, box): x, y, w, h box roi img[y:yh, x:xw] # 转灰度后做一次 OTSU 二值化 gray_roi cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) _, binary_roi cv2.threshold( gray_roi, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU ) # 找到所有字符轮廓 contours, _ cv2.findContours( binary_roi, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) # 按 x 坐标排序保证字符从左到右 char_boxes [] for cnt in contours: cx, cy, cw, ch cv2.boundingRect(cnt) if ch h * 0.3: # 过滤掉太小的噪点 char_boxes.append((cx, cy, cw, ch)) char_boxes.sort(keylambda b: b[0]) return roi, binary_roi, char_boxesOTSU 二值化在这里比自适应阈值更合适因为 ROI 区域已经限定在车牌内背景相对单一OTSU 能自动找到最佳分割阈值。字符高度过滤用h * 0.3是因为车牌字符高度一般占车牌高度的 60% 到 80%取 30% 作为下限能有效滤掉铆钉和边框产生的碎轮廓。提示如果定位到的车牌有明显倾斜cv2.minAreaRect返回的角度需要做归一化处理——OpenCV 4.x 里角度范围是[0, 90)而 3.x 是[-90, 0)直接套用旋转矩阵会转错方向。稳妥做法是先判断角度正负再决定旋转方向。4. 字符分割与常见翻车点排查4.1 垂直投影法切分字符轮廓法在字符粘连时容易把两个字符框在一起这时候垂直投影更稳def segment_by_projection(binary_roi): # 垂直投影统计每列白色像素个数 height, width binary_roi.shape projection np.sum(binary_roi 255, axis0) # 设定阈值低于阈值的列认为是字符间隙 threshold height * 0.05 gaps [] in_char False start 0 for i, val in enumerate(projection): if val threshold and not in_char: in_char True start i elif val threshold and in_char: in_char False if i - start 3: # 过滤太窄的干扰 gaps.append((start, i)) # 处理最后一个字符延伸到边缘的情况 if in_char: gaps.append((start, width)) return gaps投影法的逻辑是字符所在列白色像素多间隙列白色像素少。阈值取height * 0.05是个保守值能容忍二值化后字符边缘的毛刺。i - start 3这个过滤是为了去掉单个噪点列。这套方法对字符间距均匀的蓝牌效果好但新能源绿牌字符间距有变化可能需要把阈值调低到0.03。4.2 避坑与排查五个血泪教训现象一程序跑完没有任何输出也不报错。原因通常是cv2.imread读到了空图但代码里没做 None 判断后续操作全在空数组上执行OpenCV 不抛异常直接返回空结果。解决每个imread后面强制加if img is None判断把路径打印出来确认。现象二车牌定位框到了车灯或保险杠上。原因是形态学核太宽把车灯边缘也连成了块。解决把核宽度从 17 降到 13 试试同时把宽高比上限从 5.5 收紧到 4.5。如果还不行加一个颜色筛选——蓝牌在 HSV 空间里 H 通道集中在 100 到 124 之间可以先做颜色掩膜再定位。现象三字符分割把“京”字切成了两半。原因是垂直投影阈值太高字符中间笔画稀疏的列被误判为间隙。解决把阈值从height * 0.05降到height * 0.02或者改用轮廓法做交叉验证两种方法结果不一致时取轮廓法的结果。现象四PyCharm 里cv2.imshow窗口一闪而过。原因是没加cv2.waitKey(0)或者加了但后面没跟cv2.destroyAllWindows()。在 PyCharm 的 Run 模式下窗口关闭后进程直接结束。解决调试阶段统一用cv2.imwrite把中间结果存到本地看比imshow稳定得多。现象五换一张新图就完全失效。原因是所有参数都是针对测试图硬调的泛化能力为零。解决把blockSize、形态学核尺寸、宽高比范围这几个参数抽成配置字典针对不同分辨率准备几组预设值运行时根据图像尺寸自动选。5. 从能跑到好用参数自适应与批量验证的实操习惯把单张图跑通只是起点真正要落地得解决批量处理和参数自适应。我一般会写一个参数扫描脚本把关键参数做成网格用一批标注好的图跑准确率import itertools import os def grid_search(image_dir, label_dir): # 待扫描的参数组合 block_sizes [15, 17, 19, 21] kernel_widths [13, 15, 17, 19] aspect_ranges [(2.5, 5.0), (2.5, 5.5), (3.0, 5.5)] best_acc 0 best_params None for bs, kw, ar in itertools.product( block_sizes, kernel_widths, aspect_ranges ): correct 0 total 0 for fname in os.listdir(image_dir): if not fname.endswith(.jpg): continue total 1 # 用当前参数跑一遍定位 pred_box run_pipeline( os.path.join(image_dir, fname), bs, kw, ar ) # 和标注框算 IoU gt_box load_label(label_dir, fname) if iou(pred_box, gt_box) 0.5: correct 1 acc correct / total if total 0 else 0 if acc best_acc: best_acc acc best_params (bs, kw, ar) return best_params, best_acc这个脚本的价值在于把“凭感觉调参”变成“用数据选参”。blockSize扫描范围 15 到 21 是因为低于 15 时自适应阈值对噪声太敏感高于 21 时局部特征被过度平滑。核宽度 13 到 19 覆盖了常见分辨率下的字符间距。宽高比三组预设对应不同拍摄角度。IoU 阈值 0.5 是目标检测里的通用标准定位框和标注框重叠超过一半就算对。跑完网格搜索后把最优参数写回配置文件再跑一遍全量测试确认没有过拟合。我习惯在utils.py里加一个save_debug_image函数把每一步的中间结果拼成一张大图存下来出问题时直接看这张图就能定位到是哪一步崩的比在 PyCharm 里打断点快得多。注意网格搜索的计算量是参数组合数的乘积。上面这个例子是 4×4×348 组每组跑一遍全量图如果图多的话耗时会比较长。实际用的时候可以先抽 20% 的图做粗筛锁定 Top 3 组合后再全量验证。从那以后我每次拿到新的图像处理项目都强制先跑一遍参数网格搜索再动手调业务逻辑——凭肉眼调参的玄学程度太高翻车概率远大于省下来的那点时间。希望这套思路帮到你。本文还有配套的精品资源点击获取