findHomography,getPerspectiveTransform和getAffineTransform是与图像处理相关的函数。

40
这个问题是关于OpenCV函数findHomography, getPerspectiveTransformgetAffineTransform的。
1.在findHomographygetPerspectiveTransform之间有什么区别?从文档中了解到,getPerspectiveTransform使用4个对应点计算变换(这是计算单应性/透视变换所需的最小值),而findHomography即使提供多于4个对应点也可以计算变换(可能使用最小二乘法等方法)。我的理解正确吗?如果是这样,那么OpenCV继续支持getPerspectiveTransform的唯一原因应该是为了向后兼容?
2.我想知道是否有一个相当于findHomography用于计算仿射变换的函数?即一个使用最小二乘法或等效鲁棒方法来计算仿射变换的函数。根据文档,getAffineTransform只需要3个对应点(这是计算仿射变换所需的最小量)。
最好,

1
也许estimateRigidTransform能够满足您的需求。 - cgat
5个回答

44

问题1: findHomography 试图找到两组点之间最佳的变换。它使用比最小二乘更智能的东西,叫做 RANSAC,它有能力拒绝离群值 - 如果至少50%+1的数据点是好的,RANSAC会尽最大努力找到它们,并建立可靠的变换。

getPerspectiveTransform 有许多有用的理由保留 - 它是 findHomography 的基础,在许多只有4个点的情况下非常有用,并且您知道这些点是正确的。通常使用 findHomography 自动检测点集 - 您可以找到许多这样的点集,但置信度较低。当您确定有4个角落时,如手动标记或自动检测矩形时,getPerspectiveTransform 是很好的选择。

问题2: 不存在仿射变换的等效方法。您可以使用 findHomography,因为仿射变换是同质变换的子集。


1
截至OpenCV 4.X版本,问题2的回答不正确:有estimateAffine2D和estimateAffine3D两个函数,它们是仿射变换的等效函数,类似于findHomography。 - Nicolas Busca

14

我同意@vasile的观点。我想要补充一些观察:

getPerspectiveTransform()getAffineTransform() 是用于处理已知正确对应关系的4个3个点(分别),在使用真实相机拍摄的实际图片中,无论是自动标记还是手动标记都不可能获得如此精确的对应关系。

总会有离群值。只需看一个简单的例子:想要通过点来拟合曲线(例如,采用带噪声的生成式y1 = f(x) = 3.12x + gauss_noise y2 = g(x) = 0.1x^2 + 3.1x + gauss_noise)。在这两种情况下,找到一个好的二次函数以估计点比找到一个好的线性函数容易得多。二次函数可能有点过头,但在大多数情况下不会(在去除离群值后),如果你想要拟合一条直线,你最好确信它是正确的模型,否则你会得到无用的结果。

话虽如此,如果你确信仿射变换是正确的,那么在这里提供一个建议:

  • 使用findHomography,该函数内置了RANSAC,以排除离群值并获取图像转换的初始估计
  • 选择3个正确匹配的点-对应关系(与找到的单应性一致),或者将第一个图像中的3个点(使用单应性)重新投影到第二个图像中
  • getAffineTransform()中使用这些(最接近正确的)3个匹配项
  • 如果需要,可以将所有这些步骤包装在你自己的findAffine()中 - 完成!

有没有一种方法可以找到最优的“仿射”矩阵?我想强制将单应性矩阵的最后一行设置为[0,0,1]。 - Royi
1
@Drazick 我写的“算法”几乎可以做到这一点——它使用findHomography来消除异常值,这样你就不必编写自己的RANSAC,然后你可以在任意3个点上使用getAffineTransform()来获得最接近最佳仿射变换。或者,你可以使用getAffineTransform()代替getPerspectiveTransform()作为核心函数编写自己的RANSAC算法。 - penelope
@penleope,我找到了一种解决方案,可以使用SVD以类似于估计最佳单应性矩阵的方式来查找最佳(L2明智的)仿射变换。 - Royi
@Drazick 很棒。能否用一篇回答来描述它? - penelope

4

对于超定方程组的仿射变换问题,有一个简单的解决方案。

  1. 需要注意的是,通常情况下,通过使用伪逆或类似技术,仿射变换可以找到线性方程组Ax=B的解,因此

x = (A At )-1 At B

此外,在核心openCV功能中,只需调用solve(A, B, X)即可处理此问题。

  1. 熟悉opencv/modules/imgproc/src/imgwarp.cpp中的仿射变换代码,它实际上只执行两个操作:

    a. 重新排列输入以创建Ax=B系统;

    b. 然后调用solve(A, B, X)。

注意:忽略openCV代码中的函数注释-它们会让人感到困惑,并且不反映矩阵中元素的实际顺序。如果您正在解决[u,v]’= Affine * [x,y,1],则重新排列如下:

         x1 y1 1 0  0  1
         0  0  0 x1 y1 1
         x2 y2 1 0  0  1
    A =  0  0  0 x2 y2 1
         x3 y3 1 0  0  1
         0  0  0 x3 y3 1

    X = [Affine11, Affine12, Affine13, Affine21, Affine22, Affine23]’

         u1 v1
    B =  u2 v2
         u3 v3 

您只需添加更多的点即可。想要使Solve(A, B, X)适用于过度确定的系统,请添加DECOMP_SVD参数。如果您想查看有关此主题的幻灯片,请使用此链接。如果您想在计算机视觉背景下了解伪逆的更多信息,最好的来源是:ComputerVision,请参见第15章和附录C。
如果您仍然不确定如何添加更多的点,请参考以下代码:
// extension for n points;
cv::Mat getAffineTransformOverdetermined( const Point2f src[], const Point2f dst[], int n )
{
    Mat M(2, 3, CV_64F), X(6, 1, CV_64F, M.data); // output
    double* a = (double*)malloc(12*n*sizeof(double));
    double* b = (double*)malloc(2*n*sizeof(double));
    Mat A(2*n, 6, CV_64F, a), B(2*n, 1, CV_64F, b); // input

    for( int i = 0; i < n; i++ )
    {
        int j = i*12;   // 2 equations (in x, y) with 6 members: skip 12 elements
        int k = i*12+6; // second equation: skip extra 6 elements
        a[j] = a[k+3] = src[i].x;
        a[j+1] = a[k+4] = src[i].y;
        a[j+2] = a[k+5] = 1;
        a[j+3] = a[j+4] = a[j+5] = 0;
        a[k] = a[k+1] = a[k+2] = 0;
        b[i*2] = dst[i].x;
        b[i*2+1] = dst[i].y;
    }

    solve( A, B, X, DECOMP_SVD );
    delete a;
    delete b;
    return M;
}

// call original transform
vector<Point2f> src(3);
vector<Point2f> dst(3);
src[0] = Point2f(0.0, 0.0);src[1] = Point2f(1.0, 0.0);src[2] = Point2f(0.0, 1.0);
dst[0] = Point2f(0.0, 0.0);dst[1] = Point2f(1.0, 0.0);dst[2] = Point2f(0.0, 1.0);
Mat M = getAffineTransform(Mat(src), Mat(dst));
cout<<M<<endl;
// call new transform
src.resize(4); src[3] = Point2f(22, 2);
dst.resize(4); dst[3] = Point2f(22, 2);
Mat M2 = getAffineTransformOverdetermined(src.data(), dst.data(), src.size());
cout<<M2<<endl;

4

关于第2个问题,estimateRigidTransform是getAffineTransform的扩展版本,它进行了过采样。我不知道在此帖子最初发布时它是否存在于OCV中,但在2.4中已经可用。


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接