Skip to content

仿射变换

在本教程中,你将学到如何:

  • 使用 OpenCV 函数 cv::warpAffine 实现简单的重映射例程。
  • 使用 OpenCV 函数 cv::getRotationMatrix2D 获取一个 2×32 \times 3 旋转矩阵。
  1. 一种可以表示为矩阵乘法(线性变换)之后跟随一次向量加法(平移)的变换。

  2. 由上可知,我们可以用仿射变换(Affine Transformation)来表达:

    1. 旋转(线性变换)
    2. 平移(向量加法)
    3. 缩放操作(线性变换)

    可以看到,从本质上说,仿射变换表示的是两幅图像之间的一种关系。

  3. 表示仿射变换的常用方式是使用一个 2×32 \times 3 矩阵。

    A=[a00a01a10a11]2×2B=[b00b10]2×1A = \begin{bmatrix} a_{00} & a_{01} \\ a_{10} & a_{11} \end{bmatrix}_{2 \times 2} B = \begin{bmatrix} b_{00} \\ b_{10} \end{bmatrix}_{2 \times 1} M=[AB]=[a00a01b00a10a11b10]2×3M = \begin{bmatrix} A & B \end{bmatrix} = \begin{bmatrix} a_{00} & a_{01} & b_{00} \\ a_{10} & a_{11} & b_{10} \end{bmatrix}_{2 \times 3}

    考虑到我们要用 AA 和 BB 来变换一个二维向量 X=[xy]X = \begin{bmatrix}x \\ y\end{bmatrix},等价的做法是:

    T=A⋅[xy]+B或T=M⋅[x,y,1]TT = A \cdot \begin{bmatrix}x \\ y\end{bmatrix} + B \quad \text{或} \quad T = M \cdot [x, y, 1]^{T}

    a_{00}x + a_{01}y + b_{00} \\ a_{10}x + a_{11}y + b_{10} \end{bmatrix}$$
  1. 我们提到过,仿射变换本质上是两幅图像之间的一种关系。关于这种关系的信息,大致可以通过两种方式获得:

    1. 我们已知 XX 和 TT,并且知道它们相互关联。那么我们的任务就是求出 MM。
    2. 我们已知 MM 和 XX。要得到 TT,只需应用 T=M⋅XT = M \cdot X。我们关于 MM 的信息可能是显式的(即直接拥有这个 2×3 矩阵),也可能以点与点之间几何关系的形式给出。
  2. 让我们用更好的方式(即上面的第二种情形)来解释。既然 MM 关联两幅图像,我们可以分析最简单的情形:它关联两幅图像中的三个点。

    点 1、2、3(在图像 1 中构成一个三角形)被映射到图像 2 中,仍然构成一个三角形,但形状已经发生了明显的变化。如果我们用这 3 个点(可以任意选取)求出仿射变换,就可以把求得的关系应用到图像中的所有像素上。

  • 这个程序做什么?
    • 加载一幅图像
    • 对图像应用一个仿射变换。该变换由三个点之间的关系求得,我们使用 cv::warpAffine 函数完成。
    • 在变换之后对图像应用一次旋转,旋转中心为图像中心。
    • 等待用户退出程序。

C++: 本教程的代码如下所示。你也可以从 这里 下载。

