You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何自定义3D转2D鱼眼投影结果与Blender PANO相机不符?

Blender等距PANO相机3D到2D投影实现问题

需求概述

需要将3D点转换为Blender中等距相机模型的PANO相机对应的鱼眼图像,需明确以下转换步骤的实现细节:

  • 世界坐标系到相机坐标系的转换
  • 相机笛卡尔坐标系转球面坐标系(点投影至单位球面后再到图像平面)
  • 球面相机坐标系到传感器平面的转换
  • 将点从主点偏移至传感器左上角的归一化处理

现有bpy_extras模块仅支持透视相机,无等距相机模型的转换代码,自定义实现结果与Blender原生输出存在垂直轴镜像差异,尝试多种旋转组合仍未解决。

自定义实现代码

main.py

#!/usr/bin/env python3

import numpy as np
np.set_printoptions(precision=4, suppress=True)
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
import plotting

def compute_R(alpha_deg, beta_deg, gamma_deg):
    ## Rotation Matrix R (code from CV2 exercise)

    # convert into radians...
    alpha = alpha_deg * np.pi / 180
    beta  = beta_deg  * np.pi / 180
    gamma = gamma_deg * np.pi / 180

    # prepare sine and cosine values
    ca = np.cos(alpha)
    sa = np.sin(alpha)
    cb = np.cos(beta)
    sb = np.sin(beta)
    cg = np.cos(gamma)
    sg = np.sin(gamma)
    
    # rotation about x axis
    R_x = np.array([
        [1,  0,   0],
        [0, ca, -sa],
        [0, sa,  ca],
    ], dtype=float)

    # rotation about y axis
    R_y = np.array([
        [ cb, 0, sb],
        [  0, 1,  0],
        [-sb, 0, cb]    
    ], dtype=float)

    # rotation about z axis
    R_z = np.array([
        [cg, -sg, 0],
        [sg,  cg, 0],
        [ 0,   0, 1]    
    ], dtype=float)

    # compose R_xyz
    R = R_z.dot(R_y.dot(R_x))
        
    return R

def cart2spherical_wiki(X_cam):
    ## from https://en.wikipedia.org/wiki/Spherical_coordinate_system
    r = np.sqrt(X_cam[0]**2 + X_cam[1]**2 + X_cam[2]**2)
    print(f'manual_{r=}')
    #r = np.linalg.norm(X_cam, axis=0)
    #print(f'np.linalg_{r=}')
    theta = np.arccos(X_cam[2] / r)
    phi = -np.arctan2(X_cam[1], X_cam[0])

    return theta, phi

def cart2spherical_blender(X_cam, fov):
    # can be ignored for the moment
    r = np.arctan2(np.sqrt(X_cam[1] * X_cam[1] + X_cam[2] * X_cam[2]), X_cam[0]) / fov
    phi = np.arctan2(X_cam[2], X_cam[1])

    return r, phi

def spherical2normimg(f_x, f_y, theta, phi):
    # from phd-thesis michel
    x_img_norm = f_x * theta * np.cos(phi)
    y_img_norm = f_y * theta * np.sin(phi)
    #print(x_img_norm)
    #print(y_img_norm)

    return x_img_norm, y_img_norm

def spherical2normimg_blender(r, phi):
    # can be ignored for the moment
    x_img_norm = r * np.cos(phi) + 0.5
    y_img_norm = r * np.sin(phi) + 0.5

    return x_img_norm, y_img_norm

def points_to_image(x, y, resolution):
    
    # Sensorpunkt (0,0) liegt im Bild an Position (w/2, h/2)
    # deshalb Addition der halben Bildauflösung
    # (x,y) - Sensorpunkte
    # (x_new, y_new) - Bildpunkte
    x_new = x + resolution[0]//2
    y_new = y + resolution[1]//2

    return x_new, y_new

