mirror of https://github.com/vladmandic/human
new face rotation calculations
parent
1fc99eb1a7
commit
54195efd2a
|
@ -9,11 +9,12 @@ Repository: **<git+https://github.com/vladmandic/human.git>**
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
### **HEAD -> main** 2021/03/27 mandic00@live.com
|
### **HEAD -> main** 2021/03/28 animethemegadget@gmail.com
|
||||||
|
|
||||||
|
|
||||||
### **origin/main** 2021/03/27 mandic00@live.com
|
|
||||||
|
|
||||||
|
- rotationmatrixtoeulerangle, and fixes
|
||||||
|
- experimental: add efficientpose
|
||||||
|
- implement nanodet
|
||||||
|
- face rotation matrix
|
||||||
- start working on efficientpose
|
- start working on efficientpose
|
||||||
|
|
||||||
### **1.2.5** 2021/03/25 mandic00@live.com
|
### **1.2.5** 2021/03/25 mandic00@live.com
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -136,7 +136,7 @@ export async function face(inCanvas: HTMLCanvasElement, result: Array<any>) {
|
||||||
const emotion = f.emotion.map((a) => `${Math.trunc(100 * a.score)}% ${a.emotion}`);
|
const emotion = f.emotion.map((a) => `${Math.trunc(100 * a.score)}% ${a.emotion}`);
|
||||||
labels.push(emotion.join(' '));
|
labels.push(emotion.join(' '));
|
||||||
}
|
}
|
||||||
if (f.angle && f.angle.roll) labels.push(`roll: ${Math.trunc(100 * f.angle.roll) / 100} yaw:${Math.trunc(100 * f.angle.yaw) / 100} pitch:${Math.trunc(100 * f.angle.pitch) / 100}`);
|
if (f.rotation && f.rotation.angle && f.rotation.angle.roll) labels.push(`roll: ${Math.trunc(100 * f.rotation.angle.roll) / 100} yaw:${Math.trunc(100 * f.rotation.angle.yaw) / 100} pitch:${Math.trunc(100 * f.rotation.angle.pitch) / 100}`);
|
||||||
if (labels.length === 0) labels.push('face');
|
if (labels.length === 0) labels.push('face');
|
||||||
ctx.fillStyle = drawOptions.color;
|
ctx.fillStyle = drawOptions.color;
|
||||||
for (let i = labels.length - 1; i >= 0; i--) {
|
for (let i = labels.length - 1; i >= 0; i--) {
|
||||||
|
|
100
src/faceall.ts
100
src/faceall.ts
|
@ -8,76 +8,77 @@ import * as faceres from './faceres/faceres';
|
||||||
|
|
||||||
type Tensor = typeof tf.Tensor;
|
type Tensor = typeof tf.Tensor;
|
||||||
|
|
||||||
const calculateFaceAngle = (face, image_size): { pitch: number, yaw: number, row: number, matrix: [number, number, number, number, number, number, number, number, number] } => {
|
const calculateFaceAngle = (face, image_size): { angle: { pitch: number, yaw: number, roll: number }, matrix: [number, number, number, number, number, number, number, number, number] } => {
|
||||||
// normalize vector
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||||
function normalize(v) {
|
const degrees = (theta) => (theta * 180) / Math.PI;
|
||||||
|
// const degrees = (theta) => Math.abs(((theta * 180) / Math.PI) % 360);
|
||||||
|
const normalize = (v) => { // 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)
|
||||||
// vector subtraction (a - b)
|
|
||||||
function subVectors(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)
|
||||||
// vector cross product (a x b)
|
|
||||||
function crossVectors(a, 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
|
const rotationMatrixToEulerAngle = (r) => {
|
||||||
// https://www.geometrictools.com/Documentation/EulerAngles.pdf
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||||
function rotationMatrixToEulerAngle(r) {
|
|
||||||
// r01 is not used yet (no-unused-vars)
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const [r00, r01, r02, r10, r11, r12, r20, r21, r22] = r;
|
const [r00, r01, r02, r10, r11, r12, r20, r21, r22] = r;
|
||||||
const pi = Math.PI;
|
|
||||||
|
|
||||||
let thetaX; let thetaY; let thetaZ;
|
let thetaX; let thetaY; let thetaZ;
|
||||||
|
if (r10 < 1) { // YZX calculation
|
||||||
// YZX
|
|
||||||
if (r10 < 1) {
|
|
||||||
if (r10 > -1) {
|
if (r10 > -1) {
|
||||||
thetaZ = Math.asin(r10);
|
thetaZ = Math.asin(r10);
|
||||||
// thetaY = Math.atan2(-r20, r00);
|
|
||||||
thetaX = Math.atan2(-r12, r11);
|
thetaX = Math.atan2(-r12, r11);
|
||||||
} else {
|
} else {
|
||||||
thetaZ = -pi / 2;
|
thetaZ = -Math.PI / 2;
|
||||||
// thetaY = -Math.atan2(r21, r22);
|
|
||||||
thetaX = 0;
|
thetaX = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
thetaZ = pi / 2;
|
thetaZ = Math.PI / 2;
|
||||||
// thetaY = Math.atan2(r21, r22);
|
|
||||||
thetaX = 0;
|
thetaX = 0;
|
||||||
}
|
}
|
||||||
|
if (r02 < 1) { // compensate Y rotation which is not accurate and too small in YZX calculation
|
||||||
// compensate Y rotation (from XYZ rotation order routine) which is not accurate and too small in YZX calculation
|
|
||||||
if (r02 < 1) {
|
|
||||||
if (r02 > -1) {
|
if (r02 > -1) {
|
||||||
thetaY = Math.asin(r02);
|
thetaY = Math.asin(r02);
|
||||||
} else {
|
} else {
|
||||||
thetaY = -pi / 2;
|
thetaY = -Math.PI / 2;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
thetaY = pi / 2;
|
thetaY = Math.PI / 2;
|
||||||
}
|
|
||||||
|
|
||||||
// pitch, yaw, row
|
|
||||||
return [-thetaX, -thetaY, -thetaZ];
|
|
||||||
}
|
}
|
||||||
|
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 = {
|
||||||
|
// values are in radians in range of -pi/2 to pi/2 which is -90 to +90 degrees
|
||||||
|
// value of 0 means center
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
const mesh = face.meshRaw;
|
const mesh = face.meshRaw;
|
||||||
if (!mesh || mesh.length < 300) return { pitch: 0, yaw: 0, row: 0, matrix: [1, 0, 0, 0, 1, 0, 0, 0, 1] };
|
if (!mesh || mesh.length < 300) return { angle: { pitch: 0, yaw: 0, roll: 0 }, matrix: [1, 0, 0, 0, 1, 0, 0, 0, 1] };
|
||||||
|
|
||||||
const size = Math.max(face.boxRaw[2] * image_size[0], face.boxRaw[3] * image_size[1]) / 1.5;
|
const size = Math.max(face.boxRaw[2] * image_size[0], face.boxRaw[3] * image_size[1]) / 1.5;
|
||||||
// top, bottom, left, right
|
// top, bottom, left, right
|
||||||
|
@ -96,20 +97,14 @@ const calculateFaceAngle = (face, image_size): { pitch: number, yaw: number, row
|
||||||
|
|
||||||
// Rotation Matrix from Axis Vectors - http://renderdan.blogspot.com/2006/05/rotation-matrix-from-axis-vectors.html
|
// Rotation Matrix from Axis Vectors - http://renderdan.blogspot.com/2006/05/rotation-matrix-from-axis-vectors.html
|
||||||
// 3x3 rotation matrix is flatten to array in row-major order. Note that the rotation represented by this matrix is inverted.
|
// 3x3 rotation matrix is flatten to array in row-major order. Note that the rotation represented by this matrix is inverted.
|
||||||
const r_matrix = [
|
const matrix: [number, number, number, number, number, number, number, number, number] = [
|
||||||
x_axis[0], x_axis[1], x_axis[2],
|
x_axis[0], x_axis[1], x_axis[2],
|
||||||
y_axis[0], y_axis[1], y_axis[2],
|
y_axis[0], y_axis[1], y_axis[2],
|
||||||
z_axis[0], z_axis[1], z_axis[2],
|
z_axis[0], z_axis[1], z_axis[2],
|
||||||
];
|
];
|
||||||
|
const angle = rotationMatrixToEulerAngle(matrix);
|
||||||
const [pitch, yaw, row] = rotationMatrixToEulerAngle(r_matrix);
|
// const angle = meshToEulerAngle(mesh);
|
||||||
|
return { angle, matrix };
|
||||||
return {
|
|
||||||
pitch,
|
|
||||||
yaw,
|
|
||||||
row,
|
|
||||||
matrix: r_matrix,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const detectFace = async (parent, input): Promise<any> => {
|
export const detectFace = async (parent, input): Promise<any> => {
|
||||||
|
@ -136,7 +131,10 @@ export const detectFace = async (parent, input): Promise<any> => {
|
||||||
emotion: string,
|
emotion: string,
|
||||||
embedding: number[],
|
embedding: number[],
|
||||||
iris: number,
|
iris: number,
|
||||||
angle: { pitch: number, yaw: number, row: number, matrix: [number, number, number, number, number, number, number, number, number] },
|
rotation: {
|
||||||
|
angle: { pitch: number, yaw: number, roll: number },
|
||||||
|
matrix: [number, number, number, number, number, number, number, number, number]
|
||||||
|
},
|
||||||
tensor: Tensor,
|
tensor: Tensor,
|
||||||
}> = [];
|
}> = [];
|
||||||
parent.state = 'run:face';
|
parent.state = 'run:face';
|
||||||
|
@ -153,7 +151,7 @@ export const detectFace = async (parent, input): Promise<any> => {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const angle = calculateFaceAngle(face, [input.shape[2], input.shape[1]]);
|
const rotation = calculateFaceAngle(face, [input.shape[2], input.shape[1]]);
|
||||||
|
|
||||||
// run age, inherits face from blazeface
|
// run age, inherits face from blazeface
|
||||||
parent.analyze('Start Age:');
|
parent.analyze('Start Age:');
|
||||||
|
@ -240,7 +238,7 @@ export const detectFace = async (parent, input): Promise<any> => {
|
||||||
embedding: descRes.descriptor || embeddingRes,
|
embedding: descRes.descriptor || embeddingRes,
|
||||||
emotion: emotionRes,
|
emotion: emotionRes,
|
||||||
iris: (irisSize !== 0) ? Math.trunc(irisSize) / 100 : 0,
|
iris: (irisSize !== 0) ? Math.trunc(irisSize) / 100 : 0,
|
||||||
angle,
|
rotation,
|
||||||
tensor: parent.config.face.detector.return ? face.image?.squeeze() : null,
|
tensor: parent.config.face.detector.return ? face.image?.squeeze() : null,
|
||||||
});
|
});
|
||||||
// dispose original face tensor
|
// dispose original face tensor
|
||||||
|
|
2
wiki
2
wiki
|
@ -1 +1 @@
|
||||||
Subproject commit 11e68676b2bf9aadd7ff1e2cc80dd4a70052f9d3
|
Subproject commit aae30ba3904eb7d76847895e11c55926e35ad00b
|
Loading…
Reference in New Issue