NeRF 코드 분석: [6] 블렌더 데이터셋 분석

상솜공방·2025년 2월 12일
0

NeRF

목록 보기
6/8

모든 코드는 아래 링크에서 다운받을 수 있습니다.
https://github.com/Foxhead-Studio/NeRF-Code-Analysis/tree/main

[6] Blender Dataset Analysis

이번 장에서는 NeRF에서 대표적으로 쓰인 블렌더 데이터셋에 대해 살펴보고, 이것이 모델에 입력되기 전까지의 과정을 추적합니다.
이 데이터셋의 가장 큰 특징은 카메라의 위치를 나타내는 행렬(Cam to World Matrix, 이하 c2w)이 주어져있다는 것입니다.
블렌더는 가상의 공간에서 오브젝트를 렌더링한 것이므로 이 행렬을 알 수 있습니다.
그러나 실상황에서는 카메라의 위치를 알 수 없으니, COLMAP 알고리즘을 통하여 camera pose estimation을 수행해 c2w를 추론합니다.

1. 필요한 라이브러리 불러오기

import os
import cv2
import time
import json
import random
import numpy as np
import imageio.v2 as imageio

import torch
import torch.nn as nn
import torch.nn.functional as F

import argparse
from tqdm import tqdm, trange
import matplotlib.pyplot as plt
print(os.getcwd())
/home/white/Desktop/white/NeRF

2. argparse

# 이 함수는 `argparse.ArgumentParser` 객체를 생성하고, 그 안에 여러 인자를 추가한 뒤, 최종적으로 파서(parser) 객체를 리턴합니다.  
# 다른 곳에서 `config_parser()`를 호출하여, 그 결과로 리턴된 `parser` 객체를 통해 명령줄 인자를 파싱하게 됩니다.
def config_parser():
    parser = argparse.ArgumentParser() # parser라는 이름의 ArgumentParser 객체 생성

    parser.add_argument("--expname", type=str, 
                        help='experiment name') # 실험 이름
    parser.add_argument("--basedir", type=str, default='./logs/', 
                        help='where to store ckpts and logs') # 체크포인트와 로그 파일을 저장할 디렉토리
    parser.add_argument("--datadir", type=str, default='./data/llff/fern', 
                        help='input data directory') # 학습과 테스트에 사용할 데이터셋 디렉토리

    # training options
    parser.add_argument("--netdepth", type=int, default=8, # coarse 네트워크 레이어 개수
                        help='layers in network')
    parser.add_argument("--netwidth", type=int, default=256, # coarse 네트워크 채널 수
                        help='channels per layer')
    parser.add_argument("--netdepth_fine", type=int, default=8, # fine 네트워크 레이어 개수
                        help='layers in fine network')
    parser.add_argument("--netwidth_fine", type=int, default=256, 
                        help='channels per layer in fine network') # fine 네트워크 채널 수
    parser.add_argument("--N_rand", type=int, default=32*32*4, 
                        help='batch size (number of random rays per gradient step)') # 한 번의 학습 스텝마다 학습할 광선의 개수
    parser.add_argument("--lrate", type=float, default=5e-4, 
                        help='learning rate') # 초기 학습률
    parser.add_argument("--lrate_decay", type=int, default=250, 
                        help='exponential learning rate decay (in 1000 steps)') # 지수적 학습률 감소를 적용할 스텝의 수 (250 스텝마다 감소율 적용)
    parser.add_argument("--chunk", type=int, default=1024*32, 
                        help='number of rays processed in parallel, decrease if running out of memory') # 한 번에 병렬 처리할 광선의 개수
    parser.add_argument("--netchunk", type=int, default=1024*64, 
                        help='number of pts sent through network in parallel, decrease if running out of memory') # 한 번에 병렬처리할 샘플의 개수
    parser.add_argument("--no_batching", action='store_true', 
                        help='only take random rays from 1 image at a time') # 여러 이미지에서 광선을 샘플링하지 않고, 한 장의 이미지에서만 광선을 뽑아옴
    parser.add_argument("--no_reload", action='store_true', 
                        help='do not reload weights from saved ckpt') # 웨이트 파일을 불러오지 않음
    parser.add_argument("--ft_path", type=str, default=None, 
                        help='specific weights npy file to reload for coarse network') # fine-tuning 할 때 모델의 가중치를 불러오는 경로

    # rendering options
    parser.add_argument("--N_samples", type=int, default=64, 
                        help='number of coarse samples per ray') # coarse 학습 단계에서 하나의 광선에서 뽑을 샘플의 개수 (디폴트: 64)
    parser.add_argument("--N_importance", type=int, default=0,
                        help='number of additional fine samples per ray') # fine 학습 단계에서 하나의 광선에서 뽑을 샘플의 개수 (디폴트: 128)
    parser.add_argument("--perturb", type=float, default=1.,
                        help='set to 0. for no jitter, 1. for jitter') # 난수(jitter)를 추가하여 샘플링 위치를 조금씩 흔들어주는 정도. 0이면 완전 균일, 1이면 논문에서 제안한 섭동(perturb)
    parser.add_argument("--use_viewdirs", action='store_true', 
                        help='use full 5D input instead of 3D') # 방향 정보를 추가로 포함할지 여부 (True이면 5D 입력)
    parser.add_argument("--i_embed", type=int, default=0, 
                        help='set 0 for default positional encoding, -1 for none') # 포지셔널 인코딩 방식 선택. 0이면 기본, -1이면 사용 안 함.
    parser.add_argument("--multires", type=int, default=10, 
                        help='log2 of max freq for positional encoding (3D location)') # 위치 정보(3차원)에 대한 포지셔널 인코딩에서 사용하는 옥타브 수 (L)
    parser.add_argument("--multires_views", type=int, default=4, 
                        help='log2 of max freq for positional encoding (2D direction)') # 방향 정보(3차원)에 대한 포지셔널 인코딩에서 사용하는 옥타브 수 (L)
    parser.add_argument("--raw_noise_std", type=float, default=0., 
                        help='std dev of noise added to regularize sigma_a output, 1e0 recommended') # 밀도(sigma)를 정규화 하기 위해 노이즈를 추가할 때 사용하는 표준편차(std dev)

    parser.add_argument("--render_only", action='store_true', 
                        help='do not optimize, reload weights and render out render_poses path') # 학습을 수행하지 않고, 저장된 가중치를 불러와 렌더링만 실행
    parser.add_argument("--render_test", action='store_true', 
                        help='render the test set instead of render_poses path') # 학습을 수행하지 않고, 테스트 세트를 렌더링
    parser.add_argument("--render_factor", type=int, default=0, 
                        help='downsampling factor to speed up rendering, set 4 or 8 for fast preview') # 렌더링 시 해상도를 낮춰 다운샘플링을 할 때, 이미지의 축소 비율 (0이면 사용 안 함)

    # training options
    parser.add_argument("--precrop_iters", type=int, default=0,
                        help='number of steps to train on central crops') # 이미지 중앙 부분만 잘라내 중심부만 집중적으로 학습하는 pre-cropping에 대해, 이 방식으로 학습할 스텝의 수
    parser.add_argument("--precrop_frac", type=float,
                        default=.5, help='fraction of img taken for central crops') # 이미지를 얼마나 크게 잘라서 학습할지 결정 (0.5의 경우 중앙 영역에서 50%만 사용)

    # dataset options
    parser.add_argument("--dataset_type", type=str, default='llff', 
                        help='options: llff / blender / deepvoxels') # llff, blender, deepvoxels 등 원하는 데이터셋 선택
    parser.add_argument("--testskip", type=int, default=8, 
                        help='will load 1/N images from test/val sets, useful for large datasets like deepvoxels') # 테스트 데이터가 방대할 때 1/N 이미지만 로드하여 사용 (train의 경우 이 비율을 무시)

    ## deepvoxels flags
    parser.add_argument("--shape", type=str, default='greek', 
                        help='options : armchair / cube / greek / vase') # deepvoxels 데이터셋을 사용할 때, 어떤 오브젝트를 사용할지 결정

    ## blender flags
    parser.add_argument("--white_bkgd", action='store_true', 
                        help='set to render synthetic data on a white bkgd (always use for dvoxels)') # 이미지의 배경을 흰색으로 할지 여부
    parser.add_argument("--half_res", action='store_true', 
                        help='load blender synthetic data at 400x400 instead of 800x800') # 800x800 대신 400x400으로 해상도를 절반으로 낮춰서 학습 시간을 단축

    ## llff flags
    parser.add_argument("--factor", type=int, default=8, 
                        help='downsample factor for LLFF images') # LLFF 이미지를 얼마나 다운샘플링할지(해상도 감소) 결정.
    parser.add_argument("--no_ndc", action='store_true', 
                        help='do not use normalized device coordinates (set for non-forward facing scenes)') # NDC (Normalized Device Coordinates) 사용 여부 (카메라가 전방을 향해 있지 않은 데이터셋일 경우나, 구체적인 좌표 변환 방식이 다를 때 활용)
    parser.add_argument("--lindisp", action='store_true', 
                        help='sampling linearly in disparity rather than depth') # 깊이에에 대한 선형 샘플링 대신 깊이의 역수인 disparity(1/Depth)에 대한 선형 샘플링 여부 (가까운 위치의 좌표를 더 많이 추출)
    parser.add_argument("--spherify", action='store_true', 
                        help='set for spherical 360 scenes') # 장면이 360도로 되어있을 때 설정
    parser.add_argument("--llffhold", type=int, default=8, 
                        help='will take every 1/N images as LLFF test set, paper uses 8') # LLFF에서 테스트 세트를 만들기 위해 1/N 이미지 간격만큼 나누는 옵션 (디폴트 8)

    # logging/saving options
    parser.add_argument("--i_print",   type=int, default=100, 
                        help='frequency of console printout and metric loggin') # 콘솔에 학습 상태를 출력하는 빈도 (단위는 스텝)
    parser.add_argument("--i_img",     type=int, default=500, 
                        help='frequency of tensorboard image logging') # 텐서보드에 이미지를 로그하는 빈도
    parser.add_argument("--i_weights", type=int, default=10000, 
                        help='frequency of weight ckpt saving') # 모델 가중치를 저장하는 빈도
    parser.add_argument("--i_testset", type=int, default=50000, 
                        help='frequency of testset saving') # 테스트 셋을 돌려서 결과를 저장하는 빈도
    parser.add_argument("--i_video",   type=int, default=50000,
                        help='frequency of render_poses video saving') # render_poses를 영상으로 만들어 저장하는 빈도

    return parser
parser = config_parser() # 위에서 정의한 config_parser() 함수를 호출해 객체 생성
args = parser.parse_args(args=[]) # args = []를 통해 모든 인자가 디폴트 값인채로 args 객체 생성
args.config = 'configs/lego.txt' # args에 config라는 속성을 추가. (설정 파일의 경로 지정)

"""
<configs/lego.txt>
expname = blender_paper_lego
basedir = ./logs
datadir = ./data/nerf_synthetic/lego
dataset_type = blender

no_batching = True

use_viewdirs = True
white_bkgd = True
lrate_decay = 500

N_samples = 64
N_importance = 128
N_rand = 1024

precrop_iters = 500
precrop_frac = 0.5

half_res = True
"""
'\n<configs/lego.txt>\nexpname = blender_paper_lego\nbasedir = ./logs\ndatadir = ./data/nerf_synthetic/lego\ndataset_type = blender\n\nno_batching = True\n\nuse_viewdirs = True\nwhite_bkgd = True\nlrate_decay = 500\n\nN_samples = 64\nN_importance = 128\nN_rand = 1024\n\nprecrop_iters = 500\nprecrop_frac = 0.5\n\nhalf_res = True\n'
parser = config_parser() # 먼저 config_parser() 함수를 호출해 parser(ArgumentParser 객체)를 받습니다.
args = parser.parse_args(args=[]) # parse_args(args=[])는 빈 리스트를 인자로 넘기는 것이므로, 실제로는 명령줄 인자를 주지 않은 채 모든 인자를 기본값으로 파싱합니다.

