将图像扭曲以显示在圆柱投影中

29

我想将一张平面图像变形,使其看起来像是来自一个圆柱体的投影。

我有这样一张平面图像:

我有这张平面图像

我想将其展示为类似于以下2D图像的输出:

输出图像应该类似于此

对于几何投影的概念,我有些困惑。我查看了一些其他问题,如这个,但我不明白如何将这些圆柱坐标(theta和rho)表示为笛卡尔坐标系(x,y)平面上的x,y坐标。你们能用详细的实例帮助我吗? 我正在为iPhone编写代码,并且不使用任何第三方库,例如OpenCV等。

非常感谢。

1个回答

70

这是一个由数学和代码两部分组成的答案。

数学

我喜欢这个问题,因为涉及到的投影很有趣,但是数学上还是可以手工解决的,而且并不太困难。首先,重要的是要理解图像为什么会以这种方式变形。假设我们有一个平面图像,前面放着一个凹圆柱体。

Demo Image 1

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

Demo Image 2

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

Demo Image 3

第一个技巧是我们实际上希望反向表示这个过程。您可能首先想到要取原始图像中的每个像素并将其移动到新图像中。如果检查每个新图像中的像素在旧图像中出现的位置并设置其颜色,它实际上会更好地工作。这意味着您需要做三件事情。

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

跟踪所有东西可能有些棘手,因此我将尽力使用一致的术语。首先,我假设您希望保证您的圆柱体在图像边缘接触。如果是这样,那么您可以选择的两个自由参数是圆柱半径和焦距。

在 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)
现在我们已经知道了圆柱体的所有参数,接下来进入第二步,在相机处将线条投影到图像平面上的xim,然后再投影到圆柱体上的xc。这里是一个术语的快速示意图。 Demo4 我们知道要投影的线从原点开始,并且在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;
}
现在要做的就是在旧图像中对新图像中的每个点进行采样。有许多方法可以做到这一点,我在这里使用我所知道的最简单的方法——双线性插值。此外,这只适用于灰度图像,使其适用于彩色图像很简单,只需将该过程应用于所有三个通道即可。我只是觉得这样会更清晰一些。

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;
    }
}

enter image description here


另外,我想之前可能没有明确说明我的4张图片将组成一个360度的圆柱体。也就是说,每张图片将覆盖圆柱曲率的90度。 - Usman.3D
另一个详细说明:相机应该位于圆柱体的中心位置(以中心为视角,我们可以通过摄像头旋转并在4张图像中查看360度的全景)。因此,从相机到圆柱体任何点的射线长度始终等于半径。请根据这个详细说明反思或修改答案,谢谢。 - Usman.3D
@Usman.3D 这行计算zc的代码是 float zc = (2z0+sqrt(4z0z0-4(pc.xpc.x/(ff)+1)(z0z0-rr)))/(2 (pc.xpc.x/(f*f)+1)); 并且发布的答案中没有遗漏任何内容。 - Hammer
1
@Usman.3D,你说得完全正确。我不知道为什么会漏掉2*,我以为我只是把它粘贴进去了。祝好运。 - Hammer
@Hammer,你能告诉我如何使用OpenCV API实现相同的功能吗?或者请查看我的问题这里 - Kevin K
显示剩余5条评论

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