mirror of https://github.com/vladmandic/human
363 lines
14 KiB
JavaScript
363 lines
14 KiB
JavaScript
/*
|
|
Human
|
|
homepage: <https://github.com/vladmandic/human>
|
|
author: <https://github.com/vladmandic>'
|
|
*/
|
|
|
|
|
|
// demo/faceid/index.ts
|
|
import * as H from "../../dist/human.esm.js";
|
|
|
|
// demo/faceid/indexdb.ts
|
|
var db;
|
|
var database = "human";
|
|
var table = "person";
|
|
var log = (...msg) => console.log("indexdb", ...msg);
|
|
async function open() {
|
|
if (db)
|
|
return true;
|
|
return new Promise((resolve) => {
|
|
const request = indexedDB.open(database, 1);
|
|
request.onerror = (evt) => log("error:", evt);
|
|
request.onupgradeneeded = (evt) => {
|
|
log("create:", evt.target);
|
|
db = evt.target.result;
|
|
db.createObjectStore(table, { keyPath: "id", autoIncrement: true });
|
|
};
|
|
request.onsuccess = (evt) => {
|
|
db = evt.target.result;
|
|
log("open:", db);
|
|
resolve(true);
|
|
};
|
|
});
|
|
}
|
|
async function load() {
|
|
const faceDB = [];
|
|
if (!db)
|
|
await open();
|
|
return new Promise((resolve) => {
|
|
const cursor = db.transaction([table], "readwrite").objectStore(table).openCursor(null, "next");
|
|
cursor.onerror = (evt) => log("load error:", evt);
|
|
cursor.onsuccess = (evt) => {
|
|
if (evt.target.result) {
|
|
faceDB.push(evt.target.result.value);
|
|
evt.target.result.continue();
|
|
} else {
|
|
resolve(faceDB);
|
|
}
|
|
};
|
|
});
|
|
}
|
|
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();
|
|
const newRecord = { name: faceRecord.name, descriptor: faceRecord.descriptor, image: faceRecord.image };
|
|
db.transaction([table], "readwrite").objectStore(table).put(newRecord);
|
|
log("save:", newRecord);
|
|
}
|
|
async function remove(faceRecord) {
|
|
if (!db)
|
|
await open();
|
|
db.transaction([table], "readwrite").objectStore(table).delete(faceRecord.id);
|
|
log("delete:", faceRecord);
|
|
}
|
|
|
|
// demo/faceid/index.ts
|
|
var humanConfig = {
|
|
cacheSensitivity: 0,
|
|
modelBasePath: "../../models",
|
|
filter: { enabled: true, equalization: true },
|
|
debug: true,
|
|
face: {
|
|
enabled: true,
|
|
detector: { rotation: true, return: true, cropFactor: 1.6, mask: false },
|
|
description: { enabled: true },
|
|
iris: { enabled: true },
|
|
emotion: { enabled: false },
|
|
antispoof: { enabled: true },
|
|
liveness: { enabled: true }
|
|
},
|
|
body: { enabled: false },
|
|
hand: { enabled: false },
|
|
object: { enabled: false },
|
|
gesture: { enabled: true }
|
|
};
|
|
var matchOptions = { order: 2, multiplier: 25, min: 0.2, max: 0.8 };
|
|
var options = {
|
|
minConfidence: 0.6,
|
|
minSize: 224,
|
|
maxTime: 3e4,
|
|
blinkMin: 10,
|
|
blinkMax: 800,
|
|
threshold: 0.5,
|
|
distanceMin: 0.4,
|
|
distanceMax: 1,
|
|
mask: humanConfig.face.detector.mask,
|
|
rotation: humanConfig.face.detector.rotation,
|
|
cropFactor: humanConfig.face.detector.cropFactor,
|
|
...matchOptions
|
|
};
|
|
var ok = {
|
|
faceCount: { status: false, val: 0 },
|
|
faceConfidence: { status: false, val: 0 },
|
|
facingCenter: { status: false, val: 0 },
|
|
lookingCenter: { status: false, val: 0 },
|
|
blinkDetected: { status: false, val: 0 },
|
|
faceSize: { status: false, val: 0 },
|
|
antispoofCheck: { status: false, val: 0 },
|
|
livenessCheck: { status: false, val: 0 },
|
|
distance: { status: false, val: 0 },
|
|
age: { status: false, val: 0 },
|
|
gender: { status: false, val: 0 },
|
|
timeout: { status: true, val: 0 },
|
|
descriptor: { status: false, val: 0 },
|
|
elapsedMs: { status: void 0, val: 0 },
|
|
detectFPS: { status: void 0, val: 0 },
|
|
drawFPS: { status: void 0, val: 0 }
|
|
};
|
|
var allOk = () => ok.faceCount.status && ok.faceSize.status && ok.blinkDetected.status && ok.facingCenter.status && ok.lookingCenter.status && ok.faceConfidence.status && ok.antispoofCheck.status && ok.livenessCheck.status && ok.distance.status && ok.descriptor.status && ok.age.status && ok.gender.status;
|
|
var current = { face: null, record: null };
|
|
var blink = {
|
|
start: 0,
|
|
end: 0,
|
|
time: 0
|
|
};
|
|
var human = new H.Human(humanConfig);
|
|
human.env.perfadd = false;
|
|
human.draw.options.font = 'small-caps 18px "Lato"';
|
|
human.draw.options.lineHeight = 20;
|
|
var dom = {
|
|
video: document.getElementById("video"),
|
|
canvas: document.getElementById("canvas"),
|
|
log: document.getElementById("log"),
|
|
fps: document.getElementById("fps"),
|
|
match: document.getElementById("match"),
|
|
name: document.getElementById("name"),
|
|
save: document.getElementById("save"),
|
|
delete: document.getElementById("delete"),
|
|
retry: document.getElementById("retry"),
|
|
source: document.getElementById("source"),
|
|
ok: document.getElementById("ok")
|
|
};
|
|
var timestamp = { detect: 0, draw: 0 };
|
|
var startTime = 0;
|
|
var log2 = (...msg) => {
|
|
dom.log.innerText += msg.join(" ") + "\n";
|
|
console.log(...msg);
|
|
};
|
|
async function webCam() {
|
|
const cameraOptions = { audio: false, video: { facingMode: "user", resizeMode: "none", width: { ideal: document.body.clientWidth } } };
|
|
const stream = await navigator.mediaDevices.getUserMedia(cameraOptions);
|
|
const ready = new Promise((resolve) => {
|
|
dom.video.onloadeddata = () => resolve(true);
|
|
});
|
|
dom.video.srcObject = stream;
|
|
void dom.video.play();
|
|
await ready;
|
|
dom.canvas.width = dom.video.videoWidth;
|
|
dom.canvas.height = dom.video.videoHeight;
|
|
dom.canvas.style.width = "50%";
|
|
dom.canvas.style.height = "50%";
|
|
if (human.env.initial)
|
|
log2("video:", dom.video.videoWidth, dom.video.videoHeight, "|", stream.getVideoTracks()[0].label);
|
|
dom.canvas.onclick = () => {
|
|
if (dom.video.paused)
|
|
void dom.video.play();
|
|
else
|
|
dom.video.pause();
|
|
};
|
|
}
|
|
async function detectionLoop() {
|
|
var _a;
|
|
if (!dom.video.paused) {
|
|
if ((_a = current.face) == null ? void 0 : _a.tensor)
|
|
human.tf.dispose(current.face.tensor);
|
|
await human.detect(dom.video);
|
|
const now = human.now();
|
|
ok.detectFPS.val = Math.round(1e4 / (now - timestamp.detect)) / 10;
|
|
timestamp.detect = now;
|
|
requestAnimationFrame(detectionLoop);
|
|
}
|
|
}
|
|
function drawValidationTests() {
|
|
let y = 32;
|
|
for (const [key, val] of Object.entries(ok)) {
|
|
let el = document.getElementById(`ok-${key}`);
|
|
if (!el) {
|
|
el = document.createElement("div");
|
|
el.id = `ok-${key}`;
|
|
el.innerText = key;
|
|
el.className = "ok";
|
|
el.style.top = `${y}px`;
|
|
dom.ok.appendChild(el);
|
|
}
|
|
if (typeof val.status === "boolean")
|
|
el.style.backgroundColor = val.status ? "lightgreen" : "lightcoral";
|
|
const status = val.status ? "ok" : "fail";
|
|
el.innerText = `${key}: ${val.val === 0 ? status : val.val}`;
|
|
y += 28;
|
|
}
|
|
}
|
|
async function validationLoop() {
|
|
var _a;
|
|
const interpolated = human.next(human.result);
|
|
human.draw.canvas(dom.video, dom.canvas);
|
|
await human.draw.all(dom.canvas, interpolated);
|
|
const now = human.now();
|
|
ok.drawFPS.val = Math.round(1e4 / (now - timestamp.draw)) / 10;
|
|
timestamp.draw = now;
|
|
ok.faceCount.val = human.result.face.length;
|
|
ok.faceCount.status = ok.faceCount.val === 1;
|
|
if (ok.faceCount.status) {
|
|
const gestures = Object.values(human.result.gesture).map((gesture) => gesture.gesture);
|
|
if (gestures.includes("blink left eye") || gestures.includes("blink right eye"))
|
|
blink.start = human.now();
|
|
if (blink.start > 0 && !gestures.includes("blink left eye") && !gestures.includes("blink right eye"))
|
|
blink.end = human.now();
|
|
ok.blinkDetected.status = ok.blinkDetected.status || Math.abs(blink.end - blink.start) > options.blinkMin && Math.abs(blink.end - blink.start) < options.blinkMax;
|
|
if (ok.blinkDetected.status && blink.time === 0)
|
|
blink.time = Math.trunc(blink.end - blink.start);
|
|
ok.facingCenter.status = gestures.includes("facing center");
|
|
ok.lookingCenter.status = gestures.includes("looking center");
|
|
ok.faceConfidence.val = human.result.face[0].faceScore || human.result.face[0].boxScore || 0;
|
|
ok.faceConfidence.status = ok.faceConfidence.val >= options.minConfidence;
|
|
ok.antispoofCheck.val = human.result.face[0].real || 0;
|
|
ok.antispoofCheck.status = ok.antispoofCheck.val >= options.minConfidence;
|
|
ok.livenessCheck.val = human.result.face[0].live || 0;
|
|
ok.livenessCheck.status = ok.livenessCheck.val >= options.minConfidence;
|
|
ok.faceSize.val = Math.min(human.result.face[0].box[2], human.result.face[0].box[3]);
|
|
ok.faceSize.status = ok.faceSize.val >= options.minSize;
|
|
ok.distance.val = human.result.face[0].distance || 0;
|
|
ok.distance.status = ok.distance.val >= options.distanceMin && ok.distance.val <= options.distanceMax;
|
|
ok.descriptor.val = ((_a = human.result.face[0].embedding) == null ? void 0 : _a.length) || 0;
|
|
ok.descriptor.status = ok.descriptor.val > 0;
|
|
ok.age.val = human.result.face[0].age || 0;
|
|
ok.age.status = ok.age.val > 0;
|
|
ok.gender.val = human.result.face[0].genderScore || 0;
|
|
ok.gender.status = ok.gender.val >= options.minConfidence;
|
|
}
|
|
ok.timeout.status = ok.elapsedMs.val <= options.maxTime;
|
|
drawValidationTests();
|
|
if (allOk() || !ok.timeout.status) {
|
|
dom.video.pause();
|
|
return human.result.face[0];
|
|
}
|
|
ok.elapsedMs.val = Math.trunc(human.now() - startTime);
|
|
return new Promise((resolve) => {
|
|
setTimeout(async () => {
|
|
await validationLoop();
|
|
resolve(human.result.face[0]);
|
|
}, 30);
|
|
});
|
|
}
|
|
async function saveRecords() {
|
|
var _a, _b, _c, _d;
|
|
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: (_b = current.face) == null ? void 0 : _b.embedding, image };
|
|
await save(rec);
|
|
log2("saved face record:", rec.name, "descriptor length:", (_d = (_c = current.face) == null ? void 0 : _c.embedding) == null ? void 0 : _d.length);
|
|
log2("known face records:", await count());
|
|
} else {
|
|
log2("invalid name");
|
|
}
|
|
}
|
|
async function deleteRecord() {
|
|
if (current.record && current.record.id > 0) {
|
|
await remove(current.record);
|
|
}
|
|
}
|
|
async function detectFace() {
|
|
var _a, _b, _c, _d;
|
|
dom.canvas.style.height = "";
|
|
(_a = dom.canvas.getContext("2d")) == null ? void 0 : _a.clearRect(0, 0, options.minSize, options.minSize);
|
|
if (!((_b = current == null ? void 0 : current.face) == null ? void 0 : _b.tensor) || !((_c = current == null ? void 0 : current.face) == null ? void 0 : _c.embedding))
|
|
return false;
|
|
console.log("face record:", current.face);
|
|
log2(`detected face: ${current.face.gender} ${current.face.age || 0}y distance ${100 * (current.face.distance || 0)}cm/${Math.round(100 * (current.face.distance || 0) / 2.54)}in`);
|
|
await human.tf.browser.toPixels(current.face.tensor, dom.canvas);
|
|
if (await count() === 0) {
|
|
log2("face database is empty: nothing to compare face with");
|
|
document.body.style.background = "black";
|
|
dom.delete.style.display = "none";
|
|
return false;
|
|
}
|
|
const db2 = await load();
|
|
const descriptors = db2.map((rec) => rec.descriptor).filter((desc) => desc.length > 0);
|
|
const res = human.match.find(current.face.embedding, descriptors, matchOptions);
|
|
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 = "";
|
|
(_d = dom.source.getContext("2d")) == null ? void 0 : _d.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, _c, _d;
|
|
ok.faceCount.status = false;
|
|
ok.faceConfidence.status = false;
|
|
ok.facingCenter.status = false;
|
|
ok.blinkDetected.status = false;
|
|
ok.faceSize.status = false;
|
|
ok.antispoofCheck.status = false;
|
|
ok.livenessCheck.status = false;
|
|
ok.age.status = false;
|
|
ok.gender.status = false;
|
|
ok.elapsedMs.val = 0;
|
|
dom.match.style.display = "none";
|
|
dom.retry.style.display = "none";
|
|
dom.source.style.display = "none";
|
|
dom.canvas.style.height = "50%";
|
|
document.body.style.background = "black";
|
|
await webCam();
|
|
await detectionLoop();
|
|
startTime = human.now();
|
|
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;
|
|
}
|
|
return detectFace();
|
|
}
|
|
async function init() {
|
|
var _a, _b;
|
|
log2("human version:", human.version, "| tfjs version:", human.tf.version["tfjs-core"]);
|
|
log2("options:", JSON.stringify(options).replace(/{|}|"|\[|\]/g, "").replace(/,/g, " "));
|
|
log2("initializing webcam...");
|
|
await webCam();
|
|
log2("loading human models...");
|
|
await human.load();
|
|
log2("initializing human...");
|
|
log2("face embedding model:", humanConfig.face.description.enabled ? "faceres" : "", ((_a = humanConfig.face["mobilefacenet"]) == null ? void 0 : _a.enabled) ? "mobilefacenet" : "", ((_b = humanConfig.face["insightface"]) == null ? void 0 : _b.enabled) ? "insightface" : "");
|
|
log2("loading face database...");
|
|
log2("known face records:", await count());
|
|
dom.retry.addEventListener("click", main);
|
|
dom.save.addEventListener("click", saveRecords);
|
|
dom.delete.addEventListener("click", deleteRecord);
|
|
await human.warmup();
|
|
await main();
|
|
}
|
|
window.onload = init;
|
|
//# sourceMappingURL=index.js.map
|