face-api.js icon indicating copy to clipboard operation
face-api.js copied to clipboard

Webworker

Open patrick-nurt opened this issue 6 years ago • 83 comments

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

patrick-nurt avatar Jul 12 '18 13:07 patrick-nurt

I haven't tried it but it should work. I am assuming you want to use a webworker so that the ui doesn't get blocked because of loading the models or during face detection/recognition? Do keep in mind that you won't be able to access dom elements from a webworker.

thexiroy avatar Jul 12 '18 14:07 thexiroy

Wanted to use a tensor as input in the worker, to avoid DOM elements, would just transfer the image data to the worker and then transfer the results back m.

patrick-nurt avatar Jul 13 '18 01:07 patrick-nurt

I did it and it works, but at the moment tfjs is not compatible with offscreen canvas (https://github.com/tensorflow/tfjs/issues/102) so you don't have access to the GPU from a webworker and the result is more than slow actually ... If your purpose is to get better performances I advice you to have a try with the mtcnn model which is really faster, see the 0.9.0 version of this project.

akofman avatar Jul 16 '18 15:07 akofman

@akofman Out of curiosity, how did you make it work ?

Floby avatar Aug 13 '18 15:08 Floby

I had same experience with tracking.js library. I managed to to put its part to web worker https://github.com/eduardolundgren/tracking.js/issues/99 and completely get rid of the UI lags but code was just for POC

shershen08 avatar Oct 01 '18 21:10 shershen08

I did it and it works, but at the moment tfjs is not compatible with offscreen canvas (tensorflow/tfjs#102) so you don't have access to the GPU from a webworker and the result is more than slow actually ... If your purpose is to get better performances I advice you to have a try with the mtcnn model which is really faster, see the 0.9.0 version of this project.

@akofman I also would be interested in how you got it working in a worker, if you're willing to share!

ScottDellinger avatar Oct 14 '18 20:10 ScottDellinger

Bumping this for relevance because i'm now working on doing the same thing and with offscreen canvas. My project is here if anyone wants to check out why. I'm rendering a threejs scene and the face detection allows me to use face detection to control the perspective of the 3d scene. Each time the face detection runs at 100ms, the scene janks out briefly because the 3d part runs in about 1ms cpu time over 16ms budget for RAF calls, but the face detection part goes 40ms-100ms so you lose 3 frames at a time, making it look like the rendering is broken. Now I have to find a way to get it under budget for the reasons @thexiroy mentioned

jeffreytgilbert avatar Apr 13 '19 02:04 jeffreytgilbert

It's probably better to ask for help at tfjs regarding how to get this running in a webworker

justadudewhohacks avatar Apr 13 '19 09:04 justadudewhohacks

For those interested, there is a pull request open here which was a bit dated, but I've been working on. The OffscreenCanvas support doesn't appear that involved, but there doesn't appear to be any special consideration for web workers or transferable objects, and those may take longer to integrate in. I did see there is a branch for web workers open, but I haven't been through it.

https://github.com/tensorflow/tfjs-core/pull/1221

jeffreytgilbert avatar Apr 17 '19 03:04 jeffreytgilbert

@justadudewhohacks tfjs updates is a non-starter. Typescript wont have support for OffscreenCanvas until 3.5.0 and that's not officially released. Even if they do release it, it's currently buggy and TensorFlow wont build against 3.5.0 without ts-ignore hacks. Even if you do the ts-ignore hacks, the resulting build of tensorflow running in face-api.js barf due to those ignored incompatibilities. Models don't work. flattened maps don't work. Whole thing barfs.

So, hacking created and updated tickets for those findings. The tfjs-core update thread above was updated. I also created a typescript issue that can be tracked here: https://github.com/Microsoft/TypeScript/issues/30998

It looks like things on both those projects move pretty quickly, so hopefully this wont fall to the bottom of the thousands of filed issues on the pile and actually get some updates. For now, I will be attempting to create a fake interface in the worker thread which proxies back to the main js with updates and commands. The approach is similar to one that @mizchi took here: https://github.com/tensorflow/tfjs/issues/102#issuecomment-462167706

The difference between @mizchi 's approach and my approach will be that i am attempting to fool the tensorflow library into believing it is running under non-worker thread conditions using my new found knowledge of how it works (gained by trying to fix their code). The plan is to build a faux document object and window object, complete with interfaces and values the library checks for when creating a canvas. Instead, I'll return wrapped instances of OffscreenCanvas, maybe with Proxy Traps, and catch any calls by the library to APIs I haven't stubbed out and build adaptors for those to canvas. Because TensorFlow does not ever return canvas elements to be drawn to the screen, the only overhead I'll have to worry about is sending the data from video into the worker to be processed. Because I'll be doing this with ImageBitmap, those updates will be zero-copy transferable objects (low latency). I suppose this is somewhat of a "shim" pattern and could be added to face-api.js as an adaptor or different API call if it works.

For anyone else following this path expecting that a heroic effort down this rabbit hole will maybe allow you to get this to work, a few notes you should consider:

TensorFlow (Google) is a massive project written in Typescript (Microsoft) which is made up of monolithic modules here: "dependencies": { "@tensorflow/tfjs-converter": "1.1.0", "@tensorflow/tfjs-core": "1.1.0", "@tensorflow/tfjs-data": "1.1.0", "@tensorflow/tfjs-layers": "1.1.0" } Each of those has some dependencies of their own. You'll end up having to update core, then update all the other modules the depend on core, then rebuild the whole tensorflow project with the same version of typescript, which in the case of this feature set, would be "next" or 3.5.0+, all of which aren't compatible with that version out of the box (at this time). Typescript appears to be driven primarily by features in the IE/Edge browser suite because Microsoft owns that project, and TensorFlow being Google but subject to limitations in Typescript means they A) have their own blessed version of Typescript, B) this is older than whatever the most recent release is, and C) is not as up to date with the features of the web as Google's Chrome browser team support. Maybe, eventually, once MS moves Edge to the Chromium/Blink/Whatever engine, possibly Typescript becomes one with the universe and offers support for these DOM features in sync with minimally Chrome and Edge, but ideally all major browsers. That would be awesome! But, back to the topic, the issues I ended up seeing were related to compilation errors stemmed from code that was doing type conversion like " float32ToTypedArray ", including some map functions/loops. Those were throwing errors at compile time, but unit tests ran fine. I was never able to get browserstack tests to run correctly, so I'm not sure if it really did work in the browser or not. Best of luck! @ me if you want to chat!

