12 minute read

Spherical Harmonics (SH)์™€ DC ์„ฑ๋ถ„

  • Spherical Harmonics(SH)์€ ๊ตฌ๋ฉด ์ขŒํ‘œ๊ณ„์—์„œ ์ •์˜๋˜๋Š” ํ•จ์ˆ˜์˜ ์ง‘ํ•ฉ์œผ๋กœ, 3D ๊ทธ๋ž˜ํ”ฝ์Šค์—์„œ ์กฐ๋ช…, ๋ฐ˜์‚ฌ, ์Œํ–ฅ ๋“ฑ์„ ํ‘œํ˜„ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • SH ๊ณ„์ˆ˜๋Š” SH Level์— ๋”ฐ๋ผ DC ์„ฑ๋ถ„๊ณผ ๋‚˜๋จธ์ง€ ์„ฑ๋ถ„์œผ๋กœ ๋‚˜๋‰ฉ๋‹ˆ๋‹ค.
  • ์•„๋ž˜ ์ฝ”๋“œ์—์„œ ์‹ค์‚ฌ์šฉ ์˜ˆ์‹œ๋ฅผ ๋จผ์ € ์ดํ•ดํ•ด๋ด…์‹œ๋‹ค.

feateaures = concat([features_dc, features_rest], dim=1) # (n_points, sh ๊ณ„์ˆ˜, RGB)

# 3dgs/scene/gaussian_model.py

class GaussianModel:
...
    @property
    def get_features(self):
        features_dc = self._features_dc
        features_rest = self._features_rest
        return torch.cat((features_dc, features_rest), dim=1)

  • features_dc # (n_points, 1, RGB)
  • features_rest # (n_points, 15, RGB)
  • featrues_dc์™€ features_rest๋ฅผ sh ๊ณ„์ˆ˜ ์ฐจ์›์œผ๋กœ concat ํ•ฉ๋‹ˆ๋‹ค.

RGB 3 channel๋งˆ๋‹ค n๋ฒˆ์งธ sh ๊ณ„์ˆ˜์— ๋Œ€ํ•œ ์ฑ„๋„์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

  • features_dc[:, 0, 0] = n_points, R channel, sh 0๋ฒˆ์งธ ๊ณ„์ˆ˜, "f_dc_0"์œผ๋กœ ๋ณ€์ˆ˜์ด๋ฆ„ ์ •์˜
  • features_dc[:, 0, 1] = n_points, G channel, sh 0๋ฒˆ์งธ ๊ณ„์ˆ˜, "f_dc_1"์œผ๋กœ ๋ณ€์ˆ˜์ด๋ฆ„ ์ •์˜
  • features_dc[:, 0, 2] = n_points, B channel, sh 0๋ฒˆ์งธ ๊ณ„์ˆ˜, "f_dc_2"์œผ๋กœ ๋ณ€์ˆ˜์ด๋ฆ„ ์ •์˜
  • features_dc์™€ features_extra๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ๋Š” shape์ด (n_points, RGB, sh ๊ณ„์ˆ˜)๋กœ ์ •์˜๋ฉ๋‹ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ ํ•™์Šต์—์„œ ์‚ฌ์šฉ๋  ๋•Œ, features_dc, features_extra ๋ชจ๋‘ transpose(1, 2)ํ•˜์—ฌ self_features_dc, self._features_rest๋กœ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
  • self_features_dc, self._features_rest์˜ shape์€ (n_points, sh ๊ณ„์ˆ˜, RGB)๋กœ ๋ฐ”๋€๋‹ˆ๋‹ค.
# 3dgs/scene/gaussian_model.py

class GaussianModel:
...
    def load_ply(self, path):
...
        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]))
        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])
        # Reshape (P,F*SH_coeffs) to (P, F, SH_coeffs except DC)
        features_extra = features_extra.reshape((features_extra.shape[0], 3, (self.max_sh_degree + 1) ** 2 - 1))
...
        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))
...
  • (n_points, sh ๊ณ„์ˆ˜, RGB)๋กœ ์ฐจ์›์ด ๋ฐ”๋€ self._features_dc, self._features_rest๋Š” sh ๊ณ„์ˆ˜์ฐจ์›์ธ dim=1์—์„œ concatํ•ฉ๋‹ˆ๋‹ค.
# 3dgs/scene/gaussian_model.py

class GaussianModel:
...
    @property
    def get_features(self):
        features_dc = self._features_dc
        features_rest = self._features_rest
        return torch.cat((features_dc, features_rest), dim=1)

f_dc, self._features_dc, f_rest, self._features_rest๋Š” (n_points, sh ๊ณ„์ˆ˜, RGB) ํ˜•ํƒœ๋กœ ํ•™์Šต์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

  • self._features_dc, self._features_rest๋ฅผ ์ •์˜ํ•˜๋Š” ๋ถ€๋ถ„์„ ๋ด…์‹œ๋‹ค.
  • random_initilization์œผ๋กœ pcd๋ฅผ 100,000๊ฐœ ์ •์˜ํ•˜์˜€์„ ๋•Œ, features.shape # (n_points, RGB, sh ๊ณ„์ˆ˜) = (100000, 3, 16)

    image

  • self._features_dc๋Š” features[:,:,0:1].transpose(1, 2) # (n_points, RGB, sh 0๋ฒˆ์งธ ๊ณ„์ˆ˜).transpose(1, 2) -> (n_points, sh 0๋ฒˆ์งธ ๊ณ„์ˆ˜, RGB) = (100000, 1, 3)

    image

  • self._features_rest๋Š” features[:,:,1:].transpose(1, 2) # (n_points, RGB, sh 1~15๋ฒˆ์งธ ๊ณ„์ˆ˜).transpose(1, 2) -> (n_points, sh 1~15๋ฒˆ์งธ ๊ณ„์ˆ˜, RGB) = (100000, 15, 3)

    image

