Started identities editor.

Signed-off-by: James Ketrenos <james_gitlab@ketrenos.com>
This commit is contained in:
James Ketrenos 2020-01-19 18:19:45 -08:00
parent f0ae806fbb
commit b7ed9f05f1
4 changed files with 457 additions and 0 deletions

278
frontend/identities.html Normal file
View File

@ -0,0 +1,278 @@
<html>
<script>'<base href="BASEPATH">';</script>
<script>
/*
1. Query server for a face with no bound identity.
2. Query for faces related to that face
3. Put a UI that let's the user pick from existing identities.
4. Select / Deselect all faces that match
5. "Match" to bind the faces to the identity
6 Create new Identity
*/
function loadMore(index) {
var clusterBlock = document.body.querySelector("[cluster-index='" + index + "']");
if (!clusterBlock) {
return;
}
var faces = clusterBlock.querySelectorAll("div.face").length, i
for (i = faces; i < clusters[index].length; i++) {
if (i - faces > 10) {
return;
}
var tuple = clusters[index][i],
face = createFace(tuple[0], tuple[1]);
clusterBlock.appendChild(face);
}
if (i == clusters[index].length) {
var span = clusterBlock.querySelector("span.more");
if (span) {
span.parentElement.removeChild(span);
}
}
}
function createFace(faceId, photoId, selectable) {
var div = document.createElement("div");
div.classList.add("face");
div.setAttribute("photo-id", photoId);
div.setAttribute("face-id", faceId);
div.style.backgroundImage = "url(face-data/" + (faceId % 100) + "/" + faceId + "-original.png)";
if (!selectable) {
div.addEventListener("click", (event) => {
let photoId = parseInt(event.currentTarget.getAttribute("photo-id"));
if (photoId) {
window.open("face-explorer.html?" + photoId, "photo-" + photoId);
} else {
alert("No photo id mapped to face.");
}
});
} else {
div.addEventListener("click", (event) => {
if (event.currentTarget.hasAttribute("disabled")) {
event.currentTarget.removeAttribute("disabled");
} else {
event.currentTarget.setAttribute("disabled", "");
}
});
}
return div;
}
function shuffle(array) {
var i = array.length, tmp, random;
while (i > 0) {
random = Math.floor(Math.random() * i);
i--;
tmp = array[i];
array[i] = array[random];
array[random] = tmp;
}
return array;
}
document.addEventListener("DOMContentLoaded", (event) => {
window.fetch("api/v1/faces").then(res => res.json()).then((faces) => {
const block = document.createElement("div");
block.id = "face-editor";
block.classList.add("block");
faces.forEach((face) => {
const editor = document.createElement("div");
editor.classList.add("editor");
editor.appendChild(createFace(face.id, face.photoId));
for (let key in face) {
const row = document.createElement("div"),
left = document.createElement("div");
row.classList.add("editor-row");
left.textContent = key;
let right;
if (key == "relatedFaces") {
right = document.createElement("div");
right.classList.add("related-faces");
face.relatedFaces.forEach((face) => {
right.appendChild(createFace(face.faceId, face.photoId, true));
});
} else {
right = document.createElement("input");
right.value = face[key];
}
row.appendChild(left);
row.appendChild(right);
editor.appendChild(row);
}
block.appendChild(editor);
});
document.body.appendChild(block);
getIdentities();
});
});
function getIdentities() {
const el = document.getElementById("identities");
if (el) {
el.parentElement.removeChild(el);
}
window.fetch("api/v1/identities").then(res => res.json()).then((identities) => {
const block = document.createElement("div");
block.id = "identities";
block.classList.add("block");
identities.forEach((identity) => {
const editor = document.createElement("div");
editor.classList.add("editor");
// editor.appendChild(createFace(face.id, face.photoId));
for (let key in face) {
const row = document.createElement("div"),
left = document.createElement("div");
row.classList.add("editor-row");
left.textContent = key;
let right;
if (key == "relatedFaces") {
right = document.createElement("div");
face.relatedFaces.forEach((face) => {
right.appendChild(createFace(face.faceId, face.photoId));
});
} else {
right = document.createElement("input");
right.value = face[key];
}
row.appendChild(left);
row.appendChild(right);
editor.appendChild(row);
}
block.appendChild(editor);
});
block.appendChild(createNewIdenityEditor());
document.body.appendChild(block);
});
}
function createNewIdenityEditor() {
const block = document.createElement("div");
block.classList.add("block");
const editor = document.createElement("div");
editor.classList.add("editor");
[ "lastName", "firstName", "middleName", "name" ].forEach((key) => {
const row = document.createElement("div"),
left = document.createElement("div"),
right = document.createElement("input");
row.classList.add("editor-row");
left.textContent = key;
row.appendChild(left);
row.appendChild(right);
editor.appendChild(row);
});
const button = document.createElement("button");
button.textContent = "New Identity";
button.addEventListener("click", (event) => {
const rows = event.currentTarget.parentElement.querySelectorAll(".editor-row"),
object = {};
Array.prototype.forEach.call(rows, (row) => {
object[row.firstChild.textContent] = row.lastChild.value;
});
object.faces = [];
Array.prototype.forEach.call(document.body.querySelectorAll("#face-editor .face"), (face) => {
if (!face.hasAttribute("disabled")) {
object.faces.push(face.getAttribute("face-id"));
}
});
console.log(object);
window.fetch("api/v1/identities", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(object)
}).then(res => res.json()).then((identities) => {
console.log("Identities: ", identities);
getIdentities();
});
});
editor.appendChild(button);
block.appendChild(editor);
return block;
}
</script>
<style>
body {
margin: 0;
padding: 0;
}
.more {
cursor: pointer;
}
.more:hover {
text-decoration: underline;
}
.face[disabled] {
filter: grayscale(100%);
}
.face {
max-width: 128px;
max-height: 128px;
width: 128px;
height: 128px;
background-size: contain;
background-position: center center;
display: inline-block;
border: 1px solid black;
margin: 0.5em;
cursor: pointer;
}
.face:hover {
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.5);
}
.editor-row {
display: flex;
flex-direction: row;
padding: 0.25em 0.5em;
}
.editor-row :first-child {
width: 10em;
}
</style>
<script>
var clusters = [
/* 1. */ [[738,229],[739,230],[740,231],[741,233],[742,237],[743,238],[749,242],[751,244],[752,245],[754,247],[755,248],[756,249],[758,256],[760,258],[762,261],[764,263],[765,265],[766,267],[767,268],[770,270],[772,272],[774,274],[775,277],[776,282],[777,285],[779,287],[780,288],[781,290],[783,292],[784,294],[786,298],[787,299],[788,300]],
/* 2. */ [[791,309],[792,315],[793,316],[797,333],[798,335],[800,350],[802,353]],
/* 3. */ [[805,360],[806,362],[807,363],[808,374]],
/* 4. */ [[848,511],[849,511],[851,512],[852,512]],
/* 5. */ [[915,651],[916,654],[918,656],[919,659]],
/* 6. */ [[954,757],[955,758],[957,770],[959,780],[960,781],[961,796],[962,799],[963,800],[964,805],[982,891],[986,908],[988,916],[990,922],[993,936],[995,941],[998,951],[1000,957],[1003,961],[1004,962],[1005,965],[1009,976],[1013,988],[1015,991],[1018,996],[1023,1021],[1027,1038],[1028,1039],[1029,1044],[1033,1050],[1034,1052],[1039,1066],[1040,1068],[1041,1072],[1045,1078],[1046,1084],[1051,1089],[1052,1091],[1054,1097],[1059,1106],[1060,1107],[1102,1200],[1103,1204],[1104,1205],[1105,1206],[1106,1206],[1107,1208],[1108,1212],[1110,1222],[1113,1231],[1115,1236],[1116,1242],[1117,1242],[1121,1248],[1125,1255],[1126,1256],[1127,1258],[1128,1261],[1131,1267],[1135,1279],[1136,1282],[1138,1284],[1139,1292],[1140,1295],[1141,1296],[1142,1297],[1145,1299],[1146,1300],[1148,1302],[1150,1306],[1152,1308],[1153,1314],[1155,1329],[1156,1330],[1157,1336],[1158,1337],[1159,1348],[1162,1352],[1163,1353],[1164,1356],[1165,1361],[1166,1364],[1167,1367],[1168,1369],[1171,1372],[1172,1373],[1173,1376],[1174,1377],[1175,1378],[1176,1379],[1177,1379],[1178,1380],[1179,1382],[1180,1383],[1181,1385],[1182,1390],[1185,1418],[1186,1424],[1187,1425],[1188,1426],[1189,1434],[1190,1437],[1191,1438],[1194,1440],[1195,1441],[1196,1443],[1197,1444],[1200,1452],[1201,1457],[1325,1826],[1326,1833],[1327,1834],[1328,1836],[1329,1837],[1359,1918],[1360,1919],[1362,1921],[1363,1922],[1364,1923]],
/* 7. */ [[985,904],[989,918],[999,957],[1010,984],[1012,988],[1014,989],[1024,1024],[1037,1056],[1038,1058],[1044,1078],[1047,1084],[1050,1089],[1053,1096],[1055,1097],[1056,1098]],
/* 8. */ [[1035,182],[1380,112],[1381,120],[1382,121],[1384,124],[1385,125],[1386,128],[1387,129],[1392,177],[1393,178],[1394,179],[1395,180]],
/* 9. */ [[785,295],[908,629],[1065,1135],[1068,1145],[1070,1152],[1071,1154],[1072,1155],[1075,1165],[1076,1166],[1077,1167],[1078,1168]],
/* 10. */ [[1091,1471],[1219,1757],[1222,1762],[1223,1764],[1224,1766],[1226,1774],[1227,1775],[1228,1776]],
/* 11. */ [[1208,1506],[1210,1530],[1211,1533],[1213,1541]],
/* 12. */ [[1233,1556],[1234,1557],[1236,1559],[1237,1563],[1240,1570],[1241,1571],[1243,1583],[1245,1590],[1247,1593],[1248,1594],[1249,1596],[1251,1600],[1253,1602],[1254,1604],[1255,1605],[1256,1607],[1257,1608],[1259,1610],[1260,1611],[1261,1615],[1263,1619],[1266,1638],[1267,1639],[1269,1646],[1271,1661],[1276,1665],[1277,1666],[1278,1667],[1280,1670],[1281,1672],[1284,1678],[1285,1679],[1287,1684],[1288,1685],[1289,1686],[1291,1688],[1292,1690],[1293,1693],[1295,1712],[1296,1715],[1297,1716],[1298,1717],[1300,1722],[1302,1725],[1303,1726],[1304,1728],[1306,1730],[1308,1733]],
/* 13. */ [[1143,1298],[1160,1351],[1192,1439],[1310,1789]],
/* 14. */ [[1332,1860],[1333,1860],[1335,1861],[1336,1861]],
/* 15. */ [[935,705],[1342,1887],[1343,1888],[1347,1902],[1350,1906],[1352,1908],[1353,1909],[1354,1910],[1356,1912],[1358,1915],[1367,1931],[1368,10],[1369,9]]
];
</script>
<body>
</body>

