framefields 2.2.0-beta.1 → 2.2.0-beta.3

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 (30) hide show
  1. package/dist/effects/index.d.mts +1 -1
  2. package/dist/effects/index.mjs +1 -1
  3. package/dist/{index-DLbtJPrc.d.mts → index-YSmPWdZw.d.mts} +31 -10
  4. package/dist/index-YSmPWdZw.d.mts.map +1 -0
  5. package/dist/index.d.mts +1 -1
  6. package/dist/index.mjs +3 -3
  7. package/dist/preview/index.mjs +1 -1
  8. package/dist/{preview-BQ3BhcVX.mjs → preview-QjhWe5QE.mjs} +2 -2
  9. package/dist/{preview-BQ3BhcVX.mjs.map → preview-QjhWe5QE.mjs.map} +1 -1
  10. package/dist/preview-engine/{chunk-Z457JJCO.mjs → chunk-3NXJVSZY.mjs} +1 -1
  11. package/dist/preview-engine/chunk-NECOKNHA.mjs +174 -0
  12. package/dist/preview-engine/chunk-X6HUH6K4.mjs +1 -0
  13. package/dist/preview-engine/{chunk-HFUA47QH.mjs → chunk-XHDUZY7Y.mjs} +1 -1
  14. package/dist/preview-engine/effects.mjs +1 -1
  15. package/dist/preview-engine/framefields.mjs +1 -1
  16. package/dist/preview-engine/player.mjs +1 -1
  17. package/dist/preview-engine/{renderers-HVTQ2B3H.mjs → renderers-VFV5QWO3.mjs} +1 -1
  18. package/dist/renderer/index.mjs +1 -1
  19. package/dist/renderer-Bxa4SDm9.mjs +3 -0
  20. package/dist/{renderer-dc39dBVA.mjs → renderer-CE7fwGgw.mjs} +1015 -110
  21. package/dist/renderer-CE7fwGgw.mjs.map +1 -0
  22. package/dist/{renderer-DzDZlez7.mjs → renderer-CivH0UuT.mjs} +6 -3
  23. package/dist/renderer-CivH0UuT.mjs.map +1 -0
  24. package/package.json +55 -55
  25. package/dist/index-DLbtJPrc.d.mts.map +0 -1
  26. package/dist/preview-engine/chunk-7OMK352W.mjs +0 -1
  27. package/dist/preview-engine/chunk-K7M5EIFC.mjs +0 -98
  28. package/dist/renderer-CZKFEf_6.mjs +0 -3
  29. package/dist/renderer-DzDZlez7.mjs.map +0 -1
  30. package/dist/renderer-dc39dBVA.mjs.map +0 -1
@@ -190,7 +190,7 @@ const COCO_CLASSES = [
190
190
  ];
191
191
 
192
192
  //#endregion
