rtmlib-ts 0.0.2
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/.gitattributes +1 -0
- package/README.md +202 -0
- package/dist/core/base.d.ts +20 -0
- package/dist/core/base.d.ts.map +1 -0
- package/dist/core/base.js +40 -0
- package/dist/core/file.d.ts +11 -0
- package/dist/core/file.d.ts.map +1 -0
- package/dist/core/file.js +111 -0
- package/dist/core/modelCache.d.ts +35 -0
- package/dist/core/modelCache.d.ts.map +1 -0
- package/dist/core/modelCache.js +161 -0
- package/dist/core/posePostprocessing.d.ts +12 -0
- package/dist/core/posePostprocessing.d.ts.map +1 -0
- package/dist/core/posePostprocessing.js +76 -0
- package/dist/core/postprocessing.d.ts +10 -0
- package/dist/core/postprocessing.d.ts.map +1 -0
- package/dist/core/postprocessing.js +70 -0
- package/dist/core/preprocessing.d.ts +14 -0
- package/dist/core/preprocessing.d.ts.map +1 -0
- package/dist/core/preprocessing.js +79 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/models/rtmpose.d.ts +25 -0
- package/dist/models/rtmpose.d.ts.map +1 -0
- package/dist/models/rtmpose.js +185 -0
- package/dist/models/rtmpose3d.d.ts +28 -0
- package/dist/models/rtmpose3d.d.ts.map +1 -0
- package/dist/models/rtmpose3d.js +184 -0
- package/dist/models/yolo12.d.ts +23 -0
- package/dist/models/yolo12.d.ts.map +1 -0
- package/dist/models/yolo12.js +165 -0
- package/dist/models/yolox.d.ts +18 -0
- package/dist/models/yolox.d.ts.map +1 -0
- package/dist/models/yolox.js +167 -0
- package/dist/solution/animalDetector.d.ts +229 -0
- package/dist/solution/animalDetector.d.ts.map +1 -0
- package/dist/solution/animalDetector.js +663 -0
- package/dist/solution/body.d.ts +16 -0
- package/dist/solution/body.d.ts.map +1 -0
- package/dist/solution/body.js +52 -0
- package/dist/solution/bodyWithFeet.d.ts +16 -0
- package/dist/solution/bodyWithFeet.d.ts.map +1 -0
- package/dist/solution/bodyWithFeet.js +52 -0
- package/dist/solution/customDetector.d.ts +137 -0
- package/dist/solution/customDetector.d.ts.map +1 -0
- package/dist/solution/customDetector.js +342 -0
- package/dist/solution/hand.d.ts +14 -0
- package/dist/solution/hand.d.ts.map +1 -0
- package/dist/solution/hand.js +20 -0
- package/dist/solution/index.d.ts +10 -0
- package/dist/solution/index.d.ts.map +1 -0
- package/dist/solution/index.js +9 -0
- package/dist/solution/objectDetector.d.ts +172 -0
- package/dist/solution/objectDetector.d.ts.map +1 -0
- package/dist/solution/objectDetector.js +606 -0
- package/dist/solution/pose3dDetector.d.ts +145 -0
- package/dist/solution/pose3dDetector.d.ts.map +1 -0
- package/dist/solution/pose3dDetector.js +611 -0
- package/dist/solution/poseDetector.d.ts +198 -0
- package/dist/solution/poseDetector.d.ts.map +1 -0
- package/dist/solution/poseDetector.js +622 -0
- package/dist/solution/poseTracker.d.ts +22 -0
- package/dist/solution/poseTracker.d.ts.map +1 -0
- package/dist/solution/poseTracker.js +106 -0
- package/dist/solution/wholebody.d.ts +19 -0
- package/dist/solution/wholebody.d.ts.map +1 -0
- package/dist/solution/wholebody.js +82 -0
- package/dist/solution/wholebody3d.d.ts +22 -0
- package/dist/solution/wholebody3d.d.ts.map +1 -0
- package/dist/solution/wholebody3d.js +75 -0
- package/dist/types/index.d.ts +52 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/visualization/draw.d.ts +57 -0
- package/dist/visualization/draw.d.ts.map +1 -0
- package/dist/visualization/draw.js +400 -0
- package/dist/visualization/skeleton/coco133.d.ts +350 -0
- package/dist/visualization/skeleton/coco133.d.ts.map +1 -0
- package/dist/visualization/skeleton/coco133.js +120 -0
- package/dist/visualization/skeleton/coco17.d.ts +180 -0
- package/dist/visualization/skeleton/coco17.d.ts.map +1 -0
- package/dist/visualization/skeleton/coco17.js +48 -0
- package/dist/visualization/skeleton/halpe26.d.ts +278 -0
- package/dist/visualization/skeleton/halpe26.d.ts.map +1 -0
- package/dist/visualization/skeleton/halpe26.js +70 -0
- package/dist/visualization/skeleton/hand21.d.ts +196 -0
- package/dist/visualization/skeleton/hand21.d.ts.map +1 -0
- package/dist/visualization/skeleton/hand21.js +51 -0
- package/dist/visualization/skeleton/index.d.ts +10 -0
- package/dist/visualization/skeleton/index.d.ts.map +1 -0
- package/dist/visualization/skeleton/index.js +9 -0
- package/dist/visualization/skeleton/openpose134.d.ts +357 -0
- package/dist/visualization/skeleton/openpose134.d.ts.map +1 -0
- package/dist/visualization/skeleton/openpose134.js +116 -0
- package/dist/visualization/skeleton/openpose18.d.ts +177 -0
- package/dist/visualization/skeleton/openpose18.d.ts.map +1 -0
- package/dist/visualization/skeleton/openpose18.js +47 -0
- package/docs/ANIMAL_DETECTOR.md +450 -0
- package/docs/CUSTOM_DETECTOR.md +568 -0
- package/docs/OBJECT_DETECTOR.md +373 -0
- package/docs/POSE3D_DETECTOR.md +458 -0
- package/docs/POSE_DETECTOR.md +442 -0
- package/examples/README.md +119 -0
- package/examples/index.html +746 -0
- package/package.json +51 -0
- package/playground/README.md +114 -0
- package/playground/app/favicon.ico +0 -0
- package/playground/app/globals.css +17 -0
- package/playground/app/layout.tsx +19 -0
- package/playground/app/page.tsx +1338 -0
- package/playground/eslint.config.mjs +18 -0
- package/playground/next.config.ts +34 -0
- package/playground/package-lock.json +6723 -0
- package/playground/package.json +27 -0
- package/playground/postcss.config.mjs +7 -0
- package/playground/tsconfig.json +34 -0
- package/src/core/base.ts +66 -0
- package/src/core/file.ts +141 -0
- package/src/core/modelCache.ts +189 -0
- package/src/core/posePostprocessing.ts +91 -0
- package/src/core/postprocessing.ts +93 -0
- package/src/core/preprocessing.ts +127 -0
- package/src/index.ts +69 -0
- package/src/models/rtmpose.ts +265 -0
- package/src/models/rtmpose3d.ts +289 -0
- package/src/models/yolo12.ts +220 -0
- package/src/models/yolox.ts +214 -0
- package/src/solution/animalDetector.ts +955 -0
- package/src/solution/body.ts +89 -0
- package/src/solution/bodyWithFeet.ts +89 -0
- package/src/solution/customDetector.ts +474 -0
- package/src/solution/hand.ts +52 -0
- package/src/solution/index.ts +10 -0
- package/src/solution/objectDetector.ts +816 -0
- package/src/solution/pose3dDetector.ts +890 -0
- package/src/solution/poseDetector.ts +892 -0
- package/src/solution/poseTracker.ts +172 -0
- package/src/solution/wholebody.ts +130 -0
- package/src/solution/wholebody3d.ts +125 -0
- package/src/types/index.ts +62 -0
- package/src/visualization/draw.ts +543 -0
- package/src/visualization/skeleton/coco133.ts +131 -0
- package/src/visualization/skeleton/coco17.ts +49 -0
- package/src/visualization/skeleton/halpe26.ts +71 -0
- package/src/visualization/skeleton/hand21.ts +52 -0
- package/src/visualization/skeleton/index.ts +10 -0
- package/src/visualization/skeleton/openpose134.ts +125 -0
- package/src/visualization/skeleton/openpose18.ts +48 -0
- package/tsconfig.json +32 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "playground",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "eslint"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"next": "16.1.6",
|
|
13
|
+
"react": "19.2.3",
|
|
14
|
+
"react-dom": "19.2.3",
|
|
15
|
+
"onnxruntime-web": "^1.23.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@tailwindcss/postcss": "^4",
|
|
19
|
+
"@types/node": "^20",
|
|
20
|
+
"@types/react": "^19",
|
|
21
|
+
"@types/react-dom": "^19",
|
|
22
|
+
"eslint": "^9",
|
|
23
|
+
"eslint-config-next": "16.1.6",
|
|
24
|
+
"tailwindcss": "^4",
|
|
25
|
+
"typescript": "^5"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"next-env.d.ts",
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx",
|
|
29
|
+
".next/types/**/*.ts",
|
|
30
|
+
".next/dev/types/**/*.ts",
|
|
31
|
+
"**/*.mts"
|
|
32
|
+
],
|
|
33
|
+
"exclude": ["node_modules"]
|
|
34
|
+
}
|
package/src/core/base.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseTool - Abstract base class for all models
|
|
3
|
+
* Handles ONNX model loading and inference
|
|
4
|
+
* Compatible with onnxruntime-web (browser) and onnxruntime-node
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as ort from 'onnxruntime-web';
|
|
8
|
+
import { BackendType } from '../types/index.js';
|
|
9
|
+
|
|
10
|
+
export abstract class BaseTool {
|
|
11
|
+
protected session: ort.InferenceSession | null = null;
|
|
12
|
+
protected modelPath: string;
|
|
13
|
+
protected modelInputSize: [number, number];
|
|
14
|
+
protected mean: number[] | null;
|
|
15
|
+
protected std: number[] | null;
|
|
16
|
+
protected backend: BackendType;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
modelPath: string,
|
|
20
|
+
modelInputSize: [number, number],
|
|
21
|
+
mean: number[] | null = null,
|
|
22
|
+
std: number[] | null = null,
|
|
23
|
+
backend: BackendType = 'webgpu'
|
|
24
|
+
) {
|
|
25
|
+
this.modelPath = modelPath;
|
|
26
|
+
this.modelInputSize = modelInputSize;
|
|
27
|
+
this.mean = mean;
|
|
28
|
+
this.std = std;
|
|
29
|
+
this.backend = backend;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected async init(): Promise<void> {
|
|
33
|
+
// Configure ONNX Runtime Web - use CDN for WASM files
|
|
34
|
+
ort.env.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.23.0/dist/';
|
|
35
|
+
ort.env.wasm.simd = true;
|
|
36
|
+
ort.env.wasm.proxy = false;
|
|
37
|
+
|
|
38
|
+
// Load model from path/URL
|
|
39
|
+
this.session = await ort.InferenceSession.create(this.modelPath, {
|
|
40
|
+
executionProviders: ['wasm'],
|
|
41
|
+
graphOptimizationLevel: 'all',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
console.log(`Loaded model: ${this.modelPath}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected async inference(img: Float32Array, inputSize?: [number, number]): Promise<any[]> {
|
|
48
|
+
if (!this.session) {
|
|
49
|
+
throw new Error('Session not initialized. Call init() first.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const [h, w] = inputSize || this.modelInputSize;
|
|
53
|
+
|
|
54
|
+
// Build input tensor (1, 3, H, W)
|
|
55
|
+
const inputTensor = new (await import('onnxruntime-web')).Tensor('float32', img, [1, 3, h, w]);
|
|
56
|
+
|
|
57
|
+
const feeds: Record<string, any> = {};
|
|
58
|
+
feeds[this.session.inputNames[0]] = inputTensor;
|
|
59
|
+
|
|
60
|
+
const results = await this.session.run(feeds);
|
|
61
|
+
|
|
62
|
+
return this.session.outputNames.map((name: string) => results[name]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
abstract call(...args: unknown[]): Promise<unknown>;
|
|
66
|
+
}
|
package/src/core/file.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File utilities for downloading and loading models
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as https from 'https';
|
|
8
|
+
import JSZip from 'jszip';
|
|
9
|
+
|
|
10
|
+
const CACHE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.rtmlib', 'models');
|
|
11
|
+
|
|
12
|
+
export async function downloadCheckpoint(url: string, localPath?: string): Promise<string> {
|
|
13
|
+
// If local path provided, use it directly
|
|
14
|
+
if (localPath && fs.existsSync(localPath)) {
|
|
15
|
+
console.log(`Using local model: ${localPath}`);
|
|
16
|
+
return localPath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const fileName = path.basename(url);
|
|
20
|
+
const cachePath = path.join(CACHE_DIR, fileName.replace('.zip', '.onnx'));
|
|
21
|
+
|
|
22
|
+
if (fs.existsSync(cachePath)) {
|
|
23
|
+
console.log(`Using cached model: ${cachePath}`);
|
|
24
|
+
return cachePath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(`Downloading model from ${url}`);
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
30
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const tempPath = path.join(CACHE_DIR, fileName);
|
|
34
|
+
|
|
35
|
+
await downloadFile(url, tempPath);
|
|
36
|
+
|
|
37
|
+
if (fileName.endsWith('.zip')) {
|
|
38
|
+
await extractZip(tempPath, CACHE_DIR);
|
|
39
|
+
fs.unlinkSync(tempPath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return cachePath;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function downloadFile(url: string, dest: string): Promise<void> {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const file = fs.createWriteStream(dest);
|
|
48
|
+
|
|
49
|
+
const download = (url: string) => {
|
|
50
|
+
https.get(url, (response) => {
|
|
51
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
52
|
+
download(response.headers.location!);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
response.pipe(file);
|
|
57
|
+
file.on('finish', () => {
|
|
58
|
+
file.close();
|
|
59
|
+
resolve();
|
|
60
|
+
});
|
|
61
|
+
}).on('error', reject);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
download(url);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function extractZip(zipPath: string, dest: string): Promise<void> {
|
|
69
|
+
const data = fs.readFileSync(zipPath);
|
|
70
|
+
const zip = await JSZip.loadAsync(data);
|
|
71
|
+
|
|
72
|
+
// Find .onnx file in zip
|
|
73
|
+
for (const [filename, file] of Object.entries(zip.files)) {
|
|
74
|
+
if (filename.endsWith('.onnx')) {
|
|
75
|
+
const content = await file.async('nodebuffer');
|
|
76
|
+
const onnxPath = path.join(dest, filename);
|
|
77
|
+
|
|
78
|
+
// Create directory if needed
|
|
79
|
+
const dir = path.dirname(onnxPath);
|
|
80
|
+
if (!fs.existsSync(dir)) {
|
|
81
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fs.writeFileSync(onnxPath, content);
|
|
85
|
+
console.log(`Extracted: ${filename}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw new Error('No .onnx file found in zip');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function fileExists(filePath: string): boolean {
|
|
94
|
+
return fs.existsSync(filePath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function resolveModelPath(modelPath: string): string {
|
|
98
|
+
if (modelPath.startsWith('http://') || modelPath.startsWith('https://')) {
|
|
99
|
+
return modelPath;
|
|
100
|
+
}
|
|
101
|
+
return path.resolve(modelPath);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extract local zip file and return onnx path
|
|
106
|
+
*/
|
|
107
|
+
export async function extractLocalZip(zipPath: string): Promise<string> {
|
|
108
|
+
if (!fs.existsSync(zipPath)) {
|
|
109
|
+
throw new Error(`Zip file not found: ${zipPath}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const destDir = path.dirname(zipPath);
|
|
113
|
+
const data = fs.readFileSync(zipPath);
|
|
114
|
+
const zip = await JSZip.loadAsync(data);
|
|
115
|
+
|
|
116
|
+
// Find .onnx file in zip
|
|
117
|
+
for (const [filename, file] of Object.entries(zip.files)) {
|
|
118
|
+
if (filename.endsWith('.onnx')) {
|
|
119
|
+
const onnxPath = path.join(destDir, filename);
|
|
120
|
+
|
|
121
|
+
if (fs.existsSync(onnxPath)) {
|
|
122
|
+
console.log(`Using extracted model: ${onnxPath}`);
|
|
123
|
+
return onnxPath;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const content = await file.async('nodebuffer');
|
|
127
|
+
|
|
128
|
+
// Create directory if needed
|
|
129
|
+
const dir = path.dirname(onnxPath);
|
|
130
|
+
if (!fs.existsSync(dir)) {
|
|
131
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fs.writeFileSync(onnxPath, content);
|
|
135
|
+
console.log(`Extracted: ${filename} -> ${onnxPath}`);
|
|
136
|
+
return onnxPath;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
throw new Error('No .onnx file found in zip');
|
|
141
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model caching utility using Cache API
|
|
3
|
+
* Caches ONNX models in browser to avoid repeated downloads
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const CACHE_NAME = 'rtmlib-ts-models-v1';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if model is available in cache
|
|
10
|
+
*/
|
|
11
|
+
export async function isModelCached(url: string): Promise<boolean> {
|
|
12
|
+
if (typeof caches === 'undefined') {
|
|
13
|
+
// Cache API not available (e.g., Node.js)
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const cache = await caches.open(CACHE_NAME);
|
|
19
|
+
const response = await cache.match(url);
|
|
20
|
+
return !!response;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.warn(`[ModelCache] Failed to check cache for ${url}:`, error);
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get model from cache or fetch from network
|
|
29
|
+
* @param url - Model URL
|
|
30
|
+
* @param forceRefresh - Force refresh from network
|
|
31
|
+
*/
|
|
32
|
+
export async function getCachedModel(url: string, forceRefresh: boolean = false): Promise<ArrayBuffer> {
|
|
33
|
+
if (typeof caches === 'undefined') {
|
|
34
|
+
// Cache API not available, fetch directly
|
|
35
|
+
console.log(`[ModelCache] Cache API not available, fetching from network`);
|
|
36
|
+
return fetchModelFromNetwork(url);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const cache = await caches.open(CACHE_NAME);
|
|
41
|
+
|
|
42
|
+
// Try to get from cache first
|
|
43
|
+
if (!forceRefresh) {
|
|
44
|
+
const cachedResponse = await cache.match(url);
|
|
45
|
+
if (cachedResponse) {
|
|
46
|
+
console.log(`[ModelCache] ✅ Hit for ${url}`);
|
|
47
|
+
return await cachedResponse.arrayBuffer();
|
|
48
|
+
}
|
|
49
|
+
console.log(`[ModelCache] ❌ Miss for ${url}, fetching from network...`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fetch from network
|
|
53
|
+
const networkResponse = await fetchModelFromNetwork(url);
|
|
54
|
+
|
|
55
|
+
// Cache the response
|
|
56
|
+
const responseToCache = new Response(networkResponse, {
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/octet-stream',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await cache.put(url, responseToCache);
|
|
63
|
+
console.log(`[ModelCache] 💾 Cached ${url}`);
|
|
64
|
+
|
|
65
|
+
return networkResponse;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`[ModelCache] Failed to get/cache model ${url}:`, error);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Fetch model from network with progress tracking
|
|
74
|
+
*/
|
|
75
|
+
async function fetchModelFromNetwork(url: string): Promise<ArrayBuffer> {
|
|
76
|
+
const response = await fetch(url);
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`Failed to fetch model: HTTP ${response.status} ${response.statusText}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return await response.arrayBuffer();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Preload and cache multiple models
|
|
87
|
+
*/
|
|
88
|
+
export async function preloadModels(urls: string[]): Promise<void> {
|
|
89
|
+
console.log(`[ModelCache] Preloading ${urls.length} model(s)...`);
|
|
90
|
+
|
|
91
|
+
const results = await Promise.allSettled(
|
|
92
|
+
urls.map(url => getCachedModel(url))
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const success = results.filter(r => r.status === 'fulfilled').length;
|
|
96
|
+
const failed = results.filter(r => r.status === 'rejected').length;
|
|
97
|
+
|
|
98
|
+
console.log(`[ModelCache] Preload complete: ${success} succeeded, ${failed} failed`);
|
|
99
|
+
|
|
100
|
+
results.forEach((result, index) => {
|
|
101
|
+
if (result.status === 'rejected') {
|
|
102
|
+
console.error(`[ModelCache] Failed to preload ${urls[index]}:`, result.reason);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Clear all cached models
|
|
109
|
+
*/
|
|
110
|
+
export async function clearModelCache(): Promise<void> {
|
|
111
|
+
if (typeof caches === 'undefined') {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await caches.delete(CACHE_NAME);
|
|
117
|
+
console.log('[ModelCache] Cache cleared');
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('[ModelCache] Failed to clear cache:', error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get cache size in bytes
|
|
125
|
+
*/
|
|
126
|
+
export async function getCacheSize(): Promise<number> {
|
|
127
|
+
if (typeof caches === 'undefined' || !navigator.storage) {
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const cache = await caches.open(CACHE_NAME);
|
|
133
|
+
const keys = await cache.keys();
|
|
134
|
+
let totalSize = 0;
|
|
135
|
+
|
|
136
|
+
for (const request of keys) {
|
|
137
|
+
const response = await cache.match(request);
|
|
138
|
+
if (response) {
|
|
139
|
+
const blob = await response.blob();
|
|
140
|
+
totalSize += blob.size;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return totalSize;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.warn('[ModelCache] Failed to get cache size:', error);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get cache info
|
|
153
|
+
*/
|
|
154
|
+
export async function getCacheInfo(): Promise<{
|
|
155
|
+
cachedModels: string[];
|
|
156
|
+
totalSize: number;
|
|
157
|
+
totalSizeFormatted: string;
|
|
158
|
+
}> {
|
|
159
|
+
if (typeof caches === 'undefined') {
|
|
160
|
+
return { cachedModels: [], totalSize: 0, totalSizeFormatted: '0 B' };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const cache = await caches.open(CACHE_NAME);
|
|
165
|
+
const keys = await cache.keys();
|
|
166
|
+
const cachedModels = keys.map(k => k.url);
|
|
167
|
+
const totalSize = await getCacheSize();
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
cachedModels,
|
|
171
|
+
totalSize,
|
|
172
|
+
totalSizeFormatted: formatBytes(totalSize),
|
|
173
|
+
};
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.warn('[ModelCache] Failed to get cache info:', error);
|
|
176
|
+
return { cachedModels: [], totalSize: 0, totalSizeFormatted: '0 B' };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Format bytes to human-readable string
|
|
182
|
+
*/
|
|
183
|
+
function formatBytes(bytes: number): string {
|
|
184
|
+
if (bytes === 0) return '0 B';
|
|
185
|
+
const k = 1024;
|
|
186
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
187
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
188
|
+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
189
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-processing utilities for pose estimation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function getSimccMaximum(
|
|
6
|
+
simccX: Float32Array,
|
|
7
|
+
simccY: Float32Array
|
|
8
|
+
): { locations: number[]; scores: number[] } {
|
|
9
|
+
const numKeypoints = simccX.length / 2; // Assuming split_ratio = 2
|
|
10
|
+
|
|
11
|
+
const locations: number[] = [];
|
|
12
|
+
const scores: number[] = [];
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < numKeypoints; i++) {
|
|
15
|
+
// Find argmax for x
|
|
16
|
+
let maxX = -Infinity;
|
|
17
|
+
let argmaxX = 0;
|
|
18
|
+
for (let j = 0; j < 2; j++) {
|
|
19
|
+
const val = simccX[i * 2 + j];
|
|
20
|
+
if (val > maxX) {
|
|
21
|
+
maxX = val;
|
|
22
|
+
argmaxX = j;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Find argmax for y
|
|
27
|
+
let maxY = -Infinity;
|
|
28
|
+
let argmaxY = 0;
|
|
29
|
+
for (let j = 0; j < 2; j++) {
|
|
30
|
+
const val = simccY[i * 2 + j];
|
|
31
|
+
if (val > maxY) {
|
|
32
|
+
maxY = val;
|
|
33
|
+
argmaxY = j;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
locations.push(argmaxX, argmaxY);
|
|
38
|
+
scores.push((maxX + maxY) / 2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { locations, scores };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function convertCocoToOpenpose(
|
|
45
|
+
keypoints: number[][],
|
|
46
|
+
scores: number[]
|
|
47
|
+
): { keypoints: number[][]; scores: number[] } {
|
|
48
|
+
// COCO 17 keypoints to OpenPose 18 keypoints mapping
|
|
49
|
+
const cocoToOpenpose: number[] = [
|
|
50
|
+
0, // nose
|
|
51
|
+
1, // neck (average of shoulders)
|
|
52
|
+
2, // right_shoulder
|
|
53
|
+
3, // right_elbow
|
|
54
|
+
4, // right_wrist
|
|
55
|
+
5, // left_shoulder
|
|
56
|
+
6, // left_elbow
|
|
57
|
+
7, // left_wrist
|
|
58
|
+
8, // right_hip
|
|
59
|
+
9, // right_knee
|
|
60
|
+
10, // right_ankle
|
|
61
|
+
11, // left_hip
|
|
62
|
+
12, // left_knee
|
|
63
|
+
13, // left_ankle
|
|
64
|
+
14, // right_eye
|
|
65
|
+
15, // left_eye
|
|
66
|
+
16, // right_ear
|
|
67
|
+
17, // left_ear
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const openposeKeypoints: number[][] = [];
|
|
71
|
+
const openposeScores: number[] = [];
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < 18; i++) {
|
|
74
|
+
if (i === 1) {
|
|
75
|
+
// Neck is average of shoulders
|
|
76
|
+
const rightShoulder = keypoints[2];
|
|
77
|
+
const leftShoulder = keypoints[5];
|
|
78
|
+
openposeKeypoints.push([
|
|
79
|
+
(rightShoulder[0] + leftShoulder[0]) / 2,
|
|
80
|
+
(rightShoulder[1] + leftShoulder[1]) / 2,
|
|
81
|
+
]);
|
|
82
|
+
openposeScores.push((scores[2] + scores[5]) / 2);
|
|
83
|
+
} else {
|
|
84
|
+
const cocoIdx = cocoToOpenpose[i];
|
|
85
|
+
openposeKeypoints.push([...keypoints[cocoIdx]]);
|
|
86
|
+
openposeScores.push(scores[cocoIdx]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { keypoints: openposeKeypoints, scores: openposeScores };
|
|
91
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-processing utilities for object detection
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { BBox } from '../types/index.js';
|
|
6
|
+
|
|
7
|
+
export function multiclassNms(
|
|
8
|
+
boxes: number[][],
|
|
9
|
+
scores: number[][],
|
|
10
|
+
nmsThr: number,
|
|
11
|
+
scoreThr: number
|
|
12
|
+
): { boxes: number[][]; scores: number[]; classIds: number[] } | null {
|
|
13
|
+
const numClasses = scores[0].length;
|
|
14
|
+
const numBoxes = boxes.length;
|
|
15
|
+
|
|
16
|
+
const allDetections: Array<{ box: number[]; score: number; classId: number }> = [];
|
|
17
|
+
|
|
18
|
+
for (let c = 0; c < numClasses; c++) {
|
|
19
|
+
for (let i = 0; i < numBoxes; i++) {
|
|
20
|
+
if (scores[i][c] > scoreThr) {
|
|
21
|
+
allDetections.push({
|
|
22
|
+
box: boxes[i],
|
|
23
|
+
score: scores[i][c],
|
|
24
|
+
classId: c,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
allDetections.sort((a, b) => b.score - a.score);
|
|
31
|
+
|
|
32
|
+
const keep: typeof allDetections = [];
|
|
33
|
+
|
|
34
|
+
while (allDetections.length > 0) {
|
|
35
|
+
const current = allDetections.shift()!;
|
|
36
|
+
keep.push(current);
|
|
37
|
+
|
|
38
|
+
for (let i = allDetections.length - 1; i >= 0; i--) {
|
|
39
|
+
const other = allDetections[i];
|
|
40
|
+
if (current.classId !== other.classId) continue;
|
|
41
|
+
|
|
42
|
+
const iou = calculateIoU(current.box, other.box);
|
|
43
|
+
if (iou > nmsThr) {
|
|
44
|
+
allDetections.splice(i, 1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (keep.length === 0) return null;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
boxes: keep.map(d => d.box),
|
|
53
|
+
scores: keep.map(d => d.score),
|
|
54
|
+
classIds: keep.map(d => d.classId),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function calculateIoU(box1: number[], box2: number[]): number {
|
|
59
|
+
const x1 = Math.max(box1[0], box2[0]);
|
|
60
|
+
const y1 = Math.max(box1[1], box2[1]);
|
|
61
|
+
const x2 = Math.min(box1[2], box2[2]);
|
|
62
|
+
const y2 = Math.min(box1[3], box2[3]);
|
|
63
|
+
|
|
64
|
+
const interWidth = Math.max(0, x2 - x1);
|
|
65
|
+
const interHeight = Math.max(0, y2 - y1);
|
|
66
|
+
const interArea = interWidth * interHeight;
|
|
67
|
+
|
|
68
|
+
const box1Area = (box1[2] - box1[0]) * (box1[3] - box1[1]);
|
|
69
|
+
const box2Area = (box2[2] - box2[0]) * (box2[3] - box2[1]);
|
|
70
|
+
|
|
71
|
+
const unionArea = box1Area + box2Area - interArea;
|
|
72
|
+
return unionArea > 0 ? interArea / unionArea : 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function nms(boxes: number[][], scores: number[], nmsThr: number): number[] {
|
|
76
|
+
const indices: number[] = [];
|
|
77
|
+
const sortedIndices = scores.map((s, i) => i).sort((a, b) => scores[b] - scores[a]);
|
|
78
|
+
|
|
79
|
+
while (sortedIndices.length > 0) {
|
|
80
|
+
const current = sortedIndices.shift()!;
|
|
81
|
+
indices.push(current);
|
|
82
|
+
|
|
83
|
+
for (let i = sortedIndices.length - 1; i >= 0; i--) {
|
|
84
|
+
const other = sortedIndices[i];
|
|
85
|
+
const iou = calculateIoU(boxes[current], boxes[other]);
|
|
86
|
+
if (iou > nmsThr) {
|
|
87
|
+
sortedIndices.splice(i, 1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return indices;
|
|
93
|
+
}
|