본문 바로가기
실습 & 활동/Computer vision

[Blender] Camera parameter (Extrinsic & Intrinsic)

by sim0609 2024. 5. 23.

오늘은 blender에서 어떻게 camera parameter(intrinsic & extrinsic)를 추출할 수 있는지 소개하고자 한다. 

아래 코드는 글쓴이가 코딩한 코드는 아니고, 분석만 해봤다.

Intrinsic parameter

https://blender.stackexchange.com/questions/38009/3x4-camera-matrix-from-blender-camera/120063#120063

 

3x4 camera matrix from blender camera

In computer vision, the transformation from 3D world coordinates to pixel coordinates is often represented by a 3x4 (3 rows by 4 cols) matrix P as detailed below. Given a camera in Blender, I need ...

blender.stackexchange.com

 

def get_sensor_size(sensor_fit, sensor_x, sensor_y): # 센서의 가장 긴 부분 활용
    if sensor_fit == 'VERTICAL':
      return sensor_y
    return sensor_x

def get_sensor_fit(sensor_fit, size_x, size_y): 
    if sensor_fit == 'AUTO': # 자동으로 이미지의 가로, 세로 크기를 비교하며 sensor의 적합성 결정
        if size_x >= size_y: return 'HORIZONTAL' # 이미지의 가로가 더 길면 horizontal로 설정,
        else: return 'VERTICAL' # 이미지의 세로가 더 길면 vertical로 설정
    return sensor_fit

'''
 sensor_fit == vertical
 센서의 세로 길이를 최대한 활용해 이미지의 세로 방향을 강조할 수 있음
 이미지의 세로 방향의 왜곡을 감소시키고, 시각적인 디테일 강조 가능

'''

def get_camera_parameters_intrinsic(camera_obj, scene):
    """ Get intrinsic camera parameters: focal length and principal point. """
    # ref: https://blender.stackexchange.com/questions/38009/3x4-camera-matrix-from-blender-camera/120063#120063
    focal_length = camera_obj.data.lens # [mm], focal length
    res_x, res_y = bpy.context.scene.render.resolution_x,  bpy.context.scene.render.resolution_y # image resolution (가로, 세로)
    # res_x: 1920, res_y: 1080

    cam_data = camera_obj.data # 활성화된 camera 
    sensor_size_in_mm = get_sensor_size(cam_data.sensor_fit, cam_data.sensor_width, cam_data.sensor_height)
    # sensor_width: 1920, sensor_height: 1080 --> sensor_size_in_mm: 36

    sensor_fit = get_sensor_fit(cam_data.sensor_fit,scene.render.pixel_aspect_x * res_x,scene.render.pixel_aspect_y * res_y)
    # sensor_fit: Horizontal - 넓은 시야각과 가로로 긴 이미지 영역 제공
    # sensor_fit: Vertical - 좁은 시야각과 세로로 긴 이미지 영역 제공

    pixel_aspect_ratio = scene.render.pixel_aspect_y / scene.render.pixel_aspect_x
    # 2D 이미지 픽셀의 비율: 일반적으로 pixel은 정사각형이라 pixel_aspect_y, pixel_aspect_x는 모두 1이기에 가로 세로 픽셀의 비율은 1

    if sensor_fit == 'HORIZONTAL': view_fac_in_px = res_x
    else: view_fac_in_px = pixel_aspect_ratio * res_y
    # view_fac_in_px: 센서를 통해 투영된 이미지가 몇 개의 픽셀로 구성되는지를 나타냄

    pixel_size_mm_per_px = (sensor_size_in_mm / focal_length) / view_fac_in_px 

    f_x = 1.0 / pixel_size_mm_per_px # mm --> pixel
    # f_x = focal length * (pixel / mm)

    f_y = (1.0 / pixel_size_mm_per_px) / pixel_aspect_ratio # mm --> pixel
    # f_y = focal length * (pixel / mm) / pixel_aspect_ratio

    # ex. pixel의 가로, 세로 비율이 2:1인 경우 pixel_aspect_ratio는 1/2 이 되고,
    # --> f_y에 pixel_aspect_ratio를 나눠줘서 세로 방향의 픽셀 크기를 적절히 늘려서, pixel의 가로 비율 때문에 이미지가 가로로 늘어난 왜곡을 보정

    c_x = (res_x - 1) / 2.0 - cam_data.shift_x * view_fac_in_px 
    # cam_data.shift_x * view_fac_in_px: 센서가 x축 방향으로 이동한 양을 픽셀 단위로 변환해 보정 

    c_y = (res_y - 1) / 2.0 + (cam_data.shift_y * view_fac_in_px) / pixel_aspect_ratio
    # (cam_data.shift_y * view_fac_in_px) / pixel_aspect_ratio: 센서가 y축 방향으로 이동한 양을 픽셀 단위로 변환해 보정

    # (res_x, res_y - 1): 픽셀 인덱스가 0부터 시작하기 때문에 1 빼줌 - 이미지의 정확한 중심 찾기
    # 카메라 센서의 주점이 왜곡된 경우, 이미지의 중심에서 가로, 세로 방향으로 얼마나 이동해야 하는지를 픽셀 단위로 계산

    intrinsic_matrix = np.array([[f_x, 0, c_x], [0, f_y, c_y], [0, 0, 1]])
    # intrinsic_matrix:
    #[[2.66666667e+03 0.00000000e+00 9.59500000e+02]
    # [0.00000000e+00 2.66666667e+03 5.39500000e+02]
    # [0.00000000e+00 0.00000000e+00 1.00000000e+00]]

    return intrinsic_matrix

