Upload 5 files
Browse files- index.html +43 -0
- main.js +128 -0
- package.json +17 -0
- style.css +87 -0
- vite.config.js +6 -0
index.html
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
|
4 |
+
<head>
|
5 |
+
<meta charset="UTF-8" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7 |
+
<title>Transformers.js | Real-time background removal</title>
|
8 |
+
</head>
|
9 |
+
|
10 |
+
<body>
|
11 |
+
<h1>
|
12 |
+
Real-time background removal w/
|
13 |
+
<a href="https://github.com/huggingface/transformers.js" target="_blank">🤗 Transformers.js</a>
|
14 |
+
</h1>
|
15 |
+
<h4>
|
16 |
+
Runs locally in your browser, powered by
|
17 |
+
<a href="https://huggingface.co/Xenova/modnet" target="_blank">MODNet</a>
|
18 |
+
</h4>
|
19 |
+
<div id="container">
|
20 |
+
<video id="video" autoplay muted playsinline></video>
|
21 |
+
<canvas id="canvas" width="360" height="240"></canvas>
|
22 |
+
<canvas id="output-canvas" width="360" height="240"></canvas>
|
23 |
+
</div>
|
24 |
+
<div id="controls">
|
25 |
+
<div title="Read frames from your webcam and process them at a lower size (lower = faster)">
|
26 |
+
<label>Stream scale</label>
|
27 |
+
(<label id="scale-value">0.5</label>)
|
28 |
+
<br>
|
29 |
+
<input id="scale" type="range" min="0.1" max="1" step="0.1" value="0.5" disabled>
|
30 |
+
</div>
|
31 |
+
<div title="The length of the shortest edge of the image (lower = faster)">
|
32 |
+
<label>Image size</label>
|
33 |
+
(<label id="size-value">256</label>)
|
34 |
+
<br>
|
35 |
+
<input id="size" type="range" min="64" max="512" step="32" value="256" disabled>
|
36 |
+
</div>
|
37 |
+
</div>
|
38 |
+
<label id="status"></label>
|
39 |
+
|
40 |
+
<script type="module" src="/main.js"></script>
|
41 |
+
</body>
|
42 |
+
|
43 |
+
</html>
|
main.js
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import './style.css';
|
2 |
+
|
3 |
+
import { env, AutoModel, AutoProcessor, RawImage } from '@xenova/transformers';
|
4 |
+
|
5 |
+
env.backends.onnx.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/';
|
6 |
+
env.backends.onnx.wasm.numThreads = 1;
|
7 |
+
|
8 |
+
// Reference the elements that we will need
|
9 |
+
const status = document.getElementById('status');
|
10 |
+
const container = document.getElementById('container');
|
11 |
+
const canvas = document.getElementById('canvas');
|
12 |
+
const outputCanvas = document.getElementById('output-canvas');
|
13 |
+
const video = document.getElementById('video');
|
14 |
+
const sizeSlider = document.getElementById('size');
|
15 |
+
const sizeLabel = document.getElementById('size-value');
|
16 |
+
const scaleSlider = document.getElementById('scale');
|
17 |
+
const scaleLabel = document.getElementById('scale-value');
|
18 |
+
|
19 |
+
function setStreamSize(width, height) {
|
20 |
+
video.width = outputCanvas.width = canvas.width = Math.round(width);
|
21 |
+
video.height = outputCanvas.height = canvas.height = Math.round(height);
|
22 |
+
}
|
23 |
+
|
24 |
+
status.textContent = 'Loading model...';
|
25 |
+
|
26 |
+
// Load model and processor
|
27 |
+
const model_id = 'Xenova/modnet';
|
28 |
+
let model;
|
29 |
+
try {
|
30 |
+
model = await AutoModel.from_pretrained(model_id, {
|
31 |
+
device: 'webgpu',
|
32 |
+
dtype: 'fp32', // TODO: add fp16 support
|
33 |
+
});
|
34 |
+
} catch (err) {
|
35 |
+
status.textContent = err.message;
|
36 |
+
alert(err.message)
|
37 |
+
throw err;
|
38 |
+
}
|
39 |
+
|
40 |
+
const processor = await AutoProcessor.from_pretrained(model_id);
|
41 |
+
|
42 |
+
// Set up controls
|
43 |
+
let size = 256;
|
44 |
+
processor.feature_extractor.size = { shortest_edge: size };
|
45 |
+
sizeSlider.addEventListener('input', () => {
|
46 |
+
size = Number(sizeSlider.value);
|
47 |
+
processor.feature_extractor.size = { shortest_edge: size };
|
48 |
+
sizeLabel.textContent = size;
|
49 |
+
});
|
50 |
+
sizeSlider.disabled = false;
|
51 |
+
|
52 |
+
let scale = 0.5;
|
53 |
+
scaleSlider.addEventListener('input', () => {
|
54 |
+
scale = Number(scaleSlider.value);
|
55 |
+
setStreamSize(video.videoWidth * scale, video.videoHeight * scale);
|
56 |
+
scaleLabel.textContent = scale;
|
57 |
+
});
|
58 |
+
scaleSlider.disabled = false;
|
59 |
+
|
60 |
+
status.textContent = 'Ready';
|
61 |
+
|
62 |
+
let isProcessing = false;
|
63 |
+
let previousTime;
|
64 |
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
65 |
+
const outputContext = outputCanvas.getContext('2d', { willReadFrequently: true });
|
66 |
+
function updateCanvas() {
|
67 |
+
const { width, height } = canvas;
|
68 |
+
|
69 |
+
if (!isProcessing) {
|
70 |
+
isProcessing = true;
|
71 |
+
(async function () {
|
72 |
+
// Read the current frame from the video
|
73 |
+
context.drawImage(video, 0, 0, width, height);
|
74 |
+
const currentFrame = context.getImageData(0, 0, width, height);
|
75 |
+
const image = new RawImage(currentFrame.data, width, height, 4);
|
76 |
+
|
77 |
+
// Pre-process image
|
78 |
+
const inputs = await processor(image);
|
79 |
+
|
80 |
+
// Predict alpha matte
|
81 |
+
const { output } = await model({ input: inputs.pixel_values });
|
82 |
+
|
83 |
+
const mask = await RawImage.fromTensor(output[0].mul(255).to('uint8')).resize(width, height);
|
84 |
+
|
85 |
+
// Update alpha channel
|
86 |
+
const outPixelData = currentFrame;
|
87 |
+
for (let i = 0; i < mask.data.length; ++i) {
|
88 |
+
outPixelData.data[4 * i + 3] = mask.data[i];
|
89 |
+
}
|
90 |
+
outputContext.putImageData(outPixelData, 0, 0);
|
91 |
+
|
92 |
+
if (previousTime !== undefined) {
|
93 |
+
const fps = 1000 / (performance.now() - previousTime);
|
94 |
+
status.textContent = `FPS: ${fps.toFixed(2)}`;
|
95 |
+
}
|
96 |
+
previousTime = performance.now();
|
97 |
+
|
98 |
+
isProcessing = false;
|
99 |
+
})();
|
100 |
+
}
|
101 |
+
|
102 |
+
window.requestAnimationFrame(updateCanvas);
|
103 |
+
}
|
104 |
+
|
105 |
+
// Start the video stream
|
106 |
+
navigator.mediaDevices.getUserMedia(
|
107 |
+
{ video: true }, // Ask for video
|
108 |
+
).then((stream) => {
|
109 |
+
// Set up the video and canvas elements.
|
110 |
+
video.srcObject = stream;
|
111 |
+
video.play();
|
112 |
+
|
113 |
+
const videoTrack = stream.getVideoTracks()[0];
|
114 |
+
const { width, height } = videoTrack.getSettings();
|
115 |
+
|
116 |
+
setStreamSize(width * scale, height * scale);
|
117 |
+
|
118 |
+
// Set container width and height depending on the image aspect ratio
|
119 |
+
const ar = width / height;
|
120 |
+
const [cw, ch] = (ar > 720 / 405) ? [720, 720 / ar] : [405 * ar, 405];
|
121 |
+
container.style.width = `${cw}px`;
|
122 |
+
container.style.height = `${ch}px`;
|
123 |
+
|
124 |
+
// Start the animation loop
|
125 |
+
setTimeout(updateCanvas, 50);
|
126 |
+
}).catch((error) => {
|
127 |
+
alert(error);
|
128 |
+
});
|
package.json
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "webgpu-video-background-removal",
|
3 |
+
"private": true,
|
4 |
+
"version": "0.0.0",
|
5 |
+
"type": "module",
|
6 |
+
"scripts": {
|
7 |
+
"dev": "vite",
|
8 |
+
"build": "vite build",
|
9 |
+
"preview": "vite preview"
|
10 |
+
},
|
11 |
+
"devDependencies": {
|
12 |
+
"vite": "^5.0.12"
|
13 |
+
},
|
14 |
+
"dependencies": {
|
15 |
+
"@xenova/transformers": "^3.0.0"
|
16 |
+
}
|
17 |
+
}
|
style.css
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
* {
|
2 |
+
box-sizing: border-box;
|
3 |
+
padding: 0;
|
4 |
+
margin: 0;
|
5 |
+
font-family: sans-serif;
|
6 |
+
}
|
7 |
+
|
8 |
+
html,
|
9 |
+
body {
|
10 |
+
height: 100%;
|
11 |
+
}
|
12 |
+
|
13 |
+
body {
|
14 |
+
padding: 16px 32px;
|
15 |
+
}
|
16 |
+
|
17 |
+
body,
|
18 |
+
#container {
|
19 |
+
display: flex;
|
20 |
+
flex-direction: column;
|
21 |
+
justify-content: center;
|
22 |
+
align-items: center;
|
23 |
+
}
|
24 |
+
|
25 |
+
#controls {
|
26 |
+
display: flex;
|
27 |
+
padding: 1rem;
|
28 |
+
gap: 1rem;
|
29 |
+
}
|
30 |
+
|
31 |
+
#controls>div {
|
32 |
+
text-align: center;
|
33 |
+
}
|
34 |
+
|
35 |
+
h1,
|
36 |
+
h4 {
|
37 |
+
text-align: center;
|
38 |
+
}
|
39 |
+
|
40 |
+
h4 {
|
41 |
+
margin-top: 0.5rem;
|
42 |
+
}
|
43 |
+
|
44 |
+
#container {
|
45 |
+
position: relative;
|
46 |
+
width: 720px;
|
47 |
+
height: 405px;
|
48 |
+
max-width: 100%;
|
49 |
+
max-height: 100%;
|
50 |
+
border: 2px dashed #D1D5DB;
|
51 |
+
border-radius: 0.75rem;
|
52 |
+
overflow: hidden;
|
53 |
+
margin-top: 1rem;
|
54 |
+
background-size: 100% 100%;
|
55 |
+
background-position: center;
|
56 |
+
background-repeat: no-repeat;
|
57 |
+
}
|
58 |
+
|
59 |
+
#overlay,
|
60 |
+
canvas {
|
61 |
+
position: absolute;
|
62 |
+
width: 100%;
|
63 |
+
height: 100%;
|
64 |
+
}
|
65 |
+
|
66 |
+
#status {
|
67 |
+
min-height: 16px;
|
68 |
+
margin: 8px 0;
|
69 |
+
}
|
70 |
+
|
71 |
+
.bounding-box {
|
72 |
+
position: absolute;
|
73 |
+
box-sizing: border-box;
|
74 |
+
border: solid 2px;
|
75 |
+
}
|
76 |
+
|
77 |
+
.bounding-box-label {
|
78 |
+
color: white;
|
79 |
+
position: absolute;
|
80 |
+
font-size: 12px;
|
81 |
+
margin: -16px 0 0 -2px;
|
82 |
+
padding: 1px;
|
83 |
+
}
|
84 |
+
|
85 |
+
#video, #canvas {
|
86 |
+
display: none;
|
87 |
+
}
|
vite.config.js
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { defineConfig } from 'vite';
|
2 |
+
export default defineConfig({
|
3 |
+
build: {
|
4 |
+
target: 'esnext'
|
5 |
+
}
|
6 |
+
});
|