系统整体架构与技术选型
1.1 分层架构设计
系统采用两层解耦架构,便于算法迭代与界面扩展:
-
算法核心层:独立封装图像处理与识别逻辑,与界面完全解耦,包含预处理、定位、矫正、分割、分类五个子模块
-
界面交互层:基于PyQt5实现图形化客户端,负责图像加载、结果可视化、操作控制,通过接口调用算法层能力
1.2 核心技术栈
| 模块 | 技术选型 | 说明 |
|---|---|---|
| 开发语言 | Python 3.10 | 兼顾开发效率与库生态 |
| 图像处理 | OpenCV 4.x | 提供完整的图像运算与特征提取能力 |
| 分类算法 | SVM(支持向量机) | 小样本下分类效果稳定,训练成本低 |
| 图形界面 | PyQt5 | 组件丰富,跨平台兼容性好 |
| 开发环境 | PyCharm | 集成调试与代码管理能力 |
1.3 完整识别流水线
系统按固定流水线处理输入图像,各环节可独立调参优化:
图像尺寸归一化 → 灰度化与高斯去噪 → 边缘提取与形态学处理 → 轮廓候选区筛选 → 倾斜角度矫正 → HSV颜色校验 → 字符分割与归一化 → HOG特征提取 → SVM分类识别 → 结果输出
二、车牌定位算法设计与实现
车牌定位是系统准确率的第一道关口,单一方法易受光照、背景干扰,本方案采用「边缘特征筛选 + 颜色空间校验」的双重定位策略,提升复杂场景下的召回率。
2.1 图像预处理
预处理的目标是抑制噪声、突出车牌区域特征,为后续边缘检测做准备。
-
尺寸归一化:统一缩放图像分辨率,降低运算量,保证后续轮廓筛选阈值的通用性
-
高斯模糊去噪:使用5×5高斯核平滑图像,抑制拍摄噪声,避免伪边缘干扰
-
顶帽变换增强:通过开运算提取图像亮区细节,抵消光照不均影响,突出车牌字符区域
|-----------------------------------------------------------------|
| import cv2 |
| import numpy as np |
| |
| def image_preprocess(img, blur_ksize=5): |
| # 高斯模糊去噪 |
| if blur_ksize > 0: |
| img_blur = cv2.GaussianBlur(img, (blur_ksize, blur_ksize), 0) |
| else: |
| img_blur = img.copy() |
| |
| # 灰度化 |
| gray = cv2.cvtColor(img_blur, cv2.COLOR_BGR2GRAY) |
| |
| # 顶帽变换:增强亮区域,抵消光照不均 |
| kernel = np.ones((20, 20), np.uint8) |
| img_tophat = cv2.morphologyEx(gray, cv2.MORPH_OPEN, kernel) |
| img_enhance = cv2.addWeighted(gray, 1, img_tophat, -1, 0) |
| |
| return gray, img_enhance |
2.2 边缘检测与候选区生成
采用Canny算子提取边缘,配合形态学闭运算连接离散边缘,形成候选连通域。
-
阈值采用Otsu自适应二值化,适配不同光照场景
-
闭运算使用横向长核,强化车牌的水平矩形特征
-
开运算去除细小噪点,过滤无效边缘
|---------------------------------------------------------------------------------------|
| def edge_extract(img_enhance): |
| # 自适应二值化 |
| _, binary = cv2.threshold(img_enhance, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) |
| |
| # Canny边缘检测 |
| edges = cv2.Canny(binary, 100, 200) |
| |
| # 形态学处理:闭运算连接边缘,开运算去噪 |
| kernel_close = np.ones((4, 19), np.uint8) |
| edges_close = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel_close) |
| edges_open = cv2.morphologyEx(edges_close, cv2.MORPH_OPEN, kernel_close) |
| |
| return edges_open |
2.3 轮廓筛选与倾斜矫正
通过轮廓特征筛选候选车牌,再通过仿射变换完成倾斜矫正:
-
面积筛选:过滤面积过小的无效轮廓
-
长宽比筛选:车牌标准长宽比约为3:1,设置2.0~5.5的容差区间
-
倾斜矫正:通过最小外接矩形获取倾斜角度,使用仿射变换将车牌校正为水平状态
|---------------------------------------------------------------------------------|
| def locate_plate_candidates(edges, min_area=1000): |
| contours, _ = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) |
| candidates = [] |
| |
| for cnt in contours: |
| area = cv2.contourArea(cnt) |
| if area < min_area: |
| continue |
| |
| # 获取最小外接矩形 |
| rect = cv2.minAreaRect(cnt) |
| (cx, cy), (w, h), angle = rect |
| |
| # 统一宽高方向 |
| if w < h: |
| w, h = h, w |
| angle += 90 |
| |
| # 长宽比筛选 |
| ratio = w / h |
| if 2.0 < ratio < 5.5: |
| candidates.append(rect) |
| |
| return candidates |
2.4 HSV颜色校验
将候选区转换到HSV颜色空间,通过色调、饱和度统计判断车牌颜色,进一步排除非车牌区域,同时为后续字符分割提供颜色反转依据。
-
蓝牌:H通道 100~124
-
绿牌:H通道 35~99
-
黄牌:H通道 11~34
三、字符分割与特征提取
3.1 字符分割策略
字符分割采用「水平投影裁剪 + 垂直投影分割」的方案,同时处理常见干扰:
-
水平投影:统计每行像素和,裁剪掉上下边缘空白区域,锁定字符行范围
-
垂直投影:统计每列像素和,通过波峰波谷分割单个字符
-
干扰处理:
-
去除车牌边框与铆钉干扰
-
跳过省份简称与字母间的分隔点
-
处理汉字宽度大于字母数字的特性,单独适配第一个字符分割阈值
-
3.2 字符归一化
分割后的字符尺寸不一,需统一缩放到固定尺寸(如20×20),再提取特征。为避免缩放变形,采用等比例缩放+边界填充的方式,保证字符形态不变。
3.3 HOG特征提取
采用方向梯度直方图(HOG)作为字符特征,相比原始像素特征具备更强的鲁棒性。
-
将字符图像划分为4个cell
-
每个cell计算16维梯度方向直方图
-
采用Hellinger核做归一化,提升分类器效果
|----------------------------------------------------------------------|
| def extract_hog_features(char_imgs): |
| features = [] |
| for img in char_imgs: |
| # 计算x、y方向梯度 |
| gx = cv2.Sobel(img, cv2.CV_32F, 1, 0) |
| gy = cv2.Sobel(img, cv2.CV_32F, 0, 1) |
| mag, ang = cv2.cartToPolar(gx, gy) |
| |
| bin_n = 16 |
| bin = np.int32(bin_n * ang / (2 * np.pi)) |
| |
| # 分4块统计直方图 |
| bin_cells = bin[:10,:10], bin[10:,:10], bin[:10,10:], bin[10:,10:] |
| mag_cells = mag[:10,:10], mag[10:,:10], mag[:10,10:], mag[10:,10:] |
| |
| hists = [] |
| for b, m in zip(bin_cells, mag_cells): |
| hists.append(np.bincount(b.ravel(), m.ravel(), bin_n)) |
| hist = np.hstack(hists) |
| |
| # Hellinger归一化 |
| eps = 1e-7 |
| hist /= hist.sum() + eps |
| hist = np.sqrt(hist) |
| hist /= np.linalg.norm(hist) + eps |
| |
| features.append(hist) |
| return np.float32(features) |