OpenCV中利用knn进行数字(0-9)识别--RGB-D相机采集
本章使用一个基于MINIST的小型数据集通过OpenCV中的KNN方法进行从0到9数字识别,由于数据集小,不能完全识别正确,经测试其识别正确率能达到90%以上。这数字识别系统基本完成,但其适应性还有所欠缺,比如任意角度书写数字情况也能够高正确率识别。由于笔者能力局限,再加上参与(海南)疫情抗战导致时间仓促,特此提供本系统的所有工程代码,详细说明步骤,让大家继续深入开发,共同进步。

目录

前言

    一、KNN原理

    二、数字识别系统效果演示

    三、数字识别系统

         1.图像采集

         2.阈值分割--(定位数字区域)

         3.轮廓特征分析--(定位数字区域)

         4.数字轮廓提取

         5.扩充边界并归一化其尺寸

         6.训练数据集并预测测试样本

    总结


前言

本章使用一个基于 MINIST 的小型数据集通过OpenCV中的 KNN 方法进行从0到9数字识别,由于数据集小,不能完全识别正确,经测试其识别正确率能达到 90%以上。这数字识别系统基本完成,但其适应性还有所欠缺,比如任意角度书写数字情况也能够高正确率识别。由于笔者能力局限,再加上参与(海南)疫情抗战 导致时间仓促,特此提供本系统的所有工程代码,详细说明步骤,让大家继续深入开发,共同进步。


一、KNN原理

        KNN(K-NearestNeighbor)的任务就是在训练本集中,依据距离测度找到与测试样本最相似的K个训练样本,对于分类问题,大多采用“多数表决”的方式确定测试样本的最终分类,即在这K个训练样本中,属于那个类别的样本数最多,测试样本就属于那个类。
我们常说:“物以类聚,人以群分”。相似的东西我们就归为一类,人的一生有三个阶段,幼年、成年、老年。通过年龄大小的进行分类,比如我们现在要判断一个人处于说明阶段,就要通过其年龄大小进行分类。KNN原理可以这样通俗理解。


二、数字识别系统效果演示

          本系统任务是在一个普通的环境下找出数字区域并识别出数字。本系统是在学习桌面上进行演示系统的效果,其效果图如图所示。

 


三、数字识别系统

    1.图像采集

            本系统使用海康的MV-EB435i悉灵相机,其图像采集效果通过GIF图演示。其性能强大,且轻盈便携。



     2.阈值分割--(定位数字区域)

            由于我们在一个普通环境下,并非是复杂环境下进行定位,为此定位方法也简单,笔者采用阈值分割进行图像分析,将前景与背景区分开。在阈值分割方法中,其最常用的方法是大津法分割,其效果是较理想的,当然,其方法也不是万能的,你也知道,没有一种方法是万能的,只有较好的。

            大津法(OTSU)是一种自动选择阈值(无参数且无监督)的的图像分割方法,日本学者 Nobuyuki Otsu 1979年提出。该方法又称作最大类间方差法,因为按照大津法求得的阈值进行图像二值化分割后,前景与背景图像的类间方差最大。
    
            算法原理:①.首先假设阈值为 K(0-255),然后根据K值将灰度图分为两部分,前景与背景。

                          ②.计算前景与背景占整幅图的比例。P前景 与 P背景。其之和为1。

                          ③.算出属于前景/背景/全景中均值(灰度值),M前景、M背景与M全景。

                          ④.根据Otsu的算法原理,求类间方差。

                          ⑤.从0-255遍历K,计算每个K对应的间类方差,最大方差对应的K值即为阈值。

              通过使用大津法对采集的灰度图像进行阈值分割,其效果图如下。



    3.轮廓特征分析--(定位数字区域)

            前面将采集的图像进行二值化,我们要通过OpenCV中findContours函数进行轮廓分析,通过轮廓特征(面积,长宽比等特征)提取最大轮廓(我们假定最大轮廓包含数字)并在原图中做出标记与提取。笔者对最大轮廓进行最小外接正/斜矩形。

            其效果如下图所示:


    4.数字轮廓提取

            对最大轮廓的外接斜矩形(原图中的蓝色矩形)进行灰度值翻转操作即可。代码实现思路是遍历最小外接正矩形内区域所有点(原图中的红色区域)判断点是否为外接斜矩形区域,如果属于且该点的灰度值为 0 即为数字轮廓的点。将其灰度值翻转为255即可。

            其效果如图所示:


    5.扩充边界并归一化其尺寸

            由于我们的训练数据集中采用归一化尺寸,对测试样本也要进行归一化。其尺寸归一化为20*20,其归一化后效果如图所示。

    6.训练数据集并预测测试样本

            本文使用的小型数据集是从MINST中随机挑选出数字0-9的图片各500张,每张图片按比例缩小为20像素*20像素,然后将这5000张图片拼成一张整图,MINIST数据集如图所示。

             对这些样本作为训练集,创建并初始化KNN模型,训练分割好的训练集,然后将前面我们已经提取处理的数字区域作为测试样本进行测试,获取其预测结果,将其标注在原图中。其效果如图所示。