def main():
    # plot input/output?
    plot = 'True'
        
    ## World points
    X_wrld = np.array([
                    [1.5, 5, 1], # right foot
                    [2.5, 5, 1], # left foot
                    [2, 5, 3], # hip
                    [1, 5, 5], # right hand
                    [3, 5, 5]  # left hand
                    ])

    ## Camera
    C = np.array([2, 1, 4]).transpose() # camera projection center in world coordinates

    ## Plot World with 3D Points and Camera
    fig = plt.figure()
    if plot == 'True':
        title = 'World coordinate system: 3D Points and Camera'
        plt1 = plotting.plot_world(X_wrld, C, elev=0, azim=-90, roll=0, title=title, fig=fig)
    
    # transpose X_wrld
    X_wrld = X_wrld.T # take the transpose

    # add ones for homogenious representation
    row_ones = np.ones([1, X_wrld.shape[1]])
    X_wrld_hom = np.concatenate([X_wrld, row_ones], axis=0)
    #print(f'{X_wrld_hom=}')
    ## Use Rotation-Matrix and C for creating H-Matrix (Homography, 4x4)

    # orientation / rotation
    alpha_deg = 0 # first:  rotate about alpha degrees around the camera's x-axis
    beta_deg  = 270 # second: rotate about beta  degrees around the camera's y-axis
    gamma_deg = 0 # third:  rotate about gamma degrees around the camera's z-axis
    
    # Compute 3x3 Rotation Matrix
    R = compute_R(alpha_deg, beta_deg, gamma_deg)
    #print(f'{R=}')

    # Build H-Matrix (4x4)

    # RC ist 3x1
    RC = np.zeros([3,1])
    RC = R.dot(C)
    RC = RC.T
    H = np.column_stack( (R, -RC) )
    H = np.row_stack( (H, [0, 0, 0, 1]) )
    #print(f'{H=}')

    # Transformation from world to camera coordinates
    X_cam = H.dot(X_wrld_hom)
    print(f'{X_cam=}')

    if plot == 'True':
        title = f'camera coordinate system: camera rotation of {alpha_deg=}, {beta_deg=}, {gamma_deg=}'
        #plt2 = plotting.plot_camera(X_cam=X_cam.T, C=[0, 0, 0], elev=180, azim=90, roll=90, title=title, fig=fig)
        plt2 = plotting.plot_camera(X_cam=X_cam.T, C=[0, 0, 0], elev=alpha_deg, azim=beta_deg, roll=gamma_deg, title=title, fig=fig)
    
    ### Intrinsics ###
    # focal length from Canon FD 1:5,6 / 7,5mm (see, e.g. https://de.wikipedia.org/wiki/Fischaugenobjektiv)
    f_mm = 0.0075 #7.5mm
    # field of view
    fov = 180 * np.pi / 180
    #  Vollformat Sensor, e.g. https://de.wikipedia.org/wiki/Sony_Alpha_1
    chip_w = 35.7 * 1e-3 # 36mm
    chip_h = 23.8 * 1e-3 # 24mm
    # Resolution of camera
    res_w = 8640 # px
    res_h = 5760 # px
    # pixel densities
    m_x = res_w / chip_w # px/mm
    m_y = res_h / chip_h # px/mm
    # focal length normalized with pixel densities
    f_x = f_mm * m_x # unitless
    f_y = f_mm * m_y # unitless, f_x != f_y if pixel on the sensor is not square
    ### Intrinsics End ###

    ## From Camera coordinates (carthesian) to spherical camera coordinates (a) Wiki
    theta, phi = cart2spherical_wiki(X_cam)
    ## From spherical camera coordinates to normalized image coordinates (a) Wiki
    x_img_norm, y_img_norm = spherical2normimg(f_x, f_y, theta, phi)
    # camera to spherical from blender implementation
    #r, phi = cart2spherical_blender(X_cam, fov)
    # spherical to normalized image coordinates from blender code
    #x_img_norm, y_img_norm = spherical2normimg_blender(r, phi)

    if plot == 'True':
        title = 'Sensor coordinate system: Camera points on sensor plane'
        plt3 = plotting.plot_sensor(x_img_norm=x_img_norm, y_img_norm=y_img_norm, title=title, fig=fig)

    # From normalized image coordinates to image coordinates by shifting to left top of sensor
    x_img, y_img = points_to_image(x_img_norm, y_img_norm, (res_w, res_h))
    
    if plot == 'True':
        title = 'Image coordinate system: points on image plane shifted to left top corner'
        plotting.plot_image(x_img, y_img, (0, res_w), (0, res_h), title, fig)

    plt.show()
        
