| import os |
| import sys |
| import random |
| import torch |
| import numpy as np |
| import torch.nn.functional as F |
| from argparse import ArgumentParser |
|
|
| from core.registry import register_method |
| from core.base_method import BaseMethod |
|
|
| GS_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../GS-Pull')) |
| |
| if GS_ROOT not in sys.path: |
| sys.path.insert(0, GS_ROOT) |
| |
| |
| |
| _GS_INNER = os.path.join(GS_ROOT, 'gaussian_splatting') |
| if _GS_INNER not in sys.path: |
| sys.path.insert(0, _GS_INNER) |
|
|
| from gaussian_splatting.scene import Scene, GaussianModel |
| from gaussian_splatting.arguments import ModelParams, PipelineParams, OptimizationParams as GSOptParams |
| from gaussian_splatting.gaussian_renderer import render as vanilla_render |
|
|
| from sugar_scene.sugar_model import SuGaR |
| from sugar_scene.sugar_optimizer import OptimizationParams as SuGaROptParams, SuGaROptimizer |
| from sugar_scene.sugar_densifier import SuGaRDensifier |
| from sugar_utils.loss_utils import ssim, l1_loss |
| from np_utils.train import Runner |
|
|
| @register_method("gspull") |
| class GSPullWrapper(BaseMethod): |
| def __init__(self, dataset_config, hyperparams): |
| self.source_path = dataset_config["source_path"] |
| self.model_path = dataset_config["model_path"] |
| self.resolution = dataset_config.get("resolution", 1) |
| self.track_decoupling = hyperparams.get("track_decoupling", False) |
|
|
| parser = ArgumentParser() |
| lp = ModelParams(parser) |
| op = GSOptParams(parser) |
| pp = PipelineParams(parser) |
| args = parser.parse_args([]) |
| args.source_path = self.source_path |
| args.model_path = self.model_path |
| args.resolution = self.resolution |
| args.eval = True |
| |
| self.dataset = lp.extract(args) |
| self.gs_opt = op.extract(args) |
| self.pipe = pp.extract(args) |
| |
| |
| if not hasattr(self.gs_opt, 'shfeature_lr') and hasattr(self.gs_opt, 'feature_lr'): |
| setattr(self.gs_opt, 'shfeature_lr', getattr(self.gs_opt, 'feature_lr')) |
| |
| self.gaussians = GaussianModel(self.dataset.sh_degree) |
| |
| import sys as _sys |
| _scene, _explicit_res = None, None |
| for _i, _a in enumerate(_sys.argv[:-1]): |
| _v = _sys.argv[_i + 1] |
| if _a == "--scene": _scene = _v |
| elif _a == "--source_path": _scene = _v.rstrip("/").split("/")[-1] |
| elif _a == "--resolution": |
| try: _explicit_res = int(_v) |
| except: pass |
| _OUTDOOR_360 = {"bicycle", "flowers", "garden", "stump", "treehill"} |
| if _explicit_res is not None and _explicit_res > 0: |
| _res = _explicit_res |
| elif _scene is not None: |
| _res = 4 if _scene in _OUTDOOR_360 else 2 |
| else: |
| _res = None |
| try: |
| if _res is not None: |
| self.dataset.resolution = _res |
| print("[res-fix] scene=%s explicit=%s -> res=%s (%s)" % (_scene, _explicit_res, _res, __file__)) |
| except Exception as _e: |
| print("[res-fix] FAILED:", _e) |
| |
| self.scene = Scene(self.dataset, self.gaussians) |
| self.gaussians.training_setup(self.gs_opt) |
| |
| self.bg_color = torch.tensor([1, 1, 1] if self.dataset.white_background else [0, 0, 0], dtype=torch.float32, device="cuda") |
| self.train_cameras = self.scene.getTrainCameras().copy() |
| self.viewpoint_stack = self.train_cameras.copy() |
| |
| self.sugar = None |
| self.optimizer = None |
| self.densifier = None |
| self._iter = 0 |
| self.last_n_gaussians = len(self.gaussians.get_xyz) |
|
|
| def _init_sugar_stage(self): |
| self.sugar = SuGaR( |
| nerfmodel=None, |
| points=self.gaussians.get_xyz.detach(), |
| colors=torch.zeros_like(self.gaussians.get_xyz), |
| initialize=False, |
| sh_levels=self.gaussians.active_sh_degree + 1, |
| learnable_positions=True, |
| triangle_scale=1.0, |
| keep_track_of_knn=True, |
| knn_to_track=16, |
| beta_mode="average", |
| freeze_gaussians=False, |
| surface_mesh_to_bind=None, |
| ) |
| with torch.no_grad(): |
| self.sugar._points.copy_(self.gaussians._xyz) |
| self.sugar._scales.copy_(self.gaussians._scaling) |
| self.sugar._quaternions.copy_(self.gaussians._rotation) |
| self.sugar.all_densities.copy_(self.gaussians._opacity) |
| self.sugar._sh_coordinates_dc.copy_(self.gaussians._features_dc) |
| self.sugar._sh_coordinates_rest.copy_(self.gaussians._features_rest) |
|
|
| self.sugar.part_num = 1 |
| self.sugar.neus = Runner(self.model_path, None, part_num=self.sugar.part_num) |
| |
| spatial_lr_scale = self.scene.cameras_extent |
| opt_p = SuGaROptParams( |
| iterations=15000, position_lr_init=0.00016, position_lr_final=0.0000016, |
| position_lr_max_steps=30000, scaling_lr=0.005, opacity_lr=0.05, |
| feature_lr=0.0025, rotation_lr=0.001 |
| ) |
| self.optimizer = SuGaROptimizer(self.sugar, opt_p, spatial_lr_scale=spatial_lr_scale) |
| self.densifier = SuGaRDensifier(self.sugar, self.optimizer, 0.0002, 0.005, 20, spatial_lr_scale, 0.01) |
|
|
| def train_iteration(self, step): |
| self._iter += 1 |
| metrics = {} |
| histograms = {} |
|
|
| if self._iter <= 7000: |
| if not self.viewpoint_stack: |
| self.viewpoint_stack = self.train_cameras.copy() |
| viewpoint_cam = self.viewpoint_stack.pop(random.randint(0, len(self.viewpoint_stack) - 1)) |
| |
| render_pkg = vanilla_render(viewpoint_cam, self.gaussians, self.pipe, self.bg_color) |
| image, viewspace_point_tensor, visibility_filter, radii = render_pkg["render"], render_pkg["viewspace_points"], render_pkg["visibility_filter"], render_pkg["radii"] |
| |
| gt_image = viewpoint_cam.original_image.cuda() |
| loss = 0.8 * l1_loss(image, gt_image) + 0.2 * (1.0 - ssim(image, gt_image)) |
| loss.backward() |
|
|
| with torch.no_grad(): |
| self.gaussians.max_radii2D[visibility_filter] = torch.max(self.gaussians.max_radii2D[visibility_filter], radii[visibility_filter]) |
| self.gaussians.add_densification_stats(viewspace_point_tensor, visibility_filter) |
| if self._iter > 500 and self._iter % 100 == 0: |
| self.gaussians.densify_and_prune(self.gs_opt.densify_grad_threshold, 0.005, self.scene.cameras_extent, 20) |
| if self._iter % 3000 == 0: |
| self.gaussians.reset_opacity() |
| self.gaussians.optimizer.step() |
| self.gaussians.optimizer.zero_grad(set_to_none=True) |
| |
| num_gaussians = len(self.gaussians.get_xyz) |
| metrics = { |
| "loss": float(loss), "num_gaussians": int(num_gaussians), |
| "peak_vram_GB": float(torch.cuda.max_memory_allocated() / (1024 ** 3)) |
| } |
| self.last_n_gaussians = num_gaussians |
| return metrics, histograms |
|
|
| if self._iter == 7001: |
| self._init_sugar_stage() |
|
|
| is_pulling = self._iter > 9000 |
| if self._iter == 9001: |
| prune_mask = (self.sugar.strengths < 0.5).squeeze() |
| self.densifier.prune_points(prune_mask) |
| self.sugar.reset_neighbors() |
| self.sugar.neus.reset_datasets(self.model_path, self.sugar.points.detach().cpu().numpy(), iteration=9000, scene_name="scene") |
|
|
| if not self.viewpoint_stack: |
| self.viewpoint_stack = self.train_cameras.copy() |
| cam = self.viewpoint_stack.pop(random.randint(0, len(self.viewpoint_stack) - 1)) |
| |
| outputs = self.sugar.render_image_gaussian_rasterizer( |
| camera_indices=self.train_cameras.index(cam), bg_color=self.bg_color, |
| sh_deg=self.sugar.sh_levels-1, compute_covariance_in_rasterizer=True, return_2d_radii=True |
| ) |
| |
| pred_rgb = outputs['image'].view(-1, cam.image_height, cam.image_width, 3).transpose(-1, -2).transpose(-2, -3) |
| gt_rgb = cam.original_image.cuda().unsqueeze(0) |
| loss = 0.8 * l1_loss(pred_rgb, gt_rgb) + 0.2 * (1.0 - ssim(pred_rgb, gt_rgb)) |
| loss.backward() |
| |
| self.optimizer.step() |
| self.optimizer.zero_grad(set_to_none=True) |
| |
| metrics = {"loss": float(loss), "num_gaussians": len(self.sugar.points)} |
| return metrics, {} |
|
|
| def render(self, camera): |
| with torch.no_grad(): |
| if self._iter <= 7000: |
| return {"image": vanilla_render(camera, self.gaussians, self.pipe, self.bg_color)["render"], "depth": None} |
| idx = 0 |
| for i, c in enumerate(self.train_cameras): |
| if c.uid == camera.uid: idx = i; break |
| outputs = self.sugar.render_image_gaussian_rasterizer(camera_indices=idx) |
| return {"image": outputs["image"], "depth": None} |
|
|
| def save(self, save_dir, step): |
| if self.sugar: |
| self.sugar.save_model(path=os.path.join(save_dir, f"{step}.pt"), iteration=step) |
| else: |
| self.gaussians.save_ply(os.path.join(save_dir, "point_cloud", f"iteration_{step}", "point_cloud.ply")) |
|
|
| def load(self, model_path, iteration): |
| pass |
|
|
| def get_spatial_centers(self): |
| return self.sugar.points if self.sugar else self.gaussians.get_xyz |
|
|
| def compute_physical_metrics(self, cameras=None): |
| return {"num_gaussians": float(len(self.get_spatial_centers()))} |
|
|
| def evaluate_spatial_field(self, query_points: torch.Tensor, cameras=None) -> torch.Tensor: |
| return torch.zeros(len(query_points), device="cuda") |
|
|