
车牌识别系统做到第二篇上一篇我们把整体框架搭起来了图像采集、预处理、边缘强化这些基础环节都讲透了。这一篇直接进入整个系统里最有技术含量、也最考验工程经验的部分——车牌定位。我先把话说在前头车牌识别这条流水线里“定位”这一步决定了后面字符分割和识别的天花板。你定位出来的区域偏了、歪了、或者把车灯当成车牌框进去了后面就算识别模型再好也白搭。所以这一篇我尽量把定位环节的原理、代码、参数调优、坑位全部摊开讲依然沿用OpenCV为主的传统方案同时补充一些基于深度学习检测模型的进阶思路方便你按自己的需求对号入座。1. 车牌定位的核心思路与方案选型1.1 先搞清楚定位到底要解决什么问题车牌定位的任务听起来很简单在一张图片里把车牌所在的矩形区域找出来。但仔细琢磨一下就知道难点在哪一张真实场景的照片里可能有车灯、保险杠、车标、广告牌、路牌、树叶阴影这些元素在颜色、形状、纹理上都有可能和车牌沾边。定位算法要做的就是在这些干扰项里把真正的车牌区域筛选出来。从技术路线上看主流的定位方案大概有四类基于颜色的方法利用车牌底色蓝底白字、黄底黑字、绿底白字等在HSV颜色空间中的聚类特性通过阈值分割提取候选区域。基于边缘与形态学的方法利用车牌区域字符密集、边缘纹理丰富的特点通过边缘检测、形态学闭运算、轮廓筛选锁定位置。基于机器学习的方法用Haar、LBP等特征训练级联分类器本质上是把定位当成一个二分类问题来扫窗口。基于深度学习的方法用目标检测模型YOLO、SSD、Faster R-CNN直接回归出车牌位置这是目前工程落地的主流方案。我这次的项目选的是“颜色边缘形态学轮廓筛选”这条传统路线。原因很实在第一这个系列的目的是把识别链路讲透传统方法每一步都能可视化、能调参适合理解原理第二传统方案在光线稳定、场景相对固定的出入口场景下速度和资源占用有优势跑在树莓派、Jetson Nano这类边缘设备上毫无压力第三不依赖标注数据和GPU训练部署简单。1.2 为什么把“定位”单独拎出来讲很多初学者上手车牌识别喜欢直接调一个OCR模型把图片丢进去但实际效果往往惨不忍睹。原因就在于OCR模型接收的输入应该是“已经裁剪好的车牌区域”而不是整张场景图。你让识别模型在一张1080p的大图里自己去“找”车牌它的注意力可能全被车灯、车标、车身贴纸带跑了更不用说整图推理对算力的浪费有多严重。所以整个系统的正确打开方式一定是分阶段处理先定位把车牌区域从场景中切出来再分割把车牌中的字符一个个拆开最后识别对单字符或者整行序列做内容识别。定位是承上启下的关键节点定位不准后面全是无用功。我自己在项目里对定位模块有三个硬性要求定位框要紧凑宁可稍微紧一点也别把边上的干扰塞进框里定位速度要快单帧处理时间不能拖后腿定位结果要给后续模块提供清晰的二值图方便字符分割。这三个要求直接决定了我在传统方法里的参数选型和处理顺序。1.3 传统方法与深度学习怎么选如果你是做实际项目我的建议是分场景看。停车场道闸、小区出入口、高速收费站这类“相机位置固定、拍摄角度固定、光线可控或可补光”的场景传统图像处理方法完全够用而且稳定、可解释、好维护。你甚至可以用一台几十块钱的USB摄像头加一个树莓派就能实现全天候的车牌抓拍。但如果你要处理的是复杂场景比如道路监控里的多车道车辆、夜间强光逆光、雨天泥点遮挡、多车辆同时入镜传统方法就会频繁翻车。这时候必须上深度学习的检测模型用数据让模型自己学习“车牌长什么样”鲁棒性会高很多。我这篇文章会把传统方法的代码完整拉通同时在第4章讲清楚如何用YOLO这类检测模型来做定位以及两种方案的取舍和融合策略。2. 定位算法的原理拆解与关键预处理2.1 预处理阶段到底做了什么车牌定位不是上来就找轮廓的前面还有一大堆预处理工作要做。我用的处理链路是这样的图像缩放→灰度化→高斯滤波去噪→Sobel边缘检测→二值化→形态学闭运算→轮廓查找与筛选。每一步看着简单但每一步的细节都可能决定成败。图像缩放是第一件重要的事。原始图片如果是1920×1080直接送进算法不仅处理慢而且很多小噪点会在后续步骤中被放大干扰。我通常把图片最长边缩放到800到1000像素左右既能保留车牌的纹理细节又能显著提升处理速度。注意别缩太小不然车牌边缘纹理会被抹掉。灰度化和高斯滤波属于常规操作。灰度化降低计算量高斯滤波主要用来压制图像噪声尤其是光照不均产生的颗粒噪点。这里有一个关键参数高斯核的大小。核太大会把车牌的边缘也模糊掉核太小等于没滤波。我的经验是先用5×5的核如果发现边缘检测结果太多杂讯再考虑加大到7×7。2.2 边缘检测的选型与参数边缘检测是车牌定位的基石。车牌区域里有多个字符字符和底色之间存在高对比度的边缘这些边缘密集且方向相对统一是车牌区域最稳定的特征之一。常用的边缘检测算子有Sobel、Laplacian和Canny我在这个项目里选的是Sobel算子。为什么不直接用CannyCanny虽然能给出细化的边缘图但它的双阈值需要根据图像内容动态调整真实场景光照变化大固定阈值很难一招吃遍天。Sobel算子计算简单、对噪声不敏感而且可以指定只检测水平方向或垂直方向的边缘这对车牌场景非常友好。车牌字符的边缘在水平方向上分布密集我通常只做水平方向的Sobel检测这样能天然过滤掉一部分垂直方向的干扰边缘。关键参数是Sobel核大小ksize和输出图像的数据类型。ksize一般取3x3或5x5我习惯取5x5边缘更连续。输出结果需要取绝对值再转成8位图这一步经常被漏掉会导致后面二值化全黑全白如果你发现自己的结果图是花的先检查这里。2.3 二值化与形态学操作的深意边缘检测得到的是灰度边缘图接下来要做二值化把边缘信息压缩成黑白两色。我用的方法是固定阈值二值化阈值取100到150之间具体看边缘图的对比度。如果光照剧烈变化推荐用OTSU自适应阈值它自动计算最优分割阈值省心很多。二值化之后车牌区域的字符边缘会呈现出一条条细碎的白色纹理。这时候要给这些细碎纹理“连线搭桥”把字符之间、字符与车牌边框之间的缝隙连成一个整体这就是形态学闭运算的核心作用。闭运算包括先膨胀后腐蚀两个操作对目标区域内的细小空洞有填充效果。这里最有技术含量的是结构元素核的大小怎么定。核的宽度至少要大于单个字符间的空隙宽度否则无法把字符连起来核的高度又不能太大否则会把车牌的上下边框和邻近的干扰区域也连进来。以440mm×140mm的常见车牌比例来说图像中字符间隙约占车牌宽度的10%到15%左右我试验下来在图像最长边缩放到800像素的前提下取一个宽度为17、高度为5的矩形核效果比较稳定。这个参数值得你反复调试因为不同的图片尺寸、不同拍摄距离最优核大小都不一样。2.4 轮廓查找与候选框筛选逻辑形态学处理完图像中会出现若干连通区域其中有一个或几个就是车辆车牌的候选区域。OpenCV的findContours函数可以把这些连通区域的外轮廓找出来接下来就是筛选。筛选的核心思路是从“形状像不像”和“面积够不够”两个维度把关。常规车牌的宽高比大约在3:1左右具体根据不同国家和车型会略有浮动我设置的筛选范围通常是宽高比在2.0到4.5之间。同时面积也有要求候选框面积占整张图面积的比例下限设0.5%上限设10%左右避免过小的噪点区域和过大的车身区域混进来。除此之外我还会加一个“矩形度”的验证也就是轮廓面积和其最小外接矩形面积的比值车牌是标准矩形这个比值通常很高如果某个候选区域的矩形度低于0.5那大概率是形状不规则的干扰物。3. 基于OpenCV的车牌定位完整代码实现3.1 环境准备与依赖安装先说环境。我的开发环境是Python 3.8OpenCV 4.5操作系统随便Windows、Linux、macOS都行。主要依赖就两个opencv-python和numpy安装命令很简单pip install opencv-python numpy如果你需要可视化调试可以再装一个matplotlib用来显示中间过程的图像方便调参。我自己调试时习惯把边缘图、二值图、形态学结果图全部画出来对比哪个环节出问题一目了然。3.2 核心定位函数的完整实现下面这段代码是我在项目里实际使用的定位函数注释写得很详细可以直接拿去跑。import cv2 import numpy as np def locate_license_plate(img): 输入: 单帧图像 (BGR) 输出: 车牌区域裁剪图、车牌外接矩形坐标未找到返回 None # 保持宽高比将最长边缩放到 800降低计算量同时保留细节 h, w img.shape[:2] scale 800.0 / max(h, w) if scale 1.0: img cv2.resize(img, (int(w * scale), int(h * scale))) # 1. 转灰度 高斯滤波去噪 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray cv2.GaussianBlur(gray, (5, 5), 0) # 2. 水平方向 Sobel 边缘检测因为车牌字符边缘在水平方向密集 sobel_x cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize5) sobel_x cv2.convertScaleAbs(sobel_x) # 3. 二值化固定阈值 手动调优光照变化大时推荐 cv2.THRESH_OTSU _, binary cv2.threshold(sobel_x, 120, 255, cv2.THRESH_BINARY) # 4. 形态学闭运算先膨胀再腐蚀把字符缝隙连接起来 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 5. 查找轮廓 contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) h_img, w_img closed.shape[:2] candidates [] for cnt in contours: x, y, w_cnt, h_cnt cv2.boundingRect(cnt) area_ratio (w_cnt * h_cnt) / (w_img * h_img) aspect_ratio w_cnt / max(h_cnt, 1) # 核心筛选条件宽高比 面积占比 矩形度 if 2.0 aspect_ratio 4.5 and 0.005 area_ratio 0.1: rect_area w_cnt * h_cnt cnt_area cv2.contourArea(cnt) solidity cnt_area / rect_area if rect_area 0 else 0 if solidity 0.5: candidates.append((w_cnt * h_cnt, (x, y, w_cnt, h_cnt))) if not candidates: return None, None # 6. 按面积从大到小排序返回面积最大的候选框 candidates.sort(keylambda s: s[0], reverseTrue) _, best_rect candidates[0] x, y, w_rect, h_rect best_rect # 边缘收紧减少冗余背景干扰 margin_x int(w_rect * 0.05) margin_y int(h_rect * 0.1) x max(0, x - margin_x) y max(0, y - margin_y) x2 min(w_img, x w_rect margin_x * 2) y2 min(h_img, y h_rect margin_y * 2) plate_img img[y:y2, x:x2] return plate_img, best_rect3.3 主流程与可视化调试代码有了核心定位函数主流程就简单了。我通常会把中间结果保留下来写一个小工具函数把各个阶段的图拼在一起展示。这里有一个完整的调用示例if __name__ __main__: img cv2.imread(test_car.jpg) plate_img, rect locate_license_plate(img) if plate_img is not None: x, y, w, h rect result img.copy() cv2.rectangle(result, (x, y), (x w, y h), (0, 255, 0), 2) cv2.imwrite(plate_detect_result.jpg, result) cv2.imwrite(plate_crop.jpg, plate_img) print(定位成功矩形坐标, rect) else: print(未定位到车牌)我强烈建议在调试阶段把gray、sobel_x、binary、closed这几张中间图都保存下来放在一起对比看。很多时候定位失败一眼看到某一步的图像烂了直接就知道该调哪个参数。我自己的调试脚本里还会加上滑动条实时调阈值观察效果这一步能帮你节省大量调参时间。3.4 传统定位方案的局限性上面这套代码在光线稳定、角度正常的场景下定位成功率还是相当可观的我实测在停车场出入口那种环境能达到95%以上。但这套方案有它的硬伤。一是对黄色车牌的适应性差。黄色车牌和蓝色车牌的灰度特征差异很大蓝色车牌的灰度值和车身接近在灰度图上区分度不高所以靠边缘和形态学能处理蓝牌但对黄色车牌的召回率会有波动。解决方案是增加颜色通道的识别比如在HSV空间里先粗定位蓝色或黄色区域再结合边缘结果融合判断。二是对倾斜和畸变敏感。如果拍摄角度过大车牌在图像里是梯形甚至平行四边形边缘检测后形成的连通区域形状会不太规整单纯用矩形宽高比筛选很容易漏检。这时候需要加一步透视矫正或者用旋转外接矩形去拟合。三是受复杂背景干扰严重。车身贴纸、保险杠镀铬条、路牌、灯箱广告这些都可能形成与车牌相似的边缘密度和形状有时候一次会筛出三四个候选框。这时候就要靠后续的字符分割和识别结果来反馈验证把识别置信度最高的框作为真车牌。4. 进阶方案把深度学习模型引入定位环节4.1 为什么商业项目里更常用YOLO前面说过传统方法在要求不高的场景下够用但我必须提醒一句如果你把目标定位在完整的商业项目上深度学习检测方案才是真正的主流选择。原因有三个复杂场景鲁棒性、算法维护成本、对新场景的适配能力。传统方法的阈值参数高度依赖场景换一个摄像头位置可能就要重新调一遍。深度学习模型本质上是用数据驱动的方式自动学习车牌特征换场景后只需要补充一些新场景的标注数据做微调不需要从零开始设计特征规则。而且在多目标场景下一个检测模型可以一次把所有车牌都框出来传统方法要处理得加很多逻辑。目前工程上用得最多的检测模型集中在YOLO系列从YOLOv5到YOLOv8都有成熟的开源实现部署工具链也很完善。车牌检测这种目标特征非常明确、类别单一的任务效果可以做得非常好。4.2 基于YOLO的车牌检测代码骨架这里我给一个训练和推理的最小闭环示例模型结构直接用目标检测框架自带的。以YOLOv8为例# 安装依赖 # pip install ultralytics from ultralytics import YOLO # 加载预训练模型也可以用 yolo11n.pt 等新版本权重 model YOLO(yolov8n.pt) # 训练数据集格式为 YOLO 格式的 label 文件 # data.yaml 里配置 train/val 路径、类别数量和名称 model.train(dataplate_data.yaml, epochs100, imgsz640, batch16) # 推理直接检测图片中的车牌 results model.predict(test_car.jpg, conf0.5, saveTrue) for r in results: boxes r.boxes.xyxy.cpu().numpy() scores r.boxes.conf.cpu().numpy() for box, score in zip(boxes, scores): x1, y1, x2, y2 map(int, box) print(f车牌框: ({x1}, {y1}, {x2}, {y2}), 置信度: {score:.2f})代码看着很短但实际工程里难点在数据侧。车牌标注的规范很重要比如是否包含车牌边框、是否要包含螺丝孔区域各家项目约定不同。我自己的习惯是标注框紧贴字符区域不包含外边框和螺丝孔这样有利于后续做字符分割时减少干扰。当然如果你只做检测不管识别标注规范可以更随意一些。4.3 数据准备、训练调优与部署要点数据集这块开源的车牌检测数据集不少但真实场景千差万别我建议至少要自己采集500到1000张现场图像做补充标注。采集时尽量覆盖不同时间段白天强光、黄昏、夜间、不同天气、不同角度。数据增强也很关键YOLO训练时自带的mosaic、翻转、色彩空间变换已经很好用我还会额外加入轻微的模糊和噪声模拟提升模型的抗干扰能力。训练参数上imgsz一般取640。如果车牌目标在整张图中偏小可以适当提高到960或1280但推理速度会下降。epochs看数据量数据量小两三千张100轮足够数据量大就提前用早停机制避免过拟合。batch_size根据显存来调整我的经验是训练集几千张图的规模下16到32都不错。部署上如果你跑在Jetson Nano这类设备上推荐用TensorRT做加速可以把YOLOv8n的推理时间压到20毫秒以下。如果跑在普通PC上直接用PyTorch或者ONNX Runtime也能满足实时性要求。边缘设备上做模型量化FP16或INT8时要注意精度回退尤其是夜间场景量化后漏检率可能会上升。4.4 传统方法和深度学习方案的融合经验对实际项目来说没必要把两种方案对立起来。我见过不少落地方案是把两者串成一个pipeline先用YOLO检测出候选车牌区域这块速度快、召回率高然后用传统图像处理做精调和验证裁出来的区域再做一次倾斜矫正和清晰度判断质量不合格的触发重拍或补光。这样既能保证召回率又能控制误检率。另外有一个非常实用的融合技巧用深度模型检测结果做置信度评分当模型置信度低于某个阈值时再调用传统颜色形态学方案做第二路确认两路结果一致就放行不一致就判定为不确定交给后续字符识别模块二次裁决。这个策略在极暗光线下效果尤其明显能把整体识别成功率拉高好几个百分点。5. 定位环节的常见问题与排查技巧实录5.1 高频问题速查表我把这段时间被问得最多的几个问题整理成一个表格都是我实际踩过的坑你遇到类似情况可以直接对照排查。现象可能原因解决方案定位框完全找不到车牌Sobel之后图像太黑边缘没出来检查sobel_x是否做convertScaleAbs确认二值化阈值是否过高定位框老是框到车灯或车身贴纸形态学核太大把相邻区域连成一片缩小核的宽度和高度或者增加矩形度筛选条件远景图片定位不到车牌缩放后车牌区域边长小于阈值轮廓过小被过滤缩放到800像素的基础上减小面积下限或对ROI做局部放大检测夜间图片定位率大跌光线太暗边缘纹理不清晰预处理加自适应直方图均衡化CLAHE或降低二值化阈值运行速度太慢原图太大形态学核太大先缩放再处理缩放到640即可核大小同步缩小蓝牌和绿牌一个能定位一个不行灰度空间里两种车牌对比度差异大增加HSV颜色空间的并行检测融合两次结果5.2 最值得讲的一个隐藏坑核大小与图像尺寸的联动关系这个坑我一开始没意识到后来在换了一组新摄像头数据时定位率突然暴跌排查了很久才发现是形态学核大小没有跟着图像缩放比例调整。很多人写代码时图省事把核大小写死成(17, 5)。但如果你的图像预处理从1280缩放到了800而另一组测试数据从1920缩放到了1000同一个核在两种尺度下的实际效果是完全不同的。合理做法是让核的尺寸和图像尺寸保持一个近似的比例关系比如在最长边800像素时用17的宽度在600像素时就应该缩到13左右在1000像素时就放大到21。我做了一个简单的计算公式来辅助调参核宽度约等于图像最小边长的1/50到1/30核高度取宽度的1/3到1/4。这个范围不一定最优但比写死一个值要稳健得多。类似的动态参数思路也适用于二值化阈值和面积筛选区间本质上是让算法在不同分辨率下保持一致性。5.3 调试工具与肉眼验证心得最后一个建议一定要把定位结果可视化出来而且要看中间过程不要只看最终框。我调试时一定会保存四张图原始图、Sobel边缘图、二值图、闭运算结果图再和最终定位框放在一起对比。哪个环节出了问题一眼就能找到。另外批量跑测试的时候我习惯把所有定位失败和定位偏差大的图片单独归到一个文件夹里定期翻出来看。很多问题在单张图上不明显但几十张放一起就能看出规律比如“下午三点的逆光图全部失败”“所有白色车辆的图都会多框一个行李架区域”这些规律就是后续优化的切入点。车牌定位做完下一步就是字符分割和识别了。字符分割的思路和定位环节有不少相通的地方还是先从边缘、投影、连通域这几个角度去做拆解下一篇我会把字符分割的坑和识别模型的接入一起写。定位这块如果你正卡在参数调不出来或者出现什么离谱的误检建议把我上面的调试流程完整跑一遍很多问题其实都出在预处理链条中不起眼的细节上。