if __name__ == "__main__":
    main()

plotting.py

#!/usr/bin/env python3

import numpy as np
np.set_printoptions(precision=4, suppress=True)
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d


def plot_world(X_wrld, C, elev, azim, roll, title, fig):
    plt1 = fig.add_subplot(2, 2, 1, projection='3d')
    colors = ['r', 'b', 'g', 'm', 'y']
    labels = ['right foot', 'left foot', 'hip', 'right hand', 'left hand']    
    for X,Y,Z,c,l in zip(X_wrld[:,0], X_wrld[:,1], X_wrld[:,2], colors, labels):
        plt1.plot3D(X, Y, Z, c=c, marker=".", linestyle='None', label=l)
    
    plt1.plot3D(C[0], C[1], C[2], c="red", marker="v", linestyle='None', label="camera")
    plt1.set(xlabel='X', ylabel='Y', zlabel='Z')
    plt1.set_xlim(0, 4)
    plt1.set_ylim(0, 6)
    plt1.set_zlim(0, 6)
    plt1.legend()
    plt1.set_title(title)
    plt1.view_init(elev=elev, azim=azim, roll=roll, vertical_axis='z')

    return plt1

def plot_camera(X_cam, C, elev, azim, roll, title, fig):
    plt2 = fig.add_subplot(2, 2, 2, projection='3d')
    colors = ['r', 'b', 'g', 'm', 'y']
    labels = ['right foot', 'left foot', 'hip', 'right hand', 'left hand']      
    for X,Y,Z,c,l in zip(X_cam[:,0], X_cam[:,1], X_cam[:,2], colors, labels):
        plt2.plot3D(X, Y, Z, c=c, marker=".", linestyle='None', label=l)
    
    plt2.plot3D(C[0], C[1], C[2], c="red", marker="v", linestyle='None', label="camera")
    plt2.set(xlabel='X', ylabel='Y', zlabel='Z')
    plt2.legend()
    plt2.set_title(title)
    plt2.view_init(elev=elev, azim=azim, roll=roll, vertical_axis='z')

    return plt2

def plot_sensor(x_img_norm, y_img_norm, title, fig):
    plt3 = fig.add_subplot(2, 2, 3)
    colors = ['r', 'b', 'g', 'm', 'y']
    labels = ['right foot', 'left foot', 'hip', 'right hand', 'left hand']
    for x, y, c, l in zip(x_img_norm, y_img_norm, colors, labels):
        plt3.plot(x, y, c=c, marker=".", linestyle='None', label=l)
    
    plt3.set_title(title)
    return plt3

def plot_image(x_img_norm, y_img_norm, limit_x, limit_y, title, fig):
    plt4 = fig.add_subplot(2, 2, 4)
    colors = ['r', 'b', 'g', 'm', 'y']
    labels = ['right foot', 'left foot', 'hip', 'right hand', 'left hand']
    for x, y, c, l in zip(x_img_norm, y_img_norm, colors, labels):
        plt4.plot(x, y, marker='.', label= l, linestyle='None', c=c)

    plt4.set(xlim=limit_x, ylim=limit_y)
    plt4.set_title(title)
    plt4.legend()

    return plt4

补充说明

需获取Blender等距PANO相机3D到2D转换对应的学术论文或技术报告,明确上述四个转换步骤的官方实现逻辑。


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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 04:05:23