mirror of https://github.com/vladmandic/human
update
parent
69a080e64b
commit
e41664dd18
|
@ -9,11 +9,14 @@
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### **HEAD -> main** 2021/12/28 mandic00@live.com
|
||||||
|
|
||||||
|
- fix samples
|
||||||
|
- fix(src): typo
|
||||||
|
- change on how face box is calculated
|
||||||
|
|
||||||
### **2.5.7** 2021/12/27 mandic00@live.com
|
### **2.5.7** 2021/12/27 mandic00@live.com
|
||||||
|
|
||||||
|
|
||||||
### **origin/main** 2021/12/22 mandic00@live.com
|
|
||||||
|
|
||||||
- fix posenet
|
- fix posenet
|
||||||
|
|
||||||
### **release: 2.5.6** 2021/12/15 mandic00@live.com
|
### **release: 2.5.6** 2021/12/15 mandic00@live.com
|
||||||
|
|
|
@ -5,17 +5,21 @@
|
||||||
export interface GenericConfig {
|
export interface GenericConfig {
|
||||||
/** is module enabled? */
|
/** is module enabled? */
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
/** path to model json file */
|
/** path to model json file (relative to `modelBasePath` */
|
||||||
modelPath: string,
|
modelPath: string,
|
||||||
/** how many max frames to go without re-running model if cached results are acceptable */
|
/** how many max frames to go without re-running model if cached results are acceptable
|
||||||
|
* for two-phase models such as face and hand caching applies to bounding boxes detection only */
|
||||||
skipFrames: number,
|
skipFrames: number,
|
||||||
/** how many max milliseconds to go without re-running model if cached results are acceptable */
|
/** how many max milliseconds to go without re-running model if cached results are acceptable
|
||||||
|
* for two-phase models such as face and hand caching applies to bounding boxes detection only */
|
||||||
skipTime: number,
|
skipTime: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Detector part of face configuration */
|
/** Detector part of face configuration */
|
||||||
export interface FaceDetectorConfig extends GenericConfig {
|
export interface FaceDetectorConfig extends GenericConfig {
|
||||||
/** is face rotation correction performed after detecting face? */
|
/** is face rotation correction performed after detecting face?
|
||||||
|
* used to correctly analyze faces under high angles
|
||||||
|
*/
|
||||||
rotation: boolean,
|
rotation: boolean,
|
||||||
/** maximum number of detected faces */
|
/** maximum number of detected faces */
|
||||||
maxDetected: number,
|
maxDetected: number,
|
||||||
|
@ -25,7 +29,8 @@ export interface FaceDetectorConfig extends GenericConfig {
|
||||||
iouThreshold: number,
|
iouThreshold: number,
|
||||||
/** should child models perform on masked image of a face */
|
/** should child models perform on masked image of a face */
|
||||||
mask: boolean,
|
mask: boolean,
|
||||||
/** should face detection return face tensor to be used in some other extenrnal model? */
|
/** should face detection return processed and cropped face tensor that can with an external model for addtional processing?
|
||||||
|
* if enabled it must be manually deallocated to avoid memory leak */
|
||||||
return: boolean,
|
return: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
const calculateGaze = (face): { bearing: number, strength: number } => {
|
import type { Point, FaceResult } from '../result';
|
||||||
const radians = (pt1, pt2) => Math.atan2(pt1[1] - pt2[1], pt1[0] - pt2[0]); // function to calculate angle between any two points
|
|
||||||
|
type Vector = [number, number, number];
|
||||||
|
|
||||||
|
const calculateGaze = (face: FaceResult): { bearing: number, strength: number } => {
|
||||||
|
const radians = (pt1: Point, pt2: Point) => Math.atan2(pt1[1] - pt2[1], pt1[0] - pt2[0]); // function to calculate angle between any two points
|
||||||
if (!face.annotations['rightEyeIris'] || !face.annotations['leftEyeIris']) return { bearing: 0, strength: 0 };
|
if (!face.annotations['rightEyeIris'] || !face.annotations['leftEyeIris']) return { bearing: 0, strength: 0 };
|
||||||
|
|
||||||
const offsetIris = [0, -0.1]; // iris center may not align with average of eye extremes
|
const offsetIris = [0, -0.1]; // iris center may not align with average of eye extremes
|
||||||
const eyeRatio = 1; // factor to normalize changes x vs y
|
const eyeRatio = 1; // factor to normalize changes x vs y
|
||||||
|
|
||||||
const left = face.mesh[33][2] > face.mesh[263][2]; // pick left or right eye depending which one is closer bazed on outsize point z axis
|
const left = (face.mesh[33][2] || 0) > (face.mesh[263][2] || 0); // pick left or right eye depending which one is closer bazed on outsize point z axis
|
||||||
const irisCenter = left ? face.mesh[473] : face.mesh[468];
|
const irisCenter = left ? face.mesh[473] : face.mesh[468];
|
||||||
const eyeCenter = left // eye center is average of extreme points on x axis for both x and y, ignoring y extreme points as eyelids naturally open/close more when gazing up/down so relative point is less precise
|
const eyeCenter = left // eye center is average of extreme points on x axis for both x and y, ignoring y extreme points as eyelids naturally open/close more when gazing up/down so relative point is less precise
|
||||||
? [(face.mesh[133][0] + face.mesh[33][0]) / 2, (face.mesh[133][1] + face.mesh[33][1]) / 2]
|
? [(face.mesh[133][0] + face.mesh[33][0]) / 2, (face.mesh[133][1] + face.mesh[33][1]) / 2]
|
||||||
|
@ -13,7 +17,7 @@ const calculateGaze = (face): { bearing: number, strength: number } => {
|
||||||
const eyeSize = left // eye size is difference between extreme points for both x and y, used to normalize & squarify eye dimensions
|
const eyeSize = left // eye size is difference between extreme points for both x and y, used to normalize & squarify eye dimensions
|
||||||
? [face.mesh[133][0] - face.mesh[33][0], face.mesh[23][1] - face.mesh[27][1]]
|
? [face.mesh[133][0] - face.mesh[33][0], face.mesh[23][1] - face.mesh[27][1]]
|
||||||
: [face.mesh[263][0] - face.mesh[362][0], face.mesh[253][1] - face.mesh[257][1]];
|
: [face.mesh[263][0] - face.mesh[362][0], face.mesh[253][1] - face.mesh[257][1]];
|
||||||
const eyeDiff = [ // x distance between extreme point and center point normalized with eye size
|
const eyeDiff: Point = [ // x distance between extreme point and center point normalized with eye size
|
||||||
(eyeCenter[0] - irisCenter[0]) / eyeSize[0] - offsetIris[0],
|
(eyeCenter[0] - irisCenter[0]) / eyeSize[0] - offsetIris[0],
|
||||||
eyeRatio * (irisCenter[1] - eyeCenter[1]) / eyeSize[1] - offsetIris[1],
|
eyeRatio * (irisCenter[1] - eyeCenter[1]) / eyeSize[1] - offsetIris[1],
|
||||||
];
|
];
|
||||||
|
@ -23,33 +27,33 @@ const calculateGaze = (face): { bearing: number, strength: number } => {
|
||||||
return { bearing, strength };
|
return { bearing, strength };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calculateFaceAngle = (face, imageSize): {
|
export const calculateFaceAngle = (face: FaceResult, imageSize: [number, number]): {
|
||||||
angle: { pitch: number, yaw: number, roll: number },
|
angle: { pitch: number, yaw: number, roll: number },
|
||||||
matrix: [number, number, number, number, number, number, number, number, number],
|
matrix: [number, number, number, number, number, number, number, number, number],
|
||||||
gaze: { bearing: number, strength: number },
|
gaze: { bearing: number, strength: number },
|
||||||
} => {
|
} => {
|
||||||
// const degrees = (theta) => Math.abs(((theta * 180) / Math.PI) % 360);
|
// const degrees = (theta) => Math.abs(((theta * 180) / Math.PI) % 360);
|
||||||
const normalize = (v) => { // normalize vector
|
const normalize = (v: Vector): Vector => { // normalize vector
|
||||||
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
||||||
v[0] /= length;
|
v[0] /= length;
|
||||||
v[1] /= length;
|
v[1] /= length;
|
||||||
v[2] /= length;
|
v[2] /= length;
|
||||||
return v;
|
return v;
|
||||||
};
|
};
|
||||||
const subVectors = (a, b) => { // vector subtraction (a - b)
|
const subVectors = (a: Vector, b: Vector): Vector => { // vector subtraction (a - b)
|
||||||
const x = a[0] - b[0];
|
const x = a[0] - b[0];
|
||||||
const y = a[1] - b[1];
|
const y = a[1] - b[1];
|
||||||
const z = a[2] - b[2];
|
const z = a[2] - b[2];
|
||||||
return [x, y, z];
|
return [x, y, z];
|
||||||
};
|
};
|
||||||
const crossVectors = (a, b) => { // vector cross product (a x b)
|
const crossVectors = (a: Vector, b: Vector): Vector => { // vector cross product (a x b)
|
||||||
const x = a[1] * b[2] - a[2] * b[1];
|
const x = a[1] * b[2] - a[2] * b[1];
|
||||||
const y = a[2] * b[0] - a[0] * b[2];
|
const y = a[2] * b[0] - a[0] * b[2];
|
||||||
const z = a[0] * b[1] - a[1] * b[0];
|
const z = a[0] * b[1] - a[1] * b[0];
|
||||||
return [x, y, z];
|
return [x, y, z];
|
||||||
};
|
};
|
||||||
// 3x3 rotation matrix to Euler angles based on https://www.geometrictools.com/Documentation/EulerAngles.pdf
|
// 3x3 rotation matrix to Euler angles based on https://www.geometrictools.com/Documentation/EulerAngles.pdf
|
||||||
const rotationMatrixToEulerAngle = (r) => {
|
const rotationMatrixToEulerAngle = (r: number[]): { pitch: number, yaw: number, roll: number } => {
|
||||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||||
const [r00, _r01, _r02, r10, r11, r12, r20, r21, r22] = r;
|
const [r00, _r01, _r02, r10, r11, r12, r20, r21, r22] = r;
|
||||||
let thetaX: number;
|
let thetaX: number;
|
||||||
|
@ -93,10 +97,10 @@ export const calculateFaceAngle = (face, imageSize): {
|
||||||
|
|
||||||
const size = Math.max(face.boxRaw[2] * imageSize[0], face.boxRaw[3] * imageSize[1]) / 1.5;
|
const size = Math.max(face.boxRaw[2] * imageSize[0], face.boxRaw[3] * imageSize[1]) / 1.5;
|
||||||
// top, bottom, left, right
|
// top, bottom, left, right
|
||||||
const pts = [mesh[10], mesh[152], mesh[234], mesh[454]].map((pt) => [pt[0] * imageSize[0] / size, pt[1] * imageSize[1] / size, pt[2]]); // make the xyz coordinates proportional, independent of the image/box size
|
const pts: Point[] = [mesh[10], mesh[152], mesh[234], mesh[454]].map((pt) => [pt[0] * imageSize[0] / size, pt[1] * imageSize[1] / size, pt[2]] as Point); // make the xyz coordinates proportional, independent of the image/box size
|
||||||
|
|
||||||
const y_axis = normalize(subVectors(pts[1], pts[0]));
|
const y_axis = normalize(subVectors(pts[1] as Vector, pts[0] as Vector));
|
||||||
let x_axis = normalize(subVectors(pts[3], pts[2]));
|
let x_axis = normalize(subVectors(pts[3] as Vector, pts[2] as Vector));
|
||||||
const z_axis = normalize(crossVectors(x_axis, y_axis));
|
const z_axis = normalize(crossVectors(x_axis, y_axis));
|
||||||
// adjust x_axis to make sure that all axes are perpendicular to each other
|
// adjust x_axis to make sure that all axes are perpendicular to each other
|
||||||
x_axis = crossVectors(y_axis, z_axis);
|
x_axis = crossVectors(y_axis, z_axis);
|
||||||
|
|
|
@ -36,7 +36,7 @@ export async function load(config: Config): Promise<GraphModel> {
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeBounds(boxOutputs) {
|
function decodeBounds(boxOutputs: Tensor) {
|
||||||
const t: Record<string, Tensor> = {};
|
const t: Record<string, Tensor> = {};
|
||||||
t.boxStarts = tf.slice(boxOutputs, [0, 1], [-1, 2]);
|
t.boxStarts = tf.slice(boxOutputs, [0, 1], [-1, 2]);
|
||||||
t.centers = tf.add(t.boxStarts, anchors);
|
t.centers = tf.add(t.boxStarts, anchors);
|
||||||
|
|
|
@ -16,26 +16,28 @@ import * as gear from '../gear/gear';
|
||||||
import * as ssrnetAge from '../gear/ssrnet-age';
|
import * as ssrnetAge from '../gear/ssrnet-age';
|
||||||
import * as ssrnetGender from '../gear/ssrnet-gender';
|
import * as ssrnetGender from '../gear/ssrnet-gender';
|
||||||
import * as mobilefacenet from './mobilefacenet';
|
import * as mobilefacenet from './mobilefacenet';
|
||||||
import type { FaceResult } from '../result';
|
import type { FaceResult, Emotion, Gender, Race } from '../result';
|
||||||
import type { Tensor } from '../tfjs/types';
|
import type { Tensor } from '../tfjs/types';
|
||||||
import type { Human } from '../human';
|
import type { Human } from '../human';
|
||||||
import { calculateFaceAngle } from './angles';
|
import { calculateFaceAngle } from './angles';
|
||||||
|
|
||||||
|
type DescRes = { age: number, gender: Gender, genderScore: number, descriptor: number[], race?: { score: number, race: Race }[] };
|
||||||
|
|
||||||
export const detectFace = async (instance: Human /* instance of human */, input: Tensor): Promise<FaceResult[]> => {
|
export const detectFace = async (instance: Human /* instance of human */, input: Tensor): Promise<FaceResult[]> => {
|
||||||
// run facemesh, includes blazeface and iris
|
// run facemesh, includes blazeface and iris
|
||||||
// eslint-disable-next-line no-async-promise-executor
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
let timeStamp;
|
let timeStamp: number = now();
|
||||||
let ageRes;
|
let ageRes: { age: number } | Promise<{ age: number }> | null;
|
||||||
let gearRes;
|
let gearRes: gear.GearType | Promise<gear.GearType> | null;
|
||||||
let genderRes;
|
let genderRes: { gender: string, genderScore: number } | Promise<{ gender: string, genderScore: number }> | null;
|
||||||
let emotionRes;
|
let emotionRes: { score: number, emotion: Emotion }[] | Promise<{ score: number, emotion: Emotion }[]>;
|
||||||
let mobilefacenetRes;
|
let mobilefacenetRes: number[] | Promise<number[]> | null;
|
||||||
let antispoofRes;
|
let antispoofRes: number | Promise<number> | null;
|
||||||
let livenessRes;
|
let livenessRes: number | Promise<number> | null;
|
||||||
let descRes;
|
let descRes: DescRes | Promise<DescRes> | null;
|
||||||
|
|
||||||
const faceRes: Array<FaceResult> = [];
|
const faceRes: Array<FaceResult> = [];
|
||||||
instance.state = 'run:face';
|
instance.state = 'run:face';
|
||||||
timeStamp = now();
|
|
||||||
|
|
||||||
const faces = await facemesh.predict(input, instance.config);
|
const faces = await facemesh.predict(input, instance.config);
|
||||||
instance.performance.face = env.perfadd ? (instance.performance.face || 0) + Math.trunc(now() - timeStamp) : Math.trunc(now() - timeStamp);
|
instance.performance.face = env.perfadd ? (instance.performance.face || 0) + Math.trunc(now() - timeStamp) : Math.trunc(now() - timeStamp);
|
||||||
|
@ -65,11 +67,11 @@ export const detectFace = async (instance: Human /* instance of human */, input:
|
||||||
// run emotion, inherits face from blazeface
|
// run emotion, inherits face from blazeface
|
||||||
instance.analyze('Start Emotion:');
|
instance.analyze('Start Emotion:');
|
||||||
if (instance.config.async) {
|
if (instance.config.async) {
|
||||||
emotionRes = instance.config.face.emotion?.enabled ? emotion.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : null;
|
emotionRes = instance.config.face.emotion?.enabled ? emotion.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : [];
|
||||||
} else {
|
} else {
|
||||||
instance.state = 'run:emotion';
|
instance.state = 'run:emotion';
|
||||||
timeStamp = now();
|
timeStamp = now();
|
||||||
emotionRes = instance.config.face.emotion?.enabled ? await emotion.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : null;
|
emotionRes = instance.config.face.emotion?.enabled ? await emotion.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : [];
|
||||||
instance.performance.emotion = env.perfadd ? (instance.performance.emotion || 0) + Math.trunc(now() - timeStamp) : Math.trunc(now() - timeStamp);
|
instance.performance.emotion = env.perfadd ? (instance.performance.emotion || 0) + Math.trunc(now() - timeStamp) : Math.trunc(now() - timeStamp);
|
||||||
}
|
}
|
||||||
instance.analyze('End Emotion:');
|
instance.analyze('End Emotion:');
|
||||||
|
@ -77,11 +79,11 @@ export const detectFace = async (instance: Human /* instance of human */, input:
|
||||||
// run antispoof, inherits face from blazeface
|
// run antispoof, inherits face from blazeface
|
||||||
instance.analyze('Start AntiSpoof:');
|
instance.analyze('Start AntiSpoof:');
|
||||||
if (instance.config.async) {
|
if (instance.config.async) {
|
||||||
antispoofRes = instance.config.face.antispoof?.enabled ? antispoof.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : null;
|
antispoofRes = instance.config.face.antispoof?.enabled ? antispoof.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : 0;
|
||||||
} else {
|
} else {
|
||||||
instance.state = 'run:antispoof';
|
instance.state = 'run:antispoof';
|
||||||
timeStamp = now();
|
timeStamp = now();
|
||||||
antispoofRes = instance.config.face.antispoof?.enabled ? await antispoof.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : null;
|
antispoofRes = instance.config.face.antispoof?.enabled ? await antispoof.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : 0;
|
||||||
instance.performance.antispoof = env.perfadd ? (instance.performance.antispoof || 0) + Math.trunc(now() - timeStamp) : Math.trunc(now() - timeStamp);
|
instance.performance.antispoof = env.perfadd ? (instance.performance.antispoof || 0) + Math.trunc(now() - timeStamp) : Math.trunc(now() - timeStamp);
|
||||||
}
|
}
|
||||||
instance.analyze('End AntiSpoof:');
|
instance.analyze('End AntiSpoof:');
|
||||||
|
@ -89,11 +91,11 @@ export const detectFace = async (instance: Human /* instance of human */, input:
|
||||||
// run liveness, inherits face from blazeface
|
// run liveness, inherits face from blazeface
|
||||||
instance.analyze('Start Liveness:');
|
instance.analyze('Start Liveness:');
|
||||||
if (instance.config.async) {
|
if (instance.config.async) {
|
||||||
livenessRes = instance.config.face.liveness?.enabled ? liveness.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : null;
|
livenessRes = instance.config.face.liveness?.enabled ? liveness.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : 0;
|
||||||
} else {
|
} else {
|
||||||
instance.state = 'run:liveness';
|
instance.state = 'run:liveness';
|
||||||
timeStamp = now();
|
timeStamp = now();
|
||||||
livenessRes = instance.config.face.liveness?.enabled ? await liveness.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : null;
|
livenessRes = instance.config.face.liveness?.enabled ? await liveness.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : 0;
|
||||||
instance.performance.liveness = env.perfadd ? (instance.performance.antispoof || 0) + Math.trunc(now() - timeStamp) : Math.trunc(now() - timeStamp);
|
instance.performance.liveness = env.perfadd ? (instance.performance.antispoof || 0) + Math.trunc(now() - timeStamp) : Math.trunc(now() - timeStamp);
|
||||||
}
|
}
|
||||||
instance.analyze('End Liveness:');
|
instance.analyze('End Liveness:');
|
||||||
|
@ -101,11 +103,11 @@ export const detectFace = async (instance: Human /* instance of human */, input:
|
||||||
// run gear, inherits face from blazeface
|
// run gear, inherits face from blazeface
|
||||||
instance.analyze('Start GEAR:');
|
instance.analyze('Start GEAR:');
|
||||||
if (instance.config.async) {
|
if (instance.config.async) {
|
||||||
gearRes = instance.config.face['gear']?.enabled ? gear.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : {};
|
gearRes = instance.config.face['gear']?.enabled ? gear.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : null;
|
||||||
} else {
|
} else {
|
||||||
instance.state = 'run:gear';
|
instance.state = 'run:gear';
|
||||||
timeStamp = now();
|
timeStamp = now();
|
||||||
gearRes = instance.config.face['gear']?.enabled ? await gear.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : {};
|
gearRes = instance.config.face['gear']?.enabled ? await gear.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : null;
|
||||||
instance.performance.gear = Math.trunc(now() - timeStamp);
|
instance.performance.gear = Math.trunc(now() - timeStamp);
|
||||||
}
|
}
|
||||||
instance.analyze('End GEAR:');
|
instance.analyze('End GEAR:');
|
||||||
|
@ -113,13 +115,13 @@ export const detectFace = async (instance: Human /* instance of human */, input:
|
||||||
// run gear, inherits face from blazeface
|
// run gear, inherits face from blazeface
|
||||||
instance.analyze('Start SSRNet:');
|
instance.analyze('Start SSRNet:');
|
||||||
if (instance.config.async) {
|
if (instance.config.async) {
|
||||||
ageRes = instance.config.face['ssrnet']?.enabled ? ssrnetAge.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : {};
|
ageRes = instance.config.face['ssrnet']?.enabled ? ssrnetAge.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : null;
|
||||||
genderRes = instance.config.face['ssrnet']?.enabled ? ssrnetGender.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : {};
|
genderRes = instance.config.face['ssrnet']?.enabled ? ssrnetGender.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : null;
|
||||||
} else {
|
} else {
|
||||||
instance.state = 'run:ssrnet';
|
instance.state = 'run:ssrnet';
|
||||||
timeStamp = now();
|
timeStamp = now();
|
||||||
ageRes = instance.config.face['ssrnet']?.enabled ? await ssrnetAge.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : {};
|
ageRes = instance.config.face['ssrnet']?.enabled ? await ssrnetAge.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : null;
|
||||||
genderRes = instance.config.face['ssrnet']?.enabled ? await ssrnetGender.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : {};
|
genderRes = instance.config.face['ssrnet']?.enabled ? await ssrnetGender.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : null;
|
||||||
instance.performance.ssrnet = Math.trunc(now() - timeStamp);
|
instance.performance.ssrnet = Math.trunc(now() - timeStamp);
|
||||||
}
|
}
|
||||||
instance.analyze('End SSRNet:');
|
instance.analyze('End SSRNet:');
|
||||||
|
@ -127,11 +129,11 @@ export const detectFace = async (instance: Human /* instance of human */, input:
|
||||||
// run gear, inherits face from blazeface
|
// run gear, inherits face from blazeface
|
||||||
instance.analyze('Start MobileFaceNet:');
|
instance.analyze('Start MobileFaceNet:');
|
||||||
if (instance.config.async) {
|
if (instance.config.async) {
|
||||||
mobilefacenetRes = instance.config.face['mobilefacenet']?.enabled ? mobilefacenet.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : {};
|
mobilefacenetRes = instance.config.face['mobilefacenet']?.enabled ? mobilefacenet.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : null;
|
||||||
} else {
|
} else {
|
||||||
instance.state = 'run:mobilefacenet';
|
instance.state = 'run:mobilefacenet';
|
||||||
timeStamp = now();
|
timeStamp = now();
|
||||||
mobilefacenetRes = instance.config.face['mobilefacenet']?.enabled ? await mobilefacenet.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : {};
|
mobilefacenetRes = instance.config.face['mobilefacenet']?.enabled ? await mobilefacenet.predict(faces[i].tensor || tf.tensor([]), instance.config, i, faces.length) : null;
|
||||||
instance.performance.mobilefacenet = Math.trunc(now() - timeStamp);
|
instance.performance.mobilefacenet = Math.trunc(now() - timeStamp);
|
||||||
}
|
}
|
||||||
instance.analyze('End MobileFaceNet:');
|
instance.analyze('End MobileFaceNet:');
|
||||||
|
@ -154,11 +156,26 @@ export const detectFace = async (instance: Human /* instance of human */, input:
|
||||||
}
|
}
|
||||||
instance.analyze('Finish Face:');
|
instance.analyze('Finish Face:');
|
||||||
|
|
||||||
// override age/gender if alternative models are used
|
if (instance.config.face['ssrnet']?.enabled && ageRes && genderRes) { // override age/gender if ssrnet model is used
|
||||||
if (instance.config.face['ssrnet']?.enabled && ageRes && genderRes) descRes = { age: ageRes.age, gender: genderRes.gender, genderScore: genderRes.genderScore };
|
descRes = {
|
||||||
if (instance.config.face['gear']?.enabled && gearRes) descRes = { age: gearRes.age, gender: gearRes.gender, genderScore: gearRes.genderScore, race: gearRes.race };
|
...(descRes as DescRes),
|
||||||
// override descriptor if embedding model is used
|
age: (ageRes as { age: number}).age,
|
||||||
if (instance.config.face['mobilefacenet']?.enabled && mobilefacenetRes) descRes.descriptor = mobilefacenetRes;
|
gender: (genderRes as { gender: Gender, genderScore: number }).gender,
|
||||||
|
genderScore: (genderRes as { gender: Gender, genderScore: number }).genderScore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (instance.config.face['gear']?.enabled && gearRes) { // override age/gender/race if gear model is used
|
||||||
|
descRes = {
|
||||||
|
...(descRes as DescRes),
|
||||||
|
age: (gearRes as gear.GearType).age,
|
||||||
|
gender: (gearRes as gear.GearType).gender,
|
||||||
|
genderScore: (gearRes as gear.GearType).genderScore,
|
||||||
|
race: (gearRes as gear.GearType).race,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (instance.config.face['mobilefacenet']?.enabled && mobilefacenetRes) { // override descriptor if embedding model is used
|
||||||
|
(descRes as DescRes).descriptor = mobilefacenetRes as number[];
|
||||||
|
}
|
||||||
|
|
||||||
// calculate iris distance
|
// calculate iris distance
|
||||||
// iris: array[ center, left, top, right, bottom]
|
// iris: array[ center, left, top, right, bottom]
|
||||||
|
@ -183,14 +200,14 @@ export const detectFace = async (instance: Human /* instance of human */, input:
|
||||||
...faces[i],
|
...faces[i],
|
||||||
id: i,
|
id: i,
|
||||||
};
|
};
|
||||||
if (descRes?.age) res.age = descRes.age;
|
if ((descRes as DescRes)?.age) res.age = (descRes as DescRes).age as number;
|
||||||
if (descRes?.gender) res.gender = descRes.gender;
|
if ((descRes as DescRes)?.gender) res.gender = (descRes as DescRes).gender as Gender;
|
||||||
if (descRes?.genderScore) res.genderScore = descRes?.genderScore;
|
if ((descRes as DescRes)?.genderScore) res.genderScore = (descRes as DescRes)?.genderScore as number;
|
||||||
if (descRes?.descriptor) res.embedding = descRes?.descriptor;
|
if ((descRes as DescRes)?.descriptor) res.embedding = (descRes as DescRes)?.descriptor as Array<number>;
|
||||||
if (descRes?.race) res.race = descRes?.race;
|
if ((descRes as DescRes)?.race) res.race = (descRes as DescRes)?.race as { score: number, race: Race }[];
|
||||||
if (emotionRes) res.emotion = emotionRes;
|
if (emotionRes) res.emotion = emotionRes as Array<{ score: number, emotion: Emotion }>;
|
||||||
if (antispoofRes) res.real = antispoofRes;
|
if (antispoofRes) res.real = antispoofRes as number;
|
||||||
if (livenessRes) res.live = livenessRes;
|
if (livenessRes) res.live = livenessRes as number;
|
||||||
if (irisSize && irisSize !== 0) res.iris = Math.trunc(500 / irisSize / 11.7) / 100;
|
if (irisSize && irisSize !== 0) res.iris = Math.trunc(500 / irisSize / 11.7) / 100;
|
||||||
if (rotation) res.rotation = rotation;
|
if (rotation) res.rotation = rotation;
|
||||||
if (tensor) res.tensor = tensor;
|
if (tensor) res.tensor = tensor;
|
||||||
|
|
|
@ -13,11 +13,14 @@ import * as tf from '../../dist/tfjs.esm.js';
|
||||||
import { constants } from '../tfjs/constants';
|
import { constants } from '../tfjs/constants';
|
||||||
import type { Tensor, GraphModel } from '../tfjs/types';
|
import type { Tensor, GraphModel } from '../tfjs/types';
|
||||||
import type { Config } from '../config';
|
import type { Config } from '../config';
|
||||||
|
import type { Gender, Race } from '../result';
|
||||||
|
|
||||||
|
export type FaceRes = { age: number, gender: Gender, genderScore: number, descriptor: number[], race?: { score: number, race: Race }[] };
|
||||||
|
|
||||||
let model: GraphModel | null;
|
let model: GraphModel | null;
|
||||||
const last: Array<{
|
const last: Array<{
|
||||||
age: number,
|
age: number,
|
||||||
gender: string,
|
gender: Gender,
|
||||||
genderScore: number,
|
genderScore: number,
|
||||||
descriptor: number[],
|
descriptor: number[],
|
||||||
}> = [];
|
}> = [];
|
||||||
|
@ -63,7 +66,7 @@ export function enhance(input): Tensor {
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function predict(image: Tensor, config: Config, idx, count): Promise<{ age: number, gender: string, genderScore: number, descriptor: number[] }> {
|
export async function predict(image: Tensor, config: Config, idx: number, count: number): Promise<FaceRes> {
|
||||||
if (!model) return { age: 0, gender: 'unknown', genderScore: 0, descriptor: [] };
|
if (!model) return { age: 0, gender: 'unknown', genderScore: 0, descriptor: [] };
|
||||||
const skipFrame = skipped < (config.face.description?.skipFrames || 0);
|
const skipFrame = skipped < (config.face.description?.skipFrames || 0);
|
||||||
const skipTime = (config.face.description?.skipTime || 0) > (now() - lastTime);
|
const skipTime = (config.face.description?.skipTime || 0) > (now() - lastTime);
|
||||||
|
@ -75,7 +78,7 @@ export async function predict(image: Tensor, config: Config, idx, count): Promis
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
const obj = {
|
const obj = {
|
||||||
age: <number>0,
|
age: <number>0,
|
||||||
gender: <string>'unknown',
|
gender: <Gender>'unknown',
|
||||||
genderScore: <number>0,
|
genderScore: <number>0,
|
||||||
descriptor: <number[]>[],
|
descriptor: <number[]>[],
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,7 @@ import type { Config } from '../config';
|
||||||
import type { GraphModel, Tensor } from '../tfjs/types';
|
import type { GraphModel, Tensor } from '../tfjs/types';
|
||||||
import { env } from '../util/env';
|
import { env } from '../util/env';
|
||||||
|
|
||||||
type GearType = { age: number, gender: Gender, genderScore: number, race: Array<{ score: number, race: Race }> }
|
export type GearType = { age: number, gender: Gender, genderScore: number, race: Array<{ score: number, race: Race }> }
|
||||||
let model: GraphModel | null;
|
let model: GraphModel | null;
|
||||||
const last: Array<GearType> = [];
|
const last: Array<GearType> = [];
|
||||||
const raceNames = ['white', 'black', 'asian', 'indian', 'other'];
|
const raceNames = ['white', 'black', 'asian', 'indian', 'other'];
|
||||||
|
@ -32,7 +32,7 @@ export async function load(config: Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export async function predict(image: Tensor, config: Config, idx, count): Promise<GearType> {
|
export async function predict(image: Tensor, config: Config, idx: number, count: number): Promise<GearType> {
|
||||||
if (!model) return { age: 0, gender: 'unknown', genderScore: 0, race: [] };
|
if (!model) return { age: 0, gender: 'unknown', genderScore: 0, race: [] };
|
||||||
const skipFrame = skipped < (config.face['gear']?.skipFrames || 0);
|
const skipFrame = skipped < (config.face['gear']?.skipFrames || 0);
|
||||||
const skipTime = (config.face['gear']?.skipTime || 0) > (now() - lastTime);
|
const skipTime = (config.face['gear']?.skipTime || 0) > (now() - lastTime);
|
||||||
|
|
|
@ -172,7 +172,7 @@ async function detectFingers(input: Tensor, h: HandDetectResult, config: Config)
|
||||||
]);
|
]);
|
||||||
hand.landmarks = fingerPose.analyze(hand.keypoints) as HandResult['landmarks']; // calculate finger gestures
|
hand.landmarks = fingerPose.analyze(hand.keypoints) as HandResult['landmarks']; // calculate finger gestures
|
||||||
for (const key of Object.keys(fingerMap)) { // map keypoints to per-finger annotations
|
for (const key of Object.keys(fingerMap)) { // map keypoints to per-finger annotations
|
||||||
hand.annotations[key] = fingerMap[key].map((index) => (hand.landmarks && hand.keypoints[index] ? hand.keypoints[index] : null));
|
hand.annotations[key] = fingerMap[key].map((index: number) => (hand.landmarks && hand.keypoints[index] ? hand.keypoints[index] : null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object.keys(t).forEach((tensor) => tf.dispose(t[tensor]));
|
Object.keys(t).forEach((tensor) => tf.dispose(t[tensor]));
|
||||||
|
|
|
@ -31,11 +31,11 @@ export async function load(config: Config): Promise<GraphModel> {
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function process(res: Tensor | null, outputShape, config: Config) {
|
async function process(res: Tensor | null, outputShape: [number, number], config: Config) {
|
||||||
if (!res) return [];
|
if (!res) return [];
|
||||||
const t: Record<string, Tensor> = {};
|
const t: Record<string, Tensor> = {};
|
||||||
const results: Array<ObjectResult> = [];
|
const results: Array<ObjectResult> = [];
|
||||||
const detections = await res.array();
|
const detections = await res.array() as number[][][];
|
||||||
t.squeeze = tf.squeeze(res);
|
t.squeeze = tf.squeeze(res);
|
||||||
const arr = tf.split(t.squeeze, 6, 1) as Tensor[]; // x1, y1, x2, y2, score, class
|
const arr = tf.split(t.squeeze, 6, 1) as Tensor[]; // x1, y1, x2, y2, score, class
|
||||||
t.stack = tf.stack([arr[1], arr[0], arr[3], arr[2]], 1); // reorder dims as tf.nms expects y, x
|
t.stack = tf.stack([arr[1], arr[0], arr[3], arr[2]], 1); // reorder dims as tf.nms expects y, x
|
||||||
|
@ -43,7 +43,7 @@ async function process(res: Tensor | null, outputShape, config: Config) {
|
||||||
t.scores = tf.squeeze(arr[4]);
|
t.scores = tf.squeeze(arr[4]);
|
||||||
t.classes = tf.squeeze(arr[5]);
|
t.classes = tf.squeeze(arr[5]);
|
||||||
tf.dispose([res, ...arr]);
|
tf.dispose([res, ...arr]);
|
||||||
t.nms = await tf.image.nonMaxSuppressionAsync(t.boxes, t.scores, config.object.maxDetected, config.object.iouThreshold, config.object.minConfidence);
|
t.nms = await tf.image.nonMaxSuppressionAsync(t.boxes, t.scores, config.object.maxDetected, config.object.iouThreshold, (config.object.minConfidence || 0));
|
||||||
const nms = await t.nms.data();
|
const nms = await t.nms.data();
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const id of Array.from(nms)) {
|
for (const id of Array.from(nms)) {
|
||||||
|
@ -81,7 +81,7 @@ export async function predict(input: Tensor, config: Config): Promise<ObjectResu
|
||||||
}
|
}
|
||||||
skipped = 0;
|
skipped = 0;
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
const outputSize = [input.shape[2], input.shape[1]];
|
const outputSize = [input.shape[2] || 0, input.shape[1] || 0] as [number, number];
|
||||||
const resize = tf.image.resizeBilinear(input, [inputSize, inputSize]);
|
const resize = tf.image.resizeBilinear(input, [inputSize, inputSize]);
|
||||||
const objectT = config.object.enabled ? model?.execute(resize, ['tower_0/detections']) as Tensor : null;
|
const objectT = config.object.enabled ? model?.execute(resize, ['tower_0/detections']) as Tensor : null;
|
||||||
lastTime = now();
|
lastTime = now();
|
||||||
|
|
|
@ -13,25 +13,26 @@ import type { GraphModel, Tensor } from '../tfjs/types';
|
||||||
import type { Config } from '../config';
|
import type { Config } from '../config';
|
||||||
import { env } from '../util/env';
|
import { env } from '../util/env';
|
||||||
|
|
||||||
let model;
|
let model: GraphModel;
|
||||||
let last: Array<ObjectResult> = [];
|
let last: Array<ObjectResult> = [];
|
||||||
let lastTime = 0;
|
let lastTime = 0;
|
||||||
let skipped = Number.MAX_SAFE_INTEGER;
|
let skipped = Number.MAX_SAFE_INTEGER;
|
||||||
|
let inputSize = 0;
|
||||||
|
|
||||||
const scaleBox = 2.5; // increase box size
|
const scaleBox = 2.5; // increase box size
|
||||||
|
|
||||||
export async function load(config: Config): Promise<GraphModel> {
|
export async function load(config: Config): Promise<GraphModel> {
|
||||||
if (!model || env.initial) {
|
if (!model || env.initial) {
|
||||||
model = await tf.loadGraphModel(join(config.modelBasePath, config.object.modelPath || ''));
|
model = await tf.loadGraphModel(join(config.modelBasePath, config.object.modelPath || '')) as unknown as GraphModel;
|
||||||
const inputs = Object.values(model.modelSignature['inputs']);
|
const inputs = Object.values(model.modelSignature['inputs']);
|
||||||
model.inputSize = Array.isArray(inputs) ? parseInt(inputs[0].tensorShape.dim[2].size) : null;
|
inputSize = Array.isArray(inputs) ? parseInt(inputs[0].tensorShape.dim[2].size) : 0;
|
||||||
if (!model || !model.modelUrl) log('load model failed:', config.object.modelPath);
|
if (!model || !model['modelUrl']) log('load model failed:', config.object.modelPath);
|
||||||
else if (config.debug) log('load model:', model.modelUrl);
|
else if (config.debug) log('load model:', model['modelUrl']);
|
||||||
} else if (config.debug) log('cached model:', model.modelUrl);
|
} else if (config.debug) log('cached model:', model['modelUrl']);
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function process(res, inputSize, outputShape, config) {
|
async function process(res: Tensor[], outputShape: [number, number], config: Config) {
|
||||||
let id = 0;
|
let id = 0;
|
||||||
let results: Array<ObjectResult> = [];
|
let results: Array<ObjectResult> = [];
|
||||||
for (const strideSize of [1, 2, 4]) { // try each stride size as it detects large/medium/small objects
|
for (const strideSize of [1, 2, 4]) { // try each stride size as it detects large/medium/small objects
|
||||||
|
@ -39,18 +40,18 @@ async function process(res, inputSize, outputShape, config) {
|
||||||
tf.tidy(async () => { // wrap in tidy to automatically deallocate temp tensors
|
tf.tidy(async () => { // wrap in tidy to automatically deallocate temp tensors
|
||||||
const baseSize = strideSize * 13; // 13x13=169, 26x26=676, 52x52=2704
|
const baseSize = strideSize * 13; // 13x13=169, 26x26=676, 52x52=2704
|
||||||
// find boxes and scores output depending on stride
|
// find boxes and scores output depending on stride
|
||||||
const scoresT = res.find((a) => (a.shape[1] === (baseSize ** 2) && a.shape[2] === labels.length))?.squeeze();
|
const scoresT = tf.squeeze(res.find((a: Tensor) => (a.shape[1] === (baseSize ** 2) && (a.shape[2] || 0) === labels.length)));
|
||||||
const featuresT = res.find((a) => (a.shape[1] === (baseSize ** 2) && a.shape[2] < labels.length))?.squeeze();
|
const featuresT = tf.squeeze(res.find((a: Tensor) => (a.shape[1] === (baseSize ** 2) && (a.shape[2] || 0) < labels.length)));
|
||||||
const boxesMax = featuresT.reshape([-1, 4, featuresT.shape[1] / 4]); // reshape [output] to [4, output / 4] where number is number of different features inside each stride
|
const boxesMax = featuresT.reshape([-1, 4, featuresT.shape[1] / 4]); // reshape [output] to [4, output / 4] where number is number of different features inside each stride
|
||||||
const boxIdx = await boxesMax.argMax(2).array(); // what we need is indexes of features with highest scores, not values itself
|
const boxIdx = await boxesMax.argMax(2).array(); // what we need is indexes of features with highest scores, not values itself
|
||||||
const scores = await scoresT.array(); // optionally use exponential scores or just as-is
|
const scores = await scoresT.array(); // optionally use exponential scores or just as-is
|
||||||
for (let i = 0; i < scoresT.shape[0]; i++) { // total strides (x * y matrix)
|
for (let i = 0; i < scoresT.shape[0]; i++) { // total strides (x * y matrix)
|
||||||
for (let j = 0; j < scoresT.shape[1]; j++) { // one score for each class
|
for (let j = 0; j < scoresT.shape[1]; j++) { // one score for each class
|
||||||
const score = scores[i][j]; // get score for current position
|
const score = scores[i][j]; // get score for current position
|
||||||
if (score > config.object.minConfidence && j !== 61) {
|
if (score > (config.object.minConfidence || 0) && j !== 61) {
|
||||||
const cx = (0.5 + Math.trunc(i % baseSize)) / baseSize; // center.x normalized to range 0..1
|
const cx = (0.5 + Math.trunc(i % baseSize)) / baseSize; // center.x normalized to range 0..1
|
||||||
const cy = (0.5 + Math.trunc(i / baseSize)) / baseSize; // center.y normalized to range 0..1
|
const cy = (0.5 + Math.trunc(i / baseSize)) / baseSize; // center.y normalized to range 0..1
|
||||||
const boxOffset = boxIdx[i].map((a) => a * (baseSize / strideSize / inputSize)); // just grab indexes of features with highest scores
|
const boxOffset = boxIdx[i].map((a: number) => a * (baseSize / strideSize / inputSize)); // just grab indexes of features with highest scores
|
||||||
const [x, y] = [
|
const [x, y] = [
|
||||||
cx - (scaleBox / strideSize * boxOffset[0]),
|
cx - (scaleBox / strideSize * boxOffset[0]),
|
||||||
cy - (scaleBox / strideSize * boxOffset[1]),
|
cy - (scaleBox / strideSize * boxOffset[1]),
|
||||||
|
@ -116,8 +117,8 @@ export async function predict(image: Tensor, config: Config): Promise<ObjectResu
|
||||||
skipped = 0;
|
skipped = 0;
|
||||||
if (!env.kernels.includes('mod') || !env.kernels.includes('sparsetodense')) return last;
|
if (!env.kernels.includes('mod') || !env.kernels.includes('sparsetodense')) return last;
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
const outputSize = [image.shape[2], image.shape[1]];
|
const outputSize = [image.shape[2] || 0, image.shape[1] || 0];
|
||||||
const resize = tf.image.resizeBilinear(image, [model.inputSize, model.inputSize], false);
|
const resize = tf.image.resizeBilinear(image, [inputSize, inputSize], false);
|
||||||
const norm = tf.div(resize, constants.tf255);
|
const norm = tf.div(resize, constants.tf255);
|
||||||
const transpose = norm.transpose([0, 3, 1, 2]);
|
const transpose = norm.transpose([0, 3, 1, 2]);
|
||||||
tf.dispose(norm);
|
tf.dispose(norm);
|
||||||
|
@ -128,7 +129,7 @@ export async function predict(image: Tensor, config: Config): Promise<ObjectResu
|
||||||
lastTime = now();
|
lastTime = now();
|
||||||
tf.dispose(transpose);
|
tf.dispose(transpose);
|
||||||
|
|
||||||
const obj = await process(objectT, model.inputSize, outputSize, config);
|
const obj = await process(objectT as Tensor[], outputSize as [number, number], config);
|
||||||
last = obj;
|
last = obj;
|
||||||
resolve(obj);
|
resolve(obj);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue