我在将平行四边形仿射变换为矩形时做错了什么?

What am I doing wrong with Affine Transform of Parallelogram into Rectangle?

我有两种形状,一个矩形和一个平行四边形,表示两个龙门系统。一个龙门系统上有一个摄像头,可以检测位于上方的另一个龙门系统的位置。我无法通过一系列变换(平移、旋转、剪切 x、剪切 y、平移)让它甚至更接近于适应系统 1。请问 pointers/insight 我做错了什么?

我已经用单位向量测试了每个变换,所以我知道数学是可行的。我怀疑要么我使用了不正确的角度(虽然在单位向量上使用了相同的角度),但存在线性问题,它不是很线性,因此变换不会起作用(由于物理性质,这似乎也不太可能),或者很可能我的操作顺序不正确。

from matplotlib import pyplot as plt
import numpy as np
from mpl_toolkits.axes_grid1.inset_locator import TransformedBbox, BboxPatch, BboxConnector


def get_angle(array, array_2, side=3):
    if side == 0:
        # Get start and end points from array
        vector = array[1] - array[0]
        # Get start and end points from array
        vector_2 = array_2[1] - array_2[0]
    elif side == 1:
        # Get start and end points from array
        vector = array[2] - array[1]
        # Get start and end points from array
        vector_2 = array_2[2] - array_2[1]
    elif side == 2:
        # Get start and end points from array
        vector = array[2] - array[3]
        # Get start and end points from array
        vector_2 = array_2[2] - array_2[3]
    elif side == 3:
        # Get start and end points from array
        vector = array[3] - array[0]
        # Get start and end points from array
        vector_2 = array_2[3] - array_2[0]

    # Calculate unit vectors
    dot = vector[0] * vector_2[0] + vector[1] * vector_2[1]  # dot product between [x1, y1] and [x2, y2]
    det = vector[0] * vector_2[1] - vector[1] * vector_2[0]  # determinant
    angle = np.arctan2(det, dot)  # atan2(y, x) or atan2(sin, cos)
    return angle


def shear_trans_x(coords, phi):
    shear_x = np.array([[1, np.tan(phi), 0],
                        [0, 1, 0],
                        [0, 0, 1]])
    coords = np.append(coords, np.ones((coords.shape[0], 1)), axis=1)
    resultant = coords @ shear_x.T
    return resultant[:, 0:2]


def shear_trans_y(coords, psi):
    shear_y = np.array([[1, 0, 0],
                        [np.tan(psi), 1, 0],
                        [0, 0, 1]])
    coords = np.append(coords, np.ones((coords.shape[0], 1)), axis=1)
    resultant = coords @ shear_y.T
    return resultant[:, 0:2]


def translate(coordinates, offset):
    coordinates = np.append(coordinates, np.ones((coordinates.shape[0], 1)), axis=1)
    a = np.array([[1, 0, offset[0]],
                  [0, 1, offset[1]],
                  [0, 0,     1   ]])

    result = coordinates @ a.T
    return result[:, 0:2]


def rotate(coords, theta, origin=[0,0]):
    cos = np.cos(theta)
    sin = np.sin(theta)
    a = np.array([[cos, -sin, 0],
                  [sin, cos, 0],
                  [0, 0, 1]])
    if np.all(origin == [0, 0]):
        coords = np.append(coords, np.ones((coords.shape[0], 1)), axis=1)
        result = coords @ a.T
        return result[:, 0:2]
    else:
        coords = translate(coords, -origin)
        coords = rotate(coords, theta, origin=[0, 0])
        coords = translate(coords, origin)
        return coords


def mark_inset(parent_axes, inset_axes, loc1a=1, loc1b=1, loc2a=2, loc2b=2, **kwargs):
    '''
    draw a bbox of the region of the inset axes in the parent axes and
    connecting lines between the bbox and the inset axes area
    loc1, loc2 : {1, 2, 3, 4}
    '''
    rect = TransformedBbox(inset_axes.viewLim, parent_axes.transData)

    p1 = BboxConnector(inset_axes.bbox, rect, loc1=loc1a, loc2=loc1b, **kwargs)
    inset_axes.add_patch(p1)
    p1.set_clip_on(False)
    p2 = BboxConnector(inset_axes.bbox, rect, loc1=loc2a, loc2=loc2b, **kwargs)
    inset_axes.add_patch(p2)
    p2.set_clip_on(False)

    pp = BboxPatch(rect, fill=False, **kwargs)
    parent_axes.add_patch(pp)

    return pp, p1, p2


