一、仿射变换
在OpenCV中没有专门用于图像旋转的函数,而是通过图像的仿射变换实现图像的旋转。实现图像的旋转首先需要确定旋转角度和旋转中心,之后确定旋转矩阵,最终通过仿射变换实现图像旋转。OpenCV 4提供了getRotationMatrix2D()函数用于计算旋转矩阵和warpAffine()函数用于实现图像的仿射变换
1. getRotationMatrix2D()
cv::Mat cv::getRotationMatrix2D(
cv::Point2f center, // 旋转中心点坐标。 通常以图像的中心为旋转中心
double angle, // 旋转角度(正值表示逆时针旋转)
double scale // 缩放因子。 默认值为1.0,表示不进行缩放
);
返回值是一个 2x3 的仿射变换矩阵
为什么仿射变换矩阵是一个2*3的形式?
先看仿射变换的概率:仿射变换其实就是图像的旋转、平移和缩放操作的统称,可以表示为线性变换和平移变换的叠加。
仿射变换的数学表示是先乘以一个线形变换矩阵再加上一个平移向量,即:T = aX + b。但一个二维平面的像素坐标为(x,y),是一个1×2的向量矩阵,那公式就变成如下:
根据矩阵相乘的规则,其中线性变换矩阵A为2×2的矩阵,平移向量B为2×1的向量,两者结合就是一个2×3的变换矩阵。
2. warpAffine()
(1)getRotationMatrix2D()是为了得到一个仿射变换矩阵,然后通过 cv::warpAffine 函数应用到图像上进行旋转操作(如果我们已知图像旋转矩阵,可以自己生成旋转矩阵而不调用该函数)。
void cv::warpAffine(
cv::InputArray src, // 输入图像
cv::OutputArray dst, // 输出图像
cv::InputArray M, // 2x3 的仿射变换矩阵
cv::Size dsize, // 输出图像的尺寸
int flags = cv::INTER_LINEAR,// 插值方法,默认为线性插值
int borderMode = cv::BORDER_CONSTANT, // 边界填充模式,默认为常数填充
const cv::Scalar& borderValue = cv::Scalar() // 边界填充颜色,默认为黑色(0,0,0)
);
注:
常用的边界填充模式有 cv::BORDER_CONSTANT(常数填充)和 cv::BORDER_REPLICATE(复制边界像素)
边界填充颜色,仅当 borderMode 设置为 cv::BORDER_CONSTANT 时有效
(2)在使用 warpAffine() 函数进行旋转时,通常保留的是原图大小,这会导致目标保存的不完整。可以改变缩放因子来获得完整目标,但这会导致目标尺寸变小,如下:
#include <opencv2/opencv.hpp>
#include<iostream>
using namespace std;
int main()
{
cv::Mat img = cv::imread("C:/Users/Opencv/temp/lena.png", 0);
int h = img.rows;
int w = img.cols;
// 定义仿射旋转矩阵rM
cv::Point2f center(h / 2, w / 2); // 以图像中心为坐标
double angle = 30; // 设定30的逆时针旋转角度
double scale = 1.0; // 不进行缩放
// double scale = 0.5; // 缩放0.5
cv::Mat rM= cv::getRotationMatrix2D(center, angle, scale);
// 进行普通旋转操作(保存原图大小)
cv::Mat rotationImg1, rotationImg2;
cv::warpAffine(img, rotationImg1, rM, img.size());
(3)如果想要保持目标大小不变,就只能加大输出图片。
红色框是源图像 ,宽和高分别为 h 和 w,黑色框是逆时针旋转 θ 后的图像 。可以看到,如果旋转后图像的宽和高保持不变,那么肯定会有一部分图片会被裁掉。而如果想要保证旋转后图片的所有目标都保留下来,那么新图像就必须至少为浅蓝色框这么大。从图看出,浅蓝色框的尺寸为:
w
1
=
w
∗
c
o
s
θ
+
h
∗
s
i
n
θ
,
h
1
=
w
∗
s
i
n
θ
+
h
∗
c
o
s
θ
w 1=w∗cosθ+h∗sinθ , h 1=w∗sinθ+h∗cosθ
w1=w∗cosθ+h∗sinθ,h1=w∗sinθ+h∗cosθ
现在我们知道了在warpAffine() 中要设置的输出尺寸应为cv::Size(w1,h1)。但是还有一点:上图的红色框和蓝色框看起来中心点是一样的,因为红色框位于蓝色框中心位置。但实际上红色框应该是在左上角的,所以两个框的中心点存在偏移,而我们就需要计算出偏移量,根据偏移量来移动旋转后的图片,使其位于中心位置。
那怎么计算偏移量,并对旋转图片进行移动呢?
(1)偏移量就是蓝色框中心(newH, newW)和红色框中心(H,W)的差距,即
[
(
n
e
w
H
−
H
)
/
2
,
(
n
e
w
W
−
W
)
/
2
)
]
[(newH-H)/ 2,(newW - W) / 2) ]
[(newH−H)/2,(newW−W)/2)]
((2)前面说到,仿射变换矩阵是一个2×3的形式,而它的第三列就是平移向量b0,b1。所以在平移向量上加上偏移量就能对旋转图片进行移动到中心位置。
代码示例如下:
#include <opencv2/opencv.hpp>
#include <cmath>
#include<iostream>
using namespace std;
int main()
{
cv::Mat img = cv::imread("C:/Opencv/temp/lena.png", 0);
int h = img.rows;
int w = img.cols;
// 定义仿射旋转矩阵
cv::Point2f center(h / 2, w / 2);
double angle = 30;
double scale = 1;
cv::Mat rM= cv::getRotationMatrix2D(center, angle, scale);
// 进行普通旋转操作(保存原图大小)
cv::Mat rotationImg1, rotationImg2, rotationImg3;
cv::warpAffine(img, rotationImg1, rM, img.size());
//cv::imshow("旋转1,缩放0.5", rotationImg1);
// 进行特殊旋转操作(保存完整目标)
angle = angle / 180 * CV_PI; // 转为弧度制
// 新图的尺寸
int h1 = static_cast<int>(w * fabs(sin(angle)) + h * fabs(cos(angle)));
int w1 = static_cast<int>(w * fabs(cos(angle)) + h * fabs(sin(angle)));
cv::warpAffine(img, rotationImg2, rM, cv::Size(w1,h1));
cv::imshow("旋转2", rotationImg2);
// 加上偏移量
rM.at<double>(0, 2) += (w1 - w) / 2;
rM.at<double>(1, 2) += (h1 - h) / 2;
cv::warpAffine(img, rotationImg3, rM, cv::Size(w1, h1));
cv::imshow("旋转3", rotationImg3);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
注:
(1)在实际使用中,往往是把存在偏移角度的目标进行旋转,让目标保持水平。为此旋转角度通常为逆时针或超过180度,所以计算新尺寸时最好加上绝对值(C++中用库的fabs,python中用Numpy库的np.fbs)
(2)上述例子是自己设定旋转角度和旋转中心。在实际使用时,要旋转多少角度比较好呢? 其中一种方式就是使用minAreaRect()函数,它是获得目标最小外接矩阵的一种函数,返回值就有中心位置和与水平线的角度。将其作为getRotationMatrix2D()的输入参数,就能旋转图片,使目标呈水平线放置。还有一种方式,当你有一张原图和要旋转的结果图时,可以通过这两张图计算它们之间存在的仿射变换,从而得到一个仿射变换矩阵,关键函数getAffineTransform(),使用方法跟下面的透射变换的getPerspectiveTransform 函数相似。
二、透射变换
仿射变换是一种在平面上的操作,而透射变换可以看成一种空间上的操作,将图像从一个视角映射到另一个视角(OCR中常用)。OpenCV提供了cv::warpPerspective函数来进行透射变换。和仿射变换一样,需要一个透射变换矩阵,常用的方式就是使用 cv::getPerspectiveTransform 函数计算得到透视变换矩阵
getPerspectiveTransform 函数和上面提到的仿射变换的getAffineTransform()相似,只不过仿射变换只需要三个像素坐标,而透射变换中需要四个像素坐标。
注:源图像srcPoints的四个坐标要与目标图像dstPoints的四个坐标一一对应
cv::Mat perspectiveMatrix = cv::getPerspectiveTransform(srcPoints, dstPoints);
返回值是一个 3x3 的透射变换矩阵
warpPerspective函数和仿射变换的warpAffine函数使用方式一样。透射变换的关键问题是在于如何获取源图像的4个像素坐标和目标图像的4个像素坐标。常用的一种方式是使用角点检测,来获取目标的边界角点。
cv::Mat img = imread("111.png");
Point2f src_points[4];
Point2f dst_points[4];
//设定源图像4个点的像素坐标
src_points[0] = cv::Point2f(94.0, 374.0);
src_points[1] = cv::Point2f(507.0, 380.0);
src_points[2] = cv::Point2f(1.0, 623.0);
src_points[3] = cv::Point2f(627.0, 627.0);
//设置期望透视变换后这四个点的像素坐标
dst_points[0] = cv::Point2f(0.0, 0.0);
dst_points[1] = cv::Point2f(627.0, 0.0);
dst_points[2] = cv::Point2f(0.0, 627.0);
dst_points[3] = cv::Point2f(627.0, 627.0);
cv::Mat rotation, img_warp;
rotation = cv::getPerspectiveTransform(src_points, dst_points); //计算透视变换矩阵
cv::warpPerspective(img, img_warp, rotation, img.size()); //透视变换投影
三、极坐标变换
极坐标变换就是将图像在直角坐标系与极坐标系中互相变换,它可以将一圆形图像变换成一个矩形图像,常用于处理钟表、圆盘等图像。圆形图案边缘上的文字经过及坐标变换后可以垂直的排列在新图像的边缘,便于对文字的识别和检测。OpenCV中提供了cv::warpPolar()函数用于实现图像的极坐标变换
void cv::warpPolar(InputArray src,
OutputArray dst,
Size dsize, 目标图像大小
Point2f center, // 极坐标变换时极坐标的原点坐标
double maxRadius, // 变换时边界圆的半径,它也决定了逆变换时的比例参数
int flags // 插值方法与极坐标映射方法标志,两个方法之间通过“+”或者“|”号进行连接
)
可以对图像进行极坐标正变换也可以进行逆变换,关键在于最后一个参数如何选择 :
WARP_POLAR_LINEAR 极坐标变换
WARP_POLAR_LOG 半对数极坐标变换
WARP_INVERSE_MAP 逆变换
示例代码如下:
#include <opencv2/opencv.hpp>
#include<iostream>
using namespace std;
int main()
{
cv::Mat img = cv::imread("C:/Opencv/temp/yuan.png");
cv::Mat img1, img2;
cv::Point2f center = cv::Point2f(img.cols / 2, img.rows / 2); //极坐标在图像中的原点
// 正极坐标变换
cv::warpPolar(img, img1, cv::Size(400, 800), center, center.x, cv::INTER_LINEAR | cv::WARP_POLAR_LINEAR);
// 逆极坐标变换
cv::warpPolar(img1, img2, cv::Size(img.cols, img.rows), center, center.x, cv::INTER_LINEAR | cv::WARP_INVERSE_MAP);
cv::imshow("原图", img);
cv::imshow("正极坐标变换", img1);
cv::imshow("负极坐标变换", img2);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}