如何根据其他基本矩阵和相机运动找到基本矩阵?

How to find fundamental matrix based on other fundamental matrix and camera movement?

我正在尝试加速某些依赖于计算每对相机之间的基本矩阵的多相机系统。

请注意以下是伪代码。 @表示矩阵乘法,|表示串联。

我有计算每对 calculate_f(camera_matrix1_3x4, camera_matrix1_3x4)F 的代码,天真的解决方案是

for c1 in cameras:
    for c2 in cameras:
        if c1 != c2:
            f = calculate_f(c1.proj_matrix, c2.proj_matrix)

这很慢,我想加快速度。我有 ~5000 台相机。


我已经预先计算了每对相机之间的所有旋转和平移(在世界坐标中),以及内部参数k,因此对于每个相机c,它认为c.matrix = c.k @ (c.rot | c.t)

我可以使用参数 r, t 来帮助加快 F 的后续计算吗?


以数学形式,对于 3 种不同的相机 c1c2c3 我有

f12=(c1.proj_matrix, c2.proj_matrix),我想要 f23=(c2.proj_matrix, c3.proj_matrix)f13=(c1.proj_matrix, c3.proj_matrix) 具有某些功能 f23, f13 = fast_f(f12, c1.r, c1.t, c2.r, c2.t, c3.r, c3.t)?


在numpy中计算基本矩阵的工作函数:

def fundamental_3x3_from_projections(p_left_3x4: np.array, p_right__3x4: np.array) -> np.array:
    # The following is based on OpenCv-contrib's c++ implementation.
    # see https://github.com/opencv/opencv_contrib/blob/master/modules/sfm/src/fundamental.cpp#L109
    # see https://sourishghosh.com/2016/fundamental-matrix-from-camera-matrices/
    # see https://answers.opencv.org/question/131017/how-do-i-compute-the-fundamental-matrix-from-2-projection-matrices/
    f_3x3 = np.zeros((3, 3))
    p1, p2 = p_left_3x4, p_right__3x4

    x = np.empty((3, 2, 4), dtype=np.float)
    x[0, :, :] = np.vstack([p1[1, :], p1[2, :]])
    x[1, :, :] = np.vstack([p1[2, :], p1[0, :]])
    x[2, :, :] = np.vstack([p1[0, :], p1[1, :]])

    y = np.empty((3, 2, 4), dtype=np.float)
    y[0, :, :] = np.vstack([p2[1, :], p2[2, :]])
    y[1, :, :] = np.vstack([p2[2, :], p2[0, :]])
    y[2, :, :] = np.vstack([p2[0, :], p2[1, :]])

    for i in range(3):
        for j in range(3):
            xy = np.vstack([x[j, :], y[i, :]])
            f_3x3[i, j] = np.linalg.det(xy)

    return f_3x3

Numpy 显然没有针对处理小矩阵进行优化。 CPython 输入对象的解析、内部检查和函数调用引入了显着的开销,这远远大于执行实际计算所需的执行时间。更不用说创建 许多临时数组 也很昂贵。解决此问题的一种方法是使用 Numba 或 Cython

此外,行列式的计算可以进行很多优化,因为您知道矩阵的确切大小并且矩阵的一部分并不总是变化。事实上,使用 4x4 行列式的基本代数表达式 有助于编译器优化整体计算,这要归功于 common sub-expression elimination(不是由 CPython 解释器执行)和删除np.linalg.det.

中的复杂 loops/conditionals

这是结果代码:

import numba as nb

@nb.njit('float64(float64[:,::1])')
def det_4x4(mat):
    a, b, c, d = mat[0,0], mat[0,1], mat[0,2], mat[0,3]
    e, f, g, h = mat[1,0], mat[1,1], mat[1,2], mat[1,3]
    i, j, k, l = mat[2,0], mat[2,1], mat[2,2], mat[2,3]
    m, n, o, p = mat[3,0], mat[3,1], mat[3,2], mat[3,3]
    return a * (f * (k*p - l*o) + g * (l*n - j*p) + h * (j*o - k*n)) + \
            b * (e * (l*o - k*p) + g * (i*p - l*m) + h * (k*m - i*o)) + \
            c * (e * (j*p - l*n) + f * (l*m - i*p) + h * (i*n - j*m)) + \
            d * (e * (k*n - j*o) + f * (i*o - k*m) + g * (j*m - i*n))

