@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
package/src/prepare.ts ADDED
@@ -0,0 +1,306 @@
1
+ /**
2
+ * A frame as a model takes it: the preparation each upstream does to its own inputs, done here so a
3
+ * host hands the engine pixels and nothing else.
4
+ *
5
+ * **Depth Anything 3**, as its `InputProcessor` prepares a frame: the longest side brought to 504
6
+ * by an area resize, each side then rounded to the nearest whole patch of 14 by another, the bytes
7
+ * divided by 255 and normalised by ImageNet's mean and deviation, and laid out channel by row. A
8
+ * clip's aspect ratio decides the grid, which is why the graph is rebuilt per size.
9
+ *
10
+ * **MobileSAM**, as its predictor prepares one: the longest side to 1,024 by Pillow's bilinear,
11
+ * normalised by the upstream's pixel mean and deviation in 0–255, and padded with zeros after its
12
+ * last row and column to the square the encoder takes. The prepared frame's own size is answered
13
+ * too, since a prompt's coordinates are in it and a mask comes back cropped to it.
14
+ *
15
+ * **SAM 2.1**, as its processor prepares one: the frame to a 1,024 square whatever its shape —
16
+ * each axis scaled by its own factor, which is why a prompt's coordinates scale by their own too —
17
+ * through torchvision's antialiased bilinear, divided by 255 and normalised by ImageNet's.
18
+ *
19
+ * **OWLv2**, as its processor prepares one, which is the odd one: the bytes divided by 255 first,
20
+ * the frame padded with zeros after its last row and column to a square — so a box's coordinates
21
+ * scale by the longer side — blurred by a gaussian wide enough for the reduction, resized to 960
22
+ * by a plain bilinear with no antialiasing of its own, and normalised by CLIP's statistics. The
23
+ * blur is what the antialiasing is here, which is why the resize after it has none.
24
+ *
25
+ * **A frame smaller than the model's size is refused rather than guessed at.** The upstream scales
26
+ * such a frame up with a cubic resize, which this does not have; a wrong preparation is a wrong
27
+ * depth that looks like a right one, and a refusal names what is missing. What would lift it is
28
+ * OpenCV's `INTER_CUBIC`, written and held to the same parity as the area resize.
29
+ */
30
+ import { exactExp } from '@driftengine/core';
31
+ import { resize } from '@driftengine/texture';
32
+
33
+ import { areaResize, triangleResize, TORCHVISION_PRECISION } from './resample.ts';
34
+
35
+ export const DEPTH_PIXEL_MEAN = [0.485, 0.456, 0.406] as const;
36
+ export const DEPTH_PIXEL_STD = [0.229, 0.224, 0.225] as const;
37
+
38
+ export interface PreparedFrame {
39
+ /** `[3, height, width]`, normalised. */
40
+ readonly pixels: Float32Array;
41
+ readonly width: number;
42
+ readonly height: number;
43
+ }
44
+
45
+ /** Python's `round`: halves to even, which is what the upstream's sizes are rounded by. */
46
+ function halfToEven(value: number): number {
47
+ const down = Math.floor(value);
48
+ const rest = value - down;
49
+ if (rest > 0.5) return down + 1;
50
+ if (rest < 0.5) return down;
51
+ return down % 2 === 0 ? down : down + 1;
52
+ }
53
+
54
+ /** The nearest multiple of `patch`, the larger one on a tie, as the upstream rounds it. */
55
+ function nearestMultiple(value: number, patch: number): number {
56
+ const down = Math.floor(value / patch) * patch;
57
+ const up = down + patch;
58
+ return up - value <= value - down ? up : down;
59
+ }
60
+
61
+ /**
62
+ * `rgba`, `width × height` pixels of four bytes, as Depth Anything 3 takes it at `size`, in whole
63
+ * patches of `patch`.
64
+ */
65
+ export function prepareDepthFrame(
66
+ rgba: Uint8Array,
67
+ width: number,
68
+ height: number,
69
+ size = 504,
70
+ patch = 14,
71
+ ): PreparedFrame {
72
+ const longest = Math.max(width, height);
73
+ if (longest < size) {
74
+ throw new RangeError(
75
+ `a frame of ${width}×${height} is smaller than the model's ${size} and would have to be ` +
76
+ 'scaled up, which needs the cubic resize this does not have',
77
+ );
78
+ }
79
+ const scale = size / longest;
80
+ let held = rgba;
81
+ let at = { width, height };
82
+ if (longest !== size) {
83
+ at = {
84
+ width: Math.max(1, halfToEven(width * scale)),
85
+ height: Math.max(1, halfToEven(height * scale)),
86
+ };
87
+ held = new Uint8Array(at.width * at.height * 4);
88
+ areaResize(rgba, width, height, 4, held, at.width, at.height);
89
+ }
90
+ const whole = {
91
+ width: Math.max(1, nearestMultiple(at.width, patch)),
92
+ height: Math.max(1, nearestMultiple(at.height, patch)),
93
+ };
94
+ if (whole.width > at.width || whole.height > at.height) {
95
+ throw new RangeError(
96
+ `a frame of ${at.width}×${at.height} rounds up to ${whole.width}×${whole.height} whole ` +
97
+ 'patches, which needs the cubic resize this does not have',
98
+ );
99
+ }
100
+ if (whole.width !== at.width || whole.height !== at.height) {
101
+ const rounded = new Uint8Array(whole.width * whole.height * 4);
102
+ areaResize(held, at.width, at.height, 4, rounded, whole.width, whole.height);
103
+ held = rounded;
104
+ at = whole;
105
+ }
106
+ const pixels = new Float32Array(3 * at.width * at.height);
107
+ const cells = at.width * at.height;
108
+ for (let channel = 0; channel < 3; channel += 1) {
109
+ const mean = DEPTH_PIXEL_MEAN[channel] as number;
110
+ const deviation = DEPTH_PIXEL_STD[channel] as number;
111
+ for (let cell = 0; cell < cells; cell += 1) {
112
+ const value = (held[cell * 4 + channel] as number) / 255;
113
+ pixels[channel * cells + cell] = Math.fround((Math.fround(value) - mean) / deviation);
114
+ }
115
+ }
116
+ return { pixels, width: at.width, height: at.height };
117
+ }
118
+
119
+ /** The size a frame is resized to, its longest side at `size`, rounded up at a half. */
120
+ export function longestSideSize(
121
+ width: number,
122
+ height: number,
123
+ size: number,
124
+ ): { readonly width: number; readonly height: number } {
125
+ const scale = size / Math.max(width, height);
126
+ return {
127
+ width: Math.trunc(width * scale + 0.5),
128
+ height: Math.trunc(height * scale + 0.5),
129
+ };
130
+ }
131
+
132
+ /**
133
+ * `rgba` as MobileSAM's predictor prepares it: `[3, square, square]`, the frame itself in the top
134
+ * left at the size this answers, and zeros after it.
135
+ */
136
+ export function prepareSamFrame(
137
+ rgba: Uint8Array,
138
+ width: number,
139
+ height: number,
140
+ mean: readonly number[],
141
+ deviation: readonly number[],
142
+ square = 1024,
143
+ ): PreparedFrame {
144
+ const at = longestSideSize(width, height, square);
145
+ let held = rgba;
146
+ if (at.width !== width || at.height !== height) {
147
+ held = new Uint8Array(at.width * at.height * 4);
148
+ triangleResize(rgba, width, height, 4, held, at.width, at.height);
149
+ }
150
+ const pixels = new Float32Array(3 * square * square);
151
+ for (let channel = 0; channel < 3; channel += 1) {
152
+ const centre = mean[channel] as number;
153
+ const spread = deviation[channel] as number;
154
+ for (let y = 0; y < at.height; y += 1) {
155
+ for (let x = 0; x < at.width; x += 1) {
156
+ const value = held[(y * at.width + x) * 4 + channel] as number;
157
+ pixels[channel * square * square + y * square + x] = Math.fround((value - centre) / spread);
158
+ }
159
+ }
160
+ }
161
+ return { pixels, width: at.width, height: at.height };
162
+ }
163
+
164
+ /**
165
+ * `rgba` as SAM 2's processor prepares it: `[3, square, square]`, each axis scaled on its own, and
166
+ * normalised by `mean` and `deviation` in 0–1.
167
+ */
168
+ export function prepareSam2Frame(
169
+ rgba: Uint8Array,
170
+ width: number,
171
+ height: number,
172
+ mean: readonly number[] = DEPTH_PIXEL_MEAN,
173
+ deviation: readonly number[] = DEPTH_PIXEL_STD,
174
+ square = 1024,
175
+ ): PreparedFrame {
176
+ let held = rgba;
177
+ if (width !== square || height !== square) {
178
+ held = new Uint8Array(square * square * 4);
179
+ triangleResize(rgba, width, height, 4, held, square, square, TORCHVISION_PRECISION);
180
+ }
181
+ const pixels = new Float32Array(3 * square * square);
182
+ const cells = square * square;
183
+ for (let channel = 0; channel < 3; channel += 1) {
184
+ const centre = mean[channel] as number;
185
+ const spread = deviation[channel] as number;
186
+ for (let cell = 0; cell < cells; cell += 1) {
187
+ const value = Math.fround((held[cell * 4 + channel] as number) / 255);
188
+ pixels[channel * cells + cell] = Math.fround((value - centre) / spread);
189
+ }
190
+ }
191
+ return { pixels, width: square, height: square };
192
+ }
193
+
194
+ /* torchvision's 1-D gaussian: samples about the centre, normalised to sum to one. */
195
+ function gaussianKernel(size: number, sigma: number): Float32Array {
196
+ const half = (size - 1) * 0.5;
197
+ const kernel = new Float32Array(size);
198
+ let total = 0;
199
+ for (let i = 0; i < size; i += 1) {
200
+ /* `linspace` from −half to half, which is how torchvision places the samples. */
201
+ const at = size === 1 ? 0 : -half + (2 * half * i) / (size - 1);
202
+ const value = Math.fround(exactExp(-0.5 * (at / sigma) * (at / sigma)));
203
+ kernel[i] = value;
204
+ total += value;
205
+ }
206
+ for (let i = 0; i < size; i += 1) kernel[i] = Math.fround((kernel[i] as number) / total);
207
+ return kernel;
208
+ }
209
+
210
+ /* A separable blur with the edges reflected, as torchvision's `gaussian_blur` pads them. */
211
+ function blur(
212
+ values: Float32Array,
213
+ channels: number,
214
+ height: number,
215
+ width: number,
216
+ down: Float32Array,
217
+ across: Float32Array,
218
+ ): Float32Array {
219
+ const reflect = (at: number, length: number): number => {
220
+ let index = at;
221
+ while (index < 0 || index >= length) index = index < 0 ? -index : 2 * length - index - 2;
222
+ return index;
223
+ };
224
+ const middle = new Float32Array(values.length);
225
+ const half = (across.length - 1) / 2;
226
+ for (let c = 0; c < channels; c += 1) {
227
+ for (let y = 0; y < height; y += 1) {
228
+ for (let x = 0; x < width; x += 1) {
229
+ let sum = 0;
230
+ for (let k = 0; k < across.length; k += 1) {
231
+ sum = Math.fround(
232
+ sum +
233
+ Math.fround(
234
+ (values[(c * height + y) * width + reflect(x + k - half, width)] as number) *
235
+ (across[k] as number),
236
+ ),
237
+ );
238
+ }
239
+ middle[(c * height + y) * width + x] = sum;
240
+ }
241
+ }
242
+ }
243
+ const out = new Float32Array(values.length);
244
+ const tall = (down.length - 1) / 2;
245
+ for (let c = 0; c < channels; c += 1) {
246
+ for (let y = 0; y < height; y += 1) {
247
+ for (let x = 0; x < width; x += 1) {
248
+ let sum = 0;
249
+ for (let k = 0; k < down.length; k += 1) {
250
+ sum = Math.fround(
251
+ sum +
252
+ Math.fround(
253
+ (middle[(c * height + reflect(y + k - tall, height)) * width + x] as number) *
254
+ (down[k] as number),
255
+ ),
256
+ );
257
+ }
258
+ out[(c * height + y) * width + x] = sum;
259
+ }
260
+ }
261
+ }
262
+ return out;
263
+ }
264
+
265
+ /** `rgba` as OWLv2's processor prepares it: `[3, square, square]` at the model's size. */
266
+ export function prepareOwlv2Frame(
267
+ rgba: Uint8Array,
268
+ width: number,
269
+ height: number,
270
+ mean: readonly number[],
271
+ deviation: readonly number[],
272
+ square = 960,
273
+ ): PreparedFrame {
274
+ const side = Math.max(width, height);
275
+ const padded = new Float32Array(3 * side * side);
276
+ for (let channel = 0; channel < 3; channel += 1) {
277
+ for (let y = 0; y < height; y += 1) {
278
+ for (let x = 0; x < width; x += 1) {
279
+ padded[(channel * side + y) * side + x] = Math.fround(
280
+ (rgba[(y * width + x) * 4 + channel] as number) / 255,
281
+ );
282
+ }
283
+ }
284
+ }
285
+ const factor = side / square;
286
+ const sigma = Math.max(0, (factor - 1) / 2);
287
+ let blurred: Float32Array = padded;
288
+ if (sigma > 0) {
289
+ const size = 2 * Math.ceil(3 * sigma) + 1;
290
+ const kernel = gaussianKernel(size, sigma);
291
+ blurred = blur(padded, 3, side, side, kernel, kernel);
292
+ }
293
+ const resized = new Float32Array(3 * square * square);
294
+ resize(resized, blurred, 3, side, side, square, square, 'bilinear', false);
295
+ const cells = square * square;
296
+ for (let channel = 0; channel < 3; channel += 1) {
297
+ const centre = mean[channel] as number;
298
+ const spread = deviation[channel] as number;
299
+ for (let cell = 0; cell < cells; cell += 1) {
300
+ resized[channel * cells + cell] = Math.fround(
301
+ ((resized[channel * cells + cell] as number) - centre) / spread,
302
+ );
303
+ }
304
+ }
305
+ return { pixels: resized, width: square, height: square };
306
+ }
package/src/propose.ts ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Each region as something a scene could hold — a *proposal*, and the word is load-bearing.
3
+ *
4
+ * **This is stage four and it is labelled as such.** A capture that guesses what an object is and
5
+ * acts on the guess is a capture that puts a door where a cupboard was, and the cost is paid by
6
+ * somebody who cannot see why. So a region arrives with what is known about it and nothing more,
7
+ * and **a region nobody labelled becomes static scenery** — the safe default in the strict sense:
8
+ * drawn, solid, and doing nothing. Being wrong about that costs nothing at all.
9
+ *
10
+ * The component names are the engine's own, as data. Nothing here imports an entity model, because
11
+ * a proposal a consumer has not accepted yet should not have built anything.
12
+ */
13
+ import { exactCos } from '@driftengine/core';
14
+
15
+ import type { Region } from './segment.ts';
16
+
17
+ export interface ProposeOptions {
18
+ /** The steepest surface a character could stand on, in degrees. */
19
+ readonly slopeDegrees?: number;
20
+ }
21
+
22
+ const DEFAULT_SLOPE = 45;
23
+
24
+ /** What a scene could make of a region. */
25
+ export interface EntityProposal {
26
+ /** Its index in the regions it came from. */
27
+ readonly region: number;
28
+ readonly bounds: Float64Array;
29
+ readonly label: string | null;
30
+ /**
31
+ * Components a scene might give it, by the engine's own names.
32
+ *
33
+ * A *suggestion*, and the word is load-bearing: nothing here knows what the thing is, only what
34
+ * shape it is and whether anybody named it.
35
+ */
36
+ readonly components: readonly string[];
37
+ /** Whether a character could stand on it. */
38
+ readonly walkable: boolean;
39
+ }
40
+
41
+ /** Drawn and solid and nothing else, which is what an unlabelled region gets. */
42
+ export const SCENERY_COMPONENTS: readonly string[] = ['transform', 'mesh', 'collider'];
43
+ /** The same, and a navigation surface on top of it. */
44
+ export const WALKABLE_COMPONENTS: readonly string[] = [
45
+ 'transform',
46
+ 'mesh',
47
+ 'collider',
48
+ 'navigation',
49
+ ];
50
+ /** Something small enough and named: a scene may want to move it. */
51
+ export const MOVABLE_COMPONENTS: readonly string[] = ['transform', 'mesh', 'collider', 'body'];
52
+
53
+ /**
54
+ * Each region as a proposal.
55
+ *
56
+ * **An unlabelled region is scenery**, whatever its shape: drawn, solid, and doing nothing. A named
57
+ * region small enough to pick up is offered as movable, and the floor — the largest surface a
58
+ * character could stand on — is offered with a navigation surface. Nothing here is decided; a
59
+ * consumer reads `components` as a starting point and overrides whatever it likes.
60
+ */
61
+ export function proposeEntities(
62
+ regions: readonly Region[],
63
+ options: ProposeOptions = {},
64
+ ): EntityProposal[] {
65
+ const slope = exactCos(((options.slopeDegrees ?? DEFAULT_SLOPE) * Math.PI) / 180);
66
+ let floor = -1;
67
+ let widest = 0;
68
+ regions.forEach((region, at) => {
69
+ if ((region.normal[1] as number) < slope) return;
70
+ if (region.area <= widest) return;
71
+ widest = region.area;
72
+ floor = at;
73
+ });
74
+
75
+ return regions.map((region, at) => {
76
+ const walkable = at === floor;
77
+ const span = Math.max(
78
+ (region.bounds[3] as number) - (region.bounds[0] as number),
79
+ (region.bounds[4] as number) - (region.bounds[1] as number),
80
+ (region.bounds[5] as number) - (region.bounds[2] as number),
81
+ );
82
+ /* Named and small enough that a person could shift it: the only case that is not scenery. */
83
+ const movable = region.label !== null && span < 1.2;
84
+ return {
85
+ region: at,
86
+ bounds: region.bounds,
87
+ label: region.label,
88
+ components: walkable
89
+ ? WALKABLE_COMPONENTS
90
+ : movable
91
+ ? MOVABLE_COMPONENTS
92
+ : SCENERY_COMPONENTS,
93
+ walkable,
94
+ };
95
+ });
96
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * The resampling a model's own preparation does, as the library its upstream uses does it.
3
+ *
4
+ * **Area resizing is OpenCV's `INTER_AREA`**, which Depth Anything 3's `InputProcessor` resizes a
5
+ * frame down with: an output pixel is the average of the source rectangle it covers, with the
6
+ * pixels at either end weighted by the fraction of them inside it. It is the right filter for
7
+ * shrinking a photograph — every source pixel reaches the output exactly once, so nothing aliases
8
+ * and nothing is dropped — and it is what the model was trained on, which is the reason that
9
+ * matters here.
10
+ *
11
+ * **The antialiased bilinear is the other one**, which MobileSAM's predictor and SAM 2's processor
12
+ * each resize a frame with: a triangle filter whose support grows with the reduction, so shrinking
13
+ * averages rather than samples. One axis at a time, through an eight-bit intermediate, with
14
+ * coefficients normalised to sum to one and held in fixed point — **and how many fraction bits they
15
+ * keep is the whole difference between the two libraries**: Pillow keeps 22 and torchvision's
16
+ * eight-bit path keeps 15, and at each the engine answers what that library answers, measured on
17
+ * the pinned frames at 0 of 3,145,728 values apart. A coefficient rounded differently is a pixel
18
+ * rounded differently, which is why this is a number rather than an approximation.
19
+ *
20
+ * **The area resize is summed the way OpenCV sums it**: one weight per source pixel of the rectangle, the row's
21
+ * times the column's, accumulated in single precision, and rounded by halves to even as `cvRound`
22
+ * does. Measured against OpenCV on the two pinned frames, that leaves **24 values of 846,720 a
23
+ * single level of 255 apart** — a sum landing either side of a half — where summing in double and
24
+ * rounding halves away from zero leaves 46. What would close the rest is OpenCV's own fixed-point
25
+ * table, which is worth writing when a measurement says a level of 255 matters.
26
+ */
27
+
28
+ const f = Math.fround;
29
+
30
+ /** `cvRound`: halves to even, which is what OpenCV's saturating cast does. */
31
+ function toByte(value: number): number {
32
+ const down = Math.floor(value);
33
+ const rest = value - down;
34
+ const rounded = rest > 0.5 ? down + 1 : rest < 0.5 ? down : down % 2 === 0 ? down : down + 1;
35
+ return Math.min(255, Math.max(0, rounded));
36
+ }
37
+
38
+ /** One output axis's source span: where it starts, and the weight of each source pixel in it. */
39
+ interface Span {
40
+ readonly first: number;
41
+ readonly weights: Float64Array;
42
+ }
43
+
44
+ function spans(from: number, to: number): Span[] {
45
+ const scale = from / to;
46
+ const out: Span[] = [];
47
+ for (let at = 0; at < to; at += 1) {
48
+ const start = at * scale;
49
+ const end = start + scale;
50
+ const first = Math.ceil(start);
51
+ const last = Math.floor(end);
52
+ /* The cell is the part of it inside the image, as OpenCV's `resizeArea` measures it. */
53
+ const cell = Math.min(scale, from - start);
54
+ const weights: number[] = [];
55
+ if (first > start) weights.push((first - start) / cell);
56
+ for (let source = first; source < last; source += 1) weights.push(1 / cell);
57
+ if (end > last && last < from) weights.push((end - last) / cell);
58
+ out.push({
59
+ first: Math.min(first > start ? first - 1 : first, from - 1),
60
+ weights: Float64Array.from(weights),
61
+ });
62
+ }
63
+ return out;
64
+ }
65
+
66
+ /**
67
+ * `src`, `width × height` pixels of `channels` bytes, into `out` at `outWidth × outHeight`. Only
68
+ * shrinking is defined, which is what a frame's preparation asks for.
69
+ */
70
+ export function areaResize(
71
+ src: Uint8Array,
72
+ width: number,
73
+ height: number,
74
+ channels: number,
75
+ out: Uint8Array,
76
+ outWidth: number,
77
+ outHeight: number,
78
+ ): void {
79
+ const across = spans(width, outWidth);
80
+ const down = spans(height, outHeight);
81
+ for (let y = 0; y < outHeight; y += 1) {
82
+ const rows = down[y] as Span;
83
+ for (let x = 0; x < outWidth; x += 1) {
84
+ const columns = across[x] as Span;
85
+ for (let c = 0; c < channels; c += 1) {
86
+ let sum = 0;
87
+ for (let dy = 0; dy < rows.weights.length; dy += 1) {
88
+ const row = (rows.first + dy) * width;
89
+ for (let dx = 0; dx < columns.weights.length; dx += 1) {
90
+ const weight = f(f(rows.weights[dy] as number) * f(columns.weights[dx] as number));
91
+ sum = f(sum + f((src[(row + columns.first + dx) * channels + c] as number) * weight));
92
+ }
93
+ }
94
+ out[(y * outWidth + x) * channels + c] = toByte(sum);
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ /** Pillow's fraction bits, and torchvision's eight-bit path's. */
101
+ export const PILLOW_PRECISION = 22;
102
+ export const TORCHVISION_PRECISION = 15;
103
+
104
+ /** One output pixel's source pixels and their fixed-point weights. */
105
+ function triangleCoefficients(
106
+ from: number,
107
+ to: number,
108
+ precision: number,
109
+ ): { first: number; weights: Int32Array }[] {
110
+ const scale = from / to;
111
+ const filterScale = Math.max(1, scale);
112
+ const support = filterScale;
113
+ const out: { first: number; weights: Int32Array }[] = [];
114
+ for (let at = 0; at < to; at += 1) {
115
+ const centre = (at + 0.5) * scale;
116
+ const first = Math.max(0, Math.trunc(centre - support + 0.5));
117
+ const last = Math.min(from, Math.trunc(centre + support + 0.5));
118
+ const weights: number[] = [];
119
+ let total = 0;
120
+ for (let source = first; source < last; source += 1) {
121
+ /* The triangle, over a support the reduction widens. */
122
+ const distance = Math.abs((source - centre + 0.5) / filterScale);
123
+ const weight = distance < 1 ? 1 - distance : 0;
124
+ weights.push(weight);
125
+ total += weight;
126
+ }
127
+ const fixed = new Int32Array(weights.length);
128
+ for (let i = 0; i < weights.length; i += 1) {
129
+ const share = total === 0 ? 0 : (weights[i] as number) / total;
130
+ /* A coefficient rounds away from zero at this precision. */
131
+ fixed[i] =
132
+ share < 0
133
+ ? -Math.floor(-share * (1 << precision) + 0.5)
134
+ : Math.floor(share * (1 << precision) + 0.5);
135
+ }
136
+ out.push({ first, weights: fixed });
137
+ }
138
+ return out;
139
+ }
140
+
141
+ /** The clamp of a fixed-point sum back to a byte. */
142
+ function clip8(sum: number, precision: number): number {
143
+ const value = Math.floor(sum / (1 << precision));
144
+ return value < 0 ? 0 : value > 255 ? 255 : value;
145
+ }
146
+
147
+ /**
148
+ * `src` into `out` by the antialiased bilinear: across first, then down, through an eight-bit
149
+ * intermediate, which is the order both libraries' passes have. `precision` says whose rounding.
150
+ */
151
+ export function triangleResize(
152
+ src: Uint8Array,
153
+ width: number,
154
+ height: number,
155
+ channels: number,
156
+ out: Uint8Array,
157
+ outWidth: number,
158
+ outHeight: number,
159
+ precision: number = PILLOW_PRECISION,
160
+ ): void {
161
+ const rounding = 1 << (precision - 1);
162
+ const across = triangleCoefficients(width, outWidth, precision);
163
+ const middle = width === outWidth ? src : new Uint8Array(outWidth * height * channels);
164
+ if (middle !== src) {
165
+ for (let y = 0; y < height; y += 1) {
166
+ for (let x = 0; x < outWidth; x += 1) {
167
+ const { first, weights } = across[x] as { first: number; weights: Int32Array };
168
+ for (let c = 0; c < channels; c += 1) {
169
+ let sum = rounding;
170
+ for (let i = 0; i < weights.length; i += 1) {
171
+ sum += (src[(y * width + first + i) * channels + c] as number) * (weights[i] as number);
172
+ }
173
+ middle[(y * outWidth + x) * channels + c] = clip8(sum, precision);
174
+ }
175
+ }
176
+ }
177
+ }
178
+ const down = triangleCoefficients(height, outHeight, precision);
179
+ if (height === outHeight) {
180
+ out.set(middle.subarray(0, outWidth * outHeight * channels));
181
+ return;
182
+ }
183
+ for (let y = 0; y < outHeight; y += 1) {
184
+ const { first, weights } = down[y] as { first: number; weights: Int32Array };
185
+ for (let x = 0; x < outWidth; x += 1) {
186
+ for (let c = 0; c < channels; c += 1) {
187
+ let sum = rounding;
188
+ for (let i = 0; i < weights.length; i += 1) {
189
+ sum +=
190
+ (middle[((first + i) * outWidth + x) * channels + c] as number) *
191
+ (weights[i] as number);
192
+ }
193
+ out[(y * outWidth + x) * channels + c] = clip8(sum, precision);
194
+ }
195
+ }
196
+ }
197
+ }
package/src/run.ts ADDED
@@ -0,0 +1,7 @@
1
+ /** How a capture runs a graph: the device's runner, or the reference evaluator. */
2
+ import type { NetworkGraph } from '@driftengine/texture';
3
+
4
+ export type GraphRun = (
5
+ graph: NetworkGraph,
6
+ inputs: ReadonlyMap<string, Float32Array>,
7
+ ) => Promise<ReadonlyMap<string, Float32Array>>;