# args.config라는 속성을 추가로 지정하여, config_parser에서 정의된 인자 외에 **‘config’**라는 속성을 하나 더 줍니다.
# 'configs/lego.txt'라는 텍스트 파일(설정 파일) 경로를 저장해 둡니다.
args.config = 'configs/lego.txt'
# load config
with open(args.config, 'r') as fp: # lego.txt 파일 불러오기
    lines = fp.readlines() # fp.readlines()로 모든 줄을 읽어 lines 리스트를 얻습니다.
    lines = [line.strip() for line in lines] # line.strip()을 통해 앞뒤 공백을 제거하고,
    lines = [line.split(' = ') for line in lines if len(line)>0] # ' = '을 기준으로 split해서 ['expname', 'lego'] 같은 구조로 만듭니다. (단순히 ' = ' 구분자를 사용해 key/value로 나누는 방식)

configs = {} # 그런 다음 configs = {} 딕셔너리를 만든 뒤
for line in lines: # 각 줄(line)을 순회하면서
    configs[line[0]] = line[1] # configs[line[0]] = line[1]로 key-value 형태로 저장합니다.

for key, value in configs.items():
    print(f"{key}: {value}")
expname: blender_paper_lego
basedir: ./logs
datadir: ./data/nerf_synthetic/lego
dataset_type: blender
no_batching: True
use_viewdirs: True
white_bkgd: True
lrate_decay: 500
N_samples: 64
N_importance: 128
N_rand: 1024
precrop_iters: 500
precrop_frac: 0.5
half_res: True
args.expname = configs['expname']
args.basedir = configs['basedir']
args.datadir = configs['datadir']
args.dataset_type = configs['dataset_type']
args.no_batching = configs['no_batching']
args.use_viewdirs = configs['use_viewdirs']
args.white_bkgd = configs['white_bkgd']
args.lrate_decay = int(configs['lrate_decay'])
args.N_samples = int(configs['N_samples'])
args.N_importance = int(configs['N_importance'])
args.N_rand = int(configs['N_rand'])
args.precrop_iters = int(configs['precrop_iters'])
args.precrop_frac = float(configs['precrop_frac'])
args.half_res = configs['half_res']
args.no_reload = True

for key, value in vars(args).items():
    print(f"{key}: {value}")

device = torch.device("cuda")
DEBUG=False
print(device)
expname: blender_paper_lego
basedir: ./logs
datadir: ./data/nerf_synthetic/lego
netdepth: 8
netwidth: 256
netdepth_fine: 8
netwidth_fine: 256
N_rand: 1024
lrate: 0.0005
lrate_decay: 500
chunk: 32768
netchunk: 65536
no_batching: True
no_reload: True
ft_path: None
N_samples: 64
N_importance: 128
perturb: 1.0
use_viewdirs: True
i_embed: 0
multires: 10
multires_views: 4
raw_noise_std: 0.0
render_only: False
render_test: False
render_factor: 0
precrop_iters: 500
precrop_frac: 0.5
dataset_type: blender
testskip: 8
shape: greek
white_bkgd: True
half_res: True
factor: 8
no_ndc: False
lindisp: False
spherify: False
llffhold: 8
i_print: 100
i_img: 500
i_weights: 10000
i_testset: 50000
i_video: 50000
config: configs/lego.txt
cuda

3. 블렌더 데이터셋 분석

pose_sherical()

이제 본격적으로 블렌더 데이터셋의 구조를 살펴보겠습니다.
우선 아래의 pose_spherical 함수가 기억나지 않는다면 1장을 복습해주세요.
이 함수는 우리가 보고 싶은 각도의 카메라 위치를 계산해서 반환합니다.
블렌더 데이터를 로드해오는 load_blender_data() 함수 내부에서, 렌더링 테스트를 하고 싶은 위치의 c2w 매트릭스 집합인 render_poses를 만들 때 한 번 쓰입니다.
그러나 카메라의 이동을 회전과 평행이동으로 분해하여 해석하는 과정은 다중 시점 기하학의 기초이므로 꼭 이해해두기 바랍니다.

# Z축으로 t만큼 이동
trans_t = lambda t : torch.Tensor([
    [1,0,0,0],
    [0,1,0,0],
    [0,0,1,t],
    [0,0,0,1]]).float()

# X축 기준 phi만큼 회전
rot_phi = lambda phi : torch.Tensor([
    [1,0,0,0],
    [0,np.cos(phi),-np.sin(phi),0],
    [0,np.sin(phi), np.cos(phi),0],
    [0,0,0,1]]).float()

# Y축 기준 -theta만큼 회전
rot_theta = lambda th : torch.Tensor([
    [np.cos(th),0,-np.sin(th),0],
    [0,1,0,0],
    [np.sin(th),0, np.cos(th),0],
    [0,0,0,1]]).float()

def pose_spherical(theta, phi, radius):
    c2w = trans_t(radius) # Z축으로 t만큼 이동
    c2w = rot_phi(phi/180.*np.pi) @ c2w # X축으로 phi만큼 회전
    c2w = rot_theta(theta/180.*np.pi) @ c2w # Y축으로 -theta만큼 회전
    c2w = torch.Tensor(np.array([[-1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]])) @ c2w # world 좌표계 x-axis를 반대로, y-axis & z-axis 바꾸기
    # 즉 X축으로 -phi, Y축으로 -theta만큼 회전한 후 Y, Z축 바꾸기
    # look at point는 보통 원점, 혹은 원점 근처. 따라서 camera 가 바라보는 방향은 camera 의 - z-axis
    return c2w

데이터셋에 들어있는 메타 데이터와 실제 이미지를 살펴보겠습니다.

  • transform_matrixes: 이미지에 대응하는 c2w 행렬
  • rotations: 카메라의 회전 각도
# data practice
_datadir = './data/nerf_synthetic/lego'

# 
with open(os.path.join(_datadir, 'transforms_train.json'), 'r') as fp:
    meta = json.load(fp)

_frames = meta['frames']
_frame = _frames[0]
print(_frame) # 하나의 이미지에 대한 메타 데이터

_fname = os.path.join(_datadir, _frame['file_path']+'.png')
print(_fname) # 이미지 파일 경로

img = imageio.imread(_fname)
print(img.shape) # 4 channel rgba

print(len(meta['frames'])) # 이미지는 총 100장

rotations = []
transform_matrixes = []

for i in range(len(meta['frames'])):
    rotations.append(meta['frames'][i]['rotation'])
    transform_matrixes.append(meta['frames'][i]['transform_matrix'])

print(rotations)
print(transform_matrixes)

