OpenCV4学习笔记(45)——基于自定义特征向量和SVM线性分类器实现的数字识别

OpenCV4学习笔记(43)OpenCV4学习笔记(42)这两篇笔记中,分别整理记录了基于HOG特征检测和SVM线性分类器来实现的自定义对象检测以及行人检测。而今天所要整理的内容,依然是利用SVM线性分类器实现对印刷体数字的检测,只不过不再使用HOG特征,而是通过自定义特征向量来实现对印刷体数字特征提取。

自定义特征向量的提取思路如下:

1、通过将每一个数字图像进行网格分割,分割为4x5的20个区域cell,分别计算每个区域cell的背景像素数量,并进行归一化处理,目的是使得提取出来的特征能在数字图像放缩时也能有比较好的鲁棒性。这样就得到了20个浮点类型的特征值。
2、再将数字图像进行水平方向和垂直方向的像素数量投影,得到x、y方向的两个像素数量投影直方图,直方图分为10个区间bin。并对直方图的10个区间值进行统计并做归一化,得到10个特征值。由于有x、y两个方向,所以总共可以得到20个特征值。
进行投影统计操作如下图所示(画的不好,应该能看。。。):
在这里插入图片描述

3、将上述两个步骤得到的40个特征值组合起来,得到一个特征向量,这就是提取出来的自定义特征向量。

由于在数字图像分割的时候精确到了浮点数级别,所以能很好的提高特征向量的独特性和精确性。
并且通过归一化操作来使得自定义特征向量对于测试图像的尺寸放缩也具有一定的适应能力,提高了该特征提取算法的鲁棒性。

对测试图像提取特征向量后,可以通过计算该特征向量和已知所有数字图像的特征向量之间的距离来进行识别分类,与某一个分类的距离最小则该测试图像属于距离最小的这一个分类。

但通过距离度量的方式进行识别的话,每次识别都要计算0~9所有数字的特征向量,所以我们可以采用SVM线性分类器来训练一个模型,这样以后就只需要加载模型就可以进行数字识别了。

我们使用的训练图像是:
在这里插入图片描述

下面是训练SVM分类器时的演示代码,其中调用了一些通过头文件封装好了的函数:

	//生成0~9十个数字图像
	Mat digits_image1 = imread("D:\\opencv_c++\\Learning-OpenCV\\0~9印刷体数字识别\\0~9印刷体数字识别\\0~9印刷体数字识别\\image\\train\\train1.png");
	vector<Mat> ROI_digits1;
	vector<Rect>digits_box1;
	ROI_digits1 = GetTrainData(digits_image1, digits_box1);
	//获取0~9十个数字的特征向量
	vector<vector<float>>features;
	features= AllFeatures(ROI_digits1, features);

	//生成0~9十个数字图像
	Mat digits_image2 = imread("D:\\opencv_c++\\Learning-OpenCV\\0~9印刷体数字识别\\0~9印刷体数字识别\\0~9印刷体数字识别\\image\\train\\train2.png");
	vector<Mat> ROI_digits2;
	vector<Rect>digits_box2;
	ROI_digits2 = GetTrainData(digits_image2, digits_box2);
	//获取0~9十个数字的特征向量
	features = AllFeatures(ROI_digits2, features);

	//生成标签
	vector<int> labels;
	labels = GetTrainLabels();
	//训练SVM分类器
	TrainSVM(features, labels);

“AllFeatures.h” 封装好的函数:

