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,150 @@
|
|
|
1
|
+
#include <math.h>
|
|
2
|
+
#include <string.h>
|
|
3
|
+
#include "common.h"
|
|
4
|
+
#include "rasterization.h"
|
|
5
|
+
|
|
6
|
+
void gs_init_rasterization_backward(VALUE native);
|
|
7
|
+
|
|
8
|
+
static size_t
|
|
9
|
+
gs_pixel_index(const gs_raster_forward_args *args, size_t camera, int x, int y)
|
|
10
|
+
{
|
|
11
|
+
return (camera * args->height + (size_t)y) * args->width + (size_t)x;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static void
|
|
15
|
+
gs_raster_pixel(
|
|
16
|
+
const gs_raster_forward_args *args,
|
|
17
|
+
size_t camera,
|
|
18
|
+
int x,
|
|
19
|
+
int y,
|
|
20
|
+
size_t range_start,
|
|
21
|
+
size_t range_end
|
|
22
|
+
)
|
|
23
|
+
{
|
|
24
|
+
size_t pixel = gs_pixel_index(args, camera, x, y);
|
|
25
|
+
float *output = args->render_colors + pixel * args->channels;
|
|
26
|
+
float transmittance = 1.0f;
|
|
27
|
+
for (size_t intersection = range_start; intersection < range_end; ++intersection) {
|
|
28
|
+
int32_t gaussian = args->flatten_ids[intersection];
|
|
29
|
+
float delta_x = args->means[gaussian * 2] - ((float)x + 0.5f);
|
|
30
|
+
float delta_y = args->means[gaussian * 2 + 1] - ((float)y + 0.5f);
|
|
31
|
+
const float *conic = args->conics + gaussian * 3;
|
|
32
|
+
float sigma = 0.5f * (
|
|
33
|
+
conic[0] * delta_x * delta_x + conic[2] * delta_y * delta_y
|
|
34
|
+
) + conic[1] * delta_x * delta_y;
|
|
35
|
+
float alpha = args->opacities[gaussian] * expf(-sigma);
|
|
36
|
+
if (alpha > GS_ALPHA_CLAMP) alpha = GS_ALPHA_CLAMP;
|
|
37
|
+
if (sigma < 0.0f || alpha < GS_ALPHA_SKIP) continue;
|
|
38
|
+
float next_transmittance = transmittance * (1.0f - alpha);
|
|
39
|
+
if (next_transmittance <= GS_TRANSMITTANCE_STOP) break;
|
|
40
|
+
float visibility = alpha * transmittance;
|
|
41
|
+
const float *color = args->colors + gaussian * args->channels;
|
|
42
|
+
for (size_t channel = 0; channel < args->channels; ++channel) {
|
|
43
|
+
output[channel] += visibility * color[channel];
|
|
44
|
+
}
|
|
45
|
+
transmittance = next_transmittance;
|
|
46
|
+
args->last_ids[pixel] = (int32_t)intersection;
|
|
47
|
+
}
|
|
48
|
+
if (args->backgrounds != NULL) {
|
|
49
|
+
const float *background = args->backgrounds + camera * args->channels;
|
|
50
|
+
for (size_t channel = 0; channel < args->channels; ++channel) {
|
|
51
|
+
output[channel] += transmittance * background[channel];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
args->render_alphas[pixel] = 1.0f - transmittance;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
void *
|
|
58
|
+
gs_raster_forward_without_gvl(void *opaque)
|
|
59
|
+
{
|
|
60
|
+
gs_raster_forward_args *args = (gs_raster_forward_args *)opaque;
|
|
61
|
+
size_t tiles_per_camera = (size_t)args->tile_width * args->tile_height;
|
|
62
|
+
size_t total_tiles = args->cameras * tiles_per_camera;
|
|
63
|
+
#ifdef GSPLAT_OPENMP
|
|
64
|
+
#pragma omp parallel for schedule(dynamic, 1)
|
|
65
|
+
#endif
|
|
66
|
+
for (size_t tile = 0; tile < total_tiles; ++tile) {
|
|
67
|
+
size_t camera = tile / tiles_per_camera;
|
|
68
|
+
size_t local_tile = tile % tiles_per_camera;
|
|
69
|
+
int tile_y = (int)(local_tile / args->tile_width);
|
|
70
|
+
int tile_x = (int)(local_tile % args->tile_width);
|
|
71
|
+
int x_start = tile_x * args->tile_size;
|
|
72
|
+
int y_start = tile_y * args->tile_size;
|
|
73
|
+
int x_end = x_start + args->tile_size;
|
|
74
|
+
int y_end = y_start + args->tile_size;
|
|
75
|
+
if (x_end > args->width) x_end = args->width;
|
|
76
|
+
if (y_end > args->height) y_end = args->height;
|
|
77
|
+
if (x_start >= args->width || y_start >= args->height) continue;
|
|
78
|
+
size_t range_start = (size_t)args->offsets[tile];
|
|
79
|
+
size_t range_end = tile + 1 < total_tiles ?
|
|
80
|
+
(size_t)args->offsets[tile + 1] : args->intersection_count;
|
|
81
|
+
int masked = args->masks != NULL && args->masks[tile] == 0;
|
|
82
|
+
if ((masked || range_start == range_end) && args->backgrounds == NULL) continue;
|
|
83
|
+
for (int y = y_start; y < y_end; ++y) {
|
|
84
|
+
for (int x = x_start; x < x_end; ++x) {
|
|
85
|
+
if (masked) {
|
|
86
|
+
size_t pixel = gs_pixel_index(args, camera, x, y);
|
|
87
|
+
if (args->backgrounds != NULL) {
|
|
88
|
+
memcpy(
|
|
89
|
+
args->render_colors + pixel * args->channels,
|
|
90
|
+
args->backgrounds + camera * args->channels,
|
|
91
|
+
args->channels * sizeof(float)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
gs_raster_pixel(args, camera, x, y, range_start, range_end);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return NULL;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static VALUE
|
|
104
|
+
gs_raster_forward(int argc, VALUE *argv, VALUE self)
|
|
105
|
+
{
|
|
106
|
+
(void)self;
|
|
107
|
+
if (argc != 11) rb_raise(rb_eArgError, "expected 11 raster forward arguments");
|
|
108
|
+
gs_require_sfloat(argv[0], "means2d");
|
|
109
|
+
gs_require_sfloat(argv[1], "conics");
|
|
110
|
+
gs_require_sfloat(argv[2], "colors");
|
|
111
|
+
gs_require_sfloat(argv[3], "opacities");
|
|
112
|
+
if (!NIL_P(argv[4])) gs_require_sfloat(argv[4], "backgrounds");
|
|
113
|
+
if (!NIL_P(argv[5])) gs_require_int32(argv[5], "masks");
|
|
114
|
+
gs_require_int32(argv[9], "isect_offsets");
|
|
115
|
+
gs_require_int32(argv[10], "flatten_ids");
|
|
116
|
+
narray_t *means = gs_narray(argv[0]);
|
|
117
|
+
narray_t *colors = gs_narray(argv[2]);
|
|
118
|
+
narray_t *offsets = gs_narray(argv[9]);
|
|
119
|
+
size_t color_shape[4] = {
|
|
120
|
+
means->shape[0], (size_t)NUM2INT(argv[7]), (size_t)NUM2INT(argv[6]), colors->shape[2]
|
|
121
|
+
};
|
|
122
|
+
size_t alpha_shape[4] = {means->shape[0], color_shape[1], color_shape[2], 1};
|
|
123
|
+
size_t last_shape[3] = {means->shape[0], color_shape[1], color_shape[2]};
|
|
124
|
+
VALUE render_colors = nary_new(numo_cSFloat, 4, color_shape);
|
|
125
|
+
VALUE render_alphas = nary_new(numo_cSFloat, 4, alpha_shape);
|
|
126
|
+
VALUE last_ids = nary_new(numo_cInt32, 3, last_shape);
|
|
127
|
+
memset(GS_SFLOAT_WRITE(render_colors), 0, gs_narray(render_colors)->size * sizeof(float));
|
|
128
|
+
memset(GS_SFLOAT_WRITE(render_alphas), 0, gs_narray(render_alphas)->size * sizeof(float));
|
|
129
|
+
memset(GS_INT32_WRITE(last_ids), 0, gs_narray(last_ids)->size * sizeof(int32_t));
|
|
130
|
+
gs_raster_forward_args args = {
|
|
131
|
+
GS_SFLOAT_READ(argv[0]), GS_SFLOAT_READ(argv[1]), GS_SFLOAT_READ(argv[2]),
|
|
132
|
+
GS_SFLOAT_READ(argv[3]), NIL_P(argv[4]) ? NULL : GS_SFLOAT_READ(argv[4]),
|
|
133
|
+
NIL_P(argv[5]) ? NULL : GS_INT32_READ(argv[5]), GS_INT32_READ(argv[9]),
|
|
134
|
+
GS_INT32_READ(argv[10]), GS_SFLOAT_WRITE(render_colors), GS_SFLOAT_WRITE(render_alphas),
|
|
135
|
+
GS_INT32_WRITE(last_ids), means->shape[0], means->shape[1], colors->shape[2],
|
|
136
|
+
gs_narray(argv[10])->size, NUM2INT(argv[6]), NUM2INT(argv[7]), NUM2INT(argv[8]),
|
|
137
|
+
(int)offsets->shape[2], (int)offsets->shape[1]
|
|
138
|
+
};
|
|
139
|
+
rb_thread_call_without_gvl(gs_raster_forward_without_gvl, &args, RUBY_UBF_IO, NULL);
|
|
140
|
+
RB_GC_GUARD(argv[0]);
|
|
141
|
+
RB_GC_GUARD(render_colors);
|
|
142
|
+
return rb_ary_new_from_args(3, render_colors, render_alphas, last_ids);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
void
|
|
146
|
+
gs_init_rasterization(VALUE native)
|
|
147
|
+
{
|
|
148
|
+
rb_define_singleton_method(native, "rasterize_forward_sfloat", gs_raster_forward, -1);
|
|
149
|
+
gs_init_rasterization_backward(native);
|
|
150
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#ifndef GSPLAT_RASTERIZATION_H
|
|
2
|
+
#define GSPLAT_RASTERIZATION_H
|
|
3
|
+
|
|
4
|
+
#include <stddef.h>
|
|
5
|
+
#include <stdint.h>
|
|
6
|
+
|
|
7
|
+
#define GS_ALPHA_CLAMP 0.999f
|
|
8
|
+
#define GS_ALPHA_SKIP (1.0f / 255.0f)
|
|
9
|
+
#define GS_TRANSMITTANCE_STOP 1.0e-4f
|
|
10
|
+
|
|
11
|
+
typedef struct {
|
|
12
|
+
const float *means;
|
|
13
|
+
const float *conics;
|
|
14
|
+
const float *colors;
|
|
15
|
+
const float *opacities;
|
|
16
|
+
const float *backgrounds;
|
|
17
|
+
const int32_t *masks;
|
|
18
|
+
const int32_t *offsets;
|
|
19
|
+
const int32_t *flatten_ids;
|
|
20
|
+
float *render_colors;
|
|
21
|
+
float *render_alphas;
|
|
22
|
+
int32_t *last_ids;
|
|
23
|
+
size_t cameras;
|
|
24
|
+
size_t gaussians;
|
|
25
|
+
size_t channels;
|
|
26
|
+
size_t intersection_count;
|
|
27
|
+
int width;
|
|
28
|
+
int height;
|
|
29
|
+
int tile_size;
|
|
30
|
+
int tile_width;
|
|
31
|
+
int tile_height;
|
|
32
|
+
} gs_raster_forward_args;
|
|
33
|
+
|
|
34
|
+
typedef struct {
|
|
35
|
+
gs_raster_forward_args forward;
|
|
36
|
+
const float *render_alphas;
|
|
37
|
+
const int32_t *last_ids;
|
|
38
|
+
const float *grad_render_colors;
|
|
39
|
+
const float *grad_render_alphas;
|
|
40
|
+
float *grad_means;
|
|
41
|
+
float *grad_conics;
|
|
42
|
+
float *grad_colors;
|
|
43
|
+
float *grad_opacities;
|
|
44
|
+
float *grad_backgrounds;
|
|
45
|
+
float *grad_means_abs;
|
|
46
|
+
} gs_raster_backward_args;
|
|
47
|
+
|
|
48
|
+
void *gs_raster_forward_without_gvl(void *opaque);
|
|
49
|
+
void *gs_raster_backward_without_gvl(void *opaque);
|
|
50
|
+
|
|
51
|
+
#endif
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#include <math.h>
|
|
2
|
+
#include "common.h"
|
|
3
|
+
|
|
4
|
+
typedef struct {
|
|
5
|
+
int degree;
|
|
6
|
+
size_t count;
|
|
7
|
+
size_t basis_count;
|
|
8
|
+
size_t channels;
|
|
9
|
+
const float *directions;
|
|
10
|
+
const float *coefficients;
|
|
11
|
+
float *output;
|
|
12
|
+
} gs_sh_args;
|
|
13
|
+
|
|
14
|
+
static void
|
|
15
|
+
gs_sh_basis(int degree, float x, float y, float z, float *basis)
|
|
16
|
+
{
|
|
17
|
+
const float c0 = 0.2820947917738781f;
|
|
18
|
+
const float c1 = 0.48860251190292f;
|
|
19
|
+
basis[0] = c0;
|
|
20
|
+
if (degree < 1) return;
|
|
21
|
+
basis[1] = -c1 * y;
|
|
22
|
+
basis[2] = c1 * z;
|
|
23
|
+
basis[3] = -c1 * x;
|
|
24
|
+
if (degree < 2) return;
|
|
25
|
+
basis[4] = 1.092548430592079f * x * y;
|
|
26
|
+
basis[5] = -1.092548430592079f * y * z;
|
|
27
|
+
basis[6] = 0.3153915652525201f * (3.0f * z * z - 1.0f);
|
|
28
|
+
basis[7] = -1.092548430592079f * x * z;
|
|
29
|
+
basis[8] = 0.5462742152960395f * (x * x - y * y);
|
|
30
|
+
if (degree < 3) return;
|
|
31
|
+
basis[9] = -0.5900435899266435f * y * (3.0f * x * x - y * y);
|
|
32
|
+
basis[10] = 2.890611442640554f * x * y * z;
|
|
33
|
+
basis[11] = -0.4570457994644658f * y * (5.0f * z * z - 1.0f);
|
|
34
|
+
basis[12] = 0.3731763325901154f * z * (5.0f * z * z - 3.0f);
|
|
35
|
+
basis[13] = -0.4570457994644658f * x * (5.0f * z * z - 1.0f);
|
|
36
|
+
basis[14] = 1.445305721320277f * z * (x * x - y * y);
|
|
37
|
+
basis[15] = -0.5900435899266435f * x * (x * x - 3.0f * y * y);
|
|
38
|
+
if (degree < 4) return;
|
|
39
|
+
float x2 = x * x, y2 = y * y, z2 = z * z;
|
|
40
|
+
basis[16] = 2.5033429417967046f * x * y * (x2 - y2);
|
|
41
|
+
basis[17] = -1.770130769779931f * y * z * (3.0f * x2 - y2);
|
|
42
|
+
basis[18] = 0.9461746957575601f * x * y * (7.0f * z2 - 1.0f);
|
|
43
|
+
basis[19] = -0.6690465435572892f * y * z * (7.0f * z2 - 3.0f);
|
|
44
|
+
basis[20] = 0.1057855469152043f * (z2 * (35.0f * z2 - 30.0f) + 3.0f);
|
|
45
|
+
basis[21] = -0.6690465435572892f * x * z * (7.0f * z2 - 3.0f);
|
|
46
|
+
basis[22] = 0.47308734787878f * (x2 - y2) * (7.0f * z2 - 1.0f);
|
|
47
|
+
basis[23] = -1.770130769779931f * x * z * (x2 - 3.0f * y2);
|
|
48
|
+
basis[24] = 0.6258357354491763f * (x2 * x2 - 6.0f * x2 * y2 + y2 * y2);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static void *
|
|
52
|
+
gs_sh_without_gvl(void *opaque)
|
|
53
|
+
{
|
|
54
|
+
gs_sh_args *args = (gs_sh_args *)opaque;
|
|
55
|
+
#ifdef GSPLAT_OPENMP
|
|
56
|
+
#pragma omp parallel for
|
|
57
|
+
#endif
|
|
58
|
+
for (size_t index = 0; index < args->count; ++index) {
|
|
59
|
+
const float *direction = args->directions + index * 3;
|
|
60
|
+
float norm = sqrtf(
|
|
61
|
+
direction[0] * direction[0] +
|
|
62
|
+
direction[1] * direction[1] +
|
|
63
|
+
direction[2] * direction[2]
|
|
64
|
+
);
|
|
65
|
+
if (norm < 1e-12f) norm = 1e-12f;
|
|
66
|
+
float basis[25] = {0};
|
|
67
|
+
gs_sh_basis(
|
|
68
|
+
args->degree,
|
|
69
|
+
direction[0] / norm,
|
|
70
|
+
direction[1] / norm,
|
|
71
|
+
direction[2] / norm,
|
|
72
|
+
basis
|
|
73
|
+
);
|
|
74
|
+
for (size_t channel = 0; channel < args->channels; ++channel) {
|
|
75
|
+
float color = 0.0f;
|
|
76
|
+
for (size_t coefficient = 0; coefficient < args->basis_count; ++coefficient) {
|
|
77
|
+
size_t offset = (index * args->basis_count + coefficient) * args->channels + channel;
|
|
78
|
+
color += basis[coefficient] * args->coefficients[offset];
|
|
79
|
+
}
|
|
80
|
+
args->output[index * args->channels + channel] = color;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return NULL;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static VALUE
|
|
87
|
+
gs_sh_forward(VALUE self, VALUE degree_value, VALUE directions, VALUE coefficients)
|
|
88
|
+
{
|
|
89
|
+
(void)self;
|
|
90
|
+
gs_require_sfloat(directions, "directions");
|
|
91
|
+
gs_require_sfloat(coefficients, "coefficients");
|
|
92
|
+
int degree = NUM2INT(degree_value);
|
|
93
|
+
narray_t *direction_array = gs_narray(directions);
|
|
94
|
+
narray_t *coefficient_array = gs_narray(coefficients);
|
|
95
|
+
if (degree < 0 || degree > 4 || direction_array->shape[direction_array->ndim - 1] != 3) {
|
|
96
|
+
rb_raise(rb_eArgError, "invalid spherical harmonics inputs");
|
|
97
|
+
}
|
|
98
|
+
size_t count = direction_array->size / 3;
|
|
99
|
+
size_t basis_count = coefficient_array->shape[coefficient_array->ndim - 2];
|
|
100
|
+
size_t channels = coefficient_array->shape[coefficient_array->ndim - 1];
|
|
101
|
+
if (basis_count > 25 || basis_count < (size_t)((degree + 1) * (degree + 1)) ||
|
|
102
|
+
coefficient_array->size != count * basis_count * channels) {
|
|
103
|
+
rb_raise(rb_eArgError, "invalid spherical harmonics coefficient shape");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
size_t *shape = ALLOCA_N(size_t, direction_array->ndim);
|
|
107
|
+
for (int axis = 0; axis < direction_array->ndim - 1; ++axis) {
|
|
108
|
+
shape[axis] = direction_array->shape[axis];
|
|
109
|
+
}
|
|
110
|
+
shape[direction_array->ndim - 1] = channels;
|
|
111
|
+
VALUE output = nary_new(numo_cSFloat, direction_array->ndim, shape);
|
|
112
|
+
gs_sh_args args = {
|
|
113
|
+
degree, count, basis_count, channels,
|
|
114
|
+
GS_SFLOAT_READ(directions),
|
|
115
|
+
GS_SFLOAT_READ(coefficients),
|
|
116
|
+
GS_SFLOAT_WRITE(output)
|
|
117
|
+
};
|
|
118
|
+
rb_thread_call_without_gvl(gs_sh_without_gvl, &args, RUBY_UBF_IO, NULL);
|
|
119
|
+
RB_GC_GUARD(directions);
|
|
120
|
+
RB_GC_GUARD(coefficients);
|
|
121
|
+
RB_GC_GUARD(output);
|
|
122
|
+
return output;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
void
|
|
126
|
+
gs_init_spherical_harmonics(VALUE native)
|
|
127
|
+
{
|
|
128
|
+
rb_define_singleton_method(native, "spherical_harmonics_forward_sfloat", gs_sh_forward, 3);
|
|
129
|
+
}
|
data/gsplat.gemspec
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/gsplat/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "gsplat"
|
|
7
|
+
spec.version = Gsplat::VERSION
|
|
8
|
+
spec.authors = ["Yudai Takada"]
|
|
9
|
+
spec.email = ["t.yudai92@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Differentiable 3D Gaussian splatting for Ruby"
|
|
12
|
+
spec.description = "A CPU differentiable 3D Gaussian splatting renderer, trainer, and IO toolkit " \
|
|
13
|
+
"with Numo::NArray and optional OpenMP acceleration."
|
|
14
|
+
spec.homepage = "https://github.com/ydah/gsplat"
|
|
15
|
+
spec.license = "Apache-2.0"
|
|
16
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
17
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/gsplat/#{spec.version}"
|
|
20
|
+
|
|
21
|
+
release_files = %w[LICENSE.txt README.md gsplat.gemspec]
|
|
22
|
+
release_roots = %w[docs/ examples/ ext/ lib/]
|
|
23
|
+
spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
|
|
24
|
+
ls.readlines("\x0", chomp: true).select do |file|
|
|
25
|
+
release_files.include?(file) || file.start_with?(*release_roots)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
spec.files |= release_files.select { |file| File.file?(File.join(__dir__, file)) }
|
|
29
|
+
spec.extra_rdoc_files = %w[README.md LICENSE.txt]
|
|
30
|
+
spec.bindir = "exe"
|
|
31
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
32
|
+
spec.extensions = ["ext/gsplat_native/extconf.rb"]
|
|
33
|
+
spec.require_paths = ["lib"]
|
|
34
|
+
|
|
35
|
+
spec.add_dependency "numo-narray", ">= 0.9", "< 1.0"
|
|
36
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
# Lightweight reverse-mode differentiation for coarse gsplat operations.
|
|
5
|
+
module Autograd
|
|
6
|
+
# Thread-local key used to suspend graph recording.
|
|
7
|
+
# @api private
|
|
8
|
+
GRAD_ENABLED_KEY = :gsplat_autograd_grad_enabled
|
|
9
|
+
|
|
10
|
+
# Per-operation values retained for the backward pass.
|
|
11
|
+
class Context
|
|
12
|
+
attr_reader :inputs, :needs_input_grad, :saved_values
|
|
13
|
+
|
|
14
|
+
# @param needs_input_grad [Array<Boolean>] gradient requirement for each input
|
|
15
|
+
# @param inputs [Array<Object, Variable>] original Function inputs
|
|
16
|
+
def initialize(needs_input_grad, inputs = [])
|
|
17
|
+
@needs_input_grad = needs_input_grad.freeze
|
|
18
|
+
@inputs = inputs
|
|
19
|
+
@saved_values = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Saves values required by the operation's backward implementation.
|
|
23
|
+
#
|
|
24
|
+
# @param values [Array<Object>]
|
|
25
|
+
# @return [void]
|
|
26
|
+
def save(*values)
|
|
27
|
+
@saved_values.concat(values)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Releases references retained for backward.
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
33
|
+
def clear
|
|
34
|
+
@saved_values.clear
|
|
35
|
+
@inputs = []
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# Runs a block without recording an autograd graph.
|
|
42
|
+
#
|
|
43
|
+
# @yieldreturn [Object]
|
|
44
|
+
def no_grad
|
|
45
|
+
thread = Thread.current
|
|
46
|
+
previous = thread.thread_variable_get(GRAD_ENABLED_KEY)
|
|
47
|
+
thread.thread_variable_set(GRAD_ENABLED_KEY, false)
|
|
48
|
+
yield
|
|
49
|
+
ensure
|
|
50
|
+
thread.thread_variable_set(GRAD_ENABLED_KEY, previous)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Whether new operations should attach backward nodes.
|
|
54
|
+
#
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def grad_enabled?
|
|
57
|
+
Thread.current.thread_variable_get(GRAD_ENABLED_KEY) != false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Autograd
|
|
5
|
+
# A recorded invocation shared by all outputs of one Function.
|
|
6
|
+
class GraphNode
|
|
7
|
+
attr_reader :function, :context, :inputs
|
|
8
|
+
attr_accessor :outputs
|
|
9
|
+
|
|
10
|
+
def initialize(function, context, inputs)
|
|
11
|
+
@function = function
|
|
12
|
+
@context = context
|
|
13
|
+
@inputs = inputs
|
|
14
|
+
@outputs = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Delegates output gradients to the operation's backward method.
|
|
18
|
+
# @api private
|
|
19
|
+
def backward(*grad_outputs)
|
|
20
|
+
function.backward(context, *grad_outputs)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Releases graph references after a backward traversal.
|
|
24
|
+
# @api private
|
|
25
|
+
def release
|
|
26
|
+
outputs.each { |output| output.clear_creator(self) }
|
|
27
|
+
context.clear
|
|
28
|
+
@inputs = []
|
|
29
|
+
@outputs = []
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Base class for differentiable coarse-grained operations.
|
|
34
|
+
class Function
|
|
35
|
+
class << self
|
|
36
|
+
# Executes forward and records a graph node when any input needs a gradient.
|
|
37
|
+
#
|
|
38
|
+
# @param inputs [Array<Object, Variable>]
|
|
39
|
+
# @return [Variable, Array<Variable>]
|
|
40
|
+
def apply(*inputs, **)
|
|
41
|
+
needs_input_grad = inputs.map { |input| input.is_a?(Variable) && input.requires_grad? }
|
|
42
|
+
context = Context.new(needs_input_grad, inputs)
|
|
43
|
+
raw_inputs = inputs.map { |input| input.is_a?(Variable) ? input.data : input }
|
|
44
|
+
result = forward(context, *raw_inputs, **)
|
|
45
|
+
multiple_outputs = result.is_a?(Array)
|
|
46
|
+
output_data = multiple_outputs ? result : [result]
|
|
47
|
+
requires_grad = Autograd.grad_enabled? && needs_input_grad.any?
|
|
48
|
+
node = GraphNode.new(self, context, inputs) if requires_grad
|
|
49
|
+
outputs = output_data.map do |data|
|
|
50
|
+
validate_output!(data)
|
|
51
|
+
Variable.new(data, requires_grad: requires_grad, creator: node)
|
|
52
|
+
end
|
|
53
|
+
node.outputs = outputs if node
|
|
54
|
+
|
|
55
|
+
multiple_outputs ? outputs : outputs.first
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def validate_output!(data)
|
|
61
|
+
return if data.is_a?(Numo::NArray)
|
|
62
|
+
|
|
63
|
+
raise Gsplat::Error, "Function.forward must return Numo::NArray output(s), got #{data.class}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Autograd
|
|
5
|
+
# A Numo array with an optional reverse-mode gradient and creator node.
|
|
6
|
+
class Variable
|
|
7
|
+
attr_reader :absgrad, :data, :grad, :creator
|
|
8
|
+
|
|
9
|
+
# @param data [Numo::NArray] tensor data
|
|
10
|
+
# @param requires_grad [Boolean] whether to accumulate a gradient
|
|
11
|
+
# @param creator [GraphNode, nil] producing operation
|
|
12
|
+
def initialize(data, requires_grad: false, creator: nil)
|
|
13
|
+
raise ArgumentError, "Variable data must be a Numo::NArray" unless data.is_a?(Numo::NArray)
|
|
14
|
+
|
|
15
|
+
@data = data
|
|
16
|
+
@requires_grad = requires_grad
|
|
17
|
+
@creator = creator
|
|
18
|
+
@grad = nil
|
|
19
|
+
@absgrad = nil
|
|
20
|
+
@absgrad_targets = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Whether this variable accumulates gradients.
|
|
24
|
+
#
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
def requires_grad?
|
|
27
|
+
@requires_grad
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Clears an accumulated leaf gradient.
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
33
|
+
def zero_grad!
|
|
34
|
+
@grad = nil
|
|
35
|
+
@absgrad = nil
|
|
36
|
+
@absgrad_targets.each { |target, key| target[key] = nil }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Accumulates an auxiliary absolute gradient produced by rasterization.
|
|
40
|
+
#
|
|
41
|
+
# @api private
|
|
42
|
+
# @param gradient [Numo::NArray]
|
|
43
|
+
# @return [void]
|
|
44
|
+
def accumulate_absgrad(gradient)
|
|
45
|
+
gradient = cast_gradient(gradient)
|
|
46
|
+
unless gradient.shape == data.shape
|
|
47
|
+
raise ShapeError, "absgrad shape mismatch: expected #{data.shape.inspect}, got #{gradient.shape.inspect}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@absgrad = @absgrad ? @absgrad + gradient : gradient.dup
|
|
51
|
+
@absgrad_targets.each { |target, key| target[key] = @absgrad }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Binds auxiliary absolute-gradient updates to a metadata hash entry.
|
|
55
|
+
#
|
|
56
|
+
# @api private
|
|
57
|
+
# @return [void]
|
|
58
|
+
def bind_absgrad(target, key)
|
|
59
|
+
@absgrad_targets << [target, key]
|
|
60
|
+
target[key] = @absgrad
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Replaces parameter storage after a first-axis structural edit.
|
|
64
|
+
#
|
|
65
|
+
# @api private
|
|
66
|
+
# @param value [Numo::NArray]
|
|
67
|
+
# @return [void]
|
|
68
|
+
def replace_data!(value)
|
|
69
|
+
raise ArgumentError, "replacement data must be a Numo::NArray" unless value.is_a?(Numo::NArray)
|
|
70
|
+
|
|
71
|
+
@data = value
|
|
72
|
+
zero_grad!
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Removes a creator after its graph node has completed backward.
|
|
76
|
+
#
|
|
77
|
+
# @api private
|
|
78
|
+
# @param node [GraphNode]
|
|
79
|
+
# @return [void]
|
|
80
|
+
def clear_creator(node)
|
|
81
|
+
@creator = nil if @creator.equal?(node)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Runs reverse-mode differentiation from this variable.
|
|
85
|
+
#
|
|
86
|
+
# @param gradient [Numo::NArray, Numeric, nil] output gradient; nil is valid for scalar output
|
|
87
|
+
# @return [void]
|
|
88
|
+
def backward(gradient = nil)
|
|
89
|
+
raise Gsplat::Error, "cannot call backward on a variable that does not require gradients" unless requires_grad?
|
|
90
|
+
|
|
91
|
+
accumulate_grad(initial_gradient(gradient))
|
|
92
|
+
topological_nodes.reverse_each do |node|
|
|
93
|
+
grad_outputs = node.outputs.map { |output| output.grad || zeros_like(output.data) }
|
|
94
|
+
grad_inputs = normalize_grad_inputs(node.backward(*grad_outputs), node.inputs.length)
|
|
95
|
+
node.inputs.each_with_index do |input, index|
|
|
96
|
+
next unless input.is_a?(Variable) && node.context.needs_input_grad.fetch(index)
|
|
97
|
+
|
|
98
|
+
input.accumulate_grad(grad_inputs.fetch(index))
|
|
99
|
+
end
|
|
100
|
+
node.release
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
protected
|
|
105
|
+
|
|
106
|
+
def accumulate_grad(gradient)
|
|
107
|
+
return if gradient.nil?
|
|
108
|
+
|
|
109
|
+
gradient = cast_gradient(gradient)
|
|
110
|
+
unless gradient.shape == data.shape
|
|
111
|
+
raise ShapeError, "gradient shape mismatch: expected #{data.shape.inspect}, got #{gradient.shape.inspect}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
@grad = @grad ? @grad + gradient : gradient.dup
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def initial_gradient(gradient)
|
|
120
|
+
return cast_gradient(gradient) unless gradient.nil?
|
|
121
|
+
raise Gsplat::Error, "backward on a non-scalar output requires an explicit gradient" unless data.size == 1
|
|
122
|
+
|
|
123
|
+
data.shape.empty? ? data.class.cast(1) : data.class.ones(*data.shape)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def cast_gradient(gradient)
|
|
127
|
+
gradient = gradient.data if gradient.is_a?(Variable)
|
|
128
|
+
data.class.cast(gradient)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def zeros_like(array)
|
|
132
|
+
array.shape.empty? ? array.class.cast(0) : array.class.zeros(*array.shape)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def normalize_grad_inputs(result, input_count)
|
|
136
|
+
return result if input_count == 1 && result.is_a?(Array) && result.length == 1
|
|
137
|
+
return [result] if input_count == 1
|
|
138
|
+
return result if result.is_a?(Array) && result.length == input_count
|
|
139
|
+
|
|
140
|
+
raise Gsplat::Error, "Function.backward returned gradients for the wrong number of inputs"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def topological_nodes
|
|
144
|
+
ordered = []
|
|
145
|
+
visited = {}.compare_by_identity
|
|
146
|
+
visit = lambda do |node|
|
|
147
|
+
return unless node
|
|
148
|
+
return if visited.key?(node)
|
|
149
|
+
|
|
150
|
+
visited[node] = true
|
|
151
|
+
node.inputs.each { |input| visit.call(input.creator) if input.is_a?(Variable) }
|
|
152
|
+
ordered << node
|
|
153
|
+
end
|
|
154
|
+
visit.call(creator)
|
|
155
|
+
ordered
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|