如何通过坐标对计算变换矩阵?含Python+Numpy实现需求
需求概述
我现在有两个坐标变换需求,需要用Python+Numpy实现:
- 世界坐标→相机1像素坐标:已知4组世界坐标(Zw恒为0)和对应的相机1像素坐标,求变换矩阵,输入(Xw,Yw)就能直接得到(Xc,Yc)
- 相机2像素坐标→相机1像素坐标:已知2组相机2和相机1的对应像素点(相机1的坐标是估算值,需实际计算),且已获取相机2的标定数据,实现两者的坐标转换
已知数据
1. 世界坐标与相机1像素坐标对应表
| Xw | Yw | Zw | Xc | Yc |
|---|---|---|---|---|
| 0.0 | 0.0 | 0.0 | 582 | 344 |
| 7.0 | 0.0 | 0.0 | 834 | 338 |
| 0.0 | 5.0 | 0.0 | 586 | 529 |
| 7.0 | 5.0 | 0.0 | 841 | 522 |
2. 相机2与相机1像素坐标对应表(相机1坐标为估算值,需实际计算)
| Xc2 | Yc2 | Xc1 | Yc1 |
|---|---|---|---|
| 1250 | 433 | 209 | 771 |
| 528 | 452 | 1277 | 730 |
实现思路与代码
一、世界坐标→相机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})")
代码讲解
- 方程组构造:把透视变换的方程展开,转换成9维的线性方程组,用SVD求解能避免矩阵不可逆的问题,鲁棒性更强。
- 归一化处理:单应性矩阵的尺度是任意的,我们把最后一个元素归一为1,方便后续计算和理解。
- 坐标转换:输入世界坐标的齐次形式,乘以矩阵后再除以第三个元素(齐次坐标的缩放因子),就能得到相机像素坐标。
二、相机2像素坐标→相机1像素坐标:两种实现方案
方案1:利用相机标定数据的两步转换法
如果有相机2的内参、畸变系数、外参(相对于世界坐标系的旋转/平移),可以通过世界坐标中转,精度更高:
- 相机2像素坐标 → 去畸变 → 相机2归一化平面坐标
- 相机2归一化坐标 → 世界坐标
- 世界坐标 → 相机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})")
代码讲解
- 两步法优势:利用相机标定数据,符合真实的成像模型,转换精度更高,适合对精度要求高的场景。
- 直接变换法:适合没有标定数据的场景,但对对应点的数量和精度要求高,点越少误差越大。
关键注意事项
- 对应点精度:所有对应点的测量精度直接决定变换矩阵的准确性,建议尽可能多采集几组点,用最小二乘法降低误差。
- 畸变校正:如果相机存在畸变,一定要先去畸变再进行坐标转换,否则会引入较大误差。
- 变换类型选择:仿射变换适用于相机与平面平行的场景(无透视变形),单应性变换适用于任意平面场景(有透视变形),根据实际场景选择。
内容的提问来源于stack exchange,提问作者 Ignis




