From 553a80fce112c2281a4c26fcd22f28f6378f39a6 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Sun, 5 Jan 2020 15:25:41 -0800 Subject: [PATCH] Add face-crop creation Signed-off-by: James Ketrenos --- server/face-recognizer.js | 525 +++++++++++++++++++------------------- server/face.js | 202 ++++++++++++--- 2 files changed, 438 insertions(+), 289 deletions(-) diff --git a/server/face-recognizer.js b/server/face-recognizer.js index d6fba36..744e18d 100644 --- a/server/face-recognizer.js +++ b/server/face-recognizer.js @@ -28,7 +28,7 @@ const config = require("config"), fs = require("fs"), canvas = require("canvas"); -const { Canvas, Image, ImageData } = canvas; +const { createCanvas, Canvas, Image, ImageData } = canvas; faceapi.env.monkeyPatch({ Canvas, Image, ImageData }); @@ -43,211 +43,245 @@ let photoDB = null; console.log("Loading pictures out of: " + picturesPath); -faceapi.nets.ssdMobilenetv1.loadFromDisk('./models') -.then(() => { - console.log("ssdMobileNetv1 loaded."); +process.stdout.write("Loading DB."); +require("./db/photos").then(function(db) { + process.stdout.write("done\n"); + photoDB = db; +}).then(() => { + console.log("DB connected."); + process.stdout.write("Loading models."); + return faceapi.nets.ssdMobilenetv1.loadFromDisk('./models'); +}).then(() => { + process.stdout.write("."); return faceapi.nets.faceLandmark68Net.loadFromDisk('./models'); }).then(() => { - console.log("landmark68 loaded."); + process.stdout.write("."); return faceapi.nets.faceRecognitionNet.loadFromDisk('./models'); }).then(() => { - console.log("faceRecognitionNet loaded."); - return require("./db/photos").then(function(db) { - photoDB = db; - }).then(() => { - console.log("DB connected."); - }).then(() => { - console.log("Beginning face detection scanning."); - return photoDB.sequelize.query("SELECT photos.id,photos.filename,photos.width,photos.height,albums.path " + - "FROM photos " + - "LEFT JOIN albums ON (albums.id=photos.albumId) " + - "WHERE faces=-1 AND photos.duplicate=0 AND photos.deleted=0 ORDER BY albums.path,photos.filename", { - type: photoDB.sequelize.QueryTypes.SELECT, - raw: true - } - ).then((results) => { - const total = results.length; - let remaining = total, - processed = 0, - lastStatus = Date.now(); - console.log(`${results.length} photos have not had faces scanned.`); - return Promise.map(results, (photo) => { - const photoPath = photo.path + photo.filename; + console.log("Beginning face detection scanning."); + return photoDB.sequelize.query("SELECT photos.id,photos.filename,photos.width,photos.height,albums.path " + + "FROM photos " + + "LEFT JOIN albums ON (albums.id=photos.albumId) " + + "WHERE faces=-1 AND photos.duplicate=0 AND photos.deleted=0 ORDER BY albums.path,photos.filename", { + type: photoDB.sequelize.QueryTypes.SELECT, + raw: true + }); +}).then((needToScan) => { + const total = needToScan.length; + let remaining = total, + processed = 0, + lastStatus = Date.now(); + + console.log(`${needToScan.length} photos have not had faces scanned.`); - console.log(`Processing ${photoPath}...`); + return Promise.map(needToScan, (photo) => { + const photoPath = photo.path + photo.filename; - /* Remove any existing face data for this photo */ - return photoDB.sequelize.query("SELECT id FROM faces WHERE photoId=:id", { - replacements: photo, - }).then((faces) => { - /* For each face-id, remove any face-data files, and then remove all the entries - * from the DB */ - return Promise.map(faces, (face) => { - const id = face.id, - dataPath = faceData + "/" + (id % 100) + "/" + id + "-data.json"; - return exists(dataPath).then((result) => { - if (result) { - console.log(`...removing ${dataPath}`); - return unlink(dataPath); - } - }); - }).then(() => { - return photoDB.sequelize.query("DELETE FROM faces WHERE photoId=:id", { - replacements: photo, - }); + console.log(`Processing ${photoPath}...`); + + /* Remove any existing face data for this photo */ + return photoDB.sequelize.query("SELECT id FROM faces WHERE photoId=:id", { + replacements: photo, + }).then((faces) => { + /* For each face-id, remove any face-data files, and then remove all the entries + * from the DB */ + return Promise.map(faces, (face) => { + return Promise.map([ "-data.json", "-original.png" ], (suffix) => { + const id = face.id, + dataPath = faceData + "/" + (id % 100) + "/" + id + suffix; + return exists(dataPath).then((result) => { + if (result) { + console.log(`...removing ${dataPath}`); + return unlink(dataPath); + } }); - }).then(async () => { - const image = await canvas.loadImage(picturesPath + photoPath); - - const detections = await faceapi.detectAllFaces(image, - new faceapi.SsdMobilenetv1Options({ - minConfidence: 0.8 - }) - ).withFaceLandmarks().withFaceDescriptors(); - - if (detections.length > 0) { - console.log(`...${detections.length} faces identified.`); - } - - return Promise.map(detections, (face) => { - const detection = face.detection; - const width = detection._box._width, - height = detection._box._height, - replacements = { - id: photo.id, - top: detection._box._y / detection._imageDims.height, - left: detection._box._x / detection._imageDims.width, - bottom: (detection._box._y + height) / detection._imageDims.height, - right: (detection._box._x + width) / detection._imageDims.width, - faceConfidence: detection._score - }; - - return photoDB.sequelize.query("INSERT INTO faces (photoId,top,left,bottom,right,faceConfidence) " + - "VALUES (:id,:top,:left,:bottom,:right,:faceConfidence)", { - replacements: replacements - }).spread((results, metadata) => { - return metadata.lastID; - }).then((id) => { - console.log(`...DB id ${id}. Writing descriptor data...`); - const path = faceData + "/" + (id % 100); - return mkdir(path).then(() => { - const dataPath = path + "/" + id + "-data.json", - data = []; - for (let i = 0; i < 128; i++) { - data.push(face.descriptor[i]); - } - fs.writeFileSync(dataPath, JSON.stringify(data)); - }); - }); - }).then(() => { - return photoDB.sequelize.query("UPDATE photos SET faces=:faces WHERE id=:id", { - replacements: { - id: photo.id, - faces: detections.length - }, - }); - }); - }).catch((error) => { - console.warn("Skipping out on image " + photoPath + " and marking to 0 faces to prevent future scanning."); - return photoDB.sequelize.query("UPDATE photos SET faces=:faces WHERE id=:id", { - replacements: { - id: photo.id, - faces: 0 - }, - }); - }).then(() => { - processed++; - const now = Date.now(); - if (now - lastStatus > 5000) { - const rate = Math.round(10000 * (remaining - (total - processed)) / (now - lastStatus)) / 10, - eta = Math.round((total - processed) / rate); - lastStatus = now; - remaining = total - processed; - console.log(`Processing ${rate} images per second. ${remaining} images to be processed. ETA: ${eta}s`); - } }); - }, { - concurrency: maxConcurrency + }).then(() => { + return photoDB.sequelize.query("DELETE FROM faces WHERE photoId=:id", { + replacements: photo, + }); }); - }); - }).then(() => { - console.log("Looking for face distances that need to be updated..."); - const descriptors = {}; + }).then(async () => { + /* Process image for faces data */ + const image = await canvas.loadImage(picturesPath + photoPath); + const detections = await faceapi.detectAllFaces(image, + new faceapi.SsdMobilenetv1Options({ + minConfidence: 0.8 + }) + ).withFaceLandmarks().withFaceDescriptors(); - return photoDB.sequelize.query("SELECT faces.id FROM faces ORDER BY faces.id DESC LIMIT 1", { + if (detections.length > 0) { + console.log(`...${detections.length} faces identified in ${photoPath}.`); + } + + return Promise.map(detections, (face) => { + const detection = face.detection; + const width = detection._box._width, + height = detection._box._height, + replacements = { + id: photo.id, + top: detection._box._y / detection._imageDims.height, + left: detection._box._x / detection._imageDims.width, + bottom: (detection._box._y + height) / detection._imageDims.height, + right: (detection._box._x + width) / detection._imageDims.width, + faceConfidence: detection._score + }; + + return photoDB.sequelize.query("INSERT INTO faces (photoId,top,left,bottom,right,faceConfidence) " + + "VALUES (:id,:top,:left,:bottom,:right,:faceConfidence)", { + replacements: replacements + }).spread((results, metadata) => { + return metadata.lastID; + }).then((id) => { + const path = faceData + (id % 100); + return mkdir(path).then(() => { + const dataPath = `${path}/${id}-data.json`, data = []; + console.log(`...writing descriptor data to ${dataPath}...`); + for (let i = 0; i < 128; i++) { + data.push(face.descriptor[i]); + } + fs.writeFileSync(dataPath, JSON.stringify(data)); + }).then(() => { + const canvas = createCanvas(200, 200), + target = `${path}/${id}-original.png`, + ctx = canvas.getContext('2d'), + box = face.detection._box, + aspect = box._width / box._height, + dx = (aspect > 1.0) ? 200 : (200 * aspect), + dy = (aspect < 1.0) ? 200 : (200 / aspect); + ctx.fillStyle = "rgba(0, 0, 0, 0)"; + ctx.fillRect(0, 0, 200, 200); + ctx.drawImage(image, box._x, box._y, box._width, box._height, + Math.floor((200 - dx) * 0.5), + Math.floor((200 - dy) * 0.5), dx, dy); + console.log(`...writing face crop to ${target}.`); + fs.writeFileSync(target, canvas.toBuffer("image/png", { + quality: 0.95, + chromaSubsampling: false + })); + }).catch((error) => { + console.error(error); + process.exit(-1); + }); + }); + }).then(() => { + return photoDB.sequelize.query("UPDATE photos SET faces=:faces WHERE id=:id", { + replacements: { + id: photo.id, + faces: detections.length + }, + }); + }); + }).catch((error) => { + console.warn("Skipping out on image " + photoPath + " and marking to 0 faces to prevent future scanning."); + return photoDB.sequelize.query("UPDATE photos SET faces=:faces WHERE id=:id", { + replacements: { + id: photo.id, + faces: 0 + }, + }); + }).then(() => { + processed++; + const now = Date.now(); + if (now - lastStatus > 5000) { + const rate = Math.round(10000 * (remaining - (total - processed)) / (now - lastStatus)) / 10, + eta = Math.round((total - processed) / rate); + lastStatus = now; + remaining = total - processed; + console.log(`Processing ${rate} images per second. ${remaining} images to be processed. ETA: ${eta}s`); + } + }); + }, { + concurrency: maxConcurrency + }); +}).then(() => { + console.log("Looking for face distances that need to be updated..."); + let maxId; + + return photoDB.sequelize.query("SELECT faces.id FROM faces ORDER BY faces.id DESC LIMIT 1", { + type: photoDB.sequelize.QueryTypes.SELECT, + raw: true + }).then((results) => { + if (!results.length) { + console.log("...no faces exist yet to generate distances."); + maxId = 0; + return []; + } + maxId = results[0].id; + return photoDB.sequelize.query( + "SELECT faces.id,faces.lastComparedId " + + "FROM faces INNER JOIN photos ON photos.duplicate=0 AND photos.deleted=0 AND photos.id=faces.photoId " + + "WHERE faces.lastComparedId<:maxId OR faces.lastComparedId IS NULL " + + "ORDER BY faces.id ASC", { + replacements: { + maxId: maxId + }, type: photoDB.sequelize.QueryTypes.SELECT, raw: true - }).then((results) => { - if (!results.length) { - console.log("...no faces exist yet to generate distances."); - return; - } - const maxId = results[0].id; - return photoDB.sequelize.query( - "SELECT faces.id,faces.lastComparedId " + - "FROM faces INNER JOIN photos ON photos.duplicate=0 AND photos.deleted=0 AND photos.id=faces.photoId " + - "WHERE faces.lastComparedId<:maxId OR faces.lastComparedId IS NULL", { - replacements: { - maxId: maxId - }, - type: photoDB.sequelize.QueryTypes.SELECT, - raw: true - }).then((facesToUpdate) => { - console.log(`...${facesToUpdate.length} faces need to be updated.`); - if (facesToUpdate.length == 0) { - return facesToUpdate; + }); + }).then((facesToUpdate) => { + console.log(`...${facesToUpdate.length} faces need distances updated.`); + if (facesToUpdate.length == 0) { + return facesToUpdate; + } + + const descriptors = {}; + + return photoDB.sequelize.query( + "SELECT id FROM faces ORDER BY id ASC", { + type: photoDB.sequelize.QueryTypes.SELECT, + raw: true + }).then((allFaces) => { + console.log(`...reading ${allFaces.length} descriptors...`); + return Promise.map(allFaces, (face) => { + const id = face.id, + dataPath = faceData + "/" + (id % 100) + "/" + id + "-data.json"; + + if (id in descriptors) { + return; } - console.log("...removing old assets."); - - return photoDB.sequelize.query( - "SELECT id FROM faces", { - type: photoDB.sequelize.QueryTypes.SELECT, - raw: true - }).then((allFaces) => { - console.log(`...reading ${allFaces.length} descriptors...`); - return Promise.map(allFaces, (face) => { - const id = face.id, - dataPath = faceData + "/" + (id % 100) + "/" + id + "-data.json"; - - if (id in descriptors) { - return; - } - - return exists(dataPath).then((doesExist) => { - if (!doesExist) { - console.warn(`${dataPath} is missing!`); - return; - } - - descriptors[id] = JSON.parse(fs.readFileSync(dataPath)); - }); - }, { - concurrency: maxConcurrency - }); - }).then(() => { - return facesToUpdate; - }); - }).then((facesToUpdate) => { - const total = facesToUpdate.length; - let remaining = total, - processed = 0, - lastStatus = Date.now(), - targets = []; - - for (let target in descriptors) { - targets.push({ id: target, descriptor: descriptors[target] }); - } - - return Promise.mapSeries(facesToUpdate, (face) => { - if (!(face.id in descriptors)) { - console.warn(`...attempt to compare distance with no descriptor for ${face.id}`); + return exists(dataPath).then((doesExist) => { + if (!doesExist) { + console.warn(`${dataPath} is missing!`); return; } - const faceDescriptor = descriptors[face.id]; + descriptors[id] = JSON.parse(fs.readFileSync(dataPath)); + }); + }); + }).then(() => { + const total = facesToUpdate.length; + let remaining = total, + processed = 0, + lastStatus = Date.now(), + targets = []; - return photoDB.sequelize.transaction((transaction) => { + for (let target in descriptors) { + targets.push({ id: target, descriptor: descriptors[target] }); + } + + return Promise.mapSeries(facesToUpdate, (face) => { + if (!(face.id in descriptors)) { + console.warn(`...attempt to compare distance with no descriptor for ${face.id}`); + return; + } + + const faceDescriptor = descriptors[face.id]; + + return photoDB.sequelize.transaction((transaction) => { + return photoDB.sequelize.query( + "SELECT distance,face1Id,face2Id " + + "FROM facedistances " + + "WHERE face1Id=:id OR face2Id=:id " + + "ORDER BY face1Id ASC", { + replacements: { + id: face.id + }, + type: photoDB.sequelize.QueryTypes.SELECT, + raw: true, + transaction: transaction + }).then((distances) => { return Promise.map(targets, (target) => { /* Skip comparing to self */ if (target.id == face.id) { @@ -259,87 +293,66 @@ faceapi.nets.ssdMobilenetv1.loadFromDisk('./models') return; } + const index = distances.findIndex((distance) => { + return distance.face1Id == target.id || distance.face2Id == target.id + }); + + if (index != -1) { + /* A distance has already been calculated between face and target */ + return; + } + + const distance = faceapi.euclideanDistance(faceDescriptor, target.descriptor); + + /* If the distance > 0.6, we don't want to store this in the DB */ + if (distance > 0.6) { + return; + } + + if (distance < 0.4) { + console.log(`Face ${face.id} and ${target.id} have a distance of: ${distance}`); + } + return photoDB.sequelize.query( - "SELECT distance " + - "FROM facedistances " + - "WHERE face1Id=:first AND face2Id=:second", { + "INSERT INTO facedistances (face1Id,face2Id,distance) " + + "VALUES (:first,:second,:distance)", { replacements: { first: Math.min(face.id, target.id), - second: Math.max(face.id, target.id) + second: Math.max(face.id, target.id), + distance: distance }, - type: photoDB.sequelize.QueryTypes.SELECT, - raw: true, transaction: transaction - }).then((results) => { - if (results.length) { - return; - } - const distance = faceapi.euclideanDistance(faceDescriptor, target.descriptor); - - if (distance < 0.4) { - console.log(`Face ${face.id} and ${target.id} have a distance of: ${distance}`); - } - - return photoDB.sequelize.query( - "INSERT INTO facedistances (face1Id, face2Id, distance) " + - "VALUES (:first, :second, :distance)", { - replacements: { - first: Math.min(face.id, target.id), - second: Math.max(face.id, target.id), - distance: distance - }, - transaction: transaction - }); }); }, { concurrency: maxConcurrency - }).then(() => { - return photoDB.sequelize.query( - "UPDATE faces SET lastComparedId=:lastId WHERE id=:id", { - replacements: { - lastId: maxId, - id: face.id - }, - transaction: transaction - }); }); }).then(() => { - processed++; - const now = Date.now(); - if (now - lastStatus > 5000) { - const rate = Math.round(10000 * (remaining - (total - processed)) / (now - lastStatus)) / 10, - eta = Math.round((total - processed) / rate); - lastStatus = now; - remaining = total - processed; - console.log(`Processing ${rate} faces per second. ${remaining} faces to be processed. ETA: ${eta}s`); - } + return photoDB.sequelize.query( + "UPDATE faces SET lastComparedId=:lastId WHERE id=:id", { + replacements: { + lastId: maxId, + id: face.id + }, + transaction: transaction + }); }); + }).then(() => { + processed++; + const now = Date.now(); + if (now - lastStatus > 5000) { + const rate = Math.round(10000 * (remaining - (total - processed)) / (now - lastStatus)) / 10, + eta = Math.round((total - processed) / rate); + lastStatus = now; + remaining = total - processed; + console.log(`Processing ${rate} faces per second. ${remaining} faces to be processed. ETA: ${eta}s`); + } }); }); }); - }).then(() => { - console.log("Face detection scanning completed."); - }).catch((error) => { - console.error(error); - process.exit(-1); }); +}).then(() => { + console.log("Face detection scanning completed."); +}).catch((error) => { + console.error(error); + process.exit(-1); }); - -/* TODO: -1. For each path / person, look up highest face confidence and tag -2. Use highest face and identity confidence for input into -https://github.com/justadudewhohacks/face-api.js#face-recognition-by-matching-descriptors - -const labeledDescriptors = [ - new faceapi.LabeledFaceDescriptors( - 'obama', - [descriptorObama1, descriptorObama2] - ), - new faceapi.LabeledFaceDescriptors( - 'trump', - [descriptorTrump] - ) -] - -const faceMatcher = new faceapi.FaceMatcher(labeledDescriptors) -*/ diff --git a/server/face.js b/server/face.js index e159541..b077517 100644 --- a/server/face.js +++ b/server/face.js @@ -4,6 +4,8 @@ process.env.TZ = "Etc/GMT"; require('@tensorflow/tfjs-node'); +let photoDB = null; + const config = require("config"), Promise = require("bluebird"), { exists, mkdir, unlink } = require("./lib/util"), @@ -11,7 +13,7 @@ const config = require("config"), fs = require("fs"), canvas = require("canvas"); -const { Canvas, Image, ImageData } = canvas; +const { createCanvas, Canvas, Image, ImageData } = canvas; faceapi.env.monkeyPatch({ Canvas, Image, ImageData }); @@ -19,45 +21,179 @@ const maxConcurrency = require("os").cpus().length; require("./console-line.js"); /* Monkey-patch console.log with line numbers */ -faceapi.nets.ssdMobilenetv1.loadFromDisk('./models') -.then(() => { - console.log("ssdMobileNetv1 loaded."); +const picturesPath = config.get("picturesPath").replace(/\/$/, "") + "/", + faceData = picturesPath + "face-data/"; + +process.stdout.write("Loading DB."); +require("./db/photos").then(function(db) { + process.stdout.write("done\n"); + photoDB = db; +}).then(() => { + console.log("DB connected."); + process.stdout.write("Loading models."); + return faceapi.nets.ssdMobilenetv1.loadFromDisk('./models'); +}).then(() => { + process.stdout.write("."); return faceapi.nets.faceLandmark68Net.loadFromDisk('./models'); }).then(() => { - console.log("landmark68 loaded."); + process.stdout.write("."); return faceapi.nets.faceRecognitionNet.loadFromDisk('./models'); }).then(async () => { - console.log("faceRecognitionNet loaded."); - let faces = []; - for (let a = 2; a < process.argv.length; a++) { - const file = process.argv[a]; - process.stdout.write(`Loading ${file}...`); - const image = await canvas.loadImage(file), - detectors = await faceapi.detectAllFaces(image, - new faceapi.SsdMobilenetv1Options({ - minConfidence: 0.8 + process.stdout.write(".done\n"); + + if (process.argv[0].match(/node/)) { + process.argv.shift(); /* node */ + } + process.argv.shift(); /* script name */ + + return Promise.resolve().then(() => { + console.log(process.argv.length); + + if (process.argv.length != 0) { + return process.argv; + } + + /* If no parameters provided, scan all faces to create image crops */ + return photoDB.sequelize.query("SELECT id FROM faces ORDER BY id ASC", { + type: photoDB.sequelize.QueryTypes.SELECT, + raw: true + }).then((results) => { + return results.map(result => result.id); + }); + }); +}).then((args) => { + const faces = []; + + return Promise.map(args, (arg) => { + const file = arg, + id = parseInt(arg); + + let loader; + + if (id == file) { + /* This is a face id */ + console.log(`Looking up face-id ${id}...`); + loader = photoDB.sequelize.query( + "SELECT albums.path,photos.filename,photos.width,photos.height,faces.* " + + "FROM faces,photos,albums " + + "WHERE photos.id=faces.photoId " + + "AND albums.id=photos.albumId " + + "AND faces.id=:id", { + replacements: { + id: id + }, + type: photoDB.sequelize.QueryTypes.SELECT, + raw: true + }).then((results) => { + if (results.length != 1) { + console.error(`...error. No face-id found: ${id}.\n`); + process.exit(-1); + } + const photo = results[0]; + console.log(`...loading ${photo.filename}`); + + const file = photo.path + photo.filename; + return canvas.loadImage(picturesPath + file).then(async (image) => { + const detectors = [ { + detection: { + _box: { + _x: photo.left * photo.width, + _y: photo.top * photo.height, + _width: (photo.right - photo.left) * photo.width, + _height: (photo.bottom - photo.top) * photo.height, + } + }, + descriptor: JSON.parse(fs.readFileSync(faceData + (id % 100) + "/" + id + "-data.json")) + } ]; + return [ file, image, detectors ]; + }); + }); + } else { + /* This is a file */ + console.log(`Loading ${file}...`); + id = undefined; + loader = canvas.loadImage(picturesPath + file).then(async (image) => { + const detectors = await faceapi.detectAllFaces(image, + new faceapi.SsdMobilenetv1Options({ + minConfidence: 0.8 + }) + ).withFaceLandmarks().withFaceDescriptors(); + detectors.forEach((detector) => { + const data = []; + /* Confert from sparse object to dense array */ + for (let i = 0; i < 128; i++) { + data.push(detector.descriptor[i]); + } + detector.descriptor = data; + }); + return [ file, image, detectors ]; + }); + } + + return loader.spread((filepath, image, detectors) => { + process.stdout.write(`${detectors.length} faces.\n`); + + return Promise.map(detectors, (face, index) => { + faces.push({ + filepath: filepath, + index: index, + descriptor: face.descriptor }) - ).withFaceLandmarks().withFaceDescriptors(); - process.stdout.write(`${detectors.length} faces.\n`); - - detectors.forEach((face, index) => { - faces.push({ - file: file, - index: index, - descriptor: face.descriptor + + /* If this is a face-id, output the -original.png + * meta-data file */ + if (!id) { + return; + } + + const path = "face-data/" + (id % 100), + target = `${path}/${id}-original.png`, + box = face.detection._box, + aspect = box._width / box._height, + dx = (aspect > 1.0) ? 200 : (200 * aspect), + dy = (aspect < 1.0) ? 200 : (200 / aspect); + + return exists(target).then((doesExist) => { + if (doesExist) { + console.log(`...${target} already exists.`); + return; + } + const canvas = createCanvas(200, 200), + ctx = canvas.getContext('2d'); + + ctx.fillStyle = "rgba(0, 0, 0, 0)"; + ctx.fillRect(0, 0, 200, 200); + ctx.drawImage(image, box._x, box._y, box._width, box._height, + Math.floor((200 - dx) * 0.5), + Math.floor((200 - dy) * 0.5), dx, dy); + + console.log(`...writing to ${target}.`); + + return mkdir(path).then(() => { + fs.writeFileSync(picturesPath + target, canvas.toBuffer("image/png", { + quality: 0.95, + chromaSubsampling: false + })); + }); + }); + }); + }); + }, { + concurrency: maxConcurrency + }).then(() => { + console.log("Face detection scanning completed."); + faces.forEach((a, i) => { + faces.forEach((b, j) => { + if (i == j) { + return; + } + const distance = faceapi.euclideanDistance(a.descriptor, b.descriptor); + if (distance < 0.4) { + console.log(`${a.filepath}.${a.index} is similar to ${b.filepath}.${b.index}: ${distance}`); + } }) }); - } - - for (let i = 0; i < faces.length; i++) { - for (let j = 0; j < faces.length; j++) { - const a = faces[i], b = faces[j]; - const distance = faceapi.euclideanDistance(a.descriptor, b.descriptor); - console.log(`${a.file}.${a.index} to ${b.file}.${b.index} = ${distance}`); - } - } -}).then(() => { - console.log("Face detection scanning completed."); + }); }).catch((error) => { console.error(error); process.exit(-1);