# 3dgs/scene/gaussian_model.py

class GaussianModel:
...
    def create_from_pcd(self, pcd : BasicPointCloud, spatial_lr_scale : float):
        self.spatial_lr_scale = spatial_lr_scale
        fused_point_cloud = torch.tensor(np.asarray(pcd.points)).float().cuda()
        fused_color = RGB2SH(torch.tensor(np.asarray(pcd.colors)).float().cuda())
        features = torch.zeros((fused_color.shape[0], 3, (self.max_sh_degree + 1) ** 2)).float().cuda()
        features[:, :3, 0 ] = fused_color
        features[:, 3:, 1:] = 0.0

        print("Number of points at initialisation : ", fused_point_cloud.shape[0])

        dist2 = torch.clamp_min(distCUDA2(torch.from_numpy(np.asarray(pcd.points)).float().cuda()), 0.0000001)
        scales = torch.log(torch.sqrt(dist2))[...,None].repeat(1, 3)
        rots = torch.zeros((fused_point_cloud.shape[0], 4), device="cuda")
        rots[:, 0] = 1

        opacities = inverse_sigmoid(0.1 * torch.ones((fused_point_cloud.shape[0], 1), dtype=torch.float, device="cuda"))

        self._xyz = nn.Parameter(fused_point_cloud.requires_grad_(True))
        self._features_dc = nn.Parameter(features[:,:,0:1].transpose(1, 2).contiguous().requires_grad_(True))
        self._features_rest = nn.Parameter(features[:,:,1:].transpose(1, 2).contiguous().requires_grad_(True))
...

์ตœ์ข…์ ์œผ๋กœ self._features_dc๊ณผ self._features_rest๋Š” sh ๊ณ„์ˆ˜ ์ฑ„๋„์—์„œ concatํ•˜์—ฌ ํ•™์Šต์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

class GaussianModel:
...

    @property
    def get_features(self):
        features_dc = self._features_dc
        features_rest = self._features_rest
        return torch.cat((features_dc, features_rest), dim=1)

  • features_dc.shape # (n_points, sh 0๋ฒˆ์งธ ๊ณ„์ˆ˜, RGB) = (100000, 1, 3)

    image

  • features_rest # (n_points, sh 1~15๋ฒˆ์งธ ๊ณ„์ˆ˜, RGB) = (100000, 15, 3)

    image

  • torch.cat((features_dc, features_rest), dim=1) # (n_points, sh 0~16๋ฒˆ์งธ ๊ณ„์ˆ˜, RBG) = (100000, 16, 3)

    image

ํ•™์Šต๋œ self._features_dc, self._features_rest๋ฅผ ์ €์žฅํ•  ๋•Œ๋Š” ๋‹ค์‹œ # (n_points, RGB, sh ๊ณ„์ˆ˜)๋กœ shape์„ ๋งŒ๋“ค์–ด ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

# 3dgs/scene/gaussian_model.py

class GaussianModel:
...
    def save_ply(self, path):
        mkdir_p(os.path.dirname(path))

        xyz = self._xyz.detach().cpu().numpy()
        normals = np.zeros_like(xyz)
        f_dc = self._features_dc.detach().transpose(1, 2).flatten(start_dim=1).contiguous().cpu().numpy()
        f_rest = self._features_rest.detach().transpose(1, 2).flatten(start_dim=1).contiguous().cpu().numpy()
        opacities = self._opacity.detach().cpu().numpy()
        scale = self._scaling.detach().cpu().numpy()
        rotation = self._rotation.detach().cpu().numpy()

        dtype_full = [(attribute, 'f4') for attribute in self.construct_list_of_attributes()]

        elements = np.empty(xyz.shape[0], dtype=dtype_full)
        attributes = np.concatenate((xyz, normals, f_dc, f_rest, opacities, scale, rotation), axis=1)
        elements[:] = list(map(tuple, attributes))
        el = PlyElement.describe(elements, 'vertex')
        PlyData([el]).write(path)

initial self._features_dc # (n_points, sh 0๋ฒˆ์งธ ๊ณ„์ˆ˜, RGB) = (100000, 1, 3)

  • self._features_dc # (optimized n_points, sh 0๋ฒˆ์งธ ๊ณ„์ˆ˜, RGB) = (317737, 1 3)

image

  • self._features_dc.detach().transpose(1, 2) # (optimized n_points, RGB, sh 0๋ฒˆ์งธ ๊ณ„์ˆ˜) = (317737, 3, 1)

image

  • self._features_dc.detach().transpose(1, 2).flatten(start_dim=1) # (optimized n_points, RGB * sh 0๋ฒˆ์งธ ๊ณ„์ˆ˜) = (317737, 3)

