improve box rescaling for all modules

pull/356/head
Vladimir Mandic 2021-11-03 16:32:07 -04:00
parent cd1c8fd003
commit 3a0436bc54
9 changed files with 111 additions and 98 deletions

View File

@ -9,11 +9,9 @@
## Changelog ## Changelog
### **HEAD -> main** 2021/10/31 mandic00@live.com ### **HEAD -> main** 2021/11/02 mandic00@live.com
### **origin/main** 2021/10/31 mandic00@live.com
- refactor predict with execute
- patch tfjs type defs - patch tfjs type defs
- start 2.5 major version - start 2.5 major version
- build and docs cleanup - build and docs cleanup

View File

@ -9,12 +9,7 @@ import Human from "../../dist/human.esm.js";
var config = { var config = {
modelBasePath: "../../models", modelBasePath: "../../models",
backend: "humangl", backend: "humangl",
async: true, async: true
face: { enabled: true },
body: { enabled: true },
hand: { enabled: true },
object: { enabled: false },
gesture: { enabled: true }
}; };
var human = new Human(config); var human = new Human(config);
human.env.perfadd = false; human.env.perfadd = false;
@ -39,7 +34,7 @@ var perf = (msg) => {
}; };
async function webCam() { async function webCam() {
status("starting webcam..."); status("starting webcam...");
const options = { audio: false, video: { facingMode: "user", resizeMode: "none", width: { ideal: document.body.clientWidth } } }; const options = { audio: false, video: { facingMode: "user", resizeMode: "crop-and-scale", width: { ideal: document.body.clientWidth } } };
const stream = await navigator.mediaDevices.getUserMedia(options); const stream = await navigator.mediaDevices.getUserMedia(options);
const ready = new Promise((resolve) => { const ready = new Promise((resolve) => {
dom.video.onloadeddata = () => resolve(true); dom.video.onloadeddata = () => resolve(true);

View File

@ -15,11 +15,11 @@ const config = {
modelBasePath: '../../models', modelBasePath: '../../models',
backend: 'humangl', backend: 'humangl',
async: true, async: true,
face: { enabled: true }, // face: { enabled: true, detector: { rotation: true }, iris: { enabled: false }, description: { enabled: false }, emotion: { enabled: false } },
body: { enabled: true }, // body: { enabled: false },
hand: { enabled: true }, // hand: { enabled: false },
object: { enabled: false }, // object: { enabled: false },
gesture: { enabled: true }, // gesture: { enabled: true },
}; };
const human = new Human(config); const human = new Human(config);
@ -50,7 +50,7 @@ const perf = (msg) => {
async function webCam() { async function webCam() {
status('starting webcam...'); status('starting webcam...');
const options = { audio: false, video: { facingMode: 'user', resizeMode: 'none', width: { ideal: document.body.clientWidth } } }; const options = { audio: false, video: { facingMode: 'user', resizeMode: 'crop-and-scale', width: { ideal: document.body.clientWidth } } };
const stream: MediaStream = await navigator.mediaDevices.getUserMedia(options); const stream: MediaStream = await navigator.mediaDevices.getUserMedia(options);
const ready = new Promise((resolve) => { dom.video.onloadeddata = () => resolve(true); }); const ready = new Promise((resolve) => { dom.video.onloadeddata = () => resolve(true); });
dom.video.srcObject = stream; dom.video.srcObject = stream;

View File

@ -18,48 +18,45 @@ import type { FaceResult, Point } from '../result';
import type { Config } from '../config'; import type { Config } from '../config';
import { env } from '../util/env'; import { env } from '../util/env';
type BoxCache = { startPoint: Point, endPoint: Point, landmarks: Array<Point>, confidence: number, faceConfidence?: number | undefined }; type BoxCache = { startPoint: Point, endPoint: Point, landmarks: Array<Point>, confidence: number };
let boxCache: Array<BoxCache> = []; let boxCache: Array<BoxCache> = [];
let model: GraphModel | null = null; let model: GraphModel | null = null;
let inputSize = 0; let inputSize = 0;
let skipped = Number.MAX_SAFE_INTEGER; let skipped = Number.MAX_SAFE_INTEGER;
let lastTime = 0; let lastTime = 0;
let detectedFaces = 0; const enlargeFact = 1.6;
export async function predict(input: Tensor, config: Config): Promise<FaceResult[]> { export async function predict(input: Tensor, config: Config): Promise<FaceResult[]> {
// reset cached boxes // reset cached boxes
const skipTime = (config.face.detector?.skipTime || 0) > (now() - lastTime); const skipTime = (config.face.detector?.skipTime || 0) > (now() - lastTime);
const skipFrame = skipped < (config.face.detector?.skipFrames || 0); const skipFrame = skipped < (config.face.detector?.skipFrames || 0);
if (!config.skipAllowed || !skipTime || !skipFrame || detectedFaces === 0) { if (!config.skipAllowed || !skipTime || !skipFrame || boxCache.length === 0) {
const newBoxes = await blazeface.getBoxes(input, config); // get results from blazeface detector const possibleBoxes = await blazeface.getBoxes(input, config); // get results from blazeface detector
lastTime = now(); lastTime = now();
boxCache = []; // empty cache boxCache = []; // empty cache
for (const possible of newBoxes.boxes) { // extract data from detector for (const possible of possibleBoxes.boxes) { // extract data from detector
const startPoint = await possible.box.startPoint.data() as unknown as Point; const box: BoxCache = {
const endPoint = await possible.box.endPoint.data() as unknown as Point; startPoint: await possible.box.startPoint.data() as unknown as Point,
const landmarks = await possible.landmarks.array() as Array<Point>; endPoint: await possible.box.endPoint.data() as unknown as Point,
boxCache.push({ startPoint, endPoint, landmarks, confidence: possible.confidence }); landmarks: await possible.landmarks.array() as Array<Point>,
} confidence: possible.confidence,
newBoxes.boxes.forEach((prediction) => tf.dispose([prediction.box.startPoint, prediction.box.endPoint, prediction.landmarks])); };
for (let i = 0; i < boxCache.length; i++) { // enlarge and squarify detected boxes boxCache.push(util.squarifyBox(util.enlargeBox(util.scaleBoxCoordinates(box, possibleBoxes.scaleFactor), Math.sqrt(enlargeFact))));
const scaledBox = util.scaleBoxCoordinates({ startPoint: boxCache[i].startPoint, endPoint: boxCache[i].endPoint }, newBoxes.scaleFactor);
const enlargedBox = util.enlargeBox(scaledBox);
const squarifiedBox = util.squarifyBox(enlargedBox);
boxCache[i] = { ...squarifiedBox, confidence: boxCache[i].confidence, landmarks: boxCache[i].landmarks };
} }
possibleBoxes.boxes.forEach((prediction) => tf.dispose([prediction.box.startPoint, prediction.box.endPoint, prediction.landmarks]));
skipped = 0; skipped = 0;
} else { } else {
skipped++; skipped++;
} }
const faces: Array<FaceResult> = []; const faces: Array<FaceResult> = [];
const newBoxes: Array<BoxCache> = []; const newCache: Array<BoxCache> = [];
let id = 0; let id = 0;
for (let box of boxCache) { for (let i = 0; i < boxCache.length; i++) {
let box = boxCache[i];
let angle = 0; let angle = 0;
let rotationMatrix; let rotationMatrix;
const face: FaceResult = { const face: FaceResult = { // init face result
id: id++, id: id++,
mesh: [], mesh: [],
meshRaw: [], meshRaw: [],
@ -74,16 +71,15 @@ export async function predict(input: Tensor, config: Config): Promise<FaceResult
if (config.face.detector?.rotation && config.face.mesh?.enabled && env.kernels.includes('rotatewithoffset')) { if (config.face.detector?.rotation && config.face.mesh?.enabled && env.kernels.includes('rotatewithoffset')) {
[angle, rotationMatrix, face.tensor] = util.correctFaceRotation(box, input, inputSize); [angle, rotationMatrix, face.tensor] = util.correctFaceRotation(box, input, inputSize);
} else { } else {
rotationMatrix = util.IDENTITY_MATRIX; rotationMatrix = util.fixedRotationMatrix;
const cut = util.cutBoxFromImageAndResize({ startPoint: box.startPoint, endPoint: box.endPoint }, input, config.face.mesh?.enabled ? [inputSize, inputSize] : [blazeface.size(), blazeface.size()]); face.tensor = util.cutBoxFromImageAndResize(box, input, config.face.mesh?.enabled ? [inputSize, inputSize] : [blazeface.size(), blazeface.size()]);
face.tensor = tf.div(cut, 255);
tf.dispose(cut);
} }
face.boxScore = Math.round(100 * box.confidence) / 100; face.boxScore = Math.round(100 * box.confidence) / 100;
if (!config.face.mesh?.enabled) { // mesh not enabled, return resuts from detector only if (!config.face.mesh?.enabled) { // mesh not enabled, return resuts from detector only
face.box = util.getClampedBox(box, input); face.box = util.getClampedBox(box, input);
face.boxRaw = util.getRawBox(box, input); face.boxRaw = util.getRawBox(box, input);
face.score = Math.round(100 * box.confidence || 0) / 100; face.boxScore = Math.round(100 * box.confidence || 0) / 100;
face.score = face.boxScore;
face.mesh = box.landmarks.map((pt) => [ face.mesh = box.landmarks.map((pt) => [
((box.startPoint[0] + box.endPoint[0])) / 2 + ((box.endPoint[0] + box.startPoint[0]) * pt[0] / blazeface.size()), ((box.startPoint[0] + box.endPoint[0])) / 2 + ((box.endPoint[0] + box.startPoint[0]) * pt[0] / blazeface.size()),
((box.startPoint[1] + box.endPoint[1])) / 2 + ((box.endPoint[1] + box.startPoint[1]) * pt[1] / blazeface.size()), ((box.startPoint[1] + box.endPoint[1])) / 2 + ((box.endPoint[1] + box.startPoint[1]) * pt[1] / blazeface.size()),
@ -94,37 +90,36 @@ export async function predict(input: Tensor, config: Config): Promise<FaceResult
if (config.debug) log('face mesh detection requested, but model is not loaded'); if (config.debug) log('face mesh detection requested, but model is not loaded');
} else { // mesh enabled } else { // mesh enabled
const [contours, confidence, contourCoords] = model.execute(face.tensor as Tensor) as Array<Tensor>; // first returned tensor represents facial contours which are already included in the coordinates. const [contours, confidence, contourCoords] = model.execute(face.tensor as Tensor) as Array<Tensor>; // first returned tensor represents facial contours which are already included in the coordinates.
tf.dispose(contours); const faceConfidence = await confidence.data();
const faceConfidence = (await confidence.data())[0] as number; face.faceScore = Math.round(100 * faceConfidence[0]) / 100;
tf.dispose(confidence);
const coordsReshaped = tf.reshape(contourCoords, [-1, 3]); const coordsReshaped = tf.reshape(contourCoords, [-1, 3]);
let rawCoords = await coordsReshaped.array(); let rawCoords = await coordsReshaped.array();
tf.dispose(contourCoords); tf.dispose([contourCoords, coordsReshaped, confidence, contours]);
tf.dispose(coordsReshaped); if (face.faceScore < (config.face.detector?.minConfidence || 1)) { // low confidence in detected mesh
if (faceConfidence < (config.face.detector?.minConfidence || 1)) { box.confidence = face.faceScore; // reset confidence of cached box
box.confidence = faceConfidence; // reset confidence of cached box
} else { } else {
if (config.face.iris?.enabled) rawCoords = await iris.augmentIris(rawCoords, face.tensor, config, inputSize); // augment results with iris if (config.face.iris?.enabled) rawCoords = await iris.augmentIris(rawCoords, face.tensor, config, inputSize); // augment results with iris
face.mesh = util.transformRawCoords(rawCoords, box, angle, rotationMatrix, inputSize); // get processed mesh face.mesh = util.transformRawCoords(rawCoords, box, angle, rotationMatrix, inputSize); // get processed mesh
face.meshRaw = face.mesh.map((pt) => [pt[0] / (input.shape[2] || 0), pt[1] / (input.shape[1] || 0), (pt[2] || 0) / inputSize]); face.meshRaw = face.mesh.map((pt) => [pt[0] / (input.shape[2] || 0), pt[1] / (input.shape[1] || 0), (pt[2] || 0) / inputSize]);
box = { ...util.enlargeBox(util.calculateLandmarksBoundingBox(face.mesh), 1.5), confidence: box.confidence }; // redefine box with mesh calculated one
for (const key of Object.keys(coords.meshAnnotations)) face.annotations[key] = coords.meshAnnotations[key].map((index) => face.mesh[index]); // add annotations for (const key of Object.keys(coords.meshAnnotations)) face.annotations[key] = coords.meshAnnotations[key].map((index) => face.mesh[index]); // add annotations
if (config.face.detector?.rotation && config.face.mesh.enabled && config.face.description?.enabled && env.kernels.includes('rotatewithoffset')) { // do rotation one more time with mesh keypoints if we want to return perfect image box = util.squarifyBox(util.enlargeBox(util.calculateLandmarksBoundingBox(face.mesh), enlargeFact)); // redefine box with mesh calculated one
tf.dispose(face.tensor); // dispose so we can overwrite original face
[angle, rotationMatrix, face.tensor] = util.correctFaceRotation(box, input, inputSize);
}
face.box = util.getClampedBox(box, input); // update detected box with box around the face mesh face.box = util.getClampedBox(box, input); // update detected box with box around the face mesh
face.boxRaw = util.getRawBox(box, input); face.boxRaw = util.getRawBox(box, input);
face.score = Math.round(100 * faceConfidence || 100 * box.confidence || 0) / 100; face.score = face.faceScore;
face.faceScore = Math.round(100 * faceConfidence) / 100; newCache.push(box);
box = { ...util.squarifyBox(box), confidence: box.confidence, faceConfidence }; // updated stored cache values
// other modules prefer wider crop for a face so we dispose it and do it again
/*
tf.dispose(face.tensor);
face.tensor = config.face.detector?.rotation && config.face.mesh?.enabled && env.kernels.includes('rotatewithoffset')
? face.tensor = util.correctFaceRotation(util.enlargeBox(box, Math.sqrt(enlargeFact)), input, inputSize)[2]
: face.tensor = util.cutBoxFromImageAndResize(util.enlargeBox(box, Math.sqrt(enlargeFact)), input, [inputSize, inputSize]);
*/
} }
} }
faces.push(face); faces.push(face);
newBoxes.push(box);
} }
if (config.face.mesh?.enabled) boxCache = newBoxes.filter((a) => a.confidence > (config.face.detector?.minConfidence || 0)); // remove cache entries for detected boxes on low confidence boxCache = [...newCache]; // reset cache
detectedFaces = faces.length;
return faces; return faces;
} }

View File

@ -32,36 +32,39 @@ export const getRawBox = (box, input): Box => (box ? [
export const scaleBoxCoordinates = (box, factor) => { export const scaleBoxCoordinates = (box, factor) => {
const startPoint = [box.startPoint[0] * factor[0], box.startPoint[1] * factor[1]]; const startPoint = [box.startPoint[0] * factor[0], box.startPoint[1] * factor[1]];
const endPoint = [box.endPoint[0] * factor[0], box.endPoint[1] * factor[1]]; const endPoint = [box.endPoint[0] * factor[0], box.endPoint[1] * factor[1]];
return { startPoint, endPoint }; return { startPoint, endPoint, landmarks: box.landmarks, confidence: box.confidence };
}; };
export const cutBoxFromImageAndResize = (box, image, cropSize) => { export const cutBoxFromImageAndResize = (box, image, cropSize) => {
const h = image.shape[1]; const h = image.shape[1];
const w = image.shape[2]; const w = image.shape[2];
return tf.image.cropAndResize(image, [[box.startPoint[1] / h, box.startPoint[0] / w, box.endPoint[1] / h, box.endPoint[0] / w]], [0], cropSize); const crop = tf.image.cropAndResize(image, [[box.startPoint[1] / h, box.startPoint[0] / w, box.endPoint[1] / h, box.endPoint[0] / w]], [0], cropSize);
const norm = tf.div(crop, 255);
tf.dispose(crop);
return norm;
}; };
export const enlargeBox = (box, factor = 1.5) => { export const enlargeBox = (box, factor) => {
const center = getBoxCenter(box); const center = getBoxCenter(box);
const size = getBoxSize(box); const size = getBoxSize(box);
const halfSize: [number, number] = [factor * size[0] / 2, factor * size[1] / 2]; const halfSize: [number, number] = [factor * size[0] / 2, factor * size[1] / 2];
return { startPoint: [center[0] - halfSize[0], center[1] - halfSize[1]] as Point, endPoint: [center[0] + halfSize[0], center[1] + halfSize[1]] as Point, landmarks: box.landmarks }; return { startPoint: [center[0] - halfSize[0], center[1] - halfSize[1]] as Point, endPoint: [center[0] + halfSize[0], center[1] + halfSize[1]] as Point, landmarks: box.landmarks, confidence: box.confidence };
}; };
export const squarifyBox = (box) => { export const squarifyBox = (box) => {
const centers = getBoxCenter(box); const centers = getBoxCenter(box);
const size = getBoxSize(box); const size = getBoxSize(box);
const halfSize = Math.max(...size) / 2; const halfSize = Math.max(...size) / 2;
return { startPoint: [Math.round(centers[0] - halfSize), Math.round(centers[1] - halfSize)] as Point, endPoint: [Math.round(centers[0] + halfSize), Math.round(centers[1] + halfSize)] as Point, landmarks: box.landmarks }; return { startPoint: [Math.round(centers[0] - halfSize), Math.round(centers[1] - halfSize)] as Point, endPoint: [Math.round(centers[0] + halfSize), Math.round(centers[1] + halfSize)] as Point, landmarks: box.landmarks, confidence: box.confidence };
}; };
export const calculateLandmarksBoundingBox = (landmarks) => { export const calculateLandmarksBoundingBox = (landmarks) => {
const xs = landmarks.map((d) => d[0]); const xs = landmarks.map((d) => d[0]);
const ys = landmarks.map((d) => d[1]); const ys = landmarks.map((d) => d[1]);
return { startPoint: [Math.min(...xs), Math.min(...ys)], endPoint: [Math.max(...xs), Math.max(...ys)], landmarks }; return { startPoint: [Math.min(...xs), Math.min(...ys)] as Point, endPoint: [Math.max(...xs), Math.max(...ys)] as Point, landmarks };
}; };
export const IDENTITY_MATRIX = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; export const fixedRotationMatrix = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
export const normalizeRadians = (angle) => angle - 2 * Math.PI * Math.floor((angle + Math.PI) / (2 * Math.PI)); export const normalizeRadians = (angle) => angle - 2 * Math.PI * Math.floor((angle + Math.PI) / (2 * Math.PI));
@ -71,7 +74,7 @@ export const radToDegrees = (rad) => rad * 180 / Math.PI;
export const buildTranslationMatrix = (x, y) => [[1, 0, x], [0, 1, y], [0, 0, 1]]; export const buildTranslationMatrix = (x, y) => [[1, 0, x], [0, 1, y], [0, 0, 1]];
export const dot = (v1, v2) => { export const dot = (v1: number[], v2: number[]) => {
let product = 0; let product = 0;
for (let i = 0; i < v1.length; i++) product += v1[i] * v2[i]; for (let i = 0; i < v1.length; i++) product += v1[i] * v2[i];
return product; return product;
@ -133,16 +136,17 @@ export function generateAnchors(inputSize) {
return anchors; return anchors;
} }
export function transformRawCoords(rawCoords, box, angle, rotationMatrix, inputSize) { export function transformRawCoords(coordsRaw, box, angle, rotationMatrix, inputSize) {
const boxSize = getBoxSize({ startPoint: box.startPoint, endPoint: box.endPoint }); const boxSize = getBoxSize(box);
const coordsScaled = rawCoords.map((coord) => ([ const coordsScaled = coordsRaw.map((coord) => ([ // scaled around zero-point
boxSize[0] / inputSize * (coord[0] - inputSize / 2), boxSize[0] / inputSize * (coord[0] - inputSize / 2),
boxSize[1] / inputSize * (coord[1] - inputSize / 2), boxSize[1] / inputSize * (coord[1] - inputSize / 2),
coord[2] || 0, coord[2] || 0,
])); ]));
const coordsRotationMatrix = (angle !== 0) ? buildRotationMatrix(angle, [0, 0]) : IDENTITY_MATRIX; const largeAngle = angle && (angle !== 0) && (Math.abs(angle) > 0.2);
const coordsRotated = (angle !== 0) ? coordsScaled.map((coord) => ([...rotatePoint(coord, coordsRotationMatrix), coord[2]])) : coordsScaled; const coordsRotationMatrix = largeAngle ? buildRotationMatrix(angle, [0, 0]) : fixedRotationMatrix;
const inverseRotationMatrix = (angle !== 0) ? invertTransformMatrix(rotationMatrix) : IDENTITY_MATRIX; const coordsRotated = largeAngle ? coordsScaled.map((coord) => ([...rotatePoint(coord, coordsRotationMatrix), coord[2]])) : coordsScaled;
const inverseRotationMatrix = largeAngle ? invertTransformMatrix(rotationMatrix) : fixedRotationMatrix;
const boxCenter = [...getBoxCenter({ startPoint: box.startPoint, endPoint: box.endPoint }), 1]; const boxCenter = [...getBoxCenter({ startPoint: box.startPoint, endPoint: box.endPoint }), 1];
return coordsRotated.map((coord) => ([ return coordsRotated.map((coord) => ([
Math.round(coord[0] + dot(boxCenter, inverseRotationMatrix[0])), Math.round(coord[0] + dot(boxCenter, inverseRotationMatrix[0])),
@ -154,13 +158,19 @@ export function transformRawCoords(rawCoords, box, angle, rotationMatrix, inputS
export function correctFaceRotation(box, input, inputSize) { export function correctFaceRotation(box, input, inputSize) {
const symmetryLine = (box.landmarks.length >= coords.meshLandmarks.count) ? coords.meshLandmarks.symmetryLine : coords.blazeFaceLandmarks.symmetryLine; const symmetryLine = (box.landmarks.length >= coords.meshLandmarks.count) ? coords.meshLandmarks.symmetryLine : coords.blazeFaceLandmarks.symmetryLine;
const angle: number = computeRotation(box.landmarks[symmetryLine[0]], box.landmarks[symmetryLine[1]]); const angle: number = computeRotation(box.landmarks[symmetryLine[0]], box.landmarks[symmetryLine[1]]);
const largeAngle = angle && (angle !== 0) && (Math.abs(angle) > 0.2);
let rotationMatrix;
let face;
if (largeAngle) {
const faceCenter: Point = getBoxCenter({ startPoint: box.startPoint, endPoint: box.endPoint }); const faceCenter: Point = getBoxCenter({ startPoint: box.startPoint, endPoint: box.endPoint });
const faceCenterNormalized: Point = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]]; const faceCenterNormalized: Point = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]];
const rotated = tf.image.rotateWithOffset(input, angle, 0, faceCenterNormalized); // rotateWithOffset is not defined for tfjs-node const rotated = tf.image.rotateWithOffset(input, angle, 0, faceCenterNormalized); // rotateWithOffset is not defined for tfjs-node
const rotationMatrix = buildRotationMatrix(-angle, faceCenter); rotationMatrix = buildRotationMatrix(-angle, faceCenter);
const cut = cutBoxFromImageAndResize({ startPoint: box.startPoint, endPoint: box.endPoint }, rotated, [inputSize, inputSize]); face = cutBoxFromImageAndResize(box, rotated, [inputSize, inputSize]);
const face = tf.div(cut, 255);
tf.dispose(cut);
tf.dispose(rotated); tf.dispose(rotated);
} else {
rotationMatrix = fixedRotationMatrix;
face = cutBoxFromImageAndResize(box, input, [inputSize, inputSize]);
}
return [angle, rotationMatrix, face]; return [angle, rotationMatrix, face];
} }

View File

@ -43,13 +43,14 @@ export function enhance(input): Tensor {
const tensor = input.image || input.tensor || input; const tensor = input.image || input.tensor || input;
if (!(tensor instanceof tf.Tensor)) return null; if (!(tensor instanceof tf.Tensor)) return null;
// do a tight crop of image and resize it to fit the model // 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 box = [[0.0, 0.0, 1.0, 1.0]]; // basically no crop for test
if (!model?.inputs[0].shape) return null; // model has no shape so no point continuing if (!model?.inputs[0].shape) return null; // model has no shape so no point continuing
const crop = tf.image.resizeBilinear(tensor, [model.inputs[0].shape[2], model.inputs[0].shape[1]], false);
/*
const box = [[0.05, 0.15, 0.85, 0.85]]; // empyrical values for top, left, bottom, right
const crop = (tensor.shape.length === 3) 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(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]]); : 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 // 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); const crop = tf.image.resizeBilinear(tensor, [model.inputs[0].shape[2], model.inputs[0].shape[1]], false);

View File

@ -43,7 +43,11 @@ export async function predict(image: Tensor, config: Config, idx, count) {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
const obj: Array<{ score: number, emotion: string }> = []; const obj: Array<{ score: number, emotion: string }> = [];
if (config.face.emotion?.enabled) { if (config.face.emotion?.enabled) {
const resize = tf.image.resizeBilinear(image, [model?.inputs[0].shape ? model.inputs[0].shape[2] : 0, model?.inputs[0].shape ? model.inputs[0].shape[1] : 0], false); const inputSize = model?.inputs[0].shape ? model.inputs[0].shape[2] : 0;
const resize = tf.image.resizeBilinear(image, [inputSize, inputSize], false);
// const box = [[0.15, 0.15, 0.85, 0.85]]; // empyrical values for top, left, bottom, right
// const resize = tf.image.cropAndResize(image, box, [0], [inputSize, inputSize]);
const [red, green, blue] = tf.split(resize, 3, 3); const [red, green, blue] = tf.split(resize, 3, 3);
tf.dispose(resize); tf.dispose(resize);
// weighted rgb to grayscale: https://www.mathworks.com/help/matlab/ref/rgb2gray.html // weighted rgb to grayscale: https://www.mathworks.com/help/matlab/ref/rgb2gray.html

View File

@ -100,11 +100,14 @@ export const iris = (res): GestureResult[] => {
gestures.push({ iris: i, gesture: 'facing center' }); gestures.push({ iris: i, gesture: 'facing center' });
} }
const rightIrisCenterX = Math.abs(res[i].mesh[33][0] - res[i].annotations.rightEyeIris[0][0]) / res[i].box[2];
const leftIrisCenterX = Math.abs(res[i].mesh[263][0] - res[i].annotations.leftEyeIris[0][0]) / res[i].box[2]; const leftIrisCenterX = Math.abs(res[i].mesh[263][0] - res[i].annotations.leftEyeIris[0][0]) / res[i].box[2];
const rightIrisCenterX = Math.abs(res[i].mesh[33][0] - res[i].annotations.rightEyeIris[0][0]) / res[i].box[2];
if (leftIrisCenterX > 0.06 || rightIrisCenterX > 0.06) center = false; if (leftIrisCenterX > 0.06 || rightIrisCenterX > 0.06) center = false;
if (leftIrisCenterX > rightIrisCenterX) { // check eye with bigger offset
if (leftIrisCenterX > 0.05) gestures.push({ iris: i, gesture: 'looking right' }); if (leftIrisCenterX > 0.05) gestures.push({ iris: i, gesture: 'looking right' });
} else {
if (rightIrisCenterX > 0.05) gestures.push({ iris: i, gesture: 'looking left' }); if (rightIrisCenterX > 0.05) gestures.push({ iris: i, gesture: 'looking left' });
}
const rightIrisCenterY = Math.abs(res[i].mesh[145][1] - res[i].annotations.rightEyeIris[0][1]) / res[i].box[3]; const rightIrisCenterY = Math.abs(res[i].mesh[145][1] - res[i].annotations.rightEyeIris[0][1]) / res[i].box[3];
const leftIrisCenterY = Math.abs(res[i].mesh[374][1] - res[i].annotations.leftEyeIris[0][1]) / res[i].box[3]; const leftIrisCenterY = Math.abs(res[i].mesh[374][1] - res[i].annotations.leftEyeIris[0][1]) / res[i].box[3];

View File

@ -188,12 +188,19 @@ async function test(Human, inputConfig) {
else log('state', 'passed: warmup none result match'); else log('state', 'passed: warmup none result match');
config.warmup = 'face'; config.warmup = 'face';
res = await testWarmup(human, 'default'); res = await testWarmup(human, 'default');
if (!res || res?.face?.length !== 1 || res?.body?.length !== 1 || res?.hand?.length !== 1 || res?.gesture?.length !== 8) log('error', 'failed: warmup face result mismatch', res?.face?.length, res?.body?.length, res?.hand?.length, res?.gesture?.length); if (!res || res?.face?.length !== 1 || res?.body?.length !== 1 || res?.hand?.length !== 1 || res?.gesture?.length !== 7) log('error', 'failed: warmup face result mismatch', res?.face?.length, res?.body?.length, res?.hand?.length, res?.gesture?.length);
else log('state', 'passed: warmup face result match'); else log('state', 'passed: warmup face result match');
config.warmup = 'body'; config.warmup = 'body';
res = await testWarmup(human, 'default'); res = await testWarmup(human, 'default');
if (!res || res?.face?.length !== 1 || res?.body?.length !== 1 || res?.hand?.length !== 1 || res?.gesture?.length !== 7) log('error', 'failed: warmup body result mismatch', res?.face?.length, res?.body?.length, res?.hand?.length, res?.gesture?.length); if (!res || res?.face?.length !== 1 || res?.body?.length !== 1 || res?.hand?.length !== 1 || res?.gesture?.length !== 6) log('error', 'failed: warmup body result mismatch', res?.face?.length, res?.body?.length, res?.hand?.length, res?.gesture?.length);
else log('state', 'passed: warmup body result match'); else log('state', 'passed: warmup body result match');
log('state', 'details:', {
face: { boxScore: res.face[0].boxScore, faceScore: res.face[0].faceScore, age: res.face[0].age, gender: res.face[0].gender, genderScore: res.face[0].genderScore },
emotion: res.face[0].emotion,
body: { score: res.body[0].score, keypoints: res.body[0].keypoints.length },
hand: { boxScore: res.hand[0].boxScore, fingerScore: res.hand[0].fingerScore, keypoints: res.hand[0].keypoints.length },
gestures: res.gesture,
});
// test default config async // test default config async
log('info', 'test default'); log('info', 'test default');
@ -201,8 +208,8 @@ async function test(Human, inputConfig) {
config.async = true; config.async = true;
config.cacheSensitivity = 0; config.cacheSensitivity = 0;
res = await testDetect(human, 'samples/in/ai-body.jpg', 'default'); res = await testDetect(human, 'samples/in/ai-body.jpg', 'default');
if (!res || res?.face?.length !== 1 || res?.face[0].gender !== 'female') log('error', 'failed: default result face mismatch', res?.face?.length, res?.body?.length, res?.hand?.length, res?.gesture?.length); if (!res || res?.face?.length !== 1 || res?.face[0].gender !== 'female') log('error', 'failed: default result face mismatch', res?.face?.length, res?.face[0].gender, res?.face[0].genderScore);
else log('state', 'passed: default result face match'); else log('state', 'passed: default result face match', res?.face?.length, res?.face[0].gender, res?.face[0].genderScore);
// test default config sync // test default config sync
log('info', 'test sync'); log('info', 'test sync');
@ -210,8 +217,8 @@ async function test(Human, inputConfig) {
config.async = false; config.async = false;
config.cacheSensitivity = 0; config.cacheSensitivity = 0;
res = await testDetect(human, 'samples/in/ai-body.jpg', 'default'); res = await testDetect(human, 'samples/in/ai-body.jpg', 'default');
if (!res || res?.face?.length !== 1 || res?.face[0].gender !== 'female') log('error', 'failed: default sync', res?.face?.length, res?.body?.length, res?.hand?.length, res?.gesture?.length); if (!res || res?.face?.length !== 1 || res?.face[0].gender !== 'female') log('error', 'failed: default sync', res?.face?.length, res?.face[0].gender, res?.face[0].genderScore);
else log('state', 'passed: default sync'); else log('state', 'passed: default sync', res?.face?.length, res?.face[0].gender, res?.face[0].genderScore);
// test image processing // test image processing
const img1 = await human.image(null); const img1 = await human.image(null);
@ -240,7 +247,7 @@ async function test(Human, inputConfig) {
res1 = human.similarity(desc1, desc1); res1 = human.similarity(desc1, desc1);
res2 = human.similarity(desc1, desc2); res2 = human.similarity(desc1, desc2);
res3 = human.similarity(desc1, desc3); res3 = human.similarity(desc1, desc3);
if (res1 < 1 || res2 < 0.55 || res3 < 0.5) log('error', 'failed: face similarity', { similarity: [res1, res2, res3], descriptors: [desc1?.length, desc2?.length, desc3?.length] }); if (res1 < 1 || res2 < 0.50 || res3 < 0.50) log('error', 'failed: face similarity', { similarity: [res1, res2, res3], descriptors: [desc1?.length, desc2?.length, desc3?.length] });
else log('state', 'passed: face similarity', { similarity: [res1, res2, res3], descriptors: [desc1?.length, desc2?.length, desc3?.length] }); else log('state', 'passed: face similarity', { similarity: [res1, res2, res3], descriptors: [desc1?.length, desc2?.length, desc3?.length] });
// test face matching // test face matching
@ -271,7 +278,7 @@ async function test(Human, inputConfig) {
config.body = { minConfidence: 0.0001 }; config.body = { minConfidence: 0.0001 };
config.hand = { minConfidence: 0.0001 }; config.hand = { minConfidence: 0.0001 };
res = await testDetect(human, 'samples/in/ai-body.jpg', 'default'); res = await testDetect(human, 'samples/in/ai-body.jpg', 'default');
if (!res || res?.face?.length !== 1 || res?.body?.length !== 1 || res?.hand?.length !== 2 || res?.gesture?.length !== 9) log('error', 'failed: sensitive result mismatch', res?.face?.length, res?.body?.length, res?.hand?.length, res?.gesture?.length); if (!res || res?.face?.length !== 1 || res?.body?.length !== 1 || res?.hand?.length !== 2 || res?.gesture?.length !== 8) log('error', 'failed: sensitive result mismatch', res?.face?.length, res?.body?.length, res?.hand?.length, res?.gesture?.length);
else log('state', 'passed: sensitive result match'); else log('state', 'passed: sensitive result match');
// test sensitive details face // test sensitive details face
@ -280,7 +287,7 @@ async function test(Human, inputConfig) {
log('error', 'failed: sensitive face result mismatch', res?.face?.length, face?.box?.length, face?.mesh?.length, face?.embedding?.length, face?.rotation?.matrix?.length); log('error', 'failed: sensitive face result mismatch', res?.face?.length, face?.box?.length, face?.mesh?.length, face?.embedding?.length, face?.rotation?.matrix?.length);
} else log('state', 'passed: sensitive face result match'); } else log('state', 'passed: sensitive face result match');
if (!face || face?.emotion?.length < 3) log('error', 'failed: sensitive face emotion result mismatch', face?.emotion.length); if (!face || face?.emotion?.length < 3) log('error', 'failed: sensitive face emotion result mismatch', face?.emotion.length);
else log('state', 'passed: sensitive face emotion result mismatch', face?.emotion.length); else log('state', 'passed: sensitive face emotion result', face?.emotion.length);
// test sensitive details body // test sensitive details body
const body = res && res.body ? res.body[0] : null; const body = res && res.body ? res.body[0] : null;