# rotations 도표화
plt.figure(figsize=(10, 5))
plt.plot(rotations, marker='o', linestyle='-', markersize=4, label='Value Trend')
plt.title("Rotations", fontsize=14)
plt.xlabel("Index", fontsize=12)
plt.ylabel("Value", fontsize=12)
plt.grid(alpha=0.5)
plt.legend()
plt.show()
{'file_path': './train/r_0', 'rotation': 0.012566370614359171, 'transform_matrix': [[-0.9999021887779236, 0.004192245192825794, -0.013345719315111637, -0.05379832163453102], [-0.013988681137561798, -0.2996590733528137, 0.95394366979599, 3.845470428466797], [-4.656612873077393e-10, 0.9540371894836426, 0.29968830943107605, 1.2080823183059692], [0.0, 0.0, 0.0, 1.0]]}
./data/nerf_synthetic/lego/./train/r_0.png
(800, 800, 4)
100
[0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171, 0.012566370614359171]
[[[-0.9999021887779236, 0.004192245192825794, -0.013345719315111637, -0.05379832163453102], [-0.013988681137561798, -0.2996590733528137, 0.95394366979599, 3.845470428466797], [-4.656612873077393e-10, 0.9540371894836426, 0.29968830943107605, 1.2080823183059692], [0.0, 0.0, 0.0, 1.0]], [[-0.9305422306060791, 0.11707554012537003, -0.34696459770202637, -1.398659110069275], [-0.3661845624446869, -0.29751041531562805, 0.8817007541656494, 3.5542497634887695], [7.450580596923828e-09, 0.9475130438804626, 0.3197172284126282, 1.2888214588165283], [0.0, 0.0, 0.0, 1.0]], [[0.4429636299610138, 0.31377720832824707, -0.8398374915122986, -3.385493516921997], [-0.8965396881103516, 0.1550314873456955, -0.41494810581207275, -1.6727094650268555], [0.0, 0.936754584312439, 0.3499869406223297, 1.4108426570892334], [0.0, 0.0, 0.0, 1.0]], [[0.7956318259239197, 0.5209260582923889, -0.3092023432254791, -1.2464345693588257], [-0.6057805418968201, 0.6841840147972107, -0.40610620379447937, -1.6370664834976196], [-1.4901161193847656e-08, 0.5104197859764099, 0.859925389289856, 3.4664700031280518], [0.0, 0.0, 0.0, 1.0]], [[-0.6566542387008667, -0.2367822825908661, 0.7160581946372986, 2.8865230083465576], [0.7541918158531189, -0.20615987479686737, 0.623452365398407, 2.513216972351074], [-1.4901162970204496e-08, 0.9494377970695496, 0.31395500898361206, 1.26559317111969], [0.0, 0.0, 0.0, 1.0]], [[-0.12173833698034286, 0.6862695217132568, -0.7170870900154114, -2.8906705379486084], [-0.9925621747970581, -0.084171362221241, 0.08795115351676941, 0.35454243421554565], [0.0, 0.7224606275558472, 0.691412091255188, 2.7871713638305664], [0.0, 0.0, 0.0, 1.0]], [[0.6570732593536377, -0.5113321542739868, 0.5538899898529053, 2.232801914215088], [0.7538267374038696, 0.44570285081863403, -0.48279836773872375, -1.9462224245071411], [-1.4901161193847656e-08, 0.7347709536552429, 0.6783152222633362, 2.7343761920928955], [0.0, 0.0, 0.0, 1.0]], [[0.9045065641403198, 0.42524266242980957, -0.03219788148999214, -0.12979382276535034], [-0.426459938287735, 0.9019248485565186, -0.0682905837893486, -0.2752881646156311], [0.0, 0.07550014555454254, 0.9971457123756409, 4.019623279571533], [0.0, 0.0, 0.0, 1.0]], [[0.159989595413208, 0.33046624064445496, -0.930158793926239, -3.7495899200439453], [-0.9871187806129456, 0.053561095148324966, -0.15075767040252686, -0.607723593711853], [0.0, 0.9422966837882996, 0.33477866649627686, 1.3495359420776367], [0.0, 0.0, 0.0, 1.0]], [[-0.23337772488594055, 0.44256603717803955, -0.8658348917961121, -3.4902923107147217], [-0.9723862409591675, -0.1062181368470192, 0.2078048586845398, 0.8376882672309875], [7.450581485102248e-09, 0.8904228806495667, 0.45513397455215454, 1.834703803062439], [0.0, 0.0, 0.0, 1.0]], [[0.5218667387962341, 0.18684424459934235, -0.8323126435279846, -3.3551595211029053], [-0.8530270457267761, 0.1143079623579979, -0.509194016456604, -2.052626848220825], [-7.450580596923828e-09, 0.9757166504859924, 0.21903668344020844, 0.882965087890625], [0.0, 0.0, 0.0, 1.0]], [[-0.9757387638092041, -0.17523755133152008, 0.13124708831310272, 0.52907395362854], [0.21893838047981262, -0.7809780836105347, 0.5849266052246094, 2.357914447784424], [-7.450581485102248e-09, 0.5994703769683838, 0.8003968596458435, 3.2265028953552246], [0.0, 0.0, 0.0, 1.0]], [[-0.9713073372840881, 0.20477895438671112, -0.12094451487064362, -0.48754292726516724], [-0.23782765865325928, -0.8363337516784668, 0.4939471483230591, 1.9911646842956543], [7.450580596923828e-09, 0.5085384249687195, 0.8610392212867737, 3.4709601402282715], [0.0, 0.0, 0.0, 1.0]], [[0.842908501625061, -0.09502744674682617, 0.5295989513397217, 2.1348819732666016], [0.5380570292472839, 0.14886793494224548, -0.8296582698822021, -3.3444597721099854], [7.450582373280668e-09, 0.9842804074287415, 0.17661221325397491, 0.7119466662406921], [0.0, 0.0, 0.0, 1.0]], [[-0.06348517537117004, 0.8717355132102966, -0.48584654927253723, -1.9585098028182983], [-0.9979828596115112, -0.05545414239168167, 0.030906395986676216, 0.12458765506744385], [0.0, 0.4868285059928894, 0.8734976053237915, 3.521181106567383], [0.0, 0.0, 0.0, 1.0]], [[-0.6865485906600952, -0.3086281418800354, 0.6583309769630432, 2.6538169384002686], [0.7270838618278503, -0.2914219796657562, 0.6216287612915039, 2.5058655738830566], [0.0, 0.9054401516914368, 0.42447394132614136, 1.7111091613769531], [0.0, 0.0, 0.0, 1.0]], [[0.1303573101758957, 0.5973315834999084, -0.7913291454315186, -3.1899499893188477], [-0.9914670586585999, 0.07853669673204422, -0.10404333472251892, -0.41941213607788086], [7.4505797087454084e-09, 0.7981396317481995, 0.6024724841117859, 2.4286444187164307], [0.0, 0.0, 0.0, 1.0]], [[0.28662192821502686, -0.25357696413993835, 0.9238758683204651, 3.7242627143859863], [0.95804363489151, 0.07586367428302765, -0.27639979124069214, -1.1142032146453857], [0.0, 0.9643357992172241, 0.26468199491500854, 1.066967248916626], [0.0, 0.0, 0.0, 1.0]], [[-0.09750017523765564, -0.9662679433822632, 0.23836953938007355, 0.9608983993530273], [0.9952355623245239, -0.09466230124235153, 0.023352332413196564, 0.09413626790046692], [-1.862645371275562e-09, 0.23951061069965363, 0.9708936214447021, 3.913797616958618], [0.0, 0.0, 0.0, 1.0]], [[0.5655173063278198, -0.2501452565193176, 0.7858863472938538, 3.1680095195770264], [0.8247364163398743, 0.17152325809001923, -0.5388780236244202, -2.1722869873046875], [0.0, 0.9528940916061401, 0.30330324172973633, 1.2226545810699463], [0.0, 0.0, 0.0, 1.0]], [[0.8422731161117554, -0.25616011023521423, 0.47429734468460083, 1.9119539260864258], [0.5390509963035583, 0.40025293827056885, -0.7410947680473328, -2.9874486923217773], [1.4901161193847656e-08, 0.8798747658729553, 0.475205659866333, 1.915615439414978], [0.0, 0.0, 0.0, 1.0]], [[0.9048085808753967, 0.3769117295742035, -0.19813843071460724, -0.7987216114997864], [-0.4258183538913727, 0.8008883595466614, -0.4210183620452881, -1.6971794366836548], [0.0, 0.4653121531009674, 0.8851466178894043, 3.5681405067443848], [0.0, 0.0, 0.0, 1.0]], [[-0.4602559208869934, -0.5367125868797302, 0.7071803212165833, 2.8507351875305176], [0.887786328792572, -0.2782484292984009, 0.3666241765022278, 1.4779094457626343], [0.0, 0.7965658903121948, 0.6045514941215515, 2.437025308609009], [0.0, 0.0, 0.0, 1.0]], [[-0.4675160050392151, 0.5285975337028503, -0.7085289359092712, -2.8561718463897705], [-0.8839846849441528, -0.27956119179725647, 0.37472212314605713, 1.5105533599853516], [0.0, 0.8015173077583313, 0.5979713797569275, 2.4105000495910645], [0.0, 0.0, 0.0, 1.0]], [[0.7739522457122803, 0.35971799492836, -0.5211533904075623, -2.1008365154266357], [-0.633243978023529, 0.4396481513977051, -0.6369548439979553, -2.5676472187042236], [0.0, 0.8229899406433105, 0.5680559277534485, 2.2899067401885986], [0.0, 0.0, 0.0, 1.0]], [[-0.1613457202911377, -0.3997095823287964, 0.9023301005363464, 3.637409210205078], [0.9868979454040527, -0.06534761935472488, 0.14751990139484406, 0.5946717858314514], [3.725290298461914e-09, 0.914309561252594, 0.40501612424850464, 1.6326723098754883], [0.0, 0.0, 0.0, 1.0]], [[-0.9752575159072876, -0.03920060396194458, 0.21756872534751892, 0.8770476579666138], [0.22107206284999847, -0.17293314635753632, 0.9598026275634766, 3.8690884113311768], [3.725290742551124e-09, 0.9841530919075012, 0.1773204803466797, 0.7148017883300781], [0.0, 0.0, 0.0, 1.0]], [[-0.9902960658073425, 0.09176140278577805, -0.10437164455652237, -0.42073559761047363], [-0.13897335529327393, -0.6538731455802917, 0.7437312006950378, 2.9980766773223877], [0.0, 0.7510190606117249, 0.6602804064750671, 2.6616756916046143], [0.0, 0.0, 0.0, 1.0]], [[0.7852338552474976, -0.33010709285736084, 0.5238673686981201, 2.111776828765869], [0.619199275970459, 0.4186233878135681, -0.6643393039703369, -2.678037405014038], [-1.4901161193847656e-08, 0.8460399508476257, 0.5331193804740906, 2.1490728855133057], [0.0, 0.0, 0.0, 1.0]], [[0.8931812047958374, 0.20586314797401428, -0.3998093008995056, -1.6116830110549927], [-0.44969668984413147, 0.4088824987411499, -0.7940956354141235, -3.2011020183563232], [0.0, 0.889064371585846, 0.45778220891952515, 1.8453792333602905], [0.0, 0.0, 0.0, 1.0]], [[-0.28673890233039856, -0.8562213182449341, 0.42972761392593384, 1.7322875261306763], [0.9580087661743164, -0.25627318024635315, 0.12862055003643036, 0.5184860825538635], [0.0, 0.44856342673301697, 0.8937509655952454, 3.60282564163208], [0.0, 0.0, 0.0, 1.0]], [[-0.15501070022583008, 0.9340341687202454, -0.32179465889930725, -1.2971957921981812], [-0.9879127740859985, -0.1465567648410797, 0.050491925328969955, 0.20353946089744568], [0.0, 0.3257317543029785, 0.945462167263031, 3.8112800121307373], [0.0, 0.0, 0.0, 1.0]], [[6.262858369154856e-05, 0.21157889068126678, -0.9773608446121216, -3.9398677349090576], [-1.0, 1.3250886695459485e-05, -6.121072510723025e-05, -0.0002467483573127538], [0.0, 0.9773607850074768, 0.21157889068126678, 0.8529018759727478], [0.0, 0.0, 0.0, 1.0]], [[0.7431973218917847, -0.5280798077583313, 0.41083991527557373, 1.6561487913131714], [0.6690723896026611, 0.5865845680236816, -0.4563559293746948, -1.8396297693252563], [0.0, 0.6140441298484802, 0.7892715930938721, 3.1816558837890625], [0.0, 0.0, 0.0, 1.0]], [[-0.7566120028495789, 0.34157976508140564, -0.5575495362281799, -2.247554063796997], [-0.6538640856742859, -0.39525550603866577, 0.6451627016067505, 2.600733995437622], [0.0, 0.8526995182037354, 0.5224018096923828, 2.1058690547943115], [0.0, 0.0, 0.0, 1.0]], [[-0.3977896273136139, 0.3703862130641937, -0.8393910527229309, -3.3836936950683594], [-0.9174765944480896, -0.1605880707502365, 0.36393412947654724, 1.4670655727386475], [0.0, 0.9148910045623779, 0.40370094776153564, 1.6273707151412964], [0.0, 0.0, 0.0, 1.0]], [[0.7656702995300293, -0.4731244146823883, 0.4357779026031494, 1.7566769123077393], [0.6432332396507263, 0.5631818175315857, -0.5187265872955322, -2.0910537242889404], [2.9802320611338473e-08, 0.6774803400039673, 0.7355409264564514, 2.965060234069824], [0.0, 0.0, 0.0, 1.0]], [[-0.46346595883369446, -0.8589088916778564, 0.21788737177848816, 0.8783321380615234], [0.886114776134491, -0.4492363929748535, 0.11396195739507675, 0.4593953788280487], [0.0, 0.24589067697525024, 0.969297468662262, 3.9073634147644043], [0.0, 0.0, 0.0, 1.0]], [[-0.8262399435043335, 0.21388964354991913, -0.5211319923400879, -2.10075044631958], [-0.5633181929588318, -0.3137199878692627, 0.7643639445304871, 3.081249952316284], [1.4901159417490817e-08, 0.9251114130020142, 0.3796958923339844, 1.5306031703948975], [0.0, 0.0, 0.0, 1.0]], [[-0.9966232776641846, 0.07444921880960464, -0.034631188958883286, -0.13960279524326324], [-0.0821097195148468, -0.9036424160003662, 0.4203430414199829, 1.6944570541381836], [0.0, 0.4217672646045685, 0.9067041277885437, 3.655041217803955], [0.0, 0.0, 0.0, 1.0]], [[-0.4379815459251404, 0.34720176458358765, -0.8292304277420044, -3.3427348136901855], [-0.8989839553833008, -0.16915537416934967, 0.40399786829948425, 1.6285674571990967], [-1.4901161193847656e-08, 0.922408401966095, 0.3862157464027405, 1.5568854808807373], [0.0, 0.0, 0.0, 1.0]], [[-0.994414210319519, 0.02083449251949787, -0.10347140580415726, -0.41710659861564636], [-0.10554812848567963, -0.19629065692424774, 0.9748484492301941, 3.9297399520874023], [3.7252898543727042e-09, 0.9803244471549988, 0.1973932385444641, 0.7957176566123962], [0.0, 0.0, 0.0, 1.0]], [[-0.9928660988807678, -0.019224023446440697, 0.11767476797103882, 0.47436219453811646], [0.11923471093177795, -0.160078227519989, 0.9798764586448669, 3.9500086307525635], [0.0, 0.9869171380996704, 0.16122838854789734, 0.6499325037002563], [0.0, 0.0, 0.0, 1.0]], [[0.6021551489830017, -0.635220468044281, 0.48363634943962097, 1.9496005773544312], [0.7983791828155518, 0.47909730672836304, -0.36476919054985046, -1.4704318046569824], [-1.4901162970204496e-08, 0.6057727932929993, 0.7956376075744629, 3.207318067550659], [0.0, 0.0, 0.0, 1.0]], [[0.49585968255996704, 0.7388697266578674, -0.45628345012664795, -1.8393374681472778], [-0.8684027194976807, 0.4218961298465729, -0.260538786649704, -1.0502654314041138], [0.0, 0.5254284143447876, 0.850837767124176, 3.4298367500305176], [0.0, 0.0, 0.0, 1.0]], [[-0.5594504475593567, 0.5238707065582275, -0.6423196196556091, -2.589273452758789], [-0.8288636207580566, -0.3535921573638916, 0.43354055285453796, 1.7476580142974854], [0.0, 0.774940013885498, 0.6320346593856812, 2.5478134155273438], [0.0, 0.0, 0.0, 1.0]], [[0.9999964833259583, -0.0018918740097433329, 0.0018659689230844378, 0.007521961349993944], [0.0026572593487799168, 0.7119618654251099, -0.7022131085395813, -2.830711603164673], [0.0, 0.7022156119346619, 0.7119643688201904, 2.8700201511383057], [0.0, 0.0, 0.0, 1.0]], [[-0.9168165326118469, -0.3853738605976105, 0.1045677587389946, 0.42152610421180725], [0.3993086814880371, -0.8848220705986023, 0.2400885820388794, 0.9678280353546143], [-7.450580596923828e-09, 0.26187196373939514, 0.9651026129722595, 3.8904531002044678], [0.0, 0.0, 0.0, 1.0]], [[0.9999156594276428, -0.012454097159206867, 0.003698349464684725, 0.014908524230122566], [0.012991628609597683, 0.9585440158843994, -0.28464773297309875, -1.1474518775939941], [0.0, 0.2846718430519104, 0.9586249589920044, 3.8643410205841064], [0.0, 0.0, 0.0, 1.0]], [[0.3739576041698456, -0.8564795851707458, 0.35580646991729736, 1.434301733970642], [0.9274457693099976, 0.3453431725502014, -0.1434655785560608, -0.5783282518386841], [0.0, 0.38364121317863464, 0.9234821796417236, 3.7226758003234863], [0.0, 0.0, 0.0, 1.0]], [[-0.9326563477516174, -0.22499778866767883, 0.282007098197937, 1.1368069648742676], [0.36076587438583374, -0.5816670060157776, 0.7290481925010681, 2.938887357711792], [0.0, 0.7816900014877319, 0.6236670613288879, 2.514082431793213], [0.0, 0.0, 0.0, 1.0]], [[-0.8471797108650208, 0.14040516316890717, -0.5124186873435974, -2.0656259059906006], [-0.5313065052032471, -0.22387906908988953, 0.8170626759529114, 3.2936851978302], [7.450580596923828e-09, 0.9644504189491272, 0.26426392793655396, 1.0652819871902466], [0.0, 0.0, 0.0, 1.0]], [[0.5719918608665466, 0.34370848536491394, -0.7447749376296997, -3.002284049987793], [-0.8202592730522156, 0.23967842757701874, -0.5193543434143066, -2.0935845375061035], [-1.4901160305669237e-08, 0.9079751372337341, 0.41902410984039307, 1.6891403198242188], [0.0, 0.0, 0.0, 1.0]], [[0.2079109102487564, 0.16523794829845428, -0.9640898704528809, -3.886370897293091], [-0.9781477451324463, 0.03512227535247803, -0.2049228399991989, -0.8260704278945923], [0.0, 0.9856281876564026, 0.16892939805984497, 0.6809762716293335], [0.0, 0.0, 0.0, 1.0]], [[-0.30069297552108765, 0.47792530059814453, -0.8253308534622192, -3.3270153999328613], [-0.9537209868431091, -0.15068219602108002, 0.26021361351013184, 1.048954725265503], [7.450580596923828e-09, 0.8653798699378967, 0.5011163353919983, 2.0200648307800293], [0.0, 0.0, 0.0, 1.0]], [[0.933196485042572, -0.07844070345163345, 0.3507015109062195, 1.413723111152649], [0.35936683416366577, 0.2036931961774826, -0.9106944799423218, -3.6711270809173584], [0.0, 0.9758873581886292, 0.21827472746372223, 0.8798936009407043], [0.0, 0.0, 0.0, 1.0]], [[-0.5703680515289307, 0.8067229390144348, -0.15452639758586884, -0.6229158639907837], [-0.8213893175125122, -0.5601838231086731, 0.10730224847793579, 0.43254923820495605], [-7.450581485102248e-09, 0.18812799453735352, 0.9821444153785706, 3.959151029586792], [0.0, 0.0, 0.0, 1.0]], [[-0.9822137951850891, -0.1677587777376175, 0.08433839678764343, 0.339978963136673], [0.1877657175064087, -0.8775562047958374, 0.44117921590805054, 1.7784502506256104], [0.0, 0.4491681754589081, 0.89344722032547, 3.6016008853912354], [0.0, 0.0, 0.0, 1.0]], [[-0.33302783966064453, 0.7478148937225342, -0.5743390917778015, -2.315235137939453], [-0.942916989326477, -0.2641199231147766, 0.20285022258758545, 0.8177154660224915], [1.4901161193847656e-08, 0.6091088652610779, 0.7930864691734314, 3.1970341205596924], [0.0, 0.0, 0.0, 1.0]], [[-0.28166845440864563, -0.5504832863807678, 0.7858948707580566, 3.168043851852417], [0.9595118761062622, -0.16159653663635254, 0.2307025045156479, 0.929991602897644], [2.980232594040899e-08, 0.819057047367096, 0.5737119317054749, 2.31270694732666], [0.0, 0.0, 0.0, 1.0]], [[-0.7199268341064453, 0.1475599855184555, -0.6781823635101318, -2.7338404655456543], [-0.6940498948097229, -0.1530616134405136, 0.7034677267074585, 2.8357691764831543], [0.0, 0.9771378040313721, 0.21260714530944824, 0.8570468425750732], [0.0, 0.0, 0.0, 1.0]], [[-0.999970555305481, 0.0056571876630187035, -0.00517683569341898, -0.020868491381406784], [-0.007668337319046259, -0.7377116084098816, 0.675072431564331, 2.721303939819336], [-4.6566125955216364e-10, 0.675092339515686, 0.7377332448959351, 2.973897933959961], [0.0, 0.0, 0.0, 1.0]], [[-0.26151391863822937, 0.9203336238861084, -0.2908549904823303, -1.1724741458892822], [-0.9651997685432434, -0.24935775995254517, 0.07880505919456482, 0.31767338514328003], [0.0, 0.3013418912887573, 0.9535161256790161, 3.8437466621398926], [0.0, 0.0, 0.0, 1.0]], [[0.7933609485626221, -0.19958899915218353, 0.5751022100448608, 2.3183114528656006], [0.6087514758110046, 0.2601161599159241, -0.7495071887969971, -3.021360397338867], [0.0, 0.9447241425514221, 0.3278660774230957, 1.3216705322265625], [0.0, 0.0, 0.0, 1.0]], [[-0.9780508875846863, 0.035429153591394424, -0.2053319364786148, -0.8277195692062378], [-0.20836611092090607, -0.16630110144615173, 0.9638087749481201, 3.885237693786621], [0.0, 0.9854384064674377, 0.17003315687179565, 0.6854256391525269], [0.0, 0.0, 0.0, 1.0]], [[0.9800372123718262, -0.039007220417261124, 0.1949501782655716, 0.7858693599700928], [0.19881436228752136, 0.1922825276851654, -0.9609891176223755, -3.873871326446533], [0.0, 0.9805640578269958, 0.1961991935968399, 0.7909042835235596], [0.0, 0.0, 0.0, 1.0]], [[-0.7107511758804321, 0.5660614967346191, -0.41762080788612366, -1.683483362197876], [-0.7034434676170349, -0.5719420313835144, 0.42195925116539, 1.7009721994400024], [-1.4901160305669237e-08, 0.5936806797981262, 0.8047007322311401, 3.243852376937866], [0.0, 0.0, 0.0, 1.0]], [[-0.9999840259552002, 0.002792684594169259, -0.004916718695312738, -0.019819926470518112], [-0.005654485896229744, -0.4938804805278778, 0.8695114254951477, 3.505112648010254], [-2.3283064365386963e-10, 0.8695253133773804, 0.49388840794563293, 1.990927815437317], [0.0, 0.0, 0.0, 1.0]], [[0.5215556025505066, -0.4440380930900574, 0.7285671234130859, 2.9369475841522217], [0.8532173037528992, 0.2714320719242096, -0.4453592896461487, -1.7953004837036133], [0.0, 0.8539056777954102, 0.520427942276001, 2.097911834716797], [0.0, 0.0, 0.0, 1.0]], [[0.23253430426120758, 0.1524328589439392, -0.9605684876441956, -3.872175693511963], [-0.9725881814956665, 0.03644489124417305, -0.2296605408191681, -0.925791323184967], [0.0, 0.9876416325569153, 0.1567290723323822, 0.6317951679229736], [0.0, 0.0, 0.0, 1.0]], [[0.9624335765838623, -0.0828590914607048, 0.2585652470588684, 1.0423099994659424], [0.271517276763916, 0.29370641708374023, -0.9165232181549072, -3.6946234703063965], [7.450580596923828e-09, 0.9522977471351624, 0.30517053604125977, 1.2301819324493408], [0.0, 0.0, 0.0, 1.0]], [[0.9513999223709106, 0.208729088306427, -0.22642995417118073, -0.9127683639526367], [-0.30795836448669434, 0.6448431015014648, -0.6995278000831604, -2.8198866844177246], [0.0, 0.7352616190910339, 0.6777834296226501, 2.7322323322296143], [0.0, 0.0, 0.0, 1.0]], [[0.327425479888916, -0.32604190707206726, 0.8868421912193298, 3.5749754905700684], [0.9448769688606262, 0.1129823625087738, -0.3073148727416992, -1.238826036453247], [7.450580596923828e-09, 0.9385795593261719, 0.3450627624988556, 1.3909926414489746], [0.0, 0.0, 0.0, 1.0]], [[0.6466457843780518, -0.4112951159477234, 0.6424060463905334, 2.5896217823028564], [0.7627904415130615, 0.3486701548099518, -0.5445914268493652, -2.1953184604644775], [-1.4901161193847656e-08, 0.842179000377655, 0.5391979217529297, 2.173576593399048], [0.0, 0.0, 0.0, 1.0]], [[-0.7232142090797424, -0.2793499827384949, 0.6316049695014954, 2.546081066131592], [0.6906238794326782, -0.29253241419792175, 0.6614102125167847, 2.666229724884033], [-1.4901161193847656e-08, 0.9145427346229553, 0.4044893682003021, 1.6305487155914307], [0.0, 0.0, 0.0, 1.0]], [[-0.9798775315284729, 0.03400515764951706, -0.19668211042881012, -0.7928509712219238], [-0.19960013031959534, -0.16693821549415588, 0.9655523896217346, 3.892266273498535], [3.725290742551124e-09, 0.9853807687759399, 0.17036642134189606, 0.6867690086364746], [0.0, 0.0, 0.0, 1.0]], [[0.9977991580963135, 0.025931671261787415, -0.061029236763715744, -0.246016725897789], [-0.06631003320217133, 0.39020639657974243, -0.9183364510536194, -3.701932668685913], [0.0, 0.920362114906311, 0.39106711745262146, 1.576442003250122], [0.0, 0.0, 0.0, 1.0]], [[-0.30372685194015503, -0.8604696989059448, 0.40907466411590576, 1.649032473564148], [0.9527590870857239, -0.27430620789527893, 0.13040752708911896, 0.5256894826889038], [-7.4505792646561986e-09, 0.4293578565120697, 0.9031345248222351, 3.640651226043701], [0.0, 0.0, 0.0, 1.0]], [[-0.6693150401115417, 0.18098634481430054, -0.7205979228019714, -2.904823064804077], [-0.7429786920547485, -0.16304218769073486, 0.6491532325744629, 2.6168203353881836], [7.450580152834618e-09, 0.9698768854141235, 0.24359561502933502, 0.9819653034210205], [0.0, 0.0, 0.0, 1.0]], [[-0.9993038773536682, -0.0326223149895668, 0.018094748258590698, 0.07294226437807083], [0.037304628640413284, -0.8738756775856018, 0.4847160875797272, 1.9539530277252197], [0.0, 0.48505374789237976, 0.8744843602180481, 3.5251591205596924], [0.0, 0.0, 0.0, 1.0]], [[0.3921033442020416, -0.6728718876838684, 0.6272944808006287, 2.52870512008667], [0.9199212193489075, 0.2868020832538605, -0.2673753499984741, -1.077824592590332], [0.0, 0.6819003224372864, 0.7314450144767761, 2.948549509048462], [0.0, 0.0, 0.0, 1.0]], [[-0.8941853642463684, 0.14062994718551636, -0.4250360429286957, -1.7133750915527344], [-0.4476967751979828, -0.2808804214000702, 0.8489250540733337, 3.422126293182373], [-1.4901160305669237e-08, 0.9493837356567383, 0.31411871314048767, 1.2662529945373535], [0.0, 0.0, 0.0, 1.0]], [[-0.9460079073905945, 0.30761945247650146, -0.10217288136482239, -0.4118720591068268], [-0.32414352893829346, -0.8977826833724976, 0.2981899678707123, 1.2020422220230103], [-7.450580596923828e-09, 0.31520867347717285, 0.9490223526954651, 3.8256313800811768], [0.0, 0.0, 0.0, 1.0]], [[-0.048472680151462555, 0.48035264015197754, -0.8757349252700806, -3.530200481414795], [-0.9988245964050293, -0.023311378434300423, 0.04249916970729828, 0.1713196337223053], [0.0, 0.8767656087875366, 0.48091796040534973, 1.9386422634124756], [0.0, 0.0, 0.0, 1.0]], [[0.9660431146621704, -0.19308900833129883, 0.17168912291526794, 0.692101001739502], [0.25838056206703186, 0.721928596496582, -0.641917884349823, -2.587653875350952], [0.0, 0.6644815802574158, 0.7473046779632568, 3.012481451034546], [0.0, 0.0, 0.0, 1.0]], [[-0.14186327159404755, 0.16584104299545288, -0.9758952260017395, -3.933959484100342], [-0.9898862242698669, -0.023767130449414253, 0.1398581862449646, 0.5637863874435425], [-1.8626450382086546e-09, 0.9858660101890564, 0.1675354689359665, 0.6753571033477783], [0.0, 0.0, 0.0, 1.0]], [[0.7959837317466736, -0.5319463610649109, 0.28886470198631287, 1.1644508838653564], [0.6053178310394287, 0.6995013952255249, -0.3798527419567108, -1.5312353372573853], [1.4901159417490817e-08, 0.4772116243839264, 0.878788411617279, 3.5425093173980713], [0.0, 0.0, 0.0, 1.0]], [[0.44463157653808594, 0.7389402389526367, -0.5062312483787537, -2.0406837463378906], [-0.8957135677337646, 0.3668093979358673, -0.2512928247451782, -1.0129939317703247], [0.0, 0.5651710033416748, 0.8249737024307251, 3.325575590133667], [0.0, 0.0, 0.0, 1.0]], [[-0.9978623390197754, 0.06534235179424286, -0.0011283498024567962, -0.00454852357506752], [-0.06535211205482483, -0.9977134466171265, 0.01722879149019718, 0.0694514811038971], [1.1641533570472262e-10, 0.01726444624364376, 0.9998509287834167, 4.0305280685424805], [0.0, 0.0, 0.0, 1.0]], [[0.9958323836326599, 0.01890719123184681, -0.08922122418880463, -0.359662264585495], [-0.09120257198810577, 0.2064458727836609, -0.9741982221603394, -3.9271185398101807], [1.862645149230957e-09, 0.9782753586769104, 0.20730985701084137, 0.8356927633285522], [0.0, 0.0, 0.0, 1.0]], [[0.08438806980848312, 0.515834391117096, -0.8525218367576599, -3.4366254806518555], [-0.9964329600334167, 0.04368609935045242, -0.0722002163529396, -0.2910483777523041], [0.0, 0.8555737137794495, 0.5176809430122375, 2.086838722229004], [0.0, 0.0, 0.0, 1.0]], [[0.9833629727363586, -0.17399616539478302, 0.0521787591278553, 0.21033930778503418], [0.1816515475511551, 0.941920816898346, -0.2824675142765045, -1.1386629343032837], [7.450580152834618e-09, 0.2872464954853058, 0.9578567147254944, 3.861243963241577], [0.0, 0.0, 0.0, 1.0]], [[0.8475823402404785, 0.40216803550720215, -0.3462150990962982, -1.3956377506256104], [-0.5306636691093445, 0.6423475742340088, -0.5529788732528687, -2.2291290760040283], [0.0, 0.6524189114570618, 0.7578585147857666, 3.055025339126587], [0.0, 0.0, 0.0, 1.0]], [[0.47693684697151184, 0.7483182549476624, -0.4610324800014496, -1.8584816455841064], [-0.8789376616477966, 0.40605902671813965, -0.2501695156097412, -1.0084656476974487], [-1.4901162970204496e-08, 0.5245338678359985, 0.8513894081115723, 3.432060956954956], [0.0, 0.0, 0.0, 1.0]], [[-0.47700220346450806, 0.4823406934738159, -0.7347218990325928, -2.961758852005005], [-0.8789020776748657, -0.2617783844470978, 0.39875200390815735, 1.6074209213256836], [1.4901161193847656e-08, 0.8359542489051819, 0.5487990379333496, 2.212280035018921], [0.0, 0.0, 0.0, 1.0]], [[0.4534713923931122, -0.11268524080514908, 0.8841186165809631, 3.5639960765838623], [0.8912708163261414, 0.057333339005708694, -0.44983240962028503, -1.813332438468933], [0.0, 0.9919751882553101, 0.12643210589885712, 0.5096641182899475], [0.0, 0.0, 0.0, 1.0]], [[-0.5146697163581848, 0.42225345969200134, -0.746201753616333, -3.008035659790039], [-0.8573886156082153, -0.2534685730934143, 0.44792693853378296, 1.805651307106018], [0.0, 0.8703193664550781, 0.4924877882003784, 1.9852819442749023], [0.0, 0.0, 0.0, 1.0]], [[-0.08526185154914856, 0.49009639024734497, -0.8674882650375366, -3.4969570636749268], [-0.9963586330413818, -0.04193924367427826, 0.07423397898674011, 0.29924672842025757], [0.0, 0.8706587553024292, 0.49188753962516785, 1.982862114906311], [0.0, 0.0, 0.0, 1.0]], [[0.9965562224388123, 0.03072895109653473, -0.07701651006937027, -0.31046348810195923], [-0.08292051404714584, 0.3693070113658905, -0.9256006479263306, -3.731215476989746], [0.0, 0.9287990927696228, 0.37058329582214355, 1.4938690662384033], [0.0, 0.0, 0.0, 1.0]], [[0.4406329095363617, -0.730517566204071, 0.5217151641845703, 2.1031012535095215], [0.8976874351501465, 0.35857704281806946, -0.25608566403388977, -1.032314419746399], [-1.4901161193847656e-08, 0.5811769366264343, 0.8137771487236023, 3.2804408073425293], [0.0, 0.0, 0.0, 1.0]]]
fig, axis = plt.subplots(1, 10, figsize=(30,300))
for i in range(10):
    _frame = _frames[i]
    _fname = os.path.join(_datadir, _frame['file_path']+'.png')
    img = imageio.imread(_fname)
    axis[i].imshow(img)