image

  • f_dc = self._features_dc.detach().transpose(1, 2).flatten(start_dim=1).contiguous().cpu().numpy()์ด๋ฏ€๋กœ

image

initial self._features_rest # (n_points, sh 1~15๋ฒˆ์งธ ๊ณ„์ˆ˜, RGB) = (100000, 15, 3)

  • self._features_rest # (optimized n_points, sh 1~15๋ฒˆ์งธ ๊ณ„์ˆ˜, RGB) = (317737, 15 3)

image

  • self._features_rest.detach().transpose(1, 2) # (optimized n_points, RGB, sh 1~15๋ฒˆ์งธ ๊ณ„์ˆ˜) = (317737, 3, 15)

image

  • self._features_rest.detach().transpose(1, 2).flatten(start_dim=1) # (optimized n_points, RGB * sh 1~15๋ฒˆ์งธ ๊ณ„์ˆ˜) = (317737, 45)

image

  • f_rest = self._features_rest.detach().transpose(1, 2).flatten(start_dim=1).contiguous().cpu().numpy()์ด๋ฏ€๋กœ

image


DC ์„ฑ๋ถ„์ด๋ž€?

  • DC ์„ฑ๋ถ„ (Direct Current ์„ฑ๋ถ„): SH ํ‘œํ˜„์˜ ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์ฃผํŒŒ์ˆ˜ ์„ฑ๋ถ„์œผ๋กœ, ์ƒ์ˆ˜ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ์ด๋Š” ๊ตฌ๋ฉด์—์„œ ๋ชจ๋“  ๋ฐฉํ–ฅ์— ๋Œ€ํ•ด ์ผ์ •ํ•œ ๊ฐ’์„ ๊ฐ€์ง€๋ฉฐ, 0์ฐจ ๊ตฌ๋ฉด ์กฐํ™” ํ•จ์ˆ˜๋กœ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์—ญํ• :
    • ๊ธฐ๋ณธ ์กฐ๋ช… ํ‘œํ˜„: ๋ฐฉํ–ฅ์— ๊ด€๊ณ„์—†์ด ์ผ์ •ํ•œ ์กฐ๋ช…์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
    • ๋ฒ ์ด์Šค๋ผ์ธ ์„ค์ •: ๊ณ ์ฐจ ๊ตฌ๋ฉด ์กฐํ™” ํ•จ์ˆ˜์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋˜์–ด ๋ฐฉํ–ฅ์— ๋”ฐ๋ฅธ ์กฐ๋ช… ๋ณ€ํ™”๋ฅผ ๋” ์ž˜ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

SH ๊ณ„์ˆ˜์˜ ๊ตฌ์กฐ

๊ฐ SH Level๋งˆ๋‹ค DC ์„ฑ๋ถ„๊ณผ rest ์„ฑ๋ถ„์ด ์กด์žฌํ•˜๋ฉฐ, RGB ์ฑ„๋„๋ณ„๋กœ ๊ฐ๊ฐ์˜ ๊ณ„์ˆ˜๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

Level 0

  • DC ์„ฑ๋ถ„: 1๊ฐœ (RGB ๊ฐ๊ฐ 1๊ฐœ์”ฉ ์ด 3๊ฐœ)
  • Rest ์„ฑ๋ถ„: ์—†์Œ
  • ์ด ๊ณ„์ˆ˜: 1๊ฐœ์˜ DC ์„ฑ๋ถ„ x 3 (RGB ์ฑ„๋„) = 3๊ฐœ

Level 1

  • DC ์„ฑ๋ถ„: 1๊ฐœ (RGB ๊ฐ๊ฐ 1๊ฐœ์”ฉ ์ด 3๊ฐœ)
  • Rest ์„ฑ๋ถ„: 3๊ฐœ (๊ฐ RGB ์ฑ„๋„๋งˆ๋‹ค 3๊ฐœ์˜ ์ถ”๊ฐ€ ๊ณ„์ˆ˜)
  • ์ด ๊ณ„์ˆ˜: 1๊ฐœ์˜ DC ์„ฑ๋ถ„ + 3๊ฐœ์˜ rest ์„ฑ๋ถ„ = 4๊ฐœ
    • RGB ๊ฐ๊ฐ์— ๋Œ€ํ•ด 4๊ฐœ์˜ ๊ณ„์ˆ˜ = 4 x 3 = 12๊ฐœ

Level 2

  • DC ์„ฑ๋ถ„: 1๊ฐœ (RGB ๊ฐ๊ฐ 1๊ฐœ์”ฉ ์ด 3๊ฐœ)
  • Rest ์„ฑ๋ถ„: 8๊ฐœ (๊ฐ RGB ์ฑ„๋„๋งˆ๋‹ค 8๊ฐœ์˜ ์ถ”๊ฐ€ ๊ณ„์ˆ˜)
  • ์ด ๊ณ„์ˆ˜: 1๊ฐœ์˜ DC ์„ฑ๋ถ„ + 8๊ฐœ์˜ rest ์„ฑ๋ถ„ = 9๊ฐœ
    • RGB ๊ฐ๊ฐ์— ๋Œ€ํ•ด 9๊ฐœ์˜ ๊ณ„์ˆ˜ = 9 x 3 = 27๊ฐœ

