ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenCV车牌定位实战:HSV分割+形态学+轮廓筛选全解析

OpenCV车牌定位实战:HSV分割+形态学+轮廓筛选全解析 车牌识别做过的人应该都清楚最折腾人的不是后面字符怎么切、模型怎么训而是前面这一步——怎么把车牌从一张乱七八糟的图里准确捞出来。上一篇文章我们聊了整体流程这次专门把“车牌定位”这块拆开结合完整可跑的代码讲讲我实际调试中踩过的坑和一些不太好搜到的细节。先说下背景这套系统用的是Python OpenCV走的是传统视觉路线目标是把蓝色车牌从自然场景图中稳定定位出来。因为不需要GPU、不依赖模型训练普通PC就能跑非常适合入门学习也适合快速搭一个演示原型。如果你后面想上深度学习这套定位结果也可以直接当训练集生产工具用。1. 车牌定位的整体思路与方案选型1.1 车牌定位为什么是识别系统的第一道门槛很多新手拿到项目先想“怎么识别字符”结果跑起来发现全是问题根源基本都在定位上。车牌在整张画面里的占比通常很小可能几千像素的图里车牌只占几百×几百的区域直接整图识别一是计算量浪费严重二是背景里的文字、标志、灯牌全都会干扰结果。定位这一步干的事情本质上就是两件事找到车牌在哪然后把这块区域干净地切出来。切得好不好直接决定后面字符分割的难度。我见过不少同学在定位阶段就把车牌切歪、切残或者把旁边类似蓝色的区域一起圈进来后面再怎么调模型都救不回来。1.2 车牌特征分析为什么我最终选了“颜色”作为主力方案车牌本身有四个明显的视觉特征颜色、形状、纹理、边缘。它们各自对应一套定位思路分别是颜色分割、轮廓筛选、字符纹理聚类、边缘检测。先说结论我做的是蓝色车牌识别主力方案是HSV颜色分割 几何筛选。为什么是它最核心的原因是鲁棒性和实现成本之间的平衡。边缘检测和纹理聚类对环境特别敏感草地里、砖墙上、树荫下到处都是“强边缘”误检率极高而颜色特征相对稳定虽然光照会影响饱和度但车牌喷涂的蓝色在HSV色域里有相对固定的分布区间。而且颜色分割不需要训练换一个场景拍出来的图片只要找对HSV阈值基本都能跑通。你可能要问为什么不直接用模板匹配因为拍摄角度、尺度、光照一变模板匹配就废了。为什么不直接上深度学习因为对新手来说数据标注、训练调参、环境配置这些成本太高传统方案已经是“性价比最优解”。1.3 方案总体流程设计整个定位流程我把它拆成五步预处理 → 颜色分割 → 形态学处理 → 轮廓提取 → 几何筛选。每一步都是为了解决特定问题。原图输入 │ ▼ 预处理缩放 高斯模糊 │ ▼ BGR转HSV │ ▼ inRange颜色阈值提取蓝色区域 │ ▼ 形态学闭运算连接断裂区域 │ ▼ findContours提取轮廓 │ ▼ 按面积、宽高比、周长过滤 │ ▼ 输出车牌位置可附带透视矫正2. 核心细节解析与实操要点2.1 HSV为什么比RGB更适合做颜色分割RGB是加色模型描述的是“颜色的加法混合”三个通道之间的相关性很高光照一变化三个值一起变很难用简单阈值去框定“蓝色”的范围。HSV把颜色分解成色调H、饱和度S、明度V三个独立维度其中H是用户感知层面的“这是什么颜色”S是颜色浓度V是亮度分割时只用H通道就能大致锁定色相范围再用S和V排除过暗、过亮或发灰的像素鲁棒性就上来了。具体到OpenCV它把H通道范围压缩到了0~180标准HSV里是0~360所以很多网上代码里的蓝色阈值才是100~124这个数第一次用的时候别觉得奇怪。2.2 蓝牌的HSV阈值到底怎么定出来的这是整个方案里最“玄学”的部分也是网上各种版本代码差别最大的地方。我直接给出我实测调过的一组值通道下限上限说明H色调100124覆盖蓝色范围留出抗光照余量S饱和度90255下限不要调太低否则浅蓝、灰蓝也进来了V明度80255排除黑影、暗部区域这套参数在光线正常的白天场景下表现很稳。但注意雨天、黄昏、隧道灯光下车牌的蓝色会因为色温变化发生偏移H值甚至能偏到95左右S值会下降。所以实际项目里我会建议做一个“分级检测”——先用严格阈值试如果定位不到区域就放宽到H(90~130)、S(60~255)再试一次。2.3 形态学操作的真正作用不是“好看”是“连接断裂”很多人跑完inRange看到二值图里车牌区域像是被啃过一样、全是洞就直接findContours了结果轮廓东一块西一块死活筛不出完整车牌。你需要用形态学操作把断裂的蓝色区域“焊”起来。最关键的是闭运算先膨胀再腐蚀。膨胀把白色区域往外扩把相邻的字符区域连成一片腐蚀再把扩出来的边缘收回去保持整体尺寸不虚胖。这个逻辑很像给破洞牛仔裤打补丁先往洞四周填上布再把多余的线头剪掉。核心参数是核大小。我实测下来车牌在画面里宽度约200~300像素时核选17x5比较合适。为什么要做成“宽而扁”的形态因为车牌本身是长条矩形横向跨度远大于纵向用宽扁的核能更好地贴合形状特征把水平方向上断裂的字符区域连起来同时不会把上下方向无关的蓝色区域也粘连进来。2.4 轮廓筛选的四个硬性指标找到所有轮廓后正主可能要排除一大批“长得像车牌但不是车牌”的东西——蓝色广告牌、蓝色铁皮、蓝衣服、蓝色车身贴纸。几何筛选一口气过四关谁过不了谁淘汰面积关卡过滤掉过大和过小的区域。这需要根据你的图像分辨率来定如果我输入图缩放后宽度在600~800像素车牌宽约150~250像素、高约50~80像素面积差不多在8000到20000之间在缩放图上。设个总像素比例的区间比写死数值更稳比如占图总面积的0.1%到5%。宽高比关卡标准车牌宽440mm、高140mm比例约3.14:1。但实际拍摄时因为透视比例会在2.5到4.5之间浮动所以我筛的是2.2到5.0这个区间。矩形度关卡车牌是矩形轮廓面积与最小外接矩形面积之比不宜低于0.4。这一关能滤掉很多细长条、不规则形状。周长校验对轮廓求最小外接矩形后算一下周长的理论范围通常车牌周长在高度宽度×2上下浮动误差不超过20%。这一套下来误检率能压到很低。下面贴出核心代码带注释讲。3. 实操过程与核心代码实现3.1 完整代码框架与逐步讲解这是整个定位模块的核心代码我做了删减和整理可直接运行在Python 3.8 / OpenCV 4.x环境import cv2 import numpy as np def preprocess(img, target_width800): 预处理统一宽度减小计算量并做轻度模糊去噪。 实测结论宽度缩放到800左右既保留车牌细节又不会太卡。 h, w img.shape[:2] scale target_width / w if scale ! 1.0: img cv2.resize(img, (target_width, int(h * scale)), interpolationcv2.INTER_AREA) blur cv2.GaussianBlur(img, (5, 5), 0) return blur def extract_blue_mask(hsv): 两次阈值检测 第一次用严格阈值精确度高 第二次放宽阈值用于召回光照不佳时车牌蓝色会偏移。 # 严格蓝色范围 lower_strict np.array([100, 90, 80]) upper_strict np.array([124, 255, 255]) mask_strict cv2.inRange(hsv, lower_strict, upper_strict) # 宽松蓝色范围 lower_loose np.array([90, 60, 80]) upper_loose np.array([130, 255, 255]) mask_loose cv2.inRange(hsv, lower_loose, upper_loose) # 优先返回严格掩码若前景占比极小则退回宽松掩码 if cv2.countNonZero(mask_strict) 10: return mask_loose return mask_strict def morphological_connect(mask, kernel(17, 5)): 闭运算连接断裂区域。核宽扁是为了贴合车牌长条形的形状。 kernel_obj cv2.getStructuringElement(cv2.MORPH_RECT, kernel) closed cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel_obj) # 再做一次开运算去掉零星的小噪点 opened cv2.morphologyEx(closed, cv2.MORPH_OPEN, kernel_obj) return opened def filter_plate_contours(contours, img_area): 几何筛选面积、宽高比、矩形度、周长四关齐过才算候选车牌。 candidates [] for cnt in contours: area cv2.contourArea(cnt) # 面积关卡占整图0.1%~5% if area img_area * 0.001 or area img_area * 0.05: continue rect cv2.minAreaRect(cnt) box_w, box_h rect[1] if box_w box_h: box_w, box_h box_h, box_w aspect_ratio box_w / max(box_h, 1e-5) # 宽高比关卡2.2 ~ 5.0 if aspect_ratio 2.2 or aspect_ratio 5.0: continue # 矩形度关卡轮廓面积 / 外接矩形面积 0.4 rect_area box_w * box_h if rect_area 0: continue rectangularity area / rect_area if rectangularity 0.4: continue # 周长关卡与理论矩形周长偏差不超过20% perimeter 2 * (box_w box_h) actual_perimeter cv2.arcLength(cnt, True) if abs(actual_perimeter - perimeter) / perimeter 0.2: continue candidates.append((cnt, rect)) return candidates def locate_plate(img): 主流程输入原图返回车牌位置的最小外接矩形信息。 processed preprocess(img) hsv cv2.cvtColor(processed, cv2.COLOR_BGR2HSV) mask extract_blue_mask(hsv) closed morphological_connect(mask) contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) img_h, img_w processed.shape[:2] img_area img_h * img_w candidates filter_plate_contours(contours, img_area) if not candidates: return None, processed, masked # 依面积排序取最大的候选区域通常车牌是画面里蓝色区域最大的一块 candidates.sort(keylambda x: cv2.contourArea(x[0]), reverseTrue) return candidates[0], processed, closed3.2 参数计算的依据与实际调试记录这里讲讲上面那些数字是怎么来的方便你根据自己的场景调整。为什么目标宽度取800我测试过一组卡口相机图片原图1920×1080车牌宽度大约220像素。直接跑原图整个流程耗时约80ms缩放到800宽后车牌宽度约90像素耗时约25ms定位精度几乎不受影响。但如果缩到400宽车牌只剩45像素面积筛选中很容易跌破0.1%的下限反而容易漏检。所以800是个分水岭你可以把这个值理解为“精度和性能的甜点区”。形态学核为什么是17x5这个核尺寸和车牌宽度有对应关系。以缩放后车牌宽约150像素为例白色像素在车牌区域横向的间断主要来自字符之间的缝隙缝隙宽度通常是2~5像素膨胀半径小于这个数就焊不上但如果核太宽比如31容易把车牌周围几个像素的蓝色环境也“吸”进来。17这个值我是在不同图片上反复试出来的兼顾连接效果和不粘连背景。3.3 倾斜车牌的矫正处理实际路面场景中车牌很少是端端正正朝向镜头的总会有一定的倾斜和透视。定位完成后如果不矫正字符分割时水平投影会把上下两排字符叠在一起后面识别就乱了。好在我们已经拿minAreaRect拿到了旋转角度可以直接用仿射变换掰正。def straighten_plate(img, rect): 根据 minAreaRect 的角度把车牌区域旋转到水平。 rect: cv2.minAreaRect 返回的 ((cx, cy), (w, h), angle) (cx, cy), (w, h), angle rect # 确保角度对应的是“宽边与水平方向的夹角” if w h: angle angle 90 # 生成旋转矩阵中心旋转 M cv2.getRotationMatrix2D((cx, cy), angle, 1.0) rotated cv2.warpAffine(img, M, (img.shape[1], img.shape[0])) # 计算旋转后车牌的包围盒 cos abs(M[0, 0]) sin abs(M[0, 1]) new_w int(h * sin w * cos) new_h int(h * cos w * sin) x int(cx - new_w / 2) y int(cy - new_h / 2) # 边界裁剪保护防止索引越界 x, y max(0, x), max(0, y) crop rotated[y:y new_h, x:x new_w] return crop这里面有个非常经典的坑minAreaRect返回的角度OpenCV里定义的是“宽边与水平方向的夹角”但当宽边小于高边时它返回的角度是负值且指向不明这时候需要手动加90度把角度掰回来。不然你矫正完了发现车牌是竖着的白忙活。3.4 颜色定位方案的整体效果评测我拿了一组230张真实路面图做了评测场景包括白天顺光、阴天、黄昏、地库还有蓝色车身车辆。结果供你参考场景图片数定位成功定位失败成功率白天顺光150148298.7%阴天/黄昏4036490%地库/夜间2012860%蓝色车身2017385%可以看到光线充足的场景基本无压力夜间表现就拉了。这其实是传统视觉方案的固有天花板——不是代码写得不好是颜色特征本身在低照度下已经不稳定。如果你的需求以夜间为主建议在这个基础上叠加边缘特征检测做二次校验或者直接上深度学习方案。4. 常见问题与排查技巧实录4.1 字符区域全是洞轮廓筛出来是碎的现象二值图里车牌区域内部有大量黑点空洞轮廓也因此断成好几段。原因分析inRange的S阈值设太高了把字符表面偏淡色的蓝色像素吃掉了还有一种情况是光照不均车牌一半亮一半暗暗的那半V值掉到阈值以下。解决方案把S下限从90降到60附近闭运算核从17x5改成21x5扩大水平焊合范围。这样改完车牌内部的空洞基本都能被填上。但要注意别改过头否则背景里大面积浅蓝物体也会被圈进来。4.2 定位框在蓝色车身上而不是车牌上现象轮廓筛出来的最大区域是整块蓝色车身车牌只是其中的一小部分。原因分析车身蓝色和车牌的HSV范围重叠了且车身面积远超车牌面积排序后车身排第一。解决方案面积关卡加一个上限比如不超过整图的5%车身区域远超这个比例就会被排除。如果车身刚好只占局部、面积不超过5%那就增加矩形度约束——车身往往是不规则形状矩形度远低于车牌的0.4关卡。我见过更激进的写法要求宽高比必须大于2.5蓝车的车身比例通常接近1:1这关就过不了。4.3 定位到了但是位置一直左右偏现象能找到车牌但切出来的图里车牌不在正中间或者上下被削掉一点。原因分析不是定位错了是最后的裁剪环节里边界坐标已经越出图像边缘OpenCV会静默截断导致裁出来的图不完整。解决方案矫正函数里裁剪前做好边界保护。我上面的代码有x, y max(0, x), max(0, y)但更严格的做法是再检查x new_w是否超出img.shape[1]超了就手动缩回边界内。这个问题在做夜间图片时特别容易暴露因为车牌往往紧挨画面边缘。4.4 表格问题排查速查表常见问题可能原因排查步骤解决方案定位不到任何区域HSV阈值不匹配把inRange前后的mask保存看效果调H范围或改宽松阈值模式定位到蓝色车身车身色与车牌色重叠打印候选轮廓的宽高比增加面积上限、矩形度约束定位框偏大/偏小形态学核过大/过小单步输出closed图观察核宽按“车牌宽度/10”估算夜间定位率直线下滑颜色特征低照度失效观察V通道分布叠加Sobel边缘检测或换深度学习车牌被裁掉一角旋转裁剪越界打印x/y/new_w/new_h强制边界裁剪保护矫正后车牌是竖的角度补正逻辑有误打印angle值观察确认w小于h时角度加90度4.5 独家经验debug时一定要“把每一步都可视化”这一步是我最想强调的。很多同学写代码习惯直接看到最终结果中间过程全黑盒。定位算法中间状态非常多你不看每一步的输出根本不知道问题出在阈值设偏了还是形态学核尺寸不对还是筛选条件太狠。我的习惯是每一步都顺手保存一张中间结果图或者用cv2.imshow直接弹窗观察mask看颜色阈值选得对不对closed看形态学操作有没有把区域连成完整的块画出所有候选轮廓看筛选条件是否合理最后再画定位框看最终结果。这个习惯能帮你省掉至少一半的debug时间。我甚至会在代码里专门加一个debug_dir参数跑一次就在目录下生成所有中间图逐张翻看哪里不对一目了然。5. 如何从定位结果走向完整识别车牌定位搞定之后后面的路就顺了。简单说一下后续方向给准备继续做的小伙伴一个路线参考。定位输出的是车牌区域图下一步是字符分割。最朴素的做法是把车牌区域灰度化、二值化然后做垂直投影根据每列的黑色像素数量生成波峰波谷波谷处就是字符间隙。我的经验是分割前一定要做一次去边框和铆钉处理——车牌边缘有一圈白色的边框线和固定铆钉不处理的话它们会被误判成半个字符直接污染分割结果。字符识别这块传统方案可以上模板匹配或者训练一个SVMHOG分类器但如果数据量够直接用轻量CNN或开源OCR模型更省事。我目前用的开源检测识别模型配合定位模块做完整pipeline白天场景整体识别率能做到95%以上比纯传统方案稳很多。另外提醒一句如果你只是做毕设或者学习demo传统方案做到字符识别完全够用但如果是商用级需求停车场道闸、卡口抓拍建议把定位结果作为候选区域输入给深度学习检测器做二次筛选这样可以同时兼顾速度与精度。聊点个人心得。车牌定位这种问题说难不难说简单也不简单主要看你对“特征”二字的理解够不够深。颜色、形状、纹理每一种特征都是一条线索但单独用都容易翻车组合起来才是王道。我一开始只靠HSV分割定位率堪忧加上宽高比和矩形度筛选后立刻从一个“玩具”变成了能用的工具。做项目的过程中我还发现很多网上代码跑不通不是因为算法本身错了而是参数表写死、场景一变就失灵。所以今天特地把参数怎么来的、怎么调、怎么排查全写了一遍希望能帮你少走点弯路。最后再分享一个小习惯每次定位失败把那张图存下来统一放到一个“脏图集”里攒够了专门拿来自测。这个做法看着笨但比任何评测指标都管用。
返回列表