目录
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 ¢er,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/
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