Level 3

  • DC ์„ฑ๋ถ„: 1๊ฐœ (RGB ๊ฐ๊ฐ 1๊ฐœ์”ฉ ์ด 3๊ฐœ)
  • Rest ์„ฑ๋ถ„: 15๊ฐœ (๊ฐ RGB ์ฑ„๋„๋งˆ๋‹ค 15๊ฐœ์˜ ์ถ”๊ฐ€ ๊ณ„์ˆ˜)
  • ์ด ๊ณ„์ˆ˜: 1๊ฐœ์˜ DC ์„ฑ๋ถ„ + 15๊ฐœ์˜ rest ์„ฑ๋ถ„ = 16๊ฐœ
    • RGB ๊ฐ๊ฐ์— ๋Œ€ํ•ด 16๊ฐœ์˜ ๊ณ„์ˆ˜ = 16 x 3 = 48๊ฐœ

์š”์•ฝ

  • DC ์„ฑ๋ถ„: ๊ธฐ๋ณธ์ ์ธ SH ์„ฑ๋ถ„์œผ๋กœ, ๋ชจ๋“  ๋ฐฉํ–ฅ์—์„œ ์ผ์ •ํ•œ ๊ฐ’์„ ๊ฐ€์ง.
  • Rest ์„ฑ๋ถ„: DC ์„ฑ๋ถ„์„ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€ ๊ณ ์ฐจ SH ์„ฑ๋ถ„๋“ค.
  • SH Levels์™€ ๊ณ„์ˆ˜: ๊ฐ Level์— ๋”ฐ๋ผ DC ์„ฑ๋ถ„์„ ํฌํ•จํ•œ ์ด ๊ณ„์ˆ˜์˜ ์ˆ˜๊ฐ€ ๊ฒฐ์ •๋จ.
  • RGB ์ฑ„๋„๋ณ„ ๋…๋ฆฝ์  ๊ณ„์ˆ˜: ๊ฐ RGB ์ฑ„๋„์— ๋Œ€ํ•ด SH ๊ณ„์ˆ˜๋“ค์ด ๊ฐœ๋ณ„์ ์œผ๋กœ ์กด์žฌํ•˜์—ฌ ์ปฌ๋Ÿฌ ์ •๋ณด๋ฅผ ์ •ํ™•ํžˆ ํ‘œํ˜„ํ•จ.

open3d mesh๋ฅผ ๋ถˆ๋Ÿฌ์™€์„œ initializeํ•˜๋Š” ๋ฒ•์„ ์•Œ์•„๋ด…์‹œ๋‹ค.

  • surface_mesh_to_bind๋Š” o3d mesh์ด๊ณ 
  • n_points = surface_mesh_to_bind์˜ triangle ์ˆ˜ * triangle๋‹น gaussian ์ˆ˜ ์ž…๋‹ˆ๋‹ค.
  • ์ฆ‰, n_points๋Š” triangle๋“ค ์œ„์— ์ •์˜ํ•œ gaussian๋“ค์˜ ์ด ๊ฐœ์ˆ˜๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.
    @property
    def n_points(self):
        if not self.binded_to_surface_mesh:
            return len(self._points)
        else:
            return self._n_points

n_points์—์„œ spherical harmonics (sh)์˜ dc์™€ rest๋ฅผ ์ •์˜ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๋ด…์‹œ๋‹ค.

  • colors # shape (n_vertices, n_coords) ์ž…๋‹ˆ๋‹ค.
  • ์ด๋•Œ n_coords๋Š” vertices์— color ์ •๋ณด์ž…๋‹ˆ๋‹ค!! ์ •ํ™•ํžˆ ์ฃผ์„์„ ํ‘œํ˜„ํ•˜๋ฉด n_coords๊ฐ€ ์•„๋‹ˆ๋ผ rgb๋กœ ํ‘œ์‹œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ colors์˜ rgb 3์ฐจ์›๊ณผ, vertices์˜ xyz 3์ฐจ์› ์ •๋ณด๊ฐ€ ์ฐจ์›์ด ๊ฐ™์•„์„œ ์ฃผ์„๋„ n_coords๋กœ ํ†ต์ผํ•œ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.

    colors # shape (n_vertices, n_coords) <-- vertices์— ๋Œ€ํ•œ rgb 3์ฐจ์› color ๊ฐ’ = n_coords๋กœ ํ‘œํ˜„
    vertices # shape (n_vertices, n_coords) <-- vertices์— ๋Œ€ํ•œ xyz 3์ฐจ์› ์ขŒํ‘œ๊ฐ’ = n_coords๋กœ ํ‘œํ˜„
    
  • colors์—์„œ sh_coordinates_dc๋ฅผ ๋งŒ๋“œ๋Š” ๊ณผ์ •์„ ๋ด…์‹œ๋‹ค.
def RGB2SH(rgb):
    return (rgb - 0.5) / C0

