File size: 6,570 Bytes
36cb1b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// Initialize variables
let isVirtual = false;

// DOM elements
const videoContainer = document.getElementById('videoContainer');
const videoElement = document.getElementById('videoElement');
const canvasElement = document.getElementById('backgroundCanvas');
const backgroundImage = document.getElementById('yourBackgroundImage');
const ctx = canvasElement.getContext('2d');
const startButton = document.getElementById('startButton');
const stopButton = document.getElementById('stopButton');
const errorText = document.getElementById('errorText');

 // MobileNetV1 or ResNet50
const BodyPixModel = 'MobileNetV1';

async function startWebCamStream() {
    try {
        // Start the webcam stream
        const stream = await navigator.mediaDevices.getUserMedia({ video: true });
        videoElement.srcObject = stream;

        // Wait for the video to play
        await videoElement.play();
    } catch (error) {
        displayError(error)
    }
}

// Function to start the virtual background
async function startVirtualBackground() {
    try {
        // Set canvas dimensions to match video dimensions
        canvasElement.width = videoElement.videoWidth;
        canvasElement.height = videoElement.videoHeight;

        // Load the BodyPix model: 
        let net = null;

        switch (BodyPixModel) {
            case 'MobileNetV1':
                /*
                    This is a lightweight architecture that is suitable for real-time applications and has lower computational requirements. 
                    It provides good performance for most use cases.
                */
                net = await bodyPix.load({
                    architecture: 'MobileNetV1',
                    outputStride: 16, // Output stride (16 or 32). 16 is faster, 32 is more accurate.
                    multiplier: 0.75, // The model's depth multiplier. Options: 0.50, 0.75, or 1.0.
                    quantBytes: 2, // The number of bytes to use for quantization (4 or 2).
                });
                break;
            case 'ResNet50':
                /*
                    This is a deeper and more accurate architecture compared to MobileNetV1. 
                    It may provide better segmentation accuracy, but it requires more computational resources and can be slower.
                */
                net = await bodyPix.load({
                    architecture: 'ResNet50',
                    outputStride: 16, // Output stride (16 or 32). 16 is faster, 32 is more accurate.
                    quantBytes: 4, // The number of bytes to use for quantization (4 or 2).
                });
                break;
            default:
                break;
        }

        // Start the virtual background loop
        isVirtual = true;
        videoElement.hidden = true;
        canvasElement.hidden = false;
        display(canvasElement, 'block');

        // Show the stop button and hide the start button
        startButton.style.display = 'none';
        stopButton.style.display = 'block';

        async function updateCanvas() {
            if (isVirtual) {
                // 1. Segmentation Calculation
                const segmentation = await net.segmentPerson(videoElement, {
                    flipHorizontal: false, // Whether to flip the input video horizontally
                    internalResolution: 'medium', // The resolution for internal processing (options: 'low', 'medium', 'high')
                    segmentationThreshold: 0.7, // Segmentation confidence threshold (0.0 - 1.0)
                    maxDetections: 10, // Maximum number of detections to return
                    scoreThreshold: 0.2, // Confidence score threshold for detections (0.0 - 1.0)
                    nmsRadius: 20, // Non-Maximum Suppression (NMS) radius for de-duplication
                    minKeypointScore: 0.3, // Minimum keypoint detection score (0.0 - 1.0)
                    refineSteps: 10, // Number of refinement steps for segmentation
                });

                // 2. Creating a Background Mask
                const background = { r: 0, g: 0, b: 0, a: 0 };
                const mask = bodyPix.toMask(segmentation, background, { r: 0, g: 0, b: 0, a: 255 });

                if (mask) {
                    ctx.putImageData(mask, 0, 0);
                    ctx.globalCompositeOperation = 'source-in';

                    // 3. Drawing the Background
                    if (backgroundImage.complete) {
                        ctx.drawImage(backgroundImage, 0, 0, canvasElement.width, canvasElement.height);
                    } else {
                        // If the image is not loaded yet, wait for it to load and then draw
                        backgroundImage.onload = () => {
                            ctx.drawImage(backgroundImage, 0, 0, canvasElement.width, canvasElement.height);
                        };
                    }

                    // Draw the mask (segmentation)
                    ctx.globalCompositeOperation = 'destination-over';
                    ctx.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
                    ctx.globalCompositeOperation = 'source-over';

                    // Add a delay to control the frame rate (adjust as needed) less CPU intensive
                    // await new Promise((resolve) => setTimeout(resolve, 100));

                    // Continue updating the canvas
                    requestAnimationFrame(updateCanvas);
                }
            }
        }
        // Start update canvas
        updateCanvas();
    } catch (error) {
        stopVirtualBackground();
        displayError(error);
    }
}

// Function to stop the virtual background
function stopVirtualBackground() {
    isVirtual = false;
    videoElement.hidden = false;
    canvasElement.hidden = true;
    display(canvasElement, 'none');

    // Hide the stop button and show the start button
    startButton.style.display = 'block';
    stopButton.style.display = 'none';
}

// Helper function to set the display style of an element
function display(element, style) {
    element.style.display = style;
}

// Helper function to display errors
function displayError(error){
    console.error(error);
    // Display error message in the <p> element
    errorText.textContent = 'An error occurred: ' + error.message;
}

// Add click event listeners to the buttons
startButton.addEventListener('click', startVirtualBackground);
stopButton.addEventListener('click', stopVirtualBackground);

// Start video stream
startWebCamStream();