mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(smart-crop): overhaul with face detection, social presets, and 3 modes
Replace the confusing 2-mode smart crop with a clear 3-mode system: - Subject Focus: Sharp attention/entropy saliency crop with social media presets - Face Focus: MediaPipe face detection with headshot framing presets - Auto Trim: Border removal with optional pad-to-square Adds detectFaces() to AI package, face preset constants, backward compatibility for old mode names, and comprehensive integration tests.
This commit is contained in:
@@ -15,6 +15,7 @@ def main():
|
||||
|
||||
blur_radius = settings.get("blurRadius", 30)
|
||||
sensitivity = settings.get("sensitivity", 0.5)
|
||||
detect_only = settings.get("detectOnly", False)
|
||||
|
||||
try:
|
||||
emit_progress(10, "Preparing")
|
||||
@@ -64,26 +65,30 @@ def main():
|
||||
w = int(bbox.width * iw)
|
||||
h = int(bbox.height * ih)
|
||||
|
||||
# Add padding around the face
|
||||
pad = int(max(w, h) * 0.1)
|
||||
x1 = max(0, x - pad)
|
||||
y1 = max(0, y - pad)
|
||||
x2 = min(img.width, x + w + pad)
|
||||
y2 = min(img.height, y + h + pad)
|
||||
if not detect_only:
|
||||
# Add padding around the face
|
||||
pad = int(max(w, h) * 0.1)
|
||||
x1 = max(0, x - pad)
|
||||
y1 = max(0, y - pad)
|
||||
x2 = min(img.width, x + w + pad)
|
||||
y2 = min(img.height, y + h + pad)
|
||||
|
||||
face_region = img.crop((x1, y1, x2, y2))
|
||||
blurred = face_region.filter(
|
||||
ImageFilter.GaussianBlur(blur_radius)
|
||||
)
|
||||
img.paste(blurred, (x1, y1))
|
||||
emit_progress(
|
||||
50 + int((i + 1) / num_faces * 40),
|
||||
f"Blurring face {i + 1} of {num_faces}",
|
||||
)
|
||||
|
||||
face_region = img.crop((x1, y1, x2, y2))
|
||||
blurred = face_region.filter(
|
||||
ImageFilter.GaussianBlur(blur_radius)
|
||||
)
|
||||
img.paste(blurred, (x1, y1))
|
||||
faces.append({"x": x, "y": y, "w": w, "h": h})
|
||||
emit_progress(
|
||||
50 + int((i + 1) / num_faces * 40),
|
||||
f"Blurring face {i + 1} of {num_faces}",
|
||||
)
|
||||
|
||||
emit_progress(95, "Saving result")
|
||||
img.save(output_path)
|
||||
if not detect_only:
|
||||
emit_progress(95, "Saving result")
|
||||
img.save(output_path)
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { readFile, unlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { type ProgressCallback, runPythonWithProgress } from "./bridge.js";
|
||||
|
||||
@@ -7,6 +8,10 @@ export interface BlurFacesOptions {
|
||||
sensitivity?: number;
|
||||
}
|
||||
|
||||
export interface DetectFacesOptions {
|
||||
sensitivity?: number;
|
||||
}
|
||||
|
||||
export interface FaceRegion {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -20,6 +25,11 @@ export interface BlurFacesResult {
|
||||
faces: FaceRegion[];
|
||||
}
|
||||
|
||||
export interface DetectFacesResult {
|
||||
facesDetected: number;
|
||||
faces: FaceRegion[];
|
||||
}
|
||||
|
||||
export async function blurFaces(
|
||||
inputBuffer: Buffer,
|
||||
outputDir: string,
|
||||
@@ -48,3 +58,32 @@ export async function blurFaces(
|
||||
faces: result.faces ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
export async function detectFaces(
|
||||
inputBuffer: Buffer,
|
||||
options: DetectFacesOptions = {},
|
||||
onProgress?: ProgressCallback,
|
||||
): Promise<DetectFacesResult> {
|
||||
const inputPath = join(tmpdir(), `detect_faces_${Date.now()}.png`);
|
||||
|
||||
try {
|
||||
await writeFile(inputPath, inputBuffer);
|
||||
const { stdout } = await runPythonWithProgress(
|
||||
"detect_faces.py",
|
||||
[inputPath, "unused", JSON.stringify({ ...options, detectOnly: true })],
|
||||
{ onProgress },
|
||||
);
|
||||
|
||||
const result = JSON.parse(stdout);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Face detection failed");
|
||||
}
|
||||
|
||||
return {
|
||||
facesDetected: result.facesDetected,
|
||||
faces: result.faces ?? [],
|
||||
};
|
||||
} finally {
|
||||
await unlink(inputPath).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { removeBackground } from "./background-removal.js";
|
||||
export { isGpuAvailable, shutdownDispatcher } from "./bridge.js";
|
||||
export { blurFaces } from "./face-detection.js";
|
||||
export type { DetectFacesResult, FaceRegion } from "./face-detection.js";
|
||||
export { blurFaces, detectFaces } from "./face-detection.js";
|
||||
export { inpaint } from "./inpainting.js";
|
||||
export { extractText } from "./ocr.js";
|
||||
export { seamCarve } from "./seam-carving.js";
|
||||
|
||||
Reference in New Issue
Block a user