计算透视变换目标图像的宽高比

12

我最近在我的安卓应用中使用OpenCV实现了透视变换(Perspective Transform)。几乎一切都没有问题,但是其中一个方面需要更多的工作。

问题在于我不知道如何计算透视变换中目标图像的正确宽高比(无需手动设置),使其可以根据摄像头的角度计算出图像和真实物体/图像尺寸的宽高比(不考虑摄像头角度)。请注意,起始坐标并不形成梯形,而是形成四边形。

如果我有一本书以约45度角拍摄的照片,并且我想要目标图像的宽高比与这本书的宽高比相同。由于只有2D照片很难做到,但是CamScanner应用程序完美地实现了这一点。我已经找到了一种非常简单的方法来计算我的目标图像的大小(并没有期望它可以按照我想要的方式工作),但是它使得从45度角的图像短了大约20%,当角度降低时,图像高度显著降低,而CamScanner尽管角度不一样也能完美实现:

enter image description here

在这里,CamScanner保持目标图像(第二个)的宽高比与书本一样,即使在约20度的角度下也基本准确。

同时,我的代码看起来像这样(在计算目标图像大小时我并没有打算按照这个问题所要求的方式工作):

public static Mat PerspectiveTransform(Point[] cropCoordinates, float ratioW, float ratioH, Bitmap croppedImage)
{
    if (cropCoordinates.length != 4) return null;

    double width1, width2, height1, height2, avgw, avgh;
    Mat src = new Mat();
    List<Point> startCoords = new ArrayList<>();
    List<Point> resultCoords = new ArrayList<>();

    Utils.bitmapToMat(croppedImage, src);

    for (int i = 0; i < 4; i++)
    {
        if (cropCoordinates[i].y < 0 ) new Point(cropCoordinates[i].x, 0);
        startCoords.add(new Point(cropCoordinates[i].x * ratioW, cropCoordinates[i].y * ratioH));
    }

    width1 = Math.sqrt(Math.pow(startCoords.get(2).x - startCoords.get(3).x,2) + Math.pow(startCoords.get(2).y - startCoords.get(3).y,2));
    width2 = Math.sqrt(Math.pow(startCoords.get(1).x - startCoords.get(0).x,2) + Math.pow(startCoords.get(1).y - startCoords.get(0).y,2));
    height1 = Math.sqrt(Math.pow(startCoords.get(1).x - startCoords.get(2).x, 2) + Math.pow(startCoords.get(1).y - startCoords.get(2).y, 2));
    height2 = Math.sqrt(Math.pow(startCoords.get(0).x - startCoords.get(3).x, 2) + Math.pow(startCoords.get(0).y - startCoords.get(3).y, 2));
    avgw = (width1 + width2) / 2;
    avgh = (height1 + height2) / 2;

    resultCoords.add(new Point(0, 0));
    resultCoords.add(new Point(avgw-1, 0));
    resultCoords.add(new Point(avgw-1, avgh-1));
    resultCoords.add(new Point(0, avgh-1));

    Mat start = Converters.vector_Point2f_to_Mat(startCoords);
    Mat result = Converters.vector_Point2d_to_Mat(resultCoords);
    start.convertTo(start, CvType.CV_32FC2);
    result.convertTo(result,CvType.CV_32FC2);

    Mat mat = new Mat();
    Mat perspective = Imgproc.getPerspectiveTransform(start, result);
    Imgproc.warpPerspective(src, mat, perspective, new Size(avgw, avgh));

    return mat;
}

而从相对同一角度,我的方法产生了这个结果:

输入图像描述

我想知道的是如何实现?我很感兴趣,他们是如何通过仅有4个角的坐标来计算对象的长度。如果可能的话,请提供一些代码、数学说明或类似/相同的文章。

提前致谢。


你知道实际物体的长宽比吗(因为float ratioW,float ratioH是输入参数)? - Micka
3
好的,我猜我知道如何计算真实矩形物体的宽高比。我回想起我的计算机图形学课程,并记住了使用2点透视法来测量投影中的距离。一旦你知道真实物体的宽高比,你就能解决问题了,对吧?请查看以下链接:http://computergraphics.stackexchange.com/questions/1762/calculate-aspect-ratio-from-2d-shape-in-3d-space 和 http://www.handprint.com/HP/WCL/perspect3.html。 - Micka
看了我提供的示例图像,我认为该方法仍然可能不起作用(顺便说一句,这是一个有趣的好资源),因为似乎只有一个消失点(在顶部),因为顶部和底部线条几乎平行,所以比率会非常高。如果我错了,请告诉我。 - Dainius Šaltenis
@DainiusŠaltenis,也许有类似的方法适用于一点透视,但我不确定。 - Micka
2
你可能会发现https://dev59.com/yHM_5IYBdhLWcg3w3nNs很有趣。 - jodag
显示剩余4条评论
2个回答

