mirror of
https://github.com/xuthus83/pigallery2.git
synced 2025-01-14 14:43:17 +08:00
10x performance improvement for listing faces
This commit is contained in:
parent
402bf0e134
commit
bd60900f7c
@ -1,8 +1,8 @@
|
|||||||
import {NextFunction, Request, Response} from 'express';
|
import {NextFunction, Request, Response} from 'express';
|
||||||
import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
|
import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
|
||||||
import {ObjectManagers} from '../model/ObjectManagers';
|
import {ObjectManagers} from '../model/ObjectManagers';
|
||||||
import {PersonDTO} from '../../common/entities/PersonDTO';
|
import {PersonDTO, PersonWithSampleRegion} from '../../common/entities/PersonDTO';
|
||||||
import {PhotoDTO} from '../../common/entities/PhotoDTO';
|
import {Utils} from '../../common/Utils';
|
||||||
|
|
||||||
|
|
||||||
export class PersonMWs {
|
export class PersonMWs {
|
||||||
@ -24,6 +24,22 @@ export class PersonMWs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async getPerson(req: Request, res: Response, next: NextFunction) {
|
||||||
|
if (!req.params.name) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
req.resultPipe = await ObjectManagers.getInstance()
|
||||||
|
.PersonManager.get(req.params.name as string);
|
||||||
|
return next();
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
return next(new ErrorDTO(ErrorCodes.PERSON_ERROR, 'Error during updating a person', err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static async listPersons(req: Request, res: Response, next: NextFunction) {
|
public static async listPersons(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
req.resultPipe = await ObjectManagers.getInstance()
|
req.resultPipe = await ObjectManagers.getInstance()
|
||||||
@ -37,33 +53,14 @@ export class PersonMWs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static async addSamplePhotoForAll(req: Request, res: Response, next: NextFunction) {
|
public static async cleanUpPersonResults(req: Request, res: Response, next: NextFunction) {
|
||||||
if (!req.resultPipe) {
|
if (!req.resultPipe) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const persons = (req.resultPipe as PersonWithPhoto[]);
|
const persons = Utils.clone(req.resultPipe as PersonWithSampleRegion[]);
|
||||||
const photoMap = await ObjectManagers.getInstance()
|
|
||||||
.PersonManager.getSamplePhotos(persons.map(p => p.name));
|
|
||||||
persons.forEach(p => p.samplePhoto = photoMap[p.name]);
|
|
||||||
|
|
||||||
req.resultPipe = persons;
|
|
||||||
return next();
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
return next(new ErrorDTO(ErrorCodes.PERSON_ERROR, 'Error during adding sample photo for all persons', err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static async removeSamplePhotoForAll(req: Request, res: Response, next: NextFunction) {
|
|
||||||
if (!req.resultPipe) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const persons = (req.resultPipe as PersonWithPhoto[]);
|
|
||||||
for (let i = 0; i < persons.length; i++) {
|
for (let i = 0; i < persons.length; i++) {
|
||||||
delete persons[i].samplePhoto;
|
delete persons[i].sampleRegion;
|
||||||
}
|
}
|
||||||
req.resultPipe = persons;
|
req.resultPipe = persons;
|
||||||
return next();
|
return next();
|
||||||
@ -74,29 +71,6 @@ export class PersonMWs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static async getSamplePhoto(req: Request, res: Response, next: NextFunction) {
|
|
||||||
if (!req.params.name) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
const name = req.params.name;
|
|
||||||
try {
|
|
||||||
const photo = await ObjectManagers.getInstance()
|
|
||||||
.PersonManager.getSamplePhoto(name);
|
|
||||||
|
|
||||||
if (photo === null) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
req.resultPipe = photo;
|
|
||||||
return next();
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
return next(new ErrorDTO(ErrorCodes.PERSON_ERROR, 'Error during getting sample photo for a person', err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface PersonWithPhoto extends PersonDTO {
|
|
||||||
samplePhoto: PhotoDTO;
|
|
||||||
}
|
|
||||||
|
@ -8,9 +8,8 @@ import {ProjectPath} from '../../ProjectPath';
|
|||||||
import {Config} from '../../../common/config/private/Config';
|
import {Config} from '../../../common/config/private/Config';
|
||||||
import {ThumbnailSourceType} from '../../model/threading/PhotoWorker';
|
import {ThumbnailSourceType} from '../../model/threading/PhotoWorker';
|
||||||
import {MediaDTO} from '../../../common/entities/MediaDTO';
|
import {MediaDTO} from '../../../common/entities/MediaDTO';
|
||||||
import {PersonWithPhoto} from '../PersonMWs';
|
|
||||||
import {PhotoProcessing} from '../../model/fileprocessing/PhotoProcessing';
|
import {PhotoProcessing} from '../../model/fileprocessing/PhotoProcessing';
|
||||||
import {PhotoDTO} from '../../../common/entities/PhotoDTO';
|
import {PersonWithSampleRegion} from '../../../common/entities/PersonDTO';
|
||||||
|
|
||||||
|
|
||||||
export class ThumbnailGeneratorMWs {
|
export class ThumbnailGeneratorMWs {
|
||||||
@ -49,15 +48,15 @@ export class ThumbnailGeneratorMWs {
|
|||||||
try {
|
try {
|
||||||
const size: number = Config.Client.Media.Thumbnail.personThumbnailSize;
|
const size: number = Config.Client.Media.Thumbnail.personThumbnailSize;
|
||||||
|
|
||||||
const persons: PersonWithPhoto[] = req.resultPipe;
|
const persons: PersonWithSampleRegion[] = req.resultPipe;
|
||||||
for (let i = 0; i < persons.length; i++) {
|
for (let i = 0; i < persons.length; i++) {
|
||||||
// load parameters
|
// load parameters
|
||||||
const mediaPath = path.join(ProjectPath.ImageFolder,
|
const mediaPath = path.join(ProjectPath.ImageFolder,
|
||||||
persons[i].samplePhoto.directory.path,
|
persons[i].sampleRegion.media.directory.path,
|
||||||
persons[i].samplePhoto.directory.name, persons[i].samplePhoto.name);
|
persons[i].sampleRegion.media.directory.name, persons[i].sampleRegion.media.name);
|
||||||
|
|
||||||
// generate thumbnail path
|
// generate thumbnail path
|
||||||
const thPath = PhotoProcessing.generatePersonThumbnailPath(mediaPath, persons[i].samplePhoto.metadata.faces[0], size);
|
const thPath = PhotoProcessing.generatePersonThumbnailPath(mediaPath, persons[i].sampleRegion, size);
|
||||||
|
|
||||||
persons[i].readyThumbnail = fs.existsSync(thPath);
|
persons[i].readyThumbnail = fs.existsSync(thPath);
|
||||||
}
|
}
|
||||||
@ -76,14 +75,14 @@ export class ThumbnailGeneratorMWs {
|
|||||||
if (!req.resultPipe) {
|
if (!req.resultPipe) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
const photo: PhotoDTO = req.resultPipe;
|
const person: PersonWithSampleRegion = req.resultPipe;
|
||||||
try {
|
try {
|
||||||
req.resultPipe = await PhotoProcessing.generatePersonThumbnail(photo);
|
req.resultPipe = await PhotoProcessing.generatePersonThumbnail(person);
|
||||||
return next();
|
return next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return next(new ErrorDTO(ErrorCodes.THUMBNAIL_GENERATION_ERROR,
|
return next(new ErrorDTO(ErrorCodes.THUMBNAIL_GENERATION_ERROR,
|
||||||
'Error during generating face thumbnail: ' + photo.name, error.toString()));
|
'Error during generating face thumbnail: ' + person.name, error.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
import {PersonEntry} from '../sql/enitites/PersonEntry';
|
import {PersonEntry} from '../sql/enitites/PersonEntry';
|
||||||
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
|
||||||
import {PersonDTO} from '../../../../common/entities/PersonDTO';
|
import {PersonDTO} from '../../../../common/entities/PersonDTO';
|
||||||
|
|
||||||
export interface IPersonManager {
|
export interface IPersonManager {
|
||||||
getAll(): Promise<PersonEntry[]>;
|
getAll(): Promise<PersonEntry[]>;
|
||||||
|
|
||||||
getSamplePhoto(name: string): Promise<PhotoDTO>;
|
|
||||||
|
|
||||||
getSamplePhotos(names: string[]): Promise<{ [key: string]: PhotoDTO }>;
|
|
||||||
|
|
||||||
get(name: string): Promise<PersonEntry>;
|
get(name: string): Promise<PersonEntry>;
|
||||||
|
|
||||||
saveAll(names: string[]): Promise<void>;
|
saveAll(names: string[]): Promise<void>;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import {IPersonManager} from '../interfaces/IPersonManager';
|
import {IPersonManager} from '../interfaces/IPersonManager';
|
||||||
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
|
||||||
import {PersonDTO} from '../../../../common/entities/PersonDTO';
|
import {PersonDTO} from '../../../../common/entities/PersonDTO';
|
||||||
|
|
||||||
export class PersonManager implements IPersonManager {
|
export class PersonManager implements IPersonManager {
|
||||||
@ -8,14 +7,6 @@ export class PersonManager implements IPersonManager {
|
|||||||
throw new Error('not supported by memory DB');
|
throw new Error('not supported by memory DB');
|
||||||
}
|
}
|
||||||
|
|
||||||
getSamplePhoto(name: string): Promise<PhotoDTO> {
|
|
||||||
throw new Error('not supported by memory DB');
|
|
||||||
}
|
|
||||||
|
|
||||||
getSamplePhotos(names: string[]): Promise<{ [key: string]: PhotoDTO }> {
|
|
||||||
throw new Error('not supported by memory DB');
|
|
||||||
}
|
|
||||||
|
|
||||||
get(name: string): Promise<any> {
|
get(name: string): Promise<any> {
|
||||||
throw new Error('not supported by memory DB');
|
throw new Error('not supported by memory DB');
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
import {SQLConnection} from './SQLConnection';
|
import {SQLConnection} from './SQLConnection';
|
||||||
import {PersonEntry} from './enitites/PersonEntry';
|
import {PersonEntry} from './enitites/PersonEntry';
|
||||||
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
|
||||||
import {MediaEntity} from './enitites/MediaEntity';
|
|
||||||
import {FaceRegionEntry} from './enitites/FaceRegionEntry';
|
import {FaceRegionEntry} from './enitites/FaceRegionEntry';
|
||||||
import {PersonDTO} from '../../../../common/entities/PersonDTO';
|
import {PersonDTO} from '../../../../common/entities/PersonDTO';
|
||||||
import {Utils} from '../../../../common/Utils';
|
|
||||||
import {SelectQueryBuilder} from 'typeorm';
|
|
||||||
import {ISQLPersonManager} from './IPersonManager';
|
import {ISQLPersonManager} from './IPersonManager';
|
||||||
|
|
||||||
|
|
||||||
export class PersonManager implements ISQLPersonManager {
|
export class PersonManager implements ISQLPersonManager {
|
||||||
samplePhotos: { [key: string]: PhotoDTO } = {};
|
// samplePhotos: { [key: string]: PhotoDTO } = {};
|
||||||
persons: PersonEntry[] = [];
|
persons: PersonEntry[] = null;
|
||||||
|
|
||||||
async updatePerson(name: string, partialPerson: PersonDTO): Promise<PersonEntry> {
|
async updatePerson(name: string, partialPerson: PersonDTO): Promise<PersonEntry> {
|
||||||
const connection = await SQLConnection.getConnection();
|
const connection = await SQLConnection.getConnection();
|
||||||
@ -34,92 +30,44 @@ export class PersonManager implements ISQLPersonManager {
|
|||||||
return person;
|
return person;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSamplePhoto(name: string): Promise<PhotoDTO> {
|
|
||||||
return (await this.getSamplePhotos([name]))[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private async loadAll(): Promise<void> {
|
||||||
async getSamplePhotos(names: string[]): Promise<{ [key: string]: PhotoDTO }> {
|
|
||||||
const hasAll = names.reduce((prev, name) => prev && !!this.samplePhotos[name], true);
|
|
||||||
if (!hasAll) {
|
|
||||||
const connection = await SQLConnection.getConnection();
|
|
||||||
const namesObj: any = {};
|
|
||||||
let queryStr = '';
|
|
||||||
names.forEach((n, i) => {
|
|
||||||
if (i > 0) {
|
|
||||||
queryStr += ', ';
|
|
||||||
}
|
|
||||||
queryStr += ':n' + i + ' COLLATE utf8_general_ci';
|
|
||||||
namesObj['n' + i] = n;
|
|
||||||
});
|
|
||||||
const query: SelectQueryBuilder<MediaEntity> = await (connection
|
|
||||||
.getRepository(MediaEntity)
|
|
||||||
.createQueryBuilder('media') as SelectQueryBuilder<MediaEntity>)
|
|
||||||
.select(['media.name', 'media.id', 'person.name', 'directory.name',
|
|
||||||
'directory.path', 'media.metadata.size.width', 'media.metadata.size.height'])
|
|
||||||
.leftJoin('media.directory', 'directory')
|
|
||||||
.leftJoinAndSelect('media.metadata.faces', 'faces')
|
|
||||||
.leftJoin('faces.person', 'person')
|
|
||||||
.groupBy('person.name, media.name, media.id, directory.name, faces.id');
|
|
||||||
// TODO: improve it. SQLITE does not support case-insensitive special characters like ÁÉÚŐ
|
|
||||||
for (let i = 0; i < names.length; ++i) {
|
|
||||||
const opt: any = {};
|
|
||||||
opt['n' + i] = names[i];
|
|
||||||
query.orWhere(`person.name LIKE :n${i} COLLATE utf8_general_ci`, opt);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawAndEntities = await query.getRawAndEntities();
|
|
||||||
for (let i = 0; i < rawAndEntities.raw.length; ++i) {
|
|
||||||
this.samplePhotos[rawAndEntities.raw[i].person_name.toLowerCase()] =
|
|
||||||
Utils.clone(rawAndEntities.entities.find(m => m.name === rawAndEntities.raw[i].media_name));
|
|
||||||
this.samplePhotos[rawAndEntities.raw[i].person_name.toLowerCase()].metadata.faces =
|
|
||||||
[FaceRegionEntry.fromRawToDTO(rawAndEntities.raw[i])];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const photoMap: { [key: string]: PhotoDTO } = {};
|
|
||||||
names.forEach(n => photoMap[n] = this.samplePhotos[n.toLowerCase()]);
|
|
||||||
return photoMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async loadAll(): Promise<void> {
|
|
||||||
const connection = await SQLConnection.getConnection();
|
const connection = await SQLConnection.getConnection();
|
||||||
const personRepository = connection.getRepository(PersonEntry);
|
const personRepository = connection.getRepository(PersonEntry);
|
||||||
this.persons = await personRepository.find();
|
this.persons = await personRepository.find({
|
||||||
|
relations: ['sampleRegion',
|
||||||
|
'sampleRegion.media',
|
||||||
|
'sampleRegion.media.directory']
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(): Promise<PersonEntry[]> {
|
public async getAll(): Promise<PersonEntry[]> {
|
||||||
await this.loadAll();
|
if (this.persons === null) {
|
||||||
|
await this.loadAll();
|
||||||
|
}
|
||||||
return this.persons;
|
return this.persons;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async countFaces(): Promise<number> {
|
/**
|
||||||
|
* Used for statistic
|
||||||
|
*/
|
||||||
|
public async countFaces(): Promise<number> {
|
||||||
const connection = await SQLConnection.getConnection();
|
const connection = await SQLConnection.getConnection();
|
||||||
return await connection.getRepository(FaceRegionEntry)
|
return await connection.getRepository(FaceRegionEntry)
|
||||||
.createQueryBuilder('faceRegion')
|
.createQueryBuilder('faceRegion')
|
||||||
.getCount();
|
.getCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(name: string): Promise<PersonEntry> {
|
public async get(name: string): Promise<PersonEntry> {
|
||||||
|
if (this.persons === null) {
|
||||||
let person = this.persons.find(p => p.name === name);
|
await this.loadAll();
|
||||||
if (!person) {
|
|
||||||
const connection = await SQLConnection.getConnection();
|
|
||||||
const personRepository = connection.getRepository(PersonEntry);
|
|
||||||
person = await personRepository.findOne({name: name});
|
|
||||||
if (!person) {
|
|
||||||
person = await personRepository.save(<PersonEntry>{name: name});
|
|
||||||
}
|
|
||||||
this.persons.push(person);
|
|
||||||
}
|
}
|
||||||
return person;
|
return this.persons.find(p => p.name === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async saveAll(names: string[]): Promise<void> {
|
public async saveAll(names: string[]): Promise<void> {
|
||||||
const toSave: { name: string }[] = [];
|
const toSave: { name: string }[] = [];
|
||||||
const connection = await SQLConnection.getConnection();
|
const connection = await SQLConnection.getConnection();
|
||||||
const personRepository = connection.getRepository(PersonEntry);
|
const personRepository = connection.getRepository(PersonEntry);
|
||||||
@ -137,7 +85,7 @@ export class PersonManager implements ISQLPersonManager {
|
|||||||
for (let i = 0; i < toSave.length / 200; i++) {
|
for (let i = 0; i < toSave.length / 200; i++) {
|
||||||
await personRepository.insert(toSave.slice(i * 200, (i + 1) * 200));
|
await personRepository.insert(toSave.slice(i * 200, (i + 1) * 200));
|
||||||
}
|
}
|
||||||
this.persons = await personRepository.find();
|
await this.loadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -145,10 +93,11 @@ export class PersonManager implements ISQLPersonManager {
|
|||||||
|
|
||||||
public async onGalleryIndexUpdate() {
|
public async onGalleryIndexUpdate() {
|
||||||
await this.updateCounts();
|
await this.updateCounts();
|
||||||
this.samplePhotos = {};
|
await this.updateSamplePhotos();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateCounts() {
|
|
||||||
|
private async updateCounts() {
|
||||||
const connection = await SQLConnection.getConnection();
|
const connection = await SQLConnection.getConnection();
|
||||||
await connection.query('update person_entry set count = ' +
|
await connection.query('update person_entry set count = ' +
|
||||||
' (select COUNT(1) from face_region_entry where face_region_entry.personId = person_entry.id)');
|
' (select COUNT(1) from face_region_entry where face_region_entry.personId = person_entry.id)');
|
||||||
@ -161,4 +110,15 @@ export class PersonManager implements ISQLPersonManager {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async updateSamplePhotos() {
|
||||||
|
const connection = await SQLConnection.getConnection();
|
||||||
|
await connection.query('update person_entry set sampleRegionId = ' +
|
||||||
|
'(Select face_region_entry.id from media_entity ' +
|
||||||
|
'left join face_region_entry on media_entity.id = face_region_entry.mediaId ' +
|
||||||
|
'where face_region_entry.personId=person_entry.id ' +
|
||||||
|
'order by media_entity.metadataCreationdate desc ' +
|
||||||
|
'limit 1)');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -26,11 +26,9 @@ export class FaceRegionEntry {
|
|||||||
@Column(type => FaceRegionBoxEntry)
|
@Column(type => FaceRegionBoxEntry)
|
||||||
box: FaceRegionBoxEntry;
|
box: FaceRegionBoxEntry;
|
||||||
|
|
||||||
// @PrimaryColumn('int')
|
|
||||||
@ManyToOne(type => MediaEntity, media => media.metadata.faces, {onDelete: 'CASCADE', nullable: false})
|
@ManyToOne(type => MediaEntity, media => media.metadata.faces, {onDelete: 'CASCADE', nullable: false})
|
||||||
media: MediaEntity;
|
media: MediaEntity;
|
||||||
|
|
||||||
// @PrimaryColumn('int')
|
|
||||||
@ManyToOne(type => PersonEntry, person => person.faces, {onDelete: 'CASCADE', nullable: false})
|
@ManyToOne(type => PersonEntry, person => person.faces, {onDelete: 'CASCADE', nullable: false})
|
||||||
person: PersonEntry;
|
person: PersonEntry;
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import {Column, Entity, Index, OneToMany, PrimaryGeneratedColumn, Unique} from 'typeorm';
|
import {Column, Entity, Index, ManyToOne, OneToMany, PrimaryGeneratedColumn, Unique} from 'typeorm';
|
||||||
import {FaceRegionEntry} from './FaceRegionEntry';
|
import {FaceRegionEntry} from './FaceRegionEntry';
|
||||||
import {PersonDTO} from '../../../../../common/entities/PersonDTO';
|
|
||||||
import {columnCharsetCS} from './EntityUtils';
|
import {columnCharsetCS} from './EntityUtils';
|
||||||
|
import {PersonWithSampleRegion} from '../../../../../common/entities/PersonDTO';
|
||||||
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Unique(['name'])
|
@Unique(['name'])
|
||||||
export class PersonEntry implements PersonDTO {
|
export class PersonEntry implements PersonWithSampleRegion {
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@PrimaryGeneratedColumn({unsigned: true})
|
@PrimaryGeneratedColumn({unsigned: true})
|
||||||
@ -24,5 +24,8 @@ export class PersonEntry implements PersonDTO {
|
|||||||
@OneToMany(type => FaceRegionEntry, faceRegion => faceRegion.person)
|
@OneToMany(type => FaceRegionEntry, faceRegion => faceRegion.person)
|
||||||
public faces: FaceRegionEntry[];
|
public faces: FaceRegionEntry[];
|
||||||
|
|
||||||
|
@ManyToOne(type => FaceRegionEntry, {onDelete: 'SET NULL', nullable: true})
|
||||||
|
sampleRegion: FaceRegionEntry;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import {ITaskExecuter, TaskExecuter} from '../threading/TaskExecuter';
|
|||||||
import {FaceRegion, PhotoDTO} from '../../../common/entities/PhotoDTO';
|
import {FaceRegion, PhotoDTO} from '../../../common/entities/PhotoDTO';
|
||||||
import {SupportedFormats} from '../../../common/SupportedFormats';
|
import {SupportedFormats} from '../../../common/SupportedFormats';
|
||||||
import {ServerConfig} from '../../../common/config/private/PrivateConfig';
|
import {ServerConfig} from '../../../common/config/private/PrivateConfig';
|
||||||
|
import {PersonWithSampleRegion} from '../../../common/entities/PersonDTO';
|
||||||
|
|
||||||
|
|
||||||
export class PhotoProcessing {
|
export class PhotoProcessing {
|
||||||
@ -45,19 +46,14 @@ export class PhotoProcessing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static async generatePersonThumbnail(photo: PhotoDTO) {
|
public static async generatePersonThumbnail(person: PersonWithSampleRegion) {
|
||||||
|
|
||||||
// load parameters
|
|
||||||
|
|
||||||
if (!photo.metadata.faces || photo.metadata.faces.length !== 1) {
|
|
||||||
throw new Error('Photo does not contain a face');
|
|
||||||
}
|
|
||||||
|
|
||||||
// load parameters
|
// load parameters
|
||||||
|
const photo: PhotoDTO = person.sampleRegion.media;
|
||||||
const mediaPath = path.join(ProjectPath.ImageFolder, photo.directory.path, photo.directory.name, photo.name);
|
const mediaPath = path.join(ProjectPath.ImageFolder, photo.directory.path, photo.directory.name, photo.name);
|
||||||
const size: number = Config.Client.Media.Thumbnail.personThumbnailSize;
|
const size: number = Config.Client.Media.Thumbnail.personThumbnailSize;
|
||||||
// generate thumbnail path
|
// generate thumbnail path
|
||||||
const thPath = PhotoProcessing.generatePersonThumbnailPath(mediaPath, photo.metadata.faces[0], size);
|
const thPath = PhotoProcessing.generatePersonThumbnailPath(mediaPath, person.sampleRegion, size);
|
||||||
|
|
||||||
|
|
||||||
// check if thumbnail already exist
|
// check if thumbnail already exist
|
||||||
@ -69,8 +65,8 @@ export class PhotoProcessing {
|
|||||||
|
|
||||||
|
|
||||||
const margin = {
|
const margin = {
|
||||||
x: Math.round(photo.metadata.faces[0].box.width * (Config.Server.Media.Thumbnail.personFaceMargin)),
|
x: Math.round(person.sampleRegion.box.width * (Config.Server.Media.Thumbnail.personFaceMargin)),
|
||||||
y: Math.round(photo.metadata.faces[0].box.height * (Config.Server.Media.Thumbnail.personFaceMargin))
|
y: Math.round(person.sampleRegion.box.height * (Config.Server.Media.Thumbnail.personFaceMargin))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -82,10 +78,10 @@ export class PhotoProcessing {
|
|||||||
outPath: thPath,
|
outPath: thPath,
|
||||||
makeSquare: false,
|
makeSquare: false,
|
||||||
cut: {
|
cut: {
|
||||||
left: Math.round(Math.max(0, photo.metadata.faces[0].box.left - margin.x / 2)),
|
left: Math.round(Math.max(0, person.sampleRegion.box.left - margin.x / 2)),
|
||||||
top: Math.round(Math.max(0, photo.metadata.faces[0].box.top - margin.y / 2)),
|
top: Math.round(Math.max(0, person.sampleRegion.box.top - margin.y / 2)),
|
||||||
width: photo.metadata.faces[0].box.width + margin.x,
|
width: person.sampleRegion.box.width + margin.x,
|
||||||
height: photo.metadata.faces[0].box.height + margin.y
|
height: person.sampleRegion.box.height + margin.y
|
||||||
},
|
},
|
||||||
qualityPriority: Config.Server.Media.Thumbnail.qualityPriority
|
qualityPriority: Config.Server.Media.Thumbnail.qualityPriority
|
||||||
};
|
};
|
||||||
|
@ -38,9 +38,9 @@ export class PersonRouter {
|
|||||||
|
|
||||||
// specific part
|
// specific part
|
||||||
PersonMWs.listPersons,
|
PersonMWs.listPersons,
|
||||||
PersonMWs.addSamplePhotoForAll,
|
// PersonMWs.addSamplePhotoForAll,
|
||||||
ThumbnailGeneratorMWs.addThumbnailInfoForPersons,
|
ThumbnailGeneratorMWs.addThumbnailInfoForPersons,
|
||||||
PersonMWs.removeSamplePhotoForAll,
|
PersonMWs.cleanUpPersonResults,
|
||||||
RenderingMWs.renderResult
|
RenderingMWs.renderResult
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -53,7 +53,7 @@ export class PersonRouter {
|
|||||||
VersionMWs.injectGalleryVersion,
|
VersionMWs.injectGalleryVersion,
|
||||||
|
|
||||||
// specific part
|
// specific part
|
||||||
PersonMWs.getSamplePhoto,
|
PersonMWs.getPerson,
|
||||||
ThumbnailGeneratorMWs.generatePersonThumbnail,
|
ThumbnailGeneratorMWs.generatePersonThumbnail,
|
||||||
RenderingMWs.renderFile
|
RenderingMWs.renderFile
|
||||||
);
|
);
|
||||||
|
@ -1 +1 @@
|
|||||||
export const DataStructureVersion = 16;
|
export const DataStructureVersion = 17;
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
import {FaceRegionEntry} from '../../backend/model/database/sql/enitites/FaceRegionEntry';
|
||||||
|
|
||||||
|
export interface PersonWithSampleRegion extends PersonDTO {
|
||||||
|
sampleRegion: FaceRegionEntry;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PersonDTO {
|
export interface PersonDTO {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -9,6 +9,8 @@ import {SQLConnection} from '../../../../../src/backend/model/database/sql/SQLCo
|
|||||||
import {PhotoEntity} from '../../../../../src/backend/model/database/sql/enitites/PhotoEntity';
|
import {PhotoEntity} from '../../../../../src/backend/model/database/sql/enitites/PhotoEntity';
|
||||||
import {DirectoryEntity} from '../../../../../src/backend/model/database/sql/enitites/DirectoryEntity';
|
import {DirectoryEntity} from '../../../../../src/backend/model/database/sql/enitites/DirectoryEntity';
|
||||||
import {VideoEntity} from '../../../../../src/backend/model/database/sql/enitites/VideoEntity';
|
import {VideoEntity} from '../../../../../src/backend/model/database/sql/enitites/VideoEntity';
|
||||||
|
import {Utils} from '../../../../../src/common/Utils';
|
||||||
|
import {PersonWithSampleRegion} from '../../../../../src/common/entities/PersonDTO';
|
||||||
|
|
||||||
|
|
||||||
// to help WebStorm to handle the test cases
|
// to help WebStorm to handle the test cases
|
||||||
@ -23,12 +25,13 @@ describe('PersonManager', (sqlHelper: SQLTestHelper) => {
|
|||||||
|
|
||||||
|
|
||||||
const dir = TestHelper.getDirectoryEntry();
|
const dir = TestHelper.getDirectoryEntry();
|
||||||
const p = TestHelper.getPhotoEntry1(dir);
|
let p = TestHelper.getPhotoEntry1(dir);
|
||||||
const p2 = TestHelper.getPhotoEntry2(dir);
|
let p2 = TestHelper.getPhotoEntry2(dir);
|
||||||
const p_faceLess = TestHelper.getPhotoEntry2(dir);
|
let p_faceLess = TestHelper.getPhotoEntry2(dir);
|
||||||
delete p_faceLess.metadata.faces;
|
delete p_faceLess.metadata.faces;
|
||||||
p_faceLess.name = 'fl';
|
p_faceLess.name = 'fl';
|
||||||
const v = TestHelper.getVideoEntry1(dir);
|
const v = TestHelper.getVideoEntry1(dir);
|
||||||
|
const savedPerson: PersonWithSampleRegion[] = [];
|
||||||
|
|
||||||
const setUpSqlDB = async () => {
|
const setUpSqlDB = async () => {
|
||||||
await sqlHelper.initDB();
|
await sqlHelper.initDB();
|
||||||
@ -36,25 +39,28 @@ describe('PersonManager', (sqlHelper: SQLTestHelper) => {
|
|||||||
const savePhoto = async (photo: PhotoDTO) => {
|
const savePhoto = async (photo: PhotoDTO) => {
|
||||||
const savedPhoto = await pr.save(photo);
|
const savedPhoto = await pr.save(photo);
|
||||||
if (!photo.metadata.faces) {
|
if (!photo.metadata.faces) {
|
||||||
return;
|
return savedPhoto;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < photo.metadata.faces.length; i++) {
|
for (let i = 0; i < photo.metadata.faces.length; i++) {
|
||||||
const face = photo.metadata.faces[i];
|
const face = photo.metadata.faces[i];
|
||||||
const person = await conn.getRepository(PersonEntry).save({name: face.name});
|
const person = await conn.getRepository(PersonEntry).save({name: face.name});
|
||||||
await conn.getRepository(FaceRegionEntry).save({box: face.box, person: person, media: savedPhoto});
|
savedPhoto.metadata.faces[i] = await conn.getRepository(FaceRegionEntry).save({box: face.box, person: person, media: savedPhoto});
|
||||||
|
savedPerson.push(person);
|
||||||
}
|
}
|
||||||
|
return savedPhoto;
|
||||||
};
|
};
|
||||||
const conn = await SQLConnection.getConnection();
|
const conn = await SQLConnection.getConnection();
|
||||||
|
|
||||||
const pr = conn.getRepository(PhotoEntity);
|
const pr = conn.getRepository(PhotoEntity);
|
||||||
|
|
||||||
await conn.getRepository(DirectoryEntity).save(p.directory);
|
await conn.getRepository(DirectoryEntity).save(p.directory);
|
||||||
await savePhoto(p);
|
p = await savePhoto(p);
|
||||||
await savePhoto(p2);
|
console.log(p.id);
|
||||||
await savePhoto(p_faceLess);
|
p2 = await savePhoto(p2);
|
||||||
|
p_faceLess = await savePhoto(p_faceLess);
|
||||||
|
|
||||||
await conn.getRepository(VideoEntity).save(v);
|
await conn.getRepository(VideoEntity).save(v);
|
||||||
|
await (new PersonManager()).onGalleryIndexUpdate();
|
||||||
await SQLConnection.close();
|
await SQLConnection.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,46 +74,20 @@ describe('PersonManager', (sqlHelper: SQLTestHelper) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const mapPhoto = (photo: PhotoDTO) => {
|
it('should get person', async () => {
|
||||||
const map: { [key: string]: PhotoDTO } = {};
|
|
||||||
photo.metadata.faces.forEach(face => {
|
|
||||||
map[face.name] = <any>{
|
|
||||||
id: photo.id,
|
|
||||||
name: photo.name,
|
|
||||||
directory: {
|
|
||||||
path: photo.directory.path,
|
|
||||||
name: photo.directory.name,
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
size: photo.metadata.size,
|
|
||||||
faces: [photo.metadata.faces.find(f => f.name === face.name)]
|
|
||||||
},
|
|
||||||
readyIcon: false,
|
|
||||||
readyThumbnails: []
|
|
||||||
};
|
|
||||||
|
|
||||||
});
|
|
||||||
return map;
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should get sample photos', async () => {
|
|
||||||
const pm = new PersonManager();
|
const pm = new PersonManager();
|
||||||
const map = mapPhoto(p);
|
const person = Utils.clone(savedPerson[0]);
|
||||||
expect(await pm.getSamplePhotos(p.metadata.faces.map(f => f.name))).to.deep.equal(map);
|
person.sampleRegion = <any>{
|
||||||
|
id: p.metadata.faces[0].id,
|
||||||
|
box: p.metadata.faces[0].box
|
||||||
|
};
|
||||||
|
const tmp = p.metadata.faces;
|
||||||
|
delete p.metadata.faces;
|
||||||
|
person.sampleRegion.media = Utils.clone(p);
|
||||||
|
p.metadata.faces = tmp;
|
||||||
|
person.count = 1;
|
||||||
|
expect(await pm.get(person.name)).to.deep.equal(person);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should get sample photos case insensitive', async () => {
|
|
||||||
const pm = new PersonManager();
|
|
||||||
const map = mapPhoto(p);
|
|
||||||
for (const k of Object.keys(map)) {
|
|
||||||
if (k.toLowerCase() !== k) {
|
|
||||||
map[k.toLowerCase()] = map[k];
|
|
||||||
delete map[k];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(await pm.getSamplePhotos(p.metadata.faces.map(f => f.name.toLowerCase()))).to.deep.equal(map);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user