justadudewhohacks / face-api.js

JavaScript API for face detection and face recognition in the browser and nodejs with tensorflow.js
MIT License
16.48k stars 3.68k forks source link

Webworker #47

Open patrick-nurt opened 6 years ago

patrick-nurt commented 6 years ago

Hi there! Great work on this plugin! Has anybody managed to run this in a webworker?

flatsiedatsie commented 4 years ago

Here is my webworker:

importScripts('faceEnvWorkerPatch.js'); 
importScripts('js/face-api.min.js');               

let model;
var detector_options;

let selected_face_detector = 'tiny_face_detector'; //'ssd_mobilenetv1';
let inputSize = 512
let scoreThreshold = 0.5
const minConfidence = 0.05; // expression

var loaded_models = [];
var selected_use = 'face'; // can be changed with a command from the main script.

faceapi_worker_loaded = false;
beauty_worker_loaded = false;
ethnicity_worker_loaded = false;

async function load_mobile_net() {
    try{
        await faceapi.nets.ssdMobilenetv1.load('models/');
        console.log("loaded mobilenet face detection");
        detector_options = getFaceDetectorOptions();
    }
    catch(e){console.log(e);}
}

async function load_tiny_net() {
    try{
        await faceapi.nets.tinyFaceDetector.load('models/');
        console.log("loaded tiny face detection");
        detector_options = getFaceDetectorOptions();
    }
    catch(e){console.log(e);}
    postMessage({'loaded':'tiny'});
}

async function load_age_and_gender() {
    try{
        await faceapi.nets.ageGenderNet.load('models/'); // gender
        console.log("loaded age and gender detection model");
    }
    catch(e){console.log(e);}
}

async function load_landmarks() {
    try{
        await faceapi.loadFaceLandmarkModel('models/'); // landmarks
        console.log("loaded landmark detection model");
    }
    catch(e){console.log(e);}
}

async function load_expression() {
    try{
        await faceapi.loadFaceExpressionModel('models/'); // expression
        console.log("loaded expression detection model");
    }
    catch(e){console.log(e);}    
}

async function load_recognition() {
    try{
        await faceapi.loadFaceRecognitionModel('models/') // face vectors
        console.log("loaded face recognition detection model");
    }
    catch(e){console.log(e);}     
}

function getCurrentFaceDetectionNet() {
  if (selected_face_detector === SSD_MOBILENETV1) {
    return faceapi.nets.ssdMobilenetv1
  }
  if (selected_face_detector === TINY_FACE_DETECTOR) {
    return faceapi.nets.tinyFaceDetector
  }
}

function getFaceDetectorOptions() {
    //new faceapi.SsdMobilenetv1Options({ minConfidence });
    if(selected_face_detector == 'tiny_face_detector'){
        return new faceapi.TinyFaceDetectorOptions({ inputSize, scoreThreshold });
    }
    else{
        return new faceapi.SsdMobilenetv1Options({ minConfidence });
    }
}

onmessage = function(incoming) {
    //console.log('Message received from main script:');
    //console.log(incoming.data);

    var return_message = '';

    if(incoming.data.type == 'frame'){
        console.log("worker: incoming frame");
        use(selected_use, incoming.data)
    }
    //else if(Array.isArray(incoming.data)){
    else {  

        console.log("worker: incoming init message");

        if(incoming.data.net !== undefined){
            if(incoming.data.net == 'tiny'){
                selected_face_detector = 'tiny_face_detector';
                load_tiny_net();
                console.log("worker: loading tiny face net");
            }
            else if(incoming.data.net == 'mobilenet'){
                selected_face_detector = 'ssd_mobilenetv1';
                load_mobile_net();
                console.log("worker: loading mobile face net");
            }
            //postMessage('net loaded');
        }

        if(incoming.data.load !== undefined){
            incoming.data.load.forEach(function (item, index) {
              //console.log("loading model:");
              //console.log(index, item);
              if(item == 'age_and_gender'){ 
                  load_age_and_gender(); 
                  console.log('loading age and gender model');
              }
              if(item == 'expression'){ 
                  load_expression(); 
                  console.log('loading expression model');
              }
              if(item == 'recognition'){ 
                  load_recognition(); 
                  console.log('loading face recognition model');
              }
              if(item == 'landmarks'){ 
                  load_landmarks(); 
                  console.log('loading landmarks model');
              }
              //console.log('loading ' + item);
              //postMessage("loaded");
            });
        }

        if(incoming.data.use !== undefined){
            selected_use = incoming.data.use;
            console.log("selected_use is now set to " + selected_use);

        }

    }
}