/**
* @function Geometric_Transforms_Demo.cpp
* @brief Demo code for Geometric Transforms
* @author OpenCV team
*/
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>
using namespace cv;
using namespace std;
/**
* @function main
*/
int main( int argc, char** argv )
{
CommandLineParser parser( argc, argv, "{@input | lena.jpg | input image}" );
Mat src = imread( samples::findFile( parser.get<String>( "@input" ) ) );
if( src.empty() )
{
cout << "Could not open or find the image!\n" << endl;
cout << "Usage: " << argv[0] << " <Input image>" << endl;
return -1;
}
Point2f srcTri[3];
srcTri[0] = Point2f( 0.f, 0.f );
srcTri[1] = Point2f( src.cols - 1.f, 0.f );
srcTri[2] = Point2f( 0.f, src.rows - 1.f );
Point2f dstTri[3];
dstTri[0] = Point2f( 0.f, src.rows*0.33f );
dstTri[1] = Point2f( src.cols*0.85f, src.rows*0.25f );
dstTri[2] = Point2f( src.cols*0.15f, src.rows*0.7f );
Mat warp_mat = getAffineTransform( srcTri, dstTri );
/// Set the dst image the same type and size as src
Mat warp_dst = Mat::zeros( src.rows, src.cols, src.type() );
warpAffine( src, warp_dst, warp_mat, warp_dst.size() );
/** Rotating the image after Warp */
Point center = Point( warp_dst.cols/2, warp_dst.rows/2 );
double angle = -50.0;
double scale = 0.6;
Mat rot_mat = getRotationMatrix2D( center, angle, scale );
Mat warp_rotate_dst;
warpAffine( warp_dst, warp_rotate_dst, rot_mat, warp_dst.size() );
imshow( "Source image", src );
imshow( "Warp", warp_dst );
imshow( "Warp + Rotate", warp_rotate_dst );
waitKey();
return 0;
}

Java: 本教程的代码如下所示。你也可以从 这里 下载。

import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
class GeometricTransforms {
public void run(String[] args) {
String filename = args.length > 0 ? args[0] : "../data/lena.jpg";
Mat src = Imgcodecs.imread(filename);
if (src.empty()) {
System.err.println("Cannot read image: " + filename);
System.exit(0);
}
Point[] srcTri = new Point[3];
srcTri[0] = new Point( 0, 0 );
srcTri[1] = new Point( src.cols() - 1, 0 );
srcTri[2] = new Point( 0, src.rows() - 1 );
Point[] dstTri = new Point[3];
dstTri[0] = new Point( 0, src.rows()*0.33 );
dstTri[1] = new Point( src.cols()*0.85, src.rows()*0.25 );
dstTri[2] = new Point( src.cols()*0.15, src.rows()*0.7 );
Mat warpMat = Imgproc.getAffineTransform( new MatOfPoint2f(srcTri), new MatOfPoint2f(dstTri) );
Mat warpDst = Mat.zeros( src.rows(), src.cols(), src.type() );
Imgproc.warpAffine( src, warpDst, warpMat, warpDst.size() );
/** Rotating the image after Warp */
Point center = new Point(warpDst.cols() / 2, warpDst.rows() / 2);
double angle = -50.0;
double scale = 0.6;
Mat rotMat = Imgproc.getRotationMatrix2D( center, angle, scale );
Mat warpRotateDst = new Mat();
Imgproc.warpAffine( warpDst, warpRotateDst, rotMat, warpDst.size() );
HighGui.imshow( "Source image", src );
HighGui.imshow( "Warp", warpDst );
HighGui.imshow( "Warp + Rotate", warpRotateDst );
HighGui.waitKey(0);
System.exit(0);
}
}
public class GeometricTransformsDemo {
public static void main(String[] args) {
// Load the native OpenCV library
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
new GeometricTransforms().run(args);
}
}

Python: 本教程的代码如下所示。你也可以从 这里 下载。

