update face angle algorithm

pull/46/head
Vladimir Mandic 2021-03-08 08:55:51 -05:00
parent c0d2eda2d7
commit 77fb56eb1a
21 changed files with 61 additions and 53 deletions

View File

@ -525,6 +525,9 @@ export type WithFaceLandmarks<TSource> = TSource & {
unshiftedLandmarks: FaceLandmarks unshiftedLandmarks: FaceLandmarks
landmarks: FaceLandmarks landmarks: FaceLandmarks
alignedRect: FaceDetection alignedRect: FaceDetection
angle: { roll: number, yaw: number, pitch: number }
// for angle all values are in radians in range of -pi/2 to pi/2 which is -90 to +90 degrees
// value of 0 means center
} }
``` ```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1221,7 +1221,7 @@
] ]
}, },
"src/factories/WithFaceLandmarks.ts": { "src/factories/WithFaceLandmarks.ts": {
"bytes": 3288, "bytes": 3592,
"imports": [ "imports": [
{ {
"path": "src/classes/FaceDetection.ts", "path": "src/classes/FaceDetection.ts",
@ -2591,7 +2591,7 @@
"imports": [], "imports": [],
"exports": [], "exports": [],
"inputs": {}, "inputs": {},
"bytes": 315561 "bytes": 315931
}, },
"dist/face-api.esm-nobundle.js": { "dist/face-api.esm-nobundle.js": {
"imports": [], "imports": [],
@ -2981,7 +2981,7 @@
"bytesInOutput": 420 "bytesInOutput": 420
}, },
"src/factories/WithFaceLandmarks.ts": { "src/factories/WithFaceLandmarks.ts": {
"bytesInOutput": 784 "bytesInOutput": 829
}, },
"src/draw/DrawFaceLandmarks.ts": { "src/draw/DrawFaceLandmarks.ts": {
"bytesInOutput": 1225 "bytesInOutput": 1225
@ -3188,7 +3188,7 @@
"bytesInOutput": 443 "bytesInOutput": 443
} }
}, },
"bytes": 82590 "bytes": 82635
} }
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1221,7 +1221,7 @@
] ]
}, },
"src/factories/WithFaceLandmarks.ts": { "src/factories/WithFaceLandmarks.ts": {
"bytes": 3288, "bytes": 3592,
"imports": [ "imports": [
{ {
"path": "src/classes/FaceDetection.ts", "path": "src/classes/FaceDetection.ts",
@ -2591,7 +2591,7 @@
"imports": [], "imports": [],
"exports": [], "exports": [],
"inputs": {}, "inputs": {},
"bytes": 1466296 "bytes": 1466666
}, },
"dist/face-api.esm.js": { "dist/face-api.esm.js": {
"imports": [], "imports": [],
@ -2978,7 +2978,7 @@
"bytesInOutput": 422 "bytesInOutput": 422
}, },
"src/factories/WithFaceLandmarks.ts": { "src/factories/WithFaceLandmarks.ts": {
"bytesInOutput": 790 "bytesInOutput": 835
}, },
"src/draw/DrawFaceLandmarks.ts": { "src/draw/DrawFaceLandmarks.ts": {
"bytesInOutput": 1228 "bytesInOutput": 1228
@ -3188,7 +3188,7 @@
"bytesInOutput": 446 "bytesInOutput": 446
} }
}, },
"bytes": 1127159 "bytes": 1127204
} }
} }
} }

2
dist/face-api.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
dist/face-api.json vendored
View File

