OpenCV(三)—— 车牌筛选

本篇文章要介绍如何对从候选车牌中选出最终进行字符识别的车牌。

无论是通过 Sobel 还是 HSV 计算出的候选车牌都可能不止一个,需要对它们进行评分,选出最终要进行识别的车牌。这个过程中会用到两个理论知识:支持向量机和 HOG 特征。

1、支持向量机

1.1 SVM 简介

支持向量机(Support Vector Machine,SVM)是一类按监督学习(Supervised Learning)方式对数据进行二元分类的广义线性分类器。用通俗的话来讲,就是用来分类,或者说挑选东西的。

对于车牌识别而言,车牌定位的候选车牌图可以分为两类:车牌与非车牌。SVM 可以对候选图进行测评,告诉我们图中的是不是车牌,相似程度是多少。

当然,SVM 可以进行分类的前提还是我们使用正负样本对其进行了训练。SVM 的训练数据既有特征又有标签,通过训练,让机器可以自己找到特征和标签之间的联系,在面对只有特征没有标签的数据时,可以判断出标签,这属于机器学习中的监督学习。

1.2 核函数

SVM 中有一个重要概念就是核函数。它的目标是找到一个能够将数据点分为不同类别的最优超平面(或者在非线性情况下是最优超曲面)。对于线性可分的情况,存在一个超平面可以完全将两个类别的数据分开。但是,在某些情况下,数据可能无法通过一个线性超平面进行完全分离,这就是线性不可分的情况。

SVM 线性可分:样本数据使用二维的线就可分类:

svm线性可分

SVM 线性不可分:左侧图片中的数据样本无法在二维平面内用线划分,称为线性不可分,只能像右侧图片那样用一个平面分开:

svm线性不可分

为了处理线性不可分的数据,引入了核函数的概念。核函数能够将输入数据从原始的特征空间(通常是低维空间)映射到一个更高维的特征空间,使得在新的特征空间中数据线性可分。这意味着在原始特征空间中无法线性分割的数据,在映射到高维特征空间后可以通过一个超平面进行线性分割。通常我们将这个过程称为提维,分离超平面就是通过提围计算出来的。

核函数的作用是在不显式计算映射到高维特征空间的情况下,直接在低维特征空间中进行计算。这样可以避免高维空间的计算复杂性,并且通过核函数的巧妙选择,可以实现高维特征空间的效果。

常见的核函数包括线性核函数、多项式核函数和径向基函数(Radial Basis Function,RBF)核函数。线性核函数对应于线性可分的情况,而多项式核函数和 RBF 核函数则可以处理线性不可分的情况。

1.3 SVM 训练流程

SVM 训练流程如下图:

svm训练流程

步骤:

  1. 预处理(原始数据 -> 学习数据(无标签)):预处理步骤主要处理的是原始数据到学习数据的转换过程(真正的车牌图片和不是车牌的图片)
  2. 打标签(学习数据(无标签)-> 学习数据(带标签)):将未贴标签的数据转化为贴过标签的学习数据
  3. 分组(学习数据(带标签)-> 分组数据):将数据分为训练集和测试集
  4. 训练(训练数据 -> 模型):加载待训练的车牌数据和非车牌数据,合并数据,配置 SVM 模型的训练参数进行训练

2、HOG 特征

HOG(Histogram of Oriented Gradient)特征是局部归一化的梯度方向直方图,是一种对图像局部重叠区域的密集型描述符,是用于目标检测和图像识别的特征描述方法,它通过计算局部区域的梯度方向直方图来构成特征。它在计算机视觉领域中广泛应用,特别是在行人检测等任务中取得了很好的效果。

HOG 特征的计算步骤如下:

  1. 图像预处理:将输入图像转换为灰度图像,去除颜色信息,以减少计算量。

  2. 梯度计算:计算图像中每个像素点的梯度信息。使用一阶导数(如 Sobel 算子)来计算水平和垂直方向上的梯度值,然后计算每个像素点的梯度幅值和梯度方向。

  3. 单元划分:将图像划分为小的连续区域,称为单元。通常使用 3 × 3 或 4 × 4 像素的单元。

  4. 梯度直方图统计:在每个单元中,对每个像素点的梯度方向进行统计。将梯度方向范围分成若干个区间(通常是 9 个),然后统计每个区间内的梯度幅值的累加和。这样就得到了一个梯度直方图。

  5. 块归一化:将相邻的若干个单元组成一个块,对每个块内的梯度直方图进行归一化处理。归一化可以降低光照变化对特征的影响,并增强特征的鲁棒性。

  6. 特征向量拼接:将所有块内的归一化梯度直方图按顺序拼接起来,形成最终的 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 个候选车牌,其中最后一个评分最低,是最符合标准的车牌:

2024-4-4.SVM评分选出最终图片

参考资料:

学习Opencv2.4.9(四)—SVM支持向量机

相关推荐

  1. c# opencv 识别车牌

    2024-05-04 18:32:03       62 阅读
  2. python opencv实现车牌识别

    2024-05-04 18:32:03       51 阅读
  3. OpenCV车牌识别技术详解

    2024-05-04 18:32:03       21 阅读
  4. Python+Opencv是实现车牌自动识别

    2024-05-04 18:32:03       28 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-05-04 18:32:03       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-05-04 18:32:03       101 阅读
  3. 在Django里面运行非项目文件

    2024-05-04 18:32:03       82 阅读
  4. Python语言-面向对象

    2024-05-04 18:32:03       91 阅读

热门阅读

  1. 分割等和子集

    2024-05-04 18:32:03       33 阅读
  2. Vue3 + TS + Element-Plus 封装的 Table 表格组件

    2024-05-04 18:32:03       23 阅读
  3. python调用微信自带OCR实现内容识别(全)

    2024-05-04 18:32:03       29 阅读
  4. 2024.4.28力扣每日一题——负二进制转换

    2024-05-04 18:32:03       30 阅读
  5. OneFlow深度学习框架入门与实践

    2024-05-04 18:32:03       36 阅读
  6. 云计算技术概述_3.云计算的部署方式

    2024-05-04 18:32:03       28 阅读
  7. Apache RocketMQ知识点表格总结及示例

    2024-05-04 18:32:03       28 阅读
  8. [UUCTF 2022 新生赛]ezsql

    2024-05-04 18:32:03       30 阅读
  9. C#装箱拆箱是怎么回事

    2024-05-04 18:32:03       31 阅读