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

[Gaussian Splatting] Colmap vs. Correct camera parameter

by sim0609 2024. 4. 12.

오늘은 Gaussian Splatting의 초기값으로 이용되는 camera parameter와 3D point cloud를 Colmap으로 추정한 값이 아닌 실제 정확한 값을 넣어보려 한다. 어떻게 정확한 camera parameter와 3D point cloud 값을 뽑았는지는 이후에 설명하도록 하겠다. (blender에서 camera parameter와 3D point cloud를 추출하는 코드를 작성하면 됨)

Camera parameter and 3D point cloud from Colmap 3.9 ver (Estimate)

이전글에서 Colmap 프로그램이나 cmd를 이용하면 추정된 camera parameter와 3D point cloud 값을 얻을 수 있다고 했다. Colmap을 돌린 결과는 C:\Users\user\Desktop\colmap_data\output\sparse\0 경로에서 아래와 같이 나온다. 

 

다음으로 gaussian_splatting 프로젝트 폴더에서 data 경로(C:\Users\user\gaussian-splatting\data\Scene1_output)에 새로운 input folder(ex. Scene1_output)를 생성한 후 아래와 같이 Colmap에서 얻은 sparse 폴더를 그대로 복붙해주면 된다.

그리고나서 cmd에 아래와 같은 명령어를 작성하면 gaussian-splatting 결과를 얻을 수 있다. 

python train.py -s <path to COLMAP or NeRF Synthetic dataset>

 

최종적으로 SIBR_viewer로 gaussian-splatting 결과를 보기위해서 아래 명령어를 작성해주면 다음과 같은 결과를 확인할 수 있다.

./<SIBR install dir>/bin/SIBR_gaussianViewer_app -m <path to trained model>

 

 

추정한 parameter들을 이용했을 때도 나름 3D reconstruction이 잘 이뤄졌다. 하지만, 여전히 손 부분이 잘 최적화된 것 같진 않다. 

Correct Camera parameter (Real Value)

위에서도 말했지만, 정확한 camera pose와 3D point cloud에 해당하는 값은 blender에서 3D scene을 2D image로 rendering할 때 얻어내면 된다. 정확한 camera pose와 3D point cloud를 포함한 폴더는C:\Users\user\gaussian-splatting\data\Scene1_output\sparse\0\Scene1_output 경로에 넣어주면 된다. 

 

그리고 기존 gaussian-splatting에서는 Colmap에서 추정된 camera pose와 3D point cloud가 이진 파일로 작성돼 있어 colmap_loader.py라는 python 코드에서 read_extrinsics_binary, read_points3D_binary, read_intrinsics_binary 함수를 통해 parameter들을 읽어들여 3D gaussian_splatting을 최적화한다. 하지만 이제는 정확한 값을 읽어들여 사용할거니까 read_extrinsics_binary, read_points3D_binary, read_intrinsics_binary 함수를 아래와 같은 read_extrinsics_custom, read_points3D_custom, read_intrinsics_custom 함수로 바꿔주면 된다.

def read_extrinsics_custom(path_to_model_file): 
    """
    see: src/base/reconstruction.cc
        void Reconstruction::ReadImagesBinary(const std::string& path)
        void Reconstruction::WriteImagesBinary(const std::string& path)
    """
    dir_img = os.path.join(path_to_model_file, "Scene1_output/image/")

    image_extension = ".png"
    dir_camera = os.path.join(path_to_model_file, "Scene1_output/camera/")

    # 이미지 파일 개수 세기
    image_count = 0
    for filename in os.listdir(dir_img):
      if os.path.splitext(filename)[1].lower() in image_extension:
          image_count += 1

    num = image_count
    images = {}
    for i in range(1, num+1):
      tvec_file_name = f"{i:03}_extrinsic.npy"
      tvec_file_path = os.path.join(dir_camera, tvec_file_name)

      extinsic_matrix = np.load(tvec_file_path)
      
      image_id = i
      qvec = rotmat2qvec(extinsic_matrix[:3, :3])  # quaternions need to be normalized if used in practice
      tvec = extinsic_matrix[:3, 3]
      rotmat = extinsic_matrix[:3, :3]
      image_name = f"rgb_0000_{i:03}.png"
      camera_id = 1
      #xys = np.random.rand(np.random.randint(10, 20), 2) * np.array([6000, 4000])
      #point3D_ids = np.full(xys.shape[0], -1)
      xys = np.array([[0, 0]])
      point3D_ids = np.array([-1])
      images[i] = Image(
                id=image_id, qvec=qvec, rotmat=rotmat, tvec=tvec,
                camera_id=camera_id, name=image_name,
                xys=xys, point3D_ids=point3D_ids)
      
    return images
    