random.shuffle(_frames)
fig, axis = plt.subplots(1, 10, figsize=(30,300))
imgs = []
for i in range(10):
    _frame = _frames[i]
    _fname = os.path.join(_datadir, _frame['file_path']+'.png')
    img = imageio.imread(_fname)
    imgs.append(img)
    axis[i].imshow(img)

imgs = (np.array(imgs) / 255.).astype(np.float32)
print(imgs.shape)

imgs_orig = imgs[...,:3] # rgb값만 저장
imgs_mask = imgs[...,-1] # 불투명도값만 저장
imgs = imgs[...,:3]*imgs[...,-1:] + (1-imgs[...,-1:]) # 흰 배경으로

fig, axis = plt.subplots(1,3)
axis[0].imshow(imgs_orig[0])
axis[1].imshow(imgs_mask[0], cmap='gray')
axis[2].imshow(imgs[0])
(10, 800, 800, 4)





<matplotlib.image.AxesImage at 0x7f79518e50d0>

load_blender_data()

이 함수는 블렌더 데이터셋 경로를 입력 받아 다음 값을 반환합니다.

  • imgs: train, test, val 데이터의 모든 이미지들 [N, H, W, 4(rgb&alpha)]
  • poses: train, test, val 데이터의 모든 카메라 위치(c2w matrix) [N, 4, 4]
  • render_poses: 위에서 정의한 pose_spherical 함수를 통해 우리가 보고 싶은 카메라 위치 [n, 4, 4]
  • [H, W, focal]: 카메라 내부 파라미터를 구하기 위한 이미지 세로 픽셀 수(H), 가로 픽셀 수(W), focal length (focal)
  • i_split: train, test, val 데이터의 인덱스 (testskip에 따라 val, test는 건너 뛰며 sparse하게 샘플링합니다.)
