2022-05-19 09:02:40 +00:00
|
|
|
import { setWasmPaths } from '@tensorflow/tfjs-backend-wasm';
|
|
|
|
import { Human, Config, FaceResult } from '@vladmandic/human';
|
|
|
|
|
2022-06-10 12:19:18 +00:00
|
|
|
import { DETECTION_TYPES, FACE_DETECTION_SCORE_THRESHOLD, FACE_EXPRESSIONS_NAMING_MAPPING } from './constants';
|
2022-05-19 09:02:40 +00:00
|
|
|
|
|
|
|
type DetectInput = {
|
|
|
|
image: ImageBitmap | ImageData,
|
|
|
|
threshold: number
|
|
|
|
};
|
|
|
|
|
|
|
|
type FaceBox = {
|
|
|
|
left: number,
|
|
|
|
right: number,
|
|
|
|
width?: number
|
|
|
|
};
|
|
|
|
|
|
|
|
type InitInput = {
|
|
|
|
baseUrl: string,
|
2022-06-16 11:50:31 +00:00
|
|
|
detectionTypes: string[]
|
2022-05-19 09:02:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type DetectOutput = {
|
|
|
|
faceExpression?: string,
|
2022-06-16 11:50:31 +00:00
|
|
|
faceBox?: FaceBox,
|
|
|
|
faceCount: number
|
2022-05-19 09:02:40 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
export interface FaceLandmarksHelper {
|
2022-06-16 11:50:31 +00:00
|
|
|
getFaceBox(detections: Array<FaceResult>, threshold: number): FaceBox | undefined;
|
|
|
|
getFaceExpression(detections: Array<FaceResult>): string | undefined;
|
|
|
|
getFaceCount(detections : Array<FaceResult>): number;
|
|
|
|
getDetections(image: ImageBitmap | ImageData): Promise<Array<FaceResult>>;
|
2022-05-19 09:02:40 +00:00
|
|
|
init(): Promise<void>;
|
2022-06-16 11:50:31 +00:00
|
|
|
detect({ image, threshold } : DetectInput): Promise<DetectOutput>;
|
2022-05-19 09:02:40 +00:00
|
|
|
getDetectionInProgress(): boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper class for human library
|
|
|
|
*/
|
|
|
|
export class HumanHelper implements FaceLandmarksHelper {
|
|
|
|
protected human: Human | undefined;
|
|
|
|
protected faceDetectionTypes: string[];
|
|
|
|
protected baseUrl: string;
|
|
|
|
private detectionInProgress = false;
|
|
|
|
private lastValidFaceBox: FaceBox | undefined;
|
|
|
|
/**
|
|
|
|
* Configuration for human.
|
|
|
|
*/
|
|
|
|
private config: Partial<Config> = {
|
|
|
|
backend: 'humangl',
|
|
|
|
async: true,
|
|
|
|
warmup: 'none',
|
|
|
|
cacheModels: true,
|
|
|
|
cacheSensitivity: 0,
|
|
|
|
debug: false,
|
|
|
|
deallocate: true,
|
|
|
|
filter: { enabled: false },
|
|
|
|
face: {
|
2022-06-08 17:28:41 +00:00
|
|
|
enabled: false,
|
2022-05-19 09:02:40 +00:00
|
|
|
detector: {
|
|
|
|
enabled: false,
|
|
|
|
rotation: false,
|
2022-06-08 17:28:41 +00:00
|
|
|
modelPath: 'blazeface-front.json',
|
2022-06-16 11:50:31 +00:00
|
|
|
maxDetected: 20
|
2022-05-19 09:02:40 +00:00
|
|
|
},
|
|
|
|
mesh: { enabled: false },
|
|
|
|
iris: { enabled: false },
|
|
|
|
emotion: {
|
|
|
|
enabled: false,
|
|
|
|
modelPath: 'emotion.json'
|
|
|
|
},
|
|
|
|
description: { enabled: false }
|
|
|
|
},
|
|
|
|
hand: { enabled: false },
|
|
|
|
gesture: { enabled: false },
|
|
|
|
body: { enabled: false },
|
|
|
|
segmentation: { enabled: false }
|
|
|
|
};
|
|
|
|
|
2022-06-16 11:50:31 +00:00
|
|
|
constructor({ baseUrl, detectionTypes }: InitInput) {
|
2022-05-19 09:02:40 +00:00
|
|
|
this.faceDetectionTypes = detectionTypes;
|
|
|
|
this.baseUrl = baseUrl;
|
|
|
|
this.init();
|
|
|
|
}
|
|
|
|
|
|
|
|
async init(): Promise<void> {
|
|
|
|
|
|
|
|
if (!this.human) {
|
|
|
|
this.config.modelBasePath = this.baseUrl;
|
|
|
|
if (!self.OffscreenCanvas) {
|
|
|
|
this.config.backend = 'wasm';
|
|
|
|
this.config.wasmPath = this.baseUrl;
|
|
|
|
setWasmPaths(this.baseUrl);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.faceDetectionTypes.length > 0 && this.config.face) {
|
|
|
|
this.config.face.enabled = true
|
2022-06-08 17:28:41 +00:00
|
|
|
}
|
2022-05-19 09:02:40 +00:00
|
|
|
|
|
|
|
if (this.faceDetectionTypes.includes(DETECTION_TYPES.FACE_BOX) && this.config.face?.detector) {
|
|
|
|
this.config.face.detector.enabled = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.faceDetectionTypes.includes(DETECTION_TYPES.FACE_EXPRESSIONS) && this.config.face?.emotion) {
|
|
|
|
this.config.face.emotion.enabled = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const initialHuman = new Human(this.config);
|
|
|
|
try {
|
|
|
|
await initialHuman.load();
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.human = initialHuman;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-16 11:50:31 +00:00
|
|
|
getFaceBox(detections: Array<FaceResult>, threshold: number): FaceBox | undefined {
|
|
|
|
if (this.getFaceCount(detections) !== 1) {
|
2022-05-19 09:02:40 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-06-10 12:19:18 +00:00
|
|
|
|
2022-05-19 09:02:40 +00:00
|
|
|
const faceBox: FaceBox = {
|
|
|
|
// normalize to percentage based
|
2022-06-16 11:50:31 +00:00
|
|
|
left: Math.round(detections[0].boxRaw[0] * 100),
|
|
|
|
right: Math.round((detections[0].boxRaw[0] + detections[0].boxRaw[2]) * 100)
|
2022-05-19 09:02:40 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
faceBox.width = Math.round(faceBox.right - faceBox.left);
|
|
|
|
|
|
|
|
if (this.lastValidFaceBox && threshold && Math.abs(this.lastValidFaceBox.left - faceBox.left) < threshold) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.lastValidFaceBox = faceBox;
|
|
|
|
|
|
|
|
return faceBox;
|
|
|
|
}
|
|
|
|
|
2022-06-16 11:50:31 +00:00
|
|
|
getFaceExpression(detections: Array<FaceResult>): string | undefined {
|
|
|
|
if (this.getFaceCount(detections) !== 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (detections[0].emotion) {
|
|
|
|
return FACE_EXPRESSIONS_NAMING_MAPPING[detections[0].emotion[0].emotion];
|
2022-05-19 09:02:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-16 11:50:31 +00:00
|
|
|
getFaceCount(detections: Array<FaceResult> | undefined): number {
|
|
|
|
if (detections) {
|
|
|
|
return detections.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getDetections(image: ImageBitmap | ImageData): Promise<Array<FaceResult>> {
|
|
|
|
if (!this.human || !this.faceDetectionTypes.length) {
|
|
|
|
return [];
|
2022-06-10 12:19:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.human.tf.engine().startScope();
|
|
|
|
|
|
|
|
const imageTensor = this.human.tf.browser.fromPixels(image);
|
|
|
|
const { face: detections } = await this.human.detect(imageTensor, this.config);
|
|
|
|
|
|
|
|
this.human.tf.engine().endScope();
|
|
|
|
|
|
|
|
return detections.filter(detection => detection.score > FACE_DETECTION_SCORE_THRESHOLD);
|
|
|
|
}
|
|
|
|
|
2022-06-16 11:50:31 +00:00
|
|
|
public async detect({ image, threshold } : DetectInput): Promise<DetectOutput> {
|
2022-05-19 09:02:40 +00:00
|
|
|
let detections;
|
|
|
|
let faceExpression;
|
|
|
|
let faceBox;
|
|
|
|
|
|
|
|
this.detectionInProgress = true;
|
|
|
|
|
2022-06-16 11:50:31 +00:00
|
|
|
detections = await this.getDetections(image);
|
2022-05-19 09:02:40 +00:00
|
|
|
|
2022-06-16 11:50:31 +00:00
|
|
|
if (this.faceDetectionTypes.includes(DETECTION_TYPES.FACE_EXPRESSIONS)) {
|
|
|
|
faceExpression = this.getFaceExpression(detections);
|
2022-05-19 09:02:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.faceDetectionTypes.includes(DETECTION_TYPES.FACE_BOX)) {
|
2022-06-16 11:50:31 +00:00
|
|
|
//if more than one face is detected the face centering will be disabled.
|
|
|
|
if (this.getFaceCount(detections) > 1 ) {
|
|
|
|
this.faceDetectionTypes.splice(this.faceDetectionTypes.indexOf(DETECTION_TYPES.FACE_BOX), 1);
|
|
|
|
|
|
|
|
//face-box for re-centering
|
|
|
|
faceBox = {
|
|
|
|
left: 0,
|
|
|
|
right: 100,
|
|
|
|
width: 100,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
faceBox = this.getFaceBox(detections, threshold);
|
2022-05-19 09:02:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2022-06-16 11:50:31 +00:00
|
|
|
|
2022-05-19 09:02:40 +00:00
|
|
|
this.detectionInProgress = false;
|
|
|
|
|
|
|
|
return {
|
|
|
|
faceExpression,
|
2022-06-16 11:50:31 +00:00
|
|
|
faceBox,
|
|
|
|
faceCount: this.getFaceCount(detections)
|
2022-05-19 09:02:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public getDetectionInProgress(): boolean {
|
|
|
|
return this.detectionInProgress;
|
|
|
|
}
|
|
|
|
}
|