@driftengine/capture 4.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 (199) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +29 -0
  3. package/README.md +68 -0
  4. package/dist/browserFrames.d.ts +31 -0
  5. package/dist/browserFrames.js +105 -0
  6. package/dist/collide.d.ts +59 -0
  7. package/dist/collide.js +92 -0
  8. package/dist/decimate.d.ts +42 -0
  9. package/dist/decimate.js +439 -0
  10. package/dist/delight.d.ts +42 -0
  11. package/dist/delight.js +351 -0
  12. package/dist/delightScene.d.ts +37 -0
  13. package/dist/delightScene.js +183 -0
  14. package/dist/depth.d.ts +44 -0
  15. package/dist/depth.js +53 -0
  16. package/dist/features.d.ts +33 -0
  17. package/dist/features.js +260 -0
  18. package/dist/frames.d.ts +47 -0
  19. package/dist/frames.js +87 -0
  20. package/dist/fusion.d.ts +77 -0
  21. package/dist/fusion.js +152 -0
  22. package/dist/gaussians/adam.d.ts +57 -0
  23. package/dist/gaussians/adam.js +88 -0
  24. package/dist/gaussians/depth.d.ts +8 -0
  25. package/dist/gaussians/depth.js +73 -0
  26. package/dist/gaussians/gradients.d.ts +36 -0
  27. package/dist/gaussians/gradients.js +228 -0
  28. package/dist/gaussians/harmonics.d.ts +46 -0
  29. package/dist/gaussians/harmonics.js +99 -0
  30. package/dist/gaussians/optimise.d.ts +50 -0
  31. package/dist/gaussians/optimise.js +125 -0
  32. package/dist/gaussians/parameters.d.ts +35 -0
  33. package/dist/gaussians/parameters.js +165 -0
  34. package/dist/gaussians/project.d.ts +72 -0
  35. package/dist/gaussians/project.js +137 -0
  36. package/dist/gaussians/projectGradient.d.ts +15 -0
  37. package/dist/gaussians/projectGradient.js +213 -0
  38. package/dist/gaussians/rasterise.d.ts +13 -0
  39. package/dist/gaussians/rasterise.js +65 -0
  40. package/dist/gaussians/refine.d.ts +36 -0
  41. package/dist/gaussians/refine.js +136 -0
  42. package/dist/gaussians/similarity.d.ts +25 -0
  43. package/dist/gaussians/similarity.js +80 -0
  44. package/dist/index.d.ts +101 -0
  45. package/dist/index.js +96 -0
  46. package/dist/marching.d.ts +38 -0
  47. package/dist/marching.js +258 -0
  48. package/dist/masks.d.ts +59 -0
  49. package/dist/masks.js +165 -0
  50. package/dist/math/dense.d.ts +68 -0
  51. package/dist/math/dense.js +294 -0
  52. package/dist/math/schur.d.ts +23 -0
  53. package/dist/math/schur.js +140 -0
  54. package/dist/models/batchNorm.d.ts +17 -0
  55. package/dist/models/batchNorm.js +24 -0
  56. package/dist/models/clipTokenizer.d.ts +40 -0
  57. package/dist/models/clipTokenizer.js +122 -0
  58. package/dist/models/clipTower.d.ts +27 -0
  59. package/dist/models/clipTower.js +45 -0
  60. package/dist/models/depthAnything2.d.ts +38 -0
  61. package/dist/models/depthAnything2.js +163 -0
  62. package/dist/models/depthAnything3.d.ts +26 -0
  63. package/dist/models/depthAnything3.js +171 -0
  64. package/dist/models/dualDpt.d.ts +19 -0
  65. package/dist/models/dualDpt.js +138 -0
  66. package/dist/models/hiera.d.ts +49 -0
  67. package/dist/models/hiera.js +120 -0
  68. package/dist/models/hieraAttention.d.ts +16 -0
  69. package/dist/models/hieraAttention.js +68 -0
  70. package/dist/models/miniature.d.ts +36 -0
  71. package/dist/models/miniature.js +216 -0
  72. package/dist/models/mobileSam.d.ts +41 -0
  73. package/dist/models/mobileSam.js +88 -0
  74. package/dist/models/owlv2.d.ts +49 -0
  75. package/dist/models/owlv2.js +131 -0
  76. package/dist/models/owlv2Detect.d.ts +25 -0
  77. package/dist/models/owlv2Detect.js +80 -0
  78. package/dist/models/owlv2Miniature.d.ts +27 -0
  79. package/dist/models/owlv2Miniature.js +87 -0
  80. package/dist/models/rope.d.ts +23 -0
  81. package/dist/models/rope.js +84 -0
  82. package/dist/models/sam21.d.ts +45 -0
  83. package/dist/models/sam21.js +126 -0
  84. package/dist/models/sam21Miniature.d.ts +37 -0
  85. package/dist/models/sam21Miniature.js +194 -0
  86. package/dist/models/sam21Video.d.ts +30 -0
  87. package/dist/models/sam21Video.js +101 -0
  88. package/dist/models/sam2Choice.d.ts +19 -0
  89. package/dist/models/sam2Choice.js +55 -0
  90. package/dist/models/sam2Decoder.d.ts +36 -0
  91. package/dist/models/sam2Decoder.js +108 -0
  92. package/dist/models/sam2Memories.d.ts +54 -0
  93. package/dist/models/sam2Memories.js +93 -0
  94. package/dist/models/sam2Memory.d.ts +30 -0
  95. package/dist/models/sam2Memory.js +136 -0
  96. package/dist/models/sam2Positions.d.ts +24 -0
  97. package/dist/models/sam2Positions.js +94 -0
  98. package/dist/models/sam2Tracker.d.ts +69 -0
  99. package/dist/models/sam2Tracker.js +172 -0
  100. package/dist/models/samDecoder.d.ts +45 -0
  101. package/dist/models/samDecoder.js +96 -0
  102. package/dist/models/samMiniature.d.ts +43 -0
  103. package/dist/models/samMiniature.js +161 -0
  104. package/dist/models/samPrompt.d.ts +57 -0
  105. package/dist/models/samPrompt.js +160 -0
  106. package/dist/models/seeded.d.ts +16 -0
  107. package/dist/models/seeded.js +48 -0
  108. package/dist/models/tinyVit.d.ts +38 -0
  109. package/dist/models/tinyVit.js +82 -0
  110. package/dist/models/twoWay.d.ts +46 -0
  111. package/dist/models/twoWay.js +67 -0
  112. package/dist/models/vit.d.ts +45 -0
  113. package/dist/models/vit.js +157 -0
  114. package/dist/models/windowAttention.d.ts +25 -0
  115. package/dist/models/windowAttention.js +104 -0
  116. package/dist/poses.d.ts +31 -0
  117. package/dist/poses.js +750 -0
  118. package/dist/prepare.d.ts +30 -0
  119. package/dist/prepare.js +238 -0
  120. package/dist/propose.d.ts +36 -0
  121. package/dist/propose.js +63 -0
  122. package/dist/resample.d.ts +39 -0
  123. package/dist/resample.js +165 -0
  124. package/dist/run.d.ts +3 -0
  125. package/dist/run.js +1 -0
  126. package/dist/scene.d.ts +104 -0
  127. package/dist/scene.js +109 -0
  128. package/dist/segment.d.ts +40 -0
  129. package/dist/segment.js +184 -0
  130. package/dist/testScene.d.ts +55 -0
  131. package/dist/testScene.js +214 -0
  132. package/dist/twoView.d.ts +22 -0
  133. package/dist/twoView.js +435 -0
  134. package/package.json +63 -0
  135. package/src/browserFrames.ts +145 -0
  136. package/src/collide.ts +128 -0
  137. package/src/decimate.ts +510 -0
  138. package/src/delight.ts +427 -0
  139. package/src/delightScene.ts +249 -0
  140. package/src/depth.ts +100 -0
  141. package/src/features.ts +302 -0
  142. package/src/frames.ts +111 -0
  143. package/src/fusion.ts +229 -0
  144. package/src/gaussians/adam.ts +131 -0
  145. package/src/gaussians/depth.ts +87 -0
  146. package/src/gaussians/gradients.ts +285 -0
  147. package/src/gaussians/harmonics.ts +122 -0
  148. package/src/gaussians/optimise.ts +191 -0
  149. package/src/gaussians/parameters.ts +186 -0
  150. package/src/gaussians/project.ts +207 -0
  151. package/src/gaussians/projectGradient.ts +234 -0
  152. package/src/gaussians/rasterise.ts +81 -0
  153. package/src/gaussians/refine.ts +176 -0
  154. package/src/gaussians/similarity.ts +87 -0
  155. package/src/index.ts +225 -0
  156. package/src/marching.ts +358 -0
  157. package/src/masks.ts +201 -0
  158. package/src/math/dense.ts +317 -0
  159. package/src/math/schur.ts +167 -0
  160. package/src/models/batchNorm.ts +57 -0
  161. package/src/models/clipTokenizer.ts +129 -0
  162. package/src/models/clipTower.ts +87 -0
  163. package/src/models/depthAnything2.ts +276 -0
  164. package/src/models/depthAnything3.ts +229 -0
  165. package/src/models/dualDpt.ts +233 -0
  166. package/src/models/hiera.ts +231 -0
  167. package/src/models/hieraAttention.ts +121 -0
  168. package/src/models/miniature.ts +241 -0
  169. package/src/models/mobileSam.ts +135 -0
  170. package/src/models/owlv2.ts +190 -0
  171. package/src/models/owlv2Detect.ts +116 -0
  172. package/src/models/owlv2Miniature.ts +108 -0
  173. package/src/models/rope.ts +111 -0
  174. package/src/models/sam21.ts +182 -0
  175. package/src/models/sam21Miniature.ts +225 -0
  176. package/src/models/sam21Video.ts +154 -0
  177. package/src/models/sam2Choice.ts +62 -0
  178. package/src/models/sam2Decoder.ts +218 -0
  179. package/src/models/sam2Memories.ts +165 -0
  180. package/src/models/sam2Memory.ts +247 -0
  181. package/src/models/sam2Positions.ts +106 -0
  182. package/src/models/sam2Tracker.ts +300 -0
  183. package/src/models/samDecoder.ts +225 -0
  184. package/src/models/samMiniature.ts +190 -0
  185. package/src/models/samPrompt.ts +227 -0
  186. package/src/models/seeded.ts +50 -0
  187. package/src/models/tinyVit.ts +178 -0
  188. package/src/models/twoWay.ts +122 -0
  189. package/src/models/vit.ts +265 -0
  190. package/src/models/windowAttention.ts +155 -0
  191. package/src/poses.ts +894 -0
  192. package/src/prepare.ts +306 -0
  193. package/src/propose.ts +96 -0
  194. package/src/resample.ts +197 -0
  195. package/src/run.ts +7 -0
  196. package/src/scene.ts +141 -0
  197. package/src/segment.ts +220 -0
  198. package/src/testScene.ts +283 -0
  199. package/src/twoView.ts +536 -0
