UNPKG

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