总结

        以上就是本系统采用的方案,本系统充其量为小儿科水平,其局限性较大,比如我们要解决在任意角度下也能够高正确识别出数字。因为一个企业级项目考虑条件是非常复杂的,我深知自己的能力不足,欠缺考虑复杂条件,所以让我们共同学习,共同进步。

    演示动画:https://blog.csdn.net/weixin_55984718/article/details/126471867

     代码:

#include "../common/common.hpp"
#include "../common/RenderImage.hpp"
#include <opencv2/opencv.hpp>

using namespace cv;

void* handle = NULL;
RIFrameInfo depth = { 0 };
RIFrameInfo rgb = { 0 };
vector<Mat> DigitImgs;


//int类型转string类型函数
//参数:int类型的预测数字结果
//作用:是将预测结果标记在视频上,故需要转换类型
string intToString(int number)
{
	ostringstream ss;
	ss << number;
	return ss.str();
}
	
//初始化+开启设备
void HIK_initialization()
{
	MV3D_RGBD_VERSION_INFO stVersion;
	ASSERT_OK(MV3D_RGBD_GetSDKVersion(&stVersion));
	LOGD("dll version: %d.%d.%d", stVersion.nMajor, stVersion.nMinor, stVersion.nRevision);

	ASSERT_OK(MV3D_RGBD_Initialize());

	unsigned int nDevNum = 0;
	ASSERT_OK(MV3D_RGBD_GetDeviceNumber(DeviceType_Ethernet | DeviceType_USB, &nDevNum));
	LOGD("MV3D_RGBD_GetDeviceNumber success! nDevNum:%d.", nDevNum);
	ASSERT(nDevNum);

	// 查找设备
	std::vector<MV3D_RGBD_DEVICE_INFO> devs(nDevNum);
	ASSERT_OK(MV3D_RGBD_GetDeviceList(DeviceType_Ethernet | DeviceType_USB, &devs[0], nDevNum, &nDevNum));
	for (unsigned int i = 0; i < nDevNum; i++)
	{
		LOG("Index[%d]. SerialNum[%s] IP[%s] name[%s].\r\n", i, devs[i].chSerialNumber, devs[i].SpecialInfo.stNetInfo.chCurrentIp, devs[i].chModelName);
	}

	//打开设备
	unsigned int nIndex = 0;
	ASSERT_OK(MV3D_RGBD_OpenDevice(&handle, &devs[nIndex]));
	LOGD("OpenDevice success.");

	//改变分辨率参数,0x00010001为 1280×720, 0x00020002为 640×360
	//MV3D_RGBD_PARAM stparam;
	//stparam.enParamType = ParamType_Enum;
	//stparam.ParamInfo.stEnumParam.nCurValue = 0x00010001;
	//ASSERT_OK(MV3D_RGBD_SetParam(handle, MV3D_RGBD_ENUM_RESOLUTION, &stparam));

	//改变曝光参数
	//MV3D_RGBD_PARAM stparam;
	//stparam.enParamType = ParamType_Float;;
	//stparam.ParamInfo.stFloatParam.fCurValue = 100.0000;
	//ASSERT_OK(MV3D_RGBD_SetParam(handle, MV3D_RGBD_FLOAT_EXPOSURETIME, &stparam));
	//LOGD("EXPOSURETIME: (%f)", stparam.ParamInfo.stFloatParam.fCurValue);

	// 开始工作流程
	ASSERT_OK(MV3D_RGBD_Start(handle));
	LOGD("Start work success.");
}

