Commit 174e0c28 by vincent

FaceMatcher

parent 397c05ae
...@@ -30,40 +30,24 @@ function renderFaceImageSelectList(selectListId, onChange, initialValue) { ...@@ -30,40 +30,24 @@ function renderFaceImageSelectList(selectListId, onChange, initialValue) {
} }
// fetch first image of each class and compute their descriptors // fetch first image of each class and compute their descriptors
async function initBbtFaceDescriptors(net, numImagesForTraining = 1) { async function createBbtFaceMatcher(numImagesForTraining = 1) {
const maxAvailableImagesPerClass = 5 const maxAvailableImagesPerClass = 5
numImagesForTraining = Math.min(numImagesForTraining, maxAvailableImagesPerClass) numImagesForTraining = Math.min(numImagesForTraining, maxAvailableImagesPerClass)
return Promise.all(classes.map(
const labeledFaceDescriptors = await Promise.all(classes.map(
async className => { async className => {
const descriptors = [] const descriptors = []
for (let i = 1; i < (numImagesForTraining + 1); i++) { for (let i = 1; i < (numImagesForTraining + 1); i++) {
const img = await faceapi.fetchImage(getFaceImageUri(className, i)) const img = await faceapi.fetchImage(getFaceImageUri(className, i))
descriptors.push(await faceapi.computeFaceDescriptor(img)) descriptors.push(await faceapi.computeFaceDescriptor(img))
} }
return {
descriptors,
className
}
}
))
}
function getBestMatch(descriptorsByClass, queryDescriptor) { return new faceapi.LabeledFaceDescriptors(
function computeMeanDistance(descriptorsOfClass) { className,
return faceapi.round( descriptors
descriptorsOfClass
.map(d => faceapi.euclideanDistance(d, queryDescriptor))
.reduce((d1, d2) => d1 + d2, 0)
/ (descriptorsOfClass.length || 1)
) )
} }
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 @@ ...@@ -48,8 +48,7 @@
<script> <script>
let images = [] let images = []
let referenceDescriptorsByClass = [] let faceMatcher = null
let descriptorsByFace = []
let numImages = 16 let numImages = 16
let maxDistance = 0.6 let maxDistance = 0.6
...@@ -68,15 +67,12 @@ ...@@ -68,15 +67,12 @@
const canvas = faceapi.createCanvasFromMedia(img) const canvas = faceapi.createCanvasFromMedia(img)
$('#faceContainer').append(canvas) $('#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 const x = 20, y = canvas.height - 20
faceapi.drawText( faceapi.drawText(
canvas.getContext('2d'), canvas.getContext('2d'),
x, x,
y, y,
text, faceMatcher.findBestMatch(descriptor).toString(),
Object.assign(faceapi.getDefaultDrawOptions(), { color: 'red', fontSize: 16 }) Object.assign(faceapi.getDefaultDrawOptions(), { color: 'red', fontSize: 16 })
) )
} }
...@@ -106,7 +102,7 @@ ...@@ -106,7 +102,7 @@
async function run() { async function run() {
await faceapi.loadFaceRecognitionModel('/') await faceapi.loadFaceRecognitionModel('/')
referenceDescriptorsByClass = await initBbtFaceDescriptors(faceapi.recognitionNet, 1) faceMatcher = await createBbtFaceMatcher(1)
$('#loader').hide() $('#loader').hide()
const imgUris = classes const imgUris = classes
......
...@@ -63,13 +63,10 @@ ...@@ -63,13 +63,10 @@
</div> </div>
<script> <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 interval = 2000
let isStop = false let isStop = false
let referenceDescriptorsByClass = [] let faceMatcher = null
let currImageIdx = 2, currClassIdx = 0 let currImageIdx = 2, currClassIdx = 0
let to = null let to = null
...@@ -116,8 +113,8 @@ ...@@ -116,8 +113,8 @@
const descriptor = await faceapi.computeFaceDescriptor(input) const descriptor = await faceapi.computeFaceDescriptor(input)
displayTimeStats(Date.now() - ts) displayTimeStats(Date.now() - ts)
const bestMatch = getBestMatch(referenceDescriptorsByClass, descriptor) const bestMatch = faceMatcher.findBestMatch(descriptor)
$('#prediction').val(`${bestMatch.distance < threshold ? bestMatch.className : 'unknown'} (${bestMatch.distance})`) $('#prediction').val(bestMatch.toString())
currImageIdx = currClassIdx === (classes.length - 1) currImageIdx = currClassIdx === (classes.length - 1)
? currImageIdx + 1 ? currImageIdx + 1
...@@ -138,7 +135,7 @@ ...@@ -138,7 +135,7 @@
setStatusText('computing initial descriptors...') setStatusText('computing initial descriptors...')
referenceDescriptorsByClass = await initBbtFaceDescriptors(faceapi.recognitionNet) faceMatcher = await createBbtFaceMatcher(1)
$('#loader').hide() $('#loader').hide()
runFaceRecognition() runFaceRecognition()
......
...@@ -139,8 +139,7 @@ ...@@ -139,8 +139,7 @@
</body> </body>
<script> <script>
const maxDescriptorDistance = 0.6 let faceMatcher = null
let referenceDescriptorsByClass = []
async function updateResults() { async function updateResults() {
if (!isFaceDetectionModelLoaded()) { if (!isFaceDetectionModelLoaded()) {
...@@ -159,20 +158,17 @@ ...@@ -159,20 +158,17 @@
} }
function drawFaceRecognitionResults(results) { function drawFaceRecognitionResults(results) {
const { width, height } = $('#inputImg').get(0)
const canvas = $('#overlay').get(0) const canvas = $('#overlay').get(0)
canvas.width = width
canvas.height = height
// resize detection and landmarks in case displayed image is smaller than // resize detection and landmarks in case displayed image is smaller than
// original size // original size
results = results.map(res => res.forSize(width, height)) resizedResults = resizeCanvasAndResults($('#inputImg').get(0), canvas, results)
const boxesWithText = results.map(({ detection, descriptor }) => { const boxesWithText = resizedResults.map(({ detection, descriptor }) =>
const bestMatch = getBestMatch(referenceDescriptorsByClass, descriptor) new faceapi.BoxWithText(
const text = `${bestMatch.distance < maxDescriptorDistance ? bestMatch.className : 'unknown'} (${bestMatch.distance})` detection.box,
return new faceapi.BoxWithText(detection.box, text) faceMatcher.findBestMatch(descriptor).toString()
}) )
)
faceapi.drawDetection(canvas, boxesWithText) faceapi.drawDetection(canvas, boxesWithText)
} }
...@@ -182,8 +178,8 @@ ...@@ -182,8 +178,8 @@
await faceapi.loadFaceLandmarkModel('/') await faceapi.loadFaceLandmarkModel('/')
await faceapi.loadFaceRecognitionModel('/') await faceapi.loadFaceRecognitionModel('/')
// initialize reference descriptors (1 per bbt character) // initialize face matcher with 1 reference descriptor per bbt character
referenceDescriptorsByClass = await initBbtFaceDescriptors(faceapi.recognitionNet, 1) faceMatcher = await createBbtFaceMatcher(1)
// start processing image // start processing image
updateResults() 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'; ...@@ -4,7 +4,7 @@ import { FaceLandmarks } from './FaceLandmarks';
import { FaceLandmarks68 } from './FaceLandmarks68'; import { FaceLandmarks68 } from './FaceLandmarks68';
export interface IFullFaceDescription<TFaceLandmarks extends FaceLandmarks = FaceLandmarks68> export interface IFullFaceDescription<TFaceLandmarks extends FaceLandmarks = FaceLandmarks68>
extends IFaceDetectionWithLandmarks<TFaceLandmarks>{ extends IFaceDetectionWithLandmarks<TFaceLandmarks> {
detection: FaceDetection, detection: FaceDetection,
landmarks: TFaceLandmarks, 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'; ...@@ -2,4 +2,6 @@ export * from './FaceDetection';
export * from './FaceLandmarks'; export * from './FaceLandmarks';
export * from './FaceLandmarks5'; export * from './FaceLandmarks5';
export * from './FaceLandmarks68'; export * from './FaceLandmarks68';
export * from './FullFaceDescription'; export * from './FaceMatch';
\ No newline at end of file export * from './FullFaceDescription';
export * from './LabeledFaceDescriptors';
\ No newline at end of file
...@@ -38,7 +38,7 @@ function denseBlock( ...@@ -38,7 +38,7 @@ function denseBlock(
export class FaceLandmark68Net extends FaceLandmark68NetBase<NetParams> { export class FaceLandmark68Net extends FaceLandmark68NetBase<NetParams> {
constructor() { constructor() {
super('FaceLandmark68LargeNet') super('FaceLandmark68Net')
} }
public runNet(input: NetInput): tf.Tensor2D { public runNet(input: NetInput): tf.Tensor2D {
...@@ -46,7 +46,7 @@ export class FaceLandmark68Net extends FaceLandmark68NetBase<NetParams> { ...@@ -46,7 +46,7 @@ export class FaceLandmark68Net extends FaceLandmark68NetBase<NetParams> {
const { params } = this const { params } = this
if (!params) { if (!params) {
throw new Error('FaceLandmark68LargeNet - load model before inference') throw new Error('FaceLandmark68Net - load model before inference')
} }
return tf.tidy(() => { 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' ...@@ -3,6 +3,7 @@ export * from './ComposableTask'
export * from './ComputeFaceDescriptorsTasks' export * from './ComputeFaceDescriptorsTasks'
export * from './DetectFacesTasks' export * from './DetectFacesTasks'
export * from './DetectFaceLandmarksTasks' export * from './DetectFaceLandmarksTasks'
export * from './FaceMatcher'
export * from './nets' export * from './nets'
export * from './types' 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