async function use(use_case, image_data){

    console.log("in worker USE. use_case = " + use_case + ", imagedata:");
    console.log(image_data);
    // props is the message from the main thread
    const imgData = new ImageData(
       new Uint8ClampedArray(image_data.data),
        image_data.width,
        image_data.height
    );

    console.log(imgData);

    // Create a canvas from our rgbaBuffer
    const img = faceapi.createCanvasFromMedia(imgData);

    //const results = await faceapi.detectAllFaces(img, this.faceDetectorOptions)

    var result = {};
    if(use_case == 'face'){ 
        result = await faceapi.detectSingleFace(img, detector_options);
    }
    else if(use_case == 'landmarks'){
        result = await faceapi.detectSingleFace(img, detector_options).withFaceLandmarks();

        try {
            const jawOutline = result.landmarks.getJawOutline();
            const nose = result.landmarks.getNose();
            const mouth = result.landmarks.getMouth();
            const leftEye = result.landmarks.getLeftEye();
            const rightEye = result.landmarks.getRightEye();
            const leftEyeBrow = result.landmarks.getLeftEyeBrow();
            const rightEyeBrow = result.landmarks.getRightEyeBrow();
            postMessage({
                "landmarks":result.landmarks.positions,
                "jawOutline":jawOutline,
                "nose":nose,
                "mouth":mouth,
                "leftEye":leftEye,
                "rightEye":rightEye,
                "leftEyeBrow":leftEyeBrow,
                "rightEyeBrow":rightEyeBrow,
                "faceparts":true
            });
        }
        catch (e) {
            postMessage({"faceparts":false});
        }

    }
    else if(use_case == 'age_and_gender'){
        result = await faceapi.detectSingleFace(img, detector_options).withFaceLandmarks().withAgeAndGender();
    }
    else if(use_case == 'expression'){
        result = await faceapi.detectSingleFace(img, detector_options).withFaceLandmarks().withFaceExpressions()
    }
    else if(use_case == 'recognition'){
        result = await faceapi.detectSingleFace(img, detector_options).withFaceLandmarks().withFaceDescriptor();
    }
    else if(use_case == 'all'){
        result = await faceapi.detectSingleFace(img, detector_options).withFaceLandmarks().withFaceExpressions().withAgeAndGender().withFaceDescriptor();
        console.log("use case all result was: ");
        console.log(result);
    }
    else{
        console.log("Error: no fitting use case provided");
        postMessage({"error":"selected use does not exist"});
    }

    if( result === undefined){
        result = {'error':'result undefined'};
    }
    postMessage(result);
}

That file also loads second patch file called faceEnvWorkerPatch.js to make faceapijs work as a webworker. It contains:

// From: https://github.com/justadudewhohacks/face-api.js/issues/47

self.Canvas = self.HTMLCanvasElement = OffscreenCanvas;
self.HTMLCanvasElement.name = 'HTMLCanvasElement';
self.Canvas.name = 'Canvas';

self.CanvasRenderingContext2D = OffscreenCanvasRenderingContext2D;

function HTMLImageElement(){}
function HTMLVideoElement(){}

self.Image = HTMLImageElement;
self.Video = HTMLVideoElement;

// Canvas.prototype = Object.create(OffscreenCanvas.prototype);

function Storage () {
    let _data = {};
    this.clear = function(){ return _data = {}; };
    this.getItem = function(id){ return _data.hasOwnProperty(id) ? _data[id] : undefined; };
    this.removeItem = function(id){ return delete _data[id]; };
    this.setItem = function(id, val){ return _data[id] = String(val); };
}
class Document extends EventTarget {}

