mirror of
https://github.com/immich-app/immich.git
synced 2026-05-25 00:52:31 -04:00
feat: image editing (#24155)
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { AssetFace } from 'src/database';
|
||||
import { AssetOcrResponseDto } from 'src/dtos/ocr.dto';
|
||||
import { ImageDimensions } from 'src/types';
|
||||
|
||||
type BoundingBox = {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
};
|
||||
|
||||
export const boundingBoxOverlap = (boxA: BoundingBox, boxB: BoundingBox) => {
|
||||
const overlapX1 = Math.max(boxA.x1, boxB.x1);
|
||||
const overlapY1 = Math.max(boxA.y1, boxB.y1);
|
||||
const overlapX2 = Math.min(boxA.x2, boxB.x2);
|
||||
const overlapY2 = Math.min(boxA.y2, boxB.y2);
|
||||
|
||||
const overlapArea = Math.max(0, overlapX2 - overlapX1) * Math.max(0, overlapY2 - overlapY1);
|
||||
const faceArea = (boxA.x2 - boxA.x1) * (boxA.y2 - boxA.y1);
|
||||
return overlapArea / faceArea;
|
||||
};
|
||||
|
||||
const scale = (box: BoundingBox, target: ImageDimensions, source?: ImageDimensions) => {
|
||||
const { width: sourceWidth = 1, height: sourceHeight = 1 } = source ?? {};
|
||||
|
||||
return {
|
||||
x1: (box.x1 / sourceWidth) * target.width,
|
||||
y1: (box.y1 / sourceHeight) * target.height,
|
||||
x2: (box.x2 / sourceWidth) * target.width,
|
||||
y2: (box.y2 / sourceHeight) * target.height,
|
||||
};
|
||||
};
|
||||
|
||||
export const checkFaceVisibility = (
|
||||
faces: AssetFace[],
|
||||
originalAssetDimensions: ImageDimensions,
|
||||
crop?: BoundingBox,
|
||||
): { visible: AssetFace[]; hidden: AssetFace[] } => {
|
||||
if (!crop) {
|
||||
return {
|
||||
visible: faces.filter((face) => !face.isVisible),
|
||||
hidden: [],
|
||||
};
|
||||
}
|
||||
|
||||
const status = faces.map((face) => {
|
||||
const scaledFace = scale(
|
||||
{
|
||||
x1: face.boundingBoxX1,
|
||||
y1: face.boundingBoxY1,
|
||||
x2: face.boundingBoxX2,
|
||||
y2: face.boundingBoxY2,
|
||||
},
|
||||
originalAssetDimensions,
|
||||
{ width: face.imageWidth, height: face.imageHeight },
|
||||
);
|
||||
|
||||
const overlapPercentage = boundingBoxOverlap(scaledFace, crop);
|
||||
|
||||
return {
|
||||
face,
|
||||
isVisible: overlapPercentage >= 0.5,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
visible: status.filter((s) => s.isVisible).map((s) => s.face),
|
||||
hidden: status.filter((s) => !s.isVisible).map((s) => s.face),
|
||||
};
|
||||
};
|
||||
|
||||
export const checkOcrVisibility = (
|
||||
ocrs: (AssetOcrResponseDto & { isVisible: boolean })[],
|
||||
originalAssetDimensions: ImageDimensions,
|
||||
crop?: BoundingBox,
|
||||
): { visible: AssetOcrResponseDto[]; hidden: AssetOcrResponseDto[] } => {
|
||||
if (!crop) {
|
||||
return {
|
||||
visible: ocrs.filter((ocr) => !ocr.isVisible),
|
||||
hidden: [],
|
||||
};
|
||||
}
|
||||
|
||||
const status = ocrs.map((ocr) => {
|
||||
const ocrBox = scale(
|
||||
{
|
||||
x1: Math.min(ocr.x1, ocr.x2, ocr.x3, ocr.x4),
|
||||
y1: Math.min(ocr.y1, ocr.y2, ocr.y3, ocr.y4),
|
||||
x2: Math.max(ocr.x1, ocr.x2, ocr.x3, ocr.x4),
|
||||
y2: Math.max(ocr.y1, ocr.y2, ocr.y3, ocr.y4),
|
||||
},
|
||||
originalAssetDimensions,
|
||||
);
|
||||
|
||||
const overlapPercentage = boundingBoxOverlap(ocrBox, crop);
|
||||
|
||||
return {
|
||||
ocr,
|
||||
isVisible: overlapPercentage >= 0.5,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
visible: status.filter((s) => s.isVisible).map((s) => s.ocr),
|
||||
hidden: status.filter((s) => !s.isVisible).map((s) => s.ocr),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user