☰
PyCharm+OpenCV车牌识别实战:从图像预处理到字符分割的完整流程
2026/10/9 20:17:02 网站建设 项目流程

简介:本资源是一套基于Python、PyCharm与OpenCV的车牌识别完整项目源码,面向具备一定Python基础、希望入门计算机视觉与深度学习实战的开发者,可用于交通监控、智能停车等场景的学习与二次开发。压缩包共69个文件,约35.89MB,包含21个py脚本、23个pyc编译文件、7个h5模型权重、7张jpg示例图及gif动图、ttf字体、mp4演示视频、xml与json配置等,覆盖从图像预处理、车牌定位、文本分割到字符识别的完整链路,并附带训练好的模型文件与字体资源。项目结构清晰,含检测脚本、模型配置与说明文档,便于读者直接运行调试、理解各模块职责,并在此基础上尝试数据增强、模型优化与后处理等改进思路。目前已有876人学习下载,适合作为课程设计、毕业设计或视觉入门练手参考。

1. 从一张歪斜的车牌照片说起:PyCharm + Python + OpenCV 车牌识别到底能落地到什么程度

很多人第一次做车牌识别,手里拿到的是一张手机随手拍的照片:角度歪、光线偏、车牌占画面不到十分之一。直觉上会觉得这种输入根本没法处理,但实际跑一遍完整流程会发现,真正卡住你的往往不是识别算法本身,而是预处理阶段那几个参数没调对。PyCharm 作为 Python 开发环境,配合 OpenCV 这套计算机视觉库,能在一台普通笔记本上把「读图 → 定位车牌 → 字符分割 → 字符识别」这条链路完整跑通,不需要 GPU,也不需要联网调接口。

这套方案适合两类人:一类是想理解车牌识别底层流程、不愿意直接调云端 API 的开发者;另一类是在做智能停车、门禁模拟、课程设计这类项目,需要本地可控、能改参数、能看中间结果的场景。它解决的核心问题是:把一张含车牌的图片,经过一系列图像处理步骤,输出车牌上的字符序列。下面按实际动手顺序拆开讲,每一步都给可复现的代码和参数说明。

2. 环境搭好之前先想清楚:PyCharm 工程结构与 OpenCV 版本选择

2.1 为什么建议用虚拟环境而不是全局装包

在 PyCharm 里新建工程时,第一件事是选解释器。我一般会选「New environment using Virtualenv」,位置放在工程目录下的venv文件夹。原因很直接:OpenCV 的 Python 包在不同版本之间 API 有差异,比如cv2.findContours在 OpenCV 3.x 返回三个值,在 4.x 只返回两个值。如果全局环境里已经装了别的版本,你的代码可能在别人机器上直接报ValueError: not enough values to unpack。虚拟环境把依赖锁在工程内,换机器时导出requirements.txt就能复现。

创建完工程后,在 PyCharm 底部的 Terminal 里执行安装命令。这里注意包名:日常说 OpenCV 指的是opencv-python,但如果后续要用到 SIFT、SURF 这类专利算法,需要装opencv-contrib-python。车牌识别流程里通常用不到这些,装基础版就够。

# 在 PyCharm 内置 Terminal 中执行 pip install opencv-python==4.8.1.78 pip install numpy==1.24.3 pip install pytesseract==0.3.10

opencv-python负责图像读写、滤波、边缘检测、轮廓查找;numpy负责矩阵运算,OpenCV 读进来的图像本质就是 numpy 数组;pytesseract是 Tesseract OCR 的 Python 封装,用于最后一步字符识别。版本号写死是为了避免不同版本间行为漂移,实际使用时可以按自己环境调整,但建议先按这个组合跑通再升级。

2.2 Tesseract OCR 引擎的安装与路径配置

pytesseract只是调用接口,真正的识别引擎是 Tesseract,需要单独安装。Windows 下下载安装包后记住安装路径,Linux 下用包管理器安装。装完后必须在代码里指定可执行文件路径,否则会报TesseractNotFoundError。

import pytesseract # Windows 下指定安装路径,Linux 下通常不需要这行 pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' # 验证是否可用 print(pytesseract.get_tesseract_version())

如果这行能打印出版本号,说明引擎通了。打印不出来就检查路径里有没有中文、空格,以及是否把tesseract.exe写成了目录。这一步是新手最容易翻车的地方,很多人代码逻辑没问题,卡在环境上半天。

2.3 工程目录怎么分,中间结果往哪放

建议在工程根目录下建三个文件夹:images放输入图,output放每一步的中间结果,debug放调试用的可视化图。中间结果一定要落盘,因为车牌识别是流水线,某一步参数不对,后面全错,你必须能回看是哪一步把车牌区域切没了。

import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) IMG_DIR = os.path.join(BASE_DIR, 'images') OUT_DIR = os.path.join(BASE_DIR, 'output') DEBUG_DIR = os.path.join(BASE_DIR, 'debug') for d in [IMG_DIR, OUT_DIR, DEBUG_DIR]: os.makedirs(d, exist_ok=True)

用os.path.abspath(__file__)取工程路径,而不是写死绝对路径,这样换电脑不用改代码。exist_ok=True保证文件夹已存在时不报错。这三行看着简单,但能省掉后面大量「图片存哪去了」的排查时间。

3. 车牌定位:从彩色图到候选矩形框的完整链路

3.1 灰度化、高斯模糊、Sobel 边缘检测的参数含义

车牌定位的常见思路是:车牌区域在图像里表现为一块边缘密集、颜色相对统一的矩形。所以第一步是把彩色图转灰度,减少计算量;第二步做高斯模糊,去掉高频噪声,避免把车牌上的字符边缘和背景噪声混在一起;第三步用 Sobel 算子提取垂直边缘,因为车牌字符是垂直排列的,垂直边缘比水平边缘更密集。

import cv2 import numpy as np def locate_plate(image_path): # 读取原图 img = cv2.imread(image_path) if img is None: raise FileNotFoundError(f'无法读取图片: {image_path}') # 转灰度 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊,核大小 (5,5),标准差 0 表示由核大小自动推算 blurred = cv2.GaussianBlur(gray, (5, 5), 0) # Sobel 垂直边缘检测,ksize=3 表示 3x3 卷积核 sobel_x = cv2.Sobel(blurred, cv2.CV_16S, 1, 0, ksize=3) abs_x = cv2.convertScaleAbs(sobel_x) # 保存中间结果便于排查 cv2.imwrite(os.path.join(DEBUG_DIR, 'sobel_x.jpg'), abs_x) return img, abs_x

cv2.CV_16S是因为 Sobel 结果有正有负,用无符号 8 位会截断负值。convertScaleAbs取绝对值并转回 8 位,方便后续二值化。ksize=3是最常用的值,核越大边缘越粗,车牌定位阶段不需要太粗的边缘,3 就够。如果图片分辨率很高,比如 4000 像素宽,可以先把图缩到 1000 像素宽再处理,速度会快很多,定位精度基本不受影响。

3.2 二值化与形态学闭运算:把边缘连成块

Sobel 出来的图是边缘图,边缘是断断续续的线,不能直接找轮廓。需要先二值化,再用形态学闭运算把相邻的边缘连起来,形成一个完整的矩形区域。

def morph_close(abs_x): # 二值化,阈值 200,超过 200 的置 255,否则置 0 _, binary = cv2.threshold(abs_x, 200, 255, cv2.THRESH_BINARY) # 定义闭运算核,宽 17 高 5,横向连接字符边缘 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) # 闭运算:先膨胀后腐蚀,填补边缘间空隙 closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) cv2.imwrite(os.path.join(DEBUG_DIR, 'closed.jpg'), closed) return closed

阈值 200 是个经验值,针对的是边缘图里灰度较高的部分。如果图片偏暗,边缘整体灰度低,这个阈值会把所有边缘都滤掉,结果就是一片黑。排查方法很简单:把binary存下来看一眼,如果全黑就降阈值,比如降到 100 再试。闭运算核的宽高比很关键,车牌是扁长矩形,所以核宽大于高,17×5 是常见配置。核太大会把相邻的非车牌区域也连进来,核太小则车牌内部字符边缘连不成块。

3.3 轮廓筛选:用长宽比和面积把车牌框出来

闭运算之后,车牌区域会变成一个白色实心块。用findContours找出所有轮廓,然后按长宽比和面积过滤。中国蓝牌的长宽比大约在 3:1 到 4:1 之间,面积不能太小也不能太大。

def find_candidates(closed, img): # OpenCV 4.x 返回两个值 contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates = [] for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) aspect_ratio = w / float(h) area = w * h # 长宽比 2.5 到 5.0,面积大于 2000 像素 if 2.5 < aspect_ratio < 5.0 and area > 2000: candidates.append((x, y, w, h)) cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.imwrite(os.path.join(DEBUG_DIR, 'candidates.jpg'), img) return candidates

RETR_EXTERNAL只取最外层轮廓,避免车牌内部字符轮廓干扰。CHAIN_APPROX_SIMPLE压缩轮廓点,省内存。长宽比范围放宽到 2.5 到 5.0 是为了兼容不同拍摄角度,太严会漏掉倾斜车牌。面积阈值 2000 是针对 1000 像素宽左右的图,如果原图很大,这个值要按比例放大。如果候选框很多,可以按面积从大到小排序,取前几个再进一步筛选。

4. 字符分割与识别:把车牌切成单个字符再喂给 OCR

4.1 透视校正:把倾斜车牌拉正

定位到的车牌可能是倾斜的,直接分割字符会导致字符粘连或断裂。常见做法是用cv2.minAreaRect拿到最小外接矩形,再做透视变换。

def correct_perspective(img, rect): # rect 是 (center, (w, h), angle) box = cv2.boxPoints(rect) box = np.int0(box) # 按 x 坐标排序,区分左右 left = box[np.argsort(box[:, 0])[:2]] right = box[np.argsort(box[:, 0])[2:]] # 左上、右上、右下、左下 tl = left[np.argsort(left[:, 1])[0]] bl = left[np.argsort(left[:, 1])[1]] tr = right[np.argsort(right[:, 1])[0]] br = right[np.argsort(right[:, 1])[1]] src = np.float32([tl, tr, br, bl]) w = int(max(np.linalg.norm(tr - tl), np.linalg.norm(br - bl))) h = int(max(np.linalg.norm(bl - tl), np.linalg.norm(br - tr))) dst = np.float32([[0, 0], [w, 0], [w, h], [0, h]]) M = cv2.getPerspectiveTransform(src, dst) warped = cv2.warpPerspective(img, M, (w, h)) return warped

boxPoints把旋转矩形转成四个顶点,排序逻辑是先按 x 分左右两组,再在每组内按 y 分上下。透视变换的目标矩形宽高由源四边形边长推算,保证不变形。这一步做完,车牌应该是一个水平矩形,字符垂直排列。

4.2 垂直投影法分割字符

校正后的车牌是灰度图,先二值化,再沿垂直方向统计每列白色像素数量,字符所在列白色像素多,字符间隙列白色像素少,据此切分。

def split_characters(warped): gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 垂直投影:每列白色像素数 projection = np.sum(binary == 255, axis=0) # 找字符边界 char_imgs = [] in_char = False start = 0 for i, val in enumerate(projection): if val > 0 and not in_char: in_char = True start = i elif val == 0 and in_char: in_char = False if i - start > 5: # 宽度小于 5 的忽略 char_imgs.append(binary[:, start:i]) return char_imgs

THRESH_OTSU自动算阈值,比手动指定更稳。投影值大于 0 表示该列有白色像素,连续大于 0 的区间就是一个字符。宽度过滤 5 像素是为了去掉噪点。如果字符粘连,投影中间不会归零,切出来就是一个宽块,这时候需要先做一次开运算把字符分开,或者用连通域分析替代投影法。

4.3 用 Tesseract 做单字符识别与白名单约束

切出来的字符图直接喂给 Tesseract,但必须加白名单,否则会把「0」识别成「O」,「1」识别成「I」。

def recognize_chars(char_imgs): results = [] for char_img in char_imgs: # 缩放到高度 40,Tesseract 对小图识别率低 h, w = char_img.shape scale = 40.0 / h resized = cv2.resize(char_img, (int(w * scale), 40)) # 加白名单,只允许数字和大写字母 config = '--psm 10 -c tessedit_char_whitelist=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' text = pytesseract.image_to_string(resized, config=config).strip() results.append(text) return ''.join(results)

--psm 10表示把图片当作单个字符处理,这是关键参数,默认的 psm 3 会按整页文本处理,单字符图会识别失败。白名单里包含数字和大写字母,覆盖中国车牌字符集。缩放高度到 40 是经验值,Tesseract 在字符高度 30 到 50 之间表现最稳。如果识别结果为空,先检查resized是不是全黑或全白,再检查 psm 参数。

