[3D CV ์ฐ๊ตฌ] 3DGS input & output .ply vertex properties & Meshlab Vert & Spherical Harmonics (SH) & Mesh
Meshlab์์ ํํ๋๋ Vert์ ์๋ฏธ๋ ์ผ๋ฐ์ ์ผ๋ก point cloud ๋๋ 3D mesh์ ์์ด์ ๊ฐ๊ฐ์ ์ (์ ์ , vertex)๋ฅผ ์๋ฏธํฉ๋๋ค. ์ด ์ ๋ค์ ๊ณต๊ฐ ๋ด ์์น์ ํด๋น ์์น์ ์์ฑ(i.e. color, normal vector)์ ๋ํ๋ ๋๋ค.
3dgs์์ ๊ฐ Vert(์ ์ ) ๋ฐ์ดํฐ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
1) Random initialization์ผ๋ก ์์ฑํ input.ply๋ ๋ค์์ ํฌํจํฉ๋๋ค.
- x, y, z (position)
- nx, ny, nz (normal)
- red, green, blue (color)
์ฒซ 5๊ฐ์ Vert(์ ์ ) ๋ฐ์ดํฐ๋ฅผ ์ถ๋ ฅํ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ๋ถ๋ฌ์ค๋ ์ฝ๋๋ 3dgs/scene/dataset_readers.py์์
fecthPly
์ ๊ทธ๋๋ก ์ฌ์ฉํ๊ณ print๋ฌธ๋ง ์ถ๊ฐํ์ฌ ์ถ๋ ฅํ์์ต๋๋ค.
input.ply
import numpy as np
from plyfile import PlyData
def fetchPly(path):
plydata = PlyData.read(path)
vertices = plydata['vertex']
positions = np.vstack([vertices['x'], vertices['y'], vertices['z']]).T
colors = np.vstack([vertices['red'], vertices['green'], vertices['blue']]).T / 255.0
normals = np.vstack([vertices['nx'], vertices['ny'], vertices['nz']]).T
# ์ฒซ 5๊ฐ์ ์์ ์ถ๋ ฅ
print("First 5 elements:")
print("x:", vertices['x'][:5])
print("y:", vertices['y'][:5])
print("z:", vertices['z'][:5])
print("nx:", vertices['nx'][:5])
print("ny:", vertices['ny'][:5])
print("nz:", vertices['nz'][:5])
print("red:", vertices['red'][:5])
print("green:", vertices['green'][:5])
print("blue:", vertices['blue'][:5])
# PLY ํ์ผ ๊ฒฝ๋ก๋ฅผ ์ง์ ํฉ๋๋ค.
ply_file_path = "C:/Users/MNL/KHS/gaussian-splatting/output/realsense_d435/180deg@15/input.ply"
# PLY ํ์ผ์ ์ฝ์ด์ค๊ณ ์ฒซ 5๊ฐ ์์๋ฅผ ์ถ๋ ฅํฉ๋๋ค.
fetchPly(ply_file_path)
2) Depth camera๋ก ์ดฌ์ํ์ฌ ์ป์ input.ply ๋ฐ์ดํฐ๋ ๋ค์์ ํฌํจํฉ๋๋ค.
- x, y, z (position)
- nx, ny, nz (normal)
- red, green, blue (color)
์ฒซ 5๊ฐ์ Vert(์ ์ ) ๋ฐ์ดํฐ๋ฅผ ์ถ๋ ฅํ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
input.ply
import numpy as np
from plyfile import PlyData
def fetchPly(path):
plydata = PlyData.read(path)
vertices = plydata['vertex']
positions = np.vstack([vertices['x'], vertices['y'], vertices['z']]).T
colors = np.vstack([vertices['red'], vertices['green'], vertices['blue']]).T / 255.0
normals = np.vstack([vertices['nx'], vertices['ny'], vertices['nz']]).T
# ์ฒซ 5๊ฐ์ ์์ ์ถ๋ ฅ
print("First 5 elements:")
print("x:", vertices['x'][:5])
print("y:", vertices['y'][:5])
print("z:", vertices['z'][:5])
print("nx:", vertices['nx'][:5])
print("ny:", vertices['ny'][:5])
print("nz:", vertices['nz'][:5])
print("red:", vertices['red'][:5])
print("green:", vertices['green'][:5])
print("blue:", vertices['blue'][:5])
# PLY ํ์ผ ๊ฒฝ๋ก๋ฅผ ์ง์ ํฉ๋๋ค.
ply_file_path = "C:/Users/MNL/KHS/gaussian-splatting/output/realsense_d435/180deg@15_cam_poses_bbox_pcd/input.ply"
# PLY ํ์ผ์ ์ฝ์ด์ค๊ณ ์ฒซ 5๊ฐ ์์๋ฅผ ์ถ๋ ฅํฉ๋๋ค.
fetchPly(ply_file_path)
์ฆ, random initialization์ผ๋ก ์์ฑํ๋ input.ply์ depth camera๋ก depth ์ ๋ณด๊น์ง ์ป์ด ์์ฑํ input.ply๊ฐ ๊ฐ์ง๋ ์ ๋ณด์ ์ข ๋ฅ๋ ๊ฐ์ต๋๋ค.
3) 3dgs output์ธ point_cloud.ply๋ฅผ ๊ตฌ์ฑํ๋ ์์์ default shape์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- x, y, z (position) # (n_points, 3)
- f_dc_0, f_dc_1, f_dc_2 (Spherical Harmonics 0๋ฒ์งธ band์ rgb์์ ์ฑ๋๋ง๋ค ๋ค๋ฅธ ์์ ๊ฐ) # (n_points, 1, 3)
- f_rest_0 ~ f_rest_44 (์ฌ์ฉ ๊ฐ๋ฅํ ๋ค์ํ ์ถ๊ฐ ์์ฑ, i.e. Spherical Harmonics๋ก ์ฌ์ฉ๊ฐ๋ฅํจ) # (n_points, 15, 3)
-
f_rest shape์์ ์์ 15๋ max_sh_degree = 3์ผ ๋ 15๊ฐ, ๋ค์ 3์ rgb ์ฑ๋์ ์๋ฏธ
-
-
opacity (๋ถํฌ๋ช ๋) # (n_points, 1)
-
scale_n (์ค์ผ์ผ ์ ๋ณด) # (n_points, 3)
-
rot_n (ํ์ ์ ๋ณด) # (n_points, 4) # quaternion์ด๋ผ์ 4๊ฐ๋ก rot_0, rot_1, rot_2, rot_3
3dgs์ output์ธ point_cloud.ply์ ์ฒซ ๋ฒ์งธ Vert(์ ์ ) ๋ฐ์ดํฐ๋ฅผ ์ถ๋ ฅํ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ๋ถ๋ฌ์ค๋ ์ฝ๋๋ 3dgs/scene/gaussian_model.py์์
load_ply
๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ์ฌ print๋ฌธ๋ง ์ถ๋ ฅํ์์ต๋๋ค. plydata.elements[0]
์์ 0๋ฒ์งธ๋ง ์ธ๋ฑ์ฑํ๋ ์ด์ ๋ ํ๋์ point cloud data๋ง ์ ์ฅํ๊ณ ์๋ ply ํ์ผ์ด๊ธฐ ๋๋ฌธ์ ๋๋ค.print(plydata.elements)
print(len(plydata.elements))
print(plydata.elements[0])
output.ply
import numpy as np
import torch
from torch import nn
from plyfile import PlyData
class GaussianModel:
def __init__(self, max_sh_degree):
self.max_sh_degree = max_sh_degree
def load_ply(self, path):
plydata = PlyData.read(path)
# print("plydata.elements:", plydata.elements)
# print("len(plydata.elements):", len(plydata.elements))
# print("plydata.elements[0]:", plydata.elements[0])
xyz = np.stack((np.asarray(plydata.elements[0]["x"]),
np.asarray(plydata.elements[0]["y"]),
np.asarray(plydata.elements[0]["z"])), axis=1)
opacities = np.asarray(plydata.elements[0]["opacity"])[..., np.newaxis]
features_dc = np.zeros((xyz.shape[0], 3, 1))
features_dc[:, 0, 0] = np.asarray(plydata.elements[0]["f_dc_0"])
features_dc[:, 1, 0] = np.asarray(plydata.elements[0]["f_dc_1"])
features_dc[:, 2, 0] = np.asarray(plydata.elements[0]["f_dc_2"])
extra_f_names = [p.name for p in plydata.elements[0].properties if p.name.startswith("f_rest_")]
extra_f_names = sorted(extra_f_names, key=lambda x: int(x.split('_')[-1]))
print(extra_f_names)
assert len(extra_f_names) == 3 * (self.max_sh_degree + 1) ** 2 - 3
features_extra = np.zeros((xyz.shape[0], len(extra_f_names)))
for idx, attr_name in enumerate(extra_f_names):
features_extra[:, idx] = np.asarray(plydata.elements[0][attr_name])
features_extra = features_extra.reshape((features_extra.shape[0], 3, (self.max_sh_degree + 1) ** 2 - 1))
scale_names = [p.name for p in plydata.elements[0].properties if p.name.startswith("scale_")]
scale_names = sorted(scale_names, key=lambda x: int(x.split('_')[-1]))
print(scale_names)
scales = np.zeros((xyz.shape[0], len(scale_names)))
for idx, attr_name in enumerate(scale_names):
scales[:, idx] = np.asarray(plydata.elements[0][attr_name])
rot_names = [p.name for p in plydata.elements[0].properties if p.name.startswith("rot")]
rot_names = sorted(rot_names, key=lambda x: int(x.split('_')[-1]))
print(rot_names)
rots = np.zeros((xyz.shape[0], len(rot_names)))
for idx, attr_name in enumerate(rot_names):
rots[:, idx] = np.asarray(plydata.elements[0][attr_name])
self._xyz = nn.Parameter(torch.tensor(xyz, dtype=torch.float, device="cuda").requires_grad_(True))
self._features_dc = nn.Parameter(torch.tensor(features_dc, dtype=torch.float, device="cuda").transpose(1, 2).contiguous().requires_grad_(True))
self._features_rest = nn.Parameter(torch.tensor(features_extra, dtype=torch.float, device="cuda").transpose(1, 2).contiguous().requires_grad_(True))
self._opacity = nn.Parameter(torch.tensor(opacities, dtype=torch.float, device="cuda").requires_grad_(True))
self._scaling = nn.Parameter(torch.tensor(scales, dtype=torch.float, device="cuda").requires_grad_(True))
self._rotation = nn.Parameter(torch.tensor(rots, dtype=torch.float, device="cuda").requires_grad_(True))
self.active_sh_degree = self.max_sh_degree
def print_parameters(self):
parameters = {
"XYZ": self._xyz.cpu().detach().numpy(),
"Features DC": self._features_dc.cpu().detach().numpy(),
"Features Rest": self._features_rest.cpu().detach().numpy(),
"Opacity": self._opacity.cpu().detach().numpy(),
"Scaling": self._scaling.cpu().detach().numpy(),
"Rotation": self._rotation.cpu().detach().numpy(),
}
for key, value in parameters.items():
print(f"{key} shape: {value.shape}")
print(f"{key} first 5 elements:\n{value[:5]}\n")
# ํด๋์ค ์ธ๋ถ์์ ๊ฐ์ฒด ์์ฑ ๋ฐ PLY ํ์ผ ๋ก๋ ๋ฐ ์ถ๋ ฅ
# max_sh_degree ๊ฐ์ ์ง์ ํฉ๋๋ค. ์๋ฅผ ๋ค์ด 3์ผ๋ก ์ค์ ํฉ๋๋ค.
model = GaussianModel(max_sh_degree=3)
# PLY ํ์ผ ๊ฒฝ๋ก๋ฅผ ์ง์ ํฉ๋๋ค.
ply_file_path = "C:/Users/MNL/KHS/gaussian-splatting/output/realsense_d435/180deg@15_cam_poses_bbox_pcd/point_cloud/iteration_30000/point_cloud.ply"
# PLY ํ์ผ์ ์ฝ์ด์ต๋๋ค.
model.load_ply(ply_file_path)
# ๊ฐ ํ๋ผ๋ฏธํฐ ๊ฐ์ ์ถ๋ ฅํฉ๋๋ค.
model.print_parameters()
Meshlab์์ .ply๋ฅผ visualizeํด๋ณด๋ฉด
input์ color์ ๋ํด Vert๋ฅผ ๊ฐ์ง๊ณ ์์ผ๋, output.ply๋ color์ ๋ํด Vert๋ฅผ ๊ฐ์ง๊ณ ์์ง ์์ต๋๋ค.
1) Random initialization input.ply
2) Depth camera๋ก ์ป์ input.ply
3) 3dgs output์ธ point_cloud.ply
3dgs code์์ output.ply๋ point_cloud.ply ์ด๋ฆ์ผ๋ก ์ ์ฅ๋ฉ๋๋ค.
Color์ ๋ํ Vert
- input.ply: ๊ฐ ์ ์ (Vertex)์ ์์ ์ ๋ณด red, green, blue (color)๊ฐ ํฌํจ๋์ด ์์ต๋๋ค. ์ ์ ๋ง๋ค ๊ณ ์ ํ ์์ ์ ๋ณด๊ฐ ์์ด, ๋ฉ์ฌ ๋๋ ํฌ์ธํธ ํด๋ผ์ฐ๋๊ฐ ์ปฌ๋ฌ๋ก ์๊ฐํ๋ฉ๋๋ค.
- output.ply: ๊ฐ ์ ์ (Vertex)์ ์์ ์ ๋ณด red, green, blue (color)๊ฐ ํฌํจ๋์ด ์์ง ์์ต๋๋ค. ์์ ์ ๋ณด๊ฐ ์์ผ๋ฏ๋ก, Meshlab์์ ์๊ฐํํ ๋ ํ์์กฐ๋ก ํ์๋ ๊ฐ๋ฅ์ฑ์ด ํฝ๋๋ค.
Shading์ ๋ํ Vert
input.ply์ output.ply ๋ชจ๋: ๊ฐ ์ ์ (Vertex)์ ๋ฒ์ ๋ฒกํฐ(normal) ์ ๋ณด๊ฐ ํฌํจ๋์ด ์์ผ๋ 0์ผ๋ก ์ด๊ธฐํ ๋ฉ๋๋ค.
- ๋ฒ์ ๋ฒกํฐ๋ ํ๋ฉด์ ๋ฐฉํฅ์ ๋ํ๋ด๋ฉฐ, ์กฐ๋ช ๊ณผ ์์(Shading)์ ๊ณ์ฐํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค.
- ๋ฒ์ ๋ฒกํฐ๋ ์กฐ๋ช ๊ณผ ์์ ํจ๊ณผ๋ฅผ ํตํด 3D ๋ชจ๋ธ์ ๋ณด๋ค ํ์ค์ ์ผ๋ก ์๊ฐํํ๋ ๋ฐ ๋์์ ์ค๋๋ค.
Spherical Harmonics๋ orignal 3dgs paper & code์์ ์ฌ์ฉํ๊ณ ์์ง ์์ต๋๋ค. RGB๋ก๋ง ์์ ๊ฐ์ ์ต์ ํ ํ์ต๋๋ค.
Q) Spherical Harmonics๋ฅผ ์ฌ์ฉํ์ง ์์ ๊ฒฝ์ฐ ์์์ ํํํ๋ ๋ฐฉ๋ฒ์ ๋ฌด์์ผ๊น์?
A) ๋ชจ๋ ๋ฐฉํฅ์ ๋ํด ์ผ์ ํ ์์์ ๊ฐ์ง๋ 0๋ฒ์งธ band๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๋๋ค. ์ด๋ฅผ ํตํด ๋จ์ํ RGB ์์ ๊ฐ์ ์ต์ ํํ์์ต๋๋ค.
https://github.com/graphdeco-inria/gaussian-splatting/issues/73
3DGS๋ point cloud๋ฅผ ์์ฑํ์ง, mesh๋ฅผ ์์ฑํ์ง๋ ์์ต๋๋ค.
3dgs์ output์ point_cloud๋ฅผ .ply ํํ๋ก ๋ด๋ณด๋ ๋๋ค. ์ ์๋ mesh ์์ฒด๋ฅผ 3dgs๋ก๋ถํฐ directly produceํ๋ ๊ฒ์ ๊ณํํ๊ณ ์์ง ์๋ค๊ณ ๋ฐํ์ต๋๋ค.
https://github.com/graphdeco-inria/gaussian-splatting/issues/125
ํ์ง๋ง 3dgs๋ก ์ป์ .ply ํ์ผ์ธ point cloud์ poisson reconstruction์ ์ ์ฉํ๋ฉด mesh๋ฅผ ์์ฑํด๋ผ ์ ์์ต๋๋ค.
Leave a comment