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

如何在Blender中利用SolvePnP向量修正相机位姿?

问题:OpenCV SolvePnP计算相机位姿后在Blender中还原出错,相机位置偏离预期

我正在学习OpenCV,并在Blender中做实践测试,尝试还原朝向平面的相机位姿。参考案例写了包含SolvePnP计算、3D/2D点匹配逻辑的代码,但生成的相机旋转和平移参数有问题,相机位置和预期不符,求修正位姿的方法。

原始代码

import bpy
import cv2
import numpy as np
from mathutils import Matrix, Vector
from scipy.spatial.transform import Rotation

def focalMM_to_focalPixel(focalMM, sensorWidth, imageWidth):
    pixelPitch = sensorWidth / imageWidth
    return focalMM / pixelPitch

# Read Image
im = cv2.imread("assets/cameraView.jpg")

imageWidth = 1920
imageHeight = 1080
imageSize = [imageWidth, imageHeight]

points_2D = np.array([
    (949, 49),
    (1415, 45),
    (962, 913),
    (1398, 977)
], dtype="double")

points_3D = np.array([
    (-.30208, -1.8218, 8.3037),
    (-.30208, -1.8303, 8.3037),
    (-.30208, -1.8218, 1.381),
    (-.30208, -1.8303, 1.381)
])

focalLengthMM = 50
sensorWidth = 36

fLength = focalMM_to_focalPixel(focalLengthMM, sensorWidth, imageWidth)
print("focalLengthPixel", fLength)

K = np.array([
    [fLength, 0, imageWidth/2],
    [0, fLength, imageHeight/2],
    [0, 0, 1]
])
distCoeffs = np.zeros((5, 1))

success, rvecs, tvecs = cv2.solvePnP(points_3D, points_2D, K, distCoeffs, flags=cv2.SOLVEPNP_ITERATIVE)

np_rodrigues = np.asarray(rvecs[:,:], np.float64)
rmat = cv2.Rodrigues(np_rodrigues)[0]
camera_position = -np.matrix(rmat).T @ np.matrix(tvecs)

# Test the solvePnP by projecting the 3D Points to camera
projPoints = cv2.projectPoints(points_3D, rvecs, tvecs, K, distCoeffs)[0]

for p in points_2D:
    cv2.circle(im, (int(p[0]), int(p[1])), 3, (0, 255, 0), -1)

for p in projPoints:
    cv2.circle(im, (int(p[0][0]), int(p[0][1])), 3, (255, 0, 0), -1)

# cv2.imshow("image", im)
# cv2.waitKey(0)

r = Rotation.from_rotvec([rvecs[0][0], rvecs[1][0], rvecs[2][0]])
rot = r.as_euler('xyz', degrees=True)

tx = camera_position[0][0]
ty = camera_position[1][0]
tz = camera_position[2][0]

rx = round(180 - rot[0], 5)
ry = round(rot[1], 5)
rz = round(rot[2], 5)

# Creating the camera in Blender
bpy.ops.object.camera_add()
camera = bpy.context.object
camera.location = (tx, ty, tz)

# Convert rotation from degrees to radians for Blender
camera.rotation_euler = (np.radians(rx), np.radians(ry), np.radians(rz))

# Setting the camera parameters
camera.data.lens = focalLengthMM
camera.data.sensor_width = sensorWidth 
camera.data.sensor_height = sensorWidth * (imageHeight / imageWidth)
camera.data.shift_x = (imageWidth / 2 - K[0, 2]) / imageWidth
camera.data.shift_y = (imageHeight / 2 - K[1, 2]) / imageHeight

核心问题分析与修正方案

1. 坐标系不匹配是根本原因

OpenCV与Blender的坐标系轴定义存在差异:

  • OpenCV相机坐标系:右手系,X轴向右、Y轴向下、Z轴向前(指向场景)
  • Blender世界坐标系:右手系,X轴向右、Y轴向前、Z轴向上

直接将OpenCV计算的位姿参数导入Blender,必然出现轴方向错位,导致相机位置和旋转偏离预期。

2. 具体修正步骤

(1)修正相机位置坐标映射

将OpenCV计算出的相机位置转换为Blender坐标系:

# 原代码的位置计算保留,仅修改轴映射
camera_position = -np.matrix(rmat).T @ np.matrix(tvecs)
# OpenCV → Blender 轴映射:X不变,Z→Y,Y→-Z
tx = camera_position[0][0]
ty = camera_position[2][0]  # OpenCV的Z轴对应Blender的Y轴
tz = -camera_position[1][0] # OpenCV的Y轴对应Blender的-Z轴

(2)修正旋转参数适配Blender

通过坐标系转换矩阵,将OpenCV的旋转矩阵转换为Blender兼容的形式:

