gsplat 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +236 -0
- data/docs/ACCEPTANCE.md +60 -0
- data/docs/BENCHMARKS.md +79 -0
- data/docs/DECISIONS.md +52 -0
- data/docs/MIGRATION.md +117 -0
- data/docs/PROFILE.md +75 -0
- data/docs/PROGRESS.md +199 -0
- data/docs/decisions/0000-template.md +17 -0
- data/docs/decisions/0001-pin-golden-reference-and-cpu-validation.md +27 -0
- data/docs/decisions/0002-retain-ruby-fallbacks-for-native-backend.md +26 -0
- data/docs/decisions/0003-share-projection-for-distorted-cameras.md +26 -0
- data/docs/decisions/0004-use-portable-world-space-reference-paths.md +26 -0
- data/docs/decisions/0005-reuse-ewa-core-for-2dgs.md +24 -0
- data/docs/decisions/0006-keep-eval3d-as-portable-reference.md +25 -0
- data/docs/decisions/0007-share-compositor-semantics-for-contribution-indices.md +25 -0
- data/examples/data/README.md +14 -0
- data/examples/data/colmap/images/view_000.png +0 -0
- data/examples/data/colmap/images/view_001.png +0 -0
- data/examples/data/colmap/images/view_002.png +0 -0
- data/examples/data/colmap/sparse/0/cameras.txt +2 -0
- data/examples/data/colmap/sparse/0/images.txt +6 -0
- data/examples/data/colmap/sparse/0/points3D.txt +17 -0
- data/examples/data/splats.ply +0 -0
- data/examples/fit_image.rb +35 -0
- data/examples/generate_sample_data.rb +147 -0
- data/examples/render_path.rb +117 -0
- data/examples/simple_trainer.rb +76 -0
- data/ext/gsplat_native/common.h +54 -0
- data/ext/gsplat_native/extconf.rb +39 -0
- data/ext/gsplat_native/gsplat_native.c +60 -0
- data/ext/gsplat_native/intersections.c +211 -0
- data/ext/gsplat_native/projection.c +199 -0
- data/ext/gsplat_native/raster_backward.c +129 -0
- data/ext/gsplat_native/raster_backward_bridge.c +69 -0
- data/ext/gsplat_native/raster_forward.c +150 -0
- data/ext/gsplat_native/rasterization.h +51 -0
- data/ext/gsplat_native/spherical_harmonics.c +129 -0
- data/gsplat.gemspec +36 -0
- data/lib/gsplat/autograd/context.rb +60 -0
- data/lib/gsplat/autograd/function.rb +68 -0
- data/lib/gsplat/autograd/variable.rb +159 -0
- data/lib/gsplat/backend/ruby/accumulate.rb +139 -0
- data/lib/gsplat/backend/ruby/accumulate_backward.rb +40 -0
- data/lib/gsplat/backend/ruby/eval3d_rasterizer.rb +175 -0
- data/lib/gsplat/backend/ruby/isect_tiles.rb +198 -0
- data/lib/gsplat/backend/ruby/projection.rb +251 -0
- data/lib/gsplat/backend/ruby/projection_backward.rb +190 -0
- data/lib/gsplat/backend/ruby/projection_covariance_vjp.rb +72 -0
- data/lib/gsplat/backend/ruby/projection_input_vjp.rb +252 -0
- data/lib/gsplat/backend/ruby/quat_scale_to_covar_preci.rb +139 -0
- data/lib/gsplat/backend/ruby/rasterize_to_indices_in_range.rb +121 -0
- data/lib/gsplat/backend/ruby/rasterize_to_pixels.rb +199 -0
- data/lib/gsplat/backend/ruby/rasterize_to_pixels_backward.rb +121 -0
- data/lib/gsplat/backend/ruby/spherical_harmonics.rb +135 -0
- data/lib/gsplat/backend/ruby/tile_compositor.rb +50 -0
- data/lib/gsplat/backend/ruby/tile_compositor_backward.rb +84 -0
- data/lib/gsplat/backend.rb +79 -0
- data/lib/gsplat/compression/grid_sort.rb +79 -0
- data/lib/gsplat/compression/kmeans.rb +121 -0
- data/lib/gsplat/compression/png.rb +147 -0
- data/lib/gsplat/compression/png_codec.rb +133 -0
- data/lib/gsplat/compression/quantizer.rb +79 -0
- data/lib/gsplat/io/checkpoint.rb +145 -0
- data/lib/gsplat/io/colmap.rb +175 -0
- data/lib/gsplat/io/colmap_binary.rb +98 -0
- data/lib/gsplat/io/colmap_text.rb +84 -0
- data/lib/gsplat/io/image.rb +63 -0
- data/lib/gsplat/io/image_backends.rb +81 -0
- data/lib/gsplat/io/npy.rb +189 -0
- data/lib/gsplat/io/ply.rb +185 -0
- data/lib/gsplat/io/ply_reader.rb +142 -0
- data/lib/gsplat/io/zip_archive.rb +183 -0
- data/lib/gsplat/math/camera_distortion.rb +123 -0
- data/lib/gsplat/math/camera_projection.rb +202 -0
- data/lib/gsplat/math/mat.rb +114 -0
- data/lib/gsplat/math/quaternion.rb +175 -0
- data/lib/gsplat/math/small_matrix_primitives.rb +112 -0
- data/lib/gsplat/math/spherical_harmonic_basis.rb +148 -0
- data/lib/gsplat/math/ssim.rb +153 -0
- data/lib/gsplat/native.rb +30 -0
- data/lib/gsplat/native_ops.rb +148 -0
- data/lib/gsplat/native_raster_ops.rb +76 -0
- data/lib/gsplat/ops/accumulate.rb +68 -0
- data/lib/gsplat/ops/eval3d_rasterize.rb +62 -0
- data/lib/gsplat/ops/isect_tiles.rb +38 -0
- data/lib/gsplat/ops/projection.rb +154 -0
- data/lib/gsplat/ops/quat_scale_to_covar_preci.rb +118 -0
- data/lib/gsplat/ops/rasterize_to_indices_in_range.rb +38 -0
- data/lib/gsplat/ops/rasterize_to_pixels.rb +105 -0
- data/lib/gsplat/ops/relocation.rb +94 -0
- data/lib/gsplat/ops/spherical_harmonics.rb +71 -0
- data/lib/gsplat/ops/tensor_shape_ops.rb +173 -0
- data/lib/gsplat/ops/tensor_value_ops.rb +150 -0
- data/lib/gsplat/optim/adam.rb +212 -0
- data/lib/gsplat/optim/lr_scheduler.rb +36 -0
- data/lib/gsplat/optim/selective_adam.rb +68 -0
- data/lib/gsplat/rasterization.rb +130 -0
- data/lib/gsplat/rasterization_2dgs.rb +140 -0
- data/lib/gsplat/rasterization_helpers.rb +162 -0
- data/lib/gsplat/rasterization_validation.rb +99 -0
- data/lib/gsplat/strategy/base.rb +49 -0
- data/lib/gsplat/strategy/default.rb +188 -0
- data/lib/gsplat/strategy/mcmc.rb +103 -0
- data/lib/gsplat/strategy/mcmc_ops.rb +143 -0
- data/lib/gsplat/strategy/ops.rb +165 -0
- data/lib/gsplat/training/config.rb +91 -0
- data/lib/gsplat/training/image_fitter.rb +158 -0
- data/lib/gsplat/training/losses.rb +191 -0
- data/lib/gsplat/training/scene.rb +116 -0
- data/lib/gsplat/training/trainer.rb +236 -0
- data/lib/gsplat/utils.rb +110 -0
- data/lib/gsplat/version.rb +6 -0
- data/lib/gsplat.rb +98 -0
- metadata +181 -0
|
@@ -0,0 +1,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
|
+
}
|