human/src/draw/face.ts

159 lines
7.4 KiB
TypeScript
Raw Normal View History

2022-04-11 17:45:24 +02:00
import { TRI468 as triangulation } from '../face/facemeshcoords';
import { mergeDeep } from '../util/util';
2022-10-18 16:18:40 +02:00
import { getCanvasContext, rad2deg, rect, point, lines, arrow, labels, replace } from './primitives';
2022-04-11 17:45:24 +02:00
import { options } from './options';
2022-05-30 03:12:18 +02:00
import * as facemeshConstants from '../face/constants';
2022-04-11 17:45:24 +02:00
import type { FaceResult } from '../result';
import type { AnyCanvas, DrawOptions } from '../exports';
2022-10-18 16:18:40 +02:00
let localOptions: DrawOptions;
2022-04-18 17:29:45 +02:00
function drawLabels(f: FaceResult, ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D) {
2022-10-18 16:18:40 +02:00
if (!localOptions.drawLabels || (localOptions.faceLabels?.length === 0)) return;
let l = localOptions.faceLabels.slice();
if (f.score) l = replace(l, '[score]', 100 * f.score);
if (f.gender) l = replace(l, '[gender]', f.gender);
if (f.genderScore) l = replace(l, '[genderScore]', 100 * f.genderScore);
if (f.age) l = replace(l, '[age]', f.age);
2022-11-16 23:47:28 +01:00
if (f.iris) l = replace(l, '[distance]', 100 * f.iris);
2022-10-18 16:18:40 +02:00
if (f.real) l = replace(l, '[real]', 100 * f.real);
if (f.live) l = replace(l, '[live]', 100 * f.live);
if (f.emotion && f.emotion.length > 0) {
const emotion = f.emotion.map((a) => `${Math.trunc(100 * a.score)}% ${a.emotion}`);
if (emotion.length > 3) emotion.length = 3;
l = replace(l, '[emotions]', emotion.join(' '));
2022-04-18 17:29:45 +02:00
}
2022-10-18 16:18:40 +02:00
if (f.rotation?.angle?.roll) l = replace(l, '[roll]', rad2deg(f.rotation.angle.roll));
if (f.rotation?.angle?.yaw) l = replace(l, '[yaw]', rad2deg(f.rotation.angle.yaw));
if (f.rotation?.angle?.pitch) l = replace(l, '[pitch]', rad2deg(f.rotation.angle.pitch));
if (f.rotation?.gaze?.bearing) l = replace(l, '[gaze]', rad2deg(f.rotation.gaze.bearing));
labels(ctx, l, f.box[0], f.box[1], localOptions);
2022-04-18 17:29:45 +02:00
}
function drawIrisElipse(f: FaceResult, ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D) {
// iris: array[center, left, top, right, bottom]
2022-08-21 21:23:03 +02:00
if (f.annotations?.leftEyeIris && f.annotations?.leftEyeIris[0]) {
2022-10-18 16:18:40 +02:00
ctx.strokeStyle = localOptions.useDepth ? 'rgba(255, 200, 255, 0.3)' : localOptions.color;
2022-04-18 17:29:45 +02:00
ctx.beginPath();
2022-08-21 19:34:51 +02:00
const sizeX = Math.abs(f.annotations.leftEyeIris[3][0] - f.annotations.leftEyeIris[1][0]) / 2;
const sizeY = Math.abs(f.annotations.leftEyeIris[4][1] - f.annotations.leftEyeIris[2][1]) / 2;
ctx.ellipse(f.annotations.leftEyeIris[0][0], f.annotations.leftEyeIris[0][1], sizeX, sizeY, 0, 0, 2 * Math.PI);
2022-04-18 17:29:45 +02:00
ctx.stroke();
2022-10-18 16:18:40 +02:00
if (localOptions.fillPolygons) {
ctx.fillStyle = localOptions.useDepth ? 'rgba(255, 255, 200, 0.3)' : localOptions.color;
2022-04-18 17:29:45 +02:00
ctx.fill();
}
}
2022-08-21 21:23:03 +02:00
if (f.annotations?.rightEyeIris && f.annotations?.rightEyeIris[0]) {
2022-10-18 16:18:40 +02:00
ctx.strokeStyle = localOptions.useDepth ? 'rgba(255, 200, 255, 0.3)' : localOptions.color;
2022-04-18 17:29:45 +02:00
ctx.beginPath();
2022-08-21 19:34:51 +02:00
const sizeX = Math.abs(f.annotations.rightEyeIris[3][0] - f.annotations.rightEyeIris[1][0]) / 2;
const sizeY = Math.abs(f.annotations.rightEyeIris[4][1] - f.annotations.rightEyeIris[2][1]) / 2;
ctx.ellipse(f.annotations.rightEyeIris[0][0], f.annotations.rightEyeIris[0][1], sizeX, sizeY, 0, 0, 2 * Math.PI);
2022-04-18 17:29:45 +02:00
ctx.stroke();
2022-10-18 16:18:40 +02:00
if (localOptions.fillPolygons) {
ctx.fillStyle = localOptions.useDepth ? 'rgba(255, 255, 200, 0.3)' : localOptions.color;
2022-04-18 17:29:45 +02:00
ctx.fill();
}
}
}
function drawGazeSpheres(f: FaceResult, ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D) {
2022-10-18 16:18:40 +02:00
if (localOptions.drawGaze && f.rotation?.angle && typeof Path2D !== 'undefined') {
2022-04-18 17:29:45 +02:00
ctx.strokeStyle = 'pink';
const valX = (f.box[0] + f.box[2] / 2) - (f.box[3] * rad2deg(f.rotation.angle.yaw) / 90);
const valY = (f.box[1] + f.box[3] / 2) + (f.box[2] * rad2deg(f.rotation.angle.pitch) / 90);
const pathV = new Path2D(`
M ${f.box[0] + f.box[2] / 2} ${f.box[1]}
C
${valX} ${f.box[1]},
${valX} ${f.box[1] + f.box[3]},
${f.box[0] + f.box[2] / 2} ${f.box[1] + f.box[3]}
`);
const pathH = new Path2D(`
M ${f.box[0]} ${f.box[1] + f.box[3] / 2}
C
${f.box[0]} ${valY},
${f.box[0] + f.box[2]} ${valY},
${f.box[0] + f.box[2]} ${f.box[1] + f.box[3] / 2}
`);
ctx.stroke(pathH);
ctx.stroke(pathV);
}
}
function drawGazeArrows(f: FaceResult, ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D) {
2022-10-18 16:18:40 +02:00
if (localOptions.drawGaze && f.rotation?.gaze.strength && f.rotation.gaze.bearing && f.annotations.leftEyeIris && f.annotations.rightEyeIris && f.annotations.leftEyeIris[0] && f.annotations.rightEyeIris[0]) {
2022-04-18 17:29:45 +02:00
ctx.strokeStyle = 'pink';
ctx.fillStyle = 'pink';
const leftGaze = [
2022-08-21 19:34:51 +02:00
f.annotations.leftEyeIris[0][0] + (Math.sin(f.rotation.gaze.bearing) * f.rotation.gaze.strength * f.box[3]),
f.annotations.leftEyeIris[0][1] + (Math.cos(f.rotation.gaze.bearing) * f.rotation.gaze.strength * f.box[2]),
2022-04-18 17:29:45 +02:00
];
2022-08-21 19:34:51 +02:00
arrow(ctx, [f.annotations.leftEyeIris[0][0], f.annotations.leftEyeIris[0][1]], [leftGaze[0], leftGaze[1]], 4);
2022-04-18 17:29:45 +02:00
const rightGaze = [
2022-08-21 19:34:51 +02:00
f.annotations.rightEyeIris[0][0] + (Math.sin(f.rotation.gaze.bearing) * f.rotation.gaze.strength * f.box[3]),
f.annotations.rightEyeIris[0][1] + (Math.cos(f.rotation.gaze.bearing) * f.rotation.gaze.strength * f.box[2]),
2022-04-18 17:29:45 +02:00
];
2022-08-21 19:34:51 +02:00
arrow(ctx, [f.annotations.rightEyeIris[0][0], f.annotations.rightEyeIris[0][1]], [rightGaze[0], rightGaze[1]], 4);
2022-04-18 17:29:45 +02:00
}
}
function drawFacePolygons(f: FaceResult, ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D) {
2022-10-18 16:18:40 +02:00
if (localOptions.drawPolygons && f.mesh.length >= 468) {
2022-04-18 17:29:45 +02:00
ctx.lineWidth = 1;
for (let i = 0; i < triangulation.length / 3; i++) {
const points = [triangulation[i * 3 + 0], triangulation[i * 3 + 1], triangulation[i * 3 + 2]].map((index) => f.mesh[index]);
2022-10-18 16:18:40 +02:00
lines(ctx, points, localOptions);
2022-04-18 17:29:45 +02:00
}
drawIrisElipse(f, ctx);
}
2022-07-13 18:08:23 +02:00
/*
2022-10-18 16:18:40 +02:00
if (localOptions.drawPolygons && f.contours.length > 1) {
2022-07-13 18:08:23 +02:00
ctx.lineWidth = 5;
lines(ctx, f.contours, opt);
}
ctx.lineWidth = 1;
*/
2022-04-18 17:29:45 +02:00
}
function drawFacePoints(f: FaceResult, ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D) {
2022-10-18 16:18:40 +02:00
if (localOptions.drawPoints && f.mesh.length >= 468) {
2022-04-18 18:26:05 +02:00
for (let i = 0; i < f.mesh.length; i++) {
2022-10-18 16:18:40 +02:00
point(ctx, f.mesh[i][0], f.mesh[i][1], f.mesh[i][2], localOptions);
if (localOptions.drawAttention) {
if (facemeshConstants.LANDMARKS_REFINEMENT_LIPS_CONFIG.includes(i)) point(ctx, f.mesh[i][0], f.mesh[i][1], (f.mesh[i][2] as number) + 127, localOptions);
if (facemeshConstants.LANDMARKS_REFINEMENT_LEFT_EYE_CONFIG.includes(i)) point(ctx, f.mesh[i][0], f.mesh[i][1], (f.mesh[i][2] as number) - 127, localOptions);
if (facemeshConstants.LANDMARKS_REFINEMENT_RIGHT_EYE_CONFIG.includes(i)) point(ctx, f.mesh[i][0], f.mesh[i][1], (f.mesh[i][2] as number) - 127, localOptions);
2022-04-18 18:26:05 +02:00
}
}
2022-04-18 17:29:45 +02:00
}
}
function drawFaceBoxes(f: FaceResult, ctx) {
2022-10-18 16:18:40 +02:00
if (localOptions.drawBoxes) {
rect(ctx, f.box[0], f.box[1], f.box[2], f.box[3], localOptions);
2022-04-18 17:29:45 +02:00
}
}
2022-04-11 17:45:24 +02:00
/** draw detected faces */
2022-08-21 21:23:03 +02:00
export function face(inCanvas: AnyCanvas, result: FaceResult[], drawOptions?: Partial<DrawOptions>) {
2022-10-18 16:18:40 +02:00
localOptions = mergeDeep(options, drawOptions);
2022-04-11 17:45:24 +02:00
if (!result || !inCanvas) return;
2022-11-04 18:20:56 +01:00
const ctx = getCanvasContext(inCanvas) as CanvasRenderingContext2D;
2022-04-11 17:45:24 +02:00
if (!ctx) return;
2022-10-18 16:18:40 +02:00
ctx.font = localOptions.font;
ctx.strokeStyle = localOptions.color;
ctx.fillStyle = localOptions.color;
2022-04-11 17:45:24 +02:00
for (const f of result) {
2022-04-18 17:29:45 +02:00
drawFaceBoxes(f, ctx);
drawLabels(f, ctx);
2022-04-11 17:45:24 +02:00
if (f.mesh && f.mesh.length > 0) {
2022-04-18 17:29:45 +02:00
drawFacePoints(f, ctx);
drawFacePolygons(f, ctx);
drawGazeSpheres(f, ctx);
drawGazeArrows(f, ctx);
2022-04-11 17:45:24 +02:00
}
}
}