def SH2RGB(sh):
    return sh * C0 + 0.5
  • sh_coordinates_dc = RGB2SH(colors).unsqueeze(dim=1) # shape (n_vertices, 1, n_coords)
  • ์ฃผ์„์„ ์ œ๋Œ€๋กœ ์“ฐ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
  • sh_coordinates_dc = RGB2SH(colors).unsqueeze(dim=1) # shape (n_vertices, 1, rgb)
  • ์ฆ‰, ๋งˆ์ง€๋ง‰ rgb 3๊ฐœ์˜ ์ฐจ์›์— ๋Œ€ํ•ด, sh_coordiantes_dc๋Š” ๊ฐ๊ฐ sh๋ฅผ 1๊ฐœ์”ฉ ๊ฐ€์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  • self._sh_coordinates_rest๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
  • self._sh_coordinates_rest = torch.zeros(n_points, sh_levels**2 - 1, 3) # shape (n_points, sh_levels**2 - 1, 3)
  • ์ฆ‰, ๋งˆ์ง€๋ง‰ rgb 3๊ฐœ์˜ ์ฐจ์›์— ๋Œ€ํ•ด, self._sh_coordinates_rest๋Š” ๊ฐ๊ฐ sh_levels**2-1 ๊ฐœ๋งŒํผ์„ ๊ฐ€์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ์ฆ‰, rgb ๊ฐ ์ฑ„๋„๋ณ„๋กœ sh ๊ณ„์ˆ˜๊ฐ€ ํ• ๋‹น๋ฉ๋‹ˆ๋‹ค.
        # Initialize color features
        self.sh_levels = sh_levels
        sh_coordinates_dc = RGB2SH(colors).unsqueeze(dim=1)
        self._sh_coordinates_dc = nn.Parameter(
            sh_coordinates_dc.to(self.nerfmodel.device),
            requires_grad=True and (not freeze_gaussians)
        ).to(self.nerfmodel.device)
        
        self._sh_coordinates_rest = nn.Parameter(
            torch.zeros(n_points, sh_levels**2 - 1, 3).to(self.nerfmodel.device),
            requires_grad=True and (not freeze_gaussians)
        ).to(self.nerfmodel.device)
# gaussian-splatting/scene/gaussian_model.py

class GaussianModel:

...

    @property
    def get_features(self):
        features_dc = self._features_dc
        features_rest = self._features_rest
        return torch.cat((features_dc, features_rest), dim=1)

3dgs์—์„œ _features_dc, _features_rest๋Š” spherical harmonics์˜ ๊ณ„์ˆ˜์— ํ•ด๋Œฑํ•ฉ๋‹ˆ๋‹ค.

  • ๋”ฐ๋ผ์„œ ์ด๋ฅผ SuGaR์—์„œ๋Š” ๋ณ€์ˆ˜์ด๋ฆ„์„ ๊ทธ๋ƒฅ _sh_coordinates_dc[...], _sh_coordinates_rest[...]๋กœ ๋ถˆ๋Ÿฌ์™€์„œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
# SuGaR/sugar_extractors/coarse_mesh.py

def extract_mesh_from_coarse_sugar(args):

...


            sugar._sh_coordinates_dc[...] = nerfmodel.gaussians._features_dc.detach()
            sugar._sh_coordinates_rest[...] = nerfmodel.gaussians._features_rest.detach()
    

3dgs์—์„œ _features_dc๋Š” RGB์— ํ•ด๋‹นํ•˜๋Š” sh(spherical harmonics)๊ณ„์ˆ˜์ž…๋‹ˆ๋‹ค.

  • ๋”ฐ๋ผ์„œ ์ด๋ฅผ SuGaR์—์„œ๋Š” SH2RGB ํ•จ์ˆ˜๋กœ sh๋ฅผ rgb๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ colors ๋ณ€์ˆ˜์— ํ• ๋‹นํ•ฉ๋‹ˆ๋‹ค.
# SuGaR/sugar_extractors/coarse_mesh.py

def extract_mesh_from_coarse_sugar(args):

...

        CONSOLE.print(f"\nLoading the coarse SuGaR model from path {sugar_checkpoint_path}...")
        checkpoint = torch.load(sugar_checkpoint_path, map_location=nerfmodel.device)
        colors = SH2RGB(checkpoint['state_dict']['_sh_coordinates_dc'][:, 0, :])
        sugar = SuGaR(
            nerfmodel=nerfmodel,
            points=checkpoint['state_dict']['_points'],
            colors=colors,
            initialize=True,
            sh_levels=nerfmodel.gaussians.active_sh_degree+1,
            keep_track_of_knn=True,
            knn_to_track=16,
            beta_mode='average',  # 'learnable', 'average', 'weighted_average'
            primitive_types='diamond',  # 'diamond', 'square'
            surface_mesh_to_bind=None,  # Open3D mesh
            )
        sugar.load_state_dict(checkpoint['state_dict'])
    sugar.eval()
  • ๊ทธ๋ฆฌ๊ณ  ์ด colors๋Š” ๋ถˆ๋Ÿฌ์™€์„œ RGB2SHํ•จ์ˆ˜๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  unsqueezeํ•˜์—ฌ ์ฐจ์›์„ ๋งž์ถฐ ํ•™์Šต์˜ initialize๋กœ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

SH ๊ณ„์ˆ˜๋ฅผ RGB๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ์ •๋ณด