let window, document = new Document();

window = self.Window = self;
self.localStorage = new Storage();

function createElement(element) {
    // console.log('FAKE ELELEMT instance', createElement, 'type', createElement, '(', createElement.arguments, ')');
    switch(element) {
        case 'canvas':
            // console.log('creating canvas');
            let canvas = new Canvas(1,1);
            canvas.localName = 'canvas';
            canvas.nodeName = 'CANVAS';
            canvas.tagName = 'CANVAS';
            canvas.nodeType = 1;
            canvas.innerHTML = '';
            canvas.remove = () => { console.log('nope'); };
            // console.log('returning canvas', canvas);
            return canvas;
        default:
            console.log('arg', element);
            break;
    }
}

document.createElement = createElement;
document.location = self.location;
//console.log('*faked* Document object for the worker', document);

if(!typeof window == 'object') {
    console.warn("Check failed: window");
}
if(typeof document === 'undefined') {
    console.warn("Check failed: document");
}
if(typeof HTMLImageElement === 'undefined') {
    console.warn("Check failed: HTMLImageElement");
}
if(typeof HTMLCanvasElement === 'undefined') {
    console.warn("Check failed: HTMLCanvasElement");
}
if(typeof HTMLVideoElement === 'undefined') {
    console.warn("Check failed: HTMLVideoElement");
}
if(typeof ImageData === 'undefined') {
    console.warn("Check failed: ImageData");
}
if(typeof CanvasRenderingContext2D === 'undefined') {
    console.warn("Check failed: CanvasRenderingContext2D");
}

self.window = window;
self.document = document;
self.HTMLImageElement = HTMLImageElement;
self.HTMLVideoElement = HTMLVideoElement;

const isBrowserCheck = typeof window === 'object'
&& typeof document !== 'undefined'
&& typeof HTMLImageElement !== 'undefined'
&& typeof HTMLCanvasElement !== 'undefined'
&& typeof HTMLVideoElement !== 'undefined'
&& typeof ImageData !== 'undefined'
&& typeof CanvasRenderingContext2D !== 'undefined';
;
// console.warn("++ Check passed locally:", isBrowserCheck);
if(!isBrowserCheck) {
    throw new Error("Failed to monkey patch for face-api, face-api will fail");
}

The webworker is interacted with from the main script. Here is some of that code:

// If the browser supports web workers...
if (typeof(Worker) !== "undefined") {

    window.faceapi_worker = new Worker('faceapi-worker.js');
    window.faceapi_worker.onmessage = function(incoming) {

        console.log('Message received from faceapi worker .data: ' + JSON.stringify(incoming.data,null,2));

        if( 'data' in incoming ){

            if(incoming.data === undefined){
                console.log("incoming.data was undefined?")
            }
            else if('loaded' in incoming.data ){
                console.log("incoming message that ML was loaded");
            }
            else if('detection' in incoming.data ){
                console.log(incoming.data); // Do something with the data here.
                const face_box = incoming.data.detection['_box']; // example
            }
            else{
                console.log("No detection data in incoming data..");
            }

        }
        else{
            console.log("undefined result from worker.");
        }

    }
    window.faceapi_worker.postMessage({'net':'tiny','load':['landmarks','expression','age_and_gender','recognition'],'use':['all']});

}

And here's the part of the main code that sends a webcam frame to the webworker:

    canvy_context.drawImage(webcam_video,0,0,640,480);    
    var imgData2 = canvy_context.getImageData(0, 0, 640, 480);

    console.log(imgData2);

    const { height, width, data } = imgData2;
    console.log("height:" + height);
    // Transfer the buffer via a transferlist. See https://stackoverflow.com/questions/41497124/allowable-format-for-webworker-data-transfer

    window.faceapi_worker.postMessage({
        type: "frame",
        height,
        width,
        data,
    }, [ data.buffer ]);
rlaurente commented 4 years ago

