mirror of
https://github.com/graphdeco-inria/gaussian-splatting
synced 2025-06-26 18:18:11 +00:00
862 lines
36 KiB
Python
862 lines
36 KiB
Python
#
|
|
# Copyright (C) 2023, Inria
|
|
# GRAPHDECO research group, https://team.inria.fr/graphdeco
|
|
# All rights reserved.
|
|
#
|
|
# This software is free for non-commercial, research and evaluation use
|
|
# under the terms of the LICENSE.md file.
|
|
#
|
|
# For inquiries contact george.drettakis@inria.fr
|
|
#
|
|
|
|
import os
|
|
import torch
|
|
from random import randint
|
|
from utils.loss_utils import l1_loss, ssim
|
|
from gaussian_renderer import render, network_gui
|
|
import sys
|
|
from scene import Scene, GaussianModel
|
|
from utils.general_utils import safe_state
|
|
import uuid
|
|
from tqdm import tqdm
|
|
from utils.image_utils import psnr
|
|
from argparse import ArgumentParser, Namespace
|
|
from arguments import ModelParams, PipelineParams, OptimizationParams
|
|
|
|
import matplotlib.pyplot as plt
|
|
from mpl_toolkits.mplot3d import Axes3D
|
|
# import open3d as o3d
|
|
import numpy as np
|
|
import cv2
|
|
from collections import Counter
|
|
import pandas as pd
|
|
import ffmpeg
|
|
|
|
|
|
def save_ply(xyz_tensor, output_file_path):
|
|
"""
|
|
xyz_tensor: torch.Tensor of shape [N, 3]
|
|
output_file_path: string, path to save the PLY file
|
|
"""
|
|
# 출력 디렉토리가 존재하지 않으면 생성합니다.
|
|
output_dir = os.path.dirname(output_file_path)
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# 텐서를 CPU로 이동하고 NumPy 배열로 변환합니다.
|
|
xyz_np = xyz_tensor.detach().cpu().numpy()
|
|
|
|
# PLY 파일의 헤더를 작성합니다.
|
|
num_vertices = xyz_np.shape[0]
|
|
ply_header = f'''ply
|
|
format ascii 1.0
|
|
element vertex {num_vertices}
|
|
property float x
|
|
property float y
|
|
property float z
|
|
end_header
|
|
'''
|
|
|
|
# PLY 파일에 데이터를 저장합니다.
|
|
with open(output_file_path, 'w') as f:
|
|
f.write(ply_header)
|
|
np.savetxt(f, xyz_np, fmt='%f %f %f')
|
|
|
|
print(f"PLY 파일이 {output_file_path} 경로에 저장되었습니다.")
|
|
|
|
def pad_image_to_even_dimensions(image):
|
|
# image는 (height, width, channels)의 NumPy 배열입니다.
|
|
height, width = image.shape[:2]
|
|
new_height = height if height % 2 == 0 else height + 1
|
|
new_width = width if width % 2 == 0 else width + 1
|
|
|
|
# 새로운 크기의 배열을 생성하고, 패딩된 영역은 0으로 채웁니다.
|
|
padded_image = np.zeros((new_height, new_width, *image.shape[2:]), dtype=image.dtype)
|
|
padded_image[:height, :width, ...] = image
|
|
|
|
return padded_image
|
|
|
|
def compression_h265(projected_data, output_directory):
|
|
# 출력 디렉토리가 존재하지 않으면 생성합니다.
|
|
os.makedirs(output_directory, exist_ok=True)
|
|
|
|
# 압축할 텐서 목록
|
|
tensors_to_process = {
|
|
'feature_dc_tensor': projected_data.get('feature_dc_tensor'),
|
|
'scaling_tensor': projected_data.get('scaling_tensor'),
|
|
'opacity_tensor': projected_data.get('opacity_tensor'),
|
|
'rotation_tensor': projected_data.get('rotation_tensor'),
|
|
}
|
|
|
|
# 지정된 텐서들을 처리합니다.
|
|
for tensor_name, tensor in tensors_to_process.items():
|
|
if tensor is None:
|
|
print(f"{tensor_name}가 projected_data에 없습니다.")
|
|
continue
|
|
|
|
# 텐서를 CPU로 이동하고 NumPy 배열로 변환합니다.
|
|
tensor_np = tensor.detach().cpu().numpy()
|
|
|
|
# 데이터 타입이 uint8인지 확인하고 변환합니다.
|
|
if tensor_np.dtype != np.uint8:
|
|
tensor_np = tensor_np.astype(np.uint8)
|
|
|
|
# 텐서의 차원을 확인합니다.
|
|
if tensor_np.ndim != 4:
|
|
print(f"{tensor_name} 텐서의 차원이 4가 아닙니다. 건너뜁니다.")
|
|
continue
|
|
|
|
num_frames, height, width, channels = tensor_np.shape
|
|
print(f"{tensor_name} 크기: {tensor_np.shape}")
|
|
|
|
# 채널 수에 따른 픽셀 포맷 설정
|
|
if channels == 1:
|
|
pix_fmt = 'gray'
|
|
tensor_np = tensor_np.squeeze(-1) # 채널 차원 제거
|
|
elif channels == 3:
|
|
pix_fmt = 'rgb24'
|
|
elif channels == 4:
|
|
pix_fmt = 'rgba'
|
|
else:
|
|
print(f"{tensor_name}의 채널 수 {channels}는 지원되지 않습니다. 건너뜁니다.")
|
|
continue
|
|
|
|
# 모든 프레임에 대해 패딩을 적용합니다.
|
|
padded_frames = []
|
|
for frame in tensor_np:
|
|
padded_frame = pad_image_to_even_dimensions(frame)
|
|
padded_frames.append(padded_frame)
|
|
|
|
# 패딩된 프레임의 크기를 가져옵니다.
|
|
padded_height, padded_width = padded_frames[0].shape[:2]
|
|
print(f"{tensor_name} 패딩 후 크기: {(padded_height, padded_width)}")
|
|
|
|
# 출력 파일 경로 설정
|
|
output_video_path = os.path.join(output_directory, f"{tensor_name}.mp4")
|
|
|
|
# ffmpeg 프로세스 설정
|
|
process = (
|
|
ffmpeg
|
|
.input('pipe:', format='rawvideo', pix_fmt=pix_fmt, s=f'{padded_width}x{padded_height}')
|
|
.output(output_video_path, vcodec='libx265', pix_fmt='yuv420p', crf=23)
|
|
.overwrite_output()
|
|
.run_async(pipe_stdin=True)
|
|
)
|
|
|
|
# 패딩된 프레임 데이터를 ffmpeg 프로세스에 전달
|
|
for frame in padded_frames:
|
|
process.stdin.write(
|
|
frame.tobytes()
|
|
)
|
|
|
|
# 프로세스 종료
|
|
process.stdin.close()
|
|
process.wait()
|
|
|
|
print(f"{tensor_name}가 {output_video_path}에 저장되었습니다.")
|
|
|
|
# feature_rest_tensors 처리 (15개의 텐서)
|
|
feature_rest_tensors = projected_data.get('feature_rest_tensors')
|
|
if feature_rest_tensors is None:
|
|
print("feature_rest_tensors가 projected_data에 없습니다.")
|
|
else:
|
|
for idx, tensor in enumerate(feature_rest_tensors):
|
|
tensor_name = f'feature_rest_tensor_{idx}'
|
|
|
|
# 텐서를 CPU로 이동하고 NumPy 배열로 변환합니다.
|
|
tensor_np = tensor.detach().cpu().numpy()
|
|
|
|
# 데이터 타입이 uint8인지 확인하고 변환합니다.
|
|
if tensor_np.dtype != np.uint8:
|
|
tensor_np = tensor_np.astype(np.uint8)
|
|
|
|
# 텐서의 차원을 확인합니다.
|
|
if tensor_np.ndim != 4:
|
|
print(f"{tensor_name} 텐서의 차원이 4가 아닙니다. 건너뜁니다.")
|
|
continue
|
|
|
|
num_frames, height, width, channels = tensor_np.shape
|
|
print(f"{tensor_name} 크기: {tensor_np.shape}")
|
|
|
|
# 채널 수에 따른 픽셀 포맷 설정
|
|
if channels == 1:
|
|
pix_fmt = 'gray'
|
|
tensor_np = tensor_np.squeeze(-1) # 채널 차원 제거
|
|
elif channels == 3:
|
|
pix_fmt = 'rgb24'
|
|
elif channels == 4:
|
|
pix_fmt = 'rgba'
|
|
else:
|
|
print(f"{tensor_name}의 채널 수 {channels}는 지원되지 않습니다. 건너뜁니다.")
|
|
continue
|
|
|
|
# 모든 프레임에 대해 패딩을 적용합니다.
|
|
padded_frames = []
|
|
for frame in tensor_np:
|
|
padded_frame = pad_image_to_even_dimensions(frame)
|
|
padded_frames.append(padded_frame)
|
|
|
|
# 패딩된 프레임의 크기를 가져옵니다.
|
|
padded_height, padded_width = padded_frames[0].shape[:2]
|
|
print(f"{tensor_name} 패딩 후 크기: {(padded_height, padded_width)}")
|
|
|
|
# 출력 파일 경로 설정
|
|
output_video_path = os.path.join(output_directory, f"{tensor_name}.mp4")
|
|
|
|
# ffmpeg 프로세스 설정
|
|
process = (
|
|
ffmpeg
|
|
.input('pipe:', format='rawvideo', pix_fmt=pix_fmt, s=f'{padded_width}x{padded_height}')
|
|
.output(output_video_path, vcodec='libx265', pix_fmt='yuv420p', crf=23)
|
|
.overwrite_output()
|
|
.run_async(pipe_stdin=True)
|
|
)
|
|
|
|
# 패딩된 프레임 데이터를 ffmpeg 프로세스에 전달
|
|
for frame in padded_frames:
|
|
process.stdin.write(
|
|
frame.tobytes()
|
|
)
|
|
|
|
# 프로세스 종료
|
|
process.stdin.close()
|
|
process.wait()
|
|
|
|
print(f"{tensor_name}가 {output_video_path}에 저장되었습니다.")
|
|
|
|
def compression_ffv1(projected_data, output_directory):
|
|
# 출력 디렉토리가 존재하지 않으면 생성합니다.
|
|
os.makedirs(output_directory, exist_ok=True)
|
|
|
|
# 압축할 텐서 목록
|
|
tensors_to_process = {
|
|
'feature_dc_tensor': projected_data.get('feature_dc_tensor'),
|
|
'scaling_tensor': projected_data.get('scaling_tensor'),
|
|
'xyz_tensor': projected_data.get('xyz_tensor'),
|
|
'opacity_tensor': projected_data.get('opacity_tensor'),
|
|
'rotation_tensor': projected_data.get('rotation_tensor'),
|
|
}
|
|
|
|
# 지정된 텐서들을 처리합니다.
|
|
for tensor_name, tensor in tensors_to_process.items():
|
|
# 텐서를 CPU로 이동하고 NumPy 배열로 변환합니다.
|
|
tensor_np = tensor.detach().cpu().numpy()
|
|
|
|
# 데이터 타입이 uint16인지 확인하고 변환합니다.
|
|
if tensor_np.dtype != np.uint16:
|
|
tensor_np = tensor_np.astype(np.uint16)
|
|
|
|
# 텐서의 차원을 확인합니다.
|
|
if tensor_np.ndim != 4:
|
|
print(f"{tensor_name} 텐서의 차원이 4가 아닙니다. 건너뜁니다.")
|
|
continue
|
|
|
|
num_frames, height, width, channels = tensor_np.shape
|
|
print(f"{tensor_name} 크기: {tensor_np.shape}")
|
|
|
|
# 채널 수에 따른 픽셀 포맷 설정
|
|
if channels == 1:
|
|
pix_fmt = 'gray16le'
|
|
tensor_np = tensor_np.squeeze(-1) # 채널 차원 제거
|
|
elif channels == 3:
|
|
pix_fmt = 'rgb48le'
|
|
elif channels == 4:
|
|
pix_fmt = 'rgba64le'
|
|
else:
|
|
print(f"{tensor_name}의 채널 수 {channels}는 지원되지 않습니다. 건너뜁니다.")
|
|
continue
|
|
|
|
# 출력 파일 경로 설정
|
|
output_video_path = os.path.join(output_directory, f"{tensor_name}.mkv")
|
|
|
|
# ffmpeg 프로세스 설정
|
|
process = (
|
|
ffmpeg
|
|
.input('pipe:', format='rawvideo', pix_fmt=pix_fmt, s=f'{width}x{height}')
|
|
.output(output_video_path, format='matroska', vcodec='ffv1', pix_fmt=pix_fmt)
|
|
.overwrite_output()
|
|
.run_async(pipe_stdin=True)
|
|
)
|
|
|
|
# 프레임 데이터를 ffmpeg 프로세스에 전달
|
|
for frame in tensor_np:
|
|
process.stdin.write(
|
|
frame.tobytes()
|
|
)
|
|
|
|
# 프로세스 종료
|
|
process.stdin.close()
|
|
process.wait()
|
|
|
|
print(f"{tensor_name}가 {output_video_path}에 저장되었습니다.")
|
|
|
|
# feature_rest_tensors 처리 (15개의 텐서)
|
|
feature_rest_tensors = projected_data.get('feature_rest_tensors')
|
|
if feature_rest_tensors is None:
|
|
print("feature_rest_tensors가 projected_data에 없습니다.")
|
|
else:
|
|
for idx, tensor in enumerate(feature_rest_tensors):
|
|
print(tensor.shape)
|
|
tensor_name = f'feature_rest_tensor_{idx}'
|
|
|
|
# 텐서를 CPU로 이동하고 NumPy 배열로 변환합니다.
|
|
tensor_np = tensor.detach().cpu().numpy()
|
|
|
|
# 데이터 타입이 uint16인지 확인하고 변환합니다.
|
|
if tensor_np.dtype != np.uint16:
|
|
tensor_np = tensor_np.astype(np.uint16)
|
|
|
|
# 텐서의 차원을 확인합니다.
|
|
if tensor_np.ndim != 4:
|
|
print(f"{tensor_name} 텐서의 차원이 4가 아닙니다. 건너뜁니다.")
|
|
continue
|
|
|
|
num_frames, height, width, channels = tensor_np.shape
|
|
print(f"{tensor_name} 크기: {tensor_np.shape}")
|
|
|
|
# 채널 수에 따른 픽셀 포맷 설정
|
|
if channels == 1:
|
|
pix_fmt = 'gray16le'
|
|
tensor_np = tensor_np.squeeze(-1) # 채널 차원 제거
|
|
elif channels == 3:
|
|
pix_fmt = 'rgb48le'
|
|
elif channels == 4:
|
|
pix_fmt = 'rgba64le'
|
|
else:
|
|
print(f"{tensor_name}의 채널 수 {channels}는 지원되지 않습니다. 건너뜁니다.")
|
|
continue
|
|
|
|
# 출력 파일 경로 설정
|
|
output_video_path = os.path.join(output_directory, f"{tensor_name}.mkv")
|
|
|
|
# ffmpeg 프로세스 설정
|
|
process = (
|
|
ffmpeg
|
|
.input('pipe:', format='rawvideo', pix_fmt=pix_fmt, s=f'{width}x{height}')
|
|
.output(output_video_path, format='matroska', vcodec='ffv1', pix_fmt=pix_fmt)
|
|
.overwrite_output()
|
|
.run_async(pipe_stdin=True)
|
|
)
|
|
|
|
# 프레임 데이터를 ffmpeg 프로세스에 전달
|
|
for frame in tensor_np:
|
|
process.stdin.write(
|
|
frame.tobytes()
|
|
)
|
|
|
|
# 프로세스 종료
|
|
process.stdin.close()
|
|
process.wait()
|
|
|
|
print(f"{tensor_name}가 {output_video_path}에 저장되었습니다.")
|
|
|
|
|
|
def quantization(tensor, tensor_min=None, tensor_max=None):
|
|
# 텐서를 CPU로 이동하고 NumPy 배열로 변환합니다.
|
|
tensor_np = tensor.detach().cpu().numpy()
|
|
|
|
# 최소값과 최대값을 계산합니다.
|
|
if tensor_min is None:
|
|
tensor_min = tensor_np.min()
|
|
if tensor_max is None:
|
|
tensor_max = tensor_np.max()
|
|
|
|
# 데이터 범위를 계산합니다.
|
|
data_range = tensor_max - tensor_min
|
|
if data_range == 0:
|
|
data_range = 1e-6 # 0으로 나누는 것을 방지
|
|
|
|
# [0, 65535] 범위로 스케일링하여 uint16으로 변환합니다.
|
|
# quantized_tensor = ((tensor_np - tensor_min) / data_range * 65535).astype(np.uint16)
|
|
quantized_tensor = ((tensor_np - tensor_min) / data_range * 255).astype(np.uint8)
|
|
|
|
return quantized_tensor, tensor_min, tensor_max
|
|
|
|
def dequantization(quantized_tensor, tensor_min, tensor_max, device):
|
|
# 데이터 범위를 계산합니다.
|
|
data_range = tensor_max - tensor_min
|
|
if data_range == 0:
|
|
data_range = 1e-6 # 0으로 나누는 것을 방지
|
|
|
|
# uint16 데이터를 float32로 복원합니다.
|
|
# tensor_np = quantized_tensor.astype(np.float32) / 65535 * data_range + tensor_min
|
|
tensor_np = quantized_tensor.astype(np.float32) / 255 * data_range + tensor_min
|
|
|
|
# 텐서를 생성합니다.
|
|
tensor = torch.from_numpy(tensor_np)
|
|
|
|
return tensor.to(device)
|
|
|
|
def slice(data_values, num_voxels_x, num_voxels_z, linear_indices, num_pixels, device):
|
|
# 데이터 채널 수 확인
|
|
num_channels = data_values.shape[1]
|
|
|
|
# 이미지 합계 및 카운트 배열 생성
|
|
image_sums = torch.zeros((num_pixels, num_channels), dtype=torch.float32, device=device)
|
|
counts = torch.zeros((num_pixels), dtype=torch.float32, device=device)
|
|
|
|
# 각 픽셀 위치에 데이터 값을 누적합니다.
|
|
image_sums.index_add_(0, linear_indices, data_values)
|
|
counts.index_add_(0, linear_indices, torch.ones_like(linear_indices, dtype=torch.float32))
|
|
|
|
# 평균 데이터 값을 계산합니다.
|
|
counts_mask = counts > 0
|
|
image_means = torch.zeros_like(image_sums)
|
|
image_means[counts_mask] = image_sums[counts_mask] / counts[counts_mask].unsqueeze(1)
|
|
|
|
# 이미지를 (num_voxels_z, num_voxels_x, 채널 수) 형태로 변환합니다.
|
|
image_means = image_means.view(num_voxels_z, num_voxels_x, num_channels)
|
|
|
|
return image_means
|
|
|
|
def projection(gaussians, opt, pipe, testing_iterations, saving_iterations, checkpoint_iterations, debug_from, fix_xyz, quantize):
|
|
# xyz 좌표와 feature_dc, scaling 값을 가져옵니다.
|
|
# print("===================")
|
|
xyz = gaussians._xyz # [N, 3]
|
|
feature_dc = gaussians._features_dc.squeeze(1) # [N, 3]
|
|
feature_rest = gaussians._features_rest # [N, 15, 3]
|
|
scaling = gaussians._scaling # [N, 3]
|
|
rotation = gaussians._rotation # [N, 4]
|
|
opacity = gaussians._opacity # [N, 1]
|
|
|
|
# 각 데이터의 최소값과 최대값을 계산합니다.
|
|
feature_dc_min = feature_dc.min(dim=0).values
|
|
feature_dc_max = feature_dc.max(dim=0).values
|
|
|
|
scaling_min = scaling.min(dim=0).values
|
|
scaling_max = scaling.max(dim=0).values
|
|
|
|
opacity_min = opacity.min()
|
|
opacity_max = opacity.max()
|
|
|
|
rotation_min = rotation.min(dim=0).values
|
|
rotation_max = rotation.max(dim=0).values
|
|
|
|
min_vals = xyz.min(dim=0).values
|
|
max_vals = xyz.max(dim=0).values
|
|
x_min, y_min, z_min = min_vals
|
|
x_max, y_max, z_max = max_vals
|
|
|
|
# 복셀 크기를 설정합니다.
|
|
voxel_size_x = 0.0035
|
|
voxel_size_y = 0.0035
|
|
voxel_size_z = 0.0035
|
|
|
|
# 각 축에 대한 복셀 수를 계산합니다.
|
|
num_voxels_x = torch.ceil((x_max - x_min) / voxel_size_x).long()
|
|
num_voxels_y = torch.ceil((y_max - y_min) / voxel_size_y).long()
|
|
num_voxels_z = torch.ceil((z_max - z_min) / voxel_size_z).long()
|
|
|
|
print("===============================")
|
|
print(f"Number of voxels (x, y, z): {num_voxels_x}, {num_voxels_y}, {num_voxels_z}")
|
|
|
|
# 각 점의 복셀 인덱스를 계산합니다.
|
|
voxel_indices_x = torch.floor((xyz[:, 0] - x_min) / voxel_size_x).long()
|
|
voxel_indices_y = torch.floor((xyz[:, 1] - y_min) / voxel_size_y).long()
|
|
voxel_indices_z = torch.floor((xyz[:, 2] - z_min) / voxel_size_z).long()
|
|
|
|
# 인덱스가 범위를 벗어나지 않도록 클램핑합니다.
|
|
voxel_indices_x = voxel_indices_x.clamp(0, num_voxels_x - 1)
|
|
voxel_indices_y = voxel_indices_y.clamp(0, num_voxels_y - 1)
|
|
voxel_indices_z = voxel_indices_z.clamp(0, num_voxels_z - 1)
|
|
|
|
|
|
# with open("./voxel_indices.txt", "w") as f:
|
|
# for x, y, z in zip(voxel_indices_x, voxel_indices_y, voxel_indices_z):
|
|
# f.write(f"{x.item()}, {y.item()}, {z.item()}\n")
|
|
# 복셀 인덱스를 결합하여 [N, 3] 형태로 만듭니다.
|
|
voxel_indices = torch.stack((voxel_indices_x, voxel_indices_y, voxel_indices_z), dim=1)
|
|
|
|
# 각 복셀에 포함된 포인트 수를 계산합니다.
|
|
unique_voxels, counts = torch.unique(voxel_indices, dim=0, return_counts=True)
|
|
total_voxels = num_voxels_x * num_voxels_y * num_voxels_z
|
|
empty_voxels = total_voxels.item() - unique_voxels.size(0)
|
|
|
|
|
|
save = True
|
|
|
|
if save == True:
|
|
device = xyz.device
|
|
|
|
# 각 데이터를 저장할 리스트를 초기화합니다.
|
|
feature_dc_images = []
|
|
scaling_images = []
|
|
xyz_images = []
|
|
opacity_images = []
|
|
rotation_images = []
|
|
feature_rest_images = [[] for _ in range(15)] # 15개의 SH 계수에 대한 리스트
|
|
|
|
# y축 기준으로 슬라이싱
|
|
for y in range(num_voxels_y):
|
|
slice_mask = (voxel_indices[:, 1] == y) # y축 기준으로 슬라이싱
|
|
|
|
# 슬라이스된 점들의 인덱스와 값을 가져옵니다.
|
|
x_indices = voxel_indices_x[slice_mask] # [M]
|
|
z_indices = voxel_indices_z[slice_mask] # [M]
|
|
linear_indices = z_indices * num_voxels_x + x_indices # [M]
|
|
num_pixels = num_voxels_z * num_voxels_x
|
|
|
|
# feature_dc 처리
|
|
feature_dc_values = feature_dc[slice_mask] # [M, 3]
|
|
image_means_dc = slice(
|
|
data_values=feature_dc_values,
|
|
num_voxels_x=num_voxels_x,
|
|
num_voxels_z=num_voxels_z,
|
|
linear_indices=linear_indices,
|
|
num_pixels=num_pixels,
|
|
device=device
|
|
)
|
|
feature_dc_images.append(image_means_dc.unsqueeze(0)) # [1, Z, X, 3]
|
|
|
|
# scaling 처리
|
|
scaling_values = scaling[slice_mask] # [M, 3]
|
|
image_means_scaling = slice(
|
|
data_values=scaling_values,
|
|
num_voxels_x=num_voxels_x,
|
|
num_voxels_z=num_voxels_z,
|
|
linear_indices=linear_indices,
|
|
num_pixels=num_pixels,
|
|
device=device
|
|
)
|
|
scaling_images.append(image_means_scaling.unsqueeze(0))
|
|
|
|
# xyz 처리 (x와 z 좌표만 사용)
|
|
if fix_xyz == False:
|
|
xyz_values = xyz[slice_mask][:, [0, 2]] # [M, 2]
|
|
image_means_xyz = slice(
|
|
data_values=xyz_values,
|
|
num_voxels_x=num_voxels_x,
|
|
num_voxels_z=num_voxels_z,
|
|
linear_indices=linear_indices,
|
|
num_pixels=num_pixels,
|
|
device=device
|
|
)
|
|
xyz_images.append(image_means_xyz.unsqueeze(0))
|
|
|
|
# opacity 처리
|
|
opacity_values = opacity[slice_mask] # [M, 1]
|
|
image_means_opacity = slice(
|
|
data_values=opacity_values,
|
|
num_voxels_x=num_voxels_x,
|
|
num_voxels_z=num_voxels_z,
|
|
linear_indices=linear_indices,
|
|
num_pixels=num_pixels,
|
|
device=device
|
|
)
|
|
opacity_images.append(image_means_opacity.unsqueeze(0))
|
|
|
|
# rotation 처리
|
|
rotation_values = rotation[slice_mask] if slice_mask.sum() > 0 else torch.zeros((0, 4), dtype=rotation.dtype, device=device)
|
|
image_means_rotation = slice(
|
|
data_values=rotation_values,
|
|
num_voxels_x=num_voxels_x,
|
|
num_voxels_z=num_voxels_z,
|
|
linear_indices=linear_indices,
|
|
num_pixels=num_pixels,
|
|
device=device
|
|
)
|
|
rotation_images.append(image_means_rotation.unsqueeze(0))
|
|
|
|
# feature_rest 처리 (15개의 SH 계수)
|
|
feature_rest_values = feature_rest[slice_mask] # [M, 15, 3]
|
|
for i in range(15):
|
|
coeff_values = feature_rest_values[:, i, :] # [M, 3]
|
|
image_means_coeff = slice(
|
|
data_values=coeff_values,
|
|
num_voxels_x=num_voxels_x,
|
|
num_voxels_z=num_voxels_z,
|
|
linear_indices=linear_indices,
|
|
num_pixels=num_pixels,
|
|
device=device
|
|
)
|
|
feature_rest_images[i].append(image_means_coeff.unsqueeze(0))
|
|
|
|
# y축 방향으로 이미지를 쌓습니다.
|
|
feature_dc_tensor = torch.cat(feature_dc_images, dim=0) # [Y, Z, X, 3]
|
|
scaling_tensor = torch.cat(scaling_images, dim=0) # [Y, Z, X, 3]
|
|
opacity_tensor = torch.cat(opacity_images, dim=0) # [Y, Z, X, 1]
|
|
rotation_tensor = torch.cat(rotation_images, dim=0) # [Y, Z, X, 4]
|
|
|
|
|
|
feature_rest_tensors = []
|
|
for i in range(15):
|
|
coeff_tensor = torch.cat(feature_rest_images[i], dim=0) # [Y, Z, X, 3]
|
|
feature_rest_tensors.append(coeff_tensor)
|
|
|
|
# 필요한 값들을 딕셔너리에 저장합니다.
|
|
|
|
if fix_xyz == False:
|
|
xyz_tensor = torch.cat(xyz_images, dim=0)
|
|
else:
|
|
xyz_tensor = gaussians._xyz
|
|
print(xyz_tensor.shape)
|
|
|
|
|
|
# 양자화 단계
|
|
if quantize == True:
|
|
xyz_tensor, xyz_min, xyz_max = quantization(xyz_tensor)
|
|
feature_dc_tensor, feature_dc_min, feature_dc_max = quantization(feature_dc_tensor)
|
|
scaling_tensor, scaling_min, scaling_max = quantization(scaling_tensor)
|
|
opacity_tensor, opacity_min, opacity_max = quantization(opacity_tensor)
|
|
rotation_tensor, rotation_min, rotation_max = quantization(rotation_tensor)
|
|
|
|
quantized_feature_rest = []
|
|
feature_rest_mins = []
|
|
feature_rest_maxs = []
|
|
for tensor in feature_rest_tensors:
|
|
quantized_tensor, tensor_min, tensor_max = quantization(tensor)
|
|
quantized_feature_rest.append(quantized_tensor)
|
|
feature_rest_mins.append(tensor_min)
|
|
feature_rest_maxs.append(tensor_max)
|
|
|
|
|
|
# 복원 단계
|
|
xyz_tensor = dequantization(xyz_tensor, xyz_min, xyz_max, device)
|
|
feature_dc_tensor = dequantization(feature_dc_tensor, feature_dc_min, feature_dc_max, device)
|
|
scaling_tensor = dequantization(scaling_tensor, scaling_min, scaling_max, device)
|
|
opacity_tensor = dequantization(opacity_tensor, opacity_min, opacity_max, device)
|
|
rotation_tensor = dequantization(rotation_tensor, rotation_min, rotation_max, device)
|
|
|
|
feature_rest_tensors = []
|
|
for idx, quantized_tensor in enumerate(quantized_feature_rest):
|
|
tensor_min = feature_rest_mins[idx]
|
|
tensor_max = feature_rest_maxs[idx]
|
|
restored_tensor = dequantization(quantized_tensor, tensor_min, tensor_max, device)
|
|
feature_rest_tensors.append(restored_tensor)
|
|
|
|
torch.cuda.empty_cache()
|
|
print("feature dc :",feature_dc_tensor.shape)
|
|
print("scaling :",scaling_tensor.shape)
|
|
print("opacity :",opacity_tensor.shape)
|
|
print("rotation_tensor :",rotation_tensor.shape)
|
|
|
|
projected_data = {
|
|
'feature_dc_tensor': feature_dc_tensor,
|
|
'scaling_tensor': scaling_tensor,
|
|
'xyz_tensor': xyz_tensor,
|
|
'opacity_tensor': opacity_tensor,
|
|
'rotation_tensor': rotation_tensor,
|
|
'feature_rest_tensors': feature_rest_tensors,
|
|
'x_min': x_min,
|
|
'y_min': y_min,
|
|
'z_min': z_min,
|
|
'voxel_size_x': voxel_size_x,
|
|
'voxel_size_y': voxel_size_y,
|
|
'voxel_size_z': voxel_size_z,
|
|
'num_voxels_x': num_voxels_x,
|
|
'num_voxels_y': num_voxels_y,
|
|
'num_voxels_z': num_voxels_z,
|
|
'voxel_indices_x': voxel_indices_x,
|
|
'voxel_indices_y': voxel_indices_y,
|
|
'voxel_indices_z': voxel_indices_z,
|
|
}
|
|
|
|
|
|
return projected_data
|
|
|
|
def unprojection(projected_data, gaussians, device):
|
|
print("=========unprojection=========")
|
|
# 필요한 텐서들을 가져옵니다.
|
|
xyz_tensor = projected_data['xyz_tensor'] # [Y, Z, X, 2]
|
|
# n,3
|
|
feature_dc_tensor = projected_data['feature_dc_tensor'] # [Y, Z, X, 3]
|
|
scaling_tensor = projected_data['scaling_tensor'] # [Y, Z, X, 3]
|
|
opacity_tensor = projected_data['opacity_tensor'] # [Y, Z, X, 1]
|
|
rotation_tensor = projected_data['rotation_tensor'] # [Y, Z, X, 4]
|
|
feature_rest_tensors = projected_data['feature_rest_tensors'] # 리스트 형태
|
|
|
|
y_min = projected_data['y_min']
|
|
voxel_size_y = projected_data['voxel_size_y']
|
|
|
|
num_voxels_x = projected_data['num_voxels_x']
|
|
num_voxels_z = projected_data['num_voxels_z']
|
|
|
|
# xyz_tensor에서 유효한 복셀 위치를 찾습니다.
|
|
xyz_mask = torch.any(xyz_tensor != 0, dim=-1) # [Y, Z, X]
|
|
print("xyz mask, ", xyz_mask.shape)
|
|
valid_indices = torch.nonzero(xyz_mask, as_tuple=False) # [N, 3]
|
|
del xyz_mask
|
|
|
|
# 유효한 복셀의 인덱스를 사용하여 데이터를 추출합니다.
|
|
linear_indices = valid_indices[:, 0] * (num_voxels_z * num_voxels_x) + valid_indices[:, 1] * num_voxels_x + valid_indices[:, 2] # [N]
|
|
|
|
del num_voxels_x
|
|
del num_voxels_z
|
|
# 각 텐서를 평탄화합니다.
|
|
xyz_tensor = xyz_tensor.reshape(-1, 2) # [total_voxels, 2]
|
|
feature_dc_tensor = feature_dc_tensor.reshape(-1, 3) # [total_voxels, 3]
|
|
scaling_tensor = scaling_tensor.reshape(-1, 3) # [total_voxels, 3]
|
|
opacity_tensor = opacity_tensor.reshape(-1, 1) # [total_voxels, 1]
|
|
rotation_tensor = rotation_tensor.reshape(-1, 4) # [total_voxels, 4]
|
|
feature_rest_tensors = [tensor.reshape(-1, 3) for tensor in feature_rest_tensors] # 리스트 형태
|
|
|
|
# 유효한 복셀에서 데이터 추출
|
|
linear_indices = linear_indices.to(device)
|
|
xyz_tensor = xyz_tensor[linear_indices] # [N, 2]
|
|
feature_dc_tensor = feature_dc_tensor[linear_indices] # [N, 3]
|
|
scaling_tensor = scaling_tensor[linear_indices] # [N, 3]
|
|
opacity_tensor = opacity_tensor[linear_indices] # [N, 1]
|
|
rotation_tensor = rotation_tensor[linear_indices] # [N, 4]
|
|
feature_rest_tensors = [tensor[linear_indices] for tensor in feature_rest_tensors] # 리스트 형태
|
|
del linear_indices
|
|
# x, y, z 좌표를 복원합니다.
|
|
voxel_indices_y = valid_indices[:, 0].to(device)
|
|
del valid_indices
|
|
|
|
# x와 z 좌표는 원래 데이터에서 가져옵니다.
|
|
x_coords = xyz_tensor[:, 0]
|
|
z_coords = xyz_tensor[:, 1]
|
|
# y 좌표는 복셀 인덱스로부터 복원합니다.
|
|
y_coords = y_min + (voxel_indices_y.float() + 0.5) * voxel_size_y
|
|
del voxel_indices_y
|
|
|
|
# 최종 xyz 좌표를 생성합니다.
|
|
xyz_tensor = torch.stack((x_coords, y_coords, z_coords), dim=1) # [N, 3]
|
|
del x_coords
|
|
del z_coords
|
|
del y_coords
|
|
|
|
# 가우시안 모델에 데이터 할당
|
|
gaussians._xyz = xyz_tensor # [N, 3]
|
|
gaussians._features_dc = feature_dc_tensor.unsqueeze(1) # [N, 1, 3]
|
|
gaussians._scaling = scaling_tensor # [N, 3]
|
|
gaussians._opacity = opacity_tensor # [N, 1]
|
|
gaussians._rotation = rotation_tensor # [N, 4]
|
|
|
|
# feature_rest를 [N, 15, 3] 형태로 결합합니다.
|
|
feature_rest_tensors = torch.stack(feature_rest_tensors, dim=1) # [N, 15, 3]
|
|
gaussians._features_rest = feature_rest_tensors # [N, 15, 3]
|
|
|
|
return gaussians
|
|
|
|
def fix_xyz_unprojection(projected_data, gaussians, device):
|
|
print("=========fix_xyz_unprojection=========")
|
|
# 필요한 텐서들을 가져옵니다.
|
|
xyz_tensor = projected_data['xyz_tensor']
|
|
print("fix xyz unpro :",xyz_tensor.shape)
|
|
feature_dc_tensor = projected_data['feature_dc_tensor'] # [Y, Z, X, 3]
|
|
scaling_tensor = projected_data['scaling_tensor'] # [Y, Z, X, 3]
|
|
opacity_tensor = projected_data['opacity_tensor'] # [Y, Z, X, 1]
|
|
rotation_tensor = projected_data['rotation_tensor'] # [Y, Z, X, 4]
|
|
feature_rest_tensors = projected_data['feature_rest_tensors'] # 리스트 형태
|
|
|
|
voxel_indices_x = projected_data['voxel_indices_x'] # [N]
|
|
voxel_indices_y = projected_data['voxel_indices_y'] # [N]
|
|
voxel_indices_z = projected_data['voxel_indices_z'] # [N]
|
|
|
|
# 각 포인트의 복셀 인덱스를 사용하여 속성 값을 가져옵니다.
|
|
# 복셀 인덱스를 텐서 인덱스로 사용하기 위해 차원을 확장합니다.
|
|
voxel_indices_y = voxel_indices_y.to(device).unsqueeze(-1) # [N, 1]
|
|
voxel_indices_z = voxel_indices_z.to(device).unsqueeze(-1) # [N, 1]
|
|
voxel_indices_x = voxel_indices_x.to(device).unsqueeze(-1) # [N, 1]
|
|
|
|
# 속성 텐서에서 해당 복셀의 속성 값을 가져옵니다.
|
|
reconstructed_feature_dc = feature_dc_tensor[voxel_indices_y, voxel_indices_z, voxel_indices_x].squeeze(1) # [N, 3]
|
|
reconstructed_scaling = scaling_tensor[voxel_indices_y, voxel_indices_z, voxel_indices_x].squeeze(1) # [N, 3]
|
|
reconstructed_opacity = opacity_tensor[voxel_indices_y, voxel_indices_z, voxel_indices_x].squeeze(1) # [N, 1]
|
|
reconstructed_rotation = rotation_tensor[voxel_indices_y, voxel_indices_z, voxel_indices_x].squeeze(1) # [N, 4]
|
|
reconstructed_feature_rest = []
|
|
|
|
for tensor in feature_rest_tensors:
|
|
value = tensor[voxel_indices_y, voxel_indices_z, voxel_indices_x].squeeze(1) # [N, 3]
|
|
reconstructed_feature_rest.append(value)
|
|
|
|
gaussians._xyz = xyz_tensor
|
|
gaussians._features_dc = reconstructed_feature_dc.unsqueeze(1) # [N, 1, 3]
|
|
gaussians._scaling = reconstructed_scaling # [N, 3]
|
|
gaussians._opacity = reconstructed_opacity # [N, 1]
|
|
gaussians._rotation = reconstructed_rotation # [N, 4]
|
|
|
|
# feature_rest를 [N, 15, 3] 형태로 결합합니다.
|
|
reconstructed_feature_rest = torch.stack(reconstructed_feature_rest, dim=1) # [N, 15, 3]
|
|
gaussians._features_rest = reconstructed_feature_rest # [N, 15, 3]
|
|
|
|
return gaussians
|
|
|
|
|
|
def training_report(reconstructed_gaussians, scene, renderFunc, renderArgs):
|
|
|
|
# 테스트 및 훈련 데이터셋 검증
|
|
torch.cuda.empty_cache()
|
|
validation_configs = ({
|
|
'name': 'test',
|
|
'cameras' : scene.getTestCameras()
|
|
}, {
|
|
'name': 'train',
|
|
'cameras' : [scene.getTrainCameras()[idx % len(scene.getTrainCameras())] for idx in range(5, 30, 5)]
|
|
})
|
|
|
|
|
|
for config in validation_configs:
|
|
if config['cameras'] and len(config['cameras']) > 0:
|
|
psnr_test = 0.0
|
|
for idx, viewpoint in enumerate(config['cameras']):
|
|
# 재구성된 Gaussian 모델로 렌더링
|
|
image = torch.clamp(renderFunc(viewpoint, reconstructed_gaussians, *renderArgs)["render"], 0.0, 1.0)
|
|
gt_image = torch.clamp(viewpoint.original_image.to("cuda"), 0.0, 1.0)
|
|
psnr_test += psnr(image, gt_image).mean().double()
|
|
psnr_test /= len(config['cameras'])
|
|
|
|
# PSNR과 L1 Test 값 출력
|
|
print(f"\nEvaluating {config['name']}: PSNR {psnr_test:.4f}")
|
|
|
|
torch.cuda.empty_cache()
|
|
# python .\projection.py -s .\data\gaussian_splatting\tandt_db\tandt\truck --start_checkpoint .\output\lego\point_cloud\iteration_30000\point_cloud.ply
|
|
# python .\projection.py -s ..\data\nerf_synthetic\nerf_synthetic\lego --start_checkpoint .\output\lego\point_cloud\iteration_30000\point_cloud.ply
|
|
|
|
if __name__ == "__main__":
|
|
# Set up command line argument parser
|
|
parser = ArgumentParser(description="Training script parameters")
|
|
lp = ModelParams(parser)
|
|
op = OptimizationParams(parser)
|
|
pp = PipelineParams(parser)
|
|
parser.add_argument('--ip', type=str, default="127.0.0.1")
|
|
parser.add_argument('--port', type=int, default=6009)
|
|
parser.add_argument('--debug_from', type=int, default=-1)
|
|
parser.add_argument('--detect_anomaly', action='store_true', default=False)
|
|
parser.add_argument("--test_iterations", nargs="+", type=int, default=[7_000, 30_000])
|
|
parser.add_argument("--save_iterations", nargs="+", type=int, default=[7_000, 30_000])
|
|
parser.add_argument("--quiet", action="store_true")
|
|
parser.add_argument("--checkpoint_iterations", nargs="+", type=int, default=[])
|
|
parser.add_argument("--start_checkpoint", type=str, default = None)
|
|
args = parser.parse_args(sys.argv[1:])
|
|
args.save_iterations.append(args.iterations)
|
|
|
|
print("Optimizing " + args.model_path)
|
|
|
|
# Initialize system state (RNG)
|
|
safe_state(args.quiet)
|
|
|
|
# Start GUI server, configure and run training
|
|
network_gui.init(args.ip, args.port)
|
|
torch.autograd.set_detect_anomaly(args.detect_anomaly)
|
|
gaussians = GaussianModel(lp.extract(args).sh_degree)
|
|
scene = Scene(lp.extract(args), gaussians)
|
|
|
|
fix_xyz = True
|
|
quantize = False
|
|
gaussians.load_ply(args.start_checkpoint)
|
|
projected_data = projection(gaussians, op.extract(args), pp.extract(args), args.test_iterations, args.save_iterations, args.checkpoint_iterations, args.debug_from, fix_xyz, quantize)
|
|
|
|
output_video_path = './output/lego/point_cloud/iteration_30000/compression_uint8/'
|
|
# compression_ffv1(projected_data, output_video_path)
|
|
# compression_h265(projected_data, output_video_path)
|
|
output_ply_path = './output/lego/point_cloud/iteration_30000/compression_uint8/xyz.ply'
|
|
xyz_tensor = projected_data['xyz_tensor']
|
|
# save_ply(xyz_tensor, output_ply_path)
|
|
|
|
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
|
if fix_xyz == False:
|
|
reconstructed_gaussians = unprojection(projected_data, gaussians, device)
|
|
else:
|
|
reconstructed_gaussians = fix_xyz_unprojection(projected_data, gaussians, device)
|
|
|
|
bg_color = [1, 1, 1] if lp.extract(args).white_background else [0, 0, 0]
|
|
background = torch.tensor(bg_color, dtype=torch.float32, device="cuda")
|
|
training_report(gaussians, scene, render, (pp.extract(args), background))
|
|
# 재구성된 가우시안을 저장하려면 다음과 같이 저장합니다.
|
|
if reconstructed_gaussians is not None:
|
|
reconstructed_gaussians.save_ply('./output/lego/point_cloud/iteration_30000/uint8_fix.ply')
|
|
|
|
# All done
|
|
print("\nTraining complete.")
|