SH ๊ณ„์ˆ˜๋ฅผ RGB๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋‹ค์Œ์˜ ์ •๋ณด๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค: sh์˜ deg, sh์˜ ๊ณ„์ˆ˜, camera center์—์„œ point๊นŒ์ง€์˜ direction.

  • SuGaR์—์„œ๋Š” eval_sh(deg, sh, dirs)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŠน์ • ์นด๋ฉ”๋ผ ๋ฐฉํ–ฅ์—์„œ ํฌ์ธํŠธ๊นŒ์ง€์˜ ๋ Œ๋”๋ง ๋ฐฉํ–ฅ์— ๋”ฐ๋ผ SH ๊ณ„์ˆ˜๋ฅผ ํ•˜๋‚˜์˜ RGB ์ปฌ๋Ÿฌ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • RGB 3๊ฐœ์˜ ์ฑ„๋„๋งˆ๋‹ค SH ๊ณ„์ˆ˜๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
  • DC๋Š” ์กฐ๋ช…์˜ ์ „์ฒด์ ์ธ ๋ฐ๊ธฐ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ƒ์ˆ˜์ž…๋‹ˆ๋‹ค.

SH์˜ DC (0๋ฒˆ์งธ Band)

  • sh[..., 0]์€ 0๋ฒˆ์งธ deg์— ํ•ด๋‹นํ•˜๋Š” SH์˜ DC ๊ณ„์ˆ˜๋กœ, ๋ชจ๋“  RGB ์ฑ„๋„์— ๋Œ€ํ•ด ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

SH์˜ Rest (1๋ฒˆ์งธ, 2๋ฒˆ์งธ, 3๋ฒˆ์งธ, โ€ฆ)

  • sh[..., 1]๋ถ€ํ„ฐ sh[..., 24]๊นŒ์ง€์˜ ๊ณ„์ˆ˜๋Š” ๊ฐ๋„๊ฐ€ ์žˆ๋Š” ์กฐ๋ช… ๊ตฌ์„ฑ ์š”์†Œ์ž…๋‹ˆ๋‹ค.
  • SH ๊ณ„์ˆ˜๋Š” ๊ฐ RGB ์ฑ„๋„์— ๋Œ€ํ•ด ๊ฐœ๋ณ„์ ์œผ๋กœ ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, R, G, B ๊ฐ ์ฑ„๋„์€ ์„œ๋กœ ๋‹ค๋ฅธ SH ๊ณ„์ˆ˜๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์‹œ๋กœ ์„ค๋ช…

์˜ˆ๋ฅผ ๋“ค์–ด, RGB ์ฑ„๋„ ๊ฐ๊ฐ์— ๋Œ€ํ•ด SH ๊ณ„์ˆ˜๊ฐ€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

R ์ฑ„๋„

sh[..., 0] = 0.5
sh[..., 1] = 0.1
sh[..., 2] = 0.3
...

G ์ฑ„๋„

sh[..., 0] = 0.4
sh[..., 1] = 0.2
sh[..., 2] = 0.6
...

B ์ฑ„๋„

sh[..., 0] = 0.7
sh[..., 1] = 0.3
sh[..., 2] = 0.5
...

๊ฐ ๊ณ„์ˆ˜๋Š” ๊ฐ RGB ์ฑ„๋„์— ๋Œ€ํ•ด ๊ฐœ๋ณ„์ ์œผ๋กœ ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค:

  • sh[โ€ฆ, 1]์€ 1๋ฒˆ์งธ deg์— ํ•ด๋‹นํ•˜๋Š” SH์˜ ๊ณ„์ˆ˜๋กœ, ๊ฐ RGB ์ฑ„๋„์— ๋Œ€ํ•ด ๋…๋ฆฝ์ ์œผ๋กœ ๊ณ„์‚ฐ๋œ ๊ฐ’์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
  • sh[โ€ฆ, 2]์€ 2๋ฒˆ์งธ deg์— ํ•ด๋‹นํ•˜๋Š” SH์˜ ๊ณ„์ˆ˜๋กœ, ๊ฐ RGB ์ฑ„๋„์— ๋Œ€ํ•ด ๋…๋ฆฝ์ ์œผ๋กœ ๊ณ„์‚ฐ๋œ ๊ฐ’์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
  • sh[โ€ฆ, 3]์€ 3๋ฒˆ์งธ deg์— ํ•ด๋‹นํ•˜๋Š” SH์˜ ๊ณ„์ˆ˜๋กœ, ๊ฐ RGB ์ฑ„๋„์— ๋Œ€ํ•ด ๋…๋ฆฝ์ ์œผ๋กœ ๊ณ„์‚ฐ๋œ ๊ฐ’์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
  • โ€ฆ
  • sh[โ€ฆ, 24]์€ 24๋ฒˆ์งธ deg์— ํ•ด๋‹นํ•˜๋Š” SH์˜ ๊ณ„์ˆ˜๋กœ, ๊ฐ RGB ์ฑ„๋„์— ๋Œ€ํ•ด ๋…๋ฆฝ์ ์œผ๋กœ ๊ณ„์‚ฐ๋œ ๊ฐ’์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค.