Hi Guys,

I able to make faceapi working with web worker using offscreencanvas. My issue now is android webview doesn't support offscreen canvas, is there any other workaround ?

Thanks

sprengerst commented 4 years ago

Hi @rlaurente could you please add a Gist how you did it? Best, Stefan

rlaurente commented 4 years ago

@sprengerst using @flatsiedatsie code above. It works perfectly in browser now but android webview doesn't support OffscreenCanvas. Still finding a way to make it work in capacitor / cordova without switching to crosswalk library.

flatsiedatsie commented 4 years ago

@rlaurente Please let us know if you find a way. I'm also running into the OffScreenCanvas not working on mobile browsers issue.

rlaurente commented 4 years ago

@flatsiedatsie

I think the only way for now is using this plugin https://github.com/JaneaSystems/nodejs-mobile lol. Haven't tried yet though. I hope there's a simpler workaround.

remipassmoilesel commented 3 years ago

Hi !

These lines of monkey patch cause errors on the latest version of Chrome, when used elsewhere than localhost:

self.HTMLCanvasElement.name = 'HTMLCanvasElement';
self.Canvas.name = 'Canvas';
Cannot assign to read only property name of Canvas

As face-api.js works well with OffscreenCanvas, shouldn't this type be added as a possible source ? (for detectSingleFace() per example)

remipassmoilesel commented 3 years ago

If you are interested in a PR let me know.

josiahbryan commented 3 years ago

Anyone seeing massive memory leaks in WebWorkers in the latest Chrome using detectAllFaces? I've found detectAllFaces to be leaking memory like a siv - https://github.com/justadudewhohacks/face-api.js/issues/732

cindyloo commented 3 years ago

Hey about your questions on displaying it on a canvas, you actually can just render to a canvas from INSIDE the web worker and it will AUTOMATICALLY update the canvas outside the webworker.

How?

Well, in my code, I passed in the canvas from the main thread like this:

const canvasFromMainThread = detectionResultsCanvas.transferControlToOffscreen();

myWebWorker.postMessage({
    type: "setCanvasFromMainThread",
    canvasFromMainThread,
}, [ canvasFromMainThread ]);

One tip: The detectionResultsCanvas that I pass IN to the web worker will NOT have the video/image on it that the web worker is using - it is only for OUTPUT of the results.

So, in my main thread, what I really have is something like this:

  • CanvasContainer

    • Canvas 1 - LiveVideoCanvas
    • Canvas 2 - OverlayCanvas

I use simple CSS to make the LiveVideoCanvas be position: absolute; top: 0; left: 0; z-index: 0, and then the OverlayCanvas is position: absolute; top: 0; left: 0; z-index: 1

Since the OverlayCanvas is positioned on top of the LiveVideoCanvas and it is being cleared every time before detection results and it is transparent, what we end up having is the live video showing underneath the overlay with the live results from the web worker being rendered on top of the live video.

Make sense? Feel free to ping with questions!

I am pulling frames off the live video element, drawing them into a canvasVideoBuffer, then passing the imageData via postMessage to my worker. this worker draws the face detection box into self.offScreenCanvasCtx which was set up at the loading of the worker and the models. It's super slow however - the canvasVideoBuffer doesn't seem to keep up with the video feed (both canvas elements are visible/overlaid over the source video). Should I be setting the canvasVideoBuffer display to none or change the frame intervals or something to speed it up? is it b/c I'm not clearing the canvas target out? thanks!

josiahbryan commented 3 years ago

Hey @cindyloo - yeah the canvas I use for capture-and-transfer is invisible / hidden. The user's see the live <video> element. I do draw that video element to a canvas then do .getImageData to xfer it to the worker, but the users don't see the canvas, so they don't know how fast/slow I'm drawing. They just see the raw <video> feed, so they feel like it's real time.

The only thing they see is the "overlay" canvas from the worker - and yeah, that is a bit laggy at 12fps (I actually wrote a tuner to automatically move FPS up/down as needed, can share if desired) - but yeah, the overlay can be slow, but IMHO that's okay as long as the video behind it is buttery smooth.