@ -1221,7 +1221,7 @@
] ]
}, },
"src/factories/WithFaceLandmarks.ts": { "src/factories/WithFaceLandmarks.ts": {
"bytes": 3288, "bytes": 3592,
"imports": [ "imports": [
{ {
"path": "src/classes/FaceDetection.ts", "path": "src/classes/FaceDetection.ts",
@ -2591,7 +2591,7 @@
"imports": [], "imports": [],
"exports": [], "exports": [],
"inputs": {}, "inputs": {},
"bytes": 1466303 "bytes": 1466673
}, },
"dist/face-api.js": { "dist/face-api.js": {
"imports": [], "imports": [],
@ -2860,7 +2860,7 @@
"bytesInOutput": 422 "bytesInOutput": 422
}, },
"src/factories/WithFaceLandmarks.ts": { "src/factories/WithFaceLandmarks.ts": {
"bytesInOutput": 790 "bytesInOutput": 835
}, },
"src/draw/DrawFaceLandmarks.ts": { "src/draw/DrawFaceLandmarks.ts": {
"bytesInOutput": 1228 "bytesInOutput": 1228
@ -3067,7 +3067,7 @@
"bytesInOutput": 446 "bytesInOutput": 446
} }
}, },
"bytes": 1127322 "bytes": 1127367
} }
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1221,7 +1221,7 @@
] ]
}, },
"src/factories/WithFaceLandmarks.ts": { "src/factories/WithFaceLandmarks.ts": {
"bytes": 3288, "bytes": 3592,
"imports": [ "imports": [
{ {
"path": "src/classes/FaceDetection.ts", "path": "src/classes/FaceDetection.ts",
@ -2591,7 +2591,7 @@
"imports": [], "imports": [],
"exports": [], "exports": [],
"inputs": {}, "inputs": {},
"bytes": 315437 "bytes": 315807
}, },
"dist/face-api.node-cpu.js": { "dist/face-api.node-cpu.js": {
"imports": [], "imports": [],
@ -2860,7 +2860,7 @@
"bytesInOutput": 420 "bytesInOutput": 420
}, },
"src/factories/WithFaceLandmarks.ts": { "src/factories/WithFaceLandmarks.ts": {
"bytesInOutput": 784 "bytesInOutput": 829
}, },
"src/draw/DrawFaceLandmarks.ts": { "src/draw/DrawFaceLandmarks.ts": {
"bytesInOutput": 1225 "bytesInOutput": 1225
@ -3067,7 +3067,7 @@
"bytesInOutput": 443 "bytesInOutput": 443
} }
}, },
"bytes": 83302 "bytes": 83347
} }
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1221,7 +1221,7 @@
] ]
}, },
"src/factories/WithFaceLandmarks.ts": { "src/factories/WithFaceLandmarks.ts": {
"bytes": 3288, "bytes": 3592,
"imports": [ "imports": [
{ {
"path": "src/classes/FaceDetection.ts", "path": "src/classes/FaceDetection.ts",
@ -2591,7 +2591,7 @@
"imports": [], "imports": [],
"exports": [], "exports": [],
"inputs": {}, "inputs": {},
"bytes": 315446 "bytes": 315816
}, },
"dist/face-api.node-gpu.js": { "dist/face-api.node-gpu.js": {
"imports": [], "imports": [],
@ -2860,7 +2860,7 @@
"bytesInOutput": 420 "bytesInOutput": 420
}, },
"src/factories/WithFaceLandmarks.ts": { "src/factories/WithFaceLandmarks.ts": {
"bytesInOutput": 784 "bytesInOutput": 829
}, },
"src/draw/DrawFaceLandmarks.ts": { "src/draw/DrawFaceLandmarks.ts": {
"bytesInOutput": 1225 "bytesInOutput": 1225
@ -3067,7 +3067,7 @@
"bytesInOutput": 443 "bytesInOutput": 443
} }
}, },
"bytes": 83311 "bytes": 83356
} }
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1221,7 +1221,7 @@
] ]
}, },
"src/factories/WithFaceLandmarks.ts": { "src/factories/WithFaceLandmarks.ts": {
"bytes": 3288, "bytes": 3592,
"imports": [ "imports": [
{ {
"path": "src/classes/FaceDetection.ts", "path": "src/classes/FaceDetection.ts",
@ -2591,7 +2591,7 @@
"imports": [], "imports": [],
"exports": [], "exports": [],
"inputs": {}, "inputs": {},
"bytes": 315438 "bytes": 315808
}, },
"dist/face-api.node.js": { "dist/face-api.node.js": {
"imports": [], "imports": [],
@ -2860,7 +2860,7 @@
"bytesInOutput": 420 "bytesInOutput": 420
}, },
"src/factories/WithFaceLandmarks.ts": { "src/factories/WithFaceLandmarks.ts": {
"bytesInOutput": 784 "bytesInOutput": 829
}, },
"src/draw/DrawFaceLandmarks.ts": { "src/draw/DrawFaceLandmarks.ts": {
"bytesInOutput": 1225 "bytesInOutput": 1225
@ -3067,7 +3067,7 @@
"bytesInOutput": 443 "bytesInOutput": 443
} }
}, },
"bytes": 83303 "bytes": 83348
} }
} }
} }

View File