์š”์•ฝ

  • SH ๊ณ„์ˆ˜๋Š” ๊ฐ RGB ์ฑ„๋„์— ๋Œ€ํ•ด ๊ฐœ๋ณ„์ ์œผ๋กœ ์กด์žฌํ•˜๊ณ , ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค.
  • ์ด๋Š” ๋™์ผํ•œ ๊ฐ’์„ ๊ฐ€์ง„๋‹ค๋Š” ์˜๋ฏธ๊ฐ€ ์•„๋‹ˆ๋ฉฐ, ๊ฐ ์ฑ„๋„๋ณ„๋กœ ๋‹ค๋ฅธ ๊ฐ’์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๊ฐ ์ฑ„๋„์˜ SH ๊ณ„์ˆ˜๋Š” ๊ฐœ๋ณ„์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๋ฉฐ, eval_sh ํ•จ์ˆ˜๋Š” ๊ฐ ์ฑ„๋„์— ๋Œ€ํ•ด ๋…๋ฆฝ์ ์œผ๋กœ SH ๊ณ„์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ RGB ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.
# SuGaR/sugar_scene/sugar_model.py

    def get_points_rgb(
        self,
        positions:torch.Tensor=None,
        camera_centers:torch.Tensor=None,
        directions:torch.Tensor=None,
        sh_levels:int=None,
        sh_coordinates:torch.Tensor=None,
        ):
        """Returns the RGB color of the points for the given camera pose.

        Args:
            positions (torch.Tensor, optional): Shape (n_pts, 3). Defaults to None.
            camera_centers (torch.Tensor, optional): Shape (n_pts, 3) or (1, 3). Defaults to None.
            directions (torch.Tensor, optional): _description_. Defaults to None.

        Raises:
            ValueError: _description_

        Returns:
            _type_: _description_
        """
            
        if positions is None:
            positions = self.points

        if camera_centers is not None:
            render_directions = torch.nn.functional.normalize(positions - camera_centers, dim=-1)
        elif directions is not None:
            render_directions = directions
        else:
            raise ValueError("Either camera_centers or directions must be provided.")

        if sh_coordinates is None:
            sh_coordinates = self.sh_coordinates
            
        if sh_levels is None:
            sh_coordinates = sh_coordinates
        else:
            sh_coordinates = sh_coordinates[:, :sh_levels**2]

        shs_view = sh_coordinates.transpose(-1, -2).view(-1, 3, sh_levels**2)
        sh2rgb = eval_sh(sh_levels-1, shs_view, render_directions)
        colors = torch.clamp_min(sh2rgb + 0.5, 0.0).view(-1, 3)
        
        return colors
# SuGaR/sugar_utils/spherical_harmonics.py

def eval_sh(deg, sh, dirs):
    """
    Evaluate spherical harmonics at unit directions
    using hardcoded SH polynomials.
    Works with torch/np/jnp.
    ... Can be 0 or more batch dimensions.
    Args:
        deg: int SH deg. Currently, 0-3 supported
        sh: jnp.ndarray SH coeffs [..., C, (deg + 1) ** 2]
        dirs: jnp.ndarray unit directions [..., 3]
    Returns:
        [..., C]
    """
    assert deg <= 4 and deg >= 0
    coeff = (deg + 1) ** 2
    assert sh.shape[-1] >= coeff

    result = C0 * sh[..., 0]
    if deg > 0:
        x, y, z = dirs[..., 0:1], dirs[..., 1:2], dirs[..., 2:3]
        result = (result -
                C1 * y * sh[..., 1] +
                C1 * z * sh[..., 2] -
                C1 * x * sh[..., 3])

        if deg > 1:
            xx, yy, zz = x * x, y * y, z * z
            xy, yz, xz = x * y, y * z, x * z
            result = (result +
                    C2[0] * xy * sh[..., 4] +
                    C2[1] * yz * sh[..., 5] +
                    C2[2] * (2.0 * zz - xx - yy) * sh[..., 6] +
                    C2[3] * xz * sh[..., 7] +
                    C2[4] * (xx - yy) * sh[..., 8])

            if deg > 2:
                result = (result +
                C3[0] * y * (3 * xx - yy) * sh[..., 9] +
                C3[1] * xy * z * sh[..., 10] +
                C3[2] * y * (4 * zz - xx - yy)* sh[..., 11] +
                C3[3] * z * (2 * zz - 3 * xx - 3 * yy) * sh[..., 12] +
                C3[4] * x * (4 * zz - xx - yy) * sh[..., 13] +
                C3[5] * z * (xx - yy) * sh[..., 14] +
                C3[6] * x * (xx - 3 * yy) * sh[..., 15])

                if deg > 3:
                    result = (result + C4[0] * xy * (xx - yy) * sh[..., 16] +
                            C4[1] * yz * (3 * xx - yy) * sh[..., 17] +
                            C4[2] * xy * (7 * zz - 1) * sh[..., 18] +
                            C4[3] * yz * (7 * zz - 3) * sh[..., 19] +
                            C4[4] * (zz * (35 * zz - 30) + 3) * sh[..., 20] +
                            C4[5] * xz * (7 * zz - 3) * sh[..., 21] +
                            C4[6] * (xx - yy) * (7 * zz - 1) * sh[..., 22] +
                            C4[7] * xz * (xx - 3 * yy) * sh[..., 23] +
                            C4[8] * (xx * (xx - 3 * yy) - yy * (3 * xx - yy)) * sh[..., 24])
    return result