Ping with specific questions for more details, happy to share!

cindyloo commented 3 years ago

@josiahbryan so your face detection overlay is a bit laggy too? I need it to be as fast to real-time as possible, looking at setting up an interval instead of sending the image to the worker at every render.. any suggestions are welcome!

josiahbryan commented 3 years ago

@cindyloo and anyone else who is interested - I know you didn't ask, but here's the FPS Tuner I use in some of my projects as needed. Using it right now in a bespoke project with face-api and a webworker to tune the FPS up/down as performance allows: https://gist.github.com/josiahbryan/c4716f7c9f051d7c084b1536bc8240a0

josiahbryan commented 3 years ago

I mean, I don't need the faces 100% real time - as long as within a few frames of the actual real time, I'm happy.

But yeah, the bottle neck is the detection, not the sending of frames. I can get 16-20fps when I force it on a MacBook pro with tons of ram and cpu.

I haven't tried this yet with faceapi, but I do it with OpenCV in another worker: Specifically, to improve performance, I resize the video WHEN I DRAW IT into the canvas down to a smaller size:

faceCaptureCtx.drawImage(videoEl, 0, 0, smallWidth, smallHeight);

The small size is calculated from the aspect ratio of the video and downsized to something like 420px x whatever for best results.

I imagine doing something like that before sending the frame to the worker for face-api would improve both the transfer and the detection, but haven't tried it yet myself @cindyloo

Just fair warning tho - might not matter for your application - but the smaller your source frame (e.g. the smaller you resize the video), the worse the detector will be at finding small faces. Will still work fine with faces that fill "a lot" of the frame, but will fail to find smaller faces as you resize smaller. Just FYI

josiahbryan commented 3 years ago

These lines of monkey patch cause errors on the latest version of Chrome, when used elsewhere than localhost:

Hey @remipassmoilesel , you're right, when I revisited this project this year, I did have to update the monkeypatch I wrote to work with lates chrome. I've updated the gist above, but here is the updated monkeypatch script:

https://gist.github.com/josiahbryan/770ca1a9d72f1b35c13219ba84dc0495

Also, unrelated, but for others (@cindyloo or whoever), here's the writeup on FpsTuner I did a while ago on how to use it:

https://github.com/josiahbryan/fps-auto-tuner

remipassmoilesel commented 3 years ago

Hey @josiahbryan ! What do you think about just using @OffscreenCanvas as an input source ? It works well for me

josiahbryan commented 3 years ago

@remipassmoilesel not sure what you mean...can you be more specific? :-D Like pseudo code example or bulletpoints? Afaik you can't render into the canvas from outside the worker once you xfer a canvas to the worker? Or maybe I'm not understanding what you're saying?

remipassmoilesel commented 3 years ago

Hi @josiahbryan,

I mean presently this sample code needs a type assertion on input (as TNetInput):

  public async faceDetection(input: HTMLVideoElement | OffscreenCanvas): Promise<FaceDetection | undefined> {
    return faceApi
      .detectSingleFace(input as TNetInput, new faceApi.TinyFaceDetectorOptions())
      .withFaceLandmarks()
      .withFaceDescriptor()
      .withAgeAndGender();
  }

OffscreenCanvas cannot be used as input according to type definitions in face-api.js/build/commonjs/dom/types.d.ts. But I tried it and it work. I don't know if it is a desirable practice but if it is, it can simplify the use of face-api.js in a worker.

jeffreytgilbert commented 3 years ago

The bottlenecks to making this work without jank are not simply cpu bound. On a machine without a discrete or very new mobile GPU, the GPU will lock up and cause jank on the main thread. See, the GPU is also used to render the main thread, and you can pin it with ease on intel integrated graphics chips that only have like 40ish cores. All the discrete GPUs these days start in the 1000+ cores range, so i think it just gets geeked when it cant handle the same quantity of tasks. I was never able to get an exact answer for just why that is the case, but it's the case. So in your dev tools, you can monitor GPU too. I would watch that one if you see issues on your target browser/feature support level.

