OpenCV(三)—— 车牌筛选

news2024/11/20 20:31:34

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

无论是通过 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支持向量机

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1641756.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

爬虫自动化之drissionpage实现随时切换代理ip

目录 一、视频二、dp首次启动设置代理三、dp利用插件随时切换代理一、视频 视频直接点击学习SwitchyOmega插件使用其它二、dp首次启动设置代理 from DrissionPage import ChromiumPage, ChromiumOptions from loguru

MySQL与金蝶云星空对接集成执行查询语句-v2打通销售退货新增V1

MySQL与金蝶云星空对接集成执行查询语句-v2打通销售退货新增V1 数据源系统:MySQL mysql是一个关系数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;所谓的关系型数据库&#xff0c;是建立在关系模型基础上的数据库&#xff0c;借助于集合代数等数学概念和方法来处理数…

深度解析 Spring 源码:探寻Bean的生命周期

文章目录 一、 Bean生命周期概述二、Bean生命周期流程图三、Bean生命周期验证3.1 代码案例3.2 执行结果 四、Bean生命周期源码4.1 setBeanName()4.2 setBeanFactory()4.3 setApplicationContext()4.4 postProcessBeforeInitialization()4.5 afterPropertiesSet()4.6 postProces…

npm安装依赖报错解决办法

项目场景&#xff1a; 安装gitee上的开源vue3TS项目&#xff0c;npm安装依赖报错 问题描述 克隆项目到VSCode之后&#xff0c;使用npm install安装依赖&#xff0c;报错 npm install npm ERR! code ENOENT npm ERR! syscall open npm ERR! path Y:\WebStudyBlog\MovieWeb/p…

【Linux】学习笔记

文章目录 [toc]第一章&#xff1a;基础篇01|课程介绍02|内容综述03|什么是Linux04|Linux的内核版本及常见发行版内核版本发行版本Red Hat Enterprise LinuxFedoraCentOSDebianUbuntu 05|安装VirtualBox虚拟机VirtualBox下载url 06|在虚拟机中安装Linux系统Linux安装镜像下载 07…

基于OpenCv的图像傅里叶变换

⚠申明&#xff1a; 未经许可&#xff0c;禁止以任何形式转载&#xff0c;若要引用&#xff0c;请标注链接地址。 全文共计3077字&#xff0c;阅读大概需要3分钟 &#x1f308;更多学习内容&#xff0c; 欢迎&#x1f44f;关注&#x1f440;【文末】我的个人微信公众号&#xf…

【C++并发编程】(四)条件变量

文章目录 &#xff08;四&#xff09;条件变量 &#xff08;四&#xff09;条件变量 条件变量&#xff08;Condition Variable&#xff09;用于线程间的同步&#xff0c;允许一个或多个线程在特定条件不满足时等待&#xff0c;并在条件满足时被其他线程唤醒。C标准库中提供了的…

D3CTF2024

文章目录 前言notewrite_flag_where【复现】D3BabyEscapePwnShell 前言 本次比赛笔者就做出两道简单题&#xff0c;但队里师傅太快了&#xff0c;所以也没我啥事。然后 WebPwn 那题命令行通了&#xff0c;但是浏览器不会调试&#xff0c;然后就简单记录一下。 note 只开了 N…

【热门话题】如何构建具有高度扩展性的系统

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 如何构建具有高度扩展性的系统引言一、理解扩展性1.1 扩展性的定义1.2 扩展性的…

3GPP官网下载协议步骤

1.打开官网 https://www.3gpp.org/ 2.点击 3.在界面选择要找的series&#xff0c;跳转到查找界面 以V2X通信协议为例&#xff0c;论文中通常会看到许多应用&#xff1a; [7] “Study on evaluation methodology of new Vehicle-to-Everything (V2X) use cases for LTE and NR…

2-手工sql注入(进阶篇) sqlilabs靶场1-4题

1. 阅读&#xff0c;学习本章前&#xff0c;可以先去看看基础篇&#xff1a;1-手工sql注入(基础篇)-CSDN博客 2. 本章通过对sqlilabs靶场的实战&#xff0c;关于sqlilabs靶场的搭建&#xff1a;Linux搭建靶场-CSDN博客 3. 本章会使用到sqlmap&#xff0c;关于sqlmap的命令&…

C 深入指针(2)

目录 1 野指针 1.1 成因 1.2 如何规避野指针 2 assert 断言 2.1 用法 2.2 assert 的优点 2.1 assert 的缺点 3 小注解 3.1 Debug 和 Release 1 野指针 【概念】&#xff1a; 野指针就是指针指向的位置是不可知的&#xff08;随机的、不正确的、没有明确限制的&#…

解决windows下无法安装Hyper-V

在windows中打开hyper-v的办法如下&#xff1a;&#xff08;但不适用无法安装hyper-v的windows系统如win10家庭版&#xff09; 通过「控制面板」是启用 Hyper-V 最直接的方法&#xff1a; 1 使用Windows R快捷键打开「运行」对话框&#xff0c;执行appwiz.cpl以打开「控制面…

关于win平台c语言引入开源库的问题与解决

许久不写博客&#xff0c;五一还在加班&#xff0c;就浅浅写一篇吧 最近除了做物联网平台 还对网关二次开发程序做了修改&#xff0c;网关的二次开发去年年底的时候做过&#xff0c;但是当时的逻辑不是十分完善&#xff0c;差不多已经过了半年了&#xff0c;很多细节已经忘记了…

Java--方法的使用

1.1什么是方法 方法顾名思义就是解决问题的办法&#xff0c;在程序员写代码的时候&#xff0c;会遇到很多逻辑结构一样&#xff0c;解决相同问题时&#xff0c;每次都写一样的代码&#xff0c;这会使代码看起来比较绒余&#xff0c;代码量也比较多&#xff0c;为了解决这个问题…

VBA 根据表格指定列拆分多sheet

一. 需求 ⏹ 根据部分列&#xff0c;拆分数据到多个sheet页 二. 代码 ⏹ 重点代码摘要 CreateObject("scripting.dictionary")&#xff1a;创建一个字典对象&#xff0c;相当于Java中的MapDim aRef() As String&#xff1a;定义一个存储字符串类型的数组ReDim aRe…

Linux学习之IP协议

前言&#xff1a; 在学习IP协议i前&#xff0c;我们其实知道网络协议栈是一层层的&#xff0c;上层封装好之后就传给下层&#xff0c;对于我们治安学习到的TCP协议&#xff0c;在对数据进行封装之后&#xff0c;并不是直接就将数据进行传输&#xff0c;而是交给下一层网络层进…

【讲解如何OpenCV入门】

&#x1f308;个人主页: 程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

GDPU unity游戏开发 碰撞器与触发器

砰砰叫&#xff0c;谁动了她的奶酪让你的小鹿乱撞了。基于此&#xff0c;亦即碰撞与触发的过程。 碰撞器与触发器的区别 通俗点讲&#xff0c;碰撞器检测碰撞&#xff0c;触发器检测触发&#xff0c;讲了跟没讲似的。碰撞器是用来检测碰撞事件的&#xff0c;在unity中&#xff…

发表博客之:transformer 架构 推理时候运算流程详细讲解,小白都可以看得懂,AI推理工程师必备技能!

文章目录 [发表博客之&#xff1a;transformer 架构 推理时候 详细讲解&#xff0c;小白都可以看得懂&#xff0c;AI推理工程师必备技能&#xff01;](https://cyj666.blog.csdn.net/article/details/138439826)总结一下 发表博客之&#xff1a;transformer 架构 推理时候 详细…