update node demos

pull/46/head
Vladimir Mandic 2021-03-26 10:26:02 -04:00
parent 23bdd3f086
commit 40b3a65bdc
27 changed files with 245674 additions and 5111 deletions

View File

@ -1,7 +1,7 @@
# @vladmandic/face-api
Version: **1.1.4**
Description: **FaceAPI: AI-powered Face Detection, Face Embedding & Recognition Using Tensorflow/JS**
Version: **1.1.5**
Description: **FaceAPI: AI-powered Face Detection, Description & Recognition using Tensorflow/JS**
Author: **Vladimir Mandic <mandic00@live.com>**
License: **MIT** </LICENSE>
@ -9,8 +9,12 @@ Repository: **<git+https://github.com/vladmandic/face-api.git>**
## Changelog
### **HEAD -> master** 2021/03/19 mandic00@live.com
### **HEAD -> master** 2021/03/25 mandic00@live.com
### **1.1.5** 2021/03/23 mandic00@live.com
- add node-canvas demo
- refactoring
### **1.1.4** 2021/03/18 mandic00@live.com

View File

@ -54,9 +54,7 @@ Example can be accessed directly using Git pages using URL:
Three NodeJS examples are:
- `/demo/node-singleprocess.js`:
Regular usage of `FaceAPI` from `NodeJS`
- `/demo/node-singleprocess.js`:
- `/demo/node.js`:
Regular usage of `FaceAPI` from `NodeJS`
Using `TFJS` native methods to load images
- `/demo/node-canvas.js`:

View File