def load_blender_data(basedir, half_res=False, testskip=1):
    splits = ['train', 'val', 'test']

    metas = {} # 메타 정보는 여기에 저장한다.
    for s in splits:
        with open(os.path.join(basedir, 'transforms_{}.json'.format(s)), 'r') as fp:
            metas[s] = json.load(fp)
    # {'train' : 'transforms_train_json', ...}

    all_imgs = [] # train, test, val의 모든 이미지를 넣는 리스트
    all_poses = [] # 카메라 포즈에 대한 모든 행렬을 넣는 리스트
    counts = [0]

    for s in splits: # train, val, test
        meta = metas[s]
        imgs = []
        poses = []
        if s=='train' or testskip==0:
            skip = 1 # train 데이터를 만들 때는 하나도 건너뛰지 않고 모두 저장
        else:
            skip = testskip # train이 아닐 경우 testskip만큼 건너 뛰면서 데이터 샘플링 진행

        for frame in meta['frames'][::skip]:
            fname = os.path.join(basedir, frame['file_path'] + '.png') # 이미지 파일 경로를 리스트에 저장
            imgs.append(imageio.imread(fname)) # 이미지를 리스트에 저장
            poses.append(np.array(frame['transform_matrix'])) # cam to world 매트릭스를 리스트에 저장

        imgs = (np.array(imgs) / 255.).astype(np.float32) # keep all 4 channels (RGBA)
        poses = np.array(poses).astype(np.float32)
        counts.append(counts[-1] + imgs.shape[0]) # start index, end of train index, end of val index, end of test index
        # counts = [0, 200, 210, 220]

        # for문 이전에 선언한 모든 데이터셋을 담아둘 리스트에 이미지와 c2w 매트릭스 저장
        all_imgs.append(imgs)
        all_poses.append(poses)
    
    i_split = [np.arange(counts[i], counts[i+1]) for i in range(3)] # index for each split(train, val, test)
    
    imgs = np.concatenate(all_imgs, 0)
    poses = np.concatenate(all_poses, 0)
    
    H, W = imgs[0].shape[:2] # 이미지의 세로, 가로축 길이
    camera_angle_x = float(meta['camera_angle_x'])
    focal = .5 * W / np.tan(.5 * camera_angle_x)
    # focal lenth = (1/2 * width) / (tan(1/2 * theta)) (theta = camera_angle_x)
    
    render_poses = torch.stack([pose_spherical(angle, -30.0, 4.0) for angle in np.linspace(-180,180,40+1)[:-1]], 0)
    # rendering 할 pose 미리 정의
    # pose_spherical를 통해 cam to world 매트릭스를 만든다.
    # 첫 번째 인자 angle은 X축 좌우 회전 각도 (-180부터 180까지)
    # 두 번째 인자는 Y축 기준 30도 회전 (pose_sherical은 X, Y의 회전 각도가 반대!)

    # 이미지를 1/2 스케일로 줄이는 것 (학습 속도를 높이기 위함)
    if half_res:
        H = H//2
        W = W//2
        # important - change focal
        focal = focal/2. # 이미지가 작아지면 focal도 당연히 1/2가 된다!

        imgs_half_res = np.zeros((imgs.shape[0], H, W, 4))
        for i, img in enumerate(imgs):
            imgs_half_res[i] = cv2.resize(img, (W, H), interpolation=cv2.INTER_AREA)
        imgs = imgs_half_res

        
    return imgs, poses, render_poses, [H, W, focal], i_split

블렌더 데이터셋의 경우 이미지의 높이, 너비, 초점 거리는 아래와 같습니다.
(주의해야 할 점은, args.half_res를 True로 세팅하여 두었기 때문에 세 값이 모두 절반이 되었다는 것입니다.)

  • H: 400
  • W: 400
  • focal: 555.555
images, poses, render_poses, hwf, i_split = load_blender_data(args.datadir, args.half_res, args.testskip)
# Cast intrinsics to right types
H, W, focal = hwf
H, W = int(H), int(W)
hwf = [H, W, focal]
print(hwf)
[400, 400, 555.5555155968841]
print(i_split) # 이중 어레이
print(i_split[0][0]) # train 이미지 첫 인덱스 (100장)
print(i_split[1][0]) # val 이미지 첫 인덱스 (13장)
print(i_split[2][0]) # test 이미지 첫 인덱스 (20장)
[array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16,
       17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
       34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
       51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67,
       68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
       85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]), array([100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112]), array([113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125,
       126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137])]
0
100
113
print(len(images)) # train, val, test 전체 이미지 개수
print(images[0].shape)
print(len(poses)) # train, val, test 전체 c2w 개수
print(poses[0])
138
(400, 400, 4)
138
[[-9.9990219e-01  4.1922452e-03 -1.3345719e-02 -5.3798322e-02]
 [-1.3988681e-02 -2.9965907e-01  9.5394367e-01  3.8454704e+00]
 [-4.6566129e-10  9.5403719e-01  2.9968831e-01  1.2080823e+00]
 [ 0.0000000e+00  0.0000000e+00  0.0000000e+00  1.0000000e+00]]

render_poses

