mirror of https://github.com/vladmandic/human
complete async work
parent
26b3fa28cf
commit
38714b023f
|
@ -41,9 +41,9 @@ let userConfig = {
|
|||
flip: false,
|
||||
},
|
||||
face: { enabled: true,
|
||||
detector: { return: false },
|
||||
detector: { return: false, rotation: true },
|
||||
mesh: { enabled: true },
|
||||
iris: { enabled: false },
|
||||
iris: { enabled: true },
|
||||
description: { enabled: false },
|
||||
emotion: { enabled: false },
|
||||
},
|
||||
|
@ -441,9 +441,9 @@ function webWorker(input, image, canvas, timestamp) {
|
|||
// main processing function when input is webcam, can use direct invocation or web worker
|
||||
function runHumanDetect(input, canvas, timestamp) {
|
||||
// if live video
|
||||
const videoLive = (input.readyState > 2) && (!input.paused);
|
||||
const cameraLive = input.srcObject && (input.srcObject.getVideoTracks()[0].readyState === 'live');
|
||||
const live = videoLive || cameraLive;
|
||||
const videoLive = input.readyState > 2;
|
||||
const cameraLive = input.srcObject?.getVideoTracks()[0].readyState === 'live';
|
||||
const live = (videoLive || cameraLive) && (!input.paused);
|
||||
if (!live) {
|
||||
// stop ui refresh
|
||||
// if (ui.drawThread) cancelAnimationFrame(ui.drawThread);
|
||||
|
|
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
|
@ -512,7 +512,7 @@ var BlazeFaceModel = class {
|
|||
}
|
||||
const boxesOut = decodeBounds(batchOut, this.anchors, [this.inputSize, this.inputSize]);
|
||||
const logits = tf3.slice(batchOut, [0, 0], [-1, 1]);
|
||||
const scoresOut = tf3.squeeze(tf3.sigmoid(logits)).dataSync();
|
||||
const scoresOut = tf3.squeeze(tf3.sigmoid(logits));
|
||||
return [batchOut, boxesOut, scoresOut];
|
||||
});
|
||||
this.config = mergeDeep(this.config, userConfig);
|
||||
|
@ -520,8 +520,9 @@ var BlazeFaceModel = class {
|
|||
const nms = await nmsTensor.array();
|
||||
tf3.dispose(nmsTensor);
|
||||
const annotatedBoxes = [];
|
||||
const scoresData = await scores.data();
|
||||
for (let i = 0; i < nms.length; i++) {
|
||||
const confidence = scores[nms[i]];
|
||||
const confidence = scoresData[nms[i]];
|
||||
if (confidence > this.config.face.detector.minConfidence) {
|
||||
const boundingBox = tf3.slice(boxes, [nms[i], 0], [1, -1]);
|
||||
const localBox = createBox(boundingBox);
|
||||
|
@ -533,6 +534,7 @@ var BlazeFaceModel = class {
|
|||
}
|
||||
tf3.dispose(batch);
|
||||
tf3.dispose(boxes);
|
||||
tf3.dispose(scores);
|
||||
return {
|
||||
boxes: annotatedBoxes,
|
||||
scaleFactor: [inputImage.shape[2] / this.inputSize, inputImage.shape[1] / this.inputSize]
|
||||
|
@ -3910,7 +3912,9 @@ var Pipeline = class {
|
|||
box6.endPoint[0] / this.meshSize
|
||||
]], [0], [this.irisSize, this.irisSize]);
|
||||
if (flip && tf4.ENV.flags.IS_BROWSER) {
|
||||
crop = tf4.image.flipLeftRight(crop);
|
||||
const flipped = tf4.image.flipLeftRight(crop);
|
||||
tf4.dispose(crop);
|
||||
crop = flipped;
|
||||
}
|
||||
return { box: box6, boxSize, crop };
|
||||
}
|
||||
|
@ -3942,6 +3946,47 @@ var Pipeline = class {
|
|||
return [coord[0], coord[1], z];
|
||||
});
|
||||
}
|
||||
correctFaceRotation(config3, box6, input) {
|
||||
const [indexOfMouth, indexOfForehead] = box6.landmarks.length >= meshLandmarks.count ? meshLandmarks.symmetryLine : blazeFaceLandmarks.symmetryLine;
|
||||
const angle = computeRotation(box6.landmarks[indexOfMouth], box6.landmarks[indexOfForehead]);
|
||||
const faceCenter = getBoxCenter({ startPoint: box6.startPoint, endPoint: box6.endPoint });
|
||||
const faceCenterNormalized = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]];
|
||||
const rotatedImage = tf4.image.rotateWithOffset(input, angle, 0, faceCenterNormalized);
|
||||
const rotationMatrix = buildRotationMatrix(-angle, faceCenter);
|
||||
const cut = config3.face.mesh.enabled ? cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.meshSize, this.meshSize]) : cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.boxSize, this.boxSize]);
|
||||
const face5 = tf4.div(cut, 255);
|
||||
tf4.dispose(cut);
|
||||
tf4.dispose(rotatedImage);
|
||||
return [angle, rotationMatrix, face5];
|
||||
}
|
||||
async augmentIris(rawCoords, face5) {
|
||||
const { box: leftEyeBox, boxSize: leftEyeBoxSize, crop: leftEyeCrop } = this.getEyeBox(rawCoords, face5, eyeLandmarks.leftBounds[0], eyeLandmarks.leftBounds[1], true);
|
||||
const { box: rightEyeBox, boxSize: rightEyeBoxSize, crop: rightEyeCrop } = this.getEyeBox(rawCoords, face5, eyeLandmarks.rightBounds[0], eyeLandmarks.rightBounds[1]);
|
||||
const combined = tf4.concat([leftEyeCrop, rightEyeCrop]);
|
||||
tf4.dispose(leftEyeCrop);
|
||||
tf4.dispose(rightEyeCrop);
|
||||
const eyePredictions = this.irisModel.predict(combined);
|
||||
tf4.dispose(combined);
|
||||
const eyePredictionsData = await eyePredictions.data();
|
||||
tf4.dispose(eyePredictions);
|
||||
const leftEyeData = eyePredictionsData.slice(0, irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: leftEyeRawCoords, iris: leftIrisRawCoords } = this.getEyeCoords(leftEyeData, leftEyeBox, leftEyeBoxSize, true);
|
||||
const rightEyeData = eyePredictionsData.slice(irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: rightEyeRawCoords, iris: rightIrisRawCoords } = this.getEyeCoords(rightEyeData, rightEyeBox, rightEyeBoxSize);
|
||||
const leftToRightEyeDepthDifference = this.getLeftToRightEyeDepthDifference(rawCoords);
|
||||
if (Math.abs(leftToRightEyeDepthDifference) < 30) {
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, "left", null);
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, "right", null);
|
||||
} else if (leftToRightEyeDepthDifference < 1) {
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, "left", ["EyeUpper0", "EyeLower0"]);
|
||||
} else {
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, "right", ["EyeUpper0", "EyeLower0"]);
|
||||
}
|
||||
const adjustedLeftIrisCoords = this.getAdjustedIrisCoords(rawCoords, leftIrisRawCoords, "left");
|
||||
const adjustedRightIrisCoords = this.getAdjustedIrisCoords(rawCoords, rightIrisRawCoords, "right");
|
||||
const newCoords = rawCoords.concat(adjustedLeftIrisCoords).concat(adjustedRightIrisCoords);
|
||||
return newCoords;
|
||||
}
|
||||
async predict(input, config3) {
|
||||
let useFreshBox = false;
|
||||
let detector;
|
||||
|
@ -3985,93 +4030,65 @@ var Pipeline = class {
|
|||
tf4.dispose(prediction.landmarks);
|
||||
});
|
||||
}
|
||||
const results = tf4.tidy(() => this.storedBoxes.map((box6, i) => {
|
||||
const results = [];
|
||||
for (let i = 0; i < this.storedBoxes.length; i++) {
|
||||
let box6 = this.storedBoxes[i];
|
||||
let face5;
|
||||
let angle = 0;
|
||||
let rotationMatrix;
|
||||
if (config3.face.detector.rotation && config3.face.mesh.enabled && tf4.ENV.flags.IS_BROWSER) {
|
||||
const [indexOfMouth, indexOfForehead] = box6.landmarks.length >= meshLandmarks.count ? meshLandmarks.symmetryLine : blazeFaceLandmarks.symmetryLine;
|
||||
angle = computeRotation(box6.landmarks[indexOfMouth], box6.landmarks[indexOfForehead]);
|
||||
const faceCenter = getBoxCenter({ startPoint: box6.startPoint, endPoint: box6.endPoint });
|
||||
const faceCenterNormalized = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]];
|
||||
const rotatedImage = tf4.image.rotateWithOffset(input, angle, 0, faceCenterNormalized);
|
||||
rotationMatrix = buildRotationMatrix(-angle, faceCenter);
|
||||
if (config3.face.mesh.enabled)
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.meshSize, this.meshSize]), 255);
|
||||
else
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.boxSize, this.boxSize]), 255);
|
||||
[angle, rotationMatrix, face5] = this.correctFaceRotation(config3, box6, input);
|
||||
} else {
|
||||
rotationMatrix = IDENTITY_MATRIX;
|
||||
const clonedImage = input.clone();
|
||||
if (config3.face.mesh.enabled)
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, clonedImage, [this.meshSize, this.meshSize]), 255);
|
||||
else
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, clonedImage, [this.boxSize, this.boxSize]), 255);
|
||||
const cut = config3.face.mesh.enabled ? cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, clonedImage, [this.meshSize, this.meshSize]) : cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, clonedImage, [this.boxSize, this.boxSize]);
|
||||
face5 = tf4.div(cut, 255);
|
||||
tf4.dispose(cut);
|
||||
tf4.dispose(clonedImage);
|
||||
}
|
||||
if (!config3.face.mesh.enabled) {
|
||||
const prediction2 = {
|
||||
results.push({
|
||||
mesh: [],
|
||||
box: box6,
|
||||
faceConfidence: null,
|
||||
boxConfidence: box6.confidence,
|
||||
confidence: box6.confidence,
|
||||
image: face5
|
||||
};
|
||||
return prediction2;
|
||||
}
|
||||
const [, confidence, contourCoords] = this.meshDetector.execute(face5);
|
||||
const faceConfidence = confidence.dataSync()[0];
|
||||
if (faceConfidence < config3.face.detector.minConfidence) {
|
||||
this.storedBoxes[i].confidence = faceConfidence;
|
||||
return null;
|
||||
}
|
||||
const coordsReshaped = tf4.reshape(contourCoords, [-1, 3]);
|
||||
let rawCoords = coordsReshaped.arraySync();
|
||||
if (config3.face.iris.enabled) {
|
||||
const { box: leftEyeBox, boxSize: leftEyeBoxSize, crop: leftEyeCrop } = this.getEyeBox(rawCoords, face5, eyeLandmarks.leftBounds[0], eyeLandmarks.leftBounds[1], true);
|
||||
const { box: rightEyeBox, boxSize: rightEyeBoxSize, crop: rightEyeCrop } = this.getEyeBox(rawCoords, face5, eyeLandmarks.rightBounds[0], eyeLandmarks.rightBounds[1]);
|
||||
const eyePredictions = this.irisModel.predict(tf4.concat([leftEyeCrop, rightEyeCrop]));
|
||||
const eyePredictionsData = eyePredictions.dataSync();
|
||||
const leftEyeData = eyePredictionsData.slice(0, irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: leftEyeRawCoords, iris: leftIrisRawCoords } = this.getEyeCoords(leftEyeData, leftEyeBox, leftEyeBoxSize, true);
|
||||
const rightEyeData = eyePredictionsData.slice(irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: rightEyeRawCoords, iris: rightIrisRawCoords } = this.getEyeCoords(rightEyeData, rightEyeBox, rightEyeBoxSize);
|
||||
const leftToRightEyeDepthDifference = this.getLeftToRightEyeDepthDifference(rawCoords);
|
||||
if (Math.abs(leftToRightEyeDepthDifference) < 30) {
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, "left", null);
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, "right", null);
|
||||
} else if (leftToRightEyeDepthDifference < 1) {
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, "left", ["EyeUpper0", "EyeLower0"]);
|
||||
});
|
||||
} else {
|
||||
const [contours, confidence, contourCoords] = this.meshDetector.execute(face5);
|
||||
tf4.dispose(contours);
|
||||
const faceConfidence = (await confidence.data())[0];
|
||||
tf4.dispose(confidence);
|
||||
const coordsReshaped = tf4.reshape(contourCoords, [-1, 3]);
|
||||
let rawCoords = await coordsReshaped.array();
|
||||
tf4.dispose(contourCoords);
|
||||
tf4.dispose(coordsReshaped);
|
||||
if (faceConfidence < config3.face.detector.minConfidence) {
|
||||
this.storedBoxes[i].confidence = faceConfidence;
|
||||
tf4.dispose(face5);
|
||||
} else {
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, "right", ["EyeUpper0", "EyeLower0"]);
|
||||
if (config3.face.iris.enabled)
|
||||
rawCoords = await this.augmentIris(rawCoords, face5);
|
||||
const mesh = this.transformRawCoords(rawCoords, box6, angle, rotationMatrix);
|
||||
const storeConfidence = box6.confidence;
|
||||
box6 = enlargeBox(calculateLandmarksBoundingBox(mesh), 1.5);
|
||||
box6.confidence = storeConfidence;
|
||||
if (config3.face.detector.rotation && config3.face.mesh.enabled && config3.face.description.enabled && tf4.ENV.flags.IS_BROWSER) {
|
||||
[angle, rotationMatrix, face5] = this.correctFaceRotation(config3, box6, input);
|
||||
}
|
||||
results.push({
|
||||
mesh,
|
||||
box: box6,
|
||||
faceConfidence,
|
||||
boxConfidence: box6.confidence,
|
||||
confidence: faceConfidence,
|
||||
image: face5
|
||||
});
|
||||
this.storedBoxes[i] = { ...squarifyBox(box6), confidence: box6.confidence, faceConfidence };
|
||||
}
|
||||
const adjustedLeftIrisCoords = this.getAdjustedIrisCoords(rawCoords, leftIrisRawCoords, "left");
|
||||
const adjustedRightIrisCoords = this.getAdjustedIrisCoords(rawCoords, rightIrisRawCoords, "right");
|
||||
rawCoords = rawCoords.concat(adjustedLeftIrisCoords).concat(adjustedRightIrisCoords);
|
||||
}
|
||||
const mesh = this.transformRawCoords(rawCoords, box6, angle, rotationMatrix);
|
||||
const storeConfidence = box6.confidence;
|
||||
box6 = enlargeBox(calculateLandmarksBoundingBox(mesh), 1.5);
|
||||
box6.confidence = storeConfidence;
|
||||
if (config3.face.detector.rotation && config3.face.mesh.enabled && config3.face.description.enabled && tf4.ENV.flags.IS_BROWSER) {
|
||||
const [indexOfMouth, indexOfForehead] = box6.landmarks.length >= meshLandmarks.count ? meshLandmarks.symmetryLine : blazeFaceLandmarks.symmetryLine;
|
||||
angle = computeRotation(box6.landmarks[indexOfMouth], box6.landmarks[indexOfForehead]);
|
||||
const faceCenter = getBoxCenter({ startPoint: box6.startPoint, endPoint: box6.endPoint });
|
||||
const faceCenterNormalized = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]];
|
||||
const rotatedImage = tf4.image.rotateWithOffset(tf4.cast(input, "float32"), angle, 0, faceCenterNormalized);
|
||||
rotationMatrix = buildRotationMatrix(-angle, faceCenter);
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.meshSize, this.meshSize]), 255);
|
||||
}
|
||||
const prediction = {
|
||||
mesh,
|
||||
box: box6,
|
||||
faceConfidence,
|
||||
boxConfidence: box6.confidence,
|
||||
image: face5
|
||||
};
|
||||
this.storedBoxes[i] = { ...squarifyBox(box6), confidence: box6.confidence, faceConfidence };
|
||||
return prediction;
|
||||
}));
|
||||
}
|
||||
if (config3.face.mesh.enabled)
|
||||
this.storedBoxes = this.storedBoxes.filter((a) => a.confidence > config3.face.detector.minConfidence);
|
||||
this.detectedFaces = results.length;
|
||||
|
@ -4239,19 +4256,19 @@ async function predict2(image18, config3, idx, count2) {
|
|||
resT = await model.predict(enhanced);
|
||||
tf6.dispose(enhanced);
|
||||
if (resT) {
|
||||
tf6.tidy(() => {
|
||||
const gender = resT.find((t) => t.shape[1] === 1).dataSync();
|
||||
const confidence = Math.trunc(200 * Math.abs(gender[0] - 0.5)) / 100;
|
||||
if (confidence > config3.face.description.minConfidence) {
|
||||
obj.gender = gender[0] <= 0.5 ? "female" : "male";
|
||||
obj.genderScore = Math.min(0.99, confidence);
|
||||
}
|
||||
const age = tf6.argMax(resT.find((t) => t.shape[1] === 100), 1).dataSync()[0];
|
||||
const all2 = resT.find((t) => t.shape[1] === 100).dataSync();
|
||||
obj.age = Math.round(all2[age - 1] > all2[age + 1] ? 10 * age - 100 * all2[age - 1] : 10 * age + 100 * all2[age + 1]) / 10;
|
||||
const desc = resT.find((t) => t.shape[1] === 1024);
|
||||
obj.descriptor = [...desc.dataSync()];
|
||||
});
|
||||
const gender = await resT.find((t) => t.shape[1] === 1).data();
|
||||
const confidence = Math.trunc(200 * Math.abs(gender[0] - 0.5)) / 100;
|
||||
if (confidence > config3.face.description.minConfidence) {
|
||||
obj.gender = gender[0] <= 0.5 ? "female" : "male";
|
||||
obj.genderScore = Math.min(0.99, confidence);
|
||||
}
|
||||
const argmax = tf6.argMax(resT.find((t) => t.shape[1] === 100), 1);
|
||||
const age = (await argmax.data())[0];
|
||||
const all2 = await resT.find((t) => t.shape[1] === 100).data();
|
||||
obj.age = Math.round(all2[age - 1] > all2[age + 1] ? 10 * age - 100 * all2[age - 1] : 10 * age + 100 * all2[age + 1]) / 10;
|
||||
const desc = resT.find((t) => t.shape[1] === 1024);
|
||||
const descriptor = await desc.data();
|
||||
obj.descriptor = [...descriptor];
|
||||
resT.forEach((t) => tf6.dispose(t));
|
||||
}
|
||||
last[idx] = obj;
|
||||
|
@ -7909,7 +7926,7 @@ var HandPipeline = class {
|
|||
tf11.dispose(rotatedImage);
|
||||
const [confidenceT, keypoints3] = await this.handPoseModel.predict(handImage);
|
||||
tf11.dispose(handImage);
|
||||
const confidence = confidenceT.dataSync()[0];
|
||||
const confidence = (await confidenceT.data())[0];
|
||||
tf11.dispose(confidenceT);
|
||||
if (confidence >= config3.hand.minConfidence) {
|
||||
const keypointsReshaped = tf11.reshape(keypoints3, [-1, 3]);
|
||||
|
@ -8303,8 +8320,8 @@ async function predict8(image18, config3) {
|
|||
if (!model6.inputs[0].shape)
|
||||
return null;
|
||||
const resize = tf15.image.resizeBilinear(image18, [model6.inputs[0].shape[2], model6.inputs[0].shape[1]], false);
|
||||
const cast5 = tf15.cast(resize, "int32");
|
||||
return cast5;
|
||||
const cast4 = tf15.cast(resize, "int32");
|
||||
return cast4;
|
||||
});
|
||||
let resT;
|
||||
if (config3.body.enabled)
|
||||
|
@ -11375,10 +11392,24 @@ var Human = class {
|
|||
if (this.config.backend && this.config.backend.length > 0) {
|
||||
if (typeof window === "undefined" && typeof WorkerGlobalScope !== "undefined" && this.config.debug)
|
||||
log("running inside web worker");
|
||||
if (this.tf.ENV.flags.IS_BROWSER && this.config.backend === "tensorflow")
|
||||
this.config.backend = "webgl";
|
||||
if (this.tf.ENV.flags.IS_NODE && (this.config.backend === "webgl" || this.config.backend === "humangl"))
|
||||
if (this.tf.ENV.flags.IS_BROWSER && this.config.backend === "tensorflow") {
|
||||
if (this.config.debug)
|
||||
log("override: backend set to tensorflow while running in browser");
|
||||
this.config.backend = "humangl";
|
||||
}
|
||||
if (this.tf.ENV.flags.IS_NODE && (this.config.backend === "webgl" || this.config.backend === "humangl")) {
|
||||
if (this.config.debug)
|
||||
log("override: backend set to webgl while running in nodejs");
|
||||
this.config.backend = "tensorflow";
|
||||
}
|
||||
const available = Object.keys(this.tf.engine().registryFactory);
|
||||
if (this.config.debug)
|
||||
log("available backends:", available);
|
||||
if (!available.includes(this.config.backend)) {
|
||||
log(`error: backend ${this.config.backend} not found in registry`);
|
||||
this.config.backend = this.tf.ENV.flags.IS_NODE ? "tensorflow" : "humangl";
|
||||
log(`override: using backend ${this.config.backend} instead`);
|
||||
}
|
||||
if (this.config.debug)
|
||||
log("setting backend:", this.config.backend);
|
||||
if (this.config.backend === "wasm") {
|
||||
|
|
|
@ -513,7 +513,7 @@ var BlazeFaceModel = class {
|
|||
}
|
||||
const boxesOut = decodeBounds(batchOut, this.anchors, [this.inputSize, this.inputSize]);
|
||||
const logits = tf3.slice(batchOut, [0, 0], [-1, 1]);
|
||||
const scoresOut = tf3.squeeze(tf3.sigmoid(logits)).dataSync();
|
||||
const scoresOut = tf3.squeeze(tf3.sigmoid(logits));
|
||||
return [batchOut, boxesOut, scoresOut];
|
||||
});
|
||||
this.config = mergeDeep(this.config, userConfig);
|
||||
|
@ -521,8 +521,9 @@ var BlazeFaceModel = class {
|
|||
const nms = await nmsTensor.array();
|
||||
tf3.dispose(nmsTensor);
|
||||
const annotatedBoxes = [];
|
||||
const scoresData = await scores.data();
|
||||
for (let i = 0; i < nms.length; i++) {
|
||||
const confidence = scores[nms[i]];
|
||||
const confidence = scoresData[nms[i]];
|
||||
if (confidence > this.config.face.detector.minConfidence) {
|
||||
const boundingBox = tf3.slice(boxes, [nms[i], 0], [1, -1]);
|
||||
const localBox = createBox(boundingBox);
|
||||
|
@ -534,6 +535,7 @@ var BlazeFaceModel = class {
|
|||
}
|
||||
tf3.dispose(batch);
|
||||
tf3.dispose(boxes);
|
||||
tf3.dispose(scores);
|
||||
return {
|
||||
boxes: annotatedBoxes,
|
||||
scaleFactor: [inputImage.shape[2] / this.inputSize, inputImage.shape[1] / this.inputSize]
|
||||
|
@ -3911,7 +3913,9 @@ var Pipeline = class {
|
|||
box6.endPoint[0] / this.meshSize
|
||||
]], [0], [this.irisSize, this.irisSize]);
|
||||
if (flip && tf4.ENV.flags.IS_BROWSER) {
|
||||
crop = tf4.image.flipLeftRight(crop);
|
||||
const flipped = tf4.image.flipLeftRight(crop);
|
||||
tf4.dispose(crop);
|
||||
crop = flipped;
|
||||
}
|
||||
return { box: box6, boxSize, crop };
|
||||
}
|
||||
|
@ -3943,6 +3947,47 @@ var Pipeline = class {
|
|||
return [coord[0], coord[1], z];
|
||||
});
|
||||
}
|
||||
correctFaceRotation(config3, box6, input) {
|
||||
const [indexOfMouth, indexOfForehead] = box6.landmarks.length >= meshLandmarks.count ? meshLandmarks.symmetryLine : blazeFaceLandmarks.symmetryLine;
|
||||
const angle = computeRotation(box6.landmarks[indexOfMouth], box6.landmarks[indexOfForehead]);
|
||||
const faceCenter = getBoxCenter({ startPoint: box6.startPoint, endPoint: box6.endPoint });
|
||||
const faceCenterNormalized = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]];
|
||||
const rotatedImage = tf4.image.rotateWithOffset(input, angle, 0, faceCenterNormalized);
|
||||
const rotationMatrix = buildRotationMatrix(-angle, faceCenter);
|
||||
const cut = config3.face.mesh.enabled ? cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.meshSize, this.meshSize]) : cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.boxSize, this.boxSize]);
|
||||
const face5 = tf4.div(cut, 255);
|
||||
tf4.dispose(cut);
|
||||
tf4.dispose(rotatedImage);
|
||||
return [angle, rotationMatrix, face5];
|
||||
}
|
||||
async augmentIris(rawCoords, face5) {
|
||||
const { box: leftEyeBox, boxSize: leftEyeBoxSize, crop: leftEyeCrop } = this.getEyeBox(rawCoords, face5, eyeLandmarks.leftBounds[0], eyeLandmarks.leftBounds[1], true);
|
||||
const { box: rightEyeBox, boxSize: rightEyeBoxSize, crop: rightEyeCrop } = this.getEyeBox(rawCoords, face5, eyeLandmarks.rightBounds[0], eyeLandmarks.rightBounds[1]);
|
||||
const combined = tf4.concat([leftEyeCrop, rightEyeCrop]);
|
||||
tf4.dispose(leftEyeCrop);
|
||||
tf4.dispose(rightEyeCrop);
|
||||
const eyePredictions = this.irisModel.predict(combined);
|
||||
tf4.dispose(combined);
|
||||
const eyePredictionsData = await eyePredictions.data();
|
||||
tf4.dispose(eyePredictions);
|
||||
const leftEyeData = eyePredictionsData.slice(0, irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: leftEyeRawCoords, iris: leftIrisRawCoords } = this.getEyeCoords(leftEyeData, leftEyeBox, leftEyeBoxSize, true);
|
||||
const rightEyeData = eyePredictionsData.slice(irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: rightEyeRawCoords, iris: rightIrisRawCoords } = this.getEyeCoords(rightEyeData, rightEyeBox, rightEyeBoxSize);
|
||||
const leftToRightEyeDepthDifference = this.getLeftToRightEyeDepthDifference(rawCoords);
|
||||
if (Math.abs(leftToRightEyeDepthDifference) < 30) {
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, "left", null);
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, "right", null);
|
||||
} else if (leftToRightEyeDepthDifference < 1) {
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, "left", ["EyeUpper0", "EyeLower0"]);
|
||||
} else {
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, "right", ["EyeUpper0", "EyeLower0"]);
|
||||
}
|
||||
const adjustedLeftIrisCoords = this.getAdjustedIrisCoords(rawCoords, leftIrisRawCoords, "left");
|
||||
const adjustedRightIrisCoords = this.getAdjustedIrisCoords(rawCoords, rightIrisRawCoords, "right");
|
||||
const newCoords = rawCoords.concat(adjustedLeftIrisCoords).concat(adjustedRightIrisCoords);
|
||||
return newCoords;
|
||||
}
|
||||
async predict(input, config3) {
|
||||
let useFreshBox = false;
|
||||
let detector;
|
||||
|
@ -3986,93 +4031,65 @@ var Pipeline = class {
|
|||
tf4.dispose(prediction.landmarks);
|
||||
});
|
||||
}
|
||||
const results = tf4.tidy(() => this.storedBoxes.map((box6, i) => {
|
||||
const results = [];
|
||||
for (let i = 0; i < this.storedBoxes.length; i++) {
|
||||
let box6 = this.storedBoxes[i];
|
||||
let face5;
|
||||
let angle = 0;
|
||||
let rotationMatrix;
|
||||
if (config3.face.detector.rotation && config3.face.mesh.enabled && tf4.ENV.flags.IS_BROWSER) {
|
||||
const [indexOfMouth, indexOfForehead] = box6.landmarks.length >= meshLandmarks.count ? meshLandmarks.symmetryLine : blazeFaceLandmarks.symmetryLine;
|
||||
angle = computeRotation(box6.landmarks[indexOfMouth], box6.landmarks[indexOfForehead]);
|
||||
const faceCenter = getBoxCenter({ startPoint: box6.startPoint, endPoint: box6.endPoint });
|
||||
const faceCenterNormalized = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]];
|
||||
const rotatedImage = tf4.image.rotateWithOffset(input, angle, 0, faceCenterNormalized);
|
||||
rotationMatrix = buildRotationMatrix(-angle, faceCenter);
|
||||
if (config3.face.mesh.enabled)
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.meshSize, this.meshSize]), 255);
|
||||
else
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.boxSize, this.boxSize]), 255);
|
||||
[angle, rotationMatrix, face5] = this.correctFaceRotation(config3, box6, input);
|
||||
} else {
|
||||
rotationMatrix = IDENTITY_MATRIX;
|
||||
const clonedImage = input.clone();
|
||||
if (config3.face.mesh.enabled)
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, clonedImage, [this.meshSize, this.meshSize]), 255);
|
||||
else
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, clonedImage, [this.boxSize, this.boxSize]), 255);
|
||||
const cut = config3.face.mesh.enabled ? cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, clonedImage, [this.meshSize, this.meshSize]) : cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, clonedImage, [this.boxSize, this.boxSize]);
|
||||
face5 = tf4.div(cut, 255);
|
||||
tf4.dispose(cut);
|
||||
tf4.dispose(clonedImage);
|
||||
}
|
||||
if (!config3.face.mesh.enabled) {
|
||||
const prediction2 = {
|
||||
results.push({
|
||||
mesh: [],
|
||||
box: box6,
|
||||
faceConfidence: null,
|
||||
boxConfidence: box6.confidence,
|
||||
confidence: box6.confidence,
|
||||
image: face5
|
||||
};
|
||||
return prediction2;
|
||||
}
|
||||
const [, confidence, contourCoords] = this.meshDetector.execute(face5);
|
||||
const faceConfidence = confidence.dataSync()[0];
|
||||
if (faceConfidence < config3.face.detector.minConfidence) {
|
||||
this.storedBoxes[i].confidence = faceConfidence;
|
||||
return null;
|
||||
}
|
||||
const coordsReshaped = tf4.reshape(contourCoords, [-1, 3]);
|
||||
let rawCoords = coordsReshaped.arraySync();
|
||||
if (config3.face.iris.enabled) {
|
||||
const { box: leftEyeBox, boxSize: leftEyeBoxSize, crop: leftEyeCrop } = this.getEyeBox(rawCoords, face5, eyeLandmarks.leftBounds[0], eyeLandmarks.leftBounds[1], true);
|
||||
const { box: rightEyeBox, boxSize: rightEyeBoxSize, crop: rightEyeCrop } = this.getEyeBox(rawCoords, face5, eyeLandmarks.rightBounds[0], eyeLandmarks.rightBounds[1]);
|
||||
const eyePredictions = this.irisModel.predict(tf4.concat([leftEyeCrop, rightEyeCrop]));
|
||||
const eyePredictionsData = eyePredictions.dataSync();
|
||||
const leftEyeData = eyePredictionsData.slice(0, irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: leftEyeRawCoords, iris: leftIrisRawCoords } = this.getEyeCoords(leftEyeData, leftEyeBox, leftEyeBoxSize, true);
|
||||
const rightEyeData = eyePredictionsData.slice(irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: rightEyeRawCoords, iris: rightIrisRawCoords } = this.getEyeCoords(rightEyeData, rightEyeBox, rightEyeBoxSize);
|
||||
const leftToRightEyeDepthDifference = this.getLeftToRightEyeDepthDifference(rawCoords);
|
||||
if (Math.abs(leftToRightEyeDepthDifference) < 30) {
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, "left", null);
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, "right", null);
|
||||
} else if (leftToRightEyeDepthDifference < 1) {
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, "left", ["EyeUpper0", "EyeLower0"]);
|
||||
});
|
||||
} else {
|
||||
const [contours, confidence, contourCoords] = this.meshDetector.execute(face5);
|
||||
tf4.dispose(contours);
|
||||
const faceConfidence = (await confidence.data())[0];
|
||||
tf4.dispose(confidence);
|
||||
const coordsReshaped = tf4.reshape(contourCoords, [-1, 3]);
|
||||
let rawCoords = await coordsReshaped.array();
|
||||
tf4.dispose(contourCoords);
|
||||
tf4.dispose(coordsReshaped);
|
||||
if (faceConfidence < config3.face.detector.minConfidence) {
|
||||
this.storedBoxes[i].confidence = faceConfidence;
|
||||
tf4.dispose(face5);
|
||||
} else {
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, "right", ["EyeUpper0", "EyeLower0"]);
|
||||
if (config3.face.iris.enabled)
|
||||
rawCoords = await this.augmentIris(rawCoords, face5);
|
||||
const mesh = this.transformRawCoords(rawCoords, box6, angle, rotationMatrix);
|
||||
const storeConfidence = box6.confidence;
|
||||
box6 = enlargeBox(calculateLandmarksBoundingBox(mesh), 1.5);
|
||||
box6.confidence = storeConfidence;
|
||||
if (config3.face.detector.rotation && config3.face.mesh.enabled && config3.face.description.enabled && tf4.ENV.flags.IS_BROWSER) {
|
||||
[angle, rotationMatrix, face5] = this.correctFaceRotation(config3, box6, input);
|
||||
}
|
||||
results.push({
|
||||
mesh,
|
||||
box: box6,
|
||||
faceConfidence,
|
||||
boxConfidence: box6.confidence,
|
||||
confidence: faceConfidence,
|
||||
image: face5
|
||||
});
|
||||
this.storedBoxes[i] = { ...squarifyBox(box6), confidence: box6.confidence, faceConfidence };
|
||||
}
|
||||
const adjustedLeftIrisCoords = this.getAdjustedIrisCoords(rawCoords, leftIrisRawCoords, "left");
|
||||
const adjustedRightIrisCoords = this.getAdjustedIrisCoords(rawCoords, rightIrisRawCoords, "right");
|
||||
rawCoords = rawCoords.concat(adjustedLeftIrisCoords).concat(adjustedRightIrisCoords);
|
||||
}
|
||||
const mesh = this.transformRawCoords(rawCoords, box6, angle, rotationMatrix);
|
||||
const storeConfidence = box6.confidence;
|
||||
box6 = enlargeBox(calculateLandmarksBoundingBox(mesh), 1.5);
|
||||
box6.confidence = storeConfidence;
|
||||
if (config3.face.detector.rotation && config3.face.mesh.enabled && config3.face.description.enabled && tf4.ENV.flags.IS_BROWSER) {
|
||||
const [indexOfMouth, indexOfForehead] = box6.landmarks.length >= meshLandmarks.count ? meshLandmarks.symmetryLine : blazeFaceLandmarks.symmetryLine;
|
||||
angle = computeRotation(box6.landmarks[indexOfMouth], box6.landmarks[indexOfForehead]);
|
||||
const faceCenter = getBoxCenter({ startPoint: box6.startPoint, endPoint: box6.endPoint });
|
||||
const faceCenterNormalized = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]];
|
||||
const rotatedImage = tf4.image.rotateWithOffset(tf4.cast(input, "float32"), angle, 0, faceCenterNormalized);
|
||||
rotationMatrix = buildRotationMatrix(-angle, faceCenter);
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.meshSize, this.meshSize]), 255);
|
||||
}
|
||||
const prediction = {
|
||||
mesh,
|
||||
box: box6,
|
||||
faceConfidence,
|
||||
boxConfidence: box6.confidence,
|
||||
image: face5
|
||||
};
|
||||
this.storedBoxes[i] = { ...squarifyBox(box6), confidence: box6.confidence, faceConfidence };
|
||||
return prediction;
|
||||
}));
|
||||
}
|
||||
if (config3.face.mesh.enabled)
|
||||
this.storedBoxes = this.storedBoxes.filter((a) => a.confidence > config3.face.detector.minConfidence);
|
||||
this.detectedFaces = results.length;
|
||||
|
@ -4240,19 +4257,19 @@ async function predict2(image18, config3, idx, count2) {
|
|||
resT = await model.predict(enhanced);
|
||||
tf6.dispose(enhanced);
|
||||
if (resT) {
|
||||
tf6.tidy(() => {
|
||||
const gender = resT.find((t) => t.shape[1] === 1).dataSync();
|
||||
const confidence = Math.trunc(200 * Math.abs(gender[0] - 0.5)) / 100;
|
||||
if (confidence > config3.face.description.minConfidence) {
|
||||
obj.gender = gender[0] <= 0.5 ? "female" : "male";
|
||||
obj.genderScore = Math.min(0.99, confidence);
|
||||
}
|
||||
const age = tf6.argMax(resT.find((t) => t.shape[1] === 100), 1).dataSync()[0];
|
||||
const all2 = resT.find((t) => t.shape[1] === 100).dataSync();
|
||||
obj.age = Math.round(all2[age - 1] > all2[age + 1] ? 10 * age - 100 * all2[age - 1] : 10 * age + 100 * all2[age + 1]) / 10;
|
||||
const desc = resT.find((t) => t.shape[1] === 1024);
|
||||
obj.descriptor = [...desc.dataSync()];
|
||||
});
|
||||
const gender = await resT.find((t) => t.shape[1] === 1).data();
|
||||
const confidence = Math.trunc(200 * Math.abs(gender[0] - 0.5)) / 100;
|
||||
if (confidence > config3.face.description.minConfidence) {
|
||||
obj.gender = gender[0] <= 0.5 ? "female" : "male";
|
||||
obj.genderScore = Math.min(0.99, confidence);
|
||||
}
|
||||
const argmax = tf6.argMax(resT.find((t) => t.shape[1] === 100), 1);
|
||||
const age = (await argmax.data())[0];
|
||||
const all2 = await resT.find((t) => t.shape[1] === 100).data();
|
||||
obj.age = Math.round(all2[age - 1] > all2[age + 1] ? 10 * age - 100 * all2[age - 1] : 10 * age + 100 * all2[age + 1]) / 10;
|
||||
const desc = resT.find((t) => t.shape[1] === 1024);
|
||||
const descriptor = await desc.data();
|
||||
obj.descriptor = [...descriptor];
|
||||
resT.forEach((t) => tf6.dispose(t));
|
||||
}
|
||||
last[idx] = obj;
|
||||
|
@ -7910,7 +7927,7 @@ var HandPipeline = class {
|
|||
tf11.dispose(rotatedImage);
|
||||
const [confidenceT, keypoints3] = await this.handPoseModel.predict(handImage);
|
||||
tf11.dispose(handImage);
|
||||
const confidence = confidenceT.dataSync()[0];
|
||||
const confidence = (await confidenceT.data())[0];
|
||||
tf11.dispose(confidenceT);
|
||||
if (confidence >= config3.hand.minConfidence) {
|
||||
const keypointsReshaped = tf11.reshape(keypoints3, [-1, 3]);
|
||||
|
@ -8304,8 +8321,8 @@ async function predict8(image18, config3) {
|
|||
if (!model6.inputs[0].shape)
|
||||
return null;
|
||||
const resize = tf15.image.resizeBilinear(image18, [model6.inputs[0].shape[2], model6.inputs[0].shape[1]], false);
|
||||
const cast5 = tf15.cast(resize, "int32");
|
||||
return cast5;
|
||||
const cast4 = tf15.cast(resize, "int32");
|
||||
return cast4;
|
||||
});
|
||||
let resT;
|
||||
if (config3.body.enabled)
|
||||
|
@ -11376,10 +11393,24 @@ var Human = class {
|
|||
if (this.config.backend && this.config.backend.length > 0) {
|
||||
if (typeof window === "undefined" && typeof WorkerGlobalScope !== "undefined" && this.config.debug)
|
||||
log("running inside web worker");
|
||||
if (this.tf.ENV.flags.IS_BROWSER && this.config.backend === "tensorflow")
|
||||
this.config.backend = "webgl";
|
||||
if (this.tf.ENV.flags.IS_NODE && (this.config.backend === "webgl" || this.config.backend === "humangl"))
|
||||
if (this.tf.ENV.flags.IS_BROWSER && this.config.backend === "tensorflow") {
|
||||
if (this.config.debug)
|
||||
log("override: backend set to tensorflow while running in browser");
|
||||
this.config.backend = "humangl";
|
||||
}
|
||||
if (this.tf.ENV.flags.IS_NODE && (this.config.backend === "webgl" || this.config.backend === "humangl")) {
|
||||
if (this.config.debug)
|
||||
log("override: backend set to webgl while running in nodejs");
|
||||
this.config.backend = "tensorflow";
|
||||
}
|
||||
const available = Object.keys(this.tf.engine().registryFactory);
|
||||
if (this.config.debug)
|
||||
log("available backends:", available);
|
||||
if (!available.includes(this.config.backend)) {
|
||||
log(`error: backend ${this.config.backend} not found in registry`);
|
||||
this.config.backend = this.tf.ENV.flags.IS_NODE ? "tensorflow" : "humangl";
|
||||
log(`override: using backend ${this.config.backend} instead`);
|
||||
}
|
||||
if (this.config.debug)
|
||||
log("setting backend:", this.config.backend);
|
||||
if (this.config.backend === "wasm") {
|
||||
|
|
|
@ -512,7 +512,7 @@ var BlazeFaceModel = class {
|
|||
}
|
||||
const boxesOut = decodeBounds(batchOut, this.anchors, [this.inputSize, this.inputSize]);
|
||||
const logits = tf3.slice(batchOut, [0, 0], [-1, 1]);
|
||||
const scoresOut = tf3.squeeze(tf3.sigmoid(logits)).dataSync();
|
||||
const scoresOut = tf3.squeeze(tf3.sigmoid(logits));
|
||||
return [batchOut, boxesOut, scoresOut];
|
||||
});
|
||||
this.config = mergeDeep(this.config, userConfig);
|
||||
|
@ -520,8 +520,9 @@ var BlazeFaceModel = class {
|
|||
const nms = await nmsTensor.array();
|
||||
tf3.dispose(nmsTensor);
|
||||
const annotatedBoxes = [];
|
||||
const scoresData = await scores.data();
|
||||
for (let i = 0; i < nms.length; i++) {
|
||||
const confidence = scores[nms[i]];
|
||||
const confidence = scoresData[nms[i]];
|
||||
if (confidence > this.config.face.detector.minConfidence) {
|
||||
const boundingBox = tf3.slice(boxes, [nms[i], 0], [1, -1]);
|
||||
const localBox = createBox(boundingBox);
|
||||
|
@ -533,6 +534,7 @@ var BlazeFaceModel = class {
|
|||
}
|
||||
tf3.dispose(batch);
|
||||
tf3.dispose(boxes);
|
||||
tf3.dispose(scores);
|
||||
return {
|
||||
boxes: annotatedBoxes,
|
||||
scaleFactor: [inputImage.shape[2] / this.inputSize, inputImage.shape[1] / this.inputSize]
|
||||
|
@ -3910,7 +3912,9 @@ var Pipeline = class {
|
|||
box6.endPoint[0] / this.meshSize
|
||||
]], [0], [this.irisSize, this.irisSize]);
|
||||
if (flip && tf4.ENV.flags.IS_BROWSER) {
|
||||
crop = tf4.image.flipLeftRight(crop);
|
||||
const flipped = tf4.image.flipLeftRight(crop);
|
||||
tf4.dispose(crop);
|
||||
crop = flipped;
|
||||
}
|
||||
return { box: box6, boxSize, crop };
|
||||
}
|
||||
|
@ -3942,6 +3946,47 @@ var Pipeline = class {
|
|||
return [coord[0], coord[1], z];
|
||||
});
|
||||
}
|
||||
correctFaceRotation(config3, box6, input) {
|
||||
const [indexOfMouth, indexOfForehead] = box6.landmarks.length >= meshLandmarks.count ? meshLandmarks.symmetryLine : blazeFaceLandmarks.symmetryLine;
|
||||
const angle = computeRotation(box6.landmarks[indexOfMouth], box6.landmarks[indexOfForehead]);
|
||||
const faceCenter = getBoxCenter({ startPoint: box6.startPoint, endPoint: box6.endPoint });
|
||||
const faceCenterNormalized = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]];
|
||||
const rotatedImage = tf4.image.rotateWithOffset(input, angle, 0, faceCenterNormalized);
|
||||
const rotationMatrix = buildRotationMatrix(-angle, faceCenter);
|
||||
const cut = config3.face.mesh.enabled ? cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.meshSize, this.meshSize]) : cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.boxSize, this.boxSize]);
|
||||
const face5 = tf4.div(cut, 255);
|
||||
tf4.dispose(cut);
|
||||
tf4.dispose(rotatedImage);
|
||||
return [angle, rotationMatrix, face5];
|
||||
}
|
||||
async augmentIris(rawCoords, face5) {
|
||||
const { box: leftEyeBox, boxSize: leftEyeBoxSize, crop: leftEyeCrop } = this.getEyeBox(rawCoords, face5, eyeLandmarks.leftBounds[0], eyeLandmarks.leftBounds[1], true);
|
||||
const { box: rightEyeBox, boxSize: rightEyeBoxSize, crop: rightEyeCrop } = this.getEyeBox(rawCoords, face5, eyeLandmarks.rightBounds[0], eyeLandmarks.rightBounds[1]);
|
||||
const combined = tf4.concat([leftEyeCrop, rightEyeCrop]);
|
||||
tf4.dispose(leftEyeCrop);
|
||||
tf4.dispose(rightEyeCrop);
|
||||
const eyePredictions = this.irisModel.predict(combined);
|
||||
tf4.dispose(combined);
|
||||
const eyePredictionsData = await eyePredictions.data();
|
||||
tf4.dispose(eyePredictions);
|
||||
const leftEyeData = eyePredictionsData.slice(0, irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: leftEyeRawCoords, iris: leftIrisRawCoords } = this.getEyeCoords(leftEyeData, leftEyeBox, leftEyeBoxSize, true);
|
||||
const rightEyeData = eyePredictionsData.slice(irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: rightEyeRawCoords, iris: rightIrisRawCoords } = this.getEyeCoords(rightEyeData, rightEyeBox, rightEyeBoxSize);
|
||||
const leftToRightEyeDepthDifference = this.getLeftToRightEyeDepthDifference(rawCoords);
|
||||
if (Math.abs(leftToRightEyeDepthDifference) < 30) {
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, "left", null);
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, "right", null);
|
||||
} else if (leftToRightEyeDepthDifference < 1) {
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, "left", ["EyeUpper0", "EyeLower0"]);
|
||||
} else {
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, "right", ["EyeUpper0", "EyeLower0"]);
|
||||
}
|
||||
const adjustedLeftIrisCoords = this.getAdjustedIrisCoords(rawCoords, leftIrisRawCoords, "left");
|
||||
const adjustedRightIrisCoords = this.getAdjustedIrisCoords(rawCoords, rightIrisRawCoords, "right");
|
||||
const newCoords = rawCoords.concat(adjustedLeftIrisCoords).concat(adjustedRightIrisCoords);
|
||||
return newCoords;
|
||||
}
|
||||
async predict(input, config3) {
|
||||
let useFreshBox = false;
|
||||
let detector;
|
||||
|
@ -3985,93 +4030,65 @@ var Pipeline = class {
|
|||
tf4.dispose(prediction.landmarks);
|
||||
});
|
||||
}
|
||||
const results = tf4.tidy(() => this.storedBoxes.map((box6, i) => {
|
||||
const results = [];
|
||||
for (let i = 0; i < this.storedBoxes.length; i++) {
|
||||
let box6 = this.storedBoxes[i];
|
||||
let face5;
|
||||
let angle = 0;
|
||||
let rotationMatrix;
|
||||
if (config3.face.detector.rotation && config3.face.mesh.enabled && tf4.ENV.flags.IS_BROWSER) {
|
||||
const [indexOfMouth, indexOfForehead] = box6.landmarks.length >= meshLandmarks.count ? meshLandmarks.symmetryLine : blazeFaceLandmarks.symmetryLine;
|
||||
angle = computeRotation(box6.landmarks[indexOfMouth], box6.landmarks[indexOfForehead]);
|
||||
const faceCenter = getBoxCenter({ startPoint: box6.startPoint, endPoint: box6.endPoint });
|
||||
const faceCenterNormalized = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]];
|
||||
const rotatedImage = tf4.image.rotateWithOffset(input, angle, 0, faceCenterNormalized);
|
||||
rotationMatrix = buildRotationMatrix(-angle, faceCenter);
|
||||
if (config3.face.mesh.enabled)
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.meshSize, this.meshSize]), 255);
|
||||
else
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.boxSize, this.boxSize]), 255);
|
||||
[angle, rotationMatrix, face5] = this.correctFaceRotation(config3, box6, input);
|
||||
} else {
|
||||
rotationMatrix = IDENTITY_MATRIX;
|
||||
const clonedImage = input.clone();
|
||||
if (config3.face.mesh.enabled)
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, clonedImage, [this.meshSize, this.meshSize]), 255);
|
||||
else
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, clonedImage, [this.boxSize, this.boxSize]), 255);
|
||||
const cut = config3.face.mesh.enabled ? cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, clonedImage, [this.meshSize, this.meshSize]) : cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, clonedImage, [this.boxSize, this.boxSize]);
|
||||
face5 = tf4.div(cut, 255);
|
||||
tf4.dispose(cut);
|
||||
tf4.dispose(clonedImage);
|
||||
}
|
||||
if (!config3.face.mesh.enabled) {
|
||||
const prediction2 = {
|
||||
results.push({
|
||||
mesh: [],
|
||||
box: box6,
|
||||
faceConfidence: null,
|
||||
boxConfidence: box6.confidence,
|
||||
confidence: box6.confidence,
|
||||
image: face5
|
||||
};
|
||||
return prediction2;
|
||||
}
|
||||
const [, confidence, contourCoords] = this.meshDetector.execute(face5);
|
||||
const faceConfidence = confidence.dataSync()[0];
|
||||
if (faceConfidence < config3.face.detector.minConfidence) {
|
||||
this.storedBoxes[i].confidence = faceConfidence;
|
||||
return null;
|
||||
}
|
||||
const coordsReshaped = tf4.reshape(contourCoords, [-1, 3]);
|
||||
let rawCoords = coordsReshaped.arraySync();
|
||||
if (config3.face.iris.enabled) {
|
||||
const { box: leftEyeBox, boxSize: leftEyeBoxSize, crop: leftEyeCrop } = this.getEyeBox(rawCoords, face5, eyeLandmarks.leftBounds[0], eyeLandmarks.leftBounds[1], true);
|
||||
const { box: rightEyeBox, boxSize: rightEyeBoxSize, crop: rightEyeCrop } = this.getEyeBox(rawCoords, face5, eyeLandmarks.rightBounds[0], eyeLandmarks.rightBounds[1]);
|
||||
const eyePredictions = this.irisModel.predict(tf4.concat([leftEyeCrop, rightEyeCrop]));
|
||||
const eyePredictionsData = eyePredictions.dataSync();
|
||||
const leftEyeData = eyePredictionsData.slice(0, irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: leftEyeRawCoords, iris: leftIrisRawCoords } = this.getEyeCoords(leftEyeData, leftEyeBox, leftEyeBoxSize, true);
|
||||
const rightEyeData = eyePredictionsData.slice(irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: rightEyeRawCoords, iris: rightIrisRawCoords } = this.getEyeCoords(rightEyeData, rightEyeBox, rightEyeBoxSize);
|
||||
const leftToRightEyeDepthDifference = this.getLeftToRightEyeDepthDifference(rawCoords);
|
||||
if (Math.abs(leftToRightEyeDepthDifference) < 30) {
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, "left", null);
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, "right", null);
|
||||
} else if (leftToRightEyeDepthDifference < 1) {
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, "left", ["EyeUpper0", "EyeLower0"]);
|
||||
});
|
||||
} else {
|
||||
const [contours, confidence, contourCoords] = this.meshDetector.execute(face5);
|
||||
tf4.dispose(contours);
|
||||
const faceConfidence = (await confidence.data())[0];
|
||||
tf4.dispose(confidence);
|
||||
const coordsReshaped = tf4.reshape(contourCoords, [-1, 3]);
|
||||
let rawCoords = await coordsReshaped.array();
|
||||
tf4.dispose(contourCoords);
|
||||
tf4.dispose(coordsReshaped);
|
||||
if (faceConfidence < config3.face.detector.minConfidence) {
|
||||
this.storedBoxes[i].confidence = faceConfidence;
|
||||
tf4.dispose(face5);
|
||||
} else {
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, "right", ["EyeUpper0", "EyeLower0"]);
|
||||
if (config3.face.iris.enabled)
|
||||
rawCoords = await this.augmentIris(rawCoords, face5);
|
||||
const mesh = this.transformRawCoords(rawCoords, box6, angle, rotationMatrix);
|
||||
const storeConfidence = box6.confidence;
|
||||
box6 = enlargeBox(calculateLandmarksBoundingBox(mesh), 1.5);
|
||||
box6.confidence = storeConfidence;
|
||||
if (config3.face.detector.rotation && config3.face.mesh.enabled && config3.face.description.enabled && tf4.ENV.flags.IS_BROWSER) {
|
||||
[angle, rotationMatrix, face5] = this.correctFaceRotation(config3, box6, input);
|
||||
}
|
||||
results.push({
|
||||
mesh,
|
||||
box: box6,
|
||||
faceConfidence,
|
||||
boxConfidence: box6.confidence,
|
||||
confidence: faceConfidence,
|
||||
image: face5
|
||||
});
|
||||
this.storedBoxes[i] = { ...squarifyBox(box6), confidence: box6.confidence, faceConfidence };
|
||||
}
|
||||
const adjustedLeftIrisCoords = this.getAdjustedIrisCoords(rawCoords, leftIrisRawCoords, "left");
|
||||
const adjustedRightIrisCoords = this.getAdjustedIrisCoords(rawCoords, rightIrisRawCoords, "right");
|
||||
rawCoords = rawCoords.concat(adjustedLeftIrisCoords).concat(adjustedRightIrisCoords);
|
||||
}
|
||||
const mesh = this.transformRawCoords(rawCoords, box6, angle, rotationMatrix);
|
||||
const storeConfidence = box6.confidence;
|
||||
box6 = enlargeBox(calculateLandmarksBoundingBox(mesh), 1.5);
|
||||
box6.confidence = storeConfidence;
|
||||
if (config3.face.detector.rotation && config3.face.mesh.enabled && config3.face.description.enabled && tf4.ENV.flags.IS_BROWSER) {
|
||||
const [indexOfMouth, indexOfForehead] = box6.landmarks.length >= meshLandmarks.count ? meshLandmarks.symmetryLine : blazeFaceLandmarks.symmetryLine;
|
||||
angle = computeRotation(box6.landmarks[indexOfMouth], box6.landmarks[indexOfForehead]);
|
||||
const faceCenter = getBoxCenter({ startPoint: box6.startPoint, endPoint: box6.endPoint });
|
||||
const faceCenterNormalized = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]];
|
||||
const rotatedImage = tf4.image.rotateWithOffset(tf4.cast(input, "float32"), angle, 0, faceCenterNormalized);
|
||||
rotationMatrix = buildRotationMatrix(-angle, faceCenter);
|
||||
face5 = tf4.div(cutBoxFromImageAndResize({ startPoint: box6.startPoint, endPoint: box6.endPoint }, rotatedImage, [this.meshSize, this.meshSize]), 255);
|
||||
}
|
||||
const prediction = {
|
||||
mesh,
|
||||
box: box6,
|
||||
faceConfidence,
|
||||
boxConfidence: box6.confidence,
|
||||
image: face5
|
||||
};
|
||||
this.storedBoxes[i] = { ...squarifyBox(box6), confidence: box6.confidence, faceConfidence };
|
||||
return prediction;
|
||||
}));
|
||||
}
|
||||
if (config3.face.mesh.enabled)
|
||||
this.storedBoxes = this.storedBoxes.filter((a) => a.confidence > config3.face.detector.minConfidence);
|
||||
this.detectedFaces = results.length;
|
||||
|
@ -4239,19 +4256,19 @@ async function predict2(image18, config3, idx, count2) {
|
|||
resT = await model.predict(enhanced);
|
||||
tf6.dispose(enhanced);
|
||||
if (resT) {
|
||||
tf6.tidy(() => {
|
||||
const gender = resT.find((t) => t.shape[1] === 1).dataSync();
|
||||
const confidence = Math.trunc(200 * Math.abs(gender[0] - 0.5)) / 100;
|
||||
if (confidence > config3.face.description.minConfidence) {
|
||||
obj.gender = gender[0] <= 0.5 ? "female" : "male";
|
||||
obj.genderScore = Math.min(0.99, confidence);
|
||||
}
|
||||
const age = tf6.argMax(resT.find((t) => t.shape[1] === 100), 1).dataSync()[0];
|
||||
const all2 = resT.find((t) => t.shape[1] === 100).dataSync();
|
||||
obj.age = Math.round(all2[age - 1] > all2[age + 1] ? 10 * age - 100 * all2[age - 1] : 10 * age + 100 * all2[age + 1]) / 10;
|
||||
const desc = resT.find((t) => t.shape[1] === 1024);
|
||||
obj.descriptor = [...desc.dataSync()];
|
||||
});
|
||||
const gender = await resT.find((t) => t.shape[1] === 1).data();
|
||||
const confidence = Math.trunc(200 * Math.abs(gender[0] - 0.5)) / 100;
|
||||
if (confidence > config3.face.description.minConfidence) {
|
||||
obj.gender = gender[0] <= 0.5 ? "female" : "male";
|
||||
obj.genderScore = Math.min(0.99, confidence);
|
||||
}
|
||||
const argmax = tf6.argMax(resT.find((t) => t.shape[1] === 100), 1);
|
||||
const age = (await argmax.data())[0];
|
||||
const all2 = await resT.find((t) => t.shape[1] === 100).data();
|
||||
obj.age = Math.round(all2[age - 1] > all2[age + 1] ? 10 * age - 100 * all2[age - 1] : 10 * age + 100 * all2[age + 1]) / 10;
|
||||
const desc = resT.find((t) => t.shape[1] === 1024);
|
||||
const descriptor = await desc.data();
|
||||
obj.descriptor = [...descriptor];
|
||||
resT.forEach((t) => tf6.dispose(t));
|
||||
}
|
||||
last[idx] = obj;
|
||||
|
@ -7909,7 +7926,7 @@ var HandPipeline = class {
|
|||
tf11.dispose(rotatedImage);
|
||||
const [confidenceT, keypoints3] = await this.handPoseModel.predict(handImage);
|
||||
tf11.dispose(handImage);
|
||||
const confidence = confidenceT.dataSync()[0];
|
||||
const confidence = (await confidenceT.data())[0];
|
||||
tf11.dispose(confidenceT);
|
||||
if (confidence >= config3.hand.minConfidence) {
|
||||
const keypointsReshaped = tf11.reshape(keypoints3, [-1, 3]);
|
||||
|
@ -8303,8 +8320,8 @@ async function predict8(image18, config3) {
|
|||
if (!model6.inputs[0].shape)
|
||||
return null;
|
||||
const resize = tf15.image.resizeBilinear(image18, [model6.inputs[0].shape[2], model6.inputs[0].shape[1]], false);
|
||||
const cast5 = tf15.cast(resize, "int32");
|
||||
return cast5;
|
||||
const cast4 = tf15.cast(resize, "int32");
|
||||
return cast4;
|
||||
});
|
||||
let resT;
|
||||
if (config3.body.enabled)
|
||||
|
@ -11375,10 +11392,24 @@ var Human = class {
|
|||
if (this.config.backend && this.config.backend.length > 0) {
|
||||
if (typeof window === "undefined" && typeof WorkerGlobalScope !== "undefined" && this.config.debug)
|
||||
log("running inside web worker");
|
||||
if (this.tf.ENV.flags.IS_BROWSER && this.config.backend === "tensorflow")
|
||||
this.config.backend = "webgl";
|
||||
if (this.tf.ENV.flags.IS_NODE && (this.config.backend === "webgl" || this.config.backend === "humangl"))
|
||||
if (this.tf.ENV.flags.IS_BROWSER && this.config.backend === "tensorflow") {
|
||||
if (this.config.debug)
|
||||
log("override: backend set to tensorflow while running in browser");
|
||||
this.config.backend = "humangl";
|
||||
}
|
||||
if (this.tf.ENV.flags.IS_NODE && (this.config.backend === "webgl" || this.config.backend === "humangl")) {
|
||||
if (this.config.debug)
|
||||
log("override: backend set to webgl while running in nodejs");
|
||||
this.config.backend = "tensorflow";
|
||||
}
|
||||
const available = Object.keys(this.tf.engine().registryFactory);
|
||||
if (this.config.debug)
|
||||
log("available backends:", available);
|
||||
if (!available.includes(this.config.backend)) {
|
||||
log(`error: backend ${this.config.backend} not found in registry`);
|
||||
this.config.backend = this.tf.ENV.flags.IS_NODE ? "tensorflow" : "humangl";
|
||||
log(`override: using backend ${this.config.backend} instead`);
|
||||
}
|
||||
if (this.config.debug)
|
||||
log("setting backend:", this.config.backend);
|
||||
if (this.config.backend === "wasm") {
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
"node-fetch": "^2.6.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"seedrandom": "^3.0.5",
|
||||
"simple-git": "^2.43.0",
|
||||
"simple-git": "^2.44.0",
|
||||
"tslib": "^2.3.1",
|
||||
"typedoc": "0.21.5",
|
||||
"typescript": "4.3.5"
|
||||
|
|
|
@ -57,7 +57,7 @@ export class BlazeFaceModel {
|
|||
}
|
||||
const boxesOut = decodeBounds(batchOut, this.anchors, [this.inputSize, this.inputSize]);
|
||||
const logits = tf.slice(batchOut, [0, 0], [-1, 1]);
|
||||
const scoresOut = tf.squeeze(tf.sigmoid(logits)).dataSync(); // inside tf.tidy
|
||||
const scoresOut = tf.squeeze(tf.sigmoid(logits)); // inside tf.tidy
|
||||
return [batchOut, boxesOut, scoresOut];
|
||||
});
|
||||
|
||||
|
@ -67,8 +67,9 @@ export class BlazeFaceModel {
|
|||
const nms = await nmsTensor.array();
|
||||
tf.dispose(nmsTensor);
|
||||
const annotatedBoxes: Array<{ box: { startPoint: Tensor, endPoint: Tensor }, landmarks: Tensor, anchor: number[], confidence: number }> = [];
|
||||
const scoresData = await scores.data();
|
||||
for (let i = 0; i < nms.length; i++) {
|
||||
const confidence = scores[nms[i]];
|
||||
const confidence = scoresData[nms[i]];
|
||||
if (confidence > this.config.face.detector.minConfidence) {
|
||||
const boundingBox = tf.slice(boxes, [nms[i], 0], [1, -1]);
|
||||
const localBox = box.createBox(boundingBox);
|
||||
|
@ -80,6 +81,7 @@ export class BlazeFaceModel {
|
|||
}
|
||||
tf.dispose(batch);
|
||||
tf.dispose(boxes);
|
||||
tf.dispose(scores);
|
||||
return {
|
||||
boxes: annotatedBoxes,
|
||||
scaleFactor: [inputImage.shape[2] / this.inputSize, inputImage.shape[1] / this.inputSize],
|
||||
|
|
|
@ -115,7 +115,9 @@ export class Pipeline {
|
|||
box.endPoint[0] / this.meshSize,
|
||||
]], [0], [this.irisSize, this.irisSize]);
|
||||
if (flip && tf.ENV.flags.IS_BROWSER) {
|
||||
crop = tf.image.flipLeftRight(crop); // flipLeftRight is not defined for tfjs-node
|
||||
const flipped = tf.image.flipLeftRight(crop); // flipLeftRight is not defined for tfjs-node
|
||||
tf.dispose(crop);
|
||||
crop = flipped;
|
||||
}
|
||||
return { box, boxSize, crop };
|
||||
}
|
||||
|
@ -153,6 +155,53 @@ export class Pipeline {
|
|||
});
|
||||
}
|
||||
|
||||
correctFaceRotation(config, box, input) {
|
||||
const [indexOfMouth, indexOfForehead] = (box.landmarks.length >= meshLandmarks.count) ? meshLandmarks.symmetryLine : blazeFaceLandmarks.symmetryLine;
|
||||
const angle = util.computeRotation(box.landmarks[indexOfMouth], box.landmarks[indexOfForehead]);
|
||||
const faceCenter = bounding.getBoxCenter({ startPoint: box.startPoint, endPoint: box.endPoint });
|
||||
const faceCenterNormalized = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]];
|
||||
const rotatedImage = tf.image.rotateWithOffset(input, angle, 0, faceCenterNormalized); // rotateWithOffset is not defined for tfjs-node
|
||||
const rotationMatrix = util.buildRotationMatrix(-angle, faceCenter);
|
||||
const cut = config.face.mesh.enabled
|
||||
? bounding.cutBoxFromImageAndResize({ startPoint: box.startPoint, endPoint: box.endPoint }, rotatedImage, [this.meshSize, this.meshSize])
|
||||
: bounding.cutBoxFromImageAndResize({ startPoint: box.startPoint, endPoint: box.endPoint }, rotatedImage, [this.boxSize, this.boxSize]);
|
||||
const face = tf.div(cut, 255);
|
||||
tf.dispose(cut);
|
||||
tf.dispose(rotatedImage);
|
||||
return [angle, rotationMatrix, face];
|
||||
}
|
||||
|
||||
async augmentIris(rawCoords, face) {
|
||||
const { box: leftEyeBox, boxSize: leftEyeBoxSize, crop: leftEyeCrop } = this.getEyeBox(rawCoords, face, eyeLandmarks.leftBounds[0], eyeLandmarks.leftBounds[1], true);
|
||||
const { box: rightEyeBox, boxSize: rightEyeBoxSize, crop: rightEyeCrop } = this.getEyeBox(rawCoords, face, eyeLandmarks.rightBounds[0], eyeLandmarks.rightBounds[1]);
|
||||
const combined = tf.concat([leftEyeCrop, rightEyeCrop]);
|
||||
tf.dispose(leftEyeCrop);
|
||||
tf.dispose(rightEyeCrop);
|
||||
const eyePredictions = this.irisModel.predict(combined) as Tensor;
|
||||
tf.dispose(combined);
|
||||
const eyePredictionsData = await eyePredictions.data(); // inside tf.tidy
|
||||
tf.dispose(eyePredictions);
|
||||
const leftEyeData = eyePredictionsData.slice(0, irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: leftEyeRawCoords, iris: leftIrisRawCoords } = this.getEyeCoords(leftEyeData, leftEyeBox, leftEyeBoxSize, true);
|
||||
const rightEyeData = eyePredictionsData.slice(irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: rightEyeRawCoords, iris: rightIrisRawCoords } = this.getEyeCoords(rightEyeData, rightEyeBox, rightEyeBoxSize);
|
||||
const leftToRightEyeDepthDifference = this.getLeftToRightEyeDepthDifference(rawCoords);
|
||||
if (Math.abs(leftToRightEyeDepthDifference) < 30) { // User is looking straight ahead.
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, 'left', null);
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, 'right', null);
|
||||
// If the user is looking to the left or to the right, the iris coordinates tend to diverge too much from the mesh coordinates for them to be merged
|
||||
// So we only update a single contour line above and below the eye.
|
||||
} else if (leftToRightEyeDepthDifference < 1) { // User is looking towards the right.
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, 'left', ['EyeUpper0', 'EyeLower0']);
|
||||
} else { // User is looking towards the left.
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, 'right', ['EyeUpper0', 'EyeLower0']);
|
||||
}
|
||||
const adjustedLeftIrisCoords = this.getAdjustedIrisCoords(rawCoords, leftIrisRawCoords, 'left');
|
||||
const adjustedRightIrisCoords = this.getAdjustedIrisCoords(rawCoords, rightIrisRawCoords, 'right');
|
||||
const newCoords = rawCoords.concat(adjustedLeftIrisCoords).concat(adjustedRightIrisCoords);
|
||||
return newCoords;
|
||||
}
|
||||
|
||||
async predict(input, config) {
|
||||
let useFreshBox = false;
|
||||
// run new detector every skipFrames unless we only want box to start with
|
||||
|
@ -198,106 +247,78 @@ export class Pipeline {
|
|||
tf.dispose(prediction.landmarks);
|
||||
});
|
||||
}
|
||||
const results = tf.tidy(() => this.storedBoxes.map((box, i) => {
|
||||
// The facial bounding box landmarks could come either from blazeface (if we are using a fresh box), or from the mesh model (if we are reusing an old box).
|
||||
|
||||
const results: Array<{ mesh, box, faceConfidence, boxConfidence, confidence, image }> = [];
|
||||
for (let i = 0; i < this.storedBoxes.length; i++) {
|
||||
let box = this.storedBoxes[i]; // The facial bounding box landmarks could come either from blazeface (if we are using a fresh box), or from the mesh model (if we are reusing an old box).
|
||||
let face;
|
||||
let angle = 0;
|
||||
let rotationMatrix;
|
||||
|
||||
if (config.face.detector.rotation && config.face.mesh.enabled && tf.ENV.flags.IS_BROWSER) {
|
||||
const [indexOfMouth, indexOfForehead] = (box.landmarks.length >= meshLandmarks.count) ? meshLandmarks.symmetryLine : blazeFaceLandmarks.symmetryLine;
|
||||
angle = util.computeRotation(box.landmarks[indexOfMouth], box.landmarks[indexOfForehead]);
|
||||
const faceCenter = bounding.getBoxCenter({ startPoint: box.startPoint, endPoint: box.endPoint });
|
||||
const faceCenterNormalized = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]];
|
||||
const rotatedImage = tf.image.rotateWithOffset(input, angle, 0, faceCenterNormalized); // rotateWithOffset is not defined for tfjs-node
|
||||
rotationMatrix = util.buildRotationMatrix(-angle, faceCenter);
|
||||
if (config.face.mesh.enabled) face = tf.div(bounding.cutBoxFromImageAndResize({ startPoint: box.startPoint, endPoint: box.endPoint }, rotatedImage, [this.meshSize, this.meshSize]), 255);
|
||||
else face = tf.div(bounding.cutBoxFromImageAndResize({ startPoint: box.startPoint, endPoint: box.endPoint }, rotatedImage, [this.boxSize, this.boxSize]), 255);
|
||||
[angle, rotationMatrix, face] = this.correctFaceRotation(config, box, input);
|
||||
} else {
|
||||
rotationMatrix = util.IDENTITY_MATRIX;
|
||||
const clonedImage = input.clone();
|
||||
if (config.face.mesh.enabled) face = tf.div(bounding.cutBoxFromImageAndResize({ startPoint: box.startPoint, endPoint: box.endPoint }, clonedImage, [this.meshSize, this.meshSize]), 255);
|
||||
else face = tf.div(bounding.cutBoxFromImageAndResize({ startPoint: box.startPoint, endPoint: box.endPoint }, clonedImage, [this.boxSize, this.boxSize]), 255);
|
||||
const cut = config.face.mesh.enabled
|
||||
? bounding.cutBoxFromImageAndResize({ startPoint: box.startPoint, endPoint: box.endPoint }, clonedImage, [this.meshSize, this.meshSize])
|
||||
: bounding.cutBoxFromImageAndResize({ startPoint: box.startPoint, endPoint: box.endPoint }, clonedImage, [this.boxSize, this.boxSize]);
|
||||
face = tf.div(cut, 255);
|
||||
tf.dispose(cut);
|
||||
tf.dispose(clonedImage);
|
||||
}
|
||||
|
||||
// if we're not going to produce mesh, don't spend time with further processing
|
||||
if (!config.face.mesh.enabled) {
|
||||
const prediction = {
|
||||
results.push({
|
||||
mesh: [],
|
||||
box,
|
||||
faceConfidence: null,
|
||||
boxConfidence: box.confidence,
|
||||
confidence: box.confidence,
|
||||
image: face,
|
||||
};
|
||||
return prediction;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const [contours, confidence, contourCoords] = this.meshDetector.execute(face) as Array<Tensor>; // The first returned tensor represents facial contours which are already included in the coordinates.
|
||||
tf.dispose(contours);
|
||||
const faceConfidence = (await confidence.data())[0] as number; // inside tf.tidy
|
||||
tf.dispose(confidence);
|
||||
const coordsReshaped = tf.reshape(contourCoords, [-1, 3]);
|
||||
let rawCoords = await coordsReshaped.array();
|
||||
tf.dispose(contourCoords);
|
||||
tf.dispose(coordsReshaped);
|
||||
if (faceConfidence < config.face.detector.minConfidence) {
|
||||
this.storedBoxes[i].confidence = faceConfidence; // reset confidence of cached box
|
||||
tf.dispose(face);
|
||||
} else {
|
||||
if (config.face.iris.enabled) rawCoords = await this.augmentIris(rawCoords, face);
|
||||
|
||||
const [, confidence, contourCoords] = this.meshDetector.execute(face) as Array<Tensor>; // The first returned tensor represents facial contours which are already included in the coordinates.
|
||||
const faceConfidence = confidence.dataSync()[0] as number; // inside tf.tidy
|
||||
if (faceConfidence < config.face.detector.minConfidence) {
|
||||
this.storedBoxes[i].confidence = faceConfidence; // reset confidence of cached box
|
||||
return null; // if below confidence just exit
|
||||
}
|
||||
const coordsReshaped = tf.reshape(contourCoords, [-1, 3]);
|
||||
let rawCoords = coordsReshaped.arraySync();
|
||||
// override box from detection with one calculated from mesh
|
||||
const mesh = this.transformRawCoords(rawCoords, box, angle, rotationMatrix);
|
||||
const storeConfidence = box.confidence;
|
||||
// @ts-ignore enlargeBox does not include confidence so we append it manually
|
||||
box = bounding.enlargeBox(bounding.calculateLandmarksBoundingBox(mesh), 1.5); // redefine box with mesh calculated one
|
||||
box.confidence = storeConfidence;
|
||||
|
||||
if (config.face.iris.enabled) {
|
||||
const { box: leftEyeBox, boxSize: leftEyeBoxSize, crop: leftEyeCrop } = this.getEyeBox(rawCoords, face, eyeLandmarks.leftBounds[0], eyeLandmarks.leftBounds[1], true);
|
||||
const { box: rightEyeBox, boxSize: rightEyeBoxSize, crop: rightEyeCrop } = this.getEyeBox(rawCoords, face, eyeLandmarks.rightBounds[0], eyeLandmarks.rightBounds[1]);
|
||||
const eyePredictions = this.irisModel.predict(tf.concat([leftEyeCrop, rightEyeCrop])) as Tensor;
|
||||
const eyePredictionsData = eyePredictions.dataSync(); // inside tf.tidy
|
||||
const leftEyeData = eyePredictionsData.slice(0, irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: leftEyeRawCoords, iris: leftIrisRawCoords } = this.getEyeCoords(leftEyeData, leftEyeBox, leftEyeBoxSize, true);
|
||||
const rightEyeData = eyePredictionsData.slice(irisLandmarks.numCoordinates * 3);
|
||||
const { rawCoords: rightEyeRawCoords, iris: rightIrisRawCoords } = this.getEyeCoords(rightEyeData, rightEyeBox, rightEyeBoxSize);
|
||||
const leftToRightEyeDepthDifference = this.getLeftToRightEyeDepthDifference(rawCoords);
|
||||
if (Math.abs(leftToRightEyeDepthDifference) < 30) { // User is looking straight ahead.
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, 'left', null);
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, 'right', null);
|
||||
// If the user is looking to the left or to the right, the iris coordinates tend to diverge too much from the mesh coordinates for them to be merged
|
||||
// So we only update a single contour line above and below the eye.
|
||||
} else if (leftToRightEyeDepthDifference < 1) { // User is looking towards the right.
|
||||
replaceRawCoordinates(rawCoords, leftEyeRawCoords, 'left', ['EyeUpper0', 'EyeLower0']);
|
||||
} else { // User is looking towards the left.
|
||||
replaceRawCoordinates(rawCoords, rightEyeRawCoords, 'right', ['EyeUpper0', 'EyeLower0']);
|
||||
// do rotation one more time with mesh keypoints if we want to return perfect image
|
||||
if (config.face.detector.rotation && config.face.mesh.enabled && config.face.description.enabled && tf.ENV.flags.IS_BROWSER) {
|
||||
[angle, rotationMatrix, face] = this.correctFaceRotation(config, box, input);
|
||||
}
|
||||
|
||||
results.push({
|
||||
mesh,
|
||||
box,
|
||||
faceConfidence,
|
||||
boxConfidence: box.confidence,
|
||||
confidence: faceConfidence,
|
||||
image: face,
|
||||
});
|
||||
|
||||
// updated stored cache values
|
||||
this.storedBoxes[i] = { ...bounding.squarifyBox(box), confidence: box.confidence, faceConfidence };
|
||||
}
|
||||
const adjustedLeftIrisCoords = this.getAdjustedIrisCoords(rawCoords, leftIrisRawCoords, 'left');
|
||||
const adjustedRightIrisCoords = this.getAdjustedIrisCoords(rawCoords, rightIrisRawCoords, 'right');
|
||||
rawCoords = rawCoords.concat(adjustedLeftIrisCoords).concat(adjustedRightIrisCoords);
|
||||
}
|
||||
|
||||
// override box from detection with one calculated from mesh
|
||||
const mesh = this.transformRawCoords(rawCoords, box, angle, rotationMatrix);
|
||||
const storeConfidence = box.confidence;
|
||||
// @ts-ignore enlargeBox does not include confidence so we append it manually
|
||||
box = bounding.enlargeBox(bounding.calculateLandmarksBoundingBox(mesh), 1.5); // redefine box with mesh calculated one
|
||||
box.confidence = storeConfidence;
|
||||
|
||||
// do rotation one more time with mesh keypoints if we want to return perfect image
|
||||
if (config.face.detector.rotation && config.face.mesh.enabled && config.face.description.enabled && tf.ENV.flags.IS_BROWSER) {
|
||||
const [indexOfMouth, indexOfForehead] = (box.landmarks.length >= meshLandmarks.count) ? meshLandmarks.symmetryLine : blazeFaceLandmarks.symmetryLine;
|
||||
angle = util.computeRotation(box.landmarks[indexOfMouth], box.landmarks[indexOfForehead]);
|
||||
const faceCenter = bounding.getBoxCenter({ startPoint: box.startPoint, endPoint: box.endPoint });
|
||||
const faceCenterNormalized = [faceCenter[0] / input.shape[2], faceCenter[1] / input.shape[1]];
|
||||
const rotatedImage = tf.image.rotateWithOffset(tf.cast(input, 'float32'), angle, 0, faceCenterNormalized); // rotateWithOffset is not defined for tfjs-node
|
||||
rotationMatrix = util.buildRotationMatrix(-angle, faceCenter);
|
||||
face = tf.div(bounding.cutBoxFromImageAndResize({ startPoint: box.startPoint, endPoint: box.endPoint }, rotatedImage, [this.meshSize, this.meshSize]), 255);
|
||||
}
|
||||
|
||||
const prediction = {
|
||||
mesh,
|
||||
box,
|
||||
faceConfidence,
|
||||
boxConfidence: box.confidence,
|
||||
image: face,
|
||||
};
|
||||
|
||||
// updated stored cache values
|
||||
this.storedBoxes[i] = { ...bounding.squarifyBox(box), confidence: box.confidence, faceConfidence };
|
||||
|
||||
return prediction;
|
||||
}));
|
||||
}
|
||||
|
||||
// results = results.filter((a) => a !== null);
|
||||
// remove cache entries for detected boxes on low confidence
|
||||
|
|
|
@ -34,14 +34,10 @@ export async function load(config: Config): Promise<GraphModel> {
|
|||
function max2d(inputs, minScore) {
|
||||
const [width, height] = inputs.shape;
|
||||
return tf.tidy(() => {
|
||||
// modulus op implemented in tf
|
||||
const mod = (a, b) => tf.sub(a, tf.mul(tf.div(a, tf.scalar(b, 'int32')), tf.scalar(b, 'int32')));
|
||||
// combine all data
|
||||
const reshaped = tf.reshape(inputs, [height * width]);
|
||||
// get highest score
|
||||
const newScore = tf.max(reshaped, 0).dataSync()[0]; // inside tf.tidy
|
||||
if (newScore > minScore) {
|
||||
// skip coordinate calculation is score is too low
|
||||
const mod = (a, b) => tf.sub(a, tf.mul(tf.div(a, tf.scalar(b, 'int32')), tf.scalar(b, 'int32'))); // modulus op implemented in tf
|
||||
const reshaped = tf.reshape(inputs, [height * width]); // combine all data
|
||||
const newScore = tf.max(reshaped, 0).dataSync()[0]; // get highest score // inside tf.tidy
|
||||
if (newScore > minScore) { // skip coordinate calculation is score is too low
|
||||
const coords = tf.argMax(reshaped, 0);
|
||||
const x = mod(coords, width).dataSync()[0]; // inside tf.tidy
|
||||
const y = tf.div(coords, tf.scalar(width, 'int32')).dataSync()[0]; // inside tf.tidy
|
||||
|
|
|
@ -95,7 +95,7 @@ export async function predict(input, config): Promise<number[]> {
|
|||
let data: Array<number> = [];
|
||||
if (config.face.embedding.enabled) {
|
||||
const image = enhance(input);
|
||||
data = tf.tidy(() => {
|
||||
const dataT = tf.tidy(() => {
|
||||
/*
|
||||
// if needed convert from NHWC to NCHW
|
||||
const nchw = image.transpose([3, 0, 1, 2]);
|
||||
|
@ -125,9 +125,11 @@ export async function predict(input, config): Promise<number[]> {
|
|||
const reshape = tf.reshape(res, [128, 2]); // split 256 vectors into 128 x 2
|
||||
const reduce = reshape.logSumExp(1); // reduce 2nd dimension by calculating logSumExp on it
|
||||
|
||||
const output: Array<number> = reduce.dataSync(); // inside tf.tidy
|
||||
return [...output]; // convert typed array to simple array
|
||||
return reduce;
|
||||
});
|
||||
const output: Array<number> = await dataT.data();
|
||||
data = [...output]; // convert typed array to simple array
|
||||
tf.dispose(dataT);
|
||||
tf.dispose(image);
|
||||
}
|
||||
resolve(data);
|
||||
|
|
|
@ -133,23 +133,23 @@ export async function predict(image: Tensor, config: Config, idx, count) {
|
|||
tf.dispose(enhanced);
|
||||
|
||||
if (resT) {
|
||||
tf.tidy(() => {
|
||||
const gender = resT.find((t) => t.shape[1] === 1).dataSync(); // inside tf.tidy
|
||||
const confidence = Math.trunc(200 * Math.abs((gender[0] - 0.5))) / 100;
|
||||
if (confidence > config.face.description.minConfidence) {
|
||||
obj.gender = gender[0] <= 0.5 ? 'female' : 'male';
|
||||
obj.genderScore = Math.min(0.99, confidence);
|
||||
}
|
||||
const age = tf.argMax(resT.find((t) => t.shape[1] === 100), 1).dataSync()[0]; // inside tf.tidy
|
||||
const all = resT.find((t) => t.shape[1] === 100).dataSync(); // inside tf.tidy
|
||||
obj.age = Math.round(all[age - 1] > all[age + 1] ? 10 * age - 100 * all[age - 1] : 10 * age + 100 * all[age + 1]) / 10;
|
||||
const gender = await resT.find((t) => t.shape[1] === 1).data();
|
||||
const confidence = Math.trunc(200 * Math.abs((gender[0] - 0.5))) / 100;
|
||||
if (confidence > config.face.description.minConfidence) {
|
||||
obj.gender = gender[0] <= 0.5 ? 'female' : 'male';
|
||||
obj.genderScore = Math.min(0.99, confidence);
|
||||
}
|
||||
const argmax = tf.argMax(resT.find((t) => t.shape[1] === 100), 1);
|
||||
const age = (await argmax.data())[0];
|
||||
const all = await resT.find((t) => t.shape[1] === 100).data(); // inside tf.tidy
|
||||
obj.age = Math.round(all[age - 1] > all[age + 1] ? 10 * age - 100 * all[age - 1] : 10 * age + 100 * all[age + 1]) / 10;
|
||||
|
||||
const desc = resT.find((t) => t.shape[1] === 1024);
|
||||
// const reshape = desc.reshape([128, 8]); // reshape large 1024-element descriptor to 128 x 8
|
||||
// const reduce = reshape.logSumExp(1); // reduce 2nd dimension by calculating logSumExp on it which leaves us with 128-element descriptor
|
||||
const desc = resT.find((t) => t.shape[1] === 1024);
|
||||
// const reshape = desc.reshape([128, 8]); // reshape large 1024-element descriptor to 128 x 8
|
||||
// const reduce = reshape.logSumExp(1); // reduce 2nd dimension by calculating logSumExp on it which leaves us with 128-element descriptor
|
||||
|
||||
obj.descriptor = [...desc.dataSync()]; // inside tf.tidy
|
||||
});
|
||||
const descriptor = await desc.data();
|
||||
obj.descriptor = [...descriptor];
|
||||
resT.forEach((t) => tf.dispose(t));
|
||||
}
|
||||
|
||||
|
|
|
@ -67,14 +67,14 @@ export async function predict(image: Tensor, config: Config) {
|
|||
// tf.dispose(enhance);
|
||||
|
||||
if (ageT) {
|
||||
// const data = ageT.dataSync();
|
||||
// const data = await ageT.data();
|
||||
// {0: 'below_20', 1: '21-25', 2: '26-30', 3: '31-40',4: '41-50', 5: '51-60', 6: 'Above60'}
|
||||
}
|
||||
if (genderT) {
|
||||
// const data = genderT.dataSync();
|
||||
// const data = await genderT.data();
|
||||
}
|
||||
if (raceT) {
|
||||
// const data = raceT.dataSync();
|
||||
// const data = await raceT.data();
|
||||
// {0: 'white', 1: 'black', 2: 'asian', 3: 'indian', 4: 'others'}
|
||||
}
|
||||
|
||||
|
|
|
@ -87,10 +87,10 @@ export async function predict(image: Tensor, config: Config | any) {
|
|||
obj.confidence = Math.min(0.99, confidence);
|
||||
}
|
||||
/*
|
||||
let age = genderT[1].argMax(1).dataSync()[0];
|
||||
const all = genderT[1].dataSync();
|
||||
let age = (await genderT[1].argMax(1).data())[0];
|
||||
const all = await genderT[1].data();
|
||||
age = Math.round(all[age - 1] > all[age + 1] ? 10 * age - 100 * all[age - 1] : 10 * age + 100 * all[age + 1]) / 10;
|
||||
const descriptor = genderT[1].dataSync();
|
||||
const descriptor = await genderT[1].data();
|
||||
*/
|
||||
genderT.forEach((t) => tf.dispose(t));
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ export class HandPipeline {
|
|||
tf.dispose(rotatedImage);
|
||||
const [confidenceT, keypoints] = await this.handPoseModel.predict(handImage) as Array<Tensor>;
|
||||
tf.dispose(handImage);
|
||||
const confidence = confidenceT.dataSync()[0];
|
||||
const confidence = (await confidenceT.data())[0];
|
||||
tf.dispose(confidenceT);
|
||||
if (confidence >= config.hand.minConfidence) {
|
||||
const keypointsReshaped = tf.reshape(keypoints, [-1, 3]);
|
||||
|
|
21
src/human.ts
21
src/human.ts
|
@ -302,8 +302,23 @@ export class Human {
|
|||
if (typeof window === 'undefined' && typeof WorkerGlobalScope !== 'undefined' && this.config.debug) log('running inside web worker');
|
||||
|
||||
// force browser vs node backend
|
||||
if (this.tf.ENV.flags.IS_BROWSER && this.config.backend === 'tensorflow') this.config.backend = 'webgl';
|
||||
if (this.tf.ENV.flags.IS_NODE && (this.config.backend === 'webgl' || this.config.backend === 'humangl')) this.config.backend = 'tensorflow';
|
||||
if (this.tf.ENV.flags.IS_BROWSER && this.config.backend === 'tensorflow') {
|
||||
if (this.config.debug) log('override: backend set to tensorflow while running in browser');
|
||||
this.config.backend = 'humangl';
|
||||
}
|
||||
if (this.tf.ENV.flags.IS_NODE && (this.config.backend === 'webgl' || this.config.backend === 'humangl')) {
|
||||
if (this.config.debug) log('override: backend set to webgl while running in nodejs');
|
||||
this.config.backend = 'tensorflow';
|
||||
}
|
||||
|
||||
const available = Object.keys(this.tf.engine().registryFactory);
|
||||
if (this.config.debug) log('available backends:', available);
|
||||
|
||||
if (!available.includes(this.config.backend)) {
|
||||
log(`error: backend ${this.config.backend} not found in registry`);
|
||||
this.config.backend = this.tf.ENV.flags.IS_NODE ? 'tensorflow' : 'humangl';
|
||||
log(`override: using backend ${this.config.backend} instead`);
|
||||
}
|
||||
|
||||
if (this.config.debug) log('setting backend:', this.config.backend);
|
||||
|
||||
|
@ -363,7 +378,7 @@ export class Human {
|
|||
// use tensor sum
|
||||
/*
|
||||
const sumT = this.tf.sum(reduced);
|
||||
const sum = sumT.dataSync()[0] as number;
|
||||
const sum = await sumT.data()[0] as number;
|
||||
sumT.dispose();
|
||||
*/
|
||||
// use js loop sum, faster than uploading tensor to gpu calculating and downloading back
|
||||
|
|
Loading…
Reference in New Issue