OpenCV 入门系列:
OpenCV 入门(一)—— OpenCV 基础
OpenCV 入门(二)—— 车牌定位
OpenCV 入门(三)—— 车牌筛选
OpenCV 入门(四)—— 车牌号识别
OpenCV 入门(五)—— 人脸识别模型训练与 Windows 下的人脸识别
OpenCV 入门(六)—— Android 下的人脸识别
OpenCV 入门(七)—— 身份证识别
本篇文章要介绍如何对从候选车牌中选出最终进行字符识别的车牌。
无论是通过 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支持向量机