jeffreytgilbert avatar Apr 18 '19 16:04 jeffreytgilbert

function isBrowser() { return typeof window === 'object' && typeof document !== 'undefined' && typeof HTMLImageElement !== 'undefined' && typeof HTMLCanvasElement !== 'undefined' && typeof HTMLVideoElement !== 'undefined' && typeof ImageData !== 'undefined'; }

This is a horrible function. If someone (me) wants to fake out a library into thinking it's in a browser, don't stifle that person by doing some oddball browser check (this is not how you detect if you're in a browser) and then be really silent and confusing when the library errors. I've been working against an error i thought was in tensorflow for hours only to realize it came from face-api.js code:

Error: getEnv - environment is not defined, check isNodejs() and isBrowser()

Tensorflow already has browser and node checks and it's own idea of environment. Why did you guys reinvent the wheel? :|

jeffreytgilbert avatar Apr 23 '19 20:04 jeffreytgilbert

I've been working against an error i thought was in tensorflow for hours only to realize it came from face-api.js code

I agree, that the error message might not be the best, but by looking at the stack trace one could have figured where the error message comes from.

The browser check is that complex, because we want to only initialize the corresponding environment of the library in case we are in a valid browser or nodejs environment to avoid errors at runtime. In any other case, it is up to the user to initialize the environment manually. All environment specifics can be monkey patched using faceapi.env.monkeyPatch.

justadudewhohacks avatar Apr 23 '19 20:04 justadudewhohacks

Ok, so I'm posting this update to let everyone in this thread know that it is possible today to fool both tensorflow and face-api.js into running in a web worker and that they will run GPU accelerated, however you shouldn't get your hopes way up for perfectly jank free UX.

In my app, face detection takes approximately 60ms on a MacBook Pro (Retina, 15-inch, Mid 2015), which is only processing 640x480 stills. The stills are transferred to the worker using zero copy, so they avoid the serialize/deserialize and structured copy performance hits.

The app itself is only taking 1-2ms for any given RAF cycle, but visual jank is still occurring on Chrome when the worker thread takes longer than expected. I'm not even seeing any GC issues. The jank appears to happen while processing microtasks. I see bunches of timers being set. I'd have to look further into the face-api.js source to see if it's breaking apart workloads into chunks using 0ms setTimeout calls. If it is, those should be converted to Promises. Allowing the browser to handle batch processing stacks of timeouts will definitely result in slower performance if that's what's happening. Timeouts can take 2-4ms to resolve and Promises are almost immediate. I believe the details of how promise scheduling is done is still on a per browser basis, but if you're in this thread, you're today only interested in the ones that support OffscreenCanvas, and that's Chrome. Chrome handles them async.

Here's the admittedly over engineered code for creating a worker environment that tensorflow and face-api.js will run in:

Parent `

	var screenCopy = {};
	for(let key in screen){
		screenCopy[key] = +screen[key];
	}
	screenCopy.orientation = {};
	for(let key in screen.orientation){
		if (typeof screen.orientation[key] !== 'function') {
			screenCopy.orientation[key] = screen.orientation[key];
		}
	}

	var visualViewportCopy = {};
	if (typeof window['visualViewport'] !== 'undefined') {
		for(let key in visualViewport){
			if(typeof visualViewport[key] !== 'function') {
				visualViewportCopy[key] = +visualViewport[key];
			}
		}
	}

	var styleMediaCopy = {};
	if (typeof window['styleMedia'] !== 'undefined') {
		for(let key in styleMedia){
			if(typeof styleMedia[key] !== 'function') {
				styleMediaCopy[key] = styleMedia[key];
			}
		}
	}

	let fakeWindow = {};
	Object.getOwnPropertyNames(window).forEach(name => {
		try {
			if (typeof window[name] !== 'function'){
				if (typeof window[name] !== 'object' && 
					name !== 'undefined' && 
					name !== 'NaN' && 
					name !== 'Infinity' && 
					name !== 'event' && 
					name !== 'name' 
				) {
					fakeWindow[name] = window[name];
				} else if (name === 'visualViewport') {
					console.log('want this?', name, JSON.parse(JSON.stringify(window[name])));
				} else if (name === 'styleMedia') {
					console.log('want this?', name, JSON.parse(JSON.stringify(window[name])));
				}
			}
		} catch (ex){
			console.log('Access denied for a window property');
		}
	});

	fakeWindow.screen = screenCopy;
	fakeWindow.visualViewport = visualViewportCopy;
	fakeWindow.styleMedia = styleMediaCopy;
	console.log(fakeWindow);

	let fakeDocument = {};
	for(let name in document){
		try {
			if(name === 'all') {
				// o_O
			} else if (typeof document[name] !== 'function' && typeof document[name] !== 'object') {
					fakeDocument[name] = document[name];
			} else if (typeof document[name] === 'object') {
				fakeDocument[name] = null;
			} else if(typeof document[name] === 'function') {
				fakeDocument[name] = { type:'*function*', name: document[name].name };
			}
		} catch (ex){
			console.log('Access denied for a window property');
		}
	}

`

Worker `

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

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

Image = HTMLImageElement; 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();

		// do terrible things to the worker's global namespace to fool tensorflow
		for (let key in event.data.fakeWindow) {
			if (!self[key]) {
				self[key] = event.data.fakeWindow[key];
			} 
		}
		window = Window = self;
		localStorage = new Storage();
		console.log('*faked* Window object for the worker', window);

		for (let key in event.data.fakeDocument) {
			if (document[key]) { continue; }

			let d = event.data.fakeDocument[key];
			// request to create a fake function (instead of doing a proxy trap, fake better)
			if (d && d.type && d.type === '*function*') {
				document[key] = function(){ console.log('FAKE instance', key, 'type', document[key].name, '(',document[key].arguments,')'); };
				document[key].name = d.name;
			} else {
				document[key] = d;
			}
		}
		console.log('*faked* Document object for the worker', document);

		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);

`

jeffreytgilbert avatar Apr 24 '19 02:04 jeffreytgilbert

Screen Shot 2019-04-23 at 9 05 50 PM Here's what I'm seeing btw. I'm going to try your suggestion of looking at using a different model that might process faster.

jeffreytgilbert avatar Apr 24 '19 02:04 jeffreytgilbert

Screen Shot 2019-04-24 at 12 42 36 AM

Check this out. This is what I'm talking about when I'm making this correlation. When timers are used in bulk, they appear to mess up the process scheduling by spamming the event loop. Promises don't appear to have the same problem. Chrome bundles them up nicely and still has the ability to handle requestAnimationFrame requests. I'd like to see if there's a way in face-api.js to fix the workload splitting so it doesn't rely on setTimeout

jeffreytgilbert avatar Apr 24 '19 05:04 jeffreytgilbert

I'd like to see if there's a way in face-api.js to fix the workload splitting so it doesn't rely on setTimeout

Hmm, actually there are no calls to setTimeout, tf.nextFrame or requestAnimationFrame in face-api.js. Could it be, that the async behaviour you are encountering here is due to downloading data from the GPU via tf.data()?

justadudewhohacks avatar Apr 24 '19 07:04 justadudewhohacks

Screen Shot 2019-04-24 at 2 34 49 AM

ok, possibly disproved the timer spam theory. I'm now pointing to the GPU work. While I was overwriting everything sacred (window, document, etc) I rewrote the setTimeout function so it uses promises and request animation frame for 0 ms setTimeouts, and falls back to setInterval for actual timers. It worked exactly how I anticipated it would, except that the jank is still present and the only thing left to point a finger at is the GPU load that's 2 frames long. 👎

So, for everyone watching, probably keep your GPU load in mind. It can block things just like anything else.

jeffreytgilbert avatar Apr 24 '19 07:04 jeffreytgilbert

Timeout replacement code

// More really bad practices to fix closed libraries. Here we overload setTimeout to replace it with a flawed promise implementation which sometimes cant be canceled.

let callStackCount = 0; const maxiumCallStackSize = 750; // chrome specific 10402, of 774 in my tests

setTimeout = function (timerHandler, timeout) { let args = Array.prototype.slice.call(arguments); args = args.length <3 ? [] : args.slice(2, args.length); if (timeout === 0) { if (callStackCount < maxiumCallStackSize) { var cancelator = {cancelable: false }; callStackCount++; new Promise(resolve=>{ resolve(timerHandler.apply(self, args)); }); return cancelator; } else { requestAnimationFrame(()=>{ timerHandler.apply(self, args); }); callStackCount = 0; return; } } const i = setInterval(()=>{ clearInterval(i); timerHandler.apply(self, args); }, timeout); return i; };

clearTimeout = (id)=>{ console.log(id); if(id && id.cancelable === false) { console.error('woops. cant cancel a 0ms timeout anymore! already ran it'); } else { clearInterval(id);} };

// var x = setTimeout((x,y,z)=>{console.log(x,y,z);}, 0, 'hello', 'im', 'cassius'); // var y = setTimeout((x,y,z)=>{console.log(x,y,z);}, 1000, 'hello', 'im', 'cassius'); // clearTimeout(x); // clearTimeout(y);

jeffreytgilbert avatar Apr 24 '19 07:04 jeffreytgilbert

Is there any other "cleaner" way to do this ?

@justadudewhohacks you mentioned the faceapi.env.monkeyPatch but how does it work exactly ?

I mean, lets say I have a main.js that only do this:

const worker = new Worker('worker.js');

worker.postMessage('foo');

and a worker where I want to be able to do this:

import * as faceapi from 'face-api.js';

faceapi.loadFaceExpressionMode('assets/models/');

onmessage = function(event) {
  console.log(event);
}

where and how should I use the faceapi.env.monkeyPatch ?

The error raised atm is the following:Uncaught (in promise) Error: getEnv - environment is not defined, check isNodejs() and isBrowser().

hyakki avatar Apr 24 '19 23:04 hyakki

@maximeparisse you would monkey patch environment specific after importing the package. In the nodejs examples we monkey patch Canvas, Image and ImageData for example, as shown here.

Refer to the Environment type to see what can be overridden.

justadudewhohacks avatar Apr 25 '19 07:04 justadudewhohacks

@justadudewhohacks : Thank you for your reply, i will give a shot and give my feedbacks here in case that can help others.

hyakki avatar Apr 25 '19 10:04 hyakki

@justadudewhohacks : I've tried without success to do that in a web worker. I understand how you patched the env spec in nodejs but i can't see how i can reproduce it for a web worker.

hyakki avatar Apr 25 '19 15:04 hyakki

@maximeparisse face-api.js only looks for those native methods as a node server vs browser check, per my example above. There are also tfjs detections. You have to set those values inside the worker before loading the libraries in order to fool the libraries into believing they're in a browser. If they fall into the node detection block, they will fail that check too, then bail out to a null result rather than a default (browser). A good patch to apply to face-api.js would be to add worker cases and change the detection to if/elseif/elseif/else style blocks so there is always a default case and more reasonable fallbacks. This is doable, but the trick is in supporting workers for browsers other than Chrome or Firefox with the flag set to enable OffscreenCanvas.

jeffreytgilbert avatar May 06 '19 15:05 jeffreytgilbert

Anyone manage to get this working?

ivanbacher avatar Sep 18 '19 15:09 ivanbacher

Yes. Turned out the integrated gpu was the biggest insurmountable bottleneck to avoid blocking the rendering pipeline. I’d be willing to revisit this once tensorflow and this lib have been updated to allow for offscreencanvas support, which is necessary to avoid excessive monkey patching and environmental fake outs to the two libs.

jeffreytgilbert avatar Sep 20 '19 14:09 jeffreytgilbert

Also: https://caniuse.com/#feat=offscreencanvas

jeffreytgilbert avatar Sep 20 '19 14:09 jeffreytgilbert

@jeffreytgilbert It appears TensorFlow.js now supports Offscreen Canvas... At least according to this article: https://medium.com/@wl1508/webworker-in-tensorflowjs-49a306ed60aa - does that jive with what you're seeing? Should face-api/tfjs "just work" in WebWorkers now...?

josiahbryan avatar Feb 03 '20 18:02 josiahbryan

I can sadly confirm that face-api does not Just Work, even with monkeyPatch.

When I do the following in my worker:

import * as faceapi from 'face-api.js';

faceapi.env.monkeyPatch({ Canvas: OffscreenCanvas })

I get:

Uncaught Error: monkeyPatch - environment is not defined, check isNodejs() and isBrowser()
    at Object.monkeyPatch (index.ts:38)

I've checked the isBrowser module, and I've done a TON of monkey patching of my own BEFORE calling monekyPatch(), and got the following checks to pass in my own code:

// isBrowserCheck is true in my tests
const isBrowserCheck = typeof window === 'object'
&& typeof document !== 'undefined'
&& typeof HTMLImageElement !== 'undefined'
&& typeof HTMLCanvasElement !== 'undefined'
&& typeof HTMLVideoElement !== 'undefined'
&& typeof ImageData !== 'undefined'
&& typeof CanvasRenderingContext2D !== 'undefined';

My own monkey patching is based on @jeffreytgilbert 's example above, with a few edits to make it compile, and I added CanvasRenderingContext2D = OffscreenCanvasRenderingContext2D;.

Bottom line: faceapi.env.monkeyPatch does not even try to monkey patch because of the error above.

Anyone have any suggestions on how to get this to even work? GPU or no GPU, I just want to try to get it to work. (Chrome 79 on a brand new MacBook Pro 15", so yes, OffscreenCanvas is supported.)

josiahbryan avatar Feb 03 '20 21:02 josiahbryan

Update: Got it working.

How? Use this gist: https://gist.github.com/josiahbryan/770ca1a9d72f1b35c13219ba84dc0495

Import it into your worker. If you have a bundler setup for your worker, just do (assuming you put it in your utils/ folder):

import './utils/faceEnvWorkerPatch';

Don't need to call faceapi's monkeyPatch if you use that.

Fair warning: That gist is NOT pretty. It is a conglomeration of hacks and workarounds and whatever else. But it works. Face detection is working for me now in a web worker.

Ideally, face-api would support a WebWorker WITHOUT having to do that horrendous hack of a monkey patch I just uploaded, but, yeah. At least this works now.

josiahbryan avatar Feb 03 '20 23:02 josiahbryan

I found my own way of monkey patching. Pretty simple but only supports OffscreenCanvas,

faceapi.env.setEnv(faceapi.env.createNodejsEnv());

faceapi.env.monkeyPatch({
    Canvas: OffscreenCanvas,
    createCanvasElement: () => {
        return new OffscreenCanvas(480, 270);
    },
});

No need to import canvas, supports OffscreenCanvas rigidly, and seems this is the easiest valid way.

wodnjs6512 avatar Feb 18 '20 10:02 wodnjs6512