@ume-group/contracts 0.2.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 +37 -0
- package/dist/adserving.d.ts +150 -0
- package/dist/adserving.d.ts.map +1 -0
- package/dist/adserving.js +8 -0
- package/dist/campaigns.d.ts +37 -0
- package/dist/campaigns.d.ts.map +1 -0
- package/dist/campaigns.js +8 -0
- package/dist/gausst.d.ts +236 -0
- package/dist/gausst.d.ts.map +1 -0
- package/dist/gausst.js +307 -0
- package/dist/gausst.test.d.ts +2 -0
- package/dist/gausst.test.d.ts.map +1 -0
- package/dist/gausst.test.js +71 -0
- package/dist/index.d.ts +1531 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1112 -0
- package/dist/layer2/index.d.ts +9 -0
- package/dist/layer2/index.d.ts.map +1 -0
- package/dist/layer2/index.js +10 -0
- package/dist/layer2/shaders.d.ts +185 -0
- package/dist/layer2/shaders.d.ts.map +1 -0
- package/dist/layer2/shaders.js +604 -0
- package/dist/layer2/webcam-utils.d.ts +113 -0
- package/dist/layer2/webcam-utils.d.ts.map +1 -0
- package/dist/layer2/webcam-utils.js +147 -0
- package/dist/layer2/webcam-utils.test.d.ts +2 -0
- package/dist/layer2/webcam-utils.test.d.ts.map +1 -0
- package/dist/layer2/webcam-utils.test.js +18 -0
- package/dist/layer2.d.ts +558 -0
- package/dist/layer2.d.ts.map +1 -0
- package/dist/layer2.js +376 -0
- package/dist/layer2.test.d.ts +2 -0
- package/dist/layer2.test.d.ts.map +1 -0
- package/dist/layer2.test.js +65 -0
- package/dist/perspective.d.ts +28 -0
- package/dist/perspective.d.ts.map +1 -0
- package/dist/perspective.js +157 -0
- package/dist/segmentation/MediaPipeSegmenter.d.ts +201 -0
- package/dist/segmentation/MediaPipeSegmenter.d.ts.map +1 -0
- package/dist/segmentation/MediaPipeSegmenter.js +434 -0
- package/dist/segmentation/index.d.ts +5 -0
- package/dist/segmentation/index.d.ts.map +1 -0
- package/dist/segmentation/index.js +4 -0
- package/dist/webcam/GarbageMatteDragManager.d.ts +63 -0
- package/dist/webcam/GarbageMatteDragManager.d.ts.map +1 -0
- package/dist/webcam/GarbageMatteDragManager.js +183 -0
- package/dist/webcam/WebcamStreamManager.d.ts +103 -0
- package/dist/webcam/WebcamStreamManager.d.ts.map +1 -0
- package/dist/webcam/WebcamStreamManager.js +356 -0
- package/dist/webcam/index.d.ts +5 -0
- package/dist/webcam/index.d.ts.map +1 -0
- package/dist/webcam/index.js +2 -0
- package/openapi/admetise.yaml +632 -0
- package/openapi/includu.yaml +621 -0
- package/openapi/integration.yaml +372 -0
- package/openapi/shared/schemas.yaml +227 -0
- package/package.json +53 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MediaPipeSegmenter.ts - AI-based background segmentation using MediaPipe
|
|
3
|
+
*
|
|
4
|
+
* Wraps MediaPipe Image Segmenter to process webcam frames and produce
|
|
5
|
+
* segmentation masks for background removal.
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - Async initialization (model loading)
|
|
9
|
+
* - Frame-by-frame processing with mask output
|
|
10
|
+
* - Mobile-optimized resolution scaling
|
|
11
|
+
* - Temporal smoothing with anti-flicker measures
|
|
12
|
+
* - Proper resource cleanup
|
|
13
|
+
*
|
|
14
|
+
* ArchiMate: Application Component (AI Segmentation Engine)
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* AI model quality mode
|
|
18
|
+
* - 'fast': selfie_segmenter (float16, 256x256) — quick, good for close-up
|
|
19
|
+
* - 'quality': DeepLabV3 (float32, PASCAL VOC 21-class) — better at distance, slower
|
|
20
|
+
*/
|
|
21
|
+
export type AIModelType = 'fast' | 'quality';
|
|
22
|
+
/**
|
|
23
|
+
* Asset URLs for MediaPipe loading
|
|
24
|
+
* Can be configured to use self-hosted assets for better reliability at scale
|
|
25
|
+
*/
|
|
26
|
+
export interface MediaPipeAssetUrls {
|
|
27
|
+
/** WASM files URL (default: jsdelivr CDN) */
|
|
28
|
+
wasmUrl: string;
|
|
29
|
+
/** Model file URL (default: Google Storage) */
|
|
30
|
+
modelUrl: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Default asset URLs - external CDNs
|
|
34
|
+
* For production at scale, self-host these on Cloudflare R2
|
|
35
|
+
*/
|
|
36
|
+
export declare const DEFAULT_ASSET_URLS: MediaPipeAssetUrls;
|
|
37
|
+
/**
|
|
38
|
+
* DeepLabV3 model URL — PASCAL VOC 21-class segmentation
|
|
39
|
+
* Person = class index 15. Float32, heavier but better at distance.
|
|
40
|
+
*/
|
|
41
|
+
export declare const DEEPLAB_MODEL_URL = "https://storage.googleapis.com/mediapipe-models/image_segmenter/deeplab_v3/float32/latest/deeplab_v3.tflite";
|
|
42
|
+
/**
|
|
43
|
+
* Self-hosted asset URLs on Includu R2 (for enterprise scale)
|
|
44
|
+
* TODO: Upload MediaPipe assets to R2 and enable these URLs
|
|
45
|
+
*/
|
|
46
|
+
export declare const SELF_HOSTED_ASSET_URLS: MediaPipeAssetUrls;
|
|
47
|
+
/**
|
|
48
|
+
* Configuration for the segmenter
|
|
49
|
+
*/
|
|
50
|
+
export interface SegmenterConfig {
|
|
51
|
+
/** Processing resolution width (default: 640) */
|
|
52
|
+
width: number;
|
|
53
|
+
/** Processing resolution height (default: 480) */
|
|
54
|
+
height: number;
|
|
55
|
+
/** Mask threshold 0-1 (default: 0.7) */
|
|
56
|
+
threshold: number;
|
|
57
|
+
/** Edge blur radius in pixels (default: 3) */
|
|
58
|
+
edgeBlur: number;
|
|
59
|
+
/** Asset URLs for MediaPipe (default: external CDNs) */
|
|
60
|
+
assetUrls?: MediaPipeAssetUrls;
|
|
61
|
+
/** Model type: 'fast' (selfie_segmenter) or 'quality' (DeepLabV3) */
|
|
62
|
+
modelType?: AIModelType;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Default configuration optimized for balance of quality and performance
|
|
66
|
+
*/
|
|
67
|
+
export declare const DEFAULT_SEGMENTER_CONFIG: SegmenterConfig;
|
|
68
|
+
/**
|
|
69
|
+
* Result from segmentation processing
|
|
70
|
+
*/
|
|
71
|
+
export interface SegmentationResult {
|
|
72
|
+
/** The mask as ImageData (grayscale, white = person) */
|
|
73
|
+
maskData: ImageData;
|
|
74
|
+
/** The mask canvas element (can be used as texture source) */
|
|
75
|
+
maskCanvas: HTMLCanvasElement;
|
|
76
|
+
/** Processing time in milliseconds */
|
|
77
|
+
processingTimeMs: number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* MediaPipe-based background segmentation
|
|
81
|
+
*
|
|
82
|
+
* Usage:
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const segmenter = new MediaPipeSegmenter();
|
|
85
|
+
* await segmenter.initialize();
|
|
86
|
+
*
|
|
87
|
+
* // In render loop
|
|
88
|
+
* const result = segmenter.process(videoElement);
|
|
89
|
+
* // Use result.maskCanvas as texture source
|
|
90
|
+
*
|
|
91
|
+
* // Cleanup
|
|
92
|
+
* segmenter.dispose();
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export declare class MediaPipeSegmenter {
|
|
96
|
+
private segmenter;
|
|
97
|
+
private workCanvas;
|
|
98
|
+
private workCtx;
|
|
99
|
+
private outputCanvas;
|
|
100
|
+
private outputCtx;
|
|
101
|
+
private tempCanvas;
|
|
102
|
+
private tempCtx;
|
|
103
|
+
private config;
|
|
104
|
+
private isInitialized;
|
|
105
|
+
private isInitializing;
|
|
106
|
+
private _modelType;
|
|
107
|
+
private static readonly DEEPLAB_PERSON_INDEX;
|
|
108
|
+
private previousAlpha;
|
|
109
|
+
private backgroundPersistence;
|
|
110
|
+
private readonly persistenceThreshold;
|
|
111
|
+
private readonly persistenceThresholdLowLatency;
|
|
112
|
+
private readonly temporalBlendNormal;
|
|
113
|
+
private readonly temporalBlendLowLatency;
|
|
114
|
+
private readonly hysteresisOffset;
|
|
115
|
+
private lowLatencyMode;
|
|
116
|
+
constructor(config?: Partial<SegmenterConfig>);
|
|
117
|
+
/**
|
|
118
|
+
* Initialize MediaPipe model
|
|
119
|
+
*/
|
|
120
|
+
initialize(): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Process a video frame and return segmentation mask
|
|
123
|
+
*/
|
|
124
|
+
process(video: HTMLVideoElement): SegmentationResult | null;
|
|
125
|
+
/**
|
|
126
|
+
* Process MediaPipe result into a usable mask
|
|
127
|
+
*/
|
|
128
|
+
private processMask;
|
|
129
|
+
/**
|
|
130
|
+
* Copy work canvas to output canvas
|
|
131
|
+
*/
|
|
132
|
+
private copyWorkToOutput;
|
|
133
|
+
/**
|
|
134
|
+
* Apply morphological dilation to fill small holes in the mask.
|
|
135
|
+
* Uses a 4-connected max-filter: draws the mask shifted 1px in each
|
|
136
|
+
* cardinal direction with 'lighten' blend (pixel max). This expands
|
|
137
|
+
* bright regions by 1px, filling 1-2px holes common at distance.
|
|
138
|
+
*/
|
|
139
|
+
private applyDilation;
|
|
140
|
+
/**
|
|
141
|
+
* Apply Gaussian blur to soften mask edges
|
|
142
|
+
*/
|
|
143
|
+
private applyEdgeBlur;
|
|
144
|
+
/**
|
|
145
|
+
* Reset temporal smoothing buffers.
|
|
146
|
+
* Call when the video source changes (camera switch) to avoid
|
|
147
|
+
* stale mask data from the previous camera bleeding into the new one.
|
|
148
|
+
*/
|
|
149
|
+
reset(): void;
|
|
150
|
+
/**
|
|
151
|
+
* Update segmentation threshold
|
|
152
|
+
*/
|
|
153
|
+
setThreshold(threshold: number): void;
|
|
154
|
+
/**
|
|
155
|
+
* Update edge blur radius
|
|
156
|
+
*/
|
|
157
|
+
setEdgeBlur(edgeBlur: number): void;
|
|
158
|
+
/**
|
|
159
|
+
* Enable/disable low latency mode
|
|
160
|
+
*/
|
|
161
|
+
setLowLatency(enabled: boolean): void;
|
|
162
|
+
/**
|
|
163
|
+
* Switch between fast (selfie_segmenter) and quality (DeepLabV3) models.
|
|
164
|
+
* Disposes the current model and re-initializes with the new one.
|
|
165
|
+
* Returns a promise that resolves when the new model is ready.
|
|
166
|
+
*/
|
|
167
|
+
switchModel(modelType: AIModelType): Promise<void>;
|
|
168
|
+
/**
|
|
169
|
+
* Get the current model type
|
|
170
|
+
*/
|
|
171
|
+
get currentModelType(): AIModelType;
|
|
172
|
+
/**
|
|
173
|
+
* Get current configuration
|
|
174
|
+
*/
|
|
175
|
+
getConfig(): SegmenterConfig;
|
|
176
|
+
/**
|
|
177
|
+
* Check if segmenter is ready
|
|
178
|
+
*/
|
|
179
|
+
get ready(): boolean;
|
|
180
|
+
/**
|
|
181
|
+
* Check if segmenter is currently loading
|
|
182
|
+
*/
|
|
183
|
+
get loading(): boolean;
|
|
184
|
+
/**
|
|
185
|
+
* Get the output mask canvas for use as texture source
|
|
186
|
+
*/
|
|
187
|
+
getMaskCanvas(): HTMLCanvasElement;
|
|
188
|
+
/**
|
|
189
|
+
* Clean up resources
|
|
190
|
+
*/
|
|
191
|
+
dispose(): void;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Detect if running on mobile device
|
|
195
|
+
*/
|
|
196
|
+
export declare function isMobileDevice(): boolean;
|
|
197
|
+
/**
|
|
198
|
+
* Get optimal processing configuration based on device
|
|
199
|
+
*/
|
|
200
|
+
export declare function getOptimalConfig(): Partial<SegmenterConfig>;
|
|
201
|
+
//# sourceMappingURL=MediaPipeSegmenter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaPipeSegmenter.d.ts","sourceRoot":"","sources":["../../src/segmentation/MediaPipeSegmenter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MediaPipeSegmenter.ts - AI-based background segmentation using MediaPipe
|
|
3
|
+
*
|
|
4
|
+
* Wraps MediaPipe Image Segmenter to process webcam frames and produce
|
|
5
|
+
* segmentation masks for background removal.
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - Async initialization (model loading)
|
|
9
|
+
* - Frame-by-frame processing with mask output
|
|
10
|
+
* - Mobile-optimized resolution scaling
|
|
11
|
+
* - Temporal smoothing with anti-flicker measures
|
|
12
|
+
* - Proper resource cleanup
|
|
13
|
+
*
|
|
14
|
+
* ArchiMate: Application Component (AI Segmentation Engine)
|
|
15
|
+
*/
|
|
16
|
+
import { DEBUG } from '../index';
|
|
17
|
+
import { ImageSegmenter, FilesetResolver } from '@mediapipe/tasks-vision';
|
|
18
|
+
/**
|
|
19
|
+
* Default asset URLs - external CDNs
|
|
20
|
+
* For production at scale, self-host these on Cloudflare R2
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_ASSET_URLS = {
|
|
23
|
+
wasmUrl: 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.32/wasm',
|
|
24
|
+
modelUrl: 'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite'
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* DeepLabV3 model URL — PASCAL VOC 21-class segmentation
|
|
28
|
+
* Person = class index 15. Float32, heavier but better at distance.
|
|
29
|
+
*/
|
|
30
|
+
export const DEEPLAB_MODEL_URL = 'https://storage.googleapis.com/mediapipe-models/image_segmenter/deeplab_v3/float32/latest/deeplab_v3.tflite';
|
|
31
|
+
/**
|
|
32
|
+
* Self-hosted asset URLs on Includu R2 (for enterprise scale)
|
|
33
|
+
* TODO: Upload MediaPipe assets to R2 and enable these URLs
|
|
34
|
+
*/
|
|
35
|
+
export const SELF_HOSTED_ASSET_URLS = {
|
|
36
|
+
wasmUrl: 'https://pub-55570832bee446f4a0bb149a53ad7050.r2.dev/mediapipe/wasm',
|
|
37
|
+
modelUrl: 'https://pub-55570832bee446f4a0bb149a53ad7050.r2.dev/mediapipe/models/selfie_segmenter.tflite'
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Default configuration optimized for balance of quality and performance
|
|
41
|
+
*/
|
|
42
|
+
export const DEFAULT_SEGMENTER_CONFIG = {
|
|
43
|
+
width: 640,
|
|
44
|
+
height: 480,
|
|
45
|
+
threshold: 0.7,
|
|
46
|
+
edgeBlur: 3,
|
|
47
|
+
assetUrls: DEFAULT_ASSET_URLS
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* MediaPipe-based background segmentation
|
|
51
|
+
*
|
|
52
|
+
* Usage:
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const segmenter = new MediaPipeSegmenter();
|
|
55
|
+
* await segmenter.initialize();
|
|
56
|
+
*
|
|
57
|
+
* // In render loop
|
|
58
|
+
* const result = segmenter.process(videoElement);
|
|
59
|
+
* // Use result.maskCanvas as texture source
|
|
60
|
+
*
|
|
61
|
+
* // Cleanup
|
|
62
|
+
* segmenter.dispose();
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export class MediaPipeSegmenter {
|
|
66
|
+
segmenter = null;
|
|
67
|
+
// Double-buffered mask canvases to prevent flickering
|
|
68
|
+
workCanvas;
|
|
69
|
+
workCtx;
|
|
70
|
+
outputCanvas;
|
|
71
|
+
outputCtx;
|
|
72
|
+
tempCanvas;
|
|
73
|
+
tempCtx;
|
|
74
|
+
config;
|
|
75
|
+
isInitialized = false;
|
|
76
|
+
isInitializing = false;
|
|
77
|
+
_modelType = 'fast';
|
|
78
|
+
// DeepLabV3 PASCAL VOC person class index
|
|
79
|
+
static DEEPLAB_PERSON_INDEX = 15;
|
|
80
|
+
// Temporal smoothing buffers
|
|
81
|
+
previousAlpha = null;
|
|
82
|
+
backgroundPersistence = null;
|
|
83
|
+
// Anti-flicker settings — tuned to prevent "big chunk" flicker at ~1m distance
|
|
84
|
+
// where the model's confidence oscillates on large body regions.
|
|
85
|
+
// Persistence: how many consecutive low-confidence frames before converting to background
|
|
86
|
+
// Higher values = more stable but slower to remove background when person moves out
|
|
87
|
+
persistenceThreshold = 8;
|
|
88
|
+
persistenceThresholdLowLatency = 4;
|
|
89
|
+
// Temporal blend: how much of current frame vs previous (1.0 = all current, 0.0 = all previous)
|
|
90
|
+
// Lower values = smoother but more lag; critical for noisy confidence at distance
|
|
91
|
+
temporalBlendNormal = 0.40;
|
|
92
|
+
temporalBlendLowLatency = 0.65;
|
|
93
|
+
// Hysteresis: once a pixel is "person", it needs to drop further to become background
|
|
94
|
+
// Higher values = more stable at edges where confidence fluctuates
|
|
95
|
+
hysteresisOffset = 0.25;
|
|
96
|
+
lowLatencyMode = true;
|
|
97
|
+
constructor(config = {}) {
|
|
98
|
+
this.config = { ...DEFAULT_SEGMENTER_CONFIG, ...config };
|
|
99
|
+
this._modelType = this.config.modelType ?? 'fast';
|
|
100
|
+
// Create work canvas
|
|
101
|
+
this.workCanvas = document.createElement('canvas');
|
|
102
|
+
this.workCanvas.width = this.config.width;
|
|
103
|
+
this.workCanvas.height = this.config.height;
|
|
104
|
+
this.workCtx = this.workCanvas.getContext('2d', { willReadFrequently: true });
|
|
105
|
+
// Create output canvas
|
|
106
|
+
this.outputCanvas = document.createElement('canvas');
|
|
107
|
+
this.outputCanvas.width = this.config.width;
|
|
108
|
+
this.outputCanvas.height = this.config.height;
|
|
109
|
+
this.outputCtx = this.outputCanvas.getContext('2d', { willReadFrequently: true });
|
|
110
|
+
// Create temp canvas
|
|
111
|
+
this.tempCanvas = document.createElement('canvas');
|
|
112
|
+
this.tempCanvas.width = this.config.width;
|
|
113
|
+
this.tempCanvas.height = this.config.height;
|
|
114
|
+
this.tempCtx = this.tempCanvas.getContext('2d', { willReadFrequently: true });
|
|
115
|
+
// Initialize buffers
|
|
116
|
+
this.previousAlpha = new Uint8Array(this.config.width * this.config.height);
|
|
117
|
+
this.backgroundPersistence = new Uint8Array(this.config.width * this.config.height);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Initialize MediaPipe model
|
|
121
|
+
*/
|
|
122
|
+
async initialize() {
|
|
123
|
+
if (this.isInitialized || this.isInitializing) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.isInitializing = true;
|
|
127
|
+
try {
|
|
128
|
+
const assetUrls = this.config.assetUrls ?? DEFAULT_ASSET_URLS;
|
|
129
|
+
if (DEBUG)
|
|
130
|
+
console.log('[MediaPipeSegmenter] Loading MediaPipe vision WASM from:', assetUrls.wasmUrl);
|
|
131
|
+
const vision = await FilesetResolver.forVisionTasks(assetUrls.wasmUrl);
|
|
132
|
+
// Select model URL based on modelType
|
|
133
|
+
const modelUrl = this._modelType === 'quality'
|
|
134
|
+
? DEEPLAB_MODEL_URL
|
|
135
|
+
: assetUrls.modelUrl;
|
|
136
|
+
if (DEBUG)
|
|
137
|
+
console.log(`[MediaPipeSegmenter] Creating image segmenter (${this._modelType}) with model:`, modelUrl);
|
|
138
|
+
// CPU delegate avoids WebGL context conflicts with Three.js.
|
|
139
|
+
// GPU delegate creates its own WebGL context which can produce
|
|
140
|
+
// all-zero inference results when sharing the GPU with Three.js.
|
|
141
|
+
this.segmenter = await ImageSegmenter.createFromOptions(vision, {
|
|
142
|
+
baseOptions: {
|
|
143
|
+
modelAssetPath: modelUrl,
|
|
144
|
+
delegate: 'CPU'
|
|
145
|
+
},
|
|
146
|
+
runningMode: 'VIDEO',
|
|
147
|
+
outputCategoryMask: false,
|
|
148
|
+
outputConfidenceMasks: true
|
|
149
|
+
});
|
|
150
|
+
this.isInitialized = true;
|
|
151
|
+
if (DEBUG)
|
|
152
|
+
console.log(`[MediaPipeSegmenter] Initialization complete (${this._modelType})`);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.error('[MediaPipeSegmenter] Failed to initialize:', error);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
this.isInitializing = false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Process a video frame and return segmentation mask
|
|
164
|
+
*/
|
|
165
|
+
process(video) {
|
|
166
|
+
if (!this.isInitialized || !this.segmenter) {
|
|
167
|
+
console.warn('[MediaPipeSegmenter] Not initialized, skipping frame');
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
if (video.readyState < 2) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const startTime = performance.now();
|
|
174
|
+
try {
|
|
175
|
+
// Contrast enhancement: mild boost helps the model distinguish
|
|
176
|
+
// person from background, especially at distance or in flat lighting.
|
|
177
|
+
// Applied before inference so the model sees a clearer input.
|
|
178
|
+
this.tempCtx.filter = 'contrast(1.15) saturate(1.1)';
|
|
179
|
+
this.tempCtx.drawImage(video, 0, 0, this.config.width, this.config.height);
|
|
180
|
+
this.tempCtx.filter = 'none';
|
|
181
|
+
const result = this.segmenter.segmentForVideo(this.tempCanvas, performance.now());
|
|
182
|
+
this.processMask(result);
|
|
183
|
+
const processingTimeMs = performance.now() - startTime;
|
|
184
|
+
return {
|
|
185
|
+
maskData: this.outputCtx.getImageData(0, 0, this.config.width, this.config.height),
|
|
186
|
+
maskCanvas: this.outputCanvas,
|
|
187
|
+
processingTimeMs
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
console.error('[MediaPipeSegmenter] Processing error:', error);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Process MediaPipe result into a usable mask
|
|
197
|
+
*/
|
|
198
|
+
processMask(result) {
|
|
199
|
+
const confidenceMasks = result.confidenceMasks;
|
|
200
|
+
if (!confidenceMasks || confidenceMasks.length === 0) {
|
|
201
|
+
this.workCtx.clearRect(0, 0, this.config.width, this.config.height);
|
|
202
|
+
this.copyWorkToOutput();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// selfie_segmenter: person = confidenceMasks[0]
|
|
206
|
+
// DeepLabV3 PASCAL VOC: person = confidenceMasks[15] (21 classes)
|
|
207
|
+
const personIndex = this._modelType === 'quality'
|
|
208
|
+
? MediaPipeSegmenter.DEEPLAB_PERSON_INDEX
|
|
209
|
+
: 0;
|
|
210
|
+
if (personIndex >= confidenceMasks.length) {
|
|
211
|
+
this.workCtx.clearRect(0, 0, this.config.width, this.config.height);
|
|
212
|
+
this.copyWorkToOutput();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const personMask = confidenceMasks[personIndex];
|
|
216
|
+
const maskData = personMask.getAsFloat32Array();
|
|
217
|
+
const imageData = this.workCtx.createImageData(this.config.width, this.config.height);
|
|
218
|
+
const data = imageData.data;
|
|
219
|
+
const threshold = this.config.threshold;
|
|
220
|
+
const blendFactor = this.lowLatencyMode ? this.temporalBlendLowLatency : this.temporalBlendNormal;
|
|
221
|
+
const prevAlpha = this.previousAlpha;
|
|
222
|
+
const persistence = this.backgroundPersistence;
|
|
223
|
+
const persistThreshold = this.lowLatencyMode ? this.persistenceThresholdLowLatency : this.persistenceThreshold;
|
|
224
|
+
const hysteresis = this.hysteresisOffset;
|
|
225
|
+
for (let i = 0; i < maskData.length; i++) {
|
|
226
|
+
const confidence = maskData[i];
|
|
227
|
+
const wasPerson = prevAlpha && prevAlpha[i] > 127;
|
|
228
|
+
const effectiveThreshold = wasPerson ? threshold - hysteresis : threshold;
|
|
229
|
+
const isPersonThisFrame = confidence >= effectiveThreshold;
|
|
230
|
+
let alpha;
|
|
231
|
+
if (isPersonThisFrame) {
|
|
232
|
+
if (persistence) {
|
|
233
|
+
persistence[i] = 0;
|
|
234
|
+
}
|
|
235
|
+
if (confidence >= effectiveThreshold + 0.1) {
|
|
236
|
+
alpha = 255;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
alpha = Math.round(((confidence - effectiveThreshold) / 0.1) * 127.5 + 127.5);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
if (persistence) {
|
|
244
|
+
persistence[i] = Math.min(255, persistence[i] + 1);
|
|
245
|
+
if (persistence[i] < persistThreshold) {
|
|
246
|
+
alpha = prevAlpha ? prevAlpha[i] : 0;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
if (confidence <= effectiveThreshold - 0.1) {
|
|
250
|
+
alpha = 0;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
alpha = Math.round(((confidence - (effectiveThreshold - 0.1)) / 0.1) * 127.5);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
alpha = confidence <= effectiveThreshold - 0.1 ? 0 :
|
|
259
|
+
Math.round(((confidence - (effectiveThreshold - 0.1)) / 0.2) * 255);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (prevAlpha) {
|
|
263
|
+
alpha = Math.round(alpha * blendFactor + prevAlpha[i] * (1 - blendFactor));
|
|
264
|
+
prevAlpha[i] = alpha;
|
|
265
|
+
}
|
|
266
|
+
data[i * 4] = alpha;
|
|
267
|
+
data[i * 4 + 1] = alpha;
|
|
268
|
+
data[i * 4 + 2] = alpha;
|
|
269
|
+
data[i * 4 + 3] = 255;
|
|
270
|
+
}
|
|
271
|
+
this.workCtx.putImageData(imageData, 0, 0);
|
|
272
|
+
// Dilation pass: fills small 1-2px holes common when subject is far from camera
|
|
273
|
+
this.applyDilation();
|
|
274
|
+
if (this.config.edgeBlur > 0) {
|
|
275
|
+
this.applyEdgeBlur();
|
|
276
|
+
}
|
|
277
|
+
this.copyWorkToOutput();
|
|
278
|
+
// Close all returned masks to free memory.
|
|
279
|
+
// DeepLabV3 returns 21 masks (PASCAL VOC classes), selfie_segmenter returns 1.
|
|
280
|
+
for (const mask of confidenceMasks) {
|
|
281
|
+
mask.close();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Copy work canvas to output canvas
|
|
286
|
+
*/
|
|
287
|
+
copyWorkToOutput() {
|
|
288
|
+
this.outputCtx.drawImage(this.workCanvas, 0, 0);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Apply morphological dilation to fill small holes in the mask.
|
|
292
|
+
* Uses a 4-connected max-filter: draws the mask shifted 1px in each
|
|
293
|
+
* cardinal direction with 'lighten' blend (pixel max). This expands
|
|
294
|
+
* bright regions by 1px, filling 1-2px holes common at distance.
|
|
295
|
+
*/
|
|
296
|
+
applyDilation() {
|
|
297
|
+
// Copy current mask to temp canvas as read-only source
|
|
298
|
+
// (tempCanvas is safe to reuse — video frame processing is complete)
|
|
299
|
+
this.tempCtx.clearRect(0, 0, this.config.width, this.config.height);
|
|
300
|
+
this.tempCtx.drawImage(this.workCanvas, 0, 0);
|
|
301
|
+
// Draw 4 shifted copies using 'lighten' (takes max per-pixel)
|
|
302
|
+
this.workCtx.save();
|
|
303
|
+
this.workCtx.globalCompositeOperation = 'lighten';
|
|
304
|
+
this.workCtx.drawImage(this.tempCanvas, -1, 0);
|
|
305
|
+
this.workCtx.drawImage(this.tempCanvas, 1, 0);
|
|
306
|
+
this.workCtx.drawImage(this.tempCanvas, 0, -1);
|
|
307
|
+
this.workCtx.drawImage(this.tempCanvas, 0, 1);
|
|
308
|
+
this.workCtx.restore();
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Apply Gaussian blur to soften mask edges
|
|
312
|
+
*/
|
|
313
|
+
applyEdgeBlur() {
|
|
314
|
+
this.workCtx.filter = `blur(${this.config.edgeBlur}px)`;
|
|
315
|
+
this.workCtx.drawImage(this.workCanvas, 0, 0);
|
|
316
|
+
this.workCtx.filter = 'none';
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Reset temporal smoothing buffers.
|
|
320
|
+
* Call when the video source changes (camera switch) to avoid
|
|
321
|
+
* stale mask data from the previous camera bleeding into the new one.
|
|
322
|
+
*/
|
|
323
|
+
reset() {
|
|
324
|
+
if (this.previousAlpha)
|
|
325
|
+
this.previousAlpha.fill(0);
|
|
326
|
+
if (this.backgroundPersistence)
|
|
327
|
+
this.backgroundPersistence.fill(0);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Update segmentation threshold
|
|
331
|
+
*/
|
|
332
|
+
setThreshold(threshold) {
|
|
333
|
+
this.config.threshold = Math.max(0, Math.min(1, threshold));
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Update edge blur radius
|
|
337
|
+
*/
|
|
338
|
+
setEdgeBlur(edgeBlur) {
|
|
339
|
+
this.config.edgeBlur = Math.max(0, Math.min(20, edgeBlur));
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Enable/disable low latency mode
|
|
343
|
+
*/
|
|
344
|
+
setLowLatency(enabled) {
|
|
345
|
+
this.lowLatencyMode = enabled;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Switch between fast (selfie_segmenter) and quality (DeepLabV3) models.
|
|
349
|
+
* Disposes the current model and re-initializes with the new one.
|
|
350
|
+
* Returns a promise that resolves when the new model is ready.
|
|
351
|
+
*/
|
|
352
|
+
async switchModel(modelType) {
|
|
353
|
+
if (modelType === this._modelType && this.isInitialized) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
this._modelType = modelType;
|
|
357
|
+
// Dispose current model and re-initialize
|
|
358
|
+
if (this.segmenter) {
|
|
359
|
+
this.segmenter.close();
|
|
360
|
+
this.segmenter = null;
|
|
361
|
+
}
|
|
362
|
+
this.isInitialized = false;
|
|
363
|
+
this.reset();
|
|
364
|
+
await this.initialize();
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Get the current model type
|
|
368
|
+
*/
|
|
369
|
+
get currentModelType() {
|
|
370
|
+
return this._modelType;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Get current configuration
|
|
374
|
+
*/
|
|
375
|
+
getConfig() {
|
|
376
|
+
return { ...this.config };
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Check if segmenter is ready
|
|
380
|
+
*/
|
|
381
|
+
get ready() {
|
|
382
|
+
return this.isInitialized;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Check if segmenter is currently loading
|
|
386
|
+
*/
|
|
387
|
+
get loading() {
|
|
388
|
+
return this.isInitializing;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Get the output mask canvas for use as texture source
|
|
392
|
+
*/
|
|
393
|
+
getMaskCanvas() {
|
|
394
|
+
return this.outputCanvas;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Clean up resources
|
|
398
|
+
*/
|
|
399
|
+
dispose() {
|
|
400
|
+
if (this.segmenter) {
|
|
401
|
+
this.segmenter.close();
|
|
402
|
+
this.segmenter = null;
|
|
403
|
+
}
|
|
404
|
+
this.isInitialized = false;
|
|
405
|
+
if (DEBUG)
|
|
406
|
+
console.log('[MediaPipeSegmenter] Disposed');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Detect if running on mobile device
|
|
411
|
+
*/
|
|
412
|
+
export function isMobileDevice() {
|
|
413
|
+
if (typeof navigator === 'undefined')
|
|
414
|
+
return false;
|
|
415
|
+
return (/iPhone|iPad|Android/i.test(navigator.userAgent) ||
|
|
416
|
+
(typeof window !== 'undefined' && window.innerWidth < 768));
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Get optimal processing configuration based on device
|
|
420
|
+
*/
|
|
421
|
+
export function getOptimalConfig() {
|
|
422
|
+
if (isMobileDevice()) {
|
|
423
|
+
return {
|
|
424
|
+
width: 320,
|
|
425
|
+
height: 240,
|
|
426
|
+
edgeBlur: 2
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
width: 640,
|
|
431
|
+
height: 480,
|
|
432
|
+
edgeBlur: 3
|
|
433
|
+
};
|
|
434
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Segmentation module exports
|
|
3
|
+
*/
|
|
4
|
+
export { MediaPipeSegmenter, type SegmenterConfig, type SegmentationResult, type MediaPipeAssetUrls, type AIModelType, DEFAULT_SEGMENTER_CONFIG, DEFAULT_ASSET_URLS, DEEPLAB_MODEL_URL, SELF_HOSTED_ASSET_URLS, isMobileDevice, getOptimalConfig } from './MediaPipeSegmenter';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/segmentation/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,wBAAwB,EACxB,kBAAkB,EAClB,iBAAiB,EACjB,sBAAsB,EACtB,cAAc,EACd,gBAAgB,EAChB,MAAM,sBAAsB,CAAC"}
|