//关闭释放设备
void HIK_stop()
{
	ASSERT_OK(MV3D_RGBD_Stop(handle));
	ASSERT_OK(MV3D_RGBD_CloseDevice(&handle));
	ASSERT_OK(MV3D_RGBD_Release());

	LOGD("Main done!");
}

//作用:判断点是否在矩形框内
//参数:参数一表示包含平面上的旋转矩形,参数二表示预测点
//返回值为true表示点在矩形框内
bool DoesRectangleContainPoint(RotatedRect rectangle, Point2f point)
{
	//转化为轮廓
	Point2f corners[4];
	rectangle.points(corners);
	Point2f *lastItemPointer = (corners + sizeof corners / sizeof corners[0]);
	vector<Point2f> contour(corners, lastItemPointer);
	//判断
	double indicator = pointPolygonTest(contour, point, false); //pointPolygonTest 检测点是否在轮廓内
	//pointPolygonTest:
	//C++: double pointPolygonTest(InputArray contour, Point2f pt, bool measureDist)
	//用于测试一个点是否在多边形中
	//当measureDist设置为true时,返回实际距离值。若返回值为正,表示点在多边形内部,返回值为负,表示在多边形外部,返回值为0,表示在多边形上。
	//当measureDist设置为false时,返回 - 1、0、1三个固定值。若返回值为 + 1,表示点在多边形内部,返回值为 - 1,表示在多边形外部,返回值为0,表示在多边形上。

	if (indicator >= 0)  
		return true;
	else 
		return false;
}


//作用:提取数字轮廓
//参数:参数一为采集BGR格式图,参数二为采集的灰度图
Mat getNumberControus(Mat &bgrImg,Mat &grayImg)
{
	/*******************************************************/
	//注意:这是看自己摄像头摆放位置是否需要图片翻转!!!
	//void cv::flip(InputArray src,OutputArray dst,int flipCode);
	//flipCode            Anno
	//  1				水平翻转
	//  0				垂直翻转
	// -1				水平垂直翻转
	//图像翻转
	flip(bgrImg, bgrImg, -1);
	flip(grayImg, grayImg, -1);
	/*******************************************************/

	/*******************************************************/
	//Step1:阈值化(采用大津法-最大间类方差)
	Mat binary;
	threshold(grayImg,binary,0,255,THRESH_BINARY|THRESH_OTSU);
	namedWindow("binary", 0);
	resizeWindow("binary", 640, 360);
	imshow("binary",binary);
	/*******************************************************/

	/*******************************************************/
	//Step2:轮廓分析
	//这里我们假设最大轮廓是白底黑字
	int max_area_contour_idx = 0;
	double max_area = -1;
	vector<vector<Point>> contours;
	findContours(binary, contours, RETR_LIST, CHAIN_APPROX_SIMPLE);
	Rect roiRect; //最小外接正矩形
	RotatedRect roiRoRect;//最小外接斜矩形

	//如果轮廓数量为0则退出函数返回空
	if (contours.size() == 0)
		return Mat();

	 //遍历每个轮廓寻找含数字的轮廓
	for (uint i = 0; i < contours.size(); i++) {
		//计算轮廓面积
		double temp_area = contourArea(contours[i]);
		//比较轮廓面积,找出最大轮廓
		if (max_area < temp_area) {
			max_area_contour_idx = i;
			max_area = temp_area;
		}
	}

	//最小外接正矩形
	roiRect = boundingRect(contours[max_area_contour_idx]);
	//最小外接斜矩形
	roiRoRect = minAreaRect(contours[max_area_contour_idx]);
	
	Mat roiMat = Mat::zeros(roiRect.size(),CV_8UC1);
	
	//如果面积过小/大则判断为伪轮廓
	if (roiRect.area() <= 100 || roiRect.area() > bgrImg.size().area()*0.75)
		return Mat();
	else//绘制标记区域
	{ 
		//绘制最小外接正矩形
		rectangle(bgrImg, roiRect, Scalar(0, 0, 255), 3, 8);
		Point2f P[4];
		roiRoRect.points(P);
		//绘制最小外接斜矩形
		for (int j = 0; j <= 3; j++)
		{
			line(bgrImg, P[j], P[(j + 1) % 4], Scalar(255), 2);
		}
	}
	namedWindow("putSignImg", 0);
	resizeWindow("putSignImg", 640, 360);
	imshow("putSignImg", bgrImg);
	/*******************************************************/

	/*******************************************************/
	//由于前面我们提取轮廓是白底黑字的轮廓,并没有提取出数字轮廓,为此我们只需要在白底黑字轮廓进行处理,将其翻转即可获得数字轮廓
	//Step3 提取数字区域
	for (int h = roiRect.y + 5; h < roiRect.y + roiRect.height - 5; h++)
	{
		for (int w = roiRect.x + 5; w < roiRect.x + roiRect.width - 5; w++)
		{
			Point pt(w,h);
			//如果点在最小外接矩形内且该点灰度图为黑则属于数字区域,将该点的灰度图置为255即可
			if (DoesRectangleContainPoint(roiRoRect, pt) && !binary.at<uchar>(h,w))
			{
				roiMat.at<uchar>(h - roiRect.y, w - roiRect.x) = 255;
			}
		}
	}
	//imshow("roiImg", roiMat);
	/*******************************************************/


	/*******************************************************/
	//Step4分析数字轮廓
	//这里我们假定数字轮廓同样为最大轮廓
	findContours(roiMat, contours, RETR_LIST, CHAIN_APPROX_SIMPLE);
	if (contours.size() == 0)
		return Mat();
	max_area_contour_idx = 0;
	max_area = -1;
	for (uint i = 0; i < contours.size(); i++) {
		double temp_area = contourArea(contours[i]);
		if (max_area < temp_area) {
			max_area_contour_idx = i;
			max_area = temp_area;
		}
	}
	//求数字轮廓外接正矩形
	Rect rroiRect = boundingRect(contours[max_area_contour_idx]);
	//求数字轮廓外接斜矩形
	RotatedRect rroiRoRect = minAreaRect(contours[max_area_contour_idx]);
	//如果轮廓区域面积过小则返回
	if (max_area_contour_idx == 0 || max_area < 500)
		return Mat();

	roiMat = roiMat(rroiRect).clone();
	
	//Step5 扩充数字轮廓,扩充为正方形 
	Mat NumMat;
	int top, bottom, left, right;
	top = rroiRect.height / 5;
	bottom = top;
	left =  (rroiRect.height * 7 / 5 - rroiRect.width)/2 < 0 ? 0 : (rroiRect.height * 7 / 5 - rroiRect.width) / 2;
	right = left;
	copyMakeBorder(roiMat, NumMat,top,bottom,left,right,BORDER_CONSTANT,Scalar(0));
	namedWindow("NumberImg", 0);
	resizeWindow("NumberImg", 640, 360);
	imshow("NumberImg", NumMat);
	return NumMat;
	 
}


