OpenCV中对的旋转一些思考

目录

1. 问题描述

2. “旋转”的三种解法

2.1 应用迭代法进行求解

2.2 应用特征椭圆进行求解

2.3 应用PCA主成分分析进行求解

3. 归纳与比较


1.问题描述

数字图像处理是一门很有意思的学问,在现实生活中往往一个很简单的问题在数字图像中有时会非常复杂,旋转便是一类非常有意思的问题。如何在离散图像中高精度、快速求解图像的旋转角度,这个问题我思考了很长时间,下面会使用三种不同的算法逐一计算轮廓的旋转。

如图1-1所示,这是一幅鸟的轮廓:

如果这副图像发生了旋转,会出现什么样的情况呢?如图1-2所示,绿色的线表示旋转后的轮廓,在这里我设置了旋转角度为50°,旋转中心为轮廓的形心。

                                         

这也就是说,给定白色和绿色的两条轮廓,我们需要解出它们的旋转角度(50°);

2.旋转的三种解法

2.1 应用迭代法进行求解

算法的基本参数:

1.给定轮廓相似程度的度量方式,这里我采用了ShapeContextDistance;

2.给定旋转的步长,也就是每次匹配时绿色轮廓旋转的变化量,这里为了实验方便步长为10°;

3.给定迭代的终止条件,这里为了快速迭代,迭代次数为10;

如何迭代:

每次旋转后计算ShapeContextDistance,达到阈值或者达到迭代上限即跳出循环。

code:

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace std;
using namespace cv;

