2020-10-12 01:22:43 +02:00
/* eslint-disable class-methods-use-this */
2020-11-10 14:57:39 +01:00
import { tf } from '../tf.js' ;
2020-11-10 02:13:38 +01:00
import * as bounding from './box' ;
import * as util from './util' ;
2020-11-12 20:52:32 +01:00
import * as coords from './coords.js' ;
2020-10-12 01:22:43 +02:00
const LANDMARKS _COUNT = 468 ;
const MESH _MOUTH _INDEX = 13 ;
2020-11-12 20:52:32 +01:00
const MESH _KEYPOINTS _LINE _OF _SYMMETRY _INDICES = [ MESH _MOUTH _INDEX , coords . MESH _ANNOTATIONS [ 'midwayBetweenEyes' ] [ 0 ] ] ;
2020-10-12 01:22:43 +02:00
const BLAZEFACE _MOUTH _INDEX = 3 ;
const BLAZEFACE _NOSE _INDEX = 2 ;
const BLAZEFACE _KEYPOINTS _LINE _OF _SYMMETRY _INDICES = [ BLAZEFACE _MOUTH _INDEX , BLAZEFACE _NOSE _INDEX ] ;
2020-11-12 20:52:32 +01:00
const LEFT _EYE _OUTLINE = coords . MESH _ANNOTATIONS [ 'leftEyeLower0' ] ;
2020-10-12 01:22:43 +02:00
const LEFT _EYE _BOUNDS = [ LEFT _EYE _OUTLINE [ 0 ] , LEFT _EYE _OUTLINE [ LEFT _EYE _OUTLINE . length - 1 ] ] ;
2020-11-12 20:52:32 +01:00
const RIGHT _EYE _OUTLINE = coords . MESH _ANNOTATIONS [ 'rightEyeLower0' ] ;
2020-10-12 01:22:43 +02:00
const RIGHT _EYE _BOUNDS = [ RIGHT _EYE _OUTLINE [ 0 ] , RIGHT _EYE _OUTLINE [ RIGHT _EYE _OUTLINE . length - 1 ] ] ;
const IRIS _UPPER _CENTER _INDEX = 3 ;
const IRIS _LOWER _CENTER _INDEX = 4 ;
const IRIS _IRIS _INDEX = 71 ;
const IRIS _NUM _COORDINATES = 76 ;
2020-10-14 19:23:02 +02:00
2020-10-12 01:22:43 +02:00
// Replace the raw coordinates returned by facemesh with refined iris model coordinates. Update the z coordinate to be an average of the original and the new. This produces the best visual effect.
function replaceRawCoordinates ( rawCoords , newCoords , prefix , keys ) {
2020-11-12 20:52:32 +01:00
for ( let i = 0 ; i < coords . MESH _TO _IRIS _INDICES _MAP . length ; i ++ ) {
const { key , indices } = coords . MESH _TO _IRIS _INDICES _MAP [ i ] ;
const originalIndices = coords . MESH _ANNOTATIONS [ ` ${ prefix } ${ key } ` ] ;
2020-10-12 01:22:43 +02:00
const shouldReplaceAllKeys = keys == null ;
if ( shouldReplaceAllKeys || keys . includes ( key ) ) {
for ( let j = 0 ; j < indices . length ; j ++ ) {
const index = indices [ j ] ;
rawCoords [ originalIndices [ j ] ] = [
newCoords [ index ] [ 0 ] , newCoords [ index ] [ 1 ] ,
( newCoords [ index ] [ 2 ] + rawCoords [ originalIndices [ j ] ] [ 2 ] ) / 2 ,
] ;
}
}
}
}
// The Pipeline coordinates between the bounding box and skeleton models.
class Pipeline {
constructor ( boundingBoxDetector , meshDetector , irisModel , config ) {
// An array of facial bounding boxes.
2020-11-09 20:26:10 +01:00
this . storedBoxes = [ ] ;
2020-10-12 01:22:43 +02:00
this . runsWithoutFaceDetector = 0 ;
this . boundingBoxDetector = boundingBoxDetector ;
this . meshDetector = meshDetector ;
this . irisModel = irisModel ;
this . meshWidth = config . mesh . inputSize ;
this . meshHeight = config . mesh . inputSize ;
2020-10-14 19:23:02 +02:00
this . irisSize = config . iris . inputSize ;
2020-11-08 18:26:45 +01:00
this . irisEnlarge = 2.3 ;
2020-11-09 20:26:10 +01:00
this . skipped = 1000 ;
this . detectedFaces = 0 ;
2020-10-12 01:22:43 +02:00
}
transformRawCoords ( rawCoords , box , angle , rotationMatrix ) {
const boxSize = bounding . getBoxSize ( { startPoint : box . startPoint , endPoint : box . endPoint } ) ;
const scaleFactor = [ boxSize [ 0 ] / this . meshWidth , boxSize [ 1 ] / this . meshHeight ] ;
const coordsScaled = rawCoords . map ( ( coord ) => ( [
scaleFactor [ 0 ] * ( coord [ 0 ] - this . meshWidth / 2 ) ,
scaleFactor [ 1 ] * ( coord [ 1 ] - this . meshHeight / 2 ) , coord [ 2 ] ,
] ) ) ;
const coordsRotationMatrix = util . buildRotationMatrix ( angle , [ 0 , 0 ] ) ;
const coordsRotated = coordsScaled . map ( ( coord ) => ( [ ... util . rotatePoint ( coord , coordsRotationMatrix ) , coord [ 2 ] ] ) ) ;
const inverseRotationMatrix = util . invertTransformMatrix ( rotationMatrix ) ;
const boxCenter = [ ... bounding . getBoxCenter ( { startPoint : box . startPoint , endPoint : box . endPoint } ) , 1 ] ;
const originalBoxCenter = [
util . dot ( boxCenter , inverseRotationMatrix [ 0 ] ) ,
util . dot ( boxCenter , inverseRotationMatrix [ 1 ] ) ,
] ;
return coordsRotated . map ( ( coord ) => ( [
coord [ 0 ] + originalBoxCenter [ 0 ] ,
coord [ 1 ] + originalBoxCenter [ 1 ] , coord [ 2 ] ,
] ) ) ;
}
getLeftToRightEyeDepthDifference ( rawCoords ) {
const leftEyeZ = rawCoords [ LEFT _EYE _BOUNDS [ 0 ] ] [ 2 ] ;
const rightEyeZ = rawCoords [ RIGHT _EYE _BOUNDS [ 0 ] ] [ 2 ] ;
return leftEyeZ - rightEyeZ ;
}
// Returns a box describing a cropped region around the eye fit for passing to the iris model.
getEyeBox ( rawCoords , face , eyeInnerCornerIndex , eyeOuterCornerIndex , flip = false ) {
2020-10-14 19:23:02 +02:00
const box = bounding . squarifyBox ( bounding . enlargeBox ( this . calculateLandmarksBoundingBox ( [ rawCoords [ eyeInnerCornerIndex ] , rawCoords [ eyeOuterCornerIndex ] ] ) , this . irisEnlarge ) ) ;
2020-10-12 01:22:43 +02:00
const boxSize = bounding . getBoxSize ( box ) ;
let crop = tf . image . cropAndResize ( face , [ [
box . startPoint [ 1 ] / this . meshHeight ,
box . startPoint [ 0 ] / this . meshWidth , box . endPoint [ 1 ] / this . meshHeight ,
box . endPoint [ 0 ] / this . meshWidth ,
2020-10-14 19:23:02 +02:00
] ] , [ 0 ] , [ this . irisSize , this . irisSize ] ) ;
2020-10-12 01:22:43 +02:00
if ( flip ) {
crop = tf . image . flipLeftRight ( crop ) ;
}
return { box , boxSize , crop } ;
}
// Given a cropped image of an eye, returns the coordinates of the contours surrounding the eye and the iris.
getEyeCoords ( eyeData , eyeBox , eyeBoxSize , flip = false ) {
const eyeRawCoords = [ ] ;
for ( let i = 0 ; i < IRIS _NUM _COORDINATES ; i ++ ) {
const x = eyeData [ i * 3 ] ;
const y = eyeData [ i * 3 + 1 ] ;
const z = eyeData [ i * 3 + 2 ] ;
eyeRawCoords . push ( [
( flip
2020-10-14 19:23:02 +02:00
? ( 1 - ( x / this . irisSize ) )
: ( x / this . irisSize ) ) * eyeBoxSize [ 0 ] + eyeBox . startPoint [ 0 ] ,
( y / this . irisSize ) * eyeBoxSize [ 1 ] + eyeBox . startPoint [ 1 ] , z ,
2020-10-12 01:22:43 +02:00
] ) ;
}
return { rawCoords : eyeRawCoords , iris : eyeRawCoords . slice ( IRIS _IRIS _INDEX ) } ;
}
// The z-coordinates returned for the iris are unreliable, so we take the z values from the surrounding keypoints.
getAdjustedIrisCoords ( rawCoords , irisCoords , direction ) {
2020-11-12 20:52:32 +01:00
const upperCenterZ = rawCoords [ coords . MESH _ANNOTATIONS [ ` ${ direction } EyeUpper0 ` ] [ IRIS _UPPER _CENTER _INDEX ] ] [ 2 ] ;
const lowerCenterZ = rawCoords [ coords . MESH _ANNOTATIONS [ ` ${ direction } EyeLower0 ` ] [ IRIS _LOWER _CENTER _INDEX ] ] [ 2 ] ;
2020-10-12 01:22:43 +02:00
const averageZ = ( upperCenterZ + lowerCenterZ ) / 2 ;
// Iris indices: 0: center | 1: right | 2: above | 3: left | 4: below
return irisCoords . map ( ( coord , i ) => {
let z = averageZ ;
if ( i === 2 ) {
z = upperCenterZ ;
} else if ( i === 4 ) {
z = lowerCenterZ ;
}
return [ coord [ 0 ] , coord [ 1 ] , z ] ;
} ) ;
}
2020-10-14 19:23:02 +02:00
async predict ( input , config ) {
2020-11-09 20:26:10 +01:00
this . skipped ++ ;
let useFreshBox = false ;
// run new detector every skipFrames unless we only want box to start with
2020-11-06 19:50:16 +01:00
let detector ;
2020-11-09 20:26:10 +01:00
if ( ( this . skipped > config . detector . skipFrames ) || ! config . mesh . enabled ) {
detector = await this . boundingBoxDetector . getBoundingBoxes ( input ) ;
// don't reset on test image
if ( ( input . shape [ 1 ] !== 255 ) && ( input . shape [ 2 ] !== 255 ) ) this . skipped = 0 ;
}
// if detector result count doesn't match current working set, use it to reset current working set
if ( detector && detector . boxes && ( detector . boxes . length > 0 ) && ( ! config . mesh . enabled || ( detector . boxes . length !== this . detectedFaces ) && ( this . detectedFaces !== config . detector . maxFaces ) ) ) {
this . storedBoxes = [ ] ;
this . detectedFaces = 0 ;
for ( const possible of detector . boxes ) {
this . storedBoxes . push ( { startPoint : possible . box . startPoint . dataSync ( ) , endPoint : possible . box . endPoint . dataSync ( ) , landmarks : possible . landmarks , confidence : possible . confidence } ) ;
}
if ( this . storedBoxes . length > 0 ) useFreshBox = true ;
}
2020-11-06 19:50:16 +01:00
if ( useFreshBox ) {
2020-11-04 07:11:24 +01:00
if ( ! detector || ! detector . boxes || ( detector . boxes . length === 0 ) ) {
2020-11-09 20:26:10 +01:00
this . storedBoxes = [ ] ;
2020-11-06 19:50:16 +01:00
this . detectedFaces = 0 ;
2020-10-12 01:22:43 +02:00
return null ;
}
2020-11-09 20:26:10 +01:00
for ( const i in this . storedBoxes ) {
const scaledBox = bounding . scaleBoxCoordinates ( { startPoint : this . storedBoxes [ i ] . startPoint , endPoint : this . storedBoxes [ i ] . endPoint } , detector . scaleFactor ) ;
2020-10-12 01:22:43 +02:00
const enlargedBox = bounding . enlargeBox ( scaledBox ) ;
2020-11-09 20:26:10 +01:00
const landmarks = this . storedBoxes [ i ] . landmarks . arraySync ( ) ;
const confidence = this . storedBoxes [ i ] . confidence ;
this . storedBoxes [ i ] = { ... enlargedBox , confidence , landmarks } ;
}
2020-11-06 22:21:20 +01:00
this . runsWithoutFaceDetector = 0 ;
}
if ( detector && detector . boxes ) {
detector . boxes . forEach ( ( prediction ) => {
2020-10-17 16:06:02 +02:00
prediction . box . startPoint . dispose ( ) ;
prediction . box . endPoint . dispose ( ) ;
2020-10-13 04:01:35 +02:00
prediction . landmarks . dispose ( ) ;
2020-10-12 01:22:43 +02:00
} ) ;
}
2020-11-09 20:26:10 +01:00
// console.log(this.skipped, config.detector.skipFrames, this.detectedFaces, config.detector.maxFaces, detector?.boxes.length, this.storedBoxes.length);
let results = tf . tidy ( ( ) => this . storedBoxes . map ( ( box , i ) => {
2020-10-12 01:22:43 +02:00
let angle = 0 ;
// The facial bounding box landmarks could come either from blazeface (if we are using a fresh box), or from the mesh model (if we are reusing an old box).
const boxLandmarksFromMeshModel = box . landmarks . length >= LANDMARKS _COUNT ;
let [ indexOfMouth , indexOfForehead ] = MESH _KEYPOINTS _LINE _OF _SYMMETRY _INDICES ;
if ( boxLandmarksFromMeshModel === false ) {
[ indexOfMouth , indexOfForehead ] = BLAZEFACE _KEYPOINTS _LINE _OF _SYMMETRY _INDICES ;
}
angle = util . computeRotation ( box . landmarks [ indexOfMouth ] , box . landmarks [ indexOfForehead ] ) ;
const faceCenter = bounding . getBoxCenter ( { startPoint : box . startPoint , endPoint : box . endPoint } ) ;
const faceCenterNormalized = [ faceCenter [ 0 ] / input . shape [ 2 ] , faceCenter [ 1 ] / input . shape [ 1 ] ] ;
let rotatedImage = input ;
let rotationMatrix = util . IDENTITY _MATRIX ;
if ( angle !== 0 ) {
rotatedImage = tf . image . rotateWithOffset ( input , angle , 0 , faceCenterNormalized ) ;
rotationMatrix = util . buildRotationMatrix ( - angle , faceCenter ) ;
}
2020-11-13 22:13:35 +01:00
const face = bounding . cutBoxFromImageAndResize ( { startPoint : box . startPoint , endPoint : box . endPoint } , rotatedImage , [ this . meshHeight , this . meshWidth ] ) . div ( 255 ) ;
const outputFace = config . detector . rotation ? tf . image . rotateWithOffset ( face , angle ) : face ;
2020-11-09 20:26:10 +01:00
// if we're not going to produce mesh, don't spend time with further processing
if ( ! config . mesh . enabled ) {
const prediction = {
coords : null ,
box ,
faceConfidence : null ,
confidence : box . confidence ,
2020-11-13 22:13:35 +01:00
image : outputFace ,
2020-11-09 20:26:10 +01:00
} ;
return prediction ;
}
2020-10-12 01:22:43 +02:00
// The first returned tensor represents facial contours, which are included in the coordinates.
2020-11-12 20:52:32 +01:00
const [ , confidence , contourCoords ] = this . meshDetector . predict ( face ) ;
2020-11-06 19:50:16 +01:00
const confidenceVal = confidence . dataSync ( ) [ 0 ] ;
confidence . dispose ( ) ;
if ( confidenceVal < config . detector . minConfidence ) {
2020-11-12 20:52:32 +01:00
contourCoords . dispose ( ) ;
2020-11-06 19:50:16 +01:00
return null ;
}
2020-11-12 20:52:32 +01:00
const coordsReshaped = tf . reshape ( contourCoords , [ - 1 , 3 ] ) ;
2020-10-12 01:22:43 +02:00
let rawCoords = coordsReshaped . arraySync ( ) ;
2020-10-14 19:23:02 +02:00
if ( config . iris . enabled ) {
2020-10-12 01:22:43 +02:00
const { box : leftEyeBox , boxSize : leftEyeBoxSize , crop : leftEyeCrop } = this . getEyeBox ( rawCoords , face , LEFT _EYE _BOUNDS [ 0 ] , LEFT _EYE _BOUNDS [ 1 ] , true ) ;
const { box : rightEyeBox , boxSize : rightEyeBoxSize , crop : rightEyeCrop } = this . getEyeBox ( rawCoords , face , RIGHT _EYE _BOUNDS [ 0 ] , RIGHT _EYE _BOUNDS [ 1 ] ) ;
const eyePredictions = ( this . irisModel . predict ( tf . concat ( [ leftEyeCrop , rightEyeCrop ] ) ) ) ;
const eyePredictionsData = eyePredictions . dataSync ( ) ;
2020-10-13 04:01:35 +02:00
eyePredictions . dispose ( ) ;
2020-10-12 01:22:43 +02:00
const leftEyeData = eyePredictionsData . slice ( 0 , IRIS _NUM _COORDINATES * 3 ) ;
const { rawCoords : leftEyeRawCoords , iris : leftIrisRawCoords } = this . getEyeCoords ( leftEyeData , leftEyeBox , leftEyeBoxSize , true ) ;
const rightEyeData = eyePredictionsData . slice ( IRIS _NUM _COORDINATES * 3 ) ;
const { rawCoords : rightEyeRawCoords , iris : rightIrisRawCoords } = this . getEyeCoords ( rightEyeData , rightEyeBox , rightEyeBoxSize ) ;
const leftToRightEyeDepthDifference = this . getLeftToRightEyeDepthDifference ( rawCoords ) ;
if ( Math . abs ( leftToRightEyeDepthDifference ) < 30 ) { // User is looking straight ahead.
replaceRawCoordinates ( rawCoords , leftEyeRawCoords , 'left' ) ;
replaceRawCoordinates ( rawCoords , rightEyeRawCoords , 'right' ) ;
// If the user is looking to the left or to the right, the iris coordinates tend to diverge too much from the mesh coordinates for them to be merged. So we only update a single contour line above and below the eye.
} else if ( leftToRightEyeDepthDifference < 1 ) { // User is looking towards the right.
replaceRawCoordinates ( rawCoords , leftEyeRawCoords , 'left' , [ 'EyeUpper0' , 'EyeLower0' ] ) ;
} else { // User is looking towards the left.
replaceRawCoordinates ( rawCoords , rightEyeRawCoords , 'right' , [ 'EyeUpper0' , 'EyeLower0' ] ) ;
}
const adjustedLeftIrisCoords = this . getAdjustedIrisCoords ( rawCoords , leftIrisRawCoords , 'left' ) ;
const adjustedRightIrisCoords = this . getAdjustedIrisCoords ( rawCoords , rightIrisRawCoords , 'right' ) ;
rawCoords = rawCoords . concat ( adjustedLeftIrisCoords ) . concat ( adjustedRightIrisCoords ) ;
}
const transformedCoordsData = this . transformRawCoords ( rawCoords , box , angle , rotationMatrix ) ;
tf . dispose ( rawCoords ) ;
const landmarksBox = bounding . enlargeBox ( this . calculateLandmarksBoundingBox ( transformedCoordsData ) ) ;
2020-11-09 20:26:10 +01:00
const transformedCoords = tf . tensor2d ( transformedCoordsData ) ;
2020-10-12 01:22:43 +02:00
const prediction = {
2020-11-09 20:26:10 +01:00
coords : transformedCoords ,
2020-10-12 01:22:43 +02:00
box : landmarksBox ,
2020-11-09 20:26:10 +01:00
faceConfidence : confidenceVal ,
confidence : box . confidence ,
2020-11-13 22:13:35 +01:00
image : outputFace ,
2020-10-12 01:22:43 +02:00
} ;
2020-11-09 20:26:10 +01:00
this . storedBoxes [ i ] = { ... landmarksBox , landmarks : transformedCoords . arraySync ( ) , confidence : box . confidence , faceConfidence : confidenceVal } ;
2020-10-12 01:22:43 +02:00
return prediction ;
} ) ) ;
2020-11-06 19:50:16 +01:00
results = results . filter ( ( a ) => a !== null ) ;
this . detectedFaces = results . length ;
2020-10-13 04:01:35 +02:00
return results ;
2020-10-12 01:22:43 +02:00
}
calculateLandmarksBoundingBox ( landmarks ) {
const xs = landmarks . map ( ( d ) => d [ 0 ] ) ;
const ys = landmarks . map ( ( d ) => d [ 1 ] ) ;
const startPoint = [ Math . min ( ... xs ) , Math . min ( ... ys ) ] ;
const endPoint = [ Math . max ( ... xs ) , Math . max ( ... ys ) ] ;
2020-10-17 16:06:02 +02:00
return { startPoint , endPoint , landmarks } ;
2020-10-12 01:22:43 +02:00
}
}
exports . Pipeline = Pipeline ;