https://github.com/justadudewhohacks/face-api.js/issues/47#issuecomment-486106670

cindyloo commented 3 years ago

I couldn't get close to the speed of this example and as such implemented a webworker. Combined with an upgrade to tfjs and a throttle of the frames sent to the worker, I have a fairly responsive detector w landmarks. Now that I'm realizing we can't use offscreenCanvases on mobile browsers, I'm questioning my decision. Any thoughts/comments?

jeffreytgilbert commented 3 years ago

It's not mobile browsers. It's embedded webviews in apps. Mobile browsers probably support what you need unless the android version is too old.

As some have suggested above, if offscreen canvas is not supported for web workers and gpu accelerated detection, you can alternatively use a more cpu bound brute force technique with opencv. It's a different library built for cpu not gpu acceleration, so it should work. The web supported portions of this back in 2010s when a team at opera was leading the charge on perspective driven face detection and 3d, mimicking the nintendo wii functionality using your face, and orienting the 3d camera and depth based on where you are perceived to be standing from the display. Worked well back then on just cpu, but with much simpler models. Tensorflow is a great library, but if you cant use it because of offscreen canvas support, browser support, gpu support, etc, the thing to do would be go down the cpu accelerated route and simpler models like opencv and webassembly and stuff like that which might be good enough for your use case.

cindyloo commented 3 years ago

thanks @jeffreytgilbert. The example cited above doesn't appear to work on iphone X chrome (error attached). I have used opencv but not opencv.js. I'll have a look. IMG_0978

jeffreytgilbert commented 3 years ago

thanks @jeffreytgilbert. The example cited above doesn't appear to work on iphone X chrome (error attached). I have used opencv but not opencv.js. I'll have a look.

IMG_0978

That's unfortunate. Dumb question, but did you grant access to the microphone and camera for the site? That looks like either the support for getUserMedia isn't there which would be very strange since chrome were the ones to introduce the webrtc apis i believe, or potentially an access restriction issue. You wouldnt be able to access this from within the webworker, but you should be able to get to it from the main window.

ScottDellinger commented 3 years ago

Trying to use anything other than Safari on an iPhone or iPad is not going to work well, in my experience. Apple doesn't give full WebRTC support to those browsers the way it does to Safari.

jeffreytgilbert commented 3 years ago

Trying to use anything other than Safari on an iPhone or iPad is not going to work well, in my experience. Apple doesn't give full WebRTC support to those browsers the way it does to Safari.

I believe they recently discontinued their policy around locking down other browser apps so they have to use the webkit engine. Chrome may still use it. Havent looked to see who swapped out webkit for their own engine.

aendra-rininsland commented 3 years ago

So, I did this for a project and had it working fairly well in a worker with OffscreenCanvas, but honestly wasn't getting any better perf from it than having it on the main thread, ended up removing it. I think the data transfer between working and main thread might be the bottleneck?

josiahbryan commented 3 years ago

Entirely possible that it is the bottleneck. The only reason I ever did things in a worker thread was so that video playing in the main UI thread didn't stutter or be affected. When I tried doing face detection in the main thread, the video display from the camera was seriously affected, but when I do face detection in a worker thread, the video display in the main thread is butter smooth

On Thu, Mar 18, 2021, 9:10 AM Ændrew Rininsland @.***> wrote:

So, I did this for a project and had it working fairly well in a worker with OffscreenCanvas, but honestly wasn't getting any better perf from it than having it on the main thread, ended up removing it. I think the data transfer between working and main thread might be the bottleneck?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/justadudewhohacks/face-api.js/issues/47#issuecomment-801959968, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABEZELFQ5XZCCRNKFGUK4KTTEICWFANCNFSM4FJTKRRA .

aendra-rininsland commented 3 years ago

@josiahbryan Ohhh okay that totally makes sense. I was rendering the video at the same speed as the face-api.js processing so that there wouldn't be lag when drawing stuff on top of the canvas element, but if you're wanting the video to play in realtime that would be beneficial. Cool! 👍

