Updating slideshow and identity editor

Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
James Ketr 2023-01-13 14:09:46 -08:00
parent ebee258bb3
commit d019af070d
7 changed files with 274 additions and 202 deletions

View File

@ -18,4 +18,5 @@ services:
- ${PWD}/config/local.json:/website/config/local.json - ${PWD}/config/local.json:/website/config/local.json
- /opt/ketrface/models:/root/.deepface - /opt/ketrface/models:/root/.deepface
# - ${PWD}:/website # - ${PWD}:/website
- ${PWD}/frontend:/website/frontend
- ${PWD}/server:/website/server - ${PWD}/server:/website/server

View File

@ -18,7 +18,8 @@ function createFace(faceId, photoId, selectable) {
div.classList.add("face"); div.classList.add("face");
div.setAttribute("photo-id", photoId); div.setAttribute("photo-id", photoId);
div.setAttribute("face-id", faceId); div.setAttribute("face-id", faceId);
div.style.backgroundImage = "url(face-data/" + (faceId % 100) + "/" + faceId + "-original.png)"; const dir = String(faceId % 100).padStart(2, '0')
div.style.backgroundImage = `url(faces/${dir}/${faceId}.jpg)`;
div.addEventListener("click", (event) => { div.addEventListener("click", (event) => {
if (event.shiftKey) { /* identities */ if (event.shiftKey) { /* identities */
let faceId = parseInt(event.currentTarget.getAttribute("face-id")); let faceId = parseInt(event.currentTarget.getAttribute("face-id"));

View File

@ -77,39 +77,73 @@ body {
<div id="loading"></div> <div id="loading"></div>
</div> </div>
<script> <script>
var base, placeholder, info, photos = [], photoIndex;
function shuffle(array) { let base,
var index = array.length, tmp, random; placeholder,
info,
photos = [],
photoIndex = -1;
while (index) { let mode, filter;
random = Math.floor(Math.random() * index);
index--;
// And swap it with the current element. const days = [
tmp = array[index]; "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
array[index] = array[random]; "Saturday"
array[random] = tmp; ];
} const months = [
"January", "February", "March", "April", "May", "June",
return array; "July", "August", "September", "October", "November", "December"
} ];
var countdown = 15;
const days = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ],
months = [ "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December" ];
let activeFaces = []; let activeFaces = [];
function makeFaceBoxes() { let paused = false,
tap = 0;
let countdown = 15;
let scheduled = false;
const onClick = (e) => {
const now = new Date().getTime();
if (tap && (now - tap < 300)) {
toggleFullscreen();
tap = 0;
} else {
tap = new Date().getTime();
}
};
const shuffle = (arr) => {
var index = arr.length, tmp, random;
while (index) {
random = Math.floor(Math.random() * index);
index--;
tmp = arr[index];
arr[index] = arr[random];
arr[random] = tmp;
}
return arr;
};
const faceClick = (event, face) => {
console.log(face);
face.relatedFaces.forEach((photo) => {
window.open(base + photo.path);
});
event.preventDefault = true;
event.stopImmediatePropagation();
event.stopPropagation();
return false;
};
const makeFaceBoxes = () => {
Array.prototype.forEach.call(document.querySelectorAll('.face'), (el) => { Array.prototype.forEach.call(document.querySelectorAll('.face'), (el) => {
el.parentElement.removeChild(el); el.parentElement.removeChild(el);
}); });
console.log("Not showing face boxes"); if (activeFaces.length === 0) {
return; return;
}
const el = document.getElementById("photo"), const el = document.getElementById("photo"),
photo = photos[photoIndex]; photo = photos[photoIndex];
@ -129,7 +163,6 @@ return;
offsetTop = 0; offsetTop = 0;
} }
activeFaces.forEach((face) => { activeFaces.forEach((face) => {
const box = document.createElement("div"); const box = document.createElement("div");
box.classList.add("face"); box.classList.add("face");
@ -138,20 +171,11 @@ return;
box.style.top = offsetTop + Math.floor(face.top * height) + "%"; box.style.top = offsetTop + Math.floor(face.top * height) + "%";
box.style.width = Math.floor((face.right - face.left) * width) + "%"; box.style.width = Math.floor((face.right - face.left) * width) + "%";
box.style.height = Math.floor((face.bottom - face.top) * height) + "%"; box.style.height = Math.floor((face.bottom - face.top) * height) + "%";
box.addEventListener("click", (event) => { box.addEventListener("click", (e) => { return faceClick(e, face); });
console.log(face);
face.relatedPhotos.forEach((photo) => {
window.open(base + photo.path);
});
event.preventDefault = true;
event.stopImmediatePropagation();
event.stopPropagation();
return false;
});
}); });
} };
function loadPhoto(index) { const loadPhoto = (index) => {
const photo = photos[index], const photo = photos[index],
xml = new XMLHttpRequest(), xml = new XMLHttpRequest(),
url = base + photo.path + "thumbs/scaled/" + photo.filename, url = base + photo.path + "thumbs/scaled/" + photo.filename,
@ -169,23 +193,29 @@ function loadPhoto(index) {
} }
}; };
xml.onload = function(event) { xml.onload = async (event) => {
info.textContent = info.textContent =
days[taken.getDay()] + ", " + days[taken.getDay()] + ", " +
months[taken.getMonth()] + " " + months[taken.getMonth()] + " " +
taken.getDate() + " " + taken.getDate() + " " +
taken.getFullYear(); taken.getFullYear();
document.getElementById("photo").style.backgroundImage = "url(" + encodeURI(url).replace(/\(/g, '%28').replace(/\)/g, '%29') + ")"; activeFaces = [];
makeFaceBoxes();
document.getElementById("photo").style.backgroundImage =
`url(${encodeURI(url).replace(/\(/g, '%28').replace(/\)/g, '%29')})`;
countdown = 15; countdown = 15;
tick(); tick();
window.fetch("api/v1/photos/faces/" + photo.id).then(res => res.json()).then((faces) => {
try {
const res = await window.fetch("api/v1/photos/faces/" + photo.id);
const faces = await res.json();
activeFaces = faces; activeFaces = faces;
makeFaceBoxes(photo); makeFaceBoxes(photo);
}).catch(function(error) { } catch (error) {
console.error(error); console.error(error);
info.textContent += "Unable to obtain face information :("; info.textContent += "Unable to obtain face information :(";
}); }
} };
xml.onerror = function(event) { xml.onerror = function(event) {
info.textContent = "Error loading photo. Trying next photo."; info.textContent = "Error loading photo. Trying next photo.";
@ -196,14 +226,77 @@ function loadPhoto(index) {
xml.send(); xml.send();
} }
function nextPhoto() { const prevPhoto = async () => {
photoIndex = (photoIndex + 1) % photos.length; if (photoIndex > 0) {
loadPhoto(photoIndex); photoIndex--;
loadPhoto(photoIndex);
return;
}
if (mode !== 'random/') {
photoIndex = photos.length;
loadPhoto(photoIndex);
return;
}
try {
const res = await window.fetch(
`api/v1/photos/${mode}${filter.replace(/ +/g, "%20")}`);
const data = await res.json();
if (data && data.items) {
info.textContent = photos.length + " photos found. Shuffling.";
photos = shuffle(data.items);
photoIndex = (photoIndex + 1) % photos.length;
loadPhoto(photoIndex);
} else if (data) {
photos.push(data);
photoIndex = photos.length - 1;
loadPhoto(photoIndex);
} else {
info.textContent = "No photos found for " + filter + ".";
}
} catch(error) {
console.error(error);
info.textContent = "Unable to fetch " + mode + "=" + filter + " :(";
}
} }
var scheduled = false; const nextPhoto = async () => {
if (photoIndex < photos.length - 1) {
photoIndex++;
loadPhoto(photoIndex);
return;
}
function tick() { if (mode !== 'random/') {
photoIndex = 0;
loadPhoto(photoIndex);
return;
}
try {
const res = await window.fetch(
`api/v1/photos/${mode}${filter.replace(/ +/g, "%20")}`);
const data = await res.json();
if (data && data.items) {
info.textContent = photos.length + " photos found. Shuffling.";
photos = shuffle(data.items);
photoIndex = 0;
loadPhoto(photoIndex);
} else if (data) {
photos.push(data);
photoIndex = photos.length - 1;
loadPhoto(photoIndex);
} else {
info.textContent = "No photos found for " + filter + ".";
}
} catch (error) {
console.error(error);
info.textContent = "Unable to fetch " + mode + "=" + filter + " :(";
}
}
const tick = () => {
if (scheduled) { if (scheduled) {
clearTimeout(scheduled); clearTimeout(scheduled);
} }
@ -219,16 +312,16 @@ function tick() {
countdown = 15; countdown = 15;
nextPhoto(); nextPhoto();
} }
} };
function schedule() { const schedule = () => {
if (scheduled) { if (scheduled) {
clearTimeout(scheduled); clearTimeout(scheduled);
} }
tick(); tick();
} };
function toggleFullscreen() { const toggleFullscreen = () => {
if (!document.fullscreenElement) { if (!document.fullscreenElement) {
document.documentElement.requestFullscreen(); document.documentElement.requestFullscreen();
} else { } else {
@ -236,104 +329,75 @@ function toggleFullscreen() {
document.exitFullscreen(); document.exitFullscreen();
} }
} }
} };
var paused = false, const onKeyDown = (event) => {
tap = 0; switch (event.keyCode) {
case 32: /* space */
paused = !paused;
if (!paused) {
tick();
} else {
document.getElementById("loading").textContent = "||";
clearTimeout(scheduled);
scheduled = null;
}
return;
case 37: /* left */
prevPhoto();
return;
case 39: /* right */
nextPhoto();
return;
case 13: /* enter */
toggleFullscreen();
return;
}
};
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
var tmp = document.querySelector("base"); var tmp = document.querySelector("base");
if (tmp) { if (tmp) {
base = new URL(tmp.href).pathname.replace(/\/$/, "") + "/"; /* Make sure there is a trailing slash */ /* Make sure there is a trailing slash */
} else { base = new URL(tmp.href).pathname.replace(/\/$/, "") + "/";
base = "/"; } else {
base = "/";
}
var timeout = 0;
window.addEventListener("resize", (event) => {
if (timeout) {
window.clearTimeout(timeout);
} }
timeout = window.setTimeout(makeFaceBoxes, 250);
});
var timeout = 0; document.addEventListener("click", onClick);
window.addEventListener("resize", (event) => { document.addEventListener("keydown", onKeyDown);
if (timeout) {
window.clearTimeout(timeout);
}
timeout = window.setTimeout(makeFaceBoxes, 250);
});
document.addEventListener("click", function(event) { info = document.getElementById("info");
toggleFullscreen();
var now = new Date().getTime();
if (tap && (now - tap < 300)) {
toggleFullscreen();
tap = 0;
} else {
tap = new Date().getTime();
}
});
document.addEventListener("keydown", function(event) { /* Trim off everything up to and including the ? (if its there) */
switch (event.keyCode) { var parts = window.location.href.match(/^.*\?(.*)$/);
case 32: /* space */ if (parts) {
paused = !paused; parts = parts[1].split("=");
if (!paused) { if (parts.length == 1) {
tick(); mode = "holiday/";
} else { filter = parts[0];
document.getElementById("loading").textContent = "||";
clearTimeout(scheduled);
scheduled = null;
}
return;
case 37: /* left */
if (photoIndex == 0) {
photoIndex = photos.length;
}
photoIndex--;
loadPhoto(photoIndex);
return;
case 39: /* right */
photoIndex = (photoIndex + 1) % photos.length;
loadPhoto(photoIndex);
return;
case 13: /* enter */
toggleFullscreen();
return;
}
console.log(event.keyCode);
});
info = document.getElementById("info");
/* Trim off everything up to and including the ? (if its there) */
var parts = window.location.href.match(/^.*\?(.*)$/),
mode = "holiday",
filter = "";
if (parts) {
parts = parts[1].split("=");
if (parts.length == 1) {
filter = parts[0];
} else {
mode = parts[0];
filter = parts[1];
}
} else { } else {
filter = "memorial day"; mode = parts[0].replace(/\/*$/, '/');
filter = parts[1];
} }
mode = mode
} else {
mode = "random/"
filter = "";
}
window.fetch("api/v1/photos/" + mode + "/" + filter.replace(/ +/g, "%20")) nextPhoto();
.then(res => res.json()).then(function(data) {
if (data.items.length) {
info.textContent = photos.length + " photos found. Shuffling.";
photos = shuffle(data.items);
photoIndex = -1;
nextPhoto();
} else {
info.textContent = "No photos found for " + filter + ".";
}
}).catch(function(error) {
console.error(error);
info.textContent = "Unable to fetch " + mode + "=" + filter + " :(";
});
}); });
</script> </script>
</body> </body>

View File

@ -3,12 +3,15 @@ import json
import os import os
import piexif import piexif
import argparse
from PIL import Image, ImageOps from PIL import Image, ImageOps
from deepface import DeepFace from deepface import DeepFace
from deepface.detectors import FaceDetector from deepface.detectors import FaceDetector
from retinaface import RetinaFace from retinaface import RetinaFace
import numpy as np import numpy as np
import cv2 import cv2
from ketrface.util import * from ketrface.util import *
from ketrface.db import * from ketrface.db import *
from ketrface.config import * from ketrface.config import *
@ -25,7 +28,8 @@ model_name = 'VGG-Face' # 'ArcFace'
detector_backend = 'mtcnn' # 'retinaface' detector_backend = 'mtcnn' # 'retinaface'
model = DeepFace.build_model(model_name) model = DeepFace.build_model(model_name)
# Derived from https://github.com/serengil/deepface/blob/master/deepface/detectors/MtcnnWrapper.py # Derived from
# https://github.com/serengil/deepface/blob/master/deepface/detectors/MtcnnWrapper.py
# Add parameters to MTCNN # Add parameters to MTCNN
from mtcnn import MTCNN from mtcnn import MTCNN
face_detector = MTCNN(min_face_size = 30) face_detector = MTCNN(min_face_size = 30)
@ -63,7 +67,9 @@ def variance_of_laplacian(image):
# measure, which is simply the variance of the Laplacian # measure, which is simply the variance of the Laplacian
return cv2.Laplacian(image, cv2.CV_64F).var() return cv2.Laplacian(image, cv2.CV_64F).var()
def extract_faces(img, threshold=0.95, allow_upscaling = True, focus_threshold = 100): def extract_faces(
img, threshold=0.95, allow_upscaling = True, focus_threshold = 100):
if detector_backend == 'retinaface': if detector_backend == 'retinaface':
faces = RetinaFace.detect_faces( faces = RetinaFace.detect_faces(
img_path = img, img_path = img,
@ -103,8 +109,6 @@ def extract_faces(img, threshold=0.95, allow_upscaling = True, focus_threshold =
} }
to_drop = []
# Re-implementation of 'extract_faces' with the addition of keeping a # Re-implementation of 'extract_faces' with the addition of keeping a
# copy of the face image for caching on disk # copy of the face image for caching on disk
for k, key in enumerate(faces): for k, key in enumerate(faces):
@ -182,12 +186,15 @@ def extract_faces(img, threshold=0.95, allow_upscaling = True, focus_threshold =
identity['image'] = Image.fromarray(resized) identity['image'] = Image.fromarray(resized)
# for key in to_drop:
# faces.pop(key)
return faces return faces
parser = argparse.ArgumentParser(description = 'Detect faces in images.')
parser.add_argument('photos', metavar='PHOTO', type=int, nargs='*',
help='PHOTO ID to scan (default: all unscanned photos)')
args = parser.parse_args()
print(args)
base = '/pictures/' base = '/pictures/'
conn = create_connection('../db/photos.db') conn = create_connection('../db/photos.db')
with conn: with conn:
@ -222,11 +229,6 @@ with conn:
image = face['image'] image = face['image']
print(f'Writing face {j+1}/{len(faces)}') print(f'Writing face {j+1}/{len(faces)}')
#face['analysis'] = DeepFace.analyze(img_path = img, actions = ['age', 'gender', 'race', 'emotion'], enforce_detection = False)
#face['analysis'] = DeepFace.analyze(img, actions = ['emotion'])
# TODO: Add additional meta-data allowing back referencing to original
# photo
face['version'] = 1 # version 1 doesn't add much... face['version'] = 1 # version 1 doesn't add much...
data = {k: face[k] for k in set(list(face.keys())) - set(['image', 'facial_area', 'landmarks'])} data = {k: face[k] for k in set(list(face.keys())) - set(['image', 'facial_area', 'landmarks'])}

View File

@ -45,7 +45,6 @@ def create_face(conn, face):
conn.commit() conn.commit()
return cur.lastrowid return cur.lastrowid
def create_face_descriptor(conn, face): def create_face_descriptor(conn, face):
""" """
Create a new face in the faces table Create a new face in the faces table

View File

@ -95,7 +95,7 @@ function euclideanDistance(a, b) {
let A = bufferToFloat32Array(a); let A = bufferToFloat32Array(a);
let B = bufferToFloat32Array(b); let B = bufferToFloat32Array(b);
let sum = 0; let sum = 0;
for (let i = 0; i < 128; i++) { for (let i = 0; i < A.length; i++) {
let delta = A[i] - B[i]; let delta = A[i] - B[i];
sum += delta * delta; sum += delta * delta;
} }
@ -118,14 +118,13 @@ router.get("/:id?", (req, res) => {
"identities.*," + "identities.*," +
"GROUP_CONCAT(faces.id) AS relatedFaceIds," + "GROUP_CONCAT(faces.id) AS relatedFaceIds," +
"GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds," + "GROUP_CONCAT(faces.photoId) AS relatedFacePhotoIds," +
"GROUP_CONCAT(faces.identityDistance) AS relatedIdentityDistances " + "GROUP_CONCAT(facedescriptors.descriptors) AS relatedIdentityDescriptors " +
"FROM identities " + "FROM identities " +
"INNER JOIN facedescriptors ON facedescriptors.id=faces.descriptorId " +
"INNER JOIN faces ON identities.id=faces.identityId " + "INNER JOIN faces ON identities.id=faces.identityId " +
filter + filter +
"GROUP BY identities.id", { "GROUP BY identities.id", {
replacements: { replacements: { id },
id: id
},
type: photoDB.Sequelize.QueryTypes.SELECT, type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true raw: true
}).then((identities) => { }).then((identities) => {
@ -139,10 +138,14 @@ router.get("/:id?", (req, res) => {
delete identity.relatedFaceIds; delete identity.relatedFaceIds;
delete identity.relatedFacePhotoIds; delete identity.relatedFacePhotoIds;
identity.relatedFaces = relatedFaces.map((faceId, index) => { identity.relatedFaces = relatedFaces.map((faceId, index) => {
const distance = euclideanDistance(
relatedIdentityDistances[index],
identity.descriptors
);
return { return {
faceId: faceId, faceId: faceId,
photoId: relatedFacePhotos[index], photoId: relatedFacePhotos[index],
distance: parseFloat(relatedIdentityDistances[index] !== undefined ? relatedIdentityDistances[index] : -1) distance
}; };
}); });
}); });

View File

@ -951,7 +951,7 @@ router.get("/faces/:id", (req, res) => {
}); });
}); });
router.get("/random/:id?", (req, res) => { router.get("/random/:id?", async (req, res) => {
let id = parseInt(req.params.id), let id = parseInt(req.params.id),
filter = ""; filter = "";
@ -959,54 +959,56 @@ router.get("/random/:id?", (req, res) => {
console.log("GET /random/" + id); console.log("GET /random/" + id);
filter = "AND id=:id"; filter = "AND id=:id";
} else { } else {
filter = "AND faces>0"; console.log("GET /random/");
filter = "";//AND faces>0";
id = undefined; id = undefined;
} }
return photoDB.sequelize.query("SELECT id,duplicate FROM photos WHERE deleted=0 " + filter, { /* If the requested ID is a duplicate, we need to find the original
replacements: { * photo ID */
id: id const results = await photoDB.sequelize.query(
}, "SELECT id,duplicate FROM photos WHERE deleted=0 " + filter, {
replacements: { id },
type: photoDB.Sequelize.QueryTypes.SELECT, type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true raw: true
}).then((results) => {
if (!results.length) {
return [];
}
if (id) {
if (results[0].duplicate) {
id = results[0].duplicate;
}
} else {
id = results[Math.floor(Math.random() * results.length)].id;
}
return photoDB.sequelize.query(
"SELECT photos.*,albums.path AS path FROM photos " +
"INNER JOIN albums ON albums.id=photos.albumId " +
"WHERE photos.duplicate=0 AND photos.deleted=0 AND photos.scanned NOT NULL AND photos.id=:id", {
replacements: {
id: id,
},
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
});
}).then(function(photos) {
if (!photos.length) {
return res.status(404).send({ message: id + " not found." });
}
const photo = photos[0];
for (var key in photo) {
if (photo[key] instanceof Date) {
photo[key] = moment(photo[key]);
}
}
return getFacesForPhoto(photo.id).then((faces) => {
photo.faces = faces;
return res.status(200).json(photo);
})
}); });
})
if (!results.length) {
return res.status(404).send({ message: id + " not found." });
}
if (id) {
if (results[0].duplicate) {
id = results[0].duplicate;
}
} else {
id = results[Math.floor(Math.random() * results.length)].id;
}
const photos = await photoDB.sequelize.query(
"SELECT photos.*,albums.path AS path FROM photos " +
"INNER JOIN albums ON albums.id=photos.albumId " +
"WHERE photos.duplicate=0 AND photos.deleted=0 AND photos.scanned NOT NULL AND photos.id=:id", {
replacements: { id },
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
});
if (photos.length === 0) {
return res.status(404).send({ message: `Error obtaining ${id}.`});
}
const photo = photos[0];
for (var key in photo) {
if (photo[key] instanceof Date) {
photo[key] = moment(photo[key]);
}
}
const faces = await getFacesForPhoto(photo.id);
photo.faces = faces;
return res.status(200).json(photo);
});
router.get("/mvimg/*", function(req, res/*, next*/) { router.get("/mvimg/*", function(req, res/*, next*/) {
let limit = parseInt(req.query.limit) || 50, let limit = parseInt(req.query.limit) || 50,