Get face previews in single query
parent
ff502b5068
commit
991515e9e5
|
@ -26,7 +26,6 @@ return [
|
||||||
['name' => 'api#day', 'url' => '/api/days/{id}', 'verb' => 'GET'],
|
['name' => 'api#day', 'url' => '/api/days/{id}', 'verb' => 'GET'],
|
||||||
['name' => 'api#tags', 'url' => '/api/tags', 'verb' => 'GET'],
|
['name' => 'api#tags', 'url' => '/api/tags', 'verb' => 'GET'],
|
||||||
['name' => 'api#faces', 'url' => '/api/faces', 'verb' => 'GET'],
|
['name' => 'api#faces', 'url' => '/api/faces', 'verb' => 'GET'],
|
||||||
['name' => 'api#facePreviews', 'url' => '/api/face-previews/{id}', 'verb' => 'GET'],
|
|
||||||
['name' => 'api#imageInfo', 'url' => '/api/info/{id}', 'verb' => 'GET'],
|
['name' => 'api#imageInfo', 'url' => '/api/info/{id}', 'verb' => 'GET'],
|
||||||
['name' => 'api#imageEdit', 'url' => '/api/edit/{id}', 'verb' => 'PATCH'],
|
['name' => 'api#imageEdit', 'url' => '/api/edit/{id}', 'verb' => 'PATCH'],
|
||||||
['name' => 'api#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'],
|
['name' => 'api#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'],
|
||||||
|
|
|
@ -359,45 +359,32 @@ class ApiController extends Controller {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Preload all face previews
|
// Preload all face previews
|
||||||
|
$previews = $this->timelineQuery->getFacePreviews($folder);
|
||||||
|
|
||||||
|
// Convert to map with key as cluster_id
|
||||||
|
$previews_map = [];
|
||||||
|
foreach ($previews as &$preview) {
|
||||||
|
$key = $preview["cluster_id"];
|
||||||
|
if (!array_key_exists($key, $previews_map)) {
|
||||||
|
$previews_map[$key] = [];
|
||||||
|
}
|
||||||
|
unset($preview["cluster_id"]);
|
||||||
|
$previews_map[$key][] = $preview;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all previews to list
|
||||||
foreach ($list as &$face) {
|
foreach ($list as &$face) {
|
||||||
$face["previews"] = $this->timelineQuery->getFacePreviews(
|
$key = $face["id"];
|
||||||
$folder, $face["id"],
|
if (array_key_exists($key, $previews_map)) {
|
||||||
);
|
$face["previews"] = $previews_map[$key];
|
||||||
|
} else {
|
||||||
|
$face["previews"] = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new JSONResponse($list, Http::STATUS_OK);
|
return new JSONResponse($list, Http::STATUS_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @NoAdminRequired
|
|
||||||
*
|
|
||||||
* Get preview objects for a face ID
|
|
||||||
* @return JSONResponse
|
|
||||||
*/
|
|
||||||
public function facePreviews(string $id): JSONResponse {
|
|
||||||
$user = $this->userSession->getUser();
|
|
||||||
if (is_null($user)) {
|
|
||||||
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check faces enabled for this user
|
|
||||||
if (!$this->recognizeIsEnabled()) {
|
|
||||||
return new JSONResponse(["message" => "Recognize app not enabled"], Http::STATUS_PRECONDITION_FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this isn't the timeline folder then things aren't going to work
|
|
||||||
$folder = $this->getRequestFolder();
|
|
||||||
if (is_null($folder)) {
|
|
||||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run actual query
|
|
||||||
$list = $this->timelineQuery->getFacePreviews(
|
|
||||||
$folder, intval($id),
|
|
||||||
);
|
|
||||||
return new JSONResponse($list, Http::STATUS_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoAdminRequired
|
* @NoAdminRequired
|
||||||
*
|
*
|
||||||
|
|
|
@ -53,12 +53,20 @@ trait TimelineQueryFaces {
|
||||||
return $faces;
|
return $faces;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFacePreviews(Folder $folder, int $faceId) {
|
public function getFacePreviews(Folder $folder) {
|
||||||
$query = $this->connection->getQueryBuilder();
|
$query = $this->connection->getQueryBuilder();
|
||||||
|
|
||||||
|
$rowNumber = $query->createFunction('ROW_NUMBER() OVER (PARTITION BY rfd.cluster_id) as n');
|
||||||
|
|
||||||
// SELECT face detections for ID
|
// SELECT face detections for ID
|
||||||
$query->select('rfd.file_id', 'rfd.x', 'rfd.y', 'rfd.width', 'rfd.height', 'f.etag')->from('recognize_face_detections', 'rfd');
|
$query->select(
|
||||||
$query->where($query->expr()->eq('rfd.cluster_id', $query->createNamedParameter($faceId)));
|
'rfd.cluster_id',
|
||||||
|
'rfd.file_id',
|
||||||
|
'rfd.x', 'rfd.y', 'rfd.width', 'rfd.height',
|
||||||
|
'f.etag',
|
||||||
|
$rowNumber,
|
||||||
|
)->from('recognize_face_detections', 'rfd');
|
||||||
|
$query->where($query->expr()->isNotNull('rfd.cluster_id'));
|
||||||
|
|
||||||
// WHERE these photos are memories indexed
|
// WHERE these photos are memories indexed
|
||||||
$query->innerJoin('rfd', 'memories', 'm', $query->expr()->eq('m.fileid', 'rfd.file_id'));
|
$query->innerJoin('rfd', 'memories', 'm', $query->expr()->eq('m.fileid', 'rfd.file_id'));
|
||||||
|
@ -66,20 +74,31 @@ trait TimelineQueryFaces {
|
||||||
// WHERE these photos are in the user's requested folder recursively
|
// WHERE these photos are in the user's requested folder recursively
|
||||||
$query->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, true, false));
|
$query->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, true, false));
|
||||||
|
|
||||||
// MAX 4 results
|
// Make this a sub query
|
||||||
$query->setMaxResults(4);
|
$fun = $query->createFunction('(' . $query->getSQL() . ')');
|
||||||
|
|
||||||
|
// Create outer query
|
||||||
|
$outerQuery = $this->connection->getQueryBuilder();
|
||||||
|
$outerQuery->setParameters($query->getParameters());
|
||||||
|
$outerQuery->select('*')->from($fun, 't');
|
||||||
|
$outerQuery->where($query->expr()->lte('t.n', $outerQuery->createParameter('nc')));
|
||||||
|
$outerQuery->setParameter('nc', 4, IQueryBuilder::PARAM_INT);
|
||||||
|
|
||||||
// FETCH all face detections
|
// FETCH all face detections
|
||||||
$previews = $query->executeQuery()->fetchAll();
|
$previews = $outerQuery->executeQuery()->fetchAll();
|
||||||
|
|
||||||
// Post-process, everthing is a number
|
// Post-process, everthing is a number
|
||||||
foreach($previews as &$row) {
|
foreach($previews as &$row) {
|
||||||
|
$row["cluster_id"] = intval($row["cluster_id"]);
|
||||||
$row["fileid"] = intval($row["file_id"]);
|
$row["fileid"] = intval($row["file_id"]);
|
||||||
unset($row["file_id"]);
|
|
||||||
$row["x"] = floatval($row["x"]);
|
$row["x"] = floatval($row["x"]);
|
||||||
$row["y"] = floatval($row["y"]);
|
$row["y"] = floatval($row["y"]);
|
||||||
$row["width"] = floatval($row["width"]);
|
$row["width"] = floatval($row["width"]);
|
||||||
$row["height"] = floatval($row["height"]);
|
$row["height"] = floatval($row["height"]);
|
||||||
|
|
||||||
|
// remove stale
|
||||||
|
unset($row["file_id"]);
|
||||||
|
unset($row["n"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $previews;
|
return $previews;
|
||||||
|
|
|
@ -96,9 +96,7 @@ export default class Tag extends Mixins(GlobalMixin) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.isFace) {
|
if (!this.isFace) {
|
||||||
await this.refreshPreviewsFace();
|
|
||||||
} else {
|
|
||||||
await this.refreshPreviewsTag();
|
await this.refreshPreviewsTag();
|
||||||
}
|
}
|
||||||
this.processPreviews();
|
this.processPreviews();
|
||||||
|
@ -115,13 +113,6 @@ export default class Tag extends Mixins(GlobalMixin) {
|
||||||
this.data.previews = res.data;
|
this.data.previews = res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Refresh previews for face */
|
|
||||||
async refreshPreviewsFace() {
|
|
||||||
const url = `/apps/memories/api/face-previews/${this.data.faceid}`;
|
|
||||||
const res = await axios.get<IFaceDetection[]>(generateUrl(url));
|
|
||||||
this.data.previews = res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Process previews */
|
/** Process previews */
|
||||||
processPreviews() {
|
processPreviews() {
|
||||||
this.data.previews.forEach((p) => p.flag = 0);
|
this.data.previews.forEach((p) => p.flag = 0);
|
||||||
|
|
Loading…
Reference in New Issue