29

我想以一种看起来像是来自圆柱体的投影的方式扭曲平面图像。

我有一个像这样的平面图像:

我有这个平面图像

我想在 2D 图像中显示它是这样的:

输出图像应该是这样的

我对几何投影有点退步了。我访问了类似这样的其他一些问题,但我不明白如何将这些圆柱坐标(theta 和 rho)表示为笛卡尔(x,y)平面中的 x,y 坐标。你们能帮我举一个详尽的例子吗?我正在为 iPhone 编写代码,我没有使用任何第三方库,如 OpenCV 等。

谢谢一堆。

4

1 回答 1

68

这是两部分的答案,数学和代码

数学

我喜欢这个问题,因为所涉及的投影很有趣,但数学仍然可以手动解决,没有太多困难。首先,重要的是要了解为什么图像会以这种方式扭曲。假设我们有一个平面图像,前面有一个凹圆柱体。

演示图片 1

第一步是进行正交投影,将图像移动到曲面上。

演示图 2

然后将这些点以透视的方式投影回图像平面上。请注意,在这种情况下,整个图像会缩小,因为圆柱体的所有部分都具有比图像平面更大的 z 坐标。在您的情况下,圆柱体在左右边缘接触图像平面,因此不会发生收缩。当这些点被投影回来时,它们不再在图像平面上形成一条直线,因为圆柱的 z 坐标随 x 变化,所以有一条曲线。

演示图 3

第一个技巧是我们实际上想要向后表示这个过程。您可能首先认为您想要获取原始图像中的每个像素并将其移动到新图像中。如果您检查新图像中出现在旧图像中的每个像素并设置其颜色,实际上效果会更好。这意味着你需要做 3 件事。

  1. 设置气缸参数
  2. 从相机投射一条射线,穿过新图像中的每个点,并在圆柱体上找到它的 x,y,z 坐标
  3. 使用正交投影将该射线移回图像平面(仅意味着删除 z 分量)

跟踪所有内容可能有点棘手,因此我将尝试使用一致的术语。首先,我假设你想保证你的圆柱体在边缘接触到你的图像。如果这是真的,那么您可以选择的 2 个自由参数是圆柱半径和焦距。

zx平面上的圆方程为

x^2+(z-z0)^2 = r^2

假设圆的中心位于 z 轴上。如果圆柱体的边缘将接触宽度为 w 焦距为 f 的像平面的边缘,则

omega^2+(f-z0)^2 = r^2 //define omega = width/2, it cleans it up a bit
z0 = f-sqrt(r^2-omega^2)

现在我们知道了圆柱体的所有参数,我们继续进行第 2 步,从相机投影线,通过 xim 处的图像平面到 xc 处的圆柱体。这是术语的快速图表。

演示4

我们知道我们投影的线从原点开始并在 xim 处穿过图像平面。我们可以把它的方程写成

x = xim*z/f

因为我们想要 x 坐标通过圆柱体时结合方程

xim^2*z^2/f^2 + z^2 - 2*z*z0 +z0^2 - r^2 = 0

您可以使用二次方程求解 z,然后将其代入线性方程以获得 x。这两个解决方案对应于线与圆相接触的两个位置,因为我们只对图像平面之后发生的那个感兴趣,并且那个总是会有更大的 x 坐标,使用 -b + sqrt(...) . 然后

xc = xim*z/f;
yc = yim*z/f;

移除正交投影的最后一步很简单,只需删除 z 分量即可。

代码

我知道您说过您没有使用 openCV,但我将在演示中将其用作图像容器。所有操作都是逐个像素完成的,因此您应该不难将其转换为适用于您正在使用的任何图像容器。首先,我创建了一个函数,将最终图像中的图像坐标转换为原始图像中的坐标。OpenCV 将其图像原点放在左上角,这就是为什么我从减去 w/2 和 h/2 开始,最后将它们加回

cv::Point2f convert_pt(cv::Point2f point,int w,int h)
{
    //center the point at 0,0
    cv::Point2f pc(point.x-w/2,point.y-h/2);

    //these are your free parameters
    float f = w;
    float r = w;

    float omega = w/2;
    float z0 = f - sqrt(r*r-omega*omega);

    float zc = (2*z0+sqrt(4*z0*z0-4*(pc.x*pc.x/(f*f)+1)*(z0*z0-r*r)))/(2* (pc.x*pc.x/(f*f)+1)); 
    cv::Point2f final_point(pc.x*zc/f,pc.y*zc/f);
    final_point.x += w/2;
    final_point.y += h/2;
    return final_point;
}

现在剩下的就是在旧图像上对新图像中的每个点进行采样。有很多方法可以做到这一点,我在这里做了我所知道的最简单的一种,双线性插值。此外,这仅设置为在灰度上工作,使其在颜色上工作很简单,只需将该过程应用于所有 3 个通道。我只是认为这样会更清楚一些。


for(int y = 0; y < height; y++)
{
    for(int x = 0; x < width; x++)
    {
        cv::Point2f current_pos(x,y);
        current_pos = convert_pt(current_pos, width, height);

        cv::Point2i top_left((int)current_pos.x,(int)current_pos.y); //top left because of integer rounding

        //make sure the point is actually inside the original image
        if(top_left.x < 0 ||
           top_left.x > width-2 ||
           top_left.y < 0 ||
           top_left.y > height-2)
        {
            continue;
        }

        //bilinear interpolation
        float dx = current_pos.x-top_left.x;
        float dy = current_pos.y-top_left.y;

        float weight_tl = (1.0 - dx) * (1.0 - dy);
        float weight_tr = (dx)       * (1.0 - dy);
        float weight_bl = (1.0 - dx) * (dy);
        float weight_br = (dx)       * (dy);

        uchar value =   weight_tl * image.at<uchar>(top_left) +
        weight_tr * image.at<uchar>(top_left.y,top_left.x+1) +
        weight_bl * image.at<uchar>(top_left.y+1,top_left.x) +
        weight_br * image.at<uchar>(top_left.y+1,top_left.x+1);

        dest_im.at<uchar>(y,x) = value;
    }
}

这是 f = w/2 和 r = w 的示例输出。

在此处输入图像描述

于 2012-08-19T19:58:54.123 回答