이전 글에서 3D공간에 점을 찍어 직접 시각화까지 해보았었다.
[3DGS 구현] 3. COLMAP의 출력 바이너리 파일의 구조파악, 파싱
COLMAP의 출력 파일의 종류COLMAP을 돌리면 그 결과가 바이너리 파일로 나온다.총 3개의 파일이 생성된다. 1. cameras.bin2. images.bin3. points3D.bin 이 파일들을 얻는 법은 이전 글을 참고하면 된다. [3DGS 구
april2901.tistory.com
이제는 이 점을 중심으로 그려질 가우시안들의 scale을 정하기 위해 필요한 코드를 알아보자.
인접한 3개의 점까지의 거리 평균을 scale로 사용한다.
import numpy as np
from scipy.spatial import KDTree
from points3d_bin_parser import Point3DLoader
path = "./360_v2/bicycle/sparse/0/points3D.bin"
loader = Point3DLoader(path)
points = loader.load()
def compute_initial_scaling(points_dict):
xyz = np.array([p['xyz'] for p in points_dict.values()])
print(f"총 {len(xyz)}개의 점으로 KNN 계산.")
tree = KDTree(xyz)
#근처 3개 점 거리 평균
dists, _ = tree.query(xyz, k=4)
avg_dists = np.mean(dists[:, 1:], axis=1)
avg_dists = np.maximum(avg_dists, 1e-7)
print("계산끝")
return avg_dists
initial_scales = compute_initial_scaling(points)
compute_initial_scaling함수의 인자인 points_dict는 딕셔너리 타입으로
{point_id: {'xyz': [...], 'rgb': [...], ...}}
이런 형태를 가지고 있다.
따라서 이 중 'xyz'에 대응되는 값만 모아 xyz라는 numpy array자료형으로 바꾼다.
이 xyz는 (점 개수, 3)의 차원을 가진다.
이를 KDTree의 인자로 넣으면 이 정보를 이진 트리 구조로 바꿔주어 검색을 빠르게 도와준다.
dists에는 각 점으로부터 가장 가까운 이웃 4개까지의 거리가 저장된다. (점 개수, 4)의 차원을 가진다.
tree.query에서는 두번째 반환값으로 각 점의 index도 알려주는데 필요 없으므로 '_'로 받았다.
이 때 본인을 포함시키기 때문에 3개에 1을 더해 4를 넣어주어야 한다.
따라서 실제로 주변 3개의 거리를 가져오려면 dists[:,1:]을 사용한다.
이 점들을 대상으로 평균(mean)을 계산해서 avg_dists라는 변수에 저장한다.
차원은 (점 개수, 1)의 1차원 벡터이다.
이 수치는 내부적으로 로그를 씌워서 저장한다.
그리고 이 값에 대해 조정을 해서 최적화를 한다.
실제 계산 시에는 이 값을 다시 $e^x$ 함수에 넣어 계산하는데, 이렇게 해야 가우시안의 scale이 양수로 보장되기 때문이다.
그런데 그냥 0을 로그에 넣으면 음의 무한대가 되어 문제가 발생하기 때문에 1e-7처럼 작은 양수 이하로 떨어지지 않게 처리를 한 번 해준다.
이제 정리를 해보자.

여기서 temp.py는 open3d를 사용한 임시 시각화 코드일 뿐이므로 무시하고 다른 파일에 대해 정리해보자.
cameras_bin_parser.py는 이미지들을 찍은 서로 다른 카메라들의 정보를 추출한다.
우리가 쓸 bicycle이미지 셋에서는 한 종류의 카메라만 사용되었다.
images_bin_parser.py는 각 이미지셋의 각 이미지 별로, 월드 좌표계에서 해당 카메라 좌표계로 점들을 전환시켜줄 때 필요한 q,t벡터를 추출한다. 추가로 각 이미지를 찍은 카메라의 모델도 추출한다.(bicycle의 경우 모두 같은 카메라 모델)
points3d_bin_parser.py는 3D공간에 살아남은 점들의 위치 좌표와 RGB값을 추출한다.
knn.py는 이 점들을 씨앗으로 그려질 가우시안들의 초기 scale을 구한다.
가우시안들을 학습시켜 조정하고 나면 원본이미지와 얼마나 유사한지를 확인해야할 것이다.
이때 해당 3D모델을 원본 이미지와 같은 카메라 위치/각도에서 바라본 정보와 원본을 비교해야하므로
각 이미지별 카메라의 정보/위치/각도 등의 정보가 필요하다.
따라서 이 정보들을 추려 json파일로 만드는 코드를 만들자.
import json
import os
import numpy as np
from cameras_bin_parser import CameraLoader
from images_bin_parser import ImageLoader
def save_camera_json(cameras, images, output_path="./360_v2/bicycle/sparse/0/cameras.json"):
json_data = []
print(f"camera.json 실행")
for img_id in images:
img = images[img_id] # {'name': '....JPG', 'qvec': ( , , , ), 'tvec': ( , , ), 'camera_id': 1}
cam = cameras[img["camera_id"]] # cameras[1] = {'model_id': 1, 'width': , 'height': , 'params': (, , , )}
# 카메라 파라미터
cam_dict = {
"id": img["camera_id"],
"img_name": img["name"],
"width": cam["width"],
"height": cam["height"],
"fx": float(cam["params"][0]),
"fy": float(cam["params"][0]),
"cx": float(cam["params"][1]),
"cy": float(cam["params"][2]),
"rotation": list(img["qvec"]),
"position": list(img["tvec"])
}
json_data.append(cam_dict)
# JSON 파일로 저장
with open(output_path, 'w') as f:
json.dump(json_data, f, indent=4)
print(f"{len(json_data)}개의 카메라 정보가 {output_path}에 저장되었습니다.")
cameras = CameraLoader("./360_v2/bicycle/sparse/0/cameras.bin").load()
images = ImageLoader("./360_v2/bicycle/sparse/0/images.bin").load()
save_camera_json(cameras, images)
먼저 이전에 만들어 놨던 CameraLoader, ImageLoader를 사용해서 정보를 받아온다.
for img_id in images:
이렇게 사용하면 img_id에는 images의 key인 정수 숫자 하나씩이 들어가게된다.
따라서 이의 value를 사용하기 위해 images[img_id]를 통해 각 이미지를 찍은 카메라의 이름/위치/각도/카메라 id를 가져온다.
각 데이터의 타입 및 내용은 위 코드의 주석부분에 써놓았으니 참고하면 이해가 쉬울 것이다.
cam에는 카메라의 속성 정보가 들어간다.
이를 사용해 cam_dict라는 딕셔너리를 만들고, json파일에 추가해준다.
최종적으로 위 코드로 생성된 json파일의 내용은 아래와 같다.

'프로젝트, 연구 > 3DGS 구현' 카테고리의 다른 글
| [3DGS 구현] 6. 이미지 전처리 및 GPU로딩 (0) | 2026.03.24 |
|---|---|
| [3DGS 구현] 5. gaussian 초기화 및 SIBR뷰어를 통한 시각화 (0) | 2026.03.23 |
| [3DGS 구현] 3. COLMAP의 출력 바이너리 파일의 구조파악, 파싱 (0) | 2026.03.13 |
| [3DGS 구현] 2. Kaggle에서 데이터셋 다운받기 (5) | 2026.03.12 |
| [3DGS 구현] 1. 환경 세팅 (3) | 2026.03.04 |