3

I am doing some image processing stuff on a live webRTC camera view in a <video> with OpenCV.js. The processing has a single particularly slow operation that can take between 100ms to 400ms. Even though I execute this within a delay / setTimeout loop and have also tried promises, this leads to a significant amount of blocking and lag / choppiness in the video.

As a result I want to move this operation to a WebWorker. The issue I am facing is that there is no obvious way to transfer video to a WebWorker. I read this can be done through a canvas but this seems pretty convoluted / inefficient. Is there an easier way to do this?

I'm using an OpenCV.js implementation like this but mine is a bit more computationally expensive and with webRTC.

In essence my issue with a WebWorker is two fold:

  1. Images in OpenCV.js are read from <img> or <canvas> tags. How can I pass these to a WebWorker?
  2. Videos in OpenCV.js are read directly from the <video> tag. Which is super convenient is my current setup but how can I pass this into a WebWorker.
  3. How can I load the WASM (opencv.js) file from within the WebWorker

My Code

<!DOCTYPE html>
<html lang="en">
<script src="./opencv.js" type="text/javascript"></script>

<head>
    <meta charset="utf-8" />
</head>

<body>
    <video id="video"></video>
    <script type="text/javascript">
        var video; // INPUT WEBRTC as <video> tag
        var drawingCanvas;
        var img; // INPUT AS <img> tag
        var cameras = ["user", "environment"]; // USUAL CAMERA TYPES

        // WAITS FOR WASM TO LOAD
        cv['onRuntimeInitialized'] = () => {
            // LOADING INDICATOR STOP

            var options = []; // CAMERAS AVAILABLE
            navigator.mediaDevices.enumerateDevices().then((devices) => {
                let index = 0;
                devices.find((device) => {
                    if (device.kind === 'videoinput') {
                        if (device.deviceId == '') {
                            options.push({
                                audio: false,
                                video: {
                                    facingMode: {
                                        exact: cameras[index]
                                    }
                                }
                            });
                            index++;
                        } else {
                            options.push({
                                audio: false,
                                video: {
                                    deviceId: {
                                        exact: device.deviceId
                                    }
                                }
                            });
                        }
                    }
                });

                if (options.length == 0) {
                    console.log("NO DEVICES FOUND");
                } else {

                    navigator.mediaDevices.getUserMedia(options[options.length - 1]).then(stream => {

                        video = document.getElementById('video');
                        video.setAttribute('playsinline', 'playsinline');
                        video.setAttribute('id', 'v');
                        video.setAttribute('position', 'absolute');
                        video.setAttribute('top', '0');
                        video.setAttribute('left', '0');

                        //document.body.appendChild(video);
                        try {
                            video.srcObject = stream;
                            video.style.display = 'block';
                            video.play();
                        } catch (error) {
                            video.src = URL.createObjectURL(stream);
                            video.style.display = 'block';
                            video.play();
                        }

                        let videoSettings = stream.getVideoTracks()[0].getSettings()

                        video.width = window.innerWidth; //videoSettings.width/2;
                        video.height = window.innerWidth * (videoSettings.height / videoSettings.width); //

                        img = document.createElement('img');
                        img.setAttribute("id", "img1");
                        img.setAttribute("src", "volviclogo.png");
                        document.body.appendChild(img);

                        brisk = new cv.BRISK(30, 3, 1); //new cv.BRISK(30, 1, 3);

                        isDetecting = true;

                        setTimeout(mainFunction, 1000);

                    })
                }
            }).catch(err => {
                console.log(err);
            });

        }

        // VIDEO MAX FRAME RATE
        var FPS = 24;

        // OPENCV VARS CACHED
        var brisk;
        var img1;
        var kp1;
        var des1;
        var img1Raw;

        // ADDITIONAL LOADINGS FIRST PASS
        var isFirst = true;

        // TRACKING OF VIDEO FEED
        var cap;
        var frame;

        function mainFunction() {

            if (isFirst) {
                cap = new cv.VideoCapture(video);
                frame = new cv.Mat(video.height, video.width, cv.CV_8UC4);
                cap.read(frame);
            }

            subFunction();

            function subFunction() {
                try {

                    let begin = Date.now();

                    cap.read(frame);
                    img2 = new cv.Mat();
                    const mask = new cv.Mat();
                    kp2 = new cv.KeyPointVector();
                    des2 = new cv.Mat();
                    cv.cvtColor(frame, img2, cv.COLOR_RGBA2GRAY, 0);
                    brisk.detectAndCompute(img2, mask, kp2, des2, false); // SUPER SLOW

                    img2.delete();

                    if (isFirst) {
                        img1Raw = cv.imread("img1");
                        img1 = new cv.Mat();
                        cv.cvtColor(img1Raw, img1, cv.COLOR_RGBA2GRAY, 0);
                        kp1 = new cv.KeyPointVector();
                        des1 = new cv.Mat();
                        brisk.detectAndCompute(img1, mask, kp1, des1, false); // SUPER SLOW BUT ONLY ONCE
                        isFirst = false;
                    }

                    kp2.delete();
                    des2.delete();

                    let delay = 1000 / FPS - (Date.now() - begin);
                    setTimeout(subFunction, delay);

                } catch (err) {
                    console.log("ERROR");
                    console.log(err);
                }
            }
        }
    </script>

</body>

</html>
Anters Bear
  • 1,816
  • 1
  • 15
  • 41
  • 3
    I don't believe WebWorkers support any of the DOM APIs if I remember correctly so you're not going to be able to pass elements directly in. You're likely going to have to get the image data for images/canvasses and pass it in to your WebWorker as a base64 encoded string to handover to OpenCV. You /might/ need to pull code out of OpenCV and change it to support base64 encoded data rather than DOM elements directly for use with your WebWorker. I imagine the same process will likely work for video data as well. – Daniel Lane Jul 24 '19 at 09:50
  • Thanks for your valuable input. I'm hoping there's something simpler, otherwise it's really quite a bit of work for a small performance increase. – Anters Bear Jul 24 '19 at 09:54
  • 1
    Base64 encoding is not required to pass image data. You can use `HTMLCanvasElement.transferControlToOffscreen` and `OffscreenCanvas.transferToImageBitmap` to transfer data stored directly to the GPU using the `transfer` option of `webWorker.postMessage(message, targetOrigin, [transfer]);` – maiermic Jun 11 '20 at 10:56

1 Answers1

1

To pass the frame to the web worker use

worker.postMessage( canvasContext.getImageData(0, 0, width, height).data )

and in your worker to convert it to a cv.Mat:

const frame = new cv.Mat(dimensions.height, dimensions.width, cv.CV_8UC4);
frame.data.set( /* the data from message listener */ );

make sure to pass the dimensions of the video before hand to construct the cv.Mat basically you pass frame by frame to the web worker and wait for the result