@@ -0,0 +1,33 @@
1
+ /** The features of one frame, in the arrays a caller owns. */
2
+ export interface FeatureSet {
3
+ readonly x: Float32Array;
4
+ readonly y: Float32Array;
5
+ /** Shi–Tomasi's smaller eigenvalue: how strong a corner it is. */
6
+ readonly response: Float32Array;
7
+ /** The orientation, as a unit vector rather than an angle. */
8
+ readonly cos: Float32Array;
9
+ readonly sin: Float32Array;
10
+ count: number;
11
+ }
12
+ /** The descriptions of a frame's features: `BYTES` bytes each, bit by comparison. */
13
+ export interface Descriptors {
14
+ readonly bits: Uint8Array;
15
+ readonly count: number;
16
+ }
17
+ /** A description is 256 bits, which is 32 bytes. */
18
+ export declare const DESCRIPTOR_BYTES = 32;
19
+ export declare function createFeatureSet(budget: number): FeatureSet;
20
+ /**
21
+ * The strongest corners of `image`, RGBA at `width × height`, into `out` — at most `budget` of
22
+ * them, strongest first.
23
+ */
24
+ export declare function detectFeatures(image: Uint8Array, width: number, height: number, out: FeatureSet, budget: number): void;
25
+ /** A description of every feature in `features`, 32 bytes each. */
26
+ export declare function describeFeatures(image: Uint8Array, width: number, height: number, features: FeatureSet): Descriptors;
27
+ /**
28
+ * The matches between two frames' descriptions into `out`, two indices each, and how many there
29
+ * are. A match is mutual and clears Lowe's `ratio`, which defaults to 0.8.
30
+ */
31
+ export declare function matchFeatures(a: Descriptors, b: Descriptors, out: Int32Array, options?: {
32
+ readonly ratio?: number;
33
+ }): number;
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Corners in a frame, a binary description of each, and the matches between two frames.
3
+ *
4
+ * **A corner is a place where the image moves in two directions at once**, which is the one thing
5
+ * that can be found again from another angle: an edge slides along itself and a flat patch could be
6
+ * anywhere. So the response is Shi–Tomasi's — the smaller eigenvalue of the gradient's structure
7
+ * tensor, which is how much the weaker direction moves — kept where it is the largest in its own
8
+ * neighbourhood, and the strongest `budget` of those are the frame's features.
9
+ *
10
+ * **Each carries an orientation, and it is a vector rather than an angle.** The intensity centroid
11
+ * of a patch points away from its darker side; normalised, that is the cosine and sine the
12
+ * descriptor's pattern is turned by, so no inverse tangent is taken — which matters because this
13
+ * package is inside the determinism gate and `atan2` is one of the twenty-two functions no two
14
+ * engines must agree on.
15
+ *
16
+ * **The description is 256 comparisons of a blurred patch**, at pairs of places drawn once from a
17
+ * seed and turned by the feature's own orientation: a bit per pair, so two features compare in a
18
+ * few instructions and a frame's worth fits in memory. Blurred, because a comparison of two raw
19
+ * pixels is a comparison of two samples of the noise.
20
+ *
21
+ * **A match is mutual and unambiguous.** Each feature's nearest description in the other frame must
22
+ * also have it as its nearest, and each must be clearly nearer than its own second nearest —
23
+ * Lowe's ratio, applied from both sides — because a repeated texture answers every query equally
24
+ * well and a wrong match at this stage is a wrong camera later. **From both sides rather than one**,
25
+ * so matching the frames the other way round gives the same pairs; tested one-sided, it gave 46
26
+ * pairs one way and 48 the other, which would make a capture's answer depend on which frame it
27
+ * called first.
28
+ */
29
+ import { mulberry32 } from '@driftengine/core';
30
+ /** A description is 256 bits, which is 32 bytes. */
31
+ export const DESCRIPTOR_BYTES = 32;
32
+ const PAIRS = DESCRIPTOR_BYTES * 8;
33
+ /** The patch a description is drawn from, and the one an orientation is measured over. */
34
+ const PATCH = 15;
35
+ const SUPPRESS = 3;
36
+ /** A corner must reach this share of the frame's strongest to be one at all. */
37
+ const FLOOR = 0.01;
38
+ /**
39
+ * And this much movement outright, in luma squared. **A share alone is not enough**: a wall carries
40
+ * a level or two of sensor noise, and a share of the strongest noise is still noise — measured, an
41
+ * uncorrelated level of ±1 answers with 265 "corners" of up to 6, and ±2 with 268 of up to 17,
42
+ * while the test scene's weakest real corner is 180 and its strongest 12,100. **25 sits between
43
+ * them**, three orders below what a texture scores and above what noise reaches.
44
+ */
45
+ const LEAST = 25;
46
+ export function createFeatureSet(budget) {
47
+ return {
48
+ x: new Float32Array(budget),
49
+ y: new Float32Array(budget),
50
+ response: new Float32Array(budget),
51
+ cos: new Float32Array(budget),
52
+ sin: new Float32Array(budget),
53
+ count: 0,
54
+ };
55
+ }
56
+ /* The pattern, drawn once: pairs of offsets inside the patch, the same on every machine. */
57
+ const PATTERN = (() => {
58
+ const random = mulberry32(0x0c0ffee1);
59
+ const out = new Int8Array(PAIRS * 4);
60
+ const reach = (PATCH - 1) / 2;
61
+ for (let i = 0; i < PAIRS * 4; i += 1) {
62
+ /* A rounded draw over the patch, which keeps the pairs inside it once they are turned. */
63
+ out[i] = Math.round((random() * 2 - 1) * reach * 0.7);
64
+ }
65
+ return out;
66
+ })();
67
+ /** The frame as luma, one byte a pixel, which everything below reads. */
68
+ function luma(image, width, height) {
69
+ const out = new Float32Array(width * height);
70
+ for (let at = 0; at < width * height; at += 1) {
71
+ out[at] =
72
+ 0.299 * image[at * 4] +
73
+ 0.587 * image[at * 4 + 1] +
74
+ 0.114 * image[at * 4 + 2];
75
+ }
76
+ return out;
77
+ }
78
+ /** A 3 × 3 box blur, which is what a description compares rather than raw samples. */
79
+ function blurred(grey, width, height) {
80
+ const out = new Float32Array(grey.length);
81
+ for (let y = 0; y < height; y += 1) {
82
+ for (let x = 0; x < width; x += 1) {
83
+ let sum = 0;
84
+ for (let dy = -1; dy <= 1; dy += 1) {
85
+ const row = Math.min(height - 1, Math.max(0, y + dy)) * width;
86
+ for (let dx = -1; dx <= 1; dx += 1) {
87
+ sum += grey[row + Math.min(width - 1, Math.max(0, x + dx))];
88
+ }
89
+ }
90
+ out[y * width + x] = sum / 9;
91
+ }
92
+ }
93
+ return out;
94
+ }
95
+ /**
96
+ * The strongest corners of `image`, RGBA at `width × height`, into `out` — at most `budget` of
97
+ * them, strongest first.
98
+ */
99
+ export function detectFeatures(image, width, height, out, budget) {
100
+ const grey = luma(image, width, height);
101
+ const response = new Float32Array(width * height);
102
+ const border = (PATCH - 1) / 2 + 1;
103
+ let strongest = 0;
104
+ for (let y = border; y < height - border; y += 1) {
105
+ for (let x = border; x < width - border; x += 1) {
106
+ /* The structure tensor over a 3 × 3 window of central differences. */
107
+ let xx = 0;
108
+ let yy = 0;
109
+ let xy = 0;
110
+ for (let dy = -1; dy <= 1; dy += 1) {
111
+ for (let dx = -1; dx <= 1; dx += 1) {
112
+ const at = (y + dy) * width + x + dx;
113
+ const gx = (grey[at + 1] - grey[at - 1]) / 2;
114
+ const gy = (grey[at + width] - grey[at - width]) / 2;
115
+ xx += gx * gx;
116
+ yy += gy * gy;
117
+ xy += gx * gy;
118
+ }
119
+ }
120
+ const half = (xx + yy) / 2;
121
+ const side = (xx - yy) / 2;
122
+ /* Shi–Tomasi: the smaller eigenvalue, which is the weaker direction's movement. */
123
+ const smaller = half - Math.sqrt(side * side + xy * xy);
124
+ response[y * width + x] = smaller;
125
+ strongest = Math.max(strongest, smaller);
126
+ }
127
+ }
128
+ /* Keep a response that is the largest within its neighbourhood and above the floor. */
129
+ const floor = Math.max(strongest * FLOOR, LEAST);
130
+ const kept = [];
131
+ for (let y = border; y < height - border; y += 1) {
132
+ for (let x = border; x < width - border; x += 1) {
133
+ const here = response[y * width + x];
134
+ if (here < floor)
135
+ continue;
136
+ let best = true;
137
+ for (let dy = -SUPPRESS; dy <= SUPPRESS && best; dy += 1) {
138
+ for (let dx = -SUPPRESS; dx <= SUPPRESS; dx += 1) {
139
+ if (dx === 0 && dy === 0)
140
+ continue;
141
+ const other = response[(y + dy) * width + x + dx];
142
+ /* A tie goes to the earlier pixel, so the answer does not depend on the scan. */
143
+ if (other > here || (other === here && (dy < 0 || (dy === 0 && dx < 0)))) {
144
+ best = false;
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ if (best)
150
+ kept.push(y * width + x);
151
+ }
152
+ }
153
+ kept.sort((a, b) => response[b] - response[a] || a - b);
154
+ const reach = (PATCH - 1) / 2;
155
+ out.count = Math.min(budget, out.x.length, kept.length);
156
+ for (let i = 0; i < out.count; i += 1) {
157
+ const at = kept[i];
158
+ const x = at % width;
159
+ const y = (at - x) / width;
160
+ out.x[i] = x;
161
+ out.y[i] = y;
162
+ out.response[i] = response[at];
163
+ /* The intensity centroid of the patch, as a direction. */
164
+ let mx = 0;
165
+ let my = 0;
166
+ for (let dy = -reach; dy <= reach; dy += 1) {
167
+ for (let dx = -reach; dx <= reach; dx += 1) {
168
+ if (dx * dx + dy * dy > reach * reach)
169
+ continue;
170
+ const value = grey[(y + dy) * width + x + dx];
171
+ mx += dx * value;
172
+ my += dy * value;
173
+ }
174
+ }
175
+ const length = Math.sqrt(mx * mx + my * my);
176
+ out.cos[i] = length === 0 ? 1 : mx / length;
177
+ out.sin[i] = length === 0 ? 0 : my / length;
178
+ }
179
+ }
180
+ /** A description of every feature in `features`, 32 bytes each. */
181
+ export function describeFeatures(image, width, height, features) {
182
+ const smooth = blurred(luma(image, width, height), width, height);
183
+ const bits = new Uint8Array(features.count * DESCRIPTOR_BYTES);
184
+ for (let i = 0; i < features.count; i += 1) {
185
+ const x = features.x[i];
186
+ const y = features.y[i];
187
+ const cos = features.cos[i];
188
+ const sin = features.sin[i];
189
+ for (let pair = 0; pair < PAIRS; pair += 1) {
190
+ const sample = (at) => {
191
+ const dx = PATTERN[pair * 4 + at * 2];
192
+ const dy = PATTERN[pair * 4 + at * 2 + 1];
193
+ /* The pattern turns with the feature, which is what makes it survive a rotation. */
194
+ const rx = Math.round(x + cos * dx - sin * dy);
195
+ const ry = Math.round(y + sin * dx + cos * dy);
196
+ const cx = Math.min(width - 1, Math.max(0, rx));
197
+ const cy = Math.min(height - 1, Math.max(0, ry));
198
+ return smooth[cy * width + cx];
199
+ };
200
+ if (sample(0) < sample(1)) {
201
+ bits[i * DESCRIPTOR_BYTES + (pair >> 3)] |= 1 << (pair & 7);
202
+ }
203
+ }
204
+ }
205
+ return { bits, count: features.count };
206
+ }
207
+ const ONES = (() => {
208
+ const out = new Uint8Array(256);
209
+ for (let i = 0; i < 256; i += 1)
210
+ out[i] = (i & 1) + out[i >> 1];
211
+ return out;
212
+ })();
213
+ function distance(a, at, b, to) {
214
+ let sum = 0;
215
+ for (let byte = 0; byte < DESCRIPTOR_BYTES; byte += 1) {
216
+ sum += ONES[a[at + byte] ^ b[to + byte]];
217
+ }
218
+ return sum;
219
+ }
220
+ /**
221
+ * The matches between two frames' descriptions into `out`, two indices each, and how many there
222
+ * are. A match is mutual and clears Lowe's `ratio`, which defaults to 0.8.
223
+ */
224
+ export function matchFeatures(a, b, out, options = {}) {
225
+ const ratio = options.ratio ?? 0.8;
226
+ const bestOf = (from, to, index) => {
227
+ let at = -1;
228
+ let best = Infinity;
229
+ let second = Infinity;
230
+ for (let j = 0; j < to.count; j += 1) {
231
+ const off = distance(from.bits, index * DESCRIPTOR_BYTES, to.bits, j * DESCRIPTOR_BYTES);
232
+ if (off < best) {
233
+ second = best;
234
+ best = off;
235
+ at = j;
236
+ }
237
+ else if (off < second) {
238
+ second = off;
239
+ }
240
+ }
241
+ return { at, best, second };
242
+ };
243
+ let count = 0;
244
+ for (let i = 0; i < a.count && count * 2 + 1 < out.length; i += 1) {
245
+ const forward = bestOf(a, b, i);
246
+ if (forward.at < 0)
247
+ continue;
248
+ /* Unambiguous: the nearest must be clearly nearer than whatever came second. */
249
+ if (!(forward.best < ratio * forward.second))
250
+ continue;
251
+ /* And mutual, by the same rule from the other side: matching either way gives one answer. */
252
+ const back = bestOf(b, a, forward.at);
253
+ if (back.at !== i || !(back.best < ratio * back.second))
254
+ continue;
255
+ out[count * 2] = i;
256
+ out[count * 2 + 1] = forward.at;
257
+ count += 1;
258
+ }
259
+ return count;
260
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * A clip's frames, and which of them a capture keeps.
3
+ *
4
+ * **The engine never opens a file.** A host hands it a `FrameSource` — a video element in a
5
+ * browser, a decoder on a native host, a folder of stills in a test — and the engine reads it one
6
+ * frame at a time, in order, into a buffer of its own. A minute of 1080p is eight gigabytes of
7
+ * pixels, so a clip is walked rather than held, and what a capture keeps is chosen as it goes past.
8
+ *
9
+ * **A frame may arrive later than it is asked for**, since every browser decoder is asynchronous, so
10
+ * `frameAt` may answer a promise and the two readers here are `async`. A host with its frames
11
+ * already in hand answers without one and pays nothing: an awaited value that is not a promise
12
+ * costs a microtask, and this is a clip's pace rather than a frame's.
13
+ *
14
+ * **Frames are chosen by motion, not by number.** What reconstruction needs is baseline: two views
15
+ * far enough apart to triangulate, and no more of them than the budget allows. So the frames kept
16
+ * are spaced by equal motion — a slow pan gives up most of its frames, a fast one keeps them — and
17
+ * a clip that never moves is one frame, which is what it holds. `every` is the other way to choose,
18
+ * for a caller that knows its clip: every Nth frame, under the same budget.
19
+ *
20
+ * **Motion is the mean absolute difference between one frame and the last**, over the colours and
21
+ * not the alpha, which is a measure of how much the picture changed rather than of how far anything
22
+ * travelled. **What it gives up** is telling a pan from a light being switched on; what would make
23
+ * it wrong is a clip whose brightness swings without the camera moving, and the answer then is the
24
+ * feature matching of Task 9 rather than a cleverer average.
25
+ */
26
+ export interface FrameSize {
27
+ readonly width: number;
28
+ readonly height: number;
29
+ }
30
+ export interface FrameSource {
31
+ frameCount(): number;
32
+ /**
33
+ * Writes frame `index` as RGBA into `out` and answers the frame's size. A buffer too small for
34
+ * the frame is left untouched, so a caller may ask with an empty one to learn the size first.
35
+ */
36
+ frameAt(index: number, out: Uint8Array): FrameSize | Promise<FrameSize>;
37
+ }
38
+ export interface FrameChoice {
39
+ /** At most this many frames. */
40
+ readonly budget: number;
41
+ /** Every Nth frame instead of by motion. */
42
+ readonly every?: number;
43
+ }
44
+ /** Every frame in order, each read over the last into one buffer the visitor must not keep. */
45
+ export declare function eachFrame(source: FrameSource, visit: (index: number, pixels: Uint8Array, width: number, height: number) => void): Promise<void>;
46
+ /** The frames a capture keeps, in order, the first among them. */
47
+ export declare function selectFrames(source: FrameSource, choice: FrameChoice): Promise<number[]>;
package/dist/frames.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * A clip's frames, and which of them a capture keeps.
3
+ *
4
+ * **The engine never opens a file.** A host hands it a `FrameSource` — a video element in a
5
+ * browser, a decoder on a native host, a folder of stills in a test — and the engine reads it one
6
+ * frame at a time, in order, into a buffer of its own. A minute of 1080p is eight gigabytes of
7
+ * pixels, so a clip is walked rather than held, and what a capture keeps is chosen as it goes past.
8
+ *
9
+ * **A frame may arrive later than it is asked for**, since every browser decoder is asynchronous, so
10
+ * `frameAt` may answer a promise and the two readers here are `async`. A host with its frames
11
+ * already in hand answers without one and pays nothing: an awaited value that is not a promise
12
+ * costs a microtask, and this is a clip's pace rather than a frame's.
13
+ *
14
+ * **Frames are chosen by motion, not by number.** What reconstruction needs is baseline: two views
15
+ * far enough apart to triangulate, and no more of them than the budget allows. So the frames kept
16
+ * are spaced by equal motion — a slow pan gives up most of its frames, a fast one keeps them — and
17
+ * a clip that never moves is one frame, which is what it holds. `every` is the other way to choose,
18
+ * for a caller that knows its clip: every Nth frame, under the same budget.
19
+ *
20
+ * **Motion is the mean absolute difference between one frame and the last**, over the colours and
21
+ * not the alpha, which is a measure of how much the picture changed rather than of how far anything
22
+ * travelled. **What it gives up** is telling a pan from a light being switched on; what would make
23
+ * it wrong is a clip whose brightness swings without the camera moving, and the answer then is the
24
+ * feature matching of Task 9 rather than a cleverer average.
25
+ */
26
+ const NOTHING = new Uint8Array(0);
27
+ /** Every frame in order, each read over the last into one buffer the visitor must not keep. */
28
+ export async function eachFrame(source, visit) {
29
+ const count = source.frameCount();
30
+ if (count <= 0)
31
+ return;
32
+ const { width, height } = await source.frameAt(0, NOTHING);
33
+ const pixels = new Uint8Array(width * height * 4);
34
+ for (let index = 0; index < count; index += 1) {
35
+ const size = await source.frameAt(index, pixels);
36
+ visit(index, pixels, size.width, size.height);
37
+ }
38
+ }
39
+ /** The frames a capture keeps, in order, the first among them. */
40
+ export async function selectFrames(source, choice) {
41
+ const count = source.frameCount();
42
+ const budget = Math.max(1, Math.floor(choice.budget));
43
+ if (count <= 0)
44
+ return [];
45
+ if (choice.every !== undefined) {
46
+ const kept = [];
47
+ for (let index = 0; index < count && kept.length < budget; index += choice.every) {
48
+ kept.push(index);
49
+ }
50
+ return kept;
51
+ }
52
+ if (count <= budget)
53
+ return Array.from({ length: count }, (_, index) => index);
54
+ /* One pass for the motion between each frame and the last, keeping a copy of that last frame. */
55
+ const travelled = new Float64Array(count);
56
+ let last = null;
57
+ await eachFrame(source, (index, pixels) => {
58
+ if (last !== null) {
59
+ let sum = 0;
60
+ for (let at = 0; at < pixels.length; at += 4) {
61
+ sum +=
62
+ Math.abs(pixels[at] - last[at]) +
63
+ Math.abs(pixels[at + 1] - last[at + 1]) +
64
+ Math.abs(pixels[at + 2] - last[at + 2]);
65
+ }
66
+ travelled[index] = travelled[index - 1] + sum / ((pixels.length / 4) * 3);
67
+ }
68
+ else {
69
+ last = new Uint8Array(pixels.length);
70
+ }
71
+ last.set(pixels);
72
+ });
73
+ const total = travelled[count - 1];
74
+ if (total === 0)
75
+ return [0];
76
+ /* Spaced so the whole clip's motion divides into the budget's frames, the last one included. */
77
+ const step = total / (budget - 1);
78
+ const kept = [0];
79
+ let since = 0;
80
+ for (let index = 1; index < count && kept.length < budget; index += 1) {
81
+ if (travelled[index] - since >= step * (1 - 1e-9)) {
82
+ kept.push(index);
83
+ since = travelled[index];
84
+ }
85
+ }
86
+ return kept;
87
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Many views of a scene fused into one volume: a truncated signed distance, weighted by how
3
+ * squarely each view saw the surface.
4
+ *
5
+ * **Why a volume rather than the depth maps themselves.** Every view has a surface in it and no two
6
+ * agree exactly — noise, a fit's own error, and the silhouettes where a splat cloud thins out. A
7
+ * volume is where they are averaged: each view writes its opinion of the distance to the nearest
8
+ * surface into the samples along its own rays, and what comes out is the surface every view agrees
9
+ * on. `marchVolume` then reads the crossing.
10
+ *
11
+ * **Negative inside**, which is `ObjectSdf`'s convention in `@driftengine/assets` and the one the
12
+ * rest of this engine reads a distance field by. A sample nearer the camera than the surface is
13
+ * outside it and positive.
14
+ *
15
+ * **Truncated, because a depth map knows nothing about the far side of what it sees.** Past a few
16
+ * samples behind the surface a view has no information at all — the space there might be solid,
17
+ * might be another room — so writing a large negative distance would be inventing one. Everything
18
+ * beyond the truncation on the far side is left alone; everything beyond it on the near side is
19
+ * free space and is written as such, which is what carves the empty air out.
20
+ *
21
+ * **Weighted by the angle, which is what stops a grazing view winning.** A surface seen almost
22
+ * edge-on is a few pixels wide and every one of them is uncertain in depth by the whole width of
23
+ * the surface; seen square on, the same surface is measured well. The weight is the cosine between
24
+ * the view's own ray and the surface's normal, taken from the depth map's own gradient, so a
25
+ * grazing view contributes what it is worth rather than as much as any other.
26
+ *
27
+ * **A surface one view saw is kept, at the weight one view is worth.** Dropping it would leave a
28
+ * hole where the capture only ever looked once, which is most of a real clip's edges.
29
+ */
30
+ import type { RasterCamera } from './gaussians/project.ts';
31
+ /**
32
+ * One view's opinion: its depth, how well covered each pixel was, and where it stood.
33
+ *
34
+ * **Not `DepthView`, which is the depth *model's* shape** — that one carries a confidence and its
35
+ * own 3 × 3 intrinsics and does not know the size of its own frame. The two say much the same thing
36
+ * in two forms, and the pipeline that joins them is the place to convert, not here.
37
+ */
38
+ export interface SurfaceView {
39
+ /** Along the camera's own axis, in metres; zero where nothing was seen. */
40
+ readonly depth: Float32Array;
41
+ /** Nought to one, as `renderDepth` answers it. */
42
+ readonly coverage: Float32Array;
43
+ readonly camera: RasterCamera;
44
+ }
45
+ /** A field of samples over a box of the world, `x` fastest, then `y`, then `z`. */
46
+ export interface Volume {
47
+ readonly dims: readonly [number, number, number];
48
+ /** Where the first sample sits, in the capture's world. */
49
+ readonly origin: readonly [number, number, number];
50
+ /** Metres between samples, the same on every axis. */
51
+ readonly spacing: number;
52
+ /** Truncated signed distance in metres, **negative inside**. */
53
+ readonly distance: Float32Array;
54
+ /** How much evidence each sample has; zero where no view ever saw it. */
55
+ readonly weight: Float32Array;
56
+ }
57
+ export interface FuseOptions {
58
+ /**
59
+ * How far either side of a surface a view is believed, in metres.
60
+ *
61
+ * Three samples by default: enough that the crossing is bracketed by samples with real evidence
62
+ * on both sides, which is what marching needs, and short enough that one view's surface does not
63
+ * reach through a wall into the next room.
64
+ */
65
+ readonly truncation?: number;
66
+ /** Below this coverage a pixel's depth is an average of too little light to believe. */
67
+ readonly minimumCoverage?: number;
68
+ }
69
+ export declare function createVolume(dims: readonly [number, number, number], origin: readonly [number, number, number], spacing: number): Volume;
70
+ /**
71
+ * Every view's surface written into `volume`, which accumulates rather than replaces.
72
+ *
73
+ * Calling this twice with two halves of the views is the same as calling it once with all of them,
74
+ * because a weighted mean is what each sample holds — which is what lets a clip be fused as it
75
+ * arrives rather than all at once.
76
+ */
77
+ export declare function fuseDepth(views: readonly SurfaceView[], volume: Volume, options?: FuseOptions): void;
package/dist/fusion.js ADDED
@@ -0,0 +1,152 @@
1
+ const DEFAULT_COVERAGE = 0.5;
2
+ export function createVolume(dims, origin, spacing) {
3
+ const [nx, ny, nz] = dims;
4
+ return {
5
+ dims,
6
+ origin,
7
+ spacing,
8
+ distance: new Float32Array(nx * ny * nz),
9
+ weight: new Float32Array(nx * ny * nz),
10
+ };
11
+ }
12
+ /**
13
+ * Every view's surface written into `volume`, which accumulates rather than replaces.
14
+ *
15
+ * Calling this twice with two halves of the views is the same as calling it once with all of them,
16
+ * because a weighted mean is what each sample holds — which is what lets a clip be fused as it
17
+ * arrives rather than all at once.
18
+ */
19
+ export function fuseDepth(views, volume, options = {}) {
20
+ const [nx, ny, nz] = volume.dims;
21
+ const truncation = options.truncation ?? volume.spacing * 3;
22
+ const minimumCoverage = options.minimumCoverage ?? DEFAULT_COVERAGE;
23
+ if (!(truncation > 0))
24
+ throw new RangeError('capture: a fusion needs a truncation above zero');
25
+ for (const view of views) {
26
+ const { width, height, intrinsics } = view.camera;
27
+ const [fx, fy, cx, cy] = intrinsics;
28
+ const m = view.camera.worldToCamera;
29
+ const normals = surfaceNormals(view);
30
+ for (let k = 0; k < nz; k += 1) {
31
+ for (let j = 0; j < ny; j += 1) {
32
+ for (let i = 0; i < nx; i += 1) {
33
+ const world = [
34
+ volume.origin[0] + i * volume.spacing,
35
+ volume.origin[1] + j * volume.spacing,
36
+ volume.origin[2] + k * volume.spacing,
37
+ ];
38
+ let viewX = 0;
39
+ let viewY = 0;
40
+ let viewZ = 0;
41
+ for (let r = 0; r < 3; r += 1) {
42
+ const value = m[r * 4] * world[0] +
43
+ m[r * 4 + 1] * world[1] +
44
+ m[r * 4 + 2] * world[2] +
45
+ m[r * 4 + 3];
46
+ if (r === 0)
47
+ viewX = value;
48
+ else if (r === 1)
49
+ viewY = value;
50
+ else
51
+ viewZ = value;
52
+ }
53
+ if (!(viewZ > 0))
54
+ continue;
55
+ const px = Math.floor((fx * viewX) / viewZ + cx);
56
+ const py = Math.floor((fy * viewY) / viewZ + cy);
57
+ if (px < 0 || px >= width || py < 0 || py >= height)
58
+ continue;
59
+ const pixel = py * width + px;
60
+ if (view.coverage[pixel] < minimumCoverage)
61
+ continue;
62
+ const measured = view.depth[pixel];
63
+ if (!(measured > 0))
64
+ continue;
65
+ /*
66
+ * How squarely this view saw that surface: the ray through the pixel against the
67
+ * surface's own normal, both in the camera's frame. A view looking straight at it is
68
+ * worth one and a grazing view almost nothing.
69
+ */
70
+ const rayX = (px + 0.5 - cx) / fx;
71
+ const rayY = (py + 0.5 - cy) / fy;
72
+ const rayLength = Math.sqrt(rayX * rayX + rayY * rayY + 1);
73
+ const facing = -((rayX * normals[pixel * 3] +
74
+ rayY * normals[pixel * 3 + 1] +
75
+ normals[pixel * 3 + 2]) /
76
+ rayLength);
77
+ const angle = facing > 0 ? facing : 0;
78
+ const weight = angle * view.coverage[pixel];
79
+ if (!(weight > 0))
80
+ continue;
81
+ /*
82
+ * **In front of the surface is outside it, and the distance is measured along the
83
+ * surface's normal rather than along the camera's axis.** The difference of two depths is
84
+ * a distance along the view's own ray, which is the perpendicular distance only where the
85
+ * surface faces the camera squarely; everywhere else it overstates it by the obliquity.
86
+ * For a plane the correction is exact, and for a curved surface it is the first-order
87
+ * term — measured on a sphere fused from eight views, it took the median error in the
88
+ * band from 25 mm to 11 and the ninetieth percentile from 55 mm to 43. **The worst sample
89
+ * does not move**, because it is one at the very edge of the band whose distance
90
+ * saturates at the truncation either way.
91
+ */
92
+ const signed = (measured - viewZ) * rayLength * angle;
93
+ if (signed < -truncation)
94
+ continue;
95
+ const clamped = signed > truncation ? truncation : signed;
96
+ const at = (k * ny + j) * nx + i;
97
+ const held = volume.weight[at];
98
+ const total = held + weight;
99
+ volume.distance[at] = (volume.distance[at] * held + clamped * weight) / total;
100
+ volume.weight[at] = total;
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ /**
107
+ * A normal per pixel, in the camera's frame, from the depth map's own gradient.
108
+ *
109
+ * **Central differences, and a pixel whose neighbours are not both surfaces gets the view's own
110
+ * axis instead.** At a silhouette the difference is across the gap between a near surface and a far
111
+ * one, which is not a gradient of anything; answering the axis there says "seen square on", which
112
+ * is generous rather than wrong, and the coverage is what actually discounts those pixels.
113
+ */
114
+ function surfaceNormals(view) {
115
+ const { width, height, intrinsics } = view.camera;
116
+ const [fx, fy, cx, cy] = intrinsics;
117
+ const out = new Float64Array(width * height * 3);
118
+ const at = (x, y) => {
119
+ if (x < 0 || x >= width || y < 0 || y >= height)
120
+ return null;
121
+ const z = view.depth[y * width + x];
122
+ if (!(z > 0))
123
+ return null;
124
+ return [((x + 0.5 - cx) * z) / fx, ((y + 0.5 - cy) * z) / fy, z];
125
+ };
126
+ for (let y = 0; y < height; y += 1) {
127
+ for (let x = 0; x < width; x += 1) {
128
+ const pixel = y * width + x;
129
+ out[pixel * 3 + 2] = -1;
130
+ const left = at(x - 1, y);
131
+ const right = at(x + 1, y);
132
+ const up = at(x, y - 1);
133
+ const down = at(x, y + 1);
134
+ if (left === null || right === null || up === null || down === null)
135
+ continue;
136
+ const ax = [right[0] - left[0], right[1] - left[1], right[2] - left[2]];
137
+ const ay = [down[0] - up[0], down[1] - up[1], down[2] - up[2]];
138
+ const nx = ay[1] * ax[2] - ay[2] * ax[1];
139
+ const ny = ay[2] * ax[0] - ay[0] * ax[2];
140
+ const nz = ay[0] * ax[1] - ay[1] * ax[0];
141
+ const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
142
+ if (!(length > 0))
143
+ continue;
144
+ /* Towards the camera, which is the negative z half of its own frame. */
145
+ const sign = nz > 0 ? -1 : 1;
146
+ out[pixel * 3] = (sign * nx) / length;
147
+ out[pixel * 3 + 1] = (sign * ny) / length;
148
+ out[pixel * 3 + 2] = (sign * nz) / length;
149
+ }
150
+ }
151
+ return out;
152
+ }