193
- //#region ../vision/dist/src-BjTNynFj.mjs
193
+ //#region ../vision/dist/src-COBo2ty9.mjs
194
194
  function computeIoU(boxA, boxB) {
195
195
  const xA = Math.max(boxA.originX, boxB.originX);
196
196
  const yA = Math.max(boxA.originY, boxB.originY);
@@ -263,11 +263,11 @@ var TemporalObjectTracker = class {
263
263
  for (let d = 0; d < detections.length; d++) {
264
264
  const det = detections[d];
265
265
  if (trk.category !== det.category) continue;
266
- const iou = computeIoU(trk.box, det.boundingBox);
267
- if (iou >= this._iouThreshold) candidateMatches.push({
266
+ const iou$1 = computeIoU(trk.box, det.boundingBox);
267
+ if (iou$1 >= this._iouThreshold) candidateMatches.push({
268
268
  trackIdx: t,
269
269
  detIdx: d,
270
- iou
270
+ iou: iou$1
271
271
  });
272
272
  }
273
273
  }
@@ -858,11 +858,13 @@ function decodeRtmdetIns(out, opts) {
858
858
  kept.sort((a, b) => b.detection.score - a.detection.score);
859
859
  const detections = kept.map((k) => k.detection);
860
860
  const masks = [];
861
- if (!out.masks) return {
861
+ if (!out.masks && !out.maskPlane) return {
862
862
  detections,
863
863
  masks
864
864
  };
865
- const alphaOf = probabilityToAlpha(opts.maskThreshold ?? .5, opts.featherRadius ?? .05);
865
+ const threshold = opts.maskThreshold ?? .5;
866
+ const feather = opts.featherRadius ?? .05;
867
+ const alphaOf = out.maskLogits ? logitToAlpha(threshold, feather) : probabilityToAlpha(threshold, feather);
866
868
  const { maskWidth: mw, maskHeight: mh } = out;
867
869
  const plane = mw * mh;
868
870
  const gx = mw / transform.inputWidth;
@@ -870,7 +872,8 @@ function decodeRtmdetIns(out, opts) {
870
872
  for (let d = 0; d < kept.length; d++) {
871
873
  const { index, detection } = kept[d];
872
874
  const box = detection.boundingBox;
873
- const base = index * plane;
875
+ const data = out.maskPlane ? out.maskPlane(index) : out.masks;
876
+ const base = out.maskPlane ? 0 : index * plane;
874
877
  const bx0 = Math.max(0, Math.floor(box.originX) - MASK_BOX_PAD_PX);
875
878
  const by0 = Math.max(0, Math.floor(box.originY) - MASK_BOX_PAD_PX);
876
879
  const bx1 = Math.min(sw - 1, Math.ceil(box.originX + box.width) + MASK_BOX_PAD_PX);
@@ -888,7 +891,7 @@ function decodeRtmdetIns(out, opts) {
888
891
  const mx0 = clamp$1(Math.floor(mx), 0, mw - 1);
889
892
  const mx1 = Math.min(mw - 1, mx0 + 1);
890
893
  const fx = clamp$1(mx - mx0, 0, 1);
891
- const alpha = alphaOf(out.masks[base + my0 * mw + mx0] * (1 - fx) * (1 - fy) + out.masks[base + my0 * mw + mx1] * fx * (1 - fy) + out.masks[base + my1 * mw + mx0] * (1 - fx) * fy + out.masks[base + my1 * mw + mx1] * fx * fy);
894
+ const alpha = alphaOf(data[base + my0 * mw + mx0] * (1 - fx) * (1 - fy) + data[base + my0 * mw + mx1] * fx * (1 - fy) + data[base + my1 * mw + mx0] * (1 - fx) * fy + data[base + my1 * mw + mx1] * fx * fy);
892
895
  if (alpha > 0) {
893
896
  mask[row + sx] = alpha;
894
897
  area += alpha / 255;
@@ -922,6 +925,21 @@ function probabilityToAlpha(threshold, feather) {
922
925
  const inv = 255 / (2 * feather);
923
926
  return (p) => p <= lo ? 0 : p >= threshold + feather ? 255 : (p - lo) * inv;
924
927
  }
928
+ /**
929
+ * Logit → 0..255 alpha: `probabilityToAlpha` after a sigmoid, which is only
930
+ * evaluated inside the feather band.
931
+ */
932
+ function logitToAlpha(threshold, feather) {
933
+ const toAlpha = probabilityToAlpha(threshold, feather);
934
+ const logit = (p) => p <= 0 ? -Infinity : p >= 1 ? Infinity : Math.log(p / (1 - p));
935
+ if (feather <= 0) {
936
+ const edge = logit(threshold);
937
+ return (l) => l > edge ? 255 : 0;
938
+ }
939
+ const lo = logit(threshold - feather);
940
+ const hi = logit(threshold + feather);
941
+ return (l) => l <= lo ? 0 : l >= hi ? 255 : toAlpha(1 / (1 + Math.exp(-l)));
942
+ }
925
943
  function clamp$1(v, lo, hi) {
926
944
  return v < lo ? lo : v > hi ? hi : v;
927
945
  }
@@ -972,9 +990,9 @@ function decodeRtmo(out, opts) {
972
990
  });
973
991
  }
974
992
  people.sort((a, b) => b.score - a.score);
975
- const iou = opts.iouThreshold ?? .6;
993
+ const iou$1 = opts.iouThreshold ?? .6;
976
994
  const kept = [];
977
- for (const p of people) if (!kept.some((k) => isDuplicate(k, p, iou))) kept.push(p);
995
+ for (const p of people) if (!kept.some((k) => isDuplicate(k, p, iou$1))) kept.push(p);
978
996
  return { people: kept };
979
997
  }
980
998
  function clamp(v, lo, hi) {
@@ -985,8 +1003,8 @@ function clamp(v, lo, hi) {
985
1003
  * within 10% of the stronger detection's box size of each other (boxes may differ when one
986
1004
  * of them also covers a flowing garment or a shadow).
987
1005
  */
988
- function isDuplicate(a, b, iou) {
989
- if (computeIoU(a.boundingBox, b.boundingBox) > iou) return true;
1006
+ function isDuplicate(a, b, iou$1) {
1007
+ if (computeIoU(a.boundingBox, b.boundingBox) > iou$1) return true;
990
1008
  const scale = Math.sqrt(a.boundingBox.width * a.boundingBox.height);
991
1009
  let sum = 0;
992
1010
  let n = 0;
@@ -1028,6 +1046,286 @@ function decodeSelfie(alphas, matteWidth, matteHeight, opts) {
1028
1046
  coverage: sw * sh > 0 ? sum / (255 * sw * sh) : 0
1029
1047
  };
1030
1048
  }
1049
+ /**
1050
+ * Frame preprocessing on the GPU: a texture becomes a model's NCHW float input
1051
+ * in a storage buffer, so a frame drawn by the renderer reaches the model
1052
+ * without a round trip through the CPU. The math is `imageToTensor`'s
1053
+ * (pixel-centre bilinear sampling, letterbox padding, channel order,
1054
+ * normalisation); see decode/preprocess.ts for the per-model contracts.
1055
+ *
1056
+ * Also: a content hash of a texture (two order-independent 32-bit sums, read
1057
+ * back as 8 bytes), so layers showing the same pixels can share a pass, and a
1058
+ * plain RGBA readback for paths that need pixels on the CPU.
1059
+ */
1060
+ const TENSOR_WGSL = `
1061
+ struct Params {
1062
+ src: vec2u,
1063
+ dst: vec2u,
1064
+ scale: vec2f,
1065
+ offset: vec2f,
1066
+ lo: vec2i,
1067
+ hi: vec2i,
1068
+ mean: vec4f,
1069
+ invStd: vec4f,
1070
+ pad: vec4f,
1071
+ order: vec4u,
1072
+ };
1073
+
1074
+ @group(0) @binding(0) var<uniform> p: Params;
1075
+ @group(0) @binding(1) var src: texture_2d<f32>;
1076
+ @group(0) @binding(2) var<storage, read_write> dst: array<f32>;
1077
+
1078
+ @compute @workgroup_size(16, 16)
1079
+ fn main(@builtin(global_invocation_id) id: vec3u) {
1080
+ if (id.x >= p.dst.x || id.y >= p.dst.y) { return; }
1081
+ let x = i32(id.x);
1082
+ let y = i32(id.y);
1083
+ let plane = p.dst.x * p.dst.y;
1084
+ let o = id.y * p.dst.x + id.x;
1085
+ var v = p.pad.xyz;
1086
+ if (x >= p.lo.x && x <= p.hi.x && y >= p.lo.y && y <= p.hi.y) {
1087
+ let maxX = i32(p.src.x) - 1;
1088
+ let maxY = i32(p.src.y) - 1;
1089
+ let sy = max(0.0, (f32(y) + 0.5 - p.offset.y) / p.scale.y - 0.5);
1090
+ let iy0 = min(maxY, i32(floor(sy)));
1091
+ let iy1 = min(maxY, iy0 + 1);
1092
+ let fy = sy - f32(iy0);
1093
+ let sx = max(0.0, (f32(x) + 0.5 - p.offset.x) / p.scale.x - 0.5);
1094
+ let ix0 = min(maxX, i32(floor(sx)));
1095
+ let ix1 = min(maxX, ix0 + 1);
1096
+ let fx = sx - f32(ix0);
1097
+ var rgba =
1098
+ textureLoad(src, vec2i(ix0, iy0), 0) * ((1.0 - fx) * (1.0 - fy)) +
1099
+ textureLoad(src, vec2i(ix1, iy0), 0) * (fx * (1.0 - fy)) +
1100
+ textureLoad(src, vec2i(ix0, iy1), 0) * ((1.0 - fx) * fy) +
1101
+ textureLoad(src, vec2i(ix1, iy1), 0) * (fx * fy);
1102
+ rgba = rgba * 255.0;
1103
+ let s = vec3f(rgba[p.order.x], rgba[p.order.y], rgba[p.order.z]);
1104
+ v = (s - p.mean.xyz) * p.invStd.xyz;
1105
+ }
1106
+ dst[o] = v.x;
1107
+ dst[plane + o] = v.y;
1108
+ dst[2u * plane + o] = v.z;
1109
+ }
1110
+ `;
1111
+ const HASH_WGSL = `
1112
+ @group(0) @binding(0) var src: texture_2d<f32>;
1113
+ @group(0) @binding(1) var<storage, read_write> acc: array<atomic<u32>, 2>;
1114
+
1115
+ var<workgroup> local: array<atomic<u32>, 2>;
1116
+
1117
+ @compute @workgroup_size(16, 16)
1118
+ fn main(
1119
+ @builtin(global_invocation_id) id: vec3u,
1120
+ @builtin(local_invocation_index) li: u32,
1121
+ ) {
1122
+ let dim = textureDimensions(src);
1123
+ if (id.x < dim.x && id.y < dim.y) {
1124
+ let c = pack4x8unorm(textureLoad(src, vec2i(id.xy), 0));
1125
+ let i = id.y * dim.x + id.x;
1126
+ let a = (c ^ (i * 0x9e3779b9u)) * 0x85ebca6bu;
1127
+ let b = (c + i * 0xc2b2ae35u) * 0x27d4eb2fu;
1128
+ atomicAdd(&local[0], a ^ (a >> 15u));
1129
+ atomicAdd(&local[1], b ^ (b >> 13u));
1130
+ }
1131
+ workgroupBarrier();
1132
+ if (li == 0u) {
1133
+ atomicAdd(&acc[0], atomicLoad(&local[0]));
1134
+ atomicAdd(&acc[1], atomicLoad(&local[1]));
1135
+ }
1136
+ }
1137
+ `;
1138
+ const PARAMS_BYTES = 112;
1139
+ const pipelines = /* @__PURE__ */ new WeakMap();
1140
+ var FrameTensorPipeline = class FrameTensorPipeline$1 {
1141
+ tensorPipeline;
1142
+ hashPipeline;
1143
+ params;
1144
+ /** One per device. */
1145
+ static for(device) {
1146
+ let pipeline = pipelines.get(device);
1147
+ if (!pipeline) {
1148
+ pipeline = new FrameTensorPipeline$1(device);
1149
+ pipelines.set(device, pipeline);
1150
+ }
1151
+ return pipeline;
1152
+ }
1153
+ constructor(device) {
1154
+ this.device = device;
1155
+ this.tensorPipeline = device.createComputePipeline({
1156
+ label: "vision_frame_tensor",
1157
+ layout: "auto",
1158
+ compute: {
1159
+ module: device.createShaderModule({ code: TENSOR_WGSL }),
1160
+ entryPoint: "main"
1161
+ }
1162
+ });
1163
+ this.hashPipeline = device.createComputePipeline({
1164
+ label: "vision_frame_hash",
1165
+ layout: "auto",
1166
+ compute: {
1167
+ module: device.createShaderModule({ code: HASH_WGSL }),
1168
+ entryPoint: "main"
1169
+ }
1170
+ });
1171
+ this.params = device.createBuffer({
1172
+ label: "vision_frame_tensor_params",
1173
+ size: PARAMS_BYTES,
1174
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
1175
+ });
1176
+ }
1177
+ /** Bytes of the NCHW float tensor for `spec`. */
1178
+ static tensorBytes(spec) {
1179
+ return 3 * spec.width * spec.height * 4;
1180
+ }
1181
+ /**
1182
+ * Writes `image` as the model input described by `spec` into `out` (at least
1183
+ * `tensorBytes(spec)`, STORAGE usage). Submits its own command buffer, so it
1184
+ * runs before anything submitted later (e.g. the inference reading `out`).
1185
+ */
1186
+ toTensor(image, spec, out) {
1187
+ const transform = computeInputTransform(image.width, image.height, spec);
1188
+ const { scaleX, scaleY, offsetX, offsetY } = transform;
1189
+ const params = new ArrayBuffer(PARAMS_BYTES);
1190
+ const u32 = new Uint32Array(params);
1191
+ const f32 = new Float32Array(params);
1192
+ const i32 = new Int32Array(params);
1193
+ u32.set([
1194
+ image.width,
1195
+ image.height,
1196
+ spec.width,
1197
+ spec.height
1198
+ ], 0);
1199
+ f32.set([
1200
+ scaleX,
1201
+ scaleY,
1202
+ offsetX,
1203
+ offsetY
1204
+ ], 4);
1205
+ i32.set([
1206
+ Math.ceil(offsetX),
1207
+ Math.ceil(offsetY),
1208
+ Math.min(spec.width, Math.floor(offsetX + image.width * scaleX)) - 1,
1209
+ Math.min(spec.height, Math.floor(offsetY + image.height * scaleY)) - 1
1210
+ ], 8);
1211
+ const inv = spec.std.map((s) => 1 / s);
1212
+ f32.set([...spec.mean, 0], 12);
1213
+ f32.set([...inv, 0], 16);
1214
+ f32.set([
1215
+ 0,
1216
+ 1,
1217
+ 2
1218
+ ].map((c) => (spec.padValue - spec.mean[c]) * inv[c]), 20);
1219
+ u32.set(spec.channels === "bgr" ? [
1220
+ 2,
1221
+ 1,
1222
+ 0,
1223
+ 3
1224
+ ] : [
1225
+ 0,
1226
+ 1,
1227
+ 2,
1228
+ 3
1229
+ ], 24);
1230
+ this.device.queue.writeBuffer(this.params, 0, params);
1231
+ const encoder$1 = this.device.createCommandEncoder({ label: "vision_frame_tensor" });
1232
+ const pass = encoder$1.beginComputePass();
1233
+ pass.setPipeline(this.tensorPipeline);
1234
+ pass.setBindGroup(0, this.device.createBindGroup({
1235
+ layout: this.tensorPipeline.getBindGroupLayout(0),
1236
+ entries: [
1237
+ {
1238
+ binding: 0,
1239
+ resource: { buffer: this.params }
1240
+ },
1241
+ {
1242
+ binding: 1,
1243
+ resource: image.texture.createView()
1244
+ },
1245
+ {
1246
+ binding: 2,
1247
+ resource: { buffer: out }
1248
+ }
1249
+ ]
1250
+ }));
1251
+ pass.dispatchWorkgroups(Math.ceil(spec.width / 16), Math.ceil(spec.height / 16));
1252
+ pass.end();
1253
+ this.device.queue.submit([encoder$1.finish()]);
1254
+ return transform;
1255
+ }
1256
+ /** A key that is equal for textures with equal size and pixels. */
1257
+ async contentKey(image) {
1258
+ const acc = this.device.createBuffer({
1259
+ size: 8,
1260
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
1261
+ });
1262
+ const staging = this.device.createBuffer({
1263
+ size: 8,
1264
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
1265
+ });
1266
+ const encoder$1 = this.device.createCommandEncoder({ label: "vision_frame_hash" });
1267
+ const pass = encoder$1.beginComputePass();
1268
+ pass.setPipeline(this.hashPipeline);
1269
+ pass.setBindGroup(0, this.device.createBindGroup({
1270
+ layout: this.hashPipeline.getBindGroupLayout(0),
1271
+ entries: [{
1272
+ binding: 0,
1273
+ resource: image.texture.createView()
1274
+ }, {
1275
+ binding: 1,
1276
+ resource: { buffer: acc }
1277
+ }]
1278
+ }));
1279
+ pass.dispatchWorkgroups(Math.ceil(image.width / 16), Math.ceil(image.height / 16));
1280
+ pass.end();
1281
+ encoder$1.copyBufferToBuffer(acc, 0, staging, 0, 8);
1282
+ this.device.queue.submit([encoder$1.finish()]);
1283
+ await staging.mapAsync(GPUMapMode.READ);
1284
+ const [a, b] = new Uint32Array(staging.getMappedRange().slice(0));
1285
+ staging.unmap();
1286
+ staging.destroy();
1287
+ acc.destroy();
1288
+ return `${image.width}x${image.height}:gpu:${a.toString(36)}:${b.toString(36)}`;
1289
+ }
1290
+ };
1291
+ /** Reads an 8-bit RGBA/BGRA texture back as RGBA pixels. */
1292
+ async function readTexturePixels(image) {
1293
+ const { device, texture, width, height } = image;
1294
+ const unaligned = width * 4;
1295
+ const bytesPerRow = Math.ceil(unaligned / 256) * 256;
1296
+ const staging = device.createBuffer({
1297
+ size: bytesPerRow * height,
1298
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
1299
+ label: "vision_frame_readback"
1300
+ });
1301
+ const encoder$1 = device.createCommandEncoder({ label: "vision_frame_readback" });
1302
+ encoder$1.copyTextureToBuffer({ texture }, {
1303
+ buffer: staging,
1304
+ bytesPerRow,
1305
+ rowsPerImage: height
1306
+ }, [
1307
+ width,
1308
+ height,
1309
+ 1
1310
+ ]);
1311
+ device.queue.submit([encoder$1.finish()]);
1312
+ await staging.mapAsync(GPUMapMode.READ);
1313
+ const mapped = new Uint8Array(staging.getMappedRange());
1314
+ const data = new Uint8ClampedArray(width * height * 4);
1315
+ for (let y = 0; y < height; y++) data.set(mapped.subarray(y * bytesPerRow, y * bytesPerRow + unaligned), y * unaligned);
1316
+ staging.unmap();
1317
+ staging.destroy();
1318
+ if (texture.format.startsWith("bgra")) for (let i = 0; i < data.length; i += 4) {
1319
+ const b = data[i];
1320
+ data[i] = data[i + 2];
1321
+ data[i + 2] = b;
1322
+ }
1323
+ return {
1324
+ data,
1325
+ width,
1326
+ height
1327
+ };
1328
+ }
1031
1329
  /** COCO-17 keypoint indices (RTMO output order). */
1032
1330
  const COCO17_KEYPOINTS = {
1033
1331
  NOSE: 0,
@@ -1474,6 +1772,431 @@ function createNeutralLandmarks() {
1474
1772
  });
1475
1773
  return landmarks;
1476
1774
  }
1775
+ /**
1776
+ * RTMDet-Ins post-processing, done here instead of inside the ONNX graph.
1777
+ *
1778
+ * The mmdeploy end2end export runs top-k, per-class NMS and mask assembly in
1779
+ * the graph and returns a full input-sized mask for each of up to 100
1780
+ * detections (40 on a typical frame: ~65 MB of floats). The runner instead
1781
+ * cuts the graph before its first data-dependent op (see `RTMDET_INS_HEAD`)
1782
+ * and reproduces the tail exactly:
1783
+ *
1784
+ * 1. top 5000 anchors by their best class score;
1785
+ * 2. ONNX NonMaxSuppression per class (IoU > 0.6 suppresses, scores > 0.05,
1786
+ * at most 200 per class);
1787
+ * 3. the 100 best (class, box) pairs;
1788
+ * 4. masks on demand: a 3-layer dynamic 1×1 conv over the 8 mask features
1789
+ * plus each pixel's offset from the anchor, at 1/8 of the input size.
1790
+ * The logits are upsampled (bilinear) and squashed by the decoder.
1791
+ *
1792
+ * Constants and layer layout match the export's graph; `models.e2e.test.ts`
1793
+ * checks the result against the end2end outputs.
1794
+ */
1795
+ /** Head tensors of the mmdeploy RTMDet-Ins exports (all variants). */
1796
+ const RTMDET_INS_HEAD = {
1797
+ scores: "/Sigmoid_output_0",
1798
+ boxes: "/Concat_7_output_0",
1799
+ kernels: "/Concat_5_output_0",
1800
+ maskFeatures: "/bbox_head/mask_head/projection/Conv_output_0"
1801
+ };
1802
+ const PRE_NMS = 5e3;
1803
+ const IOU_THRESHOLD = Math.fround(.6);
1804
+ const SCORE_FLOOR = Math.fround(.05);
1805
+ const MAX_PER_CLASS = 200;
1806
+ const MAX_DETECTIONS = 100;
1807
+ const STRIDES = [
1808
+ 8,
1809
+ 16,
1810
+ 32
1811
+ ];
1812
+ /** Mask features are at stride 8; relative offsets are divided by stride × 8. */
1813
+ const MASK_STRIDE = 8;
1814
+ const MASK_CHANNELS = 8;
1815
+ const KERNEL_SIZE = 169;
1816
+ /** Steps 1–3: the detections the end2end graph would return, best first. */
1817
+ function selectInstances(head) {
1818
+ const { scores, boxes, anchors, classes } = head;
1819
+ const best = new Float32Array(anchors);
1820
+ for (let a = 0; a < anchors; a++) {
1821
+ let max = 0;
1822
+ const row = a * classes;
1823
+ for (let c = 0; c < classes; c++) {
1824
+ const s = scores[row + c];
1825
+ if (s > max) max = s;
1826
+ }
1827
+ best[a] = max;
1828
+ }
1829
+ const order = [];
1830
+ for (let a = 0; a < anchors; a++) if (best[a] > SCORE_FLOOR) order.push(a);
1831
+ order.sort((x, y) => best[y] - best[x] || x - y);
1832
+ if (order.length > PRE_NMS) order.length = PRE_NMS;
1833
+ const picked = [];
1834
+ const candidates = [];
1835
+ const kept = [];
1836
+ for (let c = 0; c < classes; c++) {
1837
+ candidates.length = 0;
1838
+ for (const a of order) if (scores[a * classes + c] > SCORE_FLOOR) candidates.push(a);
1839
+ if (candidates.length === 0) continue;
1840
+ candidates.sort((x, y) => scores[y * classes + c] - scores[x * classes + c] || x - y);
1841
+ kept.length = 0;
1842
+ for (const a of candidates) {
1843
+ if (kept.length >= MAX_PER_CLASS) break;
1844
+ let suppressed = false;
1845
+ for (const k of kept) if (iou(boxes, a, k) > IOU_THRESHOLD) {
1846
+ suppressed = true;
1847
+ break;
1848
+ }
1849
+ if (!suppressed) {
1850
+ kept.push(a);
1851
+ picked.push({
1852
+ anchor: a,
1853
+ classIndex: c,
1854
+ score: scores[a * classes + c]
1855
+ });
1856
+ }
1857
+ }
1858
+ }
1859
+ picked.sort((x, y) => y.score - x.score);
1860
+ if (picked.length > MAX_DETECTIONS) picked.length = MAX_DETECTIONS;
1861
+ return picked;
1862
+ }
1863
+ function iou(boxes, a, b) {
1864
+ const a0 = boxes[a * 4];
1865
+ const a1 = boxes[a * 4 + 1];
1866
+ const a2 = boxes[a * 4 + 2];
1867
+ const a3 = boxes[a * 4 + 3];
1868
+ const b0 = boxes[b * 4];
1869
+ const b1 = boxes[b * 4 + 1];
1870
+ const b2 = boxes[b * 4 + 2];
1871
+ const b3 = boxes[b * 4 + 3];
1872
+ const w = Math.min(a2, b2) - Math.max(a0, b0);
1873
+ const h = Math.min(a3, b3) - Math.max(a1, b1);
1874
+ if (w <= 0 || h <= 0) return 0;
1875
+ const inter = w * h;
1876
+ const union = (a2 - a0) * (a3 - a1) + (b2 - b0) * (b3 - b1) - inter;
1877
+ return union > 0 ? inter / union : 0;
1878
+ }
1879
+ /** Anchor position and stride, in input pixels (mmdet point priors, offset 0). */
1880
+ function anchorPrior(anchor, inputWidth, inputHeight) {
1881
+ let base = 0;
1882
+ for (const stride of STRIDES) {
1883
+ const cols = Math.ceil(inputWidth / stride);
1884
+ const count = cols * Math.ceil(inputHeight / stride);
1885
+ if (anchor < base + count) {
1886
+ const i = anchor - base;
1887
+ return {
1888
+ x: i % cols * stride,
1889
+ y: Math.floor(i / cols) * stride,
1890
+ stride
1891
+ };
1892
+ }
1893
+ base += count;
1894
+ }
1895
+ throw new RangeError(`anchor ${anchor} is out of range`);
1896
+ }
1897
+ /**
1898
+ * Step 4: mask logits for one anchor, `maskHeight × maskWidth`, row-major.
1899
+ * Writes into `out` when it is large enough.
1900
+ */
1901
+ function maskLogits(head, anchor, out) {
1902
+ const { kernels, maskFeatures, maskWidth: mw, maskHeight: mh } = head;
1903
+ const plane = mw * mh;
1904
+ const logits = out && out.length >= plane ? out : new Float32Array(plane);
1905
+ const prior = anchorPrior(anchor, head.inputWidth, head.inputHeight);
1906
+ const k = kernels.subarray(anchor * KERNEL_SIZE, (anchor + 1) * KERNEL_SIZE);
1907
+ const w1 = 0;
1908
+ const w2 = 80;
1909
+ const w3 = 144;
1910
+ const b1 = 152;
1911
+ const b2 = 160;
1912
+ const b3 = 168;
1913
+ const scale = 1 / (prior.stride * MASK_STRIDE);
1914
+ const input = new Float32Array(2 + MASK_CHANNELS);
1915
+ const hidden1 = new Float32Array(8);
1916
+ const hidden2 = new Float32Array(8);
1917
+ for (let y = 0; y < mh; y++) {
1918
+ const relY = (prior.y - y * MASK_STRIDE) * scale;
1919
+ for (let x = 0; x < mw; x++) {
1920
+ const p = y * mw + x;
1921
+ input[0] = (prior.x - x * MASK_STRIDE) * scale;
1922
+ input[1] = relY;
1923
+ for (let c = 0; c < MASK_CHANNELS; c++) input[2 + c] = maskFeatures[c * plane + p];
1924
+ for (let o = 0; o < 8; o++) {
1925
+ let sum$1 = k[b1 + o];
1926
+ const row = w1 + o * 10;
1927
+ for (let i = 0; i < 10; i++) sum$1 += k[row + i] * input[i];
1928
+ hidden1[o] = sum$1 > 0 ? sum$1 : 0;
1929
+ }
1930
+ for (let o = 0; o < 8; o++) {
1931
+ let sum$1 = k[b2 + o];
1932
+ const row = w2 + o * 8;
1933
+ for (let i = 0; i < 8; i++) sum$1 += k[row + i] * hidden1[i];
1934
+ hidden2[o] = sum$1 > 0 ? sum$1 : 0;
1935
+ }
1936
+ let sum = k[b3];
1937
+ for (let i = 0; i < 8; i++) sum += k[w3 + i] * hidden2[i];
1938
+ logits[p] = sum;
1939
+ }
1940
+ }
1941
+ return logits;
1942
+ }
1943
+ /**
1944
+ * Minimal ONNX protobuf wire-format helpers: split a message into fields,
1945
+ * read strings, write length-delimited fields. Only what the graph editors
1946
+ * (onnx-subgraph.ts, onnx-float16.ts) need; unknown fields are copied as raw
1947
+ * bytes, so nothing else in a model is touched.
1948
+ */
1949
+ const MODEL_GRAPH = 7;
1950
+ const GRAPH_NODE = 1;
1951
+ const GRAPH_INITIALIZER = 5;
1952
+ const GRAPH_INPUT = 11;
1953
+ const GRAPH_OUTPUT = 12;
1954
+ const GRAPH_VALUE_INFO = 13;
1955
+ const NODE_INPUT = 1;
1956
+ const NODE_OUTPUT$1 = 2;
1957
+ const VALUE_INFO_NAME = 1;
1958
+ const TENSOR_NAME = 8;
1959
+ const WIRE_VARINT = 0;
1960
+ const WIRE_I64 = 1;
1961
+ const WIRE_LEN = 2;
1962
+ const WIRE_I32 = 5;
1963
+ function readVarint(buf, pos) {
1964
+ let value = 0;
1965
+ let scale = 1;
1966
+ for (;;) {
1967
+ if (pos >= buf.length) throw new Error("ONNX model: truncated varint");
1968
+ const byte = buf[pos++];
1969
+ value += (byte & 127) * scale;
1970
+ if (byte < 128) return [value, pos];
1971
+ scale *= 128;
1972
+ }
1973
+ }
1974
+ function fields(buf) {
1975
+ const out = [];
1976
+ let pos = 0;
1977
+ while (pos < buf.length) {
1978
+ const start = pos;
1979
+ const [tag, afterTag] = readVarint(buf, pos);
1980
+ pos = afterTag;
1981
+ const number = Math.floor(tag / 8);
1982
+ const wire = tag & 7;
1983
+ let body = buf.subarray(pos, pos);
1984
+ if (wire === WIRE_VARINT) pos = readVarint(buf, pos)[1];
1985
+ else if (wire === WIRE_I64) pos += 8;
1986
+ else if (wire === WIRE_I32) pos += 4;
1987
+ else if (wire === WIRE_LEN) {
1988
+ const [length, afterLength] = readVarint(buf, pos);
1989
+ body = buf.subarray(afterLength, afterLength + length);
1990
+ pos = afterLength + length;
1991
+ } else throw new Error(`ONNX model: unsupported wire type ${wire}`);
1992
+ if (pos > buf.length) throw new Error("ONNX model: truncated field");
1993
+ out.push({
1994
+ number,
1995
+ wire,
1996
+ raw: buf.subarray(start, pos),
1997
+ body
1998
+ });
1999
+ }
2000
+ return out;
2001
+ }
2002
+ const decoder = new TextDecoder();
2003
+ const encoder = new TextEncoder();
2004
+ function strings(buf, number) {
2005
+ return fields(buf).filter((f) => f.number === number && f.wire === WIRE_LEN).map((f) => decoder.decode(f.body));
2006
+ }
2007
+ function firstString(buf, number) {
2008
+ return strings(buf, number)[0];
2009
+ }
2010
+ function varint(value) {
2011
+ const bytes = [];
2012
+ while (value >= 128) {
2013
+ bytes.push(value % 128 | 128);
2014
+ value = Math.floor(value / 128);
2015
+ }
2016
+ bytes.push(value);
2017
+ return bytes;
2018
+ }
2019
+ function lengthDelimited(number, payload) {
2020
+ const head = [...varint(number * 8 + WIRE_LEN), ...varint(payload.length)];
2021
+ const out = new Uint8Array(head.length + payload.length);
2022
+ out.set(head, 0);
2023
+ out.set(payload, head.length);
2024
+ return out;
2025
+ }
2026
+ function concat(parts) {
2027
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
2028
+ let offset = 0;
2029
+ for (const part of parts) {
2030
+ out.set(part, offset);
2031
+ offset += part.length;
2032
+ }
2033
+ return out;
2034
+ }
2035
+ /**
2036
+ * Cuts an ONNX model down to the nodes that compute a chosen set of tensors,
2037
+ * which become the graph's outputs. Works on the protobuf wire format directly
2038
+ * (only the fields it needs are decoded; everything else is copied byte for
2039
+ * byte), so it needs no protobuf library and keeps the weights untouched.
2040
+ *
2041
+ * Used to run a detector's fixed-shape body on the GPU and do its
2042
+ * data-dependent tail (top-k, NMS, mask assembly) ourselves.
2043
+ */
2044
+ /** ValueInfoProto for a float tensor of unknown shape. */
2045
+ function floatOutput(name) {
2046
+ const typeProto = lengthDelimited(1, new Uint8Array([8, 1]));
2047
+ return concat([lengthDelimited(VALUE_INFO_NAME, encoder.encode(name)), lengthDelimited(2, typeProto)]);
2048
+ }
2049
+ /**
2050
+ * Returns a model whose outputs are `outputs` (tensors of the original graph),
2051
+ * keeping only the nodes, initializers and inputs needed to compute them.
2052
+ * Output types come from the graph's value_info when present; otherwise they
2053
+ * are declared as float tensors.
2054
+ */
2055
+ function extractSubgraph(model, outputs) {
2056
+ const modelFields = fields(model);
2057
+ const graphField = modelFields.find((f) => f.number === MODEL_GRAPH && f.wire === WIRE_LEN);
2058
+ if (!graphField) throw new Error("ONNX model has no graph");
2059
+ const graphFields = fields(graphField.body);
2060
+ const nodes = graphFields.filter((f) => f.number === GRAPH_NODE).map((f) => ({
2061
+ field: f,
2062
+ inputs: strings(f.body, NODE_INPUT),
2063
+ outputs: strings(f.body, NODE_OUTPUT$1)
2064
+ }));
2065
+ const producer = /* @__PURE__ */ new Map();
2066
+ nodes.forEach((node, index) => {
2067
+ for (const name of node.outputs) producer.set(name, index);
2068
+ });
2069
+ const keepNode = /* @__PURE__ */ new Set();
2070
+ const needed = /* @__PURE__ */ new Set();
2071
+ const stack = [...outputs];
2072
+ for (const name of outputs) if (!producer.has(name)) throw new Error(`ONNX model has no tensor '${name}'`);
2073
+ while (stack.length > 0) {
2074
+ const name = stack.pop();
2075
+ if (needed.has(name)) continue;
2076
+ needed.add(name);
2077
+ const index = producer.get(name);
2078
+ if (index === void 0 || keepNode.has(index)) continue;
2079
+ keepNode.add(index);
2080
+ for (const input of nodes[index].inputs) if (input) stack.push(input);
2081
+ }
2082
+ const valueInfo = /* @__PURE__ */ new Map();
2083
+ for (const f of graphFields) {
2084
+ if (f.number !== GRAPH_VALUE_INFO) continue;
2085
+ const name = firstString(f.body, VALUE_INFO_NAME);
2086
+ if (name !== void 0) valueInfo.set(name, f.body);
2087
+ }
2088
+ const parts = [];
2089
+ let nodeIndex = 0;
2090
+ for (const f of graphFields) switch (f.number) {
2091
+ case GRAPH_NODE:
2092
+ if (keepNode.has(nodeIndex)) parts.push(f.raw);
2093
+ nodeIndex++;
2094
+ break;
2095
+ case GRAPH_INITIALIZER: {
2096
+ const name = firstString(f.body, TENSOR_NAME);
2097
+ if (name !== void 0 && needed.has(name)) parts.push(f.raw);
2098
+ break;
2099
+ }
2100
+ case GRAPH_INPUT:
2101
+ case GRAPH_VALUE_INFO: {
2102
+ const name = firstString(f.body, VALUE_INFO_NAME);
2103
+ if (name !== void 0 && needed.has(name)) parts.push(f.raw);
2104
+ break;
2105
+ }
2106
+ case GRAPH_OUTPUT: break;
2107
+ default: parts.push(f.raw);
2108
+ }
2109
+ for (const name of outputs) parts.push(lengthDelimited(GRAPH_OUTPUT, valueInfo.get(name) ?? floatOutput(name)));
2110
+ const graph = concat(parts);
2111
+ return concat(modelFields.map((f) => f === graphField ? lengthDelimited(MODEL_GRAPH, graph) : f.raw));
2112
+ }
2113
+ /**
2114
+ * onnxruntime-web 1.30's WebGPU EP computes grouped convolutions wrong in its
2115
+ * default NHWC layout when a group has more than one channel and the count
2116
+ * isn't a multiple of 4 (RTMO's head: 18 per group → duplicate skeletons).
2117
+ * NCHW is correct but ~30% slower, so only models with such a convolution get
2118
+ * it. Depthwise convolutions (one channel per group: RTMDet-Ins, Selfie) are
2119
+ * fine in NHWC.
2120
+ */
2121
+ const NODE_OP_TYPE = 4;
2122
+ const NODE_ATTRIBUTE = 5;
2123
+ const NODE_OUTPUT = 2;
2124
+ const ATTR_NAME = 1;
2125
+ const ATTR_I = 3;
2126
+ const ATTR_T = 5;
2127
+ const TENSOR_DIMS = 1;
2128
+ function varintValues(field) {
2129
+ const values = [];
2130
+ if (field.wire === WIRE_LEN) {
2131
+ let pos = 0;
2132
+ while (pos < field.body.length) {
2133
+ const [value, next] = readVarint(field.body, pos);
2134
+ values.push(value);
2135
+ pos = next;
2136
+ }
2137
+ } else if (field.wire === WIRE_VARINT) {
2138
+ let pos = 0;
2139
+ pos = readVarint(field.raw, pos)[1];
2140
+ values.push(readVarint(field.raw, pos)[0]);
2141
+ }
2142
+ return values;
2143
+ }
2144
+ function tensorDims(tensor) {
2145
+ return fields(tensor).filter((f) => f.number === TENSOR_DIMS).flatMap(varintValues);
2146
+ }
2147
+ /**
2148
+ * True when the model has a grouped convolution the NHWC kernels get wrong,
2149
+ * and when it can't be read (NCHW is always correct).
2150
+ */
2151
+ function needsNchwLayout(model) {
2152
+ let graph;
2153
+ try {
2154
+ graph = fields(model).find((f) => f.number === MODEL_GRAPH && f.wire === WIRE_LEN);
2155
+ } catch {
2156
+ return true;
2157
+ }
2158
+ if (!graph) return true;
2159
+ const dims = /* @__PURE__ */ new Map();
2160
+ const convs = [];
2161
+ for (const f of fields(graph.body)) if (f.number === GRAPH_INITIALIZER) {
2162
+ const name = firstString(f.body, TENSOR_NAME);
2163
+ if (name !== void 0) dims.set(name, tensorDims(f.body));
2164
+ } else if (f.number === GRAPH_NODE) {
2165
+ const op = firstString(f.body, NODE_OP_TYPE);
2166
+ if (op !== "Conv" && op !== "Constant") continue;
2167
+ const attributes = fields(f.body).filter((a) => a.number === NODE_ATTRIBUTE).map((a) => a.body);
2168
+ if (op === "Constant") {
2169
+ const value = attributes.find((a) => firstString(a, ATTR_NAME) === "value");
2170
+ const tensor = value && fields(value).find((a) => a.number === ATTR_T);
2171
+ const output$1 = strings(f.body, NODE_OUTPUT)[0];
2172
+ if (tensor && output$1) dims.set(output$1, tensorDims(tensor.body));
2173
+ continue;
2174
+ }
2175
+ const groupAttr = attributes.find((a) => firstString(a, ATTR_NAME) === "group");
2176
+ const groupField = groupAttr && fields(groupAttr).find((a) => a.number === ATTR_I);
2177
+ const group = groupField ? varintValues(groupField)[0] ?? 1 : 1;
2178
+ const weight = strings(f.body, NODE_INPUT)[1];
2179
+ if (group > 1 && weight) convs.push({
2180
+ weight,
2181
+ group
2182
+ });
2183
+ }
2184
+ return convs.some(({ weight }) => {
2185
+ const perGroup = dims.get(weight)?.[1];
2186
+ return perGroup !== void 0 && perGroup > 1 && perGroup % 4 !== 0;
2187
+ });
2188
+ }
2189
+ /**
2190
+ * Browser / WebGPU session provider .
2191
+ *
2192
+ * Mirrors `NodeSessionProvider` but uses `onnxruntime-web` with the WebGPU execution provider
2193
+ * and a WASM fallback. The ORT module is imported lazily behind an injectable loader, so:
2194
+ * - the Node core entry never pulls browser code, and
2195
+ * - unit tests can run fully offline with a fake module.
2196
+ *
2197
+ * `onnxruntime-web` is an OPTIONAL peer dependency — consumers of `@framefields/vision/web`
2198
+ * provide it; the main `@framefields/vision` entry never touches it.
2199
+ */
1477
2200
  const ORT_WEB_SPECIFIER = "onnxruntime-web";
1478
2201
  const defaultLoader = () => import(
1479
2202
  /* @vite-ignore */
@@ -1485,10 +2208,12 @@ var WebGPUProvider = class {
1485
2208
  _providers;
1486
2209
  _wasmFallback;
1487
2210
  _ortPromise;
2211
+ device;
1488
2212
  constructor(options = {}) {
1489
2213
  this._loader = options.loader ?? defaultLoader;
1490
2214
  this._providers = options.executionProviders ?? ["webgpu", "wasm"];
1491
2215
  this._wasmFallback = options.wasmFallback !== false;
2216
+ this.device = options.device;
1492
2217
  }
1493
2218
  ort() {
1494
2219
  if (!this._ortPromise) this._ortPromise = this._loader().catch((err) => {
@@ -1501,8 +2226,10 @@ var WebGPUProvider = class {
1501
2226
  const ort = await this.ort();
1502
2227
  let session;
1503
2228
  try {
2229
+ const device = this.device;
2230
+ const layout = needsNchwLayout(modelBytes) ? "NCHW" : "NHWC";
1504
2231
  session = await ort.InferenceSession.create(modelBytes, {
1505
- executionProviders: [...this._providers],
2232
+ executionProviders: this._providers.map((name) => withProviderOptions(name, device, layout)),
1506
2233
  graphOptimizationLevel: "all",
1507
2234
  logSeverityLevel: 3
1508
2235
  });
@@ -1517,11 +2244,15 @@ var WebGPUProvider = class {
1517
2244
  return {
1518
2245
  run: async (input) => {
1519
2246
  const inputName = session.inputNames.includes(input.name) ? input.name : session.inputNames[0] ?? input.name;
1520
- const results = await session.run({ [inputName]: new ort.Tensor("float32", input.data, [...input.dims]) });
2247
+ const tensor = input.gpuBuffer && ort.Tensor.fromGpuBuffer ? ort.Tensor.fromGpuBuffer(input.gpuBuffer, {
2248
+ dataType: "float32",
2249
+ dims: [...input.dims]
2250
+ }) : new ort.Tensor("float32", input.data, [...input.dims]);
2251
+ const results = await session.run({ [inputName]: tensor });
1521
2252
  const outputs = {};
1522
- for (const [name, tensor] of Object.entries(results)) outputs[name] = {
1523
- data: tensor.data,
1524
- dims: [...tensor.dims]
2253
+ for (const [name, tensor$1] of Object.entries(results)) outputs[name] = {
2254
+ data: tensor$1.data,
2255
+ dims: [...tensor$1.dims]
1525
2256
  };
1526
2257
  return outputs;
1527
2258
  },
@@ -1531,6 +2262,23 @@ var WebGPUProvider = class {
1531
2262
  };
1532
2263
  }
1533
2264
  };
2265
+ /**
2266
+ * The WebGPU EP runs in NCHW. Its default NHWC layout computes grouped convolutions wrong
2267
+ * when the channels per group aren't a multiple of 4 (onnxruntime-web 1.30) — RTMO's head
2268
+ * has group=8 convs over 104 channels (13 per group), which turned one dancer into several
2269
+ * skeletons. NCHW matches the CPU EP on every pinned model.
2270
+ */
2271
+ function withProviderOptions(name, device, preferredLayout) {
2272
+ if (name !== "webgpu") return name;
2273
+ return device ? {
2274
+ name,
2275
+ preferredLayout,
2276
+ device
2277
+ } : {
2278
+ name,
2279
+ preferredLayout
2280
+ };
2281
+ }
1534
2282
  const ORT_WEBGPU_SPECIFIER = "onnxruntime-web/webgpu";
1535
2283
  const WEBGPU_SPECIFIER = "webgpu";
1536
2284
  let nodeWebGPUReady;
@@ -1609,6 +2357,7 @@ function patchSupportedFeatures(features) {
1609
2357
  }
1610
2358
  var NodeWebGPUProvider = class {
1611
2359
  kind = "webgpu";
2360
+ device;
1612
2361
  _inner;
1613
2362
  constructor(options = {}) {
1614
2363
  const loader = options.loader ?? (async () => {
@@ -1621,6 +2370,7 @@ var NodeWebGPUProvider = class {
1621
2370
  if (mod.env?.webgpu) mod.env.webgpu.powerPreference = "high-performance";
1622
2371
  return mod;
1623
2372
  });
2373
+ this.device = options.device;
1624
2374
  this._inner = new WebGPUProvider({
1625
2375
  wasmFallback: false,
1626
2376
  ...options,
@@ -1632,6 +2382,27 @@ var NodeWebGPUProvider = class {
1632
2382
  }
1633
2383
  };
1634
2384
  let defaultProvider;
2385
+ let visionDevice;
2386
+ /**
2387
+ * The GPU device the default provider runs WebGPU sessions on — the renderer's,
2388
+ * so frames can go from its textures into the models without a CPU round trip.
2389
+ * Applies to sessions created afterwards; without it, onnxruntime-web creates
2390
+ * its own device.
2391
+ */
2392
+ function setVisionDevice(device) {
2393
+ visionDevice = device;
2394
+ }
2395
+ /**
2396
+ * Hosts (the renderer) register a listener under this symbol to log the
2397
+ * backend; a global keeps it working however the packages are bundled.
2398
+ */
2399
+ const VISION_BACKEND_LISTENER = Symbol.for("framefields.vision.backend");
2400
+ function reportBackend(info) {
2401
+ const listener = globalThis[VISION_BACKEND_LISTENER];
2402
+ if (typeof listener === "function") try {
2403
+ listener(info);
2404
+ } catch {}
2405
+ }
1635
2406
  function requestedVisionBackend() {
1636
2407
  const value = typeof process !== "undefined" ? process.env.FRAMEFIELDS_VISION_PROVIDER?.trim().toLowerCase() : void 0;
1637
2408
  return value === "webgpu" || value === "cuda" || value === "cpu" ? value : "auto";
@@ -1653,6 +2424,9 @@ var AutoSessionProvider = class {
1653
2424
  get kind() {
1654
2425
  return this._resolved?.kind ?? "webgpu";
1655
2426
  }
2427
+ get device() {
2428
+ return this._resolved?.device;
2429
+ }
1656
2430
  async createSession(modelBytes) {
1657
2431
  if (this._resolved) return this._resolved.createSession(modelBytes);
1658
2432
  if (!this._resolving) {
@@ -1670,16 +2444,42 @@ var AutoSessionProvider = class {
1670
2444
  }
1671
2445
  async resolve(modelBytes) {
1672
2446
  const backend = requestedVisionBackend();
1673
- if (backend === "cpu") return { provider: new NodeSessionProvider() };
1674
- if (backend === "cuda") return { provider: new NodeSessionProvider({ executionProviders: ["cuda", "cpu"] }) };
1675
- const webgpu = new NodeWebGPUProvider({ wasmFallback: false });
2447
+ if (backend === "cpu") {
2448
+ reportBackend({
2449
+ backend: "cpu",
2450
+ sharedDevice: false,
2451
+ reason: "FRAMEFIELDS_VISION_PROVIDER=cpu"
2452
+ });
2453
+ return { provider: new NodeSessionProvider() };
2454
+ }
2455
+ if (backend === "cuda") {
2456
+ reportBackend({
2457
+ backend: "cuda",
2458
+ sharedDevice: false
2459
+ });
2460
+ return { provider: new NodeSessionProvider({ executionProviders: ["cuda", "cpu"] }) };
2461
+ }
2462
+ const webgpu = new NodeWebGPUProvider({
2463
+ wasmFallback: false,
2464
+ device: visionDevice
2465
+ });
1676
2466
  try {
2467
+ const first = await webgpu.createSession(modelBytes);
2468
+ reportBackend({
2469
+ backend: "webgpu",
2470
+ sharedDevice: !!visionDevice
2471
+ });
1677
2472
  return {
1678
2473
  provider: webgpu,
1679
- first: await webgpu.createSession(modelBytes)
2474
+ first
1680
2475
  };
1681
2476
  } catch (err) {
1682
2477
  if (backend === "webgpu") throw err;
2478
+ reportBackend({
2479
+ backend: "cpu",
2480
+ sharedDevice: false,
2481
+ reason: err instanceof Error ? err.message : String(err)
2482
+ });
1683
2483
  return { provider: new NodeSessionProvider() };
1684
2484
  }
1685
2485
  }
@@ -1775,6 +2575,9 @@ var NodeSessionProvider = class {
1775
2575
  };
1776
2576
  }
1777
2577
  };
2578
+ function isGpuImage(image) {
2579
+ return image.texture !== void 0;
2580
+ }
1778
2581
  const MAX_INSTANCE_PASSES = 4;
1779
2582
  /**
1780
2583
  * Lazy vision runner.
@@ -1811,6 +2614,7 @@ var VisionRunner = class VisionRunner$1 {
1811
2614
  sessions: /* @__PURE__ */ new Map(),
1812
2615
  tensorScratch: /* @__PURE__ */ new Map(),
1813
2616
  runQueue: /* @__PURE__ */ new Map(),
2617
+ gpuInputs: /* @__PURE__ */ new Map(),
1814
2618
  instancePasses: []
1815
2619
  };
1816
2620
  }
@@ -1865,7 +2669,8 @@ var VisionRunner = class VisionRunner$1 {
1865
2669
  const pass = await this.instancePass(image);
1866
2670
  return decodeRtmdetIns({
1867
2671
  ...pass.outputs,
1868
- masks: void 0
2672
+ masks: void 0,
2673
+ maskPlane: void 0
1869
2674
  }, this.instanceDecodeOptions(image, pass.transform)).detections;
1870
2675
  }
1871
2676
  /** COCO-80 boxes plus a frame-aligned soft mask per instance. */
@@ -1909,30 +2714,21 @@ var VisionRunner = class VisionRunner$1 {
1909
2714
  for (const promise of core.sessions.values()) promise.then((session) => session.release()).catch(() => void 0);
1910
2715
  core.sessions.clear();
1911
2716
  core.runQueue.clear();
2717
+ for (const { buffer } of core.gpuInputs.values()) buffer.destroy();
2718
+ core.gpuInputs.clear();
1912
2719
  core.instancePasses.length = 0;
1913
2720
  core.store.clearMemoryCache();
1914
2721
  }
1915
- instancePass(image) {
2722
+ async instancePass(image) {
1916
2723
  const passes = this._core.instancePasses;
1917
- const content = contentKey(image);
2724
+ const content = isGpuImage(image) ? await FrameTensorPipeline.for(image.device).contentKey(image) : contentKey(image);
1918
2725
  const byContent = passes.find((pass$1) => pass$1.content === content);
1919
2726
  if (byContent) return byContent.result;
1920
2727
  const key = this.modelKeyFor("segment");
1921
- const result = this.infer(key, image).then(({ outputs, transform }) => {
1922
- const dets = output(outputs, "dets", key);
1923
- const masks = output(outputs, "masks", key);
1924
- return {
1925
- transform,
1926
- outputs: {
1927
- dets: dets.data,
1928
- labels: output(outputs, "labels", key).data,
1929
- masks: masks.data,
1930
- count: dets.dims[1] ?? 0,
1931
- maskHeight: masks.dims[masks.dims.length - 2] ?? 0,
1932
- maskWidth: masks.dims[masks.dims.length - 1] ?? 0
1933
- }
1934
- };
1935
- });
2728
+ const result = this.infer(key, image).then(({ outputs, transform }) => ({
2729
+ transform,
2730
+ outputs: RTMDET_INS_HEAD.scores in outputs ? headPassOutputs(outputs, key, transform) : end2endPassOutputs(outputs, key)
2731
+ }));
1936
2732
  const pass = {
1937
2733
  content,
1938
2734
  result
@@ -1971,10 +2767,33 @@ var VisionRunner = class VisionRunner$1 {
1971
2767
  });
1972
2768
  return run;
1973
2769
  }
1974
- async inferNow(key, image) {
2770
+ async inferNow(key, frame) {
1975
2771
  const session = await this.session(key);
1976
2772
  const desc = VISION_MODELS[key];
1977
2773
  const spec = preprocessSpec(desc.family, desc.input);
2774
+ const dims = [
2775
+ 1,
2776
+ 3,
2777
+ spec.height,
2778
+ spec.width
2779
+ ];
2780
+ let image;
2781
+ if (isGpuImage(frame)) {
2782
+ if (this._core.provider.device === frame.device) {
2783
+ const buffer = this.gpuInput(key, frame.device, spec);
2784
+ const transform$1 = FrameTensorPipeline.for(frame.device).toTensor(frame, spec, buffer);
2785
+ return {
2786
+ outputs: await session.run({
2787
+ name: "input",
2788
+ data: EMPTY_TENSOR,
2789
+ gpuBuffer: buffer,
2790
+ dims
2791
+ }),
2792
+ transform: transform$1
2793
+ };
2794
+ }
2795
+ image = await readTexturePixels(frame);
2796
+ } else image = frame;
1978
2797
  const size = 3 * spec.width * spec.height;
1979
2798
  const scratchByKey = this._core.tensorScratch;
1980
2799
  let scratch = scratchByKey.get(key);
@@ -1987,21 +2806,33 @@ var VisionRunner = class VisionRunner$1 {
1987
2806
  outputs: await session.run({
1988
2807
  name: "input",
1989
2808
  data: tensor,
1990
- dims: [
1991
- 1,
1992
- 3,
1993
- spec.height,
1994
- spec.width
1995
- ]
2809
+ dims
1996
2810
  }),
1997
2811
  transform
1998
2812
  };
1999
2813
  }
2814
+ gpuInput(key, device, spec) {
2815
+ const inputs = this._core.gpuInputs;
2816
+ const size = FrameTensorPipeline.tensorBytes(spec);
2817
+ const current = inputs.get(key);
2818
+ if (current?.device === device && current.buffer.size >= size) return current.buffer;
2819
+ current?.buffer.destroy();
2820
+ const buffer = device.createBuffer({
2821
+ label: `vision_input_${key}`,
2822
+ size,
2823
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
2824
+ });
2825
+ inputs.set(key, {
2826
+ device,
2827
+ buffer
2828
+ });
2829
+ return buffer;
2830
+ }
2000
2831
  session(key) {
2001
2832
  const { sessions, store, provider } = this._core;
2002
2833
  let promise = sessions.get(key);
2003
2834
  if (!promise) {
2004
- promise = store.ensure(key).then((bytes) => provider.createSession(bytes)).catch((err) => {
2835
+ promise = store.ensure(key).then((bytes) => provider.createSession(VISION_MODELS[key].family === "rtmdet-ins" ? rtmdetInsBody(bytes) : bytes)).catch((err) => {
2005
2836
  sessions.delete(key);
2006
2837
  store.evict(key);
2007
2838
  throw err;
@@ -2011,6 +2842,74 @@ var VisionRunner = class VisionRunner$1 {
2011
2842
  return promise;
2012
2843
  }
2013
2844
  };
2845
+ const EMPTY_TENSOR = new Float32Array(0);
2846
+ /**
2847
+ * The RTMDet-Ins graph up to its head (see `RTMDET_INS_HEAD`): no NMS or
2848
+ * full-size masks in the graph, so the GPU part has fixed shapes and only
2849
+ * ~9 MB comes back. Models without those tensors run end to end.
2850
+ */
2851
+ function rtmdetInsBody(bytes) {
2852
+ try {
2853
+ return extractSubgraph(bytes, Object.values(RTMDET_INS_HEAD));
2854
+ } catch {
2855
+ return bytes;
2856
+ }
2857
+ }
2858
+ /** Outputs of the end2end graph: NMS done, one input-sized mask per detection. */
2859
+ function end2endPassOutputs(outputs, key) {
2860
+ const dets = output(outputs, "dets", key);
2861
+ const masks = output(outputs, "masks", key);
2862
+ return {
2863
+ dets: dets.data,
2864
+ labels: output(outputs, "labels", key).data,
2865
+ masks: masks.data,
2866
+ count: dets.dims[1] ?? 0,
2867
+ maskHeight: masks.dims[masks.dims.length - 2] ?? 0,
2868
+ maskWidth: masks.dims[masks.dims.length - 1] ?? 0
2869
+ };
2870
+ }
2871
+ /** Outputs of the head: selection here, mask logits only for detections asked for. */
2872
+ function headPassOutputs(outputs, key, transform) {
2873
+ const scores = output(outputs, RTMDET_INS_HEAD.scores, key);
2874
+ const features = output(outputs, RTMDET_INS_HEAD.maskFeatures, key);
2875
+ const head = {
2876
+ scores: scores.data,
2877
+ boxes: output(outputs, RTMDET_INS_HEAD.boxes, key).data,
2878
+ kernels: output(outputs, RTMDET_INS_HEAD.kernels, key).data,
2879
+ maskFeatures: features.data,
2880
+ anchors: scores.dims[1] ?? 0,
2881
+ classes: scores.dims[2] ?? 0,
2882
+ maskHeight: features.dims[2] ?? 0,
2883
+ maskWidth: features.dims[3] ?? 0,
2884
+ inputWidth: transform.inputWidth,
2885
+ inputHeight: transform.inputHeight
2886
+ };
2887
+ const picked = selectInstances(head);
2888
+ const dets = new Float32Array(picked.length * 5);
2889
+ const labels = new Int32Array(picked.length);
2890
+ picked.forEach(({ anchor, classIndex, score }, i) => {
2891
+ dets.set(head.boxes.subarray(anchor * 4, anchor * 4 + 4), i * 5);
2892
+ dets[i * 5 + 4] = score;
2893
+ labels[i] = classIndex;
2894
+ });
2895
+ const planes = /* @__PURE__ */ new Map();
2896
+ return {
2897
+ dets,
2898
+ labels,
2899
+ count: picked.length,
2900
+ maskWidth: head.maskWidth,
2901
+ maskHeight: head.maskHeight,
2902
+ maskLogits: true,
2903
+ maskPlane: (index) => {
2904
+ let plane = planes.get(index);
2905
+ if (!plane) {
2906
+ plane = maskLogits(head, picked[index].anchor);
2907
+ planes.set(index, plane);
2908
+ }
2909
+ return plane;
2910
+ }
2911
+ };
2912
+ }
2014
2913
  /** Size plus a 64-bit hash of the pixels: equal keys mean the same picture. */
2015
2914
  function contentKey(image) {
2016
2915
  const { data } = image;
@@ -2035,10 +2934,13 @@ function output(outputs, name, key) {
2035
2934
  * The frame's primary instance: the largest person when present, else the most confident
2036
2935
  * instance (lowest `detectionIndex` — detections are score-sorted). Size alone is a poor
2037
2936
  * signal without a person: the largest instance is usually a backdrop ("dining table").
2937
+ * `classes` (case-insensitive) limits which instances can be the subject.
2038
2938
  */
2039
- function selectSubjectMask(masks) {
2939
+ function selectSubjectMask(masks, classes) {
2940
+ const allowed = classes && new Set(classes.map((c) => c.toLowerCase()));
2040
2941
  let best;
2041
2942
  for (const m of masks) {
2943
+ if (allowed && !allowed.has(m.category.toLowerCase())) continue;
2042
2944
  if (!best) {
2043
2945
  best = m;
2044
2946
  continue;
@@ -2079,16 +2981,17 @@ function maskBounds(mask) {
2079
2981
  }
2080
2982
  /**
2081
2983
  * Builds the full subject silhouette: the primary instance plus every comparably sized
2082
- * instance whose bounds sit mostly inside or across it (`overlap` = intersection / smaller
2083
- * box area; `maxGrowth` caps a part's box area relative to the subject's).
2984
+ * instance whose bounds sit mostly inside or across it.
2084
2985
  *
2085
2986
  * COCO has no "clothing" class, so a flowing dress, a held guitar or a ridden bike comes back
2086
2987
  * as its own instance (often mislabeled) — merging them keeps the whole figure in the cutout.
2087
- * The size cap keeps containers out: a small figure inside a tunnel or window detected as a
2088
- * huge "clock" must not drag the whole frame into the subject.
2988
+ * That is why `classes` only picks the subject: a dress detected as a "kite" still merges
2989
+ * into a "person" subject. The size cap keeps containers out: a small figure inside a tunnel
2990
+ * or window detected as a huge "clock" must not drag the whole frame into the subject.
2089
2991
  */
2090
- function mergeSubjectMask(masks, overlap = .5, maxGrowth = 2) {
2091
- const subject = selectSubjectMask(masks);
2992
+ function mergeSubjectMask(masks, options = {}) {
2993
+ const { classes, overlap = .5, maxGrowth = 2 } = options;
2994
+ const subject = selectSubjectMask(masks, classes);
2092
2995
  if (!subject) return void 0;
2093
2996
  const sb = maskBounds(subject);
2094
2997
  if (!sb) return subject;
@@ -2217,16 +3120,16 @@ function matchPoseToTracks(people, trackedObjects, minIoU = .15) {
2217
3120
  const pBox = getPersonBoundingBox(person);
2218
3121
  const isNormalized = pBox.width <= 1.05 && pBox.height <= 1.05;
2219
3122
  for (const trk of personTracks) {
2220
- const iou = computeIoU(pBox, isNormalized && trk.boundingBox.normalizedWidth > 0 ? {
3123
+ const iou$1 = computeIoU(pBox, isNormalized && trk.boundingBox.normalizedWidth > 0 ? {
2221
3124
  originX: trk.boundingBox.normalizedX,
2222
3125
  originY: trk.boundingBox.normalizedY,
2223
3126
  width: trk.boundingBox.normalizedWidth,
2224
3127
  height: trk.boundingBox.normalizedHeight
2225
3128
  } : trk.boundingBox);
2226
- if (iou >= minIoU) candidateMatches.push({
3129
+ if (iou$1 >= minIoU) candidateMatches.push({
2227
3130
  personIdx: p,
2228
3131
  trackId: trk.trackId,
2229
- iou
3132
+ iou: iou$1
2230
3133
  });
2231
3134
  }
2232
3135
  }
@@ -2427,14 +3330,14 @@ var VisionBundle = class {
2427
3330
  let bestIoU = .1;
2428
3331
  for (const p of poseRes.people) {
2429
3332
  const pBox = getPersonBoundingBox(p);
2430
- const iou = computeIoU(pBox, pBox.width <= 1.05 && pBox.height <= 1.05 && trk.boundingBox.normalizedWidth > 0 ? {
3333
+ const iou$1 = computeIoU(pBox, pBox.width <= 1.05 && pBox.height <= 1.05 && trk.boundingBox.normalizedWidth > 0 ? {
2431
3334
  originX: trk.boundingBox.normalizedX,
2432
3335
  originY: trk.boundingBox.normalizedY,
2433
3336
  width: trk.boundingBox.normalizedWidth,
2434
3337
  height: trk.boundingBox.normalizedHeight
2435
3338
  } : trk.boundingBox);
2436
- if (iou > bestIoU) {
2437
- bestIoU = iou;
3339
+ if (iou$1 > bestIoU) {
3340
+ bestIoU = iou$1;
2438
3341
  bestPerson = p;
2439
3342
  }
2440
3343
  }
@@ -3197,7 +4100,7 @@ var VisionNode = class VisionNode$1 {
3197
4100
  };
3198
4101
 
3199
4102
  //#endregion
3200
- //#region ../../nodes/node-vision/dist/renderers-BcBWZ-Rc.mjs
4103
+ //#region ../../nodes/node-vision/dist/renderers-C8sGL0II.mjs
3201
4104
  let scratchValues = new Uint8Array(0);
3202
4105
  let scratchLengths = new Uint32Array(0);
3203
4106
  function encodeMask(mask, width, height) {
@@ -3404,7 +4307,7 @@ var MatteCompositor = class {
3404
4307
  return tex;
3405
4308
  }
3406
4309
  /** Clears `target` and draws `picture` through `mask` into it. */
3407
- draw(encoder, target, picture, mask, mode, bounds) {
4310
+ draw(encoder$1, target, picture, mask, mode, bounds) {
3408
4311
  const w = mask.width;
3409
4312
  const h = mask.height;
3410
4313
  const data = new Float32Array(8);
@@ -3450,7 +4353,7 @@ var MatteCompositor = class {
3450
4353
  }
3451
4354
  ]
3452
4355
  });
3453
- const pass = encoder.beginRenderPass({
4356
+ const pass = encoder$1.beginRenderPass({
3454
4357
  label: "vision_matte_composite",
3455
4358
  colorAttachments: [{
3456
4359
  view: target,
@@ -3484,8 +4387,9 @@ let sharedCompositorFormat = null;
3484
4387
  const frameCache = new VisionFrameCache();
3485
4388
  /** The frame each node's child texture holds (drawn on its last call). */
3486
4389
  const heldFrameByNode = /* @__PURE__ */ new Map();
3487
- /** Per node: a copy of the held frame, and the composited matte. */
4390
+ /** Per node: a copy of the held frame, the frame being analysed, and the composited matte. */
3488
4391
  const heldTextures = /* @__PURE__ */ new Map();
4392
+ const analysisTextures = /* @__PURE__ */ new Map();
3489
4393
  const matteTextures = /* @__PURE__ */ new Map();
3490
4394
  /** One tracker per vision node — track ids must not mix across nodes/sources. */
3491
4395
  const objectTrackers = /* @__PURE__ */ new Map();
@@ -3524,6 +4428,7 @@ function nodeTexture(textures, device, key, like, usage, label) {
3524
4428
  return tex;
3525
4429
  }
3526
4430
  const heldTexture = (device, key, like) => nodeTexture(heldTextures, device, key, like, GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING, "vision_held_frame");
4431
+ const analysisTexture = (device, key, like) => nodeTexture(analysisTextures, device, key, like, GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC, "vision_analysis_frame");
3527
4432
  const matteTexture = (device, key, like) => nodeTexture(matteTextures, device, key, like, GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC, "vision_matte");
3528
4433
  /** Everything that decides a frame's results besides the frame itself. */
3529
4434
  function cacheSource(nodeKey, child, op, width, height) {
@@ -3633,6 +4538,12 @@ function normalizePoseKeypoints(result, width, height) {
3633
4538
  }))
3634
4539
  })) };
3635
4540
  }
4541
+ /** Case-insensitive class filter, as the runner's decode applies it. */
4542
+ function matchesClasses(category, classes) {
4543
+ if (!classes) return true;
4544
+ const c = category.toLowerCase();
4545
+ return classes.some((k) => k.toLowerCase() === c);
4546
+ }
3636
4547
  /** Assigns each decoded instance mask to the temporal track whose box center is nearest the mask's centroid. */
3637
4548
  function assignMasksToTracks(masks, tracked) {
3638
4549
  return masks.map((mask) => {
@@ -3825,8 +4736,9 @@ function maskCentroid(mask) {
3825
4736
  };
3826
4737
  }
3827
4738
  const VisionWebGPURenderer = async (args) => {
3828
- const { ctx, encoder, pass, targetView, targetWidth, targetHeight, props, drawChild } = args;
4739
+ const { ctx, encoder: encoder$1, pass, targetView, targetWidth, targetHeight, props, drawChild } = args;
3829
4740
  const { virtualMedia } = props;
4741
+ setVisionDevice(ctx.device);
3830
4742
  const rawOp = virtualMedia?.operation;
3831
4743
  if (rawOp?.op !== "Vision") return;
3832
4744
  const op = rawOp;
@@ -3874,11 +4786,11 @@ const VisionWebGPURenderer = async (args) => {
3874
4786
  const wantsPose = op.enablePose === true || mode === "skeleton";
3875
4787
  const cacheable = !wantsSelfie && !wantsPose && (wantsDetection || wantsSegmentation);
3876
4788
  const source = cacheable ? cacheSource(nodeKeyStr, childMedia, op, targetWidth, targetHeight) : "";
3877
- const keyThresholdAt = (frame) => op.keyBackground === true ? backgroundKeyThreshold(op, compositionFrame - (frameIdx - frame), fps) : void 0;
3878
- const lookup = (frame) => {
3879
- if (!cacheable || frame === void 0) return void 0;
3880
- const hit = frameCache.get(source, frame);
3881
- if (!hit || hit.keyThreshold !== keyThresholdAt(frame)) return void 0;
4789
+ const keyThresholdAt = (frame$1) => op.keyBackground === true ? backgroundKeyThreshold(op, compositionFrame - (frameIdx - frame$1), fps) : void 0;
4790
+ const lookup = (frame$1) => {
4791
+ if (!cacheable || frame$1 === void 0) return void 0;
4792
+ const hit = frameCache.get(source, frame$1);
4793
+ if (!hit || hit.keyThreshold !== keyThresholdAt(frame$1)) return void 0;
3882
4794
  if (wantsSegmentation && visionBundle && !hit.masks) return void 0;
3883
4795
  return hit;
3884
4796
  };
@@ -3889,43 +4801,33 @@ const VisionWebGPURenderer = async (args) => {
3889
4801
  let heldTex;
3890
4802
  if (isMatteMode && hasPreviousFrame && !exact) {
3891
4803
  heldTex = heldTexture(ctx.device, nodeKeyStr, childTex);
3892
- encoder.copyTextureToTexture({ texture: childTex }, { texture: heldTex }, [
4804
+ encoder$1.copyTextureToTexture({ texture: childTex }, { texture: heldTex }, [
3893
4805
  targetWidth,
3894
4806
  targetHeight,
3895
4807
  1
3896
4808
  ]);
3897
4809
  }
4810
+ let frame;
3898
4811
  let framePixels = null;
3899
4812
  if (hasPreviousFrame && !cached) {
3900
- const unalignedBytesPerRow = targetWidth * 4;
3901
- const bytesPerRow = Math.ceil(unalignedBytesPerRow / 256) * 256;
3902
- const bufferSize = bytesPerRow * targetHeight;
3903
- const stagingBuffer = ctx.device.createBuffer({
3904
- size: bufferSize,
3905
- usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
3906
- label: "vision_frame_staging"
3907
- });
3908
- const readbackEncoder = ctx.device.createCommandEncoder({ label: "vision_readback_encoder" });
3909
- readbackEncoder.copyTextureToBuffer({ texture: childTex }, {
3910
- buffer: stagingBuffer,
3911
- bytesPerRow,
3912
- rowsPerImage: targetHeight
3913
- }, [
4813
+ const analysis = analysisTexture(ctx.device, nodeKeyStr, childTex);
4814
+ const copy = ctx.device.createCommandEncoder({ label: "vision_analysis_copy" });
4815
+ copy.copyTextureToTexture({ texture: childTex }, { texture: analysis }, [
3914
4816
  targetWidth,
3915
4817
  targetHeight,
3916
4818
  1
3917
4819
  ]);
3918
- ctx.device.queue.submit([readbackEncoder.finish()]);
3919
- await stagingBuffer.mapAsync(GPUMapMode.READ);
3920
- const mappedBytes = new Uint8Array(stagingBuffer.getMappedRange());
3921
- framePixels = new Uint8ClampedArray(targetWidth * targetHeight * 4);
3922
- if (bytesPerRow === unalignedBytesPerRow) framePixels.set(mappedBytes.subarray(0, framePixels.length));
3923
- else for (let y = 0; y < targetHeight; y++) framePixels.set(mappedBytes.subarray(y * bytesPerRow, y * bytesPerRow + unalignedBytesPerRow), y * unalignedBytesPerRow);
3924
- stagingBuffer.unmap();
3925
- stagingBuffer.destroy();
4820
+ ctx.device.queue.submit([copy.finish()]);
4821
+ frame = {
4822
+ texture: analysis,
4823
+ device: ctx.device,
4824
+ width: targetWidth,
4825
+ height: targetHeight
4826
+ };
4827
+ if (isMatteMode && op.keyBackground === true) framePixels = (await readTexturePixels(frame)).data;
3926
4828
  }
3927
4829
  const childView = childTex.createView();
3928
- ctx.renderer.beginFrame(encoder, childView, {
4830
+ ctx.renderer.beginFrame(encoder$1, childView, {
3929
4831
  r: 0,
3930
4832
  g: 0,
3931
4833
  b: 0,
@@ -3955,13 +4857,9 @@ const VisionWebGPURenderer = async (args) => {
3955
4857
  frame: frameIdx
3956
4858
  });
3957
4859
  }
3958
- } else if (framePixels) {
4860
+ } else if (frame) {
3959
4861
  const runner = getSharedRunner(op);
3960
- const image = {
3961
- data: framePixels,
3962
- width: targetWidth,
3963
- height: targetHeight
3964
- };
4862
+ const image = frame;
3965
4863
  let detections = [];
3966
4864
  if (wantsDetection) {
3967
4865
  detections = await runner.detect(image);
@@ -3973,8 +4871,15 @@ const VisionWebGPURenderer = async (args) => {
3973
4871
  rawDetections: detections
3974
4872
  });
3975
4873
  }
4874
+ let subjectMasks = [];
3976
4875
  if (wantsSegmentation) {
3977
- frameMasks = assignMasksToTracks((await runner.segment(image)).masks, frameObjects);
4876
+ const subjectFromAllClasses = isMatteMode && op.matteSource !== "selfie" && op.classes !== void 0;
4877
+ const segRes = await (subjectFromAllClasses ? getSharedRunner({
4878
+ ...op,
4879
+ classes: void 0
4880
+ }) : runner).segment(image);
4881
+ frameMasks = assignMasksToTracks(subjectFromAllClasses ? segRes.masks.filter((m) => matchesClasses(m.category, op.classes)) : segRes.masks, frameObjects);
4882
+ subjectMasks = subjectFromAllClasses ? segRes.masks : frameMasks;
3978
4883
  visionBundle?.setMaskResult(frameIdx, frameMasks);
3979
4884
  }
3980
4885
  if (wantsSelfie) {
@@ -3986,9 +4891,9 @@ const VisionWebGPURenderer = async (args) => {
3986
4891
  visionBundle?.setPoseResult(frameIdx, currentPoseRes);
3987
4892
  }
3988
4893
  if (isMatteMode) {
3989
- subject = selectSubject(op.matteSource === "selfie" ? personMatteAsMask(personMatte) : mergeSubjectMask(frameMasks.length > 0 ? frameMasks : visionBundle?.getMaskResult(frameIdx) ?? []), nodeKeyStr, frameIdx);
4894
+ subject = selectSubject(op.matteSource === "selfie" ? personMatteAsMask(personMatte) : mergeSubjectMask(subjectMasks.length > 0 ? subjectMasks : visionBundle?.getMaskResult(frameIdx) ?? [], { classes: op.classes }), nodeKeyStr, frameIdx);
3990
4895
  const threshold = keyThresholdAt(heldFrame ?? frameIdx);
3991
- if (subject && threshold !== void 0) subject = {
4896
+ if (subject && threshold !== void 0 && framePixels) subject = {
3992
4897
  ...subject,
3993
4898
  mask: growMaskIntoForeground(subject.mask, subject.width, subject.height, framePixels, threshold, op.featherRadius)
3994
4899
  };
@@ -4004,7 +4909,7 @@ const VisionWebGPURenderer = async (args) => {
4004
4909
  if (isMatteMode) {
4005
4910
  const picture = exact ? childTex : heldTex;
4006
4911
  if (!subject || !picture) {
4007
- ctx.renderer.beginFrame(encoder, targetView, {
4912
+ ctx.renderer.beginFrame(encoder$1, targetView, {
4008
4913
  r: 0,
4009
4914
  g: 0,
4010
4915
  b: 0,
@@ -4018,9 +4923,9 @@ const VisionWebGPURenderer = async (args) => {
4018
4923
  }
4019
4924
  const maskTex = sharedCompositor.uploadMask(nodeKeyStr, subject.mask, subject.width, subject.height);
4020
4925
  const matteTex = matteTexture(ctx.device, nodeKeyStr, childTex);
4021
- sharedCompositor.draw(encoder, matteTex.createView(), picture, maskTex, mode, mode === "crop" ? maskBounds(subject) ?? void 0 : void 0);
4926
+ sharedCompositor.draw(encoder$1, matteTex.createView(), picture, maskTex, mode, mode === "crop" ? maskBounds(subject) ?? void 0 : void 0);
4022
4927
  visionBundle?.setStencilTexture(matteTex);
4023
- const outPass$1 = ctx.renderer.beginFrame(encoder, targetView, {
4928
+ const outPass$1 = ctx.renderer.beginFrame(encoder$1, targetView, {
4024
4929
  r: 0,
4025
4930
  g: 0,
4026
4931
  b: 0,
@@ -4043,7 +4948,7 @@ const VisionWebGPURenderer = async (args) => {
4043
4948
  width: targetWidth,
4044
4949
  height: targetHeight
4045
4950
  });
4046
- const skelPass = ctx.renderer.beginFrame(encoder, targetView, {
4951
+ const skelPass = ctx.renderer.beginFrame(encoder$1, targetView, {
4047
4952
  r: 0,
4048
4953
  g: 0,
4049
4954
  b: 0,
@@ -4060,7 +4965,7 @@ const VisionWebGPURenderer = async (args) => {
4060
4965
  }
4061
4966
  }
4062
4967
  if (mode === "boxes" || mode === "tracking") {
4063
- const outPass$1 = ctx.renderer.beginFrame(encoder, targetView, {
4968
+ const outPass$1 = ctx.renderer.beginFrame(encoder$1, targetView, {
4064
4969
  r: 0,
4065
4970
  g: 0,
4066
4971
  b: 0,
@@ -4127,7 +5032,7 @@ const VisionWebGPURenderer = async (args) => {
4127
5032
  outPass$1.end();
4128
5033
  return;
4129
5034
  }
4130
- const outPass = ctx.renderer.beginFrame(encoder, targetView, {
5035
+ const outPass = ctx.renderer.beginFrame(encoder$1, targetView, {
4131
5036
  r: 0,
4132
5037
  g: 0,
4133
5038
  b: 0,
@@ -4159,4 +5064,4 @@ var renderers_default = defineRenderer({ WebGPURenderer: VisionWebGPURenderer })
4159
5064
 
4160
5065
  //#endregion
4161
5066
  export { PoseSkeletonRenderer as a, TemporalObjectTracker as c, VisionRunner as d, analyzeSequence as f, COCO_CLASSES as h, COCO17_KEYPOINT_NAMES as i, VisionBundle as l, pinNodeToObject as m, COCO17_BONES as n, SegmentationTexturePool as o, pinNodeToLandmark as p, COCO17_KEYPOINTS as r, SpatialLandmarkTransformer as s, renderers_default as t, VisionNode as u };
4162
- //# sourceMappingURL=renderer-dc39dBVA.mjs.map
5067
+ //# sourceMappingURL=renderer-CE7fwGgw.mjs.map