Integration with facerecognition
parent
3f92e5ec6a
commit
aeffe628f2
|
@ -23,7 +23,8 @@ return [
|
||||||
// Routes with params
|
// Routes with params
|
||||||
w(['name' => 'Page#folder', 'url' => '/folders/{path}', 'verb' => 'GET'], 'path'),
|
w(['name' => 'Page#folder', 'url' => '/folders/{path}', 'verb' => 'GET'], 'path'),
|
||||||
w(['name' => 'Page#albums', 'url' => '/albums/{id}', 'verb' => 'GET'], 'id'),
|
w(['name' => 'Page#albums', 'url' => '/albums/{id}', 'verb' => 'GET'], 'id'),
|
||||||
w(['name' => 'Page#people', 'url' => '/people/{name}', 'verb' => 'GET'], 'name'),
|
w(['name' => 'Page#recognize', 'url' => '/recognize/{name}', 'verb' => 'GET'], 'name'),
|
||||||
|
w(['name' => 'Page#facerecognition', 'url' => '/facerecognition/{name}', 'verb' => 'GET'], 'name'),
|
||||||
w(['name' => 'Page#tags', 'url' => '/tags/{name}', 'verb' => 'GET'], 'name'),
|
w(['name' => 'Page#tags', 'url' => '/tags/{name}', 'verb' => 'GET'], 'name'),
|
||||||
|
|
||||||
// Public folder share
|
// Public folder share
|
||||||
|
@ -50,8 +51,10 @@ return [
|
||||||
['name' => 'Tags#tags', 'url' => '/api/tags', 'verb' => 'GET'],
|
['name' => 'Tags#tags', 'url' => '/api/tags', 'verb' => 'GET'],
|
||||||
['name' => 'Tags#preview', 'url' => '/api/tags/preview/{tag}', 'verb' => 'GET'],
|
['name' => 'Tags#preview', 'url' => '/api/tags/preview/{tag}', 'verb' => 'GET'],
|
||||||
|
|
||||||
['name' => 'Faces#faces', 'url' => '/api/faces', 'verb' => 'GET'],
|
['name' => 'People#recognizePeople', 'url' => '/api/recognize/people', 'verb' => 'GET'],
|
||||||
['name' => 'Faces#preview', 'url' => '/api/faces/preview/{id}', 'verb' => 'GET'],
|
['name' => 'People#recognizePeoplePreview', 'url' => '/api/recognize/people/preview/{id}', 'verb' => 'GET'],
|
||||||
|
['name' => 'People#facerecognitionPeople', 'url' => '/api/facerecognition/people', 'verb' => 'GET'],
|
||||||
|
['name' => 'People#facerecognitionPeoplePreview', 'url' => '/api/facerecognition/people/preview/{id}', 'verb' => 'GET'],
|
||||||
|
|
||||||
['name' => 'Archive#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'],
|
['name' => 'Archive#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'],
|
||||||
|
|
||||||
|
|
|
@ -291,6 +291,20 @@ class ApiBase extends Controller
|
||||||
return \OCA\Memories\Util::recognizeIsEnabled($this->appManager);
|
return \OCA\Memories\Util::recognizeIsEnabled($this->appManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if facerecognition is installed and enabled for this user.
|
||||||
|
protected function facerecognitionIsInstalled(): bool
|
||||||
|
{
|
||||||
|
return \OCA\Memories\Util::facerecognitionIsInstalled($this->appManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if facerecognition is enabled for this user.
|
||||||
|
*/
|
||||||
|
protected function facerecognitionIsEnabled(): bool
|
||||||
|
{
|
||||||
|
return \OCA\Memories\Util::facerecognitionIsEnabled($this->config, $this->getUID());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to get one file or null from a fiolder.
|
* Helper to get one file or null from a fiolder.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -198,16 +198,24 @@ class DaysController extends ApiBase
|
||||||
$transforms[] = [$this->timelineQuery, 'transformVideoFilter'];
|
$transforms[] = [$this->timelineQuery, 'transformVideoFilter'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter only for one face
|
// Filter only for one face on Recognize
|
||||||
if ($this->recognizeIsEnabled()) {
|
if (($recognize = $this->request->getParam('recognize')) && $this->recognizeIsEnabled()) {
|
||||||
$face = $this->request->getParam('face');
|
$transforms[] = [$this->timelineQuery, 'transformPeopleRecognitionFilter', $recognize];
|
||||||
if ($face) {
|
|
||||||
$transforms[] = [$this->timelineQuery, 'transformFaceFilter', $face];
|
|
||||||
}
|
|
||||||
|
|
||||||
$faceRect = $this->request->getParam('facerect');
|
$faceRect = $this->request->getParam('facerect');
|
||||||
if ($faceRect && !$aggregateOnly) {
|
if ($faceRect && !$aggregateOnly) {
|
||||||
$transforms[] = [$this->timelineQuery, 'transformFaceRect', $face];
|
$transforms[] = [$this->timelineQuery, 'transformPeopleRecognizeRect', $recognize];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter only for one face on Face Recognition
|
||||||
|
if (($face = $this->request->getParam('facerecognition')) && $this->facerecognitionIsEnabled()) {
|
||||||
|
$currentModel = (int) $this->config->getAppValue('facerecognition', 'model', -1);
|
||||||
|
$transforms[] = [$this->timelineQuery, 'transformPeopleFaceRecognitionFilter', $currentModel, $face];
|
||||||
|
|
||||||
|
$faceRect = $this->request->getParam('facerect');
|
||||||
|
if ($faceRect && !$aggregateOnly) {
|
||||||
|
$transforms[] = [$this->timelineQuery, 'transformPeopleFaceRecognitionRect', $face];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,8 @@ class PageController extends Controller
|
||||||
$this->initialState->provideInitialState('systemtags', true === $this->appManager->isEnabledForUser('systemtags'));
|
$this->initialState->provideInitialState('systemtags', true === $this->appManager->isEnabledForUser('systemtags'));
|
||||||
$this->initialState->provideInitialState('maps', true === $this->appManager->isEnabledForUser('maps'));
|
$this->initialState->provideInitialState('maps', true === $this->appManager->isEnabledForUser('maps'));
|
||||||
$this->initialState->provideInitialState('recognize', \OCA\Memories\Util::recognizeIsEnabled($this->appManager));
|
$this->initialState->provideInitialState('recognize', \OCA\Memories\Util::recognizeIsEnabled($this->appManager));
|
||||||
|
$this->initialState->provideInitialState('facerecognitionInstalled', \OCA\Memories\Util::facerecognitionIsInstalled($this->appManager));
|
||||||
|
$this->initialState->provideInitialState('facerecognitionEnabled', \OCA\Memories\Util::facerecognitionIsEnabled($this->config, $uid));
|
||||||
$this->initialState->provideInitialState('albums', \OCA\Memories\Util::albumsIsEnabled($this->appManager));
|
$this->initialState->provideInitialState('albums', \OCA\Memories\Util::albumsIsEnabled($this->appManager));
|
||||||
|
|
||||||
// App version
|
// App version
|
||||||
|
@ -181,7 +183,17 @@ class PageController extends Controller
|
||||||
*
|
*
|
||||||
* @NoCSRFRequired
|
* @NoCSRFRequired
|
||||||
*/
|
*/
|
||||||
public function people()
|
public function recognize()
|
||||||
|
{
|
||||||
|
return $this->main();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*/
|
||||||
|
public function facerecognition()
|
||||||
{
|
{
|
||||||
return $this->main();
|
return $this->main();
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,14 +29,14 @@ use OCP\AppFramework\Http\DataResponse;
|
||||||
use OCP\AppFramework\Http\JSONResponse;
|
use OCP\AppFramework\Http\JSONResponse;
|
||||||
use OCP\Files\FileInfo;
|
use OCP\Files\FileInfo;
|
||||||
|
|
||||||
class FacesController extends ApiBase
|
class PeopleController extends ApiBase
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @NoAdminRequired
|
* @NoAdminRequired
|
||||||
*
|
*
|
||||||
* Get list of faces with counts of images
|
* Get list of faces with counts of images
|
||||||
*/
|
*/
|
||||||
public function faces(): JSONResponse
|
public function recognizePeople(): JSONResponse
|
||||||
{
|
{
|
||||||
$user = $this->userSession->getUser();
|
$user = $this->userSession->getUser();
|
||||||
if (null === $user) {
|
if (null === $user) {
|
||||||
|
@ -45,7 +45,7 @@ class FacesController extends ApiBase
|
||||||
|
|
||||||
// Check faces enabled for this user
|
// Check faces enabled for this user
|
||||||
if (!$this->recognizeIsEnabled()) {
|
if (!$this->recognizeIsEnabled()) {
|
||||||
return new JSONResponse(['message' => 'Recognize app not enabled or not v3+'], Http::STATUS_PRECONDITION_FAILED);
|
return new JSONResponse(['message' => 'Recognize app not enabled or not v3+.'], Http::STATUS_PRECONDITION_FAILED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this isn't the timeline folder then things aren't going to work
|
// If this isn't the timeline folder then things aren't going to work
|
||||||
|
@ -55,7 +55,7 @@ class FacesController extends ApiBase
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run actual query
|
// Run actual query
|
||||||
$list = $this->timelineQuery->getFaces(
|
$list = $this->timelineQuery->getPeopleRecognize(
|
||||||
$root,
|
$root,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ class FacesController extends ApiBase
|
||||||
*
|
*
|
||||||
* @return DataResponse
|
* @return DataResponse
|
||||||
*/
|
*/
|
||||||
public function preview(string $id): Http\Response
|
public function recognizePeoplePreview(int $id): Http\Response
|
||||||
{
|
{
|
||||||
$user = $this->userSession->getUser();
|
$user = $this->userSession->getUser();
|
||||||
if (null === $user) {
|
if (null === $user) {
|
||||||
|
@ -90,11 +90,115 @@ class FacesController extends ApiBase
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run actual query
|
// Run actual query
|
||||||
$detections = $this->timelineQuery->getFacePreviewDetection($root, (int) $id);
|
$detections = $this->timelineQuery->getPeopleRecognizePreview($root, $id);
|
||||||
|
|
||||||
if (null === $detections || 0 === \count($detections)) {
|
if (null === $detections || 0 === \count($detections)) {
|
||||||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $this->getPreviewResponse($detections, $user, 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*
|
||||||
|
* Get list of faces with counts of images
|
||||||
|
*/
|
||||||
|
public function facerecognitionPeople(): JSONResponse
|
||||||
|
{
|
||||||
|
$user = $this->userSession->getUser();
|
||||||
|
if (null === $user) {
|
||||||
|
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if face recognition is installed and enabled for this user
|
||||||
|
if (!$this->facerecognitionIsInstalled()) {
|
||||||
|
return new DataResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this isn't the timeline folder then things aren't going to work
|
||||||
|
$root = $this->getRequestRoot();
|
||||||
|
if ($root->isEmpty()) {
|
||||||
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user has recognition disabled, just returns an empty response.
|
||||||
|
if (!$this->facerecognitionIsEnabled()) {
|
||||||
|
return new JSONResponse([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run actual query
|
||||||
|
$currentModel = (int) $this->config->getAppValue('facerecognition', 'model', -1);
|
||||||
|
$list = $this->timelineQuery->getPeopleFaceRecognition(
|
||||||
|
$root,
|
||||||
|
$currentModel,
|
||||||
|
);
|
||||||
|
// Just append unnamed clusters to the end.
|
||||||
|
$list = array_merge($list, $this->timelineQuery->getPeopleFaceRecognition(
|
||||||
|
$root,
|
||||||
|
$currentModel,
|
||||||
|
true
|
||||||
|
));
|
||||||
|
|
||||||
|
return new JSONResponse($list, Http::STATUS_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*
|
||||||
|
* Get face preview image cropped with imagick
|
||||||
|
*
|
||||||
|
* @return DataResponse
|
||||||
|
*/
|
||||||
|
public function facerecognitionPeoplePreview(string $id): Http\Response
|
||||||
|
{
|
||||||
|
$user = $this->userSession->getUser();
|
||||||
|
if (null === $user) {
|
||||||
|
return new DataResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if face recognition is installed and enabled for this user
|
||||||
|
if (!$this->facerecognitionIsInstalled()) {
|
||||||
|
return new DataResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get folder to search for
|
||||||
|
$root = $this->getRequestRoot();
|
||||||
|
if ($root->isEmpty()) {
|
||||||
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user has facerecognition disabled, just returns an empty response.
|
||||||
|
if (!$this->facerecognitionIsEnabled()) {
|
||||||
|
return new JSONResponse([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run actual query
|
||||||
|
$currentModel = (int) $this->config->getAppValue('facerecognition', 'model', -1);
|
||||||
|
$detections = $this->timelineQuery->getFaceRecognitionPreview($root, $currentModel, $id);
|
||||||
|
|
||||||
|
if (null === $detections || 0 === \count($detections)) {
|
||||||
|
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getPreviewResponse($detections, $user, 1.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get face preview image cropped with imagick.
|
||||||
|
*
|
||||||
|
* @param array $detections Array of detections to search
|
||||||
|
* @param \OCP\IUser $user User to search for
|
||||||
|
* @param int $padding Padding to add to the face in preview
|
||||||
|
*/
|
||||||
|
private function getPreviewResponse(
|
||||||
|
array $detections,
|
||||||
|
\OCP\IUser $user,
|
||||||
|
float $padding
|
||||||
|
): Http\Response
|
||||||
|
{
|
||||||
// Get preview manager
|
// Get preview manager
|
||||||
$previewManager = \OC::$server->get(\OCP\IPreview::class);
|
$previewManager = \OC::$server->get(\OCP\IPreview::class);
|
||||||
|
|
||||||
|
@ -152,7 +256,7 @@ class FacesController extends ApiBase
|
||||||
$dh = (float) $detection['height'];
|
$dh = (float) $detection['height'];
|
||||||
$dcx = (float) $detection['x'] + (float) $detection['width'] / 2;
|
$dcx = (float) $detection['x'] + (float) $detection['width'] / 2;
|
||||||
$dcy = (float) $detection['y'] + (float) $detection['height'] / 2;
|
$dcy = (float) $detection['y'] + (float) $detection['height'] / 2;
|
||||||
$faceDim = max($dw * $iw, $dh * $ih) * 1.5;
|
$faceDim = max($dw * $iw, $dh * $ih) * $padding;
|
||||||
$image->cropImage(
|
$image->cropImage(
|
||||||
(int) $faceDim,
|
(int) $faceDim,
|
||||||
(int) $faceDim,
|
(int) $faceDim,
|
|
@ -11,10 +11,11 @@ class TimelineQuery
|
||||||
{
|
{
|
||||||
use TimelineQueryAlbums;
|
use TimelineQueryAlbums;
|
||||||
use TimelineQueryDays;
|
use TimelineQueryDays;
|
||||||
use TimelineQueryFaces;
|
|
||||||
use TimelineQueryFilters;
|
use TimelineQueryFilters;
|
||||||
use TimelineQueryFolders;
|
use TimelineQueryFolders;
|
||||||
use TimelineQueryLivePhoto;
|
use TimelineQueryLivePhoto;
|
||||||
|
use TimelineQueryPeopleFaceRecognition;
|
||||||
|
use TimelineQueryPeopleRecognize;
|
||||||
use TimelineQueryTags;
|
use TimelineQueryTags;
|
||||||
|
|
||||||
protected IDBConnection $connection;
|
protected IDBConnection $connection;
|
||||||
|
|
|
@ -299,7 +299,8 @@ trait TimelineQueryDays
|
||||||
}
|
}
|
||||||
|
|
||||||
// All transform processing
|
// All transform processing
|
||||||
$this->processFace($row);
|
$this->processPeopleRecognizeDetection($row);
|
||||||
|
$this->processFaceRecognitionDetection($row);
|
||||||
|
|
||||||
// We don't need these fields
|
// We don't need these fields
|
||||||
unset($row['datetaken'], $row['rootid']);
|
unset($row['datetaken'], $row['rootid']);
|
||||||
|
|
|
@ -0,0 +1,241 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\Memories\Db;
|
||||||
|
|
||||||
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
|
use OCP\Files\Folder;
|
||||||
|
use OCP\IDBConnection;
|
||||||
|
|
||||||
|
trait TimelineQueryPeopleFaceRecognition
|
||||||
|
{
|
||||||
|
protected IDBConnection $connection;
|
||||||
|
|
||||||
|
public function transformPeopleFaceRecognitionFilter(IQueryBuilder &$query, string $userId, int $currentModel, string $personStr)
|
||||||
|
{
|
||||||
|
// Get title and uid of face user
|
||||||
|
$personNames = explode('/', $personStr);
|
||||||
|
if (2 !== \count($personNames)) {
|
||||||
|
throw new \Exception('Invalid person query');
|
||||||
|
}
|
||||||
|
|
||||||
|
$personUid = $personNames[0];
|
||||||
|
$personName = $personNames[1];
|
||||||
|
|
||||||
|
// Join with images
|
||||||
|
$query->innerJoin('m', 'facerecog_images', 'fri', $query->expr()->andX(
|
||||||
|
$query->expr()->eq('fri.file', 'm.fileid'),
|
||||||
|
$query->expr()->eq('fri.model', $query->createNamedParameter($currentModel)),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Join with faces
|
||||||
|
$query->innerJoin(
|
||||||
|
'fri',
|
||||||
|
'facerecog_faces',
|
||||||
|
'frf',
|
||||||
|
$query->expr()->eq('frf.image', 'fri.id')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Join with persons
|
||||||
|
$nameField = is_numeric($personName) ? 'frp.id' : 'frp.name';
|
||||||
|
$query->innerJoin('frf', 'facerecog_persons', 'frp', $query->expr()->andX(
|
||||||
|
$query->expr()->eq('frf.person', 'frp.id'),
|
||||||
|
$query->expr()->eq('frp.user', $query->createNamedParameter($personUid)),
|
||||||
|
$query->expr()->eq($nameField, $query->createNamedParameter($personName)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transformPeopleFaceRecognitionRect(IQueryBuilder &$query, string $userId)
|
||||||
|
{
|
||||||
|
// Include detection params in response
|
||||||
|
$query->addSelect(
|
||||||
|
'frf.x AS face_x',
|
||||||
|
'frf.y AS face_y',
|
||||||
|
'frf.width AS face_width',
|
||||||
|
'frf.height AS face_height',
|
||||||
|
'm.w AS image_width',
|
||||||
|
'm.h AS image_height',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPeopleFaceRecognition(TimelineRoot &$root, int $currentModel, bool $show_clusters = false, bool $show_singles = false, bool $show_hidden = false)
|
||||||
|
{
|
||||||
|
$query = $this->connection->getQueryBuilder();
|
||||||
|
|
||||||
|
// SELECT all face clusters
|
||||||
|
$count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count');
|
||||||
|
$query->select('frp.id', 'frp.user as user_id', 'frp.name', $count)->from('facerecog_persons', 'frp');
|
||||||
|
|
||||||
|
// WHERE there are faces with this cluster
|
||||||
|
$query->innerJoin('frp', 'facerecog_faces', 'frf', $query->expr()->eq('frp.id', 'frf.person'));
|
||||||
|
|
||||||
|
// WHERE faces are from images.
|
||||||
|
$query->innerJoin('frf', 'facerecog_images', 'fri', $query->expr()->eq('fri.id', 'frf.image'));
|
||||||
|
|
||||||
|
// WHERE these items are memories indexed photos
|
||||||
|
$query->innerJoin('fri', 'memories', 'm', $query->expr()->andX(
|
||||||
|
$query->expr()->eq('fri.file', 'm.fileid'),
|
||||||
|
$query->expr()->eq('fri.model', $query->createNamedParameter($currentModel)),
|
||||||
|
));
|
||||||
|
|
||||||
|
// WHERE these photos are in the user's requested folder recursively
|
||||||
|
$query = $this->joinFilecache($query, $root, true, false);
|
||||||
|
|
||||||
|
if ($show_clusters) {
|
||||||
|
// GROUP by ID of face cluster
|
||||||
|
$query->groupBy('frp.id');
|
||||||
|
$query->where($query->expr()->isNull('frp.name'));
|
||||||
|
} else {
|
||||||
|
// GROUP by name of face clusters
|
||||||
|
$query->groupBy('frp.name');
|
||||||
|
$query->where($query->expr()->isNotNull('frp.name'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default hides individual faces when they have no name.
|
||||||
|
if ($show_clusters && !$show_singles) {
|
||||||
|
$query->having($query->expr()->gt('count', $query->createNamedParameter(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default it shows the people who were not hidden
|
||||||
|
if (!$show_hidden) {
|
||||||
|
$query->andWhere($query->expr()->eq('frp.is_visible', $query->createNamedParameter(true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ORDER by number of faces in cluster
|
||||||
|
$query->orderBy('count', 'DESC');
|
||||||
|
$query->addOrderBy('name', 'ASC');
|
||||||
|
$query->addOrderBy('frp.id'); // tie-breaker
|
||||||
|
|
||||||
|
// FETCH all faces
|
||||||
|
$cursor = $this->executeQueryWithCTEs($query);
|
||||||
|
$faces = $cursor->fetchAll();
|
||||||
|
|
||||||
|
// Post process
|
||||||
|
foreach ($faces as &$row) {
|
||||||
|
$row['id'] = $row['name'] ?: (int) $row['id'];
|
||||||
|
$row['count'] = (int) $row['count'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFaceRecognitionPreview(TimelineRoot &$root, $currentModel, $previewId)
|
||||||
|
{
|
||||||
|
$query = $this->connection->getQueryBuilder();
|
||||||
|
|
||||||
|
// SELECT face detections
|
||||||
|
$query->select(
|
||||||
|
'fri.file as file_id', // Get actual file
|
||||||
|
'frf.x', // Image cropping
|
||||||
|
'frf.y',
|
||||||
|
'frf.width',
|
||||||
|
'frf.height',
|
||||||
|
'm.w as image_width', // Scoring
|
||||||
|
'm.h as image_height',
|
||||||
|
'frf.confidence',
|
||||||
|
'm.fileid',
|
||||||
|
'm.datetaken', // Just in case, for postgres
|
||||||
|
)->from('facerecog_faces', 'frf');
|
||||||
|
|
||||||
|
// WHERE faces are from images and current model.
|
||||||
|
$query->innerJoin('frf', 'facerecog_images', 'fri', $query->expr()->andX(
|
||||||
|
$query->expr()->eq('fri.id', 'frf.image'),
|
||||||
|
$query->expr()->eq('fri.model', $query->createNamedParameter($currentModel)),
|
||||||
|
));
|
||||||
|
|
||||||
|
// WHERE these photos are memories indexed
|
||||||
|
$query->innerJoin('fri', 'memories', 'm', $query->expr()->eq('m.fileid', 'fri.file'));
|
||||||
|
|
||||||
|
$query->innerJoin('frf', 'facerecog_persons', 'frp', $query->expr()->eq('frp.id', 'frf.person'));
|
||||||
|
if (is_numeric($previewId)) {
|
||||||
|
// WHERE faces are from id persons (a cluster).
|
||||||
|
$query->where($query->expr()->eq('frp.id', $query->createNamedParameter($previewId)));
|
||||||
|
} else {
|
||||||
|
// WHERE faces are from name on persons.
|
||||||
|
$query->where($query->expr()->eq('frp.name', $query->createNamedParameter($previewId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// WHERE these photos are in the user's requested folder recursively
|
||||||
|
$query = $this->joinFilecache($query, $root, true, false);
|
||||||
|
|
||||||
|
// LIMIT results
|
||||||
|
$query->setMaxResults(15);
|
||||||
|
|
||||||
|
// Sort by date taken so we get recent photos
|
||||||
|
$query->orderBy('m.datetaken', 'DESC');
|
||||||
|
$query->addOrderBy('m.fileid', 'DESC'); // tie-breaker
|
||||||
|
|
||||||
|
// FETCH face detections
|
||||||
|
$cursor = $this->executeQueryWithCTEs($query);
|
||||||
|
$previews = $cursor->fetchAll();
|
||||||
|
if (empty($previews)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score the face detections
|
||||||
|
foreach ($previews as &$p) {
|
||||||
|
// Get actual pixel size of face
|
||||||
|
$iw = min((int) ($p['image_width'] ?: 512), 2048);
|
||||||
|
$ih = min((int) ($p['image_height'] ?: 512), 2048);
|
||||||
|
|
||||||
|
// Get percentage position and size
|
||||||
|
$p['x'] = (float) $p['x'] / $p['image_width'];
|
||||||
|
$p['y'] = (float) $p['y'] / $p['image_height'];
|
||||||
|
$p['width'] = (float) $p['width'] / $p['image_width'];
|
||||||
|
$p['height'] = (float) $p['height'] / $p['image_height'];
|
||||||
|
|
||||||
|
$w = (float) $p['width'];
|
||||||
|
$h = (float) $p['height'];
|
||||||
|
|
||||||
|
// Get center of face
|
||||||
|
$x = (float) $p['x'] + (float) $p['width'] / 2;
|
||||||
|
$y = (float) $p['y'] + (float) $p['height'] / 2;
|
||||||
|
|
||||||
|
// 3D normal distribution - if the face is closer to the center, it's better
|
||||||
|
$positionScore = exp(-($x - 0.5) ** 2 * 4) * exp(-($y - 0.5) ** 2 * 4);
|
||||||
|
|
||||||
|
// Root size distribution - if the image is bigger, it's better,
|
||||||
|
// but it doesn't matter beyond a certain point
|
||||||
|
$imgSizeScore = ($iw * 100) ** (1 / 2) * ($ih * 100) ** (1 / 2);
|
||||||
|
|
||||||
|
// Faces occupying too much of the image don't look particularly good
|
||||||
|
$faceSizeScore = (-$w ** 2 + $w) * (-$h ** 2 + $h);
|
||||||
|
|
||||||
|
// Combine scores
|
||||||
|
$p['score'] = $positionScore * $imgSizeScore * $faceSizeScore * $p['confidence'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort previews by score descending
|
||||||
|
usort($previews, function ($a, $b) {
|
||||||
|
return $b['score'] <=> $a['score'];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $previews;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert face fields to object */
|
||||||
|
private function processFaceRecognitionDetection(&$row, $days = false)
|
||||||
|
{
|
||||||
|
if (!isset($row)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Differentiate Recognize queries from Face Recognition
|
||||||
|
if (!isset($row['face_width']) || !isset($row['image_width'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$days) {
|
||||||
|
$row['facerect'] = [
|
||||||
|
// Get percentage position and size
|
||||||
|
'w' => (float) $row['face_width'] / $row['image_width'],
|
||||||
|
'h' => (float) $row['face_height'] / $row['image_height'],
|
||||||
|
'x' => (float) $row['face_x'] / $row['image_width'],
|
||||||
|
'y' => (float) $row['face_y'] / $row['image_height'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($row['face_x'], $row['face_y'], $row['face_w'], $row['face_h'], $row['image_height'], $row['image_width']);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,13 +7,13 @@ namespace OCA\Memories\Db;
|
||||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
use OCP\IDBConnection;
|
use OCP\IDBConnection;
|
||||||
|
|
||||||
trait TimelineQueryFaces
|
trait TimelineQueryPeopleRecognize
|
||||||
{
|
{
|
||||||
protected IDBConnection $connection;
|
protected IDBConnection $connection;
|
||||||
|
|
||||||
public function transformFaceFilter(IQueryBuilder &$query, string $userId, string $faceStr)
|
public function transformPeopleRecognitionFilter(IQueryBuilder &$query, string $userId, string $faceStr)
|
||||||
{
|
{
|
||||||
// Get title and uid of face user
|
// Get name and uid of face user
|
||||||
$faceNames = explode('/', $faceStr);
|
$faceNames = explode('/', $faceStr);
|
||||||
if (2 !== \count($faceNames)) {
|
if (2 !== \count($faceNames)) {
|
||||||
throw new \Exception('Invalid face query');
|
throw new \Exception('Invalid face query');
|
||||||
|
@ -35,7 +35,7 @@ trait TimelineQueryFaces
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function transformFaceRect(IQueryBuilder &$query, string $userId)
|
public function transformPeopleRecognizeRect(IQueryBuilder &$query, string $userId)
|
||||||
{
|
{
|
||||||
// Include detection params in response
|
// Include detection params in response
|
||||||
$query->addSelect(
|
$query->addSelect(
|
||||||
|
@ -46,7 +46,7 @@ trait TimelineQueryFaces
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFaces(TimelineRoot &$root)
|
public function getPeopleRecognize(TimelineRoot &$root)
|
||||||
{
|
{
|
||||||
$query = $this->connection->getQueryBuilder();
|
$query = $this->connection->getQueryBuilder();
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ trait TimelineQueryFaces
|
||||||
return $faces;
|
return $faces;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFacePreviewDetection(TimelineRoot &$root, int $id)
|
public function getPeopleRecognizePreview(TimelineRoot &$root, int $id)
|
||||||
{
|
{
|
||||||
$query = $this->connection->getQueryBuilder();
|
$query = $this->connection->getQueryBuilder();
|
||||||
|
|
||||||
|
@ -159,8 +159,9 @@ trait TimelineQueryFaces
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert face fields to object */
|
/** Convert face fields to object */
|
||||||
private function processFace(&$row, $days = false)
|
private function processPeopleRecognizeDetection(&$row, $days = false)
|
||||||
{
|
{
|
||||||
|
// Differentiate Recognize queries from Face Recognition
|
||||||
if (!isset($row) || !isset($row['face_w'])) {
|
if (!isset($row) || !isset($row['face_w'])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
41
lib/Util.php
41
lib/Util.php
|
@ -4,6 +4,9 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCA\Memories;
|
namespace OCA\Memories;
|
||||||
|
|
||||||
|
use OCP\App\IAppManager;
|
||||||
|
use OCP\IConfig;
|
||||||
|
|
||||||
class Util
|
class Util
|
||||||
{
|
{
|
||||||
public static $TAG_DAYID_START = -(1 << 30); // the world surely didn't exist
|
public static $TAG_DAYID_START = -(1 << 30); // the world surely didn't exist
|
||||||
|
@ -46,10 +49,8 @@ class Util
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if albums are enabled for this user.
|
* Check if albums are enabled for this user.
|
||||||
*
|
|
||||||
* @param mixed $appManager
|
|
||||||
*/
|
*/
|
||||||
public static function albumsIsEnabled(&$appManager): bool
|
public static function albumsIsEnabled(IAppManager &$appManager): bool
|
||||||
{
|
{
|
||||||
if (!$appManager->isEnabledForUser('photos')) {
|
if (!$appManager->isEnabledForUser('photos')) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -72,10 +73,8 @@ class Util
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if recognize is enabled for this user.
|
* Check if recognize is enabled for this user.
|
||||||
*
|
|
||||||
* @param mixed $appManager
|
|
||||||
*/
|
*/
|
||||||
public static function recognizeIsEnabled(&$appManager): bool
|
public static function recognizeIsEnabled(IAppManager &$appManager): bool
|
||||||
{
|
{
|
||||||
if (!$appManager->isEnabledForUser('recognize')) {
|
if (!$appManager->isEnabledForUser('recognize')) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -87,11 +86,33 @@ class Util
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if link sharing is allowed.
|
* Check if Face Recognition is enabled by the user.
|
||||||
*
|
|
||||||
* @param mixed $config
|
|
||||||
*/
|
*/
|
||||||
public static function isLinkSharingEnabled(&$config): bool
|
public static function facerecognitionIsEnabled(IConfig &$config, string $userId): bool
|
||||||
|
{
|
||||||
|
$e = $config->getUserValue($userId, 'facerecognition', 'enabled', 'false');
|
||||||
|
|
||||||
|
return 'true' === $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Face Recognition is installed and enabled for this user.
|
||||||
|
*/
|
||||||
|
public static function facerecognitionIsInstalled(IAppManager &$appManager): bool
|
||||||
|
{
|
||||||
|
if (!$appManager->isEnabledForUser('facerecognition')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = $appManager->getAppInfo('facerecognition')['version'];
|
||||||
|
|
||||||
|
return version_compare($v, '0.9.10-beta.2', '>=');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if link sharing is allowed.
|
||||||
|
*/
|
||||||
|
public static function isLinkSharingEnabled(IConfig &$config): bool
|
||||||
{
|
{
|
||||||
// Check if the shareAPI is enabled
|
// Check if the shareAPI is enabled
|
||||||
if ('yes' !== $config->getAppValue('core', 'shareapi_enabled', 'yes')) {
|
if ('yes' !== $config->getAppValue('core', 'shareapi_enabled', 'yes')) {
|
||||||
|
|
48
src/App.vue
48
src/App.vue
|
@ -99,7 +99,7 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
|
||||||
|
|
||||||
private metadataComponent!: Metadata;
|
private metadataComponent!: Metadata;
|
||||||
|
|
||||||
private readonly navItemsAll = [
|
private readonly navItemsAll = (self: typeof this) => [
|
||||||
{
|
{
|
||||||
name: "timeline",
|
name: "timeline",
|
||||||
icon: ImageMultiple,
|
icon: ImageMultiple,
|
||||||
|
@ -124,13 +124,19 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
|
||||||
name: "albums",
|
name: "albums",
|
||||||
icon: AlbumIcon,
|
icon: AlbumIcon,
|
||||||
title: t("memories", "Albums"),
|
title: t("memories", "Albums"),
|
||||||
if: (self: any) => self.showAlbums,
|
if: self.showAlbums,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "people",
|
name: "recognize",
|
||||||
icon: PeopleIcon,
|
icon: PeopleIcon,
|
||||||
title: t("memories", "People"),
|
title: self.recognize,
|
||||||
if: (self: any) => self.showPeople,
|
if: self.recognize,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "facerecognition",
|
||||||
|
icon: PeopleIcon,
|
||||||
|
title: self.facerecognition,
|
||||||
|
if: self.facerecognition,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "archive",
|
name: "archive",
|
||||||
|
@ -146,13 +152,13 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
|
||||||
name: "tags",
|
name: "tags",
|
||||||
icon: TagsIcon,
|
icon: TagsIcon,
|
||||||
title: t("memories", "Tags"),
|
title: t("memories", "Tags"),
|
||||||
if: (self: any) => self.config_tagsEnabled,
|
if: self.config_tagsEnabled,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "maps",
|
name: "maps",
|
||||||
icon: MapIcon,
|
icon: MapIcon,
|
||||||
title: t("memories", "Maps"),
|
title: t("memories", "Maps"),
|
||||||
if: (self: any) => self.config_mapsEnabled,
|
if: self.config_mapsEnabled,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -163,8 +169,28 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
|
||||||
return Number(version[0]);
|
return Number(version[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
get showPeople() {
|
get recognize() {
|
||||||
return this.config_recognizeEnabled || getCurrentUser()?.isAdmin;
|
if (!this.config_recognizeEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config_facerecognitionInstalled) {
|
||||||
|
return t("memories", "People (Recognize)");
|
||||||
|
}
|
||||||
|
|
||||||
|
return t("memories", "People");
|
||||||
|
}
|
||||||
|
|
||||||
|
get facerecognition() {
|
||||||
|
if (!this.config_facerecognitionInstalled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config_recognizeEnabled) {
|
||||||
|
return t("memories", "People (Face Recognition)");
|
||||||
|
}
|
||||||
|
|
||||||
|
return t("memories", "People");
|
||||||
}
|
}
|
||||||
|
|
||||||
get isFirstStart() {
|
get isFirstStart() {
|
||||||
|
@ -192,8 +218,8 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
|
||||||
this.doRouteChecks();
|
this.doRouteChecks();
|
||||||
|
|
||||||
// Populate navigation
|
// Populate navigation
|
||||||
this.navItems = this.navItemsAll.filter(
|
this.navItems = this.navItemsAll(this).filter(
|
||||||
(item) => !item.if || item.if(this)
|
(item) => typeof item.if === "undefined" || Boolean(item.if)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Store CSS variables modified
|
// Store CSS variables modified
|
||||||
|
|
|
@ -209,13 +209,13 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
name: t("memories", "Move to another person"),
|
name: t("memories", "Move to another person"),
|
||||||
icon: MoveIcon,
|
icon: MoveIcon,
|
||||||
callback: this.moveSelectionToPerson.bind(this),
|
callback: this.moveSelectionToPerson.bind(this),
|
||||||
if: () => this.$route.name === "people",
|
if: () => this.$route.name === "recognize",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t("memories", "Remove from person"),
|
name: t("memories", "Remove from person"),
|
||||||
icon: CloseIcon,
|
icon: CloseIcon,
|
||||||
callback: this.removeSelectionFromPerson.bind(this),
|
callback: this.removeSelectionFromPerson.bind(this),
|
||||||
if: () => this.$route.name === "people",
|
if: () => this.$route.name === "recognize",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -846,7 +846,7 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
private async removeSelectionFromPerson(selection: Selection) {
|
private async removeSelectionFromPerson(selection: Selection) {
|
||||||
// Make sure route is valid
|
// Make sure route is valid
|
||||||
const { user, name } = this.$route.params;
|
const { user, name } = this.$route.params;
|
||||||
if (this.$route.name !== "people" || !user || !name) {
|
if (this.$route.name !== "recognize" || !user || !name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,12 @@
|
||||||
<!-- No content found and nothing is loading -->
|
<!-- No content found and nothing is loading -->
|
||||||
<NcEmptyContent
|
<NcEmptyContent
|
||||||
title="Nothing to show here"
|
title="Nothing to show here"
|
||||||
|
:description="emptyViewDescription"
|
||||||
v-if="loading === 0 && list.length === 0"
|
v-if="loading === 0 && list.length === 0"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<PeopleIcon v-if="$route.name === 'people'" />
|
<PeopleIcon v-if="routeIsPeople" />
|
||||||
<ArchiveIcon v-else-if="$route.name === 'archive'" />
|
<ArchiveIcon v-else-if="routeIsArchive" />
|
||||||
<ImageMultipleIcon v-else />
|
<ImageMultipleIcon v-else />
|
||||||
</template>
|
</template>
|
||||||
</NcEmptyContent>
|
</NcEmptyContent>
|
||||||
|
@ -43,7 +44,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OnThisDay
|
<OnThisDay
|
||||||
v-if="$route.name === 'timeline'"
|
v-if="routeIsBase"
|
||||||
:key="config_timelinePath"
|
:key="config_timelinePath"
|
||||||
:viewer="$refs.viewer"
|
:viewer="$refs.viewer"
|
||||||
@load="scrollerManager.adjust()"
|
@load="scrollerManager.adjust()"
|
||||||
|
@ -308,6 +309,18 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
window.removeEventListener("resize", this.handleResizeWithDelay);
|
window.removeEventListener("resize", this.handleResizeWithDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get routeIsBase() {
|
||||||
|
return this.$route.name === "timeline";
|
||||||
|
}
|
||||||
|
|
||||||
|
get routeIsPeople() {
|
||||||
|
return ["recognize", "facerecognition"].includes(this.$route.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
get routeIsArchive() {
|
||||||
|
return this.$route.name === "archive";
|
||||||
|
}
|
||||||
|
|
||||||
updateLoading(delta: number) {
|
updateLoading(delta: number) {
|
||||||
this.loading += delta;
|
this.loading += delta;
|
||||||
}
|
}
|
||||||
|
@ -574,12 +587,12 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
|
|
||||||
// People
|
// People
|
||||||
if (
|
if (
|
||||||
this.$route.name === "people" &&
|
this.routeIsPeople &&
|
||||||
this.$route.params.user &&
|
this.$route.params.user &&
|
||||||
this.$route.params.name
|
this.$route.params.name
|
||||||
) {
|
) {
|
||||||
query.set(
|
query.set(
|
||||||
"face",
|
this.$route.name, // "recognize" or "facerecognition"
|
||||||
`${this.$route.params.user}/${this.$route.params.name}`
|
`${this.$route.params.user}/${this.$route.params.name}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -618,7 +631,8 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
return this.t("memories", "Your Timeline");
|
return this.t("memories", "Your Timeline");
|
||||||
case "favorites":
|
case "favorites":
|
||||||
return this.t("memories", "Favorites");
|
return this.t("memories", "Favorites");
|
||||||
case "people":
|
case "recognize":
|
||||||
|
case "facerecognition":
|
||||||
return this.t("memories", "People");
|
return this.t("memories", "People");
|
||||||
case "videos":
|
case "videos":
|
||||||
return this.t("memories", "Videos");
|
return this.t("memories", "Videos");
|
||||||
|
@ -663,6 +677,33 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
return head.name;
|
return head.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Get a friendly description of empty view */
|
||||||
|
get emptyViewDescription() {
|
||||||
|
switch (this.$route.name) {
|
||||||
|
case "facerecognition":
|
||||||
|
if (this.config_facerecognitionEnabled)
|
||||||
|
return this.t(
|
||||||
|
"memories",
|
||||||
|
"You will find your friends soon. Please, be patient."
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return this.t(
|
||||||
|
"memories",
|
||||||
|
"Face Recognition is disabled. Enable in settings to find your friends."
|
||||||
|
);
|
||||||
|
case "timeline":
|
||||||
|
case "favorites":
|
||||||
|
case "recognize":
|
||||||
|
case "videos":
|
||||||
|
case "albums":
|
||||||
|
case "archive":
|
||||||
|
case "thisday":
|
||||||
|
case "tags":
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Fetch timeline main call */
|
/** Fetch timeline main call */
|
||||||
async fetchDays(noCache = false) {
|
async fetchDays(noCache = false) {
|
||||||
const url = API.Q(API.DAYS(), this.getQuery());
|
const url = API.Q(API.DAYS(), this.getQuery());
|
||||||
|
@ -683,8 +724,8 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
data = await dav.getOnThisDayData();
|
data = await dav.getOnThisDayData();
|
||||||
} else if (this.$route.name === "tags" && !this.$route.params.name) {
|
} else if (this.$route.name === "tags" && !this.$route.params.name) {
|
||||||
data = await dav.getTagsData();
|
data = await dav.getTagsData();
|
||||||
} else if (this.$route.name === "people" && !this.$route.params.name) {
|
} else if (this.routeIsPeople && !this.$route.params.name) {
|
||||||
data = await dav.getPeopleData();
|
data = await dav.getPeopleData(this.$route.name as any);
|
||||||
} else if (this.$route.name === "albums" && !this.$route.params.name) {
|
} else if (this.$route.name === "albums" && !this.$route.params.name) {
|
||||||
data = await dav.getAlbumsData("3");
|
data = await dav.getAlbumsData("3");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -58,7 +58,7 @@ export default class Tag extends Mixins(GlobalMixin) {
|
||||||
|
|
||||||
get previewUrl() {
|
get previewUrl() {
|
||||||
if (this.face) {
|
if (this.face) {
|
||||||
return API.FACE_PREVIEW(this.face.fileid);
|
return API.FACE_PREVIEW(this.faceApp, this.face.fileid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.album) {
|
if (this.album) {
|
||||||
|
@ -78,7 +78,16 @@ export default class Tag extends Mixins(GlobalMixin) {
|
||||||
}
|
}
|
||||||
|
|
||||||
get face() {
|
get face() {
|
||||||
return this.data.flag & constants.c.FLAG_IS_FACE ? this.data : null;
|
return this.data.flag & constants.c.FLAG_IS_FACE_RECOGNIZE ||
|
||||||
|
this.data.flag & constants.c.FLAG_IS_FACE_RECOGNITION
|
||||||
|
? this.data
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get faceApp() {
|
||||||
|
return this.data.flag & constants.c.FLAG_IS_FACE_RECOGNITION
|
||||||
|
? "facerecognition"
|
||||||
|
: "recognize";
|
||||||
}
|
}
|
||||||
|
|
||||||
get album() {
|
get album() {
|
||||||
|
@ -94,7 +103,7 @@ export default class Tag extends Mixins(GlobalMixin) {
|
||||||
if (this.face) {
|
if (this.face) {
|
||||||
const name = this.face.name || this.face.fileid.toString();
|
const name = this.face.name || this.face.fileid.toString();
|
||||||
const user = this.face.user_id;
|
const user = this.face.user_id;
|
||||||
return { name: "people", params: { name, user } };
|
return { name: this.faceApp, params: { name, user } };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.album) {
|
if (this.album) {
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { getCurrentUser } from "@nextcloud/auth";
|
||||||
import Modal from "./Modal.vue";
|
import Modal from "./Modal.vue";
|
||||||
import GlobalMixin from "../../mixins/GlobalMixin";
|
import GlobalMixin from "../../mixins/GlobalMixin";
|
||||||
import client from "../../services/DavClient";
|
import client from "../../services/DavClient";
|
||||||
|
import * as dav from "../../services/DavRequests";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
@ -74,8 +75,12 @@ export default class FaceDeleteModal extends Mixins(GlobalMixin) {
|
||||||
|
|
||||||
public async save() {
|
public async save() {
|
||||||
try {
|
try {
|
||||||
await client.deleteFile(`/recognize/${this.user}/faces/${this.name}`);
|
if (this.$route.name === "recognize") {
|
||||||
this.$router.push({ name: "people" });
|
await client.deleteFile(`/recognize/${this.user}/faces/${this.name}`);
|
||||||
|
} else {
|
||||||
|
await dav.setVisibilityPeopleFaceRecognition(this.name, false);
|
||||||
|
}
|
||||||
|
this.$router.push({ name: this.$route.name });
|
||||||
this.close();
|
this.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { getCurrentUser } from "@nextcloud/auth";
|
||||||
import Modal from "./Modal.vue";
|
import Modal from "./Modal.vue";
|
||||||
import GlobalMixin from "../../mixins/GlobalMixin";
|
import GlobalMixin from "../../mixins/GlobalMixin";
|
||||||
import client from "../../services/DavClient";
|
import client from "../../services/DavClient";
|
||||||
|
import * as dav from "../../services/DavRequests";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
@ -83,12 +84,16 @@ export default class FaceEditModal extends Mixins(GlobalMixin) {
|
||||||
|
|
||||||
public async save() {
|
public async save() {
|
||||||
try {
|
try {
|
||||||
await client.moveFile(
|
if (this.$route.name === "recognize") {
|
||||||
`/recognize/${this.user}/faces/${this.oldName}`,
|
await client.moveFile(
|
||||||
`/recognize/${this.user}/faces/${this.name}`
|
`/recognize/${this.user}/faces/${this.oldName}`,
|
||||||
);
|
`/recognize/${this.user}/faces/${this.name}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await dav.renamePeopleFaceRecognition(this.oldName, this.name);
|
||||||
|
}
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: "people",
|
name: this.$route.name,
|
||||||
params: { user: this.user, name: this.name },
|
params: { user: this.user, name: this.name },
|
||||||
});
|
});
|
||||||
this.close();
|
this.close();
|
||||||
|
|
|
@ -44,10 +44,18 @@ export default class FaceMergeModal extends Mixins(GlobalMixin) {
|
||||||
this.name = this.$route.params.name || "";
|
this.name = this.$route.params.name || "";
|
||||||
this.detail = null;
|
this.detail = null;
|
||||||
|
|
||||||
const data = await dav.getPeopleData();
|
let data = [];
|
||||||
|
let flags = this.c.FLAG_IS_TAG;
|
||||||
|
if (this.$route.name === "recognize") {
|
||||||
|
data = await dav.getPeopleData("recognize");
|
||||||
|
flags |= this.c.FLAG_IS_FACE_RECOGNIZE;
|
||||||
|
} else {
|
||||||
|
data = await dav.getPeopleData("facerecognition");
|
||||||
|
flags |= this.c.FLAG_IS_FACE_RECOGNITION;
|
||||||
|
}
|
||||||
let detail = data[0].detail;
|
let detail = data[0].detail;
|
||||||
detail.forEach((photo: IPhoto) => {
|
detail.forEach((photo: IPhoto) => {
|
||||||
photo.flag = this.c.FLAG_IS_FACE | this.c.FLAG_IS_TAG;
|
photo.flag = flags;
|
||||||
});
|
});
|
||||||
detail = detail.filter((photo: ITag) => {
|
detail = detail.filter((photo: ITag) => {
|
||||||
const pname = photo.name || photo.fileid.toString();
|
const pname = photo.name || photo.fileid.toString();
|
||||||
|
|
|
@ -135,7 +135,7 @@ export default class FaceMergeModal extends Mixins(GlobalMixin) {
|
||||||
// Go to new face
|
// Go to new face
|
||||||
if (failures === 0) {
|
if (failures === 0) {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: "people",
|
name: "recognize",
|
||||||
params: { user: face.user_id, name: newName },
|
params: { user: face.user_id, name: newName },
|
||||||
});
|
});
|
||||||
this.close();
|
this.close();
|
||||||
|
|
|
@ -99,7 +99,7 @@ export default class FaceTopMatter extends Mixins(GlobalMixin, UserConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
back() {
|
back() {
|
||||||
this.$router.push({ name: "people" });
|
this.$router.push({ name: this.$route.name });
|
||||||
}
|
}
|
||||||
|
|
||||||
changeShowFaceRect() {
|
changeShowFaceRect() {
|
||||||
|
|
|
@ -47,7 +47,8 @@ export default class TopMatter extends Mixins(GlobalMixin) {
|
||||||
return this.$route.params.name
|
return this.$route.params.name
|
||||||
? TopMatterType.TAG
|
? TopMatterType.TAG
|
||||||
: TopMatterType.NONE;
|
: TopMatterType.NONE;
|
||||||
case "people":
|
case "recognize":
|
||||||
|
case "facerecognition":
|
||||||
return this.$route.params.name
|
return this.$route.params.name
|
||||||
? TopMatterType.FACE
|
? TopMatterType.FACE
|
||||||
: TopMatterType.NONE;
|
: TopMatterType.NONE;
|
||||||
|
|
|
@ -26,6 +26,12 @@ export default class UserConfig extends Vue {
|
||||||
config_recognizeEnabled = Boolean(
|
config_recognizeEnabled = Boolean(
|
||||||
loadState("memories", "recognize", <string>"")
|
loadState("memories", "recognize", <string>"")
|
||||||
);
|
);
|
||||||
|
config_facerecognitionInstalled = Boolean(
|
||||||
|
loadState("memories", "facerecognitionInstalled", <string>"")
|
||||||
|
);
|
||||||
|
config_facerecognitionEnabled = Boolean(
|
||||||
|
loadState("memories", "facerecognitionEnabled", <string>"")
|
||||||
|
);
|
||||||
config_mapsEnabled = Boolean(loadState("memories", "maps", <string>""));
|
config_mapsEnabled = Boolean(loadState("memories", "maps", <string>""));
|
||||||
config_albumsEnabled = Boolean(loadState("memories", "albums", <string>""));
|
config_albumsEnabled = Boolean(loadState("memories", "albums", <string>""));
|
||||||
|
|
||||||
|
|
|
@ -77,9 +77,18 @@ export default new Router({
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "/people/:user?/:name?",
|
path: "/recognize/:user?/:name?",
|
||||||
component: Timeline,
|
component: Timeline,
|
||||||
name: "people",
|
name: "recognize",
|
||||||
|
props: (route) => ({
|
||||||
|
rootTitle: t("memories", "People"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: "/facerecognition/:user?/:name?",
|
||||||
|
component: Timeline,
|
||||||
|
name: "facerecognition",
|
||||||
props: (route) => ({
|
props: (route) => ({
|
||||||
rootTitle: t("memories", "People"),
|
rootTitle: t("memories", "People"),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -50,12 +50,15 @@ export class API {
|
||||||
return gen(`${BASE}/tags/preview/{tag}`, { tag });
|
return gen(`${BASE}/tags/preview/{tag}`, { tag });
|
||||||
}
|
}
|
||||||
|
|
||||||
static FACE_LIST() {
|
static FACE_LIST(app: "recognize" | "facerecognition") {
|
||||||
return gen(`${BASE}/faces`);
|
return gen(`${BASE}/${app}/people`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static FACE_PREVIEW(face: string | number) {
|
static FACE_PREVIEW(
|
||||||
return gen(`${BASE}/faces/preview/{face}`, { face });
|
app: "recognize" | "facerecognition",
|
||||||
|
face: string | number
|
||||||
|
) {
|
||||||
|
return gen(`${BASE}/${app}/people/preview/{face}`, { face });
|
||||||
}
|
}
|
||||||
|
|
||||||
static ARCHIVE(fileid: number) {
|
static ARCHIVE(fileid: number) {
|
||||||
|
|
|
@ -212,7 +212,12 @@ export function convertFlags(photo: IPhoto) {
|
||||||
delete photo.isfolder;
|
delete photo.isfolder;
|
||||||
}
|
}
|
||||||
if (photo.isface) {
|
if (photo.isface) {
|
||||||
photo.flag |= constants.c.FLAG_IS_FACE;
|
const app = photo.isface;
|
||||||
|
if (app === "recognize") {
|
||||||
|
photo.flag |= constants.c.FLAG_IS_FACE_RECOGNIZE;
|
||||||
|
} else if (app === "facerecognition") {
|
||||||
|
photo.flag |= constants.c.FLAG_IS_FACE_RECOGNITION;
|
||||||
|
}
|
||||||
delete photo.isface;
|
delete photo.isface;
|
||||||
}
|
}
|
||||||
if (photo.istag) {
|
if (photo.istag) {
|
||||||
|
@ -310,10 +315,11 @@ export const constants = {
|
||||||
FLAG_IS_FAVORITE: 1 << 3,
|
FLAG_IS_FAVORITE: 1 << 3,
|
||||||
FLAG_IS_FOLDER: 1 << 4,
|
FLAG_IS_FOLDER: 1 << 4,
|
||||||
FLAG_IS_TAG: 1 << 5,
|
FLAG_IS_TAG: 1 << 5,
|
||||||
FLAG_IS_FACE: 1 << 6,
|
FLAG_IS_FACE_RECOGNIZE: 1 << 6,
|
||||||
FLAG_IS_ALBUM: 1 << 7,
|
FLAG_IS_FACE_RECOGNITION: 1 << 7,
|
||||||
FLAG_SELECTED: 1 << 8,
|
FLAG_IS_ALBUM: 1 << 8,
|
||||||
FLAG_LEAVING: 1 << 9,
|
FLAG_SELECTED: 1 << 9,
|
||||||
|
FLAG_LEAVING: 1 << 10,
|
||||||
},
|
},
|
||||||
|
|
||||||
TagDayID: TagDayID,
|
TagDayID: TagDayID,
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import axios from "@nextcloud/axios";
|
import axios from "@nextcloud/axios";
|
||||||
import { showError } from "@nextcloud/dialogs";
|
import { showError } from "@nextcloud/dialogs";
|
||||||
import { translate as t } from "@nextcloud/l10n";
|
import { translate as t } from "@nextcloud/l10n";
|
||||||
|
import { generateUrl } from "@nextcloud/router";
|
||||||
import { IDay, IPhoto } from "../../types";
|
import { IDay, IPhoto } from "../../types";
|
||||||
import { API } from "../API";
|
import { API } from "../API";
|
||||||
import client from "../DavClient";
|
|
||||||
import { constants } from "../Utils";
|
import { constants } from "../Utils";
|
||||||
|
import client from "../DavClient";
|
||||||
import * as base from "./base";
|
import * as base from "./base";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get list of tags and convert to Days response
|
* Get list of tags and convert to Days response
|
||||||
*/
|
*/
|
||||||
export async function getPeopleData(): Promise<IDay[]> {
|
export async function getPeopleData(
|
||||||
|
app: "recognize" | "facerecognition"
|
||||||
|
): Promise<IDay[]> {
|
||||||
// Query for photos
|
// Query for photos
|
||||||
let data: {
|
let data: {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -19,7 +22,7 @@ export async function getPeopleData(): Promise<IDay[]> {
|
||||||
previews: IPhoto[];
|
previews: IPhoto[];
|
||||||
}[] = [];
|
}[] = [];
|
||||||
try {
|
try {
|
||||||
const res = await axios.get<typeof data>(API.FACE_LIST());
|
const res = await axios.get<typeof data>(API.FACE_LIST(app));
|
||||||
data = res.data;
|
data = res.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -39,13 +42,48 @@ export async function getPeopleData(): Promise<IDay[]> {
|
||||||
...face,
|
...face,
|
||||||
fileid: face.id,
|
fileid: face.id,
|
||||||
istag: true,
|
istag: true,
|
||||||
isface: true,
|
isface: app,
|
||||||
} as any)
|
} as any)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updatePeopleFaceRecognition(
|
||||||
|
name: string,
|
||||||
|
params: object
|
||||||
|
) {
|
||||||
|
if (Number.isInteger(Number(name))) {
|
||||||
|
return await axios.put(
|
||||||
|
generateUrl(`/apps/facerecognition/api/2.0/cluster/${name}`),
|
||||||
|
params
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return await axios.put(
|
||||||
|
generateUrl(`/apps/facerecognition/api/2.0/person/${name}`),
|
||||||
|
params
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renamePeopleFaceRecognition(
|
||||||
|
name: string,
|
||||||
|
newName: string
|
||||||
|
) {
|
||||||
|
return await updatePeopleFaceRecognition(name, {
|
||||||
|
name: newName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setVisibilityPeopleFaceRecognition(
|
||||||
|
name: string,
|
||||||
|
visibility: boolean
|
||||||
|
) {
|
||||||
|
return await updatePeopleFaceRecognition(name, {
|
||||||
|
visible: visibility,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove images from a face.
|
* Remove images from a face.
|
||||||
*
|
*
|
||||||
|
|
|
@ -105,7 +105,7 @@ export type IPhoto = {
|
||||||
/** Is this an album */
|
/** Is this an album */
|
||||||
isalbum?: boolean;
|
isalbum?: boolean;
|
||||||
/** Is this a face */
|
/** Is this a face */
|
||||||
isface?: boolean;
|
isface?: "recognize" | "facerecognition";
|
||||||
/** Optional datetaken epoch */
|
/** Optional datetaken epoch */
|
||||||
datetaken?: number;
|
datetaken?: number;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue