PyCharm+OpenCV车牌识别实战:从图像预处理到字符分割的完整流程
简介本资源是一套基于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-python4.8.1.78 pip install numpy1.24.3 pip install pytesseract0.3.10opencv-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 rC:\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_okTrue)用os.path.abspath(__file__)取工程路径而不是写死绝对路径这样换电脑不用改代码。exist_okTrue保证文件夹已存在时不报错。这三行看着简单但能省掉后面大量「图片存哪去了」的排查时间。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 垂直边缘检测ksize3 表示 3x3 卷积核 sobel_x cv2.Sobel(blurred, cv2.CV_16S, 1, 0, ksize3) abs_x cv2.convertScaleAbs(sobel_x) # 保存中间结果便于排查 cv2.imwrite(os.path.join(DEBUG_DIR, sobel_x.jpg), abs_x) return img, abs_xcv2.CV_16S是因为 Sobel 结果有正有负用无符号 8 位会截断负值。convertScaleAbs取绝对值并转回 8 位方便后续二值化。ksize3是最常用的值核越大边缘越粗车牌定位阶段不需要太粗的边缘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 candidatesRETR_EXTERNAL只取最外层轮廓避免车牌内部字符轮廓干扰。CHAIN_APPROX_SIMPLE压缩轮廓点省内存。长宽比范围放宽到 2.5 到 5.0 是为了兼容不同拍摄角度太严会漏掉倾斜车牌。面积阈值 2000 是针对 1000 像素宽左右的图如果原图很大这个值要按比例放大。如果候选框很多可以按面积从大到小排序取前几个再进一步筛选。4. 字符分割与识别把车牌切成单个字符再喂给 OCR4.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 warpedboxPoints把旋转矩形转成四个顶点排序逻辑是先按 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, axis0) # 找字符边界 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_imgsTHRESH_OTSU自动算阈值比手动指定更稳。投影值大于 0 表示该列有白色像素连续大于 0 的区间就是一个字符。宽度过滤 5 像素是为了去掉噪点。如果字符粘连投影中间不会归零切出来就是一个宽块这时候需要先做一次开运算把字符分开或者用连通域分析替代投影法。4.3 用 Tesseract 做单字符识别与白名单约束切出来的字符图直接喂给 Tesseract但必须加白名单否则会把「0」识别成「O」「1」识别成「I」。def recognize_chars(char_imgs): results [] for char_img in char_imgs: # 缩放到高度 40Tesseract 对小图识别率低 h, w char_img.shape scale 40.0 / h resized cv2.resize(char_img, (int(w * scale), 40)) # 加白名单只允许数字和大写字母 config --psm 10 -c tessedit_char_whitelist0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ text pytesseract.image_to_string(resized, configconfig).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 到 7Sobel ksize边缘细闭运算连不上边缘粗相邻区域粘连3二值化阈值边缘丢失候选框少噪声入选候选框多150 到 220闭运算核宽字符边缘连不成块车牌与车身粘连11 到 17字符图高度OCR 识别率低计算慢收益递减35 到 50最后说一个我踩过的坑有段时间识别率死活上不去查了两天才发现是测试图里车牌有反光Sobel 边缘检测把反光区域也当成边缘闭运算后车牌区域被撑大了一倍透视校正时把背景也拉进来了。后来在灰度化之后加了一步直方图均衡化反光影响小了很多。这个经历让我养成了一个习惯拿到新图先看直方图亮度分布太集中就先做均衡化再进流水线。希望帮到你。本文还有配套的精品资源点击获取