vector<vector<float>> AllFeatures(vector<Mat>ROI_digits, vector<vector<float>>features)
{
	vector<float> digit_feature;
	for (int i = 0;i < ROI_digits.size();i++)
	{
		Mat digit = ROI_digits[i];
		//计算4x5网格内每个区域cell的前景像素数,并归一化,得到20个特征值
		float width = digit.cols;
		float height = digit.rows;
		//4x5网格分割
		float cell_w = float(width) / 4;
		float cell_h = float(height) / 5;
		vector<Mat>cells;
		for (float row = 0.0; row < height; row += cell_h)
		{
			for (float col = 0.0;col < width;col += cell_w)
			{
				int r = floor(row);
				int c = floor(col);
				Rect cell_box(c, r, floor(cell_w), floor(cell_h));
				Mat cell = digit(cell_box).clone();
				cells.push_back(cell);
			}
		}
		//计算每个cell中的背景像素数并做归一化
		for (int i = 0; i < cells.size();i++)
		{
			float pixelNum_cell = 0;
			for (int row = 0;row < cells[i].rows;row++)
			{
				for (int col = 0;col < cells[i].cols;col++)
				{
					uchar pixelValue_cell = cells[i].at<uchar>(row, col);
					if (0 == pixelValue_cell)
					{
						pixelNum_cell++;
					}
				}
			}
			pixelNum_cell = pixelNum_cell / float(cells[i].rows * cells[i].cols);
			digit_feature.push_back(pixelNum_cell);
		}

		//计算x方向的投影像素数,分为10个bin
		int bin = 10;
		float bin_y = height / bin;
		vector<Mat>bins_y;
		for (float row = 0.0; row < height; row += bin_y)
		{
			Rect bin_box(0, floor(row), width, floor(bin_y));
			Mat bin = digit(bin_box).clone();
			bins_y.push_back(bin);
			if (10 == bins_y.size())
			{
				break;
			}
		}
		for (int i = 0;i < bins_y.size();i++)
		{
			float pixelNum_bin = 0.0;
			for (int row = 0; row < bins_y[i].rows;row++)
			{
				for (int col = 0; col < bins_y[i].cols;col++)
				{
					uchar pixelVal_bin = bins_y[i].at<uchar>(row, col);
					if (0 == pixelVal_bin)
					{
						pixelNum_bin++;
					}
				}
			}
			pixelNum_bin = pixelNum_bin / float(bins_y[i].rows * bins_y[i].cols);
			digit_feature.push_back(pixelNum_bin);
		}

		//计算y方向的投影像素数,分为10个bin
		float bin_x = width / bin;
		vector<Mat>bins_x;
		for (float col = 0.0; col < width; col += bin_x)
		{
			if (bin_x < 1)
			{
				int new_bin_x = 1;
				int f_bin_x = new_bin_x - bin_x;
				Rect bin_box(floor(col), 0, new_bin_x, height);
				Mat bin = digit(bin_box).clone();
				bins_x.push_back(bin);
				if (9 == bins_x.size())
				{
					int flag = floor(f_bin_x * 10);
					bins_x.push_back(bins_x[flag]);
					break;
				}
			}
			else
			{
				Rect bin_box(floor(col), 0, floor(bin_x), height);
				Mat bin = digit(bin_box).clone();
				bins_x.push_back(bin);
				if (10 == bins_x.size())
				{
					break;
				}
			}
			
		}
		for (int i = 0;i < bins_x.size();i++)
		{
			float pixelNum_bin_y = 0.0;
			for (int row = 0; row < bins_x[i].rows;row++)
			{
				for (int col = 0; col < bins_x[i].cols;col++)
				{
					uchar pixelVal_bin = bins_x[i].at<uchar>(row, col);
					if (0 == pixelVal_bin)
					{
						pixelNum_bin_y++;
					}
				}
			}
			pixelNum_bin_y = pixelNum_bin_y / float(bins_x[i].rows * bins_x[i].cols);
			digit_feature.push_back(pixelNum_bin_y);
		}

		features.push_back(digit_feature);
		digit_feature.clear();
	}
	
	return features;
}


//计算某个数字的特征向量
vector<float> GetDigitFeature(Mat src)
{
	vector<float> digit_feature;
	Mat digit = src;
	//计算4x5网格内每个区域cell的前景像素数,并归一化,得到20个特征值
	float width = digit.cols;
	float height = digit.rows;
	//4x5网格分割
	float cell_w = float(width) / 4;
	float cell_h = float(height) / 5;
	vector<Mat>cells;
	for (float row = 0.0; row < height; row += cell_h)
	{
		for (float col = 0.0;col < width;col += cell_w)
		{
			int r = floor(row);
			int c = floor(col);
			Rect cell_box(c, r, floor(cell_w), floor(cell_h));
			Mat cell = digit(cell_box).clone();
			cells.push_back(cell);
		}
	}
	//计算每个cell中的背景像素数并做归一化
	for (int i = 0; i < cells.size();i++)
	{
		float pixelNum_cell = 0;
		for (int row = 0;row < cells[i].rows;row++)
		{
			for (int col = 0;col < cells[i].cols;col++)
			{
				uchar pixelValue_cell = cells[i].at<uchar>(row, col);
				if (0 == pixelValue_cell)
				{
					pixelNum_cell++;
				}
			}
		}
		pixelNum_cell = pixelNum_cell / float(cells[i].rows * cells[i].cols);
		digit_feature.push_back(pixelNum_cell);
	}

	//计算x方向的投影像素数,分为10个bin
	int bin = 10;
	float bin_y = height / bin;
	vector<Mat>bins_y;
	for (float row = 0.0; row < height; row += bin_y)
	{
		Rect bin_box(0, floor(row), width, floor(bin_y));
		Mat bin = digit(bin_box).clone();
		bins_y.push_back(bin);
		if (10 == bins_y.size())
		{
			break;
		}
	}
	for (int i = 0;i < bins_y.size();i++)
	{
		float pixelNum_bin = 0.0;
		for (int row = 0; row < bins_y[i].rows;row++)
		{
			for (int col = 0; col < bins_y[i].cols;col++)
			{
				uchar pixelVal_bin = bins_y[i].at<uchar>(row, col);
				if (0 == pixelVal_bin)
				{
					pixelNum_bin++;
				}
			}
		}
		pixelNum_bin = pixelNum_bin / float(bins_y[i].rows * bins_y[i].cols);
		digit_feature.push_back(pixelNum_bin);
	}

	//计算y方向的投影像素数,分为10个bin
	float bin_x = width / bin;
	vector<Mat>bins_x;
	for (float col = 0.0; col < width; col += bin_x)
	{
		Rect bin_box(floor(col), 0, floor(bin_x), height);
		Mat bin = digit(bin_box).clone();
		bins_x.push_back(bin);
		if (10 == bins_x.size())
		{
			break;
		}
	}
	for (int i = 0;i < bins_x.size();i++)
	{
		float pixelNum_bin_y = 0.0;
		for (int row = 0; row < bins_x[i].rows;row++)
		{
			for (int col = 0; col < bins_x[i].cols;col++)
			{
				uchar pixelVal_bin = bins_x[i].at<uchar>(row, col);
				if (0 == pixelVal_bin)
				{
					pixelNum_bin_y++;
				}
			}
		}
		pixelNum_bin_y = pixelNum_bin_y / float(bins_x[i].rows * bins_x[i].cols);
		digit_feature.push_back(pixelNum_bin_y);
	}

	return digit_feature;
}