View File

@ -264,7 +264,9 @@ app.use(basePath + "api/v1/photos", require("./routes/photos"));
app.use(basePath + "api/v1/days", require("./routes/days"));
app.use(basePath + "api/v1/albums", require("./routes/albums"));
app.use(basePath + "api/v1/holidays", require("./routes/holidays"));
app.use(basePath + "api/v1/faces", require("./routes/faces"));
app.use(basePath + "api/v1/scan", require("./routes/scan")(scanner));
app.use(basePath + "api/v1/identities", require("./routes/identities"));
/* Declare the "catch all" index route last; the final route is a 404 dynamic router */
app.use(basePath, index);

134
server/routes/faces.js Normal file
View File

@ -0,0 +1,134 @@
"use strict";
const express = require("express"),
config = require("config"),
crypto = require("crypto"),
Promise = require("bluebird");
let photoDB;
require("../db/photos").then(function(db) {
photoDB = db;
});
const router = express.Router();
const picturesPath = config.get("picturesPath").replace(/\/$/, "") + "/";
router.put("/:id", function(req, res/*, next*/) {
if (!req.user.maintainer) {
return res.status(401).send("Unauthorized to modify photos.");
}
return res.status(400).send("Invalid request");
});
router.delete("/:id?", function(req, res/*, next*/) {
if (!req.user.maintainer) {
return res.status(401).send("Unauthorized to delete photos.");
}
return res.status(400).send("Invalid request");
});
function getFacesForPhoto(id) {
/* Get the set of faces in this photo */
return photoDB.sequelize.query(
"SELECT * FROM faces WHERE photoId=:id AND faceConfidence>0.9", {
replacements: {
id: id,
},
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then((faces) => {
/* For each face in the photo, get the related faces */
return photoDB.sequelize.query(
"SELECT relatedFaces.photoId AS photoId,fd.face1Id,fd.face2Id,fd.distance,relatedFaces.faceConfidence " +
"FROM (SELECT id,photoId,faceConfidence FROM faces WHERE faces.faceConfidence>=0.9 AND faces.id IN (:ids)) AS faces " +
"INNER JOIN faces AS relatedFaces ON relatedFaces.faceConfidence>=0.9 AND relatedFaces.id IN (fd.face1Id,fd.face2Id) " +
"INNER JOIN facedistances AS fd ON fd.distance<=0.5 " +
" AND (fd.face1Id=faces.id OR fd.face2Id=faces.id) " +
"WHERE (faces.id=fd.face1Id OR faces.id=fd.face2Id) " +
"ORDER BY fd.distance ASC", {
replacements: {
ids: faces.map(face => face.id),
},
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then((relatedFaces) => {
faces.forEach((face) => {
face.relatedFaces = relatedFaces.filter((related) => {
return (related.photoId != id && (related.face1Id == face.id || related.face2Id == face.id));
}).map((related) => {
return {
distance: related.distance,
faceConfidence: related.faceConfidence,
photoId: related.photoId,
faceId: related.face1Id != face.id ? related.face1Id : related.face2Id
}
});
});
return faces;
});
});
}
router.get("/:id?", (req, res) => {
let id;
if (req.params.id) {
id = parseInt(req.params.id);
if (id != req.params.id) {
return res.status(400).send({ message: "Usage /[id]"});
}
}
return photoDB.sequelize.query("SELECT COUNT(id) AS count FROM faces WHERE faceConfidence>=0.9 AND identityId IS NULL", {
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then((results) => {
const random = Math.floor(Math.random() * results[0].count);
return photoDB.sequelize.query(
"SELECT * FROM faces WHERE faceConfidence>=0.9 AND identityId IS NULL ORDER BY id LIMIT :index,1", {
replacements: {
index: random
},
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
});
}).then((faces) => {
return photoDB.sequelize.query(
"SELECT relatedFaces.photoId AS photoId,fd.face1Id,fd.face2Id,fd.distance,relatedFaces.faceConfidence " +
"FROM (SELECT id,photoId,faceConfidence FROM faces WHERE faces.id IN (:ids)) AS faces " +
"INNER JOIN faces AS relatedFaces ON relatedFaces.faceConfidence>=0.9 AND relatedFaces.id IN (fd.face1Id,fd.face2Id) " +
"INNER JOIN facedistances AS fd ON fd.distance<=0.5 " +
" AND (fd.face1Id=faces.id OR fd.face2Id=faces.id) " +
"WHERE (faces.id=fd.face1Id OR faces.id=fd.face2Id) " +
"ORDER BY fd.distance ASC",{
replacements: {
ids: faces.map(face => face.id)
},
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then((relatedFaces) => {
faces.forEach((face) => {
face.relatedFaces = relatedFaces.filter((related) => {
return (related.photoId != id && (related.face1Id == face.id || related.face2Id == face.id));
}).map((related) => {
return {
distance: related.distance,
faceConfidence: related.faceConfidence,
photoId: related.photoId,
faceId: related.face1Id != face.id ? related.face1Id : related.face2Id
}
});
});
return faces;
});
}).then((results) => {
return res.status(200).json(results);
}).catch((error) => {
console.error(error);
return res.status(500).send("Error processing request.");
});
});
module.exports = router;

View File

@ -0,0 +1,43 @@
"use strict";
const express = require("express");
let photoDB;
require("../db/photos").then(function(db) {
photoDB = db;
});
const router = express.Router();
router.post("/", (req, res) => {
console.log(req.body);
return res.status(200).json({});
});
router.get("/:id?", (req, res) => {
let id;
if (req.params.id) {
id = parseInt(req.params.id);
if (id != req.params.id) {
return res.status(400).send({ message: "Usage /[id]"});
}
}
const filter = id ? "WHERE id=:id" : "";
return photoDB.sequelize.query(`SELECT * FROM identities ${filter}`, {
replacements: {
id: id
},
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then((results) => {
return res.status(200).json(results);
}).catch((error) => {
console.error(error);
return res.status(500).send("Error processing request.");
});
});
module.exports = router;