Files
immich/server/src/utils/editor.ts
2026-01-09 17:59:52 -05:00

108 lines
2.9 KiB
TypeScript

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),
};
};