“GetTrainData.h” 封装好的函数:

//生成图像数据
vector<Mat> GetTrainData(Mat src, vector<Rect>&box_digits)
{
	Mat gray_digits_image, binary_digits_image;
	cvtColor(src, gray_digits_image, COLOR_BGR2GRAY);
	threshold(gray_digits_image, binary_digits_image, 0, 0255, THRESH_BINARY_INV | THRESH_OTSU);

	vector<vector<Point>>contours;
	findContours(binary_digits_image, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE, Point());
	for (int i = 0; i < contours.size();i++)
	{
		Rect box_digit = boundingRect(contours[i]);
		box_digits.push_back(box_digit);
		rectangle(src, box_digit, Scalar(0, 255, 0), 1, 8);
		//imshow("dd", src);
	}
	for (int i = 0;i < box_digits.size() - 1;i++)
	{
		for (int j = i + 1;j < box_digits.size(); j++)
		{
			if (box_digits[i].x > box_digits[j].x)
			{
				Rect temp = box_digits[i];
				box_digits[i] = box_digits[j];
				box_digits[j] = temp;
			}
		}
	}

	//剪切十个数字区域
	vector<Mat>ROI_digits;
	for (int i = 0;i < box_digits.size();i++)
	{
		Mat ROI_digit = binary_digits_image(box_digits[i]).clone();
		ROI_digits.push_back(ROI_digit);
	}

	return ROI_digits;
}


//生成标签集
vector<int> GetTrainLabels()
{
	vector<int>labels;

	for (int i = 0; i < 9;i++)
	{
		labels.push_back(i + 1);
	}
	labels.push_back(0);

	return labels;
}


void TrainSVM(vector<vector<float>>features, vector<int> labels)
{
	Mat train_digits_feature = Mat::zeros(Size(40, 10), CV_32FC1);
	for (int row = 0;row < train_digits_feature.rows;row++)
	{
		for (int col = 0;col < train_digits_feature.cols;col++)
		{
			train_digits_feature.at<float>(row, col) = features[row][col];
		}
	}
	Mat train_labels = Mat::zeros(Size(1, 10), CV_32SC1);
	for (int row = 0;row < train_labels.rows;row++)
	{
		train_labels.at<int>(row, 0) = labels[row];
	}

	auto train_svm = ml::SVM::create();
	train_svm->trainAuto(train_digits_feature, ml::ROW_SAMPLE, train_labels,100);
	train_svm->save("model.xml");
}

特别要注意的是在SVM分类器的训练过程中,由于我们只使用了0~9各两个数字特征向量,也就是总共只有二十个训练数据,每个数字由两幅尺寸不同的图像进行训练。

当每个类别的训练数据很少的时候,使用SVM分类器的.trainAuto()方法进行选择最优参数自动训练时,很可能会出现报错,错误信息如下:

OpenCV(4.2.0) Error: Bad argument (While cross-validation one or more of the classes have been fell out of the sample. Try to reduce Params::k_fold) in cv::ml::SVMImpl::do_train

