@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,358 @@
1
+ /**
2
+ * A volume's zero crossing as a mesh, facing outwards.
3
+ *
4
+ * **Tetrahedra rather than cubes, and the reason is watertightness rather than taste.** Marching
5
+ * cubes classifies a cube's eight corners into 256 cases, and a handful of those are *ambiguous*:
6
+ * the same corner signs admit two different surfaces, and two neighbouring cubes that resolve one
7
+ * the two different ways leave a hole between them. Every implementation patches this, and the
8
+ * patches are the part that gets subtly wrong. A tetrahedron has four corners and sixteen cases,
9
+ * **none of them ambiguous** — a plane through a tetrahedron cuts it one way — so a mesh built this
10
+ * way is closed wherever the volume is known, by construction rather than by a table of exceptions.
11
+ *
12
+ * What it costs is triangles: roughly twice what marching cubes emits for the same surface, because
13
+ * a cube is cut six ways rather than once. `decimate` is the answer to that, and it is a better
14
+ * answer than an ambiguity table because it is driven by the error it introduces.
15
+ *
16
+ * **A cube with an unknown corner is not marched at all.** A sample no view ever saw has no
17
+ * distance, and a surface built through it is one the capture invented — which is exactly the
18
+ * spurious closing surface an open scene must not grow. The boundary of what was seen is left open,
19
+ * and that is the honest shape of a clip that walked through a room rather than around an object.
20
+ *
21
+ * **Normals come from the volume's gradient, and their sign is checked against it.** An inverted
22
+ * mesh collides inside-out: a character walks through the floor and falls out of the world, which
23
+ * reads as a physics fault and is a meshing one.
24
+ */
25
+ import type { MeshData } from '@driftengine/drft';
26
+
27
+ import type { Volume } from './fusion.ts';
28
+
29
+ /**
30
+ * The six tetrahedra a cube is cut into, by corner index.
31
+ *
32
+ * Corner `n` is the one at `(n & 1, (n >> 1) & 1, (n >> 2) & 1)`. Every tetrahedron holds corners
33
+ * 0 and 7 — the cube's main diagonal — which is what makes the cut the same for every cube and so
34
+ * agrees across their shared faces. A cut chosen per cube would leave the faces between them
35
+ * triangulated two different ways, which is the hole this arrangement exists to avoid.
36
+ */
37
+ const TETRAHEDRA: readonly (readonly [number, number, number, number])[] = [
38
+ [0, 7, 1, 3],
39
+ [0, 7, 3, 2],
40
+ [0, 7, 2, 6],
41
+ [0, 7, 6, 4],
42
+ [0, 7, 4, 5],
43
+ [0, 7, 5, 1],
44
+ ];
45
+
46
+ /** The six edges of a tetrahedron, as pairs of its own corner slots. */
47
+ const TETRA_EDGES: readonly (readonly [number, number])[] = [
48
+ [0, 1],
49
+ [0, 2],
50
+ [0, 3],
51
+ [1, 2],
52
+ [1, 3],
53
+ [2, 3],
54
+ ];
55
+
56
+ export interface MarchOptions {
57
+ /** A sample with less evidence than this is treated as unknown, and its cube is not marched. */
58
+ readonly minimumWeight?: number;
59
+ }
60
+
61
+ const DEFAULT_WEIGHT = 1e-6;
62
+
63
+ /**
64
+ * How far a crossing is kept from either end of the edge it is on, as a fraction of that edge.
65
+ *
66
+ * **A surface passing exactly through a sample is the one degenerate case tetrahedra do not save
67
+ * you from.** Every edge out of that sample crosses at its very end, so several crossings land on
68
+ * the same point and the triangles between them have no area at all — measured on a sphere whose
69
+ * radius is a whole number of samples: **280 faces of 3,960 with zero area**, and 256 of those with
70
+ * no direction to wind by, because a degenerate triangle has no normal. They cannot simply be
71
+ * dropped: their edges are shared with real faces, and dropping them opens the mesh.
72
+ *
73
+ * So the crossing is held a thousandth of an edge away from each end instead. At a tenth-metre
74
+ * spacing that is a tenth of a millimetre of geometry given up, and it makes the case stop
75
+ * existing rather than handling it.
76
+ */
77
+ const MARGIN = 1e-3;
78
+
79
+ /**
80
+ * `volume`'s surface, welded so that neighbours share vertices exactly.
81
+ *
82
+ * A vertex belongs to the *edge* it was found on, so two tetrahedra meeting along an edge find the
83
+ * same one — which is what closes the mesh. Answers the mesh; a volume with no crossing answers an
84
+ * empty one rather than throwing, because a capture of nothing is a real thing to have.
85
+ */
86
+ export function marchVolume(volume: Volume, options: MarchOptions = {}): MeshData {
87
+ const [nx, ny, nz] = volume.dims;
88
+ const minimumWeight = options.minimumWeight ?? DEFAULT_WEIGHT;
89
+ const positions: number[] = [];
90
+ const normals: number[] = [];
91
+ const indices: number[] = [];
92
+ /* Which vertex each crossed edge already has, keyed by its two sample indices. */
93
+ const vertexOf = new Map<number, number>();
94
+
95
+ const corner = new Int32Array(8);
96
+ const value = new Float64Array(8);
97
+ for (let k = 0; k + 1 < nz; k += 1) {
98
+ for (let j = 0; j + 1 < ny; j += 1) {
99
+ for (let i = 0; i + 1 < nx; i += 1) {
100
+ let known = true;
101
+ for (let n = 0; n < 8; n += 1) {
102
+ const at = (((k + ((n >> 2) & 1)) * ny + (j + ((n >> 1) & 1))) * nx +
103
+ (i + (n & 1))) as number;
104
+ corner[n] = at;
105
+ value[n] = volume.distance[at] as number;
106
+ if ((volume.weight[at] as number) < minimumWeight) known = false;
107
+ }
108
+ if (!known) continue;
109
+
110
+ for (const tetra of TETRAHEDRA) {
111
+ marchTetrahedron(volume, corner, value, tetra, vertexOf, positions, normals, indices);
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ const colors = new Float32Array(positions.length).fill(1);
118
+ return {
119
+ positions: Float32Array.from(positions),
120
+ normals: Float32Array.from(normals),
121
+ colors,
122
+ /*
123
+ * **One float a vertex, which is what `MeshData` means by emissive** — how much of the
124
+ * surface's own colour it emits — rather than a colour of its own. It was three, and nothing
125
+ * noticed until a captured mesh was written to a file: every consumer of it so far measured
126
+ * geometry, and the container is the first thing that checks an attribute's length against
127
+ * the vertex count. A short attribute buffer is the failure `writeDrft` refuses by name,
128
+ * because a driver handed one may draw zeroes or drop the draw.
129
+ */
130
+ emissive: new Float32Array(positions.length / 3),
131
+ indices: Uint32Array.from(indices),
132
+ };
133
+ }
134
+
135
+ function marchTetrahedron(
136
+ volume: Volume,
137
+ corner: Int32Array,
138
+ value: Float64Array,
139
+ tetra: readonly [number, number, number, number],
140
+ vertexOf: Map<number, number>,
141
+ positions: number[],
142
+ normals: number[],
143
+ indices: number[],
144
+ ): void {
145
+ /* Which of the four corners are inside, as a four-bit pattern. */
146
+ let pattern = 0;
147
+ for (let slot = 0; slot < 4; slot += 1) {
148
+ if ((value[tetra[slot] as number] as number) < 0) pattern |= 1 << slot;
149
+ }
150
+ if (pattern === 0 || pattern === 15) return;
151
+
152
+ /* The edges this pattern crosses: one end inside, the other out. */
153
+ const crossings: number[] = [];
154
+ for (const [a, b] of TETRA_EDGES) {
155
+ const insideA = (pattern & (1 << a)) !== 0;
156
+ const insideB = (pattern & (1 << b)) !== 0;
157
+ if (insideA === insideB) continue;
158
+ crossings.push(
159
+ vertexOn(
160
+ volume,
161
+ corner[tetra[a] as number] as number,
162
+ corner[tetra[b] as number] as number,
163
+ value[tetra[a] as number] as number,
164
+ value[tetra[b] as number] as number,
165
+ vertexOf,
166
+ positions,
167
+ normals,
168
+ ),
169
+ );
170
+ }
171
+ /*
172
+ * Three crossings where one corner is alone on its side of the surface, four where they are two
173
+ * against two — a quadrilateral, which becomes two triangles. There is no other case: this is the
174
+ * whole of the ambiguity marching cubes has and tetrahedra do not.
175
+ */
176
+ if (crossings.length === 3) {
177
+ emit(
178
+ volume,
179
+ positions,
180
+ normals,
181
+ indices,
182
+ crossings[0] as number,
183
+ crossings[1] as number,
184
+ crossings[2] as number,
185
+ );
186
+ return;
187
+ }
188
+ if (crossings.length !== 4) return;
189
+ /*
190
+ * The four are in edge order, which walks the quadrilateral as 0–1–3–2 rather than 0–1–2–3: the
191
+ * edges opposite each other in the list share no corner. Taking them in the listed order would
192
+ * fold the quad into a bowtie, which is a pair of triangles that overlap and leave a hole.
193
+ */
194
+ emit(
195
+ volume,
196
+ positions,
197
+ normals,
198
+ indices,
199
+ crossings[0] as number,
200
+ crossings[1] as number,
201
+ crossings[3] as number,
202
+ );
203
+ emit(
204
+ volume,
205
+ positions,
206
+ normals,
207
+ indices,
208
+ crossings[0] as number,
209
+ crossings[3] as number,
210
+ crossings[2] as number,
211
+ );
212
+ }
213
+
214
+ /** The crossing on the edge between two samples, made once and shared by everything that meets it. */
215
+ function vertexOn(
216
+ volume: Volume,
217
+ a: number,
218
+ b: number,
219
+ valueA: number,
220
+ valueB: number,
221
+ vertexOf: Map<number, number>,
222
+ positions: number[],
223
+ normals: number[],
224
+ ): number {
225
+ /* Keyed by the pair, smaller first, so the two tetrahedra either side agree on the key. */
226
+ const low = a < b ? a : b;
227
+ const high = a < b ? b : a;
228
+ const key = low * volume.distance.length + high;
229
+ const held = vertexOf.get(key);
230
+ if (held !== undefined) return held;
231
+
232
+ const first = low === a ? valueA : valueB;
233
+ const second = low === a ? valueB : valueA;
234
+ /* Where the line between the two samples is zero; a pair that does not straddle takes the middle. */
235
+ const span = second - first;
236
+ const found = span === 0 ? 0.5 : first / -span;
237
+ const t = found < MARGIN ? MARGIN : found > 1 - MARGIN ? 1 - MARGIN : found;
238
+ const index = positions.length / 3;
239
+ const [nx, ny] = volume.dims;
240
+ for (let axis = 0; axis < 3; axis += 1) {
241
+ const from = sampleAxis(low, axis, nx, ny);
242
+ const to = sampleAxis(high, axis, nx, ny);
243
+ positions.push((volume.origin[axis] as number) + (from + (to - from) * t) * volume.spacing);
244
+ }
245
+ const normal = gradientAt(volume, low, high, t);
246
+ normals.push(normal[0] as number, normal[1] as number, normal[2] as number);
247
+ vertexOf.set(key, index);
248
+ return index;
249
+ }
250
+
251
+ function sampleAxis(at: number, axis: number, nx: number, ny: number): number {
252
+ if (axis === 0) return at % nx;
253
+ if (axis === 1) return Math.floor(at / nx) % ny;
254
+ return Math.floor(at / (nx * ny));
255
+ }
256
+
257
+ /**
258
+ * The volume's gradient partway along an edge, normalised: which way is out.
259
+ *
260
+ * Taken at each end and blended, rather than at the point, because the gradient is only defined at
261
+ * samples. A gradient of nothing — a flat patch of volume — answers the edge's own direction, which
262
+ * is the one thing that is certainly not tangent to the surface there.
263
+ */
264
+ function gradientAt(volume: Volume, low: number, high: number, t: number): readonly number[] {
265
+ const first = gradientAtSample(volume, low);
266
+ const second = gradientAtSample(volume, high);
267
+ const out = [0, 0, 0];
268
+ for (let axis = 0; axis < 3; axis += 1) {
269
+ out[axis] = (first[axis] as number) * (1 - t) + (second[axis] as number) * t;
270
+ }
271
+ const length = Math.sqrt(
272
+ (out[0] as number) * (out[0] as number) +
273
+ (out[1] as number) * (out[1] as number) +
274
+ (out[2] as number) * (out[2] as number),
275
+ );
276
+ if (length > 1e-12) {
277
+ for (let axis = 0; axis < 3; axis += 1) out[axis] = (out[axis] as number) / length;
278
+ return out;
279
+ }
280
+ const [nx, ny] = volume.dims;
281
+ const fallback = [0, 0, 0];
282
+ for (let axis = 0; axis < 3; axis += 1) {
283
+ fallback[axis] = sampleAxis(high, axis, nx, ny) - sampleAxis(low, axis, nx, ny);
284
+ }
285
+ const span = Math.sqrt(
286
+ (fallback[0] as number) * (fallback[0] as number) +
287
+ (fallback[1] as number) * (fallback[1] as number) +
288
+ (fallback[2] as number) * (fallback[2] as number),
289
+ );
290
+ return span > 0 ? fallback.map((value) => (value as number) / span) : [0, 0, 1];
291
+ }
292
+
293
+ /** A central difference where both neighbours exist, and a one-sided one at the volume's edge. */
294
+ function gradientAtSample(volume: Volume, at: number): readonly number[] {
295
+ const [nx, ny, nz] = volume.dims;
296
+ const x = at % nx;
297
+ const y = Math.floor(at / nx) % ny;
298
+ const z = Math.floor(at / (nx * ny));
299
+ const here = volume.distance[at] as number;
300
+ const out = [0, 0, 0];
301
+ const steps = [1, nx, nx * ny];
302
+ const limits = [
303
+ [x, nx],
304
+ [y, ny],
305
+ [z, nz],
306
+ ];
307
+ for (let axis = 0; axis < 3; axis += 1) {
308
+ const [position, size] = limits[axis] as [number, number];
309
+ const step = steps[axis] as number;
310
+ const low = position > 0 ? (volume.distance[at - step] as number) : here;
311
+ const high = position + 1 < size ? (volume.distance[at + step] as number) : here;
312
+ const span = (position > 0 ? 1 : 0) + (position + 1 < size ? 1 : 0);
313
+ out[axis] = span === 0 ? 0 : (high - low) / (span * volume.spacing);
314
+ }
315
+ return out;
316
+ }
317
+
318
+ /**
319
+ * One triangle, wound so that its own normal agrees with the volume's gradient.
320
+ *
321
+ * **This is the check the whole module exists to pass.** A mesh whose faces point inwards collides
322
+ * inside-out — a character walks through the floor and out of the world — and it looks entirely
323
+ * correct until something stands on it.
324
+ */
325
+ function emit(
326
+ volume: Volume,
327
+ positions: number[],
328
+ normals: number[],
329
+ indices: number[],
330
+ a: number,
331
+ b: number,
332
+ c: number,
333
+ ): void {
334
+ if (a === b || b === c || a === c) return;
335
+ const ax = positions[a * 3] as number;
336
+ const ay = positions[a * 3 + 1] as number;
337
+ const az = positions[a * 3 + 2] as number;
338
+ const ux = (positions[b * 3] as number) - ax;
339
+ const uy = (positions[b * 3 + 1] as number) - ay;
340
+ const uz = (positions[b * 3 + 2] as number) - az;
341
+ const vx = (positions[c * 3] as number) - ax;
342
+ const vy = (positions[c * 3 + 1] as number) - ay;
343
+ const vz = (positions[c * 3 + 2] as number) - az;
344
+ const fx = uy * vz - uz * vy;
345
+ const fy = uz * vx - ux * vz;
346
+ const fz = ux * vy - uy * vx;
347
+ /* The three vertices' own normals, which came from the gradient, say which way out is. */
348
+ let outward = 0;
349
+ for (const vertex of [a, b, c]) {
350
+ outward +=
351
+ fx * (normals[vertex * 3] as number) +
352
+ fy * (normals[vertex * 3 + 1] as number) +
353
+ fz * (normals[vertex * 3 + 2] as number);
354
+ }
355
+ if (outward >= 0) indices.push(a, b, c);
356
+ else indices.push(a, c, b);
357
+ void volume;
358
+ }
package/src/masks.ts ADDED
@@ -0,0 +1,201 @@
1
+ /**
2
+ * A segmentation model's masks brought onto the mesh, and a detector's names brought onto the masks.
3
+ *
4
+ * **Where a region comes from when weights are loaded.** A prompt lattice asks the model what is in
5
+ * the frame, the masks that come back are voted onto the mesh across every view that saw a
6
+ * triangle, and a detector's boxes put names to the mask indices. Nothing here runs a model: masks
7
+ * and detections arrive as data, so this module is the same code whether they were produced on a
8
+ * device, on a native host, or by hand in a test.
9
+ *
10
+ * **Voting rather than taking the best view**, because a mask boundary is where a model is least
11
+ * sure and every view puts it somewhere slightly different — and **a triangle no view claimed is
12
+ * left unassigned**, which is what keeps an unlabelled region unlabelled rather than absorbed.
13
+ */
14
+ import type { MeshData } from '@driftengine/drft';
15
+
16
+ const DEFAULT_OVERLAP = 0.25;
17
+
18
+ import type { Owlv2Detection } from './models/owlv2Detect.ts';
19
+ import { planeOf, regionOf, type Region } from './segment.ts';
20
+
21
+ /** Where a model is prompted, and what came back for each prompt. */
22
+ export interface MaskView {
23
+ /** One mask index per pixel, or −1 where the model claimed nothing. */
24
+ readonly mask: Int32Array;
25
+ readonly width: number;
26
+ readonly height: number;
27
+ /** 3 × 4 row-major world-to-camera. */
28
+ readonly worldToCamera: Float64Array;
29
+ /** `fx`, `fy`, `cx`, `cy`. */
30
+ readonly intrinsics: readonly [number, number, number, number];
31
+ }
32
+
33
+ /**
34
+ * A lattice of prompt points over an image, seeded.
35
+ *
36
+ * **A lattice rather than a scatter**, because a scatter leaves gaps a small object falls into and
37
+ * a lattice does not; the seeded jitter is there so that a repeated object does not land on the
38
+ * same part of every instance, which is how a grid misses the same feature twice. Two per pixel
39
+ * pair — `x` then `y` — in reading order, which is the order a model's masks come back in.
40
+ */
41
+ export function promptGrid(
42
+ width: number,
43
+ height: number,
44
+ across: number,
45
+ down: number,
46
+ random: () => number,
47
+ ): Float32Array {
48
+ const out = new Float32Array(across * down * 2);
49
+ for (let j = 0; j < down; j += 1) {
50
+ for (let i = 0; i < across; i += 1) {
51
+ const at = (j * across + i) * 2;
52
+ const cellX = (width * (i + 0.5)) / across;
53
+ const cellY = (height * (j + 0.5)) / down;
54
+ /*
55
+ * A sixth of a cell either way. A centre sits half a cell from each of its edges, so a point
56
+ * cannot leave the cell it belongs to however the seed falls — which is what keeps this a
57
+ * lattice, and is why nothing here clamps to the frame.
58
+ */
59
+ const jitterX = ((random() - 0.5) * width) / across / 3;
60
+ const jitterY = ((random() - 0.5) * height) / down / 3;
61
+ out[at] = cellX + jitterX;
62
+ out[at + 1] = cellY + jitterY;
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /**
69
+ * Masks from several views lifted onto the mesh, by what most of the views that saw a triangle say.
70
+ *
71
+ * **Voting rather than taking the best view**, because a mask boundary is where a model is least
72
+ * sure and every view puts it somewhere slightly different. A triangle no view claimed is left
73
+ * unassigned rather than given to its neighbour, which is what keeps an unlabelled region
74
+ * unlabelled.
75
+ */
76
+ export function liftMasks(
77
+ views: readonly MaskView[],
78
+ mesh: MeshData,
79
+ labels: readonly string[],
80
+ ): Region[] {
81
+ const triangles = mesh.indices.length / 3;
82
+ const planes = new Float64Array(triangles * 4);
83
+ const areas = new Float64Array(triangles);
84
+ for (let face = 0; face < triangles; face += 1) planeOf(mesh, face, planes, areas);
85
+
86
+ const votes: Map<number, number>[] = [];
87
+ for (let face = 0; face < triangles; face += 1) votes.push(new Map());
88
+ const middle = new Float64Array(3);
89
+ for (const view of views) {
90
+ const [fx, fy, cx, cy] = view.intrinsics;
91
+ const m = view.worldToCamera;
92
+ for (let face = 0; face < triangles; face += 1) {
93
+ centroid(mesh, face, middle);
94
+ let x = m[3] as number;
95
+ let y = m[7] as number;
96
+ let z = m[11] as number;
97
+ for (let k = 0; k < 3; k += 1) {
98
+ x += (m[k] as number) * (middle[k] as number);
99
+ y += (m[4 + k] as number) * (middle[k] as number);
100
+ z += (m[8 + k] as number) * (middle[k] as number);
101
+ }
102
+ if (!(z > 0)) continue;
103
+ const px = Math.floor((fx * x) / z + cx);
104
+ const py = Math.floor((fy * y) / z + cy);
105
+ if (px < 0 || px >= view.width || py < 0 || py >= view.height) continue;
106
+ const mask = view.mask[py * view.width + px] as number;
107
+ if (mask < 0) continue;
108
+ const held = votes[face] as Map<number, number>;
109
+ held.set(mask, (held.get(mask) ?? 0) + 1);
110
+ }
111
+ }
112
+
113
+ const members = new Map<number, number[]>();
114
+ for (let face = 0; face < triangles; face += 1) {
115
+ const held = votes[face] as Map<number, number>;
116
+ let best = -1;
117
+ let most = 0;
118
+ /* Ties go to the lower mask index, so two views disagreeing give the same answer every run. */
119
+ for (const [mask, count] of [...held.entries()].sort((a, b) => a[0] - b[0])) {
120
+ if (count > most) {
121
+ most = count;
122
+ best = mask;
123
+ }
124
+ }
125
+ if (best < 0) continue;
126
+ const list = members.get(best);
127
+ if (list === undefined) members.set(best, [face]);
128
+ else list.push(face);
129
+ }
130
+
131
+ const out: Region[] = [];
132
+ for (const mask of [...members.keys()].sort((a, b) => a - b)) {
133
+ const region = regionOf(mesh, members.get(mask) as number[], planes, areas);
134
+ out.push({ ...region, label: labels[mask] ?? null });
135
+ }
136
+ return out;
137
+ }
138
+
139
+ /**
140
+ * A name for each mask index, from a detector's boxes.
141
+ *
142
+ * **The join between the two models a capture runs**: one says *where the things are* and the other
143
+ * says *what they are called*, and neither can do the other's half. A mask takes the name of the
144
+ * detection it agrees with most, measured as intersection over union between the mask's own pixels
145
+ * and the box — the mask's pixels rather than the box around them, because the box around an
146
+ * L-shaped mask is mostly not the mask.
147
+ *
148
+ * **A mask that agrees with nothing gets no name, and that is a result.** `null` here reaches
149
+ * `proposeEntities` as scenery, which is the safe answer; a name taken from the nearest box
150
+ * whatever the overlap is how a chair becomes a table. `overlap` is where a caller draws that line.
151
+ */
152
+ export function labelMasks(
153
+ view: MaskView,
154
+ detections: readonly Owlv2Detection[],
155
+ queries: readonly string[],
156
+ overlap = DEFAULT_OVERLAP,
157
+ ): (string | null)[] {
158
+ let masks = 0;
159
+ for (const mask of view.mask) if (mask + 1 > masks) masks = mask + 1;
160
+ const out = new Array<string | null>(masks).fill(null);
161
+ const counts = new Int32Array(masks);
162
+ for (const mask of view.mask) if (mask >= 0) counts[mask] = (counts[mask] as number) + 1;
163
+
164
+ /* The best agreement each mask has found so far, so a tie keeps the earlier detection. */
165
+ const best = new Float64Array(masks);
166
+ const inside = new Int32Array(masks);
167
+ for (let at = 0; at < detections.length; at += 1) {
168
+ const box = (detections[at] as Owlv2Detection).box;
169
+ inside.fill(0);
170
+ for (let y = 0; y < view.height; y += 1) {
171
+ for (let x = 0; x < view.width; x += 1) {
172
+ const mask = view.mask[y * view.width + x] as number;
173
+ if (mask < 0) continue;
174
+ if (x + 0.5 < (box[0] as number) || x + 0.5 >= (box[2] as number)) continue;
175
+ if (y + 0.5 < (box[1] as number) || y + 0.5 >= (box[3] as number)) continue;
176
+ inside[mask] = (inside[mask] as number) + 1;
177
+ }
178
+ }
179
+ const area =
180
+ ((box[2] as number) - (box[0] as number)) * ((box[3] as number) - (box[1] as number));
181
+ for (let mask = 0; mask < masks; mask += 1) {
182
+ const met = inside[mask] as number;
183
+ const union = (counts[mask] as number) + area - met;
184
+ const score = union > 0 ? met / union : 0;
185
+ if (score < overlap || score <= (best[mask] as number)) continue;
186
+ best[mask] = score;
187
+ out[mask] = queries[(detections[at] as Owlv2Detection).label] ?? null;
188
+ }
189
+ }
190
+ return out;
191
+ }
192
+
193
+ function centroid(mesh: MeshData, face: number, out: Float64Array): void {
194
+ for (let k = 0; k < 3; k += 1) out[k] = 0;
195
+ for (let slot = 0; slot < 3; slot += 1) {
196
+ const vertex = mesh.indices[face * 3 + slot] as number;
197
+ for (let k = 0; k < 3; k += 1) {
198
+ out[k] = (out[k] as number) + (mesh.positions[vertex * 3 + k] as number) / 3;
199
+ }
200
+ }
201
+ }