资讯动态

Python+OpenCV车牌识别实战:从图像预处理到模板匹配全流程拆解

发布时间:2026/9/18 10:49:08 来源:尧图企业网站定制
把一张带车牌的图片扔进程序几秒钟后返回一串字符——“京A12345”。听起来很酷对不对我第一次跑通PythonOpenCV车牌自动识别的时候也兴奋了好一阵但说实话从能跑到稳定识别中间踩的坑一点都不少。这个项目虽然是经典的计算机视觉入门实战但它完全不是网上那种“复制粘贴就能跑”的玩具代码图像预处理、车牌定位、字符分割、字符识别四个环节每一环都有各自的脾气。这篇文章就把我实际做这个PythonOpenCV车牌自动识别项目的全过程拆开来讲。从环境搭建到算法细节从参数选择到翻车记录能写的基本都写了。它适合两类人一是刚学完Python基础、想找一个正经图像处理项目练手的朋友二是正在做毕设或者公司内部需要做停车管理Demo的人。这个方案走的是传统计算机视觉路线不依赖深度学习框架一台普通电脑就能跑理解起来也直观很多。1. 项目整体设计与技术选型车牌自动识别License Plate Recognition这个需求在现实里太常见了。小区出入口、园区停车场、高速收费站甚至一些工地门口都要识别车牌。但很多商用方案是整套软硬件一起卖的价格不便宜而且经常是个黑盒。自己做的好处是代码可控、逻辑透明、想改哪里改哪里还能拿来练手积累经验。1.1 为什么选择Python和OpenCV选型这事我犹豫过一阵。当时摆在面前的无非三条路纯深度学习框架PyTorch/YOLO OCR商用SDK以及OpenCV传统视觉方案。先说深度学习方案。检测车牌用YOLO识别字符用PaddleOCR或者CRNN这套流程本身没问题精度上限也高。但问题是要标注数据、要训练模型、要装CUDA环境如果你没有独立显卡训练一次小模型也够折腾而且很多人只是想把一个功能跑通并不想一上来就掉进深度学习的坑里。再说商用SDK。识别效果确实好但很多是收费的有的还带设备绑定代码拿不到核心逻辑想定制根本没门。对学生朋友或者只想做个内部工具的人来说没必要。剩下的就是OpenCV传统视觉方案。它的核心思路是用图像处理技术去“找车牌”再用模板匹配去“认字符”。这条路的好处非常明显依赖少只要opencv-python和numpy两个库逻辑链清晰每一步都能看到中间结果出问题能定位到具体环节对算力要求极低普通CPU跑起来毫无压力完全免费开源代码完全自己掌控当然它也有短板这个我后面在“常见问题”里会详细说。但至少对于室内固定机位、角度和距离可控的场景传统方案做到95%以上的识别率完全可行。Python在这里的优势就更不用多说了。语法简洁、图像处理生态完善、OpenCV的Python绑定几乎是官方一等公民。虽然C跑起来性能更好但Python做原型验证的速度快太多代码可读性也高踩坑也好踩。1.2 车牌识别的整体流程拆解我一开始以为车牌识别就是把一张图片丢进某个现成函数里。后来自己实现才发现整个流程其实是四个相对独立的环节串起来的。第一个环节是图像预处理。真实拍摄的图片不会干干净净有噪声、有亮度不均、有无关背景直接处理肯定不行。所以要先转灰度图、去噪、增强边缘。第二个环节是车牌定位。这是整个项目最核心的部分也是工作量最大的部分。要从一张完整的画面里找到“车牌在哪”。传统视觉方案靠的是车牌的几何特征——矩形、横向、有密集的竖直边缘、有特定的宽高比。第三个环节是字符分割。定位到车牌区域之后要把“京A12345”这样一个整体切分成单个字符的图片因为后续识别是一个字符一个字符地认。第四个环节是字符识别。把切出来的字符图片和预置的模板做匹配找到最像的那个模板然后输出对应的字符。这四个环节是串行依赖的关系前面出错了后面再怎么调都没用。所以我写代码的时候是分段调试的每一步都输出中间结果看一眼确认没问题再继续下一步。这是做图像处理项目最笨但最有效的调试方式。2. 环境准备与易踩坑点在开始写算法之前先把环境搞好。这一步看起来简单但我碰到的咨询问题里至少有三分之一卡在这里。很多人的代码逻辑一点问题都没有结果import cv2直接报错ModuleNotFoundError那真是欲哭无泪。2.1 Python版本和OpenCV安装这个项目对Python版本的要求并不苛刻Python 3.8到3.12都能跑。我自己用的是Python 3.10。OpenCV的话建议直接安装4.5以上版本功能完整接口稳定。安装命令非常简单pip install opencv-python opencv-contrib-python numpy这里有个细节很多人不知道opencv-python是基础库opencv-contrib-python是扩展库里面包含了很多额外的算法模块。虽然车牌识别用到的核心函数在基础库里就有但为了以后跑别的项目方便两个一起装省事。numpy是OpenCV的底层依赖必须要有因为图像在Python里本质上就是numpy数组。装完之后验证一下import cv2 import numpy as np print(cv2.__version__) print(np.__version__)如果输出了版本号比如4.8.0和1.24.3那环境就通了。2.2 环境配置最容易翻车的三个地方我见过太多人在环境上浪费时间的案例这里集中说一下。第一个坑是pip装到了错误的环境。比如你明明在Anaconda里建了一个虚拟环境结果打开终端后直接敲pip install装到了base环境里。然后在PyCharm里选了解释器但选的又不是你刚才那个虚拟环境。一来二去import cv2报错。解决办法是在PyCharm的Terminal里直接执行pip install或者在PyCharm设置里确认当前项目用的是哪个Python解释器再在对应的解释器环境里安装。记住一句话——先看解释器路径再动手装包。第二个坑是安装时网络超时。OpenCV的包很大直接pip下载经常卡住。这时候用国内镜像源就好很多pip install opencv-python opencv-contrib-python numpy -i https://pypi.tuna.tsinghua.edu.cn/simple实测下来速度能快好几倍。如果还是慢还可以用豆瓣源、阿里源都是一样的道理。第三个坑是Anaconda Prompt里找不到opencv。很多人为了管理Python包装了Anaconda但在Anaconda Prompt里运行conda install opencv之后又把自己搞晕了。其实最简单的办法就是全部用pip管理不要在conda和pip之间混着装混装经常把依赖关系搞乱。我自己现在不管什么环境统一用pip省心。最后一个忍不住提醒的点用PyCharm新建项目时有虚拟环境选项你可以让PyCharm自动帮你创建venv然后在这个venv里装依赖。新手不建议自己去手动配置系统级Python那一堆环境变量和权限问题分分钟让人崩溃。3. 车牌定位——边缘检测加形态学闭运算的完整流程车牌定位是整个识别流程的地基。这一步要是找错位置后面分割识别全白搭。我的方案是先对图像做预处理突出边缘再用形态学操作把车牌区域“连”成一块最后用轮廓检测和几何筛选锁定最终位置。3.1 预处理链路灰度化、高斯模糊、Sobel边缘检测车牌区域有一个非常明显的特征——字符密集产生大量竖直边缘。所以预处理的核心目标就是把这种“有大量竖向纹理”的区域凸显出来。第一步是灰度化。彩色图像有三个通道处理起来计算量大而且颜色信息在这个环节不是重点。cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)一行搞定。第二步是高斯模糊。图像里总有各种噪声噪声对边缘检测影响很大会把不相关的纹理也当边缘检测出来。用高斯滤波平滑一下噪声就被削弱了。核大小我选的5x5。别选太大太大了会把车牌的字符边缘也模糊掉也别太小3x3有时候压不住噪声。第三步是Sobel边缘检测。这里有个关键选择我只做X方向水平方向的梯度。因为车牌上的字符是竖直笔画的它们在水平方向上的灰度变化很明显反映在梯度上就是X方向的梯度值大。而车牌边框、车身线条这些干扰物在Y方向的梯度往往更突出。只保留X方向的边缘相当于主动过滤掉了一部分干扰。这一步是很多人忽略的细节但实测下来对定位精度提升很明显。3.2 二值化与形态学闭运算边缘检测完之后得到的是带灰度的边缘图接下来要二值化把边缘变成黑白分明的前景和背景。我用了Otsu自适应阈值二值化_, binary cv2.threshold(sobel_x, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)Otsu方法会根据图像的灰度分布自动算出一个合适的阈值不用手动调。不同光照环境下拍的图片灰度分布差异很大用固定阈值很容易失效Otsu就稳定很多。二值化之后的图像车牌的字符边缘应该是一堆竖线但字符之间、字符和边框之间还不连续轮廓检测的话会被拆成很多碎块。这时候就要请出形态学闭运算了。闭运算就是先膨胀再腐蚀。膨胀会让白色区域变大、断裂的地方连上腐蚀又把膨胀出来的边界收回去但连通性已经保留了。对车牌定位来说闭运算的作用就是让车牌的字符边缘连成一片完整的白色矩形。核大小我选的17x3。为什么是这么个尺寸因为车牌是横向矩形字符也是横向排开的所以要在X方向多连接、Y方向少扩展。尺寸太小的核连不起来太大了又容易把别的区域也连进来。17x3是根据车牌字符间距和车牌比例试出来的经验值对大多数场景都适用。kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 3)) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)3.3 轮廓查找与候选区筛选形态学处理之后图像里应该有大片连通区域其中一个就是车牌。用cv2.findContours找出所有外部轮廓然后逐个计算外接矩形。contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for contour in contours: x, y, w, h cv2.boundingRect(contour) area w * h ratio w / h if area 2000 or ratio 2.0 or ratio 5.5 or h 20: continue plate_region gray[y:y h, x:x w]筛选规则就是利用车牌的先验知识面积不能太小太小的区域不可能是车牌宽高比在2.0到5.5之间。标准车牌大约是440mm x 140mm宽高比3.14左右加上拍摄角度的影响会在这个区间浮动高度至少20像素低于这个值的区域就算找到了也没法做后续字符分割这三个条件组合起来能过滤掉绝大部分背景干扰。但也要实话实说如果图片里恰好有一块面积、比例都接近车牌的广告牌或车身涂装这里也会误检。解决办法在后面的“问题排查”章节里再详细讲。3.4 一个经常被忽略的预处理细节如果输入图片尺寸很大比如几千万像素预处理会非常慢而且小细节会被放大边缘检测结果特别碎。我的经验是先判断图片最长边是否超过1000像素超了就先用cv2.resize缩放到最长边1000再继续处理。这样定位速度能快很多而且几乎不影响准确率因为车牌区域在缩放后依然保留足够的边缘特征。等锁定车牌坐标之后再映射回原图坐标去截取保证后续字符识别的分辨率足够。4. 字符分割——投影法的思路与翻车记录车牌定位拿到的是一个矩形区域里面包含车牌字符和边框背景。字符分割要做的就是把车牌图像切成单个字符。这一步看起来简单实际翻车率极高尤其是遇到汉字的时候。4.1 车牌区域的先处理从原图截出来的车牌区域不能直接拿去分割。先要做几步清理。第一步把彩色转灰度。第二步二值化。我用的是cv2.threshold配合Otsu。第三步非常重要——去掉车牌边框。车牌上下左右是有边框的边框也是白色像素如果不先处理掉投影的时候会和字符连在一起导致分割失败。去边框的方法有几种。简单粗暴的做法是直接用固定像素裁剪比如上下各裁掉5%高度左右各裁掉5%宽度。如果车牌的拍摄角度比较正这种裁剪方式完全够用。还有一种更稳健的做法是水平投影分析统计每一行的白色像素数找到字符区域的上下边界这个后面讲投影法的时候一起说。有一点要提醒二值化的时候白色背景、黑色字符或者黑色背景、白色字符都有可能。我的代码里统一约定字符是白色、背景是黑色这样后续投影统计更方便。如果二值化后反了用cv2.bitwise_not取反就行。4.2 垂直投影法分割原理字符分割的经典思路是垂直投影法。所谓垂直投影就是把二值图像按列统计白色像素的数量。字符所在的列白色像素会很多字符与字符之间的缝隙列白色像素几乎为零。统计完之后白色像素从0跳到非0的位置就是字符的开始从非0跳到0的位置就是字符的结束。用numpy可以很简洁地实现def get_projection_columns(binary): col_sum np.sum(binary 255, axis0) positions [] in_char False start 0 for i, val in enumerate(col_sum): if val 0 and not in_char: in_char True start i elif val 0 and in_char: in_char False positions.append((start, i)) return positions然后根据这些起止坐标在车牌区域里把每一列切片出来就得到单个字符的图了。4.3 汉字分裂和粘连两个老大难问题垂直投影法有一个很经典的翻车场景汉字被切成了两半。比如“京”字左边和右边之间可能有一个白色的间隙这个间隙一旦超过阈值投影结果就把“京”字当成了两个字符。后面字符识别的时候一个汉字被当成两个字符去匹配结果自然是错的。我的处理策略是分割完之后过滤掉宽度明显小于其他字符的碎片。因为车牌字符的宽度相对均匀汉字即使分裂裂出来的每一块宽度也只有正常字符的三分之一左右。按样例字符的中位宽度做一个阈值判定把过窄的碎片合并到相邻区域。这个方法不完美但在大多数情况下能把分裂的汉字救回来。另一个问题是字符粘连。如果二值化阈值没选好或者车牌有脏污字符边缘的白色像素会连在一起投影法就分不开了。这时候可以用形态学开运算先腐蚀再膨胀把粘连的地方断开。核大小选小一点比如3x3一步步试过了就过了别把字符本身也腐蚀没了。分割完成后把每个字符统一缩放成固定尺寸比如40x20像素。这样后续模板匹配的时候模板和待测字符的尺寸就一致了不用再做额外的归一化。5. 字符识别——模板匹配在车牌场景为什么够用字符分割之后每个字符都是一张小图。接下来要把它和字符模板做比对找出最相似的模板输出对应字符。这一步我选的是模板匹配而不是OCR或者深度学习下面说说理由和实现。5.1 三种字符识别方案对比做字符识别之前我列了一个简单的对比表方案优点缺点适用场景模板匹配实现简单、无训练成本、速度快对字体变化敏感、需要建模板库字符字体统一、类别固定的场景Tesseract OCR通用性强、能认多种语言中文车牌支持差、字体稍变就失灵扫描文档、自然场景文字CNN分类器精度高、鲁棒性强需要训练数据、环境依赖多大量不同字体、复杂场景车牌识别的实际场景有个特点车牌字符的类别是有限的。省份简称汉字大约30多个、字母24个排除I和O、数字10个加起来也就70类左右。字体在标准车牌上也是相对统一的。这意味着模板匹配的短板——对字体变化敏感——在这个场景里没有想象中那么致命。Tesseract OCR我也试过它对印刷体和手写体的识别确实不错但车牌字符周围有边框、有铆钉、有反光Tesseract经常把额外的内容当成字符的一部分而且中文车牌识别率一言难尽。调试了几次之后就放弃了。所以最后坚定的选了模板匹配。这里补充一句如果你的应用场景是各种不同样式的车牌比如国外车牌字体五花八门那模板匹配就不够用了老老实实上深度学习才是正道。但标准国内蓝牌模板匹配真的能打。5.2 模板库的构建思路模板匹配的第一步是准备模板库。网上能找到别人做好的车牌字符模板包但更多时候你得自己准备。自己做的好处是可以针对你实际要识别的车牌字体来定制。模板的来源有两种。一种是直接从标准车牌图片里切出来但需要标注麻烦。另一种是找一套和车牌字体接近的字体文件用代码渲染出字符图片再经过和实际车牌相同的预处理流程灰度化、二值化、缩放生成模板。我用的是第二种简单可控而且方便随时补充字符类别。模板分为两组汉字模板和字母数字模板。汉字模板单独放一个目录字母数字模板放另一个目录。因为汉字的笔画复杂和字母数字的特征差异很大放一起匹配容易出现奇怪的结果。生成完模板后文件名就是字符本身比如“京.jpg”、“A.jpg”、“1.jpg”。这样匹配完直接拿文件名做结果输出非常方便。5.3 匹配算法与识别代码实现OpenCV的matchTemplate函数实现了模板匹配原理是在待测图像上滑动模板逐个位置计算相似度。对于单字符的识别待测字符和模板都是小图计算量很小。匹配方法我用的是cv2.TM_CCOEFF_NORMED归一化相关系数匹配。这个方法对光照变化不敏感而且返回值在0到1之间可以认为接近1的就是最佳匹配。def match_char(char_img, template_dir): best_similar -1 best_char None for template_file in glob.glob(os.path.join(template_dir, *.jpg)): template cv2.imread(template_file, cv2.IMREAD_GRAYSCALE) template cv2.resize(template, (char_img.shape[1], char_img.shape[0])) result cv2.matchTemplate(char_img, template, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ cv2.minMaxLoc(result) if max_val best_similar: best_similar max_val best_char os.path.splitext(os.path.basename(template_file))[0] return best_char这里有几个操作要点模板读取时用cv2.IMREAD_GRAYSCALE保证输入是灰度图模板resize到和待测字符相同的尺寸否则模板函数会报错匹配完成之后用minMaxLoc取出最大相似度值和对应的模板名整体识别的时候第一个字符用汉字模板库匹配后面的字符用字母数字模板库匹配。不要全部混在一起匹配。混在一起的话“0”和“O”这种相似字符很容易搞混分开匹配还能降低一点错误率。5.4 我在匹配过程中的实测心得模板匹配在实际跑的时候有几次让我印象很深。第一次是识别模糊的车牌照片字符边缘不清晰二值化之后字形有残缺。匹配结果出现了不少误判比如“B”认成“8”“D”认成“0”。后来在预处理环节加了一步中值滤波轮廓平滑了不少误判率明显下降。第二次是车牌区域有倾斜。车辆拍照的时候车牌难免歪一点字符也跟着歪歪了的字符和正着的模板匹配效果自然差。这就要在定位之后加入透视矫正步骤通过车牌四个角点做透视变换把车牌拉正再做后续处理。这个步骤有点复杂但确实能救回不少倾斜场景。第三次是把相似度阈值设得太低。一开始我只要最高匹配分数超过0.5就采用结果结果很多低质量字符被强行识别成了错误字符。后来我把阈值调到0.7低于这个值的一律返回“无法识别”整体准确率反而上去了。宁可不识别也不要认错这在车牌场景里很重要——认错一个数字可能导致完全不同的车辆信息。6. 用PyQt5做一个可视化的车牌识别工具算法链路跑通之后我突发奇想干脆给它包了一个PyQt5的GUI界面。原因很简单识别结果放在控制台里自己看看还行给别人演示或者让门卫师傅用实在上不了台面。而且做GUI的过程也能把整个项目的模块化程度拉高。6.1 界面交互思路界面设计得比较简单一个加载图片按钮一个显示原图的区域一个显示识别结果的文本框。加载图片后程序自动执行定位、分割、识别在图上用绿色矩形框标出车牌位置文本框显示识别出的车牌号码。整个流程对使用的人来说就两步——选图片看结果。6.2 OpenCV图像如何在Qt界面显示这里有一个技术细节值得讲一下OpenCV读入的图像默认是BGR格式的numpy数组而Qt的QLabel显示图片需要QImage或QPixmap。颜色通道不转换的话显示出来会整体偏蓝。转换方法如下def cv2_to_qpixmap(cv_img): rgb_image cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) height, width, channel rgb_image.shape bytes_per_line channel * width q_image QImage(rgb_image.data, width, height, bytes_per_line, QImage.Format_RGB888) return QPixmap.fromImage(q_image)核心就是cv2.cvtColor做颜色空间转换。这个坑我第一次就踩了显示出来的图片蓝汪汪一片还以为代码写错了后来才想起OpenCV用的是BGR顺序。6.3 把识别流程封装成可复用组件GUI这件事给我最大的收获不是界面本身而是强迫我把识别流程好好封装了一下。我把整个流程抽象成了几个函数preprocess_image()——预处理locate_plate()——定位车牌split_characters()——分割字符recognize_character()——识别单个字符recognize_plate()——串联整个流程串联函数大概长这样def recognize_plate(image_path): image cv2.imread(image_path) gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) plate_region, box locate_plate(gray) if plate_region is None: return 未检测到车牌, image chars split_characters(plate_region) if len(chars) 7: return 字符分割异常, image plate_text for i, char_img in enumerate(chars): if i 0: plate_text match_char(char_img, templates/chinese) else: plate_text match_char(char_img, templates/alnum) return plate_text, image封装好之后再想去适配其他输入源就方便了。比如接摄像头实时识别只需要在视频流的每一帧里调用recognize_plate再把结果叠加到帧上显示就行。我在自己的电脑摄像头测试过帧率虽然不高但跑通整个流程是没问题的。7. 常见问题与排查技巧实录做这个项目的过程里各种问题层出不穷。我把最有代表性的几个整理成一张速查表方便照着排查。现象可能原因排查/解决方案import cv2报ModuleNotFoundErrorPython解释器选错或包没装进当前环境检查解释器路径在对应环境里执行pip install opencv-python识别结果全乱码模板匹配时把汉字和字母混在一起分开模板库第一个字符走汉字匹配车牌定位到车身贴纸贴纸的宽高比和车牌接近增加颜色判断蓝牌有蓝色色调或限制ROI区域汉字被切成两半汉字内部白色像素间隙太大投影断裂用宽度中位数过滤碎片并合并“B”被识别成“8”字符模糊、字形残缺预处理增加中值滤波或调高匹配阈值图片偏色OpenCV读图是BGR显示时用了RGB格式用cvtColor转换后再显示视频流识别卡顿分辨率太高、每帧全图处理先缩放再定位定位后在原图截取识别只对ROI做分割除了表格里的还有两个我在实际中总结出来的经验性技巧。第一摄像头安装角度别太刁钻。固定机位的摄像头角度尽量正对车牌俯仰角控制在30度以内左右偏移也别太大。角度越大透视变形越严重字符分割和匹配的难度呈指数级上升。如果没法保证角度优先做透视矫正别硬扛。第二夜间场景补光比调算法更重要。夜间的车牌识别率低很大一部分原因是图像太暗、噪声太多。如果你的应用场景有夜间需求优先解决光照问题。我在测试的时候发现简单加一个补光灯或者调高摄像头的曝光补偿识别率比调一晚上算法参数提升得还明显。这个道理放在工业现场也一样——尽量从源头优化图像质量别指望后处理能救回一张烂图。最后说一下这套方案的局限性。如果车牌的字体不是标准字体比如新能源汽车绿色渐变车牌或者个性化车牌模板匹配的准确率会明显下降。还有一个就是倾斜严重的图片不矫正的话后面几乎全废。想要一个“什么情况都能识别”的系统还是得考虑引入深度学习模型来做检测或者叠加更负责的矫正算法。但作为理解图像处理原理、掌握OpenCV核心操作的实战项目这个方案已经非常值得动手做一遍了。我个人在折腾完这个项目之后最大的感受是OpenCV里每个函数单独看都不难难的是把它们按照正确的顺序、正确的参数组合起来。而车牌识别这个项目恰好把灰度变换、滤波、边缘检测、形态学、轮廓分析、模板匹配这些最基础也最常用的图像处理技术全部穿起来了。如果你能自己独立把这条链路跑通再去看其他图像处理项目思路会清晰很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价