翻译过来的意思是:cv :: ml :: SVMImpl :: do_train中的错误参数(虽然交叉验证一个或多个类已从样本中删除。尝试减少Params :: k_fold)

这个错误信息的意思是,trainAuro()在进行多次交叉验证寻找最优参数的时候,有一些样本类别没有被包含进去而引起错误,并建议我们尝试着减少k_fold的值,也就是减少进行交叉验证的集合。

一开始我也是按照错误信息修改k_fold的值进行调试,然而从默认值k_fold=10一直测试到最小接受值k_fold=2,一直都是显示这个错误,这也让我有点摸不着头脑,明明按着错误信息进行修改了,怎么还会错呢???

然后就是上网查资料,然而查阅了许久居然找不到相关的内容,尝试了网上别的一些方法也还是没有解决问题,无奈之下上了GitHub继续查找,终于让我找到了有人和我遇到相同的问题了。

经过进一步查阅后发现,这里好像有一点小bug,那就是明明样本数量很少了而类别数又比较多,所以应该增加交叉验证的集合来覆盖所有的样本类别才对,而不是减少集合,也就是说应该是要增加k_fold值才是正确的。

例如经典的二分类问题,trainAuro()的默认值k_fold=10,而现在要进行0~9这十个数字的分类识别,那么k_fold应该大于10才对,才能在进行交叉验证时覆盖所有样本类别,于是我k_fold设置为100,果真就通过了,解决了这个错误。所以这点我也不是很理解,为什么报错的错误信息会推荐减少k_fold的值,越减反而是疯狂报错。

解决了上面坑人的错误后,我们就训练好了一个用来识别印刷体数字的模型了,下面通过加载模型、并输入测试图像进行验证,看一下演示代码:

	//进行验证
	Mat test_image = imread("image\\test\\digit-02.png");
	resize(test_image, test_image, Size(), 2, 2);
	auto test_svm = SVM::load("model.xml");
	vector<Mat> ROI_digits;
	vector<Rect>box_digits;
	ROI_digits = GetTrainData(test_image, box_digits);
	Mat output = Mat::ones(test_image.size(), test_image.type());
	for (int i = 0;i < ROI_digits.size();i++)
	{
		vector<float>feature = GetDigitFeature(ROI_digits[i]);
		Mat test_digits_feature = Mat::zeros(Size(40, 1), CV_32FC1);
		for (int col = 0;col < test_digits_feature.cols;col++)
		{
			test_digits_feature.at<float>(0, col) = feature[col];
		}
		float result = test_svm->predict(test_digits_feature);
		string predict_str = to_string(int(result));
		int x = box_digits[i].x;
		int y = box_digits[i].y;
		putText(output, predict_str, Point(x, y+10), FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 1, 8);
	}
	imshow("test_image", test_image);
	imshow("output", output);

我们使用的测试图像如下图:
在这里插入图片描述
下面是识别的效果图:
在这里插入图片描述
上图中,左边是输入的测试图像,并且在其中将数字都给框了出来,右边就是识别后的结果并打印到输出窗口上,可见对于这种简单测试图像,这种识别方法达到了百分之百的正确率。

上面我们提到过,使用归一化来增强该识别算法针对图像缩放的鲁棒性,那么下面就把测试图像放大两倍后再进行识别,看看效果怎样。

在这里插入图片描述
在这里插入图片描述
可见,对于放大两倍后的测试图像,这种算法仍然表现出了很高的正确率。当然了如果是实际应用,那肯定不会是这么简单的测试图像了,还需要考虑很多方面的影响。

这个识别算法的优势在于简单方便,相比起HOG特征而言,每个图像的特征向量从3780个特征值缩小到了40个特征值,运算速度大大提高了,而且只需要很少的训练样本就可以达到比较好的效果,经过测试,每个数字只使用一个样本来进行训练,也能达到不错的准确率。

而且,不仅是印刷体数字,对于印刷体的字母同样能使用这种算法进行识别,只不过对于小写字母而言,有一些字母如i、j这种是分离的,所以需要单独每个字母生成训练图像,比较难通过轮廓进行裁剪生成。有兴趣的朋友可以参考这个算法实现大小写字母识别。

好了,这一次笔记到此结束,如果有朋友对上述代码感兴趣的,可以到我的资源里下载源代码和图像数据(包含数字图像和字母图像)哦~
谢谢阅读!!!

PS:本人的注释比较杂,既有自己的心得体会也有网上查阅资料时摘抄下的知识内容,所以如有雷同,纯属我向前辈学习的致敬,如果有前辈觉得我的笔记内容侵犯了您的知识产权,请和我联系,我会将涉及到的博文内容删除,谢谢!

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值