human/src/util/interpolate.ts

188 lines
12 KiB
TypeScript
Raw Normal View History

/**
* Results interpolation for smoothening of video detection results inbetween detected frames
*/
2022-08-21 21:23:03 +02:00
import type { Result, FaceResult, BodyResult, HandResult, ObjectResult, PersonResult, Box, Point, BodyLandmark, BodyAnnotation } from '../result';
import type { Config } from '../config';
import * as moveNetCoords from '../body/movenetcoords';
import * as blazePoseCoords from '../body/blazeposecoords';
import * as efficientPoseCoords from '../body/efficientposecoords';
import { now } from './util';
2021-10-27 15:45:38 +02:00
import { env } from './env';
2021-11-14 17:22:52 +01:00
const bufferedResult: Result = { face: [], body: [], hand: [], gesture: [], object: [], persons: [], performance: {}, timestamp: 0, error: null };
2021-10-27 15:45:38 +02:00
let interpolateTime = 0;
export function calc(newResult: Result, config: Config): Result {
const t0 = now();
2021-11-14 17:22:52 +01:00
if (!newResult) return { face: [], body: [], hand: [], gesture: [], object: [], persons: [], performance: {}, timestamp: 0, error: null };
// each record is only updated using deep clone when number of detected record changes, otherwise it will converge by itself
// otherwise bufferedResult is a shallow clone of result plus updated local calculated values
// thus mixing by-reference and by-value assignments to minimize memory operations
const elapsed = Date.now() - newResult.timestamp;
2022-09-21 19:51:49 +02:00
/* curve fitted: buffer = 8 - ln(delay)
interpolation formula: current = ((buffer - 1) * previous + live) / buffer
- at 50ms delay buffer = ~4.1 => 28% towards live data
- at 250ms delay buffer = ~2.5 => 40% towards live data
- at 500ms delay buffer = ~1.8 => 55% towards live data
- at 750ms delay buffer = ~1.4 => 71% towards live data
- at 1sec delay buffer = 1 which means live data is used
*/
2021-08-18 20:28:31 +02:00
const bufferedFactor = elapsed < 1000 ? 8 - Math.log(elapsed + 1) : 1;
2021-11-14 17:22:52 +01:00
if (newResult.canvas) bufferedResult.canvas = newResult.canvas;
if (newResult.error) bufferedResult.error = newResult.error;
2021-06-04 19:51:01 +02:00
// interpolate body results
if (!bufferedResult.body || (newResult.body.length !== bufferedResult.body.length)) {
2022-08-21 19:34:51 +02:00
bufferedResult.body = JSON.parse(JSON.stringify(newResult.body)) as BodyResult[]; // deep clone once
} else {
for (let i = 0; i < newResult.body.length; i++) {
const box = newResult.body[i].box // update box
2021-11-23 16:40:40 +01:00
.map((newBoxCoord, j) => ((bufferedFactor - 1) * bufferedResult.body[i].box[j] + newBoxCoord) / bufferedFactor) as Box;
const boxRaw = newResult.body[i].boxRaw // update boxRaw
2021-11-23 16:40:40 +01:00
.map((newBoxCoord, j) => ((bufferedFactor - 1) * bufferedResult.body[i].boxRaw[j] + newBoxCoord) / bufferedFactor) as Box;
const keypoints = (newResult.body[i].keypoints // update keypoints
2021-11-23 16:40:40 +01:00
.map((newKpt, j) => ({
score: newKpt.score,
2022-08-21 19:34:51 +02:00
part: newKpt.part,
position: [
2021-11-23 16:40:40 +01:00
bufferedResult.body[i].keypoints[j] ? ((bufferedFactor - 1) * (bufferedResult.body[i].keypoints[j].position[0] || 0) + (newKpt.position[0] || 0)) / bufferedFactor : newKpt.position[0],
bufferedResult.body[i].keypoints[j] ? ((bufferedFactor - 1) * (bufferedResult.body[i].keypoints[j].position[1] || 0) + (newKpt.position[1] || 0)) / bufferedFactor : newKpt.position[1],
bufferedResult.body[i].keypoints[j] ? ((bufferedFactor - 1) * (bufferedResult.body[i].keypoints[j].position[2] || 0) + (newKpt.position[2] || 0)) / bufferedFactor : newKpt.position[2],
],
positionRaw: [
2021-12-31 19:58:03 +01:00
bufferedResult.body[i].keypoints[j] ? ((bufferedFactor - 1) * (bufferedResult.body[i].keypoints[j].positionRaw[0] || 0) + (newKpt.positionRaw[0] || 0)) / bufferedFactor : newKpt.positionRaw[0],
bufferedResult.body[i].keypoints[j] ? ((bufferedFactor - 1) * (bufferedResult.body[i].keypoints[j].positionRaw[1] || 0) + (newKpt.positionRaw[1] || 0)) / bufferedFactor : newKpt.positionRaw[1],
bufferedResult.body[i].keypoints[j] ? ((bufferedFactor - 1) * (bufferedResult.body[i].keypoints[j].positionRaw[2] || 0) + (newKpt.positionRaw[2] || 0)) / bufferedFactor : newKpt.positionRaw[2],
],
distance: [
bufferedResult.body[i].keypoints[j] ? ((bufferedFactor - 1) * (bufferedResult.body[i].keypoints[j].distance?.[0] || 0) + (newKpt.distance?.[0] || 0)) / bufferedFactor : newKpt.distance?.[0],
bufferedResult.body[i].keypoints[j] ? ((bufferedFactor - 1) * (bufferedResult.body[i].keypoints[j].distance?.[1] || 0) + (newKpt.distance?.[1] || 0)) / bufferedFactor : newKpt.distance?.[1],
bufferedResult.body[i].keypoints[j] ? ((bufferedFactor - 1) * (bufferedResult.body[i].keypoints[j].distance?.[2] || 0) + (newKpt.distance?.[2] || 0)) / bufferedFactor : newKpt.distance?.[2],
],
2022-08-21 19:34:51 +02:00
}))) as { score: number, part: BodyLandmark, position: [number, number, number?], positionRaw: [number, number, number?] }[];
2021-12-15 15:26:32 +01:00
const annotations: Record<BodyAnnotation, Point[][]> = {} as Record<BodyAnnotation, Point[][]>; // recreate annotations
let coords = { connected: {} };
2022-08-21 19:34:51 +02:00
if (config.body.modelPath?.includes('efficientpose')) coords = efficientPoseCoords;
else if (config.body.modelPath?.includes('blazepose')) coords = blazePoseCoords;
else if (config.body.modelPath?.includes('movenet')) coords = moveNetCoords;
for (const [name, indexes] of Object.entries(coords.connected as Record<string, string[]>)) {
2022-08-21 19:34:51 +02:00
const pt: Point[][] = [];
for (let j = 0; j < indexes.length - 1; j++) {
const pt0 = keypoints.find((kp) => kp.part === indexes[j]);
const pt1 = keypoints.find((kp) => kp.part === indexes[j + 1]);
2021-11-23 16:40:40 +01:00
// if (pt0 && pt1 && pt0.score > (config.body.minConfidence || 0) && pt1.score > (config.body.minConfidence || 0)) pt.push([pt0.position, pt1.position]);
if (pt0 && pt1) pt.push([pt0.position, pt1.position]);
}
annotations[name] = pt;
}
2022-08-21 19:34:51 +02:00
bufferedResult.body[i] = { ...newResult.body[i], box, boxRaw, keypoints, annotations }; // shallow clone plus updated values
}
}
// interpolate hand results
if (!bufferedResult.hand || (newResult.hand.length !== bufferedResult.hand.length)) {
2022-08-21 19:34:51 +02:00
bufferedResult.hand = JSON.parse(JSON.stringify(newResult.hand)); // deep clone once
} else {
for (let i = 0; i < newResult.hand.length; i++) {
const box = (newResult.hand[i].box// update box
2021-09-27 15:19:43 +02:00
.map((b, j) => ((bufferedFactor - 1) * bufferedResult.hand[i].box[j] + b) / bufferedFactor)) as Box;
const boxRaw = (newResult.hand[i].boxRaw // update boxRaw
2021-09-27 15:19:43 +02:00
.map((b, j) => ((bufferedFactor - 1) * bufferedResult.hand[i].boxRaw[j] + b) / bufferedFactor)) as Box;
2021-09-21 22:48:16 +02:00
if (bufferedResult.hand[i].keypoints.length !== newResult.hand[i].keypoints.length) bufferedResult.hand[i].keypoints = newResult.hand[i].keypoints; // reset keypoints as previous frame did not have them
const keypoints = newResult.hand[i].keypoints && newResult.hand[i].keypoints.length > 0 ? newResult.hand[i].keypoints // update landmarks
.map((landmark, j) => landmark
2021-09-27 15:19:43 +02:00
.map((coord, k) => (((bufferedFactor - 1) * (bufferedResult.hand[i].keypoints[j][k] || 1) + (coord || 0)) / bufferedFactor)) as Point)
: [];
let annotations = {};
if (Object.keys(bufferedResult.hand[i].annotations).length !== Object.keys(newResult.hand[i].annotations).length) {
bufferedResult.hand[i].annotations = newResult.hand[i].annotations; // reset annotations as previous frame did not have them
annotations = bufferedResult.hand[i].annotations;
} else if (newResult.hand[i].annotations) {
2021-09-21 22:48:16 +02:00
for (const key of Object.keys(newResult.hand[i].annotations)) { // update annotations
2022-08-21 21:23:03 +02:00
annotations[key] = newResult.hand[i]?.annotations?.[key]?.[0]
? newResult.hand[i].annotations[key]
2021-12-27 16:59:56 +01:00
.map((val, j: number) => val
.map((coord: number, k: number) => ((bufferedFactor - 1) * bufferedResult.hand[i].annotations[key][j][k] + coord) / bufferedFactor))
2021-09-21 22:48:16 +02:00
: null;
}
}
2021-09-12 05:54:35 +02:00
bufferedResult.hand[i] = { ...newResult.hand[i], box, boxRaw, keypoints, annotations: annotations as HandResult['annotations'] }; // shallow clone plus updated values
}
}
// interpolate face results
if (!bufferedResult.face || (newResult.face.length !== bufferedResult.face.length)) {
2022-08-21 19:34:51 +02:00
bufferedResult.face = JSON.parse(JSON.stringify(newResult.face)) as FaceResult[]; // deep clone once
} else {
for (let i = 0; i < newResult.face.length; i++) {
const box = (newResult.face[i].box // update box
2021-09-27 15:19:43 +02:00
.map((b, j) => ((bufferedFactor - 1) * bufferedResult.face[i].box[j] + b) / bufferedFactor)) as Box;
const boxRaw = (newResult.face[i].boxRaw // update boxRaw
2021-09-27 15:19:43 +02:00
.map((b, j) => ((bufferedFactor - 1) * bufferedResult.face[i].boxRaw[j] + b) / bufferedFactor)) as Box;
2021-11-12 21:07:23 +01:00
if (newResult.face[i].rotation) {
const rotation: {
matrix: [number, number, number, number, number, number, number, number, number],
angle: { roll: number, yaw: number, pitch: number },
gaze: { bearing: number, strength: number }
} = { matrix: [0, 0, 0, 0, 0, 0, 0, 0, 0], angle: { roll: 0, yaw: 0, pitch: 0 }, gaze: { bearing: 0, strength: 0 } };
rotation.matrix = newResult.face[i].rotation?.matrix as [number, number, number, number, number, number, number, number, number];
rotation.angle = {
2022-09-21 19:51:49 +02:00
roll: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.angle?.roll || 0) + (newResult.face[i].rotation?.angle?.roll || 0)) / bufferedFactor,
yaw: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.angle?.yaw || 0) + (newResult.face[i].rotation?.angle?.yaw || 0)) / bufferedFactor,
pitch: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.angle?.pitch || 0) + (newResult.face[i].rotation?.angle?.pitch || 0)) / bufferedFactor,
2021-11-12 21:07:23 +01:00
};
rotation.gaze = {
// not fully correct due projection on circle, also causes wrap-around draw on jump from negative to positive
2022-08-21 19:34:51 +02:00
bearing: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.gaze.bearing || 0) + (newResult.face[i].rotation?.gaze.bearing || 0)) / bufferedFactor,
strength: ((bufferedFactor - 1) * (bufferedResult.face[i].rotation?.gaze.strength || 0) + (newResult.face[i].rotation?.gaze.strength || 0)) / bufferedFactor,
2021-11-12 21:07:23 +01:00
};
bufferedResult.face[i] = { ...newResult.face[i], rotation, box, boxRaw }; // shallow clone plus updated values
2022-09-21 19:51:49 +02:00
} else {
bufferedResult.face[i] = { ...newResult.face[i], box, boxRaw }; // shallow clone plus updated values
2021-11-12 21:07:23 +01:00
}
}
}
// interpolate object detection results
if (!bufferedResult.object || (newResult.object.length !== bufferedResult.object.length)) {
2022-08-21 19:34:51 +02:00
bufferedResult.object = JSON.parse(JSON.stringify(newResult.object)) as ObjectResult[]; // deep clone once
} else {
for (let i = 0; i < newResult.object.length; i++) {
const box = (newResult.object[i].box // update box
2021-09-27 15:19:43 +02:00
.map((b, j) => ((bufferedFactor - 1) * bufferedResult.object[i].box[j] + b) / bufferedFactor)) as Box;
const boxRaw = (newResult.object[i].boxRaw // update boxRaw
2021-09-27 15:19:43 +02:00
.map((b, j) => ((bufferedFactor - 1) * bufferedResult.object[i].boxRaw[j] + b) / bufferedFactor)) as Box;
bufferedResult.object[i] = { ...newResult.object[i], box, boxRaw }; // shallow clone plus updated values
}
}
// interpolate person results
2021-06-14 16:23:06 +02:00
if (newResult.persons) {
const newPersons = newResult.persons; // trigger getter function
if (!bufferedResult.persons || (newPersons.length !== bufferedResult.persons.length)) {
2022-08-21 19:34:51 +02:00
bufferedResult.persons = JSON.parse(JSON.stringify(newPersons)) as PersonResult[];
2021-06-14 16:23:06 +02:00
} else {
for (let i = 0; i < newPersons.length; i++) { // update person box, we don't update the rest as it's updated as reference anyhow
bufferedResult.persons[i].box = (newPersons[i].box
2021-09-27 15:19:43 +02:00
.map((box, j) => ((bufferedFactor - 1) * bufferedResult.persons[i].box[j] + box) / bufferedFactor)) as Box;
2021-06-14 16:23:06 +02:00
}
}
}
// just copy latest gestures without interpolation
2022-08-21 21:23:03 +02:00
if (newResult.gesture) bufferedResult.gesture = newResult.gesture;
// append interpolation performance data
2021-10-19 17:28:59 +02:00
const t1 = now();
2021-10-27 15:45:38 +02:00
interpolateTime = env.perfadd ? interpolateTime + Math.round(t1 - t0) : Math.round(t1 - t0);
if (newResult.performance) bufferedResult.performance = { ...newResult.performance, interpolate: interpolateTime };
return bufferedResult;
}