ml5-save
Version:
441 lines (383 loc) • 16.4 kB
JavaScript
// Copyright (c) 2019 ml5
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
/* eslint prefer-destructuring: ["error", {AssignmentExpression: {array: false}}] */
/* eslint no-await-in-loop: "off" */
/*
* FaceApi: real-time face recognition, and landmark detection
* Ported and integrated from all the hard work by: https://github.com/justadudewhohacks/face-api.js?files=1
*/
import * as tf from '@tensorflow/tfjs';
import * as faceapi from 'face-api.js';
import callCallback from '../utils/callcallback';
const DEFAULTS = {
withLandmarks: true,
withDescriptors: true,
minConfidence: 0.5,
MODEL_URLS: {
Mobilenetv1Model: 'https://raw.githubusercontent.com/ml5js/ml5-data-and-models/face-api/models/faceapi/ssd_mobilenetv1_model-weights_manifest.json',
FaceLandmarkModel: 'https://raw.githubusercontent.com/ml5js/ml5-data-and-models/face-api/models/faceapi/face_landmark_68_model-weights_manifest.json',
FaceLandmark68TinyNet: 'https://raw.githubusercontent.com/ml5js/ml5-data-and-models/face-api/models/faceapi/face_landmark_68_tiny_model-weights_manifest.json',
FaceRecognitionModel: 'https://raw.githubusercontent.com/ml5js/ml5-data-and-models/face-api/models/faceapi/face_recognition_model-weights_manifest.json',
}
}
class FaceApiBase {
/**
* Create FaceApi.
* @param {HTMLVideoElement} video - An HTMLVideoElement.
* @param {object} options - An object with options.
* @param {function} callback - A callback to be called when the model is ready.
*/
constructor(video, options, callback) {
this.video = video;
this.model = null;
this.modelReady = false;
this.config = {
minConfidence: this.checkUndefined(options.minConfidence, DEFAULTS.minConfidence),
withLandmarks: this.checkUndefined(options.withLandmarks, DEFAULTS.withLandmarks),
withDescriptors: this.checkUndefined(options.withDescriptors, DEFAULTS.withDescriptors),
MODEL_URLS: {
Mobilenetv1Model: this.checkUndefined(options.Mobilenetv1Model, DEFAULTS.MODEL_URLS.Mobilenetv1Model),
FaceLandmarkModel: this.checkUndefined(options.FaceLandmarkModel, DEFAULTS.MODEL_URLS.FaceLandmarkModel),
FaceLandmark68TinyNet: this.checkUndefined(options.FaceLandmark68TinyNet, DEFAULTS.MODEL_URLS.FaceLandmark68TinyNet),
FaceRecognitionModel: this.checkUndefined(options.FaceRecognitionModel, DEFAULTS.MODEL_URLS.FaceRecognitionModel),
}
}
this.ready = callCallback(this.loadModel(), callback);
}
/**
* Load the model and set it to this.model
* @return {this} the BodyPix model.
*/
async loadModel() {
const modelOptions = [
"Mobilenetv1Model",
"FaceLandmarkModel",
"FaceLandmark68TinyNet",
"FaceRecognitionModel",
];
Object.keys(this.config.MODEL_URLS).forEach(item => {
if (modelOptions.includes(item)) {
this.config.MODEL_URLS[item] = this.getModelPath(this.config.MODEL_URLS[item]);
}
});
const {
Mobilenetv1Model,
FaceLandmarkModel,
FaceRecognitionModel,
} = this.config.MODEL_URLS;
this.model = faceapi;
const SsdMobilenetv1Options = this.model.SsdMobilenetv1Options({
minConfidence: this.minConfidence
})
await this.model.loadSsdMobilenetv1Model(Mobilenetv1Model, SsdMobilenetv1Options)
await this.model.loadFaceLandmarkModel(FaceLandmarkModel)
// await this.model.loadFaceLandmarkTinyModel(FaceLandmark68TinyNet)
await this.model.loadFaceRecognitionModel(FaceRecognitionModel)
this.modelReady = true;
return this;
}
/**
* .detect() - classifies multiple features by default
* @param {*} optionsOrCallback
* @param {*} configOrCallback
* @param {*} cb
*/
async detect(optionsOrCallback, configOrCallback, cb) {
let imgToClassify = this.video;
let callback;
let faceApiOptions = this.config;
// Handle the image to predict
if (typeof optionsOrCallback === 'function') {
imgToClassify = this.video;
callback = optionsOrCallback;
// clean the following conditional statement up!
} else if (optionsOrCallback instanceof HTMLImageElement ||
optionsOrCallback instanceof HTMLCanvasElement ||
optionsOrCallback instanceof HTMLVideoElement ||
optionsOrCallback instanceof ImageData) {
imgToClassify = optionsOrCallback;
} else if (typeof optionsOrCallback === 'object' && (optionsOrCallback.elt instanceof HTMLImageElement ||
optionsOrCallback.elt instanceof HTMLCanvasElement ||
optionsOrCallback.elt instanceof HTMLVideoElement ||
optionsOrCallback.elt instanceof ImageData)) {
imgToClassify = optionsOrCallback.elt; // Handle p5.js image
} else if (typeof optionsOrCallback === 'object' && optionsOrCallback.canvas instanceof HTMLCanvasElement) {
imgToClassify = optionsOrCallback.canvas; // Handle p5.js image
} else if (!(this.video instanceof HTMLVideoElement)) {
// Handle unsupported input
throw new Error(
'No input image provided. If you want to classify a video, pass the video element in the constructor. ',
);
}
if (typeof configOrCallback === 'object') {
faceApiOptions = configOrCallback;
} else if (typeof configOrCallback === 'function') {
callback = configOrCallback;
}
if (typeof cb === 'function') {
callback = cb;
}
return callCallback(this.detectInternal(imgToClassify, faceApiOptions), callback);
}
/**
* Detects multiple internal function
* @param {HTMLImageElement || HTMLVideoElement} imgToClassify
* @param {Object} faceApiOptions
*/
async detectInternal(imgToClassify, faceApiOptions) {
await this.ready;
await tf.nextFrame();
if (this.video && this.video.readyState === 0) {
await new Promise(resolve => {
this.video.onloadeddata = () => resolve();
});
}
// sets the return options if any are passed in during .detect() or .detectSingle()
this.config = this.setReturnOptions(faceApiOptions);
const {
withLandmarks,
withDescriptors,
} = this.config
let result;
if (withLandmarks) {
if (withDescriptors) {
result = await this.model.detectAllFaces(imgToClassify).withFaceLandmarks().withFaceDescriptors();
} else {
result = await this.model.detectAllFaces(imgToClassify).withFaceLandmarks()
}
} else if (!withLandmarks) {
result = await this.model.detectAllFaces(imgToClassify)
} else {
result = await this.model.detectAllFaces(imgToClassify).withFaceLandmarks().withFaceDescriptors();
}
// always resize the results to the input image size
result = this.resizeResults(result, imgToClassify.width, imgToClassify.height)
// assign the {parts} object after resizing
result = this.landmarkParts(result);
return result
}
/**
* .detecSinglet() - classifies a single feature with higher accuracy
* @param {*} optionsOrCallback
* @param {*} configOrCallback
* @param {*} cb
*/
async detectSingle(optionsOrCallback, configOrCallback, cb) {
let imgToClassify = this.video;
let callback;
let faceApiOptions = this.config;
// Handle the image to predict
if (typeof optionsOrCallback === 'function') {
imgToClassify = this.video;
callback = optionsOrCallback;
// clean the following conditional statement up!
} else if (optionsOrCallback instanceof HTMLImageElement ||
optionsOrCallback instanceof HTMLCanvasElement ||
optionsOrCallback instanceof HTMLVideoElement ||
optionsOrCallback instanceof ImageData) {
imgToClassify = optionsOrCallback;
} else if (typeof optionsOrCallback === 'object' && (optionsOrCallback.elt instanceof HTMLImageElement ||
optionsOrCallback.elt instanceof HTMLCanvasElement ||
optionsOrCallback.elt instanceof HTMLVideoElement ||
optionsOrCallback.elt instanceof ImageData)) {
imgToClassify = optionsOrCallback.elt; // Handle p5.js image
} else if (typeof optionsOrCallback === 'object' && optionsOrCallback.canvas instanceof HTMLCanvasElement) {
imgToClassify = optionsOrCallback.canvas; // Handle p5.js image
} else if (!(this.video instanceof HTMLVideoElement)) {
// Handle unsupported input
throw new Error(
'No input image provided. If you want to classify a video, pass the video element in the constructor. ',
);
}
if (typeof configOrCallback === 'object') {
faceApiOptions = configOrCallback;
} else if (typeof configOrCallback === 'function') {
callback = configOrCallback;
}
if (typeof cb === 'function') {
callback = cb;
}
return callCallback(this.detectSingleInternal(imgToClassify, faceApiOptions), callback);
}
/**
* Detects only a single feature
* @param {HTMLImageElement || HTMLVideoElement} imgToClassify
* @param {Object} faceApiOptions
*/
async detectSingleInternal(imgToClassify, faceApiOptions) {
await this.ready;
await tf.nextFrame();
if (this.video && this.video.readyState === 0) {
await new Promise(resolve => {
this.video.onloadeddata = () => resolve();
});
}
// sets the return options if any are passed in during .detect() or .detectSingle()
this.config = this.setReturnOptions(faceApiOptions);
const {
withLandmarks,
withDescriptors
} = this.config
let result;
if (withLandmarks) {
if (withDescriptors) {
result = await this.model.detectSingleFace(imgToClassify).withFaceLandmarks().withFaceDescriptor();
} else {
result = await this.model.detectSingleFace(imgToClassify).withFaceLandmarks()
}
} else if (!withLandmarks) {
result = await this.model.detectSingleFace(imgToClassify)
} else {
result = await this.model.detectSingleFace(imgToClassify).withFaceLandmarks().withFaceDescriptor();
}
// always resize the results to the input image size
result = this.resizeResults(result, imgToClassify.width, imgToClassify.height)
// assign the {parts} object after resizing
result = this.landmarkParts(result);
return result
}
/**
* Check if the given _param is undefined, otherwise return the _default
* @param {*} _param
* @param {*} _default
*/
checkUndefined(_param, _default) {
return _param !== undefined ? _param : _default;
}
/**
* Checks if the given string is an absolute or relative path and returns
* the path to the modelJson
* @param {String} absoluteOrRelativeUrl
*/
getModelPath(absoluteOrRelativeUrl) {
const modelJsonPath = this.isAbsoluteURL(absoluteOrRelativeUrl) ? absoluteOrRelativeUrl : window.location.pathname + absoluteOrRelativeUrl
return modelJsonPath;
}
/**
* Sets the return options for .detect() or .detectSingle() in case any are given
* @param {Object} faceApiOptions
*/
setReturnOptions(faceApiOptions) {
const output = Object.assign({}, this.config);
const options = ["withLandmarks", "withDescriptors"];
options.forEach(prop => {
if (faceApiOptions[prop] !== undefined) {
this.config[prop] = faceApiOptions[prop]
} else {
output[prop] = this.config[prop];
}
})
return output;
}
/**
* Resize results to size of input image
* @param {*} str
*/
resizeResults(detections, width, height) {
if (width === undefined || height === undefined) {
throw new Error('width and height must be defined')
}
return this.model.resizeResults(detections, {
"width": width,
"height": height
})
}
/* eslint class-methods-use-this: "off" */
isAbsoluteURL(str) {
const pattern = new RegExp('^(?:[a-z]+:)?//', 'i');
return !!pattern.test(str);
}
/**
* get parts from landmarks
* @param {*} result
*/
landmarkParts(result) {
let output;
// multiple detections is an array
if (Array.isArray(result) === true) {
output = result.map(item => {
// if landmarks exist return parts
const newItem = Object.assign({}, item);
if (newItem.landmarks) {
const {
landmarks
} = newItem;
newItem.parts = {
mouth: landmarks.getMouth(),
nose: landmarks.getNose(),
leftEye: landmarks.getLeftEye(),
leftEyeBrow: landmarks.getLeftEyeBrow(),
rightEye: landmarks.getRightEye(),
rightEyeBrow: landmarks.getRightEyeBrow(),
jawOutline: landmarks.getJawOutline(),
}
} else {
newItem.parts = {
mouth: [],
nose: [],
leftEye: [],
leftEyeBrow: [],
rightEye: [],
rightEyeBrow: [],
jawOutline: [],
}
}
return newItem;
})
// single detection is an object
} else {
output = Object.assign({}, result);
if (output.landmarks) {
const {
landmarks
} = result;
output.parts = {
mouth: landmarks.getMouth(),
nose: landmarks.getNose(),
leftEye: landmarks.getLeftEye(),
leftEyeBrow: landmarks.getLeftEyeBrow(),
rightEye: landmarks.getRightEye(),
rightEyeBrow: landmarks.getRightEyeBrow()
}
} else {
output.parts = {
mouth: [],
nose: [],
leftEye: [],
leftEyeBrow: [],
rightEye: [],
rightEyeBrow: []
}
}
}
return output;
}
}
const faceApi = (videoOrOptionsOrCallback, optionsOrCallback, cb) => {
let video;
let options = {};
let callback = cb;
if (videoOrOptionsOrCallback instanceof HTMLVideoElement) {
video = videoOrOptionsOrCallback;
} else if (
typeof videoOrOptionsOrCallback === 'object' &&
videoOrOptionsOrCallback.elt instanceof HTMLVideoElement
) {
video = videoOrOptionsOrCallback.elt; // Handle a p5.js video element
} else if (typeof videoOrOptionsOrCallback === 'object') {
options = videoOrOptionsOrCallback;
} else if (typeof videoOrOptionsOrCallback === 'function') {
callback = videoOrOptionsOrCallback;
}
if (typeof optionsOrCallback === 'object') {
options = optionsOrCallback;
console.log(options)
} else if (typeof optionsOrCallback === 'function') {
callback = optionsOrCallback;
}
const instance = new FaceApiBase(video, options, callback);
return callback ? instance : instance.ready;
}
export default faceApi;