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,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Backend
|
|
5
|
+
# Input-side VJP helpers for RubyProjectionBackward.
|
|
6
|
+
module RubyProjectionBackward
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/ParameterLists
|
|
10
|
+
def camera_mean_vjp(means, intrinsics, grad_means2d, grad_depths, grad_jacobians, width, height,
|
|
11
|
+
options)
|
|
12
|
+
# rubocop:enable Metrics/ParameterLists
|
|
13
|
+
output = means.class.zeros(*means.shape)
|
|
14
|
+
# rubocop:disable Metrics/BlockLength
|
|
15
|
+
means.shape[0].times do |camera_index|
|
|
16
|
+
camera_means = means[camera_index, true, true]
|
|
17
|
+
camera_intrinsics = intrinsics[camera_index, true, true]
|
|
18
|
+
output_gradient = grad_means2d[camera_index, true, true]
|
|
19
|
+
camera_model = options.fetch(:camera_model)
|
|
20
|
+
gradient = if camera_model.to_s == "ortho"
|
|
21
|
+
ortho_mean_vjp(camera_means, camera_intrinsics, output_gradient)
|
|
22
|
+
elsif extended_camera?(options)
|
|
23
|
+
distorted_mean_vjp(
|
|
24
|
+
camera_means,
|
|
25
|
+
camera_intrinsics,
|
|
26
|
+
output_gradient,
|
|
27
|
+
grad_jacobians[camera_index, true, true, true],
|
|
28
|
+
options,
|
|
29
|
+
camera_index
|
|
30
|
+
)
|
|
31
|
+
else
|
|
32
|
+
projected_mean_vjp(camera_means, camera_intrinsics, output_gradient) +
|
|
33
|
+
pinhole_jacobian_vjp(
|
|
34
|
+
camera_means,
|
|
35
|
+
camera_intrinsics,
|
|
36
|
+
grad_jacobians[camera_index, true, true, true],
|
|
37
|
+
width,
|
|
38
|
+
height
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
gradient = add_depth_vjp(
|
|
42
|
+
gradient,
|
|
43
|
+
camera_means,
|
|
44
|
+
grad_depths[camera_index, true],
|
|
45
|
+
options.fetch(:global_z_order, true)
|
|
46
|
+
)
|
|
47
|
+
output[camera_index, true, true] = gradient
|
|
48
|
+
end
|
|
49
|
+
# rubocop:enable Metrics/BlockLength
|
|
50
|
+
output
|
|
51
|
+
end
|
|
52
|
+
private_class_method :camera_mean_vjp
|
|
53
|
+
|
|
54
|
+
def add_depth_vjp(gradient, means, grad_depth, global_z_order)
|
|
55
|
+
if global_z_order
|
|
56
|
+
gradient[true, 2] += grad_depth
|
|
57
|
+
return gradient
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
distance = (means**2).sum(axis: 1)**0.5
|
|
61
|
+
safe_distance = distance.dup
|
|
62
|
+
safe_distance[safe_distance.lt(1e-12)] = 1 if safe_distance.lt(1e-12).any?
|
|
63
|
+
gradient + (means * (grad_depth / safe_distance).reshape(means.shape[0], 1))
|
|
64
|
+
end
|
|
65
|
+
private_class_method :add_depth_vjp
|
|
66
|
+
|
|
67
|
+
def extended_camera?(options)
|
|
68
|
+
options.fetch(:camera_model).to_s == "fisheye" ||
|
|
69
|
+
%i[radial_coeffs tangential_coeffs thin_prism_coeffs].any? { |key| options[key] }
|
|
70
|
+
end
|
|
71
|
+
private_class_method :extended_camera?
|
|
72
|
+
|
|
73
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
|
|
74
|
+
def distorted_mean_vjp(means, intrinsics, grad_projected, grad_jacobians, options, camera_index)
|
|
75
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ParameterLists
|
|
76
|
+
output = means.class.zeros(*means.shape)
|
|
77
|
+
epsilon = means.instance_of?(Numo::DFloat) ? 1e-4 : 2e-3
|
|
78
|
+
means.shape[0].times do |gaussian_index|
|
|
79
|
+
point = means[gaussian_index, true].to_a
|
|
80
|
+
3.times do |axis|
|
|
81
|
+
positive = point.dup
|
|
82
|
+
negative = point.dup
|
|
83
|
+
positive[axis] += epsilon
|
|
84
|
+
negative[axis] -= epsilon
|
|
85
|
+
positive_outputs = distorted_point_outputs(positive, intrinsics, options, camera_index, means.class)
|
|
86
|
+
negative_outputs = distorted_point_outputs(negative, intrinsics, options, camera_index, means.class)
|
|
87
|
+
projected_derivative = (positive_outputs[0] - negative_outputs[0]) / (2 * epsilon)
|
|
88
|
+
jacobian_derivative = (positive_outputs[1] - negative_outputs[1]) / (2 * epsilon)
|
|
89
|
+
output[gaussian_index, axis] =
|
|
90
|
+
(projected_derivative * grad_projected[gaussian_index, true]).sum +
|
|
91
|
+
(jacobian_derivative * grad_jacobians[gaussian_index, true, true]).sum
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
output
|
|
95
|
+
end
|
|
96
|
+
private_class_method :distorted_mean_vjp
|
|
97
|
+
|
|
98
|
+
def distorted_point_outputs(point, intrinsics, options, camera_index, type)
|
|
99
|
+
projected, jacobians = Math::CameraDistortion.project_camera(
|
|
100
|
+
type.cast([point]),
|
|
101
|
+
intrinsics,
|
|
102
|
+
options.fetch(:camera_model),
|
|
103
|
+
radial: coefficient_row(options[:radial_coeffs], camera_index),
|
|
104
|
+
tangential: coefficient_row(options[:tangential_coeffs], camera_index),
|
|
105
|
+
thin_prism: coefficient_row(options[:thin_prism_coeffs], camera_index)
|
|
106
|
+
)
|
|
107
|
+
[projected.flatten, jacobians.reshape(2, 3)]
|
|
108
|
+
end
|
|
109
|
+
private_class_method :distorted_point_outputs
|
|
110
|
+
|
|
111
|
+
# rubocop:disable Metrics/AbcSize
|
|
112
|
+
def projected_mean_vjp(means, intrinsics, gradient)
|
|
113
|
+
x_coord = means[true, 0]
|
|
114
|
+
y_coord = means[true, 1]
|
|
115
|
+
z_coord = means[true, 2]
|
|
116
|
+
homogeneous_x = (intrinsics[0, 0] * x_coord) + (intrinsics[0, 1] * y_coord) +
|
|
117
|
+
(intrinsics[0, 2] * z_coord)
|
|
118
|
+
homogeneous_y = (intrinsics[1, 0] * x_coord) + (intrinsics[1, 1] * y_coord) +
|
|
119
|
+
(intrinsics[1, 2] * z_coord)
|
|
120
|
+
grad_x = gradient[true, 0]
|
|
121
|
+
grad_y = gradient[true, 1]
|
|
122
|
+
output = means.class.zeros(*means.shape)
|
|
123
|
+
output[true, 0] = (
|
|
124
|
+
(grad_x * intrinsics[0, 0]) + (grad_y * intrinsics[1, 0])
|
|
125
|
+
) / z_coord
|
|
126
|
+
output[true, 1] = (
|
|
127
|
+
(grad_x * intrinsics[0, 1]) + (grad_y * intrinsics[1, 1])
|
|
128
|
+
) / z_coord
|
|
129
|
+
output[true, 2] = (
|
|
130
|
+
(grad_x * ((intrinsics[0, 2] * z_coord) - homogeneous_x)) +
|
|
131
|
+
(grad_y * ((intrinsics[1, 2] * z_coord) - homogeneous_y))
|
|
132
|
+
) / (z_coord**2)
|
|
133
|
+
output
|
|
134
|
+
end
|
|
135
|
+
# rubocop:enable Metrics/AbcSize
|
|
136
|
+
private_class_method :projected_mean_vjp
|
|
137
|
+
|
|
138
|
+
def ortho_mean_vjp(means, intrinsics, gradient)
|
|
139
|
+
output = means.class.zeros(*means.shape)
|
|
140
|
+
output[true, 0] = gradient[true, 0] * intrinsics[0, 0]
|
|
141
|
+
output[true, 1] = gradient[true, 1] * intrinsics[1, 1]
|
|
142
|
+
output
|
|
143
|
+
end
|
|
144
|
+
private_class_method :ortho_mean_vjp
|
|
145
|
+
|
|
146
|
+
# rubocop:disable Metrics/AbcSize
|
|
147
|
+
def pinhole_jacobian_vjp(means, intrinsics, gradient, width, height)
|
|
148
|
+
x_coord = means[true, 0]
|
|
149
|
+
y_coord = means[true, 1]
|
|
150
|
+
z_coord = means[true, 2]
|
|
151
|
+
focal_x = intrinsics[0, 0]
|
|
152
|
+
focal_y = intrinsics[1, 1]
|
|
153
|
+
ratio_x = x_coord / z_coord
|
|
154
|
+
ratio_y = y_coord / z_coord
|
|
155
|
+
min_x = -((intrinsics[0, 2] / focal_x) + (0.15 * width / focal_x))
|
|
156
|
+
max_x = ((width - intrinsics[0, 2]) / focal_x) + (0.15 * width / focal_x)
|
|
157
|
+
min_y = -((intrinsics[1, 2] / focal_y) + (0.15 * height / focal_y))
|
|
158
|
+
max_y = ((height - intrinsics[1, 2]) / focal_y) + (0.15 * height / focal_y)
|
|
159
|
+
clipped_x, inside_x = clipped_ratio_and_mask(ratio_x, min_x, max_x)
|
|
160
|
+
clipped_y, inside_y = clipped_ratio_and_mask(ratio_y, min_y, max_y)
|
|
161
|
+
inside_x = means.class.cast(inside_x)
|
|
162
|
+
inside_y = means.class.cast(inside_y)
|
|
163
|
+
output = means.class.zeros(*means.shape)
|
|
164
|
+
output[true, 0] = gradient[true, 0, 2] * (-focal_x / (z_coord**2)) * inside_x
|
|
165
|
+
output[true, 1] = gradient[true, 1, 2] * (-focal_y / (z_coord**2)) * inside_y
|
|
166
|
+
derivative_x_z = (
|
|
167
|
+
(inside_x * 2 * focal_x * x_coord / (z_coord**3)) +
|
|
168
|
+
((1 - inside_x) * focal_x * clipped_x / (z_coord**2))
|
|
169
|
+
)
|
|
170
|
+
derivative_y_z = (
|
|
171
|
+
(inside_y * 2 * focal_y * y_coord / (z_coord**3)) +
|
|
172
|
+
((1 - inside_y) * focal_y * clipped_y / (z_coord**2))
|
|
173
|
+
)
|
|
174
|
+
output[true, 2] = (
|
|
175
|
+
(gradient[true, 0, 0] * (-focal_x / (z_coord**2))) +
|
|
176
|
+
(gradient[true, 1, 1] * (-focal_y / (z_coord**2))) +
|
|
177
|
+
(gradient[true, 0, 2] * derivative_x_z) +
|
|
178
|
+
(gradient[true, 1, 2] * derivative_y_z)
|
|
179
|
+
)
|
|
180
|
+
output
|
|
181
|
+
end
|
|
182
|
+
# rubocop:enable Metrics/AbcSize
|
|
183
|
+
private_class_method :pinhole_jacobian_vjp
|
|
184
|
+
|
|
185
|
+
def clipped_ratio_and_mask(ratio, minimum, maximum)
|
|
186
|
+
clipped = ratio.dup
|
|
187
|
+
below = ratio.lt(minimum)
|
|
188
|
+
above = ratio.gt(maximum)
|
|
189
|
+
clipped[below] = minimum if below.any?
|
|
190
|
+
clipped[above] = maximum if above.any?
|
|
191
|
+
[clipped, below.eq(0) & above.eq(0)]
|
|
192
|
+
end
|
|
193
|
+
private_class_method :clipped_ratio_and_mask
|
|
194
|
+
|
|
195
|
+
def world_to_cam_vjp(grad_means, grad_covars, viewmats)
|
|
196
|
+
gaussian_count = grad_means.shape[1]
|
|
197
|
+
world_means = grad_means.class.zeros(gaussian_count, 3)
|
|
198
|
+
world_covars = grad_means.class.zeros(gaussian_count, 3, 3)
|
|
199
|
+
grad_means.shape[0].times do |camera_index|
|
|
200
|
+
rotation = viewmats[camera_index, 0...3, 0...3]
|
|
201
|
+
rotation_batch = grad_means.class.zeros(gaussian_count, 3, 3)
|
|
202
|
+
rotation_batch[true, true, true] = rotation
|
|
203
|
+
world_means += grad_means[camera_index, true, true].dot(rotation)
|
|
204
|
+
world_covars += Math::Mat.matmul_batch(
|
|
205
|
+
Math::Mat.matmul_batch(rotation_batch.transpose(0, 2, 1),
|
|
206
|
+
grad_covars[camera_index, true, true, true]),
|
|
207
|
+
rotation_batch
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
[world_means, world_covars]
|
|
211
|
+
end
|
|
212
|
+
private_class_method :world_to_cam_vjp
|
|
213
|
+
|
|
214
|
+
# rubocop:disable Metrics/ParameterLists
|
|
215
|
+
def input_covariance_gradients(means, covars, quaternions, scales, grad_means, grad_covars)
|
|
216
|
+
# rubocop:enable Metrics/ParameterLists
|
|
217
|
+
if covars
|
|
218
|
+
return [
|
|
219
|
+
grad_means,
|
|
220
|
+
format_covariance_gradient(grad_covars, covars.shape, means.shape[0]),
|
|
221
|
+
nil,
|
|
222
|
+
nil
|
|
223
|
+
]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
grad_quaternions, grad_scales = RubyQuatScaleToCovarPreci.backward(
|
|
227
|
+
means.class.cast(quaternions),
|
|
228
|
+
means.class.cast(scales),
|
|
229
|
+
grad_covars,
|
|
230
|
+
nil,
|
|
231
|
+
triu: false
|
|
232
|
+
)
|
|
233
|
+
[grad_means, nil, grad_quaternions, grad_scales]
|
|
234
|
+
end
|
|
235
|
+
private_class_method :input_covariance_gradients
|
|
236
|
+
|
|
237
|
+
def format_covariance_gradient(gradient, input_shape, gaussian_count)
|
|
238
|
+
return gradient if input_shape == [gaussian_count, 3, 3]
|
|
239
|
+
|
|
240
|
+
output = gradient.class.zeros(gaussian_count, 6)
|
|
241
|
+
output[true, 0] = gradient[true, 0, 0]
|
|
242
|
+
output[true, 1] = gradient[true, 0, 1] + gradient[true, 1, 0]
|
|
243
|
+
output[true, 2] = gradient[true, 0, 2] + gradient[true, 2, 0]
|
|
244
|
+
output[true, 3] = gradient[true, 1, 1]
|
|
245
|
+
output[true, 4] = gradient[true, 1, 2] + gradient[true, 2, 1]
|
|
246
|
+
output[true, 5] = gradient[true, 2, 2]
|
|
247
|
+
output
|
|
248
|
+
end
|
|
249
|
+
private_class_method :format_covariance_gradient
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Backend
|
|
5
|
+
# Numo implementation of quaternion/scale covariance conversion.
|
|
6
|
+
module RubyQuatScaleToCovarPreci
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def forward(quaternions, scales, compute_covar:, compute_preci:, triu:)
|
|
10
|
+
quaternions, scales, leading_shape = validate_inputs(quaternions, scales, compute_preci)
|
|
11
|
+
count = quaternions.size / 4
|
|
12
|
+
rotations = Math::Quaternion.to_rotmat(quaternions).reshape(count, 3, 3)
|
|
13
|
+
scales_flat = scales.reshape(count, 3)
|
|
14
|
+
covariance = covariance_matrix(rotations, scales_flat) if compute_covar
|
|
15
|
+
precision = precision_matrix(rotations, scales_flat) if compute_preci
|
|
16
|
+
[
|
|
17
|
+
format_output(covariance, leading_shape, triu),
|
|
18
|
+
format_output(precision, leading_shape, triu)
|
|
19
|
+
]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def backward(quaternions, scales, grad_covar, grad_preci, triu:)
|
|
23
|
+
quaternions, scales, leading_shape = validate_inputs(quaternions, scales, !grad_preci.nil?)
|
|
24
|
+
count = quaternions.size / 4
|
|
25
|
+
rotations = Math::Quaternion.to_rotmat(quaternions).reshape(count, 3, 3)
|
|
26
|
+
scales_flat = scales.reshape(count, 3)
|
|
27
|
+
grad_rotations = quaternions.class.zeros(count, 3, 3)
|
|
28
|
+
grad_scales = quaternions.class.zeros(count, 3)
|
|
29
|
+
if grad_covar
|
|
30
|
+
accumulate_covar_gradients!(
|
|
31
|
+
grad_rotations,
|
|
32
|
+
grad_scales,
|
|
33
|
+
rotations,
|
|
34
|
+
scales_flat,
|
|
35
|
+
expand_gradient(grad_covar, leading_shape, triu, quaternions.class)
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
if grad_preci
|
|
39
|
+
accumulate_preci_gradients!(
|
|
40
|
+
grad_rotations,
|
|
41
|
+
grad_scales,
|
|
42
|
+
rotations,
|
|
43
|
+
scales_flat,
|
|
44
|
+
expand_gradient(grad_preci, leading_shape, triu, quaternions.class)
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
grad_quaternions = Math::Quaternion.to_rotmat_vjp(
|
|
48
|
+
quaternions,
|
|
49
|
+
grad_rotations.reshape(*(leading_shape + [3, 3]))
|
|
50
|
+
)
|
|
51
|
+
[grad_quaternions, grad_scales.reshape(*(leading_shape + [3]))]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def validate_inputs(quaternions, scales, compute_preci)
|
|
55
|
+
unless quaternions.is_a?(Numo::NArray) && [Numo::SFloat, Numo::DFloat].include?(quaternions.class)
|
|
56
|
+
raise ArgumentError, "quaternions must be Numo::SFloat or Numo::DFloat"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
expected_scales = quaternions.shape[0...-1] + [3]
|
|
60
|
+
unless quaternions.ndim >= 1 && quaternions.shape[-1] == 4 && scales.shape == expected_scales
|
|
61
|
+
raise ShapeError,
|
|
62
|
+
"expected quaternions [...,4] and scales #{expected_scales.inspect}, " \
|
|
63
|
+
"got #{quaternions.shape.inspect} and #{scales.shape.inspect}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
scales = quaternions.class.cast(scales)
|
|
67
|
+
raise Gsplat::Error, "precision is undefined for zero scales" if compute_preci && scales.eq(0).any?
|
|
68
|
+
|
|
69
|
+
[quaternions, scales, quaternions.shape[0...-1]]
|
|
70
|
+
end
|
|
71
|
+
private_class_method :validate_inputs
|
|
72
|
+
|
|
73
|
+
def covariance_matrix(rotations, scales)
|
|
74
|
+
transform = rotations * scales.reshape(scales.shape[0], 1, 3)
|
|
75
|
+
Math::Mat.matmul_batch(transform, transform.transpose(0, 2, 1))
|
|
76
|
+
end
|
|
77
|
+
private_class_method :covariance_matrix
|
|
78
|
+
|
|
79
|
+
def precision_matrix(rotations, scales)
|
|
80
|
+
transform = rotations * (1.0 / scales).reshape(scales.shape[0], 1, 3)
|
|
81
|
+
Math::Mat.matmul_batch(transform, transform.transpose(0, 2, 1))
|
|
82
|
+
end
|
|
83
|
+
private_class_method :precision_matrix
|
|
84
|
+
|
|
85
|
+
def format_output(matrix, leading_shape, triu)
|
|
86
|
+
return nil unless matrix
|
|
87
|
+
return matrix.reshape(*(leading_shape + [3, 3])) unless triu
|
|
88
|
+
|
|
89
|
+
output = matrix.class.zeros(matrix.shape[0], 6)
|
|
90
|
+
[[0, 0], [0, 1], [0, 2], [1, 1], [1, 2], [2, 2]].each_with_index do |(row, column), index|
|
|
91
|
+
output[true, index] = matrix[true, row, column]
|
|
92
|
+
end
|
|
93
|
+
output.reshape(*(leading_shape + [6]))
|
|
94
|
+
end
|
|
95
|
+
private_class_method :format_output
|
|
96
|
+
|
|
97
|
+
def expand_gradient(gradient, leading_shape, triu, type)
|
|
98
|
+
gradient = type.cast(gradient)
|
|
99
|
+
expected = leading_shape + [triu ? 6 : 3, triu ? nil : 3].compact
|
|
100
|
+
unless gradient.shape == expected
|
|
101
|
+
raise ShapeError, "expected gradient #{expected.inspect}, got #{gradient.shape.inspect}"
|
|
102
|
+
end
|
|
103
|
+
return gradient.reshape(gradient.size / 9, 3, 3) unless triu
|
|
104
|
+
|
|
105
|
+
flat = gradient.reshape(gradient.size / 6, 6)
|
|
106
|
+
output = type.zeros(flat.shape[0], 3, 3)
|
|
107
|
+
[[0, 0], [1, 1], [2, 2]].each_with_index do |(row, column), diagonal_index|
|
|
108
|
+
output[true, row, column] = flat[true, [0, 3, 5].fetch(diagonal_index)]
|
|
109
|
+
end
|
|
110
|
+
[[0, 1, 1], [0, 2, 2], [1, 2, 4]].each do |row, column, index|
|
|
111
|
+
half = flat[true, index] * 0.5
|
|
112
|
+
output[true, row, column] = half
|
|
113
|
+
output[true, column, row] = half
|
|
114
|
+
end
|
|
115
|
+
output
|
|
116
|
+
end
|
|
117
|
+
private_class_method :expand_gradient
|
|
118
|
+
|
|
119
|
+
def accumulate_covar_gradients!(grad_rotations, grad_scales, rotations, scales, gradient)
|
|
120
|
+
symmetric = gradient + gradient.transpose(0, 2, 1)
|
|
121
|
+
transform = rotations * scales.reshape(scales.shape[0], 1, 3)
|
|
122
|
+
grad_transform = Math::Mat.matmul_batch(symmetric, transform)
|
|
123
|
+
grad_rotations[] = grad_rotations + (grad_transform * scales.reshape(scales.shape[0], 1, 3))
|
|
124
|
+
grad_scales[] = grad_scales + (grad_transform * rotations).sum(axis: 1)
|
|
125
|
+
end
|
|
126
|
+
private_class_method :accumulate_covar_gradients!
|
|
127
|
+
|
|
128
|
+
def accumulate_preci_gradients!(grad_rotations, grad_scales, rotations, scales, gradient)
|
|
129
|
+
inverse_scales = 1.0 / scales
|
|
130
|
+
symmetric = gradient + gradient.transpose(0, 2, 1)
|
|
131
|
+
transform = rotations * inverse_scales.reshape(scales.shape[0], 1, 3)
|
|
132
|
+
grad_transform = Math::Mat.matmul_batch(symmetric, transform)
|
|
133
|
+
grad_rotations[] = grad_rotations + (grad_transform * inverse_scales.reshape(scales.shape[0], 1, 3))
|
|
134
|
+
grad_scales[] = grad_scales - ((grad_transform * rotations).sum(axis: 1) / (scales**2))
|
|
135
|
+
end
|
|
136
|
+
private_class_method :accumulate_preci_gradients!
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Backend
|
|
5
|
+
# Reference implementation of contribution-index enumeration.
|
|
6
|
+
module RubyRasterizeToIndicesInRange
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/ParameterLists
|
|
10
|
+
def forward(range_start, range_end, transmittances, means2d, conics, opacities,
|
|
11
|
+
width, height, tile_size, isect_offsets, flatten_ids)
|
|
12
|
+
# rubocop:enable Metrics/ParameterLists
|
|
13
|
+
values = validate_inputs(
|
|
14
|
+
range_start, range_end, transmittances, means2d, conics, opacities,
|
|
15
|
+
width, height, tile_size, isect_offsets, flatten_ids
|
|
16
|
+
)
|
|
17
|
+
transmittances, means2d, conics, opacities, offsets, ids = values
|
|
18
|
+
gaussian_ids = []
|
|
19
|
+
pixel_ids = []
|
|
20
|
+
image_ids = []
|
|
21
|
+
means = means2d.reshape(means2d.shape[0] * means2d.shape[1], 2)
|
|
22
|
+
conic_values = conics.reshape(conics.shape[0] * conics.shape[1], 3)
|
|
23
|
+
opacity_values = opacities.flatten
|
|
24
|
+
means2d.shape[0].times do |image|
|
|
25
|
+
height.times do |row|
|
|
26
|
+
width.times do |column|
|
|
27
|
+
enumerate_pixel!(
|
|
28
|
+
gaussian_ids, pixel_ids, image_ids, range_start, range_end,
|
|
29
|
+
transmittances, means, conic_values, opacity_values, offsets, ids,
|
|
30
|
+
image, row, column, means2d.shape[1], width, tile_size
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
[Numo::Int64.cast(gaussian_ids), Numo::Int64.cast(pixel_ids), Numo::Int64.cast(image_ids)]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
39
|
+
def validate_inputs(range_start, range_end, transmittances, means2d, conics, opacities,
|
|
40
|
+
width, height, tile_size, isect_offsets, flatten_ids)
|
|
41
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
42
|
+
valid_range = range_start.is_a?(Integer) && range_end.is_a?(Integer) &&
|
|
43
|
+
range_start >= 0 && range_end >= range_start
|
|
44
|
+
raise ArgumentError, "range must be nonnegative integers with start <= end" unless valid_range
|
|
45
|
+
unless means2d.is_a?(Numo::NArray) && [Numo::SFloat, Numo::DFloat].include?(means2d.class)
|
|
46
|
+
raise ArgumentError, "means2d must be Numo::SFloat or Numo::DFloat"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
valid_means = means2d.ndim == 3 && means2d.shape[-1] == 2
|
|
50
|
+
raise ShapeError, "expected means2d [C,N,2]" unless valid_means
|
|
51
|
+
|
|
52
|
+
camera_count, gaussian_count = means2d.shape[0...2]
|
|
53
|
+
type = means2d.class
|
|
54
|
+
transmittances = type.cast(transmittances)
|
|
55
|
+
conics = type.cast(conics)
|
|
56
|
+
opacities = type.cast(opacities)
|
|
57
|
+
valid = transmittances.shape == [camera_count, height, width] &&
|
|
58
|
+
conics.shape == [camera_count, gaussian_count, 3] &&
|
|
59
|
+
opacities.shape == [camera_count, gaussian_count]
|
|
60
|
+
raise ShapeError, "invalid contribution-index tensor shapes" unless valid
|
|
61
|
+
|
|
62
|
+
offsets = Numo::Int32.cast(isect_offsets)
|
|
63
|
+
ids = Numo::Int32.cast(flatten_ids)
|
|
64
|
+
valid_offsets = offsets.ndim == 3 && offsets.shape[0] == camera_count
|
|
65
|
+
raise ShapeError, "expected isect_offsets [C,TH,TW]" unless valid_offsets
|
|
66
|
+
raise ShapeError, "expected flatten_ids [M]" unless ids.ndim == 1
|
|
67
|
+
|
|
68
|
+
RubyRasterizeToPixels.send(:validate_indices!, ids, camera_count * gaussian_count, offsets)
|
|
69
|
+
RubyRasterizeToPixels.send(
|
|
70
|
+
:validate_dimensions!, width, height, tile_size, offsets.shape[-1], offsets.shape[-2]
|
|
71
|
+
)
|
|
72
|
+
[transmittances, means2d, conics, opacities, offsets, ids]
|
|
73
|
+
end
|
|
74
|
+
private_class_method :validate_inputs
|
|
75
|
+
|
|
76
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
|
|
77
|
+
def enumerate_pixel!(gaussian_ids, pixel_ids, image_ids, range_start, range_end,
|
|
78
|
+
transmittances, means, conics, opacities, offsets, flatten_ids,
|
|
79
|
+
image, row, column, gaussian_count, width, tile_size)
|
|
80
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ParameterLists
|
|
81
|
+
range = intersection_batch(
|
|
82
|
+
offsets, flatten_ids, image, row, column, tile_size, range_start, range_end
|
|
83
|
+
)
|
|
84
|
+
transmittance = transmittances[image, row, column].to_f
|
|
85
|
+
range.each do |intersection|
|
|
86
|
+
flat_id = flatten_ids[intersection].to_i
|
|
87
|
+
delta_x = means[flat_id, 0].to_f - column - 0.5
|
|
88
|
+
delta_y = means[flat_id, 1].to_f - row - 0.5
|
|
89
|
+
conic = conics[flat_id, true]
|
|
90
|
+
sigma = (0.5 * ((conic[0] * delta_x * delta_x) + (conic[2] * delta_y * delta_y))) +
|
|
91
|
+
(conic[1] * delta_x * delta_y)
|
|
92
|
+
alpha = [opacities[flat_id].to_f * ::Math.exp(-sigma), RubyAccumulate::ALPHA_CLAMP].min
|
|
93
|
+
next if sigma.negative? || alpha < RubyAccumulate::ALPHA_SKIP
|
|
94
|
+
|
|
95
|
+
next_transmittance = transmittance * (1 - alpha)
|
|
96
|
+
break if next_transmittance <= RubyAccumulate::TRANSMITTANCE_STOP
|
|
97
|
+
|
|
98
|
+
gaussian_ids << (flat_id % gaussian_count)
|
|
99
|
+
pixel_ids << ((row * width) + column)
|
|
100
|
+
image_ids << image
|
|
101
|
+
transmittance = next_transmittance
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
private_class_method :enumerate_pixel!
|
|
105
|
+
|
|
106
|
+
# rubocop:disable Metrics/ParameterLists
|
|
107
|
+
def intersection_batch(offsets, flatten_ids, image, row, column, tile_size, first_batch, last_batch)
|
|
108
|
+
# rubocop:enable Metrics/ParameterLists
|
|
109
|
+
tile_height, tile_width = offsets.shape[-2..]
|
|
110
|
+
flat_tile = (((image * tile_height) + (row / tile_size)) * tile_width) + (column / tile_size)
|
|
111
|
+
tile_start = offsets[flat_tile].to_i
|
|
112
|
+
tile_end = flat_tile + 1 < offsets.size ? offsets[flat_tile + 1].to_i : flatten_ids.size
|
|
113
|
+
batch_size = tile_size * tile_size
|
|
114
|
+
start_index = [tile_start + (first_batch * batch_size), tile_end].min
|
|
115
|
+
end_index = [tile_start + (last_batch * batch_size), tile_end].min
|
|
116
|
+
start_index...end_index
|
|
117
|
+
end
|
|
118
|
+
private_class_method :intersection_batch
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|