fixed memory leaks and added scoped runs

pull/50/head
Vladimir Mandic 2020-10-17 10:06:02 -04:00
parent 12de5a71b5
commit 5754e5e36e
19 changed files with 823 additions and 745 deletions

View File

@ -4,6 +4,9 @@
export default {
backend: 'webgl', // select tfjs backend to use
console: true, // enable debugging output to console
scoped: false, // enable scoped runs
// some models *may* have memory leaks, this wrapps everything in a local scope at a cost of performance
// typically not needed
face: {
enabled: true, // controls if specified modul is enabled
// face.enabled is required for all face models: detector, mesh, iris, age, gender, emotion

View File

@ -3,6 +3,7 @@
import human from '../dist/human.esm.js';
import draw from './draw.js';
// ui options
const ui = {
baseColor: 'rgba(255, 200, 255, 0.3)',
baseLabel: 'rgba(255, 200, 255, 0.9)',
@ -20,11 +21,11 @@ const ui = {
drawPolygons: true,
fillPolygons: true,
useDepth: true,
console: true,
};
// configuration overrides
const config = {
backend: 'webgl',
console: true,
face: {
enabled: true,
detector: { maxFaces: 10, skipFrames: 10, minConfidence: 0.5, iouThreshold: 0.3, scoreThreshold: 0.7 },
@ -37,11 +38,14 @@ const config = {
body: { enabled: true, maxDetections: 10, scoreThreshold: 0.7, nmsRadius: 20 },
hand: { enabled: true, skipFrames: 10, minConfidence: 0.5, iouThreshold: 0.3, scoreThreshold: 0.7 },
};
// global variables
let settings;
let worker;
let timeStamp;
const fps = [];
// helper function: translates json to human readable string
function str(...msg) {
if (!Array.isArray(msg)) return msg;
let line = '';
@ -52,11 +56,13 @@ function str(...msg) {
return line;
}
// helper function: wrapper around console output
const log = (...msg) => {
// eslint-disable-next-line no-console
if (config.console) console.log(...msg);
if (ui.console) console.log(...msg);
};
// draws processed results and starts processing of a next frame
async function drawResults(input, result, canvas) {
// update fps
settings.setValue('FPS', Math.round(1000 / (performance.now() - timeStamp)));
@ -84,53 +90,7 @@ async function drawResults(input, result, canvas) {
`;
}
// simple wrapper for worker.postmessage that creates worker if one does not exist
function webWorker(input, image, canvas) {
if (!worker) {
// create new webworker and add event handler only once
log('Creating worker thread');
worker = new Worker(ui.worker, { type: 'module' });
// after receiving message from webworker, parse&draw results and send new frame for processing
worker.addEventListener('message', (msg) => drawResults(input, msg.data, canvas));
}
// pass image data as arraybuffer to worker by reference to avoid copy
worker.postMessage({ image: image.data.buffer, width: canvas.width, height: canvas.height, config }, [image.data.buffer]);
}
async function runHumanDetect(input, canvas) {
timeStamp = performance.now();
// perform detect if live video or not video at all
if (input.srcObject) {
// if video not ready, just redo
const live = (input.srcObject.getVideoTracks()[0].readyState === 'live') && (input.readyState > 2) && (!input.paused);
if (!live) {
if (!input.paused) log(`Video not ready: state: ${input.srcObject.getVideoTracks()[0].readyState} stream state: ${input.readyState}`);
setTimeout(() => runHumanDetect(input, canvas), 500);
return;
}
if (ui.useWorker) {
// get image data from video as we cannot send html objects to webworker
const offscreen = new OffscreenCanvas(canvas.width, canvas.height);
const ctx = offscreen.getContext('2d');
ctx.drawImage(input, 0, 0, input.width, input.height, 0, 0, canvas.width, canvas.height);
const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
// perform detection in worker
webWorker(input, data, canvas);
} else {
let result = {};
try {
// perform detection
result = await human.detect(input, config);
} catch (err) {
log('Error during execution:', err.message);
}
if (result.error) log(result.error);
else drawResults(input, result, canvas);
}
}
}
// eslint-disable-next-line no-unused-vars
// setup webcam
async function setupCamera() {
if (ui.busy) return null;
ui.busy = true;
@ -173,12 +133,55 @@ async function setupCamera() {
});
}
// wrapper for worker.postmessage that creates worker if one does not exist
function webWorker(input, image, canvas) {
if (!worker) {
// create new webworker and add event handler only once
log('Creating worker thread');
worker = new Worker(ui.worker, { type: 'module' });
// after receiving message from webworker, parse&draw results and send new frame for processing
worker.addEventListener('message', (msg) => drawResults(input, msg.data, canvas));
}
// pass image data as arraybuffer to worker by reference to avoid copy
worker.postMessage({ image: image.data.buffer, width: canvas.width, height: canvas.height, config }, [image.data.buffer]);
}
// main processing function when input is webcam, can use direct invocation or web worker
async function runHumanDetect(input, canvas) {
timeStamp = performance.now();
// perform detect if live video or not video at all
if (input.srcObject) {
// if video not ready, just redo
const live = (input.srcObject.getVideoTracks()[0].readyState === 'live') && (input.readyState > 2) && (!input.paused);
if (!live) {
if (!input.paused) log(`Video not ready: state: ${input.srcObject.getVideoTracks()[0].readyState} stream state: ${input.readyState}`);
setTimeout(() => runHumanDetect(input, canvas), 500);
return;
}
if (ui.useWorker) {
// get image data from video as we cannot send html objects to webworker
const offscreen = new OffscreenCanvas(canvas.width, canvas.height);
const ctx = offscreen.getContext('2d');
ctx.drawImage(input, 0, 0, input.width, input.height, 0, 0, canvas.width, canvas.height);
const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
// perform detection in worker
webWorker(input, data, canvas);
} else {
let result = {};
try {
// perform detection
result = await human.detect(input, config);
} catch (err) {
log('Error during execution:', err.message);
}
if (result.error) log(result.error);
else drawResults(input, result, canvas);
}
}
}
// main processing function when input is image, can use direct invocation or web worker
async function processImage(input) {
ui.baseColor = 'rgba(200, 255, 255, 0.5)';
ui.baseLabel = 'rgba(200, 255, 255, 0.8)';
ui.baseFont = 'small-caps 3.5rem "Segoe UI"';
ui.baseLineWidth = 16;
ui.columns = 3;
const cfg = {
backend: 'webgl',
console: true,
@ -218,6 +221,7 @@ async function processImage(input) {
});
}
// just initialize everything and call main function
async function detectVideo() {
document.getElementById('samples').style.display = 'none';
document.getElementById('canvas').style.display = 'block';
@ -236,7 +240,7 @@ async function detectVideo() {
runHumanDetect(video, canvas);
}
// eslint-disable-next-line no-unused-vars
// just initialize everything and call main function
async function detectSampleImages() {
ui.baseFont = ui.baseFontProto.replace(/{size}/, `${ui.columns}rem`);
ui.baseLineHeight = ui.baseLineHeightProto * ui.columns;
@ -246,8 +250,8 @@ async function detectSampleImages() {
for (const sample of ui.samples) await processImage(sample);
}
// setup settings panel
function setupUI() {
// add all variables to ui control panel
settings = QuickSettings.create(10, 10, 'Settings', document.getElementById('main'));
const style = document.createElement('style');
style.innerHTML = `
@ -314,7 +318,6 @@ function setupUI() {
async function main() {
log('Human demo starting ...');
setupUI();
const msg = `Human ready: version: ${human.version} TensorFlow/JS version: ${human.tf.version_core}`;
document.getElementById('log').innerText += '\n' + msg;
log(msg);

188
dist/human.cjs vendored
View File

@ -113,8 +113,8 @@ var require_blazeface = __commonJS((exports2) => {
const boxIndicesTensor = await tf2.image.nonMaxSuppressionAsync(boxes, scores, this.maxFaces, this.iouThreshold, this.scoreThreshold);
const boxIndices = await boxIndicesTensor.array();
boxIndicesTensor.dispose();
let boundingBoxes = boxIndices.map((boxIndex) => tf2.slice(boxes, [boxIndex, 0], [1, -1]));
boundingBoxes = await Promise.all(boundingBoxes.map(async (boundingBox) => {
const boundingBoxesMap = boxIndices.map((boxIndex) => tf2.slice(boxes, [boxIndex, 0], [1, -1]));
const boundingBoxes = await Promise.all(boundingBoxesMap.map(async (boundingBox) => {
const vals = await boundingBox.array();
boundingBox.dispose();
return vals;
@ -122,16 +122,19 @@ var require_blazeface = __commonJS((exports2) => {
const annotatedBoxes = [];
for (let i = 0; i < boundingBoxes.length; i++) {
const boundingBox = boundingBoxes[i];
const annotatedBox = tf2.tidy(() => {
const box = createBox(boundingBox);
const boxIndex = boxIndices[i];
const anchor = this.anchorsData[boxIndex];
const landmarks = tf2.slice(detectedOutputs, [boxIndex, NUM_LANDMARKS - 1], [1, -1]).squeeze().reshape([NUM_LANDMARKS, -1]);
const probability = tf2.slice(scores, [boxIndex], [1]);
return {box, landmarks, probability, anchor};
});
const box = createBox(boundingBox);
const boxIndex = boxIndices[i];
const anchor = this.anchorsData[boxIndex];
const sliced = tf2.slice(detectedOutputs, [boxIndex, NUM_LANDMARKS - 1], [1, -1]);
const squeezed = sliced.squeeze();
const landmarks = squeezed.reshape([NUM_LANDMARKS, -1]);
const probability = tf2.slice(scores, [boxIndex], [1]);
const annotatedBox = {box, landmarks, probability, anchor};
annotatedBoxes.push(annotatedBox);
sliced.dispose();
squeezed.dispose();
}
detectedOutputs.dispose();
boxes.dispose();
scores.dispose();
detectedOutputs.dispose();
@ -141,12 +144,11 @@ var require_blazeface = __commonJS((exports2) => {
};
}
async estimateFaces(input) {
const image = tf2.tidy(() => {
if (!(input instanceof tf2.Tensor)) {
input = tf2.browser.fromPixels(input);
}
return input.toFloat().expandDims(0);
});
const imageRaw = !(input instanceof tf2.Tensor) ? tf2.browser.fromPixels(input) : input;
const imageCast = imageRaw.toFloat();
const image = imageCast.expandDims(0);
imageRaw.dispose();
imageCast.dispose();
const {boxes, scaleFactor} = await this.getBoundingBoxes(image);
image.dispose();
return Promise.all(boxes.map(async (face) => {
@ -172,12 +174,12 @@ var require_blazeface = __commonJS((exports2) => {
}));
}
}
async function load(config2) {
async function load2(config2) {
const blazeface = await tf2.loadGraphModel(config2.detector.modelPath, {fromTFHub: config2.detector.modelPath.includes("tfhub.dev")});
const model = new BlazeFaceModel(blazeface, config2);
return model;
}
exports2.load = load;
exports2.load = load2;
exports2.BlazeFaceModel = BlazeFaceModel;
exports2.disposeBox = disposeBox;
});
@ -530,21 +532,25 @@ var require_pipeline = __commonJS((exports2) => {
this.skipFrames = config2.detector.skipFrames;
this.maxFaces = config2.detector.maxFaces;
if (this.shouldUpdateRegionsOfInterest()) {
const {boxes, scaleFactor} = await this.boundingBoxDetector.getBoundingBoxes(input);
if (boxes.length === 0) {
const detector = await this.boundingBoxDetector.getBoundingBoxes(input);
if (detector.boxes.length === 0) {
this.regionsOfInterest = [];
return null;
}
const scaledBoxes = boxes.map((prediction) => {
const scaledBoxes = detector.boxes.map((prediction) => {
const startPoint = prediction.box.startPoint.squeeze();
const endPoint = prediction.box.endPoint.squeeze();
const predictionBox = {
startPoint: prediction.box.startPoint.squeeze().arraySync(),
endPoint: prediction.box.endPoint.squeeze().arraySync()
startPoint: startPoint.arraySync(),
endPoint: endPoint.arraySync()
};
prediction.box.startPoint.dispose();
prediction.box.endPoint.dispose();
const scaledBox = bounding.scaleBoxCoordinates(predictionBox, scaleFactor);
startPoint.dispose();
endPoint.dispose();
const scaledBox = bounding.scaleBoxCoordinates(predictionBox, detector.scaleFactor);
const enlargedBox = bounding.enlargeBox(scaledBox);
const landmarks = prediction.landmarks.arraySync();
prediction.box.startPoint.dispose();
prediction.box.endPoint.dispose();
prediction.landmarks.dispose();
prediction.probability.dispose();
return {...enlargedBox, landmarks};
@ -601,13 +607,15 @@ var require_pipeline = __commonJS((exports2) => {
const transformedCoordsData = this.transformRawCoords(rawCoords, box, angle, rotationMatrix);
tf2.dispose(rawCoords);
const landmarksBox = bounding.enlargeBox(this.calculateLandmarksBoundingBox(transformedCoordsData));
const confidence = flag.squeeze();
tf2.dispose(flag);
if (config2.mesh.enabled) {
const transformedCoords = tf2.tensor2d(transformedCoordsData);
this.regionsOfInterest[i] = {...landmarksBox, landmarks: transformedCoords.arraySync()};
const prediction2 = {
coords: transformedCoords,
box: landmarksBox,
confidence: flag.squeeze(),
confidence,
image: face
};
return prediction2;
@ -615,7 +623,7 @@ var require_pipeline = __commonJS((exports2) => {
const prediction = {
coords: null,
box: landmarksBox,
confidence: flag.squeeze(),
confidence,
image: face
};
return prediction;
@ -668,7 +676,7 @@ var require_pipeline = __commonJS((exports2) => {
const ys = landmarks.map((d) => d[1]);
const startPoint = [Math.min(...xs), Math.min(...ys)];
const endPoint = [Math.max(...xs), Math.max(...ys)];
return {startPoint, endPoint};
return {startPoint, endPoint, landmarks};
}
}
exports2.Pipeline = Pipeline;
@ -3814,11 +3822,11 @@ var require_facemesh = __commonJS((exports2) => {
async estimateFaces(input, config2) {
if (config2)
this.config = config2;
const image = tf2.tidy(() => {
if (!(input instanceof tf2.Tensor))
input = tf2.browser.fromPixels(input);
return input.toFloat().expandDims(0);
});
const imageRaw = !(input instanceof tf2.Tensor) ? tf2.browser.fromPixels(input) : input;
const imageCast = imageRaw.toFloat();
const image = imageCast.expandDims(0);
imageRaw.dispose();
imageCast.dispose();
const predictions = await this.pipeline.predict(image, config2);
tf2.dispose(image);
const results = [];
@ -3844,13 +3852,17 @@ var require_facemesh = __commonJS((exports2) => {
image: prediction.image ? tf2.clone(prediction.image) : null
});
}
prediction.confidence.dispose();
prediction.image.dispose();
if (prediction.confidence)
prediction.confidence.dispose();
if (prediction.coords)
prediction.coords.dispose();
if (prediction.image)
prediction.image.dispose();
}
return results;
}
}
async function load(config2) {
async function load2(config2) {
const models2 = await Promise.all([
blazeface.load(config2),
tf2.loadGraphModel(config2.mesh.modelPath, {fromTFHub: config2.mesh.modelPath.includes("tfhub.dev")}),
@ -3859,7 +3871,7 @@ var require_facemesh = __commonJS((exports2) => {
const faceMesh = new MediaPipeFaceMesh(models2[0], models2[1], models2[2], config2);
return faceMesh;
}
exports2.load = load;
exports2.load = load2;
exports2.MediaPipeFaceMesh = MediaPipeFaceMesh;
exports2.uv_coords = uv_coords;
exports2.triangulation = triangulation;
@ -3946,7 +3958,7 @@ var require_emotion = __commonJS((exports2) => {
});
return tensor;
}
async function load(config2) {
async function load2(config2) {
if (!models2.emotion)
models2.emotion = await tf2.loadGraphModel(config2.face.emotion.modelPath);
return models2.emotion;
@ -3988,7 +4000,7 @@ var require_emotion = __commonJS((exports2) => {
return obj;
}
exports2.predict = predict;
exports2.load = load;
exports2.load = load2;
});
// src/posenet/modelBase.js
@ -4542,10 +4554,10 @@ var require_modelPoseNet = __commonJS((exports2) => {
const mobilenet = new modelMobileNet.MobileNet(graphModel, config2.outputStride);
return new PoseNet(mobilenet);
}
async function load(config2) {
async function load2(config2) {
return loadMobileNet(config2);
}
exports2.load = load;
exports2.load = load2;
});
// src/posenet/posenet.js
@ -4681,19 +4693,7 @@ var require_handdetector = __commonJS((exports2) => {
const boxes = this.normalizeBoxes(rawBoxes);
const boxesWithHandsTensor = await tf2.image.nonMaxSuppressionAsync(boxes, scores, this.maxHands, this.iouThreshold, this.scoreThreshold);
const boxesWithHands = await boxesWithHandsTensor.array();
const toDispose = [
normalizedInput,
batchedPrediction,
boxesWithHandsTensor,
prediction,
boxes,
rawBoxes,
scores
];
if (boxesWithHands.length === 0) {
toDispose.forEach((tensor) => tensor.dispose());
return null;
}
const toDispose = [normalizedInput, batchedPrediction, boxesWithHandsTensor, prediction, boxes, rawBoxes, scores];
const detectedHands = tf2.tidy(() => {
const detectedBoxes = [];
for (const i in boxesWithHands) {
@ -4705,6 +4705,7 @@ var require_handdetector = __commonJS((exports2) => {
}
return detectedBoxes;
});
toDispose.forEach((tensor) => tensor.dispose());
return detectedHands;
}
async estimateHandBounds(input, config2) {
@ -5033,7 +5034,7 @@ var require_handpose = __commonJS((exports2) => {
}
return tf2.util.fetch(url).then((d) => d.json());
}
async function load(config2) {
async function load2(config2) {
const [anchors, handDetectorModel, handPoseModel] = await Promise.all([
loadAnchors(config2.detector.anchors),
tf2.loadGraphModel(config2.detector.modelPath, {fromTFHub: config2.detector.modelPath.includes("tfhub.dev")}),
@ -5044,7 +5045,7 @@ var require_handpose = __commonJS((exports2) => {
const handpose2 = new HandPose(pipeline);
return handpose2;
}
exports2.load = load;
exports2.load = load2;
});
// config.js
@ -5055,6 +5056,7 @@ var require_config = __commonJS((exports2) => {
var config_default = {
backend: "webgl",
console: true,
scoped: false,
face: {
enabled: true,
detector: {
@ -5202,6 +5204,7 @@ const handpose = require_handpose();
const defaults = require_config().default;
const app = require_package();
let config;
let state = "idle";
const models = {
facemesh: null,
posenet: null,
@ -5217,9 +5220,21 @@ const now = () => {
return parseInt(Number(process.hrtime.bigint()) / 1e3 / 1e3);
};
const log = (...msg) => {
if (config.console)
if (msg && config.console)
console.log(...msg);
};
let numTensors = 0;
const analyzeMemoryLeaks = false;
const analyze = (...msg) => {
if (!analyzeMemoryLeaks)
return;
const current = tf.engine().state.numTensors;
const previous = numTensors;
numTensors = current;
const leaked = current - previous;
if (leaked !== 0)
log(...msg, leaked);
};
function mergeDeep(...objects) {
const isObject = (obj) => obj && typeof obj === "object";
return objects.reduce((prev, obj) => {
@ -5252,8 +5267,26 @@ function sanity(input) {
}
return null;
}
async function detect(input, userConfig) {
async function load(userConfig) {
if (userConfig)
config = mergeDeep(defaults, userConfig);
if (config.face.enabled && !models.facemesh)
models.facemesh = await facemesh.load(config.face);
if (config.body.enabled && !models.posenet)
models.posenet = await posenet.load(config.body);
if (config.hand.enabled && !models.handpose)
models.handpose = await handpose.load(config.hand);
if (config.face.enabled && config.face.age.enabled && !models.age)
models.age = await ssrnet.loadAge(config);
if (config.face.enabled && config.face.gender.enabled && !models.gender)
models.gender = await ssrnet.loadGender(config);
if (config.face.enabled && config.face.emotion.enabled && !models.emotion)
models.emotion = await emotion.load(config);
}
async function detect(input, userConfig = {}) {
state = "config";
config = mergeDeep(defaults, userConfig);
state = "check";
const error = sanity(input);
if (error) {
log(error, input);
@ -5264,34 +5297,35 @@ async function detect(input, userConfig) {
if (loadedModels === 0)
log("Human library starting");
if (tf.getBackend() !== config.backend) {
state = "backend";
log("Human library setting backend:", config.backend);
await tf.setBackend(config.backend);
await tf.ready();
}
if (config.face.enabled && !models.facemesh)
models.facemesh = await facemesh.load(config.face);
if (config.body.enabled && !models.posenet)
models.posenet = await posenet.load(config.body);
if (config.hand.enabled && !models.handpose)
models.handpose = await handpose.load(config.hand);
if (config.face.enabled && config.face.age.enabled && !models.age)
models.age = await ssrnet.loadAge(config);
if (config.face.enabled && config.face.gender.enabled && !models.gender)
models.gender = await ssrnet.loadGender(config);
if (config.face.enabled && config.face.emotion.enabled && !models.emotion)
models.emotion = await emotion.load(config);
state = "load";
await load();
const perf = {};
let timeStamp;
tf.engine().startScope();
if (config.scoped)
tf.engine().startScope();
analyze("Start Detect:");
state = "run:body";
timeStamp = now();
analyze("Start PoseNet");
const poseRes = config.body.enabled ? await models.posenet.estimatePoses(input, config.body) : [];
analyze("End PoseNet:");
perf.body = Math.trunc(now() - timeStamp);
state = "run:hand";
timeStamp = now();
analyze("Start HandPose:");
const handRes = config.hand.enabled ? await models.handpose.estimateHands(input, config.hand) : [];
analyze("End HandPose:");
perf.hand = Math.trunc(now() - timeStamp);
const faceRes = [];
if (config.face.enabled) {
state = "run:face";
timeStamp = now();
analyze("Start FaceMesh:");
const faces = await models.facemesh.estimateFaces(input, config.face);
perf.face = Math.trunc(now() - timeStamp);
for (const face of faces) {
@ -5299,13 +5333,16 @@ async function detect(input, userConfig) {
log("face object is disposed:", face.image);
continue;
}
state = "run:agegender";
timeStamp = now();
const ssrData = config.face.age.enabled || config.face.gender.enabled ? await ssrnet.predict(face.image, config) : {};
perf.agegender = Math.trunc(now() - timeStamp);
state = "run:emotion";
timeStamp = now();
const emotionData = config.face.emotion.enabled ? await emotion.predict(face.image, config) : {};
perf.emotion = Math.trunc(now() - timeStamp);
face.image.dispose();
delete face.image;
const iris = face.annotations.leftEyeIris && face.annotations.rightEyeIris ? Math.max(face.annotations.leftEyeIris[3][0] - face.annotations.leftEyeIris[1][0], face.annotations.rightEyeIris[3][0] - face.annotations.rightEyeIris[1][0]) : 0;
faceRes.push({
confidence: face.confidence,
@ -5319,8 +5356,12 @@ async function detect(input, userConfig) {
iris: iris !== 0 ? Math.trunc(100 * 11.7 / iris) / 100 : 0
});
}
analyze("End FaceMesh:");
}
tf.engine().endScope();
state = "idle";
if (config.scoped)
tf.engine().endScope();
analyze("End Scope:");
perf.total = Object.values(perf).reduce((a, b) => a + b);
resolve({face: faceRes, body: poseRes, hand: handRes, performance: perf});
});
@ -5335,4 +5376,5 @@ exports.posenet = posenet;
exports.handpose = handpose;
exports.tf = tf;
exports.version = app.version;
exports.state = state;
//# sourceMappingURL=human.cjs.map

34
dist/human.cjs.json vendored
View File

@ -1,7 +1,7 @@
{
"inputs": {
"config.js": {
"bytes": 4536,
"bytes": 4774,
"imports": []
},
"package.json": {
@ -13,7 +13,7 @@
"imports": []
},
"src/facemesh/blazeface.js": {
"bytes": 7042,
"bytes": 7407,
"imports": []
},
"src/facemesh/box.js": {
@ -21,7 +21,7 @@
"imports": []
},
"src/facemesh/facemesh.js": {
"bytes": 2649,
"bytes": 2816,
"imports": [
{
"path": "src/facemesh/blazeface.js"
@ -45,7 +45,7 @@
"imports": []
},
"src/facemesh/pipeline.js": {
"bytes": 14108,
"bytes": 14393,
"imports": [
{
"path": "src/facemesh/box.js"
@ -75,7 +75,7 @@
"imports": []
},
"src/handpose/handdetector.js": {
"bytes": 4253,
"bytes": 4296,
"imports": [
{
"path": "src/handpose/box.js"
@ -116,7 +116,7 @@
"imports": []
},
"src/index.js": {
"bytes": 6474,
"bytes": 7175,
"imports": [
{
"path": "src/facemesh/facemesh.js"
@ -253,13 +253,13 @@
"dist/human.cjs.map": {
"imports": [],
"inputs": {},
"bytes": 216628
"bytes": 219147
},
"dist/human.cjs": {
"imports": [],
"inputs": {
"src/facemesh/blazeface.js": {
"bytesInOutput": 7246
"bytesInOutput": 7398
},
"src/facemesh/keypoints.js": {
"bytesInOutput": 2771
@ -271,7 +271,7 @@
"bytesInOutput": 3027
},
"src/facemesh/pipeline.js": {
"bytesInOutput": 13162
"bytesInOutput": 13366
},
"src/facemesh/uvcoords.js": {
"bytesInOutput": 20586
@ -280,13 +280,13 @@
"bytesInOutput": 23311
},
"src/facemesh/facemesh.js": {
"bytesInOutput": 2758
"bytesInOutput": 2950
},
"src/ssrnet/ssrnet.js": {
"bytesInOutput": 2068
},
"src/emotion/emotion.js": {
"bytesInOutput": 2132
"bytesInOutput": 2134
},
"src/posenet/modelBase.js": {
"bytesInOutput": 1120
@ -316,7 +316,7 @@
"bytesInOutput": 4383
},
"src/posenet/modelPoseNet.js": {
"bytesInOutput": 1974
"bytesInOutput": 1976
},
"src/posenet/posenet.js": {
"bytesInOutput": 917
@ -325,7 +325,7 @@
"bytesInOutput": 2813
},
"src/handpose/handdetector.js": {
"bytesInOutput": 4271
"bytesInOutput": 4135
},
"src/handpose/keypoints.js": {
"bytesInOutput": 265
@ -337,19 +337,19 @@
"bytesInOutput": 7651
},
"src/handpose/handpose.js": {
"bytesInOutput": 2516
"bytesInOutput": 2518
},
"config.js": {
"bytesInOutput": 1853
"bytesInOutput": 1872
},
"package.json": {
"bytesInOutput": 2748
},
"src/index.js": {
"bytesInOutput": 5148
"bytesInOutput": 6171
}
},
"bytes": 132178
"bytes": 133638
}
}
}

4
dist/human.cjs.map vendored

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

View File

@ -1,7 +1,7 @@
{
"inputs": {
"config.js": {
"bytes": 4536,
"bytes": 4774,
"imports": []
},
"package.json": {
@ -13,7 +13,7 @@
"imports": []
},
"src/facemesh/blazeface.js": {
"bytes": 7042,
"bytes": 7407,
"imports": []
},
"src/facemesh/box.js": {
@ -21,7 +21,7 @@
"imports": []
},
"src/facemesh/facemesh.js": {
"bytes": 2649,
"bytes": 2816,
"imports": [
{
"path": "src/facemesh/blazeface.js"
@ -45,7 +45,7 @@
"imports": []
},
"src/facemesh/pipeline.js": {
"bytes": 14108,
"bytes": 14393,
"imports": [
{
"path": "src/facemesh/box.js"
@ -75,7 +75,7 @@
"imports": []
},
"src/handpose/handdetector.js": {
"bytes": 4253,
"bytes": 4296,
"imports": [
{
"path": "src/handpose/box.js"
@ -116,7 +116,7 @@
"imports": []
},
"src/index.js": {
"bytes": 6474,
"bytes": 7175,
"imports": [
{
"path": "src/facemesh/facemesh.js"
@ -253,13 +253,13 @@
"dist/human.esm-nobundle.js.map": {
"imports": [],
"inputs": {},
"bytes": 194920
"bytes": 197443
},
"dist/human.esm-nobundle.js": {
"imports": [],
"inputs": {
"src/facemesh/blazeface.js": {
"bytesInOutput": 3223
"bytesInOutput": 3255
},
"src/facemesh/keypoints.js": {
"bytesInOutput": 1950
@ -271,7 +271,7 @@
"bytesInOutput": 1176
},
"src/facemesh/pipeline.js": {
"bytesInOutput": 5541
"bytesInOutput": 5602
},
"src/facemesh/uvcoords.js": {
"bytesInOutput": 16790
@ -280,7 +280,7 @@
"bytesInOutput": 9995
},
"src/facemesh/facemesh.js": {
"bytesInOutput": 1320
"bytesInOutput": 1391
},
"src/ssrnet/ssrnet.js": {
"bytesInOutput": 1099
@ -325,31 +325,31 @@
"bytesInOutput": 1400
},
"src/handpose/handdetector.js": {
"bytesInOutput": 2074
"bytesInOutput": 2040
},
"src/handpose/keypoints.js": {
"bytesInOutput": 160
},
"src/handpose/util.js": {
"bytesInOutput": 977
"bytesInOutput": 984
},
"src/handpose/pipeline.js": {
"bytesInOutput": 3230
"bytesInOutput": 3232
},
"src/handpose/handpose.js": {
"bytesInOutput": 1326
},
"config.js": {
"bytesInOutput": 1136
"bytesInOutput": 1146
},
"package.json": {
"bytesInOutput": 2275
},
"src/index.js": {
"bytesInOutput": 2904
"bytesInOutput": 3410
}
},
"bytes": 68538
"bytes": 69193
}
}
}

486
dist/human.esm.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

28
dist/human.esm.json vendored
View File

@ -1,7 +1,7 @@
{
"inputs": {
"config.js": {
"bytes": 4536,
"bytes": 4774,
"imports": []
},
"node_modules/@tensorflow/tfjs-backend-cpu/dist/tf-backend-cpu.node.js": {
@ -161,7 +161,7 @@
]
},
"src/facemesh/blazeface.js": {
"bytes": 7042,
"bytes": 7407,
"imports": [
{
"path": "node_modules/@tensorflow/tfjs/dist/tf.node.js"
@ -177,7 +177,7 @@
]
},
"src/facemesh/facemesh.js": {
"bytes": 2649,
"bytes": 2816,
"imports": [
{
"path": "node_modules/@tensorflow/tfjs/dist/tf.node.js"
@ -204,7 +204,7 @@
"imports": []
},
"src/facemesh/pipeline.js": {
"bytes": 14108,
"bytes": 14393,
"imports": [
{
"path": "node_modules/@tensorflow/tfjs/dist/tf.node.js"
@ -241,7 +241,7 @@
]
},
"src/handpose/handdetector.js": {
"bytes": 4253,
"bytes": 4296,
"imports": [
{
"path": "node_modules/@tensorflow/tfjs/dist/tf.node.js"
@ -291,7 +291,7 @@
"imports": []
},
"src/index.js": {
"bytes": 6474,
"bytes": 7175,
"imports": [
{
"path": "node_modules/@tensorflow/tfjs/dist/tf.node.js"
@ -464,7 +464,7 @@
"dist/human.esm.js.map": {
"imports": [],
"inputs": {},
"bytes": 4955971
"bytes": 4958494
},
"dist/human.esm.js": {
"imports": [],
@ -527,7 +527,7 @@
"bytesInOutput": 765
},
"src/facemesh/blazeface.js": {
"bytesInOutput": 3238
"bytesInOutput": 3268
},
"src/facemesh/keypoints.js": {
"bytesInOutput": 1951
@ -539,7 +539,7 @@
"bytesInOutput": 1195
},
"src/facemesh/pipeline.js": {
"bytesInOutput": 5520
"bytesInOutput": 5577
},
"src/facemesh/uvcoords.js": {
"bytesInOutput": 16791
@ -548,7 +548,7 @@
"bytesInOutput": 9996
},
"src/facemesh/facemesh.js": {
"bytesInOutput": 1306
"bytesInOutput": 1376
},
"src/ssrnet/ssrnet.js": {
"bytesInOutput": 1100
@ -593,7 +593,7 @@
"bytesInOutput": 1386
},
"src/handpose/handdetector.js": {
"bytesInOutput": 2084
"bytesInOutput": 2050
},
"src/handpose/keypoints.js": {
"bytesInOutput": 161
@ -608,16 +608,16 @@
"bytesInOutput": 1312
},
"config.js": {
"bytesInOutput": 1137
"bytesInOutput": 1147
},
"package.json": {
"bytesInOutput": 2276
},
"src/index.js": {
"bytesInOutput": 2963
"bytesInOutput": 3495
}
},
"bytes": 1105435
"bytes": 1106100
}
}
}

486
dist/human.js vendored

File diff suppressed because one or more lines are too long

4
dist/human.js.map vendored

File diff suppressed because one or more lines are too long

28
dist/human.json vendored
View File

@ -1,7 +1,7 @@
{
"inputs": {
"config.js": {
"bytes": 4536,
"bytes": 4774,
"imports": []
},
"node_modules/@tensorflow/tfjs-backend-cpu/dist/tf-backend-cpu.node.js": {
@ -161,7 +161,7 @@
]
},
"src/facemesh/blazeface.js": {
"bytes": 7042,
"bytes": 7407,
"imports": [
{
"path": "node_modules/@tensorflow/tfjs/dist/tf.node.js"
@ -177,7 +177,7 @@
]
},
"src/facemesh/facemesh.js": {
"bytes": 2649,
"bytes": 2816,
"imports": [
{
"path": "node_modules/@tensorflow/tfjs/dist/tf.node.js"
@ -204,7 +204,7 @@
"imports": []
},
"src/facemesh/pipeline.js": {
"bytes": 14108,
"bytes": 14393,
"imports": [
{
"path": "node_modules/@tensorflow/tfjs/dist/tf.node.js"
@ -241,7 +241,7 @@
]
},
"src/handpose/handdetector.js": {
"bytes": 4253,
"bytes": 4296,
"imports": [
{
"path": "node_modules/@tensorflow/tfjs/dist/tf.node.js"
@ -291,7 +291,7 @@
"imports": []
},
"src/index.js": {
"bytes": 6474,
"bytes": 7175,
"imports": [
{
"path": "node_modules/@tensorflow/tfjs/dist/tf.node.js"
@ -464,7 +464,7 @@
"dist/human.js.map": {
"imports": [],
"inputs": {},
"bytes": 4955971
"bytes": 4958494
},
"dist/human.js": {
"imports": [],
@ -527,7 +527,7 @@
"bytesInOutput": 765
},
"src/facemesh/blazeface.js": {
"bytesInOutput": 3238
"bytesInOutput": 3268
},
"src/facemesh/keypoints.js": {
"bytesInOutput": 1951
@ -539,7 +539,7 @@
"bytesInOutput": 1195
},
"src/facemesh/pipeline.js": {
"bytesInOutput": 5520
"bytesInOutput": 5577
},
"src/facemesh/uvcoords.js": {
"bytesInOutput": 16791
@ -548,7 +548,7 @@
"bytesInOutput": 9996
},
"src/facemesh/facemesh.js": {
"bytesInOutput": 1306
"bytesInOutput": 1376
},
"src/ssrnet/ssrnet.js": {
"bytesInOutput": 1100
@ -593,7 +593,7 @@
"bytesInOutput": 1386
},
"src/handpose/handdetector.js": {
"bytesInOutput": 2084
"bytesInOutput": 2050
},
"src/handpose/keypoints.js": {
"bytesInOutput": 161
@ -608,16 +608,16 @@
"bytesInOutput": 1312
},
"config.js": {
"bytesInOutput": 1137
"bytesInOutput": 1147
},
"package.json": {
"bytesInOutput": 2276
},
"src/index.js": {
"bytesInOutput": 2963
"bytesInOutput": 3495
}
},
"bytes": 1105444
"bytes": 1106109
}
}
}

View File

@ -78,6 +78,7 @@ class BlazeFaceModel {
this.scoreThreshold = config.detector.scoreThreshold;
}
// toto blazeface leaks two tensors per run
async getBoundingBoxes(inputImage) {
// sanity check on input
if ((!inputImage) || (inputImage.isDisposedInternal) || (inputImage.shape.length !== 4) || (inputImage.shape[1] < 1) || (inputImage.shape[2] < 1)) return null;
@ -101,12 +102,11 @@ class BlazeFaceModel {
const scoresOut = tf.sigmoid(logits).squeeze();
return [prediction, decodedBounds, scoresOut];
});
const boxIndicesTensor = await tf.image.nonMaxSuppressionAsync(boxes, scores, this.maxFaces, this.iouThreshold, this.scoreThreshold);
const boxIndices = await boxIndicesTensor.array();
boxIndicesTensor.dispose();
let boundingBoxes = boxIndices.map((boxIndex) => tf.slice(boxes, [boxIndex, 0], [1, -1]));
boundingBoxes = await Promise.all(boundingBoxes.map(async (boundingBox) => {
const boundingBoxesMap = boxIndices.map((boxIndex) => tf.slice(boxes, [boxIndex, 0], [1, -1]));
const boundingBoxes = await Promise.all(boundingBoxesMap.map(async (boundingBox) => {
const vals = await boundingBox.array();
boundingBox.dispose();
return vals;
@ -114,19 +114,26 @@ class BlazeFaceModel {
const annotatedBoxes = [];
for (let i = 0; i < boundingBoxes.length; i++) {
const boundingBox = boundingBoxes[i];
const annotatedBox = tf.tidy(() => {
const box = createBox(boundingBox);
const boxIndex = boxIndices[i];
const anchor = this.anchorsData[boxIndex];
const landmarks = tf
.slice(detectedOutputs, [boxIndex, NUM_LANDMARKS - 1], [1, -1])
.squeeze()
.reshape([NUM_LANDMARKS, -1]);
const probability = tf.slice(scores, [boxIndex], [1]);
return { box, landmarks, probability, anchor };
});
const box = createBox(boundingBox);
const boxIndex = boxIndices[i];
const anchor = this.anchorsData[boxIndex];
const sliced = tf.slice(detectedOutputs, [boxIndex, NUM_LANDMARKS - 1], [1, -1]);
const squeezed = sliced.squeeze();
const landmarks = squeezed.reshape([NUM_LANDMARKS, -1]);
/*
const landmarks = tf
.slice(detectedOutputs, [boxIndex, NUM_LANDMARKS - 1], [1, -1])
.squeeze()
.reshape([NUM_LANDMARKS, -1]);
*/
const probability = tf.slice(scores, [boxIndex], [1]);
const annotatedBox = { box, landmarks, probability, anchor };
annotatedBoxes.push(annotatedBox);
sliced.dispose();
squeezed.dispose();
// landmarks.dispose();
}
detectedOutputs.dispose();
boxes.dispose();
scores.dispose();
detectedOutputs.dispose();
@ -137,12 +144,11 @@ class BlazeFaceModel {
}
async estimateFaces(input) {
const image = tf.tidy(() => {
if (!(input instanceof tf.Tensor)) {
input = tf.browser.fromPixels(input);
}
return input.toFloat().expandDims(0);
});
const imageRaw = !(input instanceof tf.Tensor) ? tf.browser.fromPixels(input) : input;
const imageCast = imageRaw.toFloat();
const image = imageCast.expandDims(0);
imageRaw.dispose();
imageCast.dispose();
const { boxes, scaleFactor } = await this.getBoundingBoxes(image);
image.dispose();
return Promise.all(boxes.map(async (face) => {

View File

@ -13,10 +13,11 @@ class MediaPipeFaceMesh {
async estimateFaces(input, config) {
if (config) this.config = config;
const image = tf.tidy(() => {
if (!(input instanceof tf.Tensor)) input = tf.browser.fromPixels(input);
return input.toFloat().expandDims(0);
});
const imageRaw = !(input instanceof tf.Tensor) ? tf.browser.fromPixels(input) : input;
const imageCast = imageRaw.toFloat();
const image = imageCast.expandDims(0);
imageRaw.dispose();
imageCast.dispose();
const predictions = await this.pipeline.predict(image, config);
tf.dispose(image);
const results = [];
@ -42,8 +43,9 @@ class MediaPipeFaceMesh {
image: prediction.image ? tf.clone(prediction.image) : null,
});
}
prediction.confidence.dispose();
prediction.image.dispose();
if (prediction.confidence) prediction.confidence.dispose();
if (prediction.coords) prediction.coords.dispose();
if (prediction.image) prediction.image.dispose();
}
return results;
}

View File

@ -132,21 +132,26 @@ class Pipeline {
this.skipFrames = config.detector.skipFrames;
this.maxFaces = config.detector.maxFaces;
if (this.shouldUpdateRegionsOfInterest()) {
const { boxes, scaleFactor } = await this.boundingBoxDetector.getBoundingBoxes(input);
if (boxes.length === 0) {
// const { boxes, scaleFactor } = await this.boundingBoxDetector.getBoundingBoxes(input);
const detector = await this.boundingBoxDetector.getBoundingBoxes(input);
if (detector.boxes.length === 0) {
this.regionsOfInterest = [];
return null;
}
const scaledBoxes = boxes.map((prediction) => {
const scaledBoxes = detector.boxes.map((prediction) => {
const startPoint = prediction.box.startPoint.squeeze();
const endPoint = prediction.box.endPoint.squeeze();
const predictionBox = {
startPoint: prediction.box.startPoint.squeeze().arraySync(),
endPoint: prediction.box.endPoint.squeeze().arraySync(),
startPoint: startPoint.arraySync(),
endPoint: endPoint.arraySync(),
};
prediction.box.startPoint.dispose();
prediction.box.endPoint.dispose();
const scaledBox = bounding.scaleBoxCoordinates(predictionBox, scaleFactor);
startPoint.dispose();
endPoint.dispose();
const scaledBox = bounding.scaleBoxCoordinates(predictionBox, detector.scaleFactor);
const enlargedBox = bounding.enlargeBox(scaledBox);
const landmarks = prediction.landmarks.arraySync();
prediction.box.startPoint.dispose();
prediction.box.endPoint.dispose();
prediction.landmarks.dispose();
prediction.probability.dispose();
return { ...enlargedBox, landmarks };
@ -206,13 +211,15 @@ class Pipeline {
const transformedCoordsData = this.transformRawCoords(rawCoords, box, angle, rotationMatrix);
tf.dispose(rawCoords);
const landmarksBox = bounding.enlargeBox(this.calculateLandmarksBoundingBox(transformedCoordsData));
const confidence = flag.squeeze();
tf.dispose(flag);
if (config.mesh.enabled) {
const transformedCoords = tf.tensor2d(transformedCoordsData);
this.regionsOfInterest[i] = { ...landmarksBox, landmarks: transformedCoords.arraySync() };
const prediction = {
coords: transformedCoords,
box: landmarksBox,
confidence: flag.squeeze(),
confidence,
image: face,
};
return prediction;
@ -220,7 +227,7 @@ class Pipeline {
const prediction = {
coords: null,
box: landmarksBox,
confidence: flag.squeeze(),
confidence,
image: face,
};
return prediction;
@ -278,7 +285,7 @@ class Pipeline {
const ys = landmarks.map((d) => d[1]);
const startPoint = [Math.min(...xs), Math.min(...ys)];
const endPoint = [Math.max(...xs), Math.max(...ys)];
return { startPoint, endPoint };
return { startPoint, endPoint, landmarks };
}
}
exports.Pipeline = Pipeline;

View File

@ -42,14 +42,11 @@ class HandDetector {
const boxes = this.normalizeBoxes(rawBoxes);
const boxesWithHandsTensor = await tf.image.nonMaxSuppressionAsync(boxes, scores, this.maxHands, this.iouThreshold, this.scoreThreshold);
const boxesWithHands = await boxesWithHandsTensor.array();
const toDispose = [
normalizedInput, batchedPrediction, boxesWithHandsTensor, prediction,
boxes, rawBoxes, scores,
];
if (boxesWithHands.length === 0) {
toDispose.forEach((tensor) => tensor.dispose());
return null;
}
const toDispose = [normalizedInput, batchedPrediction, boxesWithHandsTensor, prediction, boxes, rawBoxes, scores];
// if (boxesWithHands.length === 0) {
// toDispose.forEach((tensor) => tensor.dispose());
// return null;
// }
const detectedHands = tf.tidy(() => {
const detectedBoxes = [];
for (const i in boxesWithHands) {
@ -61,6 +58,7 @@ class HandDetector {
}
return detectedBoxes;
});
toDispose.forEach((tensor) => tensor.dispose());
return detectedHands;
}

View File

@ -20,17 +20,32 @@ const models = {
gender: null,
emotion: null,
};
// helper function: gets elapsed time on both browser and nodejs
const now = () => {
if (typeof performance !== 'undefined') return performance.now();
return parseInt(Number(process.hrtime.bigint()) / 1000 / 1000);
};
// helper function: wrapper around console output
const log = (...msg) => {
// eslint-disable-next-line no-console
if (config.console) console.log(...msg);
if (msg && config.console) console.log(...msg);
};
// helper function that performs deep merge of multiple objects so it allows full inheriance with overrides
// helper function: measure tensor leak
let numTensors = 0;
const analyzeMemoryLeaks = false;
const analyze = (...msg) => {
if (!analyzeMemoryLeaks) return;
const current = tf.engine().state.numTensors;
const previous = numTensors;
numTensors = current;
const leaked = current - previous;
if (leaked !== 0) log(...msg, leaked);
};
// helper function: perform deep merge of multiple objects so it allows full inheriance with overrides
function mergeDeep(...objects) {
const isObject = (obj) => obj && typeof obj === 'object';
return objects.reduce((prev, obj) => {
@ -97,12 +112,6 @@ async function detect(input, userConfig = {}) {
await tf.setBackend(config.backend);
await tf.ready();
}
// explictly enable depthwiseconv since it's diasabled by default due to issues with large shaders
// let savedWebglPackDepthwiseConvFlag;
// if (tf.getBackend() === 'webgl') {
// savedWebglPackDepthwiseConvFlag = tf.env().get('WEBGL_PACK_DEPTHWISECONV');
// tf.env().set('WEBGL_PACK_DEPTHWISECONV', true);
// }
// load models if enabled
state = 'load';
@ -111,18 +120,24 @@ async function detect(input, userConfig = {}) {
const perf = {};
let timeStamp;
tf.engine().startScope();
if (config.scoped) tf.engine().startScope();
analyze('Start Detect:');
// run posenet
state = 'run:body';
timeStamp = now();
analyze('Start PoseNet');
const poseRes = config.body.enabled ? await models.posenet.estimatePoses(input, config.body) : [];
analyze('End PoseNet:');
perf.body = Math.trunc(now() - timeStamp);
// run handpose
state = 'run:hand';
timeStamp = now();
analyze('Start HandPose:');
const handRes = config.hand.enabled ? await models.handpose.estimateHands(input, config.hand) : [];
analyze('End HandPose:');
perf.hand = Math.trunc(now() - timeStamp);
// run facemesh, includes blazeface and iris
@ -130,6 +145,7 @@ async function detect(input, userConfig = {}) {
if (config.face.enabled) {
state = 'run:face';
timeStamp = now();
analyze('Start FaceMesh:');
const faces = await models.facemesh.estimateFaces(input, config.face);
perf.face = Math.trunc(now() - timeStamp);
for (const face of faces) {
@ -149,6 +165,7 @@ async function detect(input, userConfig = {}) {
const emotionData = config.face.emotion.enabled ? await emotion.predict(face.image, config) : {};
perf.emotion = Math.trunc(now() - timeStamp);
face.image.dispose();
delete face.image;
// calculate iris distance
// iris: array[ bottom, left, top, right, center ]
const iris = (face.annotations.leftEyeIris && face.annotations.rightEyeIris)
@ -166,13 +183,13 @@ async function detect(input, userConfig = {}) {
iris: (iris !== 0) ? Math.trunc(100 * 11.7 /* human iris size in mm */ / iris) / 100 : 0,
});
}
state = 'idle';
analyze('End FaceMesh:');
}
// set depthwiseconv to original value
// tf.env().set('WEBGL_PACK_DEPTHWISECONV', savedWebglPackDepthwiseConvFlag);
state = 'idle';
tf.engine().endScope();
if (config.scoped) tf.engine().endScope();
analyze('End Scope:');
// combine and return results
perf.total = Object.values(perf).reduce((a, b) => a + b);