from __future__ import print_function
import cv2 as cv
import numpy as np
import argparse
parser = argparse.ArgumentParser(description='Code for Affine Transformations tutorial.')
parser.add_argument('--input', help='Path to input image.', default='lena.jpg')
args = parser.parse_args()
src = cv.imread(cv.samples.findFile(args.input))
if src is None:
print('Could not open or find the image:', args.input)
exit(0)
srcTri = np.array( [[0, 0], [src.shape[1] - 1, 0], [0, src.shape[0] - 1]] ).astype(np.float32)
dstTri = np.array( [[0, src.shape[1]*0.33], [src.shape[1]*0.85, src.shape[0]*0.25], [src.shape[1]*0.15, src.shape[0]*0.7]] ).astype(np.float32)
warp_mat = cv.getAffineTransform(srcTri, dstTri)
warp_dst = cv.warpAffine(src, warp_mat, (src.shape[1], src.shape[0]))
# Rotating the image after Warp
center = (warp_dst.shape[1]//2, warp_dst.shape[0]//2)
angle = -50
scale = 0.6
rot_mat = cv.getRotationMatrix2D( center, angle, scale )
warp_rotate_dst = cv.warpAffine(warp_dst, rot_mat, (warp_dst.shape[1], warp_dst.shape[0]))
cv.imshow('Source image', src)
cv.imshow('Warp', warp_dst)
cv.imshow('Warp + Rotate', warp_rotate_dst)
cv.waitKey()

Warp_Affine_Tutorial_Theory_0.jpg

  • 加载一幅图像:

    CommandLineParser parser( argc, argv, "{@input | lena.jpg | input image}" );
    Mat src = imread( samples::findFile( parser.get<String>( "@input" ) ) );
    if( src.empty() )
    {
    cout << "Could not open or find the image!\n" << endl;
    cout << "Usage: " << argv[0] << " <Input image>" << endl;
    return -1;
    }
  • 仿射变换: 如上文所述,我们需要两组各 3 个点来推导仿射变换关系。请看:

    Point2f srcTri[3];
    srcTri[0] = Point2f( 0.f, 0.f );
    srcTri[1] = Point2f( src.cols - 1.f, 0.f );
    srcTri[2] = Point2f( 0.f, src.rows - 1.f );
    Point2f dstTri[3];
    dstTri[0] = Point2f( 0.f, src.rows*0.33f );
    dstTri[1] = Point2f( src.cols*0.85f, src.rows*0.25f );
    dstTri[2] = Point2f( src.cols*0.15f, src.rows*0.7f );

    你可能想把这些点画出来,以便更好地理解它们如何变化。它们的位置大致与示例图(理论部分)中描绘的位置相同。你可以注意到,由这 3 个点定义的三角形的大小和朝向都发生了变化。

  • 有了这两组点之后,我们使用 OpenCV 函数 cv::getAffineTransform 计算仿射变换:

    Mat warp_mat = getAffineTransform( srcTri, dstTri );

    输出是一个 2×32 \times 3 矩阵(此处为 warp_mat)。

  • 然后,我们把刚求得的仿射变换应用到源图像上:

    /// Set the dst image the same type and size as src
    Mat warp_dst = Mat::zeros( src.rows, src.cols, src.type() );
    warpAffine( src, warp_dst, warp_mat, warp_dst.size() );

    其参数如下:

    • src:输入图像

    • warp_dst:输出图像

    • warp_mat:仿射变换

    • warp_dst.size():输出图像的期望大小

      我们刚刚得到了第一幅变换后的图像!稍后我们会显示它。在那之前,我们还想对它做旋转……

  • 旋转: 要旋转一幅图像,我们需要知道两件事:

    1. 图像绕哪个中心旋转。
    2. 旋转的角度。在 OpenCV 中,正角度表示逆时针方向。
    3. 可选: 一个缩放因子。

    我们用下面的代码片段定义这些参数:

    Point center = Point( warp_dst.cols/2, warp_dst.rows/2 );
    double angle = -50.0;
    double scale = 0.6;
  • 我们使用 OpenCV 函数 cv::getRotationMatrix2D 生成旋转矩阵,它返回一个 2×32 \times 3 矩阵(此处为 rot_mat):

    Mat rot_mat = getRotationMatrix2D( center, angle, scale );
  • 现在,我们把求得的旋转应用到上一步变换的输出上:

    Mat warp_rotate_dst;
    warpAffine( warp_dst, warp_rotate_dst, rot_mat, warp_dst.size() );
  • 最后,我们在两个窗口中显示结果,并附上原图:

    imshow( "Source image", src );
    imshow( "Warp", warp_dst );
    imshow( "Warp + Rotate", warp_rotate_dst );
  • 剩下的就是等待用户退出程序:

    waitKey();

Warp_Affine_Tutorial_Original_Image.jpg Warp_Affine_Tutorial_Result_Warp.jpg Warp_Affine_Tutorial_Result_Warp_Rotate.jpg

  • 编译上面的代码后,我们可以把图像路径作为参数来运行它。例如,对于这样一幅图片:

    在应用第一个仿射变换后,我们得到:

    最后,在应用一次负角度旋转(记住,负角度表示顺时针)和一个缩放因子后,我们得到: