diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index dfbb56bd1e..86c61969cd 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -44,6 +44,7 @@ import { getDimensions } from 'src/utils/asset.util'; import { ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; import { isFacialRecognitionEnabled } from 'src/utils/misc'; +import { Point, transformPoints } from 'src/utils/transform'; @Injectable() export class PersonService extends BaseService { @@ -634,15 +635,50 @@ export class PersonService extends BaseService { this.requireAccess({ auth, permission: Permission.PersonRead, ids: [dto.personId] }), ]); + const asset = await this.assetRepository.getById(dto.assetId, { edits: true, exifInfo: true }); + if (!asset) { + throw new NotFoundException('Asset not found'); + } + + const edits = asset.edits || []; + + let p1: Point = { x: dto.x, y: dto.y }; + let p2: Point = { x: dto.x + dto.width, y: dto.y + dto.height }; + + // the coordinates received from the client are based on the edited preview image + // we need to convert them to the coordinate space of the original unedited image + if (edits.length > 0) { + if (!asset.width || !asset.height || !asset.exifInfo?.exifImageWidth || !asset.exifInfo?.exifImageHeight) { + throw new BadRequestException('Asset does not have valid dimensions'); + } + + // convert from preview to full dimensions + const scaleFactor = asset.width / dto.imageWidth; + p1 = { x: p1.x * scaleFactor, y: p1.y * scaleFactor }; + p2 = { x: p2.x * scaleFactor, y: p2.y * scaleFactor }; + + const { + points: [invertedP1, invertedP2], + } = transformPoints([p1, p2], edits, { width: asset.width, height: asset.height }, { inverse: true }); + + // make sure p1 is top-left and p2 is bottom-right + p1 = { x: Math.min(invertedP1.x, invertedP2.x), y: Math.min(invertedP1.y, invertedP2.y) }; + p2 = { x: Math.max(invertedP1.x, invertedP2.x), y: Math.max(invertedP1.y, invertedP2.y) }; + + // now coordinates are in original image space + dto.imageHeight = asset.exifInfo.exifImageHeight; + dto.imageWidth = asset.exifInfo.exifImageWidth; + } + await this.personRepository.createAssetFace({ personId: dto.personId, assetId: dto.assetId, imageHeight: dto.imageHeight, imageWidth: dto.imageWidth, - boundingBoxX1: dto.x, - boundingBoxX2: dto.x + dto.width, - boundingBoxY1: dto.y, - boundingBoxY2: dto.y + dto.height, + boundingBoxX1: Math.round(p1.x), + boundingBoxX2: Math.round(p2.x), + boundingBoxY1: Math.round(p1.y), + boundingBoxY2: Math.round(p2.y), sourceType: SourceType.Manual, }); } diff --git a/server/src/utils/transform.ts b/server/src/utils/transform.ts index b57a198cc6..261595eb66 100644 --- a/server/src/utils/transform.ts +++ b/server/src/utils/transform.ts @@ -61,7 +61,7 @@ export const createAffineMatrix = ( ); }; -type Point = { x: number; y: number }; +export type Point = { x: number; y: number }; type TransformState = { points: Point[]; @@ -73,29 +73,33 @@ type TransformState = { * Transforms an array of points through a series of edit operations (crop, rotate, mirror). * Points should be in absolute pixel coordinates relative to the starting dimensions. */ -const transformPoints = ( +export const transformPoints = ( points: Point[], edits: AssetEditActionItem[], startingDimensions: ImageDimensions, + { inverse = false } = {}, ): TransformState => { let currentWidth = startingDimensions.width; let currentHeight = startingDimensions.height; let transformedPoints = [...points]; - // Handle crop first - const crop = edits.find((edit) => edit.action === 'crop'); - if (crop) { - const { x: cropX, y: cropY, width: cropWidth, height: cropHeight } = crop.parameters; - transformedPoints = transformedPoints.map((p) => ({ - x: p.x - cropX, - y: p.y - cropY, - })); - currentWidth = cropWidth; - currentHeight = cropHeight; + // Handle crop first if not inverting + if (!inverse) { + const crop = edits.find((edit) => edit.action === 'crop'); + if (crop) { + const { x: cropX, y: cropY, width: cropWidth, height: cropHeight } = crop.parameters; + transformedPoints = transformedPoints.map((p) => ({ + x: p.x - cropX, + y: p.y - cropY, + })); + currentWidth = cropWidth; + currentHeight = cropHeight; + } } // Apply rotate and mirror transforms - for (const edit of edits) { + const editSequence = inverse ? edits.toReversed() : edits; + for (const edit of editSequence) { let matrix: Matrix = identity(); if (edit.action === 'rotate') { const angleDegrees = edit.parameters.angle; @@ -105,7 +109,7 @@ const transformPoints = ( matrix = compose( translate(newWidth / 2, newHeight / 2), - rotate(angleRadians), + rotate(inverse ? -angleRadians : angleRadians), translate(-currentWidth / 2, -currentHeight / 2), ); @@ -125,6 +129,18 @@ const transformPoints = ( transformedPoints = transformedPoints.map((p) => applyToPoint(matrix, p)); } + // Handle crop last if inverting + if (inverse) { + const crop = edits.find((edit) => edit.action === 'crop'); + if (crop) { + const { x: cropX, y: cropY } = crop.parameters; + transformedPoints = transformedPoints.map((p) => ({ + x: p.x + cropX, + y: p.y + cropY, + })); + } + } + return { points: transformedPoints, currentWidth,