diff --git a/README.md b/README.md
index 7c23cce3..85f04618 100644
--- a/README.md
+++ b/README.md
@@ -13,13 +13,14 @@ Compatible with *Browser*, *WebWorker* and *NodeJS* execution on both Windows an
- Browser/WebWorker: Compatible with *CPU*, *WebGL*, *WASM* and *WebGPU* backends
- NodeJS: Compatible with software *tfjs-node* and CUDA accelerated backends *tfjs-node-gpu*
-Check out [**Live Demo**](https://vladmandic.github.io/human/demo/index.html) for processing of live WebCam video or static images
+Check out [**Live Demo**](https://vladmandic.github.io/human/demo/index.html) for processing of live WebCam video or static images
+Live demo uses `WASM` backend for faster startup, but results are slower than when using `WebGL` backend
## Project pages
-- [**Live Demo**](https://vladmandic.github.io/human/demo/index.html)
+- [**Live Demo**](https://vladmandic.github.io/human/demo/index.html)
- [**Code Repository**](https://github.com/vladmandic/human)
- [**NPM Package**](https://www.npmjs.com/package/@vladmandic/human)
- [**Issues Tracker**](https://github.com/vladmandic/human/issues)
@@ -102,5 +103,38 @@ As presented in the demo application...

+
+
+Example simple app that uses Human to process video input and
+draw output on screen using internal draw helper functions
+
+```js
+import Human from '@vladmandic/human';
+
+// create instance of human with simple configuration using default values
+const config = { backend: 'wasm' };
+const human = new Human(config);
+
+function detectVideo() {
+ // select input HTMLVideoElement and output HTMLCanvasElement from page
+ const inputVideo = document.getElementById('video-id');
+ const outputCanvas = document.getElementById('canvas-id');
+ // perform processing using default configuration
+ human.detect(inputVideo).then((result) => {
+ // result object will contain detected details as well as the processed canvas itself
+ // first draw processed frame on canvas
+ human.draw.canvas(result.canvas, outputCanvas);
+ // then draw results on the same canvas
+ human.draw.face(outputCanvas, result.face);
+ human.draw.body(outputCanvas, result.body);
+ human.draw.hand(outputCanvas, result.hand);
+ human.draw.gesture(outputCanvas, result.gesture);
+ // loop immediate to next frame
+ requestAnimationFrame(detectVideo);
+ });
+}
+
+detectVideo();
+```
diff --git a/demo/browser.js b/demo/browser.js
index 4829406b..da05564a 100644
--- a/demo/browser.js
+++ b/demo/browser.js
@@ -4,11 +4,10 @@
import Human from '../dist/human.esm.js'; // equivalent of @vladmandic/human
-import draw from './draw.js';
import Menu from './menu.js';
import GLBench from './gl-bench.js';
-const userConfig = {}; // add any user configuration overrides
+const userConfig = { backend: 'wasm' }; // add any user configuration overrides
/*
const userConfig = {
@@ -27,40 +26,31 @@ const human = new Human(userConfig);
// ui options
const ui = {
- baseColor: 'rgba(173, 216, 230, 0.3)', // 'lightblue' with light alpha channel
baseBackground: 'rgba(50, 50, 50, 1)', // 'grey'
- baseLabel: 'rgba(173, 216, 230, 1)', // 'lightblue' with dark alpha channel
- baseFontProto: 'small-caps {size} "Segoe UI"',
- baseLineWidth: 12,
- crop: true,
- columns: 2,
- busy: false,
- facing: true,
- useWorker: false,
+ crop: true, // video mode crop to size or leave full frame
+ columns: 2, // when processing sample images create this many columns
+ facing: true, // camera facing front or back
+ useWorker: false, // use web workers for processing
worker: 'worker.js',
samples: ['../assets/sample6.jpg', '../assets/sample1.jpg', '../assets/sample4.jpg', '../assets/sample5.jpg', '../assets/sample3.jpg', '../assets/sample2.jpg'],
compare: '../assets/sample-me.jpg',
- drawLabels: true,
- drawBoxes: true,
- drawPoints: true,
- drawPolygons: true,
- fillPolygons: false,
- useDepth: true,
- console: true,
- maxFPSframes: 10,
- modelsPreload: true,
- menuWidth: 0,
- menuHeight: 0,
- camera: {},
- detectFPS: [],
- drawFPS: [],
- buffered: false,
- drawWarmup: false,
- drawThread: null,
- detectThread: null,
- framesDraw: 0,
- framesDetect: 0,
- bench: false,
+ console: true, // log messages to browser console
+ maxFPSframes: 10, // keep fps history for how many frames
+ modelsPreload: true, // preload human models on startup
+ busy: false, // internal camera busy flag
+ menuWidth: 0, // internal
+ menuHeight: 0, // internal
+ camera: {}, // internal, holds details of webcam details
+ detectFPS: [], // internal, holds fps values for detection performance
+ drawFPS: [], // internal, holds fps values for draw performance
+ buffered: false, // experimental, should output be buffered between frames
+ drawWarmup: false, // debug only, should warmup image processing be displayed on startup
+ drawThread: null, // perform draw operations in a separate thread
+ detectThread: null, // perform detect operations in a separate thread
+ framesDraw: 0, // internal, statistics on frames drawn
+ framesDetect: 0, // internal, statistics on frames detected
+ bench: false, // show gl fps benchmark window
+ lastFrame: 0, // time of last frame processing
};
// global variables
@@ -90,7 +80,8 @@ function log(...msg) {
function status(msg) {
// eslint-disable-next-line no-console
- document.getElementById('status').innerText = msg;
+ const div = document.getElementById('status');
+ if (div) div.innerText = msg;
}
let original;
@@ -139,10 +130,10 @@ async function drawResults(input) {
}
// draw all results
- await draw.face(result.face, canvas, ui, human.facemesh.triangulation);
- await draw.body(result.body, canvas, ui);
- await draw.hand(result.hand, canvas, ui);
- await draw.gesture(result.gesture, canvas, ui);
+ await human.draw.face(canvas, result.face);
+ await human.draw.body(canvas, result.body);
+ await human.draw.hand(canvas, result.hand);
+ await human.draw.gesture(canvas, result.gesture);
await calcSimmilariry(result);
// update log
@@ -230,9 +221,6 @@ async function setupCamera() {
ui.menuWidth.input.setAttribute('value', video.width);
ui.menuHeight.input.setAttribute('value', video.height);
// silly font resizing for paint-on-canvas since viewport can be zoomed
- const size = Math.trunc(window.devicePixelRatio * (8 + (4 * canvas.width / window.innerWidth)));
- ui.baseFont = ui.baseFontProto.replace(/{size}/, `${size}px`);
- ui.baseLineHeight = size + 2;
if (live) video.play();
// eslint-disable-next-line no-use-before-define
if (live && !ui.detectThread) runHumanDetect(video, canvas);
@@ -404,9 +392,6 @@ async function detectVideo() {
async function detectSampleImages() {
document.getElementById('play').style.display = 'none';
userConfig.videoOptimized = false;
- const size = Math.trunc(window.devicePixelRatio * (12 + (4 * ui.columns)));
- ui.baseFont = ui.baseFontProto.replace(/{size}/, `${size}px`);
- ui.baseLineHeight = size + 2;
document.getElementById('canvas').style.display = 'none';
document.getElementById('samples-container').style.display = 'block';
log('Running detection of sample images');
@@ -439,12 +424,12 @@ function setupMenu() {
setupCamera();
});
menu.display.addHTML('
');
- menu.display.addBool('use 3D depth', ui, 'useDepth');
- menu.display.addBool('print labels', ui, 'drawLabels');
- menu.display.addBool('draw boxes', ui, 'drawBoxes');
- menu.display.addBool('draw polygons', ui, 'drawPolygons');
- menu.display.addBool('Fill Polygons', ui, 'fillPolygons');
- menu.display.addBool('draw points', ui, 'drawPoints');
+ menu.display.addBool('use 3D depth', human.draw.options, 'useDepth');
+ menu.display.addBool('print labels', human.draw.options, 'drawLabels');
+ menu.display.addBool('draw boxes', human.draw.options, 'drawBoxes');
+ menu.display.addBool('draw polygons', human.draw.options, 'drawPolygons');
+ menu.display.addBool('Fill Polygons', human.draw.options, 'fillPolygons');
+ menu.display.addBool('draw points', human.draw.options, 'drawPoints');
menu.image = new Menu(document.body, '', { top: `${document.getElementById('menubar').offsetHeight}px`, left: x[1] });
menu.image.addBool('enabled', human.config.filter, 'enabled', (val) => human.config.filter.enabled = val);
@@ -541,10 +526,7 @@ async function drawWarmup(res) {
canvas.height = res.canvas.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(res.canvas, 0, 0, res.canvas.width, res.canvas.height, 0, 0, canvas.width, canvas.height);
- await draw.face(res.face, canvas, ui, human.facemesh.triangulation);
- await draw.body(res.body, canvas, ui);
- await draw.hand(res.hand, canvas, ui);
- await draw.gesture(res.gesture, canvas, ui);
+ await human.draw.all(canvas, res);
}
async function main() {
@@ -561,7 +543,6 @@ async function main() {
if (!ui.useWorker) {
status('initializing');
const res = await human.warmup(userConfig); // this is not required, just pre-warms all models for faster initial inference
- ui.baseFont = ui.baseFontProto.replace(/{size}/, '16px');
if (res && res.canvas && ui.drawWarmup) await drawWarmup(res);
}
status('human: ready');
diff --git a/src/draw.ts b/src/draw.ts
new file mode 100644
index 00000000..5cdcc84d
--- /dev/null
+++ b/src/draw.ts
@@ -0,0 +1,436 @@
+import { TRI468 as triangulation } from './blazeface/coords';
+
+export const options = {
+ color: 'rgba(173, 216, 230, 0.3)', // 'lightblue' with light alpha channel
+ labelColor: 'rgba(173, 216, 230, 1)', // 'lightblue' with dark alpha channel
+ shadowColor: 'black',
+ font: 'small-caps 16px "Segoe UI"',
+ lineHeight: 20,
+ lineWidth: 6,
+ pointSize: 2,
+ roundRect: 8,
+ drawLabels: true,
+ drawBoxes: true,
+ drawPoints: false,
+ drawPolygons: true,
+ fillPolygons: true,
+ useDepth: true,
+ bufferedOutput: false,
+};
+
+function point(ctx, x, y) {
+ ctx.fillStyle = options.color;
+ ctx.beginPath();
+ ctx.arc(x, y, options.pointSize, 0, 2 * Math.PI);
+ ctx.fill();
+}
+
+function rect(ctx, x, y, width, height) {
+ if (options.roundRect && options.roundRect > 0) {
+ ctx.lineWidth = options.lineWidth;
+ ctx.beginPath();
+ ctx.moveTo(x + options.roundRect, y);
+ ctx.lineTo(x + width - options.roundRect, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + options.roundRect);
+ ctx.lineTo(x + width, y + height - options.roundRect);
+ ctx.quadraticCurveTo(x + width, y + height, x + width - options.roundRect, y + height);
+ ctx.lineTo(x + options.roundRect, y + height);
+ ctx.quadraticCurveTo(x, y + height, x, y + height - options.roundRect);
+ ctx.lineTo(x, y + options.roundRect);
+ ctx.quadraticCurveTo(x, y, x + options.roundRect, y);
+ ctx.closePath();
+ ctx.stroke();
+ } else {
+ rect(ctx, x, y, width, height);
+ }
+}
+
+// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
+function curve(ctx, points = []) {
+ if (points.length < 2) return;
+ ctx.lineWidth = options.lineWidth;
+ ctx.beginPath();
+ ctx.moveTo(points[0][0], points[0][1]);
+ for (let i = 0; i < points.length - 1; i++) {
+ const xMid = (points[i][0] + points[i + 1][0]) / 2;
+ const yMid = (points[i][1] + points[i + 1][1]) / 2;
+ const cpX1 = (xMid + points[i][0]) / 2;
+ const cpX2 = (xMid + points[i + 1][1]) / 2;
+ ctx.quadraticCurveTo(cpX1, points[i][1], xMid, yMid);
+ ctx.quadraticCurveTo(cpX2, points[i + 1][1], points[i + 1][0], points[i + 1][0]);
+ }
+ ctx.strokeStyle = options.color;
+ ctx.stroke();
+}
+
+// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
+function bezier(ctx, points) {
+ const tension = 0; // tension at 0 will be straight line
+ const factor = 1; // factor is normally 1, but changing the value can control the smoothness too
+ if (points.length < 2) return;
+ ctx.lineWidth = options.lineWidth;
+ ctx.strokeStyle = options.color;
+ ctx.fillStyle = options.color;
+ ctx.beginPath();
+ ctx.moveTo(points[0][0], points[0][1]);
+ let dx1 = 0;
+ let dy1 = 0;
+ let preP = points[0];
+ for (let i = 1; i < points.length; i++) {
+ const curP = points[i];
+ const nexP = points[i + 1];
+ const m = nexP ? (nexP[1] - preP[1]) / (nexP[0] - preP[0]) : 0;
+ const dx2 = nexP ? (nexP[0] - curP[0]) * -factor : 0;
+ const dy2 = nexP ? dx2 * m * tension : 0;
+ ctx.bezierCurveTo(preP[0] - dx1, preP[1] - dy1, curP[0] + dx2, curP[1] + dy2, curP[0], curP[1]);
+ dx1 = dx2;
+ dy1 = dy2;
+ preP = curP;
+ }
+ ctx.stroke();
+}
+
+// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
+function spline(ctx, points) {
+ const tension = 0.8;
+ if (points.length < 2) return;
+ const va = (arr, i, j) => [arr[2 * j] - arr[2 * i], arr[2 * j + 1] - arr[2 * i + 1]];
+ const distance = (arr, i, j) => Math.sqrt(((arr[2 * i] - arr[2 * j]) ** 2) + ((arr[2 * i + 1] - arr[2 * j + 1]) ** 2));
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
+ const ctlpts = (x1, y1, x2, y2, x3, y3) => {
+ // eslint-disable-next-line prefer-rest-params
+ const v = va(arguments, 0, 2);
+ // eslint-disable-next-line prefer-rest-params
+ const d01 = distance(arguments, 0, 1);
+ // eslint-disable-next-line prefer-rest-params
+ const d12 = distance(arguments, 1, 2);
+ const d012 = d01 + d12;
+ return [
+ x2 - v[0] * tension * d01 / d012, y2 - v[1] * tension * d01 / d012,
+ x2 + v[0] * tension * d12 / d012, y2 + v[1] * tension * d12 / d012,
+ ];
+ };
+ const pts: any[] = [];
+ for (const pt of points) {
+ pts.push(pt[0]);
+ pts.push(pt[1]);
+ }
+ let cps = [];
+ for (let i = 0; i < pts.length - 2; i += 1) {
+ // @ts-ignore
+ cps = cps.concat(ctlpts(pts[2 * i + 0], pts[2 * i + 1], pts[2 * i + 2], pts[2 * i + 3], pts[2 * i + 4], pts[2 * i + 5]));
+ }
+ ctx.lineWidth = options.lineWidth;
+ ctx.strokeStyle = options.color;
+ if (points.length === 2) {
+ ctx.beginPath();
+ ctx.moveTo(pts[0], pts[1]);
+ ctx.lineTo(pts[2], pts[3]);
+ } else {
+ ctx.beginPath();
+ ctx.moveTo(pts[0], pts[1]);
+ // first segment is a quadratic
+ ctx.quadraticCurveTo(cps[0], cps[1], pts[2], pts[3]);
+ // for all middle points, connect with bezier
+ let i;
+ for (i = 2; i < ((pts.length / 2) - 1); i += 1) {
+ ctx.bezierCurveTo(cps[(2 * (i - 1) - 1) * 2], cps[(2 * (i - 1) - 1) * 2 + 1], cps[(2 * (i - 1)) * 2], cps[(2 * (i - 1)) * 2 + 1], pts[i * 2], pts[i * 2 + 1]);
+ }
+ // last segment is a quadratic
+ ctx.quadraticCurveTo(cps[(2 * (i - 1) - 1) * 2], cps[(2 * (i - 1) - 1) * 2 + 1], pts[i * 2], pts[i * 2 + 1]);
+ }
+ ctx.stroke();
+}
+
+export async function gesture(inCanvas, result) {
+ if (!result || !inCanvas) return;
+ if (!(inCanvas instanceof HTMLCanvasElement)) return;
+ const ctx = inCanvas.getContext('2d');
+ if (!ctx) return;
+ ctx.font = options.font;
+ ctx.fillStyle = options.color;
+ let i = 1;
+ for (let j = 0; j < result.length; j++) {
+ let where:any[] = [];
+ let what:any[] = [];
+ [where, what] = Object.entries(result[j]);
+ if ((what.length > 1) && (what[1].length > 0)) {
+ const person = where[1] > 0 ? `#${where[1]}` : '';
+ const label = `${where[0]} ${person}: ${what[1]}`;
+ if (options.shadowColor && options.shadowColor !== '') {
+ ctx.fillStyle = options.shadowColor;
+ ctx.fillText(label, 8, 2 + (i * options.lineHeight));
+ }
+ ctx.fillStyle = options.labelColor;
+ ctx.fillText(label, 6, 0 + (i * options.lineHeight));
+ i += 1;
+ }
+ }
+}
+
+export async function face(inCanvas, result) {
+ if (!result || !inCanvas) return;
+ if (!(inCanvas instanceof HTMLCanvasElement)) return;
+ const ctx = inCanvas.getContext('2d');
+ if (!ctx) return;
+ for (const f of result) {
+ ctx.font = options.font;
+ ctx.strokeStyle = options.color;
+ ctx.fillStyle = options.color;
+ ctx.lineWidth = options.lineWidth;
+ ctx.beginPath();
+ if (options.drawBoxes) {
+ rect(ctx, f.box[0], f.box[1], f.box[2], f.box[3]);
+ }
+ // silly hack since fillText does not suport new line
+ const labels:string[] = [];
+ labels.push(`face confidence: ${Math.trunc(100 * f.confidence)}%`);
+ if (f.genderConfidence) labels.push(`${f.gender || ''} ${Math.trunc(100 * f.genderConfidence)}% confident`);
+ // if (f.genderConfidence) labels.push(f.gender);
+ if (f.age) labels.push(`age: ${f.age || ''}`);
+ if (f.iris) labels.push(`iris distance: ${f.iris}`);
+ if (f.emotion && f.emotion.length > 0) {
+ const emotion = f.emotion.map((a) => `${Math.trunc(100 * a.score)}% ${a.emotion}`);
+ labels.push(emotion.join(' '));
+ }
+ if (labels.length === 0) labels.push('face');
+ ctx.fillStyle = options.color;
+ for (let i = labels.length - 1; i >= 0; i--) {
+ const x = Math.max(f.box[0], 0);
+ const y = i * options.lineHeight + f.box[1];
+ if (options.shadowColor && options.shadowColor !== '') {
+ ctx.fillStyle = options.shadowColor;
+ ctx.fillText(labels[i], x + 5, y + 16);
+ }
+ ctx.fillStyle = options.labelColor;
+ ctx.fillText(labels[i], x + 4, y + 15);
+ }
+ ctx.fillStyle = options.color;
+ ctx.stroke();
+ ctx.lineWidth = 1;
+ if (f.mesh) {
+ if (options.drawPoints) {
+ for (const pt of f.mesh) {
+ ctx.fillStyle = options.useDepth ? `rgba(${127.5 + (2 * pt[2])}, ${127.5 - (2 * pt[2])}, 255, 0.5)` : options.color;
+ point(ctx, pt[0], pt[1]);
+ }
+ }
+ if (options.drawPolygons) {
+ 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]);
+ const path = new Path2D();
+ path.moveTo(points[0][0], points[0][1]);
+ for (const pt of points) {
+ path.lineTo(pt[0], pt[1]);
+ }
+ path.closePath();
+ ctx.strokeStyle = options.useDepth ? `rgba(${127.5 + (2 * points[0][2])}, ${127.5 - (2 * points[0][2])}, 255, 0.3)` : options.color;
+ ctx.stroke(path);
+ if (options.fillPolygons) {
+ ctx.fillStyle = options.useDepth ? `rgba(${127.5 + (2 * points[0][2])}, ${127.5 - (2 * points[0][2])}, 255, 0.3)` : options.color;
+ ctx.fill(path);
+ }
+ }
+ // iris: array[center, left, top, right, bottom]
+ if (f.annotations && f.annotations.leftEyeIris) {
+ ctx.strokeStyle = options.useDepth ? 'rgba(255, 200, 255, 0.3)' : options.color;
+ ctx.beginPath();
+ 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);
+ ctx.stroke();
+ if (options.fillPolygons) {
+ ctx.fillStyle = options.useDepth ? 'rgba(255, 255, 200, 0.3)' : options.color;
+ ctx.fill();
+ }
+ }
+ if (f.annotations && f.annotations.rightEyeIris) {
+ ctx.strokeStyle = options.useDepth ? 'rgba(255, 200, 255, 0.3)' : options.color;
+ ctx.beginPath();
+ 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);
+ ctx.stroke();
+ if (options.fillPolygons) {
+ ctx.fillStyle = options.useDepth ? 'rgba(255, 255, 200, 0.3)' : options.color;
+ ctx.fill();
+ }
+ }
+ }
+ }
+ }
+}
+
+const lastDrawnPose:any[] = [];
+export async function body(inCanvas, result) {
+ if (!result || !inCanvas) return;
+ if (!(inCanvas instanceof HTMLCanvasElement)) return;
+ const ctx = inCanvas.getContext('2d');
+ if (!ctx) return;
+ ctx.lineJoin = 'round';
+ for (let i = 0; i < result.length; i++) {
+ // result[i].keypoints = result[i].keypoints.filter((a) => a.score > 0.5);
+ if (!lastDrawnPose[i] && options.bufferedOutput) lastDrawnPose[i] = { ...result[i] };
+ ctx.strokeStyle = options.color;
+ ctx.font = options.font;
+ ctx.lineWidth = options.lineWidth;
+ if (options.drawPoints) {
+ for (let pt = 0; pt < result[i].keypoints.length; pt++) {
+ ctx.fillStyle = options.useDepth && result[i].keypoints[pt].position.z ? `rgba(${127.5 + (2 * result[i].keypoints[pt].position.z)}, ${127.5 - (2 * result[i].keypoints[pt].position.z)}, 255, 0.5)` : options.color;
+ if (options.drawLabels) {
+ ctx.fillText(`${result[i].keypoints[pt].part}`, result[i].keypoints[pt][0] + 4, result[i].keypoints[pt][1] + 4);
+ }
+ ctx.beginPath();
+ if (options.bufferedOutput) {
+ lastDrawnPose[i].keypoints[pt][0] = (lastDrawnPose[i].keypoints[pt][0] + result[i].keypoints[pt][0]) / 2;
+ lastDrawnPose[i].keypoints[pt][1] = (lastDrawnPose[i].keypoints[pt][1] + result[i].keypoints[pt][1]) / 2;
+ point(ctx, lastDrawnPose[i].keypoints[pt][0], lastDrawnPose[i].keypoints[pt][1]);
+ } else {
+ point(ctx, result[i].keypoints[pt][0], result[i].keypoints[pt][1]);
+ }
+ ctx.fill();
+ }
+ }
+ if (options.drawPolygons) {
+ const path = new Path2D();
+ let root;
+ let part;
+ // torso
+ root = result[i].keypoints.find((a) => a.part === 'leftShoulder');
+ if (root) {
+ path.moveTo(root.position.x, root.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'rightShoulder');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'rightHip');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'leftHip');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'leftShoulder');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ }
+ // leg left
+ root = result[i].keypoints.find((a) => a.part === 'leftHip');
+ if (root) {
+ path.moveTo(root.position.x, root.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'leftKnee');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'leftAnkle');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'leftHeel');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'leftFoot');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ }
+ // leg right
+ root = result[i].keypoints.find((a) => a.part === 'rightHip');
+ if (root) {
+ path.moveTo(root.position.x, root.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'rightKnee');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'rightAnkle');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'rightHeel');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'rightFoot');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ }
+ // arm left
+ root = result[i].keypoints.find((a) => a.part === 'leftShoulder');
+ if (root) {
+ path.moveTo(root.position.x, root.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'leftElbow');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'leftWrist');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'leftPalm');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ }
+ // arm right
+ root = result[i].keypoints.find((a) => a.part === 'rightShoulder');
+ if (root) {
+ path.moveTo(root.position.x, root.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'rightElbow');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'rightWrist');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ part = result[i].keypoints.find((a) => a.part === 'rightPalm');
+ if (part) path.lineTo(part.position.x, part.position.y);
+ }
+ // draw all
+ ctx.stroke(path);
+ }
+ }
+}
+
+export async function hand(inCanvas, result) {
+ if (!result || !inCanvas) return;
+ if (!(inCanvas instanceof HTMLCanvasElement)) return;
+ const ctx = inCanvas.getContext('2d');
+ if (!ctx) return;
+ ctx.lineJoin = 'round';
+ for (const h of result) {
+ ctx.font = options.font;
+ ctx.lineWidth = options.lineWidth;
+ if (options.drawBoxes) {
+ ctx.lineWidth = options.lineWidth;
+ ctx.beginPath();
+ ctx.strokeStyle = options.color;
+ ctx.fillStyle = options.color;
+ rect(ctx, h.box[0], h.box[1], h.box[2], h.box[3]);
+ if (options.shadowColor && options.shadowColor !== '') {
+ ctx.fillStyle = options.shadowColor;
+ ctx.fillText('hand', h.box[0] + 3, 1 + h.box[1] + options.lineHeight, h.box[2]);
+ }
+ ctx.fillStyle = options.labelColor;
+ ctx.fillText('hand', h.box[0] + 2, 0 + h.box[1] + options.lineHeight, h.box[2]);
+ ctx.stroke();
+ }
+ if (options.drawPoints) {
+ if (h.landmarks && h.landmarks.length > 0) {
+ for (const pt of h.landmarks) {
+ ctx.fillStyle = options.useDepth ? `rgba(${127.5 + (2 * pt[2])}, ${127.5 - (2 * pt[2])}, 255, 0.5)` : options.color;
+ point(ctx, pt[0], pt[1]);
+ }
+ }
+ }
+ if (options.drawPolygons) {
+ const addPart = (part) => {
+ if (!part) return;
+ for (let i = 0; i < part.length; i++) {
+ ctx.lineWidth = options.lineWidth;
+ ctx.beginPath();
+ ctx.strokeStyle = options.useDepth ? `rgba(${127.5 + (2 * part[i][2])}, ${127.5 - (2 * part[i][2])}, 255, 0.5)` : options.color;
+ ctx.moveTo(part[i > 0 ? i - 1 : 0][0], part[i > 0 ? i - 1 : 0][1]);
+ ctx.lineTo(part[i][0], part[i][1]);
+ ctx.stroke();
+ }
+ };
+ addPart(h.annotations.indexFinger);
+ addPart(h.annotations.middleFinger);
+ addPart(h.annotations.ringFinger);
+ addPart(h.annotations.pinky);
+ addPart(h.annotations.thumb);
+ // addPart(hand.annotations.palmBase);
+ }
+ }
+}
+
+export async function canvas(inCanvas, outCanvas) {
+ if (!inCanvas || !outCanvas) return;
+ if (!(inCanvas instanceof HTMLCanvasElement) || !(outCanvas instanceof HTMLCanvasElement)) return;
+ const outCtx = inCanvas.getContext('2d');
+ outCtx?.drawImage(inCanvas, 0, 0);
+}
+
+export async function all(inCanvas, result) {
+ if (!result || !inCanvas) return;
+ if (!(inCanvas instanceof HTMLCanvasElement)) return;
+ face(inCanvas, result.face);
+ body(inCanvas, result.body);
+ hand(inCanvas, result.hand);
+ gesture(inCanvas, result.gesture);
+}
diff --git a/src/human.ts b/src/human.ts
index 68961212..0f0b3516 100644
--- a/src/human.ts
+++ b/src/human.ts
@@ -16,6 +16,7 @@ import * as profile from './profile';
import * as config from '../config';
import * as sample from './sample';
import * as app from '../package.json';
+import * as draw from './draw';
// helper function: gets elapsed time on both browser and nodejs
const now = () => {
@@ -40,6 +41,7 @@ function mergeDeep(...objects) {
class Human {
tf: any;
+ draw: any;
package: any;
version: string;
config: any;
@@ -62,6 +64,7 @@ class Human {
constructor(userConfig = {}) {
this.tf = tf;
+ this.draw = draw;
this.package = app;
this.version = app.version;
this.config = mergeDeep(config.default, userConfig);
diff --git a/wiki b/wiki
index b624d76a..9173ba06 160000
--- a/wiki
+++ b/wiki
@@ -1 +1 @@
-Subproject commit b624d76a06647aa4542a65b7454cf9a8ac57c1c3
+Subproject commit 9173ba06bb5f5aa361ea391ea88b6aaf0eb34006