20

这在 Stack Overflow 上曾经被提到过几次,但我从未见过完整的回答。因此,现在就由我来回答吧。这里的实现基于此论文,该论文推导出了完整的方程式:http://research.microsoft.com/en-us/um/people/zhang/papers/tr03-39.pdf

本质上,它表明在假设针孔相机模型的情况下,可以计算出投影矩形的纵横比(但是可以预料地无法获得缩放比例)。实际上,可以解出焦距,然后得到纵横比。下面是一个使用OpenCV的Python示例实现。请注意,您需要正确排序四个检测到的角落,否则它将不起作用(请注意顺序,它是一条之字形)。报告的误差率在3-5%的范围内。

import math
import cv2
import scipy.spatial.distance
import numpy as np

img = cv2.imread('img.png')
(rows,cols,_) = img.shape

#image center
u0 = (cols)/2.0
v0 = (rows)/2.0

#detected corners on the original image
p = []
p.append((67,74))
p.append((270,64))
p.append((10,344))
p.append((343,331))

#widths and heights of the projected image
w1 = scipy.spatial.distance.euclidean(p[0],p[1])
w2 = scipy.spatial.distance.euclidean(p[2],p[3])

h1 = scipy.spatial.distance.euclidean(p[0],p[2])
h2 = scipy.spatial.distance.euclidean(p[1],p[3])

w = max(w1,w2)
h = max(h1,h2)

#visible aspect ratio
ar_vis = float(w)/float(h)

#make numpy arrays and append 1 for linear algebra
m1 = np.array((p[0][0],p[0][1],1)).astype('float32')
m2 = np.array((p[1][0],p[1][1],1)).astype('float32')
m3 = np.array((p[2][0],p[2][1],1)).astype('float32')
m4 = np.array((p[3][0],p[3][1],1)).astype('float32')

#calculate the focal disrance
k2 = np.dot(np.cross(m1,m4),m3) / np.dot(np.cross(m2,m4),m3)
k3 = np.dot(np.cross(m1,m4),m2) / np.dot(np.cross(m3,m4),m2)

n2 = k2 * m2 - m1
n3 = k3 * m3 - m1

n21 = n2[0]
n22 = n2[1]
n23 = n2[2]

n31 = n3[0]
n32 = n3[1]
n33 = n3[2]

f = math.sqrt(np.abs( (1.0/(n23*n33)) * ((n21*n31 - (n21*n33 + n23*n31)*u0 + n23*n33*u0*u0) + (n22*n32 - (n22*n33+n23*n32)*v0 + n23*n33*v0*v0))))

A = np.array([[f,0,u0],[0,f,v0],[0,0,1]]).astype('float32')

At = np.transpose(A)
Ati = np.linalg.inv(At)
Ai = np.linalg.inv(A)

#calculate the real aspect ratio
ar_real = math.sqrt(np.dot(np.dot(np.dot(n2,Ati),Ai),n2)/np.dot(np.dot(np.dot(n3,Ati),Ai),n3))

if ar_real < ar_vis:
    W = int(w)
    H = int(W / ar_real)
else:
    H = int(h)
    W = int(ar_real * H)

pts1 = np.array(p).astype('float32')
pts2 = np.float32([[0,0],[W,0],[0,H],[W,H]])

#project the image with the new w/h
M = cv2.getPerspectiveTransform(pts1,pts2)

dst = cv2.warpPerspective(img,M,(W,H))

cv2.imshow('img',img)
cv2.imshow('dst',dst)
cv2.imwrite('orig.png',img)
cv2.imwrite('proj.png',dst)

cv2.waitKey(0)

原始内容:

enter image description here

预测的结果(由于我从您的截图中裁剪了图像,因此分辨率非常低,但长宽比似乎正确):

enter image description here


