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: roi_img2:
垂直投影代码如下:
/// <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/大津阈值原理及其实现)、投影。
二值化图像、垂直投影图像:
初次分割后得到的单个字符图像:
通过垂直投影后,得到了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;
}
最终得到的图像:
2、积分图定位
在定位OCR区域时,也可以使用积分图进行精确定位到OCR区域后,再进行垂直、水平投影分割,直接得到OCR字符小图。
关于积分图,这里就不再赘述了,详情请看:图像处理之图像积分图integral()
原图:
直接放上结果图像,再上代码。
为了避免第一个字符“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小图:
剩下的步骤就跟上面一样了,直接用得到的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进行匹配,过滤出足够好的匹配点之后,用一个矩形来定位出被探测的物体。如下图:
三、
sift+ransac
sift/surf+flann+ransac,一般情况都好使。实时性的话,要看具体的要求。
四、
形状匹配
【未完,持续更新中……】
【36、这一秒不放弃,下一秒就有希望!坚持下去才可能成功!】