You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何通过坐标对计算变换矩阵?含Python+Numpy实现需求

需求概述

我现在有两个坐标变换需求,需要用Python+Numpy实现:

  1. 世界坐标→相机1像素坐标:已知4组世界坐标(Zw恒为0)和对应的相机1像素坐标,求变换矩阵,输入(Xw,Yw)就能直接得到(Xc,Yc)
  2. 相机2像素坐标→相机1像素坐标:已知2组相机2和相机1的对应像素点(相机1的坐标是估算值,需实际计算),且已获取相机2的标定数据,实现两者的坐标转换

已知数据

1. 世界坐标与相机1像素坐标对应表

XwYwZwXcYc
0.00.00.0582344
7.00.00.0834338
0.05.00.0586529
7.05.00.0841522

2. 相机2与相机1像素坐标对应表(相机1坐标为估算值,需实际计算)

Xc2Yc2Xc1Yc1
1250433209771
5284521277730

实现思路与代码

一、世界坐标→相机1像素坐标:透视变换矩阵求解

因为Zw恒为0,我们可以把世界坐标简化为2D点(Xw,Yw),相机像素坐标也是2D点(Xc,Yc)。这种平面到平面的变换属于透视变换(单应性变换),用3×3的单应性矩阵H描述,形式如下:
$$
\begin{bmatrix} Xc \ Yc \ 1 \end{bmatrix} = H \begin{bmatrix} Xw \ Yw \ 1 \end{bmatrix}
$$
4个对应点足够唯一确定这个矩阵,我们用Numpy的奇异值分解(SVD)来求解最小二乘解,比直接求逆更稳定。

代码实现

import numpy as np

# 世界坐标点 (Xw, Yw),Zw=0直接忽略
world_points = np.array([
    [0.0, 0.0],
    [7.0, 0.0],
    [0.0, 5.0],
    [7.0, 5.0]
], dtype=np.float32)

# 相机1像素坐标点 (Xc, Yc)
cam1_points = np.array([
    [582, 344],
    [834, 338],
    [586, 529],
    [841, 522]
], dtype=np.float32)

def get_homography_matrix(src, dst):
    assert src.shape[0] == dst.shape[0] and src.shape[0] >= 4, "求解单应性矩阵至少需要4个对应点"
    n = src.shape[0]
    A = []
    # 构造求解单应性矩阵的线性方程组
    for i in range(n):
        x, y = src[i]
        u, v = dst[i]
        A.append([-x, -y, -1, 0, 0, 0, x*u, y*u, u])
        A.append([0, 0, 0, -x, -y, -1, x*v, y*v, v])
    A = np.array(A, dtype=np.float32)
    # 用SVD求解最小二乘解
    _, _, V = np.linalg.svd(A)
    H = V[-1].reshape(3, 3)
    # 归一化:让矩阵最后一个元素为1(单应性矩阵尺度不变)
    H = H / H[2, 2]
    return H

# 求解世界→相机1的变换矩阵
H_world_to_cam1 = get_homography_matrix(world_points, cam1_points)
print("世界坐标→相机1的变换矩阵H:")
print(H_world_to_cam1)

# 坐标转换函数
def world_to_cam1(xw, yw, H):
    # 构造齐次坐标
    world_hom = np.array([xw, yw, 1], dtype=np.float32)
    # 矩阵乘法得到相机坐标的齐次形式
    cam1_hom = H @ world_hom
    # 转换为非齐次坐标(除以缩放因子)
    xc = cam1_hom[0] / cam1_hom[2]
    yc = cam1_hom[1] / cam1_hom[2]
    return round(xc), round(yc)

# 测试第一个点
test_xw, test_yw = 0.0, 0.0
xc, yc = world_to_cam1(test_xw, test_yw, H_world_to_cam1)
print(f"测试点({test_xw}, {test_yw})转换后:({xc}, {yc})")

代码讲解

  1. 方程组构造:把透视变换的方程展开,转换成9维的线性方程组,用SVD求解能避免矩阵不可逆的问题,鲁棒性更强。
  2. 归一化处理:单应性矩阵的尺度是任意的,我们把最后一个元素归一为1,方便后续计算和理解。
  3. 坐标转换:输入世界坐标的齐次形式,乘以矩阵后再除以第三个元素(齐次坐标的缩放因子),就能得到相机像素坐标。

二、相机2像素坐标→相机1像素坐标:两种实现方案

方案1:利用相机标定数据的两步转换法

如果有相机2的内参、畸变系数、外参(相对于世界坐标系的旋转/平移),可以通过世界坐标中转,精度更高:

  1. 相机2像素坐标 → 去畸变 → 相机2归一化平面坐标
  2. 相机2归一化坐标 → 世界坐标
  3. 世界坐标 → 相机1像素坐标(用之前求的H矩阵)
