使用高度图扭曲图像?
我有一张高度图,它告诉我每个像素在Z方向上的偏移量。我的目标是仅通过这张高度图来把一张失真的图片变平。
我该怎么做呢?我知道相机的位置,这可能会有帮助。
为了实现这个目标,我在想,假设每个像素都是平面上的一个点,然后根据高度图中的Z值,垂直移动这些点。想象一下,从上面看这些点,移动会让你从不同的角度看到这些点的位置变化。
通过这种投影的移动,我可以提取出每个像素的X和Y方向的偏移量,然后把这些信息输入到cv.Remap()
中。
不过,我不知道如何用OpenCV获取一个点的投影3D偏移,更别提如何从中构建一个偏移图了。
这是我正在做的参考图:
我知道激光的角度是45度,从校准图像中,我可以很容易地计算出书本的高度:
h(x) = sin(theta) * abs(calibration(x) - actual(x))
我对两条线都这样做,然后线性插值这两条线,利用这种方法生成一个表面(Python代码,放在循环里):
height_grid[x][y] = heights_top[x] * (cv.GetSize(image)[1] - y) + heights_bottom[x] * y
希望这能帮到你;)
现在,这是我用来去扭曲图像的代码。中间那些奇怪的东西是把3D坐标投影到相机平面上,考虑到相机的位置(以及相机的旋转等):
class Point:
def __init__(self, x = 0, y = 0, z = 0):
self.x = x
self.y = y
self.z = z
mapX = cv.CreateMat(cv.GetSize(image)[1], cv.GetSize(image)[0], cv.CV_32FC1)
mapY = cv.CreateMat(cv.GetSize(image)[1], cv.GetSize(image)[0], cv.CV_32FC1)
c = Point(CAMERA_POSITION[0], CAMERA_POSITION[1], CAMERA_POSITION[2])
theta = Point(CAMERA_ROTATION[0], CAMERA_ROTATION[1], CAMERA_ROTATION[2])
d = Point()
e = Point(0, 0, CAMERA_POSITION[2] + SENSOR_OFFSET)
costx = cos(theta.x)
costy = cos(theta.y)
costz = cos(theta.z)
sintx = sin(theta.x)
sinty = sin(theta.y)
sintz = sin(theta.z)
for x in xrange(cv.GetSize(image)[0]):
for y in xrange(cv.GetSize(image)[1]):
a = Point(x, y, heights_top[x / 2] * (cv.GetSize(image)[1] - y) + heights_bottom[x / 2] * y)
b = Point()
d.x = costy * (sintz * (a.y - c.y) + costz * (a.x - c.x)) - sinty * (a.z - c.z)
d.y = sintx * (costy * (a.z - c.z) + sinty * (sintz * (a.y - c.y) + costz * (a.x - c.x))) + costx * (costz * (a.y - c.y) - sintz * (a.x - c.x))
d.z = costx * (costy * (a.z - c.z) + sinty * (sintz * (a.y - c.y) + costz * (a.x - c.x))) - sintx * (costz * (a.y - c.y) - sintz * (a.x - c.x))
mapX[y, x] = x + (d.x - e.x) * (e.z / d.z)
mapY[y, x] = y + (d.y - e.y) * (e.z / d.z)
print
print 'Remapping original image using map...'
remapped = cv.CreateImage(cv.GetSize(image), 8, 3)
cv.Remap(image, remapped, mapX, mapY, cv.CV_INTER_LINEAR)
现在这个话题变成了一大堆图像和代码……总之,这段代码在18MP的相机图像上运行需要7分钟;这实在是太慢了,而且最终这个方法对图像没有任何效果(每个像素的偏移量是<< 1
)。
有没有什么想法?
3 个回答
基于距离相机的失真只会在透视投影中发生。如果你知道一个像素的(x,y,z)位置,可以使用相机的投影矩阵把这些像素转换回世界坐标系。通过这些信息,你可以以正交的方式来渲染这些像素。不过,由于最初的透视投影,可能会缺少一些数据。
把你的场景分成以下几个部分:
- 你有一张未知的位图图像 I(x,y) -> (r,g,b),也就是每个点(x,y)对应的颜色值(r,g,b)。
- 你有一个已知的高度场 H(x,y) -> h,表示每个点的高度。
- 你有一个相机变换 C(x,y,z) -> (u,v),这个变换把场景投影到屏幕上。
需要注意的是,相机变换会丢失一些信息(你不能得到每个屏幕像素的深度值)。在屏幕上,可能会有场景重叠的部分,这种情况下只有最前面的部分会被显示,其他的会被丢弃。所以一般来说,这个过程不是完全可逆的。
- 你有一个截图 S(u,v),这是通过 C(x,y,H(x,y)) 得到的,x,y来自 I。
- 你想生成一个新的截图 S'(u',v'),这是通过 C(x,y,0) 得到的,x,y同样来自 I。
有两种明显的方法可以解决这个问题;这两种方法都依赖于准确的相机变换值。
射线投射:对于 S 中的每个像素,向场景中投射一条射线。找出它与高度场的交点;这会给你原始图像 I 中的(x,y),而屏幕像素则给你那个点的颜色。一旦你恢复了尽可能多的 I,再进行变换以找到 S'。
双重渲染:对于 I 中的每个 x,y,进行投影以找到 (u,v) 和 (u',v')。从 S(u,v) 中获取像素颜色,并将其复制到 S'(u',v')。
这两种方法都会遇到采样问题,可以通过超采样或插值来改善;方法1在被遮挡的区域会留下空白,方法2则会从第一个表面“投影”过去。
编辑:
我原以为你指的是计算机图形风格的高度场,其中 S 中的每个像素正好在 S' 中对应的位置上;但这并不是纸张如何在表面上铺展的方式。纸张是固定在脊柱上的,不会拉伸 - 抬起纸张的中心会把自由边缘拉向脊柱。
根据你的示例图像,你需要逆向处理这种累积的拉扯 - 检测脊柱中心线的位置和方向,然后逐步向左和向右工作,找出每个垂直纸条顶部和底部的高度变化,计算出结果的宽度缩小和倾斜,并将其逆转以重新创建原始的平面纸张。
我最后实现了自己的解决方案:
for x in xrange(cv.GetSize(image)[0]):
for y in xrange(cv.GetSize(image)[1]):
a = Point(x, y, heights_top[x / 2] * (cv.GetSize(image)[1] - y) + heights_bottom[x / 2] * y)
b = Point()
d.x = costy * (sintz * (a.y - c.y) + costz * (a.x - c.x)) - sinty * (a.z - c.z)
d.y = sintx * (costy * (a.z - c.z) + sinty * (sintz * (a.y - c.y) + costz * (a.x - c.x))) + costx * (costz * (a.y - c.y) - sintz * (a.x - c.x))
d.z = costx * (costy * (a.z - c.z) + sinty * (sintz * (a.y - c.y) + costz * (a.x - c.x))) - sintx * (costz * (a.y - c.y) - sintz * (a.x - c.x))
mapX[y, x] = x + 100.0 * (d.x - e.x) * (e.z / d.z)
mapY[y, x] = y + 100.0 * (d.y - e.y) * (e.z / d.z)
print
print 'Remapping original image using map...'
remapped = cv.CreateImage(cv.GetSize(image), 8, 3)
cv.Remap(image, remapped, mapX, mapY, cv.CV_INTER_LINEAR)
这个方法(虽然有点慢)是通过使用 cv.Remap
函数来重新映射每一个像素,看起来效果还不错...