위에서 정의한 phose_sherical() 함수를 이용하여 우리가 보고싶은 각도의 c2w 매트릭스를 만들어 저장합니다.
이 때, 보편적인 카메라 기하학의 회전 행렬과 달리, NeRF에서는 X축, Y축 기준 회전 각도음의 값으로 반전시켰다는 것을 기억합시다.
이렇게 코드를 변형한 이유는 1장에 자세히 나와있습니다.

print(len(render_poses))
print(render_poses[0])
40
tensor([[ 1.0000e+00,  6.1232e-17, -1.0606e-16, -4.2423e-16],
        [-1.2246e-16,  5.0000e-01, -8.6603e-01, -3.4641e+00],
        [ 0.0000e+00,  8.6603e-01,  5.0000e-01,  2.0000e+00],
        [ 0.0000e+00,  0.0000e+00,  0.0000e+00,  1.0000e+00]])
print(H)
print(W)
print(focal)
print(hwf)
400
400
555.5555155968841
[400, 400, 555.5555155968841]

카메라 내부 파라미터

H, W, focal을 통해 구한 카메라 내부 파라미터 K를 다음과 같이 정의합니다.

K=[fx0cx0fycy001]K = \begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix}

이 때, 블렌더 데이터셋의 이미지는 H=WH = W이므로,

  • fx=fyf_x = f_y
  • cx=cyc_x = c_y

공식이 기억나지 않는다면 2장을 복습해주세요.

K = np.array([
    [focal, 0, 0.5*W],
    [0, focal, 0.5*H],
    [0, 0, 1]
])
print(K)
[[555.5555156   0.        200.       ]
 [  0.        555.5555156 200.       ]
 [  0.          0.          1.       ]]

카메라 위치 시각화

단순히 c2w 행렬을 출력해보는 것만으로는 시각적으로 이해하기 힘들기 때문에, 시각화 라이브러리를 이용하여 이미지가 촬영된 카메라의 위치를 가시화 해보겠습니다.

import numpy as np
import matplotlib.pyplot as plt
import pytransform3d.camera as pc
import pytransform3d.transformations as pt
from pytransform3d.plot_utils import plot_vector

def plot_cam(figsize, poses, elev, azim, axis_lim, plot_label):
    # 3D 플롯 생성
    fig = plt.figure(figsize=(figsize, figsize))
    ax = fig.add_subplot(111, projection='3d')

    # 월드 좌표계 그리기
    pt.plot_transform(ax=ax, A2B=np.eye(4), s=0.3, name="World")

    # 보기 편하도록 뷰 설정
    ax.view_init(elev=elev, azim=azim)

    # 카메라 위치 저장용 배열 (최대/최소 좌표 계산)
    all_camera_positions = []

    for i, c2w in enumerate(poses):
        try:
            c2w = np.array(c2w) # 텐서를 넘파이로 변환
            # 카메라 좌표계 플롯
            if plot_label:
                ax = pt.plot_transform(A2B=c2w, ax=ax, s=0.3, name=f"Step {i}")  # s 값 조정
                # 카메라 프러스텀 플롯 (가상 이미지 거리 키움)
                pc.plot_camera(
                    ax=ax,
                    cam2world=c2w,
                    M=K,
                    virtual_image_distance=0.1,
                    label=f"Step {i}",
                    color="red",
                    alpha=0.5
                )

            else:
                ax = pt.plot_transform(A2B=c2w, ax=ax, s=0.3)
                pc.plot_camera(
                    ax=ax,
                    cam2world=c2w,
                    M=K,
                    virtual_image_distance=0.1,
                    color="red",
                    alpha=0.5
                )
        except:
            continue

    ax.set_xlim([-axis_lim, axis_lim])
    ax.set_ylim([-axis_lim, axis_lim])
    ax.set_zlim([-axis_lim, axis_lim])

    ax.set_box_aspect((1, 1, 1))  # 축 비율 균일화
    # ax.auto_scale_xyz(all_camera_positions[:, 0], all_camera_positions[:, 1], all_camera_positions[:, 2])  # 자동 스케일 조정

    plt.show()

poses 가시화

train, test, val의 모든 c2w 매트릭스가 담긴 poses를 가시화 해보겠습니다.
카메라가 Z>0Z > 0인 공간에서 반구 모양으로 분포하며, 원점을 바라보며 촬영된 것을 알 수 있습니다.

plot_cam(9, poses, 10, 45, 3, False)

test 데이터셋 가시화

이제 test 데이터셋의 카메라 앵글과, 그에 대응되는 사진을 하나씩 비교해보겠습니다.
카메라가 오른손 법칙 기준 Z축의 양의 방향으로 회전함에 따라, 그에 알맞은 레고 장난감의 모습이 촬영된 것을 볼 수 있습니다.

# 테스트 데이터셋에서 카메라의 위치와 보이는 면 비교하기
# 데이터가 많을 수 있으니 10장만 가시화 해보겠습니다.
for i in range(i_split[2][0], i_split[2][9]):
    plt.imshow(images[i])
    plot_cam(5.28, [poses[i]], 90, 0, 3, False)








render_poses 가시화

pose_spherical() 함수로 생성한 카메라의 위치가 어떤지 가시화 해보겠습니다.
카메라의 순차적인 회전 및 평행이동은 1장에서 깊게 다루었으니 더이상 설명하지 않겠습니다.

plot_cam(9, render_poses, 10, 45, 3, True)

4. 광선(Ray) 생성하기

지금까지 카메라의 위치를 살펴보았으니, 이제 여기서 생성된 '광선'에 대해 설명하겠습니다.

1) 광선(Ray)의 정의

(1) 광선이란?

  • 광선(Ray): 하나의 픽셀에서 발사되는 일직선 형태의 빛
  • 3D 공간에서 시작점방향벡터의 두 가지 요소로 정의할 수 있음

2) NeRF가 광선을 계산하는 방법

(1) 이미지 플레인(Image Plane) 설정

  1. 이미지 플레인은 카메라의 초점(focal point)에서 초점 거리ff만큼 떨어진 위치에 존재

    • 그림의 예:Z=fZ = -f인 지점에 이미지 플레인이 위치
  2. 이미지 플레인의 픽셀 개수는 가로WW개, 세로HH

  3. 원점(0, 0) 은 이미지 플레인의 왼쪽 위 모서리에 위치

실제 카메라의 LCD 모니터를 떠올리면 이해하기 쉬움.

(2) 이미지 플레인을 노멀 플레인(Normalized Plane)으로 매핑

  1. 이미지 플레인의 원점이 왼쪽 위 모서리에 위치하는 것과 달리,
    노멀 플레인의 원점은 이미지 플레인의 중심(초점과 수직으로 정렬되는 지점)에 위치

  2. 일반적인 카메라 이론에서는 노멀 플레인이 초점으로부터 1 단위 거리 앞에 위치 (z=1z = 1)

  3. 그러나 NeRF에서는 노멀 플레인을 초점보다 1 단위 거리 뒤 (z=1z = -1) 에 배치

    • 따라서 그림상의 이미지 플레인 좌표(x,y)(x, y)(u,v)(u, v)가 아닌(u,v)(u^*, v^*)로 매핑
    • 또한 노멀 플레인이 초점 뒤에 있으므로, 상의 반전을 구현하기 위해 yy좌표 또한 음수로 반전
    • 이 두가지 예외적인 방법은 코드에 잘 구현돼있음
  4. 노멀 플레인을 Z=1Z = -1에 위치시키는 이유:

    • 카메라의 초점 좌표가(0,0,0)(0,0,0)이므로 직관적으로 이 점을 광선의 시작점으로 해석할 수 있음
    • 초점(0,0,0)(0,0,0)과 노멀 플레인 상의 좌표(u,v,1)(u^*, v^*, -1)을 잇는 벡터를 알아내면, 이것을 광선의 방향 벡터로 해석할 수 있음
    • 만약 노멀 플레인을 Z=1Z = 1에 위치시킬 경우, 광선의 방향벡터를 반전시켜 줘야 함

정리하자면, 초점 (0,0,0)(0,0,0)과 노멀 플레인의 좌표 (u,v,1)(u^*, v^*, -1)를 잇는 벡터를 기준으로 광선을 정의하기 위해 노멀 플레인을z=1z=-1에 위치시킴

(3) 월드 좌표로 변환 (c2w 매트릭스 적용)

  1. 위 과정을 통해 얻은 광선의 시작점광선의 방향 벡터월드 좌표계로 이동하기 위해, c2w(Camera-to-World) 변환 행렬을 활용

    • c2wc2w행렬은 회전 행렬RR평행이동 벡터tt 로 구성된 4×4 변환 행렬
    • 수식으로 표현하면:
      c2w=[R3×3t3×101×31]c2w = \begin{bmatrix} R_{3×3} & t_{3×1} \\ \mathbf{0}_{1×3} & 1 \end{bmatrix}
    • 여기서:
      • RR3×3R \in \mathbb{R}^{3×3}→ 3×3 회전 행렬 (카메라의 방향 결정)
      • tR3×1t \in \mathbb{R}^{3×1}→ 3×1 평행이동 벡터 (카메라의 위치)
      • 마지막 행[0001]\begin{bmatrix} 0 & 0 & 0 & 1 \end{bmatrix}동차 좌표(homogeneous coordinates) 를 유지하기 위한 항목
  2. 노말 플레인 좌표를 월드 좌푤로 변환하는 과정

    Xw=[Rt01]Xc\mathbf{X}_w = \begin{bmatrix} R & t \\ 0 & 1 \end{bmatrix} \mathbf{X}_c
    • 여기서
      • Xc\mathbf{X}_c: 노말 플레인의 좌표
      • Xw\mathbf{X}_w: 월드 좌표
  3. 광선의 변환 과정

    • 회전 변환 (Rotation):
      • RR은 방향을 정의하는 행렬이므로, 이 행렬을 광선의 방향벡터와 곱하여 광선의 최종 방향 rays_d 를 구함
    • 평행이동 (Translation):
      • tt는 평행이동을 정의하는 벡터이므로, 이 값을 광선의 시작점과 더하여 광선의 최종 시작점 rays_o 를 구함
        초기 광선의 시작점은 원점이기 때문에 이를 더한 값은 결국 tt와 동일
    • 벡터의 합:
      • rays_orays_d 를 더하면c2wc2w행렬에 의해 변환된 노멀 플레인의 각 좌표값을 얻을 수 있음

이제부터 광선 = 시작점 +tt× 방향벡터 로 정의된다는 점을 염두에 두고, 각 좌표 변환 과정을 코드 단위로 확인하고 직접 가시화해보겠습니다.

간단한 토이 프로젝트

아래 코드는 4 x 6의 아주 작은 이미지 플레인을 만들고, linspace 함수를 이용하여 가상의 픽셀에 좌표를 부여하는 예제입니다.

H_ = 4
W_ = 6

# torch.linspace(0,W-1,W): 0부터 W까지 W-1개의 1씩 증가하는 1차원 텐서 생성 [0, 1, 2, ... W-1]
i = torch.linspace(0, W_-1, W_)
j = torch.linspace(0, H_-1, H_)
i, j = torch.meshgrid(i, j)
print(i) # (W, H)
print(j) # (W, H)

i = i.t()
j = j.t()

print(i) # (H, W)
print(j) # (H, W)

# 텐서 i, j를 마지막 차원에 새 축으로 쌓음.
# 즉, 각 픽셀 위치마다 (i, j) 두 좌표를 [i, j] 형태로 묶어, shape가 (H, W, 2)인 텐서가 됩니다.
# 예: grid[y, x] = [i_value, j_value].
grid = torch.stack([i, j], dim = -1)
print(grid[0]) # 0번째 row의 좌표들
print(grid[0][0]) # 0번째 row, 0번째 column의 좌표
print(grid[3][4]) # 3번째 row, 4번째 column의 좌표 (x, y) = (j, i)!
tensor([[0., 0., 0., 0.],
        [1., 1., 1., 1.],
        [2., 2., 2., 2.],
        [3., 3., 3., 3.],
        [4., 4., 4., 4.],
        [5., 5., 5., 5.]])
tensor([[0., 1., 2., 3.],
        [0., 1., 2., 3.],
        [0., 1., 2., 3.],
        [0., 1., 2., 3.],
        [0., 1., 2., 3.],
        [0., 1., 2., 3.]])
tensor([[0., 1., 2., 3., 4., 5.],
        [0., 1., 2., 3., 4., 5.],
        [0., 1., 2., 3., 4., 5.],
        [0., 1., 2., 3., 4., 5.]])
