ngx-face-api-js
Version:
Angular directives for face detection and face recognition in the browser. It is a wrapper for face-api.js, so it is not dependent on the browser implementation.
791 lines (773 loc) • 25 kB
JavaScript
import { BrowserModule } from '@angular/platform-browser';
import { __awaiter } from 'tslib';
import { of, fromEvent, interval, Subscription, Subject, combineLatest } from 'rxjs';
import { resizeResults, FaceDetection, draw, round, detectAllFaces, detectSingleFace, DetectSingleFaceTask, DetectAllFacesTask, loadSsdMobilenetv1Model, loadMtcnnModel, loadFaceExpressionModel, loadFaceLandmarkModel, loadFaceRecognitionModel, loadTinyFaceDetectorModel, loadAgeGenderModel } from 'face-api.js';
import { switchMap, shareReplay, map, startWith } from 'rxjs/operators';
import { ComponentPortal, PortalModule } from '@angular/cdk/portal';
import { InjectionToken, Injectable, Inject, Optional, Component, ViewChild, ElementRef, Renderer2, HostListener, Injector, Directive, Input, NgModule } from '@angular/core';
import { OverlayConfig, Overlay, OverlayModule } from '@angular/cdk/overlay';
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class DetectTask {
/**
* @return {?}
*/
get resolveTarget() {
return this.targetResolver;
}
/**
* @param {?} option
*/
constructor(option) {
this.type = option.type;
this.tokens = option.tokens;
this.realtime = option.realtime || false;
this.target = new Promise((/**
* @param {?} resolver
* @return {?}
*/
resolver => (this.targetResolver = resolver)));
}
/**
* @private
* @param {?} patten
* @param {?} target
* @return {?}
*/
isMatchPattern(patten, target) {
return patten.every((/**
* @param {?} item
* @return {?}
*/
item => target.includes(item)));
}
/**
* @template THIS
* @this {THIS}
* @param {...?} tokens
* @return {THIS}
*/
with(...tokens) {
(/** @type {?} */ (this)).tokens.push(...tokens);
return (/** @type {?} */ (this));
}
/**
* @param {?=} option
* @return {?}
*/
detect(option) {
return __awaiter(this, void 0, void 0, function* () {
/** @type {?} */
let t;
if (this.type === 'all') {
t = detectAllFaces(yield this.target, option || undefined);
}
else {
t = detectSingleFace(yield this.target, option || undefined);
}
if (this.isMatchPattern(['expressions', 'landmarks', 'ageAndGender', 'descriptors'], this.tokens)) {
if (t instanceof DetectSingleFaceTask) {
return t
.withFaceLandmarks()
.withFaceExpressions()
.withAgeAndGender()
.withFaceDescriptor()
.run();
}
else if (t instanceof DetectAllFacesTask) {
return t
.withFaceLandmarks()
.withFaceExpressions()
.withAgeAndGender()
.withFaceDescriptors()
.run();
}
}
else if (this.isMatchPattern(['expressions', 'landmarks', 'descriptors'], this.tokens)) {
if (t instanceof DetectSingleFaceTask) {
return t
.withFaceLandmarks()
.withFaceExpressions()
.withFaceDescriptor()
.run();
}
else if (t instanceof DetectAllFacesTask) {
return t
.withFaceLandmarks()
.withFaceExpressions()
.withFaceDescriptors()
.run();
}
}
else if (this.isMatchPattern(['expressions', 'landmarks', 'ageAndGender'], this.tokens)) {
return t
.withFaceLandmarks()
.withFaceExpressions()
.withAgeAndGender()
.run();
}
else if (this.isMatchPattern(['expressions', 'landmarks'], this.tokens)) {
return t
.withFaceLandmarks()
.withFaceExpressions()
.run();
}
else if (this.isMatchPattern(['expressions', 'ageAndGender'], this.tokens)) {
return t
.withFaceExpressions()
.withAgeAndGender()
.run();
}
else if (this.isMatchPattern(['expressions'], this.tokens)) {
return t.withFaceExpressions().run();
}
else if (this.isMatchPattern(['landmarks', 'ageAndGender'], this.tokens)) {
return t
.withFaceLandmarks()
.withAgeAndGender()
.run();
}
else if (this.isMatchPattern(['landmarks'], this.tokens)) {
return t.withFaceLandmarks().run();
}
return t.run();
});
}
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/** @type {?} */
const FaceDetectionOptions = new InjectionToken('ngx-face-api-js.face-detection-options');
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/** @type {?} */
const ExpressionsFeatureToken = 'expressions';
/** @type {?} */
const LandmarksFeatureToken = 'landmarks';
/** @type {?} */
const DescriptorsFeatureToken = 'descriptors';
/** @type {?} */
const AgeAndGenderToken = 'ageAndGender';
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/** @type {?} */
const SsdMobilenetv1Model = 'SsdMobilenetv1Model';
/** @type {?} */
const MtcnnModel = 'MtcnnModel';
/** @type {?} */
const FaceExpressionModel = 'FaceExpressionModel';
/** @type {?} */
const FaceLandmarkModel = 'FaceLandmarkModel';
/** @type {?} */
const FaceRecognitionModel = 'FaceRecognitionModel';
/** @type {?} */
const TinyFaceDetectorModel = 'TinyFaceDetectorModel';
/** @type {?} */
const AgeAndGenderModel = 'AgeAndGenderModel';
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/** @type {?} */
const ModelsUrl = new InjectionToken('ngx-face-api-js.models-url');
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class ModelLoaderService {
/**
* @param {?} modelUrl
*/
constructor(modelUrl) {
this.modelUrl = modelUrl;
this.loadedModels = [];
}
/**
* @private
* @param {?} tokens
* @return {?}
*/
getReqiredModels(tokens) {
return [SsdMobilenetv1Model].concat(Object.entries({
[ExpressionsFeatureToken]: [FaceExpressionModel, SsdMobilenetv1Model],
[LandmarksFeatureToken]: [FaceLandmarkModel, SsdMobilenetv1Model],
[DescriptorsFeatureToken]: [FaceRecognitionModel],
[AgeAndGenderToken]: [AgeAndGenderModel],
})
.map((/**
* @param {?} __0
* @return {?}
*/
([key, models]) => tokens.includes((/** @type {?} */ (key))) ? models : []))
.reduce((/**
* @param {?} a
* @param {?} b
* @return {?}
*/
(a, b) => a.concat(b)))
.filter((/**
* @param {?} v
* @param {?} i
* @param {?} arr
* @return {?}
*/
(v, i, arr) => arr.indexOf(v) === i)));
}
/**
* @private
* @param {?} model
* @return {?}
*/
mapLoadFunction(model) {
switch (model) {
case SsdMobilenetv1Model:
return loadSsdMobilenetv1Model;
case MtcnnModel:
return loadMtcnnModel;
case FaceExpressionModel:
return loadFaceExpressionModel;
case FaceLandmarkModel:
return loadFaceLandmarkModel;
case FaceRecognitionModel:
return loadFaceRecognitionModel;
case TinyFaceDetectorModel:
return loadTinyFaceDetectorModel;
case AgeAndGenderModel:
return loadAgeGenderModel;
}
}
/**
* @param {?} model
* @return {?}
*/
isLoaded(model) {
return this.loadedModels.includes(model);
}
/**
* @param {...?} models
* @return {?}
*/
load(...models) {
return __awaiter(this, void 0, void 0, function* () {
/** @type {?} */
const loadTargetModels = models.filter((/**
* @param {?} m
* @return {?}
*/
m => this.isLoaded(m) === false));
yield Promise.all(loadTargetModels
.map((/**
* @param {?} m
* @return {?}
*/
m => this.mapLoadFunction(m)))
.map((/**
* @param {?} load
* @return {?}
*/
load => load(this.modelUrl))));
if (loadTargetModels.length >= 0) {
this.loadedModels = loadTargetModels.concat(this.loadedModels);
}
});
}
/**
* @param {?} tokens
* @return {?}
*/
loadForFeature(tokens) {
return __awaiter(this, void 0, void 0, function* () {
/** @type {?} */
const reqiredModels = this.getReqiredModels(tokens);
console.log({ reqiredModels });
yield this.load(...reqiredModels);
});
}
}
ModelLoaderService.decorators = [
{ type: Injectable }
];
/** @nocollapse */
ModelLoaderService.ctorParameters = () => [
{ type: String, decorators: [{ type: Inject, args: [ModelsUrl,] }] }
];
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class FaceDetectorService {
/**
* @param {?} modelLoader
* @param {?=} option
*/
constructor(modelLoader, option) {
this.modelLoader = modelLoader;
this.option = option;
}
/**
* @param {?} task
* @return {?}
*/
detect(task) {
if (task.realtime === true) {
return of(task).pipe(switchMap((/**
* @param {?} t
* @return {?}
*/
(t) => __awaiter(this, void 0, void 0, function* () { return yield t.target; }))), switchMap((/**
* @param {?} video
* @return {?}
*/
video => fromEvent(video, 'loadeddata'))), switchMap((/**
* @return {?}
*/
() => this.modelLoader.loadForFeature(task.tokens))), switchMap((/**
* @return {?}
*/
() => interval(300))), switchMap((/**
* @return {?}
*/
() => task.detect(this.option))), shareReplay(1));
}
return of(task).pipe(switchMap((/**
* @param {?} t
* @return {?}
*/
(t) => __awaiter(this, void 0, void 0, function* () { return yield t.target; }))), switchMap((/**
* @param {?} image
* @return {?}
*/
image => fromEvent(image, 'load'))), switchMap((/**
* @return {?}
*/
() => __awaiter(this, void 0, void 0, function* () { return yield this.modelLoader.loadForFeature(task.tokens); }))), switchMap((/**
* @return {?}
*/
() => task.detect(this.option))), shareReplay(1));
}
}
FaceDetectorService.decorators = [
{ type: Injectable }
];
/** @nocollapse */
FaceDetectorService.ctorParameters = () => [
{ type: ModelLoaderService },
{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [FaceDetectionOptions,] }] }
];
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class DetectionResultComponent {
/**
* @param {?} task
* @param {?} el
* @param {?} renderer
* @param {?} faceDetector
*/
constructor(task, el, renderer, faceDetector) {
this.task = task;
this.el = el;
this.renderer = renderer;
this.faceDetector = faceDetector;
this.subscription = new Subscription();
this.resize$ = new Subject();
}
/**
* @private
* @return {?}
*/
get canvas() {
return this.canvasEl.nativeElement;
}
/**
* @return {?}
*/
onResize() {
this.resize$.next('onResize');
}
/**
* @private
* @param {?} result
* @return {?}
*/
convertResultToArray(result) {
if (Array.isArray(result)) {
return result;
}
return [result];
}
/**
* @return {?}
*/
ngOnInit() {
this.subscription.add(combineLatest(this.faceDetector.detect(this.task), this.resize$.pipe(startWith('init')))
.pipe(map((/**
* @param {?} __0
* @return {?}
*/
([result]) => this.convertResultToArray(result))))
.subscribe((/**
* @param {?} result
* @return {?}
*/
result => this.draw(result))));
}
/**
* @return {?}
*/
ngOnDestroy() {
this.subscription.unsubscribe();
}
/**
* @private
* @param {?} results
* @return {?}
*/
draw(results) {
return __awaiter(this, void 0, void 0, function* () {
/** @type {?} */
const target = yield this.task.target;
let { width, height } = target;
if (target instanceof HTMLVideoElement) {
height = target.videoHeight;
width = target.videoWidth;
}
/** @type {?} */
const detectionsForSize = resizeResults(results.map((/**
* @param {?} result
* @return {?}
*/
result => result instanceof FaceDetection ? result : result.detection)), { width, height });
this.canvas.width = width;
this.canvas.height = height;
this.renderer.setStyle(this.canvas, 'width', `${width}px`);
this.renderer.setStyle(this.canvas, 'height', `${height}px`);
if (this.task.tokens.length >= 1) {
draw.drawDetections(this.canvas, detectionsForSize);
/** @type {?} */
const resizeResults$$1 = resizeResults(results, { width, height });
if (this.task.tokens.includes('expressions')) {
draw.drawFaceExpressions(this.canvas, resizeResults$$1.map((/**
* @param {?} __0
* @return {?}
*/
({ detection, expressions }) => ({
position: detection.box,
expressions,
}))));
}
if (this.task.tokens.includes('landmarks')) {
draw.drawFaceLandmarks(this.canvas, resizeResults$$1.map((/**
* @param {?} __0
* @return {?}
*/
({ landmarks }) => landmarks)));
}
if (this.task.tokens.includes('ageAndGender')) {
resizeResults$$1.forEach((/**
* @param {?} result
* @return {?}
*/
result => {
const { age, gender, genderProbability } = result;
/** @type {?} */
const text = new draw.DrawTextField([
`${round(age, 0)} years`,
`${gender} (${round(genderProbability)})`,
], result.detection.box.bottomLeft);
text.draw(this.canvas);
}));
}
}
else {
draw.drawDetections(this.canvas, detectionsForSize);
}
});
}
}
DetectionResultComponent.decorators = [
{ type: Component, args: [{
template: "<canvas #canvas></canvas>\n",
styles: ["canvas{width:100%;height:100%}"]
}] }
];
/** @nocollapse */
DetectionResultComponent.ctorParameters = () => [
{ type: DetectTask },
{ type: ElementRef },
{ type: Renderer2 },
{ type: FaceDetectorService }
];
DetectionResultComponent.propDecorators = {
canvasEl: [{ type: ViewChild, args: ['canvas',] }],
onResize: [{ type: HostListener, args: ['window:resize',] }]
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @abstract
*/
class AbstractDetectDirective {
/**
* @param {?} el
* @param {?} overlay
* @param {?} injector
*/
constructor(el, overlay, injector) {
this.el = el;
this.overlay = overlay;
this.injector = injector;
this.with = [];
}
/**
* @return {?}
*/
ngOnInit() {
this.task = new DetectTask({
type: this.type,
tokens: this.with,
realtime: this.stream,
});
}
/**
* @private
* @return {?}
*/
get orverlayPositionStrategy() {
return this.overlay
.position()
.flexibleConnectedTo(this.el)
.withPositions([
{
overlayX: 'start',
overlayY: 'top',
originX: 'start',
originY: 'top',
},
])
.withFlexibleDimensions(false)
.withLockedPosition(true);
}
/**
* @private
* @return {?}
*/
createOverlay() {
/** @type {?} */
const scrollStrategy = this.overlay.scrollStrategies.reposition();
/** @type {?} */
const config = new OverlayConfig({
positionStrategy: this.orverlayPositionStrategy,
scrollStrategy,
hasBackdrop: false,
});
return this.overlay.create(config);
}
/**
* @private
* @return {?}
*/
createInjector() {
return Injector.create({
parent: this.injector,
providers: [
{
provide: DetectTask,
useValue: this.task,
},
],
});
}
/**
* @return {?}
*/
ngAfterViewInit() {
this.task.resolveTarget(this.el.nativeElement);
/** @type {?} */
const overlayRef = this.createOverlay();
/** @type {?} */
const injector = this.createInjector();
/** @type {?} */
const portal = new ComponentPortal(DetectionResultComponent, undefined, injector);
overlayRef.attach(portal);
}
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class DetectAllFacesImgDirective extends AbstractDetectDirective {
/**
* @param {?} el
* @param {?} overlay
* @param {?} injector
*/
constructor(el, overlay, injector) {
super(el, overlay, injector);
this.with = [];
this.type = 'all';
this.stream = false;
}
}
DetectAllFacesImgDirective.decorators = [
{ type: Directive, args: [{
selector: 'img[allFaces]',
exportAs: 'faces',
},] }
];
/** @nocollapse */
DetectAllFacesImgDirective.ctorParameters = () => [
{ type: ElementRef },
{ type: Overlay },
{ type: Injector }
];
DetectAllFacesImgDirective.propDecorators = {
with: [{ type: Input }]
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class DetectSingleFaceImgDirective extends AbstractDetectDirective {
/**
* @param {?} el
* @param {?} overlay
* @param {?} injector
*/
constructor(el, overlay, injector) {
super(el, overlay, injector);
this.with = [];
this.type = 'single';
this.stream = false;
}
/**
* @return {?}
*/
ngOnInit() {
this.task = new DetectTask({
type: this.type,
tokens: this.with,
realtime: this.stream,
});
}
}
DetectSingleFaceImgDirective.decorators = [
{ type: Directive, args: [{
selector: 'img[singleFace]',
exportAs: 'face',
},] }
];
/** @nocollapse */
DetectSingleFaceImgDirective.ctorParameters = () => [
{ type: ElementRef },
{ type: Overlay },
{ type: Injector }
];
DetectSingleFaceImgDirective.propDecorators = {
with: [{ type: Input }]
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class DetectAllFacesVideoDirective extends AbstractDetectDirective {
/**
* @param {?} el
* @param {?} overlay
* @param {?} injector
*/
constructor(el, overlay, injector) {
super(el, overlay, injector);
this.with = [];
this.type = 'all';
this.stream = true;
}
}
DetectAllFacesVideoDirective.decorators = [
{ type: Directive, args: [{
selector: 'video[allFaces]',
exportAs: 'faces',
},] }
];
/** @nocollapse */
DetectAllFacesVideoDirective.ctorParameters = () => [
{ type: ElementRef },
{ type: Overlay },
{ type: Injector }
];
DetectAllFacesVideoDirective.propDecorators = {
with: [{ type: Input }]
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class NgxFaceApiJsModule {
/**
* @param {?} options
* @return {?}
*/
static forRoot(options) {
return {
ngModule: NgxFaceApiJsModule,
providers: [
{
provide: ModelsUrl,
useValue: options.modelsUrl,
},
ModelLoaderService,
FaceDetectorService,
...[
options.faceDetectionOptions
? {
provide: FaceDetectionOptions,
useValue: options.faceDetectionOptions,
}
: [],
],
],
};
}
}
NgxFaceApiJsModule.decorators = [
{ type: NgModule, args: [{
declarations: [
DetectionResultComponent,
DetectAllFacesImgDirective,
DetectSingleFaceImgDirective,
DetectAllFacesVideoDirective,
],
imports: [BrowserModule, OverlayModule, PortalModule],
exports: [
DetectAllFacesImgDirective,
DetectSingleFaceImgDirective,
DetectAllFacesVideoDirective,
],
entryComponents: [DetectionResultComponent],
},] }
];
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
export { DetectTask, FaceDetectionOptions, ExpressionsFeatureToken, LandmarksFeatureToken, DescriptorsFeatureToken, AgeAndGenderToken, SsdMobilenetv1Model, MtcnnModel, FaceExpressionModel, FaceLandmarkModel, FaceRecognitionModel, TinyFaceDetectorModel, AgeAndGenderModel, ModelsUrl, FaceDetectorService, ModelLoaderService, NgxFaceApiJsModule, DetectionResultComponent as ɵa, AbstractDetectDirective as ɵc, DetectAllFacesImgDirective as ɵb, DetectAllFacesVideoDirective as ɵe, DetectSingleFaceImgDirective as ɵd };
//# sourceMappingURL=ngx-face-api-js.js.map