@pose-tracker/pose-estimation-web 0.2.0 → 0.3.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.
- package/README.md +6 -2
- package/dist/index.cjs +275 -36
- package/dist/index.d.cts +53 -2
- package/dist/index.d.ts +53 -2
- package/dist/index.js +273 -37
- package/dist/pose-tracker.global.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,6 +41,10 @@ await pt.analyze(); // re-run on the same image
|
|
|
41
41
|
| `{ type: 'video', src }` | File / blob URL / `<video>` — stream while playing |
|
|
42
42
|
| `{ type: 'image', src }` | File / blob URL / `<img>` / ImageBitmap — one shot (+ `analyze()`) |
|
|
43
43
|
|
|
44
|
+
Full tables (RN parity, CORS, React): monorepo
|
|
45
|
+
[`docs/MEDIA_SOURCES.md`](../../docs/MEDIA_SOURCES.md) · public
|
|
46
|
+
https://docs.posetracker.com/media-sources
|
|
47
|
+
|
|
44
48
|
## Script tag (CDN)
|
|
45
49
|
|
|
46
50
|
The IIFE build exposes a **`PoseTracker`** global (`PoseTracker.createPoseTracker`, …).
|
|
@@ -57,7 +61,7 @@ The IIFE build exposes a **`PoseTracker`** global (`PoseTracker.createPoseTracke
|
|
|
57
61
|
<!-- 1) TensorFlow.js (peer — required before PoseTracker) -->
|
|
58
62
|
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.22.0/dist/tf.min.js"></script>
|
|
59
63
|
<!-- 2) PoseTracker IIFE (jsDelivr / unpkg both work) -->
|
|
60
|
-
<script src="https://cdn.jsdelivr.net/npm/@pose-tracker/pose-estimation-web@0.
|
|
64
|
+
<script src="https://cdn.jsdelivr.net/npm/@pose-tracker/pose-estimation-web@0.3.0/dist/pose-tracker.global.js"></script>
|
|
61
65
|
</head>
|
|
62
66
|
<body>
|
|
63
67
|
<div id="root" style="width: 100%; height: 100vh; background: #111"></div>
|
|
@@ -79,7 +83,7 @@ The IIFE build exposes a **`PoseTracker`** global (`PoseTracker.createPoseTracke
|
|
|
79
83
|
Equivalent unpkg URL:
|
|
80
84
|
|
|
81
85
|
```text
|
|
82
|
-
https://unpkg.com/@pose-tracker/pose-estimation-web@0.
|
|
86
|
+
https://unpkg.com/@pose-tracker/pose-estimation-web@0.3.0/dist/pose-tracker.global.js
|
|
83
87
|
```
|
|
84
88
|
|
|
85
89
|
Omitting `/dist/...` also works — `package.json` `jsdelivr` / `unpkg` fields point at the IIFE.
|
package/dist/index.cjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
var jsSha256 = require('js-sha256');
|
|
4
4
|
|
|
5
5
|
// src/version.ts
|
|
6
|
-
var SDK_VERSION = "0.
|
|
6
|
+
var SDK_VERSION = "0.3.0";
|
|
7
7
|
var SDK_NAME = "posetracker-web";
|
|
8
8
|
|
|
9
9
|
// src/api/configure.ts
|
|
@@ -24,7 +24,8 @@ async function configure(apiToken, options = {}) {
|
|
|
24
24
|
targetPlatform: "web",
|
|
25
25
|
poseModelProfile: options.poseModelProfile ?? "AdaptiveChoice",
|
|
26
26
|
locale: options.locale,
|
|
27
|
-
localVersions: options.localVersions
|
|
27
|
+
localVersions: options.localVersions,
|
|
28
|
+
engineChannel: options.engine === "v4" ? "v4" : "v3"
|
|
28
29
|
};
|
|
29
30
|
let response;
|
|
30
31
|
try {
|
|
@@ -531,6 +532,10 @@ function waitForImageReady(img, timeoutMs = 15e3) {
|
|
|
531
532
|
resolve();
|
|
532
533
|
return;
|
|
533
534
|
}
|
|
535
|
+
if (img.complete) {
|
|
536
|
+
reject(new Error("Image failed to decode (naturalWidth=0)"));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
534
539
|
const onReady = () => {
|
|
535
540
|
cleanup();
|
|
536
541
|
resolve();
|
|
@@ -614,6 +619,19 @@ function findExerciseByIdOrAlias(exerciseId, exercises) {
|
|
|
614
619
|
return exercises.find((e) => resolveExerciseName(e.id) === canonical);
|
|
615
620
|
}
|
|
616
621
|
|
|
622
|
+
// src/engineChannel.ts
|
|
623
|
+
var V4_ONLY_EXERCISE_IDS = [
|
|
624
|
+
"shoulder_roll",
|
|
625
|
+
"shoulder_deep_breath",
|
|
626
|
+
"chair_forward_fold"
|
|
627
|
+
];
|
|
628
|
+
function normalizeEngineChannel(raw) {
|
|
629
|
+
return raw === "v4" ? "v4" : "v3";
|
|
630
|
+
}
|
|
631
|
+
function requiresEngineV4(exerciseId) {
|
|
632
|
+
return V4_ONLY_EXERCISE_IDS.includes(exerciseId);
|
|
633
|
+
}
|
|
634
|
+
|
|
617
635
|
// src/events/classicMessage.ts
|
|
618
636
|
var ANGLE_RELATIONS = {
|
|
619
637
|
left_knee: "from_hip_to_ankle",
|
|
@@ -1069,6 +1087,23 @@ var BLAZE_TO_COCO = {
|
|
|
1069
1087
|
left_ankle: "left_ankle",
|
|
1070
1088
|
right_ankle: "right_ankle"
|
|
1071
1089
|
};
|
|
1090
|
+
var FACE_EXTRA_NAMES = /* @__PURE__ */ new Set([
|
|
1091
|
+
"left_eye_inner",
|
|
1092
|
+
"left_eye_outer",
|
|
1093
|
+
"right_eye_inner",
|
|
1094
|
+
"right_eye_outer",
|
|
1095
|
+
"mouth_left",
|
|
1096
|
+
"mouth_right"
|
|
1097
|
+
]);
|
|
1098
|
+
var BLAZE_MAX_SIDE = 1280;
|
|
1099
|
+
function toSourcePixels(x, y, scratchW, scratchH, srcW, srcH, maxCoord) {
|
|
1100
|
+
if (maxCoord <= 1.5 && scratchW > 2) {
|
|
1101
|
+
return { x: x * srcW, y: y * srcH };
|
|
1102
|
+
}
|
|
1103
|
+
const sx = scratchW / srcW;
|
|
1104
|
+
const sy = scratchH / srcH;
|
|
1105
|
+
return { x: sx > 0 ? x / sx : x, y: sy > 0 ? y / sy : y };
|
|
1106
|
+
}
|
|
1072
1107
|
var BlazePoseAdapter = class {
|
|
1073
1108
|
constructor(_ctx) {
|
|
1074
1109
|
this.modelId = "blazepose";
|
|
@@ -1089,7 +1124,7 @@ var BlazePoseAdapter = class {
|
|
|
1089
1124
|
{
|
|
1090
1125
|
runtime: "tfjs",
|
|
1091
1126
|
modelType: this.modelType,
|
|
1092
|
-
enableSmoothing:
|
|
1127
|
+
enableSmoothing: false
|
|
1093
1128
|
}
|
|
1094
1129
|
);
|
|
1095
1130
|
}
|
|
@@ -1097,8 +1132,26 @@ var BlazePoseAdapter = class {
|
|
|
1097
1132
|
if (!this.detector) throw new Error("BlazePose adapter not loaded");
|
|
1098
1133
|
const size = blazeInputSize(input);
|
|
1099
1134
|
if (!(size.vw > 0 && size.vh > 0)) return null;
|
|
1135
|
+
if (options.temporalSmooth === false) this.resetTemporal();
|
|
1136
|
+
let estimateTarget = input;
|
|
1137
|
+
let scratch = null;
|
|
1138
|
+
let scratchW = size.vw;
|
|
1139
|
+
let scratchH = size.vh;
|
|
1140
|
+
if (typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement || typeof HTMLVideoElement !== "undefined" && input instanceof HTMLVideoElement || typeof HTMLCanvasElement !== "undefined" && input instanceof HTMLCanvasElement) {
|
|
1141
|
+
const fit = Math.min(1, BLAZE_MAX_SIDE / Math.max(size.vw, size.vh));
|
|
1142
|
+
scratchW = Math.max(1, Math.round(size.vw * fit));
|
|
1143
|
+
scratchH = Math.max(1, Math.round(size.vh * fit));
|
|
1144
|
+
scratch = document.createElement("canvas");
|
|
1145
|
+
scratch.width = scratchW;
|
|
1146
|
+
scratch.height = scratchH;
|
|
1147
|
+
const ctx = scratch.getContext("2d");
|
|
1148
|
+
if (ctx) {
|
|
1149
|
+
ctx.drawImage(input, 0, 0, scratchW, scratchH);
|
|
1150
|
+
estimateTarget = scratch;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1100
1153
|
const t0 = performance.now();
|
|
1101
|
-
const poses = await this.detector.estimatePoses(
|
|
1154
|
+
const poses = await this.detector.estimatePoses(estimateTarget, {
|
|
1102
1155
|
flipHorizontal: false,
|
|
1103
1156
|
maxPoses: 1
|
|
1104
1157
|
});
|
|
@@ -1129,16 +1182,22 @@ var BlazePoseAdapter = class {
|
|
|
1129
1182
|
const vh = size.vh || 1;
|
|
1130
1183
|
const dispW = options.displayWidth || 1;
|
|
1131
1184
|
const dispH = options.displayHeight || 1;
|
|
1185
|
+
let maxCoord = 0;
|
|
1186
|
+
for (const kp of pose.keypoints) {
|
|
1187
|
+
maxCoord = Math.max(maxCoord, Math.abs(kp.x || 0), Math.abs(kp.y || 0));
|
|
1188
|
+
}
|
|
1132
1189
|
const byName = /* @__PURE__ */ new Map();
|
|
1190
|
+
const extrasRaw = [];
|
|
1133
1191
|
for (const kp of pose.keypoints) {
|
|
1134
1192
|
const name = String(kp.name || "").toLowerCase();
|
|
1193
|
+
const score = typeof kp.score === "number" ? kp.score : 0;
|
|
1194
|
+
const src = toSourcePixels(kp.x, kp.y, scratchW, scratchH, vw, vh, maxCoord);
|
|
1135
1195
|
const coco = BLAZE_TO_COCO[name];
|
|
1136
|
-
if (
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
y:
|
|
1140
|
-
|
|
1141
|
-
});
|
|
1196
|
+
if (coco) {
|
|
1197
|
+
byName.set(coco, { x: src.x, y: src.y, score });
|
|
1198
|
+
} else if (FACE_EXTRA_NAMES.has(name)) {
|
|
1199
|
+
extrasRaw.push({ name, x: src.x, y: src.y, score });
|
|
1200
|
+
}
|
|
1142
1201
|
}
|
|
1143
1202
|
const videoKeypoints = COCO_KEYPOINT_NAMES.map((name) => {
|
|
1144
1203
|
const k = byName.get(name);
|
|
@@ -1167,6 +1226,19 @@ var BlazePoseAdapter = class {
|
|
|
1167
1226
|
score: k.score
|
|
1168
1227
|
};
|
|
1169
1228
|
});
|
|
1229
|
+
for (const extra of extrasRaw) {
|
|
1230
|
+
const dx = extra.x * scale + ox;
|
|
1231
|
+
const dy = extra.y * scale + oy;
|
|
1232
|
+
let nx = dispW > 0 ? dx / dispW : 0;
|
|
1233
|
+
const ny = dispH > 0 ? dy / dispH : 0;
|
|
1234
|
+
if (options.facingMode === "user") nx = 1 - nx;
|
|
1235
|
+
keypoints.push({
|
|
1236
|
+
name: extra.name,
|
|
1237
|
+
x: Math.min(1, Math.max(0, nx)),
|
|
1238
|
+
y: Math.min(1, Math.max(0, ny)),
|
|
1239
|
+
score: extra.score
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1170
1242
|
return {
|
|
1171
1243
|
keypoints,
|
|
1172
1244
|
score: scoreSum / 17,
|
|
@@ -1182,6 +1254,13 @@ var BlazePoseAdapter = class {
|
|
|
1182
1254
|
}
|
|
1183
1255
|
};
|
|
1184
1256
|
}
|
|
1257
|
+
/** Clear cached ROI / landmark filters (call when the still changes). */
|
|
1258
|
+
resetTemporal() {
|
|
1259
|
+
try {
|
|
1260
|
+
this.detector?.reset?.();
|
|
1261
|
+
} catch {
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1185
1264
|
dispose() {
|
|
1186
1265
|
try {
|
|
1187
1266
|
this.detector?.dispose?.();
|
|
@@ -1205,13 +1284,14 @@ function inputSize(input) {
|
|
|
1205
1284
|
return { vw: input.width || 0, vh: input.height || 0 };
|
|
1206
1285
|
}
|
|
1207
1286
|
var INPUT_SIZE = 192;
|
|
1208
|
-
var SMOOTH_ALPHA = 0.
|
|
1287
|
+
var SMOOTH_ALPHA = 0.45;
|
|
1288
|
+
var STILL_TARGET_FILL = 0.52;
|
|
1209
1289
|
function modelNormToVideo(xNorm, yNorm, lb) {
|
|
1210
1290
|
const xSq = xNorm * INPUT_SIZE;
|
|
1211
1291
|
const ySq = yNorm * INPUT_SIZE;
|
|
1212
1292
|
return {
|
|
1213
|
-
x: (xSq - lb.offsetX) * (lb.vw / lb.drawW),
|
|
1214
|
-
y: (ySq - lb.offsetY) * (lb.vh / lb.drawH)
|
|
1293
|
+
x: (xSq - lb.offsetX) * (lb.vw / lb.drawW) + (lb.originX ?? 0),
|
|
1294
|
+
y: (ySq - lb.offsetY) * (lb.vh / lb.drawH) + (lb.originY ?? 0)
|
|
1215
1295
|
};
|
|
1216
1296
|
}
|
|
1217
1297
|
function videoToCover(vx, vy, dispW, dispH, lb) {
|
|
@@ -1228,6 +1308,18 @@ var MoveNetGraphAdapter = class {
|
|
|
1228
1308
|
this.offscreen = null;
|
|
1229
1309
|
this.offCtx = null;
|
|
1230
1310
|
this.smoothed = null;
|
|
1311
|
+
this.lastVideoKps = null;
|
|
1312
|
+
this.stillPass = 0;
|
|
1313
|
+
this.inferLb = {
|
|
1314
|
+
offsetX: 0,
|
|
1315
|
+
offsetY: 0,
|
|
1316
|
+
drawW: INPUT_SIZE,
|
|
1317
|
+
drawH: INPUT_SIZE,
|
|
1318
|
+
vw: 1,
|
|
1319
|
+
vh: 1,
|
|
1320
|
+
originX: 0,
|
|
1321
|
+
originY: 0
|
|
1322
|
+
};
|
|
1231
1323
|
this.letterbox = {
|
|
1232
1324
|
offsetX: 0,
|
|
1233
1325
|
offsetY: 0,
|
|
@@ -1267,20 +1359,81 @@ var MoveNetGraphAdapter = class {
|
|
|
1267
1359
|
if (!this.offCtx) throw new Error("2d context unavailable");
|
|
1268
1360
|
return this.offCtx;
|
|
1269
1361
|
}
|
|
1270
|
-
prepareInput(input) {
|
|
1362
|
+
prepareInput(input, trackRoi) {
|
|
1271
1363
|
const { vw: rawW, vh: rawH } = inputSize(input);
|
|
1272
1364
|
const vw = rawW || 1;
|
|
1273
1365
|
const vh = rawH || 1;
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1366
|
+
let winX = 0;
|
|
1367
|
+
let winY = 0;
|
|
1368
|
+
let winW = vw;
|
|
1369
|
+
let winH = vh;
|
|
1370
|
+
if (trackRoi && this.lastVideoKps && this.stillPass > 0) {
|
|
1371
|
+
const pts = this.lastVideoKps.filter((k) => k.score >= 0.15);
|
|
1372
|
+
if (pts.length >= 4) {
|
|
1373
|
+
let minx = Infinity;
|
|
1374
|
+
let miny = Infinity;
|
|
1375
|
+
let maxx = -Infinity;
|
|
1376
|
+
let maxy = -Infinity;
|
|
1377
|
+
for (const p of pts) {
|
|
1378
|
+
minx = Math.min(minx, p.xPx);
|
|
1379
|
+
miny = Math.min(miny, p.yPx);
|
|
1380
|
+
maxx = Math.max(maxx, p.xPx);
|
|
1381
|
+
maxy = Math.max(maxy, p.yPx);
|
|
1382
|
+
}
|
|
1383
|
+
const bw = Math.max(8, maxx - minx);
|
|
1384
|
+
const bh = Math.max(8, maxy - miny);
|
|
1385
|
+
const cx = (minx + maxx) / 2;
|
|
1386
|
+
const cy = (miny + maxy) / 2;
|
|
1387
|
+
winW = Math.max(bw / STILL_TARGET_FILL, vw * 0.35);
|
|
1388
|
+
winH = Math.max(bh / STILL_TARGET_FILL, vh * 0.35);
|
|
1389
|
+
const imgAspect = vw / vh;
|
|
1390
|
+
if (winW / winH < imgAspect) winW = winH * imgAspect;
|
|
1391
|
+
else winH = winW / imgAspect;
|
|
1392
|
+
winX = cx - winW / 2;
|
|
1393
|
+
winY = cy - winH / 2;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const scale = Math.min(INPUT_SIZE / winW, INPUT_SIZE / winH);
|
|
1397
|
+
const drawW = winW * scale;
|
|
1398
|
+
const drawH = winH * scale;
|
|
1277
1399
|
const offsetX = (INPUT_SIZE - drawW) / 2;
|
|
1278
1400
|
const offsetY = (INPUT_SIZE - drawH) / 2;
|
|
1279
|
-
this.
|
|
1401
|
+
this.inferLb = {
|
|
1402
|
+
offsetX,
|
|
1403
|
+
offsetY,
|
|
1404
|
+
drawW,
|
|
1405
|
+
drawH,
|
|
1406
|
+
vw: winW,
|
|
1407
|
+
vh: winH,
|
|
1408
|
+
originX: winX,
|
|
1409
|
+
originY: winY
|
|
1410
|
+
};
|
|
1411
|
+
this.letterbox = { offsetX: 0, offsetY: 0, drawW: vw, drawH: vh, vw, vh };
|
|
1280
1412
|
const c2d = this.poseCanvasCtx();
|
|
1281
1413
|
c2d.fillStyle = "#000";
|
|
1282
1414
|
c2d.fillRect(0, 0, INPUT_SIZE, INPUT_SIZE);
|
|
1283
|
-
|
|
1415
|
+
const srcX = Math.max(0, winX);
|
|
1416
|
+
const srcY = Math.max(0, winY);
|
|
1417
|
+
const srcR = Math.min(vw, winX + winW);
|
|
1418
|
+
const srcB = Math.min(vh, winY + winH);
|
|
1419
|
+
if (srcR > srcX && srcB > srcY) {
|
|
1420
|
+
const dx = offsetX + (srcX - winX) * (drawW / winW);
|
|
1421
|
+
const dy = offsetY + (srcY - winY) * (drawH / winH);
|
|
1422
|
+
const dw = (srcR - srcX) * (drawW / winW);
|
|
1423
|
+
const dh = (srcB - srcY) * (drawH / winH);
|
|
1424
|
+
c2d.drawImage(
|
|
1425
|
+
input,
|
|
1426
|
+
srcX,
|
|
1427
|
+
srcY,
|
|
1428
|
+
srcR - srcX,
|
|
1429
|
+
srcB - srcY,
|
|
1430
|
+
dx,
|
|
1431
|
+
dy,
|
|
1432
|
+
dw,
|
|
1433
|
+
dh
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
this.stillPass += 1;
|
|
1284
1437
|
return this.offscreen;
|
|
1285
1438
|
}
|
|
1286
1439
|
smooth(raw) {
|
|
@@ -1301,7 +1454,9 @@ var MoveNetGraphAdapter = class {
|
|
|
1301
1454
|
const size = inputSize(input);
|
|
1302
1455
|
if (!(size.vw > 0 && size.vh > 0)) return null;
|
|
1303
1456
|
const t0 = performance.now();
|
|
1304
|
-
const
|
|
1457
|
+
const isStill = typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement;
|
|
1458
|
+
const trackRoi = options.temporalSmooth !== false && isStill;
|
|
1459
|
+
const canvas = this.prepareInput(input, trackRoi);
|
|
1305
1460
|
const tensorIn = this.tf.tidy(
|
|
1306
1461
|
() => this.tf.expandDims(this.tf.browser.fromPixels(canvas), 0)
|
|
1307
1462
|
);
|
|
@@ -1312,7 +1467,8 @@ var MoveNetGraphAdapter = class {
|
|
|
1312
1467
|
if (Array.isArray(out)) out.forEach((t) => t.dispose());
|
|
1313
1468
|
else out.dispose();
|
|
1314
1469
|
const inferenceMs = performance.now() - t0;
|
|
1315
|
-
const lb = this.
|
|
1470
|
+
const lb = this.inferLb;
|
|
1471
|
+
const coverLb = this.letterbox;
|
|
1316
1472
|
const dispW = options.displayWidth || 1;
|
|
1317
1473
|
const dispH = options.displayHeight || 1;
|
|
1318
1474
|
const drawRaw = [];
|
|
@@ -1330,7 +1486,8 @@ var MoveNetGraphAdapter = class {
|
|
|
1330
1486
|
});
|
|
1331
1487
|
scoreSum += score;
|
|
1332
1488
|
}
|
|
1333
|
-
const sm = this.smooth(drawRaw);
|
|
1489
|
+
const sm = options.temporalSmooth === false ? drawRaw : this.smooth(drawRaw);
|
|
1490
|
+
this.lastVideoKps = sm.map((k) => ({ xPx: k.xPx, yPx: k.yPx, score: k.score }));
|
|
1334
1491
|
const videoKeypoints = sm.map((k, i) => ({
|
|
1335
1492
|
name: COCO_KEYPOINT_NAMES[i],
|
|
1336
1493
|
xPx: k.xPx,
|
|
@@ -1338,7 +1495,7 @@ var MoveNetGraphAdapter = class {
|
|
|
1338
1495
|
score: k.score
|
|
1339
1496
|
}));
|
|
1340
1497
|
const keypoints = videoKeypoints.map((k) => {
|
|
1341
|
-
const d = videoToCover(k.xPx, k.yPx, dispW, dispH,
|
|
1498
|
+
const d = videoToCover(k.xPx, k.yPx, dispW, dispH, coverLb);
|
|
1342
1499
|
let nx = dispW > 0 ? d.x / dispW : 0;
|
|
1343
1500
|
const ny = dispH > 0 ? d.y / dispH : 0;
|
|
1344
1501
|
if (options.facingMode === "user") nx = 1 - nx;
|
|
@@ -1355,9 +1512,15 @@ var MoveNetGraphAdapter = class {
|
|
|
1355
1512
|
score: scoreSum / 17,
|
|
1356
1513
|
inferenceMs,
|
|
1357
1514
|
videoKeypoints,
|
|
1358
|
-
letterbox:
|
|
1515
|
+
letterbox: coverLb
|
|
1359
1516
|
};
|
|
1360
1517
|
}
|
|
1518
|
+
/** Clear EMA state (call when the still / clip changes). */
|
|
1519
|
+
resetTemporal() {
|
|
1520
|
+
this.smoothed = null;
|
|
1521
|
+
this.lastVideoKps = null;
|
|
1522
|
+
this.stillPass = 0;
|
|
1523
|
+
}
|
|
1361
1524
|
dispose() {
|
|
1362
1525
|
try {
|
|
1363
1526
|
this.model?.dispose();
|
|
@@ -1365,6 +1528,8 @@ var MoveNetGraphAdapter = class {
|
|
|
1365
1528
|
}
|
|
1366
1529
|
this.model = null;
|
|
1367
1530
|
this.smoothed = null;
|
|
1531
|
+
this.lastVideoKps = null;
|
|
1532
|
+
this.stillPass = 0;
|
|
1368
1533
|
this.offscreen = null;
|
|
1369
1534
|
this.offCtx = null;
|
|
1370
1535
|
}
|
|
@@ -1985,7 +2150,11 @@ var PoseTrackerClient = class {
|
|
|
1985
2150
|
if (this.poseSource.type === "camera" && this.poseSource.facingMode) {
|
|
1986
2151
|
this.opts.facingMode = this.poseSource.facingMode;
|
|
1987
2152
|
}
|
|
2153
|
+
if (this.adapter?.resetTemporal) {
|
|
2154
|
+
this.adapter.resetTemporal();
|
|
2155
|
+
}
|
|
1988
2156
|
if (!this.shell) return;
|
|
2157
|
+
await this.ensureAdapter();
|
|
1989
2158
|
await this.ensureSource();
|
|
1990
2159
|
if (wasRunning || this.poseSource.type === "image") {
|
|
1991
2160
|
if (this.poseSource.type === "image") this.imageShotPending = true;
|
|
@@ -2097,7 +2266,30 @@ var PoseTrackerClient = class {
|
|
|
2097
2266
|
return this.configurePromise;
|
|
2098
2267
|
}
|
|
2099
2268
|
getAvailableExercises() {
|
|
2100
|
-
|
|
2269
|
+
if (this.mode !== "full-engine") return [];
|
|
2270
|
+
if (normalizeEngineChannel(this.opts.engine) === "v4") {
|
|
2271
|
+
const listed = this.engine?.listExercises?.() ?? [];
|
|
2272
|
+
if (listed.length) return listed.map((e) => this.v4ExerciseConfig(e));
|
|
2273
|
+
}
|
|
2274
|
+
return this.manifest?.exercises ?? [];
|
|
2275
|
+
}
|
|
2276
|
+
getEngineChannel() {
|
|
2277
|
+
return normalizeEngineChannel(this.opts.engine);
|
|
2278
|
+
}
|
|
2279
|
+
v4ExerciseConfig(entry) {
|
|
2280
|
+
const type = entry.type === "static" ? "static" : "dynamic";
|
|
2281
|
+
return {
|
|
2282
|
+
id: entry.id,
|
|
2283
|
+
name: entry.displayName || entry.id,
|
|
2284
|
+
type,
|
|
2285
|
+
movement: {
|
|
2286
|
+
name: entry.id,
|
|
2287
|
+
type,
|
|
2288
|
+
scale_acceptance: {},
|
|
2289
|
+
movement_steps: [],
|
|
2290
|
+
movement_initial_posture: null
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2101
2293
|
}
|
|
2102
2294
|
getAvailableCustomExercises() {
|
|
2103
2295
|
if (this.mode !== "full-engine" || !this.engine?.listCustomExercises) return [];
|
|
@@ -2133,6 +2325,28 @@ var PoseTrackerClient = class {
|
|
|
2133
2325
|
throw new Error(FREE_PLAN_FEATURES_MESSAGE);
|
|
2134
2326
|
}
|
|
2135
2327
|
}
|
|
2328
|
+
if (requiresEngineV4(exerciseId) && normalizeEngineChannel(this.opts.engine) !== "v4") {
|
|
2329
|
+
const message = `Exercise '${exerciseId}' requires engine: 'v4'`;
|
|
2330
|
+
this.reportError({ type: "error", code: "invalid_exercise", message });
|
|
2331
|
+
throw new Error(message);
|
|
2332
|
+
}
|
|
2333
|
+
if (normalizeEngineChannel(this.opts.engine) === "v4") {
|
|
2334
|
+
const listed = this.engine.listExercises?.() ?? [];
|
|
2335
|
+
const v4Hit = listed.find((e) => e.id === exerciseId);
|
|
2336
|
+
if (v4Hit) {
|
|
2337
|
+
this.beginEngineSession(this.v4ExerciseConfig(v4Hit), options);
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
const customs = this.getAvailableCustomExercises();
|
|
2341
|
+
const custom = customs.find((e) => e.id === exerciseId) ?? findExerciseByIdOrAlias(exerciseId, customs);
|
|
2342
|
+
if (custom) {
|
|
2343
|
+
this.startCustomExercise(custom, options);
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
const message = `Exercise '${exerciseId}' is not available in V4 engine`;
|
|
2347
|
+
this.reportError({ type: "error", code: "invalid_exercise", message });
|
|
2348
|
+
throw new Error(message);
|
|
2349
|
+
}
|
|
2136
2350
|
const available = this.getAvailableExercises();
|
|
2137
2351
|
const exercise = findExerciseByIdOrAlias(exerciseId, available);
|
|
2138
2352
|
if (!exercise) {
|
|
@@ -2146,8 +2360,12 @@ var PoseTrackerClient = class {
|
|
|
2146
2360
|
this.reportError({ type: "error", code: "invalid_exercise", message });
|
|
2147
2361
|
throw new Error(message);
|
|
2148
2362
|
}
|
|
2363
|
+
this.beginEngineSession(exercise, options);
|
|
2364
|
+
}
|
|
2365
|
+
beginEngineSession(exercise, options) {
|
|
2366
|
+
if (!this.engine) return;
|
|
2149
2367
|
this.stopExercise();
|
|
2150
|
-
this.currentExerciseId =
|
|
2368
|
+
this.currentExerciseId = exercise.id;
|
|
2151
2369
|
this.keypointsSuppressionLogged = false;
|
|
2152
2370
|
this.session = this.engine.createSession(
|
|
2153
2371
|
{
|
|
@@ -2514,13 +2732,19 @@ var PoseTrackerClient = class {
|
|
|
2514
2732
|
if (!this.adapter || !this.shell || !this.estimateInput) return null;
|
|
2515
2733
|
if (!this.frameReady(this.estimateInput)) return null;
|
|
2516
2734
|
const input = this.estimateInput;
|
|
2517
|
-
const
|
|
2518
|
-
const
|
|
2735
|
+
const clientW = this.shell.canvas.clientWidth || this.shell.root.clientWidth || 1;
|
|
2736
|
+
const clientH = this.shell.canvas.clientHeight || this.shell.root.clientHeight || 1;
|
|
2737
|
+
const media = mediaSize(input);
|
|
2738
|
+
const useMediaSpace = this.poseSource.type === "image" && media.width > 0 && media.height > 0;
|
|
2739
|
+
const dispW = useMediaSpace ? media.width : clientW;
|
|
2740
|
+
const dispH = useMediaSpace ? media.height : clientH;
|
|
2519
2741
|
const facing = this.poseSource.type === "camera" ? facingFromOptions(this.opts) : "environment";
|
|
2742
|
+
const stillTracked = this.poseSource.type === "image" && this.opts.stillMode === "tracked";
|
|
2520
2743
|
const result = await this.adapter.estimate(input, {
|
|
2521
2744
|
facingMode: facing,
|
|
2522
2745
|
displayWidth: dispW,
|
|
2523
|
-
displayHeight: dispH
|
|
2746
|
+
displayHeight: dispH,
|
|
2747
|
+
temporalSmooth: this.poseSource.type === "camera" || this.poseSource.type === "video" || stillTracked
|
|
2524
2748
|
});
|
|
2525
2749
|
if (!result) return null;
|
|
2526
2750
|
const ctx = this.shell.canvas.getContext("2d");
|
|
@@ -2528,12 +2752,12 @@ var PoseTrackerClient = class {
|
|
|
2528
2752
|
const mapped = mapVideoKeypointsToDisplay(
|
|
2529
2753
|
result.videoKeypoints,
|
|
2530
2754
|
result.letterbox,
|
|
2531
|
-
|
|
2532
|
-
|
|
2755
|
+
clientW,
|
|
2756
|
+
clientH
|
|
2533
2757
|
);
|
|
2534
|
-
drawSkeleton(ctx, mapped,
|
|
2758
|
+
drawSkeleton(ctx, mapped, clientW, clientH, true, this.skeletonDef);
|
|
2535
2759
|
} else if (ctx && this.opts.drawSkeleton === false) {
|
|
2536
|
-
ctx.clearRect(0, 0,
|
|
2760
|
+
ctx.clearRect(0, 0, clientW, clientH);
|
|
2537
2761
|
}
|
|
2538
2762
|
const pose = {
|
|
2539
2763
|
keypoints: result.keypoints,
|
|
@@ -2576,7 +2800,13 @@ var PoseTrackerClient = class {
|
|
|
2576
2800
|
return pose;
|
|
2577
2801
|
}
|
|
2578
2802
|
async loop(token) {
|
|
2579
|
-
if (token !== this.loopToken || !this.running || !this.
|
|
2803
|
+
if (token !== this.loopToken || !this.running || !this.shell || this.disposed) {
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
if (!this.adapter) {
|
|
2807
|
+
requestAnimationFrame(() => {
|
|
2808
|
+
void this.loop(token);
|
|
2809
|
+
});
|
|
2580
2810
|
return;
|
|
2581
2811
|
}
|
|
2582
2812
|
const input = this.estimateInput;
|
|
@@ -2584,8 +2814,8 @@ var PoseTrackerClient = class {
|
|
|
2584
2814
|
if (shouldInfer) {
|
|
2585
2815
|
this.busy = true;
|
|
2586
2816
|
try {
|
|
2587
|
-
await this.runOneFrame();
|
|
2588
|
-
if (this.poseSource.type === "image") {
|
|
2817
|
+
const pose = await this.runOneFrame();
|
|
2818
|
+
if (this.poseSource.type === "image" && pose) {
|
|
2589
2819
|
this.imageShotPending = false;
|
|
2590
2820
|
this.running = false;
|
|
2591
2821
|
this.busy = false;
|
|
@@ -2594,6 +2824,12 @@ var PoseTrackerClient = class {
|
|
|
2594
2824
|
} catch (err) {
|
|
2595
2825
|
const message = err instanceof Error ? err.message : String(err);
|
|
2596
2826
|
this.emit({ type: "error", code: "internal", message: `inference: ${message}` });
|
|
2827
|
+
if (this.poseSource.type === "image") {
|
|
2828
|
+
this.imageShotPending = false;
|
|
2829
|
+
this.running = false;
|
|
2830
|
+
this.busy = false;
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2597
2833
|
}
|
|
2598
2834
|
this.busy = false;
|
|
2599
2835
|
}
|
|
@@ -2982,6 +3218,7 @@ exports.SDK_VERSION = SDK_VERSION;
|
|
|
2982
3218
|
exports.SkeletonFetchError = SkeletonFetchError;
|
|
2983
3219
|
exports.TrackError = TrackError;
|
|
2984
3220
|
exports.UsageTracker = UsageTracker;
|
|
3221
|
+
exports.V4_ONLY_EXERCISE_IDS = V4_ONLY_EXERCISE_IDS;
|
|
2985
3222
|
exports.configure = configure;
|
|
2986
3223
|
exports.createLocalFileStore = createLocalFileStore;
|
|
2987
3224
|
exports.createLocalKeyValueStore = createLocalKeyValueStore;
|
|
@@ -3000,8 +3237,10 @@ exports.initWebGlBackend = initWebGlBackend;
|
|
|
3000
3237
|
exports.isPaidPlan = isPaidPlan;
|
|
3001
3238
|
exports.mapVideoKeypointsToDisplay = mapVideoKeypointsToDisplay;
|
|
3002
3239
|
exports.mountDomShell = mountDomShell;
|
|
3240
|
+
exports.normalizeEngineChannel = normalizeEngineChannel;
|
|
3003
3241
|
exports.normalizePoseSource = normalizePoseSource;
|
|
3004
3242
|
exports.openCameraStream = openCameraStream;
|
|
3243
|
+
exports.requiresEngineV4 = requiresEngineV4;
|
|
3005
3244
|
exports.resolveExerciseName = resolveExerciseName;
|
|
3006
3245
|
exports.resolveFeatures = resolveFeatures;
|
|
3007
3246
|
exports.resolvePoseDetection = resolvePoseDetection;
|