@ -46,7 +46,7 @@ function drawFaces(canvas, data, fps) {
ctx.fillText(`gender ${Math.round(100 * person.genderProbability)}% ${person.gender}`, person.detection.box.x, person.detection.box.y - 60); ctx.fillText(`gender ${Math.round(100 * person.genderProbability)}% ${person.gender}`, person.detection.box.x, person.detection.box.y - 60);
ctx.fillText(`expression ${Math.round(100 * expression[0][1])}% ${expression[0][0]}`, person.detection.box.x, person.detection.box.y - 42); ctx.fillText(`expression ${Math.round(100 * expression[0][1])}% ${expression[0][0]}`, person.detection.box.x, person.detection.box.y - 42);
ctx.fillText(`age ${Math.round(person.age)} years`, person.detection.box.x, person.detection.box.y - 24); ctx.fillText(`age ${Math.round(person.age)} years`, person.detection.box.x, person.detection.box.y - 24);
ctx.fillText(`roll:${Math.trunc(1000 * person.angle.roll) / 1000} pitch:${Math.trunc(1000 * person.angle.pitch) / 1000} yaw:${Math.trunc(1000 * person.angle.yaw) / 1000}`, person.detection.box.x, person.detection.box.y - 6); ctx.fillText(`roll:${person.angle.roll.toFixed(3)} pitch:${person.angle.pitch.toFixed(3)} yaw:${person.angle.yaw.toFixed(3)}`, person.detection.box.x, person.detection.box.y - 6);
// draw face points for each face // draw face points for each face
ctx.fillStyle = 'lightblue'; ctx.fillStyle = 'lightblue';
ctx.globalAlpha = 0.5; ctx.globalAlpha = 0.5;

View File

@ -23,31 +23,36 @@ export function isWithFaceLandmarks(obj: any): obj is WithFaceLandmarks<WithFace
} }
function calculateFaceAngle(mesh) { function calculateFaceAngle(mesh) {
const radians = (a1, a2, b1, b2) => Math.atan2(b2 - a2, b1 - a1); // returns the angle in the plane (in radians) between the positive x-axis and the ray from (0,0) to the point (x,y)
const radians = (a1, a2, b1, b2) => (Math.atan2(b2 - a2, b1 - a1) % Math.PI);
// convert radians to degrees
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const degrees = (theta) => (theta * 180) / Math.PI;
const angle = { roll: <number | undefined>undefined, pitch: <number | undefined>undefined, yaw: <number | undefined>undefined }; const angle = { roll: <number | undefined>undefined, pitch: <number | undefined>undefined, yaw: <number | undefined>undefined };
if (!mesh || !mesh._positions || mesh._positions.length !== 68) return angle; if (!mesh || !mesh._positions || mesh._positions.length !== 68) return angle;
const pt = mesh._positions; const pt = mesh._positions;
// roll is face lean left/right // values are in radians in range of -pi/2 to pi/2 which is -90 to +90 degrees
// value of 0 means center
// roll is face lean from left to right
// comparing x,y of outside corners of leftEye and rightEye // comparing x,y of outside corners of leftEye and rightEye
angle.roll = radians(pt[36]._x, pt[36]._y, pt[45]._x, pt[45]._y); angle.roll = -radians(pt[36]._x, pt[36]._y, pt[45]._x, pt[45]._y);
// yaw is face turn left/right // pitch is face turn from left right
// comparing x distance of bottom of nose to left and right edge of face // comparing x distance of top of nose to left and right edge of face
// and y distance of top of nose to left and right edge of face
// precision is lacking since coordinates are not precise enough // precision is lacking since coordinates are not precise enough
angle.pitch = radians(pt[30]._x - pt[0]._x, pt[27]._y - pt[0]._y, pt[16]._x - pt[30]._x, pt[27]._y - pt[16]._y); angle.pitch = radians(0, Math.abs(pt[0]._x - pt[30]._x) / pt[30]._x, Math.PI, Math.abs(pt[16]._x - pt[30]._x) / pt[30]._x);
// pitch is face move up/down // yaw is face move from up to down
// comparing size of the box around the face with top and bottom of detected landmarks // comparing size of the box around the face with top and bottom of detected landmarks
// silly hack, but this gives us face compression on y-axis // silly hack, but this gives us face compression on y-axis
// e.g., tilting head up hides the forehead that doesn't have any landmarks so ratio drops // e.g., tilting head up hides the forehead that doesn't have any landmarks so ratio drops
// value is normalized to range, but is not in actual radians
const bottom = pt.reduce((prev, cur) => (prev < cur._y ? prev : cur._y), +Infinity); const bottom = pt.reduce((prev, cur) => (prev < cur._y ? prev : cur._y), +Infinity);
const top = pt.reduce((prev, cur) => (prev > cur._y ? prev : cur._y), -Infinity); const top = pt.reduce((prev, cur) => (prev > cur._y ? prev : cur._y), -Infinity);
angle.yaw = 10 * (mesh._imgDims._height / (top - bottom) / 1.45 - 1); angle.yaw = Math.PI * (mesh._imgDims._height / (top - bottom) / 1.40 - 1);
return angle; return angle;
} }