gsplat 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +202 -0
  3. data/README.md +236 -0
  4. data/docs/ACCEPTANCE.md +60 -0
  5. data/docs/BENCHMARKS.md +79 -0
  6. data/docs/DECISIONS.md +52 -0
  7. data/docs/MIGRATION.md +117 -0
  8. data/docs/PROFILE.md +75 -0
  9. data/docs/PROGRESS.md +199 -0
  10. data/docs/decisions/0000-template.md +17 -0
  11. data/docs/decisions/0001-pin-golden-reference-and-cpu-validation.md +27 -0
  12. data/docs/decisions/0002-retain-ruby-fallbacks-for-native-backend.md +26 -0
  13. data/docs/decisions/0003-share-projection-for-distorted-cameras.md +26 -0
  14. data/docs/decisions/0004-use-portable-world-space-reference-paths.md +26 -0
  15. data/docs/decisions/0005-reuse-ewa-core-for-2dgs.md +24 -0
  16. data/docs/decisions/0006-keep-eval3d-as-portable-reference.md +25 -0
  17. data/docs/decisions/0007-share-compositor-semantics-for-contribution-indices.md +25 -0
  18. data/examples/data/README.md +14 -0
  19. data/examples/data/colmap/images/view_000.png +0 -0
  20. data/examples/data/colmap/images/view_001.png +0 -0
  21. data/examples/data/colmap/images/view_002.png +0 -0
  22. data/examples/data/colmap/sparse/0/cameras.txt +2 -0
  23. data/examples/data/colmap/sparse/0/images.txt +6 -0
  24. data/examples/data/colmap/sparse/0/points3D.txt +17 -0
  25. data/examples/data/splats.ply +0 -0
  26. data/examples/fit_image.rb +35 -0
  27. data/examples/generate_sample_data.rb +147 -0
  28. data/examples/render_path.rb +117 -0
  29. data/examples/simple_trainer.rb +76 -0
  30. data/ext/gsplat_native/common.h +54 -0
  31. data/ext/gsplat_native/extconf.rb +39 -0
  32. data/ext/gsplat_native/gsplat_native.c +60 -0
  33. data/ext/gsplat_native/intersections.c +211 -0
  34. data/ext/gsplat_native/projection.c +199 -0
  35. data/ext/gsplat_native/raster_backward.c +129 -0
  36. data/ext/gsplat_native/raster_backward_bridge.c +69 -0
  37. data/ext/gsplat_native/raster_forward.c +150 -0
  38. data/ext/gsplat_native/rasterization.h +51 -0
  39. data/ext/gsplat_native/spherical_harmonics.c +129 -0
  40. data/gsplat.gemspec +36 -0
  41. data/lib/gsplat/autograd/context.rb +60 -0
  42. data/lib/gsplat/autograd/function.rb +68 -0
  43. data/lib/gsplat/autograd/variable.rb +159 -0
  44. data/lib/gsplat/backend/ruby/accumulate.rb +139 -0
  45. data/lib/gsplat/backend/ruby/accumulate_backward.rb +40 -0
  46. data/lib/gsplat/backend/ruby/eval3d_rasterizer.rb +175 -0
  47. data/lib/gsplat/backend/ruby/isect_tiles.rb +198 -0
  48. data/lib/gsplat/backend/ruby/projection.rb +251 -0
  49. data/lib/gsplat/backend/ruby/projection_backward.rb +190 -0
  50. data/lib/gsplat/backend/ruby/projection_covariance_vjp.rb +72 -0
  51. data/lib/gsplat/backend/ruby/projection_input_vjp.rb +252 -0
  52. data/lib/gsplat/backend/ruby/quat_scale_to_covar_preci.rb +139 -0
  53. data/lib/gsplat/backend/ruby/rasterize_to_indices_in_range.rb +121 -0
  54. data/lib/gsplat/backend/ruby/rasterize_to_pixels.rb +199 -0
  55. data/lib/gsplat/backend/ruby/rasterize_to_pixels_backward.rb +121 -0
  56. data/lib/gsplat/backend/ruby/spherical_harmonics.rb +135 -0
  57. data/lib/gsplat/backend/ruby/tile_compositor.rb +50 -0
  58. data/lib/gsplat/backend/ruby/tile_compositor_backward.rb +84 -0
  59. data/lib/gsplat/backend.rb +79 -0
  60. data/lib/gsplat/compression/grid_sort.rb +79 -0
  61. data/lib/gsplat/compression/kmeans.rb +121 -0
  62. data/lib/gsplat/compression/png.rb +147 -0
  63. data/lib/gsplat/compression/png_codec.rb +133 -0
  64. data/lib/gsplat/compression/quantizer.rb +79 -0
  65. data/lib/gsplat/io/checkpoint.rb +145 -0
  66. data/lib/gsplat/io/colmap.rb +175 -0
  67. data/lib/gsplat/io/colmap_binary.rb +98 -0
  68. data/lib/gsplat/io/colmap_text.rb +84 -0
  69. data/lib/gsplat/io/image.rb +63 -0
  70. data/lib/gsplat/io/image_backends.rb +81 -0
  71. data/lib/gsplat/io/npy.rb +189 -0
  72. data/lib/gsplat/io/ply.rb +185 -0
  73. data/lib/gsplat/io/ply_reader.rb +142 -0
  74. data/lib/gsplat/io/zip_archive.rb +183 -0
  75. data/lib/gsplat/math/camera_distortion.rb +123 -0
  76. data/lib/gsplat/math/camera_projection.rb +202 -0
  77. data/lib/gsplat/math/mat.rb +114 -0
  78. data/lib/gsplat/math/quaternion.rb +175 -0
  79. data/lib/gsplat/math/small_matrix_primitives.rb +112 -0
  80. data/lib/gsplat/math/spherical_harmonic_basis.rb +148 -0
  81. data/lib/gsplat/math/ssim.rb +153 -0
  82. data/lib/gsplat/native.rb +30 -0
  83. data/lib/gsplat/native_ops.rb +148 -0
  84. data/lib/gsplat/native_raster_ops.rb +76 -0
  85. data/lib/gsplat/ops/accumulate.rb +68 -0
  86. data/lib/gsplat/ops/eval3d_rasterize.rb +62 -0
  87. data/lib/gsplat/ops/isect_tiles.rb +38 -0
  88. data/lib/gsplat/ops/projection.rb +154 -0
  89. data/lib/gsplat/ops/quat_scale_to_covar_preci.rb +118 -0
  90. data/lib/gsplat/ops/rasterize_to_indices_in_range.rb +38 -0
  91. data/lib/gsplat/ops/rasterize_to_pixels.rb +105 -0
  92. data/lib/gsplat/ops/relocation.rb +94 -0
  93. data/lib/gsplat/ops/spherical_harmonics.rb +71 -0
  94. data/lib/gsplat/ops/tensor_shape_ops.rb +173 -0
  95. data/lib/gsplat/ops/tensor_value_ops.rb +150 -0
  96. data/lib/gsplat/optim/adam.rb +212 -0
  97. data/lib/gsplat/optim/lr_scheduler.rb +36 -0
  98. data/lib/gsplat/optim/selective_adam.rb +68 -0
  99. data/lib/gsplat/rasterization.rb +130 -0
  100. data/lib/gsplat/rasterization_2dgs.rb +140 -0
  101. data/lib/gsplat/rasterization_helpers.rb +162 -0
  102. data/lib/gsplat/rasterization_validation.rb +99 -0
  103. data/lib/gsplat/strategy/base.rb +49 -0
  104. data/lib/gsplat/strategy/default.rb +188 -0
  105. data/lib/gsplat/strategy/mcmc.rb +103 -0
  106. data/lib/gsplat/strategy/mcmc_ops.rb +143 -0
  107. data/lib/gsplat/strategy/ops.rb +165 -0
  108. data/lib/gsplat/training/config.rb +91 -0
  109. data/lib/gsplat/training/image_fitter.rb +158 -0
  110. data/lib/gsplat/training/losses.rb +191 -0
  111. data/lib/gsplat/training/scene.rb +116 -0
  112. data/lib/gsplat/training/trainer.rb +236 -0
  113. data/lib/gsplat/utils.rb +110 -0
  114. data/lib/gsplat/version.rb +6 -0
  115. data/lib/gsplat.rb +98 -0
  116. metadata +181 -0
@@ -0,0 +1,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