@nb.njit('float64[:,::1](float64[:,::1], float64[:,::1])')
def fundamental_3x3_from_projections(p_left_3x4, p_right_3x4):
    f_3x3 = np.empty((3, 3))
    p1, p2 = p_left_3x4, p_right_3x4

    x = np.empty((3, 2, 4), dtype=np.float64)
    x[0, 0, :] = p1[1, :]
    x[0, 1, :] = p1[2, :]
    x[1, 0, :] = p1[2, :]
    x[1, 1, :] = p1[0, :]
    x[2, 0, :] = p1[0, :]
    x[2, 1, :] = p1[1, :]

    y = np.empty((3, 2, 4), dtype=np.float64)
    y[0, 0, :] = p2[1, :]
    y[0, 1, :] = p2[2, :]
    y[1, 0, :] = p2[2, :]
    y[1, 1, :] = p2[0, :]
    y[2, 0, :] = p2[0, :]
    y[2, 1, :] = p2[1, :]

    xy = np.empty((4, 4), dtype=np.float64)

    for i in range(3):
        xy[2:4, :] = y[i, :, :]
        for j in range(3):
            xy[0:2, :] = x[j, :, :]
            f_3x3[i, j] = det_4x4(xy)

    return f_3x3

这比我的机器快 130 倍(85.6 us VS 0.66 us)。

如果应用的函数是可交换的(即f(c1, c2) == f(c2, c1)),您可以将过程加快两倍。如果是这样,您可以只计算上半部分。事实证明你的函数有一些有趣的 属性 因为 f(c1, c2) == f(c2, c1).T 似乎总是正确的。另一个可能的优化是 运行 parallel.

中的循环

通过所有这些优化,生成的程序应该快 3 个数量级


分析方法的准确性

提供的精度似乎与原始精度相似。关于输入矩阵,结果有时比 Numpy 方法更准确,有时更不准确。这特别是由于行列式的计算。使用 24 位小数,与 reliable result of Wolphram Alpha 相比,没有明显的错误。这表明该方法是正确的,结果由于数值稳定性细节不同而不同。以下是用于测试方法准确性的代码:

# Imports

from decimal import Decimal
import numba as nb

# Definitions

def det_4x4(mat):
    a, b, c, d = mat[0,0], mat[0,1], mat[0,2], mat[0,3]
    e, f, g, h = mat[1,0], mat[1,1], mat[1,2], mat[1,3]
    i, j, k, l = mat[2,0], mat[2,1], mat[2,2], mat[2,3]
    m, n, o, p = mat[3,0], mat[3,1], mat[3,2], mat[3,3]
    return a * (f * (k*p - l*o) + g * (l*n - j*p) + h * (j*o - k*n)) + \
            b * (e * (l*o - k*p) + g * (i*p - l*m) + h * (k*m - i*o)) + \
            c * (e * (j*p - l*n) + f * (l*m - i*p) + h * (i*n - j*m)) + \
            d * (e * (k*n - j*o) + f * (i*o - k*m) + g * (j*m - i*n))

@nb.njit('float64(float64[:,::1])')
def det_4x4_numba(mat):
    a, b, c, d = mat[0,0], mat[0,1], mat[0,2], mat[0,3]
    e, f, g, h = mat[1,0], mat[1,1], mat[1,2], mat[1,3]
    i, j, k, l = mat[2,0], mat[2,1], mat[2,2], mat[2,3]
    m, n, o, p = mat[3,0], mat[3,1], mat[3,2], mat[3,3]
    return a * (f * (k*p - l*o) + g * (l*n - j*p) + h * (j*o - k*n)) + \
            b * (e * (l*o - k*p) + g * (i*p - l*m) + h * (k*m - i*o)) + \
            c * (e * (j*p - l*n) + f * (l*m - i*p) + h * (i*n - j*m)) + \
            d * (e * (k*n - j*o) + f * (i*o - k*m) + g * (j*m - i*n))


# Example matrix

precise_xy = np.array(
     [[Decimal('42'),Decimal('-6248'),Decimal('4060'),Decimal('845')],
      [Decimal('-0.00992'),Decimal('-0.704'),Decimal('-0.71173298417'),Decimal('300.532')],
      [Decimal('-8.94274'),Decimal('-7554.39'),Decimal('604.57'),Decimal('706282')],
      [Decimal('-0.0132'),Decimal('-0.2757'),Decimal('-0.961'),Decimal('247.65')]]
)

xy = precise_xy.astype(np.float64)

res_numpy = Decimal(np.linalg.det(xy))
res_numba = Decimal(det_4x4_numba(xy))
res_precise = det_4x4(precise_xy)

# The Wolphram Alpha expression used is:
# det({{42,-6248,4060,845},
#     {-0.00992,-0.704,-0.71173298417,300.532},
#     {-8.94274,-7554.39,604.57,706282},
#     {-0.0132,-0.2757,-0.961,247.65}})
res_wolframalpha = Decimal('-323312.2164828991329828243')

# The result got from Wolfram-Alpha have a 25-digit precision
# and is exactly the same than the one of det_4x4 using 24-digit decimals.
assert res_precise == res_wolframalpha

print(abs((res_numpy-res_precise)/res_precise))  # 1.7E-14
print(abs((res_numba-res_precise)/res_precise))  # 3.1E-14
# => Similar relative error (Numba slightly less accurate
#    but both are not close to the 1e-16 relative epsilon)