From bb27b3261ada36a9ac6474da9e63343ed7e6d4c6 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Tue, 18 Oct 2022 14:08:27 -0700 Subject: [PATCH] Mark person in preview (fix #79) --- lib/Controller/ApiController.php | 5 ++ lib/Db/TimelineQueryDays.php | 6 ++ lib/Db/TimelineQueryFaces.php | 50 ++++++++++++----- src/components/Timeline.vue | 16 ++++-- src/components/frame/Photo.vue | 61 +++++++++++++++++++-- src/components/top-matter/FaceTopMatter.vue | 17 +++++- src/mixins/UserConfig.ts | 2 + src/types.ts | 13 +++++ 8 files changed, 145 insertions(+), 25 deletions(-) diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index 252c0773e..d418c1044 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -99,6 +99,11 @@ private function getTransformations() { if ($face) { $transforms[] = array($this->timelineQuery, 'transformFaceFilter', $face); } + + $faceRect = $this->request->getParam('facerect'); + if ($faceRect) { + $transforms[] = array($this->timelineQuery, 'transformFaceRect', $face); + } } // Filter only for one tag diff --git a/lib/Db/TimelineQueryDays.php b/lib/Db/TimelineQueryDays.php index 7b02e1e44..c758a9f5d 100644 --- a/lib/Db/TimelineQueryDays.php +++ b/lib/Db/TimelineQueryDays.php @@ -18,6 +18,9 @@ private function processDays(&$days) { foreach($days as &$row) { $row["dayid"] = intval($row["dayid"]); $row["count"] = intval($row["count"]); + + // All transform processing + $this->processFace($row, true); } return $days; } @@ -44,6 +47,9 @@ private function processDay(&$day) { $row["isfavorite"] = 1; } unset($row["categoryid"]); + + // All transform processing + $this->processFace($row); } return $day; } diff --git a/lib/Db/TimelineQueryFaces.php b/lib/Db/TimelineQueryFaces.php index 3683b2084..7c0ec2113 100644 --- a/lib/Db/TimelineQueryFaces.php +++ b/lib/Db/TimelineQueryFaces.php @@ -17,25 +17,49 @@ public function transformFaceFilter(IQueryBuilder &$query, string $userId, strin $faceUid = $faceNames[0]; $faceName = $faceNames[1]; - // Get cluster ID - $sq = $query->getConnection()->getQueryBuilder(); - $idQuery = $sq->select('id')->from('recognize_face_clusters') - ->where($query->expr()->eq('user_id', $sq->createNamedParameter($faceUid))); - - // If name is a number then it is an ID - $nameField = is_numeric($faceName) ? 'id' : 'title'; - $idQuery->andWhere($query->expr()->eq($nameField, $sq->createNamedParameter($faceName))); - - $id = $idQuery->executeQuery()->fetchOne(); - if (!$id) throw new \Exception("Unknown person: $faceStr"); - // Join with cluster + $nameField = is_numeric($faceName) ? 'rfc.id' : 'rfc.title'; + $query->innerJoin('m', 'recognize_face_clusters', 'rfc', $query->expr()->andX( + $query->expr()->eq('user_id', $query->createNamedParameter($faceUid)), + $query->expr()->eq($nameField, $query->createNamedParameter($faceName)), + )); + + // Join with detections $query->innerJoin('m', 'recognize_face_detections', 'rfd', $query->expr()->andX( $query->expr()->eq('rfd.file_id', 'm.fileid'), - $query->expr()->eq('rfd.cluster_id', $query->createNamedParameter($id)), + $query->expr()->eq('rfd.cluster_id', 'rfc.id'), )); } + public function transformFaceRect(IQueryBuilder &$query, string $userId) { + // Include detection params in response + $query->addSelect( + 'rfd.width AS face_w', + 'rfd.height AS face_h', + 'rfd.x AS face_x', + 'rfd.y AS face_y', + ); + } + + /** Convert face fields to object */ + private function processFace(&$row, $days=false) { + if (!isset($row) || !isset($row['face_w'])) return; + + if (!$days) { + $row["facerect"] = [ + "w" => floatval($row["face_w"]), + "h" => floatval($row["face_h"]), + "x" => floatval($row["face_x"]), + "y" => floatval($row["face_y"]), + ]; + } + + unset($row["face_w"]); + unset($row["face_h"]); + unset($row["face_x"]); + unset($row["face_y"]); + } + public function getFaces(Folder $folder) { $query = $this->connection->getQueryBuilder(); diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 5a0e5037f..e62bd64ae 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -106,6 +106,7 @@ import { generateUrl } from '@nextcloud/router' import { showError } from '@nextcloud/dialogs' import { NcEmptyContent } from '@nextcloud/vue'; import GlobalMixin from '../mixins/GlobalMixin'; +import UserConfig from "../mixins/UserConfig"; import { ViewerManager } from "../services/Viewer"; import { getLayout } from "../services/Layout"; @@ -119,7 +120,6 @@ import TopMatter from "./top-matter/TopMatter.vue"; import OnThisDay from "./top-matter/OnThisDay.vue"; import SelectionManager from './SelectionManager.vue'; import ScrollerManager from './ScrollerManager.vue'; -import UserConfig from "../mixins/UserConfig"; import ArchiveIcon from 'vue-material-design-icons/PackageDown.vue'; import CheckCircle from 'vue-material-design-icons/CheckCircle.vue'; @@ -436,6 +436,11 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { // People if (this.$route.name === 'people' && this.$route.params.user && this.$route.params.name) { query.set('face', `${this.$route.params.user}/${this.$route.params.name}`); + + // Face rect + if (this.config_showFaceRect) { + query.set('facerect', '1'); + } } // Tags @@ -492,12 +497,11 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { /** Fetch timeline main call */ async fetchDays(noCache=false) { - let url = '/apps/memories/api/days'; let params: any = {}; + let url = generateUrl(this.appendQuery('/apps/memories/api/days'), params); // Try cache first let cache: IDay[]; - const cacheUrl = window.location.pathname + 'api/days'; // Make sure to refresh scroll later this.currentEnd = -1; @@ -515,18 +519,18 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) { data = await dav.getPeopleData(); } else { // Try the cache - cache = noCache ? null : (await utils.getCachedData(cacheUrl)); + cache = noCache ? null : (await utils.getCachedData(url)); if (cache) { await this.processDays(cache); this.loading--; } // Get from network - data = (await axios.get(generateUrl(this.appendQuery(url), params))).data; + data = (await axios.get(url)).data; } // Put back into cache - utils.cacheData(cacheUrl, data); + utils.cacheData(url, data); // Make sure we're still on the same page if (this.state !== startState) return; diff --git a/src/components/frame/Photo.vue b/src/components/frame/Photo.vue index 276607891..2d076cf42 100644 --- a/src/components/frame/Photo.vue +++ b/src/components/frame/Photo.vue @@ -22,16 +22,18 @@ @touchend="touchend" @touchcancel="touchend" > diff --git a/src/mixins/UserConfig.ts b/src/mixins/UserConfig.ts index 81ab3a94f..236fe8899 100644 --- a/src/mixins/UserConfig.ts +++ b/src/mixins/UserConfig.ts @@ -34,7 +34,9 @@ export default class UserConfig extends Vue { config_showHidden = loadState('memories', 'showHidden') === "true"; config_tagsEnabled = loadState('memories', 'systemtags'); config_recognizeEnabled = loadState('memories', 'recognize'); + config_squareThumbs = localStorage.getItem('memories_squareThumbs') === '1'; + config_showFaceRect = localStorage.getItem('memories_showFaceRect') === '1'; created() { subscribe(eventName, this.updateLocalSetting) diff --git a/src/types.ts b/src/types.ts index 8150f3a89..fe2b5ee68 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,6 +39,7 @@ export type IPhoto = { w?: number; /** Height of full image */ h?: number; + /** Grid display width percentage */ dispWp?: number; /** Grid display height (forced) */ @@ -49,8 +50,13 @@ export type IPhoto = { dispY?: number; /** Grid display row id (relative to head) */ dispRowNum?: number; + /** Reference to day object */ d?: IDay; + + /** Face dimensions */ + facerect?: IFaceRect; + /** Video flag from server */ isvideo?: boolean; /** Favorite flag from server */ @@ -85,6 +91,13 @@ export interface ITag extends IPhoto { previews?: IPhoto[]; } +export interface IFaceRect { + w: number; + h: number; + x: number; + y: number; +} + export type IRow = { /** Vue Recycler identifier */ id?: string;