###############################################################################
    def read_intrinsics_custom(path_to_model_file):
    """
    see: src/base/reconstruction.cc
        void Reconstruction::WriteCamerasBinary(const std::string& path)
        void Reconstruction::ReadCamerasBinary(const std::string& path)
    """
    cameras = {}

    dir_cam = os.path.join(path_to_model_file, "Scene1_output/camera/")
    dir_img = os.path.join(path_to_model_file, "Scene1_output/image/")
    
    intrinsic_file_name = f"001_intrinsic.npy"
    intrinsic_file_path = os.path.join(dir_cam, intrinsic_file_name)
    
    image_file_name = f"rgb_0000_001.png"
    image_file_path = os.path.join(dir_img, image_file_name)
    
    rgb_img = cv2.imread(image_file_path)
    height, width = rgb_img.shape[:2]
    intrinsic_matrix = np.load(intrinsic_file_path)
    camera_id = 1
    model_name = "PINHOLE"
    params = intrinsic_matrix[0][0], intrinsic_matrix[1][1], intrinsic_matrix[0][2], intrinsic_matrix[1][2]
    cameras[camera_id] = Camera(id=camera_id,
                                model=model_name,
                                width=width,
                                height=height,
                                params=np.array(params))
      
    return cameras
    
 ###############################################################################   
    def read_points3D_custom(path_to_model_file):
    """
    see: src/base/reconstruction.cc
        void Reconstruction::ReadPoints3DBinary(const std::string& path)
        void Reconstruction::WritePoints3DBinary(const std::string& path)
    """

    dir_img = os.path.join(path_to_model_file, "Scene1_output/point_color/points3D.txt")

    # 파일 읽기
    with open(dir_img, 'r') as file:
      data_str = file.read()

    # 문자열에서 배열 데이터 추출을 위한 정규 표현식
    pattern = r"array\((\[\[.*?\]\])\)"
    matches = re.findall(pattern, data_str, re.DOTALL)
    
    # 추출된 데이터를 NumPy 배열로 변환
    arrays = [np.array(eval(match)) for match in matches]
    
    # XYZ, RGB, Errors 데이터 할당
    xyzs, rgbs, errors = arrays

    return xyzs, rgbs, errors

 

colmap_loader.py라는 python 코드를 실행했을 때 intrinsic parameter, extrinsic parameter, 3D point cloud의 내용은 아래와 같은 형식으로 구성된다. 

colmap_loader.py에서 read_extrinsics_binary 함수 실행 결과

{..., 92: 

Image(

id=92, 

qvec=array([ 0.01159393, -0.17459234,  0.07754059,  0.98151442]), 

tvec=array([ 1.52849571, -0.12861904,  3.38701182]), 

camera_id=1, 

name='SSP_29_4_group10.JPG', 

xys=array([[5.77341009e+03, 3.38193115e+00],
       [5.17351682e+02, 6.94507897e+00],
       [5.17351682e+02, 6.94507897e+00],
       ...,
       [2.82861829e+03, 1.17531352e+03],
       [4.92487926e+03, 1.01604919e+03],
       [4.15170899e+03, 1.98041203e+03]]), 

point3D_ids=array([-1, -1, -1, ..., -1, -1, -1]))

}

colmap_loader.py에서 read_intrinsics_binary 함수 실행 결과

{1: Camera(id=1, model='PINHOLE', width=5999, height=3996, params=array([4791.4495127 , 4802.52744993, 2999.5       , 1998.        ]))}

colmap_loader.py에서 read_points3D_binary 함수 실행 결과

(array([[-3.49452014,  0.57867631,  3.61400152],
       [-0.96573499,  0.60572409,  4.48444386],
       [ 2.59194951,  2.3479933 ,  1.62724342],
       ...,
       [ 3.15271958, -0.17882777,  0.39905241],
       [ 2.40544774, -0.65511505,  2.66641317],
       [ 2.32393945,  1.00545333,  2.6340782 ]]), array([[ 72.,  63.,  53.],
       [ 93.,  84.,  79.],
       [163., 110.,  60.],
       ...,
       [ 95.,  61.,  42.],
       [142., 107.,  80.],
       [133.,  88.,  57.]]), array([[0.99134327],
       [1.67072138],
       [0.78799831],
       ...,
       [1.66790136],
       [1.34726359],
       [2.60064968]]))

 

위와 같은 형식이 gaussian-splatting 최적화의 input으로 사용되기 때문에read_extrinsics_custom, read_points3D_custom, read_intrinsics_custom 함수를 따로 만들어 동일한 format으로 입력되도록 코드를 변경해줬다. 이제 이후 단계는 Camera parameter and 3D point cloud from Colmap 3.9 ver (Estimate)에서 설명한 것과 같다. 

마찬가지로 SIBR_viewer로 gaussian-splatting 결과를 확인하면 다음과 같다. 

 

 

이렇게 지금까지 Colmap으로 추정한 camerac parameter와 3D point cloud로 gaussian-splatting을 돌렸을 때와 정확한 값을 이용해 gaussian-splatting을 돌렸을 때의 결과를 확인해 봤는데, 확실히 정확한 parameter를 넣어 사용해야지 손과 같은 세부적인 부분을 잘 reconstruction하는 것 같다.