Improved face dropping from photopanel

Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
James Ketr 2023-01-28 20:19:52 -08:00
parent f6685e78e1
commit e5a55de73c
5 changed files with 191 additions and 26 deletions

View File

@ -44,6 +44,7 @@ div {
width: 100%;
}
.PhotoFaces,
.Identities {
display: grid;
user-select: none;
@ -52,6 +53,13 @@ div {
width: 100%;
max-height: 100%; /* scroll if too large */
gap: 0.25rem;
}
.PhotoFaces {
grid-template-columns: repeat(auto-fill, minmax(6.25rem, auto));
}
.Identities {
grid-template-columns: repeat(auto-fill, minmax(4.25rem, auto));
}
@ -77,12 +85,14 @@ div {
display: flex;
flex-direction: column;
position: relative;
height: 100%;
}
.PhotoPanel {
display: flex;
flex-direction: column;
justify-content: center;
max-height: 100%;
}
.Guess {
@ -96,17 +106,28 @@ button {
min-width: 4rem;
}
.Image .FaceBox {
.PhotoInfo,
.FaceInfo {
display: flex;
align-content: center;
align-items: center;
align-self: center;
text-align: center;
width: 100%;
flex-grow: 1;
}
.Photo .FaceBox {
border: 1px solid red;
position: absolute;
}
.Image .FaceBox:hover {
.Photo .FaceBox:hover {
background-color: rgba(255, 255, 255, 0.2);
box-shadow: 0px 0px 10px black;
}
.Image {
.Photo {
display: flex;
position: relative;
}
@ -124,6 +145,7 @@ button {
margin-top: 0.25rem;
}
.PhotoFaces .UnknownFace,
.Identities .UnknownFace {
font-size: 2rem;
}
@ -174,6 +196,11 @@ button {
height: 10rem;
}
.PhotoFaces .Face .Image {
min-width: 6rem;
min-height: 6rem;
}
.Identities .Face .Image {
min-width: 4rem;
min-height: 4rem;
@ -210,7 +237,7 @@ button {
}
.Viewer .PhotoPanel img {
.Photo img {
object-fit: contain;
max-width: 100%;
max-height: 100%;

View File

@ -62,10 +62,27 @@ const makeFaceBoxes = (photo: any,
});
};
type PhotoData = {
photoId: number,
faces: FaceData[],
filename: string,
path: string,
taken: number
};
const EmptyPhoto = {
photoId: -1,
faces: [],
filename: '',
path: '',
taken: Date.now()
};
const Photo = ({ photoId, onFaceClick }: any) => {
const [image, setImage] = useState<any>(undefined);
const [photo, setPhoto] = useState<PhotoData>(EmptyPhoto);
const [faceInfo, setFaceInfo] = useState<string>('');
const ref = useRef(null);
const [photoSelected, setPhotoSelected] = useState<FaceData[]>([]);
const [dimensions, setDimensions] = React.useState({width: 0, height: 0});
const onFaceEnter = (e: any, face: FaceData) => {
@ -79,12 +96,12 @@ const Photo = ({ photoId, onFaceClick }: any) => {
}
const faces = useMemo(() => {
if (image === undefined || dimensions.height === 0) {
if (photo === undefined || dimensions.height === 0) {
return <></>;
}
return makeFaceBoxes(image, dimensions,
return makeFaceBoxes(photo, dimensions,
onFaceClick, onFaceEnter, onFaceLeave);
}, [image, dimensions, onFaceClick]);
}, [photo, dimensions, onFaceClick]);
const checkResize = useCallback(() => {
if (!ref.current) {
@ -111,35 +128,104 @@ const Photo = ({ photoId, onFaceClick }: any) => {
if (photoId === 0) {
return;
}
const fetchImageData = async (image: number) => {
console.log(`Loading photo ${image}`);
const res = await window.fetch(`${base}/api/v1/photos/${image}`);
const fetchPhotoData = async (photoId: number) => {
console.log(`Loading photo ${photoId}`);
const res = await window.fetch(`${base}/api/v1/photos/${photoId}`);
const photo = await res.json();
setImage(photo);
setPhoto(photo);
};
fetchImageData(photoId);
}, [photoId, setImage]);
setPhotoSelected([]);
fetchPhotoData(photoId);
}, [photoId, setPhoto, setPhotoSelected]);
if (image === undefined) {
const forget = async () => {
try {
const res = await window.fetch(
`${base}/api/v1/faces`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'forget',
faces: photoSelected.map(face => face.faceId)
})
});
await res.json();
setPhotoSelected([]);
setPhoto({...photo});
} catch (error) {
console.error(error);
}
};
const selectAll = async () => {
setPhotoSelected([...photo.faces]);
};
const clearSelection = async () => {
setPhotoSelected([]);
};
const faceClick = useCallback((e: any, face: FaceData) => {
const el = e.currentTarget;
/* Control -- select / deselect single item */
if (e.ctrlKey) {
const index = photoSelected.findIndex(x => x.faceId === face.faceId);
if (index !== -1) {
el.classList.remove('Selected');
photoSelected.splice(index, 1);
} else {
el.classList.add('Selected');
photoSelected.push(face);
}
setPhotoSelected([...photoSelected]);
return;
}
/* Shift -- select groups */
if (e.shiftKey) {
return;
}
/* Default to load image */
e.stopPropagation();
e.preventDefault();
}, [photoSelected, setPhotoSelected]);
if (photo.photoId === -1) {
return <></>
}
return (<div className="PhotoPanel">
<div className="Image" ref={ref}>
<div className="Photo" ref={ref}>
<img
alt={image.filename}
src={`${base}/../${image.path}thumbs/scaled/${image.filename}`.replace(/ /g, '%20')}/>
alt={photo.filename}
src={`${base}/../${photo.path}thumbs/scaled/${photo.filename}`.replace(/ /g, '%20')}/>
{ faces }
</div>
<div className="ImageInfo">{
moment(image.taken)
.format('MMMM Do YYYY, h:mm:ss a')
<div className="PhotoInfo">{
moment(photo.taken).format('MMMM Do YYYY, h:mm:ss a')
}, {
moment(image.taken)
.fromNow()
moment(photo.taken).fromNow()
}.</div>
<div className="FaceInfo">{ faceInfo ? faceInfo : 'Hover over face for information.'}</div>
<div className="FaceInfo">
{ faceInfo ? faceInfo : 'Hover over face for information.'}
</div>
<div className="Actions">
{ photoSelected.length !== 0 && <Button onClick={forget}>
Forget Selected</Button> }
<Button onClick={selectAll}>Select All</Button>
<Button onClick={clearSelection}>Select None</Button>
</div>
{ photoSelected.length !== 0 && <div>Selected faces (CTRL-CLICK to remove):</div> }
<div className="PhotoFaces">
{ photoSelected.map(face =>
<Face
key={face.faceId}
face={face}
onFaceClick={(e: any) => faceClick(e, face)}/>
) }
</div>
</div>
);
};

52
server/db/MODIFY.md Normal file
View File

@ -0,0 +1,52 @@
# Dump schema
```bash
cat << EOF | sqlite3 db/photos.db > photos.schema
.schema
EOF
```
# Edit schema
```bash
nano photos.schema
```
# Backup database
```bash
cp db/photos.db photos.db.bk
```
# For the table you want to modify
## Backup the table
```bash
cat << EOF | sqlite3 db/photos.db
.output faces.dump
.dump faces
.quit
EOF
```
## Drop the table
```bash
cat << EOF | sqlite3 db/photos.db
drop table faces
EOF
```
## Create the table with the modified schema
```bash
cat photos.schema | sqlite3 db/photos.db
```
## Re-populate the table
```bash
cat faces.dump | sqlite3 db/photos.db
```

View File

@ -38,7 +38,7 @@ router.put("/:id?", async (req, res/*, next*/) => {
console.log(`${action}: ${faces}`);
if ([ 'not-a-face', 'forget' ].indexOf(action) !== -1) {
await photoDB.sequelize.query(
`UPDATE faces SET classifiedBy=':action',identityId=NULL ` +
`UPDATE faces SET classifiedBy=:action,identityId=NULL ` +
`WHERE id IN (:faces)`, {
replacements: {
action,

View File

@ -533,7 +533,7 @@ const getUnknownIdentity = async (faceCount) => {
WHERE
faces.identityId IS NULL
AND faces.classifiedBy != 'not-a-face'
AND total.classifiedBy != 'forget'
AND faces.classifiedBy != 'forget'
${ limit }`;
unknownIdentity.relatedFaces = await photoDB.sequelize.query(sql, {