ARTICLE DETAIL

资讯详情

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

基于OpenCV的车牌定位完整方案:HSV颜色筛选与轮廓几何筛选实战

基于OpenCV的车牌定位完整方案:HSV颜色筛选与轮廓几何筛选实战 做车牌识别项目的人十有八九会把大部分时间耗在“识别”两个字上结果真正跑起来才发现前面的定位环节才是决定项目生死的关卡。字符识别再准定位框偏了、歪了、把旁边蓝色广告牌圈进来后面全白搭。这篇继续车牌识别系统系列把第二块硬骨头——车牌定位的完整思路和可落地代码拆开讲。全文用 OpenCV 走传统视觉路线不依赖深度学习框架好处是依赖少、速度快、逻辑透明特别适合快速验证和嵌入式场景。1.1 定位模块在整条识别流水线里的位置一套完整的车牌识别系统从摄像头取帧到输出车牌字符串大致经过五个环节图像采集、车牌定位、车牌矫正、字符分割、字符识别。定位处在最前端的图像处理部分输入是原始图片输出是一小块包含车牌的矩形区域。这一步做好了后面的字符分割和识别面对的就是一张干净、端正、对比度合适的图片。这一步做不好后面加多少模型都救不回来。实际项目中定位失败通常分两种一种是压根没找到车牌另一种是找到了错误的区域。后者在工程里更常见也更容易被忽略——因为程序没报错只是结果莫名其妙。1.2 为什么选传统视觉而不是深度学习很多朋友一上来就问我现在检测车牌不都用 YOLO 吗怎么还在用颜色和轮廓这套老办法我的回答是看场景。深度学习目标检测比如 YOLO、SSD在车牌检测上确实精度高、泛化好但代价是需要标注数据、训练环境和推理资源。在一个封闭园区、固定角度抓拍的场景里车牌特征非常稳定——蓝底白字、固定宽高比、位置相对固定——这些先验信息用传统视觉方法可以极低成本地利用起来。本文这套方案在普通 i5 处理器上处理单帧图像耗时在 30ms 以内不依赖 GPU代码量几百行就能跑通。对于教学演示、毕设项目、小型嵌入式设备这个方案非常香。当然如果你的场景是复杂城市道路、多角度多距离抓拍那就需要上深度学习方案这个后面可以单独开一篇聊。1.3 车牌本身给了我们哪些线索中国车牌的设计自带视觉特征定位就是充分利用这些特征颜色特征蓝牌主色调是蓝色新能源牌是渐变绿黄牌是黄色。这是最强的先验。几何特征标准车牌尺寸 440mm×140mm宽高比约 3.14在图像中这个比例会随透视变化但大体落在 2.0~4.5 之间。纹理特征车牌区域字符密集水平方向的边缘响应强烈。位置特征卡口相机的车牌一般出现在画面中下部。定位算法的核心逻辑就是把上面这些线索组合起来层层筛选最后锁定目标区域。我用的主线索是颜色辅以边缘和几何约束。2.1 HSV 颜色空间为什么不用 RGB 直接筛这是初学者最容易踩的坑。RGB 颜色空间受光照影响极大——同一个蓝色车牌在阳光下、阴影里、夜间路灯下RGB 三个分量的数值可能差出一倍多。直接按照 RGB 阈值去筛选要么漏检要么误检一大堆。HSV 颜色空间把色相H、饱和度S、明度V分开。色相代表了颜色本身的属性光照变化主要影响 V 分量对 H 分量影响很小。所以按 H 范围筛颜色天然对光照有一定鲁棒性。OpenCV 里的 HSV 和美术上学的 HSV 还有一个区别需要注意OpenCV 中 H 的范围是 0~180S 和 V 的范围是 0~255。网上一堆教程直接搬 H 值 0~360 的写法在新版 OpenCV 上会踩坑。2.2 各颜色车牌的阈值参考我针对蓝牌、黄牌、新能源绿牌各整理了一组经过实测的初始阈值可以直接抄。注意这些值建议在项目实际光照下调优后面避坑章节会讲怎么调。车牌类型色相 H饱和度 S明度 V蓝牌100~130100~25580~255黄牌15~35100~25580~255绿牌新能源35~8580~25560~255从实际效果看蓝牌的检测最稳定。绿牌因为渐变底色和字体颜色变化阈值区间稍宽误检率也会略高需要几何约束多兜一道。2.3 形态学操作让“碎片”连成一个整体车牌上的字符是白色的字符之间有间隙如果直接对颜色掩膜找轮廓出来的是一片碎块根本拼不成一个完整的矩形候选区。这时候就要用形态学操作。闭运算先膨胀再腐蚀能把邻近的白色字符区域连接起来填平字符之间的细小空隙同时保持整体形状不变。核的尺寸很关键核太窄连不起来核太宽会把周围无关区域也并进来。我实测蓝牌场景用(17, 5)的矩形核比较合适——宽大于高因为车牌本身是横向的字符间隙也主要在水平方向上。形态学之后再对掩膜找外轮廓得到的就是一个个闭合的区域接下来交给几何筛选。3.1 环境准备版本问题先排雷代码基于 Python 3.8 和 OpenCV 4.x。安装很简单pip install opencv-python numpy这里提醒一句OpenCV 3.x 和 4.x 的findContours返回值不一样。3.x 返回三个值image, contours, hierarchy4.x 返回两个值contours, hierarchy。如果你看到ValueError: not enough values to unpack就是版本不一致导致的按对应版本修改解包方式即可。3.2 主流程代码从读图到输出候选区域下面这个函数是完整的定位主流程包含注释可以直接跑。它接收图片路径返回定位到的车牌区域裁剪图import cv2 import numpy as np def locate_license_plate(image_path): # 1. 读取图像做缩放加速处理 img cv2.imread(image_path) if img is None: return None # 长边缩放到 800缩短处理时间同时保证小尺寸车牌不被缩得太小 h, w img.shape[:2] scale 800 / max(h, w) if scale 1: img cv2.resize(img, (int(w * scale), int(h * scale))) # 2. 高斯模糊去噪减少边缘检测时的干扰 blurred cv2.GaussianBlur(img, (5, 5), 0) # 3. 转 HSV按蓝色阈值生成二值掩膜 hsv cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV) blue_lower np.array([100, 100, 80]) blue_upper np.array([130, 255, 255]) mask cv2.inRange(hsv, blue_lower, blue_upper) # 4. 形态学闭运算连接字符碎片 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations2) # 5. 找轮廓 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 6. 按面积排序优先处理大候选区域 contours sorted(contours, keycv2.contourArea, reverseTrue) candidates [] for cnt in contours: area cv2.contourArea(cnt) # 过滤掉太小的区域 if area 2000: continue x, y, cw, ch cv2.boundingRect(cnt) aspect_ratio cw / ch # 宽高比过滤车牌比例正常在 2~4.5 之间 if aspect_ratio 1.5 or aspect_ratio 5.5: continue # 矩形度过滤轮廓面积占外接矩形面积的比例 rect_area cw * ch rectangularity area / rect_area if rectangularity 0.5: continue candidates.append((x, y, cw, ch)) # 7. 返回面积最大的候选区域 if not candidates: return None best max(candidates, keylambda r: r[2] * r[3]) bx, by, bw, bh best plate_roi img[by:by bh, bx:bx bw] return plate_roi3.3 逐个步骤解释每一步在干什么第一步里做了图像缩放。很多人忽略这个操作直接拿原始高清图跑结果一张 4000×3000 的照片光处理就要几百毫秒实时性全没了。缩放的本质是在不丢失关键信息的前提下大幅减少计算量。车牌定位不依赖极致分辨率长边 800 像素足够实测识别率几乎不受影响。第三步里的inRange函数是核心它会遍历图像的每个像素如果像素的 HSV 值落在[blue_lower, blue_upper]区间内就把掩膜对应位置置为 255白色否则置为 0黑色。输出的掩膜图是一张黑白图车牌区域通常呈现为一片白色。第五步找轮廓RETR_EXTERNAL表示只找最外层轮廓内部嵌套的轮廓不要——我们要的是车牌的完整外包围而非单个字符的轮廓。CHAIN_APPROX_SIMPLE是压缩轮廓点减少内存占用也方便后续计算。第六步的几何筛选中宽高比的上下限留了余量——实际车牌标准比例约 3.14但因为拍摄角度和裁剪误差允许在 1.5 到 5.5 之间波动。矩形度过滤则用来排除那些面积接近但不是矩形的不规则形状。3.4 一个常见问题的处理同时检测蓝牌和黄牌上面代码默认只筛蓝色。如果你要同时支持黄牌或新能源绿牌可以把不同颜色的掩膜分别生成然后做按位或运算合并def build_multi_color_mask(hsv): masks [] # 蓝牌 masks.append(cv2.inRange(hsv, (100, 100, 80), (130, 255, 255))) # 黄牌 masks.append(cv2.inRange(hsv, (15, 100, 80), (35, 255, 255))) # 新能源绿牌 masks.append(cv2.inRange(hsv, (35, 80, 60), (85, 255, 255))) combined masks[0] for m in masks[1:]: combined cv2.bitwise_or(combined, m) return combined多颜色掩膜合并后形态学操作参数基本不需要变因为车牌尺寸和比例在颜色维度上是统一的。实测中绿牌的误检率略高因为自然场景中绿色元素树木、招牌比蓝色元素更常见建议对绿牌场景把矩形度阈值从 0.5 提高到 0.6。4.1 为什么要做透视矫正大部分卡口相机的安装角度不是正对车头的拍摄出来的车牌是一个倾斜的平行四边形。如果直接把这块倾斜图像送去做字符分割垂直投影的字符边界是歪的分割结果会大受影响。透视矫正的作用就是把倾斜的车牌区域“掰正”——将任意四边形区域映射成一个正面的矩形。这一步对于后续字符分割的准确率提升非常明显。4.2 角点检测与排序透视矫正的关键透视矫正需要知道车牌四个角点在原图中的精确位置。approxPolyDP函数可以把轮廓近似成多边形。车牌轮廓近似后通常是一个四边形或接近四边形。拿到四边形的四个点后需要按固定顺序排列左上、右上、右下、左下再根据目标矩形尺寸调用透视变换。这里有一个实测经验角点排序时不能只按 x 或 y 坐标要综合处理。对于倾斜严重的车牌单纯按坐标排序很容易把顺序搞乱。下面是完整的透视矫正代码def order_points(pts): # 初始化四个角的坐标列表 rect np.zeros((4, 2), dtypefloat32) # 左上角是 xy 最小的点右下角是 xy 最大的点 s pts.sum(axis1) rect[0] pts[np.argmin(s)] rect[2] pts[np.argmax(s)] # 左下角是 x-y 最大的点右上角是 x-y 最小的点 diff np.diff(pts, axis1) rect[1] pts[np.argmin(diff)] rect[3] pts[np.argmax(diff)] return rect def four_point_transform(image, pts): # 按顺序排列角点 rect order_points(pts) (tl, tr, br, bl) rect # 计算目标矩形的宽高 width_top np.linalg.norm(br - bl) width_bottom np.linalg.norm(tr - tl) max_width max(int(width_top), int(width_bottom)) height_left np.linalg.norm(tr - br) height_right np.linalg.norm(tl - bl) max_height max(int(height_left), int(height_right)) # 目标矩形的四个顶点 dst np.array([ [0, 0], [max_width - 1, 0], [max_width - 1, max_height - 1], [0, max_height - 1] ], dtypefloat32) # 计算变换矩阵并执行 M cv2.getPerspectiveTransform(rect, dst) warped cv2.warpPerspective(image, M, (max_width, max_height)) return warped使用这个函数时传入的是车牌轮廓approxPolyDP之后得到的四个点。如果approxPolyDP输出的点多于四个可以取面积最大的轮廓再逼近或者用车牌的cv2.boxPoints(cv2.minAreaRect(cnt))获取最小外接矩形的四个顶点——后者在车牌边缘不完整时更稳定缺点是丢失了透视信息只能做旋转矫正。4.3 旋转矫正 vs 透视矫正怎么选如果车牌只是旋转、没有明显透视变形相机正对车牌但角度偏了用minAreaRect拿到旋转角度再调用cv2.getRotationMatrix2D做仿射旋转即可。这种方式计算量小也不依赖角点检测。如果车牌有明显透视变形从侧前方拍摄、斜上方俯拍必须用上面的透视变换。判断标准很简单画一下车牌的minAreaRect边框如果四边和车牌边缘贴合良好旋转矫正就够用如果明显一边长一边短就上透视变换。5.1 蓝色车身误检几何特征能挡掉七八成蓝色车身的车尤其是深蓝色、宝蓝色的轿车在颜色掩膜阶段会连成一大片蓝色区域。它的宽高比通常不满足车牌的比例约束或者即使某一小部分区域比例接近矩形度也过不了关。但有一种情况比较麻烦蓝色车身的车门缝隙、车窗边缘和周围环境组合起来恰好形成了一块类车牌形状的区域。对付这种情况我的做法是再叠加一道“水平边缘密度”判断——车牌区域的水平边缘响应会非常密集因为字符之间存在大量水平方向的黑白跳变。def edge_density(roi): # 转换灰度并计算水平方向的Sobel梯度 gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) sobel_x cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize3) abs_sobel np.abs(sobel_x) # 二值化后统计非零比例 _, binary cv2.threshold(abs_sobel, 100, 255, cv2.THRESH_BINARY) density np.count_nonzero(binary) / binary.size return density实测下来真实车牌的边缘密度通常在 0.15 以上而大部分误检区域低于这个值。这个判断可以加在候选筛选阶段能过滤掉相当一部分颜色和比例都过关的干扰项。5.2 光照和阴影HSV 阈值不是万能的白天强烈的侧光会让车牌一半亮一半暗暗部的蓝色饱和度S和明度V都会显著下降导致掩膜断裂。夜间路灯下车牌整体偏暗V 值可能掉到 60 以下上面给的蓝色下界[100, 100, 80]会直接漏掉。我的经验是对光照做一次预处理使用 CLAHE 自适应直方图均衡化来增强局部对比度再做颜色筛选def preprocess_for_color(gray_or_bgr): # 转到 LAB 色彩空间只对 L 通道做 CLAHE lab cv2.cvtColor(gray_or_bgr, cv2.COLOR_BGR2LAB) l, a, b cv2.split(lab) clahe cv2.createCLAHE(clipLimit3.0, tileGridSize(8, 8)) l clahe.apply(l) lab cv2.merge((l, a, b)) return cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)加了 CLAHE 之后夜间和阴影场景的定位成功率明显提升。代价是处理耗时增加约 3~5ms实时系统可接受。如果你做的是离线图片处理无脑加就行。5.3 参数调优一次只动一个变量调 HSV 阈值和形态学核大小最忌讳同时改好几个参数。改完发现好了但不清楚是哪个调整起的作用改完发现更差了也不知道回退哪里。我的习惯是写一个简单的调参脚本用轨迹条实时调节参数直接观察掩膜输出和候选框效果def nothing(x): pass cv2.namedWindow(Tuning) cv2.createTrackbar(H_min, Tuning, 100, 180, nothing) cv2.createTrackbar(H_max, Tuning, 130, 180, nothing) cv2.createTrackbar(S_min, Tuning, 100, 255, nothing) cv2.createTrackbar(V_min, Tuning, 80, 255, nothing) # 回调逻辑读取轨迹条数值生成掩膜可视化用这个方式对着几张典型场景的图片调一遍参数基本就能收敛。调完之后放到全部测试集上验证漏检和误检率再微调几何筛选的阈值。5.4 性能优化实时视频流的处理策略处理单张图片很快但如果接摄像头视频流25fps每帧都做全图缩放和形态学会占用不少 CPU。优化手段有两个第一个是降低处理帧率。定位不需要逐帧处理可以每 3 帧做一次定位中间的帧直接用上一帧的位置裁切 ROI 做识别。车牌在连续几帧内位置变化很小这种方案实际体验几乎无损。第二个是缩小掩膜处理的区域。利用车牌的“位置先验”把画面下方 1/3 区域作为优先搜索区只在这个区域内做颜色筛选和轮廓检测。固定角度的卡口相机特别适合这种做法处理时间可以再降一半。定位模块输出了一块干净的车牌图接下来就是字符分割和识别环节。字符分割的核心步骤是二值化把车牌图变成黑白图让字符区域为白色、底色为黑色然后做垂直投影统计每一列中白色像素的个数字符所在列的投影会出现波峰字符间隙会出现波谷根据波峰波谷的位置就能把一个个字符切出来。字符识别部分两个方向一是模板匹配准备 0~9 和 A~Z 的模板图片把分割出的字符和模板逐个比对取相似度最高的作为结果优点是轻量、训练成本为零适合字体规范的车牌二是 CNN 分类器精度上限更高对模糊、变形的字符更稳但需要准备标注数据。从我自己的项目经验看先跑通模板匹配把整个链路建立起来再按需升级成 CNN迭代节奏最舒服。另外送一个细节新能源车牌有 8 个字符普通蓝牌只有 7 个分割模块拿到定位图的宽高比后就能推断出字符数量提前设定好分割参数。黄色大车牌的字符排列间距和蓝牌不同需要单独匹配一套投影阈值。定位模块的调试门槛比想象中低只要能静下心把颜色、几何、纹理三个维度的信息利用好透明度和可控性都比直接塞一个模型进去强得多。我在实际项目里最深的一个体会是不要指望一套参数通吃所有场景——把图片按白天、夜晚、逆光、阴雨分成几个 bucket每个 bucket 一套阈值比追求“通用万能参数”靠谱得多。这种工程取舍才是定位模块真正的精髓。
返回列表