本篇文章要介绍如何对从候选车牌中选出最终进行字符识别的车牌。
无论是通过 Sobel 还是 HSV 计算出的候选车牌都可能不止一个,需要对它们进行评分,选出最终要进行识别的车牌。这个过程中会用到两个理论知识:支持向量机和 HOG 特征。
1、支持向量机
1.1 SVM 简介
支持向量机(Support Vector Machine,SVM)是一类按监督学习(Supervised Learning)方式对数据进行二元分类的广义线性分类器。用通俗的话来讲,就是用来分类,或者说挑选东西的。
对于车牌识别而言,车牌定位的候选车牌图可以分为两类:车牌与非车牌。SVM 可以对候选图进行测评,告诉我们图中的是不是车牌,相似程度是多少。
当然,SVM 可以进行分类的前提还是我们使用正负样本对其进行了训练。SVM 的训练数据既有特征又有标签,通过训练,让机器可以自己找到特征和标签之间的联系,在面对只有特征没有标签的数据时,可以判断出标签,这属于机器学习中的监督学习。
1.2 核函数
SVM 中有一个重要概念就是核函数。它的目标是找到一个能够将数据点分为不同类别的最优超平面(或者在非线性情况下是最优超曲面)。对于线性可分的情况,存在一个超平面可以完全将两个类别的数据分开。但是,在某些情况下,数据可能无法通过一个线性超平面进行完全分离,这就是线性不可分的情况。
SVM 线性可分:样本数据使用二维的线就可分类:
SVM 线性不可分:左侧图片中的数据样本无法在二维平面内用线划分,称为线性不可分,只能像右侧图片那样用一个平面分开:
为了处理线性不可分的数据,引入了核函数的概念。核函数能够将输入数据从原始的特征空间(通常是低维空间)映射到一个更高维的特征空间,使得在新的特征空间中数据线性可分。这意味着在原始特征空间中无法线性分割的数据,在映射到高维特征空间后可以通过一个超平面进行线性分割。通常我们将这个过程称为提维,分离超平面就是通过提围计算出来的。
核函数的作用是在不显式计算映射到高维特征空间的情况下,直接在低维特征空间中进行计算。这样可以避免高维空间的计算复杂性,并且通过核函数的巧妙选择,可以实现高维特征空间的效果。
常见的核函数包括线性核函数、多项式核函数和径向基函数(Radial Basis Function,RBF)核函数。线性核函数对应于线性可分的情况,而多项式核函数和 RBF 核函数则可以处理线性不可分的情况。
1.3 SVM 训练流程
SVM 训练流程如下图:
步骤:
- 预处理(原始数据 -> 学习数据(无标签)):预处理步骤主要处理的是原始数据到学习数据的转换过程(真正的车牌图片和不是车牌的图片)
- 打标签(学习数据(无标签)-> 学习数据(带标签)):将未贴标签的数据转化为贴过标签的学习数据
- 分组(学习数据(带标签)-> 分组数据):将数据分为训练集和测试集
- 训练(训练数据 -> 模型):加载待训练的车牌数据和非车牌数据,合并数据,配置 SVM 模型的训练参数进行训练
2、HOG 特征
HOG(Histogram of Oriented Gradient)特征是局部归一化的梯度方向直方图,是一种对图像局部重叠区域的密集型描述符,是用于目标检测和图像识别的特征描述方法,它通过计算局部区域的梯度方向直方图来构成特征。它在计算机视觉领域中广泛应用,特别是在行人检测等任务中取得了很好的效果。
HOG 特征的计算步骤如下:
-
图像预处理:将输入图像转换为灰度图像,去除颜色信息,以减少计算量。
-
梯度计算:计算图像中每个像素点的梯度信息。使用一阶导数(如 Sobel 算子)来计算水平和垂直方向上的梯度值,然后计算每个像素点的梯度幅值和梯度方向。
-
单元划分:将图像划分为小的连续区域,称为单元。通常使用 3 × 3 或 4 × 4 像素的单元。
-
梯度直方图统计:在每个单元中,对每个像素点的梯度方向进行统计。将梯度方向范围分成若干个区间(通常是 9 个),然后统计每个区间内的梯度幅值的累加和。这样就得到了一个梯度直方图。
-
块归一化:将相邻的若干个单元组成一个块,对每个块内的梯度直方图进行归一化处理。归一化可以降低光照变化对特征的影响,并增强特征的鲁棒性。
-
特征向量拼接:将所有块内的归一化梯度直方图按顺序拼接起来,形成最终的 HOG 特征向量。
HOG 特征的优点是能够捕捉图像中物体的边缘和纹理等局部特征,并且对光照变化相对鲁棒。它在行人检测等任务中被广泛使用,通常与支持向量机(SVM)等分类器结合使用,用于目标检测和图像识别。
3、代码实现
评分肯定是先通过正负样本学习,训练出一个特征集合,我们需要先加载这个 xml 文件:
int main() {
// 加载车牌图片
Mat src = imread("C:/Users/UserName/Desktop/Test/test5.jpg");
// 新增加载特征集合
LicensePlateRecognizer lpr("C:/Users/UserName/Desktop/Test/svm.xml");
// 识别
string str_plate = lpr.recognize(src);
cout << "车牌号码:" << str_plate << endl;
return 0;
}
在 LicensePlateRecognizer 进行识别时,需要调用评分的函数 predict():
/**
* 车牌识别 = 车牌定位 + 车牌检测 + 字符识别
*/
string LicensePlateRecognizer::recognize(Mat src)
{
// 传入原图的克隆版本,以防在原图上的绘制影响后续算法定位
Mat src_clone = src.clone();
// 1.车牌定位,使用 Sobel 算法定位
vector<Mat> sobel_plates;
sobelLocator->locate(src_clone, sobel_plates);
// 使用 HSV 算法定位
src_clone = src.clone();
vector<Mat> color_plates;
colorLocator->locate(src_clone, color_plates);
// 将两种车牌合并到一个集合中
vector<Mat> plates;
plates.insert(plates.end(), sobel_plates.begin(), sobel_plates.end());
plates.insert(plates.end(), color_plates.begin(), color_plates.end());
// 释放 sobel_plates 和 color_plates 内的 Mat
for each (Mat m in sobel_plates)
{
m.release();
}
for each (Mat m in color_plates)
{
m.release();
}
// 2.精选车牌定位得到的候选车牌图
char windowName[100];
for (int i = 0; i < plates.size(); i++)
{
sprintf(windowName, "%zd 候选车牌", i);
imshow(windowName, plates[i]);
waitKey();
}
// 评分,将最接近车牌的图片保存到 plate 中,其索引保存在 index 中
Mat plate;
int index = svmPredictor->predict(plates, plate);
src_clone.release();
// 暂时还无法识别到车牌号,返回一个测试字符串
return string("12345");
}
svmPredictor 就是通过 SVM 进行车牌评分的类,它需要创建一个 SVM 对象,还需要创建一个 HOGDescriptor:
#ifndef SVMPREDICTOR_H
#define SVMPREDICTOR_H
#include <opencv2/opencv.hpp>
#include <string>
// 机器学习 Machine Learning
#include <opencv2/ml.hpp>
using namespace std;
using namespace cv;
using namespace ml;
class SvmPredictor {
public:
SvmPredictor(const char* svm_model);
~SvmPredictor();
virtual int predict(vector<Mat> candi_plates, Mat& dst_plates);
private:
// 支持向量机对象
Ptr<SVM> svm;
// HOG 特征对象
HOGDescriptor* svmHog = nullptr;
void getHOGFeatures(HOGDescriptor* svmHog, Mat src, Mat& dst);
};
#endif // !SVMPREDICTOR_H
我们需要了解 HOGDescriptor 的创建参数:
SvmPredictor::SvmPredictor(const char* svm_model)
{
svm = SVM::load(svm_model);
svmHog = new HOGDescriptor(Size(128, 64), Size(16, 16), Size(8, 8), Size(8, 8), 3);
}
SvmPredictor::~SvmPredictor()
{
if (svm)
{
svm->clear();
svm.release();
}
}
创建 HOGDescriptor 传了 4 个 Size 对象,它们的含义如下:
/** @overload
@param _winSize 使用给定的值设置窗口大小
@param _blockSize 使用给定的值设置块大小
@param _blockStride 使用给定的值设置滑动增量大小
@param _cellSize 使用给定的值设置胞元(CellSize)大小
@param _nbins 使用给定的值设置梯度方向
*/
CV_WRAP HOGDescriptor(Size _winSize, Size _blockSize, Size _blockStride,
Size _cellSize, int _nbins, int _derivAperture=1, double _winSigma=-1,
HOGDescriptor::HistogramNormType _histogramNormType=HOGDescriptor::L2Hys,
double _L2HysThreshold=0.2, bool _gammaCorrection=false,
int _nlevels=HOGDescriptor::DEFAULT_NLEVELS, bool _signedGradient=false)
窗口大小设置为 (128, 64) ,作用是扫描图片中指定大小区域的像素,示意图如下:
一个窗口可以分成若干块,比如我们在代码中指定了块大小为 (16, 16),那么一个 (128, 64) 的窗口就可以在横向放 4 个块,纵向放 8 个块:
块滑动增量指定一个块在横纵方向上滑动步长为 (8, 8),胞元大小也指定为 (8, 8),那么一个 (16, 16) 的块中就包含 4 个胞元。最后的梯度方向 _nbins 指定为 3,在一个胞元内统计 3 个方向的梯度直方图,每个方向为 180 / 3 = 60°(将水平 180° 进行三等分)。
上面这个检测窗口可以被分为 ((128 - 16) / 8 + 1) * ((64 - 16) / 8 + 1) = 105 个块,一个块有 4 个胞元(Cell),一个胞元的 Hog 描述子向量的长度是 9。设置参数时必须要保证两个乘数内部是可以整除的。
统计梯度直方图特征,就是将梯度方向(0 ~ 360)划分为 x 个区间,将图像化为若干个 16 × 16 的窗口,每个窗口又划分为 x 个 block,每个 block 再化为 4 个 Cell(8 × 8)。对每一个 Cell,算出每一像素点的梯度方向,按梯度方向增加对应 bin 的值,最终综合 N 个 Cell 的梯度直方图组成特征。
简单来说,车牌的边缘与内部文字组成的一组信息(在边缘和角点的梯度值是很大的,边缘和角点包含了很多物体的形状信息),HOG 就是抽取这些信息组成一个直方图。
HOG:梯度方向弱化光照的影响,适合捕获轮廓
LBP:中心像素的 LBP 值反映了该像素周围区域的纹理信息
predict() 参考代码:
int SvmPredictor::predict(vector<Mat> candi_plates, Mat& dst_plate)
{
Mat plate;
float score;
float minScore = FLT_MAX;
int minIndex = -1;
for (int i = 0; i < candi_plates.size(); i++)
{
plate = candi_plates[i];
// 准备获取车牌图片的 HOG 特征,先获取灰度图
Mat gray;
cvtColor(plate, gray, COLOR_BGR2GRAY);
// 二值化(非黑即白,对比更强烈)
Mat shold;
threshold(gray, shold, 0, 255, THRESH_OTSU + THRESH_BINARY);
// 获取特征
Mat feature;
getHOGFeatures(svmHog, shold, feature);
// 获取样本
Mat sample = feature.reshape(1, 1);
// 获取评分,评分越小越像目标
score = svm->predict(sample, noArray(), StatModel::Flags::RAW_OUTPUT);
printf("SVM候选车牌%d的评分是:%f\n", i, score);
// 记录最小分数的索引
if (score<minScore)
{
minScore = score;
minIndex = i;
}
// 释放
gray.release();
shold.release();
feature.release();
sample.release();
}
// 找到了目标图片就把该图片复制给结果参数 dst_plate
if (minIndex >= 0)
{
dst_plate = candi_plates[minIndex].clone();
imshow("SVM 评测最终车牌", dst_plate);
waitKey();
}
return minIndex;
}
获取特征其实就是通过 HOGDescriptor 计算出特征集合:
void SvmPredictor::getHOGFeatures(HOGDescriptor* svmHog, Mat src, Mat& dst)
{
// 归一化处理
Mat trainImg = Mat(svmHog->winSize, CV_32S);
resize(src, trainImg, svmHog->winSize);
// 计算特征
vector<float> desc;
svmHog->compute(trainImg, desc, svmHog->winSize);
// 特征图拷贝给结果 dst
Mat feature(desc);
feature.copyTo(dst);
// 释放
feature.release();
trainImg.release();
}
运行代码,可以看到有 4 个候选车牌,其中最后一个评分最低,是最符合标准的车牌:
参考资料:
学习Opencv2.4.9(四)—SVM支持向量机