From 5d38cb47878950a171750f4140fcf6be2cd180ad Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Tue, 17 Jan 2023 15:48:24 -0800 Subject: [PATCH] Show face boxes Signed-off-by: James Ketrenos --- client/src/App.css | 24 ++++++++-- client/src/App.tsx | 98 ++++++++++++++++++++++++++++++++++++++--- server/routes/photos.js | 50 ++++++++++++++++++--- 3 files changed, 157 insertions(+), 15 deletions(-) diff --git a/client/src/App.css b/client/src/App.css index 64471b1..e238a16 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -56,6 +56,27 @@ div { border: 0.25rem solid transparent; } +.Image .FaceBox { + border: 1px solid red; + border-radius: 0.25rem; + position: absolute; +} + +.Image .FaceBox:hover { + background-color: rgba(255, 255, 255, 0.2); + box-shadow: 0px 0px 5px black; +} + +.Image { + display: flex; + width: 100%; + height: 100%; + position: relative; + background-size: contain !important; + background-repeat: no-repeat no-repeat !important; + background-position: 50% 50% !important; +} + .Face:hover .Image { border: 0.25rem solid yellow; } @@ -80,9 +101,6 @@ div { box-sizing: border-box; width: 8rem; height: 8rem; - background-size: contain !important; - background-repeat: no-repeat no-repeat !important;; - background-position: 50% 50% !important;; } .Cluster { diff --git a/client/src/App.tsx b/client/src/App.tsx index 1f2abf1..390a3e0 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,11 +1,91 @@ -import React, { useState, useMemo, useEffect } from 'react'; +import React, { useState, useMemo, useEffect, useRef } from 'react'; import { useApi } from './useApi'; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import './App.css'; +const makeFaceBoxes = (photo: any, dimensions: any): any => { + const faces: FaceData[] = photo.faces; + + let width: number, height: number, offsetLeft = 0, offsetTop = 0; + + /* If photo is wider than viewport, it will be 100% width and < 100% height */ + if (photo.width / photo.height > dimensions.width / dimensions.height) { + width = dimensions.width; + height = dimensions.height * photo.height / photo.width * + dimensions.width / dimensions.height; + offsetLeft = 0; + offsetTop = (dimensions.height - height) * 0.5; + } else { + width = dimensions.width * photo.width / photo.height * + dimensions.height / dimensions.width; + height = dimensions.height; + offsetLeft = (dimensions.width - width) * 0.5; + offsetTop = 0; + } + + return faces.map((face: FaceData) => ( +
+ )); +}; + +/* +function debounce(fn: any, ms: number) { + let timer: any; + return () => { + if (timer) clearTimeout(timer); + timer = setTimeout(() => { + timer = null + fn.apply(this as typeof Photo, arguments) + }, ms) + }; +}; +*/ + const Photo = ({ photoId }: any) => { const [image, setImage] = useState(undefined); + const ref = useRef(null); + const [dimensions, setDimensions] = React.useState({ + height: window.innerHeight, + width: window.innerWidth + }) + + const faces = useMemo(() => { + if (image === undefined) { + return <>; + } + return makeFaceBoxes(image, dimensions); + }, [image, dimensions]); + + useEffect(() : any => { + if (!ref || !ref.current) { + return; + } + + const el: Element = ref.current as Element; + + const handleResize = () => { + setDimensions({ + height: el.clientHeight, + width: el.clientWidth + }) + }; + + const debouncedHandleResize = handleResize;//debounce(handleResize, 250); + debouncedHandleResize(); + window.addEventListener('resize', debouncedHandleResize); + return () => { + window.removeEventListener('resize', debouncedHandleResize) + }; + }); useEffect(() => { if (photoId === 0) { @@ -14,8 +94,8 @@ const Photo = ({ photoId }: any) => { const fetchImageData = async (image: number) => { console.log(`Loading photo ${image}`); const res = await window.fetch(`../api/v1/photos/${image}`); - const data = await res.json(); - setImage(data[0]); + const photo = await res.json(); + setImage(photo); }; fetchImageData(photoId); @@ -26,9 +106,11 @@ const Photo = ({ photoId }: any) => { } return (
); + background: `url(../${image.path}thumbs/scaled/${image.filename})` + }}>{ faces }
+ ); }; const Face = ({ faceId, onClick, title }: any) => { @@ -181,7 +263,11 @@ type FaceData = { displayName: string, identityId: number, distance: number, - descriptors: any[] + descriptors: any[], + top: number + right: number, + bottom: number, + left: number, }; type Identity = { diff --git a/server/routes/photos.js b/server/routes/photos.js index 0a9598c..1655e7b 100755 --- a/server/routes/photos.js +++ b/server/routes/photos.js @@ -1084,24 +1084,62 @@ console.log("Trying path as: " + path); }); router.get("/:id", async (req, res) => { + console.log(`GET ${req.url}`); + const id = parseInt(req.params.id); try { - const results = await photoDB.sequelize.query( + let results; + + results = await photoDB.sequelize.query( ` - SELECT photos.*,albums.path AS path, - faces.identityId,faces.top,faces.left,faces.right,faces.bottom + SELECT photos.*,albums.path AS path FROM photos INNER JOIN albums ON albums.id=photos.albumId - INNER JOIN faces ON faces.photoId=photos.id WHERE photos.id=:id `, { - replacements: { id } + replacements: { id }, + type: photoDB.Sequelize.QueryTypes.SELECT } ); if (results.length === 0) { return res.status(404); } - return res.status(200).json(results[0]); + const photo = results[0]; + results = await photoDB.sequelize.query( + ` + SELECT faces.* FROM faces + WHERE faces.photoId=:id + `, { + replacements: { id }, + type: photoDB.Sequelize.QueryTypes.SELECT + } + ); + photo.faces = results; + /* For each face, look up the Identity and clean up any + * fields we don't want to return vai the rest API */ + await Promise.map(photo.faces, async (face) => { + face.faceId = face.id; + delete face.id; + delete face.descriptorId; + delete face.lastComparedId; + delete face.photoId; + delete face.scanVersion; + if (face.identityId) { + const results = await photoDB.sequelize.query( + ` + SELECT displayName,firstName,lastName,middleName FROM identities + WHERE id=:id + `, { + replacements: { id: face.identityId }, + type: photoDB.Sequelize.QueryTypes.SELECT + } + ); + face.identity = results[0]; + } + delete face.identityId; + }); + + return res.status(200).json(photo); } catch (error) { console.error(error); return res.status(404).json({message: `Error connecting to DB for ${id}.`})