这是一个重要的问题,而且我很惊讶任何标准库都没有更好地解决它(至少据我所知)。
我对已接受的解决方案并不满意,因为它没有利用变换的隐式平滑性。我可能会错过一些重要情况,但我无法想象既具有可逆性又在像素尺度上非平滑的映射。
平滑性意味着无需计算最近邻:最近的点是原始网格上已经靠近的那些点。
我的解决方案利用了这样一个事实,即在原始映射中,一个正方形[(i,j),(i+1,j),(i+1,j+1),(i,j+1)]映射到一个四边形 [(X[i,j], Y[i,j], X[i+1,j], Y[i+1,j], ...],该四边形内没有其他点。然后反向映射仅需要在四边形内进行插值。为此,我使用了一个反双线性插值,它将在顶点和任何其他仿射变换处给出精确结果。
该实现除了 numpy
之外没有任何其他依赖项。逻辑是遍历所有四边形,并逐步构建反向映射。我在这里复制代码,希望有足够的注释使想法变得足够清晰。
关于不太明显的内容有一些评论:
- 反双线性函数通常只返回范围为[0,1]的坐标。我删除了剪辑操作,因此超出范围的值意味着该坐标在四边形外(这是解决多边形中的点问题的一种扭曲方式!)。为了避免在边缘上丢失点,实际上我允许点超出 [0,1] 范围,这通常意味着一个索引可能被两个相邻的四边形拾取。在这些罕见的情况下,我只让结果成为两个结果的平均值,相信超出范围的点以合理的方式“外推”。
- 总的来说,所有四边形都具有不同的形状,它们与正则网格的重叠程度可以从完全没有到非常多的点。该例程一次解决所有四边形(利用
bilinear_inverse
的向量化特性),但在每次迭代中仅选择坐标(偏移其边界框)有效的四边形。
import numpy as np
def bilinear_inverse(p, vertices, numiter=4):
"""
Compute the inverse of the bilinear map from the unit square
[(0,0), (1,0), (1,1), (0,1)]
to the quadrilateral vertices = [p0, p1, p2, p4]
Parameters:
----------
p: array of shape (2, ...)
Points on which the inverse transforms are applied.
vertices: array of shape (4, 2, ...)
Coordinates of the vertices mapped to the unit square corners
numiter:
Number of Newton interations
Returns:
--------
s: array of shape (2, ...)
Mapped points.
This is a (more general) python implementation of the matlab implementation
suggested in https://dev59.com/5HRA5IYBdhLWcg3w1BmW#18332009
"""
p = np.asarray(p)
v = np.asarray(vertices)
sh = p.shape[1:]
if v.ndim == 2:
v = np.expand_dims(v, axis=tuple(range(2, 2 + len(sh))))
s = .5 * np.ones((2,) + sh)
s0, s1 = s
for k in range(numiter):
r = v[0] * (1 - s0) * (1 - s1) + v[1] * s0 * (1 - s1) + v[2] * s0 * s1 + v[3] * (1 - s0) * s1 - p
J11 = -v[0, 0] * (1 - s1) + v[1, 0] * (1 - s1) + v[2, 0] * s1 - v[3, 0] * s1
J21 = -v[0, 1] * (1 - s1) + v[1, 1] * (1 - s1) + v[2, 1] * s1 - v[3, 1] * s1
J12 = -v[0, 0] * (1 - s0) - v[1, 0] * s0 + v[2, 0] * s0 + v[3, 0] * (1 - s0)
J22 = -v[0, 1] * (1 - s0) - v[1, 1] * s0 + v[2, 1] * s0 + v[3, 1] * (1 - s0)
inv_detJ = 1. / (J11 * J22 - J12 * J21)
s0 -= inv_detJ * (J22 * r[0] - J12 * r[1])
s1 -= inv_detJ * (-J21 * r[0] + J11 * r[1])
return s
def invert_map(xmap, ymap, diagnostics=False):
"""
Generate the inverse of deformation map defined by (xmap, ymap) using inverse bilinear interpolation.
"""
quads = np.array([[ymap[:-1, :-1], xmap[:-1, :-1]],
[ymap[1:, :-1], xmap[1:, :-1]],
[ymap[1:, 1:], xmap[1:, 1:]],
[ymap[:-1, 1:], xmap[:-1, 1:]]])
x0 = np.floor(quads[:, 1, ...].min(axis=0)).astype(int)
x1 = np.ceil(quads[:, 1, ...].max(axis=0)).astype(int)
y0 = np.floor(quads[:, 0, ...].min(axis=0)).astype(int)
y1 = np.ceil(quads[:, 0, ...].max(axis=0)).astype(int)
i0, j0 = np.indices(x0.shape)
x0_offset = x0.min()
y0_offset = y0.min()
xN = x1 - x0 + 1
yN = y1 - y0 + 1
sh_dest = (1 + x1.max() - x0_offset, 1 + y1.max() - y0_offset)
yy_dest, xx_dest = np.indices(sh_dest)
xmap1 = np.zeros(sh_dest)
ymap1 = np.zeros(sh_dest)
TN = np.zeros(sh_dest, dtype=int)
epsilon = .01
for ix in range(xN.max()):
for iy in range(yN.max()):
valid = (xN > ix) * (yN > iy)
p = np.array([y0[valid] + ix, x0[valid] + iy])
s = bilinear_inverse(p, quads[:, :, valid])
in_quad = np.all((s > -epsilon) * (s < (1 + epsilon)), axis=0)
ii = p[0, in_quad] - y0_offset
jj = p[1, in_quad] - x0_offset
ymap1[ii, jj] += i0[valid][in_quad] + s[0][in_quad]
xmap1[ii, jj] += j0[valid][in_quad] + s[1][in_quad]
TN[ii, jj] += 1
ymap1 /= TN + (TN == 0)
xmap1 /= TN + (TN == 0)
if diagnostics:
diag = {'x_offset': x0_offset,
'y_offset': y0_offset,
'mask': TN > 0}
return xmap1, ymap1, diag
else:
return xmap1, ymap1
这是一个测试示例。
import cv2 as cv
from scipy import ndimage as ndi
N = 500
sh = (N, N)
t = np.random.normal(size=sh)
dx = ndi.gaussian_filter(t, 40, order=(0,1))
dy = ndi.gaussian_filter(t, 40, order=(1,0))
dx *= 30/dx.max()
dy *= 30/dy.max()
img = np.zeros(sh)
img[::10, :] = 1
img[:, ::10] = 1
img = ndi.gaussian_filter(img, 0.5)
yy, xx = np.indices(sh)
xmap = (xx-dx).astype(np.float32)
ymap = (yy-dy).astype(np.float32)
warped = cv.remap(img, xmap, ymap ,cv.INTER_LINEAR)
plt.imshow(warped, cmap='gray')
![Warped image](https://istack.dev59.com/fz5yZ.webp)
xmap1, ymap1 = invert_map(xmap, ymap)
unwarped = cv.remap(warped, xmap1.astype(np.float32), ymap1.astype(np.float32) ,cv.INTER_LINEAR)
plt.imshow(unwarped, cmap='gray')
![Unwarpped image](https://istack.dev59.com/EOVNA.webp)
{(X[i,j],Y[i,j])}
不是 在一个规则的网格上。在径向镜头畸变的情况下,它们位于被拉伸的像牛肉干一样的网格上(例如,在角落处看到第二张图像)。换句话说,这些点代表了非线性函数的样本 -- 当你有重度鱼眼畸变并添加切向畸变时,你可以忘记使用网格来查找邻居。N = 5个邻居是一个很好的金发姑娘值 -- 不太少也不太多。IDW是插值非规则网格上采样的2D表面的绝佳方法。 - wcochran