22Densification from "3D Gaussian Splatting as Markov Chain Monte Carlo"
33(as implemented in LichtFeld Studio): instead of heuristic clone/split/prune
44& opacity resets, the number of Gaussians only grows (up to `max_cap`) and
5- dead Gaussians (opacity ≤ `min_opacity`) are relocated onto alive ones with
6- an opacity/scale correction (Eq. 9) that preserves the render.
5+ dead Gaussians (opacity ≤ `min_opacity` or scale above `max_scale`·extent)
6+ are relocated onto alive ones with an opacity/scale correction (Eq. 9) that
7+ preserves the render.
78Position noise scaled by each Gaussian's covariance & opacity keeps the chain
89exploring, while opacity & scale L1 regularization (see [`regularization_loss`](@ref))
910provides the pressure that produces dead Gaussians to recycle.
@@ -14,6 +15,7 @@ mutable struct MCMCStrategy <: AbstractStrategy
1415
1516 max_cap:: Int
1617 min_opacity:: Float32
18+ max_scale:: Float32 # Relative to the scene extent.
1719 start_refine:: Int
1820 stop_refine:: Int
1921 refine_every:: Int
2729function MCMCStrategy (;
2830 max_cap:: Int = 2_000_000 ,
2931 min_opacity:: Float32 = 0.005f0 ,
32+ max_scale:: Float32 = 0.1f0 ,
3033 start_refine:: Int = 500 ,
3134 stop_refine:: Int = 25_000 ,
3235 refine_every:: Int = 100 ,
@@ -38,7 +41,7 @@ function MCMCStrategy(;
3841)
3942 MCMCStrategy (
4043 mcmc_binom_coefficients (n_max),
41- max_cap, min_opacity, start_refine, stop_refine, refine_every,
44+ max_cap, min_opacity, max_scale, start_refine, stop_refine, refine_every,
4245 grow_factor, noise_lr, opacity_reg, scale_reg, n_max)
4346end
4447
@@ -70,22 +73,30 @@ function post_train_step!(
7073 step % strategy. refine_every == 0
7174 if refining
7275 GPUArrays. unsafe_free! (cache)
73- relocate_gaussians! (strategy, gs, optimizers)
76+ relocate_gaussians! (strategy, gs, optimizers; extent )
7477 add_gaussians! (strategy, gs, optimizers)
7578 end
76- inject_noise! (strategy, gs, optimizers. points. lr)
79+ inject_noise! (strategy, gs, optimizers. points. lr; extent )
7780 return
7881end
7982
8083"""
81- Move dead Gaussians (opacity ≤ `min_opacity`) onto alive ones sampled with
82- probability ∝ opacity, correcting the target's opacity/scale (Eq. 9) so the
83- render is preserved. Adam moments of every touched Gaussian are reset.
84+ Move dead Gaussians (opacity ≤ `min_opacity` or scale above `max_scale`·extent)
85+ onto alive ones sampled with probability ∝ opacity, correcting the target's
86+ opacity/scale (Eq. 9) so the render is preserved.
87+ Adam moments of every touched Gaussian are reset.
8488"""
85- function relocate_gaussians! (strategy:: MCMCStrategy , gs:: GaussianModel , optimizers)
89+ function relocate_gaussians! (
90+ strategy:: MCMCStrategy , gs:: GaussianModel , optimizers; extent:: Float32 ,
91+ )
8692 o = Array (reshape (NU. sigmoid .(gs. opacities), :))
87- dead = findall (≤ (strategy. min_opacity), o)
88- alive = findall (> (strategy. min_opacity), o)
93+ # Oversized Gaussians join the dead set: MCMC has no other pruning
94+ # mechanism, so without this runaways survive until their opacity dies.
95+ log_max_scale = log (strategy. max_scale * extent)
96+ s_max = Array (vec (maximum (gs. scales; dims= 1 )))
97+ is_dead = @. (o ≤ strategy. min_opacity) | (s_max > log_max_scale)
98+ dead = findall (is_dead)
99+ alive = findall (! , is_dead)
89100 (isempty (dead) || isempty (alive)) && return 0
90101
91102 ids = multinomial_sample (o[alive], length (dead))
@@ -228,22 +239,29 @@ end
228239"""
229240Perturb positions with noise `∝ Σ·ξ`, gated to near-dead Gaussians by a steep
230241opacity sigmoid & scaled by the (decaying) position learning rate.
242+ Each kick is capped at a fraction of the scene extent: `‖Σ·ξ‖ ∝ scale²`
243+ would otherwise teleport big near-dead Gaussians across the scene every step.
231244"""
232- function inject_noise! (strategy:: MCMCStrategy , gs:: GaussianModel , points_lr:: Float32 )
245+ function inject_noise! (
246+ strategy:: MCMCStrategy , gs:: GaussianModel , points_lr:: Float32 ; extent:: Float32 ,
247+ )
233248 n = length (gs)
234249 n == 0 && return
235250 isotropic = size (gs. scales, 1 ) == 1
251+ # Half the relocation size threshold: kicks stay small relative
252+ # to the biggest Gaussian allowed to survive relocation.
253+ max_kick = 0.5f0 * strategy. max_scale * extent
236254 _inject_noise! (get_backend (gs))(
237255 reinterpret (SVector{3 , Float32}, gs. points),
238256 gs. opacities,
239257 isotropic ? gs. scales : reinterpret (SVector{3 , Float32}, gs. scales),
240258 reinterpret (SVector{4 , Float32}, gs. rotations),
241- points_lr * strategy. noise_lr; ndrange= n)
259+ points_lr * strategy. noise_lr, max_kick ; ndrange= n)
242260 return
243261end
244262
245263@kernel cpu= false inbounds= true function _inject_noise! (
246- points, opacities, scales, rotations, lr:: Float32 ,
264+ points, opacities, scales, rotations, lr:: Float32 , max_kick :: Float32 ,
247265)
248266 i = @index (Global)
249267 ξ = SVector {3, Float32} (randn (Float32), randn (Float32), randn (Float32))
257275 op = NU. sigmoid (opacities[i])
258276 # Cap the exponent: for opaque Gaussians `exp` overflows to `Inf`.
259277 factor = lr / (1f0 + exp (min (100f0 * op - 0.5f0 , 80f0 )))
260- points[i] = points[i] .+ factor .* Σξ
278+ Δ = factor .* Σξ
279+ l = norm (Δ)
280+ l > max_kick && (Δ = Δ .* (max_kick / l))
281+ points[i] = points[i] .+ Δ
261282end
0 commit comments