@clypra/feature-providers 1.0.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/.releaserc.json +16 -0
- package/package.json +42 -0
- package/src/chroma-key/index.ts +176 -0
- package/src/index.ts +36 -0
- package/src/manager.ts +148 -0
- package/src/segmentation/index.ts +202 -0
- package/src/types.ts +195 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +16 -0
package/.releaserc.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"branches": ["main"],
|
|
3
|
+
"plugins": [
|
|
4
|
+
"@semantic-release/commit-analyzer",
|
|
5
|
+
"@semantic-release/release-notes-generator",
|
|
6
|
+
"@semantic-release/npm",
|
|
7
|
+
[
|
|
8
|
+
"@semantic-release/git",
|
|
9
|
+
{
|
|
10
|
+
"assets": ["package.json"],
|
|
11
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"@semantic-release/github"
|
|
15
|
+
]
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clypra/feature-providers",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Extensible feature providers for Body Effect Lab",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js",
|
|
10
|
+
"./types": "./dist/types.js",
|
|
11
|
+
"./segmentation": "./dist/segmentation/index.js",
|
|
12
|
+
"./chroma-key": "./dist/chroma-key/index.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"dev": "tsup --watch",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"clean": "rm -rf dist",
|
|
20
|
+
"lint": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.14.0",
|
|
25
|
+
"tsup": "^8.3.5",
|
|
26
|
+
"typescript": "~5.8.2",
|
|
27
|
+
"vitest": "^3.2.4"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/AIEraDev/clypra-studio.git",
|
|
32
|
+
"directory": "packages/feature-providers"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/AIEraDev/clypra-studio/tree/main/packages/feature-providers#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/AIEraDev/clypra-studio/issues"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"registry": "https://registry.npmjs.org/"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clypra/feature-providers — Chroma Key Provider
|
|
3
|
+
*
|
|
4
|
+
* Color-based masking (green screen, blue screen, etc.)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FeatureProvider, FeatureMap, FeatureMapType, VideoFrame, ProviderConfig } from "../types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Chroma Key Provider
|
|
11
|
+
*
|
|
12
|
+
* Extracts masks based on color keying (e.g., green screen removal)
|
|
13
|
+
*/
|
|
14
|
+
export class ChromaKeyProvider implements FeatureProvider {
|
|
15
|
+
id = "chroma-key";
|
|
16
|
+
name = "Chroma Key";
|
|
17
|
+
outputs: FeatureMapType[] = ["mask" as FeatureMapType];
|
|
18
|
+
|
|
19
|
+
config: ProviderConfig = {
|
|
20
|
+
keyColor: {
|
|
21
|
+
type: "color",
|
|
22
|
+
default: "#00FF00",
|
|
23
|
+
label: "Key Color",
|
|
24
|
+
},
|
|
25
|
+
threshold: {
|
|
26
|
+
type: "number",
|
|
27
|
+
min: 0,
|
|
28
|
+
max: 1,
|
|
29
|
+
default: 0.4,
|
|
30
|
+
label: "Threshold",
|
|
31
|
+
},
|
|
32
|
+
smoothness: {
|
|
33
|
+
type: "number",
|
|
34
|
+
min: 0,
|
|
35
|
+
max: 1,
|
|
36
|
+
default: 0.2,
|
|
37
|
+
label: "Edge Smoothness",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
private canvas: HTMLCanvasElement | null = null;
|
|
42
|
+
private ctx: CanvasRenderingContext2D | null = null;
|
|
43
|
+
private keyColor = { r: 0, g: 255, b: 0 };
|
|
44
|
+
private threshold = 0.4;
|
|
45
|
+
private smoothness = 0.2;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize the provider
|
|
49
|
+
*/
|
|
50
|
+
async initialize(): Promise<void> {
|
|
51
|
+
this.canvas = document.createElement("canvas");
|
|
52
|
+
this.ctx = this.canvas.getContext("2d", {
|
|
53
|
+
willReadFrequently: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!this.ctx) {
|
|
57
|
+
throw new Error("Failed to create canvas context");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Process a video frame
|
|
63
|
+
*/
|
|
64
|
+
async process(frame: VideoFrame): Promise<FeatureMap[]> {
|
|
65
|
+
if (!this.canvas || !this.ctx) {
|
|
66
|
+
throw new Error("Provider not initialized");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Get frame dimensions
|
|
70
|
+
const width = frame instanceof HTMLVideoElement ? frame.videoWidth : frame.width;
|
|
71
|
+
const height = frame instanceof HTMLVideoElement ? frame.videoHeight : frame.height;
|
|
72
|
+
|
|
73
|
+
// Resize canvas if needed
|
|
74
|
+
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
75
|
+
this.canvas.width = width;
|
|
76
|
+
this.canvas.height = height;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Draw frame to canvas
|
|
80
|
+
this.ctx.drawImage(frame as any, 0, 0, width, height);
|
|
81
|
+
|
|
82
|
+
// Get image data
|
|
83
|
+
const imageData = this.ctx.getImageData(0, 0, width, height);
|
|
84
|
+
const data = imageData.data;
|
|
85
|
+
|
|
86
|
+
// Apply chroma key
|
|
87
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
88
|
+
const r = data[i];
|
|
89
|
+
const g = data[i + 1];
|
|
90
|
+
const b = data[i + 2];
|
|
91
|
+
|
|
92
|
+
// Calculate color distance
|
|
93
|
+
const distance = this.colorDistance(r, g, b, this.keyColor.r, this.keyColor.g, this.keyColor.b);
|
|
94
|
+
|
|
95
|
+
// Calculate alpha based on distance
|
|
96
|
+
let alpha = 1.0;
|
|
97
|
+
if (distance < this.threshold) {
|
|
98
|
+
// Within threshold - transparent
|
|
99
|
+
alpha = 0.0;
|
|
100
|
+
} else if (distance < this.threshold + this.smoothness) {
|
|
101
|
+
// Edge smoothing
|
|
102
|
+
alpha = (distance - this.threshold) / this.smoothness;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Set alpha channel (mask)
|
|
106
|
+
data[i + 3] = Math.floor(alpha * 255);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Put processed data back
|
|
110
|
+
this.ctx.putImageData(imageData, 0, 0);
|
|
111
|
+
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
type: "mask" as FeatureMapType,
|
|
115
|
+
data: {
|
|
116
|
+
type: "mask",
|
|
117
|
+
texture: this.canvas,
|
|
118
|
+
isBinary: false,
|
|
119
|
+
inverted: false,
|
|
120
|
+
},
|
|
121
|
+
metadata: {
|
|
122
|
+
keyColor: this.keyColor,
|
|
123
|
+
threshold: this.threshold,
|
|
124
|
+
smoothness: this.smoothness,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Clean up resources
|
|
132
|
+
*/
|
|
133
|
+
dispose(): void {
|
|
134
|
+
this.canvas = null;
|
|
135
|
+
this.ctx = null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Update configuration
|
|
140
|
+
*/
|
|
141
|
+
updateConfig(config: Record<string, any>): void {
|
|
142
|
+
if (config.keyColor) {
|
|
143
|
+
this.keyColor = this.hexToRgb(config.keyColor);
|
|
144
|
+
}
|
|
145
|
+
if (config.threshold !== undefined) {
|
|
146
|
+
this.threshold = config.threshold;
|
|
147
|
+
}
|
|
148
|
+
if (config.smoothness !== undefined) {
|
|
149
|
+
this.smoothness = config.smoothness;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Calculate color distance (Euclidean)
|
|
155
|
+
*/
|
|
156
|
+
private colorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number {
|
|
157
|
+
const dr = (r1 - r2) / 255;
|
|
158
|
+
const dg = (g1 - g2) / 255;
|
|
159
|
+
const db = (b1 - b2) / 255;
|
|
160
|
+
return Math.sqrt(dr * dr + dg * dg + db * db) / Math.sqrt(3);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Convert hex color to RGB
|
|
165
|
+
*/
|
|
166
|
+
private hexToRgb(hex: string): { r: number; g: number; b: number } {
|
|
167
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
168
|
+
return result
|
|
169
|
+
? {
|
|
170
|
+
r: parseInt(result[1], 16),
|
|
171
|
+
g: parseInt(result[2], 16),
|
|
172
|
+
b: parseInt(result[3], 16),
|
|
173
|
+
}
|
|
174
|
+
: { r: 0, g: 255, b: 0 };
|
|
175
|
+
}
|
|
176
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clypra/feature-providers
|
|
3
|
+
*
|
|
4
|
+
* Extensible feature providers for Body Effect Lab.
|
|
5
|
+
*
|
|
6
|
+
* Feature providers produce feature maps (masks, poses, depth, etc.)
|
|
7
|
+
* that body effects consume. This architecture makes body effects
|
|
8
|
+
* future-proof and infinitely extensible.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Core types
|
|
12
|
+
export * from "./types";
|
|
13
|
+
|
|
14
|
+
// Manager
|
|
15
|
+
export { FeatureProviderManager } from "./manager";
|
|
16
|
+
|
|
17
|
+
// Built-in providers
|
|
18
|
+
export { ChromaKeyProvider } from "./chroma-key";
|
|
19
|
+
export { SegmentationProvider } from "./segmentation";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create default provider manager with built-in providers
|
|
23
|
+
*/
|
|
24
|
+
import { FeatureProviderManager } from "./manager";
|
|
25
|
+
import { ChromaKeyProvider } from "./chroma-key";
|
|
26
|
+
import { SegmentationProvider } from "./segmentation";
|
|
27
|
+
|
|
28
|
+
export function createDefaultProviderManager(): FeatureProviderManager {
|
|
29
|
+
const manager = new FeatureProviderManager();
|
|
30
|
+
|
|
31
|
+
// Register built-in providers
|
|
32
|
+
manager.register(new ChromaKeyProvider());
|
|
33
|
+
manager.register(new SegmentationProvider());
|
|
34
|
+
|
|
35
|
+
return manager;
|
|
36
|
+
}
|
package/src/manager.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clypra/feature-providers — Provider Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages feature provider lifecycle and orchestrates feature map generation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FeatureProvider, IFeatureProviderManager, FeatureMapType, FeatureMap, VideoFrame } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Feature Provider Manager Implementation
|
|
11
|
+
*/
|
|
12
|
+
export class FeatureProviderManager implements IFeatureProviderManager {
|
|
13
|
+
private providers: Map<string, FeatureProvider> = new Map();
|
|
14
|
+
private activeProviders: Set<string> = new Set();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register a provider
|
|
18
|
+
*/
|
|
19
|
+
register(provider: FeatureProvider): void {
|
|
20
|
+
if (this.providers.has(provider.id)) {
|
|
21
|
+
console.warn(`Provider ${provider.id} is already registered. Replacing.`);
|
|
22
|
+
}
|
|
23
|
+
this.providers.set(provider.id, provider);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Activate a provider (initialize it)
|
|
28
|
+
*/
|
|
29
|
+
async activate(providerId: string): Promise<void> {
|
|
30
|
+
const provider = this.providers.get(providerId);
|
|
31
|
+
if (!provider) {
|
|
32
|
+
throw new Error(`Provider ${providerId} not found`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (this.activeProviders.has(providerId)) {
|
|
36
|
+
console.warn(`Provider ${providerId} is already active`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await provider.initialize();
|
|
41
|
+
this.activeProviders.add(providerId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Deactivate a provider
|
|
46
|
+
*/
|
|
47
|
+
deactivate(providerId: string): void {
|
|
48
|
+
const provider = this.providers.get(providerId);
|
|
49
|
+
if (!provider) {
|
|
50
|
+
console.warn(`Provider ${providerId} not found`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!this.activeProviders.has(providerId)) {
|
|
55
|
+
console.warn(`Provider ${providerId} is not active`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
provider.dispose();
|
|
60
|
+
this.activeProviders.delete(providerId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Process frame with all active providers
|
|
65
|
+
*/
|
|
66
|
+
async process(frame: VideoFrame): Promise<Map<FeatureMapType, FeatureMap>> {
|
|
67
|
+
const results = new Map<FeatureMapType, FeatureMap>();
|
|
68
|
+
|
|
69
|
+
for (const providerId of this.activeProviders) {
|
|
70
|
+
const provider = this.providers.get(providerId);
|
|
71
|
+
if (!provider) continue;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const maps = await provider.process(frame);
|
|
75
|
+
|
|
76
|
+
// Store feature maps by type
|
|
77
|
+
for (const map of maps) {
|
|
78
|
+
// If multiple providers produce the same type, last one wins
|
|
79
|
+
// In the future, we could have a priority system or merging strategy
|
|
80
|
+
results.set(map.type, map);
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error(`Error processing with provider ${providerId}:`, error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return results;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get providers that can produce a specific feature type
|
|
92
|
+
*/
|
|
93
|
+
getProvidersForFeature(featureType: FeatureMapType): FeatureProvider[] {
|
|
94
|
+
const providers: FeatureProvider[] = [];
|
|
95
|
+
|
|
96
|
+
for (const provider of this.providers.values()) {
|
|
97
|
+
if (provider.outputs.includes(featureType)) {
|
|
98
|
+
providers.push(provider);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return providers;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get all registered providers
|
|
107
|
+
*/
|
|
108
|
+
getAllProviders(): FeatureProvider[] {
|
|
109
|
+
return Array.from(this.providers.values());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get active providers
|
|
114
|
+
*/
|
|
115
|
+
getActiveProviders(): FeatureProvider[] {
|
|
116
|
+
return Array.from(this.activeProviders)
|
|
117
|
+
.map((id) => this.providers.get(id))
|
|
118
|
+
.filter((p): p is FeatureProvider => p !== undefined);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get a specific provider by ID
|
|
123
|
+
*/
|
|
124
|
+
getProvider(providerId: string): FeatureProvider | undefined {
|
|
125
|
+
return this.providers.get(providerId);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if a provider is active
|
|
130
|
+
*/
|
|
131
|
+
isActive(providerId: string): boolean {
|
|
132
|
+
return this.activeProviders.has(providerId);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Dispose all providers
|
|
137
|
+
*/
|
|
138
|
+
dispose(): void {
|
|
139
|
+
for (const providerId of this.activeProviders) {
|
|
140
|
+
const provider = this.providers.get(providerId);
|
|
141
|
+
if (provider) {
|
|
142
|
+
provider.dispose();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
this.activeProviders.clear();
|
|
146
|
+
this.providers.clear();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clypra/feature-providers — Segmentation Provider
|
|
3
|
+
*
|
|
4
|
+
* Person segmentation using MediaPipe or similar.
|
|
5
|
+
* This is a placeholder implementation - actual ML integration will be added later.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FeatureProvider, FeatureMap, FeatureMapType, VideoFrame, ProviderConfig } from "../types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Segmentation Provider (Placeholder)
|
|
12
|
+
*
|
|
13
|
+
* In production, this would use MediaPipe Selfie Segmentation or similar.
|
|
14
|
+
* For now, it creates a simple center-weighted mask as a placeholder.
|
|
15
|
+
*/
|
|
16
|
+
export class SegmentationProvider implements FeatureProvider {
|
|
17
|
+
id = "mediapipe-segmentation";
|
|
18
|
+
name = "Person Segmentation";
|
|
19
|
+
outputs: FeatureMapType[] = ["mask" as FeatureMapType];
|
|
20
|
+
|
|
21
|
+
config: ProviderConfig = {
|
|
22
|
+
quality: {
|
|
23
|
+
type: "select",
|
|
24
|
+
options: ["low", "medium", "high"],
|
|
25
|
+
default: "medium",
|
|
26
|
+
label: "Quality",
|
|
27
|
+
},
|
|
28
|
+
edgeSmoothing: {
|
|
29
|
+
type: "number",
|
|
30
|
+
min: 0,
|
|
31
|
+
max: 1,
|
|
32
|
+
default: 0.5,
|
|
33
|
+
label: "Edge Smoothing",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
private canvas: HTMLCanvasElement | null = null;
|
|
38
|
+
private ctx: CanvasRenderingContext2D | null = null;
|
|
39
|
+
private quality: "low" | "medium" | "high" = "medium";
|
|
40
|
+
private edgeSmoothing = 0.5;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Initialize the provider
|
|
44
|
+
*
|
|
45
|
+
* TODO: Load MediaPipe model here
|
|
46
|
+
*/
|
|
47
|
+
async initialize(): Promise<void> {
|
|
48
|
+
this.canvas = document.createElement("canvas");
|
|
49
|
+
this.ctx = this.canvas.getContext("2d", {
|
|
50
|
+
willReadFrequently: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!this.ctx) {
|
|
54
|
+
throw new Error("Failed to create canvas context");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// TODO: Initialize MediaPipe
|
|
58
|
+
// await this.initializeMediaPipe();
|
|
59
|
+
|
|
60
|
+
console.log("Segmentation provider initialized (placeholder mode)");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Process a video frame
|
|
65
|
+
*
|
|
66
|
+
* TODO: Use actual segmentation model
|
|
67
|
+
*/
|
|
68
|
+
async process(frame: VideoFrame): Promise<FeatureMap[]> {
|
|
69
|
+
if (!this.canvas || !this.ctx) {
|
|
70
|
+
throw new Error("Provider not initialized");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Get frame dimensions
|
|
74
|
+
const width = frame instanceof HTMLVideoElement ? frame.videoWidth : frame.width;
|
|
75
|
+
const height = frame instanceof HTMLVideoElement ? frame.videoHeight : frame.height;
|
|
76
|
+
|
|
77
|
+
// Resize canvas if needed
|
|
78
|
+
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
79
|
+
this.canvas.width = width;
|
|
80
|
+
this.canvas.height = height;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// TODO: Replace with actual segmentation
|
|
84
|
+
// For now, create a simple radial gradient mask as placeholder
|
|
85
|
+
this.createPlaceholderMask(width, height);
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
type: "mask" as FeatureMapType,
|
|
90
|
+
data: {
|
|
91
|
+
type: "mask",
|
|
92
|
+
texture: this.canvas,
|
|
93
|
+
isBinary: false,
|
|
94
|
+
inverted: false,
|
|
95
|
+
},
|
|
96
|
+
metadata: {
|
|
97
|
+
quality: this.quality,
|
|
98
|
+
edgeSmoothing: this.edgeSmoothing,
|
|
99
|
+
placeholder: true,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Clean up resources
|
|
107
|
+
*/
|
|
108
|
+
dispose(): void {
|
|
109
|
+
// TODO: Dispose MediaPipe resources
|
|
110
|
+
this.canvas = null;
|
|
111
|
+
this.ctx = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Update configuration
|
|
116
|
+
*/
|
|
117
|
+
updateConfig(config: Record<string, any>): void {
|
|
118
|
+
if (config.quality) {
|
|
119
|
+
this.quality = config.quality;
|
|
120
|
+
}
|
|
121
|
+
if (config.edgeSmoothing !== undefined) {
|
|
122
|
+
this.edgeSmoothing = config.edgeSmoothing;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a placeholder mask (radial gradient)
|
|
128
|
+
* This will be replaced with actual segmentation
|
|
129
|
+
*/
|
|
130
|
+
private createPlaceholderMask(width: number, height: number): void {
|
|
131
|
+
if (!this.ctx) return;
|
|
132
|
+
|
|
133
|
+
const imageData = this.ctx.createImageData(width, height);
|
|
134
|
+
const data = imageData.data;
|
|
135
|
+
|
|
136
|
+
const centerX = width / 2;
|
|
137
|
+
const centerY = height / 2;
|
|
138
|
+
const maxRadius = Math.min(width, height) * 0.4;
|
|
139
|
+
|
|
140
|
+
for (let y = 0; y < height; y++) {
|
|
141
|
+
for (let x = 0; x < width; x++) {
|
|
142
|
+
const i = (y * width + x) * 4;
|
|
143
|
+
|
|
144
|
+
// Calculate distance from center
|
|
145
|
+
const dx = x - centerX;
|
|
146
|
+
const dy = y - centerY;
|
|
147
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
148
|
+
|
|
149
|
+
// Calculate alpha based on distance (radial gradient)
|
|
150
|
+
let alpha = 1.0 - Math.min(distance / maxRadius, 1.0);
|
|
151
|
+
|
|
152
|
+
// Apply smoothing
|
|
153
|
+
if (this.edgeSmoothing > 0) {
|
|
154
|
+
alpha = this.smoothStep(alpha, this.edgeSmoothing);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Set RGBA (white with varying alpha)
|
|
158
|
+
data[i] = 255; // R
|
|
159
|
+
data[i + 1] = 255; // G
|
|
160
|
+
data[i + 2] = 255; // B
|
|
161
|
+
data[i + 3] = Math.floor(alpha * 255); // A
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.ctx.putImageData(imageData, 0, 0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Smooth step function for edge smoothing
|
|
170
|
+
*/
|
|
171
|
+
private smoothStep(value: number, smoothness: number): number {
|
|
172
|
+
const t = Math.max(0, Math.min(1, (value - (0.5 - smoothness / 2)) / smoothness));
|
|
173
|
+
return t * t * (3 - 2 * t);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* TODO: Actual MediaPipe integration
|
|
179
|
+
*
|
|
180
|
+
* Example of what the real implementation would look like:
|
|
181
|
+
*
|
|
182
|
+
* import { SelfieSegmentation } from '@mediapipe/selfie_segmentation';
|
|
183
|
+
*
|
|
184
|
+
* async initializeMediaPipe() {
|
|
185
|
+
* this.segmenter = new SelfieSegmentation({
|
|
186
|
+
* locateFile: (file) => {
|
|
187
|
+
* return `https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation/${file}`;
|
|
188
|
+
* }
|
|
189
|
+
* });
|
|
190
|
+
*
|
|
191
|
+
* this.segmenter.setOptions({
|
|
192
|
+
* modelSelection: this.quality === 'high' ? 1 : 0,
|
|
193
|
+
* });
|
|
194
|
+
*
|
|
195
|
+
* await this.segmenter.initialize();
|
|
196
|
+
* }
|
|
197
|
+
*
|
|
198
|
+
* async processWithMediaPipe(frame: VideoFrame): Promise<ImageData> {
|
|
199
|
+
* const results = await this.segmenter.send({ image: frame });
|
|
200
|
+
* return results.segmentationMask;
|
|
201
|
+
* }
|
|
202
|
+
*/
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clypra/feature-providers — Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Core types for extensible feature providers used in Body Effect Lab.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Types of feature maps that providers can produce
|
|
9
|
+
*/
|
|
10
|
+
export enum FeatureMapType {
|
|
11
|
+
// Segmentation
|
|
12
|
+
Mask = "mask",
|
|
13
|
+
MultiMask = "multi-mask",
|
|
14
|
+
|
|
15
|
+
// Spatial
|
|
16
|
+
Pose = "pose",
|
|
17
|
+
FaceMesh = "face-mesh",
|
|
18
|
+
Skeleton = "skeleton",
|
|
19
|
+
Hands = "hands",
|
|
20
|
+
|
|
21
|
+
// Depth
|
|
22
|
+
Depth = "depth",
|
|
23
|
+
Normal = "normal",
|
|
24
|
+
|
|
25
|
+
// Motion
|
|
26
|
+
OpticalFlow = "optical-flow",
|
|
27
|
+
Motion = "motion",
|
|
28
|
+
|
|
29
|
+
// Semantic
|
|
30
|
+
Hair = "hair",
|
|
31
|
+
Eyes = "eyes",
|
|
32
|
+
Skin = "skin",
|
|
33
|
+
|
|
34
|
+
// Identity
|
|
35
|
+
PersonID = "person-id",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Base feature map interface
|
|
40
|
+
*/
|
|
41
|
+
export interface FeatureMap {
|
|
42
|
+
type: FeatureMapType;
|
|
43
|
+
data: FeatureMapData;
|
|
44
|
+
metadata?: Record<string, any>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Union of all feature map data types
|
|
49
|
+
*/
|
|
50
|
+
export type FeatureMapData = MaskData | MultiMaskData | PoseData | DepthData | FlowData | MeshData;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Mask data (binary or alpha mask)
|
|
54
|
+
*/
|
|
55
|
+
export interface MaskData {
|
|
56
|
+
type: "mask";
|
|
57
|
+
texture: HTMLCanvasElement | ImageBitmap | HTMLVideoElement;
|
|
58
|
+
isBinary: boolean;
|
|
59
|
+
inverted?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Multiple masks with IDs (for multi-person scenarios)
|
|
64
|
+
*/
|
|
65
|
+
export interface MultiMaskData {
|
|
66
|
+
type: "multi-mask";
|
|
67
|
+
masks: Array<{
|
|
68
|
+
id: string;
|
|
69
|
+
texture: HTMLCanvasElement | ImageBitmap;
|
|
70
|
+
confidence: number;
|
|
71
|
+
}>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 2D keypoints (pose, hands, face)
|
|
76
|
+
*/
|
|
77
|
+
export interface PoseData {
|
|
78
|
+
type: "pose";
|
|
79
|
+
keypoints: Array<{
|
|
80
|
+
name: string;
|
|
81
|
+
x: number; // Normalized 0-1
|
|
82
|
+
y: number; // Normalized 0-1
|
|
83
|
+
confidence: number;
|
|
84
|
+
visible: boolean;
|
|
85
|
+
}>;
|
|
86
|
+
skeleton?: Array<[string, string]>; // Connections between keypoints
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Depth map data
|
|
91
|
+
*/
|
|
92
|
+
export interface DepthData {
|
|
93
|
+
type: "depth";
|
|
94
|
+
texture: HTMLCanvasElement | ImageBitmap;
|
|
95
|
+
minDepth: number;
|
|
96
|
+
maxDepth: number;
|
|
97
|
+
confidence?: HTMLCanvasElement | ImageBitmap;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Optical flow data
|
|
102
|
+
*/
|
|
103
|
+
export interface FlowData {
|
|
104
|
+
type: "optical-flow";
|
|
105
|
+
texture: HTMLCanvasElement | ImageBitmap; // RG = motion vectors
|
|
106
|
+
maxMagnitude: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 3D mesh data (face mesh, etc.)
|
|
111
|
+
*/
|
|
112
|
+
export interface MeshData {
|
|
113
|
+
type: "face-mesh";
|
|
114
|
+
vertices: Array<{ x: number; y: number; z: number }>;
|
|
115
|
+
indices: number[];
|
|
116
|
+
uvs?: Array<{ u: number; v: number }>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Configuration value types
|
|
121
|
+
*/
|
|
122
|
+
export type ConfigValue = { type: "number"; min: number; max: number; default: number; label?: string } | { type: "boolean"; default: boolean; label?: string } | { type: "select"; options: string[]; default: string; label?: string } | { type: "color"; default: string; label?: string };
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Provider configuration schema
|
|
126
|
+
*/
|
|
127
|
+
export interface ProviderConfig {
|
|
128
|
+
[key: string]: ConfigValue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Video frame input (can be HTMLVideoElement, canvas, or bitmap)
|
|
133
|
+
*/
|
|
134
|
+
export type VideoFrame = HTMLVideoElement | HTMLCanvasElement | ImageBitmap;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Feature Provider interface
|
|
138
|
+
*
|
|
139
|
+
* All feature providers must implement this interface.
|
|
140
|
+
* Providers produce feature maps that body effects consume.
|
|
141
|
+
*/
|
|
142
|
+
export interface FeatureProvider {
|
|
143
|
+
/** Unique identifier */
|
|
144
|
+
id: string;
|
|
145
|
+
|
|
146
|
+
/** Human-readable name */
|
|
147
|
+
name: string;
|
|
148
|
+
|
|
149
|
+
/** Feature maps this provider produces */
|
|
150
|
+
outputs: FeatureMapType[];
|
|
151
|
+
|
|
152
|
+
/** Configuration schema */
|
|
153
|
+
config?: ProviderConfig;
|
|
154
|
+
|
|
155
|
+
/** Initialize the provider (load models, allocate resources) */
|
|
156
|
+
initialize(): Promise<void>;
|
|
157
|
+
|
|
158
|
+
/** Process a video frame and produce feature maps */
|
|
159
|
+
process(frame: VideoFrame): Promise<FeatureMap[]>;
|
|
160
|
+
|
|
161
|
+
/** Clean up resources */
|
|
162
|
+
dispose(): void;
|
|
163
|
+
|
|
164
|
+
/** Update configuration */
|
|
165
|
+
updateConfig?(config: Record<string, any>): void;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Feature provider manager
|
|
170
|
+
*/
|
|
171
|
+
export interface IFeatureProviderManager {
|
|
172
|
+
/** Register a provider */
|
|
173
|
+
register(provider: FeatureProvider): void;
|
|
174
|
+
|
|
175
|
+
/** Activate a provider (initialize and make ready) */
|
|
176
|
+
activate(providerId: string): Promise<void>;
|
|
177
|
+
|
|
178
|
+
/** Deactivate a provider */
|
|
179
|
+
deactivate(providerId: string): void;
|
|
180
|
+
|
|
181
|
+
/** Process frame with active providers */
|
|
182
|
+
process(frame: VideoFrame): Promise<Map<FeatureMapType, FeatureMap>>;
|
|
183
|
+
|
|
184
|
+
/** Get providers that can produce a specific feature type */
|
|
185
|
+
getProvidersForFeature(featureType: FeatureMapType): FeatureProvider[];
|
|
186
|
+
|
|
187
|
+
/** Get all registered providers */
|
|
188
|
+
getAllProviders(): FeatureProvider[];
|
|
189
|
+
|
|
190
|
+
/** Get active providers */
|
|
191
|
+
getActiveProviders(): FeatureProvider[];
|
|
192
|
+
|
|
193
|
+
/** Dispose all providers */
|
|
194
|
+
dispose(): void;
|
|
195
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2022", "DOM"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"resolveJsonModule": true,
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"outDir": "./dist",
|
|
17
|
+
"rootDir": "./src"
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*"],
|
|
20
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
21
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: {
|
|
5
|
+
index: "src/index.ts",
|
|
6
|
+
types: "src/types.ts",
|
|
7
|
+
"segmentation/index": "src/segmentation/index.ts",
|
|
8
|
+
"chroma-key/index": "src/chroma-key/index.ts",
|
|
9
|
+
},
|
|
10
|
+
format: ["esm"],
|
|
11
|
+
dts: true,
|
|
12
|
+
sourcemap: true,
|
|
13
|
+
clean: true,
|
|
14
|
+
splitting: false,
|
|
15
|
+
treeshake: true,
|
|
16
|
+
});
|