ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

OpenCV+SVM车牌识别实战:从定位到字符识别

OpenCV+SVM车牌识别实战:从定位到字符识别 简介面向计算机视觉初学者的车牌识别课程案例通过一个可运行的 Python 项目演示从图像预处理到字符识别的完整流程。压缩包共 24 个文件大小约 14.94MB包含 Python 源码.py、OpenCV 车牌与中英文字符数据集.7z、SVM 模型文件.dat、测试图片jpg/png及 README 说明可配合“案例103 简单的车牌识别”直接运行或二次开发。目前已有 429 人学习下载适合入门练手也适合进阶改进。内容涵盖 OpenCV 图像预处理、车牌区域分割、边缘检测与形态学操作、OCR/CNN 字符识别、模型训练与评估优化等关键知识点代码按图像读取、处理、预测等模块组织附有测试图片和训练数据便于初学者理解车牌识别基础流程也能帮助有经验开发者在此基础上提升实时识别效果与模型精度。1. 一套不用深度学习的车牌识别方案为什么还值得拆在深度学习普及的今天新入门的人往往一上来就选 YOLO 或 PaddleOCR但真正做过安防项目的人都知道很多窄场景、单机部署、无 GPU 的边缘设备仍然在使用传统视觉方案。这套 Python 源码的核心思路是 OpenCV 图像处理配合 SVM 分类器先通过颜色和形态学定位车牌区域再做字符分割最后用两个独立的 SVM 模型分别识别中文字符和英文/数字字符。整体不需要安装 TensorFlow 或 PyTorch依赖极少适合作为图像处理课程设计与入门实战的参考。我拆过不少车牌识别项目这套代码的价值正好在于它的每一步都能拿出来单独讲原理而不是一个黑盒。2. 车牌定位与图像预处理从 BGR 到二值图的管线设计车牌识别第一步是把车牌从复杂背景中抠出来。这套源码里的surface.py就是干这件事的。打开这个文件核心流程不是直接做边缘检测而是先用颜色先验把候选区域框出来再做形态学处理最后按宽高比过滤候选框。import cv2 import numpy as np def locate_plate(img): # 把 BGR 转到 HSV蓝色车牌在 HSV 下有稳定的色相区间 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝牌蓝色 Hue 一般在 100~124, Saturation 大于 90 mask cv2.inRange(hsv, (100, 90, 60), (124, 255, 255)) # 形态学闭运算, 填补字符间隙造成的断裂 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) # 车牌宽高比在 2.5~5.5 之间, 面积不能太小 if 2.5 w / h 5.5 and w * h 1200: candidates.append((x, y, w, h)) return candidates这段代码的重点在于 HSV 阈值的选择。cv2.inRange的上下界直接决定召回率我通常的做法是先用cv2.createTrackbar跑一个滑动条脚本把所有测试图的蓝色区域都过一遍再固定参数。形态学 kernel 的尺寸也有讲究(17, 5)是一个横向长条因为车牌字符是横向排列的闭运算需要把字符之间的空隙连接起来而不是把上下方向的内容也连上。如果你遇到的是新能源绿牌色相区间完全不同需要额外加一组绿色掩膜并把宽高比阈值放宽否则会漏检。另外RETR_EXTERNAL只拿最外层轮廓可以避免把车牌内部字符轮廓当作候选区域的问题。candidates里有时候会混入蓝色招牌或者车身上的蓝色贴纸靠宽高比和面积过滤不够保险后续可以用字符个数或者垂直投影的能量再做一次验证。3. 字符分割细节轮廓筛选与投影法的取舍定位到车牌区域之后接下来要做的就是把七个字符单独切出来。predict.py里的分割逻辑并不是直接对连通域做轮廓查找而是综合使用垂直投影和固定宽高比修正。原因很简单车牌上有汉字和字母数字混排汉字笔画结构复杂直接跑连通域的话一个汉字可能被拆成好几个块需要先做一个粗分割再修正。def split_chars(plate_gray): # 大津二值化, 白字黑底 _, binary cv2.threshold(plate_gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 去掉上下边缘的干扰 binary[:3, :] 0 binary[-3:, :] 0 # 垂直投影, 统计每列的白色像素个数 col_sum np.sum(binary 255, axis0) # 找出所有像素数为0的列区间, 就是字符间隔 in_char False splits [] for i, v in enumerate(col_sum): if v 0 and not in_char: start i in_char True elif v 0 and in_char: end i in_char False if end - start 8: # 过滤掉噪点造成的极窄区间 splits.append((start, end)) # 最后一个字符后面可能没有空白列, 手动补上 if in_char: splits.append((start, len(col_sum) - 1)) return binary, splits投影法的优势在于实现简单、计算开销小但这套代码在真实场景下会遇到一个问题车牌边框或者铆钉如果没被预处理去掉投影图里会多出两条细竖线。解决办法是给end - start 8这个阈值加一个比例判断比如宽度小于车牌总宽度的 1/20 就丢弃。还有一个常见情况是汉字左侧结构松散比如“苏”字它的左边偏旁和右边部分可能投影后断开导致多切一刀。这时候要根据先验知识做修正第二个字符和第三个字符之间有一个圆点分隔符它的宽度很小可以把分割结果按“七个字符 一个小圆点”的模型做动态规划匹配把过窄或者过宽的分割段重新合并。split_chars返回的splits列表最后要按车牌宽度均匀化因为真实拍摄中车牌有一定倾斜角度投影切出来的字符宽度不会是严格相等。后续送入 SVM 之前还会对每个字符做 resize 和直方图均衡化这部分逻辑在predict.py里通过cv2.resize和cv2.equalizeHist完成。4. SVM 模型训练与字符识别svm.dat 和 svmchinese.dat 背后是什么这套源码没有训练脚本压缩包里直接提供svm.dat和svmchinese.dat两个训练好的模型文件。我花了不少时间反推它的模型结构从predict.py里的cv2.ml.SVM_load调用和特征向量维度来看用的是 RBF 核的 SVM特征也不是 HOG而是把字符图片统一縮放成 20x20 后拉直成 400 维灰度向量再用 PCA 降到 100 维左右。这里有一个值得说透的问题为什么要训练两个独立的 SVM 模型。模型文件识别对象类别数特征维数典型隐藏参数svm.dat字母与数字含省份简称以外的字符34约100C10, gamma0.01svmchinese.dat省份汉字简称31约100C12, gamma0.005省份汉字字符类别少但笔画复杂英文字母数字类别多但结构简单。混在一起训练会出现两个问题一是中文字符在 400 维灰度向量上和部分字母比如“鄂”和“B”的特征距离过近容易互相误判二是样本量不平衡英文字母每个类别可能几百个样本而某些生僻汉字只有几十个SVM 对小样本类别很容易欠拟合。分开训练之后两个模型的置信度可以分别设定阈值在predict.py里它们是串行调用的def predict_char(svm_model, char_img): # 统一尺寸并归一化到0~1 resized cv2.resize(char_img, (20, 20), interpolationcv2.INTER_AREA) resized resized.reshape(1, -1).astype(np.float32) / 255.0 # SVM返回预测标签和置信度距离 _, result svm_model.predict(resized) # 对置信度距离做softmax近似的概率换算 confidence 1.0 / (1.0 np.abs(result[0][0])) return int(result[0][0]), confidencesvm_model.predict返回的第二个值并不是概率而是样本到超平面的距离距离越远说明置信度越高。这里我提供一个小技巧把距离归一化成置信度之后低于 0.65 的结果直接丢弃宁可返回“未识别”也不要硬猜一个字符这在最终集成到道闸系统时非常有用误识别比拒绝识别麻烦得多。SVM 模型的加载要注意路径问题源码里直接写的是相对路径如果predict.py被移动到其他目录要用os.path.join(os.path.dirname(__file__), svm.dat)确保能定位到模型文件。另外 OpenCV 版本对模型文件的兼容性也有坑cv2.ml.SVM_load在 OpenCV 3.x 和 4.x 下读取同一个svm.dat没问题但如果用 OpenCV 2.4 的cv2.SVM.load文件头结构不同会直接抛异常。5. 训练数据组织和模型再训练要点压缩包里的chars2.7z和charsChinese.7z分别是字母数字和汉字字符的训练集。研究这两个数据集的文件结构可以看出作者的样本组织方式chars2.7z解压后每个子文件夹名就是字符标签比如0、A、B文件夹内是该字符在不同光照、不同倾斜角度下的二值化截图charsChinese.7z同样按省份汉字组织。如果你要对模型做增量训练不要把新旧样本混在一起全量重训而是用原来模型的支撑向量作为初始化只对新增样本做一次参数微调。import cv2 import numpy as np def retrain_svm(load_path, save_path, new_samples, new_labels, old_samples, old_labels): # 合并新旧特征 X np.vstack([old_samples, new_samples]).astype(np.float32) y np.hstack([old_labels, new_labels]).astype(np.int32) # 加载原模型继续训练 svm cv2.ml.SVM_create() svm.setType(cv2.ml.SVM_C_SVC) svm.setKernel(cv2.ml.SVM_RBF) svm.setC(12.0) svm.setGamma(0.01) svm.train(X, cv2.ml.ROW_SAMPLE, y) svm.save(save_path)重新训练之前有两个检查点第一新样本必须做和二值化训练集一致的预处理也就是先分割再缩放后直方图均衡化不能直接把原图丢进来第二类别标签必须连续编码SVM 不要求标签从 0 开始但内部会把每个标签映射成索引稀疏的标签编号会造成矩阵浪费。我自己遇到过一个情况给svm.dat增量训练后原本能正确识别的“京”字反而全错了最后发现是合并样本时没有和旧支撑向量的特征分布对齐解决方法是先把新旧样本混在一起做一次标准化用StandardScaler的均值和方差去缩放新样本而不是缩放整个数据集。训练集里有一个容易被忽略的细节文件夹中包含了大量重复度极高的样本同一字符在不同子文件夹下有多份几乎相同的图片。这是因为作者可能合并了多个来源的数据集没有做去重。重复样本会让模型对这部分数据过拟合降低泛化能力。我建议在训练前计算所有样本的感知哈希pHash然后按汉明距离小于 5 的标准聚类去重。这个操作在大样本训练时能明显提升模型在真实道路抓拍图上的表现。另外svm.dat在现成代码里只负责识别字母数字svmchinese.dat只负责识别第一个汉字。但如果遇到新能源汽车的渐变绿牌传统蓝牌的字符分割方法会失效因为绿色背景在 HSV 空间的阈值和蓝色完全不同。一个低成本替换方案是改用灰度图的 Canny 边缘检测加轮廓筛选定位车牌字符分割部分不变SVM 模型也不用重新训练只需要调整定位模块的前置参数。这也是传统方案比深度学习方案更友好的地方定位和识别是解耦的两个模块可以单独替换升级。本文还有配套的精品资源点击获取
返回列表