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,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Math
|
|
5
|
+
# OpenCV pinhole distortion and equidistant fisheye projection.
|
|
6
|
+
module CameraDistortion
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/ParameterLists
|
|
10
|
+
def project_camera(means, intrinsics, camera_model, radial: nil, tangential: nil, thin_prism: nil)
|
|
11
|
+
# rubocop:enable Metrics/ParameterLists
|
|
12
|
+
projected = means.class.zeros(means.shape[0], 2)
|
|
13
|
+
jacobians = means.class.zeros(means.shape[0], 2, 3)
|
|
14
|
+
means.shape[0].times do |index|
|
|
15
|
+
point = means[index, true].to_a.map(&:to_f)
|
|
16
|
+
projected[index, true] = project_point(
|
|
17
|
+
point, intrinsics, camera_model, radial, tangential, thin_prism
|
|
18
|
+
)
|
|
19
|
+
jacobians[index, true, true] = numerical_jacobian(
|
|
20
|
+
point, intrinsics, camera_model, radial, tangential, thin_prism, means.class
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
[projected, jacobians]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
|
|
27
|
+
def project_point(point, intrinsics, camera_model, radial, tangential, thin_prism)
|
|
28
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ParameterLists
|
|
29
|
+
x_coord = point[0] / point[2]
|
|
30
|
+
y_coord = point[1] / point[2]
|
|
31
|
+
normalized = if camera_model.to_s == "fisheye"
|
|
32
|
+
fisheye_normalized = fisheye(x_coord, y_coord, radial)
|
|
33
|
+
tangential_prism(
|
|
34
|
+
*fisheye_normalized, *fisheye_normalized, tangential, thin_prism
|
|
35
|
+
)
|
|
36
|
+
else
|
|
37
|
+
pinhole_normalized = pinhole(x_coord, y_coord, radial)
|
|
38
|
+
tangential_prism(
|
|
39
|
+
*pinhole_normalized, x_coord, y_coord, tangential, thin_prism
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
[
|
|
43
|
+
(intrinsics[0, 0].to_f * normalized[0]) +
|
|
44
|
+
(intrinsics[0, 1].to_f * normalized[1]) + intrinsics[0, 2].to_f,
|
|
45
|
+
(intrinsics[1, 0].to_f * normalized[0]) +
|
|
46
|
+
(intrinsics[1, 1].to_f * normalized[1]) + intrinsics[1, 2].to_f
|
|
47
|
+
]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def pinhole(x_coord, y_coord, radial)
|
|
51
|
+
coefficients = Array.new(6, 0.0)
|
|
52
|
+
radial&.each_with_index { |value, index| coefficients[index] = value.to_f }
|
|
53
|
+
radius2 = (x_coord * x_coord) + (y_coord * y_coord)
|
|
54
|
+
radius4 = radius2 * radius2
|
|
55
|
+
radius6 = radius4 * radius2
|
|
56
|
+
numerator = 1 + (coefficients[0] * radius2) +
|
|
57
|
+
(coefficients[1] * radius4) + (coefficients[2] * radius6)
|
|
58
|
+
denominator = 1 + (coefficients[3] * radius2) +
|
|
59
|
+
(coefficients[4] * radius4) + (coefficients[5] * radius6)
|
|
60
|
+
scale = numerator / denominator
|
|
61
|
+
[x_coord * scale, y_coord * scale]
|
|
62
|
+
end
|
|
63
|
+
private_class_method :pinhole
|
|
64
|
+
|
|
65
|
+
def fisheye(x_coord, y_coord, radial)
|
|
66
|
+
radius = ::Math.sqrt((x_coord * x_coord) + (y_coord * y_coord))
|
|
67
|
+
return [x_coord, y_coord] if radius < 1e-12
|
|
68
|
+
|
|
69
|
+
theta = ::Math.atan(radius)
|
|
70
|
+
theta2 = theta * theta
|
|
71
|
+
coefficients = Array.new(4, 0.0)
|
|
72
|
+
radial&.each_with_index { |value, index| coefficients[index] = value.to_f }
|
|
73
|
+
polynomial = 1.0
|
|
74
|
+
power = theta2
|
|
75
|
+
coefficients.each do |coefficient|
|
|
76
|
+
polynomial += coefficient * power
|
|
77
|
+
power *= theta2
|
|
78
|
+
end
|
|
79
|
+
scale = theta * polynomial / radius
|
|
80
|
+
[x_coord * scale, y_coord * scale]
|
|
81
|
+
end
|
|
82
|
+
private_class_method :fisheye
|
|
83
|
+
|
|
84
|
+
# rubocop:disable Metrics/ParameterLists
|
|
85
|
+
def tangential_prism(base_x, base_y, x_coord, y_coord, tangential, thin_prism)
|
|
86
|
+
# rubocop:enable Metrics/ParameterLists
|
|
87
|
+
p1, p2 = tangential ? tangential.map(&:to_f) : [0.0, 0.0]
|
|
88
|
+
s1, s2, s3, s4 = thin_prism ? thin_prism.map(&:to_f) : [0.0, 0.0, 0.0, 0.0]
|
|
89
|
+
radius2 = (x_coord * x_coord) + (y_coord * y_coord)
|
|
90
|
+
radius4 = radius2 * radius2
|
|
91
|
+
[
|
|
92
|
+
base_x + (2 * p1 * x_coord * y_coord) + (p2 * (radius2 + (2 * x_coord * x_coord))) +
|
|
93
|
+
(s1 * radius2) + (s2 * radius4),
|
|
94
|
+
base_y + (p1 * (radius2 + (2 * y_coord * y_coord))) + (2 * p2 * x_coord * y_coord) +
|
|
95
|
+
(s3 * radius2) + (s4 * radius4)
|
|
96
|
+
]
|
|
97
|
+
end
|
|
98
|
+
private_class_method :tangential_prism
|
|
99
|
+
|
|
100
|
+
# rubocop:disable Metrics/ParameterLists
|
|
101
|
+
def numerical_jacobian(point, intrinsics, camera_model, radial, tangential, thin_prism, type)
|
|
102
|
+
# rubocop:enable Metrics/ParameterLists
|
|
103
|
+
epsilon = type == Numo::DFloat ? 1e-6 : 1e-3
|
|
104
|
+
2.times.map do |output_axis|
|
|
105
|
+
3.times.map do |input_axis|
|
|
106
|
+
positive = point.dup
|
|
107
|
+
negative = point.dup
|
|
108
|
+
positive[input_axis] += epsilon
|
|
109
|
+
negative[input_axis] -= epsilon
|
|
110
|
+
projected_positive = project_point(
|
|
111
|
+
positive, intrinsics, camera_model, radial, tangential, thin_prism
|
|
112
|
+
)
|
|
113
|
+
projected_negative = project_point(
|
|
114
|
+
negative, intrinsics, camera_model, radial, tangential, thin_prism
|
|
115
|
+
)
|
|
116
|
+
(projected_positive[output_axis] - projected_negative[output_axis]) / (2 * epsilon)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
private_class_method :numerical_jacobian
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Math
|
|
5
|
+
# Camera-space transformation and pinhole Gaussian projection primitives.
|
|
6
|
+
module CameraProjection
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def world_to_cam(means, covars, viewmats)
|
|
10
|
+
means, covars, viewmats = validate_world_inputs(means, covars, viewmats)
|
|
11
|
+
camera_count = viewmats.shape[0]
|
|
12
|
+
gaussian_count = means.shape[0]
|
|
13
|
+
camera_means = means.class.zeros(camera_count, gaussian_count, 3)
|
|
14
|
+
camera_covars = means.class.zeros(camera_count, gaussian_count, 3, 3)
|
|
15
|
+
camera_count.times do |camera_index|
|
|
16
|
+
rotation = viewmats[camera_index, 0...3, 0...3]
|
|
17
|
+
translation = viewmats[camera_index, 0...3, 3]
|
|
18
|
+
rotation_batch = means.class.zeros(gaussian_count, 3, 3)
|
|
19
|
+
rotation_batch[true, true, true] = rotation
|
|
20
|
+
camera_means[camera_index, true, true] = means.dot(rotation.transpose) + translation
|
|
21
|
+
rotated = Mat.matmul_batch(rotation_batch, covars)
|
|
22
|
+
camera_covars[camera_index, true, true, true] = Mat.matmul_batch(
|
|
23
|
+
rotated,
|
|
24
|
+
rotation_batch.transpose(0, 2, 1)
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
[camera_means, camera_covars]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def persp_proj(means, covars, intrinsics, width, height)
|
|
31
|
+
means, covars, intrinsics = validate_projection_inputs(means, covars, intrinsics, width, height)
|
|
32
|
+
camera_count = means.shape[0]
|
|
33
|
+
gaussian_count = means.shape[1]
|
|
34
|
+
means2d = means.class.zeros(camera_count, gaussian_count, 2)
|
|
35
|
+
covars2d = means.class.zeros(camera_count, gaussian_count, 2, 2)
|
|
36
|
+
camera_count.times do |camera_index|
|
|
37
|
+
projected_means, jacobians = pinhole_camera(
|
|
38
|
+
means[camera_index, true, true],
|
|
39
|
+
intrinsics[camera_index, true, true],
|
|
40
|
+
width,
|
|
41
|
+
height
|
|
42
|
+
)
|
|
43
|
+
means2d[camera_index, true, true] = projected_means
|
|
44
|
+
covariance = covars[camera_index, true, true, true]
|
|
45
|
+
transformed = Mat.matmul_batch(jacobians, covariance)
|
|
46
|
+
covars2d[camera_index, true, true, true] = Mat.matmul_batch(
|
|
47
|
+
transformed,
|
|
48
|
+
jacobians.transpose(0, 2, 1)
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
[means2d, covars2d]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def ortho_proj(means, covars, intrinsics, width, height)
|
|
55
|
+
means, covars, intrinsics = validate_projection_inputs(means, covars, intrinsics, width, height)
|
|
56
|
+
camera_count = means.shape[0]
|
|
57
|
+
gaussian_count = means.shape[1]
|
|
58
|
+
means2d = means.class.zeros(camera_count, gaussian_count, 2)
|
|
59
|
+
covars2d = means.class.zeros(camera_count, gaussian_count, 2, 2)
|
|
60
|
+
camera_count.times do |camera_index|
|
|
61
|
+
projected_means, jacobians = ortho_camera(
|
|
62
|
+
means[camera_index, true, true],
|
|
63
|
+
intrinsics[camera_index, true, true]
|
|
64
|
+
)
|
|
65
|
+
means2d[camera_index, true, true] = projected_means
|
|
66
|
+
transformed = Mat.matmul_batch(jacobians, covars[camera_index, true, true, true])
|
|
67
|
+
covars2d[camera_index, true, true, true] = Mat.matmul_batch(
|
|
68
|
+
transformed,
|
|
69
|
+
jacobians.transpose(0, 2, 1)
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
[means2d, covars2d]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# rubocop:disable Metrics/ParameterLists
|
|
76
|
+
def distorted_proj(means, covars, intrinsics, camera_model, radial_coeffs: nil,
|
|
77
|
+
tangential_coeffs: nil, thin_prism_coeffs: nil)
|
|
78
|
+
# rubocop:enable Metrics/ParameterLists
|
|
79
|
+
camera_count = means.shape[0]
|
|
80
|
+
gaussian_count = means.shape[1]
|
|
81
|
+
means2d = means.class.zeros(camera_count, gaussian_count, 2)
|
|
82
|
+
covars2d = means.class.zeros(camera_count, gaussian_count, 2, 2)
|
|
83
|
+
camera_count.times do |camera_index|
|
|
84
|
+
projected, jacobians = CameraDistortion.project_camera(
|
|
85
|
+
means[camera_index, true, true],
|
|
86
|
+
intrinsics[camera_index, true, true],
|
|
87
|
+
camera_model,
|
|
88
|
+
radial: radial_coeffs && radial_coeffs[camera_index, true].to_a,
|
|
89
|
+
tangential: tangential_coeffs && tangential_coeffs[camera_index, true].to_a,
|
|
90
|
+
thin_prism: thin_prism_coeffs && thin_prism_coeffs[camera_index, true].to_a
|
|
91
|
+
)
|
|
92
|
+
means2d[camera_index, true, true] = projected
|
|
93
|
+
transformed = Mat.matmul_batch(jacobians, covars[camera_index, true, true, true])
|
|
94
|
+
covars2d[camera_index, true, true, true] = Mat.matmul_batch(
|
|
95
|
+
transformed,
|
|
96
|
+
jacobians.transpose(0, 2, 1)
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
[means2d, covars2d]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_world_inputs(means, covars, viewmats)
|
|
103
|
+
validate_float_array!(means, "means")
|
|
104
|
+
covars = means.class.cast(covars)
|
|
105
|
+
viewmats = means.class.cast(viewmats)
|
|
106
|
+
unless means.ndim == 2 && means.shape[-1] == 3 && covars.shape == [means.shape[0], 3, 3]
|
|
107
|
+
raise ShapeError,
|
|
108
|
+
"expected means [N,3] and covars [N,3,3], " \
|
|
109
|
+
"got #{means.shape.inspect} and #{covars.shape.inspect}"
|
|
110
|
+
end
|
|
111
|
+
unless viewmats.ndim == 3 && viewmats.shape[1..] == [4, 4]
|
|
112
|
+
raise ShapeError, "expected viewmats [C,4,4], got #{viewmats.shape.inspect}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
[means, covars, viewmats]
|
|
116
|
+
end
|
|
117
|
+
private_class_method :validate_world_inputs
|
|
118
|
+
|
|
119
|
+
def validate_projection_inputs(means, covars, intrinsics, width, height)
|
|
120
|
+
validate_float_array!(means, "means")
|
|
121
|
+
covars = means.class.cast(covars)
|
|
122
|
+
intrinsics = means.class.cast(intrinsics)
|
|
123
|
+
valid = means.ndim == 3 && means.shape[-1] == 3 &&
|
|
124
|
+
covars.shape == means.shape[0...-1] + [3, 3] &&
|
|
125
|
+
intrinsics.shape == [means.shape[0], 3, 3]
|
|
126
|
+
unless valid
|
|
127
|
+
raise ShapeError,
|
|
128
|
+
"expected means [C,N,3], covars [C,N,3,3], and intrinsics [C,3,3], " \
|
|
129
|
+
"got #{means.shape.inspect}, #{covars.shape.inspect}, and #{intrinsics.shape.inspect}"
|
|
130
|
+
end
|
|
131
|
+
unless width.is_a?(Integer) && width.positive? && height.is_a?(Integer) && height.positive?
|
|
132
|
+
raise ArgumentError, "width and height must be positive integers"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
[means, covars, intrinsics]
|
|
136
|
+
end
|
|
137
|
+
private_class_method :validate_projection_inputs
|
|
138
|
+
|
|
139
|
+
def validate_float_array!(array, name)
|
|
140
|
+
return if array.is_a?(Numo::NArray) && [Numo::SFloat, Numo::DFloat].include?(array.class)
|
|
141
|
+
|
|
142
|
+
raise ArgumentError, "#{name} must be Numo::SFloat or Numo::DFloat"
|
|
143
|
+
end
|
|
144
|
+
private_class_method :validate_float_array!
|
|
145
|
+
|
|
146
|
+
# rubocop:disable Metrics/AbcSize
|
|
147
|
+
def pinhole_camera(means, intrinsics, width, height)
|
|
148
|
+
x_coord = means[true, 0]
|
|
149
|
+
y_coord = means[true, 1]
|
|
150
|
+
z_coord = means[true, 2]
|
|
151
|
+
focal_x = intrinsics[0, 0]
|
|
152
|
+
focal_y = intrinsics[1, 1]
|
|
153
|
+
center_x = intrinsics[0, 2]
|
|
154
|
+
center_y = intrinsics[1, 2]
|
|
155
|
+
clipped_x = clip_ratio(
|
|
156
|
+
x_coord / z_coord,
|
|
157
|
+
-((center_x / focal_x) + (0.15 * width / focal_x)),
|
|
158
|
+
((width - center_x) / focal_x) + (0.15 * width / focal_x)
|
|
159
|
+
) * z_coord
|
|
160
|
+
clipped_y = clip_ratio(
|
|
161
|
+
y_coord / z_coord,
|
|
162
|
+
-((center_y / focal_y) + (0.15 * height / focal_y)),
|
|
163
|
+
((height - center_y) / focal_y) + (0.15 * height / focal_y)
|
|
164
|
+
) * z_coord
|
|
165
|
+
jacobians = means.class.zeros(means.shape[0], 2, 3)
|
|
166
|
+
jacobians[true, 0, 0] = focal_x / z_coord
|
|
167
|
+
jacobians[true, 0, 2] = -focal_x * clipped_x / (z_coord**2)
|
|
168
|
+
jacobians[true, 1, 1] = focal_y / z_coord
|
|
169
|
+
jacobians[true, 1, 2] = -focal_y * clipped_y / (z_coord**2)
|
|
170
|
+
homogeneous_x = (intrinsics[0, 0] * x_coord) + (intrinsics[0, 1] * y_coord) +
|
|
171
|
+
(intrinsics[0, 2] * z_coord)
|
|
172
|
+
homogeneous_y = (intrinsics[1, 0] * x_coord) + (intrinsics[1, 1] * y_coord) +
|
|
173
|
+
(intrinsics[1, 2] * z_coord)
|
|
174
|
+
means2d = means.class.zeros(means.shape[0], 2)
|
|
175
|
+
means2d[true, 0] = homogeneous_x / z_coord
|
|
176
|
+
means2d[true, 1] = homogeneous_y / z_coord
|
|
177
|
+
[means2d, jacobians]
|
|
178
|
+
end
|
|
179
|
+
# rubocop:enable Metrics/AbcSize
|
|
180
|
+
|
|
181
|
+
def clip_ratio(values, minimum, maximum)
|
|
182
|
+
output = values.dup
|
|
183
|
+
below = output.lt(minimum)
|
|
184
|
+
above = output.gt(maximum)
|
|
185
|
+
output[below] = minimum if below.any?
|
|
186
|
+
output[above] = maximum if above.any?
|
|
187
|
+
output
|
|
188
|
+
end
|
|
189
|
+
private_class_method :clip_ratio
|
|
190
|
+
|
|
191
|
+
def ortho_camera(means, intrinsics)
|
|
192
|
+
jacobians = means.class.zeros(means.shape[0], 2, 3)
|
|
193
|
+
jacobians[true, 0, 0] = intrinsics[0, 0]
|
|
194
|
+
jacobians[true, 1, 1] = intrinsics[1, 1]
|
|
195
|
+
means2d = means.class.zeros(means.shape[0], 2)
|
|
196
|
+
means2d[true, 0] = (means[true, 0] * intrinsics[0, 0]) + intrinsics[0, 2]
|
|
197
|
+
means2d[true, 1] = (means[true, 1] * intrinsics[1, 1]) + intrinsics[1, 2]
|
|
198
|
+
[means2d, jacobians]
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "small_matrix_primitives"
|
|
4
|
+
|
|
5
|
+
module Gsplat
|
|
6
|
+
# Quaternion, projection, image metric, and small-matrix primitives.
|
|
7
|
+
module Math
|
|
8
|
+
# Closed-form batched operations for small matrices.
|
|
9
|
+
module Mat
|
|
10
|
+
extend SmallMatrixPrimitives
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Determinant of batched 2x2 matrices.
|
|
15
|
+
#
|
|
16
|
+
# @param matrices [Numo::NArray] [...,2,2]
|
|
17
|
+
# @param dtype [Class, Symbol, nil] optional float32/float64 calculation type
|
|
18
|
+
# @return [Numo::NArray] [...]
|
|
19
|
+
def det2x2(matrices, dtype: nil)
|
|
20
|
+
_, leading_shape, flat = prepare_matrix(matrices, 2, dtype)
|
|
21
|
+
determinant = (flat[true, 0, 0] * flat[true, 1, 1]) - (flat[true, 0, 1] * flat[true, 1, 0])
|
|
22
|
+
reshape_vector(determinant, leading_shape)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Inverse of batched 2x2 matrices.
|
|
26
|
+
#
|
|
27
|
+
# @param matrices [Numo::NArray] [...,2,2]
|
|
28
|
+
# @param dtype [Class, Symbol, nil] optional float32/float64 calculation type
|
|
29
|
+
# @param epsilon [Numeric, nil] singularity threshold
|
|
30
|
+
# @return [Numo::NArray] [...,2,2]
|
|
31
|
+
def inv2x2(matrices, dtype: nil, epsilon: nil)
|
|
32
|
+
typed, leading_shape, flat = prepare_matrix(matrices, 2, dtype)
|
|
33
|
+
determinant = (flat[true, 0, 0] * flat[true, 1, 1]) - (flat[true, 0, 1] * flat[true, 1, 0])
|
|
34
|
+
ensure_invertible!(determinant, epsilon || default_epsilon(typed.class))
|
|
35
|
+
output = typed.class.zeros(flat.shape[0], 2, 2)
|
|
36
|
+
output[true, 0, 0] = flat[true, 1, 1] / determinant
|
|
37
|
+
output[true, 0, 1] = -flat[true, 0, 1] / determinant
|
|
38
|
+
output[true, 1, 0] = -flat[true, 1, 0] / determinant
|
|
39
|
+
output[true, 1, 1] = flat[true, 0, 0] / determinant
|
|
40
|
+
reshape_matrix(output, leading_shape, 2, 2)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Ordered real eigenvalues of batched 2x2 matrices.
|
|
44
|
+
#
|
|
45
|
+
# @param matrices [Numo::NArray] [...,2,2]
|
|
46
|
+
# @param dtype [Class, Symbol, nil] optional float32/float64 calculation type
|
|
47
|
+
# @return [Numo::NArray] [...,2] in ascending order
|
|
48
|
+
def eigvals2x2(matrices, dtype: nil)
|
|
49
|
+
typed, leading_shape, flat = prepare_matrix(matrices, 2, dtype)
|
|
50
|
+
half_trace = (flat[true, 0, 0] + flat[true, 1, 1]) * 0.5
|
|
51
|
+
half_difference = (flat[true, 0, 0] - flat[true, 1, 1]) * 0.5
|
|
52
|
+
discriminant = (half_difference**2) + (flat[true, 0, 1] * flat[true, 1, 0])
|
|
53
|
+
discriminant[discriminant.lt(0)] = 0
|
|
54
|
+
root = discriminant**0.5
|
|
55
|
+
output = typed.class.zeros(flat.shape[0], 2)
|
|
56
|
+
output[true, 0] = half_trace - root
|
|
57
|
+
output[true, 1] = half_trace + root
|
|
58
|
+
reshape_matrix(output, leading_shape, 2)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Determinant of batched 3x3 matrices.
|
|
62
|
+
#
|
|
63
|
+
# @param matrices [Numo::NArray] [...,3,3]
|
|
64
|
+
# @param dtype [Class, Symbol, nil] optional float32/float64 calculation type
|
|
65
|
+
# @return [Numo::NArray] [...]
|
|
66
|
+
def det3x3(matrices, dtype: nil)
|
|
67
|
+
_, leading_shape, flat = prepare_matrix(matrices, 3, dtype)
|
|
68
|
+
determinant = determinant3(flat)
|
|
69
|
+
reshape_vector(determinant, leading_shape)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Inverse of batched 3x3 matrices.
|
|
73
|
+
#
|
|
74
|
+
# @param matrices [Numo::NArray] [...,3,3]
|
|
75
|
+
# @param dtype [Class, Symbol, nil] optional float32/float64 calculation type
|
|
76
|
+
# @param epsilon [Numeric, nil] singularity threshold
|
|
77
|
+
# @return [Numo::NArray] [...,3,3]
|
|
78
|
+
def inv3x3(matrices, dtype: nil, epsilon: nil)
|
|
79
|
+
typed, leading_shape, flat = prepare_matrix(matrices, 3, dtype)
|
|
80
|
+
determinant = determinant3(flat)
|
|
81
|
+
ensure_invertible!(determinant, epsilon || default_epsilon(typed.class))
|
|
82
|
+
output = typed.class.zeros(flat.shape[0], 3, 3)
|
|
83
|
+
fill_inverse3!(output, flat, determinant)
|
|
84
|
+
reshape_matrix(output, leading_shape, 3, 3)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Batched matrix multiplication for small matrices.
|
|
88
|
+
#
|
|
89
|
+
# @param left [Numo::NArray] [...,M,K]
|
|
90
|
+
# @param right [Numo::NArray] [...,K,N]
|
|
91
|
+
# @param dtype [Class, Symbol, nil] optional float32/float64 calculation type
|
|
92
|
+
# @return [Numo::NArray] [...,M,N]
|
|
93
|
+
def matmul_batch(left, right, dtype: nil)
|
|
94
|
+
left = cast_float(left, dtype)
|
|
95
|
+
right = cast_float(right, left.class)
|
|
96
|
+
validate_rank!(left, 2)
|
|
97
|
+
validate_rank!(right, 2)
|
|
98
|
+
leading_shape = left.shape[0...-2]
|
|
99
|
+
unless right.shape[0...-2] == leading_shape && left.shape[-1] == right.shape[-2]
|
|
100
|
+
raise ShapeError, "matmul shape mismatch: left #{left.shape.inspect}, right #{right.shape.inspect}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
rows = left.shape[-2]
|
|
104
|
+
shared = left.shape[-1]
|
|
105
|
+
columns = right.shape[-1]
|
|
106
|
+
batch_size = leading_shape.empty? ? 1 : leading_shape.inject(:*)
|
|
107
|
+
left_flat = left.reshape(batch_size, rows, shared)
|
|
108
|
+
right_flat = right.reshape(batch_size, shared, columns)
|
|
109
|
+
output = multiply_flat(left_flat, right_flat, rows, shared, columns)
|
|
110
|
+
reshape_matrix(output, leading_shape, rows, columns)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Math
|
|
5
|
+
# Batched wxyz quaternion normalization, rotation conversion, and VJPs.
|
|
6
|
+
module Quaternion
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Normalizes wxyz quaternions along the last dimension.
|
|
10
|
+
#
|
|
11
|
+
# @param quaternions [Numo::NArray] [...,4]
|
|
12
|
+
# @param dtype [Class, Symbol, nil] optional float32/float64 calculation type
|
|
13
|
+
# @param epsilon [Numeric, nil] minimum norm
|
|
14
|
+
# @return [Numo::NArray] [...,4]
|
|
15
|
+
def normalize(quaternions, dtype: nil, epsilon: nil)
|
|
16
|
+
typed, leading_shape, flat = prepare(quaternions, 4, dtype)
|
|
17
|
+
denominator = norm_denominator(flat, epsilon || default_epsilon(typed.class))
|
|
18
|
+
(flat / denominator.reshape(flat.shape[0], 1)).reshape(*(leading_shape + [4]))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# VJP of quaternion normalization.
|
|
22
|
+
#
|
|
23
|
+
# @param quaternions [Numo::NArray] [...,4]
|
|
24
|
+
# @param grad_output [Numo::NArray] [...,4]
|
|
25
|
+
# @param dtype [Class, Symbol, nil] optional float32/float64 calculation type
|
|
26
|
+
# @param epsilon [Numeric, nil] minimum norm
|
|
27
|
+
# @return [Numo::NArray] [...,4]
|
|
28
|
+
def normalize_vjp(quaternions, grad_output, dtype: nil, epsilon: nil)
|
|
29
|
+
typed, leading_shape, flat = prepare(quaternions, 4, dtype)
|
|
30
|
+
gradient, = prepare_matching(grad_output, leading_shape, 4, typed.class)
|
|
31
|
+
denominator = norm_denominator(flat, epsilon || default_epsilon(typed.class))
|
|
32
|
+
normalized = flat / denominator.reshape(flat.shape[0], 1)
|
|
33
|
+
dot = (normalized * gradient).sum(axis: 1)
|
|
34
|
+
result = (gradient - (normalized * dot.reshape(flat.shape[0], 1))) / denominator.reshape(flat.shape[0], 1)
|
|
35
|
+
result.reshape(*(leading_shape + [4]))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Converts wxyz quaternions to rotation matrices.
|
|
39
|
+
#
|
|
40
|
+
# @param quaternions [Numo::NArray] [...,4]
|
|
41
|
+
# @param dtype [Class, Symbol, nil] optional float32/float64 calculation type
|
|
42
|
+
# @return [Numo::NArray] [...,3,3]
|
|
43
|
+
def to_rotmat(quaternions, dtype: nil)
|
|
44
|
+
typed, leading_shape, = prepare(quaternions, 4, dtype)
|
|
45
|
+
flat = normalize(typed).reshape(typed.size / 4, 4)
|
|
46
|
+
quat_w = flat[true, 0]
|
|
47
|
+
quat_x = flat[true, 1]
|
|
48
|
+
quat_y = flat[true, 2]
|
|
49
|
+
quat_z = flat[true, 3]
|
|
50
|
+
output = typed.class.zeros(flat.shape[0], 3, 3)
|
|
51
|
+
fill_rotation!(output, quat_w, quat_x, quat_y, quat_z)
|
|
52
|
+
output.reshape(*(leading_shape + [3, 3]))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# VJP of quaternion-to-rotation-matrix conversion.
|
|
56
|
+
#
|
|
57
|
+
# @param quaternions [Numo::NArray] [...,4]
|
|
58
|
+
# @param grad_output [Numo::NArray] [...,3,3]
|
|
59
|
+
# @param dtype [Class, Symbol, nil] optional float32/float64 calculation type
|
|
60
|
+
# @return [Numo::NArray] [...,4]
|
|
61
|
+
def to_rotmat_vjp(quaternions, grad_output, dtype: nil)
|
|
62
|
+
typed, leading_shape, = prepare(quaternions, 4, dtype)
|
|
63
|
+
gradient = cast_float(grad_output, typed.class)
|
|
64
|
+
unless gradient.shape == leading_shape + [3, 3]
|
|
65
|
+
raise ShapeError, "expected gradient #{(leading_shape + [3, 3]).inspect}, got #{gradient.shape.inspect}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
normalized = normalize(typed).reshape(typed.size / 4, 4)
|
|
69
|
+
grad_matrix = gradient.reshape(normalized.shape[0], 3, 3)
|
|
70
|
+
grad_normalized = rotation_vjp(normalized, grad_matrix)
|
|
71
|
+
normalize_vjp(typed, grad_normalized.reshape(*(leading_shape + [4])))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def prepare(quaternions, width, dtype)
|
|
75
|
+
typed = cast_float(quaternions, dtype)
|
|
76
|
+
unless typed.ndim >= 1 && typed.shape[-1] == width
|
|
77
|
+
raise ShapeError, "expected [...,#{width}], got #{typed.shape.inspect}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
leading_shape = typed.shape[0...-1]
|
|
81
|
+
[typed, leading_shape, typed.reshape(typed.size / width, width)]
|
|
82
|
+
end
|
|
83
|
+
private_class_method :prepare
|
|
84
|
+
|
|
85
|
+
def prepare_matching(value, leading_shape, width, dtype)
|
|
86
|
+
typed = cast_float(value, dtype)
|
|
87
|
+
expected = leading_shape + [width]
|
|
88
|
+
raise ShapeError, "expected #{expected.inspect}, got #{typed.shape.inspect}" unless typed.shape == expected
|
|
89
|
+
|
|
90
|
+
[typed.reshape(typed.size / width, width), leading_shape]
|
|
91
|
+
end
|
|
92
|
+
private_class_method :prepare_matching
|
|
93
|
+
|
|
94
|
+
def cast_float(array, dtype)
|
|
95
|
+
raise ArgumentError, "expected a Numo::NArray" unless array.is_a?(Numo::NArray)
|
|
96
|
+
|
|
97
|
+
type = case dtype
|
|
98
|
+
when nil then array.class
|
|
99
|
+
when :float32 then Numo::SFloat
|
|
100
|
+
when :float64 then Numo::DFloat
|
|
101
|
+
else dtype
|
|
102
|
+
end
|
|
103
|
+
unless [Numo::SFloat, Numo::DFloat].include?(type)
|
|
104
|
+
raise ArgumentError, "dtype must be Numo::SFloat or Numo::DFloat"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
type.cast(array)
|
|
108
|
+
end
|
|
109
|
+
private_class_method :cast_float
|
|
110
|
+
|
|
111
|
+
def norm_denominator(flat, epsilon)
|
|
112
|
+
norms = (flat**2).sum(axis: 1)**0.5
|
|
113
|
+
norms[norms.lt(epsilon)] = epsilon
|
|
114
|
+
norms
|
|
115
|
+
end
|
|
116
|
+
private_class_method :norm_denominator
|
|
117
|
+
|
|
118
|
+
def default_epsilon(type)
|
|
119
|
+
type == Numo::DFloat ? 1e-12 : 1e-6
|
|
120
|
+
end
|
|
121
|
+
private_class_method :default_epsilon
|
|
122
|
+
|
|
123
|
+
# rubocop:disable Metrics/AbcSize
|
|
124
|
+
def fill_rotation!(output, quat_w, quat_x, quat_y, quat_z)
|
|
125
|
+
output[true, 0, 0] = 1 - (2 * ((quat_y**2) + (quat_z**2)))
|
|
126
|
+
output[true, 0, 1] = 2 * ((quat_x * quat_y) - (quat_w * quat_z))
|
|
127
|
+
output[true, 0, 2] = 2 * ((quat_x * quat_z) + (quat_w * quat_y))
|
|
128
|
+
output[true, 1, 0] = 2 * ((quat_x * quat_y) + (quat_w * quat_z))
|
|
129
|
+
output[true, 1, 1] = 1 - (2 * ((quat_x**2) + (quat_z**2)))
|
|
130
|
+
output[true, 1, 2] = 2 * ((quat_y * quat_z) - (quat_w * quat_x))
|
|
131
|
+
output[true, 2, 0] = 2 * ((quat_x * quat_z) - (quat_w * quat_y))
|
|
132
|
+
output[true, 2, 1] = 2 * ((quat_y * quat_z) + (quat_w * quat_x))
|
|
133
|
+
output[true, 2, 2] = 1 - (2 * ((quat_x**2) + (quat_y**2)))
|
|
134
|
+
end
|
|
135
|
+
# rubocop:enable Metrics/AbcSize
|
|
136
|
+
private_class_method :fill_rotation!
|
|
137
|
+
|
|
138
|
+
# rubocop:disable Metrics/AbcSize
|
|
139
|
+
def rotation_vjp(quaternion, gradient)
|
|
140
|
+
quat_w = quaternion[true, 0]
|
|
141
|
+
quat_x = quaternion[true, 1]
|
|
142
|
+
quat_y = quaternion[true, 2]
|
|
143
|
+
quat_z = quaternion[true, 3]
|
|
144
|
+
grad_at = ->(row, column) { gradient[true, row, column] }
|
|
145
|
+
output = quaternion.class.zeros(*quaternion.shape)
|
|
146
|
+
output[true, 0] = (
|
|
147
|
+
(-2 * quat_z * grad_at.call(0, 1)) + (2 * quat_y * grad_at.call(0, 2)) +
|
|
148
|
+
(2 * quat_z * grad_at.call(1, 0)) - (2 * quat_x * grad_at.call(1, 2)) -
|
|
149
|
+
(2 * quat_y * grad_at.call(2, 0)) + (2 * quat_x * grad_at.call(2, 1))
|
|
150
|
+
)
|
|
151
|
+
output[true, 1] = (
|
|
152
|
+
(2 * quat_y * grad_at.call(0, 1)) + (2 * quat_z * grad_at.call(0, 2)) +
|
|
153
|
+
(2 * quat_y * grad_at.call(1, 0)) - (4 * quat_x * grad_at.call(1, 1)) -
|
|
154
|
+
(2 * quat_w * grad_at.call(1, 2)) + (2 * quat_z * grad_at.call(2, 0)) +
|
|
155
|
+
(2 * quat_w * grad_at.call(2, 1)) - (4 * quat_x * grad_at.call(2, 2))
|
|
156
|
+
)
|
|
157
|
+
output[true, 2] = (
|
|
158
|
+
(-4 * quat_y * grad_at.call(0, 0)) + (2 * quat_x * grad_at.call(0, 1)) +
|
|
159
|
+
(2 * quat_w * grad_at.call(0, 2)) + (2 * quat_x * grad_at.call(1, 0)) +
|
|
160
|
+
(2 * quat_z * grad_at.call(1, 2)) - (2 * quat_w * grad_at.call(2, 0)) +
|
|
161
|
+
(2 * quat_z * grad_at.call(2, 1)) - (4 * quat_y * grad_at.call(2, 2))
|
|
162
|
+
)
|
|
163
|
+
output[true, 3] = (
|
|
164
|
+
(-4 * quat_z * grad_at.call(0, 0)) - (2 * quat_w * grad_at.call(0, 1)) +
|
|
165
|
+
(2 * quat_x * grad_at.call(0, 2)) + (2 * quat_w * grad_at.call(1, 0)) -
|
|
166
|
+
(4 * quat_z * grad_at.call(1, 1)) + (2 * quat_y * grad_at.call(1, 2)) +
|
|
167
|
+
(2 * quat_x * grad_at.call(2, 0)) + (2 * quat_y * grad_at.call(2, 1))
|
|
168
|
+
)
|
|
169
|
+
output
|
|
170
|
+
end
|
|
171
|
+
# rubocop:enable Metrics/AbcSize
|
|
172
|
+
private_class_method :rotation_vjp
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|