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,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gsplat
4
+ module Optim
5
+ # Adam variant that updates only visible first-axis parameter rows.
6
+ class SelectiveAdam < Adam
7
+ # Updates visible rows while leaving hidden parameters and moments intact.
8
+ #
9
+ # @param visibility [Numo::Bit, Numo::NArray, Array<Boolean>] mask shaped [N]
10
+ # @return [SelectiveAdam] self
11
+ def step(visibility)
12
+ mask = validate_visibility(visibility)
13
+ groups.each_value do |group|
14
+ gradient = group.variable.grad
15
+ next unless gradient
16
+
17
+ update_group(group, gradient, mask)
18
+ end
19
+ self
20
+ end
21
+
22
+ private
23
+
24
+ # rubocop:disable Metrics/AbcSize
25
+ def update_group(group, gradient, mask)
26
+ state = state_for(group.name)
27
+ next_step = state.step + 1
28
+ candidate_mean = (beta1 * state.exp_avg) + ((1 - beta1) * gradient)
29
+ candidate_variance = (beta2 * state.exp_avg_sq) + ((1 - beta2) * (gradient**2))
30
+ selection = first_axis_selection(mask, gradient.ndim)
31
+ exp_avg = state.exp_avg.dup
32
+ exp_avg_sq = state.exp_avg_sq.dup
33
+ exp_avg[*selection] = candidate_mean[*selection]
34
+ exp_avg_sq[*selection] = candidate_variance[*selection]
35
+ corrected_mean = exp_avg / (1 - (beta1**next_step))
36
+ corrected_variance = exp_avg_sq / (1 - (beta2**next_step))
37
+ update = group.lr * corrected_mean / ((corrected_variance**0.5) + group.eps)
38
+ group.variable.data[*selection] = group.variable.data[*selection] - update[*selection]
39
+ @states[group.name] = State.new(
40
+ step: next_step,
41
+ exp_avg: exp_avg,
42
+ exp_avg_sq: exp_avg_sq
43
+ )
44
+ end
45
+ # rubocop:enable Metrics/AbcSize
46
+
47
+ def validate_visibility(visibility)
48
+ mask = Numo::Bit.cast(visibility)
49
+ expected = groups.values.first.variable.data.shape[0]
50
+ unless mask.ndim == 1 && mask.shape[0] == expected
51
+ raise ShapeError, "expected visibility [#{expected}], got #{mask.shape.inspect}"
52
+ end
53
+
54
+ groups.each_value do |group|
55
+ next if group.variable.data.shape[0] == expected
56
+
57
+ raise ShapeError, "all SelectiveAdam groups must share first-axis size #{expected}"
58
+ end
59
+
60
+ mask
61
+ end
62
+
63
+ def first_axis_selection(mask, dimensions)
64
+ [mask] + Array.new(dimensions - 1, true)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ # High-level differentiable rendering API.
4
+ module Gsplat
5
+ # Rendering pipeline composed from low-level operations.
6
+ module Rasterization
7
+ module_function
8
+
9
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists, Naming/MethodParameterName
10
+ def render(means:, opacities:, colors:, viewmats:, ks:, width:, height:, quats: nil, scales: nil,
11
+ covars: nil, near_plane: 0.01, far_plane: 1e10, radius_clip: 0.0, eps2d: 0.3,
12
+ sh_degree: nil, tile_size: 16, backgrounds: nil, render_mode: "RGB",
13
+ rasterize_mode: "classic", camera_model: "pinhole", absgrad: false,
14
+ channel_chunk: 32, packed: false, global_z_order: true,
15
+ radial_coeffs: nil, tangential_coeffs: nil, thin_prism_coeffs: nil,
16
+ with_eval3d: false, return_normals: false)
17
+ # rubocop:enable Metrics/ParameterLists, Naming/MethodParameterName
18
+ RasterizationValidation.validate_options!(
19
+ render_mode, rasterize_mode, sh_degree, tile_size, channel_chunk, width, height
20
+ )
21
+ RasterizationValidation.warn_unsupported_options(packed, global_z_order)
22
+ RasterizationValidation.validate_scene!(
23
+ means, quats, scales, covars, opacities, colors, viewmats, ks, sh_degree
24
+ )
25
+ RasterizationValidation.validate_eval3d!(
26
+ with_eval3d, return_normals, covars, rasterize_mode
27
+ )
28
+ camera_count = Ops::TensorOps.data(viewmats).shape[0]
29
+ calculate_compensations = rasterize_mode == "antialiased"
30
+ radii, means2d, depths, conics, compensations = Gsplat.fully_fused_projection(
31
+ means,
32
+ viewmats: viewmats,
33
+ ks: ks,
34
+ width: width,
35
+ height: height,
36
+ covars: covars,
37
+ quats: quats,
38
+ scales: scales,
39
+ eps2d: eps2d,
40
+ near_plane: near_plane,
41
+ far_plane: far_plane,
42
+ radius_clip: radius_clip,
43
+ calc_compensations: calculate_compensations,
44
+ camera_model: camera_model,
45
+ radial_coeffs: radial_coeffs,
46
+ tangential_coeffs: tangential_coeffs,
47
+ thin_prism_coeffs: thin_prism_coeffs,
48
+ global_z_order: global_z_order
49
+ )
50
+ projected_colors = RasterizationHelpers.prepare_colors(
51
+ means, colors, viewmats, radii, camera_count, sh_degree
52
+ )
53
+ projected_opacities = RasterizationHelpers.camera_broadcast(opacities, camera_count)
54
+ if calculate_compensations
55
+ projected_opacities = Ops::TensorOps.apply(
56
+ Ops::Multiply,
57
+ projected_opacities,
58
+ compensations
59
+ )
60
+ end
61
+ raster_features, depth_index = RasterizationHelpers.render_features(
62
+ projected_colors, depths, render_mode
63
+ )
64
+ tile_width = (width.to_f / tile_size).ceil
65
+ tile_height = (height.to_f / tile_size).ceil
66
+ tiles_per_gauss, isect_ids, flatten_ids = Gsplat.isect_tiles(
67
+ Ops::TensorOps.data(means2d),
68
+ Ops::TensorOps.data(radii),
69
+ Ops::TensorOps.data(depths),
70
+ tile_size,
71
+ tile_width,
72
+ tile_height
73
+ )
74
+ isect_offsets = Gsplat.isect_offset_encode(
75
+ isect_ids, camera_count, tile_width, tile_height
76
+ )
77
+ hit_distance = %w[d Ed RGB-d RGB-Ed].include?(render_mode)
78
+ eval3d_normals = nil
79
+ render_colors, render_alphas = if with_eval3d || hit_distance
80
+ eval3d_outputs = RasterizationHelpers.rasterize_eval3d_features(
81
+ means, quats, scales, raster_features, projected_opacities,
82
+ viewmats, ks, width, height, tile_size, isect_offsets,
83
+ flatten_ids, backgrounds: backgrounds,
84
+ camera_model: camera_model,
85
+ use_hit_distance: hit_distance,
86
+ return_normals: return_normals
87
+ )
88
+ eval3d_normals = eval3d_outputs[2] if return_normals
89
+ eval3d_outputs.first(2)
90
+ else
91
+ RasterizationHelpers.rasterize_features(
92
+ means2d, conics, raster_features, projected_opacities,
93
+ width, height, tile_size, isect_offsets, flatten_ids,
94
+ backgrounds: backgrounds, channel_chunk: channel_chunk,
95
+ absgrad: absgrad
96
+ )
97
+ end
98
+ if %w[Ed ED RGB-Ed RGB+ED].include?(render_mode)
99
+ render_colors = Ops::TensorOps.apply(
100
+ Ops::NormalizeDepth,
101
+ render_colors,
102
+ render_alphas,
103
+ depth_index: depth_index
104
+ )
105
+ end
106
+ meta = RasterizationHelpers.metadata(
107
+ radii, means2d, depths, conics, projected_opacities, tile_width, tile_height,
108
+ tiles_per_gauss, isect_ids, flatten_ids, isect_offsets, width, height, tile_size,
109
+ camera_count
110
+ )
111
+ meta[:normals] = eval3d_normals if return_normals
112
+ means2d.bind_absgrad(meta, :means2d_absgrad) if absgrad && means2d.is_a?(Autograd::Variable)
113
+ [render_colors, render_alphas, meta]
114
+ end
115
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
116
+ end
117
+
118
+ class << self
119
+ # Renders a dense batch of cameras with differentiable 3D Gaussians.
120
+ #
121
+ # Geometry uses float32/float64 arrays `means [N,3]`, `quats [N,4]`,
122
+ # `scales [N,3]`; colors are `[N,D]` or SH `[N,K,D]`, views are
123
+ # `[C,4,4]`, and intrinsics are `[C,3,3]`. Activated opacities are `[N]`.
124
+ #
125
+ # @return [Array] rendered colors `[C,H,W,X]`, alphas `[C,H,W,1]`, and metadata Hash
126
+ def rasterization(**)
127
+ Rasterization.render(**)
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Public 2D Gaussian splatting API.
4
+ module Gsplat
5
+ # High-level 2D Gaussian rendering composition.
6
+ module Rasterization2DGS
7
+ module_function
8
+
9
+ # rubocop:disable Metrics/ParameterLists, Naming/MethodParameterName
10
+ def render(means:, quats:, scales:, opacities:, colors:, viewmats:, ks:, width:, height:,
11
+ near_plane: 0.01, far_plane: 1e10, radius_clip: 0.0, eps2d: 0.3,
12
+ sh_degree: nil, packed: false, tile_size: 16, backgrounds: nil,
13
+ render_mode: "RGB", sparse_grad: false, absgrad: false, distloss: false,
14
+ depth_mode: "expected", rasterize_mode: "classic")
15
+ # rubocop:enable Metrics/ParameterLists, Naming/MethodParameterName
16
+ validate_options!(sparse_grad, depth_mode, rasterize_mode)
17
+ primary, alphas, meta = Gsplat.rasterization(
18
+ means: means, quats: quats, scales: scales, opacities: opacities, colors: colors,
19
+ viewmats: viewmats, ks: ks, width: width, height: height, near_plane: near_plane,
20
+ far_plane: far_plane, radius_clip: radius_clip, eps2d: eps2d, sh_degree: sh_degree,
21
+ packed: packed, tile_size: tile_size, backgrounds: backgrounds,
22
+ render_mode: render_mode, absgrad: absgrad, rasterize_mode: rasterize_mode
23
+ )
24
+ gaussian_normals = camera_normals(
25
+ Ops::TensorOps.data(means), Ops::TensorOps.data(quats), Ops::TensorOps.data(viewmats)
26
+ )
27
+ render_normals, = Gsplat.rasterization(
28
+ means: means, quats: quats, scales: scales, opacities: opacities,
29
+ colors: gaussian_normals, viewmats: viewmats, ks: ks, width: width, height: height,
30
+ near_plane: near_plane, far_plane: far_plane, radius_clip: radius_clip, eps2d: eps2d,
31
+ tile_size: tile_size, render_mode: "RGB", absgrad: false
32
+ )
33
+ render_median, = Gsplat.rasterization(
34
+ means: means, quats: quats, scales: scales, opacities: opacities,
35
+ colors: gaussian_normals, viewmats: viewmats, ks: ks, width: width, height: height,
36
+ near_plane: near_plane, far_plane: far_plane, radius_clip: radius_clip, eps2d: eps2d,
37
+ tile_size: tile_size, render_mode: "ED", absgrad: false
38
+ )
39
+ surface_normals = normalized_normals(Ops::TensorOps.data(render_normals), Ops::TensorOps.data(alphas))
40
+ render_distort = distortion_image(
41
+ Ops::TensorOps.data(render_median), Ops::TensorOps.data(alphas), distloss
42
+ )
43
+ meta = two_d_metadata(meta, gaussian_normals, render_distort)
44
+ [primary, alphas, render_normals, surface_normals, render_distort, render_median, meta]
45
+ end
46
+
47
+ def validate_options!(sparse_grad, depth_mode, rasterize_mode)
48
+ raise ArgumentError, "sparse_grad requires packed mode and is unsupported" if sparse_grad
49
+ raise ArgumentError, "2DGS only supports classic rasterization" unless rasterize_mode == "classic"
50
+ return if %w[expected median].include?(depth_mode.to_s)
51
+
52
+ raise ArgumentError, "depth_mode must be \"expected\" or \"median\""
53
+ end
54
+ private_class_method :validate_options!
55
+
56
+ def camera_normals(means, quats, viewmats)
57
+ rotations = Math::Quaternion.to_rotmat(quats)
58
+ world_normals = rotations[true, true, 2]
59
+ output = means.class.zeros(viewmats.shape[0], means.shape[0], 3)
60
+ viewmats.shape[0].times do |camera|
61
+ view_rotation = viewmats[camera, 0...3, 0...3]
62
+ camera_means = means.dot(view_rotation.transpose) + viewmats[camera, 0...3, 3]
63
+ normals = world_normals.dot(view_rotation.transpose)
64
+ facing_away = (normals * camera_means).sum(axis: 1).gt(0)
65
+ normals[facing_away, true] = -normals[facing_away, true] if facing_away.any?
66
+ output[camera, true, true] = normals
67
+ end
68
+ output
69
+ end
70
+ private_class_method :camera_normals
71
+
72
+ def normalized_normals(normals, alphas)
73
+ alpha = alphas[true, true, true, 0]
74
+ output = normals.class.zeros(*normals.shape)
75
+ valid = alpha.gt(1e-10)
76
+ return output unless valid.any?
77
+
78
+ expected = normals.dup
79
+ 3.times { |axis| expected[true, true, true, axis][valid] /= alpha[valid] }
80
+ norm = (expected**2).sum(axis: 3)**0.5
81
+ valid &= norm.gt(1e-10)
82
+ 3.times { |axis| output[true, true, true, axis][valid] = expected[true, true, true, axis][valid] / norm[valid] }
83
+ output
84
+ end
85
+ private_class_method :normalized_normals
86
+
87
+ def distortion_image(median, alphas, enabled)
88
+ return median.class.zeros(*median.shape) unless enabled
89
+
90
+ alpha = alphas[true, true, true, 0]
91
+ depth = median[true, true, true, 0]
92
+ output = median.class.zeros(*median.shape)
93
+ output[true, true, true, 0] = (depth * (1 - alpha)).abs
94
+ output
95
+ end
96
+ private_class_method :distortion_image
97
+
98
+ def two_d_metadata(meta, normals, distortion)
99
+ ray_transforms = normals.class.zeros(*(normals.shape[0...-1] + [3, 3]))
100
+ ray_transforms[true, true, true, true] = normals.class.eye(3)
101
+ meta.merge(
102
+ ray_transforms: ray_transforms,
103
+ normals: normals,
104
+ render_distort: distortion,
105
+ gradient_2dgs: meta.fetch(:means2d)
106
+ )
107
+ end
108
+ private_class_method :two_d_metadata
109
+ end
110
+
111
+ class << self
112
+ # Rasterizes oriented 2D Gaussian surfels.
113
+ #
114
+ # @return [Array] colors, alphas, normals, surface normals, distortion, median depth, metadata
115
+ def rasterization_2dgs(**)
116
+ Rasterization2DGS.render(**)
117
+ end
118
+
119
+ # Projection metadata used by the 2DGS rasterizer.
120
+ # rubocop:disable Metrics/ParameterLists, Naming/MethodParameterName
121
+ def fully_fused_projection_2dgs(means, quats:, scales:, viewmats:, ks:, width:, height:,
122
+ eps2d: 0.3, near_plane: 0.01, far_plane: 1e10,
123
+ radius_clip: 0.0)
124
+ # rubocop:enable Metrics/ParameterLists, Naming/MethodParameterName
125
+ radii, means2d, depths, conics, = fully_fused_projection(
126
+ means, quats: quats, scales: scales, viewmats: viewmats, ks: ks, width: width,
127
+ height: height, eps2d: eps2d, near_plane: near_plane, far_plane: far_plane,
128
+ radius_clip: radius_clip
129
+ )
130
+ normals = Rasterization2DGS.send(
131
+ :camera_normals, Ops::TensorOps.data(means), Ops::TensorOps.data(quats),
132
+ Ops::TensorOps.data(viewmats)
133
+ )
134
+ ray_transforms = Ops::TensorOps.data(conics).class.zeros(
135
+ *(Ops::TensorOps.data(conics).shape[0...-1] + [3, 3])
136
+ )
137
+ [radii, means2d, depths, ray_transforms, normals]
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gsplat
4
+ # Feature preparation and metadata helpers for the high-level renderer.
5
+ module RasterizationHelpers
6
+ module_function
7
+
8
+ def camera_broadcast(value, camera_count)
9
+ shape = Ops::TensorOps.data(value).shape
10
+ return value if shape[0] == camera_count && shape.length >= 2
11
+
12
+ Ops::TensorOps.apply(Ops::CameraBroadcast, value, camera_count)
13
+ end
14
+
15
+ # rubocop:disable Metrics/ParameterLists
16
+ def prepare_colors(means, colors, viewmats, radii, camera_count, sh_degree)
17
+ # rubocop:enable Metrics/ParameterLists
18
+ unless sh_degree
19
+ return colors if Ops::TensorOps.data(colors).ndim == 3
20
+
21
+ return Ops::TensorOps.apply(Ops::CameraBroadcast, colors, camera_count)
22
+ end
23
+
24
+ directions = Ops::TensorOps.apply(Ops::CameraDirections, means, viewmats)
25
+ coefficients = Ops::TensorOps.apply(Ops::CameraBroadcast, colors, camera_count)
26
+ evaluated = Gsplat.spherical_harmonics(
27
+ sh_degree,
28
+ directions,
29
+ coefficients,
30
+ masks: visible_radii(radii)
31
+ )
32
+ Ops::TensorOps.apply(Ops::AddClampMin, evaluated, offset: 0.5, minimum: 0.0)
33
+ end
34
+
35
+ def visible_radii(radii)
36
+ values = Ops::TensorOps.data(radii)
37
+ return values.gt(0) unless values.ndim >= 3 && values.shape[-1] == 2
38
+
39
+ values.min(axis: values.ndim - 1).gt(0)
40
+ end
41
+
42
+ def render_features(colors, depths, render_mode)
43
+ return [colors, nil] if render_mode == "RGB"
44
+
45
+ depth_features = Ops::TensorOps.apply(Ops::DepthFeatures, depths)
46
+ return [depth_features, 0] if %w[d Ed D ED].include?(render_mode)
47
+
48
+ feature_count = Ops::TensorOps.data(colors).shape[-1]
49
+ [Ops::TensorOps.apply(Ops::ConcatDepth, colors, depth_features), feature_count]
50
+ end
51
+
52
+ # rubocop:disable Metrics/ParameterLists
53
+ def rasterize_features(means2d, conics, features, opacities, width, height, tile_size, offsets, flatten_ids,
54
+ backgrounds:, channel_chunk:, absgrad:)
55
+ # rubocop:enable Metrics/ParameterLists
56
+ channel_count = Ops::TensorOps.data(features).shape[-1]
57
+ if channel_count <= channel_chunk
58
+ return Gsplat.rasterize_to_pixels(
59
+ means2d,
60
+ conics,
61
+ features,
62
+ opacities,
63
+ width,
64
+ height,
65
+ tile_size,
66
+ offsets,
67
+ flatten_ids,
68
+ backgrounds: backgrounds,
69
+ absgrad: absgrad
70
+ )
71
+ end
72
+
73
+ rasterize_feature_chunks(
74
+ means2d, conics, features, opacities, width, height, tile_size, offsets, flatten_ids,
75
+ backgrounds: backgrounds, channel_chunk: channel_chunk, absgrad: absgrad
76
+ )
77
+ end
78
+
79
+ # rubocop:disable Metrics/ParameterLists
80
+ def rasterize_feature_chunks(means2d, conics, features, opacities, width, height,
81
+ tile_size, offsets, flatten_ids,
82
+ backgrounds:, channel_chunk:, absgrad:)
83
+ # rubocop:enable Metrics/ParameterLists
84
+ channel_count = Ops::TensorOps.data(features).shape[-1]
85
+ rendered_chunks = []
86
+ alpha = nil
87
+ (0...channel_count).step(channel_chunk) do |start|
88
+ range = start...[start + channel_chunk, channel_count].min
89
+ feature_chunk = Ops::TensorOps.apply(Ops::FeatureSlice, features, range)
90
+ background_chunk = backgrounds && Ops::TensorOps.apply(Ops::FeatureSlice, backgrounds, range)
91
+ rendered, chunk_alpha = Gsplat.rasterize_to_pixels(
92
+ means2d,
93
+ conics,
94
+ feature_chunk,
95
+ opacities,
96
+ width,
97
+ height,
98
+ tile_size,
99
+ offsets,
100
+ flatten_ids,
101
+ backgrounds: background_chunk,
102
+ absgrad: absgrad
103
+ )
104
+ rendered_chunks << rendered
105
+ alpha ||= chunk_alpha
106
+ end
107
+ [Ops::TensorOps.apply(Ops::ConcatFeatures, *rendered_chunks), alpha]
108
+ end
109
+ private_class_method :rasterize_feature_chunks
110
+
111
+ # rubocop:disable Metrics/ParameterLists
112
+ def rasterize_eval3d_features(means, quats, scales, features, opacities, viewmats, intrinsics,
113
+ width, height, tile_size, offsets, flatten_ids, backgrounds:,
114
+ camera_model:, use_hit_distance:, return_normals:)
115
+ # rubocop:enable Metrics/ParameterLists
116
+ raise ArgumentError, "world-space rendering requires quats and scales" unless quats && scales
117
+
118
+ inputs = [means, quats, scales, features, opacities, backgrounds]
119
+ options = {
120
+ viewmats: Ops::TensorOps.data(viewmats),
121
+ intrinsics: Ops::TensorOps.data(intrinsics),
122
+ width: width,
123
+ height: height,
124
+ tile_size: tile_size,
125
+ offsets: offsets,
126
+ flatten_ids: flatten_ids,
127
+ camera_model: camera_model.to_s,
128
+ use_hit_distance: use_hit_distance,
129
+ return_normals: return_normals
130
+ }
131
+ return Ops::Eval3dRasterize.apply(*inputs, **options) if inputs.any?(Autograd::Variable)
132
+
133
+ Backend::RubyEval3dRasterizer.forward(*inputs, **options)
134
+ end
135
+
136
+ # rubocop:disable Metrics/ParameterLists
137
+ def metadata(radii, means2d, depths, conics, opacities, tile_width, tile_height,
138
+ tiles_per_gauss, isect_ids, flatten_ids, isect_offsets, width, height,
139
+ tile_size, camera_count)
140
+ # rubocop:enable Metrics/ParameterLists
141
+ {
142
+ camera_ids: nil,
143
+ gaussian_ids: nil,
144
+ radii: radii,
145
+ means2d: means2d,
146
+ depths: depths,
147
+ conics: conics,
148
+ opacities: opacities,
149
+ tile_width: tile_width,
150
+ tile_height: tile_height,
151
+ tiles_per_gauss: tiles_per_gauss,
152
+ isect_ids: isect_ids,
153
+ flatten_ids: flatten_ids,
154
+ isect_offsets: isect_offsets,
155
+ width: width,
156
+ height: height,
157
+ tile_size: tile_size,
158
+ n_cameras: camera_count
159
+ }
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gsplat
4
+ # Input and option validation for the high-level renderer.
5
+ module RasterizationValidation
6
+ RENDER_MODES = %w[RGB d Ed D ED RGB-d RGB-Ed RGB+D RGB+ED].freeze
7
+ RASTERIZE_MODES = %w[classic antialiased].freeze
8
+
9
+ module_function
10
+
11
+ # rubocop:disable Metrics/ParameterLists
12
+ def validate_options!(render_mode, rasterize_mode, sh_degree, tile_size, channel_chunk, width, height)
13
+ # rubocop:enable Metrics/ParameterLists
14
+ raise ArgumentError, "unsupported render_mode #{render_mode.inspect}" unless RENDER_MODES.include?(render_mode)
15
+ unless RASTERIZE_MODES.include?(rasterize_mode)
16
+ raise ArgumentError, "unsupported rasterize_mode #{rasterize_mode.inspect}"
17
+ end
18
+
19
+ unless sh_degree.nil? || (0..4).cover?(sh_degree)
20
+ raise ArgumentError, "sh_degree must be nil or an integer from 0 through 4"
21
+ end
22
+
23
+ values = { width: width, height: height, tile_size: tile_size, channel_chunk: channel_chunk }
24
+ invalid = values.find { |_name, value| !value.is_a?(Integer) || !value.positive? }
25
+ raise ArgumentError, "#{invalid[0]} must be a positive integer" if invalid
26
+ end
27
+
28
+ def warn_unsupported_options(packed, _global_z_order)
29
+ Gsplat.logger.warn("packed mode is unsupported; using dense mode") if packed
30
+ end
31
+
32
+ def validate_eval3d!(with_eval3d, return_normals, covars, rasterize_mode)
33
+ raise ArgumentError, "return_normals requires with_eval3d: true" if return_normals && !with_eval3d
34
+ raise ArgumentError, "with_eval3d requires quats and scales instead of covars" if with_eval3d && covars
35
+ return unless with_eval3d && rasterize_mode != "classic"
36
+
37
+ raise ArgumentError, "with_eval3d requires rasterize_mode: \"classic\""
38
+ end
39
+
40
+ # rubocop:disable Metrics/ParameterLists
41
+ def validate_scene!(means, quats, scales, covars, opacities, colors, viewmats, intrinsics, sh_degree)
42
+ # rubocop:enable Metrics/ParameterLists
43
+ means_shape = data(means).shape
44
+ unless means_shape.length == 2 && means_shape[-1] == 3
45
+ raise ShapeError, "expected means [N,3], got #{means_shape.inspect}"
46
+ end
47
+
48
+ gaussian_count = means_shape[0]
49
+ opacity_shape = data(opacities).shape
50
+ unless [[gaussian_count], [data(viewmats).shape[0], gaussian_count]].include?(opacity_shape)
51
+ raise ShapeError, "expected opacities [N] or [C,N], got #{opacity_shape.inspect}"
52
+ end
53
+
54
+ validate_geometry!(quats, scales, covars, gaussian_count)
55
+ camera_count = data(viewmats).shape[0]
56
+ unless data(viewmats).shape == [camera_count, 4, 4] && data(intrinsics).shape == [camera_count, 3, 3]
57
+ raise ShapeError,
58
+ "expected viewmats [C,4,4] and ks [C,3,3], " \
59
+ "got #{data(viewmats).shape.inspect} and #{data(intrinsics).shape.inspect}"
60
+ end
61
+
62
+ validate_color_shape!(data(colors).shape, gaussian_count, camera_count, sh_degree)
63
+ end
64
+
65
+ def validate_geometry!(quats, scales, covars, gaussian_count)
66
+ if covars
67
+ covar_shape = data(covars).shape
68
+ return if [[gaussian_count, 3, 3], [gaussian_count, 6]].include?(covar_shape)
69
+
70
+ raise ShapeError, "expected covars [N,3,3] or [N,6], got #{covar_shape.inspect}"
71
+ end
72
+ return if data(quats).shape == [gaussian_count, 4] && data(scales).shape == [gaussian_count, 3]
73
+
74
+ raise ShapeError,
75
+ "expected quats [N,4] and scales [N,3], " \
76
+ "got #{data(quats).shape.inspect} and #{data(scales).shape.inspect}"
77
+ end
78
+ private_class_method :validate_geometry!
79
+
80
+ def validate_color_shape!(shape, gaussian_count, camera_count, sh_degree)
81
+ valid = if sh_degree
82
+ shape.length == 3 && shape[0] == gaussian_count && shape[1] >= ((sh_degree + 1)**2)
83
+ else
84
+ (shape.length == 2 && shape[0] == gaussian_count) ||
85
+ (shape.length == 3 && shape[0...2] == [camera_count, gaussian_count])
86
+ end
87
+ return if valid
88
+
89
+ expected = sh_degree ? "[N,K,D] with K >= #{(sh_degree + 1)**2}" : "[N,D] or [C,N,D]"
90
+ raise ShapeError, "expected colors #{expected}, got #{shape.inspect}"
91
+ end
92
+ private_class_method :validate_color_shape!
93
+
94
+ def data(value)
95
+ Ops::TensorOps.data(value)
96
+ end
97
+ private_class_method :data
98
+ end
99
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gsplat
4
+ module Strategy
5
+ # Shared strategy lifecycle and parameter/optimizer validation.
6
+ class Base
7
+ # Parameter names required by every structural strategy.
8
+ REQUIRED_KEYS = %i[means quats scales opacities sh0 shN].freeze
9
+
10
+ def initialize_state(scene_scale:)
11
+ raise ArgumentError, "scene_scale must be positive" unless scene_scale.positive?
12
+
13
+ { scene_scale: scene_scale }
14
+ end
15
+
16
+ # rubocop:disable Metrics/AbcSize, Naming/PredicateMethod
17
+ def check_sanity(params, optimizers)
18
+ missing_params = REQUIRED_KEYS - params.keys
19
+ missing_optimizers = REQUIRED_KEYS - optimizers.keys
20
+ raise ArgumentError, "missing params: #{missing_params.join(', ')}" unless missing_params.empty?
21
+ raise ArgumentError, "missing optimizers: #{missing_optimizers.join(', ')}" unless missing_optimizers.empty?
22
+
23
+ count = nil
24
+ REQUIRED_KEYS.each do |key|
25
+ variable = params.fetch(key)
26
+ raise ArgumentError, "params[:#{key}] must be an Autograd::Variable" unless variable.is_a?(Autograd::Variable)
27
+
28
+ count ||= variable.data.shape[0]
29
+ unless variable.data.ndim.positive? && variable.data.shape[0] == count
30
+ raise ShapeError, "all params must share first-axis size #{count}; #{key}=#{variable.data.shape.inspect}"
31
+ end
32
+
33
+ optimizer = optimizers.fetch(key)
34
+ unless optimizer.is_a?(Optim::Adam) &&
35
+ optimizer.groups.values.any? { |group| group.variable.equal?(variable) }
36
+ raise ArgumentError, "optimizers[:#{key}] must optimize params[:#{key}]"
37
+ end
38
+ end
39
+ true
40
+ end
41
+ # rubocop:enable Metrics/AbcSize, Naming/PredicateMethod
42
+
43
+ def step_pre_backward(params:, optimizers:, state:, step:, info:)
44
+ [params, optimizers, state, step, info]
45
+ nil
46
+ end
47
+ end
48
+ end
49
+ end