import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { IconButton, Size } from 'browndash-components'; import * as faceapi from 'face-api.js'; import { FaceMatcher } from 'face-api.js'; import 'ldrs/ring'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; import { Utils } from '../../../../Utils'; import { Doc, DocListCast } from '../../../../fields/Doc'; import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { Cast, ImageCast, StrCast } from '../../../../fields/Types'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { undoable } from '../../../util/UndoManager'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DocumentView } from '../../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../../nodes/FieldView'; import './FaceCollectionBox.scss'; import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; import { FaceRecognitionHandler } from '../../search/FaceRecognitionHandler'; interface FaceDocumentProps { faceDoc: Doc; } /** * A componenent to visually represent a Face Document. */ @observer export class FaceDocumentItem extends ObservableReactComponent { private _dropDisposer?: DragManager.DragDropDisposer; constructor(props: FaceDocumentProps) { super(props); makeObservable(this); } @observable _displayImages: boolean = true; protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this._props.faceDoc)); }; protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { const { docDragData } = de.complete; if (docDragData) { const filteredDocs = docDragData.droppedDocuments.filter(doc => doc.type === DocumentType.IMG); filteredDocs.forEach(doc => { // If the current Face Document has no items, and the doc has more than one face descriptor, don't let the user add the document first. if ((this._props.faceDoc[DocData].face_descriptors as List>).length === 0 && (doc[DocData][FaceRecognitionHandler.FacesField(doc)] as List>).length > 1) { alert('Cannot add a document with multiple faces as the first item!'); } else { // Loop through the documents' face descriptors. // Choose the face with the smallest distance to add. const float32Array = (this._props.faceDoc[DocData].face_descriptors as List>).map(faceDescriptor => new Float32Array(Array.from(faceDescriptor))); const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(StrCast(this._props.faceDoc[DocData].face_label), float32Array); const faceDescriptors: faceapi.LabeledFaceDescriptors[] = [labeledFaceDescriptor]; const faceMatcher = new FaceMatcher(faceDescriptors, 1); let cur_lowest_distance = 1; let cur_matching_face = new List(); (doc[DocData][FaceRecognitionHandler.FacesField(doc)] as List>).forEach(face => { // If the face has the current lowest distance, mark it as such // Once that lowest distance is found, add the face descriptor to the faceDoc, and add the associated doc const convered_32_array: Float32Array = new Float32Array(Array.from(face)); const match = faceMatcher.matchDescriptor(convered_32_array); if (match.distance < cur_lowest_distance) { cur_lowest_distance = match.distance; cur_matching_face = face; } }); const faceFieldKey = FaceRecognitionHandler.FaceField(doc, this._props.faceDoc); if (doc[DocData][faceFieldKey]) { Cast(doc[DocData][faceFieldKey], listSpec('number'), null).push(cur_matching_face as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that } else { doc[DocData][faceFieldKey] = new List>([cur_matching_face]); } Doc.AddDocToList(this._props.faceDoc[DocData], 'face_docList', doc); Cast(this._props.faceDoc[DocData].face_descriptors, listSpec('number'), null).push(cur_matching_face as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that } }); return false; } return false; } /** * Toggles whether a Face Document displays its associated docs. */ @action onDisplayClick() { this._displayImages = !this._displayImages; } /** * Deletes a Face Document. */ deleteFaceDocument = undoable(() => { if (Doc.ActiveDashboard) { Doc.RemoveDocFromList(Doc.ActiveDashboard[DocData], 'faceDocuments', this._props.faceDoc); } }, 'remove face'); /** * Deletes a document from a Face Document's associated docs list. * @param doc */ @action deleteAssociatedDoc = (doc: Doc) => { this._props.faceDoc[DocData].face_descriptors = new List>( (this._props.faceDoc[DocData].face_descriptors as List>).filter(fd => !(doc[DocData][FaceRecognitionHandler.FaceField(doc, this._props.faceDoc)] as List>).includes(fd)) ); doc[DocData][FaceRecognitionHandler.FaceField(doc, this._props.faceDoc)] = new List>(); Doc.RemoveDocFromList(this._props.faceDoc[DocData], 'face_docList', doc); }; render() { return (
this.createDropTarget(ele!)}>

{StrCast(this._props.faceDoc[DocData].face_label)}

this.onDisplayClick()} icon={} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} /> {this._displayImages ? (
{DocListCast(this._props.faceDoc[DocData].face_docList).map(doc => { const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); return (
DocumentView.showDocument(doc, { willZoomCentered: true })} style={{ maxWidth: '60px', margin: '10px' }} src={`${name}_o.${type}`} />
this.deleteAssociatedDoc(doc)} icon={'x'} style={{ width: '4px' }} size={Size.XSMALL} />
); })}
) : null}
); } } @observer export class FaceCollectionBox extends ViewBoxBaseComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FaceCollectionBox, fieldKey); } public static Instance: FaceCollectionBox; @computed get currentDocs() { if (Doc.ActiveDashboard) { return DocListCast(Doc.ActiveDashboard[DocData].faceDocuments); } return []; } constructor(props: FieldViewProps) { super(props); makeObservable(this); FaceCollectionBox.Instance = this; } render() { return (
{this.currentDocs.map(doc => ( ))}
); } } Docs.Prototypes.TemplateMap.set(DocumentType.FACECOLLECTION, { layout: { view: FaceCollectionBox, dataField: 'data' }, options: { acl: '', _width: 400 }, });