OpenCV warpPerspective - 如何确定目标图像的大小?

8

好的,我必须承认我对OpenCV是个初学者,我的MATLAB/lin.代数知识可能会引入一些偏见。但我想要做的事情非常简单,但我仍然没有找到答案。

在尝试对透视变换下的图像(或部分图像)进行矫正时,你基本上要执行两个步骤(假设你有定义畸变物体的4个点):

  1. 找到某个完美矩形与畸变形状之间的变换(在OpenCV中,通过findHomography()getPerspectiveTransform()实现 - 为什么这两个函数对同一组点的操作不同是另一个故事,并且令人沮丧);这会给我们一个矩阵T。
  2. 将T的逆应用于最初的畸变形状,将其转换为矩形(在OpenCV中,这可以使用warpPerspective()完成)。

现在,这个最后的函数(warpPerspective())要求用户指定目标图像的大小。

我的问题是用户如何预先知道那个大小。低级别的方法是直接将变换矩阵T应用于发现物体的图像的角点,从而保证新转换的形状不会超出边界。

但是,即使你将T中的矩阵取出并手动将其应用于这些点,结果看起来也很奇怪。

在OpenCV中有一种方法可以做到这一点吗?谢谢!

P.S. 以下是一些代码:

float leftX, lowerY, rightX, higherY;   

float minX = std::numeric_limits<float>::max(), maxX = std::numeric_limits<float>::min(), minY = std::numeric_limits<float>::max(), maxY = std::numeric_limits<float>::min();

Mat value, pt;
for(int i=0; i<4; i++)
{
    switch(i)
    {
        case 0:
            pt = (Mat_<float>(3, 1) << 1.00,1.00,1.00);                         
            break;
        case 1:
            pt = (Mat_<float>(3, 1) << srcIm.cols,1.00,1.00);
            break;
        case 2:
            pt = (Mat_<float>(3, 1) << 1.00,srcIm.rows,1.00);
            break;
        case 3:
            pt = (Mat_<float>(3, 1) << srcIm.cols,srcIm.rows,1.00);
            break;
        default:
            cerr << "Wrong switch." << endl;
            break;
    }               
    value = invH*pt;    
    value /= value.at<float>(2);        
    minX = min(minX,value.at<float>(0));
    maxX = max(maxX,value.at<float>(0));
    minY = min(minY,value.at<float>(1));
    maxY = max(maxY,value.at<float>(1));
}
leftX = std::min<float>(1.00,-minX);
lowerY = std::min<float>(1.00,-minY);
rightX = max(srcIm.cols-minX,maxX-minX);
higherY = max(srcIm.rows-minY,maxY-minY);

warpPerspective(srcIm, dstIm, H, Size(rightX-leftX,higherY-lowerY), cv::INTER_CUBIC);

更新:也许我的结果看起来不好是因为我使用的矩阵是错误的。由于我无法观察到getPerspectiveTransform()内部发生了什么,因此我无法知道如何计算这个矩阵,但它具有一些非常小和非常大的值,这使我认为它们是垃圾数据。 以下是我从T中获取数据的方法:

for(int row=0;row<3;row++)
    for(int col=0;col<3;col++)
        T.at<float>(row,col) = ((float*)(H.data + (size_t)H.step*row))[col];

尽管getPerspectiveTransform()的输出矩阵是3x3,但是尝试直接通过T.at<float>(row,col)访问其值会导致分段错误。这样做是否正确?也许这就是出现原始问题的原因,因为我没有得到正确的矩阵...

你的低级方式在我看来是正确的。你所说的“结果看起来很奇怪”是什么意思?你能否创建一个最小示例代码来展示问题? - Tobias Hermann
我的意思是我得到的坐标是在原始图像内部的(结果看起来很奇怪,因为我显示了一张图片,但它只是其中的一部分),这可能意味着矩阵T与我预期的不同。它被归一化或类似于此,不是直接操作点的意思吗? - bloodymir
@Dobi,我添加了一些代码,展示了我如何首先获取矩阵值。也许这就是错误出现的地方... - bloodymir
为什么不提供一个最小的示例,以便我们可以立即重现您的问题呢? :) - Tobias Hermann
3个回答

