seg-cam 0.2.0 → 0.4.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 +5 -4
- package/package.json +32 -13
- package/public/jersey-detector-worker.js +42 -27
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ export function CameraApp() {
|
|
|
81
81
|
| `workerUrl` | `string` | `'/jersey-detector-worker.js'` | URL to the bundled worker JS file. |
|
|
82
82
|
| `threshold` | `number` | `0.5` | Confidence threshold for detection (0 to 1). |
|
|
83
83
|
| `targetPartId` | `number` | `12` | BodyPix Part ID to isolate (12 = Torso). |
|
|
84
|
-
| `
|
|
84
|
+
| `imagesToReturn` | `'mask' \| 'mask_with_image' \| 'none'` | `'mask'` | Controls output: 'mask' (overlay only), 'mask_with_image' (overlay + frame), 'none' (data only). |
|
|
85
85
|
| `onStatsUpdate`| `(stats: DetectionStats) => void` | - | Callback triggered on every processed frame with stats. |
|
|
86
86
|
| `onWorkerReady`| `(ready: boolean) => void` | - | Callback triggered when the TFJS model is loaded. |
|
|
87
87
|
| `onWorkerError`| `(error: string \| null) => void` | - | Callback triggered if the worker fails to initialize. |
|
|
@@ -130,9 +130,10 @@ const currentStats = detectorRef.current?.stats;
|
|
|
130
130
|
- **`targetPartId`**: (`number`, Default `12`) The BodyPix part ID to treat as the "Jersey". (e.g., 12=torso, 2=full upper body).
|
|
131
131
|
- **`backgroundShade`**: (`number`, 0-255, Default `0`) The alpha/opacity for non-jersey pixels.
|
|
132
132
|
- **`shirtShade`**: (`number`, 0-255, Default `170`) The alpha/opacity for the detected jersey pixels.
|
|
133
|
-
- **`
|
|
134
|
-
- `
|
|
135
|
-
- `
|
|
133
|
+
- **`imagesToReturn`**: (`'mask' | 'mask_with_image' | 'none'`, Default `'mask'`)
|
|
134
|
+
- `'mask'`: Returns only the overlay mask (clears canvas before drawing).
|
|
135
|
+
- `'mask_with_image'`: Draws the overlay on top of the existing canvas (may trail if not cleared externally).
|
|
136
|
+
- `'none'`: Skips bitmap generation entirely. Use this if you only need `onSegmentedImage` or stats, improving performance.
|
|
136
137
|
- **`threshold`**: (`number`, 0.0-1.0, Default `0.5`) The minimum confidence required for a person pixel to be considered valid.
|
|
137
138
|
|
|
138
139
|
## Acknowledgements
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seg-cam",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -12,6 +12,19 @@
|
|
|
12
12
|
"require": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"react",
|
|
17
|
+
"nextjs",
|
|
18
|
+
"tensorflow",
|
|
19
|
+
"tfjs",
|
|
20
|
+
"computer-vision",
|
|
21
|
+
"body-segmentation",
|
|
22
|
+
"image-segmentation",
|
|
23
|
+
"webcam",
|
|
24
|
+
"worker",
|
|
25
|
+
"jersey-detection",
|
|
26
|
+
"image-analytics"
|
|
27
|
+
],
|
|
15
28
|
"license": "CC-BY-NC-SA-4.0",
|
|
16
29
|
"files": [
|
|
17
30
|
"dist",
|
|
@@ -28,13 +41,13 @@
|
|
|
28
41
|
},
|
|
29
42
|
"dependencies": {
|
|
30
43
|
"@vercel/analytics": "1.3.1",
|
|
31
|
-
"autoprefixer": "^10.4.
|
|
44
|
+
"autoprefixer": "^10.4.24",
|
|
32
45
|
"class-variance-authority": "^0.7.1",
|
|
33
46
|
"clsx": "^2.1.1",
|
|
34
47
|
"lucide-react": "^0.454.0",
|
|
35
48
|
"next": "16.0.10",
|
|
36
49
|
"next-themes": "^0.4.6",
|
|
37
|
-
"tailwind-merge": "^3.
|
|
50
|
+
"tailwind-merge": "^3.4.0"
|
|
38
51
|
},
|
|
39
52
|
"peerDependencies": {
|
|
40
53
|
"@tensorflow-models/body-segmentation": "1.0.2",
|
|
@@ -43,14 +56,20 @@
|
|
|
43
56
|
"react-dom": ">=18"
|
|
44
57
|
},
|
|
45
58
|
"devDependencies": {
|
|
46
|
-
"@tailwindcss/postcss": "^4.1.
|
|
47
|
-
"@types/node": "^22",
|
|
48
|
-
"@types/react": "^19",
|
|
49
|
-
"@types/react-dom": "^19",
|
|
50
|
-
"esbuild": "0.27",
|
|
51
|
-
"postcss": "^8.5",
|
|
52
|
-
"tailwindcss": "^4.1.
|
|
53
|
-
"tsup": "^8.
|
|
54
|
-
"typescript": "^5"
|
|
59
|
+
"@tailwindcss/postcss": "^4.1.18",
|
|
60
|
+
"@types/node": "^22.19.7",
|
|
61
|
+
"@types/react": "^19.2.10",
|
|
62
|
+
"@types/react-dom": "^19.2.3",
|
|
63
|
+
"esbuild": "~0.27.2",
|
|
64
|
+
"postcss": "^8.5.6",
|
|
65
|
+
"tailwindcss": "^4.1.18",
|
|
66
|
+
"tsup": "^8.5.1",
|
|
67
|
+
"typescript": "^5.9.3"
|
|
68
|
+
},
|
|
69
|
+
"pnpm": {
|
|
70
|
+
"overrides": {
|
|
71
|
+
"rimraf": "^6.0.0",
|
|
72
|
+
"glob": "^11.0.0"
|
|
73
|
+
}
|
|
55
74
|
}
|
|
56
|
-
}
|
|
75
|
+
}
|
|
@@ -82270,33 +82270,48 @@ var handleDetect = async (message) => {
|
|
|
82270
82270
|
sendDebug("[Worker] No people detected, skipping mask drawing");
|
|
82271
82271
|
} else {
|
|
82272
82272
|
sendDebug(`[Worker] Processing ${people.length} people`);
|
|
82273
|
-
|
|
82274
|
-
|
|
82275
|
-
|
|
82276
|
-
|
|
82277
|
-
|
|
82278
|
-
|
|
82279
|
-
|
|
82280
|
-
|
|
82281
|
-
|
|
82282
|
-
|
|
82283
|
-
|
|
82284
|
-
|
|
82285
|
-
|
|
82286
|
-
|
|
82287
|
-
|
|
82288
|
-
const
|
|
82289
|
-
|
|
82290
|
-
|
|
82291
|
-
|
|
82292
|
-
|
|
82293
|
-
|
|
82294
|
-
|
|
82295
|
-
|
|
82296
|
-
|
|
82297
|
-
|
|
82298
|
-
|
|
82299
|
-
|
|
82273
|
+
const imagesToReturn = currentConfig?.imagesToReturn ?? "mask";
|
|
82274
|
+
if (imagesToReturn === "none") {
|
|
82275
|
+
sendDebug("[Worker] imagesToReturn is 'none', skipping visualization");
|
|
82276
|
+
for (const person of people) {
|
|
82277
|
+
const { shirtBitmap, isUpperTorso } = await processSinglePerson(person, originalImageTensor);
|
|
82278
|
+
if (shirtBitmap && isUpperTorso) {
|
|
82279
|
+
segmentedImages.push(shirtBitmap);
|
|
82280
|
+
isUpperTorso.dispose();
|
|
82281
|
+
} else if (isUpperTorso) {
|
|
82282
|
+
isUpperTorso.dispose();
|
|
82283
|
+
}
|
|
82284
|
+
}
|
|
82285
|
+
originalImageTensor.dispose();
|
|
82286
|
+
} else {
|
|
82287
|
+
let allTorsosMask = tf.fill([height, width], false, "bool");
|
|
82288
|
+
for (const person of people) {
|
|
82289
|
+
const { shirtBitmap, isUpperTorso } = await processSinglePerson(person, originalImageTensor);
|
|
82290
|
+
if (!shirtBitmap || !isUpperTorso) continue;
|
|
82291
|
+
segmentedImages.push(shirtBitmap);
|
|
82292
|
+
const oldMask = allTorsosMask;
|
|
82293
|
+
let newTotalMask = allTorsosMask.logicalOr(isUpperTorso);
|
|
82294
|
+
isUpperTorso.dispose();
|
|
82295
|
+
allTorsosMask = newTotalMask;
|
|
82296
|
+
if (oldMask !== newTotalMask) oldMask.dispose();
|
|
82297
|
+
}
|
|
82298
|
+
const overlayTensor = tf.tidy(() => {
|
|
82299
|
+
const whiteRGB = tf.fill([height, width, 3], 255, "int32");
|
|
82300
|
+
const backgroundShade = currentConfig?.backgroundShade ?? 0;
|
|
82301
|
+
const shirtShade = currentConfig?.shirtShade ?? 170;
|
|
82302
|
+
const alphaChannel = tf.where(allTorsosMask, shirtShade, backgroundShade).expandDims(2);
|
|
82303
|
+
return tf.concat([whiteRGB, alphaChannel], 2);
|
|
82304
|
+
});
|
|
82305
|
+
ctx.clearRect(0, 0, width, height);
|
|
82306
|
+
const overlayData = await overlayTensor.data();
|
|
82307
|
+
const overlayPixels = new Uint8ClampedArray(overlayData);
|
|
82308
|
+
const outputImageData = new ImageData(overlayPixels, width, height);
|
|
82309
|
+
ctx.putImageData(outputImageData, 0, 0);
|
|
82310
|
+
outputBitmap = await createImageBitmap(canvas);
|
|
82311
|
+
allTorsosMask.dispose();
|
|
82312
|
+
overlayTensor.dispose();
|
|
82313
|
+
originalImageTensor.dispose();
|
|
82314
|
+
}
|
|
82300
82315
|
}
|
|
82301
82316
|
const transferList = [];
|
|
82302
82317
|
if (outputBitmap) transferList.push(outputBitmap);
|