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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +202 -0
  3. data/README.md +236 -0
  4. data/docs/ACCEPTANCE.md +60 -0
  5. data/docs/BENCHMARKS.md +79 -0
  6. data/docs/DECISIONS.md +52 -0
  7. data/docs/MIGRATION.md +117 -0
  8. data/docs/PROFILE.md +75 -0
  9. data/docs/PROGRESS.md +199 -0
  10. data/docs/decisions/0000-template.md +17 -0
  11. data/docs/decisions/0001-pin-golden-reference-and-cpu-validation.md +27 -0
  12. data/docs/decisions/0002-retain-ruby-fallbacks-for-native-backend.md +26 -0
  13. data/docs/decisions/0003-share-projection-for-distorted-cameras.md +26 -0
  14. data/docs/decisions/0004-use-portable-world-space-reference-paths.md +26 -0
  15. data/docs/decisions/0005-reuse-ewa-core-for-2dgs.md +24 -0
  16. data/docs/decisions/0006-keep-eval3d-as-portable-reference.md +25 -0
  17. data/docs/decisions/0007-share-compositor-semantics-for-contribution-indices.md +25 -0
  18. data/examples/data/README.md +14 -0
  19. data/examples/data/colmap/images/view_000.png +0 -0
  20. data/examples/data/colmap/images/view_001.png +0 -0
  21. data/examples/data/colmap/images/view_002.png +0 -0
  22. data/examples/data/colmap/sparse/0/cameras.txt +2 -0
  23. data/examples/data/colmap/sparse/0/images.txt +6 -0
  24. data/examples/data/colmap/sparse/0/points3D.txt +17 -0
  25. data/examples/data/splats.ply +0 -0
  26. data/examples/fit_image.rb +35 -0
  27. data/examples/generate_sample_data.rb +147 -0
  28. data/examples/render_path.rb +117 -0
  29. data/examples/simple_trainer.rb +76 -0
  30. data/ext/gsplat_native/common.h +54 -0
  31. data/ext/gsplat_native/extconf.rb +39 -0
  32. data/ext/gsplat_native/gsplat_native.c +60 -0
  33. data/ext/gsplat_native/intersections.c +211 -0
  34. data/ext/gsplat_native/projection.c +199 -0
  35. data/ext/gsplat_native/raster_backward.c +129 -0
  36. data/ext/gsplat_native/raster_backward_bridge.c +69 -0
  37. data/ext/gsplat_native/raster_forward.c +150 -0
  38. data/ext/gsplat_native/rasterization.h +51 -0
  39. data/ext/gsplat_native/spherical_harmonics.c +129 -0
  40. data/gsplat.gemspec +36 -0
  41. data/lib/gsplat/autograd/context.rb +60 -0
  42. data/lib/gsplat/autograd/function.rb +68 -0
  43. data/lib/gsplat/autograd/variable.rb +159 -0
  44. data/lib/gsplat/backend/ruby/accumulate.rb +139 -0
  45. data/lib/gsplat/backend/ruby/accumulate_backward.rb +40 -0
  46. data/lib/gsplat/backend/ruby/eval3d_rasterizer.rb +175 -0
  47. data/lib/gsplat/backend/ruby/isect_tiles.rb +198 -0
  48. data/lib/gsplat/backend/ruby/projection.rb +251 -0
  49. data/lib/gsplat/backend/ruby/projection_backward.rb +190 -0
  50. data/lib/gsplat/backend/ruby/projection_covariance_vjp.rb +72 -0
  51. data/lib/gsplat/backend/ruby/projection_input_vjp.rb +252 -0
  52. data/lib/gsplat/backend/ruby/quat_scale_to_covar_preci.rb +139 -0
  53. data/lib/gsplat/backend/ruby/rasterize_to_indices_in_range.rb +121 -0
  54. data/lib/gsplat/backend/ruby/rasterize_to_pixels.rb +199 -0
  55. data/lib/gsplat/backend/ruby/rasterize_to_pixels_backward.rb +121 -0
  56. data/lib/gsplat/backend/ruby/spherical_harmonics.rb +135 -0
  57. data/lib/gsplat/backend/ruby/tile_compositor.rb +50 -0
  58. data/lib/gsplat/backend/ruby/tile_compositor_backward.rb +84 -0
  59. data/lib/gsplat/backend.rb +79 -0
  60. data/lib/gsplat/compression/grid_sort.rb +79 -0
  61. data/lib/gsplat/compression/kmeans.rb +121 -0
  62. data/lib/gsplat/compression/png.rb +147 -0
  63. data/lib/gsplat/compression/png_codec.rb +133 -0
  64. data/lib/gsplat/compression/quantizer.rb +79 -0
  65. data/lib/gsplat/io/checkpoint.rb +145 -0
  66. data/lib/gsplat/io/colmap.rb +175 -0
  67. data/lib/gsplat/io/colmap_binary.rb +98 -0
  68. data/lib/gsplat/io/colmap_text.rb +84 -0
  69. data/lib/gsplat/io/image.rb +63 -0
  70. data/lib/gsplat/io/image_backends.rb +81 -0
  71. data/lib/gsplat/io/npy.rb +189 -0
  72. data/lib/gsplat/io/ply.rb +185 -0
  73. data/lib/gsplat/io/ply_reader.rb +142 -0
  74. data/lib/gsplat/io/zip_archive.rb +183 -0
  75. data/lib/gsplat/math/camera_distortion.rb +123 -0
  76. data/lib/gsplat/math/camera_projection.rb +202 -0
  77. data/lib/gsplat/math/mat.rb +114 -0
  78. data/lib/gsplat/math/quaternion.rb +175 -0
  79. data/lib/gsplat/math/small_matrix_primitives.rb +112 -0
  80. data/lib/gsplat/math/spherical_harmonic_basis.rb +148 -0
  81. data/lib/gsplat/math/ssim.rb +153 -0
  82. data/lib/gsplat/native.rb +30 -0
  83. data/lib/gsplat/native_ops.rb +148 -0
  84. data/lib/gsplat/native_raster_ops.rb +76 -0
  85. data/lib/gsplat/ops/accumulate.rb +68 -0
  86. data/lib/gsplat/ops/eval3d_rasterize.rb +62 -0
  87. data/lib/gsplat/ops/isect_tiles.rb +38 -0
  88. data/lib/gsplat/ops/projection.rb +154 -0
  89. data/lib/gsplat/ops/quat_scale_to_covar_preci.rb +118 -0
  90. data/lib/gsplat/ops/rasterize_to_indices_in_range.rb +38 -0
  91. data/lib/gsplat/ops/rasterize_to_pixels.rb +105 -0
  92. data/lib/gsplat/ops/relocation.rb +94 -0
  93. data/lib/gsplat/ops/spherical_harmonics.rb +71 -0
  94. data/lib/gsplat/ops/tensor_shape_ops.rb +173 -0
  95. data/lib/gsplat/ops/tensor_value_ops.rb +150 -0
  96. data/lib/gsplat/optim/adam.rb +212 -0
  97. data/lib/gsplat/optim/lr_scheduler.rb +36 -0
  98. data/lib/gsplat/optim/selective_adam.rb +68 -0
  99. data/lib/gsplat/rasterization.rb +130 -0
  100. data/lib/gsplat/rasterization_2dgs.rb +140 -0
  101. data/lib/gsplat/rasterization_helpers.rb +162 -0
  102. data/lib/gsplat/rasterization_validation.rb +99 -0
  103. data/lib/gsplat/strategy/base.rb +49 -0
  104. data/lib/gsplat/strategy/default.rb +188 -0
  105. data/lib/gsplat/strategy/mcmc.rb +103 -0
  106. data/lib/gsplat/strategy/mcmc_ops.rb +143 -0
  107. data/lib/gsplat/strategy/ops.rb +165 -0
  108. data/lib/gsplat/training/config.rb +91 -0
  109. data/lib/gsplat/training/image_fitter.rb +158 -0
  110. data/lib/gsplat/training/losses.rb +191 -0
  111. data/lib/gsplat/training/scene.rb +116 -0
  112. data/lib/gsplat/training/trainer.rb +236 -0
  113. data/lib/gsplat/utils.rb +110 -0
  114. data/lib/gsplat/version.rb +6 -0
  115. data/lib/gsplat.rb +98 -0
  116. 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