4 minute read

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)

image

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)

image

์ฆ‰, 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 ์ฑ„๋„์„ ์˜๋ฏธ

      image

  • opacity (๋ถˆํˆฌ๋ช…๋„) # (n_points, 1)

  • scale_n (์Šค์ผ€์ผ ์ •๋ณด) # (n_points, 3)

    image

  • rot_n (ํšŒ์ „ ์ •๋ณด) # (n_points, 4) # quaternion์ด๋ผ์„œ 4๊ฐœ๋กœ rot_0, rot_1, rot_2, rot_3

    image

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)

    image

    print(len(plydata.elements))

    image

    print(plydata.elements[0])

    image

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()

image image image image image


Meshlab์—์„œ .ply๋ฅผ visualizeํ•ด๋ณด๋ฉด

input์€ color์— ๋Œ€ํ•ด Vert๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋‚˜, output.ply๋Š” color์— ๋Œ€ํ•ด Vert๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

1) Random initialization input.ply image

2) Depth camera๋กœ ์–ป์€ input.ply image

3) 3dgs output์ธ point_cloud.ply image

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

image


3DGS๋Š” point cloud๋ฅผ ์ƒ์„ฑํ•˜์ง€, mesh๋ฅผ ์ƒ์„ฑํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค.

3dgs์˜ output์€ point_cloud๋ฅผ .ply ํ˜•ํƒœ๋กœ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค. ์ €์ž๋Š” mesh ์ž์ฒด๋ฅผ 3dgs๋กœ๋ถ€ํ„ฐ directly produceํ•˜๋Š” ๊ฒƒ์€ ๊ณ„ํšํ•˜๊ณ  ์žˆ์ง€ ์•Š๋‹ค๊ณ  ๋ฐํ˜”์Šต๋‹ˆ๋‹ค.

https://github.com/graphdeco-inria/gaussian-splatting/issues/125

image

ํ•˜์ง€๋งŒ 3dgs๋กœ ์–ป์€ .ply ํŒŒ์ผ์ธ point cloud์— poisson reconstruction์„ ์ ์šฉํ•˜๋ฉด mesh๋ฅผ ์ƒ์„ฑํ•ด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

image

image

Leave a comment