2021-05-25 14:58:20 +02:00
/ * *
* Module that analyzes person age
* Obsolete
* /
2021-03-21 12:49:55 +01:00
import { log , now } from './helpers' ;
2021-06-01 13:37:17 +02:00
import * as tf from '../dist/tfjs.esm.js' ;
2021-04-25 22:56:10 +02:00
import * as facemesh from './blazeface/facemesh' ;
2021-03-21 12:49:55 +01:00
import * as emotion from './emotion/emotion' ;
2021-03-21 19:18:51 +01:00
import * as faceres from './faceres/faceres' ;
2021-05-22 18:33:19 +02:00
import { Face } from './result' ;
2021-05-31 16:40:07 +02:00
import { Tensor } from './tfjs/types' ;
2021-03-21 12:49:55 +01:00
2021-05-28 21:53:51 +02:00
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
2021-05-31 16:40:07 +02:00
const rad2deg = ( theta ) = > Math . round ( ( theta * 180 ) / Math . PI ) ;
2021-05-28 21:53:51 +02:00
2021-05-31 05:21:48 +02:00
const calculateGaze = ( mesh , box ) : { bearing : number , strength : number } = > {
2021-05-28 21:53:51 +02:00
const radians = ( pt1 , pt2 ) = > Math . atan2 ( pt1 [ 1 ] - pt2 [ 1 ] , pt1 [ 0 ] - pt2 [ 0 ] ) ; // function to calculate angle between any two points
2021-05-30 23:56:40 +02:00
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
2021-05-28 21:53:51 +02:00
const left = mesh [ 33 ] [ 2 ] > mesh [ 263 ] [ 2 ] ; // pick left or right eye depending which one is closer bazed on outsize point z axis
const irisCenter = left ? mesh [ 473 ] : 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
? [ ( mesh [ 133 ] [ 0 ] + mesh [ 33 ] [ 0 ] ) / 2 , ( mesh [ 133 ] [ 1 ] + mesh [ 33 ] [ 1 ] ) / 2 ]
: [ ( mesh [ 263 ] [ 0 ] + mesh [ 362 ] [ 0 ] ) / 2 , ( mesh [ 263 ] [ 1 ] + mesh [ 362 ] [ 1 ] ) / 2 ] ;
const eyeSize = left // eye size is difference between extreme points for both x and y, used to normalize & squarify eye dimensions
? [ mesh [ 133 ] [ 0 ] - mesh [ 33 ] [ 0 ] , mesh [ 23 ] [ 1 ] - mesh [ 27 ] [ 1 ] ]
: [ mesh [ 263 ] [ 0 ] - mesh [ 362 ] [ 0 ] , mesh [ 253 ] [ 1 ] - mesh [ 257 ] [ 1 ] ] ;
const eyeDiff = [ // x distance between extreme point and center point normalized with eye size
( eyeCenter [ 0 ] - irisCenter [ 0 ] ) / eyeSize [ 0 ] - offsetIris [ 0 ] ,
eyeRatio * ( irisCenter [ 1 ] - eyeCenter [ 1 ] ) / eyeSize [ 1 ] - offsetIris [ 1 ] ,
] ;
2021-05-31 16:40:07 +02:00
let strength = Math . sqrt ( ( eyeDiff [ 0 ] * * 2 ) + ( eyeDiff [ 1 ] * * 2 ) ) ; // vector length is a diagonal between two differences
strength = Math . min ( strength , box [ 2 ] / 2 , box [ 3 ] / 2 ) ; // limit strength to half of box size to avoid clipping due to low precision
const bearing = ( radians ( [ 0 , 0 ] , eyeDiff ) + ( Math . PI / 2 ) ) % Math . PI ; // using eyeDiff instead eyeCenter/irisCenter combo due to manual adjustments and rotate clockwise 90degrees
2021-05-28 21:53:51 +02:00
2021-05-31 16:40:07 +02:00
return { bearing , strength } ;
2021-05-28 21:53:51 +02:00
} ;
const calculateFaceAngle = ( face , imageSize ) : {
angle : { pitch : number , yaw : number , roll : number } ,
matrix : [ number , number , number , number , number , number , number , number , number ] ,
2021-05-31 05:21:48 +02:00
gaze : { bearing : number , strength : number } ,
2021-05-28 21:53:51 +02:00
} = > {
2021-03-28 14:40:39 +02:00
// const degrees = (theta) => Math.abs(((theta * 180) / Math.PI) % 360);
const normalize = ( v ) = > { // normalize vector
2021-03-27 09:50:33 +01:00
const length = Math . sqrt ( v [ 0 ] * v [ 0 ] + v [ 1 ] * v [ 1 ] + v [ 2 ] * v [ 2 ] ) ;
v [ 0 ] /= length ;
v [ 1 ] /= length ;
v [ 2 ] /= length ;
return v ;
2021-03-28 14:40:39 +02:00
} ;
const subVectors = ( a , b ) = > { // vector subtraction (a - b)
2021-03-27 09:50:33 +01:00
const x = a [ 0 ] - b [ 0 ] ;
const y = a [ 1 ] - b [ 1 ] ;
const z = a [ 2 ] - b [ 2 ] ;
return [ x , y , z ] ;
2021-03-28 14:40:39 +02:00
} ;
const crossVectors = ( a , b ) = > { // vector cross product (a x b)
2021-03-27 09:50:33 +01:00
const x = a [ 1 ] * b [ 2 ] - a [ 2 ] * b [ 1 ] ;
const y = a [ 2 ] * b [ 0 ] - a [ 0 ] * b [ 2 ] ;
const z = a [ 0 ] * b [ 1 ] - a [ 1 ] * b [ 0 ] ;
return [ x , y , z ] ;
2021-03-28 14:40:39 +02:00
} ;
// 3x3 rotation matrix to Euler angles based on https://www.geometrictools.com/Documentation/EulerAngles.pdf
const rotationMatrixToEulerAngle = ( r ) = > {
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const [ r00 , r01 , r02 , r10 , r11 , r12 , r20 , r21 , r22 ] = r ;
2021-03-28 12:19:25 +02:00
let thetaX ; let thetaY ; let thetaZ ;
2021-03-28 14:40:39 +02:00
if ( r10 < 1 ) { // YZX calculation
2021-03-28 12:19:25 +02:00
if ( r10 > - 1 ) {
thetaZ = Math . asin ( r10 ) ;
2021-03-28 14:49:56 +02:00
thetaY = Math . atan2 ( - r20 , r00 ) ;
2021-03-28 12:19:25 +02:00
thetaX = Math . atan2 ( - r12 , r11 ) ;
} else {
2021-03-28 14:40:39 +02:00
thetaZ = - Math . PI / 2 ;
2021-03-28 14:49:56 +02:00
thetaY = - Math . atan2 ( r21 , r22 ) ;
2021-03-28 12:19:25 +02:00
thetaX = 0 ;
}
} else {
2021-03-28 14:40:39 +02:00
thetaZ = Math . PI / 2 ;
2021-03-28 14:49:56 +02:00
thetaY = Math . atan2 ( r21 , r22 ) ;
2021-03-28 12:19:25 +02:00
thetaX = 0 ;
}
2021-03-28 14:40:39 +02:00
return { pitch : 2 * - thetaX , yaw : 2 * - thetaY , roll : 2 * - thetaZ } ;
} ;
// simple Euler angle calculation based existing 3D mesh
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const meshToEulerAngle = ( mesh ) = > {
const radians = ( a1 , a2 , b1 , b2 ) = > Math . atan2 ( b2 - a2 , b1 - a1 ) ;
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const angle = {
2021-05-18 17:26:16 +02:00
// values are in radians in range of -pi/2 to pi/2 which is -90 to +90 degrees, value of 0 means center
2021-03-28 14:40:39 +02:00
// pitch is face move up/down
pitch : radians ( mesh [ 10 ] [ 1 ] , mesh [ 10 ] [ 2 ] , mesh [ 152 ] [ 1 ] , mesh [ 152 ] [ 2 ] ) , // looking at y,z of top and bottom points of the face
// yaw is face turn left/right
yaw : radians ( mesh [ 33 ] [ 0 ] , mesh [ 33 ] [ 2 ] , mesh [ 263 ] [ 0 ] , mesh [ 263 ] [ 2 ] ) , // looking at x,z of outside corners of leftEye and rightEye
// roll is face lean left/right
roll : radians ( mesh [ 33 ] [ 0 ] , mesh [ 33 ] [ 1 ] , mesh [ 263 ] [ 0 ] , mesh [ 263 ] [ 1 ] ) , // looking at x,y of outside corners of leftEye and rightEye
} ;
return angle ;
} ;
2021-03-28 12:19:25 +02:00
2021-05-28 21:53:51 +02:00
// initialize gaze and mesh
2021-03-28 12:19:25 +02:00
const mesh = face . meshRaw ;
2021-05-31 05:21:48 +02:00
if ( ! mesh || mesh . length < 300 ) return { angle : { pitch : 0 , yaw : 0 , roll : 0 } , matrix : [ 1 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 1 ] , gaze : { bearing : 0 , strength : 0 } } ;
2021-03-28 12:19:25 +02:00
2021-05-28 21:53:51 +02:00
const size = Math . max ( face . boxRaw [ 2 ] * imageSize [ 0 ] , face . boxRaw [ 3 ] * imageSize [ 1 ] ) / 1.5 ;
2021-03-28 12:19:25 +02:00
// top, bottom, left, right
const pts = [ mesh [ 10 ] , mesh [ 152 ] , mesh [ 234 ] , mesh [ 454 ] ] . map ( ( pt ) = > [
// make the xyz coordinates proportional, independent of the image/box size
2021-05-28 21:53:51 +02:00
pt [ 0 ] * imageSize [ 0 ] / size ,
pt [ 1 ] * imageSize [ 1 ] / size ,
2021-03-28 12:19:25 +02:00
pt [ 2 ] ,
] ) ;
const y_axis = normalize ( subVectors ( pts [ 1 ] , pts [ 0 ] ) ) ;
let x_axis = normalize ( subVectors ( pts [ 3 ] , pts [ 2 ] ) ) ;
2021-03-27 09:50:33 +01:00
const z_axis = normalize ( crossVectors ( x_axis , y_axis ) ) ;
// adjust x_axis to make sure that all axes are perpendicular to each other
x_axis = crossVectors ( y_axis , z_axis ) ;
// Rotation Matrix from Axis Vectors - http://renderdan.blogspot.com/2006/05/rotation-matrix-from-axis-vectors.html
2021-03-28 12:19:25 +02:00
// 3x3 rotation matrix is flatten to array in row-major order. Note that the rotation represented by this matrix is inverted.
2021-03-28 14:40:39 +02:00
const matrix : [ number , number , number , number , number , number , number , number , number ] = [
2021-03-28 12:19:25 +02:00
x_axis [ 0 ] , x_axis [ 1 ] , x_axis [ 2 ] ,
y_axis [ 0 ] , y_axis [ 1 ] , y_axis [ 2 ] ,
z_axis [ 0 ] , z_axis [ 1 ] , z_axis [ 2 ] ,
] ;
2021-03-28 14:40:39 +02:00
const angle = rotationMatrixToEulerAngle ( matrix ) ;
// const angle = meshToEulerAngle(mesh);
2021-05-28 21:53:51 +02:00
// we have iris keypoints so we can calculate gaze direction
2021-05-31 05:21:48 +02:00
const gaze = mesh . length === 478 ? calculateGaze ( mesh , face . box ) : { bearing : 0 , strength : 0 } ;
2021-05-28 21:53:51 +02:00
return { angle , matrix , gaze } ;
2021-03-21 12:49:55 +01:00
} ;
2021-05-31 16:40:07 +02:00
export const detectFace = async ( parent /* instance of human */ , input : Tensor ) : Promise < Face [ ] > = > {
2021-03-21 12:49:55 +01:00
// run facemesh, includes blazeface and iris
// eslint-disable-next-line no-async-promise-executor
let timeStamp ;
let ageRes ;
let genderRes ;
let emotionRes ;
let embeddingRes ;
2021-03-21 19:18:51 +01:00
let descRes ;
2021-05-22 18:33:19 +02:00
const faceRes : Array < Face > = [ ] ;
2021-03-21 12:49:55 +01:00
parent . state = 'run:face' ;
timeStamp = now ( ) ;
2021-04-25 22:56:10 +02:00
const faces = await facemesh . predict ( input , parent . config ) ;
2021-05-31 16:40:07 +02:00
parent . performance . face = Math . trunc ( now ( ) - timeStamp ) ;
if ( ! input . shape || input . shape . length !== 4 ) return [ ] ;
2021-03-21 12:49:55 +01:00
if ( ! faces ) return [ ] ;
2021-05-18 17:26:16 +02:00
// for (const face of faces) {
for ( let i = 0 ; i < faces . length ; i ++ ) {
2021-03-21 12:49:55 +01:00
parent . analyze ( 'Get Face' ) ;
// is something went wrong, skip the face
2021-06-01 14:59:09 +02:00
// @ts-ignore possibly undefined
2021-06-01 13:37:17 +02:00
if ( ! faces [ i ] . image || faces [ i ] . image [ 'isDisposedInternal' ] ) {
2021-05-18 17:26:16 +02:00
log ( 'Face object is disposed:' , faces [ i ] . image ) ;
2021-03-21 12:49:55 +01:00
continue ;
}
2021-05-18 17:26:16 +02:00
const rotation = calculateFaceAngle ( faces [ i ] , [ input . shape [ 2 ] , input . shape [ 1 ] ] ) ;
2021-03-21 12:49:55 +01:00
// run emotion, inherits face from blazeface
parent . analyze ( 'Start Emotion:' ) ;
if ( parent . config . async ) {
2021-05-18 17:26:16 +02:00
emotionRes = parent . config . face . emotion . enabled ? emotion . predict ( faces [ i ] . image , parent . config , i , faces . length ) : { } ;
2021-03-21 12:49:55 +01:00
} else {
parent . state = 'run:emotion' ;
timeStamp = now ( ) ;
2021-05-18 17:26:16 +02:00
emotionRes = parent . config . face . emotion . enabled ? await emotion . predict ( faces [ i ] . image , parent . config , i , faces . length ) : { } ;
2021-05-31 16:40:07 +02:00
parent . performance . emotion = Math . trunc ( now ( ) - timeStamp ) ;
2021-03-21 12:49:55 +01:00
}
parent . analyze ( 'End Emotion:' ) ;
2021-03-21 19:18:51 +01:00
// run emotion, inherits face from blazeface
parent . analyze ( 'Start Description:' ) ;
if ( parent . config . async ) {
2021-05-18 17:26:16 +02:00
descRes = parent . config . face . description . enabled ? faceres . predict ( faces [ i ] , parent . config , i , faces . length ) : [ ] ;
2021-03-21 19:18:51 +01:00
} else {
parent . state = 'run:description' ;
timeStamp = now ( ) ;
2021-05-18 17:26:16 +02:00
descRes = parent . config . face . description . enabled ? await faceres . predict ( faces [ i ] . image , parent . config , i , faces . length ) : [ ] ;
2021-05-31 16:40:07 +02:00
parent . performance . embedding = Math . trunc ( now ( ) - timeStamp ) ;
2021-03-21 19:18:51 +01:00
}
parent . analyze ( 'End Description:' ) ;
2021-03-21 12:49:55 +01:00
// if async wait for results
if ( parent . config . async ) {
2021-03-21 19:18:51 +01:00
[ ageRes , genderRes , emotionRes , embeddingRes , descRes ] = await Promise . all ( [ ageRes , genderRes , emotionRes , embeddingRes , descRes ] ) ;
2021-03-21 12:49:55 +01:00
}
parent . analyze ( 'Finish Face:' ) ;
// calculate iris distance
// iris: array[ center, left, top, right, bottom]
2021-05-18 17:26:16 +02:00
if ( ! parent . config . face . iris . enabled && faces [ i ] ? . annotations ? . leftEyeIris && faces [ i ] ? . annotations ? . rightEyeIris ) {
delete faces [ i ] . annotations . leftEyeIris ;
delete faces [ i ] . annotations . rightEyeIris ;
2021-03-21 12:49:55 +01:00
}
2021-05-18 17:26:16 +02:00
const irisSize = ( faces [ i ] . annotations ? . leftEyeIris && faces [ i ] . annotations ? . rightEyeIris )
2021-05-24 13:16:38 +02:00
/* note: average human iris size is 11.7mm */
? Math . max ( Math . abs ( faces [ i ] . annotations . leftEyeIris [ 3 ] [ 0 ] - faces [ i ] . annotations . leftEyeIris [ 1 ] [ 0 ] ) , Math . abs ( faces [ i ] . annotations . rightEyeIris [ 4 ] [ 1 ] - faces [ i ] . annotations . rightEyeIris [ 2 ] [ 1 ] ) ) / input . shape [ 2 ]
2021-03-21 12:49:55 +01:00
: 0 ;
// combine results
faceRes . push ( {
2021-05-18 17:26:16 +02:00
. . . faces [ i ] ,
2021-06-01 13:37:17 +02:00
id : i ,
2021-04-25 00:43:59 +02:00
age : descRes.age ,
gender : descRes.gender ,
2021-06-01 14:59:09 +02:00
genderScore : descRes.genderScore ,
2021-04-25 00:43:59 +02:00
embedding : descRes.descriptor ,
2021-03-21 12:49:55 +01:00
emotion : emotionRes ,
2021-05-24 13:16:38 +02:00
iris : irisSize !== 0 ? Math . trunc ( 500 / irisSize / 11.7 ) / 100 : 0 ,
2021-03-28 14:40:39 +02:00
rotation ,
2021-06-01 13:37:17 +02:00
tensor : parent.config.face.detector.return ? tf . squeeze ( faces [ i ] . image ) : null ,
2021-03-21 12:49:55 +01:00
} ) ;
// dispose original face tensor
2021-06-01 13:37:17 +02:00
tf . dispose ( faces [ i ] . image ) ;
2021-06-02 19:35:33 +02:00
// delete temp face image
if ( faces [ i ] . image ) delete faces [ i ] . image ;
2021-03-21 12:49:55 +01:00
parent . analyze ( 'End Face' ) ;
}
parent . analyze ( 'End FaceMesh:' ) ;
if ( parent . config . async ) {
2021-05-31 16:40:07 +02:00
if ( parent . performance . face ) delete parent . performance . face ;
if ( parent . performance . age ) delete parent . performance . age ;
if ( parent . performance . gender ) delete parent . performance . gender ;
if ( parent . performance . emotion ) delete parent . performance . emotion ;
2021-03-21 12:49:55 +01:00
}
return faceRes ;
} ;