ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

OpenCV车牌识别系统详解:定位、分割、识别全流程实现

OpenCV车牌识别系统详解:定位、分割、识别全流程实现 简介基于OpenCV与Python实现的车牌号识别与提取源码面向计算机视觉开发者、智能交通从业者及编程学习者解决从图像中自动定位并读取车牌号码的问题。项目覆盖车牌定位、字符分割、字符识别等关键环节可直接用于停车场收费、交通监控、车辆管理等场景。压缩包共43个文件约2.47MB其中37张JPG车牌样本为算法提供训练与验证数据2个Python源文件实现核心流程并附带编译版本方便直接运行另有Markdown文档与readme说明帮助理解目录结构与调用关系。已有434人学习下载。借助这套源码可以掌握OpenCV图像处理与机器学习在车牌识别中的实际落地方法也能基于样本集与代码调整检测参数作为毕业设计、课程项目或智能交通系统二次开发的基础。1. 这套车牌号识别与提取源码到底解决什么问题标题里这串字说白了就是一个典型的 OpenCV 图像处理项目输入一张带车辆的图片程序用纯 Python OpenCV 把车牌区域从画面里定位出来裁剪矫正再把 7 个字符逐个切分、识别最终返回车牌号文本。整个流程拆成定位、分割、识别三段式这也是市面上几乎所有车牌识别方案的通用骨架。它不依赖深度学习框架CPU 上就能实时跑特别适合拿来做毕业设计、课程设计也适合刚接触视觉工程的开发者拿真实图片练手。这个方向之所以一直有热度是因为它把图像预处理、形态学、轮廓分析、模板匹配这些 OpenCV 核心知识点串成了一条完整链路做完一遍你对 OpenCV 的掌握程度会上一个台阶。2. 跑通之前先想清楚的事为什么选 OpenCV依赖怎么装源码怎么组织2.1 为什么这套方案坚持 OpenCV而不直接用 Halcon 或深度学习先回答一个经常被问的问题车牌识别为什么不直接用深度学习还要折腾 OpenCV答案是成本和可控性。深度学习方案比如 LPRNet、CRNN识别率确实高但要准备几千张标注车牌、要装 PyTorch 或 TensorFlow、要训练和调参对一台没有 GPU 的机器来说并不友好。OpenCV 的经典图像处理路线用 Sobel 边缘检测、形态学操作、轮廓查找就能把车牌区域定位出来再用模板匹配或轻量分类器做字符识别单张推理在 CPU 上十几毫秒就能跑完这正好覆盖了停车场入口、园区门禁这类光照相对可控的场景。再对比 Halcon。Halcon 是工业视觉里常见的商业库它的 Blob 分析、形状匹配、OCR 算子成熟但 License 费用摆在那部署到客户现场还要 runtime 授权。OpenCV 开源Python 生态直接 pip install 就能用跨平台也好带。对毕设、原型验证、中小型嵌入式项目来说OpenCV 是投入产出比最高的选择。当然如果你要处理的是恶劣天气、极端角度、夜间远距离抓拍这种高难度场景再考虑用深度学习替换掉其中的定位或识别环节第 6 章会讲怎么替换。2.2 最小可运行环境Python 版本、OpenCV 安装与验证我一般要求 Python 3.8 以上OpenCV 用 4.5 以上稳定版就行不必追最新版。numpy 是 cv2 的底层依赖pip 会自动装上但建议显式安装一遍避免版本冲突。在 Windows、Ubuntu、macOS 上标准安装流程是这样的# 创建虚拟环境避免污染系统 Python python -m venv venv # Windows 激活 venv\Scripts\activate # Ubuntu / macOS 激活 # source venv/bin/activate # 升级 pip 后安装依赖 python -m pip install --upgrade pip python -m pip install opencv-python numpy装完不要急着跑识别先做一次环境验证。这一步能提前过滤掉一半的踩坑import sys import cv2 import numpy as np print(sys.version) print(OpenCV:, cv2.__version__) print(NumPy:, np.__version__)能打印出版本号说明 cv2 模块可以被正确导入。如果这里就报 ModuleNotFoundError多半是装进了另一个 Python 环境或者 opencv-python 和 opencv-contrib-python 两个包冲突了具体排查看第 5 章。在 Ubuntu 上配置 OpenCV 时有人习惯用 apt 装 libopencv-dev但那样装的是系统级 OpenCVPython 里 import cv2 不一定认还是建议装在虚拟环境里避免和系统库纠缠。2.3 源码模块怎么组织先跑哪一段拿到任何一套车牌识别源码先别急着双击 main 文件。我习惯先把代码按职责拆开看读取图片和预处理、车牌定位、字符分割、字符识别再加一个测试图片目录和字符模板目录。很多老源码把这四步全写在一个脚本里参数写死换个环境就要改半天。你先在代码里找到车牌定位函数确认它输出的是候选区域的坐标再往下看分割和识别这样不管是调试还是换自己的图片思路都清晰。先跑通再调参。跑通的标准是拿源码自带的测试图片能输出一串车牌号哪怕识别错了。只要主流程不报错后续的工作就是逐段优化定位和识别参数。切忌一上来就追求完美识别率车牌识别主流程是环环相扣的定位框偏了后面的分割和识别全跟着错所以第 3 章先把定位做扎实。另外注意一个细节源码里读取图片或模板通常用相对路径比如images/test1.jpg或templates/0.png。这意味着你必须在源码所在目录下启动脚本直接从别的目录执行很容易报 FileNotFoundError。我拿到源码的第一件事就是把所有路径改成基于当前文件目录的绝对路径或者统一放到一个 config 字典里管理。测试图片目录和模板目录最好和源码分开后续批量测试时直接换目录就能跑。3. 车牌定位Sobel 边缘检测与 HSV 颜色筛选双路合并3.1 预处理灰度、高斯滤波与 Sobel 垂直边缘车牌在画面里的特征有两个一是字符密集产生丰富的垂直边缘二是车牌底色蓝、黄、绿有固定颜色范围。定位的第一步就是围绕这两个特征做预处理。先转灰度再用高斯滤波去掉传感器带来的高频噪声然后计算 Sobel 垂直方向梯度。import cv2 import numpy as np def preprocess(gray): # 高斯滤波核越大越模糊5x5 对 800px 宽的图够用 gray cv2.GaussianBlur(gray, (5, 5), 0) # Sobel 垂直边缘车牌字符是典型的竖纹理 sobel cv2.Sobel(gray, cv2.CV_16S, 1, 0, ksize3) sobel cv2.convertScaleAbs(sobel) # OTSU 自动阈值避免手动调亮度 _, binary cv2.threshold(sobel, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) return binary注意 Sobel 用CV_16S接收中间结果是因为梯度方向可能是负值直接存成 uint8 会截断成 0这一步很多人第一次写会翻车。ksize3是 Sobel 核大小对 800 像素宽度的图像刚好如果输入图很大ksize 可以改成 5但边缘会变粗后续形态学参数要跟着调。OTSU 自动阈值的好处是不需要根据光照手动改阈值但对整幅图直方图双峰不明显时效果一般所以后面要接 HSV 颜色筛选兜底。3.2 HSV 颜色筛选蓝牌和黄牌怎么抠边缘检测能找出所有纹理密集的区域但其中大部分是车标、散热格栅、广告字这时候要靠颜色把车牌单独捞出来。RGB 对光照敏感所以转到 HSV 空间用 inRange 按色相和饱和度抠蓝、黄两种车牌色。def color_mask(hsv): # 蓝色车牌H 100~124S 和 V 放宽一些适应阴天和傍晚 blue cv2.inRange(hsv, (100, 100, 60), (124, 255, 255)) # 黄色车牌H 15~35常见于教练车和大型车 yellow cv2.inRange(hsv, (15, 80, 80), (35, 255, 255)) return cv2.bitwise_or(blue, yellow)S 和 V 的区间别收太紧。蓝牌在傍晚或路灯下饱和度会明显下降收太紧会漏检但放到(0, 0, 0)又会把蓝天、深色车身都包进来误检暴增。我的经验是 S 下限放在 80~100V 下限放在 50~80先用 3 到 5 张真实照片验证再定最终值。H 的范围同理蓝牌的 H 通常在 100~124不要扩大到 90否则青色车身会大量混入。3.3 边缘候选与颜色掩膜合并再做轮廓筛选和透视矫正两条路子单独拿出来都有问题边缘候选噪声多颜色掩膜会遇到光照偏色。常见做法是取交集——先对边缘二值图做闭运算把字符区域连成块再用颜色掩膜过滤最后用几何特征筛选候选框。def locate(img): hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) edge_bin preprocess(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)) mask color_mask(hsv) # 闭运算把相邻字符的垂直边缘连成一个整体 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(edge_bin, cv2.MORPH_CLOSE, kernel) # 与颜色掩膜取交集去掉不符合车牌底色的区域 closed cv2.bitwise_and(closed, mask) contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes [] for c in contours: x, y, w, h cv2.boundingRect(c) if h 0: continue ratio w / h area w * h # 蓝牌标准宽高比约 440:140带边框后 2.5~4.5 都合理 if 2.0 ratio 5.5 and 1000 area 50000: boxes.append((x, y, w, h, ratio, area)) return boxes(17, 5)这个核是关键参数。横向 17 是因为字符间距很小闭运算要能把 7 个字符连起来纵向只有 5是避免把上下两行无关边缘也粘连进去。核太大会把整辆车的前脸连成一块太小字符又是断开的。宽高比 2.0~5.5 覆盖了蓝牌、黄牌和大部分新能源绿牌变体。面积阈值没有通用值取决于输入图像分辨率我习惯先把图缩放到宽度 800 再计算这样面积阈值可以相对固定。拿到候选框之后对框出来的区域还要做一步透视矫正。车牌在画面里经常是斜的直接切出来的图会影响字符分割。矫正方法是取车牌四个角点映射到一个固定尺寸矩形用 getPerspectiveTransform 算变换矩阵再 warpPerspective 投影。# 假设 pts 是车牌四角按左上、右上、右下、左下排列 pts_src np.array(pts, dtypefloat32) W, H 440, 140 pts_dst np.array([[0, 0], [W - 1, 0], [W - 1, H - 1], [0, H - 1]], dtypefloat32) M cv2.getPerspectiveTransform(pts_src, pts_dst) plate cv2.warpPerspective(img, M, (W, H))440x140 是标准蓝牌的像素宽高选这个尺寸能让后续字符分割和模板匹配的 ROI 固定下来。四角怎么取最简单的方法是拿候选框的外接矩形四角当近似值适用于倾斜不大的场景倾斜超过 10 度最好先用 minAreaRect 拿到旋转矩形再取它的四个顶点。这一步决定了后面分割出来的字符是不是正的值得多花点时间调。4. 字符分割与识别垂直投影、连通域与模板匹配4.1 二值化与垂直投影分割车牌区域矫正出来后下一步是把 7 个字符一个个切开。常见做法是垂直投影法对车牌区域的二值图统计每一列白色像素的个数字符列有明显的波峰字符间隙的列像素数接近 0波谷就是分割点。def segment(plate_gray): # 去掉边框和铆钉的影响四周各收缩 3 像素 h, w plate_gray.shape[:2] plate_crop plate_gray[3:h-3, 3:w-3] # OTSU 二值化确保车牌字符是白字 _, binary cv2.threshold(plate_crop, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) if np.sum(binary 255) np.sum(binary 0): binary 255 - binary # 垂直投影列方向白色像素数量 col_sum np.sum(binary 255, axis0) # 找出连续为 0 的列作为间隙 gaps [] in_gap False start 0 for i, v in enumerate(col_sum): if v 0 and not in_gap: in_gap True start i elif v 0 and in_gap: in_gap False gaps.append((start i) // 2) # 取间隙中点作为切分线 # 用间隙中点切出字符区间 chars [] left 0 for g in gaps: if g - left 5: # 过滤太窄的噪声块 chars.append(plate_crop[:, left:g]) left g if plate_crop.shape[1] - left 5: chars.append(plate_crop[:, left:]) return chars垂直投影是理解起来最直观的切分方式但有几个前提二值化要干净倾斜要小字符不能粘连。投影法最怕的场景是字符断裂或粘连比如铆钉恰好横在字符中间或者 8 的中间被噪点填满导致左右两半分离。遇到这种图先检查前面的透视矫正有没有把车牌拉直再考虑换连通域方法。另外间隙宽度也可以用来过滤噪声正常字符间隙在 6~15 像素之间小于 3 像素的谷不切能避免把字符内部纹路误判成边界。4.2 连通域分析与字符归一化另一种更稳的分割思路是用连通域分析。OpenCV 的 connectedComponentsWithStats 能直接拿到每个连通块的外接矩形、面积和中心点天然适合字符这种独立连通块。num, labels, stats, centers cv2.connectedComponentsWithStats(binary, 8) valid [] for i in range(1, num): x, y, w, h, area stats[i] # 过滤太小和太大的连通块 if w 5 or h 10 or area 50: continue if w binary.shape[1] * 0.5: # 宽度超过车牌一半大概率是边框 continue valid.append((x, y, w, h)) # 按 x 坐标排序保证字符顺序从左到右 valid.sort(keylambda r: r[0])连通域方法对断裂字符有天然优势即使一个字符断成两半也可以通过合并相邻连通块来还原。但它的缺点是会把车牌上的装饰图案、铆钉也当成候选需要额外过滤。实际项目中我经常把投影法和连通域结合起来先用投影法求出主分割线再用连通域修正单个字符的上下边界。不管用哪种分割切出来的字符尺寸都不一样模板匹配前必须归一化。我的做法是统一 resize 到 20x40 像素保持宽高比 1:2和标准字模一致。resize 用 INTER_CUBIC 插值效果更好字符边缘更平滑。归一化之后最好把字符也统一成黑底白字模板库里存的也是黑底白字匹配时方向一致能少很多错误。4.3 模板匹配识别匹配算法、阈值和后处理规则字符识别在所有纯 OpenCV 方案里最常见的就是模板匹配。把每个切割出来的字符和模板库里所有模板逐个比对取相似度最高且超过阈值的那个作为结果。def recognize_char(cell, template_dict): # 归一化到统一尺寸 cell cv2.resize(cell, (20, 40), interpolationcv2.INTER_CUBIC) cell cell.astype(np.float32) / 255.0 best_label, best_score None, -1.0 for label, tmpl_img in template_dict.items(): tmpl cv2.resize(tmpl_img, (20, 40), interpolationcv2.INTER_CUBIC).astype(np.float32) / 255.0 score cv2.matchTemplate(cell, tmpl, cv2.TM_CCOEFF_NORMED)[0][0] if score best_score: best_score score best_label label return best_label, best_scoreTM_CCOEFF_NORMED 会把图像和模板都减去各自的均值再算相关性对光照变化不敏感是模板匹配里最常用的选择。匹配分数要设一个下限比如 0.5低于这个值说明模板库里没有对应字符宁可输出 ? 也不要硬凑一个错的。这个阈值的设定没有绝对标准属于玄学部分得拿你自己的测试集扫一遍画一条分数分布曲线再定。模板匹配最大的坑是相似字符0 和 O、8 和 B、5 和 S在 20x40 的尺寸下肉眼都难分。这时候要靠车牌本身的语法规则做后处理——中国车牌第 1 位是省份简称汉字第 2 位是发牌机关字母且不能是 I 和 O第 3 到 7 位是字母数字混合也不能出现 I 和 O。在代码里加一条白名单过滤能把这类错误直接纠正def post_process(text): if len(text) ! 7: return text province 京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新 rules text[0] in province and text[1].isalpha() and text[1] not in IO for ch in text[2:]: if ch in IO: return text return text if rules else ?模板库的覆盖度直接决定识别率。汉字部分至少要把 31 个省份简称收齐同一个字最好收宋体、黑体两种字体字母数字部分收 24 个字母加 10 个数字。如果源码自带的模板库不全自己用画图工具生成一套也不难但要保证字体和真实车牌一致用系统自带的 Arial 或宋体就可以别用艺术字体。如果模板匹配在自己采集的图片上识别率不理想可以用 SVM 替换识别环节。OpenCV 的 ml 模块自带 SVM 训练接口特征用 HOG 或简单的像素特征。训练数据需要每类字符几十到几百个样本自己标注一批切好的字符就能训。SVM 的泛化能力比模板匹配强但训练集要覆盖光照、模糊、字体变化这部分工作量比调模板阈值大得多。很多源码里的识别模块会同时保留模板匹配和 SVM 两套接口就是给使用者留了升级余地。5. 避坑跑车牌识别源码最常见的 5 个翻车现场5.1 现象OpenCV 安装成功却找不到 cv2import 直接报 ModuleNotFoundError原因最常见的是 pip 装到了系统 Python而 IDE 或终端用的是虚拟环境 Python。或者同时装了 opencv-python 和 opencv-contrib-python两个包的文件互相覆盖卸载其中一个就崩。解决统一用python -m pip install opencv-python numpy并检查pip list里有没有重复的 opencv 包。确认当前解释器路径的方法是运行python -c import sys; print(sys.executable)看它指向的是不是你自己创建的那个 venv不是就重新激活环境再装。5.2 现象cv2.error: OpenCV(4.4.0) 报错函数一调用就崩溃原因OpenCV 4.4.0 时代的一些预编译包在老 CPU 或特定指令集上会触发非法指令网上搜到的报错信息常带有C:\users\...\pip-req-build这类编译路径本质是二进制兼容问题不是你的代码写错了。解决不要纠结这个旧版本直接升级或降级到 4.5.x 到 4.8.x 这个区间内的稳定版命令是python -m pip install opencv-python4.8.1或直接装最新稳定版。再不行就换 Python 3.8 或 3.9 重装一遍。从源码编译浪费时间没有特殊需求不建议走。5.3 现象定位不到车牌蓝牌被当成背景忽略原因预处理链路里任何一环参数不对都会导致定位失败。最常见的是闭运算核设置过大把车牌和车身边缘粘成一块轮廓筛选时宽高比超范围被过滤或者 HSV 的 S 阈值收太紧傍晚蓝牌颜色发灰就抠不出来了。解决先单独把边缘二值图和颜色掩膜用 cv2.imshow 显示出来确认哪一步丢了目标。然后一次只改一个参数先放宽颜色区间再调闭运算核每改一次就跑一遍测试集。另外确认输入图片有没有 resize 到统一宽度面积阈值是按固定宽度设的图片太大或太小都会让候选框被过滤掉。5.4 现象字符分割把 8 切开、把 1 丢掉原因垂直投影法依赖二值图干净。铆钉横在字符中间、光照在车牌上形成阴影、字符粘连严重都会让投影曲线在字符内部出现深谷导致误切。另外二值化方向反了白字变黑字投影统计的就是背景而不是字符。解决先确认二值化后的字符是白色、背景是黑色并统计白色像素占比不对就反相。然后做一次形态学开运算去掉细小噪点核用(2, 2)或(3, 3)别太大否则会把字符笔画也削掉。如果还是误切改用连通域方法或者把投影和连通域结果交叉验证。5.5 现象汉字全部识别错或者 0 和 O 混淆原因模板库里的汉字字体和实际车牌字体差太远或者模板本身没做归一化黑底白字和白底黑字混在一起。中文车牌用字接近公安字库和系统自带的宋体黑体有差异模板匹配对字体非常敏感。0 和 O 这类混淆则是因为匹配分数接近没有语法规则兜底。解决收集真实车牌照片手动切割汉字建立自己的模板库每个省份简称保留 2~3 个典型样本。字符和模板都走同一套归一化流程包括反相、resize、归一化到 0~1。如果这步做完还是错就上 SVM用 HOG 特征替代像素级匹配能吸收一部分字体变形。最后一定记得跑第 4.3 节里的 post_process 语法过滤能把 O 和 I 这类逻辑错误挡掉。6. 验证与进阶批量跑图、统计指标再决定要不要上深度学习到这一步单张图的流程大概率已经能跑了但别急着宣布成功。我的习惯是准备 30 到 50 张不同场景的真实照片覆盖白天、傍晚、阴天、倾斜 15 度以内这几类情况放进 test_imgs 目录写个批量脚本一次性跑完。import os, time, csv results [] for name in sorted(os.listdir(test_imgs)): img cv2.imread(os.path.join(test_imgs, name)) if img is None: continue t0 time.time() text recognize(img) # 你的主函数 cost_ms (time.time() - t0) * 1000 results.append((name, text, round(cost_ms, 1))) for row in sorted(results, keylambda r: -r[2])[:5]: print(row)统计三个指标定位成功率车牌区域是否正确框出、整牌识别率7 个字符全部正确、平均耗时。定位成功率低于 80% 就先别调识别说明预处理或筛选参数有问题定位成功但整牌识别率低重点看分割和模板库。你会发现多数错误都集中在分割阶段而不是识别阶段所以前面把定位和分割做扎实比后期换识别器更划算。如果验证下来识别率到了七八成想继续往上走再考虑上深度学习定位环节可以用 EAST 或 CTPN 文本检测替换 Sobel识别环节用 LPRNet 这种轻量网络但灰度化、透视矫正、二值化这些 OpenCV 预处理仍然是标配。最后说一句个人教训别指望一套模板匹配打天下我自己就吃过这个亏换了三个停车场的数据就翻车了老老实实回来补分割和模板库。希望帮到你。本文还有配套的精品资源点击获取
返回列表