import cv2

# 假设已经通过OpenCV获取相机2的标定数据
K2 = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]], dtype=np.float32)  # 内参矩阵
dist2 = np.array([k1, k2, p1, p2, k3], dtype=np.float32)  # 畸变系数
R2 = np.array([[r11, r12, r13], [r21, r22, r23], [r31, r32, r33]], dtype=np.float32)  # 旋转矩阵
t2 = np.array([tx, ty, tz], dtype=np.float32)  # 平移向量

def cam2_to_world(xc2, yc2, K2, dist2, R2, t2):
    # 1. 对相机2的像素点去畸变
    undistorted_point = cv2.undistortPoints(np.array([[[xc2, yc2]]]), K2, dist2)[0][0]
    # 2. 转换为相机2坐标系下的3D点(归一化平面z=1)
    cam2_point = np.array([undistorted_point[0], undistorted_point[1], 1], dtype=np.float32)
    # 3. 转换为世界坐标:世界坐标 = R2^T * (相机坐标 - t2)
    world_point = R2.T @ (cam2_point - t2)
    return world_point[0], world_point[1]

# 示例转换
xc2, yc2 = 1250, 433
xw, yw = cam2_to_world(xc2, yc2, K2, dist2, R2, t2)
xc1, yc1 = world_to_cam1(xw, yw, H_world_to_cam1)
print(f"相机2点({xc2}, {yc2})转换为相机1坐标:({xc1}, {yc1})")

方案2:直接求解相机2→相机1的单应性矩阵

如果没有相机2的外参,或者不想通过世界坐标中转,可以直接用对应点求解两个相机像素平面之间的单应性矩阵。注意:单应性矩阵需要至少4个对应点,如果只有2个点,只能假设是仿射变换(6自由度),建议补充更多实测对应点提升精度。

# 相机2与相机1的对应点(这里用用户提供的估算值,实际建议用准确实测点)
cam2_points = np.array([
    [1250, 433],
    [528, 452]
], dtype=np.float32)
cam1_points_est = np.array([
    [209, 771],
    [1277, 730]
], dtype=np.float32)

def get_affine_matrix(src, dst):
    assert src.shape[0] == dst.shape[0] and src.shape[0] >= 3, "仿射变换至少需要3个对应点"
    n = src.shape[0]
    A = []
    b = []
    for i in range(n):
        x, y = src[i]
        u, v = dst[i]
        A.append([x, y, 1, 0, 0, 0])
        A.append([0, 0, 0, x, y, 1])
        b.append(u)
        b.append(v)
    A = np.array(A, dtype=np.float32)
    b = np.array(b, dtype=np.float32)
    # 最小二乘法求解仿射系数
    coeffs, _, _, _ = np.linalg.lstsq(A, b, rcond=None)
    # 构造3×3的仿射矩阵
    affine_mat = np.array([
        [coeffs[0], coeffs[1], coeffs[2]],
        [coeffs[3], coeffs[4], coeffs[5]],
        [0, 0, 1]
    ], dtype=np.float32)
    return affine_mat

# 这里因为只有2个点,实际需要补充更多点,示例仅演示逻辑
H_cam2_to_cam1 = get_affine_matrix(cam2_points, cam1_points_est)
print("相机2→相机1的仿射变换矩阵:")
print(H_cam2_to_cam1)

# 转换函数
def cam2_to_cam1(xc2, yc2, H):
    cam2_hom = np.array([xc2, yc2, 1], dtype=np.float32)
    cam1_hom = H @ cam2_hom
    xc1 = round(cam1_hom[0] / cam1_hom[2])
    yc1 = round(cam1_hom[1] / cam1_hom[2])
    return xc1, yc1

# 测试转换
xc2_test, yc2_test = 1250, 433
xc1_test, yc1_test = cam2_to_cam1(xc2_test, yc2_test, H_cam2_to_cam1)
print(f"测试点({xc2_test}, {yc2_test})转换后:({xc1_test}, {yc1_test})")

代码讲解

  1. 两步法优势:利用相机标定数据,符合真实的成像模型,转换精度更高,适合对精度要求高的场景。
  2. 直接变换法:适合没有标定数据的场景,但对对应点的数量和精度要求高,点越少误差越大。

关键注意事项
  • 对应点精度:所有对应点的测量精度直接决定变换矩阵的准确性,建议尽可能多采集几组点,用最小二乘法降低误差。
  • 畸变校正:如果相机存在畸变,一定要先去畸变再进行坐标转换,否则会引入较大误差。
  • 变换类型选择:仿射变换适用于相机与平面平行的场景(无透视变形),单应性变换适用于任意平面场景(有透视变形),根据实际场景选择。

内容的提问来源于stack exchange,提问作者 Ignis

火山引擎 最新活动