@ -5,24 +5,24 @@ const process = require('process');
const path = require('path');
// eslint-disable-next-line import/no-extraneous-dependencies, node/no-unpublished-require
const log = require('@vladmandic/pilogger');
// eslint-disable-next-line import/no-extraneous-dependencies, node/no-unpublished-require
// eslint-disable-next-line import/no-extraneous-dependencies, node/no-unpublished-require, no-unused-vars
const tf = require('@tensorflow/tfjs-node');
// eslint-disable-next-line import/no-extraneous-dependencies, node/no-unpublished-require
const canvas = require('canvas');
const faceapi = require('../dist/face-api.node.js'); // this is equivalent to '@vladmandic/faceapi'
const modelPathRoot = '../model';
const imgPathRoot = './demo'; // modify to include your sample images
const minScore = 0.1;
const minConfidence = 0.15;
const maxResults = 5;
let optionsSSDMobileNet;
async function image(img) {
const buffer = fs.readFileSync(img);
const decoded = tf.node.decodeImage(buffer);
const casted = decoded.toFloat();
const result = casted.expandDims(0);
decoded.dispose();
casted.dispose();
return result;
async function image(input) {
const img = await canvas.loadImage(input);
const c = canvas.createCanvas(img.width, img.height);
const ctx = c.getContext('2d');
ctx.drawImage(img, 0, 0, img.width, img.height);
return c;
}
async function detect(tensor) {
@ -35,10 +35,19 @@ async function detect(tensor) {
return result;
}
function print(face) {
const expression = Object.entries(face.expressions).reduce((acc, val) => ((val[1] > acc[1]) ? val : acc), ['', 0]);
const box = [face.alignedRect._box._x, face.alignedRect._box._y, face.alignedRect._box._width, face.alignedRect._box._height];
const gender = `Gender: ${Math.round(100 * face.genderProbability)}% ${face.gender}`;
log.data(`Detection confidence: ${Math.round(100 * face.detection._score)}% ${gender} Age: ${Math.round(10 * face.age) / 10} Expression: ${Math.round(100 * expression[1])}% ${expression[0]} Box: ${box.map((a) => Math.round(a))}`);
}
async function main() {
log.header();
log.info('FaceAPI single-process test');
faceapi.env.monkeyPatch({ Canvas: canvas.Canvas, Image: canvas.Image, ImageData: canvas.ImageData });
await faceapi.tf.setBackend('tensorflow');
await faceapi.tf.enableProdMode();
await faceapi.tf.ENV.set('DEBUG', false);
@ -53,33 +62,27 @@ async function main() {
await faceapi.nets.faceLandmark68Net.loadFromDisk(modelPath);
await faceapi.nets.faceRecognitionNet.loadFromDisk(modelPath);
await faceapi.nets.faceExpressionNet.loadFromDisk(modelPath);
optionsSSDMobileNet = new faceapi.SsdMobilenetv1Options({ minConfidence: minScore, maxResults });
optionsSSDMobileNet = new faceapi.SsdMobilenetv1Options({ minConfidence, maxResults });
if (process.argv.length !== 3) {
const t0 = process.hrtime.bigint();
const dir = fs.readdirSync(imgPathRoot);
for (const img of dir) {
if (!img.toLocaleLowerCase().endsWith('.jpg')) continue;
const tensor = await image(path.join(imgPathRoot, img));
const result = await detect(tensor);
const c = await image(path.join(imgPathRoot, img));
const result = await detect(c);
log.data('Image:', img, 'Detected faces:', result.length);
for (const i of result) {
log.data('Gender:', Math.round(100 * i.genderProbability), 'probability', i.gender, 'with age', Math.round(10 * i.age) / 10);
}
tensor.dispose();
for (const face of result) print(face);
}
const t1 = process.hrtime.bigint();
log.info('Processed', dir.length, 'images in', Math.trunc(parseInt(t1 - t0) / 1000 / 1000), 'ms');
} else {
const param = process.argv[2];
if (fs.existsSync(param)) {
const tensor = await image(param);
const result = await detect(tensor);
const c = await image(param);
const result = await detect(c);
log.data('Image:', param, 'Detected faces:', result.length);
for (const i of result) {
log.data('Gender:', Math.round(100 * i.genderProbability), 'probability', i.gender, 'with age', Math.round(10 * i.age) / 10);
}
tensor.dispose();
for (const face of result) print(face);
}
}
}

View File

@ -12,7 +12,7 @@ const faceapi = require('../dist/face-api.node.js'); // this is equivalent to '@
// options used by faceapi
const modelPathRoot = '../model';
const minScore = 0.1;
const minConfidence = 0.15;
const maxResults = 5;
let optionsSSDMobileNet;
@ -62,7 +62,7 @@ async function main() {
await faceapi.nets.faceLandmark68Net.loadFromDisk(modelPath);
await faceapi.nets.faceRecognitionNet.loadFromDisk(modelPath);
await faceapi.nets.faceExpressionNet.loadFromDisk(modelPath);
optionsSSDMobileNet = new faceapi.SsdMobilenetv1Options({ minConfidence: minScore, maxResults });
optionsSSDMobileNet = new faceapi.SsdMobilenetv1Options({ minConfidence, maxResults });
// now we're ready, so send message back to main that it knows it can use this worker
process.send({ ready: true });

View File

@ -5,24 +5,24 @@ const process = require('process');
const path = require('path');
// eslint-disable-next-line import/no-extraneous-dependencies, node/no-unpublished-require
const log = require('@vladmandic/pilogger');
// eslint-disable-next-line import/no-extraneous-dependencies, node/no-unpublished-require, no-unused-vars
const tf = require('@tensorflow/tfjs-node');
// eslint-disable-next-line import/no-extraneous-dependencies, node/no-unpublished-require
const canvas = require('canvas');
const tf = require('@tensorflow/tfjs-node');
const faceapi = require('../dist/face-api.node.js'); // this is equivalent to '@vladmandic/faceapi'
const modelPathRoot = '../model';
const imgPathRoot = './demo'; // modify to include your sample images
const minScore = 0.1;
const minConfidence = 0.15;
const maxResults = 5;
let optionsSSDMobileNet;
async function image(input) {
const img = canvas.loadImage(input);
const c = canvas.createCanvas(img.width, img.height);
const ctx = c.getContext('2d');
ctx.drawImage(img, 0, 0, img.width, img.height);
return c;
async function image(img) {
const buffer = fs.readFileSync(img);
const decoded = tf.node.decodeImage(buffer);
const casted = decoded.toFloat();
const result = casted.expandDims(0);
decoded.dispose();
casted.dispose();
return result;
}
async function detect(tensor) {
@ -35,12 +35,17 @@ async function detect(tensor) {
return result;
}
function print(face) {
const expression = Object.entries(face.expressions).reduce((acc, val) => ((val[1] > acc[1]) ? val : acc), ['', 0]);
const box = [face.alignedRect._box._x, face.alignedRect._box._y, face.alignedRect._box._width, face.alignedRect._box._height];
const gender = `Gender: ${Math.round(100 * face.genderProbability)}% ${face.gender}`;
log.data(`Detection confidence: ${Math.round(100 * face.detection._score)}% ${gender} Age: ${Math.round(10 * face.age) / 10} Expression: ${Math.round(100 * expression[1])}% ${expression[0]} Box: ${box.map((a) => Math.round(a))}`);
}
async function main() {
log.header();
log.info('FaceAPI single-process test');
faceapi.env.monkeyPatch({ Canvas: canvas.Canvas, Image: canvas.Image, ImageData: canvas.ImageData });
await faceapi.tf.setBackend('tensorflow');
await faceapi.tf.enableProdMode();
await faceapi.tf.ENV.set('DEBUG', false);
@ -55,7 +60,7 @@ async function main() {
await faceapi.nets.faceLandmark68Net.loadFromDisk(modelPath);
await faceapi.nets.faceRecognitionNet.loadFromDisk(modelPath);
await faceapi.nets.faceExpressionNet.loadFromDisk(modelPath);
optionsSSDMobileNet = new faceapi.SsdMobilenetv1Options({ minConfidence: minScore, maxResults });
optionsSSDMobileNet = new faceapi.SsdMobilenetv1Options({ minConfidence, maxResults });
if (process.argv.length !== 3) {
const t0 = process.hrtime.bigint();
@ -65,9 +70,7 @@ async function main() {
const tensor = await image(path.join(imgPathRoot, img));
const result = await detect(tensor);
log.data('Image:', img, 'Detected faces:', result.length);
for (const i of result) {
log.data('Gender:', Math.round(100 * i.genderProbability), 'probability', i.gender, 'with age', Math.round(10 * i.age) / 10);
}
for (const face of result) print(face);
tensor.dispose();
}
const t1 = process.hrtime.bigint();
@ -78,9 +81,7 @@ async function main() {
const tensor = await image(param);
const result = await detect(tensor);
log.data('Image:', param, 'Detected faces:', result.length);
for (const i of result) {
log.data('Gender:', Math.round(100 * i.genderProbability), 'probability', i.gender, 'with age', Math.round(10 * i.age) / 10);
}
for (const face of result) print(face);
tensor.dispose();
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

77988
dist/face-api.esm.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

77878
dist/face-api.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4678
dist/face-api.node.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

76100
dist/tfjs.esm.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -21,7 +21,7 @@
"url": "git+https://github.com/vladmandic/face-api.git"
},
"scripts": {
"start": "node --trace-warnings demo/node-singleprocess.js",
"start": "node --trace-warnings demo/node.js",
"dev": "node --trace-warnings server/serve.js",
"build": "rimraf dist/* types/* typedoc/* && node server/build.js",
"lint": "eslint src/**/* demo/*.js server/*.js",
@ -43,14 +43,14 @@
"@tensorflow/tfjs-backend-wasm": "^3.3.0",
"@tensorflow/tfjs-node": "^3.3.0",
"@tensorflow/tfjs-node-gpu": "^3.3.0",
"@types/node": "^14.14.35",
"@types/node": "^14.14.36",
"@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0",
"@vladmandic/pilogger": "^0.2.15",
"canvas": "^2.7.0",
"chokidar": "^3.5.1",
"dayjs": "^1.10.4",
"esbuild": "^0.9.6",
"esbuild": "^0.10.1",
"eslint": "^7.22.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.22.1",
@ -61,7 +61,7 @@
"seedrandom": "^3.0.5",
"simple-git": "^2.37.0",
"tslib": "^2.1.0",
"typedoc": "^0.20.33",
"typedoc": "^0.20.34",
"typescript": "^4.2.3"
}
}

View File

@ -42,9 +42,9 @@ const tsconfig = {
// common configuration
const common = {
banner,
minifyWhitespace: true,
minifyIdentifiers: true,
minifySyntax: true,
minifyWhitespace: false,
minifyIdentifiers: false,
minifySyntax: false,
bundle: true,
sourcemap: true,
metafile: true,

View File

@ -4,9 +4,7 @@ import { isMediaLoaded } from './isMediaLoaded';
export function awaitMediaLoaded(media: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement) {
// eslint-disable-next-line consistent-return
return new Promise((resolve, reject) => {
if (media instanceof env.getEnv().Canvas || isMediaLoaded(media)) {
return resolve(null);
}
if (media instanceof env.getEnv().Canvas || isMediaLoaded(media)) return resolve(null);
function onError(e: Event) {
if (!e.currentTarget) return;

View File

@ -16,29 +16,17 @@ import { TNetInput } from './types';
*/
export async function extractFaces(input: TNetInput, detections: Array<FaceDetection | Rect>): Promise<HTMLCanvasElement[]> {
const { Canvas } = env.getEnv();
let canvas = input as HTMLCanvasElement;
if (!(input instanceof Canvas)) {
const netInput = await toNetInput(input);
if (netInput.batchSize > 1) {
throw new Error('extractFaces - batchSize > 1 not supported');
}
if (netInput.batchSize > 1) throw new Error('extractFaces - batchSize > 1 not supported');
const tensorOrCanvas = netInput.getInput(0);
canvas = tensorOrCanvas instanceof Canvas
? tensorOrCanvas
: await imageTensorToCanvas(tensorOrCanvas);
canvas = tensorOrCanvas instanceof Canvas ? tensorOrCanvas : await imageTensorToCanvas(tensorOrCanvas);
}
const ctx = getContext2dOrThrow(canvas);
const boxes = detections
.map((det) => (det instanceof FaceDetection
? det.forSize(canvas.width, canvas.height).box.floor()
: det))
.map((det) => (det instanceof FaceDetection ? det.forSize(canvas.width, canvas.height).box.floor() : det))
.map((box) => box.clipAtImageBorders(canvas.width, canvas.height));
return boxes.map(({ x, y, width, height }) => {
const faceImg = createCanvas({ width, height });
if (width > 0 && height > 0) getContext2dOrThrow(faceImg).putImageData(ctx.getImageData(x, y, width, height), 0, 0);

View File

@ -13,44 +13,23 @@ import { TNetInput } from './types';
* @returns A NetInput instance, which can be passed into one of the neural networks.
*/
export async function toNetInput(inputs: TNetInput): Promise<NetInput> {
if (inputs instanceof NetInput) {
return inputs;
}
const inputArgArray = Array.isArray(inputs)
? inputs
: [inputs];
if (!inputArgArray.length) {
throw new Error('toNetInput - empty array passed as input');
}
if (inputs instanceof NetInput) return inputs;
const inputArgArray = Array.isArray(inputs) ? inputs : [inputs];
if (!inputArgArray.length) throw new Error('toNetInput - empty array passed as input');
const getIdxHint = (idx: number) => (Array.isArray(inputs) ? ` at input index ${idx}:` : '');
const inputArray = inputArgArray.map(resolveInput);
inputArray.forEach((input, i) => {
if (!isMediaElement(input) && !isTensor3D(input) && !isTensor4D(input)) {
if (typeof inputArgArray[i] === 'string') {
throw new Error(`toNetInput -${getIdxHint(i)} string passed, but could not resolve HTMLElement for element id ${inputArgArray[i]}`);
}
if (typeof inputArgArray[i] === 'string') throw new Error(`toNetInput -${getIdxHint(i)} string passed, but could not resolve HTMLElement for element id ${inputArgArray[i]}`);
throw new Error(`toNetInput -${getIdxHint(i)} expected media to be of type HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | tf.Tensor3D, or to be an element id`);
}
if (isTensor4D(input)) {
// if tf.Tensor4D is passed in the input array, the batch size has to be 1
const batchSize = input.shape[0];
if (batchSize !== 1) {
throw new Error(`toNetInput -${getIdxHint(i)} tf.Tensor4D with batchSize ${batchSize} passed, but not supported in input array`);
}
if (batchSize !== 1) throw new Error(`toNetInput -${getIdxHint(i)} tf.Tensor4D with batchSize ${batchSize} passed, but not supported in input array`);
}
});
// wait for all media elements being loaded
await Promise.all(
inputArray.map((input) => isMediaElement(input) && awaitMediaLoaded(input)),
);
await Promise.all(inputArray.map((input) => isMediaElement(input) && awaitMediaLoaded(input)));
return new NetInput(inputArray, Array.isArray(inputs));
}

View File

@ -13,12 +13,8 @@ import { PredictAllFaceExpressionsTask, PredictSingleFaceExpressionsTask } from
import { FaceDetectionOptions } from './types';
export class DetectFacesTaskBase<TReturn> extends ComposableTask<TReturn> {
constructor(
// eslint-disable-next-line no-unused-vars
protected input: TNetInput,
// eslint-disable-next-line no-unused-vars
protected options: FaceDetectionOptions = new SsdMobilenetv1Options(),
) {
// eslint-disable-next-line no-unused-vars
constructor(protected input: TNetInput, protected options: FaceDetectionOptions = new SsdMobilenetv1Options()) {
super();
}
}

View File

@ -4,21 +4,13 @@ import { SsdMobilenetv1Options } from '../ssdMobilenetv1/index';
import { ITinyYolov2Options, TinyYolov2Options } from '../tinyYolov2/index';
import { detectAllFaces } from './detectFaces';
// export allFaces API for backward compatibility
export async function allFacesSsdMobilenetv1(
input: TNetInput,
minConfidence?: number,
): Promise<WithFaceDescriptor<WithFaceLandmarks<WithFaceDetection<{}>>>[]> {
export async function allFacesSsdMobilenetv1(input: TNetInput, minConfidence?: number): Promise<WithFaceDescriptor<WithFaceLandmarks<WithFaceDetection<{}>>>[]> {
return detectAllFaces(input, new SsdMobilenetv1Options(minConfidence ? { minConfidence } : {}))
.withFaceLandmarks()
.withFaceDescriptors();
}
export async function allFacesTinyYolov2(
input: TNetInput,
forwardParams: ITinyYolov2Options = {},
): Promise<WithFaceDescriptor<WithFaceLandmarks<WithFaceDetection<{}>>>[]> {
export async function allFacesTinyYolov2(input: TNetInput, forwardParams: ITinyYolov2Options = {}): Promise<WithFaceDescriptor<WithFaceLandmarks<WithFaceDetection<{}>>>[]> {
return detectAllFaces(input, new TinyYolov2Options(forwardParams))
.withFaceLandmarks()
.withFaceDescriptors();

View File

@ -43,10 +43,7 @@ export class SsdMobilenetv1 extends NeuralNetwork<NetParams> {
const { maxResults, minConfidence } = new SsdMobilenetv1Options(options);
const netInput = await toNetInput(input);
const {
boxes: _boxes,
scores: _scores,
} = this.forwardInput(netInput);
const { boxes: _boxes, scores: _scores } = this.forwardInput(netInput);
const boxes = _boxes[0];
const scores = _scores[0];
@ -57,13 +54,7 @@ export class SsdMobilenetv1 extends NeuralNetwork<NetParams> {
const scoresData = Array.from(scores.dataSync());
const iouThreshold = 0.5;
const indices = nonMaxSuppression(
boxes,
scoresData as number[],
maxResults,
iouThreshold,
minConfidence,
);
const indices = nonMaxSuppression(boxes, scoresData as number[], maxResults, iouThreshold, minConfidence);
const reshapedDims = netInput.getReshapedInputDimensions(0);
const inputSize = netInput.inputSize as number;
@ -83,16 +74,8 @@ export class SsdMobilenetv1 extends NeuralNetwork<NetParams> {
].map((val) => val * padX);
return new FaceDetection(
scoresData[idx] as number,
new Rect(
left,
top,
right - left,
bottom - top,
),
{
height: netInput.getInputHeight(0),
width: netInput.getInputWidth(0),
},
new Rect(left, top, right - left, bottom - top),
{ height: netInput.getInputHeight(0), width: netInput.getInputWidth(0) },
);
});