2
有没有想法为什么fSquare会得到负数,导致f=nAn?我试图在Java中实现这个功能... 编辑:这种情况只是偶尔发生...我的double是否可能溢出了? - 1resu
1
不确定是否有人在使用这个,但我解决了f = NaN的问题。如果图像倾斜,使得顶部的宽度小于底部的宽度,则sqrt中的数字为负数,因此必须像上面的代码一样对其取反。然而,如果顶部比底部宽,则该数字为正数,因此否定会引起问题。如果您用np.abs替换sqrt中的负数,则应该可以解决问题。 - David
奇异性怎么办?如果n23和/或n33接近于零,则系统无解。根据论文,当图像已经是矩形时,这种情况应该会发生,这将使问题易于解决。但在我的经验中,每当两条对立的线平行时(这并不奇怪),这种情况就会发生。我还没有找到在这种特定方法中解决这个问题的最佳方法。 - Elte Hupkes
1
更多的谷歌搜索得到了这篇博客文章:http://andrewkay.name/blog/post/aspect-ratio-of-a-rectangle-in-perspective/。最后他提到,如果两条线平行而另外两条线不是,则无法解决该问题。我猜那就解决了... - Elte Hupkes
1
如果你把几乎平行的线当作完全平行的线,并使用适当的计算方法来得出精确的比率,那么这个Java版本就能正常工作。在我的应用程序中,宽高比的视觉差异几乎不可察觉...只有几个像素而已。基本上,在"数值失控"之前,我会预先处理好这个问题。 - carl
显示剩余9条评论

1
感谢y300和https://dev59.com/yHM_5IYBdhLWcg3w3nNs#1222855这篇文章,我已经在Java中实现了它。如果有人遇到与我相同的问题将其转换为Java,我会把它留在这里...
public float getRealAspectRatio(int imageWidth, int imageHeight) {

    double u0 = imageWidth/2;
    double v0 = imageHeight/2;
    double m1x = mTopLeft.x - u0;
    double m1y = mTopLeft.y - v0;
    double m2x = mTopRight.x - u0;
    double m2y = mTopRight.y - v0;
    double m3x = mBottomLeft.x - u0;
    double m3y = mBottomLeft.y - v0;
    double m4x = mBottomRight.x - u0;
    double m4y = mBottomRight.y - v0;

    double k2 = ((m1y - m4y)*m3x - (m1x - m4x)*m3y + m1x*m4y - m1y*m4x) /
            ((m2y - m4y)*m3x - (m2x - m4x)*m3y + m2x*m4y - m2y*m4x) ;

    double k3 = ((m1y - m4y)*m2x - (m1x - m4x)*m2y + m1x*m4y - m1y*m4x) /
            ((m3y - m4y)*m2x - (m3x - m4x)*m2y + m3x*m4y - m3y*m4x) ;

    double f_squared =
            -((k3*m3y - m1y)*(k2*m2y - m1y) + (k3*m3x - m1x)*(k2*m2x - m1x)) /
                    ((k3 - 1)*(k2 - 1)) ;

    double whRatio = Math.sqrt(
            (Math.pow((k2 - 1),2) + Math.pow((k2*m2y - m1y),2)/f_squared + Math.pow((k2*m2x - m1x),2)/f_squared) /
                    (Math.pow((k3 - 1),2) + Math.pow((k3*m3y - m1y),2)/f_squared + Math.pow((k3*m3x - m1x),2)/f_squared)
    ) ;

    if (k2==1 && k3==1 ) {
        whRatio = Math.sqrt(
                (Math.pow((m2y-m1y),2) + Math.pow((m2x-m1x),2)) /
                        (Math.pow((m3y-m1y),2) + Math.pow((m3x-m1x),2)));
    }

    return (float)(whRatio);
}

我已经成功地处理了所有边都倾斜的四边形,但当两条线平行时,它不能给出准确的结果... 我不确定方程式的最后一部分 "if (k2==1 && k3==1)" 是否是处理平行线的部分? - carl
1
我发现问题不在于平行线,而是当线条“非常接近”平行时,计算误差非常大,因为线条的交点趋近于无穷大... 因此,我添加了一个if then语句来检测线条是否趋近于平行,然后将其恢复到第二个“whRatio”计算,就好像它们完全平行一样。此时,第一次和第二次计算之间的实际纵横比差异非常小,可以使用第二次计算而没有任何可感知的视觉差异。 - carl

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