//计算图像上所有的轮廓,并计算它的质心和对角线长度
void FindBlobs(Mat img, vector<vector<Point>> &contours,
                         vector<Point2f> &MassCentre, vector<float>&DiagonalLength)
{
	//判断图像是否为8位单通道
	if (img.empty() || img.depth() != CV_8UC1)
	{
		cout << "Invalid Input Image!";
		exit(-1);
	}
	//计算轮廓
	vector<Vec4i> hierarchy;
	findContours(img, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	//计算轮廓的最小外接矩形
	vector<Rect> boundRect(contours.size());
	vector<Moments> mu(contours.size());
	for (size_t i = 0; i < contours.size(); i++)
	{
		//计算外界矩形
		boundRect[i] = boundingRect(Mat(contours[i]));
		//计算轮廓矩
		mu[i] = moments(contours[i], false);
                                             //当binaryImage=true时,所有的非零值都视为1
		//计算质心
		MassCentre.push_back(Point2f(static_cast<float>(mu[i].m10 / mu[i].m00),
                                   static_cast<float>(mu[i].m01 / mu[i].m00)));
		//计算外接矩形的对角距离
		DiagonalLength.push_back((float)norm(boundRect[i].tl() 
                                                         - boundRect[i].br()));
	}
}

//对轮廓进行平移变化
void TransformContour(vector<vector<Point>> contours,
                       vector<vector<Point>> &contours_Trans, Vec2f Translate)
{
	//判断
	if (contours.size() == 0)
	{
		cout << "Invalid input!";
		exit(-1);
	}
	//平移变换
	contours_Trans = contours;
	for (size_t i = 0; i < contours.size(); i++)
	{
		for (int idx = 0; idx < contours[i].size(); idx++)
		{
			contours_Trans[i][idx] = Point(contours[i][idx].x
                             + Translate(0), contours[i][idx].y + Translate(1));
		}
	}
}

//旋转轮廓
//1.基于仿射变化的2阶方阵M计算旋转位置
Point RotatePoint(const Mat &M, const Point &p)
{
	Point2f rp;
	rp.x = (float)(M.at<double>(0, 0)*p.x + M.at<double>(0, 1)*p.y 
                                                          + M.at<double>(0, 2));
	rp.y = (float)(M.at<double>(1, 0)*p.x + M.at<double>(1, 1)*p.y 
                                                          + M.at<double>(1, 2));
	return rp;
}
//2.计算选择轮廓
void RotateContour(vector<vector<Point>> contours,
           vector<vector<Point>> &contours_Rotated, double Angle, Point2f Centre)
{
	//判断
	if (contours.size() == 0)
	{
		cout << "Invalid input!";
		exit(-1);
	}
	//计算仿射矩阵
	Mat M= getRotationMatrix2D(Centre, Angle, 1.0);
	//计算旋转轮廓
	contours_Rotated = contours;
	for (size_t i = 0; i < contours.size(); i++)
	{
		for (int idx = 0; idx < contours[i].size(); idx++)
		{
			contours_Rotated[i][idx] = RotatePoint(M, contours[i][idx]);
		}
	}
}

//对轮廓进行简单的采样使得轮廓点数量为300 方便计算ShapeContextDistance
static vector<Point> simpleContour(vector<vector<Point>> _contoursQuery, int n = 300)
{
	//当前数据点
	vector <Point> contoursQuery;
	for (size_t border = 0; border<_contoursQuery.size(); border++)
	{
		for (size_t p = 0; p<_contoursQuery[border].size(); p++)
		{
			contoursQuery.push_back(_contoursQuery[border][p]);
		}
	}
	//增补数据点至n=300
	int dummy = 0;
	for (int add = (int)contoursQuery.size() - 1; add<n; add++)
	{
		contoursQuery.push_back(contoursQuery[dummy++]); 
	}
	//将数据点均匀化
	random_shuffle(contoursQuery.begin(), contoursQuery.end());
	vector<Point> cont;
	for (int i = 0; i<n; i++)
	{
		cont.push_back(contoursQuery[i]);
	}
	return cont;
}

int main()
{
	//1 以GRAY的形式读入,并二值化
	Mat src = imread("rotate_consider.png",0);
	if (src.empty())
	{
		cout << "Invalid Input Image!";
		exit(-1);
	}
	threshold(src,src,50,255,CV_THRESH_BINARY);

	//2 计算轮廓及其质心、对角距离
	vector<vector<Point>> contours;
	vector<Point2f> MassCentre;
	vector<float>DiagonalLength;
	FindBlobs(src, contours, MassCentre, DiagonalLength);

	//3 对轮廓进行平移变换,构造图像空间mContourSpace,
                     并将轮廓的质心坐标移动到新坐标系下的ptCCentre位置
	vector<vector<Point>> contours_Trans(contours.size());  
	Mat mContourSpace(Size(DiagonalLength[0], DiagonalLength[0]),
                                                         CV_8UC3, Scalar(0));
	Point2f ptCCentre(DiagonalLength[0] / 2, DiagonalLength[0] / 2);
	Vec2f Translation(ptCCentre.x - MassCentre[0].x, 
                                              ptCCentre.y - MassCentre[0].y);
	TransformContour(contours, contours_Trans, Translation);
	drawContours(mContourSpace, contours_Trans, 0, Scalar(255, 255, 255), 2, 8);

	//4 对轮廓进行旋转变换
	vector<vector<Point>> contours_Rotated(contours.size());
	double Angle = -50.0;
	RotateContour(contours_Trans, contours_Rotated, Angle, ptCCentre);
	Mat _mContourSpace = mContourSpace.clone();
	drawContours(_mContourSpace, contours_Rotated, 0, Scalar(0, 255, 0), 2, 8);

	//5 基于迭代思想来计算角度
	//5.1 创建shapecontextdistance对象指针
	Ptr <ShapeContextDistanceExtractor> mysc = 
                                        cv::createShapeContextDistanceExtractor();
	float bestMatchAngle = 0;
	float bestDis = FLT_MAX;
	float test_angle = 0;
	vector<Point> QueryContour= simpleContour(contours_Trans);
	//10次迭代,每次运算后角度增加10°
	for (int i = 0; i < 10; i++)
	{
		Mat mContourMatch = mContourSpace.clone();
		test_angle += 10;
		vector<vector<Point>> tem_contours;
		RotateContour(contours_Rotated, tem_contours, test_angle, ptCCentre);
		vector<Point> TestContour = simpleContour(tem_contours);
		float dis = mysc->computeDistance(QueryContour, TestContour);
		drawContours(mContourMatch, tem_contours, (int)0, Scalar(0, 255, 0),
                                                                                2, 8);
		putText(mContourMatch, format("Distance: %f ", dis), 
                                                        Point2f(ptCCentre.x + 30, 20),
			                                               CV_FONT_HERSHEY_COMPLEX, 0.5, Scalar(0, 0, 255), 0.5);
		if (dis<bestDis)
		{
			bestMatchAngle = test_angle;
			bestDis = dis;
		}

	}

	//绘制计算结果
	Mat dst= mContourSpace.clone();
	vector<vector<Point>> result_rotate;
	RotateContour(contours_Rotated, result_rotate, bestMatchAngle, ptCCentre);
	drawContours(dst, result_rotate, (int)0, Scalar(0, 0, 255),2,8);




	return 0;
}

算法的运算结果:

                                                                                           图 初始图像

                                            图 算法的第1-3次迭代,计算的shapecontext距离值分别为:25.7、14.8、1.23

                                          图 算法的第4-6次迭代,计算的shapecontext距离值分别为0.095、0.068、0.076

                                         图 算法的第7-10次迭代,计算的shapecontext距离值分别为1.45、7.9、28.8、30.2

                                                                            图 算法返回的最终解,angle=50°   

2.2 应用特征椭圆进行求解

特征椭圆计算旋转角度方法的数学理论推导详见 Peter Corke著作的《Robotics Vision and Control Page:351-353》;在此只给出简单公式图片:

算法的实现:

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace std;
using namespace cv;

//寻找最大轮廓
vector<Point> FindBigestContour(Mat &src) {
	int imax = 0;
	int imaxcontour = -1;
	std::vector<std::vector<Point> >contours;
	findContours(src, contours, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE);
	for (int i = 0; i<contours.size(); i++) {
		int itmp = contourArea(contours[i]);
		if (imaxcontour < itmp) {
			imax = i;
			imaxcontour = itmp;
		}
	}
	return contours[imax];
}
//轮廓的特征椭圆的特征矩阵
void computeEllipse(const vector<Point> &contour,Point &center,Size &EllipseSize,
                                                                         float &angle)
{
	//计算矩
	Moments mc = moments(contour,false);
	//计算形心
	center = Point(mc.m10/mc.m00,mc.m01/mc.m00);
	//计算特征矩阵
	Mat J(2, 2, CV_32F);
	J.at<float>(0, 0) = mc.mu20;
	J.at<float>(0, 1) = mc.mu11;
	J.at<float>(1, 0) = mc.mu11;
	J.at<float>(1, 1) = mc.mu02;
	//计算J的特征矢量和特征值
	Mat eigenValues;
	Mat eigenVectors;
	eigen(J, eigenValues, eigenVectors);
	//计算椭圆的尺寸
	Mat size;
	sqrt(4*eigenValues / mc.m00,size);
	int a = size.at<float>(0);
	int b = size.at<float>(1);
	EllipseSize = Size(a, b);
	//计算椭圆长轴的角度
	angle = 180*(atan2(eigenVectors.at<float>(0,1), eigenVectors.at<float>(0,
                                                                            0)))/3.14;

}

int main()
{
	//1.准备实验的图像素材
	Mat src=imread("rotate_consider_src.png",0);
	Mat match = imread("rotate_consider_match.png", 0);
	threshold(src,src,50,255,CV_THRESH_BINARY);
	threshold(match, match, 50, 255, CV_THRESH_BINARY);
	//2.存储轮廓
	vector<Point> contour_src = FindBigestContour(src);
	vector<Point> contour_match = FindBigestContour(match);
	//3.计算特征椭圆
	Point center_src, center_match;
	Size  size_src,size_match;
	float angle_src,angle_match;
	computeEllipse(contour_src,center_src,size_src,angle_src);
	computeEllipse(contour_match, center_match, size_match, angle_match);
	//4.绘制
	vector<vector<Point>> contours;
	contours.push_back(contour_src);
	contours.push_back(contour_match);
	Mat drawMat(src.size(),CV_8UC3,Scalar(0));
	drawContours(drawMat,contours,0,Scalar(255,0,0),2,8);
	drawContours(drawMat, contours, 1, Scalar(255, 255, 255), 2, 8);
	//src
	ellipse(drawMat,center_src,size_src,angle_src,0,360,Scalar(0,0,255),2,8);
	circle(drawMat,center_src,5,Scalar(0,0,255),-1);
	//match
	ellipse(drawMat, center_match, size_match, angle_match, 0, 360,
                                                           Scalar(0,255,0), 2, 8);
	//单独绘制src
	Mat img_01(src.size(), CV_8UC3, Scalar(0));
	drawContours(img_01, contours, 0, Scalar(255, 0, 0), 2, 8);
	ellipse(img_01, center_src, size_src, angle_src, 0, 360, Scalar(0, 0, 255),
                                                                           2, 8);
	circle(img_01, center_src, 5, Scalar(0, 0, 255), -1);
	//单独绘制match
	Mat img_02(src.size(), CV_8UC3, Scalar(0));
	drawContours(img_02, contours, 1, Scalar(255, 0, 0), 2, 8);
	ellipse(img_02, center_match, size_match, angle_match, 0, 360,
                                                             Scalar(0, 255, 0), 2, 8);
	circle(img_02, center_src, 5, Scalar(0, 255,0), -1);



	//5.角度计算结果
	putText(drawMat, format("angleDiff: %f ", fabs(angle_src-angle_match))
                                                                 , Point2f(600, 40),
		CV_FONT_HERSHEY_COMPLEX, 0.5, Scalar(0, 255, 255), 0.5);


	return 0;
}

算法的计算结果:

                                                                                图 计算轮廓的特征椭圆    

                                                                        图 算法返回的最终解,angle=50.027°

2.3 应用PCA主成分分析的方法求解

PCA是机器学习里面进行数据降维的常用方法之一,不懂的小伙伴建议去读相关文献。

具体思路是计算两个轮廓的主向量,并计算主向量之间的夹角。

实现code:

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace std;
using namespace cv;

//对轮廓点进行PCA分析
void contourPCA(vector<Point> &contour,Mat &img, float &angle)
{
	//1 重新布置数据点
	Mat data_pts = Mat(contour.size(), 2, CV_64FC1);
	for (int i = 0; i < data_pts.rows; ++i)
	{
		data_pts.at<double>(i, 0) = contour[i].x;
		data_pts.at<double>(i, 1) = contour[i].y;
	}

	//2 进行PCA分析
	PCA pca_analysis(data_pts, Mat(), CV_PCA_DATA_AS_ROW);

	//3 计算轮廓中心点
	Point2f center = Point2f(pca_analysis.mean.at<double>(0, 0),pca_analysis.mean.at<double>(0, 1));
	//4 生成 特征向量矩阵 和 特征值矩阵
	Mat eigen_vecsMat = pca_analysis.eigenvectors;
	Mat eigen_valMat = pca_analysis.eigenvalues;

	//5 保存特征值和特征向量
	vector<Point2f> eigen_vecs(2);    //保存PCA分析结果,其中0组为主方向,1组为垂直方向
	vector<float> eigen_val(2);
	for (int i = 0; i < 2; ++i)
	{
		eigen_vecs[i] = Point2d(eigen_vecsMat.at<double>(i, 0), eigen_vecsMat.at<double>(i, 1));
		eigen_val[i] = eigen_valMat.at<double>(i, 0);
	}

	//6 计算外界矩形的交点
	//6.1 计算轮廓的最大外接矩形约束交点计算的范围
	Rect boundRect = boundingRect(contour);
	//6.2 计算主方向与矩形轮廓的交点
	//6.2.1 计算方向1与矩形的交点
	float k1 = eigen_vecs[0].y / eigen_vecs[0].x;//斜率k1
	angle = (atan2(eigen_vecs[0].y, eigen_vecs[0].x))*180/3.14;
	Point2f pt1 = Point2f(boundRect.x, k1*(boundRect.x - center.x) + center.y);
	Point2f pt2 = Point2f((boundRect.x + boundRect.width), k1*((boundRect.x + boundRect.width) - center.x) + center.y);
	//6.2.2 计算方向2与矩形的交点
	float k2 = eigen_vecs[1].y / eigen_vecs[1].x;//斜率k1
	Point2f pt3 = Point2f(boundRect.x, k2*(boundRect.x - center.x) + center.y);
	Point2f pt4 = Point2f((boundRect.x + boundRect.width), k2*((boundRect.x + boundRect.width) - center.x) + center.y);
	
	//7 绘图
	line(img, pt1, pt2, Scalar(0, 255, 255),2,8);
	line(img, pt3, pt4, Scalar(0, 255, 0),2,8);
	circle(img, center, 4, Scalar(0, 0, 255), -1);
}

//寻找最大轮廓
vector<Point> FindBigestContour(Mat &src) {
	int imax = 0;
	int imaxcontour = -1;
	std::vector<std::vector<Point> >contours;
	findContours(src, contours, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE);
	for (int i = 0; i<contours.size(); i++) {
		int itmp = contourArea(contours[i]);
		if (imaxcontour < itmp) {
			imax = i;
			imaxcontour = itmp;
		}
	}
	return contours[imax];
}

int main()
{
	//1.准备实验的图像素材
	Mat src = imread("rotate_consider_src.png", 0);
	Mat match = imread("rotate_consider_match.png", 0);
	threshold(src, src, 50, 255, CV_THRESH_BINARY);
	threshold(match, match, 50, 255, CV_THRESH_BINARY);
	//2.存储轮廓
	vector<Point> contour_src = FindBigestContour(src);
	vector<Point> contour_match = FindBigestContour(match);
	vector<vector<Point>> contours(2);
    contours[0]= contour_src;
    contours[1] = contour_match;
	//3.对轮廓进行主成分分析
	Mat drawMat_src(src.size(), CV_8UC3, Scalar(0));
	Mat drawMat_match(src.size(), CV_8UC3, Scalar(0));
	drawContours(drawMat_src,contours,0,Scalar(255,255,255),2,8);
	drawContours(drawMat_match, contours, 1, Scalar(255, 0, 0), 2, 8);
	float angle_src,angle_match;
	contourPCA(contour_src,drawMat_src,angle_src);
	contourPCA(contour_match, drawMat_match, angle_match);

	//5.角度计算结果
	putText(drawMat_src, format("angleDiff: %f ", angle_src), Point2f(600, 40),
		CV_FONT_HERSHEY_COMPLEX, 0.5, Scalar(255, 255, 255), 0.5);

	putText(drawMat_match, format("angleDiff: %f ", angle_match), Point2f(600, 40),
		CV_FONT_HERSHEY_COMPLEX, 0.5, Scalar(255, 0, 0), 0.5);

	return 0;

}

算法的计算结果(angle=48.52°):

                        图 轮廓的主向量用绿色和黄色画出,图1的主向量角度angle1=31.04°,图2的主向量角度angle2=79.56°

3. 结果分析

结合算法的效率和精度,选择方法2计算旋转角度最佳,方法1和方法3可在特定的条件下应用。

 

4.相关资料:

1.answeOpenCV论坛:

http://answers.opencv.org/question/113492/orientation-of-two-contours/

http://answers.opencv.org/question/28489/how-to-compare-two-contours-translated-from-one-another/

http://answers.opencv.org/question/168357/way-to-filter-out-false-positives-in-template-matching/

http://answers.opencv.org/question/51486/template-matching-is-wrong-with-specific-reference-image/

http://answers.opencv.org/question/163569/is-template-matching-the-best-approach-to-go-about-when-finding-the-exact-image-on-the-screen-multiple-times/

2.GitHub:

https://github.com/Smorodov/LogPolarFFTTemplateMatcher/blob/master/fftm.cpp

3.博客资料:

3.1 图像矩

https://blog.csdn.net/kuweicai/article/details/79027388

3.2 图像的平移、镜像和旋转

https://blog.csdn.net/qq_20823641/article/details/51925091 

3.3 字符识别与区域定位

https://blog.csdn.net/u012556077/article/details/47126311

4.参考文献

1. Book:Robotics Vision and Control  Author:Peter Corke  Page:351-353

猜你喜欢

转载自blog.csdn.net/qiao_lili/article/details/83858199
今日推荐