# 替换原旋转计算部分
r = Rotation.from_rotvec(rvecs.flatten())
r_mat = r.as_matrix()

# 定义OpenCV到Blender的坐标系转换矩阵
cv_to_blender = np.array([
    [1, 0,  0],
    [0, 0, -1],
    [0, 1,  0]
])

# 转换旋转矩阵
r_mat_blender = cv_to_blender @ r_mat @ cv_to_blender.T
r_blender = Rotation.from_matrix(r_mat_blender)
rot = r_blender.as_euler('xyz', degrees=True)

# 直接使用转换后的欧拉角,无需额外180度偏移
camera.rotation_euler = (np.radians(rot[0]), np.radians(rot[1]), np.radians(rot[2]))

(3)验证SolvePnP计算准确性

先启用投影验证代码,确认OpenCV的SolvePnP计算本身是否准确:

cv2.imshow("Projection Check", im)
cv2.waitKey(0)
cv2.destroyAllWindows()

如果绿色原始点和蓝色投影点重合度高,说明SolvePnP计算没问题,问题出在Blender的坐标系转换上。

(4)确认3D点坐标系一致性

确保points_3D是Blender世界坐标系下的坐标,如果原始3D点来自其他软件(如CAD),需要先转换到Blender的坐标系(Y向前、Z向上)。

3. 完整修正后代码

import bpy
import cv2
import numpy as np
from scipy.spatial.transform import Rotation

def focalMM_to_focalPixel(focalMM, sensorWidth, imageWidth):
    pixelPitch = sensorWidth / imageWidth
    return focalMM / pixelPitch

# Read Image
im = cv2.imread("assets/cameraView.jpg")

imageWidth = 1920
imageHeight = 1080
imageSize = [imageWidth, imageHeight]

points_2D = np.array([
    (949, 49),
    (1415, 45),
    (962, 913),
    (1398, 977)
], dtype="double")

points_3D = np.array([
    (-.30208, -1.8218, 8.3037),
    (-.30208, -1.8303, 8.3037),
    (-.30208, -1.8218, 1.381),
    (-.30208, -1.8303, 1.381)
])

focalLengthMM = 50
sensorWidth = 36

fLength = focalMM_to_focalPixel(focalLengthMM, sensorWidth, imageWidth)
print("focalLengthPixel", fLength)

K = np.array([
    [fLength, 0, imageWidth/2],
    [0, fLength, imageHeight/2],
    [0, 0, 1]
])
distCoeffs = np.zeros((5, 1))

success, rvecs, tvecs = cv2.solvePnP(points_3D, points_2D, K, distCoeffs, flags=cv2.SOLVEPNP_ITERATIVE)

# 计算相机在世界坐标系的位置(OpenCV坐标系)
rmat = cv2.Rodrigues(rvecs)[0]
camera_position = -np.matrix(rmat).T @ np.matrix(tvecs)

# 投影验证
projPoints = cv2.projectPoints(points_3D, rvecs, tvecs, K, distCoeffs)[0]
for p in points_2D:
    cv2.circle(im, (int(p[0]), int(p[1])), 3, (0, 255, 0), -1)
for p in projPoints:
    cv2.circle(im, (int(p[0][0]), int(p[0][1])), 3, (255, 0, 0), -1)

# 启用验证窗口
cv2.imshow("Projection Check", im)
cv2.waitKey(0)
cv2.destroyAllWindows()

# 坐标系转换:OpenCV → Blender
# 位置转换
tx = camera_position[0][0]
ty = camera_position[2][0]
tz = -camera_position[1][0]

# 旋转转换
r = Rotation.from_rotvec(rvecs.flatten())
r_mat = r.as_matrix()
cv_to_blender = np.array([[1,0,0],[0,0,-1],[0,1,0]])
r_mat_blender = cv_to_blender @ r_mat @ cv_to_blender.T
r_blender = Rotation.from_matrix(r_mat_blender)
rot = r_blender.as_euler('xyz', degrees=True)

# 创建Blender相机
bpy.ops.object.camera_add()
camera = bpy.context.object
camera.location = (tx, ty, tz)
camera.rotation_euler = (np.radians(rot[0]), np.radians(rot[1]), np.radians(rot[2]))

# 设置相机参数
camera.data.lens = focalLengthMM
camera.data.sensor_width = sensorWidth 
camera.data.sensor_height = sensorWidth * (imageHeight / imageWidth)
# 原代码的shift参数计算正确,因为K的主点是图像中心,所以shift为0,可保留
camera.data.shift_x = (imageWidth / 2 - K[0, 2]) / imageWidth
camera.data.shift_y = (imageHeight / 2 - K[1, 2]) / imageHeight

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 08:07:06