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,35 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require "gsplat"
6
+
7
+ options = {
8
+ width: 64,
9
+ height: 64,
10
+ n_gaussians: 5_000,
11
+ steps: 300,
12
+ learning_rate: 20.0
13
+ }
14
+ parser = OptionParser.new do |options_parser|
15
+ options_parser.banner = "Usage: bundle exec ruby examples/fit_image.rb [options]"
16
+ options_parser.on("--width N", Integer) { |value| options[:width] = value }
17
+ options_parser.on("--height N", Integer) { |value| options[:height] = value }
18
+ options_parser.on("--gaussians N", Integer) { |value| options[:n_gaussians] = value }
19
+ options_parser.on("--steps N", Integer) { |value| options[:steps] = value }
20
+ options_parser.on("--learning-rate X", Float) { |value| options[:learning_rate] = value }
21
+ options_parser.on_tail("-h", "--help", "Show this help") do
22
+ puts options_parser
23
+ exit
24
+ end
25
+ end
26
+ parser.parse!
27
+
28
+ steps = options.delete(:steps)
29
+ result = Gsplat::Training::ImageFitter.new(**options).fit(steps: steps)
30
+ warn format(
31
+ "fit complete: PSNR %<initial>.2f dB -> %<final>.2f dB (%<steps>d steps)",
32
+ initial: result.initial_psnr,
33
+ final: result.final_psnr,
34
+ steps: steps
35
+ )
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "optparse"
6
+ require "gsplat"
7
+
8
+ # Builds the deterministic miniature scene used by the runnable examples.
9
+ module SampleData
10
+ WIDTH = 16
11
+ HEIGHT = 16
12
+ CAMERA_TRANSLATIONS = [-0.12, 0.0, 0.12].freeze
13
+
14
+ module_function
15
+
16
+ def generate(root)
17
+ sparse_directory = File.join(root, "colmap", "sparse", "0")
18
+ image_directory = File.join(root, "colmap", "images")
19
+ FileUtils.mkdir_p([sparse_directory, image_directory])
20
+
21
+ parameters, colors = gaussian_parameters
22
+ views, intrinsics = cameras
23
+ images, = Gsplat.rasterization(
24
+ means: parameters.fetch(:means),
25
+ quats: parameters.fetch(:quats),
26
+ scales: Numo::NMath.exp(parameters.fetch(:scales)),
27
+ opacities: sigmoid(parameters.fetch(:opacities)),
28
+ colors: parameters.fetch(:sh0),
29
+ viewmats: views,
30
+ ks: intrinsics,
31
+ width: WIDTH,
32
+ height: HEIGHT,
33
+ sh_degree: 0
34
+ )
35
+
36
+ write_colmap_model(sparse_directory, colors, parameters.fetch(:means))
37
+ write_images(image_directory, images)
38
+ Gsplat::IO::Ply.write(File.join(root, "splats.ply"), parameters)
39
+ end
40
+
41
+ def gaussian_parameters
42
+ means = Numo::SFloat.zeros(16, 3)
43
+ colors = Numo::SFloat.zeros(16, 3)
44
+ 16.times do |index|
45
+ x_coord = ((index % 4) * 2) + 0.5
46
+ y_coord = ((index / 4) * 2) + 0.5
47
+ fraction = index.fdiv(15)
48
+ means[index, true] = [(x_coord - 4) / 4.0, (y_coord - 4) / 4.0, 2]
49
+ colors[index, true] = [fraction, 0.2 + (0.5 * fraction), 1 - fraction]
50
+ end
51
+
52
+ parameters = {
53
+ means: means,
54
+ quats: identity_quaternions(16),
55
+ scales: Numo::SFloat.ones(16, 3) * Math.log(0.2),
56
+ opacities: Numo::SFloat.ones(16) * Math.log(0.85 / 0.15),
57
+ sh0: Gsplat::Utils.rgb_to_sh(colors).reshape(16, 1, 3),
58
+ shN: Numo::SFloat.zeros(16, 0, 3)
59
+ }
60
+ [parameters, colors]
61
+ end
62
+ private_class_method :gaussian_parameters
63
+
64
+ def cameras
65
+ count = CAMERA_TRANSLATIONS.length
66
+ views = Numo::SFloat.zeros(count, 4, 4)
67
+ intrinsics = Numo::SFloat.zeros(count, 3, 3)
68
+ count.times do |index|
69
+ views[index, true, true] = Numo::SFloat.eye(4)
70
+ views[index, 0, 3] = CAMERA_TRANSLATIONS.fetch(index)
71
+ intrinsics[index, true, true] = Numo::SFloat[[16, 0, 8], [0, 16, 8], [0, 0, 1]]
72
+ end
73
+ [views, intrinsics]
74
+ end
75
+ private_class_method :cameras
76
+
77
+ def identity_quaternions(count)
78
+ quaternions = Numo::SFloat.zeros(count, 4)
79
+ quaternions[true, 0] = 1
80
+ quaternions
81
+ end
82
+ private_class_method :identity_quaternions
83
+
84
+ def sigmoid(values)
85
+ 1.0 / (1.0 + Numo::NMath.exp(-values))
86
+ end
87
+ private_class_method :sigmoid
88
+
89
+ def write_colmap_model(directory, colors, means)
90
+ File.write(
91
+ File.join(directory, "cameras.txt"),
92
+ "# One PINHOLE camera shared by all sample views.\n1 PINHOLE #{WIDTH} #{HEIGHT} 16 16 8 8\n"
93
+ )
94
+ image_records = CAMERA_TRANSLATIONS.each_index.map do |index|
95
+ format(
96
+ "%<id>d 1 0 0 0 %<tx>.6f 0 0 1 view_%<frame>03d.png\n",
97
+ id: index + 1,
98
+ tx: CAMERA_TRANSLATIONS[index],
99
+ frame: index
100
+ )
101
+ end
102
+ File.write(
103
+ File.join(directory, "images.txt"),
104
+ "# Each registered image is followed by an empty POINTS2D line.\n#{image_records.join("\n")}"
105
+ )
106
+ point_records = means.shape[0].times.map do |index|
107
+ rgb = colors[index, true].to_a.map { |value| (value * 255).round }
108
+ format(
109
+ "%<id>d %<x>.9g %<y>.9g %<z>.9g %<r>d %<g>d %<b>d 0",
110
+ id: index + 1,
111
+ x: means[index, 0],
112
+ y: means[index, 1],
113
+ z: means[index, 2],
114
+ r: rgb[0],
115
+ g: rgb[1],
116
+ b: rgb[2]
117
+ )
118
+ end
119
+ File.write(File.join(directory, "points3D.txt"), "# Synthetic colored seed points.\n#{point_records.join("\n")}\n")
120
+ end
121
+ private_class_method :write_colmap_model
122
+
123
+ def write_images(directory, images)
124
+ CAMERA_TRANSLATIONS.each_index do |index|
125
+ path = File.join(directory, format("view_%03d.png", index))
126
+ Gsplat::IO::Image.write(path, images[index, true, true, true], backend: :chunky_png)
127
+ end
128
+ end
129
+ private_class_method :write_images
130
+ end
131
+
132
+ options = { output: File.expand_path("data", __dir__) }
133
+ parser = OptionParser.new do |options_parser|
134
+ options_parser.banner = "Usage: bundle exec ruby examples/generate_sample_data.rb [options]"
135
+ options_parser.on("--output PATH", "Destination directory (default: examples/data)") do |value|
136
+ options[:output] = value
137
+ end
138
+ options_parser.on_tail("-h", "--help", "Show this help") do
139
+ puts options_parser
140
+ exit
141
+ end
142
+ end
143
+ parser.parse!
144
+
145
+ Gsplat.backend = :ruby
146
+ SampleData.generate(File.expand_path(options.fetch(:output)))
147
+ warn "sample data written to #{File.expand_path(options.fetch(:output))}"
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "optparse"
6
+ require "gsplat"
7
+
8
+ sample_ply = File.expand_path("data/splats.ply", __dir__)
9
+ options = { ply: sample_ply, output: nil, frames: nil, width: nil, height: nil, dry_run: false }
10
+ parser = OptionParser.new do |options_parser|
11
+ options_parser.banner = "Usage: bundle exec ruby examples/render_path.rb [options]"
12
+ options_parser.on("--ply FILE", "Inria PLY file (default: bundled sample)") { |value| options[:ply] = value }
13
+ options_parser.on("--output PATH") { |value| options[:output] = value }
14
+ options_parser.on("--frames N", Integer) { |value| options[:frames] = value }
15
+ options_parser.on("--width N", Integer) { |value| options[:width] = value }
16
+ options_parser.on("--height N", Integer) { |value| options[:height] = value }
17
+ options_parser.on("--dry-run", "Validate the PLY and camera path without writing images") do
18
+ options[:dry_run] = true
19
+ end
20
+ options_parser.on_tail("-h", "--help", "Show this help") do
21
+ puts options_parser
22
+ exit
23
+ end
24
+ end
25
+ parser.parse!
26
+
27
+ using_sample = File.expand_path(options.fetch(:ply)) == sample_ply
28
+ options[:output] ||= using_sample ? "renders/sample" : "renders"
29
+ options[:frames] ||= using_sample ? 12 : 60
30
+ options[:width] ||= using_sample ? 64 : 512
31
+ options[:height] ||= using_sample ? 64 : 512
32
+
33
+ def normalize(vector)
34
+ length = Math.sqrt(vector.sum { |value| value * value })
35
+ vector.map { |value| value / length }
36
+ end
37
+
38
+ def cross(left, right)
39
+ [
40
+ (left[1] * right[2]) - (left[2] * right[1]),
41
+ (left[2] * right[0]) - (left[0] * right[2]),
42
+ (left[0] * right[1]) - (left[1] * right[0])
43
+ ]
44
+ end
45
+
46
+ def look_at(position, target)
47
+ forward = normalize(target.zip(position).map { |target_value, position_value| target_value - position_value })
48
+ right = normalize(cross([0.0, 1.0, 0.0], forward))
49
+ up = cross(forward, right)
50
+ rotation = [right, up, forward]
51
+ view = Numo::SFloat.eye(4)
52
+ view[0...3, 0...3] = Numo::SFloat.cast(rotation)
53
+ view[0...3, 3] = Numo::SFloat.cast(rotation.map { |row| -row.zip(position).sum { |a, b| a * b } })
54
+ view
55
+ end
56
+
57
+ def sh_coefficients(params)
58
+ direct = params.fetch(:sh0)
59
+ rest = params.fetch(:shN)
60
+ output = direct.class.zeros(direct.shape[0], direct.shape[1] + rest.shape[1], 3)
61
+ output[true, 0...direct.shape[1], true] = direct
62
+ output[true, direct.shape[1]...output.shape[1], true] = rest unless rest.shape[1].zero?
63
+ output
64
+ end
65
+
66
+ params = Gsplat::IO::Ply.read(options.fetch(:ply))
67
+ center = params.fetch(:means).mean(axis: 0).to_a
68
+ distances = Numo::NMath.sqrt(((params.fetch(:means) - Numo::SFloat.cast(center))**2).sum(axis: 1))
69
+ radius = [distances.max.to_f * 2.0, 1.0].max
70
+ colors = sh_coefficients(params)
71
+ degree = [Math.sqrt(colors.shape[1]).to_i - 1, 4].min
72
+ focal = [options.fetch(:width), options.fetch(:height)].max.to_f
73
+ intrinsics = Numo::SFloat[
74
+ [[focal, 0, options.fetch(:width) / 2.0],
75
+ [0, focal, options.fetch(:height) / 2.0],
76
+ [0, 0, 1]]
77
+ ]
78
+ if options.fetch(:dry_run)
79
+ warn format(
80
+ "render path valid: gaussians=%<gaussians>d frames=%<frames>d degree=%<degree>d",
81
+ gaussians: params.fetch(:means).shape[0],
82
+ frames: options.fetch(:frames),
83
+ degree: degree
84
+ )
85
+ exit
86
+ end
87
+
88
+ FileUtils.mkdir_p(options.fetch(:output))
89
+
90
+ options.fetch(:frames).times do |frame|
91
+ angle = 2 * Math::PI * frame / options.fetch(:frames)
92
+ position = [
93
+ center[0] + (radius * Math.cos(angle)),
94
+ center[1] + (radius * 0.25),
95
+ center[2] + (radius * Math.sin(angle))
96
+ ]
97
+ view = look_at(position, center).reshape(1, 4, 4)
98
+ rendered, = Gsplat.rasterization(
99
+ means: params.fetch(:means),
100
+ quats: params.fetch(:quats),
101
+ scales: Numo::NMath.exp(params.fetch(:scales)),
102
+ opacities: 1.0 / (1.0 + Numo::NMath.exp(-params.fetch(:opacities))),
103
+ colors: colors,
104
+ viewmats: view,
105
+ ks: intrinsics,
106
+ width: options.fetch(:width),
107
+ height: options.fetch(:height),
108
+ sh_degree: degree
109
+ )
110
+ path = File.join(options.fetch(:output), format("frame_%04d.png", frame))
111
+ Gsplat::IO::Image.write(path, rendered[0, true, true, true])
112
+ end
113
+ warn format(
114
+ "render complete: frames=%<frames>d output=%<output>s",
115
+ frames: options.fetch(:frames),
116
+ output: options.fetch(:output)
117
+ )
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "optparse"
6
+ require "gsplat"
7
+
8
+ sample_data = File.expand_path("data/colmap", __dir__)
9
+ options = {
10
+ data: sample_data,
11
+ output_dir: nil,
12
+ max_steps: nil,
13
+ data_factor: 1,
14
+ strategy: "default",
15
+ dry_run: false
16
+ }
17
+ parser = OptionParser.new do |options_parser|
18
+ options_parser.banner = "Usage: bundle exec ruby examples/simple_trainer.rb [options]"
19
+ options_parser.on("--data PATH", "COLMAP dataset root (default: bundled sample)") { |value| options[:data] = value }
20
+ options_parser.on("--output PATH") { |value| options[:output_dir] = value }
21
+ options_parser.on("--steps N", Integer) { |value| options[:max_steps] = value }
22
+ options_parser.on("--data-factor N", Integer) { |value| options[:data_factor] = value }
23
+ options_parser.on("--strategy NAME", %w[default mcmc]) { |value| options[:strategy] = value }
24
+ options_parser.on("--dry-run", "Validate the sparse model without loading images") { options[:dry_run] = true }
25
+ options_parser.on_tail("-h", "--help", "Show this help") do
26
+ puts options_parser
27
+ exit
28
+ end
29
+ end
30
+ parser.parse!
31
+
32
+ using_sample = File.expand_path(options.fetch(:data)) == sample_data
33
+ options[:output_dir] ||= using_sample ? "results/sample" : "results"
34
+ options[:max_steps] ||= using_sample ? 10 : 30_000
35
+
36
+ if options.fetch(:dry_run)
37
+ dataset = Gsplat::IO::Colmap.read(options.fetch(:data), data_factor: options.fetch(:data_factor))
38
+ warn format(
39
+ "dataset valid: cameras=%<cameras>d images=%<images>d points=%<points>d",
40
+ cameras: dataset.cameras.length,
41
+ images: dataset.images.length,
42
+ points: dataset.points3d.length
43
+ )
44
+ exit
45
+ end
46
+
47
+ scene = Gsplat::Training::Scene.from_colmap(
48
+ options.fetch(:data),
49
+ data_factor: options.fetch(:data_factor)
50
+ )
51
+ config_options = {
52
+ max_steps: options.fetch(:max_steps),
53
+ output_dir: options.fetch(:output_dir),
54
+ eval_steps: [options.fetch(:max_steps)],
55
+ save_steps: [options.fetch(:max_steps)]
56
+ }
57
+ config_options.merge!(opacity_reg: 0.01, scale_reg: 0.01) if options.fetch(:strategy) == "mcmc"
58
+ config = Gsplat::Training::Config.new(**config_options)
59
+ strategy = if options.fetch(:strategy) == "mcmc"
60
+ Gsplat::Strategy::MCMC.new
61
+ else
62
+ Gsplat::Strategy::Default.new
63
+ end
64
+ trainer = Gsplat::Training::Trainer.new(scene: scene, config: config, strategy: strategy)
65
+ result = trainer.train
66
+ FileUtils.mkdir_p(options.fetch(:output_dir))
67
+ Gsplat::IO::Ply.write(
68
+ File.join(options.fetch(:output_dir), "splats.ply"),
69
+ trainer.params
70
+ )
71
+ warn format(
72
+ "training complete: step=%<step>d PSNR=%<psnr>.2f SSIM=%<ssim>.4f",
73
+ step: result.step,
74
+ psnr: result.final_metrics.fetch(:psnr),
75
+ ssim: result.final_metrics.fetch(:ssim)
76
+ )
@@ -0,0 +1,54 @@
1
+ #ifndef GSPLAT_COMMON_H
2
+ #define GSPLAT_COMMON_H
3
+
4
+ #include <ruby.h>
5
+ #include <ruby/thread.h>
6
+ #include <numo/narray.h>
7
+ #include <numo/intern.h>
8
+
9
+ static inline narray_t *
10
+ gs_narray(VALUE value)
11
+ {
12
+ narray_t *array;
13
+ GetNArray(value, array);
14
+ return array;
15
+ }
16
+
17
+ static inline void
18
+ gs_require_sfloat(VALUE value, const char *name)
19
+ {
20
+ if (!rb_obj_is_kind_of(value, numo_cSFloat)) {
21
+ rb_raise(rb_eTypeError, "%s must be Numo::SFloat", name);
22
+ }
23
+ }
24
+
25
+ static inline void
26
+ gs_require_int32(VALUE value, const char *name)
27
+ {
28
+ if (!rb_obj_is_kind_of(value, numo_cInt32)) {
29
+ rb_raise(rb_eTypeError, "%s must be Numo::Int32", name);
30
+ }
31
+ }
32
+
33
+ static inline void
34
+ gs_require_same_shape(VALUE left, VALUE right)
35
+ {
36
+ narray_t *left_array = gs_narray(left);
37
+ narray_t *right_array = gs_narray(right);
38
+ if (left_array->ndim != right_array->ndim ||
39
+ left_array->size != right_array->size) {
40
+ rb_raise(rb_eArgError, "NArray shapes differ");
41
+ }
42
+ for (int axis = 0; axis < left_array->ndim; ++axis) {
43
+ if (left_array->shape[axis] != right_array->shape[axis]) {
44
+ rb_raise(rb_eArgError, "NArray shapes differ");
45
+ }
46
+ }
47
+ }
48
+
49
+ #define GS_SFLOAT_READ(value) ((const float *)na_get_pointer_for_read(value))
50
+ #define GS_SFLOAT_WRITE(value) ((float *)na_get_pointer_for_write(value))
51
+ #define GS_INT32_READ(value) ((const int32_t *)na_get_pointer_for_read(value))
52
+ #define GS_INT32_WRITE(value) ((int32_t *)na_get_pointer_for_write(value))
53
+
54
+ #endif
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rubygems"
5
+
6
+ # mkmf configures extension builds through these documented globals.
7
+ # rubocop:disable Style/GlobalVars
8
+ numo_spec = Gem::Specification.find_by_name("numo-narray")
9
+ numo_include = File.join(numo_spec.full_gem_path, "lib", "numo")
10
+ $INCFLAGS = "-I#{numo_include} #{$INCFLAGS}"
11
+ $CFLAGS = "#{$CFLAGS} -O3"
12
+
13
+ abort "numo/narray.h is required" unless have_header("numo/narray.h")
14
+
15
+ openmp_enabled = try_compile("int main(void) { return 0; }", "-fopenmp")
16
+ if openmp_enabled
17
+ $CFLAGS = "#{$CFLAGS} -fopenmp"
18
+ $LDFLAGS = "#{$LDFLAGS} -fopenmp"
19
+ elsif RUBY_PLATFORM.include?("darwin")
20
+ prefixes = [
21
+ ENV.fetch("LIBOMP_PREFIX", nil),
22
+ "/opt/homebrew/opt/libomp",
23
+ "/usr/local/opt/libomp"
24
+ ].compact
25
+ prefix = prefixes.find { |candidate| File.file?(File.join(candidate, "include", "omp.h")) }
26
+ if prefix
27
+ original_flags = [$INCFLAGS, $CFLAGS, $LDFLAGS]
28
+ $INCFLAGS = "-I#{File.join(prefix, 'include')} #{$INCFLAGS}"
29
+ $CFLAGS = "#{$CFLAGS} -Xpreprocessor -fopenmp"
30
+ $LDFLAGS = "#{$LDFLAGS} -L#{File.join(prefix, 'lib')} -lomp"
31
+ openmp_enabled = try_link("#include <omp.h>\nint main(void) { return omp_get_max_threads() < 1; }")
32
+ $INCFLAGS, $CFLAGS, $LDFLAGS = original_flags unless openmp_enabled
33
+ end
34
+ end
35
+
36
+ $defs << "-DGSPLAT_OPENMP=1" if openmp_enabled
37
+ # rubocop:enable Style/GlobalVars
38
+
39
+ create_makefile("gsplat/gsplat_native")
@@ -0,0 +1,60 @@
1
+ #include "common.h"
2
+
3
+ typedef struct {
4
+ const float *left;
5
+ const float *right;
6
+ float *output;
7
+ size_t count;
8
+ } gs_add_args;
9
+
10
+ void gs_init_spherical_harmonics(VALUE native);
11
+ void gs_init_projection(VALUE native);
12
+ void gs_init_intersections(VALUE native);
13
+ void gs_init_rasterization(VALUE native);
14
+
15
+ static void *
16
+ gs_add_without_gvl(void *opaque)
17
+ {
18
+ gs_add_args *args = (gs_add_args *)opaque;
19
+ #ifdef GSPLAT_OPENMP
20
+ #pragma omp parallel for
21
+ #endif
22
+ for (size_t index = 0; index < args->count; ++index) {
23
+ args->output[index] = args->left[index] + args->right[index];
24
+ }
25
+ return NULL;
26
+ }
27
+
28
+ static VALUE
29
+ gs_native_add(VALUE self, VALUE left, VALUE right)
30
+ {
31
+ (void)self;
32
+ gs_require_sfloat(left, "left");
33
+ gs_require_sfloat(right, "right");
34
+ gs_require_same_shape(left, right);
35
+
36
+ VALUE output = rb_funcall(left, rb_intern("dup"), 0);
37
+ gs_add_args args = {
38
+ GS_SFLOAT_READ(left),
39
+ GS_SFLOAT_READ(right),
40
+ GS_SFLOAT_WRITE(output),
41
+ gs_narray(left)->size
42
+ };
43
+ rb_thread_call_without_gvl(gs_add_without_gvl, &args, RUBY_UBF_IO, NULL);
44
+ RB_GC_GUARD(left);
45
+ RB_GC_GUARD(right);
46
+ RB_GC_GUARD(output);
47
+ return output;
48
+ }
49
+
50
+ void
51
+ Init_gsplat_native(void)
52
+ {
53
+ VALUE gsplat = rb_const_get(rb_cObject, rb_intern("Gsplat"));
54
+ VALUE native = rb_define_module_under(gsplat, "Native");
55
+ rb_define_singleton_method(native, "add", gs_native_add, 2);
56
+ gs_init_spherical_harmonics(native);
57
+ gs_init_projection(native);
58
+ gs_init_intersections(native);
59
+ gs_init_rasterization(native);
60
+ }