5. 避坑与排查:车牌识别流水线里最容易翻车的五个点

5.1 现象:定位框把整个车身都框进去了

原因通常是闭运算核太大,把车身边缘和车牌边缘连成了一片。解决方法是把核宽从 17 降到 11 或 9,同时把面积阈值提高,过滤掉过大的候选框。排查时把closed.jpg打开看,如果车牌和车身连成一个大白块,就是核的问题。

5.2 现象:字符分割出来是空白图

原因多半是二值化反了。车牌是蓝底白字,转灰度后白字灰度值高,二值化后白字是 255,投影有值。但如果用了THRESH_BINARY_INV,白字变 0,投影全零,切出来就是空。解决方法是把二值化结果存下来看一眼,确认字符是白色、背景是黑色。如果反了,去掉INV或改用THRESH_BINARY。

5.3 现象:Tesseract 识别结果全是乱码

先确认tesseract_cmd路径对不对,再确认--psm 10有没有加。如果都对,检查输入字符图是不是太小或太模糊。Tesseract 对低分辨率图识别率很差,把字符图缩放到高度 40 以上再试。另外白名单里不要加小写字母,车牌里没有小写。

5.4 现象:同一张图每次跑结果不一样

OpenCV 的findContours返回轮廓顺序不保证稳定,如果代码里依赖轮廓顺序取第一个候选框,结果就会飘。解决方法是对候选框按面积或位置排序,取最靠中间、面积最接近车牌先验面积的那个。不要依赖默认顺序。

5.5 现象:中文车牌识别不出来

Tesseract 默认英文模型,中文省份简称识别不了。需要下载中文训练数据,并在 config 里指定-l chi_sim。但中文模型对单字符识别效果一般,常见做法是省份简称单独用模板匹配,后面的字母数字用 Tesseract。这个方案在纯英文数字场景下够用,中文场景需要额外处理。

6. 把识别率从「能跑」推到「能用」:三个我反复验证过的调参习惯

第一个习惯是每步都落盘。车牌识别是六级流水线,任何一步参数偏了,最终结果都是空字符串。我一般会在灰度、Sobel、闭运算、候选框、校正后车牌、二值化字符图这六个节点各存一张图,出问题时从后往前看,哪一步开始不对就调哪一步。这个习惯看着笨,但比盲猜参数快得多。

第二个习惯是固定测试集。不要每次换一张新图调参,那样永远不知道是参数变好了还是图片变简单了。我一般会挑 10 张图,覆盖正拍、斜拍、暗光、远距离、车牌占画面小这五种情况,每次改参数跑一遍这 10 张,记录识别正确的字符数。只有整体正确率上升才保留改动,否则回退。这个做法能避免「调好一张、崩掉九张」的玄学循环。

第三个习惯是给 OCR 加后处理。Tesseract 输出后,按车牌规则做校验:第一位是省份简称(中文场景)或字母,第二位是字母,后面五位是字母数字混合。如果识别结果不符合这个模式,说明某一步出了问题,可以触发重试或降级到模板匹配。这个校验逻辑不复杂,但能挡掉大量明显错误的结果。

下面这张表是我在 10 张测试图上跑出来的参数对比,供参考。不同数据集最优值会变,但趋势一致:闭运算核宽在 11 到 17 之间,阈值在 150 到 220 之间,字符图高度缩放到 40 左右,是相对稳的区间。

参数偏小表现偏大表现常用区间
高斯核大小噪声残留,候选框多边缘模糊,车牌定位失败3 到 7
Sobel ksize边缘细,闭运算连不上边缘粗,相邻区域粘连3
二值化阈值边缘丢失,候选框少噪声入选,候选框多150 到 220
闭运算核宽字符边缘连不成块车牌与车身粘连11 到 17
字符图高度OCR 识别率低计算慢,收益递减35 到 50

最后说一个我踩过的坑:有段时间识别率死活上不去,查了两天才发现是测试图里车牌有反光,Sobel 边缘检测把反光区域也当成边缘,闭运算后车牌区域被撑大了一倍,透视校正时把背景也拉进来了。后来在灰度化之后加了一步直方图均衡化,反光影响小了很多。这个经历让我养成了一个习惯:拿到新图先看直方图,亮度分布太集中就先做均衡化,再进流水线。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询