metadata: show faces

Signed-off-by: Varun Patil <radialapps@gmail.com>
pull/767/head
Varun Patil 2023-08-02 18:52:29 -07:00
parent c56911709c
commit af226344b8
6 changed files with 77 additions and 19 deletions

View File

@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
- **Feature**: Allow adding photos to multiple albums together ([#752](https://github.com/pulsejet/memories/pull/752)) - **Feature**: Allow adding photos to multiple albums together ([#752](https://github.com/pulsejet/memories/pull/752))
- **Feature**: Show albums of photo in metadata ([#752](https://github.com/pulsejet/memories/pull/752)) - **Feature**: Show albums of photo in metadata ([#752](https://github.com/pulsejet/memories/pull/752))
- **Feature**: Show faces in photo in sidebar metadata
## [v5.2.1] - 2023-07-03 ## [v5.2.1] - 2023-07-03

View File

@ -131,6 +131,10 @@ class RecognizeBackend extends Backend
public function getClusters(): array public function getClusters(): array
{ {
/** @var \OCP\IRequest $request */
$request = \OC::$server->get(\OCP\IRequest::class);
$fileid = (int) $request->getParam('fileid', -1);
$query = $this->tq->getBuilder(); $query = $this->tq->getBuilder();
// SELECT all face clusters // SELECT all face clusters
@ -149,6 +153,20 @@ class RecognizeBackend extends Backend
// WHERE this cluster belongs to the user // WHERE this cluster belongs to the user
$query->where($query->expr()->eq('rfc.user_id', $query->createNamedParameter(Util::getUID()))); $query->where($query->expr()->eq('rfc.user_id', $query->createNamedParameter(Util::getUID())));
// WHERE these clusters contain fileid if specified
if ($fileid > 0) {
$fSq = $this->tq->getBuilder()
->select('rfd.file_id')
->from('recognize_face_detections', 'rfd')
->where($query->expr()->andX(
$query->expr()->eq('rfd.cluster_id', 'rfc.id'),
$query->expr()->eq('rfd.file_id', $query->createNamedParameter($fileid, \PDO::PARAM_INT)),
))
->getSQL()
;
$query->andWhere($query->createFunction("EXISTS ({$fSq})"));
}
// GROUP by ID of face cluster // GROUP by ID of face cluster
$query->groupBy('rfc.id'); $query->groupBy('rfc.id');

View File

@ -1,5 +1,12 @@
<template> <template>
<div class="outer" v-if="fileid"> <div class="outer" v-if="fileid">
<div v-if="people.length" class="people">
<div class="section-title">{{ t('memories', 'People') }}</div>
<div class="container" v-for="face of people" :key="face.cluster_id">
<Cluster :data="face" :counters="false"> </Cluster>
</div>
</div>
<div v-if="albums.length"> <div v-if="albums.length">
<div class="section-title">{{ t('memories', 'Albums') }}</div> <div class="section-title">{{ t('memories', 'Albums') }}</div>
<AlbumsList class="albums" :albums="albums" /> <AlbumsList class="albums" :albums="albums" />
@ -64,6 +71,7 @@ import { DateTime } from 'luxon';
import UserConfig from '../mixins/UserConfig'; import UserConfig from '../mixins/UserConfig';
import AlbumsList from './modal/AlbumsList.vue'; import AlbumsList from './modal/AlbumsList.vue';
import Cluster from './frame/Cluster.vue';
import EditIcon from 'vue-material-design-icons/Pencil.vue'; import EditIcon from 'vue-material-design-icons/Pencil.vue';
import CalendarIcon from 'vue-material-design-icons/Calendar.vue'; import CalendarIcon from 'vue-material-design-icons/Calendar.vue';
@ -77,7 +85,7 @@ import * as utils from '../services/Utils';
import * as dav from '../services/DavRequests'; import * as dav from '../services/DavRequests';
import { API } from '../services/API'; import { API } from '../services/API';
import type { IAlbum, IImageInfo, IPhoto } from '../types'; import type { IAlbum, IFace, IImageInfo, IPhoto } from '../types';
interface TopField { interface TopField {
title: string; title: string;
@ -92,18 +100,24 @@ export default defineComponent({
components: { components: {
NcActions, NcActions,
NcActionButton, NcActionButton,
EditIcon,
AlbumsList, AlbumsList,
Cluster,
EditIcon,
}, },
mixins: [UserConfig], mixins: [UserConfig],
data: () => ({ data: () => ({
// Basic info and metadata
fileid: null as number | null, fileid: null as number | null,
filename: '', filename: '',
exif: {} as { [prop: string]: any }, exif: {} as { [prop: string]: any },
baseInfo: {} as IImageInfo, baseInfo: {} as IImageInfo,
// Cluster lists
albums: [] as IAlbum[], albums: [] as IAlbum[],
people: [] as IFace[],
state: 0, state: 0,
}), }),
@ -342,6 +356,7 @@ export default defineComponent({
this.fileid = null; this.fileid = null;
this.exif = {}; this.exif = {};
this.albums = []; this.albums = [];
this.people = [];
const state = this.state; const state = this.state;
const url = API.Q(utils.getImageInfoUrl(photo), { tags: 1 }); const url = API.Q(utils.getImageInfoUrl(photo), { tags: 1 });
@ -355,24 +370,26 @@ export default defineComponent({
// trigger other refreshes // trigger other refreshes
this.refreshAlbums(); this.refreshAlbums();
this.refreshPeople();
return this.baseInfo; return this.baseInfo;
}, },
async refreshAlbums(): Promise<IAlbum[]> { async refreshAlbums(): Promise<void> {
if (!this.config.albums_enabled) return []; if (!this.config.albums_enabled) return;
this.albums = await this.guardState(dav.getAlbums(1, this.fileid!));
},
async refreshPeople(): Promise<void> {
if (!this.config.recognize_enabled) return;
this.people = await this.guardState(dav.getFaceList('recognize', this.fileid!));
},
async guardState<T>(promise: Promise<T>): Promise<T> {
const state = this.state; const state = this.state;
const res = await promise;
let list: IAlbum[] = []; if (state === this.state) return res;
try { throw new Error('state changed');
list = await dav.getAlbums(1, this.fileid!);
} catch (e) {
console.error('metadata: failed to load albums', e);
}
if (state !== this.state) return list;
return (this.albums = list);
}, },
handleFileUpdated({ fileid }: { fileid: number }) { handleFileUpdated({ fileid }: { fileid: number }) {
@ -397,6 +414,19 @@ export default defineComponent({
} }
} }
.people {
> .section-title {
margin-bottom: 4px;
}
> .container {
width: calc(100% / 3);
aspect-ratio: 1;
position: relative;
display: inline-block;
font-size: 0.8em;
}
}
.top-field { .top-field {
margin: 10px; margin: 10px;
margin-bottom: 25px; margin-bottom: 25px;

View File

@ -1,6 +1,6 @@
<template> <template>
<router-link draggable="false" class="cluster fill-block" :class="{ error }" :to="target" @click.native="click"> <router-link draggable="false" class="cluster fill-block" :class="{ error }" :to="target" @click.native="click">
<div class="count-bubble" v-if="data.count"> <div class="count-bubble" v-if="counters && data.count">
<NcCounterBubble> {{ data.count }} </NcCounterBubble> <NcCounterBubble> {{ data.count }} </NcCounterBubble>
</div> </div>
<div class="name"> <div class="name">
@ -55,6 +55,10 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
counters: {
type: Boolean,
default: true,
},
}, },
computed: { computed: {

View File

@ -106,8 +106,8 @@ export class API {
return gen(`${BASE}/tags/set/{fileid}`, { fileid }); return gen(`${BASE}/tags/set/{fileid}`, { fileid });
} }
static FACE_LIST(app: 'recognize' | 'facerecognition') { static FACE_LIST(app: 'recognize' | 'facerecognition', fileid?: number) {
return gen(`${BASE}/clusters/${app}`); return API.Q(gen(`${BASE}/clusters/${app}`), { fileid });
} }
static CLUSTER_PREVIEW(backend: ClusterTypes, name: string | number) { static CLUSTER_PREVIEW(backend: ClusterTypes, name: string | number) {

View File

@ -8,8 +8,13 @@ import client from '../DavClient';
import * as utils from '../Utils'; import * as utils from '../Utils';
import * as base from './base'; import * as base from './base';
export async function getFaceList(app: 'recognize' | 'facerecognition') { /**
return (await axios.get<IFace[]>(API.FACE_LIST(app))).data; * Get list of faces
* @param app Backend app to use
* @param fileid File to filter by (optional)
*/
export async function getFaceList(app: 'recognize' | 'facerecognition', fileid?: number) {
return (await axios.get<IFace[]>(API.FACE_LIST(app, fileid))).data;
} }
/** /**