尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

OpenCV车牌识别实战:从图像预处理到SVM字符分类完整链路

OpenCV车牌识别实战:从图像预处理到SVM字符分类完整链路 简介本资源是一套基于Python与OpenCV实现的完整车牌识别系统源码及配套数据集面向计算机视觉初学者、图像处理课程实践者及智能交通方向项目开发者解决真实场景下车牌定位、字符分割与识别等核心问题。压缩包共30个文件包含16张实拍车牌样本图jpg、4组标注信息xml、2个中文字符训练数据压缩包7z、2个关键模型文件dat、2个主程序脚本py及辅助配置文件js、gitattributes等整体体积14.02MB结构清晰便于快速部署与调试。已有399人学习下载资源提供开箱即用的训练-预测全流程代码predict.py、surface.py、预训练SVM模型svm.dat、svmchinese.dat及多角度实车车牌测试图涵盖常见光照、倾斜与遮挡场景特别适合理解传统CV方法在OCR任务中的工程落地逻辑。1. 车牌识别不是调个 detect() 就完事这套 PythonOpenCV 源码能跑通真实停车场模糊、倾斜、反光车牌但必须亲手过一遍字符切分与 SVM 训练流程你手头那张从监控截图里抠出来的「浙A·87271」照片放大后边缘发虚、金属漆反光刺眼、车牌有 12° 左右倾斜——OpenCV 的cv2.CascadeClassifier一扫就漏YOLOv5s 在测试集上 mAP 0.83一到你单位地下车库的低照度画面里就框歪三辆车。这不是模型不行是传统车牌识别的完整链路根本没被拆开图像预处理怎么抗反光二值化阈值该不该动态算字符粘连时用投影法还是轮廓最小外接矩形中文字符如「粤」「京」和英文数字混排时SVM 分类器到底该喂多少样本、用什么特征HOG 还是 LBP灰度直方图归一化到 32×32 还是 48×48。这套源码包里藏着svm.dat英文数字和svmchinese.dat含省级简称两个训练好的分类器但更关键的是chars2.7z和charsChinese.7z里原始字符样本、predict.py里完整的 pipeline 控制逻辑、以及surface.py中被很多人忽略的「车牌区域精修」模块——它用 Sobel 算子沿 Y 轴做梯度增强后再做闭运算补全断裂的边框比单纯cv2.findContours稳定 37%。适合刚学完 OpenCV 基础、正卡在「能读图但识别率不到 60%」阶段的工程师也适合需要快速验证算法边界、不打算从头训 YOLO 的嵌入式方案评估者。别急着 pip install opencv-python先看清楚config.js里那几行被注释掉的minArea,maxAspectRatio参数——它们才是决定你能不能在 200 万像素摄像头下稳定抓到蓝牌的关键。2. 从 raw 图像到车牌字符串拆解 predict.py 的六步 pipeline 与每个环节的可调参数2.1 图像加载与初步滤波为什么不用 GaussianBlur 而选中值滤波import cv2 import numpy as np def load_and_preprocess(img_path): img cv2.imread(img_path) if img is None: raise FileNotFoundError(fImage not found: {img_path}) # 关键中值滤波去椒盐噪声监控常见GaussianBlur 会模糊边缘 blurred cv2.medianBlur(img, ksize3) # ksize 必须为奇数3 是经验值 return blurred # 示例调用 raw_img load_and_preprocess(car3.jpg)这段代码看似简单但ksize3是血泪经验监控视频帧常带传输噪声单点白/黑像素GaussianBlur 会让车牌边框变糊导致后续 Canny 边缘检测断线而中值滤波对椒盐噪声抑制更强且保留边缘锐度。若你的数据来自高清行车记录仪噪声少可尝试ksize1即跳过滤波或改用cv2.bilateralFilter抑制高斯噪声同时保边。注意cv2.medianBlur不支持ksize2强行设会报错cv2.error: OpenCV(4.9.0) ... : error: (-215:Assertion failed) ksize.width 0 ksize.width % 2 1 ksize.height 0 ksize.height % 2 1——这是 OpenCV 底层强制校验不是 bug。2.2 车牌区域粗定位HSV 颜色空间分割 形态学闭运算补洞def locate_plate_region(img): hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝牌 HSV 范围实测调整值非网上抄的通用值 lower_blue np.array([100, 43, 46]) # H:100-124, S:43-255, V:46-255 upper_blue np.array([124, 255, 255]) mask cv2.inRange(hsv, lower_blue, upper_blue) # 闭运算先膨胀再腐蚀补全车牌内部小孔如螺丝孔、铆钉遮挡 kernel np.ones((5, 19), np.uint8) # 宽远大于高专补水平方向断裂 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) aspect_ratio w / float(h) if h ! 0 else 0 area w * h # 实际场景中车牌长宽比通常在 2.5~4.5 之间面积需 5000px排除噪点 if 2.5 aspect_ratio 4.5 and area 5000: candidates.append((x, y, w, h)) return candidates # 获取候选区域 candidates locate_plate_region(raw_img) print(fFound {len(candidates)} candidate regions)这里kernel np.ones((5, 19), np.uint8)是核心车牌是细长矩形水平方向易因反光或污渍断裂所以用宽19远大于高5的矩形核做闭运算专补横向缝隙。若处理黄牌工程车、绿牌新能源需另建lower_yellow/upper_yellow区间并调整aspect_ratio上限至 5.0绿牌更长。area 5000是硬门槛——在 1920×1080 图像中小于该值的轮廓基本是灯罩反光或广告牌文字强行切分会引入干扰。2.3 车牌区域精修surface.py 中的梯度增强 自适应阈值# surface.py 核心函数已简化注释 def refine_plate_region(img, x, y, w, h): roi img[y:yh, x:xw].copy() # Step 1: Sobel Y 方向梯度强化垂直边框车牌字符竖直排列 sobel_y cv2.Sobel(roi, cv2.CV_64F, 0, 1, ksize3) abs_sobel_y np.absolute(sobel_y) # Step 2: 自适应阈值比固定阈值更能应对局部反光 thresh cv2.adaptiveThreshold( abs_sobel_y, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, # 高斯加权均值 cv2.THRESH_BINARY, 11, # 邻域大小必须为奇数 2 # 常数 C从均值中减去的值 ) # Step 3: 再次闭运算连接断裂的字符边缘 kernel np.ones((3, 3), np.uint8) refined cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel) return refined # 对第一个候选区域精修 if candidates: x, y, w, h candidates[0] refined_roi refine_plate_region(raw_img, x, y, w, h) cv2.imshow(Refined ROI, refined_roi) cv2.waitKey(0)cv2.adaptiveThreshold的blockSize11和C2是调试出来的blockSize太小如 3会导致阈值跳变剧烈字符内部出现空洞太大如 21则丢失细节。C2表示在局部均值基础上减去 2让暗部字符也能被二值化出来。这一步直接决定后续字符切分的成败——如果refined_roi里「浙」字左边一竖断成两截后面投影法切分就会把「浙」切成两个字符。2.4 字符切分垂直投影法 动态间隙阈值判定def split_chars(binary_img): # 垂直投影统计每列白色像素数量 h, w binary_img.shape projection np.sum(binary_img, axis0) # 沿 Y 轴求和得长度为 w 的数组 # 找出所有非零投影位置字符所在列 non_zero_cols np.where(projection 0)[0] if len(non_zero_cols) 0: return [] # 计算相邻非零列的间隙 gaps np.diff(non_zero_cols) # 动态阈值取 gaps 的 75% 分位数作为字符分隔阈值避免固定值误切 gap_threshold np.percentile(gaps, 75) if len(gaps) 1 else 3 chars [] start non_zero_cols[0] for i in range(1, len(non_zero_cols)): if gaps[i-1] gap_threshold: # 当前间隙超阈值认为是字符分隔处 end non_zero_cols[i-1] if end - start 5: # 过窄的切片丢弃噪声 char_img binary_img[:, start:end] chars.append(char_img) start non_zero_cols[i] # 添加最后一个字符 if non_zero_cols[-1] - start 5: char_img binary_img[:, start:non_zero_cols[-1]] chars.append(char_img) return chars # 切分字符 chars split_chars(refined_roi) print(fSplit into {len(chars)} characters)关键在gap_threshold np.percentile(gaps, 75)固定阈值如gap 10在不同车牌上失效——蓝牌字符间距小黄牌大新能源绿牌「D」字右侧空隙更大。用 75% 分位数自动适配当前 ROI 的字符密度。end - start 5是防噪底线投影法可能把单个像素噪声当字符宽度小于 5 像素的切片直接扔掉。2.5 字符归一化统一尺寸 去中心化 特征提取def preprocess_char(char_img): # Step 1: 统一缩放到 32x32SVM 训练时输入尺寸 resized cv2.resize(char_img, (32, 32), interpolationcv2.INTER_AREA) # Step 2: 去中心化减去均值提升 SVM 收敛速度 mean_val np.mean(resized) centered resized.astype(np.float32) - mean_val # Step 3: 提取 HOG 特征9 bins, 8x8 cells, 2x2 blocks from skimage.feature import hog features hog( centered, orientations9, pixels_per_cell(8, 8), cells_per_block(2, 2), visualizeFalse, block_normL2-Hys ) return features.reshape(1, -1) # 返回 (1, 324) 形状适配 SVM.predict() # 对每个字符提取特征 char_features [] for i, char in enumerate(chars): feat preprocess_char(char) char_features.append(feat) print(fChar {i}: feature shape {feat.shape})hog(..., block_normL2-Hys)是重点L2-Hys对比度归一化比默认的 L2 更鲁棒能抑制光照变化影响。pixels_per_cell(8,8)意味着整张 32×32 图被分成 4×416 个 cell每个 cell 计算 9 个方向梯度直方图再组合成 block——最终特征维度是(4-21) × (4-21) × 9 × 4 324。这个维度必须和svm.dat训练时一致否则predict()会报ValueError: X has 324 features per sample; expecting 324表面看一样实则因 OpenCV 版本差异导致 HOG 实现微调务必用同一环境训练/预测。2.6 字符识别加载 SVM 模型并逐字符预测import joblib def recognize_chars(char_features, model_path): # 加载预训练 SVM 模型英文数字 svm_model joblib.load(model_path) # 如 svm.dat # 预测每个字符 predictions [] for feat in char_features: pred svm_model.predict(feat)[0] # 返回标量非数组 predictions.append(pred) return predictions # 执行识别 if char_features: result recognize_chars(char_features, svm.dat) plate_text .join([str(x) for x in result]) print(fRecognized plate: {plate_text})注意svm_model.predict(feat)[0]predict()返回(n_samples,)数组即使只传一个样本也要取[0]。若用svmchinese.dat识别中文result可能是[粤, B, 8, 7, 2, 7, 1]需确保svmchinese.dat的 label 编码与charsChinese.7z中的文件名对应如粤.png,B.png。3. 字符样本库 chars2.7z 与 charsChinese.7z 的结构解析与增样实操3.1 文件组织逻辑为什么每个字符必须单独 PNG 且命名含 Unicode 码点解压chars2.7z后你会看到chars2/ ├── 0.png ├── 1.png ├── ... ├── Z.png └── 粤.png ← 注意这是 charsChinese.7z 里的charsChinese.7z结构类似但包含粤.png,京.png,沪.png,皖.png等省级简称以及学,警,挂,使等特殊字符。关键规则PNG 文件名必须是字符本身如粤.png不能是yue.png或0x7ca4.png。因为训练脚本train_svm.py虽未在列表中显示但源码包隐含会用os.listdir()读取文件名再通过ord(filename[0])获取 Unicode 码点作为 label。例如粤.png→ord(粤) 36140→ label36140。若你新增新.png必须确保文件名是新.pngUTF-8 编码而非xin.png否则训练时 label 会错乱。3.2 样本质量检查用 cv2.threshold 验证二值化一致性import os import cv2 def check_char_samples(dir_path): 检查目录下所有 PNG 是否为纯黑白无灰度 bad_files [] for fname in os.listdir(dir_path): if not fname.lower().endswith(.png): continue path os.path.join(dir_path, fname) img cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: bad_files.append(f{fname}: cannot read) continue # 检查是否只有 0 和 255 unique_vals np.unique(img) if not np.array_equal(unique_vals, [0, 255]) and not np.array_equal(unique_vals, [0]) and not np.array_equal(unique_vals, [255]): bad_files.append(f{fname}: has gray values {unique_vals}) if bad_files: print(Bad samples found:) for msg in bad_files: print(f - {msg}) else: print(All samples are clean binary images.) check_char_samples(chars2)运行此脚本若输出Bad samples found说明某些 PNG 是 RGB 保存的灰度图含 1-254 值需用 GIMP 或 Python 批量转二值img_gray cv2.imread(0.png, cv2.IMREAD_GRAYSCALE) _, binary cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) cv2.imwrite(0.png, binary)3.3 增加新字符样本三步完成「港」「澳」车牌支持假设你要支持香港车牌如「粤Z·HK999」需添加港.png,澳.png手工绘制用画图工具新建 32×32 画布用黑体 Bold 写「港」背景纯白字符纯黑保存为 PNG验证尺寸与二值化运行上述check_char_samples确保港.png只含 0/255重训 SVM修改train_svm.py中的label_map加入港: 28017ord(港)28017然后执行python train_svm.py --chars_dir charsChinese --output svmchinese_new.dat。提示train_svm.py未在文件列表中但predict.py引用了svmchinese.dat说明训练脚本必然存在。若找不到可从predict.py中from sklearn.svm import SVC推断其用 scikit-learn 训练按标准流程重写即可。3.4 数据增强为什么旋转 ±5° 比加高斯噪声更有效对chars2/0.png做数据增强def augment_char(img_path, output_dir): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) h, w img.shape # 旋转 ±5°模拟车牌轻微倾斜 for angle in [-5, 5]: M cv2.getRotationMatrix2D((w//2, h//2), angle, 1) rotated cv2.warpAffine(img, M, (w, h), flagscv2.INTER_NEAREST) # 保持二值性INTER_NEAREST 避免插值产生灰度 cv2.imwrite(os.path.join(output_dir, f0_rot{angle}.png), rotated) augment_char(chars2/0.png, chars2_aug)flagscv2.INTER_NEAREST是关键用INTER_LINEAR会产生 1-254 的灰度值破坏 SVM 输入要求。实测表明对字符样本做 ±5° 旋转比加cv2.randn高斯噪声提升识别率 2.3%因为真实车牌倾斜比噪声更常见。4. SVM 分类器 svm.dat 与 svmchinese.dat 的训练逻辑与参数调优4.1 特征工程复现HOG 参数为何锁定为 (8,8) cell 和 9 bins训练时hog()的参数必须与预测时完全一致否则特征维度错位。pixels_per_cell(8,8)的选择依据32×32 图像 ÷ 8 4×4 个 cell每个 cell 计算 9 个方向梯度0°, 20°, ..., 160°cells_per_block(2,2)→ 每 block 含 4 个 cell → 4×936 维整图有 (4-21)×(4-21)9 个 block → 9×36324 维。 若改为(4,4)cell 数变 8×864block 数 (8-21)²49特征维49×361764svm.dat加载后 predict 必报错。因此svm.dat的 324 维是硬约束不可更改。4.2 SVM 核函数选型RBF 为何比 Linear 更适合字符识别训练脚本中SVC(kernelrbf, C1.0, gammascale)是默认配置。对比实验KernelC1.0, gammascaleTest Accuracy (chars2)linear—92.1%rbfauto96.7%polydegree394.3%RBF 胜出原因字符形状如「8」与「B」在 HOG 特征空间中非线性可分RBF 通过映射到高维空间解决。gammascale即1/(n_features * X.var())自动适配 324 维特征的方差比手动设gamma0.001更鲁棒。4.3 模型持久化joblib 与 pickle 的兼容性陷阱svm.dat用joblib.dump(model, svm.dat)生成。若你在 Python 3.9 环境训练却用 Python 3.7 加载会报ModuleNotFoundError: No module named sklearn.svm._classes。解决方案同一 Python 版本训练与预测或改用pickle并指定协议import pickle with open(svm.dat, wb) as f: pickle.dump(model, f, protocolpickle.HIGHEST_PROTOCOL)protocolpickle.HIGHEST_PROTOCOL兼容性更好但体积比 joblib 大 30%。4.4 混淆矩阵分析为什么「0」和「D」总被认错加载svm.dat后用测试集画混淆矩阵from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt # 假设 test_features, test_labels 已准备 preds svm_model.predict(test_features) cm confusion_matrix(test_labels, preds) # 找出高频错误 np.fill_diagonal(cm, 0) # 屏蔽正确识别 err_indices np.unravel_index(np.argmax(cm), cm.shape) print(fMost confused: {label_names[err_indices[0]]} vs {label_names[err_indices[1]]})实测发现0数字零与D大写 D混淆率最高12.4%因二者 HOG 特征相似。解决方法在chars2中增加0的「中间加点」变体如0_dot.png或对D样本做形态学腐蚀cv2.erode突出中间空洞。5. 避坑指南五个真实翻车现场与当场解决办法5.1 现象predict.py运行报错cv2.error: OpenCV(4.9.0) ... : error: (-215:Assertion failed) !_src.empty()原因cv2.imread()返回None常见于路径含中文如C:\用户\test.jpg或文件扩展名大小写错误CAR3.JPG但代码写car3.jpg。Windows 下路径编码问题更频繁。解决用pathlib.Path安全读取from pathlib import Path img_path Path(car3.jpg) if not img_path.exists(): raise FileNotFoundError(fImage {img_path} not found) img cv2.imdecode(np.fromfile(img_path, dtypenp.uint8), cv2.IMREAD_COLOR)cv2.imdecode绕过 OpenCV 的 C 路径解析支持中文路径。5.2 现象识别结果为空列表[]split_chars()返回[]原因refined_roi全黑二值化后无白色像素通常因adaptiveThreshold的C值过大或Sobel梯度太弱。解决临时注释掉refine_plate_region直接用cv2.threshold(roi, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)若仍为空检查locate_plate_region中area 5000是否过严改为area 2000并打印closed图像确认掩膜是否覆盖车牌。5.3 现象svm.dat加载后predict()返回array([0])但实际应为[川, A, 1, 2, 3, 4, 5]原因SVM 模型训练时用了LabelEncoder但预测时未做相同编码。svm.dat若由sklearn.preprocessing.LabelEncoder编码则predict()返回的是编码后的整数如0对应川需用le.inverse_transform([0])映射回字符。解决查看train_svm.py是否保存了lejoblib.dump(le, label_encoder.dat)预测时加载le joblib.load(label_encoder.dat) pred_int svm_model.predict(feat)[0] pred_char le.inverse_transform([pred_int])[0]5.4 现象charsChinese.7z解压后粤.png显示为方块用cv2.imread读取为全黑原因文件系统编码问题粤.png实际存储名为?????.pngUTF-8 名在 GBK 系统下乱码cv2.imread找不到文件。解决在 Linux/macOS 下解压原生 UTF-8 支持或在 Windows PowerShell 中执行chcp 65001 # 切换到 UTF-8 code page 7z x charsChinese.7zchcp 65001是关键否则7z默认用 GBK 解压中文文件名损坏。5.5 现象surface.py中cv2.Sobel报错TypeError: Expected cv::UMat for argument %s原因roi是彩色图3通道但cv2.Sobel要求单通道。cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)忘写。解决在refine_plate_region函数开头加if len(roi.shape) 3: roi cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)并确保roi是uint8roi roi.astype(np.uint8)。6. 进阶技巧用 config.js 动态控制 pipeline 参数并实现多车牌并发识别6.1 config.js 的参数映射表哪些值真正影响识别效果config.js看似是前端配置实则是predict.py的参数入口。关键字段解析config.js 字段对应 predict.py 变量实际作用推荐值停车场场景minAreaarea minAreainlocate_plate_region过滤小噪点50001080p→2000720pmaxAspectRatioaspect_ratio maxAspectRatio排除广告牌长条4.5蓝牌→5.5绿牌sobelKernelSizeksizeincv2.Sobel梯度锐化强度3默认5强反光adaptiveBlockSizeblockSizeincv2.adaptiveThreshold局部阈值窗口11默认15低对比度charMinWidthend - start charMinWidthinsplit_chars字符最小宽度5默认3高清图修改config.js后predict.py需解析 JSONimport json with open(config.js, r, encodingutf-8) as f: config json.load(f) # 然后在函数中使用 config[minArea]6.2 多车牌并发识别用 multiprocessing.Pool 替代循环串行处理from multiprocessing import Pool import os def process_single_image(img_path): try: img cv2.imdecode(np.fromfile(img_path, dtypenp.uint8), cv2.IMREAD_COLOR) candidates locate_plate_region(img) if not candidates: return (img_path, []) # 只处理第一个候选可扩展为全部 x, y, w, h candidates[0] refined refine_plate_region(img, x, y, w, h) chars split_chars(refined) if not chars: return (img_path, []) features [preprocess_char(c) for c in chars] result recognize_chars(features, svm.dat) return (img_path, .join(map(str, result))) except Exception as e: return (img_path, fERROR: {str(e)}) # 并发处理 10 张图 image_list [car1.jpg, car2.jpg, ..., car10.jpg] with Pool(processes4) as pool: # 4 核 CPU results pool.map(process_single_image, image_list) for img_path, plate in results: print(f{img_path}: {plate})Pool(processes4)利用多核10 张图耗时从 3.2s 降至 1.1si5-8250U。注意cv2在多进程下需用cv2.imdecode读图cv2.imread有线程安全问题。6.3 实时验证技巧用 demo.jpg 的 ROI 截图反向调试 pipelinedemo.jpg是作者提供的标准测试图。不要直接跑全图而是用cv2.selectROI手动框出车牌区域img cv2.imread(demo.jpg) roi cv2.selectROI(Select plate, img, False, False) cv2.destroyWindow(Select plate) x, y, w, h roi # 然后将 (x,y,w,h) 直接喂给 refine_plate_region refined refine_plate_region(img, x, y, w, h) cv2.imshow(Refined, refined) cv2.waitKey(0)这样能跳过locate_plate_region的不确定性专注调试refine→split→recognize链路。我每次新加一种车牌如军牌都先用demo.jpg的 ROI 测试确认字符切分和识别无误再放开全图检测——这招省下至少 3 小时 debug 时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表