图像处理之定位

1、投影定位

投影,在立体几何中,是空间直线在某个方向上的投影,那么图像处理中也是这种投影思想。

最简单的投影:

水平投影(英文名ground plan horizontal projection),水平面方向的正投影叫水平投影。

图像中字符识别时,

水平投影是指二维图像按行向y轴方向投影,将图像数组进行行求和;

垂直投影是指二维图象按列向x轴方向投影,将图像数组进行列求和。

对于二值图像或明显特征的灰度图分割前景与背景,经常用到投影法。在OCR字符分割中,通常会用到该方法,可以很容易的获得每个OCR字符在X轴、Y轴的起始位置与终止位置坐标,进而将每个OCR字符分割出来。

思路:
1.设定大概的ROI区域,取得包含目标OCR区域的图像。
2.先通过垂直投影,获取每个字符的左右边界位置,即X轴坐标,并记录。通过取ROI获得初次分割的图像。
3.再通过对得到的图像进行水平投影,获取每个字符的上下边界,即Y轴坐标,并记录。通过取ROI获得单个目标图像。

	//读取原图
	cv::Mat src2 = cv::imread("..\\IMG_input\\USD\\100colorAB.jpg", cv::IMREAD_COLOR);
	cv::namedWindow("0", 1);
	cv::imshow("0", src2);
	cv::waitKey(0);

  图像处理之定位_计算机视觉 

原图(来自百度百科:https://baike.baidu.com/item/%E7%BE%8E%E5%85%83/484146?fr=aladdin

获取OCR大概位置(取ROI区域图像)

获取ROI有两种方法,第一种:使用矩形界定(起点坐标,宽、高);第二种:使用行列范围设定(起始行、终止行,起始列、终止列).

1.使用矩形界定(起点坐标,宽、高)

    
    //设定ROI区域
	//1.使用矩形界定(起点坐标,宽、高)
	cv::Mat roiOCR1( src2, cv::Rect(550, 220, 170, 50) );
	cv::namedWindow("roiOCR1", 0);
	cv::imshow("roiOCR1", roiOCR1);
	cv::waitKey(0);

   2.使用行列范围界定(起始行、终止行,起始列、终止列).


    //设定ROI区域
	//2.使用行列范围界定(起始行、终止行,起始列、终止列).
	cv::Mat roiOCR2 = src2( cv::Range(220, 270), cv::Range(550, 720) );
	cv::namedWindow("roiOCR2", 0);
	cv::imshow("roiOCR2", roiOCR2);
	cv::waitKey(0);

	cv::imwrite("..\\IMG_input\\USD\\roiOCR2.jpg", roiOCR2);

roi_img1:图像处理之定位_计算机视觉_02        roi_img2:图像处理之定位_定位_03

 

垂直投影代码如下:

/// <summary>
/// 垂直投影
/// </summary>
/// <param name="srcImg:输入图像(多通道彩色或者单通道灰色)"></param>
/// <returns>分割后的单个字符</returns>
vector<Mat> verticalProjectionMat_1(Mat srcImg)//垂直投影
{
	if (nullptr == srcImg.data)
	{
		std::cout << "No image!!!" << std::endl;
		return srcImg;
	}

	Mat binImg;
	if (3 == srcImg.channels())
	{
		cvtColor(srcImg, binImg, COLOR_RGB2GRAY);
		//std::cout << "binImg.channels()=" << binImg.channels() << std::endl;
	}
	else if (1 == srcImg.channels())
	{
		srcImg.copyTo(binImg);
	}
	blur(binImg, binImg, Size(3, 3));//均值滤波
	threshold(binImg, binImg, 0, 255, /*CV_*/THRESH_OTSU);

	cv::namedWindow("srcImg", 0);
	cv::imshow("srcImg", srcImg);
	cv::waitKey(0);

	cv::namedWindow("binImg", 0);
	cv::imshow("binImg", binImg);
	cv::waitKey(0);


	int perPixelValue;//每个像素的值
	int width = srcImg.cols;
	int height = srcImg.rows;
	int* projectValArry = new int[width];//创建用于储存每列白色像素个数的数组
	memset(projectValArry, 0, width * 4.0);//初始化数组
	for (int col = 0; col < width; col++)//列遍历
	{
		for (int row = 0; row < height; row++)
		{
			perPixelValue = binImg.at<uchar>(row, col);
			if (perPixelValue == 0)//如果是白底黑字
			{
				projectValArry[col]++;//每一列的有效像素个数
			}
		}
	}
	//Mat verticalProjectionMat_1(height, width, CV_8UC1);//垂直投影的画布
	//for (int i = 0; i < height; i++)
	//{
	//	for (int j = 0; j < width; j++)
	//	{
	//		perPixelValue = 255;  //背景设置为白色
	//		verticalProjectionMat_1.at<uchar>(i, j) = perPixelValue;
	//	}
	//}

	//垂直投影的画布,白底.
	Mat verticalProjectionMat_1(height, width, CV_8UC1, cv::Scalar(255));
	//绘制垂直投影直方图
	for (int i = 0; i < width; i++)
	{
		for (int j = 0; j < projectValArry[i]; j++)
		{
			perPixelValue = 0;//直方图设置为黑色  
			verticalProjectionMat_1.at<uchar>(height - 1 - j, i) = perPixelValue;
		}
	}
	cv::namedWindow("垂直投影", 0);
	cv::imshow("垂直投影", verticalProjectionMat_1);
	cv::waitKey(0);


	vector<Mat> roiList;//用于储存分割出来的每个字符
	int startIndex = 0;//记录进入字符区域的索引
	int endIndex = 0;  //记录进入空白区域的索引
	bool inBlock = false;//是否遍历到了字符区内
	for (int i = 0; i < srcImg.cols; i++)//cols=width
	{
		if (!inBlock && projectValArry[i] != 0)//进入字符区
		{
			inBlock = true;
			startIndex = i;
		}
		else if (projectValArry[i] == 0 && inBlock)//进入空白区
		{
			endIndex = i;
			inBlock = false;
			//分割出单个字符图像
			Mat roiImg = srcImg(Range(0, srcImg.rows), Range(startIndex, endIndex + 1));
			roiList.push_back(roiImg);

			//cv::namedWindow("roiImg", 0);
			//cv::imshow("roiImg", roiImg);
			//cv::waitKey(0);
		}
	}

	char szName[30] = { 0 };
	for (int i = 0; i < roiList.size(); i++)
	{
		sprintf_s(szName, "..\\IMG_output\\字符分割\\_%d.jpg", i);
		
		cv::namedWindow("分割1", 0);
		cv::imshow("分割1", roiList[i]);
		cv::imwrite(szName, roiList[i]);
		cv::waitKey(0);
	}


	delete[] projectValArry;//怎么new的,就怎么delete掉!!!
	return roiList;
}

根据图像本身的颜色特征,选择R通道(前景明显,背景较干净,便于处理)的单色图像作为输入图像,进行阈值处理(图像处理阈值分割之OTSU/大津阈值原理及其实现)、投影。

图像处理之定位_定位_04

二值化图像、垂直投影图像:

图像处理之定位_定位_05

初次分割后得到的单个字符图像:

图像处理之定位_定位_06

通过垂直投影后,得到了OCR单个字符的左右边界精确位置坐标。接下来通过水平投影,即可得到单个字符上下边界的精确位置坐标。

水平投影代码如下:

/// <summary>
/// 水平投影
/// </summary>
/// <param name="srcImg:输入图像(多通道彩色或者单通道灰色)"></param>
/// <returns>分割后的单个字符</returns>
vector<Mat> horizontalProjectionMat_1(Mat srcImg)//水平投影
{
	Mat binImg;
	if (3 == srcImg.channels())
	{
		cvtColor(srcImg, binImg, COLOR_RGB2GRAY);

		//std::vector<cv::Mat> rgbImg(3);
		//split(srcImg, rgbImg);//分离出图片的B,G,R颜色通道.

		//cv::namedWindow("B_rgbImg[0]", 0);
		//cv::namedWindow("G_rgbImg[1]", 0);
		//cv::namedWindow("R_rgbImg[2]", 0);
		//cv::imshow("B_rgbImg[0]", rgbImg[0]);
		//cv::imshow("G_rgbImg[1]", rgbImg[1]);
		//cv::imshow("R_rgbImg[2]", rgbImg[2]);
		//cv::waitKey(0);

		//binImg = rgbImg[2];
		//	std::cout << "binImg.channels()=" << binImg.channels() << std::endl;
	}
	else if (1 == srcImg.channels())
	{
		srcImg.copyTo(binImg);
	}
	blur(binImg, binImg, Size(3, 3));
	threshold(binImg, binImg, 0, 255, /*CV_*/THRESH_OTSU);
	
	int perPixelValue = 0;//每个像素的值
	int width = srcImg.cols;
	int height = srcImg.rows;
	int* projectValArry = new int[height];//创建一个储存每行白色像素个数的数组
	memset(projectValArry, 0, height*4.0);//初始化数组
	for (int row = 0; row < height; row++)//遍历每个像素点
	{
		for (int col = 0; col < width; col++)
		{
			perPixelValue = binImg.at<uchar>(row, col);
			if (perPixelValue == 0)//白底黑字
			{
				projectValArry[row]++;//每一行的有效像素数.
			}
		}
	}

	创建画布,并设为白底.
	//Mat horizontalProjectionMatIMG_1(height, width, CV_8UC1);//创建画布
	//for (int i = 0; i < height; i++)
	//{
	//	for (int j = 0; j < width; j++)
	//	{
	//		perPixelValue = 255;
	//		horizontalProjectionMatIMG_1.at<uchar>(i, j) = perPixelValue;//设置背景为白色
	//	}
	//}

	//垂直投影的画布,白底.
	Mat horizontalProjectionMatIMG_1(height, width, CV_8UC1, cv::Scalar(255));

	for (int i = 0; i < height; i++)//水平直方图
	{
		for (int j = 0; j < projectValArry[i]; j++)
		{
			perPixelValue = 0;
			horizontalProjectionMatIMG_1.at<uchar>(i, width - 1 - j) = perPixelValue;//设置直方图为黑色
		}
	}
	vector<Mat> roiList;//用于储存分割出来的每个字符
	int startIndex = 0;//记录进入字符区的索引
	int endIndex = 0;//记录进入空白区域的索引
	bool inBlock = false;//是否遍历到了字符区内
	for (int i = 0; i <srcImg.rows; i++)
	{
		if (!inBlock && projectValArry[i] != 0)//进入字符区
		{
			inBlock = true;
			startIndex = i;
		}
		else if (inBlock && projectValArry[i] == 0)//进入空白区
		{
			endIndex = i;
			inBlock = false;
			Mat roiImg = srcImg(Range(startIndex, endIndex + 1), Range(0, srcImg.cols));//从原图中截取有效图像的区域
			roiList.push_back(roiImg);
		}
	}
	delete[] projectValArry;//怎么new的,就怎么delete掉!!!
	return roiList;
}

最终得到的图像:

图像处理之定位_定位_07

图像处理之定位_定位_08

 

2、积分图定位

在定位OCR区域时,也可以使用积分图进行精确定位到OCR区域后,再进行垂直、水平投影分割,直接得到OCR字符小图。

关于积分图,这里就不再赘述了,详情请看:图像处理之图像积分图integral()

原图:图像处理之定位_opencv_09

直接放上结果图像,再上代码。

图像处理之定位_计算机视觉_10

为了避免第一个字符“L”和最后一个字符”8“分割得不完整,可以将求得的坐标(xStart, yStart)各向外扩2~3个像素,宽高也扩大2~3个像素。

积分图定位代码如下:


	//计算兴趣区域灰度值的累加值
	Mat sum_img = Mat::zeros(gray_img.rows + 1, gray_img.cols + 1, CV_32SC1);
	Mat sqsum_img = Mat::zeros(gray_img.rows + 1, gray_img.cols + 1, CV_64FC1);
	//计算积分图
	integral(gray_img, sum_img, sqsum_img);

	//OCR精确大小.
	int roiW = 155;
	int roiH = 20;
	//搜索起始位置.
	int x0 = 0;
	int y0 = 0;

	int i_xframe = x0;
	int i_yframe = y0;
	int minSum = gray_img.rows * gray_img.cols * 255 + 1;
	int integral_value = 0;
    //搜索最黑区域起点坐标
	for (y0 = 0; y0 < gray_img.rows-roiH; y0++)
	{
		//int flag = 0;
		for (x0 = 0; x0 < gray_img.cols-roiW; x0++)
		{
			//用三个加/减运算得到兴趣区域的累加值
			integral_value = sum_img.at<int>(y0 + roiH, x0 + roiW) - sum_img.at<int>(y0 + roiH, x0)
				- sum_img.at<int>(y0, x0 + roiW) + sum_img.at<int>(y0, x0);
			//搜索最黑区域起点坐标
			if (integral_value < minSum)
			{
				minSum = integral_value;

				i_xframe = x0;
				i_yframe = y0;
			}
			//cout << integral_value << endl;
		}
	}
	std::cout << "i_xframe = " << i_xframe << ", i_yframe = " << i_yframe << std::endl;

	cv::rectangle(src, cv::Rect(i_xframe, i_yframe, 155, 20), cv::Scalar(0, 0, 255), 2, 8, 0);
	cv::namedWindow("100", 1);
	cv::imshow("100", src);
	cv::waitKey(0);


得到区域更加精确的OCR小图:图像处理之定位_计算机视觉_11

剩下的步骤就跟上面一样了,直接用得到的OCR小图作为输入图像,进行垂直投影、水平投影,就可以分割出OCR的单个字符小图。

 

3、匹配定位

 

 

opencv实现

 

halcon实现

 

c++实现

 

 

 

 

一、

GMS了解一下

要求实时(精度也不差): orb+GMS

要求精度: A-SIFT+GMS

相同程度匹配,速度精度比RANSAC效果好

二、

OpenCV官方提出的“解决方案”:Features2D + Homography to find a known object。地址:Features2D + Homography to find a known object。该方案先用SURF来提取特征点然后用FLANN进行匹配,过滤出足够好的匹配点之后,用一个矩形来定位出被探测的物体。如下图:

图像处理之定位_opencv_12

 

三、

sift+ransac

sift/surf+flann+ransac,一般情况都好使。实时性的话,要看具体的要求。

四、

形状匹配

 

 

【未完,持续更新中……】

 

 

 

 

【36、这一秒不放弃,下一秒就有希望!坚持下去才可能成功!】