Started identities editor.
Signed-off-by: James Ketrenos <james_gitlab@ketrenos.com>
This commit is contained in:
parent
f0ae806fbb
commit
b7ed9f05f1
278
frontend/identities.html
Normal file
278
frontend/identities.html
Normal 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>
|
@ -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
134
server/routes/faces.js
Normal 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;
|
43
server/routes/identities.js
Normal file
43
server/routes/identities.js
Normal 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;
|
Loading…
x
Reference in New Issue
Block a user