add similarity score range normalization

pull/280/head
Vladimir Mandic 2021-11-11 17:01:10 -05:00
parent 200bccbb43
commit 471896bf5b
6 changed files with 53 additions and 39 deletions

View File

@ -9,8 +9,9 @@
## Changelog ## Changelog
### **HEAD -> main** 2021/11/10 mandic00@live.com ### **HEAD -> main** 2021/11/11 mandic00@live.com
- documentation overhaul
- auto tensor shape and channels handling - auto tensor shape and channels handling
- disable use of path2d in node - disable use of path2d in node
- add liveness module and facerecognition demo - add liveness module and facerecognition demo

View File

@ -18,7 +18,7 @@
html { font-family: 'Lato', 'Segoe UI'; font-size: 16px; font-variant: small-caps; } 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 { margin: 0; padding: 16px; background: black; color: white; overflow-x: hidden; width: 100vw; height: 100vh; }
body::-webkit-scrollbar { display: none; } 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 } .button { padding: 2px; cursor: pointer; box-shadow: 2px 2px black; width: 64px; text-align: center; margin-left: 16px; height: 16px; display: none }
</style> </style>
</head> </head>
<body> <body>

View File

@ -225,19 +225,13 @@ async function deleteRecord() {
} }
} }
async function detectFace() { async function detectFace() {
var _a; 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) if (!face || !face.tensor || !face.embedding)
return 0; return 0;
dom.canvas.width = face.tensor.shape[1] || 0;
dom.canvas.height = face.tensor.shape[0] || 0;
dom.source.width = dom.canvas.width;
dom.source.height = dom.canvas.height;
dom.canvas.style.width = "";
human.tf.browser.toPixels(face.tensor, dom.canvas); human.tf.browser.toPixels(face.tensor, dom.canvas);
const descriptors = db2.map((rec) => rec.descriptor); const descriptors = db2.map((rec) => rec.descriptor);
const res = await human.match(face.embedding, descriptors); const res = await human.match(face.embedding, descriptors);
dom.match.style.display = "flex";
dom.retry.style.display = "block";
if (res.index === -1) { if (res.index === -1) {
log2("no matches"); log2("no matches");
dom.delete.style.display = "none"; dom.delete.style.display = "none";
@ -248,11 +242,12 @@ async function detectFace() {
dom.delete.style.display = ""; dom.delete.style.display = "";
dom.name.value = current.name; dom.name.value = current.name;
dom.source.style.display = ""; dom.source.style.display = "";
(_a = dom.source.getContext("2d")) == null ? void 0 : _a.putImageData(current.image, 0, 0); (_b = dom.source.getContext("2d")) == null ? void 0 : _b.putImageData(current.image, 0, 0);
} }
return res.similarity > options.threshold; return res.similarity > options.threshold;
} }
async function main() { async function main() {
var _a, _b;
ok.faceCount = false; ok.faceCount = false;
ok.faceConfidence = false; ok.faceConfidence = false;
ok.facingCenter = false; ok.facingCenter = false;
@ -269,9 +264,16 @@ async function main() {
startTime = human.now(); startTime = human.now();
face = await validationLoop(); face = await validationLoop();
dom.fps.style.display = "none"; 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;
dom.source.width = dom.canvas.width;
dom.source.height = dom.canvas.height;
dom.canvas.style.width = "";
dom.match.style.display = "flex";
dom.retry.style.display = "block";
if (!allOk()) { if (!allOk()) {
log2("did not find valid input", face); log2("did not find valid face");
return 0; return false;
} else { } else {
const res = await detectFace(); const res = await detectFace();
document.body.style.background = res ? "darkgreen" : "maroon"; document.body.style.background = res ? "darkgreen" : "maroon";

View File

@ -179,18 +179,11 @@ async function deleteRecord() {
} }
async function detectFace() { async function detectFace() {
// draw face and dispose face tensor immediatey afterwards dom.canvas.getContext('2d')?.clearRect(0, 0, options.minSize, options.minSize);
if (!face || !face.tensor || !face.embedding) return 0; if (!face || !face.tensor || !face.embedding) return 0;
dom.canvas.width = face.tensor.shape[1] || 0;
dom.canvas.height = face.tensor.shape[0] || 0;
dom.source.width = dom.canvas.width;
dom.source.height = dom.canvas.height;
dom.canvas.style.width = '';
human.tf.browser.toPixels(face.tensor as unknown as TensorLike, dom.canvas); human.tf.browser.toPixels(face.tensor as unknown as TensorLike, dom.canvas);
const descriptors = db.map((rec) => rec.descriptor); const descriptors = db.map((rec) => rec.descriptor);
const res = await human.match(face.embedding, descriptors); const res = await human.match(face.embedding, descriptors);
dom.match.style.display = 'flex';
dom.retry.style.display = 'block';
if (res.index === -1) { if (res.index === -1) {
log('no matches'); log('no matches');
dom.delete.style.display = 'none'; dom.delete.style.display = 'none';
@ -223,9 +216,16 @@ async function main() { // main entry point
startTime = human.now(); startTime = human.now();
face = await validationLoop(); // start validation loop face = await validationLoop(); // start validation loop
dom.fps.style.display = 'none'; dom.fps.style.display = 'none';
dom.canvas.width = face?.tensor?.shape[1] || options.minSize;
dom.canvas.height = 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.retry.style.display = 'block';
if (!allOk()) { if (!allOk()) {
log('did not find valid input', face); log('did not find valid face');
return 0; return false;
} else { } else {
// log('found valid face'); // log('found valid face');
const res = await detectFace(); const res = await detectFace();

View File

@ -184,9 +184,8 @@ async function AddImageElement(index, image, length) {
return new Promise((resolve) => { return new Promise((resolve) => {
const img = new Image(128, 128); const img = new Image(128, 128);
img.onload = () => { // must wait until image is loaded img.onload = () => { // must wait until image is loaded
human.detect(img, userConfig).then(async (res) => { human.detect(img, userConfig).then((res) => {
const ok = AddFaceCanvas(index, res, image); // then wait until image is analyzed const ok = AddFaceCanvas(index, res, image); // then wait until image is analyzed
// log('Add image:', index + 1, image, 'faces:', res.face.length);
if (ok) document.getElementById('images').appendChild(img); // and finally we can add it if (ok) document.getElementById('images').appendChild(img); // and finally we can add it
resolve(true); resolve(true);
}); });
@ -250,11 +249,13 @@ async function main() {
// const promises = []; // const promises = [];
// for (let i = 0; i < images.length; i++) promises.push(AddImageElement(i, images[i], images.length)); // for (let i = 0; i < images.length; i++) promises.push(AddImageElement(i, images[i], images.length));
// await Promise.all(promises); // await Promise.all(promises);
const t0 = human.now();
for (let i = 0; i < images.length; i++) await AddImageElement(i, images[i], images.length); for (let i = 0; i < images.length; i++) await AddImageElement(i, images[i], images.length);
const t1 = human.now();
// print stats // print stats
const num = all.reduce((prev, cur) => prev += cur.length, 0); const num = all.reduce((prev, cur) => prev += cur.length, 0);
log('Extracted faces:', num, 'from images:', all.length); log('Extracted faces:', num, 'from images:', all.length, 'time:', Math.round(t1 - t0));
log(human.tf.engine().memory()); log(human.tf.engine().memory());
// if we didn't download db, generate it from current faces // if we didn't download db, generate it from current faces

View File

@ -1,6 +1,6 @@
/** Face descriptor type as number array */ /** Face descriptor type as number array */
export type Descriptor = Array<number> export type Descriptor = Array<number>
export type Options = { order?: number, threshold?: number, multiplier?: number } | undefined; export type MatchOptions = { order?: number, threshold?: number, multiplier?: number, min?: number, max?: number } | undefined;
/** Calculates distance between two descriptors /** Calculates distance between two descriptors
* @param {object} options * @param {object} options
@ -10,7 +10,7 @@ export type Options = { order?: number, threshold?: number, multiplier?: number
* - default is 20 which normalizes results to similarity above 0.5 can be considered a match * - default is 20 which normalizes results to similarity above 0.5 can be considered a match
* @returns {number} * @returns {number}
*/ */
export function distance(descriptor1: Descriptor, descriptor2: Descriptor, options: Options = { order: 2, multiplier: 20 }) { export function distance(descriptor1: Descriptor, descriptor2: Descriptor, options: MatchOptions = { order: 2, multiplier: 25 }) {
// general minkowski distance, euclidean distance is limited case where order is 2 // general minkowski distance, euclidean distance is limited case where order is 2
let sum = 0; let sum = 0;
for (let i = 0; i < descriptor1.length; i++) { for (let i = 0; i < descriptor1.length; i++) {
@ -20,19 +20,29 @@ export function distance(descriptor1: Descriptor, descriptor2: Descriptor, optio
return (options.multiplier || 20) * sum; return (options.multiplier || 20) * sum;
} }
// invert distance to similarity, normalize to given range and clamp
const normalizeDistance = (dist, order, min, max) => {
if (dist === 0) return 1; // short circuit for identical inputs
const root = order === 2 ? Math.sqrt(dist) : dist ** (1 / order); // take root of distance
const norm = (1 - (root / 100) - min) / (max - min); // normalize to range
const clamp = Math.max(Math.min(norm, 1), 0); // clamp to 0..1
return clamp;
};
/** Calculates normalized similarity between two face descriptors based on their `distance` /** Calculates normalized similarity between two face descriptors based on their `distance`
* @param {object} options * @param {object} options
* @param {number} options.order algorithm to use * @param {number} options.order algorithm to use
* - Euclidean distance if `order` is 2 (default), Minkowski distance algorithm of nth order if `order` is higher than 2 * - Euclidean distance if `order` is 2 (default), Minkowski distance algorithm of nth order if `order` is higher than 2
* @param {number} options.multiplier by how much to enhance difference analysis in range of 1..100 * @param {number} options.multiplier by how much to enhance difference analysis in range of 1..100
* - default is 20 which normalizes results to similarity above 0.5 can be considered a match * - default is 20 which normalizes results to similarity above 0.5 can be considered a match
* @param {number} options.min normalize similarity result to a given range
* @param {number} options.max normalzie similarity resutl to a given range
* - default is 0.2...0.8
* @returns {number} similarity between two face descriptors normalized to 0..1 range where 0 is no similarity and 1 is perfect similarity * @returns {number} similarity between two face descriptors normalized to 0..1 range where 0 is no similarity and 1 is perfect similarity
*/ */
export function similarity(descriptor1: Descriptor, descriptor2: Descriptor, options: Options = { order: 2, multiplier: 20 }) { export function similarity(descriptor1: Descriptor, descriptor2: Descriptor, options: MatchOptions = { order: 2, multiplier: 25, min: 0.2, max: 0.8 }) {
const dist = distance(descriptor1, descriptor2, options); const dist = distance(descriptor1, descriptor2, options);
const root = (!options.order || options.order === 2) ? Math.sqrt(dist) : dist ** (1 / options.order); return normalizeDistance(dist, options.order || 2, options.min || 0, options.max || 1);
const invert = Math.max(0, 100 - root) / 100.0;
return invert;
} }
/** Matches given descriptor to a closest entry in array of descriptors /** Matches given descriptor to a closest entry in array of descriptors
@ -46,20 +56,20 @@ export function similarity(descriptor1: Descriptor, descriptor2: Descriptor, opt
* - {@link distance} calculated `distance` of given descriptor to the best match * - {@link distance} calculated `distance` of given descriptor to the best match
* - {@link similarity} calculated normalized `similarity` of given descriptor to the best match * - {@link similarity} calculated normalized `similarity` of given descriptor to the best match
*/ */
export function match(descriptor: Descriptor, descriptors: Array<Descriptor>, options: Options = { order: 2, multiplier: 20, threshold: 0 }) { export function match(descriptor: Descriptor, descriptors: Array<Descriptor>, options: MatchOptions = { order: 2, multiplier: 25, threshold: 0, min: 0.2, max: 0.8 }) {
if (!Array.isArray(descriptor) || !Array.isArray(descriptors) || descriptor.length < 64 || descriptors.length === 0 || descriptor.length !== descriptors[0].length) { // validate input if (!Array.isArray(descriptor) || !Array.isArray(descriptors) || descriptor.length < 64 || descriptors.length === 0 || descriptor.length !== descriptors[0].length) { // validate input
return { index: -1, distance: Number.POSITIVE_INFINITY, similarity: 0 }; return { index: -1, distance: Number.POSITIVE_INFINITY, similarity: 0 };
} }
let best = Number.MAX_SAFE_INTEGER; let lowestDistance = Number.MAX_SAFE_INTEGER;
let index = -1; let index = -1;
for (let i = 0; i < descriptors.length; i++) { for (let i = 0; i < descriptors.length; i++) {
const res = distance(descriptor, descriptors[i], options); const res = distance(descriptor, descriptors[i], options);
if (res < best) { if (res < lowestDistance) {
best = res; lowestDistance = res;
index = i; index = i;
} }
if (best < (options.threshold || 0)) break; if (lowestDistance < (options.threshold || 0)) break;
} }
best = (!options.order || options.order === 2) ? Math.sqrt(best) : best ** (1 / options.order); const normalizedSimilarity = normalizeDistance(lowestDistance, options.order || 2, options.min || 0, options.max || 1);
return { index, distance: best, similarity: Math.max(0, 100 - best) / 100.0 }; return { index, distance: lowestDistance, similarity: normalizedSimilarity };
} }