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,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Backend
|
|
5
|
+
# Numo implementation of dense fused Gaussian projection.
|
|
6
|
+
module RubyProjection
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/ParameterLists
|
|
10
|
+
def forward(means, covars, quaternions, scales, viewmats, intrinsics, width, height,
|
|
11
|
+
eps2d:, near_plane:, far_plane:, radius_clip:, calc_compensations:, camera_model:,
|
|
12
|
+
radial_coeffs: nil, tangential_coeffs: nil, thin_prism_coeffs: nil,
|
|
13
|
+
global_z_order: true)
|
|
14
|
+
# rubocop:enable Metrics/ParameterLists
|
|
15
|
+
inputs = prepare_inputs(
|
|
16
|
+
means, covars, quaternions, scales, viewmats, intrinsics, width, height,
|
|
17
|
+
eps2d, near_plane, far_plane, radius_clip, camera_model,
|
|
18
|
+
radial_coeffs, tangential_coeffs, thin_prism_coeffs
|
|
19
|
+
)
|
|
20
|
+
means, covars, viewmats, intrinsics = inputs
|
|
21
|
+
camera_means, camera_covars = Math::CameraProjection.world_to_cam(means, covars, viewmats)
|
|
22
|
+
projection_means = projection_safe_means(camera_means, near_plane, far_plane)
|
|
23
|
+
means2d, covars2d = project(
|
|
24
|
+
projection_means,
|
|
25
|
+
camera_covars,
|
|
26
|
+
intrinsics,
|
|
27
|
+
width,
|
|
28
|
+
height,
|
|
29
|
+
camera_model,
|
|
30
|
+
radial_coeffs: radial_coeffs,
|
|
31
|
+
tangential_coeffs: tangential_coeffs,
|
|
32
|
+
thin_prism_coeffs: thin_prism_coeffs
|
|
33
|
+
)
|
|
34
|
+
finish_projection(
|
|
35
|
+
camera_means,
|
|
36
|
+
means2d,
|
|
37
|
+
covars2d,
|
|
38
|
+
width,
|
|
39
|
+
height,
|
|
40
|
+
eps2d,
|
|
41
|
+
near_plane,
|
|
42
|
+
far_plane,
|
|
43
|
+
radius_clip,
|
|
44
|
+
calc_compensations,
|
|
45
|
+
global_z_order: global_z_order
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
|
|
50
|
+
def prepare_inputs(means, covars, quaternions, scales, viewmats, intrinsics, width, height,
|
|
51
|
+
eps2d, near_plane, far_plane, radius_clip, camera_model,
|
|
52
|
+
radial_coeffs = nil, tangential_coeffs = nil, thin_prism_coeffs = nil)
|
|
53
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ParameterLists
|
|
54
|
+
unless means.is_a?(Numo::NArray) && [Numo::SFloat, Numo::DFloat].include?(means.class)
|
|
55
|
+
raise ArgumentError, "means must be Numo::SFloat or Numo::DFloat"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
valid_means = means.ndim == 2 && means.shape[-1] == 3
|
|
59
|
+
raise ShapeError, "expected means [N,3], got #{means.shape.inspect}" unless valid_means
|
|
60
|
+
|
|
61
|
+
covars = prepare_covariances(means, covars, quaternions, scales)
|
|
62
|
+
viewmats = means.class.cast(viewmats)
|
|
63
|
+
intrinsics = means.class.cast(intrinsics)
|
|
64
|
+
unless viewmats.ndim == 3 && viewmats.shape[1..] == [4, 4]
|
|
65
|
+
raise ShapeError, "expected viewmats [C,4,4], got #{viewmats.shape.inspect}"
|
|
66
|
+
end
|
|
67
|
+
unless intrinsics.shape == [viewmats.shape[0], 3, 3]
|
|
68
|
+
raise ShapeError, "expected intrinsics [#{viewmats.shape[0]},3,3], got #{intrinsics.shape.inspect}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
validate_options!(
|
|
72
|
+
width: width,
|
|
73
|
+
height: height,
|
|
74
|
+
eps2d: eps2d,
|
|
75
|
+
near_plane: near_plane,
|
|
76
|
+
far_plane: far_plane,
|
|
77
|
+
radius_clip: radius_clip,
|
|
78
|
+
camera_model: camera_model
|
|
79
|
+
)
|
|
80
|
+
validate_distortion!(
|
|
81
|
+
viewmats.shape[0], camera_model,
|
|
82
|
+
radial_coeffs, tangential_coeffs, thin_prism_coeffs
|
|
83
|
+
)
|
|
84
|
+
[means, covars, viewmats, intrinsics]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def prepare_covariances(means, covars, quaternions, scales)
|
|
88
|
+
if covars
|
|
89
|
+
raise ArgumentError, "provide covars or quaternions/scales, not both" if quaternions || scales
|
|
90
|
+
|
|
91
|
+
return expand_covariances(means.class.cast(covars), means.shape[0])
|
|
92
|
+
end
|
|
93
|
+
raise ArgumentError, "quaternions and scales are required when covars is nil" unless quaternions && scales
|
|
94
|
+
|
|
95
|
+
covariance, = RubyQuatScaleToCovarPreci.forward(
|
|
96
|
+
means.class.cast(quaternions),
|
|
97
|
+
means.class.cast(scales),
|
|
98
|
+
compute_covar: true,
|
|
99
|
+
compute_preci: false,
|
|
100
|
+
triu: false
|
|
101
|
+
)
|
|
102
|
+
covariance
|
|
103
|
+
end
|
|
104
|
+
private_class_method :prepare_covariances
|
|
105
|
+
|
|
106
|
+
def expand_covariances(covars, gaussian_count)
|
|
107
|
+
return covars if covars.shape == [gaussian_count, 3, 3]
|
|
108
|
+
unless covars.shape == [gaussian_count, 6]
|
|
109
|
+
raise ShapeError, "expected covars [N,3,3] or [N,6], got #{covars.shape.inspect}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
output = covars.class.zeros(gaussian_count, 3, 3)
|
|
113
|
+
[[0, 0, 0], [0, 1, 1], [0, 2, 2], [1, 1, 3], [1, 2, 4], [2, 2, 5]].each do |row, column, index|
|
|
114
|
+
output[true, row, column] = covars[true, index]
|
|
115
|
+
output[true, column, row] = covars[true, index]
|
|
116
|
+
end
|
|
117
|
+
output
|
|
118
|
+
end
|
|
119
|
+
private_class_method :expand_covariances
|
|
120
|
+
|
|
121
|
+
# rubocop:disable Metrics/ParameterLists
|
|
122
|
+
def validate_options!(width:, height:, eps2d:, near_plane:, far_plane:, radius_clip:, camera_model:)
|
|
123
|
+
# rubocop:enable Metrics/ParameterLists
|
|
124
|
+
unless width.is_a?(Integer) && width.positive? && height.is_a?(Integer) && height.positive?
|
|
125
|
+
raise ArgumentError, "width and height must be positive integers"
|
|
126
|
+
end
|
|
127
|
+
raise ArgumentError, "eps2d and radius_clip must be non-negative" if eps2d.negative? || radius_clip.negative?
|
|
128
|
+
raise ArgumentError, "near_plane must be less than far_plane" unless near_plane < far_plane
|
|
129
|
+
return if %w[pinhole ortho fisheye].include?(camera_model.to_s)
|
|
130
|
+
|
|
131
|
+
raise ArgumentError, "camera_model must be \"pinhole\", \"ortho\", or \"fisheye\""
|
|
132
|
+
end
|
|
133
|
+
private_class_method :validate_options!
|
|
134
|
+
|
|
135
|
+
def validate_distortion!(camera_count, camera_model, radial, tangential, thin_prism)
|
|
136
|
+
if camera_model.to_s == "ortho" && [radial, tangential, thin_prism].any?
|
|
137
|
+
raise ArgumentError, "distortion coefficients are unsupported for orthographic cameras"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
expected_radial = camera_model.to_s == "fisheye" ? [4] : [4, 6]
|
|
141
|
+
validate_coefficients!(radial, camera_count, expected_radial, "radial_coeffs")
|
|
142
|
+
validate_coefficients!(tangential, camera_count, [2], "tangential_coeffs")
|
|
143
|
+
validate_coefficients!(thin_prism, camera_count, [4], "thin_prism_coeffs")
|
|
144
|
+
end
|
|
145
|
+
private_class_method :validate_distortion!
|
|
146
|
+
|
|
147
|
+
def validate_coefficients!(value, camera_count, widths, name)
|
|
148
|
+
return unless value
|
|
149
|
+
unless value.is_a?(Numo::NArray) && [Numo::SFloat, Numo::DFloat].include?(value.class) &&
|
|
150
|
+
value.ndim == 2 && value.shape[0] == camera_count && widths.include?(value.shape[1])
|
|
151
|
+
raise ShapeError, "expected #{name} [#{camera_count},#{widths.join(' or ')}]"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
private_class_method :validate_coefficients!
|
|
155
|
+
|
|
156
|
+
# rubocop:disable Metrics/ParameterLists
|
|
157
|
+
def project(means, covars, intrinsics, width, height, camera_model,
|
|
158
|
+
radial_coeffs: nil, tangential_coeffs: nil, thin_prism_coeffs: nil)
|
|
159
|
+
# rubocop:enable Metrics/ParameterLists
|
|
160
|
+
if camera_model.to_s == "ortho"
|
|
161
|
+
return Math::CameraProjection.ortho_proj(means, covars, intrinsics, width, height)
|
|
162
|
+
end
|
|
163
|
+
if camera_model.to_s == "fisheye" || [radial_coeffs, tangential_coeffs, thin_prism_coeffs].any?
|
|
164
|
+
return Math::CameraProjection.distorted_proj(
|
|
165
|
+
means,
|
|
166
|
+
covars,
|
|
167
|
+
intrinsics,
|
|
168
|
+
camera_model,
|
|
169
|
+
radial_coeffs: radial_coeffs,
|
|
170
|
+
tangential_coeffs: tangential_coeffs,
|
|
171
|
+
thin_prism_coeffs: thin_prism_coeffs
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
Math::CameraProjection.persp_proj(means, covars, intrinsics, width, height)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def projection_safe_means(camera_means, near_plane, far_plane)
|
|
179
|
+
output = camera_means.dup
|
|
180
|
+
depths = output[true, true, 2]
|
|
181
|
+
culled = depths.le(near_plane) | depths.ge(far_plane)
|
|
182
|
+
depths[culled] = 1 if culled.any?
|
|
183
|
+
output[true, true, 2] = depths
|
|
184
|
+
output
|
|
185
|
+
end
|
|
186
|
+
private_class_method :projection_safe_means
|
|
187
|
+
|
|
188
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
|
|
189
|
+
def finish_projection(camera_means, means2d, covars2d, width, height, eps2d,
|
|
190
|
+
near_plane, far_plane, radius_clip, calc_compensations,
|
|
191
|
+
global_z_order: true)
|
|
192
|
+
# rubocop:enable Metrics/ParameterLists
|
|
193
|
+
determinant_original = Math::Mat.det2x2(covars2d)
|
|
194
|
+
regularized = covars2d.dup
|
|
195
|
+
regularized[true, true, 0, 0] += eps2d
|
|
196
|
+
regularized[true, true, 1, 1] += eps2d
|
|
197
|
+
determinant = Math::Mat.det2x2(regularized)
|
|
198
|
+
safe_determinant = determinant.dup
|
|
199
|
+
invalid_determinant = safe_determinant.le(0)
|
|
200
|
+
safe_determinant[invalid_determinant] = 1e-10 if invalid_determinant.any?
|
|
201
|
+
conics = conics_from_covariance(regularized, safe_determinant)
|
|
202
|
+
compensations = compensation(determinant_original, safe_determinant) if calc_compensations
|
|
203
|
+
variance_x = regularized[true, true, 0, 0].dup
|
|
204
|
+
variance_y = regularized[true, true, 1, 1].dup
|
|
205
|
+
variance_x[variance_x.lt(0)] = 0 if variance_x.lt(0).any?
|
|
206
|
+
variance_y[variance_y.lt(0)] = 0 if variance_y.lt(0).any?
|
|
207
|
+
radius_x = Numo::Int32.cast((3.33 * (variance_x**0.5)).ceil)
|
|
208
|
+
radius_y = Numo::Int32.cast((3.33 * (variance_y**0.5)).ceil)
|
|
209
|
+
z_depths = camera_means[true, true, 2]
|
|
210
|
+
depths = if global_z_order
|
|
211
|
+
z_depths.dup
|
|
212
|
+
else
|
|
213
|
+
(camera_means**2).sum(axis: 2)**0.5
|
|
214
|
+
end
|
|
215
|
+
visible = determinant.gt(0) & z_depths.gt(near_plane) & z_depths.lt(far_plane)
|
|
216
|
+
visible &= radius_x.gt(radius_clip) | radius_y.gt(radius_clip)
|
|
217
|
+
visible &= (means2d[true, true, 0] + radius_x).gt(0)
|
|
218
|
+
visible &= means2d[true, true, 0] - radius_x < width
|
|
219
|
+
visible &= (means2d[true, true, 1] + radius_y).gt(0)
|
|
220
|
+
visible &= means2d[true, true, 1] - radius_y < height
|
|
221
|
+
radius_x[visible.eq(0)] = 0
|
|
222
|
+
radius_y[visible.eq(0)] = 0
|
|
223
|
+
radii = Numo::Int32.zeros(*(depths.shape + [2]))
|
|
224
|
+
radii[true, true, 0] = radius_x
|
|
225
|
+
radii[true, true, 1] = radius_y
|
|
226
|
+
[radii, means2d, depths, conics, compensations]
|
|
227
|
+
end
|
|
228
|
+
# rubocop:enable Metrics/AbcSize
|
|
229
|
+
private_class_method :finish_projection
|
|
230
|
+
|
|
231
|
+
def conics_from_covariance(covariance, determinant)
|
|
232
|
+
output = covariance.class.zeros(*(covariance.shape[0...-2] + [3]))
|
|
233
|
+
output[true, true, 0] = covariance[true, true, 1, 1] / determinant
|
|
234
|
+
output[true, true, 1] = -(
|
|
235
|
+
covariance[true, true, 0, 1] + covariance[true, true, 1, 0]
|
|
236
|
+
) / (2.0 * determinant)
|
|
237
|
+
output[true, true, 2] = covariance[true, true, 0, 0] / determinant
|
|
238
|
+
output
|
|
239
|
+
end
|
|
240
|
+
private_class_method :conics_from_covariance
|
|
241
|
+
|
|
242
|
+
def compensation(original, regularized)
|
|
243
|
+
ratio = original / regularized
|
|
244
|
+
negative = ratio.lt(0)
|
|
245
|
+
ratio[negative] = 0 if negative.any?
|
|
246
|
+
ratio**0.5
|
|
247
|
+
end
|
|
248
|
+
private_class_method :compensation
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Backend
|
|
5
|
+
# Analytic Numo VJP for dense pinhole projection.
|
|
6
|
+
module RubyProjectionBackward
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/ParameterLists
|
|
10
|
+
def backward(means, covars, quaternions, scales, viewmats, intrinsics, width, height,
|
|
11
|
+
grad_means2d, grad_depths, grad_conics, grad_compensations, **options)
|
|
12
|
+
# rubocop:enable Metrics/ParameterLists
|
|
13
|
+
prepared = RubyProjection.prepare_inputs(
|
|
14
|
+
means, covars, quaternions, scales, viewmats, intrinsics, width, height,
|
|
15
|
+
options.fetch(:eps2d), options.fetch(:near_plane), options.fetch(:far_plane),
|
|
16
|
+
options.fetch(:radius_clip), options.fetch(:camera_model),
|
|
17
|
+
options[:radial_coeffs], options[:tangential_coeffs], options[:thin_prism_coeffs]
|
|
18
|
+
)
|
|
19
|
+
means, expanded_covars, viewmats, intrinsics = prepared
|
|
20
|
+
camera_means, camera_covars = Math::CameraProjection.world_to_cam(means, expanded_covars, viewmats)
|
|
21
|
+
projection_means = safe_means(
|
|
22
|
+
camera_means,
|
|
23
|
+
options.fetch(:near_plane),
|
|
24
|
+
options.fetch(:far_plane)
|
|
25
|
+
)
|
|
26
|
+
camera_model = options.fetch(:camera_model)
|
|
27
|
+
means2d, covars2d = RubyProjection.project(
|
|
28
|
+
projection_means,
|
|
29
|
+
camera_covars,
|
|
30
|
+
intrinsics,
|
|
31
|
+
width,
|
|
32
|
+
height,
|
|
33
|
+
camera_model,
|
|
34
|
+
radial_coeffs: options[:radial_coeffs],
|
|
35
|
+
tangential_coeffs: options[:tangential_coeffs],
|
|
36
|
+
thin_prism_coeffs: options[:thin_prism_coeffs]
|
|
37
|
+
)
|
|
38
|
+
jacobians = projection_jacobians(projection_means, intrinsics, width, height, options)
|
|
39
|
+
output_gradients = validate_output_gradients(
|
|
40
|
+
means.class,
|
|
41
|
+
means2d,
|
|
42
|
+
grad_means2d,
|
|
43
|
+
grad_depths,
|
|
44
|
+
grad_conics,
|
|
45
|
+
grad_compensations
|
|
46
|
+
)
|
|
47
|
+
grad_camera_means, grad_camera_covars = projection_vjp(
|
|
48
|
+
projection_means,
|
|
49
|
+
camera_covars,
|
|
50
|
+
covars2d,
|
|
51
|
+
jacobians,
|
|
52
|
+
intrinsics,
|
|
53
|
+
output_gradients,
|
|
54
|
+
options.fetch(:eps2d),
|
|
55
|
+
width,
|
|
56
|
+
height,
|
|
57
|
+
options
|
|
58
|
+
)
|
|
59
|
+
grad_means, grad_expanded_covars = world_to_cam_vjp(
|
|
60
|
+
grad_camera_means,
|
|
61
|
+
grad_camera_covars,
|
|
62
|
+
viewmats
|
|
63
|
+
)
|
|
64
|
+
input_covariance_gradients(
|
|
65
|
+
means,
|
|
66
|
+
covars,
|
|
67
|
+
quaternions,
|
|
68
|
+
scales,
|
|
69
|
+
grad_means,
|
|
70
|
+
grad_expanded_covars
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def safe_means(camera_means, near_plane, far_plane)
|
|
75
|
+
output = camera_means.dup
|
|
76
|
+
depths = output[true, true, 2]
|
|
77
|
+
culled = depths.le(near_plane) | depths.ge(far_plane)
|
|
78
|
+
depths[culled] = 1 if culled.any?
|
|
79
|
+
output[true, true, 2] = depths
|
|
80
|
+
output
|
|
81
|
+
end
|
|
82
|
+
private_class_method :safe_means
|
|
83
|
+
|
|
84
|
+
def projection_jacobians(means, intrinsics, width, height, options)
|
|
85
|
+
output = means.class.zeros(means.shape[0], means.shape[1], 2, 3)
|
|
86
|
+
means.shape[0].times do |camera_index|
|
|
87
|
+
camera_means = means[camera_index, true, true]
|
|
88
|
+
camera_intrinsics = intrinsics[camera_index, true, true]
|
|
89
|
+
camera_model = options.fetch(:camera_model)
|
|
90
|
+
extended = camera_model.to_s == "fisheye" ||
|
|
91
|
+
%i[radial_coeffs tangential_coeffs thin_prism_coeffs].any? { |key| options[key] }
|
|
92
|
+
_, jacobians = if camera_model.to_s == "ortho"
|
|
93
|
+
Math::CameraProjection.ortho_camera(camera_means, camera_intrinsics)
|
|
94
|
+
elsif extended
|
|
95
|
+
Math::CameraDistortion.project_camera(
|
|
96
|
+
camera_means,
|
|
97
|
+
camera_intrinsics,
|
|
98
|
+
camera_model,
|
|
99
|
+
radial: coefficient_row(options[:radial_coeffs], camera_index),
|
|
100
|
+
tangential: coefficient_row(options[:tangential_coeffs], camera_index),
|
|
101
|
+
thin_prism: coefficient_row(options[:thin_prism_coeffs], camera_index)
|
|
102
|
+
)
|
|
103
|
+
else
|
|
104
|
+
Math::CameraProjection.pinhole_camera(
|
|
105
|
+
camera_means, camera_intrinsics, width, height
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
output[camera_index, true, true, true] = jacobians
|
|
109
|
+
end
|
|
110
|
+
output
|
|
111
|
+
end
|
|
112
|
+
private_class_method :projection_jacobians
|
|
113
|
+
|
|
114
|
+
def coefficient_row(value, camera_index)
|
|
115
|
+
value && value[camera_index, true].to_a
|
|
116
|
+
end
|
|
117
|
+
private_class_method :coefficient_row
|
|
118
|
+
|
|
119
|
+
# rubocop:disable Metrics/ParameterLists
|
|
120
|
+
def validate_output_gradients(type, means2d, grad_means2d, grad_depths, grad_conics, grad_compensations)
|
|
121
|
+
# rubocop:enable Metrics/ParameterLists
|
|
122
|
+
leading_shape = means2d.shape[0...-1]
|
|
123
|
+
[
|
|
124
|
+
cast_gradient(type, grad_means2d, means2d.shape),
|
|
125
|
+
cast_gradient(type, grad_depths, leading_shape),
|
|
126
|
+
cast_gradient(type, grad_conics, leading_shape + [3]),
|
|
127
|
+
grad_compensations && cast_gradient(type, grad_compensations, leading_shape)
|
|
128
|
+
]
|
|
129
|
+
end
|
|
130
|
+
private_class_method :validate_output_gradients
|
|
131
|
+
|
|
132
|
+
def cast_gradient(type, gradient, expected_shape)
|
|
133
|
+
value = type.cast(gradient)
|
|
134
|
+
unless value.shape == expected_shape
|
|
135
|
+
raise ShapeError, "expected output gradient #{expected_shape.inspect}, got #{value.shape.inspect}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
value
|
|
139
|
+
end
|
|
140
|
+
private_class_method :cast_gradient
|
|
141
|
+
|
|
142
|
+
# rubocop:disable Metrics/ParameterLists
|
|
143
|
+
def projection_vjp(means, covars, covars2d, jacobians, intrinsics, gradients, eps2d, width, height,
|
|
144
|
+
options)
|
|
145
|
+
# rubocop:enable Metrics/ParameterLists
|
|
146
|
+
grad_means2d, grad_depths, grad_conics, grad_compensations = gradients
|
|
147
|
+
grad_covars2d = covariance_output_vjp(
|
|
148
|
+
covars2d,
|
|
149
|
+
grad_conics,
|
|
150
|
+
grad_compensations,
|
|
151
|
+
eps2d
|
|
152
|
+
)
|
|
153
|
+
count = means.shape[0] * means.shape[1]
|
|
154
|
+
jacobian_flat = jacobians.reshape(count, 2, 3)
|
|
155
|
+
covariance_flat = covars.reshape(count, 3, 3)
|
|
156
|
+
gradient_flat = grad_covars2d.reshape(count, 2, 2)
|
|
157
|
+
grad_camera_covars = Math::Mat.matmul_batch(
|
|
158
|
+
Math::Mat.matmul_batch(jacobian_flat.transpose(0, 2, 1), gradient_flat),
|
|
159
|
+
jacobian_flat
|
|
160
|
+
).reshape(*covars.shape)
|
|
161
|
+
grad_jacobians = jacobian_vjp(jacobian_flat, covariance_flat, gradient_flat)
|
|
162
|
+
grad_camera_means = camera_mean_vjp(
|
|
163
|
+
means,
|
|
164
|
+
intrinsics,
|
|
165
|
+
grad_means2d,
|
|
166
|
+
grad_depths,
|
|
167
|
+
grad_jacobians.reshape(*jacobians.shape),
|
|
168
|
+
width,
|
|
169
|
+
height,
|
|
170
|
+
options
|
|
171
|
+
)
|
|
172
|
+
[grad_camera_means, grad_camera_covars]
|
|
173
|
+
end
|
|
174
|
+
private_class_method :projection_vjp
|
|
175
|
+
|
|
176
|
+
def jacobian_vjp(jacobians, covars, gradient)
|
|
177
|
+
first = Math::Mat.matmul_batch(
|
|
178
|
+
Math::Mat.matmul_batch(gradient, jacobians),
|
|
179
|
+
covars.transpose(0, 2, 1)
|
|
180
|
+
)
|
|
181
|
+
second = Math::Mat.matmul_batch(
|
|
182
|
+
Math::Mat.matmul_batch(gradient.transpose(0, 2, 1), jacobians),
|
|
183
|
+
covars
|
|
184
|
+
)
|
|
185
|
+
first + second
|
|
186
|
+
end
|
|
187
|
+
private_class_method :jacobian_vjp
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Backend
|
|
5
|
+
# Covariance-output VJP helpers for RubyProjectionBackward.
|
|
6
|
+
module RubyProjectionBackward
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/AbcSize
|
|
10
|
+
def covariance_output_vjp(covars2d, grad_conics, grad_compensations, eps2d)
|
|
11
|
+
regularized = covars2d.dup
|
|
12
|
+
regularized[true, true, 0, 0] += eps2d
|
|
13
|
+
regularized[true, true, 1, 1] += eps2d
|
|
14
|
+
grad_regularized = conic_vjp(regularized, grad_conics)
|
|
15
|
+
return grad_regularized unless grad_compensations
|
|
16
|
+
|
|
17
|
+
original_det = Math::Mat.det2x2(covars2d)
|
|
18
|
+
regularized_det = Math::Mat.det2x2(regularized)
|
|
19
|
+
ratio = original_det / regularized_det
|
|
20
|
+
positive = ratio.gt(0)
|
|
21
|
+
compensation = ratio.class.zeros(*ratio.shape)
|
|
22
|
+
compensation[positive] = ratio[positive]**0.5 if positive.any?
|
|
23
|
+
grad_original_det = ratio.class.zeros(*ratio.shape)
|
|
24
|
+
if positive.any?
|
|
25
|
+
grad_original_det[positive] = grad_compensations[positive] /
|
|
26
|
+
(2 * compensation[positive] * regularized_det[positive])
|
|
27
|
+
end
|
|
28
|
+
grad_regularized_det = -grad_compensations * compensation / (2 * regularized_det)
|
|
29
|
+
grad_regularized + determinant_vjp(regularized, grad_regularized_det) +
|
|
30
|
+
determinant_vjp(covars2d, grad_original_det)
|
|
31
|
+
end
|
|
32
|
+
# rubocop:enable Metrics/AbcSize
|
|
33
|
+
private_class_method :covariance_output_vjp
|
|
34
|
+
|
|
35
|
+
# rubocop:disable Metrics/AbcSize
|
|
36
|
+
def conic_vjp(covariance, gradient)
|
|
37
|
+
count = gradient.size / 3
|
|
38
|
+
matrices = covariance.reshape(count, 2, 2)
|
|
39
|
+
determinant = Math::Mat.det2x2(matrices)
|
|
40
|
+
inverse = matrices.class.zeros(count, 2, 2)
|
|
41
|
+
inverse[true, 0, 0] = matrices[true, 1, 1] / determinant
|
|
42
|
+
inverse[true, 0, 1] = -matrices[true, 0, 1] / determinant
|
|
43
|
+
inverse[true, 1, 0] = -matrices[true, 1, 0] / determinant
|
|
44
|
+
inverse[true, 1, 1] = matrices[true, 0, 0] / determinant
|
|
45
|
+
grad_flat = gradient.reshape(count, 3)
|
|
46
|
+
grad_inverse = matrices.class.zeros(count, 2, 2)
|
|
47
|
+
grad_inverse[true, 0, 0] = grad_flat[true, 0]
|
|
48
|
+
grad_inverse[true, 0, 1] = grad_flat[true, 1] * 0.5
|
|
49
|
+
grad_inverse[true, 1, 0] = grad_flat[true, 1] * 0.5
|
|
50
|
+
grad_inverse[true, 1, 1] = grad_flat[true, 2]
|
|
51
|
+
inverse_transpose = inverse.transpose(0, 2, 1)
|
|
52
|
+
result = -Math::Mat.matmul_batch(
|
|
53
|
+
Math::Mat.matmul_batch(inverse_transpose, grad_inverse),
|
|
54
|
+
inverse_transpose
|
|
55
|
+
)
|
|
56
|
+
result.reshape(*covariance.shape)
|
|
57
|
+
end
|
|
58
|
+
# rubocop:enable Metrics/AbcSize
|
|
59
|
+
private_class_method :conic_vjp
|
|
60
|
+
|
|
61
|
+
def determinant_vjp(matrix, gradient)
|
|
62
|
+
output = matrix.class.zeros(*matrix.shape)
|
|
63
|
+
output[true, true, 0, 0] = gradient * matrix[true, true, 1, 1]
|
|
64
|
+
output[true, true, 0, 1] = -gradient * matrix[true, true, 1, 0]
|
|
65
|
+
output[true, true, 1, 0] = -gradient * matrix[true, true, 0, 1]
|
|
66
|
+
output[true, true, 1, 1] = gradient * matrix[true, true, 0, 0]
|
|
67
|
+
output
|
|
68
|
+
end
|
|
69
|
+
private_class_method :determinant_vjp
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|