I'm still a bit of a ways off from publishing my project but will post a working example repo once I do, have learned a lot getting this working!

josiahbryan commented 3 years ago

Yeah that makes total sense, I see what you're saying then in that case. In my case I was capturing from a WebCam and showing it on the screen so people had to see themselves moving in real time, but then the face detection would be just at 12 FPS so yeah the face detection would lag a bit but as long as people could see themselves move in real time early in a matter in my case.

Good work on getting this far! It is so much fun learning things as we go isn't it? Good job!

On Thu, Mar 18, 2021 at 9:16 AM Ændrew Rininsland @.***> wrote:

@josiahbryan https://github.com/josiahbryan Ohhh okay that totally makes sense. I was rendering the video at the same speed as the face-api.js processing so that there wouldn't be lag when drawing stuff on top of the canvas element, but if you're wanting the video to play in realtime that would be beneficial. Cool! 👍

I'm still a bit of a ways off from publishing my project but will post a working example repo once I do, have learned a lot getting this working!

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/justadudewhohacks/face-api.js/issues/47#issuecomment-801964027, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABEZELFIUKOUCUOCC3AMEWDTEIDKLANCNFSM4FJTKRRA .

-- Josiah Bryan Phone/SMS: +1-765-215-0511 WhatsApp: +972-050-794-0566 www.josiahbryan.com https://www.josiahbryan.com/?utm_source=sig @.***

jeffreytgilbert commented 3 years ago

You definitely don't need to process on every video frame. If you do that, it will choke. Rather than making it arbitrary (like say running it at 12FPS capture) you can have it async so the capture only happens when the detection has finished and the background thread is ready to process a new frame. If this causes too many hiccups in performance on the main thread, it's likely due to the video card not being fast enough to run the ML and also handle the Main UI thread. In Chrome, last I checked, even from a worker thread, the GPU is not isolated from the Main UI thread, so your GPU ends up being a bottleneck. If this is the case you are seeing where GPU (which can be evaluated in dev tools under the performance recorder) is taking too long, you'll need to get a better GPU orrrrr you can simplify your detection model orrrrr pick a simpler one. OffscreenCanvas uses zero copy to move data between parent and worker thread, however I did also notice the capture of the image data from the frame is a non-trivial hit to performance.

So, quick recap of steps to take: 1) check your GPU performance when it runs that detection to see if it takes longer than 16ms as referenced here: https://github.com/justadudewhohacks/face-api.js/issues/47#issuecomment-486106670 2) only do detection work when you're not currently already doing detection work (promises help) 3) don't capture too much data when reading from video if you can avoid it. Instead, wait until your promise resolves from the previous detection and then attempt to read more

If that doesn't work, let us know and I'll see if there's something I might have missed.

jeffreytgilbert commented 3 years ago

Also some food for thought, but if you can't buy a new GPU to run the ML without delays, you can fall back to CPU processing which would be isolated in a web worker on a background thread and won't cause visual jank. The downside on that approach is you will have fewer updates because each check will take longer and you're going to spin the fans on the CPU while it brute forces its way through the work. I don't have an exact answer on how you would force TensorFlow to CPU, but I seem to remember it doing that when it thought the runtime environment was node.js originally before I added the container hacks to fool it into thinking it was in the main UI thread context. I bet it could be done pretty trivially.

zy308718320 commented 2 years ago

Why BodyPix supports ImageData

Waishnav commented 6 months ago

Since the OverlayCanvas is positioned on top of the LiveVideoCanvas and it is being cleared every time before detection results and it is transparent, what we end up having is the live video showing underneath the overlay with the live results from the web worker being rendered on top of the live video.

@josiahbryan is this solution work for 30 FPS video. Currently in my application, I've everything rendering video/canvas, and face detection in main thread. and on video play I'm trying to process each frame. But what end up happening is that I'm only able to process the about 1/10th of all video frames. Which is not I want to implement

so my question is, for 1 minute 30 FPS video (i.e 1800 frames). can I detect all the faces from each and every frames using the Web worker solution?