Show face boxes
Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
parent
a9549d29a9
commit
5d38cb4787
@ -56,6 +56,27 @@ div {
|
|||||||
border: 0.25rem solid transparent;
|
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 {
|
.Face:hover .Image {
|
||||||
border: 0.25rem solid yellow;
|
border: 0.25rem solid yellow;
|
||||||
}
|
}
|
||||||
@ -80,9 +101,6 @@ div {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 8rem;
|
width: 8rem;
|
||||||
height: 8rem;
|
height: 8rem;
|
||||||
background-size: contain !important;
|
|
||||||
background-repeat: no-repeat no-repeat !important;;
|
|
||||||
background-position: 50% 50% !important;;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.Cluster {
|
.Cluster {
|
||||||
|
@ -1,11 +1,91 @@
|
|||||||
|
|
||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect, useRef } from 'react';
|
||||||
import { useApi } from './useApi';
|
import { useApi } from './useApi';
|
||||||
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
||||||
import './App.css';
|
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) => (
|
||||||
|
<div className="FaceBox"
|
||||||
|
key={face.faceId}
|
||||||
|
style={{
|
||||||
|
left: offsetLeft + Math.floor(face.left * width) + "px",
|
||||||
|
top: offsetTop + Math.floor(face.top * height) + "px",
|
||||||
|
width: Math.floor((face.right - face.left) * width) + "px",
|
||||||
|
height: Math.floor((face.bottom - face.top) * height) + "px"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
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 Photo = ({ photoId }: any) => {
|
||||||
const [image, setImage] = useState<any>(undefined);
|
const [image, setImage] = useState<any>(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(() => {
|
useEffect(() => {
|
||||||
if (photoId === 0) {
|
if (photoId === 0) {
|
||||||
@ -14,8 +94,8 @@ const Photo = ({ photoId }: any) => {
|
|||||||
const fetchImageData = async (image: number) => {
|
const fetchImageData = async (image: number) => {
|
||||||
console.log(`Loading photo ${image}`);
|
console.log(`Loading photo ${image}`);
|
||||||
const res = await window.fetch(`../api/v1/photos/${image}`);
|
const res = await window.fetch(`../api/v1/photos/${image}`);
|
||||||
const data = await res.json();
|
const photo = await res.json();
|
||||||
setImage(data[0]);
|
setImage(photo);
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchImageData(photoId);
|
fetchImageData(photoId);
|
||||||
@ -26,9 +106,11 @@ const Photo = ({ photoId }: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (<div className="Image"
|
return (<div className="Image"
|
||||||
|
ref={ref}
|
||||||
style={{
|
style={{
|
||||||
background: `url(${image.path}thumbs/scaled/${image.filename})`
|
background: `url(../${image.path}thumbs/scaled/${image.filename})`
|
||||||
}}/>);
|
}}>{ faces }</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Face = ({ faceId, onClick, title }: any) => {
|
const Face = ({ faceId, onClick, title }: any) => {
|
||||||
@ -181,7 +263,11 @@ type FaceData = {
|
|||||||
displayName: string,
|
displayName: string,
|
||||||
identityId: number,
|
identityId: number,
|
||||||
distance: number,
|
distance: number,
|
||||||
descriptors: any[]
|
descriptors: any[],
|
||||||
|
top: number
|
||||||
|
right: number,
|
||||||
|
bottom: number,
|
||||||
|
left: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Identity = {
|
type Identity = {
|
||||||
|
@ -1084,24 +1084,62 @@ console.log("Trying path as: " + path);
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get("/:id", async (req, res) => {
|
router.get("/:id", async (req, res) => {
|
||||||
|
console.log(`GET ${req.url}`);
|
||||||
|
|
||||||
const id = parseInt(req.params.id);
|
const id = parseInt(req.params.id);
|
||||||
try {
|
try {
|
||||||
const results = await photoDB.sequelize.query(
|
let results;
|
||||||
|
|
||||||
|
results = await photoDB.sequelize.query(
|
||||||
`
|
`
|
||||||
SELECT photos.*,albums.path AS path,
|
SELECT photos.*,albums.path AS path
|
||||||
faces.identityId,faces.top,faces.left,faces.right,faces.bottom
|
|
||||||
FROM photos
|
FROM photos
|
||||||
INNER JOIN albums ON albums.id=photos.albumId
|
INNER JOIN albums ON albums.id=photos.albumId
|
||||||
INNER JOIN faces ON faces.photoId=photos.id
|
|
||||||
WHERE photos.id=:id
|
WHERE photos.id=:id
|
||||||
`, {
|
`, {
|
||||||
replacements: { id }
|
replacements: { id },
|
||||||
|
type: photoDB.Sequelize.QueryTypes.SELECT
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
return res.status(404);
|
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) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return res.status(404).json({message: `Error connecting to DB for ${id}.`})
|
return res.status(404).json({message: `Error connecting to DB for ${id}.`})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user