File size: 10,099 Bytes
23e73f9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | 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'))
# 强隔离:清空残留路径并确保 GS-Pull 绝对优先
if GS_ROOT not in sys.path:
sys.path.insert(0, GS_ROOT)
# GS-Pull's gaussian_splatting is a vanilla 3DGS copy — its scene/__init__.py
# does `from utils.system_utils import ...` expecting the gaussian_splatting/
# subdir to be on sys.path.
_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)
# 防呆逻辑:兼容某些变体改名为 shfeature_lr 的情况
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)
# INJECTED_RES_FIX begin
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)
# INJECTED_RES_FIX end
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")
|