tensor([[0., 0., 0., 0., 0., 0.],
        [1., 1., 1., 1., 1., 1.],
        [2., 2., 2., 2., 2., 2.],
        [3., 3., 3., 3., 3., 3.]])
tensor([[0., 0.],
        [1., 0.],
        [2., 0.],
        [3., 0.],
        [4., 0.],
        [5., 0.]])
tensor([0., 0.])
tensor([4., 3.])

visualize_image_and_normal_plane()

간단한 코드로 좌표를 부여하는 방법을 알았으니, 실제 데이터를 바탕으로 이미지 플레인과 노멀 플레인의 관계를 가시화하는 코드를 살펴봅시다.
이 함수는 H, W, K를 입력받아 다음 값들을 연산합니다.

  • 이미지 플레인의 ZZ좌표는 f-f
  • 노멀 플레인의 ZZ좌표는 1-1
  • 이미지 플레인의 원점 (0,0)(0, 0)은 평면의 꼭지점에 위치
  • 노멀 플레인의 원점 (0,0)(0, 0)은 평면의 중앙에 위치
  • 이미지 플레인과 노멀 플레인의 yy좌표계는 반전
def visualize_image_and_normal_plane(H, W, K, axis_lim):
    """
    Image Plane과 Normal Plane을 각각의 3D 그래프에서 시각화하는 함수.
    """
    # --- 이미지 플레인의 좌표 지정 ---
    i, j = torch.meshgrid(
        torch.linspace(0, W-1, W),
        torch.linspace(0, H-1, H)
    )
    i = i.t()
    j = j.t()

    # --- Image Plane 계산 ---
    img_plane_x = i
    img_plane_y = j
    img_plane_z = torch.full_like(i, K[0][0])  # focal length x로 가정
    img_plane = torch.stack([img_plane_x, img_plane_y, img_plane_z], dim=-1)  # (H,W,3)
    img_plane_flat = img_plane.view(-1, 3).cpu().numpy()

    # --- Normal Plane 계산 ---
    norm_plane_x = (img_plane_x-K[0][2])/K[0][0]
    norm_plane_y = (img_plane_y-K[1][2])/K[1][1]
    norm_plane_z = torch.ones_like(img_plane_x)
    dirs = torch.stack([norm_plane_x, -norm_plane_y, -norm_plane_z], -1)  # (H, W, 3)
    dirs_flat = dirs.view(-1, 3).cpu().numpy()

    # --- Image plane 모서리 좌표 가져오기 ---
    img_corner_points = torch.stack([
        img_plane[0, 0],        # 좌상단 (Top-Left)
        img_plane[0, W-1],      # 우상단 (Top-Right)
        img_plane[H-1, 0],      # 좌하단 (Bottom-Left)
        img_plane[H-1, W-1]     # 우하단 (Bottom-Right)
    ], dim=0).cpu().numpy()

    # --- Normal plane 모서리 좌표 가져오기 ---
    corner_points = torch.stack([
        dirs[0, 0],        # 좌상단 (Top-Left)
        dirs[0, W-1],      # 우상단 (Top-Right)
        dirs[H-1, 0],      # 좌하단 (Bottom-Left)
        dirs[H-1, W-1]     # 우하단 (Bottom-Right)
    ], dim=0).cpu().numpy()

    # --- 두 개의 3D 플롯 생성 ---
    fig, axes = plt.subplots(1, 2, figsize=(14, 7), subplot_kw={'projection': '3d'})

    # --- (1) Image Plane 시각화 ---
    ax1 = axes[0]
    ax1.set_title("Image Plane")
    # pt.plot_transform(ax=ax1, A2B=np.eye(4), s=0.3, name="CameraCoord")

    ax1.scatter(img_plane_flat[:, 0], img_plane_flat[:, 1], img_plane_flat[:, 2],
                c='green', s=5, alpha=0.5, label="Image plane")
    
    ax1.scatter(img_corner_points[:, 0], img_corner_points[:, 1], img_corner_points[:, 2],
                c='red', s=50, marker='o', label="Corners")

    # 모서리 좌표 텍스트 추가
    for i, (x, y, z) in enumerate(img_corner_points):
        ax1.text(x, y, z, f"({x:.2f}, {y:.2f}, {z:.2f})",
                 color="red", fontsize=9, ha="center")

    ax1.view_init(elev=45, azim=-60)
    ax1.legend()
    ax1.set_xlim([0, W])
    ax1.set_ylim([0, H])
    ax1.set_zlim([0, K[0][0] + 5])
    ax1.set_box_aspect((1, 1, 1))

    # --- (2) Normal Plane 시각화 ---
    ax2 = axes[1]
    ax2.set_title("Normal Plane")
    # pt.plot_transform(ax=ax2, A2B=np.eye(4), s=0.3, name="CameraCoord")

    ax2.scatter(dirs_flat[:, 0], dirs_flat[:, 1], dirs_flat[:, 2],
                c='blue', s=5, alpha=0.6, label="Normal plane")

    ax2.scatter(corner_points[:, 0], corner_points[:, 1], corner_points[:, 2],
                c='red', s=50, marker='o', label="Corners")

    # 모서리 좌표 텍스트 추가
    for i, (x, y, z) in enumerate(corner_points):
        ax2.text(x, y, z, f"({x:.2f}, {y:.2f}, {z:.2f})",
                 color="red", fontsize=9, ha="center")

    ax2.view_init(elev=45, azim=-60)
    ax2.legend()
    ax2.set_xlim([-axis_lim, axis_lim])
    ax2.set_ylim([-axis_lim, axis_lim])
    ax2.set_zlim([-2, 0])
    ax2.set_box_aspect((1, 1, 1))

    plt.show()
# 실행 예시
visualize_image_and_normal_plane(H, W, K, 0.4)

visualize_normal_plane()

아래 함수는 카메라 좌표계에서 광선을 정의합니다.

  • 광선의 시작점은 초점 (0,0,0)(0, 0, 0)
  • 광선의 방향 벡터는 노멀 플레인의 좌표 (u,v,1)(u^*, v^*, -1)
  • 이 두 점으로 광선 dirs를 정의할 수 있음
def visulaize_normal_plane(H, W, K, axis_lim):
    # 이미지 플레인의 좌표 지정
    i, j = torch.meshgrid(
        torch.linspace(0, W-1, W),
        torch.linspace(0, H-1, H)
    )
    i = i.t()
    j = j.t()

    img_plane_x = i
    img_plane_y = j
    img_plane_z = torch.full_like(i, K[0][0]) # z는 focal length x로 가정 (focal length x와 focal length y는 동일한 값이므로)
    img_plane = torch.stack([img_plane_x, img_plane_y, img_plane_z], dim=-1)  # (H,W,3)
    img_plane_flat = img_plane.view(-1, 3).cpu().numpy()

    norm_plane_x = (img_plane_x-K[0][2])/K[0][0]
    norm_plane_y = (img_plane_y-K[1][2])/K[1][1]
    norm_plane_z = torch.ones_like(img_plane_x)
    dirs = torch.stack([norm_plane_x, -norm_plane_y, -norm_plane_z], -1) # (H, W, 3)
    dirs_flat = dirs.view(-1, 3).cpu().numpy()

    # --- Normal plane 모서리 좌표 가져오기 ---
    corner_points = torch.stack([
        dirs[0, 0],        # 좌상단 (Top-Left)
        dirs[0, W-1],      # 우상단 (Top-Right)
        dirs[H-1, 0],      # 좌하단 (Bottom-Left)
        dirs[H-1, W-1]     # 우하단 (Bottom-Right)
    ], dim=0).cpu().numpy()

    corner_labels = ["Top-Left", "Top-Right", "Bottom-Left", "Bottom-Right"]

    # --- 시각화 시작 ---
    fig = plt.figure(figsize=(8, 8))
    ax = fig.add_subplot(111, projection='3d')
    pt.plot_transform(ax=ax, A2B=np.eye(4), s=0.1, name="CameraCoord")

    # --- Normal plane 산점도 ---
    ax.scatter(dirs_flat[:, 0], dirs_flat[:, 1], dirs_flat[:, 2],
               c='blue', s=5, alpha=0.6, label="Normal plane (z=-1)")

    # --- 모서리 좌표 추가 (빨간색 점) ---
    ax.scatter(corner_points[:, 0], corner_points[:, 1], corner_points[:, 2],
               c='red', s=50, marker='o', label="Corners")

    # --- 원점에서 모서리로 향하는 벡터 추가 ---
    for i, (x, y, z) in enumerate(corner_points):
        ax.quiver(0, 0, 0, x, y, z, color='green', arrow_length_ratio=0.1, label="Corner Vectors" if i == 0 else "")

    # --- 모서리 좌표 라벨 추가 ---
    for i, (x, y, z) in enumerate(corner_points):
        ax.text(x, y, z, f"{corner_labels[i]}\n({x:.2f}, {y:.2f}, {z:.2f})", 
                color="black", fontsize=10, ha="center")

    # --- 시각화 설정 ---
    ax.view_init(elev=30, azim=-120)
    ax.legend()
    ax.set_xlim([-axis_lim, axis_lim])
    ax.set_ylim([-axis_lim, axis_lim])
    ax.set_zlim([-2, 0])

    ax.set_box_aspect((1, 1, 1))  # 축 비율 균일화
    plt.show()
visulaize_normal_plane(H, W, K, 0.5)

get_rays(), get_rays_np()

위에서 구한 카메라 좌표계의 광선과 c2w 매트릭스를 이용하여 월드 좌표계의 광선을 구합니다.

def get_rays(H, W, K, c2w):
    # 이미지 플레인의 좌표 지정
    i, j = torch.meshgrid(
        torch.linspace(0, W-1, W),
        torch.linspace(0, H-1, H)
    )
    i = i.t()
    j = j.t()

    img_plane_x = i
    img_plane_y = j
    img_plane_z = torch.full_like(i, K[0][0]) # z는 focal length x로 가정 (focal length x와 focal length y는 동일한 값이므로)
    img_plane = torch.stack([img_plane_x, img_plane_y, img_plane_z], dim=-1)  # (H,W,3)
    img_plane_flat = img_plane.view(-1, 3).cpu().numpy()

    norm_plane_x = (img_plane_x-K[0][2])/K[0][0]
    norm_plane_y = (img_plane_y-K[1][2])/K[1][1]
    norm_plane_z = torch.ones_like(img_plane_x)
    dirs = torch.stack([norm_plane_x, -norm_plane_y, -norm_plane_z], -1) # (H, W, 3)
    rays_d = torch.sum(dirs[..., np.newaxis, :] * c2w[:3,:3], -1) # # c2w @ dir  c2w.dot(dir) dot product, equals to: [c2w.dot(dir) for dir in dirs]
    rays_o = c2w[:3, -1].expand(rays_d.shape)

    return rays_o, rays_d
def get_rays_np(H, W, K, c2w):
    i, j = np.meshgrid(np.arange(W, dtype=np.float32), np.arange(H, dtype=np.float32), indexing='xy')
    dirs = np.stack([(i-K[0][2])/K[0][0], -(j-K[1][2])/K[1][1], -np.ones_like(i)], -1)
    # Rotate ray directions from camera frame to the world frame
    rays_d = np.sum(dirs[..., np.newaxis, :] * c2w[:3,:3], -1)  # dot product, equals to: [c2w.dot(dir) for dir in dirs]
    # dirs[..., np.newaxis, :]
    # 원본 dirs가 (H,W,3).
    # [..., np.newaxis, :] → shape (H,W,1,3).

    # c2w[:3,:3]
    # 카메라->월드 행렬에서 3×3 회전부분(R).
    # shape (3,3).

    # 원소별 곱
    # (H,W,1,3) × (3,3)에서 브로드캐스팅으로 “각 픽셀 방향벡터” × “회전행렬 R”을 수행.

    # np.sum(..., -1)
    # 마지막 축(-1=3)에 대해 합 => “dot product”와 동일한 결과.
    # 결과 shape: (H,W,3).
    # 즉, 카메라 좌표에서의 방향을 월드 좌표로 회전시킨 방향 벡터.

    # Translate camera frame's origin to the world frame. It is the origin of all rays.
    rays_o = np.broadcast_to(c2w[:3,-1], np.shape(rays_d))
    # c2w[:3, -1] = t, 즉 카메라 월드 좌표계에서의 원점(평행이동). shape = (3,).
    # np.shape(rays_d) = (H,W,3).
    # np.broadcast_to(..., (H,W,3))
    # => 카메라의 원점(3D) 값을 (H,W,3)로 복사(모든 픽셀 동일 origin).
    # 따라서 rays_o[y,x,:] 전부 t
    # 최종 shape: (H,W,3).

    return rays_o, rays_d
