mirror of
https://github.com/immich-app/immich.git
synced 2026-03-01 18:19:10 +03:00
feat(server): optimize person thumbnail generation (#7513)
* do crop and resize together * redundant `pipelineColorspace` call * formatting * fix rebase * handle orientation * remove unused import * formatting * use oriented dimensions for half size calculation * default case for orientation * simplify orientation code --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
@@ -40,12 +40,13 @@ import {
|
||||
} from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||
import { CropOptions, IMediaRepository } from 'src/interfaces/media.interface';
|
||||
import { CropOptions, IMediaRepository, ImageDimensions } from 'src/interfaces/media.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { Orientation } from 'src/services/metadata.service';
|
||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
@@ -489,11 +490,13 @@ export class PersonService {
|
||||
|
||||
const person = await this.repository.getById(data.id);
|
||||
if (!person?.faceAssetId) {
|
||||
this.logger.error(`Could not generate person thumbnail: person ${person?.id} has no face asset`);
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
const face = await this.repository.getFaceByIdWithAssets(person.faceAssetId);
|
||||
if (face === null) {
|
||||
this.logger.error(`Could not generate person thumbnail: face ${person.faceAssetId} not found`);
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
@@ -507,19 +510,29 @@ export class PersonService {
|
||||
imageHeight,
|
||||
} = face;
|
||||
|
||||
const [asset] = await this.assetRepository.getByIds([assetId]);
|
||||
if (!asset?.previewPath) {
|
||||
const asset = await this.assetRepository.getById(assetId, { exifInfo: true });
|
||||
if (!asset?.exifInfo?.exifImageHeight || !asset.exifInfo.exifImageWidth) {
|
||||
this.logger.error(`Could not generate person thumbnail: asset ${assetId} dimensions are unknown`);
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
this.logger.verbose(`Cropping face for person: ${person.id}`);
|
||||
const thumbnailPath = StorageCore.getPersonThumbnailPath(person);
|
||||
this.storageCore.ensureFolders(thumbnailPath);
|
||||
|
||||
const halfWidth = (x2 - x1) / 2;
|
||||
const halfHeight = (y2 - y1) / 2;
|
||||
const { width: exifWidth, height: exifHeight } = this.withOrientation(asset.exifInfo.orientation as Orientation, {
|
||||
width: asset.exifInfo.exifImageWidth,
|
||||
height: asset.exifInfo.exifImageHeight,
|
||||
});
|
||||
|
||||
const middleX = Math.round(x1 + halfWidth);
|
||||
const middleY = Math.round(y1 + halfHeight);
|
||||
const widthScale = exifWidth / imageWidth;
|
||||
const heightScale = exifHeight / imageHeight;
|
||||
|
||||
const halfWidth = (widthScale * (x2 - x1)) / 2;
|
||||
const halfHeight = (heightScale * (y2 - y1)) / 2;
|
||||
|
||||
const middleX = Math.round(widthScale * x1 + halfWidth);
|
||||
const middleY = Math.round(heightScale * y1 + halfHeight);
|
||||
|
||||
// zoom out 10%
|
||||
const targetHalfSize = Math.floor(Math.max(halfWidth, halfHeight) * 1.1);
|
||||
@@ -528,8 +541,8 @@ export class PersonService {
|
||||
const newHalfSize = Math.min(
|
||||
middleX - Math.max(0, middleX - targetHalfSize),
|
||||
middleY - Math.max(0, middleY - targetHalfSize),
|
||||
Math.min(imageWidth - 1, middleX + targetHalfSize) - middleX,
|
||||
Math.min(imageHeight - 1, middleY + targetHalfSize) - middleY,
|
||||
Math.min(exifWidth - 1, middleX + targetHalfSize) - middleX,
|
||||
Math.min(exifHeight - 1, middleY + targetHalfSize) - middleY,
|
||||
);
|
||||
|
||||
const cropOptions: CropOptions = {
|
||||
@@ -539,15 +552,15 @@ export class PersonService {
|
||||
height: newHalfSize * 2,
|
||||
};
|
||||
|
||||
const croppedOutput = await this.mediaRepository.crop(asset.previewPath, cropOptions);
|
||||
const thumbnailOptions = {
|
||||
format: ImageFormat.JPEG,
|
||||
size: FACE_THUMBNAIL_SIZE,
|
||||
colorspace: image.colorspace,
|
||||
quality: image.quality,
|
||||
crop: cropOptions,
|
||||
} as const;
|
||||
|
||||
await this.mediaRepository.resize(croppedOutput, thumbnailPath, thumbnailOptions);
|
||||
await this.mediaRepository.generateThumbnail(asset.originalPath, thumbnailPath, thumbnailOptions);
|
||||
await this.repository.update({ id: person.id, thumbnailPath });
|
||||
|
||||
return JobStatus.SUCCESS;
|
||||
@@ -614,4 +627,18 @@ export class PersonService {
|
||||
}
|
||||
return person;
|
||||
}
|
||||
|
||||
private withOrientation(orientation: Orientation, { width, height }: ImageDimensions): ImageDimensions {
|
||||
switch (orientation) {
|
||||
case Orientation.MirrorHorizontalRotate270CW:
|
||||
case Orientation.Rotate90CW:
|
||||
case Orientation.MirrorHorizontalRotate90CW:
|
||||
case Orientation.Rotate270CW: {
|
||||
return { width: height, height: width };
|
||||
}
|
||||
default: {
|
||||
return { width, height };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user