C0 = 0.28209479177387814
C1 = 0.4886025119029199
C2 = [
    1.0925484305920792,
    -1.0925484305920792,
    0.31539156525252005,
    -1.0925484305920792,
    0.5462742152960396
]
C3 = [
    -0.5900435899266435,
    2.890611442640554,
    -0.4570457994644658,
    0.3731763325901154,
    -0.4570457994644658,
    1.445305721320277,
    -0.5900435899266435
]
C4 = [
    2.5033429417967046,
    -1.7701307697799304,
    0.9461746957575601,
    -0.6690465435572892,
    0.10578554691520431,
    -0.6690465435572892,
    0.47308734787878004,
    -1.7701307697799304,
    0.6258357354491761,
]

SH2RGB์™€ RGB2SH๋Š” sh์˜ 0๋ฒˆ์งธ band์ธ dc ์„ฑ๋ถ„์—์„œ๋งŒ ์‚ฌ์šฉ๋˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

def RGB2SH(rgb):
    return (rgb - 0.5) / C0

def SH2RGB(sh):
    return sh * C0 + 0.5

RGB2SH ํ•จ์ˆ˜

sh_coordinates_dc = RGB2SH(colors).unsqueeze(dim=1)
  • RGB2SH ํ•จ์ˆ˜๋Š” RGB ๊ฐ’์„ ๋ฐ›์•„์„œ SH ๊ณ„์ˆ˜๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ณ€ํ™˜๋œ SH ๊ณ„์ˆ˜๋Š” DC ์„ฑ๋ถ„์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
  • RGB2SH(colors): colors๋Š” RGB ๊ฐ’์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ, DC ์„ฑ๋ถ„์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
  • ๋”ฐ๋ผ์„œ, ์ด ๋ณ€ํ™˜์€ RGB ๊ฐ’์„ DC ์„ฑ๋ถ„์˜ SH ๊ณ„์ˆ˜๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • .unsqueeze(dim=1): ์ฐจ์›์„ ์ถ”๊ฐ€ํ•˜์—ฌ SH ๊ณ„์ˆ˜์˜ DC ์„ฑ๋ถ„์„ 3D ํ…์„œ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

SH2RGB ํ•จ์ˆ˜

    refined_sugar = SuGaR(
        nerfmodel=nerfmodel,
        points=checkpoint['state_dict']['_points'],
        colors=SH2RGB(checkpoint['state_dict']['_sh_coordinates_dc'][:, 0, :]), 
        initialize=False,
        sh_levels=nerfmodel.gaussians.active_sh_degree+1,
        keep_track_of_knn=False,
        knn_to_track=0,
        beta_mode='average',
        surface_mesh_to_bind=o3d_mesh,
        n_gaussians_per_surface_triangle=n_gaussians_per_surface_triangle,
        )
    refined_sugar.load_state_dict(checkpoint['state_dict'])
textures_uv = TexturesUV(
            maps=SH2RGB(self.texture_features[..., 0, :][None]), #texture_img[None]), 
            verts_uvs=self.verts_uv[None],
            faces_uvs=self.faces_uv[None],
            sampling_mode='nearest',
            )
  • SH2RGB ํ•จ์ˆ˜๋Š” SH ๊ณ„์ˆ˜๋ฅผ ๋ฐ›์•„์„œ RGB ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ์ด ํ•จ์ˆ˜๋Š” DC ์„ฑ๋ถ„์— ํ•ด๋‹นํ•˜๋Š” SH ๊ณ„์ˆ˜๋ฅผ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • self.texture_features[..., 0, :]์€ SH ๊ณ„์ˆ˜์˜ ์ฒซ ๋ฒˆ์งธ ์„ฑ๋ถ„(DC ์„ฑ๋ถ„)์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.
  • [None]์„ ์‚ฌ์šฉํ•˜์—ฌ ์ฐจ์›์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  • SH2RGB ํ•จ์ˆ˜๋Š” ์ด DC ์„ฑ๋ถ„์„ RGB ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

๊ฒฐ๋ก 

RGB2SH์™€ SH2RGB๋Š” ์ฃผ์–ด์ง„ ์ฝ”๋“œ์—์„œ SH ๊ณ„์ˆ˜์˜ DC ์„ฑ๋ถ„์— ํ•ด๋‹นํ•˜๋Š” ๊ฐ’์„ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, ์ด ๋‘ ํ•จ์ˆ˜๋Š” DC ์„ฑ๋ถ„์—๋งŒ ํ•ด๋‹นํ•œ๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ตœ์ข… ์š”์•ฝ

  • RGB2SH ํ•จ์ˆ˜๋Š” DC ์„ฑ๋ถ„์— ํ•ด๋‹นํ•˜๋Š” RGB ๊ฐ’์„ SH ๊ณ„์ˆ˜๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • SH2RGB ํ•จ์ˆ˜๋Š” DC ์„ฑ๋ถ„์— ํ•ด๋‹นํ•˜๋Š” SH ๊ณ„์ˆ˜๋ฅผ RGB ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ์ฃผ์–ด์ง„ ์ฝ”๋“œ์—์„œ๋Š” ์ด ๋‘ ํ•จ์ˆ˜๊ฐ€ SH ๊ณ„์ˆ˜์˜ DC ์„ฑ๋ถ„์— ์ฃผ๋กœ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

[3D CV ์—ฐ๊ตฌ] 3DGS input & output .ply properties & Meshlab Vert & Spherical Harmonics (SH) & Mesh

Leave a comment