pose = poses[0] # (4, 4)
print(pose[:3, :3]) # c2w의 회전행렬 R (3, 3)
print(pose[:3, -1]) # c2w의 평행이동 t (3, )
print(pose[-1]) # 동차 좌표계에서 4x4 꼴을 맞추기 위해 붙여둔 [ 0 0 0 1 ]

pose = pose[:3, :4] # 의미 없는 [ 0 0 0 1 ]는 제외하고 get_rays_np() 함수의 입력값으로 사용
print(pose)
[[-9.9990219e-01  4.1922452e-03 -1.3345719e-02]
 [-1.3988681e-02 -2.9965907e-01  9.5394367e-01]
 [-4.6566129e-10  9.5403719e-01  2.9968831e-01]]
[-0.05379832  3.8454704   1.2080823 ]
[0. 0. 0. 1.]
[[-9.9990219e-01  4.1922452e-03 -1.3345719e-02 -5.3798322e-02]
 [-1.3988681e-02 -2.9965907e-01  9.5394367e-01  3.8454704e+00]
 [-4.6566129e-10  9.5403719e-01  2.9968831e-01  1.2080823e+00]]
rays_o, rays_d = get_rays_np(H, W, K, pose)
print(rays_o[0, 0]) # [i, j]의 모든 값이 c2w 행렬에 의해 원점이 이동한 좌표로 동일
print(rays_d[0, 0]) # [i, j]좌표에 대응하는 노말 평면 상의 좌표가 c2w 행렬에 의해 이동한 좌표
[-0.05379832  3.8454704   1.2080823 ]
[ 0.37481973 -1.056785    0.0437651 ]

visualize_rays_3d()

카메라 좌표계의 노멀 플레인이 월드 좌표계에서 어떻게 이동하는지 가시화하는 함수입니다.

  • rays_d: c2w의 회전행렬 RR과 광선의 방향벡터 dirs의 곱으로 정의
  • rays_o: c2w의 평행이동 tt와 광선의 원점 (0,0,0)(0, 0, 0)의 합으로 정의
  • rays_d + rays_o: c2w 행렬에 의해 이동된 노멀 플레인의 좌표
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection

def get_rays_from_dirs_np(H, W, K, c2w):
    i, j = np.meshgrid(np.arange(W, dtype=np.float32),
                       np.arange(H, dtype=np.float32),
                       indexing='xy')
    dirs = np.stack([
        (i - K[0, 2]) / K[0, 0],
        -(j - K[1, 2]) / K[1, 1],
        -np.ones_like(i)
    ], axis=-1)  # (H, W, 3)

    rays_d = np.sum(dirs[..., None, :] * c2w[:3, :3], axis=-1)  # (H, W, 3)
    rays_o = np.broadcast_to(c2w[:3, -1], rays_d.shape)  # (H, W, 3)

    return dirs, rays_o, rays_d

def visualize_rays_3d(H, W, K, c2w, axis_lim=2, elev=30, azim=45):
    dirs, rays_o, rays_d = get_rays_from_dirs_np(H, W, K, c2w)

    corner_indices = [(0, 0), (0, W-1), (H-1, W-1), (H-1, 0)]  # 네 벡터를 시계 방향으로 저장
    cam_origin_camera_coord = np.broadcast_to(np.array([[0, 0, 0]]), (4, 3))
    dirs_corners = np.array([dirs[i, j] for i, j in corner_indices])
    rays_o_corners = np.array([rays_o[i, j] for i, j in corner_indices])
    rays_d_corners = np.array([rays_d[i, j] for i, j in corner_indices])

    fig = plt.figure(figsize=(8, 8))
    ax = fig.add_subplot(111, projection='3d')

    world_origin = np.array([0, 0, 0])
    ax.scatter(*world_origin, c='blue', s=40, label="World Origin")

    for ro, rd in zip(cam_origin_camera_coord, dirs_corners):
        ax.quiver(*ro, *rd, color='blue', linewidth=1, arrow_length_ratio=0.1)
        endpoint = ro + rd
        ax.text(*endpoint, f'({endpoint[0]:.2f}, {endpoint[1]:.2f}, {endpoint[2]:.2f})',
                color='blue', fontsize=10, ha='right')
        
    cam_origin = c2w[:3, 3]
    ax.scatter(*cam_origin, c='red', s=40, label="Camera Origin")
        
    for ro, rd in zip(rays_o_corners, rays_d_corners):
        ax.quiver(*ro, *rd, color='red', linewidth=1, arrow_length_ratio=0.1)
        endpoint = ro + rd
        ax.text(*endpoint, f'({endpoint[0]:.2f}, {endpoint[1]:.2f}, {endpoint[2]:.2f})',
                color='red', fontsize=10, ha='right')
    
    normal_plane_camera = dirs_corners
    normal_plane_world = cam_origin + rays_d_corners

    ax.add_collection3d(Poly3DCollection([normal_plane_camera], color='blue', alpha=0.3))
    ax.add_collection3d(Poly3DCollection([normal_plane_world], color='red', alpha=0.3))

    ax.set_xlim([-axis_lim, axis_lim])
    ax.set_ylim([-axis_lim, axis_lim])
    ax.set_zlim([-axis_lim, axis_lim])
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    ax.set_box_aspect((1, 1, 1))
    ax.legend()
    plt.title("Visualizing Rays and Planes in 3D (World coords)")
    ax.view_init(elev=elev, azim=azim)
    plt.show()

if __name__ == "__main__":
    H, W = 400, 400
    K = np.array([
        [555.5555, 0., 200.],
        [0., 555.5555, 200.],
        [0., 0., 1.]
    ], dtype=np.float32)

    c2w = np.array([
        [-0.99990219, 0.00419225, -0.01334572, -0.05379832],
        [-0.01398868, -0.29965907, 0.95394367, 0.84547042],
        [-0.0, 0.95403719, 0.29968831, 1.20808232],
        [0., 0., 0., 1.]
    ], dtype=np.float32)

    visualize_rays_3d(H, W, K, c2w, axis_lim=1.2, elev=10, azim=30)

개념 정리해보기

지금까지 공부한 일련의 과정은 위 그림과 같이, 크게 두 단계로 나뉩니다.
1. 카메라 내부 매트릭스(K)를 이용하여 이미지 플레인의 좌표를 노멀 플레인의 좌표로 대응하고, 이로부터 광선 dirs을 정의
2. 카메라 외부 매트릭스(c2w)를 이용하여 dirs를 월드 좌표계로 이동하고, 최종적인 광선의 시작점 rays_o와 방향벡터 rays_d를 정의

5. 임베딩 전처리

아래의 내용은 계산된 광선을 NeRF 네트워크에 임베딩하기 위해 가공하는 과정입니다.
여러 정보를 각기 다른 차원으로 합치거나 나누는 과정이므로 간단히 이해하고 넘어가면 되겠습니다.
다음 장에서는 실제로 NeRF에 데이터를 넣어 학습하는 것까지 구현해보겠습니다.

K = None

if args.dataset_type == 'blender':# render_poses은 테스트용 포즈, hwf는 카메라 intrinsic을 만들기 위한 것, i_split은 trian, val, test에 대한 인덱스.
    images, poses, render_poses, hwf, i_split = load_blender_data(args.datadir, args.half_res, args.testskip)
    print('Loaded Blender Dataset')
    print('images.shape:', images.shape)
    print('render_poses.shape:', render_poses.shape)
    print('hwf:', hwf)
    print('datadir:', args.datadir)

    i_train, i_val, i_test = i_split

    # near, far bound는 2, 6으로 지정.
    near = 2.
    far = 6.

    if args.white_bkgd:
        print('white_bkgd: True')
        images = images[...,:3]*images[...,-1:] + (1. - images[...,-1:]) # 흰 백그라운드로
    else:
        print('white_bkgd: False')
        images = images[...,:3]
    # 이 때 중요한 것은, images의 마지막 차원인 불투명도는 제외하고 가져온다는 것이다.

else:
    print('Unknown dataset type', args.dataset_type, 'exiting')
Loaded Blender Dataset
images.shape: (138, 400, 400, 4)
render_poses.shape: torch.Size([40, 4, 4])
hwf: [400, 400, 555.5555155968841]
datadir: ./data/nerf_synthetic/lego
white_bkgd: True
# Cast intrinsics to right types
H, W, focal = hwf
H, W = int(H), int(W)
hwf = [H, W, focal]

# 카메라 내부 파라미터 설정
if K is None:
    K = np.array([
        [focal, 0, 0.5*W],
        [0, focal, 0.5*H],
        [0, 0, 1]
    ])

print('hwf: ', hwf)
print('K: ', K)

if args.render_test: # 모델을 테스트할 것이라면
    render_poses = np.array(poses[i_test]) # render_poses는 우리가 만들어둔 것이 아니라, test 데이터셋에 있는 c2w 매트릭스들로 덮어씌운다.
hwf:  [400, 400, 555.5555155968841]
K:  [[555.5555156   0.        200.       ]
 [  0.        555.5555156 200.       ]
 [  0.          0.          1.       ]]
N_rand = args.N_rand
use_batching = not args.no_batching

if use_batching:
    rays = np.stack([get_rays_np(H, W, K, p) for p in poses[:,:3,:4]], 0)
    # poses[:,:3,:4]: 3행의 [0 0 0 1]을 절삭한 3x4 행렬. 의미가 없는 데이터이기 때문에 잘라서 함수에 넣어준다.
    # return 값인 rays_o, rays_d를 []로 감싸 [2, 400, 400, 3]차원으로 만든 뒤
    # np.stack을 이용하여 0번째 차원을 추가해 데이터셋 개수만큼 쌓는다.
    # 따라서 rays의 차원은 다음과 같다.
    # [N(trian, test, val을 합친 데이터 개수), 2(광선의 시작점 ro, 광선의 방향 rd), 픽셀들의 행 개수(H), 셀들의 열 개수(W), 좌표(3)] = [138, 2, 400, 400, 3]

    print(rays.shape) # [N, 2, H, W, 3]
    print(images.shape) # [N, H, W, 3], white_bkgd이 True이건 False건 맨 마지막 차원은 절삭한 채 가져온다.
    print(images[:, None].shape) # 1번 차원을 하나 추가

    rays_rgb = np.concatenate([rays, images[:,None]], 1) # 1번 차원에 concat [N, 3(ro & rd & rgb), H, W, 3]
    print(rays_rgb.shape)

    rays_rgb = np.transpose(rays_rgb, [0,2,3,1,4]) # 순서 바꾸기 [N, H, W, 3(ro & rd & rgb), 3]
    print(rays_rgb.shape)

    rays_rgb = np.stack([rays_rgb[i] for i in i_train], 0) # train images only (N')
    print(rays_rgb.shape)

    rays_rgb = np.reshape(rays_rgb, [-1,3,3]) # [N'*H*W, 3(ro & rd & rgb), 3], N': 훈련 이미지 개수
    print(rays_rgb.shape)

    rays_rgb = rays_rgb.astype(np.float32)
    # float32로 변환하면, 신경망이나 GPU에서 계산할 때 메모리 사용량을 줄이고 연산 속도를 높일 수 있습니다.
    # 대부분의 딥러닝 라이브러리(TensorFlow, PyTorch 등)에서 기본적으로 float32를 사용하므로, 데이터 타입을 변환하는 것이 일반적입니다.

    np.random.shuffle(rays_rgb)
    # np.random.shuffle()은 배열의 첫 번째 차원(N'*H*W) 기준으로 데이터를 랜덤하게 섞습니다.

    i_batch = 0

    images = torch.Tensor(images).to(device)
    rays_rgb = torch.Tensor(rays_rgb).to(device)
    
poses = torch.Tensor(poses).to(device)
profile
상어 인형을 좋아하는 사람

0개의 댓글