Commit 174e0c28 by vincent

FaceMatcher

parent 397c05ae
......@@ -30,40 +30,24 @@ function renderFaceImageSelectList(selectListId, onChange, initialValue) {
}
// fetch first image of each class and compute their descriptors
async function initBbtFaceDescriptors(net, numImagesForTraining = 1) {
async function createBbtFaceMatcher(numImagesForTraining = 1) {
const maxAvailableImagesPerClass = 5
numImagesForTraining = Math.min(numImagesForTraining, maxAvailableImagesPerClass)
return Promise.all(classes.map(
const labeledFaceDescriptors = await Promise.all(classes.map(
async className => {
const descriptors = []
for (let i = 1; i < (numImagesForTraining + 1); i++) {
const img = await faceapi.fetchImage(getFaceImageUri(className, i))
descriptors.push(await faceapi.computeFaceDescriptor(img))
}
return {
descriptors,
className
}
}
))
}
function getBestMatch(descriptorsByClass, queryDescriptor) {
function computeMeanDistance(descriptorsOfClass) {
return faceapi.round(
descriptorsOfClass
.map(d => faceapi.euclideanDistance(d, queryDescriptor))
.reduce((d1, d2) => d1 + d2, 0)
/ (descriptorsOfClass.length || 1)
return new faceapi.LabeledFaceDescriptors(
className,
descriptors
)
}
return descriptorsByClass
.map(
({ descriptors, className }) => ({
distance: computeMeanDistance(descriptors),
className
})
)
.reduce((best, curr) => best.distance < curr.distance ? best : curr)
}
}
))
return new faceapi.FaceMatcher(labeledFaceDescriptors)
}
\ No newline at end of file
......@@ -48,8 +48,7 @@
<script>
let images = []
let referenceDescriptorsByClass = []
let descriptorsByFace = []
let faceMatcher = null
let numImages = 16
let maxDistance = 0.6
......@@ -68,15 +67,12 @@
const canvas = faceapi.createCanvasFromMedia(img)
$('#faceContainer').append(canvas)
const bestMatch = getBestMatch(referenceDescriptorsByClass, descriptor)
const text = `${bestMatch.distance < maxDistance ? bestMatch.className : 'unkown'} (${bestMatch.distance})`
const x = 20, y = canvas.height - 20
faceapi.drawText(
canvas.getContext('2d'),
x,
y,
text,
faceMatcher.findBestMatch(descriptor).toString(),
Object.assign(faceapi.getDefaultDrawOptions(), { color: 'red', fontSize: 16 })
)
}
......@@ -106,7 +102,7 @@
async function run() {
await faceapi.loadFaceRecognitionModel('/')
referenceDescriptorsByClass = await initBbtFaceDescriptors(faceapi.recognitionNet, 1)
faceMatcher = await createBbtFaceMatcher(1)
$('#loader').hide()
const imgUris = classes
......
......@@ -63,13 +63,10 @@
</div>
<script>
// for 150 x 150 sized face images 0.6 is a good threshold to
// judge whether two face descriptors are similar or not
const threshold = 0.6
let interval = 2000
let isStop = false
let referenceDescriptorsByClass = []
let faceMatcher = null
let currImageIdx = 2, currClassIdx = 0
let to = null
......@@ -116,8 +113,8 @@
const descriptor = await faceapi.computeFaceDescriptor(input)
displayTimeStats(Date.now() - ts)
const bestMatch = getBestMatch(referenceDescriptorsByClass, descriptor)
$('#prediction').val(`${bestMatch.distance < threshold ? bestMatch.className : 'unknown'} (${bestMatch.distance})`)
const bestMatch = faceMatcher.findBestMatch(descriptor)
$('#prediction').val(bestMatch.toString())
currImageIdx = currClassIdx === (classes.length - 1)
? currImageIdx + 1
......@@ -138,7 +135,7 @@
setStatusText('computing initial descriptors...')
referenceDescriptorsByClass = await initBbtFaceDescriptors(faceapi.recognitionNet)
faceMatcher = await createBbtFaceMatcher(1)
$('#loader').hide()
runFaceRecognition()
......
......@@ -139,8 +139,7 @@
</body>
<script>
const maxDescriptorDistance = 0.6
let referenceDescriptorsByClass = []
let faceMatcher = null
async function updateResults() {
if (!isFaceDetectionModelLoaded()) {
......@@ -159,20 +158,17 @@
}
function drawFaceRecognitionResults(results) {
const { width, height } = $('#inputImg').get(0)
const canvas = $('#overlay').get(0)
canvas.width = width
canvas.height = height
// resize detection and landmarks in case displayed image is smaller than
// original size
results = results.map(res => res.forSize(width, height))
const boxesWithText = results.map(({ detection, descriptor }) => {
const bestMatch = getBestMatch(referenceDescriptorsByClass, descriptor)
const text = `${bestMatch.distance < maxDescriptorDistance ? bestMatch.className : 'unknown'} (${bestMatch.distance})`
return new faceapi.BoxWithText(detection.box, text)
})
resizedResults = resizeCanvasAndResults($('#inputImg').get(0), canvas, results)
const boxesWithText = resizedResults.map(({ detection, descriptor }) =>
new faceapi.BoxWithText(
detection.box,
faceMatcher.findBestMatch(descriptor).toString()
)
)
faceapi.drawDetection(canvas, boxesWithText)
}
......@@ -182,8 +178,8 @@
await faceapi.loadFaceLandmarkModel('/')
await faceapi.loadFaceRecognitionModel('/')
// initialize reference descriptors (1 per bbt character)
referenceDescriptorsByClass = await initBbtFaceDescriptors(faceapi.recognitionNet, 1)
// initialize face matcher with 1 reference descriptor per bbt character
faceMatcher = await createBbtFaceMatcher(1)
// start processing image
updateResults()
......
import { round } from 'tfjs-image-recognition-base';
export class FaceMatch {
private _label: string
private _distance: number
constructor(label: string, distance: number) {
this._label = label
this._distance = distance
}
public get label(): string { return this._label }
public get distance(): number { return this._distance }
public toString(withDistance: boolean = true): string {
return `${this.label}${withDistance ? ` (${round(this.distance)})` : ''}`
}
}
\ No newline at end of file
......@@ -4,7 +4,7 @@ import { FaceLandmarks } from './FaceLandmarks';
import { FaceLandmarks68 } from './FaceLandmarks68';
export interface IFullFaceDescription<TFaceLandmarks extends FaceLandmarks = FaceLandmarks68>
extends IFaceDetectionWithLandmarks<TFaceLandmarks>{
extends IFaceDetectionWithLandmarks<TFaceLandmarks> {
detection: FaceDetection,
landmarks: TFaceLandmarks,
......
export class LabeledFaceDescriptors {
private _label: string
private _descriptors: Float32Array[]
constructor(label: string, descriptors: Float32Array[]) {
if (!(typeof label === 'string')) {
throw new Error('LabeledFaceDescriptors - constructor expected label to be a string')
}
if (!Array.isArray(descriptors) || descriptors.some(desc => !(desc instanceof Float32Array))) {
throw new Error('LabeledFaceDescriptors - constructor expected descriptors to be an array of Float32Array')
}
this._label = label
this._descriptors = descriptors
}
public get label(): string { return this._label }
public get descriptors(): Float32Array[] { return this._descriptors }
}
\ No newline at end of file
......@@ -2,4 +2,6 @@ export * from './FaceDetection';
export * from './FaceLandmarks';
export * from './FaceLandmarks5';
export * from './FaceLandmarks68';
export * from './FullFaceDescription';
\ No newline at end of file
export * from './FaceMatch';
export * from './FullFaceDescription';
export * from './LabeledFaceDescriptors';
\ No newline at end of file
......@@ -38,7 +38,7 @@ function denseBlock(
export class FaceLandmark68Net extends FaceLandmark68NetBase<NetParams> {
constructor() {
super('FaceLandmark68LargeNet')
super('FaceLandmark68Net')
}
public runNet(input: NetInput): tf.Tensor2D {
......@@ -46,7 +46,7 @@ export class FaceLandmark68Net extends FaceLandmark68NetBase<NetParams> {
const { params } = this
if (!params) {
throw new Error('FaceLandmark68LargeNet - load model before inference')
throw new Error('FaceLandmark68Net - load model before inference')
}
return tf.tidy(() => {
......
import { FaceMatch } from '../classes/FaceMatch';
import { FullFaceDescription } from '../classes/FullFaceDescription';
import { LabeledFaceDescriptors } from '../classes/LabeledFaceDescriptors';
import { euclideanDistance } from '../euclideanDistance';
export class FaceMatcher {
private _labeledDescriptors: LabeledFaceDescriptors[]
private _distanceThreshold: number
constructor(
inputs: LabeledFaceDescriptors | FullFaceDescription | Float32Array | Array<LabeledFaceDescriptors | FullFaceDescription | Float32Array>,
distanceThreshold: number = 0.6
) {
this._distanceThreshold = distanceThreshold
const inputArray = Array.isArray(inputs) ? inputs : [inputs]
if (!inputArray.length) {
throw new Error(`FaceRecognizer.constructor - expected atleast one input`)
}
let count = 1
const createUniqueLabel = () => `person ${count++}`
this._labeledDescriptors = inputArray.map((desc) => {
if (desc instanceof LabeledFaceDescriptors) {
return desc
}
if (desc instanceof FullFaceDescription) {
return new LabeledFaceDescriptors(createUniqueLabel(), [desc.descriptor])
}
if (desc instanceof Float32Array) {
return new LabeledFaceDescriptors(createUniqueLabel(), [desc])
}
throw new Error(`FaceRecognizer.constructor - expected inputs to be of type LabeledFaceDescriptors | FullFaceDescription | Float32Array | Array<LabeledFaceDescriptors | FullFaceDescription | Float32Array>`)
})
}
public get labeledDescriptors(): LabeledFaceDescriptors[] { return this._labeledDescriptors }
public get distanceThreshold(): number { return this._distanceThreshold }
public computeMeanDistance(queryDescriptor: Float32Array, descriptors: Float32Array[]): number {
return descriptors
.map(d => euclideanDistance(d, queryDescriptor))
.reduce((d1, d2) => d1 + d2, 0)
/ (descriptors.length || 1)
}
public matchDescriptor(queryDescriptor: Float32Array): FaceMatch {
return this.labeledDescriptors
.map(({ descriptors, label }) => new FaceMatch(
label,
this.computeMeanDistance(queryDescriptor, descriptors)
))
.reduce((best, curr) => best.distance < curr.distance ? best : curr)
}
public findBestMatch(queryDescriptor: Float32Array): FaceMatch {
const bestMatch = this.matchDescriptor(queryDescriptor)
return bestMatch.distance < this.distanceThreshold
? bestMatch
: new FaceMatch('unknown', bestMatch.distance)
}
}
\ No newline at end of file
......@@ -3,6 +3,7 @@ export * from './ComposableTask'
export * from './ComputeFaceDescriptorsTasks'
export * from './DetectFacesTasks'
export * from './DetectFaceLandmarksTasks'
export * from './FaceMatcher'
export * from './nets'
export * from './types'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment