From 4f2993a2f5392543bc56876f15a16543040af315 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 12 Nov 2021 15:07:23 -0500 Subject: [PATCH] implement optional face masking --- .eslintrc.json | 1 + CHANGELOG.md | 2 + demo/faceid/index.html | 9 +-- demo/faceid/index.js | 115 ++++++++++++++++++++++-------------- demo/faceid/index.ts | 96 +++++++++++++++++------------- demo/faceid/indexdb.ts | 9 +++ demo/facematch/facematch.js | 37 +++++------- src/config.ts | 10 ++++ src/face/blazeface.ts | 1 - src/face/face.ts | 34 +++++++---- src/face/facemesh.ts | 8 +-- src/face/faceres.ts | 23 +------- src/face/mask.ts | 40 +++++++++++++ src/gesture/gesture.ts | 7 ++- src/human.ts | 2 +- src/result.ts | 2 +- src/tfjs/backend.ts | 3 +- src/tfjs/humangl.ts | 3 +- src/util/interpolate.ts | 37 ++++++------ src/warmup.ts | 12 ++-- 20 files changed, 271 insertions(+), 180 deletions(-) create mode 100644 src/face/mask.ts diff --git a/.eslintrc.json b/.eslintrc.json index 56d41d9e..1592a643 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -40,6 +40,7 @@ "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-shadow": "error", "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/prefer-as-const": "off", "@typescript-eslint/triple-slash-reference": "off", "@typescript-eslint/no-inferrable-types": "off", "@typescript-eslint/no-empty-interface": ["error", { "allowSingleExtends": true }], diff --git a/CHANGELOG.md b/CHANGELOG.md index fcc05e74..05095b3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ ### **HEAD -> main** 2021/11/11 mandic00@live.com +- add similarity score range normalization +- add faceid demo - documentation overhaul - auto tensor shape and channels handling - disable use of path2d in node diff --git a/demo/faceid/index.html b/demo/faceid/index.html index 0367f77e..d6a731a0 100644 --- a/demo/faceid/index.html +++ b/demo/faceid/index.html @@ -18,14 +18,15 @@ html { font-family: 'Lato', 'Segoe UI'; font-size: 16px; font-variant: small-caps; } body { margin: 0; padding: 16px; background: black; color: white; overflow-x: hidden; width: 100vw; height: 100vh; } body::-webkit-scrollbar { display: none; } - .button { padding: 2px; cursor: pointer; box-shadow: 2px 2px black; width: 64px; text-align: center; margin-left: 16px; height: 16px; display: none } + .button { padding: 2px; cursor: pointer; box-shadow: 2px 2px black; width: 64px; text-align: center; place-content: center; margin-left: 16px; height: 16px; display: none } + .ok { position: absolute; top: 64px; right: 20px; width: 100px; background-color: grey; padding: 4px; color: black; font-size: 14px } -

