Improved face dropping from photopanel
Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
parent
f6685e78e1
commit
e5a55de73c
@ -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%;
|
||||
|
@ -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
52
server/db/MODIFY.md
Normal 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
|
||||
```
|
@ -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,
|
||||
|
@ -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, {
|
||||
|
Loading…
x
Reference in New Issue
Block a user