@pose-tracker/pose-estimation-web 0.2.0 → 0.3.1
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 +303 -36
- package/dist/index.d.cts +78 -2
- package/dist/index.d.ts +78 -2
- package/dist/index.js +298 -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.1";
|
|
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 === "v3" ? "v3" : "v4"
|
|
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,31 @@ 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
|
+
"chair_side_stretch"
|
|
628
|
+
];
|
|
629
|
+
var PRODUCTION_SQUAT_IDS = ["squat", "face_squat"];
|
|
630
|
+
function isProductionSquatId(exerciseId) {
|
|
631
|
+
const id = String(exerciseId || "").trim().toLowerCase();
|
|
632
|
+
return id === "squat" || id === "face_squat";
|
|
633
|
+
}
|
|
634
|
+
function normalizeEngineChannel(raw) {
|
|
635
|
+
return raw === "v3" ? "v3" : "v4";
|
|
636
|
+
}
|
|
637
|
+
function resolveMovementEngine(opts = {}) {
|
|
638
|
+
if (opts.userExercise) return "v4";
|
|
639
|
+
const requested = String(opts.engine || "").trim().toLowerCase();
|
|
640
|
+
if (isProductionSquatId(opts.exercise) && requested !== "v4") return "v3";
|
|
641
|
+
return normalizeEngineChannel(opts.engine);
|
|
642
|
+
}
|
|
643
|
+
function requiresEngineV4(exerciseId) {
|
|
644
|
+
return V4_ONLY_EXERCISE_IDS.includes(exerciseId);
|
|
645
|
+
}
|
|
646
|
+
|
|
617
647
|
// src/events/classicMessage.ts
|
|
618
648
|
var ANGLE_RELATIONS = {
|
|
619
649
|
left_knee: "from_hip_to_ankle",
|
|
@@ -1069,6 +1099,23 @@ var BLAZE_TO_COCO = {
|
|
|
1069
1099
|
left_ankle: "left_ankle",
|
|
1070
1100
|
right_ankle: "right_ankle"
|
|
1071
1101
|
};
|
|
1102
|
+
var FACE_EXTRA_NAMES = /* @__PURE__ */ new Set([
|
|
1103
|
+
"left_eye_inner",
|
|
1104
|
+
"left_eye_outer",
|
|
1105
|
+
"right_eye_inner",
|
|
1106
|
+
"right_eye_outer",
|
|
1107
|
+
"mouth_left",
|
|
1108
|
+
"mouth_right"
|
|
1109
|
+
]);
|
|
1110
|
+
var BLAZE_MAX_SIDE = 1280;
|
|
1111
|
+
function toSourcePixels(x, y, scratchW, scratchH, srcW, srcH, maxCoord) {
|
|
1112
|
+
if (maxCoord <= 1.5 && scratchW > 2) {
|
|
1113
|
+
return { x: x * srcW, y: y * srcH };
|
|
1114
|
+
}
|
|
1115
|
+
const sx = scratchW / srcW;
|
|
1116
|
+
const sy = scratchH / srcH;
|
|
1117
|
+
return { x: sx > 0 ? x / sx : x, y: sy > 0 ? y / sy : y };
|
|
1118
|
+
}
|
|
1072
1119
|
var BlazePoseAdapter = class {
|
|
1073
1120
|
constructor(_ctx) {
|
|
1074
1121
|
this.modelId = "blazepose";
|
|
@@ -1089,7 +1136,7 @@ var BlazePoseAdapter = class {
|
|
|
1089
1136
|
{
|
|
1090
1137
|
runtime: "tfjs",
|
|
1091
1138
|
modelType: this.modelType,
|
|
1092
|
-
enableSmoothing:
|
|
1139
|
+
enableSmoothing: false
|
|
1093
1140
|
}
|
|
1094
1141
|
);
|
|
1095
1142
|
}
|
|
@@ -1097,8 +1144,26 @@ var BlazePoseAdapter = class {
|
|
|
1097
1144
|
if (!this.detector) throw new Error("BlazePose adapter not loaded");
|
|
1098
1145
|
const size = blazeInputSize(input);
|
|
1099
1146
|
if (!(size.vw > 0 && size.vh > 0)) return null;
|
|
1147
|
+
if (options.temporalSmooth === false) this.resetTemporal();
|
|
1148
|
+
let estimateTarget = input;
|
|
1149
|
+
let scratch = null;
|
|
1150
|
+
let scratchW = size.vw;
|
|
1151
|
+
let scratchH = size.vh;
|
|
1152
|
+
if (typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement || typeof HTMLVideoElement !== "undefined" && input instanceof HTMLVideoElement || typeof HTMLCanvasElement !== "undefined" && input instanceof HTMLCanvasElement) {
|
|
1153
|
+
const fit = Math.min(1, BLAZE_MAX_SIDE / Math.max(size.vw, size.vh));
|
|
1154
|
+
scratchW = Math.max(1, Math.round(size.vw * fit));
|
|
1155
|
+
scratchH = Math.max(1, Math.round(size.vh * fit));
|
|
1156
|
+
scratch = document.createElement("canvas");
|
|
1157
|
+
scratch.width = scratchW;
|
|
1158
|
+
scratch.height = scratchH;
|
|
1159
|
+
const ctx = scratch.getContext("2d");
|
|
1160
|
+
if (ctx) {
|
|
1161
|
+
ctx.drawImage(input, 0, 0, scratchW, scratchH);
|
|
1162
|
+
estimateTarget = scratch;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1100
1165
|
const t0 = performance.now();
|
|
1101
|
-
const poses = await this.detector.estimatePoses(
|
|
1166
|
+
const poses = await this.detector.estimatePoses(estimateTarget, {
|
|
1102
1167
|
flipHorizontal: false,
|
|
1103
1168
|
maxPoses: 1
|
|
1104
1169
|
});
|
|
@@ -1129,16 +1194,22 @@ var BlazePoseAdapter = class {
|
|
|
1129
1194
|
const vh = size.vh || 1;
|
|
1130
1195
|
const dispW = options.displayWidth || 1;
|
|
1131
1196
|
const dispH = options.displayHeight || 1;
|
|
1197
|
+
let maxCoord = 0;
|
|
1198
|
+
for (const kp of pose.keypoints) {
|
|
1199
|
+
maxCoord = Math.max(maxCoord, Math.abs(kp.x || 0), Math.abs(kp.y || 0));
|
|
1200
|
+
}
|
|
1132
1201
|
const byName = /* @__PURE__ */ new Map();
|
|
1202
|
+
const extrasRaw = [];
|
|
1133
1203
|
for (const kp of pose.keypoints) {
|
|
1134
1204
|
const name = String(kp.name || "").toLowerCase();
|
|
1205
|
+
const score = typeof kp.score === "number" ? kp.score : 0;
|
|
1206
|
+
const src = toSourcePixels(kp.x, kp.y, scratchW, scratchH, vw, vh, maxCoord);
|
|
1135
1207
|
const coco = BLAZE_TO_COCO[name];
|
|
1136
|
-
if (
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
y:
|
|
1140
|
-
|
|
1141
|
-
});
|
|
1208
|
+
if (coco) {
|
|
1209
|
+
byName.set(coco, { x: src.x, y: src.y, score });
|
|
1210
|
+
} else if (FACE_EXTRA_NAMES.has(name)) {
|
|
1211
|
+
extrasRaw.push({ name, x: src.x, y: src.y, score });
|
|
1212
|
+
}
|
|
1142
1213
|
}
|
|
1143
1214
|
const videoKeypoints = COCO_KEYPOINT_NAMES.map((name) => {
|
|
1144
1215
|
const k = byName.get(name);
|
|
@@ -1167,6 +1238,19 @@ var BlazePoseAdapter = class {
|
|
|
1167
1238
|
score: k.score
|
|
1168
1239
|
};
|
|
1169
1240
|
});
|
|
1241
|
+
for (const extra of extrasRaw) {
|
|
1242
|
+
const dx = extra.x * scale + ox;
|
|
1243
|
+
const dy = extra.y * scale + oy;
|
|
1244
|
+
let nx = dispW > 0 ? dx / dispW : 0;
|
|
1245
|
+
const ny = dispH > 0 ? dy / dispH : 0;
|
|
1246
|
+
if (options.facingMode === "user") nx = 1 - nx;
|
|
1247
|
+
keypoints.push({
|
|
1248
|
+
name: extra.name,
|
|
1249
|
+
x: Math.min(1, Math.max(0, nx)),
|
|
1250
|
+
y: Math.min(1, Math.max(0, ny)),
|
|
1251
|
+
score: extra.score
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1170
1254
|
return {
|
|
1171
1255
|
keypoints,
|
|
1172
1256
|
score: scoreSum / 17,
|
|
@@ -1182,6 +1266,13 @@ var BlazePoseAdapter = class {
|
|
|
1182
1266
|
}
|
|
1183
1267
|
};
|
|
1184
1268
|
}
|
|
1269
|
+
/** Clear cached ROI / landmark filters (call when the still changes). */
|
|
1270
|
+
resetTemporal() {
|
|
1271
|
+
try {
|
|
1272
|
+
this.detector?.reset?.();
|
|
1273
|
+
} catch {
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1185
1276
|
dispose() {
|
|
1186
1277
|
try {
|
|
1187
1278
|
this.detector?.dispose?.();
|
|
@@ -1205,13 +1296,14 @@ function inputSize(input) {
|
|
|
1205
1296
|
return { vw: input.width || 0, vh: input.height || 0 };
|
|
1206
1297
|
}
|
|
1207
1298
|
var INPUT_SIZE = 192;
|
|
1208
|
-
var SMOOTH_ALPHA = 0.
|
|
1299
|
+
var SMOOTH_ALPHA = 0.45;
|
|
1300
|
+
var STILL_TARGET_FILL = 0.52;
|
|
1209
1301
|
function modelNormToVideo(xNorm, yNorm, lb) {
|
|
1210
1302
|
const xSq = xNorm * INPUT_SIZE;
|
|
1211
1303
|
const ySq = yNorm * INPUT_SIZE;
|
|
1212
1304
|
return {
|
|
1213
|
-
x: (xSq - lb.offsetX) * (lb.vw / lb.drawW),
|
|
1214
|
-
y: (ySq - lb.offsetY) * (lb.vh / lb.drawH)
|
|
1305
|
+
x: (xSq - lb.offsetX) * (lb.vw / lb.drawW) + (lb.originX ?? 0),
|
|
1306
|
+
y: (ySq - lb.offsetY) * (lb.vh / lb.drawH) + (lb.originY ?? 0)
|
|
1215
1307
|
};
|
|
1216
1308
|
}
|
|
1217
1309
|
function videoToCover(vx, vy, dispW, dispH, lb) {
|
|
@@ -1228,6 +1320,18 @@ var MoveNetGraphAdapter = class {
|
|
|
1228
1320
|
this.offscreen = null;
|
|
1229
1321
|
this.offCtx = null;
|
|
1230
1322
|
this.smoothed = null;
|
|
1323
|
+
this.lastVideoKps = null;
|
|
1324
|
+
this.stillPass = 0;
|
|
1325
|
+
this.inferLb = {
|
|
1326
|
+
offsetX: 0,
|
|
1327
|
+
offsetY: 0,
|
|
1328
|
+
drawW: INPUT_SIZE,
|
|
1329
|
+
drawH: INPUT_SIZE,
|
|
1330
|
+
vw: 1,
|
|
1331
|
+
vh: 1,
|
|
1332
|
+
originX: 0,
|
|
1333
|
+
originY: 0
|
|
1334
|
+
};
|
|
1231
1335
|
this.letterbox = {
|
|
1232
1336
|
offsetX: 0,
|
|
1233
1337
|
offsetY: 0,
|
|
@@ -1267,20 +1371,81 @@ var MoveNetGraphAdapter = class {
|
|
|
1267
1371
|
if (!this.offCtx) throw new Error("2d context unavailable");
|
|
1268
1372
|
return this.offCtx;
|
|
1269
1373
|
}
|
|
1270
|
-
prepareInput(input) {
|
|
1374
|
+
prepareInput(input, trackRoi) {
|
|
1271
1375
|
const { vw: rawW, vh: rawH } = inputSize(input);
|
|
1272
1376
|
const vw = rawW || 1;
|
|
1273
1377
|
const vh = rawH || 1;
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1378
|
+
let winX = 0;
|
|
1379
|
+
let winY = 0;
|
|
1380
|
+
let winW = vw;
|
|
1381
|
+
let winH = vh;
|
|
1382
|
+
if (trackRoi && this.lastVideoKps && this.stillPass > 0) {
|
|
1383
|
+
const pts = this.lastVideoKps.filter((k) => k.score >= 0.15);
|
|
1384
|
+
if (pts.length >= 4) {
|
|
1385
|
+
let minx = Infinity;
|
|
1386
|
+
let miny = Infinity;
|
|
1387
|
+
let maxx = -Infinity;
|
|
1388
|
+
let maxy = -Infinity;
|
|
1389
|
+
for (const p of pts) {
|
|
1390
|
+
minx = Math.min(minx, p.xPx);
|
|
1391
|
+
miny = Math.min(miny, p.yPx);
|
|
1392
|
+
maxx = Math.max(maxx, p.xPx);
|
|
1393
|
+
maxy = Math.max(maxy, p.yPx);
|
|
1394
|
+
}
|
|
1395
|
+
const bw = Math.max(8, maxx - minx);
|
|
1396
|
+
const bh = Math.max(8, maxy - miny);
|
|
1397
|
+
const cx = (minx + maxx) / 2;
|
|
1398
|
+
const cy = (miny + maxy) / 2;
|
|
1399
|
+
winW = Math.max(bw / STILL_TARGET_FILL, vw * 0.35);
|
|
1400
|
+
winH = Math.max(bh / STILL_TARGET_FILL, vh * 0.35);
|
|
1401
|
+
const imgAspect = vw / vh;
|
|
1402
|
+
if (winW / winH < imgAspect) winW = winH * imgAspect;
|
|
1403
|
+
else winH = winW / imgAspect;
|
|
1404
|
+
winX = cx - winW / 2;
|
|
1405
|
+
winY = cy - winH / 2;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
const scale = Math.min(INPUT_SIZE / winW, INPUT_SIZE / winH);
|
|
1409
|
+
const drawW = winW * scale;
|
|
1410
|
+
const drawH = winH * scale;
|
|
1277
1411
|
const offsetX = (INPUT_SIZE - drawW) / 2;
|
|
1278
1412
|
const offsetY = (INPUT_SIZE - drawH) / 2;
|
|
1279
|
-
this.
|
|
1413
|
+
this.inferLb = {
|
|
1414
|
+
offsetX,
|
|
1415
|
+
offsetY,
|
|
1416
|
+
drawW,
|
|
1417
|
+
drawH,
|
|
1418
|
+
vw: winW,
|
|
1419
|
+
vh: winH,
|
|
1420
|
+
originX: winX,
|
|
1421
|
+
originY: winY
|
|
1422
|
+
};
|
|
1423
|
+
this.letterbox = { offsetX: 0, offsetY: 0, drawW: vw, drawH: vh, vw, vh };
|
|
1280
1424
|
const c2d = this.poseCanvasCtx();
|
|
1281
1425
|
c2d.fillStyle = "#000";
|
|
1282
1426
|
c2d.fillRect(0, 0, INPUT_SIZE, INPUT_SIZE);
|
|
1283
|
-
|
|
1427
|
+
const srcX = Math.max(0, winX);
|
|
1428
|
+
const srcY = Math.max(0, winY);
|
|
1429
|
+
const srcR = Math.min(vw, winX + winW);
|
|
1430
|
+
const srcB = Math.min(vh, winY + winH);
|
|
1431
|
+
if (srcR > srcX && srcB > srcY) {
|
|
1432
|
+
const dx = offsetX + (srcX - winX) * (drawW / winW);
|
|
1433
|
+
const dy = offsetY + (srcY - winY) * (drawH / winH);
|
|
1434
|
+
const dw = (srcR - srcX) * (drawW / winW);
|
|
1435
|
+
const dh = (srcB - srcY) * (drawH / winH);
|
|
1436
|
+
c2d.drawImage(
|
|
1437
|
+
input,
|
|
1438
|
+
srcX,
|
|
1439
|
+
srcY,
|
|
1440
|
+
srcR - srcX,
|
|
1441
|
+
srcB - srcY,
|
|
1442
|
+
dx,
|
|
1443
|
+
dy,
|
|
1444
|
+
dw,
|
|
1445
|
+
dh
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
this.stillPass += 1;
|
|
1284
1449
|
return this.offscreen;
|
|
1285
1450
|
}
|
|
1286
1451
|
smooth(raw) {
|
|
@@ -1301,7 +1466,9 @@ var MoveNetGraphAdapter = class {
|
|
|
1301
1466
|
const size = inputSize(input);
|
|
1302
1467
|
if (!(size.vw > 0 && size.vh > 0)) return null;
|
|
1303
1468
|
const t0 = performance.now();
|
|
1304
|
-
const
|
|
1469
|
+
const isStill = typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement;
|
|
1470
|
+
const trackRoi = options.temporalSmooth !== false && isStill;
|
|
1471
|
+
const canvas = this.prepareInput(input, trackRoi);
|
|
1305
1472
|
const tensorIn = this.tf.tidy(
|
|
1306
1473
|
() => this.tf.expandDims(this.tf.browser.fromPixels(canvas), 0)
|
|
1307
1474
|
);
|
|
@@ -1312,7 +1479,8 @@ var MoveNetGraphAdapter = class {
|
|
|
1312
1479
|
if (Array.isArray(out)) out.forEach((t) => t.dispose());
|
|
1313
1480
|
else out.dispose();
|
|
1314
1481
|
const inferenceMs = performance.now() - t0;
|
|
1315
|
-
const lb = this.
|
|
1482
|
+
const lb = this.inferLb;
|
|
1483
|
+
const coverLb = this.letterbox;
|
|
1316
1484
|
const dispW = options.displayWidth || 1;
|
|
1317
1485
|
const dispH = options.displayHeight || 1;
|
|
1318
1486
|
const drawRaw = [];
|
|
@@ -1330,7 +1498,8 @@ var MoveNetGraphAdapter = class {
|
|
|
1330
1498
|
});
|
|
1331
1499
|
scoreSum += score;
|
|
1332
1500
|
}
|
|
1333
|
-
const sm = this.smooth(drawRaw);
|
|
1501
|
+
const sm = options.temporalSmooth === false ? drawRaw : this.smooth(drawRaw);
|
|
1502
|
+
this.lastVideoKps = sm.map((k) => ({ xPx: k.xPx, yPx: k.yPx, score: k.score }));
|
|
1334
1503
|
const videoKeypoints = sm.map((k, i) => ({
|
|
1335
1504
|
name: COCO_KEYPOINT_NAMES[i],
|
|
1336
1505
|
xPx: k.xPx,
|
|
@@ -1338,7 +1507,7 @@ var MoveNetGraphAdapter = class {
|
|
|
1338
1507
|
score: k.score
|
|
1339
1508
|
}));
|
|
1340
1509
|
const keypoints = videoKeypoints.map((k) => {
|
|
1341
|
-
const d = videoToCover(k.xPx, k.yPx, dispW, dispH,
|
|
1510
|
+
const d = videoToCover(k.xPx, k.yPx, dispW, dispH, coverLb);
|
|
1342
1511
|
let nx = dispW > 0 ? d.x / dispW : 0;
|
|
1343
1512
|
const ny = dispH > 0 ? d.y / dispH : 0;
|
|
1344
1513
|
if (options.facingMode === "user") nx = 1 - nx;
|
|
@@ -1355,9 +1524,15 @@ var MoveNetGraphAdapter = class {
|
|
|
1355
1524
|
score: scoreSum / 17,
|
|
1356
1525
|
inferenceMs,
|
|
1357
1526
|
videoKeypoints,
|
|
1358
|
-
letterbox:
|
|
1527
|
+
letterbox: coverLb
|
|
1359
1528
|
};
|
|
1360
1529
|
}
|
|
1530
|
+
/** Clear EMA state (call when the still / clip changes). */
|
|
1531
|
+
resetTemporal() {
|
|
1532
|
+
this.smoothed = null;
|
|
1533
|
+
this.lastVideoKps = null;
|
|
1534
|
+
this.stillPass = 0;
|
|
1535
|
+
}
|
|
1361
1536
|
dispose() {
|
|
1362
1537
|
try {
|
|
1363
1538
|
this.model?.dispose();
|
|
@@ -1365,6 +1540,8 @@ var MoveNetGraphAdapter = class {
|
|
|
1365
1540
|
}
|
|
1366
1541
|
this.model = null;
|
|
1367
1542
|
this.smoothed = null;
|
|
1543
|
+
this.lastVideoKps = null;
|
|
1544
|
+
this.stillPass = 0;
|
|
1368
1545
|
this.offscreen = null;
|
|
1369
1546
|
this.offCtx = null;
|
|
1370
1547
|
}
|
|
@@ -1985,7 +2162,11 @@ var PoseTrackerClient = class {
|
|
|
1985
2162
|
if (this.poseSource.type === "camera" && this.poseSource.facingMode) {
|
|
1986
2163
|
this.opts.facingMode = this.poseSource.facingMode;
|
|
1987
2164
|
}
|
|
2165
|
+
if (this.adapter?.resetTemporal) {
|
|
2166
|
+
this.adapter.resetTemporal();
|
|
2167
|
+
}
|
|
1988
2168
|
if (!this.shell) return;
|
|
2169
|
+
await this.ensureAdapter();
|
|
1989
2170
|
await this.ensureSource();
|
|
1990
2171
|
if (wasRunning || this.poseSource.type === "image") {
|
|
1991
2172
|
if (this.poseSource.type === "image") this.imageShotPending = true;
|
|
@@ -2097,7 +2278,30 @@ var PoseTrackerClient = class {
|
|
|
2097
2278
|
return this.configurePromise;
|
|
2098
2279
|
}
|
|
2099
2280
|
getAvailableExercises() {
|
|
2100
|
-
|
|
2281
|
+
if (this.mode !== "full-engine") return [];
|
|
2282
|
+
if (normalizeEngineChannel(this.opts.engine) === "v4") {
|
|
2283
|
+
const listed = this.engine?.listExercises?.() ?? [];
|
|
2284
|
+
if (listed.length) return listed.map((e) => this.v4ExerciseConfig(e));
|
|
2285
|
+
}
|
|
2286
|
+
return this.manifest?.exercises ?? [];
|
|
2287
|
+
}
|
|
2288
|
+
getEngineChannel() {
|
|
2289
|
+
return normalizeEngineChannel(this.opts.engine);
|
|
2290
|
+
}
|
|
2291
|
+
v4ExerciseConfig(entry) {
|
|
2292
|
+
const type = entry.type === "static" ? "static" : "dynamic";
|
|
2293
|
+
return {
|
|
2294
|
+
id: entry.id,
|
|
2295
|
+
name: entry.displayName || entry.id,
|
|
2296
|
+
type,
|
|
2297
|
+
movement: {
|
|
2298
|
+
name: entry.id,
|
|
2299
|
+
type,
|
|
2300
|
+
scale_acceptance: {},
|
|
2301
|
+
movement_steps: [],
|
|
2302
|
+
movement_initial_posture: null
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2101
2305
|
}
|
|
2102
2306
|
getAvailableCustomExercises() {
|
|
2103
2307
|
if (this.mode !== "full-engine" || !this.engine?.listCustomExercises) return [];
|
|
@@ -2133,6 +2337,40 @@ var PoseTrackerClient = class {
|
|
|
2133
2337
|
throw new Error(FREE_PLAN_FEATURES_MESSAGE);
|
|
2134
2338
|
}
|
|
2135
2339
|
}
|
|
2340
|
+
if (requiresEngineV4(exerciseId) && normalizeEngineChannel(this.opts.engine) !== "v4") {
|
|
2341
|
+
const message = `Exercise '${exerciseId}' requires engine: 'v4'`;
|
|
2342
|
+
this.reportError({ type: "error", code: "invalid_exercise", message });
|
|
2343
|
+
throw new Error(message);
|
|
2344
|
+
}
|
|
2345
|
+
const jumpId = resolveExerciseName(exerciseId) || exerciseId;
|
|
2346
|
+
const sessionEngine = resolveMovementEngine({
|
|
2347
|
+
engine: this.opts.engine,
|
|
2348
|
+
exercise: jumpId
|
|
2349
|
+
});
|
|
2350
|
+
if (sessionEngine === "v4") {
|
|
2351
|
+
const listed = this.engine.listExercises?.() ?? [];
|
|
2352
|
+
const v4Hit = listed.find((e) => e.id === exerciseId) ?? listed.find((e) => e.id === jumpId);
|
|
2353
|
+
if (v4Hit) {
|
|
2354
|
+
this.beginEngineSession(this.v4ExerciseConfig(v4Hit), options);
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
const customs = this.getAvailableCustomExercises();
|
|
2358
|
+
const custom = customs.find((e) => e.id === exerciseId) ?? findExerciseByIdOrAlias(exerciseId, customs);
|
|
2359
|
+
if (custom) {
|
|
2360
|
+
this.startCustomExercise(custom, options);
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
const message = `Exercise '${exerciseId}' is not available in V4 engine`;
|
|
2364
|
+
this.reportError({ type: "error", code: "invalid_exercise", message });
|
|
2365
|
+
throw new Error(message);
|
|
2366
|
+
}
|
|
2367
|
+
if (isProductionSquatId(jumpId) && normalizeEngineChannel(this.opts.engine) === "v4") {
|
|
2368
|
+
this.beginEngineSession(
|
|
2369
|
+
this.v4ExerciseConfig({ id: jumpId || "squat", displayName: "Squat", type: "dynamic" }),
|
|
2370
|
+
options
|
|
2371
|
+
);
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2136
2374
|
const available = this.getAvailableExercises();
|
|
2137
2375
|
const exercise = findExerciseByIdOrAlias(exerciseId, available);
|
|
2138
2376
|
if (!exercise) {
|
|
@@ -2146,8 +2384,12 @@ var PoseTrackerClient = class {
|
|
|
2146
2384
|
this.reportError({ type: "error", code: "invalid_exercise", message });
|
|
2147
2385
|
throw new Error(message);
|
|
2148
2386
|
}
|
|
2387
|
+
this.beginEngineSession(exercise, options);
|
|
2388
|
+
}
|
|
2389
|
+
beginEngineSession(exercise, options) {
|
|
2390
|
+
if (!this.engine) return;
|
|
2149
2391
|
this.stopExercise();
|
|
2150
|
-
this.currentExerciseId =
|
|
2392
|
+
this.currentExerciseId = exercise.id;
|
|
2151
2393
|
this.keypointsSuppressionLogged = false;
|
|
2152
2394
|
this.session = this.engine.createSession(
|
|
2153
2395
|
{
|
|
@@ -2155,6 +2397,7 @@ var PoseTrackerClient = class {
|
|
|
2155
2397
|
locale: this.opts.locale ?? "en",
|
|
2156
2398
|
difficulty: options.difficulty,
|
|
2157
2399
|
minGrade: this.features.minGrade ?? void 0,
|
|
2400
|
+
v4CatalogSquat: this.opts.engine === "v4",
|
|
2158
2401
|
features: {
|
|
2159
2402
|
angles: this.features.angles,
|
|
2160
2403
|
recommendations: this.features.recommendations,
|
|
@@ -2514,13 +2757,19 @@ var PoseTrackerClient = class {
|
|
|
2514
2757
|
if (!this.adapter || !this.shell || !this.estimateInput) return null;
|
|
2515
2758
|
if (!this.frameReady(this.estimateInput)) return null;
|
|
2516
2759
|
const input = this.estimateInput;
|
|
2517
|
-
const
|
|
2518
|
-
const
|
|
2760
|
+
const clientW = this.shell.canvas.clientWidth || this.shell.root.clientWidth || 1;
|
|
2761
|
+
const clientH = this.shell.canvas.clientHeight || this.shell.root.clientHeight || 1;
|
|
2762
|
+
const media = mediaSize(input);
|
|
2763
|
+
const useMediaSpace = this.poseSource.type === "image" && media.width > 0 && media.height > 0;
|
|
2764
|
+
const dispW = useMediaSpace ? media.width : clientW;
|
|
2765
|
+
const dispH = useMediaSpace ? media.height : clientH;
|
|
2519
2766
|
const facing = this.poseSource.type === "camera" ? facingFromOptions(this.opts) : "environment";
|
|
2767
|
+
const stillTracked = this.poseSource.type === "image" && this.opts.stillMode === "tracked";
|
|
2520
2768
|
const result = await this.adapter.estimate(input, {
|
|
2521
2769
|
facingMode: facing,
|
|
2522
2770
|
displayWidth: dispW,
|
|
2523
|
-
displayHeight: dispH
|
|
2771
|
+
displayHeight: dispH,
|
|
2772
|
+
temporalSmooth: this.poseSource.type === "camera" || this.poseSource.type === "video" || stillTracked
|
|
2524
2773
|
});
|
|
2525
2774
|
if (!result) return null;
|
|
2526
2775
|
const ctx = this.shell.canvas.getContext("2d");
|
|
@@ -2528,12 +2777,12 @@ var PoseTrackerClient = class {
|
|
|
2528
2777
|
const mapped = mapVideoKeypointsToDisplay(
|
|
2529
2778
|
result.videoKeypoints,
|
|
2530
2779
|
result.letterbox,
|
|
2531
|
-
|
|
2532
|
-
|
|
2780
|
+
clientW,
|
|
2781
|
+
clientH
|
|
2533
2782
|
);
|
|
2534
|
-
drawSkeleton(ctx, mapped,
|
|
2783
|
+
drawSkeleton(ctx, mapped, clientW, clientH, true, this.skeletonDef);
|
|
2535
2784
|
} else if (ctx && this.opts.drawSkeleton === false) {
|
|
2536
|
-
ctx.clearRect(0, 0,
|
|
2785
|
+
ctx.clearRect(0, 0, clientW, clientH);
|
|
2537
2786
|
}
|
|
2538
2787
|
const pose = {
|
|
2539
2788
|
keypoints: result.keypoints,
|
|
@@ -2576,7 +2825,13 @@ var PoseTrackerClient = class {
|
|
|
2576
2825
|
return pose;
|
|
2577
2826
|
}
|
|
2578
2827
|
async loop(token) {
|
|
2579
|
-
if (token !== this.loopToken || !this.running || !this.
|
|
2828
|
+
if (token !== this.loopToken || !this.running || !this.shell || this.disposed) {
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
if (!this.adapter) {
|
|
2832
|
+
requestAnimationFrame(() => {
|
|
2833
|
+
void this.loop(token);
|
|
2834
|
+
});
|
|
2580
2835
|
return;
|
|
2581
2836
|
}
|
|
2582
2837
|
const input = this.estimateInput;
|
|
@@ -2584,8 +2839,8 @@ var PoseTrackerClient = class {
|
|
|
2584
2839
|
if (shouldInfer) {
|
|
2585
2840
|
this.busy = true;
|
|
2586
2841
|
try {
|
|
2587
|
-
await this.runOneFrame();
|
|
2588
|
-
if (this.poseSource.type === "image") {
|
|
2842
|
+
const pose = await this.runOneFrame();
|
|
2843
|
+
if (this.poseSource.type === "image" && pose) {
|
|
2589
2844
|
this.imageShotPending = false;
|
|
2590
2845
|
this.running = false;
|
|
2591
2846
|
this.busy = false;
|
|
@@ -2594,6 +2849,12 @@ var PoseTrackerClient = class {
|
|
|
2594
2849
|
} catch (err) {
|
|
2595
2850
|
const message = err instanceof Error ? err.message : String(err);
|
|
2596
2851
|
this.emit({ type: "error", code: "internal", message: `inference: ${message}` });
|
|
2852
|
+
if (this.poseSource.type === "image") {
|
|
2853
|
+
this.imageShotPending = false;
|
|
2854
|
+
this.running = false;
|
|
2855
|
+
this.busy = false;
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2597
2858
|
}
|
|
2598
2859
|
this.busy = false;
|
|
2599
2860
|
}
|
|
@@ -2974,6 +3235,7 @@ exports.MoveNetGraphAdapter = MoveNetGraphAdapter;
|
|
|
2974
3235
|
exports.ONLINE_POSE_DETECTION_VERSION = ONLINE_POSE_DETECTION_VERSION;
|
|
2975
3236
|
exports.ONLINE_TFJS_VERSION = ONLINE_TFJS_VERSION;
|
|
2976
3237
|
exports.POSETRACKER_LOGO_URL = POSETRACKER_LOGO_URL;
|
|
3238
|
+
exports.PRODUCTION_SQUAT_IDS = PRODUCTION_SQUAT_IDS;
|
|
2977
3239
|
exports.PoseCamera = PoseCamera;
|
|
2978
3240
|
exports.PoseCamera_mount = PoseCamera_mount;
|
|
2979
3241
|
exports.PoseTrackerClient = PoseTrackerClient;
|
|
@@ -2982,6 +3244,7 @@ exports.SDK_VERSION = SDK_VERSION;
|
|
|
2982
3244
|
exports.SkeletonFetchError = SkeletonFetchError;
|
|
2983
3245
|
exports.TrackError = TrackError;
|
|
2984
3246
|
exports.UsageTracker = UsageTracker;
|
|
3247
|
+
exports.V4_ONLY_EXERCISE_IDS = V4_ONLY_EXERCISE_IDS;
|
|
2985
3248
|
exports.configure = configure;
|
|
2986
3249
|
exports.createLocalFileStore = createLocalFileStore;
|
|
2987
3250
|
exports.createLocalKeyValueStore = createLocalKeyValueStore;
|
|
@@ -2998,12 +3261,16 @@ exports.findExerciseByIdOrAlias = findExerciseByIdOrAlias;
|
|
|
2998
3261
|
exports.freeBlockedFeatures = freeBlockedFeatures;
|
|
2999
3262
|
exports.initWebGlBackend = initWebGlBackend;
|
|
3000
3263
|
exports.isPaidPlan = isPaidPlan;
|
|
3264
|
+
exports.isProductionSquatId = isProductionSquatId;
|
|
3001
3265
|
exports.mapVideoKeypointsToDisplay = mapVideoKeypointsToDisplay;
|
|
3002
3266
|
exports.mountDomShell = mountDomShell;
|
|
3267
|
+
exports.normalizeEngineChannel = normalizeEngineChannel;
|
|
3003
3268
|
exports.normalizePoseSource = normalizePoseSource;
|
|
3004
3269
|
exports.openCameraStream = openCameraStream;
|
|
3270
|
+
exports.requiresEngineV4 = requiresEngineV4;
|
|
3005
3271
|
exports.resolveExerciseName = resolveExerciseName;
|
|
3006
3272
|
exports.resolveFeatures = resolveFeatures;
|
|
3273
|
+
exports.resolveMovementEngine = resolveMovementEngine;
|
|
3007
3274
|
exports.resolvePoseDetection = resolvePoseDetection;
|
|
3008
3275
|
exports.resolvePoseModel = resolvePoseModel;
|
|
3009
3276
|
exports.resolveTf = resolveTf;
|