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,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