+    

     

     
-    
retry
-
+
retry
+
diff --git a/demo/faceid/index.js b/demo/faceid/index.js index d48ffc3f..8c7af2db 100644 --- a/demo/faceid/index.js +++ b/demo/faceid/index.js @@ -47,6 +47,15 @@ async function load() { }; }); } +async function count() { + if (!db) + await open(); + return new Promise((resolve) => { + const store = db.transaction([table], "readwrite").objectStore(table).count(); + store.onerror = (evt) => log("count error:", evt); + store.onsuccess = () => resolve(store.result); + }); +} async function save(faceRecord) { if (!db) await open(); @@ -62,15 +71,12 @@ async function remove(faceRecord) { } // demo/faceid/index.ts -var db2 = []; -var face; -var current; var humanConfig = { modelBasePath: "../../models", filter: { equalization: true }, face: { enabled: true, - detector: { rotation: true, return: true }, + detector: { rotation: true, return: true, cropFactor: 1.6, mask: false }, description: { enabled: true }, iris: { enabled: true }, emotion: { enabled: false }, @@ -88,19 +94,24 @@ var options = { maxTime: 1e4, blinkMin: 10, blinkMax: 800, - threshold: 0.5 + threshold: 0.5, + mask: humanConfig.face.detector.mask, + rotation: humanConfig.face.detector.rotation, + cropFactor: humanConfig.face.detector.cropFactor }; var ok = { faceCount: false, faceConfidence: false, facingCenter: false, + lookingCenter: false, blinkDetected: false, faceSize: false, antispoofCheck: false, livenessCheck: false, elapsedMs: 0 }; -var allOk = () => ok.faceCount && ok.faceSize && ok.blinkDetected && ok.facingCenter && ok.faceConfidence && ok.antispoofCheck && ok.livenessCheck; +var allOk = () => ok.faceCount && ok.faceSize && ok.blinkDetected && ok.facingCenter && ok.lookingCenter && ok.faceConfidence && ok.antispoofCheck && ok.livenessCheck; +var current = { face: null, record: null }; var blink = { start: 0, end: 0, @@ -115,13 +126,13 @@ var dom = { canvas: document.getElementById("canvas"), log: document.getElementById("log"), fps: document.getElementById("fps"), - status: document.getElementById("status"), match: document.getElementById("match"), name: document.getElementById("name"), save: document.getElementById("save"), delete: document.getElementById("delete"), retry: document.getElementById("retry"), - source: document.getElementById("source") + source: document.getElementById("source"), + ok: document.getElementById("ok") }; var timestamp = { detect: 0, draw: 0 }; var fps = { detect: 0, draw: 0 }; @@ -131,7 +142,6 @@ var log2 = (...msg) => { console.log(...msg); }; var printFPS = (msg) => dom.fps.innerText = msg; -var printStatus = (msg) => dom.status.innerText = "status: " + JSON.stringify(msg).replace(/"|{|}/g, "").replace(/,/g, " | "); async function webCam() { printFPS("starting webcam..."); const cameraOptions = { audio: false, video: { facingMode: "user", resizeMode: "none", width: { ideal: document.body.clientWidth } } }; @@ -155,8 +165,8 @@ async function webCam() { } async function detectionLoop() { if (!dom.video.paused) { - if (face && face.tensor) - human.tf.dispose(face.tensor); + if (current.face && current.face.tensor) + human.tf.dispose(current.face.tensor); await human.detect(dom.video); const now = human.now(); fps.detect = 1e3 / (now - timestamp.detect); @@ -179,16 +189,32 @@ async function validationLoop() { blink.start = human.now(); if (blink.start > 0 && !gestures.includes("blink left eye") && !gestures.includes("blink right eye")) blink.end = human.now(); - ok.blinkDetected = ok.blinkDetected || blink.end - blink.start > options.blinkMin && blink.end - blink.start < options.blinkMax; + ok.blinkDetected = ok.blinkDetected || Math.abs(blink.end - blink.start) > options.blinkMin && Math.abs(blink.end - blink.start) < options.blinkMax; if (ok.blinkDetected && blink.time === 0) blink.time = Math.trunc(blink.end - blink.start); - ok.facingCenter = gestures.includes("facing center") && gestures.includes("looking center"); + ok.facingCenter = gestures.includes("facing center"); + ok.lookingCenter = gestures.includes("looking center"); ok.faceConfidence = (human.result.face[0].boxScore || 0) > options.minConfidence && (human.result.face[0].faceScore || 0) > options.minConfidence && (human.result.face[0].genderScore || 0) > options.minConfidence; ok.antispoofCheck = (human.result.face[0].real || 0) > options.minConfidence; ok.livenessCheck = (human.result.face[0].live || 0) > options.minConfidence; ok.faceSize = human.result.face[0].box[2] >= options.minSize && human.result.face[0].box[3] >= options.minSize; } - printStatus(ok); + let y = 32; + for (const [key, val] of Object.entries(ok)) { + let el = document.getElementById(`ok-${key}`); + if (!el) { + el = document.createElement("div"); + el.innerText = key; + el.className = "ok"; + el.style.top = `${y}px`; + dom.ok.appendChild(el); + } + if (typeof val === "boolean") + el.style.backgroundColor = val ? "lightgreen" : "lightcoral"; + else + el.innerText = `${key}:${val}`; + y += 28; + } if (allOk()) { dom.video.pause(); return human.result.face[0]; @@ -208,46 +234,48 @@ async function validationLoop() { } } async function saveRecords() { - var _a; + var _a, _b; if (dom.name.value.length > 0) { const image = (_a = dom.canvas.getContext("2d")) == null ? void 0 : _a.getImageData(0, 0, dom.canvas.width, dom.canvas.height); - const rec = { id: 0, name: dom.name.value, descriptor: face.embedding, image }; + const rec = { id: 0, name: dom.name.value, descriptor: (_b = current.face) == null ? void 0 : _b.embedding, image }; await save(rec); log2("saved face record:", rec.name); - db2.push(rec); } else { log2("invalid name"); } } async function deleteRecord() { - if (current.id > 0) { - await remove(current); + if (current.record && current.record.id > 0) { + await remove(current.record); } } async function detectFace() { var _a, _b; (_a = dom.canvas.getContext("2d")) == null ? void 0 : _a.clearRect(0, 0, options.minSize, options.minSize); - if (!face || !face.tensor || !face.embedding) - return 0; - human.tf.browser.toPixels(face.tensor, dom.canvas); - const descriptors = db2.map((rec) => rec.descriptor); - const res = await human.match(face.embedding, descriptors); - if (res.index === -1) { - log2("no matches"); + if (!current.face || !current.face.tensor || !current.face.embedding) + return false; + human.tf.browser.toPixels(current.face.tensor, dom.canvas); + if (await count() === 0) { + log2("face database is empty"); + document.body.style.background = "black"; dom.delete.style.display = "none"; - dom.source.style.display = "none"; - } else { - current = db2[res.index]; - log2(`best match: ${current.name} | id: ${current.id} | similarity: ${Math.round(1e3 * res.similarity) / 10}%`); - dom.delete.style.display = ""; - dom.name.value = current.name; - dom.source.style.display = ""; - (_b = dom.source.getContext("2d")) == null ? void 0 : _b.putImageData(current.image, 0, 0); + return false; } + const db2 = await load(); + const descriptors = db2.map((rec) => rec.descriptor); + const res = await human.match(current.face.embedding, descriptors); + current.record = db2[res.index] || null; + if (current.record) { + log2(`best match: ${current.record.name} | id: ${current.record.id} | similarity: ${Math.round(1e3 * res.similarity) / 10}%`); + dom.name.value = current.record.name; + dom.source.style.display = ""; + (_b = dom.source.getContext("2d")) == null ? void 0 : _b.putImageData(current.record.image, 0, 0); + } + document.body.style.background = res.similarity > options.threshold ? "darkgreen" : "maroon"; return res.similarity > options.threshold; } async function main() { - var _a, _b; + var _a, _b, _c, _d; ok.faceCount = false; ok.faceConfidence = false; ok.facingCenter = false; @@ -258,34 +286,33 @@ async function main() { ok.elapsedMs = 0; dom.match.style.display = "none"; dom.retry.style.display = "none"; + dom.source.style.display = "none"; document.body.style.background = "black"; await webCam(); await detectionLoop(); startTime = human.now(); - face = await validationLoop(); - dom.fps.style.display = "none"; - dom.canvas.width = ((_a = face == null ? void 0 : face.tensor) == null ? void 0 : _a.shape[1]) || options.minSize; - dom.canvas.height = ((_b = face == null ? void 0 : face.tensor) == null ? void 0 : _b.shape[0]) || options.minSize; + current.face = await validationLoop(); + dom.canvas.width = ((_b = (_a = current.face) == null ? void 0 : _a.tensor) == null ? void 0 : _b.shape[1]) || options.minSize; + dom.canvas.height = ((_d = (_c = current.face) == null ? void 0 : _c.tensor) == null ? void 0 : _d.shape[0]) || options.minSize; dom.source.width = dom.canvas.width; dom.source.height = dom.canvas.height; dom.canvas.style.width = ""; dom.match.style.display = "flex"; + dom.save.style.display = "flex"; + dom.delete.style.display = "flex"; dom.retry.style.display = "block"; if (!allOk()) { log2("did not find valid face"); return false; } else { - const res = await detectFace(); - document.body.style.background = res ? "darkgreen" : "maroon"; - return res; + return detectFace(); } } async function init() { log2("human version:", human.version, "| tfjs version:", human.tf.version_core); log2("options:", JSON.stringify(options).replace(/{|}|"|\[|\]/g, "").replace(/,/g, " ")); printFPS("loading..."); - db2 = await load(); - log2("loaded face records:", db2.length); + log2("known face records:", await count()); await webCam(); await human.load(); printFPS("initializing..."); diff --git a/demo/faceid/index.ts b/demo/faceid/index.ts index c4311b0b..2ea9b5c8 100644 --- a/demo/faceid/index.ts +++ b/demo/faceid/index.ts @@ -10,16 +10,12 @@ import { Human, TensorLike, FaceResult } from '../../dist/human.esm.js'; // equivalent of @vladmandic/Human import * as indexDb from './indexdb'; // methods to deal with indexdb -let db: Array = []; // face descriptor database stored in indexdb -let face: FaceResult; // face result from human.detect -let current: indexDb.FaceRecord; // currently matched db record - const humanConfig = { // user configuration for human, used to fine-tune behavior modelBasePath: '../../models', filter: { equalization: true }, // lets run with histogram equilizer face: { enabled: true, - detector: { rotation: true, return: true }, // return tensor is used to get detected face image + detector: { rotation: true, return: true, cropFactor: 1.6, mask: false }, // return tensor is used to get detected face image description: { enabled: true }, iris: { enabled: true }, // needed to determine gaze direction emotion: { enabled: false }, // not needed @@ -39,19 +35,24 @@ const options = { blinkMin: 10, // minimum duration of a valid blink blinkMax: 800, // maximum duration of a valid blink threshold: 0.5, // minimum similarity + mask: humanConfig.face.detector.mask, + rotation: humanConfig.face.detector.rotation, + cropFactor: humanConfig.face.detector.cropFactor, }; const ok = { // must meet all rules faceCount: false, faceConfidence: false, facingCenter: false, + lookingCenter: false, blinkDetected: false, faceSize: false, antispoofCheck: false, livenessCheck: false, elapsedMs: 0, // total time while waiting for valid face }; -const allOk = () => ok.faceCount && ok.faceSize && ok.blinkDetected && ok.facingCenter && ok.faceConfidence && ok.antispoofCheck && ok.livenessCheck; +const allOk = () => ok.faceCount && ok.faceSize && ok.blinkDetected && ok.facingCenter && ok.lookingCenter && ok.faceConfidence && ok.antispoofCheck && ok.livenessCheck; +const current: { face: FaceResult | null, record: indexDb.FaceRecord | null } = { face: null, record: null }; // current face record and matched database record const blink = { // internal timers for blink start/end/duration start: 0, @@ -71,13 +72,13 @@ const dom = { // grab instances of dom objects so we dont have to look them up l canvas: document.getElementById('canvas') as HTMLCanvasElement, log: document.getElementById('log') as HTMLPreElement, fps: document.getElementById('fps') as HTMLPreElement, - status: document.getElementById('status') as HTMLPreElement, match: document.getElementById('match') as HTMLDivElement, name: document.getElementById('name') as HTMLInputElement, save: document.getElementById('save') as HTMLSpanElement, delete: document.getElementById('delete') as HTMLSpanElement, retry: document.getElementById('retry') as HTMLDivElement, source: document.getElementById('source') as HTMLCanvasElement, + ok: document.getElementById('ok') as HTMLDivElement, }; const timestamp = { detect: 0, draw: 0 }; // holds information used to calculate performance and possible memory leaks const fps = { detect: 0, draw: 0 }; // holds calculated fps information for both detect and screen refresh @@ -89,7 +90,6 @@ const log = (...msg) => { // helper method to output messages console.log(...msg); }; const printFPS = (msg) => dom.fps.innerText = msg; // print status element -const printStatus = (msg) => dom.status.innerText = 'status: ' + JSON.stringify(msg).replace(/"|{|}/g, '').replace(/,/g, ' | '); // print status element async function webCam() { // initialize webcam printFPS('starting webcam...'); @@ -111,7 +111,7 @@ async function webCam() { // initialize webcam async function detectionLoop() { // main detection loop if (!dom.video.paused) { - if (face && face.tensor) human.tf.dispose(face.tensor); // dispose previous tensor + if (current.face && current.face.tensor) human.tf.dispose(current.face.tensor); // dispose previous tensor await human.detect(dom.video); // actual detection; were not capturing output in a local variable as it can also be reached via human.result const now = human.now(); fps.detect = 1000 / (now - timestamp.detect); @@ -133,15 +133,29 @@ async function validationLoop(): Promise { // main screen refresh lo const gestures: string[] = Object.values(human.result.gesture).map((gesture) => gesture.gesture); // flatten all gestures if (gestures.includes('blink left eye') || gestures.includes('blink right eye')) blink.start = human.now(); // blink starts when eyes get closed if (blink.start > 0 && !gestures.includes('blink left eye') && !gestures.includes('blink right eye')) blink.end = human.now(); // if blink started how long until eyes are back open - ok.blinkDetected = ok.blinkDetected || (blink.end - blink.start > options.blinkMin && blink.end - blink.start < options.blinkMax); + ok.blinkDetected = ok.blinkDetected || (Math.abs(blink.end - blink.start) > options.blinkMin && Math.abs(blink.end - blink.start) < options.blinkMax); if (ok.blinkDetected && blink.time === 0) blink.time = Math.trunc(blink.end - blink.start); - ok.facingCenter = gestures.includes('facing center') && gestures.includes('looking center'); // must face camera and look at camera + ok.facingCenter = gestures.includes('facing center'); + ok.lookingCenter = gestures.includes('looking center'); // must face camera and look at camera ok.faceConfidence = (human.result.face[0].boxScore || 0) > options.minConfidence && (human.result.face[0].faceScore || 0) > options.minConfidence && (human.result.face[0].genderScore || 0) > options.minConfidence; ok.antispoofCheck = (human.result.face[0].real || 0) > options.minConfidence; ok.livenessCheck = (human.result.face[0].live || 0) > options.minConfidence; ok.faceSize = human.result.face[0].box[2] >= options.minSize && human.result.face[0].box[3] >= options.minSize; } - printStatus(ok); + let y = 32; + for (const [key, val] of Object.entries(ok)) { + let el = document.getElementById(`ok-${key}`); + if (!el) { + el = document.createElement('div'); + el.innerText = key; + el.className = 'ok'; + el.style.top = `${y}px`; + dom.ok.appendChild(el); + } + if (typeof val === 'boolean') el.style.backgroundColor = val ? 'lightgreen' : 'lightcoral'; + else el.innerText = `${key}:${val}`; + y += 28; + } if (allOk()) { // all criteria met dom.video.pause(); return human.result.face[0]; @@ -163,39 +177,41 @@ async function validationLoop(): Promise { // main screen refresh lo async function saveRecords() { if (dom.name.value.length > 0) { const image = dom.canvas.getContext('2d')?.getImageData(0, 0, dom.canvas.width, dom.canvas.height) as ImageData; - const rec = { id: 0, name: dom.name.value, descriptor: face.embedding as number[], image }; + const rec = { id: 0, name: dom.name.value, descriptor: current.face?.embedding as number[], image }; await indexDb.save(rec); log('saved face record:', rec.name); - db.push(rec); } else { log('invalid name'); } } async function deleteRecord() { - if (current.id > 0) { - await indexDb.remove(current); + if (current.record && current.record.id > 0) { + await indexDb.remove(current.record); } } async function detectFace() { dom.canvas.getContext('2d')?.clearRect(0, 0, options.minSize, options.minSize); - if (!face || !face.tensor || !face.embedding) return 0; - human.tf.browser.toPixels(face.tensor as unknown as TensorLike, dom.canvas); - const descriptors = db.map((rec) => rec.descriptor); - const res = await human.match(face.embedding, descriptors); - if (res.index === -1) { - log('no matches'); + if (!current.face || !current.face.tensor || !current.face.embedding) return false; + human.tf.browser.toPixels(current.face.tensor as unknown as TensorLike, dom.canvas); + if (await indexDb.count() === 0) { + log('face database is empty'); + document.body.style.background = 'black'; dom.delete.style.display = 'none'; - dom.source.style.display = 'none'; - } else { - current = db[res.index]; - log(`best match: ${current.name} | id: ${current.id} | similarity: ${Math.round(1000 * res.similarity) / 10}%`); - dom.delete.style.display = ''; - dom.name.value = current.name; - dom.source.style.display = ''; - dom.source.getContext('2d')?.putImageData(current.image, 0, 0); + return false; } + const db = await indexDb.load(); + const descriptors = db.map((rec) => rec.descriptor); + const res = await human.match(current.face.embedding, descriptors); + current.record = db[res.index] || null; + if (current.record) { + log(`best match: ${current.record.name} | id: ${current.record.id} | similarity: ${Math.round(1000 * res.similarity) / 10}%`); + dom.name.value = current.record.name; + dom.source.style.display = ''; + dom.source.getContext('2d')?.putImageData(current.record.image, 0, 0); + } + document.body.style.background = res.similarity > options.threshold ? 'darkgreen' : 'maroon'; return res.similarity > options.threshold; } @@ -210,27 +226,26 @@ async function main() { // main entry point ok.elapsedMs = 0; dom.match.style.display = 'none'; dom.retry.style.display = 'none'; + dom.source.style.display = 'none'; document.body.style.background = 'black'; await webCam(); await detectionLoop(); // start detection loop startTime = human.now(); - face = await validationLoop(); // start validation loop - dom.fps.style.display = 'none'; - dom.canvas.width = face?.tensor?.shape[1] || options.minSize; - dom.canvas.height = face?.tensor?.shape[0] || options.minSize; + current.face = await validationLoop(); // start validation loop + dom.canvas.width = current.face?.tensor?.shape[1] || options.minSize; + dom.canvas.height = current.face?.tensor?.shape[0] || options.minSize; dom.source.width = dom.canvas.width; dom.source.height = dom.canvas.height; dom.canvas.style.width = ''; dom.match.style.display = 'flex'; + dom.save.style.display = 'flex'; + dom.delete.style.display = 'flex'; dom.retry.style.display = 'block'; - if (!allOk()) { + if (!allOk()) { // is all criteria met? log('did not find valid face'); return false; } else { - // log('found valid face'); - const res = await detectFace(); - document.body.style.background = res ? 'darkgreen' : 'maroon'; - return res; + return detectFace(); } } @@ -238,8 +253,7 @@ async function init() { log('human version:', human.version, '| tfjs version:', human.tf.version_core); log('options:', JSON.stringify(options).replace(/{|}|"|\[|\]/g, '').replace(/,/g, ' ')); printFPS('loading...'); - db = await indexDb.load(); // load face database from indexdb - log('loaded face records:', db.length); + log('known face records:', await indexDb.count()); await webCam(); // start webcam await human.load(); // preload all models printFPS('initializing...'); diff --git a/demo/faceid/indexdb.ts b/demo/faceid/indexdb.ts index 6757366a..77fee852 100644 --- a/demo/faceid/indexdb.ts +++ b/demo/faceid/indexdb.ts @@ -43,6 +43,15 @@ export async function load(): Promise { }); } +export async function count(): Promise { + if (!db) await open(); // open or create if not already done + return new Promise((resolve) => { + const store: IDBRequest = db.transaction([table], 'readwrite').objectStore(table).count(); + store.onerror = (evt) => log('count error:', evt); + store.onsuccess = () => resolve(store.result); + }); +} + export async function save(faceRecord: FaceRecord) { if (!db) await open(); // open or create if not already done const newRecord = { name: faceRecord.name, descriptor: faceRecord.descriptor, image: faceRecord.image }; // omit id as its autoincrement diff --git a/demo/facematch/facematch.js b/demo/facematch/facematch.js index 3338eca7..b7945963 100644 --- a/demo/facematch/facematch.js +++ b/demo/facematch/facematch.js @@ -135,12 +135,10 @@ async function SelectFaceCanvas(face) { title('Selected Face'); } -function AddFaceCanvas(index, res, fileName) { +async function AddFaceCanvas(index, res, fileName) { all[index] = res.face; - let ok = false; for (const i in res.face) { - if (res.face[i].mesh.length === 0) continue; - ok = true; + if (res.face[i].mesh.length === 0 || !res.face[i].tensor) continue; // did not get valid results all[index][i].fileName = fileName; const canvas = document.createElement('canvas'); canvas.tag = { sample: index, face: i, source: fileName }; @@ -155,27 +153,22 @@ function AddFaceCanvas(index, res, fileName) { gender: ${Math.round(100 * res.face[i].genderScore)}% ${res.face[i].gender} emotion: ${emotion} `.replace(/ /g, ' '); - // mouse click on any face canvas triggers analysis + await human.tf.browser.toPixels(res.face[i].tensor, canvas); + const ctx = canvas.getContext('2d'); + if (!ctx) return false; + ctx.font = 'small-caps 0.8rem "Lato"'; + ctx.fillStyle = 'rgba(255, 255, 255, 1)'; + ctx.fillText(`${res.face[i].age}y ${(100 * (res.face[i].genderScore || 0)).toFixed(1)}% ${res.face[i].gender}`, 4, canvas.height - 6); + const arr = db.map((rec) => rec.embedding); + const result = human.match(res.face[i].embedding, arr); + ctx.font = 'small-caps 1rem "Lato"'; + if (result.similarity && res.similarity > minScore) ctx.fillText(`${(100 * result.similarity).toFixed(1)}% ${db[result.index].name}`, 4, canvas.height - 30); + document.getElementById('faces').appendChild(canvas); canvas.addEventListener('click', (evt) => { log('Select:', 'Image:', evt.target.tag.sample, 'Face:', evt.target.tag.face, 'Source:', evt.target.tag.source, all[evt.target.tag.sample][evt.target.tag.face]); SelectFaceCanvas(all[evt.target.tag.sample][evt.target.tag.face]); }); - // if we actually got face image tensor, draw canvas with that face - if (res.face[i].tensor) { - human.tf.browser.toPixels(res.face[i].tensor, canvas); - document.getElementById('faces').appendChild(canvas); - const ctx = canvas.getContext('2d'); - if (!ctx) return false; - ctx.font = 'small-caps 0.8rem "Lato"'; - ctx.fillStyle = 'rgba(255, 255, 255, 1)'; - ctx.fillText(`${res.face[i].age}y ${(100 * (res.face[i].genderScore || 0)).toFixed(1)}% ${res.face[i].gender}`, 4, canvas.height - 6); - const arr = db.map((rec) => rec.embedding); - const result = human.match(res.face[i].embedding, arr); - ctx.font = 'small-caps 1rem "Lato"'; - if (result.similarity && res.similarity > minScore) ctx.fillText(`${(100 * result.similarity).toFixed(1)}% ${db[result.index].name}`, 4, canvas.height - 30); - } } - return ok; } async function AddImageElement(index, image, length) { @@ -185,8 +178,8 @@ async function AddImageElement(index, image, length) { const img = new Image(128, 128); img.onload = () => { // must wait until image is loaded human.detect(img, userConfig).then((res) => { - const ok = AddFaceCanvas(index, res, image); // then wait until image is analyzed - if (ok) document.getElementById('images').appendChild(img); // and finally we can add it + AddFaceCanvas(index, res, image); // then wait until image is analyzed + document.getElementById('images').appendChild(img); // and finally we can add it resolve(true); }); }; diff --git a/src/config.ts b/src/config.ts index f3c59abb..275262ae 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,6 +23,14 @@ export interface FaceDetectorConfig extends GenericConfig { minConfidence: number, /** @property minimum overlap between two detected faces before one is discarded */ iouThreshold: number, + /** @property factor used to expand detected face before further analysis + * - default: 1.6 + * - for high-quality inputs can be reduced to increase precision + * - for video inputs or low-quality inputs can be increased to allow for more flexible tracking + */ + cropFactor: number, + /** @property should child models perform on masked image of a face */ + mask: boolean, /** @property should face detection return face tensor to be used in some other extenrnal model? */ return: boolean, } @@ -314,6 +322,8 @@ const config: Config = { skipTime: 2500, minConfidence: 0.2, iouThreshold: 0.1, + cropFactor: 1.6, + mask: false, return: false, }, mesh: { diff --git a/src/face/blazeface.ts b/src/face/blazeface.ts index e843e9fa..35c48d71 100644 --- a/src/face/blazeface.ts +++ b/src/face/blazeface.ts @@ -17,7 +17,6 @@ let anchorsData: [number, number][] = []; let anchors: Tensor | null = null; let inputSize = 0; -// export const size = () => (model && model.inputs[0].shape ? model.inputs[0].shape[2] : 0); export const size = () => inputSize; export async function load(config: Config): Promise { diff --git a/src/face/face.ts b/src/face/face.ts index df7299c2..b14cdd83 100644 --- a/src/face/face.ts +++ b/src/face/face.ts @@ -9,13 +9,15 @@ import * as tf from '../../dist/tfjs.esm.js'; import * as facemesh from './facemesh'; import * as emotion from '../gear/emotion'; import * as faceres from './faceres'; +import * as mask from './mask'; import * as antispoof from './antispoof'; import * as liveness from './liveness'; import type { FaceResult } from '../result'; import type { Tensor } from '../tfjs/types'; +import type { Human } from '../human'; import { calculateFaceAngle } from './angles'; -export const detectFace = async (parent /* instance of human */, input: Tensor): Promise => { +export const detectFace = async (parent: Human /* instance of human */, input: Tensor): Promise => { // run facemesh, includes blazeface and iris // eslint-disable-next-line no-async-promise-executor let timeStamp; @@ -46,16 +48,24 @@ export const detectFace = async (parent /* instance of human */, input: Tensor): continue; } - const rotation = calculateFaceAngle(faces[i], [input.shape[2], input.shape[1]]); + // optional face mask + if (parent.config.face.detector?.mask) { + const masked = await mask.mask(faces[i]); + tf.dispose(faces[i].tensor); + faces[i].tensor = masked as Tensor; + } + + // calculate face angles + const rotation = faces[i].mesh && (faces[i].mesh.length > 200) ? calculateFaceAngle(faces[i], [input.shape[2], input.shape[1]]) : null; // run emotion, inherits face from blazeface parent.analyze('Start Emotion:'); if (parent.config.async) { - emotionRes = parent.config.face.emotion.enabled ? emotion.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; + emotionRes = parent.config.face.emotion?.enabled ? emotion.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; } else { parent.state = 'run:emotion'; timeStamp = now(); - emotionRes = parent.config.face.emotion.enabled ? await emotion.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; + emotionRes = parent.config.face.emotion?.enabled ? await emotion.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; parent.performance.emotion = env.perfadd ? (parent.performance.emotion || 0) + Math.trunc(now() - timeStamp) : Math.trunc(now() - timeStamp); } parent.analyze('End Emotion:'); @@ -63,11 +73,11 @@ export const detectFace = async (parent /* instance of human */, input: Tensor): // run antispoof, inherits face from blazeface parent.analyze('Start AntiSpoof:'); if (parent.config.async) { - antispoofRes = parent.config.face.antispoof.enabled ? antispoof.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; + antispoofRes = parent.config.face.antispoof?.enabled ? antispoof.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; } else { parent.state = 'run:antispoof'; timeStamp = now(); - antispoofRes = parent.config.face.antispoof.enabled ? await antispoof.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; + antispoofRes = parent.config.face.antispoof?.enabled ? await antispoof.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; parent.performance.antispoof = env.perfadd ? (parent.performance.antispoof || 0) + Math.trunc(now() - timeStamp) : Math.trunc(now() - timeStamp); } parent.analyze('End AntiSpoof:'); @@ -75,11 +85,11 @@ export const detectFace = async (parent /* instance of human */, input: Tensor): // run liveness, inherits face from blazeface parent.analyze('Start Liveness:'); if (parent.config.async) { - livenessRes = parent.config.face.liveness.enabled ? liveness.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; + livenessRes = parent.config.face.liveness?.enabled ? liveness.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; } else { parent.state = 'run:liveness'; timeStamp = now(); - livenessRes = parent.config.face.liveness.enabled ? await liveness.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; + livenessRes = parent.config.face.liveness?.enabled ? await liveness.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; parent.performance.antispoof = env.perfadd ? (parent.performance.antispoof || 0) + Math.trunc(now() - timeStamp) : Math.trunc(now() - timeStamp); } parent.analyze('End Liveness:'); @@ -101,11 +111,11 @@ export const detectFace = async (parent /* instance of human */, input: Tensor): // run emotion, inherits face from blazeface parent.analyze('Start Description:'); if (parent.config.async) { - descRes = parent.config.face.description.enabled ? faceres.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; + descRes = parent.config.face.description?.enabled ? faceres.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; } else { parent.state = 'run:description'; timeStamp = now(); - descRes = parent.config.face.description.enabled ? await faceres.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; + descRes = parent.config.face.description?.enabled ? await faceres.predict(faces[i].tensor || tf.tensor([]), parent.config, i, faces.length) : null; parent.performance.description = env.perfadd ? (parent.performance.description || 0) + Math.trunc(now() - timeStamp) : Math.trunc(now() - timeStamp); } parent.analyze('End Description:'); @@ -119,7 +129,7 @@ export const detectFace = async (parent /* instance of human */, input: Tensor): // calculate iris distance // iris: array[ center, left, top, right, bottom] - if (!parent.config.face.iris.enabled && faces[i]?.annotations?.leftEyeIris && faces[i]?.annotations?.rightEyeIris) { + if (!parent.config.face.iris?.enabled && faces[i]?.annotations?.leftEyeIris && faces[i]?.annotations?.rightEyeIris) { delete faces[i].annotations.leftEyeIris; delete faces[i].annotations.rightEyeIris; } @@ -130,7 +140,7 @@ export const detectFace = async (parent /* instance of human */, input: Tensor): : 0; // note: average human iris size is 11.7mm // optionally return tensor - const tensor = parent.config.face.detector.return ? tf.squeeze(faces[i].tensor) : null; + const tensor = parent.config.face.detector?.return ? tf.squeeze(faces[i].tensor) : null; // dispose original face tensor tf.dispose(faces[i].tensor); // delete temp face image diff --git a/src/face/facemesh.ts b/src/face/facemesh.ts index 3efe0d5b..35336499 100644 --- a/src/face/facemesh.ts +++ b/src/face/facemesh.ts @@ -25,11 +25,9 @@ let model: GraphModel | null = null; let inputSize = 0; let skipped = Number.MAX_SAFE_INTEGER; let lastTime = 0; -const enlargeFact = 1.6; export async function predict(input: Tensor, config: Config): Promise { // reset cached boxes - const skipTime = (config.face.detector?.skipTime || 0) > (now() - lastTime); const skipFrame = skipped < (config.face.detector?.skipFrames || 0); if (!config.skipAllowed || !skipTime || !skipFrame || boxCache.length === 0) { @@ -43,7 +41,7 @@ export async function predict(input: Tensor, config: Config): Promise [pt[0] / (input.shape[2] || 0), pt[1] / (input.shape[1] || 0), (pt[2] || 0) / inputSize]); for (const key of Object.keys(coords.meshAnnotations)) face.annotations[key] = coords.meshAnnotations[key].map((index) => face.mesh[index]); // add annotations - box = util.squarifyBox({ ...util.enlargeBox(util.calculateLandmarksBoundingBox(face.mesh), enlargeFact), confidence: box.confidence }); // redefine box with mesh calculated one + box = util.squarifyBox({ ...util.enlargeBox(util.calculateLandmarksBoundingBox(face.mesh), (config.face.detector?.cropFactor || 1.6)), confidence: box.confidence }); // redefine box with mesh calculated one face.box = util.getClampedBox(box, input); // update detected box with box around the face mesh face.boxRaw = util.getRawBox(box, input); face.score = face.faceScore; diff --git a/src/face/faceres.ts b/src/face/faceres.ts index 98a63d6a..2f54bb94 100644 --- a/src/face/faceres.ts +++ b/src/face/faceres.ts @@ -39,22 +39,17 @@ export async function load(config: Config): Promise { export function enhance(input): Tensor { const tensor = (input.image || input.tensor || input) as Tensor; // input received from detector is already normalized to 0..1, input is also assumed to be straightened if (!model?.inputs[0].shape) return tensor; // model has no shape so no point continuing - // do a tight crop of image and resize it to fit the model const crop = tf.image.resizeBilinear(tensor, [model.inputs[0].shape[2], model.inputs[0].shape[1]], false); const norm = tf.mul(crop, 255); tf.dispose(crop); return norm; /* + // do a tight crop of image and resize it to fit the model const box = [[0.05, 0.15, 0.85, 0.85]]; // empyrical values for top, left, bottom, right const crop = (tensor.shape.length === 3) ? tf.image.cropAndResize(tf.expandDims(tensor, 0), box, [0], [model.inputs[0].shape[2], model.inputs[0].shape[1]]) // add batch dimension if missing : tf.image.cropAndResize(tensor, box, [0], [model.inputs[0].shape[2], model.inputs[0].shape[1]]); */ - /* - // just resize to fit the embedding model instead of cropping - const crop = tf.image.resizeBilinear(tensor, [model.inputs[0].shape[2], model.inputs[0].shape[1]], false); - */ - /* // convert to black&white to avoid colorization impact const rgb = [0.2989, 0.5870, 0.1140]; // factors for red/green/blue colors when converting to grayscale: https://www.mathworks.com/help/matlab/ref/rgb2gray.html @@ -65,22 +60,6 @@ export function enhance(input): Tensor { const grayscale = tf.addN([redNorm, greenNorm, blueNorm]); const merge = tf.stack([grayscale, grayscale, grayscale], 3).squeeze(4); */ - - /* - // increase image pseudo-contrast 100% - // (or do it per-channel so mean is done on each channel) - // (or calculate histogram and do it based on histogram) - const mean = merge.mean(); - const factor = 2; - const contrast = merge.sub(mean).mul(factor).add(mean); - */ - - /* - // normalize brightness from 0..1 - // silly way of creating pseudo-hdr of image - const darken = crop.sub(crop.min()); - const lighten = darken.div(darken.max()); - */ } export async function predict(image: Tensor, config: Config, idx, count) { diff --git a/src/face/mask.ts b/src/face/mask.ts new file mode 100644 index 00000000..67306b49 --- /dev/null +++ b/src/face/mask.ts @@ -0,0 +1,40 @@ +import type { Tensor } from '../tfjs/types'; +import type { FaceResult } from '../result'; +import * as tf from '../../dist/tfjs.esm.js'; +import { meshAnnotations } from './facemeshcoords'; + +const expandFact = 0.1; +const alpha = 0.5; + +// point inclusion in polygon based on https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html +function insidePoly(x: number, y: number, polygon: Array<{ x: number, y: number }>): boolean { + let inside = false; + let j = polygon.length - 1; + for (let i = 0; i < polygon.length; j = i++) { + if (((polygon[i].y > y) !== (polygon[j].y > y)) && (x < (polygon[j].x - polygon[i].x) * (y - polygon[i].y) / (polygon[j].y - polygon[i].y) + polygon[i].x)) inside = !inside; + } + return inside; +} + +export async function mask(face: FaceResult): Promise { + if (!face.tensor) return face.tensor; + const width = face.tensor.shape[2] || 0; + const height = face.tensor.shape[1] || 0; + const buffer = await face.tensor.buffer(); + let silhouette: Array<{ x: number, y: number }> = []; + for (const pt of meshAnnotations.silhouette) silhouette.push({ x: (face.mesh[pt][0] - face.box[0]) / face.box[2], y: (face.mesh[pt][1] - face.box[1]) / face.box[3] }); // add all silhouette points scaled to local box + if (expandFact && expandFact > 0) silhouette = silhouette.map((pt) => ({ x: pt.x > 0.5 ? pt.x + expandFact : pt.x - expandFact, y: pt.y > 0.5 ? pt.y + expandFact : pt.y - expandFact })); // expand silhouette + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { + const inside = insidePoly(x / width, y / width, silhouette); + if (!inside) { + buffer.set(alpha * buffer.get(0, y, x, 0), 0, y, x, 0); + buffer.set(alpha * buffer.get(0, y, x, 1), 0, y, x, 1); + buffer.set(alpha * buffer.get(0, y, x, 2), 0, y, x, 2); + } + } + } + const output = buffer.toTensor(); + tf.dispose(buffer); + return output; +} diff --git a/src/gesture/gesture.ts b/src/gesture/gesture.ts index 7f242d17..1807e23f 100644 --- a/src/gesture/gesture.ts +++ b/src/gesture/gesture.ts @@ -66,9 +66,10 @@ export const face = (res): GestureResult[] => { const gestures: Array<{ face: number, gesture: FaceGesture }> = []; for (let i = 0; i < res.length; i++) { if (res[i].mesh && res[i].mesh.length > 450) { - const eyeFacing = res[i].mesh[33][2] - res[i].mesh[263][2]; - if (Math.abs(eyeFacing) < 10) gestures.push({ face: i, gesture: 'facing center' }); - else gestures.push({ face: i, gesture: `facing ${eyeFacing < 0 ? 'left' : 'right'}` }); + const zDiff = res[i].mesh[33][2] - res[i].mesh[263][2]; + const xDiff = res[i].mesh[33][0] - res[i].mesh[263][0]; + if (Math.abs(zDiff / xDiff) <= 0.15) gestures.push({ face: i, gesture: 'facing center' }); + else gestures.push({ face: i, gesture: `facing ${zDiff < 0 ? 'left' : 'right'}` }); const openLeft = Math.abs(res[i].mesh[374][1] - res[i].mesh[386][1]) / Math.abs(res[i].mesh[443][1] - res[i].mesh[450][1]); // center of eye inner lid y coord div center of wider eye border y coord if (openLeft < 0.2) gestures.push({ face: i, gesture: 'blink left eye' }); const openRight = Math.abs(res[i].mesh[145][1] - res[i].mesh[159][1]) / Math.abs(res[i].mesh[223][1] - res[i].mesh[230][1]); // center of eye inner lid y coord div center of wider eye border y coord diff --git a/src/human.ts b/src/human.ts index a86175d2..ac26fef4 100644 --- a/src/human.ts +++ b/src/human.ts @@ -253,7 +253,7 @@ export class Human { * - `canvas` as canvas which is input image filtered with segementation data and optionally merged with background image. canvas alpha values are set to segmentation values for easy merging * - `alpha` as grayscale canvas that represents segmentation alpha values */ - async segmentation(input: Input, background?: Input): Promise<{ data: number[], canvas: HTMLCanvasElement | OffscreenCanvas | null, alpha: HTMLCanvasElement | OffscreenCanvas | null }> { + async segmentation(input: Input, background?: Input): Promise<{ data: number[] | Tensor, canvas: HTMLCanvasElement | OffscreenCanvas | null, alpha: HTMLCanvasElement | OffscreenCanvas | null }> { return segmentation.process(input, background, this.config); } diff --git a/src/result.ts b/src/result.ts index 82964cb4..360ffb2b 100644 --- a/src/result.ts +++ b/src/result.ts @@ -54,7 +54,7 @@ export interface FaceResult { angle: { roll: number, yaw: number, pitch: number }, matrix: [number, number, number, number, number, number, number, number, number], gaze: { bearing: number, strength: number }, - } + } | null, /** detected face as tensor that can be used in further pipelines */ tensor?: Tensor, } diff --git a/src/tfjs/backend.ts b/src/tfjs/backend.ts index 1f71d44f..ae0ac4ce 100644 --- a/src/tfjs/backend.ts +++ b/src/tfjs/backend.ts @@ -1,5 +1,6 @@ /** TFJS backend initialization and customization */ +import type { Human } from '../human'; import { log, now } from '../util/util'; import { env } from '../util/env'; import * as humangl from './humangl'; @@ -26,7 +27,7 @@ function registerCustomOps() { } } -export async function check(instance, force = false) { +export async function check(instance: Human, force = false) { instance.state = 'backend'; if (force || env.initial || (instance.config.backend && (instance.config.backend.length > 0) && (tf.getBackend() !== instance.config.backend))) { const timeStamp = now(); diff --git a/src/tfjs/humangl.ts b/src/tfjs/humangl.ts index 3022c388..03798aa9 100644 --- a/src/tfjs/humangl.ts +++ b/src/tfjs/humangl.ts @@ -1,5 +1,6 @@ /** TFJS custom backend registration */ +import type { Human } from '../human'; import { log } from '../util/util'; import * as tf from '../../dist/tfjs.esm.js'; import * as image from '../image/image'; @@ -40,7 +41,7 @@ function extensions(): void { * * @returns void */ -export async function register(instance): Promise { +export async function register(instance: Human): Promise { // force backend reload if gl context is not valid if (instance.config.backend !== 'humangl') return; if ((config.name in tf.engine().registry) && (!config.gl || !config.gl.getParameter(config.gl.VERSION))) { diff --git a/src/util/interpolate.ts b/src/util/interpolate.ts index 31f4f438..08c973f4 100644 --- a/src/util/interpolate.ts +++ b/src/util/interpolate.ts @@ -114,23 +114,26 @@ export function calc(newResult: Result, config: Config): Result { .map((b, j) => ((bufferedFactor - 1) * bufferedResult.face[i].box[j] + b) / bufferedFactor)) as Box; const boxRaw = (newResult.face[i].boxRaw // update boxRaw .map((b, j) => ((bufferedFactor - 1) * bufferedResult.face[i].boxRaw[j] + b) / bufferedFactor)) as Box; - const rotation: { - matrix: [number, number, number, number, number, number, number, number, number], - angle: { roll: number, yaw: number, pitch: number }, - gaze: { bearing: number, strength: number } - } = { matrix: [0, 0, 0, 0, 0, 0, 0, 0, 0], angle: { roll: 0, yaw: 0, pitch: 0 }, gaze: { bearing: 0, strength: 0 } }; - rotation.matrix = newResult.face[i].rotation?.matrix as [number, number, number, number, number, number, number, number, number]; - rotation.angle = { - roll: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.angle?.roll || 0) + (newResult.face[i].rotation?.angle?.roll || 0)) / bufferedFactor, - yaw: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.angle?.yaw || 0) + (newResult.face[i].rotation?.angle?.yaw || 0)) / bufferedFactor, - pitch: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.angle?.pitch || 0) + (newResult.face[i].rotation?.angle?.pitch || 0)) / bufferedFactor, - }; - rotation.gaze = { - // not fully correct due projection on circle, also causes wrap-around draw on jump from negative to positive - bearing: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.gaze?.bearing || 0) + (newResult.face[i].rotation?.gaze?.bearing || 0)) / bufferedFactor, - strength: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.gaze?.strength || 0) + (newResult.face[i].rotation?.gaze?.strength || 0)) / bufferedFactor, - }; - bufferedResult.face[i] = { ...newResult.face[i], rotation, box, boxRaw }; // shallow clone plus updated values + if (newResult.face[i].rotation) { + const rotation: { + matrix: [number, number, number, number, number, number, number, number, number], + angle: { roll: number, yaw: number, pitch: number }, + gaze: { bearing: number, strength: number } + } = { matrix: [0, 0, 0, 0, 0, 0, 0, 0, 0], angle: { roll: 0, yaw: 0, pitch: 0 }, gaze: { bearing: 0, strength: 0 } }; + rotation.matrix = newResult.face[i].rotation?.matrix as [number, number, number, number, number, number, number, number, number]; + rotation.angle = { + roll: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.angle?.roll || 0) + (newResult.face[i].rotation?.angle?.roll || 0)) / bufferedFactor, + yaw: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.angle?.yaw || 0) + (newResult.face[i].rotation?.angle?.yaw || 0)) / bufferedFactor, + pitch: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.angle?.pitch || 0) + (newResult.face[i].rotation?.angle?.pitch || 0)) / bufferedFactor, + }; + rotation.gaze = { + // not fully correct due projection on circle, also causes wrap-around draw on jump from negative to positive + bearing: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.gaze?.bearing || 0) + (newResult.face[i].rotation?.gaze?.bearing || 0)) / bufferedFactor, + strength: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.gaze?.strength || 0) + (newResult.face[i].rotation?.gaze?.strength || 0)) / bufferedFactor, + }; + bufferedResult.face[i] = { ...newResult.face[i], rotation, box, boxRaw }; // shallow clone plus updated values + } + bufferedResult.face[i] = { ...newResult.face[i], box, boxRaw }; // shallow clone plus updated values } } diff --git a/src/warmup.ts b/src/warmup.ts index ff0e701a..b898e6a0 100644 --- a/src/warmup.ts +++ b/src/warmup.ts @@ -8,9 +8,11 @@ import * as tf from '../dist/tfjs.esm.js'; import * as image from './image/image'; import type { Config } from './config'; import type { Result } from './result'; +import type { Human } from './human'; +import type { Tensor } from './tfjs/types'; import { env } from './util/env'; -async function warmupBitmap(instance) { +async function warmupBitmap(instance: Human) { const b64toBlob = (base64: string, type = 'application/octet-stream') => fetch(`data:${type};base64,${base64}`).then((res) => res.blob()); let blob; let res; @@ -28,7 +30,7 @@ async function warmupBitmap(instance) { return res; } -async function warmupCanvas(instance) { +async function warmupCanvas(instance: Human) { return new Promise((resolve) => { let src; // let size = 0; @@ -60,7 +62,7 @@ async function warmupCanvas(instance) { if (ctx) ctx.drawImage(img, 0, 0); // const data = ctx?.getImageData(0, 0, canvas.height, canvas.width); const tensor = await instance.image(canvas); - const res = await instance.detect(tensor.tensor, instance.config); + const res = await instance.detect(tensor.tensor as Tensor, instance.config); resolve(res); } }; @@ -69,7 +71,7 @@ async function warmupCanvas(instance) { }); } -async function warmupNode(instance) { +async function warmupNode(instance: Human) { const atob = (str: string) => Buffer.from(str, 'base64'); let img; if (instance.config.warmup === 'face') img = atob(sample.face); @@ -101,7 +103,7 @@ async function warmupNode(instance) { * - only used for `webgl` and `humangl` backends * @param userConfig?: Config */ -export async function warmup(instance, userConfig?: Partial): Promise { +export async function warmup(instance: Human, userConfig?: Partial): Promise { const t0 = now(); instance.state = 'warmup'; if (userConfig) instance.config = mergeDeep(instance.config, userConfig) as Config;