摘要
图像几何变换又称为图像空间变换, 它将一幅图像中的坐标位置映射到另一幅图像中的新坐标位置。几何变换不改变图像的像素值, 只是在图像平面上进行像素的重新安排。
几何变换大致分为仿射变换、投影变换、极坐标变换,完成几何变换需要两个独立的算法过程:
1、一个用来实现空间坐标变换的算法,用它描述每个像素如何从初始位置移动到终止位置
2、一个插值算法完成输出图像的每个像素的灰度值
放射变换
😀首先,先来分析一下放射变换的原理:
什么是放射变换?
仿射变换是从一个二维坐标系变换到另一个二维坐标系,属于线性变换。通过已知3对坐标点可以求得变换矩阵。(不共线的三对对应点,决定了唯一的变换矩阵)
其中
A
就是仿射变换矩阵一般形式,根据不同的变换,比如平移、缩放、旋转等等,仿射变换矩阵的值是不一样的。
1、平移
假设空间坐标 (x,y)先沿 x轴平移tx ,在沿 y轴平移 ty,则变换后的坐标为(x+tx,y+ty) ,此时平移变换为:
2、缩放
二维空间坐标 (x,y) 以任意一点 (x0,y0) 为中心在水平方向和垂直方向上分别缩放 sx和 sy倍,缩放后坐标为 (x0+sx (x-x0),y0+sy(y-y0)) ,通俗来讲就是缩放后的坐标离中心点的水平距离变为原坐标离中心点水平距离的 sx 倍。
当 (x0,y0) 为原点 (0,0)
当 (x0,y0) 为任意点
注意:等式右边的运算应该从右往左看
3、旋转
顺时针绕原点(0,0)旋转变换的矩阵表示为:
若以任意一点 (x0,y0)
注意:上面的运算顺序是从右向左的。
OpenCV提供的旋转函数,实现顺时针90°、180°、270°的旋转
rotate(InputArray src, Output dst, int rotateCode)
rotateCode有以下取值:
ROTATE_90_CLOCKWISE //顺时针旋转90度
ROTATE_180 //顺时针旋转180度
ROTATE_90_COUNTERCLOCKWISE //逆时针旋转90度
flip(src, dst, int flipCode)实现了图像的水平镜像、垂直镜像和逆时针旋转180°,不过并不是通过仿射变换实现的,而是通过行列互换,它与 rotate()、 transpose()函数一样都在core.hpp头文件中。
求解放射变换矩阵
以上都是知道变换前坐标求变换后的坐标,如果我们已经知道了变换前的坐标和变换后的坐标,想求出仿射变换矩阵,可以通过解方程法或矩阵法。
🤨解方程法
由于仿射变换矩阵
有6个未知数,所以我们只需三组坐标列出六个方程即可。OpenCV提供函数getAffineTransform(src, dst)通过方程法求解,其中src和dst分别为前后坐标。
对于C++来说,一种方式是将坐标存在Point2f数组中,另一种方法是保存在Mat中:
// 第一种方法
Point2f src1[] = {Pointy2f(0, 0), Point2f(200, 0), Point2f(0, 200)};
Point2f dst1[] = {Pointy2f(0, 0), Point2f(100, 0), Point2f(0, 100)};
// 第二种方法
Mat src2 = (Mat_<float>(3, 2) << 0, 0, 200, 0, 0, 200);
Mat dst2 = (Mat_<float>(3, 2) << 0, 0, 100, 0, 0, 100);
Mat A = getAffineTransform(src1, dst1);
😮矩阵法
对于等比例缩放的仿射变换,OpenCV提供函数getRotationMatrix2D(center, angle, scale)来计算矩阵,center是变换中心;angle是逆时针旋转的角度,(opencv中正角度代表逆时针旋转);scale是等比例缩放的系数。
我们通过下面的代码来定义这些参数,例如:
Point center = Point( src.cols/2, src.rows/2 ); //中心点
double angle = -50.0;
double scale = 0.6;
插值算法分析
在运算中,我们可能会遇到目标坐标有小数的情况,比如将坐标(3,3)缩放2倍变为了(1.5,1.5),但是对于图像来说并没有这个点,这时候我们就要用周围坐标的值来估算此位置的颜色,也就是插值。
(1)最近邻插值(INTER_NEAREST)
最近邻插值就是从(x,y)的四个相邻坐标中找到最近的那个来当作它的值,如(2.3,4.7),它的相邻坐标分别为(2,4)、(3,4)、(2,5)、(3,5),计算这几个相邻坐标与(2.3,4.7)坐标的距离,若最近的为(2,5),则取(2,5)的颜色值为的(2.3,4.7)值。
此种方法得到的图像会出现锯齿状外观,对于放大图像则更明显。
(2)双线性插值(INTER_LINEAR)(最常用)
进行放射变换
warpAffine进行放射变换。
warpAffine(src,dst,M,dsize,flags,bordMode, borderValue)
//src:输入图像
//dst:输出变换后图像,需要初始化一个空矩阵用来保存结果,不用设定矩阵尺寸
//M:2行3列的仿射变换矩阵
//dsize: 二元元组(宽,高),代表输出图像大小
//flags: 插值法 INTER_NEAREST、INTER_LINEAR(默认线性插值)
//bordMode: 边界像素模式,默认值BORDER_CONSTANT
//bordValue: 当填充模式为BORDER_CONSTANT时的填充值(默认为Scalar(),即0)
opencv实现放射变换
Mat src, dst_warp1, dst_warp2;
Point2f srcPoints[3];//原图中的三点 (一个包含三维点(x,y)的数组,其中x、y是浮点型数)
Point2f dstPoints[3];//目标图中的三点
int main(int argc, char** argv)
{
src = imread("D:/opencv练习图片/薛之谦.jpg");
namedWindow("SrcImage");
imshow("SrcImage", src);
//第一种仿射变换的调用方式:三点法
//三个点对的值,上面也说了,只要知道你想要变换后图的三个点的坐标,就可以实现仿射变换
srcPoints[0] = Point2f(0, 0);
srcPoints[1] = Point2f(0, src.rows);
srcPoints[2] = Point2f(src.cols, 0);
//映射后的三个坐标值
dstPoints[0] = Point2f(0, src.rows*0.3);
dstPoints[1] = Point2f(src.cols*0.25, src.rows*0.75);
dstPoints[2] = Point2f(src.cols*0.75, src.rows*0.25);
Mat M1 = getAffineTransform(srcPoints, dstPoints);//由三个点对计算变换矩阵
warpAffine(src, dst_warp1, M1, src.size());//仿射变换
//第二种仿射变换的调用方式:直接指定角度和比例
//旋转加缩放
Point2f center(src.cols / 2, src.rows / 2);//旋转中心
double angle = 45;//逆时针旋转45度
double scale = 0.5;//缩放比例
Mat M2 = getRotationMatrix2D(center, angle, scale);//计算旋转加缩放的变换矩阵
warpAffine(src, dst_warp2, M2, Size(src.cols, src.rows), INTER_LINEAR);//仿射变换
imshow("第一种放射变换", dst_warp1);
imshow("第二种放射变换", dst_warp2);
waitKey(0);
return 0;
}
投影变换(透视变换)
OpenCV提供函数getPerspectiveTransform(src, dst)实现求解投影矩阵,需要输入四组对应的坐标。
getPerspectiveTransform(const Point2f* src, const Point2f* dst)
//参数const Point2f* src:原图的四个固定顶点
//参数const Point2f* dst:目标图像的四个固定顶点
//返回值:Mat型变换矩阵,可直接用于warpAffine()函数
//注意,顶点数组长度超4个,则会自动以前4个为变换顶点;数组可用Point2f[]或Point2f*表示
OpenCV提供函数warpPerspective实现投影变换,参数说明和仿射变化类似。
warpPerspective(src,dst,M,dsize,flags,bordMode, borderValue)
//src:输入图像
//dst:输出变换后图像,需要初始化一个空矩阵用来保存结果,不用设定矩阵尺寸
//M:2行3列的仿射变换矩阵
//dsize: 二元元组(宽,高),代表输出图像大小
//flags: 插值法 INTER_NEAREST、INTER_LINEAR(默认线性插值)
//bordMode: 边界像素模式,默认值BORDER_CONSTANT
//bordValue: 当填充模式为BORDER_CONSTANT时的填充值(默认为Scalar(),即0)
😛opencv实现投影变换:
Mat src, dst_warp1;
Point2f srcPoints[4];//原图中的三点 (一个包含三维点(x,y)的数组,其中x、y是浮点型数)
Point2f dstPoints[4];//目标图中的三点
int main(int argc, char** argv)
{
src = imread("D:/opencv练习图片/薛之谦.jpg");
namedWindow("SrcImage");
imshow("SrcImage", src);
//变换前四点坐标
srcPoints[0] = Point2f(0, 0);
srcPoints[1] = Point2f(0, src.rows);
srcPoints[2] = Point2f(src.cols, 0);
srcPoints[3] = Point2f(src.cols, src.rows);
//变换后的四点坐标
dstPoints[0] = Point2f(src.cols*0.1, src.rows*0.1);
dstPoints[1] = Point2f(0, src.rows);
dstPoints[2] = Point2f(src.cols, 0);
dstPoints[3] = Point2f(src.cols*0.7, src.rows*0.8);
Mat M1 = getPerspectiveTransform(srcPoints, dstPoints);//由四个点对计算透视变换矩阵
warpPerspective(src, dst_warp1, M1, src.size());//投影变换
imshow("投影变换(四点法)", dst_warp1);
waitKey(0);
return 0;
}
极坐标变换
通常通过极坐标变化校正图像中的圆形物体或包含在圆环中的物体。
OpenCV 中提供了warpPolar()函数用于实现图像的极坐标变换,该函数的函数原型如下:
warpPolar( src, dst, Size dsize, Point2f center, double maxRadius, int flags )
- src:原图像,可以是灰度图像或者彩色图像
- dst:极坐标变换后输出图像(与原图像具有相同的数据类型和通道数)。
- dsize:输出图像大小。(自行决定)
- center:极坐标变换时极坐标原点在原图像中的位置。
- maxRadius:变换时边界圆的半径,它也决定了逆变换时的比例参数。
- flags: 插值方法与极坐标映射方法标志,两个方法之间通过“+”或者“|”号进行连接。
😄warpPolar()函数极坐标映射方法标志:
WARP_POLAR_LINEAR //极坐标正变换(直角坐标变换到极坐标)
WARP_POLAR_LOG //半对数极坐标变换
WARP_INVERSE_MAP //逆变换(极坐标变换到直角坐标)
😛opencv实现极坐标变换:
Mat src, dst;
int main(int argc, char** argv)
{
src = imread("D:/opencv练习图片/环形字符.jpg");
namedWindow("SrcImage");
imshow("SrcImage", src);
Point2f center = Point2f(src.cols / 2, src.rows / 2); //极坐标在图像中的原点
// 圆的半径
double maxRadius = min(center.y, center.x);
//正极坐标变换
warpPolar(src, dst, Size(200, 500), center, maxRadius, INTER_LINEAR | WARP_POLAR_LINEAR);
// 改变结果方向
rotate(dst, dst, ROTATE_90_COUNTERCLOCKWISE);
imshow("极坐标变换", dst);
waitKey(0);
return 0;
}
参考链接:【从零学习OpenCV 4】图像极坐标变换 - 知乎 (zhihu.com)
OpenCV算法学习笔记之几何变换 - 简书 (jianshu.com)