11

如果您在调用warpPerspective之前知道图像的大小,则可以使用perspectiveTransform转换其四个角的坐标,以查看它们在变换时的效果。假设它们不再形成一个漂亮的矩形,因此您可能需要计算最小值和最大值以获得边界框。然后,这个边界框的大小就是您想要的目标大小。(还要记得根据需要平移框,如果任何一个角落低于零)。这里是一个使用warpPerspective将变换后的图像叠加在自身上的Python示例。

from typing import Tuple
    import cv2
    import numpy as np
    import math

    # Input: a source image and perspective transform
    # Output: a warped image and 2 translation terms
    def perspective_warp(image: np.ndarray, transform: np.ndarray) -> Tuple[np.ndarray, int, int]:
        h, w = image.shape[:2]
        corners_bef = np.float32([[0, 0], [w, 0], [w, h], [0, h]]).reshape(-1, 1, 2)
        corners_aft = cv2.perspectiveTransform(corners_bef, transform)
        xmin = math.floor(corners_aft[:, 0, 0].min())
        ymin = math.floor(corners_aft[:, 0, 1].min())
        xmax = math.ceil(corners_aft[:, 0, 0].max())
        ymax = math.ceil(corners_aft[:, 0, 1].max())
        x_adj = math.floor(xmin - corners_aft[0, 0, 0])
        y_adj = math.floor(ymin - corners_aft[0, 0, 1])
        translate = np.eye(3)
        translate[0, 2] = -xmin
        translate[1, 2] = -ymin
        corrected_transform = np.matmul(translate, transform)
        return cv2.warpPerspective(image, corrected_transform, (math.ceil(xmax - xmin), math.ceil(ymax - ymin))), x_adj, y_adj

    # Just like perspective_warp, but it also returns an alpha mask that can be used for blitting
    def perspective_warp_with_mask(image: np.ndarray, transform: np.ndarray) -> Tuple[np.ndarray, np.ndarray, int, int]:
        mask_in = np.empty(image.shape, dtype = np.uint8)
        mask_in.fill(255)
        output, x_adj, y_adj = perspective_warp(image, transform)
        mask, _, _ = perspective_warp(mask_in, transform)
        return output, mask, x_adj, y_adj

    # alpha_blits src onto dest according to the alpha values in mask at location (x, y),
    # ignoring any parts that do not overlap
    def alpha_blit(dest: np.ndarray, src: np.ndarray, mask: np.ndarray, x: int, y: int) -> None:
        dl = max(x, 0)
        dt = max(y, 0)
        sl = max(-x, 0)
        st = max(-y, 0)
        sr = max(sl, min(src.shape[1], dest.shape[1] - x))
        sb = max(st, min(src.shape[0], dest.shape[0] - y))
        dr = dl + sr - sl
        db = dt + sb - st
        m = mask[st:sb, sl:sr]
        dest[dt:db, dl:dr] = (dest[dt:db, dl:dr].astype(np.float) * (255 - m) + src[st:sb, sl:sr].astype(np.float) * m) / 255

    # blits a perspective-warped src image onto dest
    def perspective_blit(dest: np.ndarray, src: np.ndarray, transform: np.ndarray) -> None:
        blitme, mask, x_adj, y_adj = perspective_warp_with_mask(src, transform)
        cv2.imwrite("blitme.png", blitme)
        alpha_blit(dest, blitme, mask, int(transform[0, 2] + x_adj), int(transform[1, 2] + y_adj))


    # Read an input image
    image: np.array = cv2.imread('input.jpg')

    # Make a perspective transform
    h, w = image.shape[:2]
    corners_in = np.float32([[[0, 0]], [[w, 0]], [[w, h]], [[0, h]]])
    corners_out = np.float32([[[100, 100]], [[300, -100]], [[500, 300]], [[-50, 500]]])
    transform = cv2.getPerspectiveTransform(corners_in, corners_out)

    # Blit the warped image on top of the original
    perspective_blit(image, image, transform)
    cv2.imwrite('output.jpg', image)

