gsplat 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +236 -0
- data/docs/ACCEPTANCE.md +60 -0
- data/docs/BENCHMARKS.md +79 -0
- data/docs/DECISIONS.md +52 -0
- data/docs/MIGRATION.md +117 -0
- data/docs/PROFILE.md +75 -0
- data/docs/PROGRESS.md +199 -0
- data/docs/decisions/0000-template.md +17 -0
- data/docs/decisions/0001-pin-golden-reference-and-cpu-validation.md +27 -0
- data/docs/decisions/0002-retain-ruby-fallbacks-for-native-backend.md +26 -0
- data/docs/decisions/0003-share-projection-for-distorted-cameras.md +26 -0
- data/docs/decisions/0004-use-portable-world-space-reference-paths.md +26 -0
- data/docs/decisions/0005-reuse-ewa-core-for-2dgs.md +24 -0
- data/docs/decisions/0006-keep-eval3d-as-portable-reference.md +25 -0
- data/docs/decisions/0007-share-compositor-semantics-for-contribution-indices.md +25 -0
- data/examples/data/README.md +14 -0
- data/examples/data/colmap/images/view_000.png +0 -0
- data/examples/data/colmap/images/view_001.png +0 -0
- data/examples/data/colmap/images/view_002.png +0 -0
- data/examples/data/colmap/sparse/0/cameras.txt +2 -0
- data/examples/data/colmap/sparse/0/images.txt +6 -0
- data/examples/data/colmap/sparse/0/points3D.txt +17 -0
- data/examples/data/splats.ply +0 -0
- data/examples/fit_image.rb +35 -0
- data/examples/generate_sample_data.rb +147 -0
- data/examples/render_path.rb +117 -0
- data/examples/simple_trainer.rb +76 -0
- data/ext/gsplat_native/common.h +54 -0
- data/ext/gsplat_native/extconf.rb +39 -0
- data/ext/gsplat_native/gsplat_native.c +60 -0
- data/ext/gsplat_native/intersections.c +211 -0
- data/ext/gsplat_native/projection.c +199 -0
- data/ext/gsplat_native/raster_backward.c +129 -0
- data/ext/gsplat_native/raster_backward_bridge.c +69 -0
- data/ext/gsplat_native/raster_forward.c +150 -0
- data/ext/gsplat_native/rasterization.h +51 -0
- data/ext/gsplat_native/spherical_harmonics.c +129 -0
- data/gsplat.gemspec +36 -0
- data/lib/gsplat/autograd/context.rb +60 -0
- data/lib/gsplat/autograd/function.rb +68 -0
- data/lib/gsplat/autograd/variable.rb +159 -0
- data/lib/gsplat/backend/ruby/accumulate.rb +139 -0
- data/lib/gsplat/backend/ruby/accumulate_backward.rb +40 -0
- data/lib/gsplat/backend/ruby/eval3d_rasterizer.rb +175 -0
- data/lib/gsplat/backend/ruby/isect_tiles.rb +198 -0
- data/lib/gsplat/backend/ruby/projection.rb +251 -0
- data/lib/gsplat/backend/ruby/projection_backward.rb +190 -0
- data/lib/gsplat/backend/ruby/projection_covariance_vjp.rb +72 -0
- data/lib/gsplat/backend/ruby/projection_input_vjp.rb +252 -0
- data/lib/gsplat/backend/ruby/quat_scale_to_covar_preci.rb +139 -0
- data/lib/gsplat/backend/ruby/rasterize_to_indices_in_range.rb +121 -0
- data/lib/gsplat/backend/ruby/rasterize_to_pixels.rb +199 -0
- data/lib/gsplat/backend/ruby/rasterize_to_pixels_backward.rb +121 -0
- data/lib/gsplat/backend/ruby/spherical_harmonics.rb +135 -0
- data/lib/gsplat/backend/ruby/tile_compositor.rb +50 -0
- data/lib/gsplat/backend/ruby/tile_compositor_backward.rb +84 -0
- data/lib/gsplat/backend.rb +79 -0
- data/lib/gsplat/compression/grid_sort.rb +79 -0
- data/lib/gsplat/compression/kmeans.rb +121 -0
- data/lib/gsplat/compression/png.rb +147 -0
- data/lib/gsplat/compression/png_codec.rb +133 -0
- data/lib/gsplat/compression/quantizer.rb +79 -0
- data/lib/gsplat/io/checkpoint.rb +145 -0
- data/lib/gsplat/io/colmap.rb +175 -0
- data/lib/gsplat/io/colmap_binary.rb +98 -0
- data/lib/gsplat/io/colmap_text.rb +84 -0
- data/lib/gsplat/io/image.rb +63 -0
- data/lib/gsplat/io/image_backends.rb +81 -0
- data/lib/gsplat/io/npy.rb +189 -0
- data/lib/gsplat/io/ply.rb +185 -0
- data/lib/gsplat/io/ply_reader.rb +142 -0
- data/lib/gsplat/io/zip_archive.rb +183 -0
- data/lib/gsplat/math/camera_distortion.rb +123 -0
- data/lib/gsplat/math/camera_projection.rb +202 -0
- data/lib/gsplat/math/mat.rb +114 -0
- data/lib/gsplat/math/quaternion.rb +175 -0
- data/lib/gsplat/math/small_matrix_primitives.rb +112 -0
- data/lib/gsplat/math/spherical_harmonic_basis.rb +148 -0
- data/lib/gsplat/math/ssim.rb +153 -0
- data/lib/gsplat/native.rb +30 -0
- data/lib/gsplat/native_ops.rb +148 -0
- data/lib/gsplat/native_raster_ops.rb +76 -0
- data/lib/gsplat/ops/accumulate.rb +68 -0
- data/lib/gsplat/ops/eval3d_rasterize.rb +62 -0
- data/lib/gsplat/ops/isect_tiles.rb +38 -0
- data/lib/gsplat/ops/projection.rb +154 -0
- data/lib/gsplat/ops/quat_scale_to_covar_preci.rb +118 -0
- data/lib/gsplat/ops/rasterize_to_indices_in_range.rb +38 -0
- data/lib/gsplat/ops/rasterize_to_pixels.rb +105 -0
- data/lib/gsplat/ops/relocation.rb +94 -0
- data/lib/gsplat/ops/spherical_harmonics.rb +71 -0
- data/lib/gsplat/ops/tensor_shape_ops.rb +173 -0
- data/lib/gsplat/ops/tensor_value_ops.rb +150 -0
- data/lib/gsplat/optim/adam.rb +212 -0
- data/lib/gsplat/optim/lr_scheduler.rb +36 -0
- data/lib/gsplat/optim/selective_adam.rb +68 -0
- data/lib/gsplat/rasterization.rb +130 -0
- data/lib/gsplat/rasterization_2dgs.rb +140 -0
- data/lib/gsplat/rasterization_helpers.rb +162 -0
- data/lib/gsplat/rasterization_validation.rb +99 -0
- data/lib/gsplat/strategy/base.rb +49 -0
- data/lib/gsplat/strategy/default.rb +188 -0
- data/lib/gsplat/strategy/mcmc.rb +103 -0
- data/lib/gsplat/strategy/mcmc_ops.rb +143 -0
- data/lib/gsplat/strategy/ops.rb +165 -0
- data/lib/gsplat/training/config.rb +91 -0
- data/lib/gsplat/training/image_fitter.rb +158 -0
- data/lib/gsplat/training/losses.rb +191 -0
- data/lib/gsplat/training/scene.rb +116 -0
- data/lib/gsplat/training/trainer.rb +236 -0
- data/lib/gsplat/utils.rb +110 -0
- data/lib/gsplat/version.rb +6 -0
- data/lib/gsplat.rb +98 -0
- metadata +181 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Optim
|
|
5
|
+
# Adam variant that updates only visible first-axis parameter rows.
|
|
6
|
+
class SelectiveAdam < Adam
|
|
7
|
+
# Updates visible rows while leaving hidden parameters and moments intact.
|
|
8
|
+
#
|
|
9
|
+
# @param visibility [Numo::Bit, Numo::NArray, Array<Boolean>] mask shaped [N]
|
|
10
|
+
# @return [SelectiveAdam] self
|
|
11
|
+
def step(visibility)
|
|
12
|
+
mask = validate_visibility(visibility)
|
|
13
|
+
groups.each_value do |group|
|
|
14
|
+
gradient = group.variable.grad
|
|
15
|
+
next unless gradient
|
|
16
|
+
|
|
17
|
+
update_group(group, gradient, mask)
|
|
18
|
+
end
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# rubocop:disable Metrics/AbcSize
|
|
25
|
+
def update_group(group, gradient, mask)
|
|
26
|
+
state = state_for(group.name)
|
|
27
|
+
next_step = state.step + 1
|
|
28
|
+
candidate_mean = (beta1 * state.exp_avg) + ((1 - beta1) * gradient)
|
|
29
|
+
candidate_variance = (beta2 * state.exp_avg_sq) + ((1 - beta2) * (gradient**2))
|
|
30
|
+
selection = first_axis_selection(mask, gradient.ndim)
|
|
31
|
+
exp_avg = state.exp_avg.dup
|
|
32
|
+
exp_avg_sq = state.exp_avg_sq.dup
|
|
33
|
+
exp_avg[*selection] = candidate_mean[*selection]
|
|
34
|
+
exp_avg_sq[*selection] = candidate_variance[*selection]
|
|
35
|
+
corrected_mean = exp_avg / (1 - (beta1**next_step))
|
|
36
|
+
corrected_variance = exp_avg_sq / (1 - (beta2**next_step))
|
|
37
|
+
update = group.lr * corrected_mean / ((corrected_variance**0.5) + group.eps)
|
|
38
|
+
group.variable.data[*selection] = group.variable.data[*selection] - update[*selection]
|
|
39
|
+
@states[group.name] = State.new(
|
|
40
|
+
step: next_step,
|
|
41
|
+
exp_avg: exp_avg,
|
|
42
|
+
exp_avg_sq: exp_avg_sq
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
# rubocop:enable Metrics/AbcSize
|
|
46
|
+
|
|
47
|
+
def validate_visibility(visibility)
|
|
48
|
+
mask = Numo::Bit.cast(visibility)
|
|
49
|
+
expected = groups.values.first.variable.data.shape[0]
|
|
50
|
+
unless mask.ndim == 1 && mask.shape[0] == expected
|
|
51
|
+
raise ShapeError, "expected visibility [#{expected}], got #{mask.shape.inspect}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
groups.each_value do |group|
|
|
55
|
+
next if group.variable.data.shape[0] == expected
|
|
56
|
+
|
|
57
|
+
raise ShapeError, "all SelectiveAdam groups must share first-axis size #{expected}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
mask
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def first_axis_selection(mask, dimensions)
|
|
64
|
+
[mask] + Array.new(dimensions - 1, true)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# High-level differentiable rendering API.
|
|
4
|
+
module Gsplat
|
|
5
|
+
# Rendering pipeline composed from low-level operations.
|
|
6
|
+
module Rasterization
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists, Naming/MethodParameterName
|
|
10
|
+
def render(means:, opacities:, colors:, viewmats:, ks:, width:, height:, quats: nil, scales: nil,
|
|
11
|
+
covars: nil, near_plane: 0.01, far_plane: 1e10, radius_clip: 0.0, eps2d: 0.3,
|
|
12
|
+
sh_degree: nil, tile_size: 16, backgrounds: nil, render_mode: "RGB",
|
|
13
|
+
rasterize_mode: "classic", camera_model: "pinhole", absgrad: false,
|
|
14
|
+
channel_chunk: 32, packed: false, global_z_order: true,
|
|
15
|
+
radial_coeffs: nil, tangential_coeffs: nil, thin_prism_coeffs: nil,
|
|
16
|
+
with_eval3d: false, return_normals: false)
|
|
17
|
+
# rubocop:enable Metrics/ParameterLists, Naming/MethodParameterName
|
|
18
|
+
RasterizationValidation.validate_options!(
|
|
19
|
+
render_mode, rasterize_mode, sh_degree, tile_size, channel_chunk, width, height
|
|
20
|
+
)
|
|
21
|
+
RasterizationValidation.warn_unsupported_options(packed, global_z_order)
|
|
22
|
+
RasterizationValidation.validate_scene!(
|
|
23
|
+
means, quats, scales, covars, opacities, colors, viewmats, ks, sh_degree
|
|
24
|
+
)
|
|
25
|
+
RasterizationValidation.validate_eval3d!(
|
|
26
|
+
with_eval3d, return_normals, covars, rasterize_mode
|
|
27
|
+
)
|
|
28
|
+
camera_count = Ops::TensorOps.data(viewmats).shape[0]
|
|
29
|
+
calculate_compensations = rasterize_mode == "antialiased"
|
|
30
|
+
radii, means2d, depths, conics, compensations = Gsplat.fully_fused_projection(
|
|
31
|
+
means,
|
|
32
|
+
viewmats: viewmats,
|
|
33
|
+
ks: ks,
|
|
34
|
+
width: width,
|
|
35
|
+
height: height,
|
|
36
|
+
covars: covars,
|
|
37
|
+
quats: quats,
|
|
38
|
+
scales: scales,
|
|
39
|
+
eps2d: eps2d,
|
|
40
|
+
near_plane: near_plane,
|
|
41
|
+
far_plane: far_plane,
|
|
42
|
+
radius_clip: radius_clip,
|
|
43
|
+
calc_compensations: calculate_compensations,
|
|
44
|
+
camera_model: camera_model,
|
|
45
|
+
radial_coeffs: radial_coeffs,
|
|
46
|
+
tangential_coeffs: tangential_coeffs,
|
|
47
|
+
thin_prism_coeffs: thin_prism_coeffs,
|
|
48
|
+
global_z_order: global_z_order
|
|
49
|
+
)
|
|
50
|
+
projected_colors = RasterizationHelpers.prepare_colors(
|
|
51
|
+
means, colors, viewmats, radii, camera_count, sh_degree
|
|
52
|
+
)
|
|
53
|
+
projected_opacities = RasterizationHelpers.camera_broadcast(opacities, camera_count)
|
|
54
|
+
if calculate_compensations
|
|
55
|
+
projected_opacities = Ops::TensorOps.apply(
|
|
56
|
+
Ops::Multiply,
|
|
57
|
+
projected_opacities,
|
|
58
|
+
compensations
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
raster_features, depth_index = RasterizationHelpers.render_features(
|
|
62
|
+
projected_colors, depths, render_mode
|
|
63
|
+
)
|
|
64
|
+
tile_width = (width.to_f / tile_size).ceil
|
|
65
|
+
tile_height = (height.to_f / tile_size).ceil
|
|
66
|
+
tiles_per_gauss, isect_ids, flatten_ids = Gsplat.isect_tiles(
|
|
67
|
+
Ops::TensorOps.data(means2d),
|
|
68
|
+
Ops::TensorOps.data(radii),
|
|
69
|
+
Ops::TensorOps.data(depths),
|
|
70
|
+
tile_size,
|
|
71
|
+
tile_width,
|
|
72
|
+
tile_height
|
|
73
|
+
)
|
|
74
|
+
isect_offsets = Gsplat.isect_offset_encode(
|
|
75
|
+
isect_ids, camera_count, tile_width, tile_height
|
|
76
|
+
)
|
|
77
|
+
hit_distance = %w[d Ed RGB-d RGB-Ed].include?(render_mode)
|
|
78
|
+
eval3d_normals = nil
|
|
79
|
+
render_colors, render_alphas = if with_eval3d || hit_distance
|
|
80
|
+
eval3d_outputs = RasterizationHelpers.rasterize_eval3d_features(
|
|
81
|
+
means, quats, scales, raster_features, projected_opacities,
|
|
82
|
+
viewmats, ks, width, height, tile_size, isect_offsets,
|
|
83
|
+
flatten_ids, backgrounds: backgrounds,
|
|
84
|
+
camera_model: camera_model,
|
|
85
|
+
use_hit_distance: hit_distance,
|
|
86
|
+
return_normals: return_normals
|
|
87
|
+
)
|
|
88
|
+
eval3d_normals = eval3d_outputs[2] if return_normals
|
|
89
|
+
eval3d_outputs.first(2)
|
|
90
|
+
else
|
|
91
|
+
RasterizationHelpers.rasterize_features(
|
|
92
|
+
means2d, conics, raster_features, projected_opacities,
|
|
93
|
+
width, height, tile_size, isect_offsets, flatten_ids,
|
|
94
|
+
backgrounds: backgrounds, channel_chunk: channel_chunk,
|
|
95
|
+
absgrad: absgrad
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
if %w[Ed ED RGB-Ed RGB+ED].include?(render_mode)
|
|
99
|
+
render_colors = Ops::TensorOps.apply(
|
|
100
|
+
Ops::NormalizeDepth,
|
|
101
|
+
render_colors,
|
|
102
|
+
render_alphas,
|
|
103
|
+
depth_index: depth_index
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
meta = RasterizationHelpers.metadata(
|
|
107
|
+
radii, means2d, depths, conics, projected_opacities, tile_width, tile_height,
|
|
108
|
+
tiles_per_gauss, isect_ids, flatten_ids, isect_offsets, width, height, tile_size,
|
|
109
|
+
camera_count
|
|
110
|
+
)
|
|
111
|
+
meta[:normals] = eval3d_normals if return_normals
|
|
112
|
+
means2d.bind_absgrad(meta, :means2d_absgrad) if absgrad && means2d.is_a?(Autograd::Variable)
|
|
113
|
+
[render_colors, render_alphas, meta]
|
|
114
|
+
end
|
|
115
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
class << self
|
|
119
|
+
# Renders a dense batch of cameras with differentiable 3D Gaussians.
|
|
120
|
+
#
|
|
121
|
+
# Geometry uses float32/float64 arrays `means [N,3]`, `quats [N,4]`,
|
|
122
|
+
# `scales [N,3]`; colors are `[N,D]` or SH `[N,K,D]`, views are
|
|
123
|
+
# `[C,4,4]`, and intrinsics are `[C,3,3]`. Activated opacities are `[N]`.
|
|
124
|
+
#
|
|
125
|
+
# @return [Array] rendered colors `[C,H,W,X]`, alphas `[C,H,W,1]`, and metadata Hash
|
|
126
|
+
def rasterization(**)
|
|
127
|
+
Rasterization.render(**)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Public 2D Gaussian splatting API.
|
|
4
|
+
module Gsplat
|
|
5
|
+
# High-level 2D Gaussian rendering composition.
|
|
6
|
+
module Rasterization2DGS
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/ParameterLists, Naming/MethodParameterName
|
|
10
|
+
def render(means:, quats:, scales:, opacities:, colors:, viewmats:, ks:, width:, height:,
|
|
11
|
+
near_plane: 0.01, far_plane: 1e10, radius_clip: 0.0, eps2d: 0.3,
|
|
12
|
+
sh_degree: nil, packed: false, tile_size: 16, backgrounds: nil,
|
|
13
|
+
render_mode: "RGB", sparse_grad: false, absgrad: false, distloss: false,
|
|
14
|
+
depth_mode: "expected", rasterize_mode: "classic")
|
|
15
|
+
# rubocop:enable Metrics/ParameterLists, Naming/MethodParameterName
|
|
16
|
+
validate_options!(sparse_grad, depth_mode, rasterize_mode)
|
|
17
|
+
primary, alphas, meta = Gsplat.rasterization(
|
|
18
|
+
means: means, quats: quats, scales: scales, opacities: opacities, colors: colors,
|
|
19
|
+
viewmats: viewmats, ks: ks, width: width, height: height, near_plane: near_plane,
|
|
20
|
+
far_plane: far_plane, radius_clip: radius_clip, eps2d: eps2d, sh_degree: sh_degree,
|
|
21
|
+
packed: packed, tile_size: tile_size, backgrounds: backgrounds,
|
|
22
|
+
render_mode: render_mode, absgrad: absgrad, rasterize_mode: rasterize_mode
|
|
23
|
+
)
|
|
24
|
+
gaussian_normals = camera_normals(
|
|
25
|
+
Ops::TensorOps.data(means), Ops::TensorOps.data(quats), Ops::TensorOps.data(viewmats)
|
|
26
|
+
)
|
|
27
|
+
render_normals, = Gsplat.rasterization(
|
|
28
|
+
means: means, quats: quats, scales: scales, opacities: opacities,
|
|
29
|
+
colors: gaussian_normals, viewmats: viewmats, ks: ks, width: width, height: height,
|
|
30
|
+
near_plane: near_plane, far_plane: far_plane, radius_clip: radius_clip, eps2d: eps2d,
|
|
31
|
+
tile_size: tile_size, render_mode: "RGB", absgrad: false
|
|
32
|
+
)
|
|
33
|
+
render_median, = Gsplat.rasterization(
|
|
34
|
+
means: means, quats: quats, scales: scales, opacities: opacities,
|
|
35
|
+
colors: gaussian_normals, viewmats: viewmats, ks: ks, width: width, height: height,
|
|
36
|
+
near_plane: near_plane, far_plane: far_plane, radius_clip: radius_clip, eps2d: eps2d,
|
|
37
|
+
tile_size: tile_size, render_mode: "ED", absgrad: false
|
|
38
|
+
)
|
|
39
|
+
surface_normals = normalized_normals(Ops::TensorOps.data(render_normals), Ops::TensorOps.data(alphas))
|
|
40
|
+
render_distort = distortion_image(
|
|
41
|
+
Ops::TensorOps.data(render_median), Ops::TensorOps.data(alphas), distloss
|
|
42
|
+
)
|
|
43
|
+
meta = two_d_metadata(meta, gaussian_normals, render_distort)
|
|
44
|
+
[primary, alphas, render_normals, surface_normals, render_distort, render_median, meta]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate_options!(sparse_grad, depth_mode, rasterize_mode)
|
|
48
|
+
raise ArgumentError, "sparse_grad requires packed mode and is unsupported" if sparse_grad
|
|
49
|
+
raise ArgumentError, "2DGS only supports classic rasterization" unless rasterize_mode == "classic"
|
|
50
|
+
return if %w[expected median].include?(depth_mode.to_s)
|
|
51
|
+
|
|
52
|
+
raise ArgumentError, "depth_mode must be \"expected\" or \"median\""
|
|
53
|
+
end
|
|
54
|
+
private_class_method :validate_options!
|
|
55
|
+
|
|
56
|
+
def camera_normals(means, quats, viewmats)
|
|
57
|
+
rotations = Math::Quaternion.to_rotmat(quats)
|
|
58
|
+
world_normals = rotations[true, true, 2]
|
|
59
|
+
output = means.class.zeros(viewmats.shape[0], means.shape[0], 3)
|
|
60
|
+
viewmats.shape[0].times do |camera|
|
|
61
|
+
view_rotation = viewmats[camera, 0...3, 0...3]
|
|
62
|
+
camera_means = means.dot(view_rotation.transpose) + viewmats[camera, 0...3, 3]
|
|
63
|
+
normals = world_normals.dot(view_rotation.transpose)
|
|
64
|
+
facing_away = (normals * camera_means).sum(axis: 1).gt(0)
|
|
65
|
+
normals[facing_away, true] = -normals[facing_away, true] if facing_away.any?
|
|
66
|
+
output[camera, true, true] = normals
|
|
67
|
+
end
|
|
68
|
+
output
|
|
69
|
+
end
|
|
70
|
+
private_class_method :camera_normals
|
|
71
|
+
|
|
72
|
+
def normalized_normals(normals, alphas)
|
|
73
|
+
alpha = alphas[true, true, true, 0]
|
|
74
|
+
output = normals.class.zeros(*normals.shape)
|
|
75
|
+
valid = alpha.gt(1e-10)
|
|
76
|
+
return output unless valid.any?
|
|
77
|
+
|
|
78
|
+
expected = normals.dup
|
|
79
|
+
3.times { |axis| expected[true, true, true, axis][valid] /= alpha[valid] }
|
|
80
|
+
norm = (expected**2).sum(axis: 3)**0.5
|
|
81
|
+
valid &= norm.gt(1e-10)
|
|
82
|
+
3.times { |axis| output[true, true, true, axis][valid] = expected[true, true, true, axis][valid] / norm[valid] }
|
|
83
|
+
output
|
|
84
|
+
end
|
|
85
|
+
private_class_method :normalized_normals
|
|
86
|
+
|
|
87
|
+
def distortion_image(median, alphas, enabled)
|
|
88
|
+
return median.class.zeros(*median.shape) unless enabled
|
|
89
|
+
|
|
90
|
+
alpha = alphas[true, true, true, 0]
|
|
91
|
+
depth = median[true, true, true, 0]
|
|
92
|
+
output = median.class.zeros(*median.shape)
|
|
93
|
+
output[true, true, true, 0] = (depth * (1 - alpha)).abs
|
|
94
|
+
output
|
|
95
|
+
end
|
|
96
|
+
private_class_method :distortion_image
|
|
97
|
+
|
|
98
|
+
def two_d_metadata(meta, normals, distortion)
|
|
99
|
+
ray_transforms = normals.class.zeros(*(normals.shape[0...-1] + [3, 3]))
|
|
100
|
+
ray_transforms[true, true, true, true] = normals.class.eye(3)
|
|
101
|
+
meta.merge(
|
|
102
|
+
ray_transforms: ray_transforms,
|
|
103
|
+
normals: normals,
|
|
104
|
+
render_distort: distortion,
|
|
105
|
+
gradient_2dgs: meta.fetch(:means2d)
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
private_class_method :two_d_metadata
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
class << self
|
|
112
|
+
# Rasterizes oriented 2D Gaussian surfels.
|
|
113
|
+
#
|
|
114
|
+
# @return [Array] colors, alphas, normals, surface normals, distortion, median depth, metadata
|
|
115
|
+
def rasterization_2dgs(**)
|
|
116
|
+
Rasterization2DGS.render(**)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Projection metadata used by the 2DGS rasterizer.
|
|
120
|
+
# rubocop:disable Metrics/ParameterLists, Naming/MethodParameterName
|
|
121
|
+
def fully_fused_projection_2dgs(means, quats:, scales:, viewmats:, ks:, width:, height:,
|
|
122
|
+
eps2d: 0.3, near_plane: 0.01, far_plane: 1e10,
|
|
123
|
+
radius_clip: 0.0)
|
|
124
|
+
# rubocop:enable Metrics/ParameterLists, Naming/MethodParameterName
|
|
125
|
+
radii, means2d, depths, conics, = fully_fused_projection(
|
|
126
|
+
means, quats: quats, scales: scales, viewmats: viewmats, ks: ks, width: width,
|
|
127
|
+
height: height, eps2d: eps2d, near_plane: near_plane, far_plane: far_plane,
|
|
128
|
+
radius_clip: radius_clip
|
|
129
|
+
)
|
|
130
|
+
normals = Rasterization2DGS.send(
|
|
131
|
+
:camera_normals, Ops::TensorOps.data(means), Ops::TensorOps.data(quats),
|
|
132
|
+
Ops::TensorOps.data(viewmats)
|
|
133
|
+
)
|
|
134
|
+
ray_transforms = Ops::TensorOps.data(conics).class.zeros(
|
|
135
|
+
*(Ops::TensorOps.data(conics).shape[0...-1] + [3, 3])
|
|
136
|
+
)
|
|
137
|
+
[radii, means2d, depths, ray_transforms, normals]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
# Feature preparation and metadata helpers for the high-level renderer.
|
|
5
|
+
module RasterizationHelpers
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def camera_broadcast(value, camera_count)
|
|
9
|
+
shape = Ops::TensorOps.data(value).shape
|
|
10
|
+
return value if shape[0] == camera_count && shape.length >= 2
|
|
11
|
+
|
|
12
|
+
Ops::TensorOps.apply(Ops::CameraBroadcast, value, camera_count)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# rubocop:disable Metrics/ParameterLists
|
|
16
|
+
def prepare_colors(means, colors, viewmats, radii, camera_count, sh_degree)
|
|
17
|
+
# rubocop:enable Metrics/ParameterLists
|
|
18
|
+
unless sh_degree
|
|
19
|
+
return colors if Ops::TensorOps.data(colors).ndim == 3
|
|
20
|
+
|
|
21
|
+
return Ops::TensorOps.apply(Ops::CameraBroadcast, colors, camera_count)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
directions = Ops::TensorOps.apply(Ops::CameraDirections, means, viewmats)
|
|
25
|
+
coefficients = Ops::TensorOps.apply(Ops::CameraBroadcast, colors, camera_count)
|
|
26
|
+
evaluated = Gsplat.spherical_harmonics(
|
|
27
|
+
sh_degree,
|
|
28
|
+
directions,
|
|
29
|
+
coefficients,
|
|
30
|
+
masks: visible_radii(radii)
|
|
31
|
+
)
|
|
32
|
+
Ops::TensorOps.apply(Ops::AddClampMin, evaluated, offset: 0.5, minimum: 0.0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def visible_radii(radii)
|
|
36
|
+
values = Ops::TensorOps.data(radii)
|
|
37
|
+
return values.gt(0) unless values.ndim >= 3 && values.shape[-1] == 2
|
|
38
|
+
|
|
39
|
+
values.min(axis: values.ndim - 1).gt(0)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render_features(colors, depths, render_mode)
|
|
43
|
+
return [colors, nil] if render_mode == "RGB"
|
|
44
|
+
|
|
45
|
+
depth_features = Ops::TensorOps.apply(Ops::DepthFeatures, depths)
|
|
46
|
+
return [depth_features, 0] if %w[d Ed D ED].include?(render_mode)
|
|
47
|
+
|
|
48
|
+
feature_count = Ops::TensorOps.data(colors).shape[-1]
|
|
49
|
+
[Ops::TensorOps.apply(Ops::ConcatDepth, colors, depth_features), feature_count]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# rubocop:disable Metrics/ParameterLists
|
|
53
|
+
def rasterize_features(means2d, conics, features, opacities, width, height, tile_size, offsets, flatten_ids,
|
|
54
|
+
backgrounds:, channel_chunk:, absgrad:)
|
|
55
|
+
# rubocop:enable Metrics/ParameterLists
|
|
56
|
+
channel_count = Ops::TensorOps.data(features).shape[-1]
|
|
57
|
+
if channel_count <= channel_chunk
|
|
58
|
+
return Gsplat.rasterize_to_pixels(
|
|
59
|
+
means2d,
|
|
60
|
+
conics,
|
|
61
|
+
features,
|
|
62
|
+
opacities,
|
|
63
|
+
width,
|
|
64
|
+
height,
|
|
65
|
+
tile_size,
|
|
66
|
+
offsets,
|
|
67
|
+
flatten_ids,
|
|
68
|
+
backgrounds: backgrounds,
|
|
69
|
+
absgrad: absgrad
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
rasterize_feature_chunks(
|
|
74
|
+
means2d, conics, features, opacities, width, height, tile_size, offsets, flatten_ids,
|
|
75
|
+
backgrounds: backgrounds, channel_chunk: channel_chunk, absgrad: absgrad
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# rubocop:disable Metrics/ParameterLists
|
|
80
|
+
def rasterize_feature_chunks(means2d, conics, features, opacities, width, height,
|
|
81
|
+
tile_size, offsets, flatten_ids,
|
|
82
|
+
backgrounds:, channel_chunk:, absgrad:)
|
|
83
|
+
# rubocop:enable Metrics/ParameterLists
|
|
84
|
+
channel_count = Ops::TensorOps.data(features).shape[-1]
|
|
85
|
+
rendered_chunks = []
|
|
86
|
+
alpha = nil
|
|
87
|
+
(0...channel_count).step(channel_chunk) do |start|
|
|
88
|
+
range = start...[start + channel_chunk, channel_count].min
|
|
89
|
+
feature_chunk = Ops::TensorOps.apply(Ops::FeatureSlice, features, range)
|
|
90
|
+
background_chunk = backgrounds && Ops::TensorOps.apply(Ops::FeatureSlice, backgrounds, range)
|
|
91
|
+
rendered, chunk_alpha = Gsplat.rasterize_to_pixels(
|
|
92
|
+
means2d,
|
|
93
|
+
conics,
|
|
94
|
+
feature_chunk,
|
|
95
|
+
opacities,
|
|
96
|
+
width,
|
|
97
|
+
height,
|
|
98
|
+
tile_size,
|
|
99
|
+
offsets,
|
|
100
|
+
flatten_ids,
|
|
101
|
+
backgrounds: background_chunk,
|
|
102
|
+
absgrad: absgrad
|
|
103
|
+
)
|
|
104
|
+
rendered_chunks << rendered
|
|
105
|
+
alpha ||= chunk_alpha
|
|
106
|
+
end
|
|
107
|
+
[Ops::TensorOps.apply(Ops::ConcatFeatures, *rendered_chunks), alpha]
|
|
108
|
+
end
|
|
109
|
+
private_class_method :rasterize_feature_chunks
|
|
110
|
+
|
|
111
|
+
# rubocop:disable Metrics/ParameterLists
|
|
112
|
+
def rasterize_eval3d_features(means, quats, scales, features, opacities, viewmats, intrinsics,
|
|
113
|
+
width, height, tile_size, offsets, flatten_ids, backgrounds:,
|
|
114
|
+
camera_model:, use_hit_distance:, return_normals:)
|
|
115
|
+
# rubocop:enable Metrics/ParameterLists
|
|
116
|
+
raise ArgumentError, "world-space rendering requires quats and scales" unless quats && scales
|
|
117
|
+
|
|
118
|
+
inputs = [means, quats, scales, features, opacities, backgrounds]
|
|
119
|
+
options = {
|
|
120
|
+
viewmats: Ops::TensorOps.data(viewmats),
|
|
121
|
+
intrinsics: Ops::TensorOps.data(intrinsics),
|
|
122
|
+
width: width,
|
|
123
|
+
height: height,
|
|
124
|
+
tile_size: tile_size,
|
|
125
|
+
offsets: offsets,
|
|
126
|
+
flatten_ids: flatten_ids,
|
|
127
|
+
camera_model: camera_model.to_s,
|
|
128
|
+
use_hit_distance: use_hit_distance,
|
|
129
|
+
return_normals: return_normals
|
|
130
|
+
}
|
|
131
|
+
return Ops::Eval3dRasterize.apply(*inputs, **options) if inputs.any?(Autograd::Variable)
|
|
132
|
+
|
|
133
|
+
Backend::RubyEval3dRasterizer.forward(*inputs, **options)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# rubocop:disable Metrics/ParameterLists
|
|
137
|
+
def metadata(radii, means2d, depths, conics, opacities, tile_width, tile_height,
|
|
138
|
+
tiles_per_gauss, isect_ids, flatten_ids, isect_offsets, width, height,
|
|
139
|
+
tile_size, camera_count)
|
|
140
|
+
# rubocop:enable Metrics/ParameterLists
|
|
141
|
+
{
|
|
142
|
+
camera_ids: nil,
|
|
143
|
+
gaussian_ids: nil,
|
|
144
|
+
radii: radii,
|
|
145
|
+
means2d: means2d,
|
|
146
|
+
depths: depths,
|
|
147
|
+
conics: conics,
|
|
148
|
+
opacities: opacities,
|
|
149
|
+
tile_width: tile_width,
|
|
150
|
+
tile_height: tile_height,
|
|
151
|
+
tiles_per_gauss: tiles_per_gauss,
|
|
152
|
+
isect_ids: isect_ids,
|
|
153
|
+
flatten_ids: flatten_ids,
|
|
154
|
+
isect_offsets: isect_offsets,
|
|
155
|
+
width: width,
|
|
156
|
+
height: height,
|
|
157
|
+
tile_size: tile_size,
|
|
158
|
+
n_cameras: camera_count
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
# Input and option validation for the high-level renderer.
|
|
5
|
+
module RasterizationValidation
|
|
6
|
+
RENDER_MODES = %w[RGB d Ed D ED RGB-d RGB-Ed RGB+D RGB+ED].freeze
|
|
7
|
+
RASTERIZE_MODES = %w[classic antialiased].freeze
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# rubocop:disable Metrics/ParameterLists
|
|
12
|
+
def validate_options!(render_mode, rasterize_mode, sh_degree, tile_size, channel_chunk, width, height)
|
|
13
|
+
# rubocop:enable Metrics/ParameterLists
|
|
14
|
+
raise ArgumentError, "unsupported render_mode #{render_mode.inspect}" unless RENDER_MODES.include?(render_mode)
|
|
15
|
+
unless RASTERIZE_MODES.include?(rasterize_mode)
|
|
16
|
+
raise ArgumentError, "unsupported rasterize_mode #{rasterize_mode.inspect}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
unless sh_degree.nil? || (0..4).cover?(sh_degree)
|
|
20
|
+
raise ArgumentError, "sh_degree must be nil or an integer from 0 through 4"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
values = { width: width, height: height, tile_size: tile_size, channel_chunk: channel_chunk }
|
|
24
|
+
invalid = values.find { |_name, value| !value.is_a?(Integer) || !value.positive? }
|
|
25
|
+
raise ArgumentError, "#{invalid[0]} must be a positive integer" if invalid
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def warn_unsupported_options(packed, _global_z_order)
|
|
29
|
+
Gsplat.logger.warn("packed mode is unsupported; using dense mode") if packed
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def validate_eval3d!(with_eval3d, return_normals, covars, rasterize_mode)
|
|
33
|
+
raise ArgumentError, "return_normals requires with_eval3d: true" if return_normals && !with_eval3d
|
|
34
|
+
raise ArgumentError, "with_eval3d requires quats and scales instead of covars" if with_eval3d && covars
|
|
35
|
+
return unless with_eval3d && rasterize_mode != "classic"
|
|
36
|
+
|
|
37
|
+
raise ArgumentError, "with_eval3d requires rasterize_mode: \"classic\""
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# rubocop:disable Metrics/ParameterLists
|
|
41
|
+
def validate_scene!(means, quats, scales, covars, opacities, colors, viewmats, intrinsics, sh_degree)
|
|
42
|
+
# rubocop:enable Metrics/ParameterLists
|
|
43
|
+
means_shape = data(means).shape
|
|
44
|
+
unless means_shape.length == 2 && means_shape[-1] == 3
|
|
45
|
+
raise ShapeError, "expected means [N,3], got #{means_shape.inspect}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
gaussian_count = means_shape[0]
|
|
49
|
+
opacity_shape = data(opacities).shape
|
|
50
|
+
unless [[gaussian_count], [data(viewmats).shape[0], gaussian_count]].include?(opacity_shape)
|
|
51
|
+
raise ShapeError, "expected opacities [N] or [C,N], got #{opacity_shape.inspect}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
validate_geometry!(quats, scales, covars, gaussian_count)
|
|
55
|
+
camera_count = data(viewmats).shape[0]
|
|
56
|
+
unless data(viewmats).shape == [camera_count, 4, 4] && data(intrinsics).shape == [camera_count, 3, 3]
|
|
57
|
+
raise ShapeError,
|
|
58
|
+
"expected viewmats [C,4,4] and ks [C,3,3], " \
|
|
59
|
+
"got #{data(viewmats).shape.inspect} and #{data(intrinsics).shape.inspect}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
validate_color_shape!(data(colors).shape, gaussian_count, camera_count, sh_degree)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_geometry!(quats, scales, covars, gaussian_count)
|
|
66
|
+
if covars
|
|
67
|
+
covar_shape = data(covars).shape
|
|
68
|
+
return if [[gaussian_count, 3, 3], [gaussian_count, 6]].include?(covar_shape)
|
|
69
|
+
|
|
70
|
+
raise ShapeError, "expected covars [N,3,3] or [N,6], got #{covar_shape.inspect}"
|
|
71
|
+
end
|
|
72
|
+
return if data(quats).shape == [gaussian_count, 4] && data(scales).shape == [gaussian_count, 3]
|
|
73
|
+
|
|
74
|
+
raise ShapeError,
|
|
75
|
+
"expected quats [N,4] and scales [N,3], " \
|
|
76
|
+
"got #{data(quats).shape.inspect} and #{data(scales).shape.inspect}"
|
|
77
|
+
end
|
|
78
|
+
private_class_method :validate_geometry!
|
|
79
|
+
|
|
80
|
+
def validate_color_shape!(shape, gaussian_count, camera_count, sh_degree)
|
|
81
|
+
valid = if sh_degree
|
|
82
|
+
shape.length == 3 && shape[0] == gaussian_count && shape[1] >= ((sh_degree + 1)**2)
|
|
83
|
+
else
|
|
84
|
+
(shape.length == 2 && shape[0] == gaussian_count) ||
|
|
85
|
+
(shape.length == 3 && shape[0...2] == [camera_count, gaussian_count])
|
|
86
|
+
end
|
|
87
|
+
return if valid
|
|
88
|
+
|
|
89
|
+
expected = sh_degree ? "[N,K,D] with K >= #{(sh_degree + 1)**2}" : "[N,D] or [C,N,D]"
|
|
90
|
+
raise ShapeError, "expected colors #{expected}, got #{shape.inspect}"
|
|
91
|
+
end
|
|
92
|
+
private_class_method :validate_color_shape!
|
|
93
|
+
|
|
94
|
+
def data(value)
|
|
95
|
+
Ops::TensorOps.data(value)
|
|
96
|
+
end
|
|
97
|
+
private_class_method :data
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Strategy
|
|
5
|
+
# Shared strategy lifecycle and parameter/optimizer validation.
|
|
6
|
+
class Base
|
|
7
|
+
# Parameter names required by every structural strategy.
|
|
8
|
+
REQUIRED_KEYS = %i[means quats scales opacities sh0 shN].freeze
|
|
9
|
+
|
|
10
|
+
def initialize_state(scene_scale:)
|
|
11
|
+
raise ArgumentError, "scene_scale must be positive" unless scene_scale.positive?
|
|
12
|
+
|
|
13
|
+
{ scene_scale: scene_scale }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# rubocop:disable Metrics/AbcSize, Naming/PredicateMethod
|
|
17
|
+
def check_sanity(params, optimizers)
|
|
18
|
+
missing_params = REQUIRED_KEYS - params.keys
|
|
19
|
+
missing_optimizers = REQUIRED_KEYS - optimizers.keys
|
|
20
|
+
raise ArgumentError, "missing params: #{missing_params.join(', ')}" unless missing_params.empty?
|
|
21
|
+
raise ArgumentError, "missing optimizers: #{missing_optimizers.join(', ')}" unless missing_optimizers.empty?
|
|
22
|
+
|
|
23
|
+
count = nil
|
|
24
|
+
REQUIRED_KEYS.each do |key|
|
|
25
|
+
variable = params.fetch(key)
|
|
26
|
+
raise ArgumentError, "params[:#{key}] must be an Autograd::Variable" unless variable.is_a?(Autograd::Variable)
|
|
27
|
+
|
|
28
|
+
count ||= variable.data.shape[0]
|
|
29
|
+
unless variable.data.ndim.positive? && variable.data.shape[0] == count
|
|
30
|
+
raise ShapeError, "all params must share first-axis size #{count}; #{key}=#{variable.data.shape.inspect}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
optimizer = optimizers.fetch(key)
|
|
34
|
+
unless optimizer.is_a?(Optim::Adam) &&
|
|
35
|
+
optimizer.groups.values.any? { |group| group.variable.equal?(variable) }
|
|
36
|
+
raise ArgumentError, "optimizers[:#{key}] must optimize params[:#{key}]"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
# rubocop:enable Metrics/AbcSize, Naming/PredicateMethod
|
|
42
|
+
|
|
43
|
+
def step_pre_backward(params:, optimizers:, state:, step:, info:)
|
|
44
|
+
[params, optimizers, state, step, info]
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|