Extrinsic parameter

 

Blender는 Opengl 좌표계를 이용하기 때문에 extrinsic matrix를 구하는 코드를 보기 전, Opengl 좌표계에서 Opencv 좌표계로 어떻게 변환할 수 있는지 확인해보자.

아래 이미지는 Opencv와 Opengl 좌표계간의 차이다. 

Opencv: x(+), y(+), z(+)

Opengl: x(+), y(-), z(-)

 

만약 opengl → opencv 또는 opencv → opengl로 좌표계를 변환하고 싶다면 extrinsic matrix에 해당하는 y, z axis(3x4 행렬에서 2, 3번째 행)에 -를 곱해주면 된다. 

 

아래 코드에서 blender가 opengl를 따르기 때문에 opengl에서 opencv로 coordinate을 변환하기 위해서 extrinsic matrix를 구할 때 np.array([[1,  0,  0], [0, -1,  0], [0,  0, -1]])에 해당하는 matrix를 곱해줬다.

def get_camera_parameters_extrinsic(camera_obj): 
# blender는 opengl을 기본으로 하고, 아래는 extrinsic matrix를 opencv의 좌표계로 변환하기 위해서 extrinsic matrix의 2, 3번째 행(y, z axis)에 -를 곱한다. 

    # ref: https://blender.stackexchange.com/questions/38009/3x4-camera-matrix-from-blender-camera
    bcam = camera_obj # 활성화된 camera 
    R_bcam2cv = np.array([[1,  0,  0], [0, -1,  0], [0,  0, -1]]) # opengl --> opencv로 coordinate을 바꾸기 위해 coordinate의 
    # blender coord = opengl: np.array([[1,  0,  0], [0, -1,  0], [0,  0, -1]]) x축: 양옆, y축: 앞/뒤, z축: 위/아래
    # 일반적인 coord = opencv: np.array([[1,  0,  0], [0, 1,  0], [0,  0, 1]]) x축: 양옆, y축: 위/아래, z축: 앞/뒤

    location = np.array([bcam.matrix_world.decompose()[0]]).T
    # camera location == translation
    # bcam.matrix_world.decompose(): (Vector((2.819077968597412, 0.0, -1.0260604619979858)), Quaternion((0.4055797755718231, 0.5792279839515686, 0.5792280435562134, 0.40557974576950073)), Vector((1.0, 1.0, 1.0)))
    # bcam.matrix_world.decompose(): position, rotation(Quaternion), scale

    R_world2bcam = np.array(bcam.matrix_world.decompose()[1].to_matrix().transposed())
    # Quanternion == bcam.matrix_world.decompose()[1]
    # Quanternion으로 3x3 rotation matrix 만들기: bcam.matrix_world.decompose()[1].to_matrix()

    # C: world coordinate 기준 카메라의 위치, Rc: world coordinate 기준 카메라의 방향
    # R_world2bcam == (Rc)T <-- (Rc)T == R, camera coordinate 기준 카메라의 위치

    T_world2bcam = np.matmul(R_world2bcam.dot(-1), location)
    # T_world2bcam == -(Rc)T C <-- -(Rc)T C == t, camera coordinate 기준 카메라의 방향

    R_world2cv = np.matmul(R_bcam2cv, R_world2bcam)
    T_world2cv = np.matmul(R_bcam2cv, T_world2bcam)
    # rotation, translation matrix(camera coord 기준) 모두 opengl에서 opencv 좌표계 표현으로 변환
    
    extr = np.concatenate((R_world2cv, T_world2cv), axis=1)
    extr = np.concatenate((extr, np.array([[0,0,0,1]])), axis=0)
    return extr