void generateDataSet(Mat &img, Mat &trainData, Mat &testData, Mat &trainLabel, Mat &testLabel, int train_rows = 5)
{
	int width_slice = 20;
	int height_slice = 20;
	int row_sample = 100;
	int col_sample = 50;
	Mat trainMat(train_rows * 20 * 10, img.cols, CV_8UC1, Scalar::all(0));
	 
	// printf("开始生成训练,测试数据...\n");
	Rect roi;
	for (int i = 1; i <= col_sample; i++)//50
	{
		//printf("第%d行\n", i);
		for (int j = 1; j <= row_sample; j++)
		{
			//第j行为训练集
			Mat temp_single_num;
			roi = Rect((j - 1) * width_slice, (i - 1)*height_slice, width_slice, height_slice);
			temp_single_num = img(roi).clone();
			{
				trainData.push_back(temp_single_num.reshape(0, 1));
			}
		}
	}
	trainData.convertTo(trainData, CV_32FC1);
	testData.convertTo(testData, CV_32FC1);
	printf("训练、测试数据已完成\n\n");

	//生成标签
	printf("开始生成标签...\n");
	for (int i = 1; i <= 10; i++)
	{
		Mat temp_label_train = Mat::ones(train_rows * row_sample, 1, CV_32FC1);
		temp_label_train = temp_label_train * (i - 1);
		Mat temp = trainLabel.rowRange((i - 1)*train_rows * row_sample, i * train_rows * row_sample);
		temp_label_train.copyTo(temp);
	}
	printf("标签数据已经生成\n\n");
}