if __name__ == '__main__':
    # calibration data
    gantry_1_coords = np.array([[169.474, 74.4851], [629.474, 74.4851], [629.474, 334.4851], [169.474, 334.4851]])
    gantry_2_coords_error = np.array([[-0.04, 0.04], [-0.04, -0.31], [0.76, -0.57], [1.03, 0.22]])
    # gantry_2_coords_error = np.array([[0.13, 0.04], [-0.13, -0.75], [0.31, -0.93], [0.58, -0.31]])
    # add error to gantry 1 coords
    gantry_2_coords = gantry_1_coords + gantry_2_coords_error

    # append first point to end for plotting to display a closed box
    gantry_1_coords = np.append(gantry_1_coords, np.array([gantry_1_coords[0]]), axis=0)
    gantry_2_coords = np.append(gantry_2_coords, np.array([gantry_2_coords[0]]), axis=0)

    # get length of diagonal direction
    magnitude = np.linalg.norm(gantry_1_coords[0] - gantry_1_coords[2])
    magnitude_gantry_2 = np.linalg.norm(gantry_2_coords[0] - gantry_2_coords[2])

    # translate to gantry_1 first position
    translated_gantry_2 = translate(gantry_2_coords, (gantry_1_coords[0] - gantry_2_coords[0]))
    print('translation_offset_1', ' = ', gantry_1_coords[0] - gantry_2_coords[0])

    # rotate gantry_2 to gantry_1
    theta = get_angle(translated_gantry_2, gantry_1_coords,  side=0)
    rotate_gantry_2_coords = rotate(translated_gantry_2, theta, translated_gantry_2[0])
    print('rotation angle', ' = ', theta)

    # un-shear x axis gantry_2
    shear_phi = get_angle(rotate_gantry_2_coords, gantry_1_coords, side=3)
    sheared_x_gantry_2 = shear_trans_x(rotate_gantry_2_coords, shear_phi)
    print('shear x angle', ' = ', shear_phi)

    # un-shear y axis gantry_2
    shear_psi = get_angle(sheared_x_gantry_2, gantry_1_coords,  side=2)
    sheared_y_gantry_2 = shear_trans_y(sheared_x_gantry_2, shear_psi)
    print('shear y angle', ' = ', shear_psi)

    # translate to gantry_1 first position
    final_gantry_2_coords = translate(sheared_y_gantry_2, (gantry_1_coords[0] - sheared_y_gantry_2[0]))
    print('translation_offset_2', ' = ', gantry_1_coords[0] - sheared_y_gantry_2[0])

    # create exaggerated errors for plotting
    ex_gantry_2_coords = (gantry_2_coords - gantry_1_coords) * 50 + gantry_2_coords
    ex_gantry_2_final_coords = (final_gantry_2_coords - gantry_1_coords) * 50 + final_gantry_2_coords

    # separate out x & y components for plotting
    gantry_1_x, gantry_1_y = gantry_1_coords.T
    gantry_2_x, gantry_2_y = ex_gantry_2_coords.T
    gantry_2_final_x, gantry_2_final_y = ex_gantry_2_final_coords.T


    # plot results
    fig, ax = plt.subplots()
    ax.plot(gantry_1_x, gantry_1_y, color='black', linestyle='--', label='gantry_1')
    ax.plot(gantry_2_x, gantry_2_y, color='blue', linestyle='--', label='gantry_2 original')
    ax.plot(gantry_2_final_x, gantry_2_final_y, color='red', linestyle='--', label='gantry_2 transformed')
    # get legend lines and labels from center graph
    lines, labels = ax.get_legend_handles_labels()
    fig.legend(lines, labels)
    plt.show()

    # print('gantry 1 positions: ', gantry_1_coords)
    # print('transformed gantry 2 positions: ', final_gantry_2_coords)

修复现有代码

就现有代码而言,我一个一个地应用了转换,我认为你在这里漏掉了一个负号:

sheared_x_gantry_2 = shear_trans_x(rotate_gantry_2_coords, -shear_phi)

                                                         # ^--- here

应用之后,图形看起来更好:

最小二乘拟合

但是,我认为这是错误的一般方法。例如,当你修复剪切时,这会破坏平移和旋转,至少是一点点。您可以重复应用修复程序,并找到正确的答案,但还有更好的方法。

相反,我会建议找到适合变换矩阵的最小二乘法,而不是建立一堆旋转和剪切矩阵。 Numpy 有一个 function 可以做到这一点。

def add_bias_term(matrix):
    return np.append(np.ones((matrix.shape[0], 1)), matrix, axis=1)

x, _, _, _ = np.linalg.lstsq(add_bias_term(gantry_2_coords), gantry_1_coords, rcond=None)
final_gantry_2_coords = add_bias_term(gantry_2_coords) @ x

这既短了很多,又更适合启动:

这是它找到的矩阵:

array([[ 0.19213806, -0.37107611],
       [ 1.00028902,  0.00123954],
       [-0.00359818,  1.00014869]])

(注意第一行是偏差项。)

尽管如此,合身性并不完美。这是残差:

array([[-0.06704727, -0.10997465],  # point 0
       [ 0.06716097,  0.11016114],  # point 1
       [-0.06720015, -0.1102254 ],  # point 2
       [ 0.06708645,  0.11003891]]) # point 3

不幸的是,根据定义,这个剩余的误差是非线性的。 (如果有一个仿射矩阵可以更好地减少错误,lstsq 就会找到它。)

添加非线性

观察残差,当 x 和 y 都很大时,误差在一个方向上,而当 x 或 y 中只有一个很大时,误差在另一个方向上。这向我表明你需要一个交互项。换句话说,您需要对输入矩阵进行预处理,使其具有 X 列、Y 列和 X*Y 列。

执行此操作的代码如下所示:

def add_bias_term(matrix):
    return np.append(np.ones((matrix.shape[0], 1)), matrix, axis=1)

def add_interaction(matrix):
    inter = (matrix[:, 0] * matrix[:, 1]).reshape(matrix.shape[0], 1)
    return np.append(inter, matrix, axis=1)
    

x, _, _, _ = np.linalg.lstsq(add_bias_term(add_interaction(gantry_2_coords)), gantry_1_coords, rcond=None)
final_gantry_2_coords = (add_bias_term(add_interaction(gantry_2_coords)) @ x)

图表如下所示:

这两个图非常接近。