示例结果:

Left: input image. Right: output image


2

如果结果看起来很奇怪,可能是因为您在getPerspectiveTransform中没有正确设置点。您的点向量需要按正确顺序排列(左上角,右上角,右下角,左下角)。

但是,回答您最初的问题,不存在“最佳输出大小”。您必须根据您想要做的事情来决定。不断尝试找到适合您的大小。

编辑:

如果问题来自于变换矩阵,则如何创建它?在openCV中一个好的方法是这样的:

vector<Point2f> corners;
corners.push_back(topleft);
corners.push_back(topright);
corners.push_back(bottomright);
corners.push_back(bottomleft);


// Corners of the destination image
// output is the output image, should be defined before this operation
vector<cv::Point2f> output_corner;
output_corner.push_back(cv::Point2f(0, 0));
output_corner.push_back(cv::Point2f(output.cols, 0));
output_corner.push_back(cv::Point2f(output.cols, output.rows));
output_corner.push_back(cv::Point2f(0, output.rows));

// Get transformation matrix
Mat H = getPerspectiveTransform(corners, output_corner);

点的顺序是正确的,因为图像(或其部分)已经被正确矫正,除非您手动指定足够大的输出尺寸,否则它将被裁剪。顺便说一句,只要点的顺序始终相同,您提出的顺序不应该固定。如果试错是将输出图像适配到正确尺寸的唯一方法,那么我必须说这相当荒谬。 - bloodymir
你想用裁剪后的图像做什么?你要裁剪的细节大小是多少?这些都是无法自动检测的事情。 - Dimitri Mockelyn
如果我理解正确的话,这不是一个规模的问题,而是一个裁剪的问题。我想要做的是将图像大小适应矫正矩形的尺寸。有一种低级的方法可以实现这个目标(请参见我与Dobi的交流),但似乎并不起作用(从数学上讲,变形只是通过转换矩阵T将每个点相乘)。那么我怎么知道那个矩形的范围是多少呢? - bloodymir
你应该发布你的代码,以帮助我们理解问题在哪里。 - Dimitri Mockelyn
2
我不认为“尝试并尝试找到适合你的尺寸”非常有建设性。 - rbaleksandar
显示剩余3条评论

1
只晚了半个十年!我将逐一回答你的问题:
“我的问题是用户如何事先知道那个大小”
实际上,你只是缺少一步。我还建议仅出于便利而使用perspectiveTransform,而不是自己计算最小和最大X和Y。
因此,一旦你计算出最小的X和Y,要认识到它们可能是负数。如果它们是负数,这意味着你的图像将被裁剪。要解决这个问题,你需要创建一个平移矩阵,然后校正原始的单应性:
Mat translate = Mat::eye(3, 3, CV_64F);
translate.at<CV_64F>(2, 0) = -minX;
translate.at<CV_64F>(2, 1) = -minY;
Mat corrected_H = translate * H;

然后,目标大小的计算就是:
Size(maxX - minX, maxY - minY)

尽管如此,请注意您需要将minXmaxXminYmaxY转换为整数。 “由于我无法观察到getPerspectiveTransform()内部发生了什么,因此我不知道该矩阵是如何计算的”

https://github.com/opencv/opencv

这是OpenCV的源代码。你肯定可以观察getPerspectiveTransform内部发生了什么。

还有这个:https://docs.opencv.org/2.4/modules/imgproc/doc/geometric_transformations.html

getPerspectiveTransform没有很好的文档说明他们在做什么,但是findHomography函数有。我相信getPerspectiveTransform只是简单情况,当你恰好有足够解决8个参数所需的最少点数(即4对点,即四个角)时。


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