int main(int argc,char** argv)
{
	HIK_initialization();
	MV3D_RGBD_FRAME_DATA stFrameData = { 0 };

	//knn
	//第一步利用QT上的打开图片的功能,返回图片地址放入ImgAddress变量中
	Mat img = imread("digits.png", 1);
	cvtColor(img, img, COLOR_BGR2GRAY);
	/*cout << img.size() << endl;*/
	int train_sample_count = 5000;
	int test_sample_count = 1000;
	int train_rows = 5;
	Mat trainData, testData;
	Mat trainLabel(train_sample_count, 1, CV_32FC1);
	Mat testLabel(test_sample_count, 1, CV_32FC1);

	generateDataSet(img, trainData, testData, trainLabel, testLabel, train_rows);

	string knnPath = "digits_knn.xml";
	cv::Ptr<cv::ml::KNearest> knn = cv::ml::KNearest::create();
	int K = 7;
	knn->setDefaultK(K);
	knn->setIsClassifier(true);
	knn->setAlgorithmType(cv::ml::KNearest::BRUTE_FORCE);

	printf("开始训练.....\n");
	knn->train(trainData, cv::ml::ROW_SAMPLE, trainLabel);
	printf("训练完成\n\n");
	//knn->save(knnPath);
	//printf("已保存模型\n\n");
			
	while (1)
	{
		// 获取图像数据
		int nRet = MV3D_RGBD_FetchFrame(handle, &stFrameData, 5000);
		if (MV3D_RGBD_OK == nRet)
		{
			LOGD("MV3D_RGBD_FetchFrame success.");
			//分析获取每帧数据
			parseFrame(&stFrameData, &depth, &rgb);
			Mat rgb_frame(rgb.nHeight, rgb.nWidth, CV_8UC3, rgb.pData);
			//LOGD("rgb: FrameNum(%d), height(%d), width(%d)。", rgb.nFrameNum, rgb.nHeight, rgb.nWidth);

			//滤波,中值滤波方法进行图像滤波
			medianBlur(rgb_frame, rgb_frame,3);

			//B、R通道交换,显示正常彩色图像
			Mat frame, gray_frame;
			cvtColor(rgb_frame, frame, COLOR_BGR2RGB);
			cvtColor(rgb_frame, gray_frame, COLOR_BGR2GRAY);

			/****************************************************/
			Mat roiImg = getNumberControus(frame, gray_frame);
			if (roiImg.empty())
			{
				continue;
			}
			resize(roiImg, roiImg, Size(20, 20), 0.0, 0.0);//缩小尺寸
			imshow("roiImg",roiImg);
			/****************************************************/

			/****************************************************/
			Mat roiImgData;
			roiImgData.push_back(roiImg.reshape(0, 1));
			roiImgData.convertTo(roiImgData, CV_32F);
			cout << roiImg.size() << endl;
			/****************************************************/

			/****************************************************/
			//printf("开始测试....\n");
			Mat result;
			//knn->findNearest(testData,K,result);
			knn->findNearest(roiImgData, K, result);
			int predict = int(result.at<float>(0));
			printf(" predict: %d\n", predict);
			/****************************************************/

			namedWindow("testImg", 0);
			resizeWindow("testImg", 640, 360);
			string text = " predict:" + intToString(predict);
			putText(frame, text, Point(5, 30), FONT_HERSHEY_COMPLEX, 1, Scalar(255,0,255), 2, 8);
			imshow("testImg", frame);

			namedWindow("HIK_face", 0);
			resizeWindow("HIK_face", 640, 360);
			imshow("HIK_face", frame);
			waitKey(1);
		}
	}

	HIK_stop();

	return  0;
 
}
附件:
版权声明:本文为V社区用户原创内容,转载时必须标注文章的来源(V社区),文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:v-club@hikrobotics.com 进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
上一篇

“悉灵杯”课题研究-RGB_D相机SDK集成及Open3d点云基本处理

下一篇

海康机器人“悉灵杯”课题研究活动-获奖公布

评论请先登录 登录
全部评论 0
Lv.0
1
创作
0
粉丝
4
获赞
相关阅读
  • 海康机器人“悉灵杯”课题研究活动-获奖公布
    2022-09-05
  • 汽车行业-汽车焊点检测案例
    2022-09-01
  • VM3D模块学习经验分享
    2022-09-22
  • SC系列智能相机图像存储的几种方式
    2022-09-08
  • 有关条码二维码相关知识
    2022-08-30

请升级浏览器版本

您正在使用的浏览器版本过低,请升级最新版本以获得更好的体验。

推荐使用以下浏览器