Gesichtserkennung → Verschleiern oder Vergrößern pro Gesicht (Rechteck / Kreis / Ring)

Erkennt Gesichter mit einem auf dem Gerät laufenden KI-Modell und verschleiert sie (Datenschutz) oder vergrößert sie (Lupe / Spotlight). ACTION = blur oder magnify; SHAPE = rect oder circle (Kreis-Modus nutzt einen Vektor-Canvas als Luminanz-Maske für weiche Kreis-Kanten). AREA_SCALE vergrößert den Bereich — nützlich wenn der Detektor zu klein erkennt oder Haare / Kragen mit erfasst werden sollen. MAGNIFY_SCALE steuert die Lupen-Vergrößerung. Optional zeichnet MARK_RING einen farbigen Rahmen um jedes Gesicht — als Datenschutz-Hinweis beim Verschleiern oder als Lupen-Rand beim Vergrößern. Der Detektor läuft als externer Subprozess; die Effekte nutzen ausschließlich Engine-native Ops (`crop` + `resize` für die Lupe, `gaussianBlur(rect)` und `applyMask` + `blendAt` fürs Verschleiern).

INPUT
INPUT — Gesichtserkennung → Verschleiern oder Vergrößern pro Gesicht (Rechteck / Kreis / Ring)
Blurred or magnified per face
Blurred or magnified per face — Gesichtserkennung → Verschleiern oder Vergrößern pro Gesicht (Rechteck / Kreis / Ring)
JavaScript
// Tool plugin demo — pre-process to help detection, then blur or magnify per face
// tool_facedetect_blur.js
//!INPUT: INPUT
//!OUTPUT: OUTPUT
//!PARAM: ACTION:enum(blur|magnify)=blur
//!PARAM: SIGMA:number=12,min=0.1,max=50
//!PARAM: MAGNIFY_SCALE:number=2.0,min=1.1,max=5.0
//!PARAM: MIN_SIZE:integer=60,min=10,max=400
//!PARAM: SCORE:number=30,min=5,max=200
//!PARAM: PROBE_CONTRAST:number=1.4,min=0.5,max=5.0
//!PARAM: PROBE_DENOISE:number=0.8,min=0.0,max=5.0
//!PARAM: SHAPE:enum(rect|circle)=circle
//!PARAM: AREA_SCALE:number=1.2,min=0.5,max=3.0
//!PARAM: MARK_RING:boolean=false
//!PARAM: MARK_RING_PX:integer=4,min=1,max=20
//!PARAM: MARK_COLOR:color=#ff3030ff

// Pico cascades work on grayscale and care a lot about local
// contrast. On dim, low-contrast or noisy inputs detection misses
// faces it would otherwise see. The trick: build a *probe* image
// (grayscale + contrast boost + mild denoise), run detection on the
// probe, then apply the actual edit to the *original* — full colour
// is preserved, the cascade just gets an easier signal.
//
// ACTION picks the per-face effect:
//   blur     — gaussianBlur (privacy redaction).
//   magnify  — crop the face region, resize it up by MAGNIFY_SCALE,
//              and composite it back centered on the same spot. Acts
//              like a lupe / magnifying glass.
//
// SHAPE controls how the affected region is shaped:
//   rect    — axis-aligned rectangle from the bounding box (simple,
//             reveals the box edges)
//   circle  — circle inscribed in the (scaled) bounding box; uses a
//             vector canvas as a luminance mask so only pixels inside
//             the circle get the effect. Looks more natural on faces.
//
// AREA_SCALE multiplies the bounding box / circle radius. 1.2 means
// "cover 20 % beyond the detected face" — useful when the cascade
// undershoots or you want hair / collar included too.
//
// MAGNIFY_SCALE (only when ACTION=magnify) — how much to enlarge the
// face. 2.0 = double size, 1.5 = subtle pop, 3.0 = strong lupe.
//
// MARK_RING draws a coloured ring around each detected face on top of
// the effect (default off). MARK_RING_PX = ring thickness in pixels;
// MARK_COLOR = hex string. Useful for debugging detection or as a
// visual privacy hint ("this region has been redacted") — also reads
// nicely as the rim of a magnifying glass when ACTION=magnify.

const img = Engine.loadImage(INPUT);

// 1) Build a detection-friendly probe (does NOT touch the output).
const probe = img.clone().grayscale().contrast(PROBE_CONTRAST);
if (PROBE_DENOISE > 0) { probe.gaussianBlur(PROBE_DENOISE); }

// 2) Detect on the probe.
const faces = Engine.detectFaces(probe, {
    minSize:        MIN_SIZE,
    scoreThreshold: SCORE,
});
probe.free();

console.log(`detected ${faces.length} face(s)` +
    ` (probe: contrast=${PROBE_CONTRAST}, denoise=${PROBE_DENOISE}, shape=${SHAPE}, scale=${AREA_SCALE})`);
for (const f of faces) {
    console.log(`  face: ${f.width}×${f.height} at (${f.x},${f.y}), Q=${f.confidence.toFixed(1)}`);
}

// Helper: scaled face region — center + half-width + half-height + radius.
function regionOf(f) {
    const cx = f.x + f.width  / 2;
    const cy = f.y + f.height / 2;
    const hw = (f.width  / 2) * AREA_SCALE;
    const hh = (f.height / 2) * AREA_SCALE;
    const r  = Math.max(hw, hh);  // circle uses the larger half-axis
    return { cx, cy, hw, hh, r };
}

// 3) Apply the chosen effect in the chosen shape.
if (ACTION === 'magnify' && faces.length > 0) {
    // Magnify path: per face, crop the region, resize up by
    // MAGNIFY_SCALE, optionally clip to a circle, blendAt back so
    // the face center stays put. Each face gets its own patch
    // (their sizes differ) — no shared mask trick like blur uses.
    for (const f of faces) {
        const { cx, cy, hw, hh, r } = regionOf(f);
        // Patch shape:
        //   SHAPE=rect   → cropping the bbox preserves the original
        //                  aspect ratio; the rect IS the visible patch.
        //   SHAPE=circle → use a square crop sized to the larger
        //                  half-axis so the circular mask (radius r =
        //                  max(hw,hh)) fits entirely inside the patch
        //                  with no clipping artifacts at the rect
        //                  boundary. Without this, non-square faces
        //                  show a hard 'cropped circle' rim.
        const phw = SHAPE === 'circle' ? r : hw;
        const phh = SHAPE === 'circle' ? r : hh;
        // Source rect on the original — clamped to image bounds.
        const sx = Math.max(0, Math.round(cx - phw));
        const sy = Math.max(0, Math.round(cy - phh));
        const sw = Math.min(img.width  - sx, Math.round(phw * 2));
        const sh = Math.min(img.height - sy, Math.round(phh * 2));
        if (sw < 4 || sh < 4) { continue; } // degenerate face — skip

        // Crop + resize the patch.
        const tw = Math.round(sw * MAGNIFY_SCALE);
        const th = Math.round(sh * MAGNIFY_SCALE);
        const patch = img.clone().crop(sx, sy, sw, sh).resize(tw, th);

        // Optional circular clip — patch is square in this branch,
        // so the circle inscribes cleanly with no rect-edge artifacts.
        if (SHAPE === 'circle') {
            const m = Engine.createCanvas(tw, th);
            m.fill('#ffffff').drawPath(Engine.createPath().circle(tw / 2, th / 2, r * MAGNIFY_SCALE));
            const mask = m.toImage();
            patch.applyMask(mask);
            mask.free();
        }

        // Composite back centered on the face. blendAt clips
        // automatically when the patch overflows the image edges.
        const px = Math.round(cx - tw / 2);
        const py = Math.round(cy - th / 2);
        img.blendAt(patch, new Px(px, py), 1.0, BlendMode.Over);
        patch.free();
    }
} else if (ACTION === 'blur' && SHAPE === 'rect') {
    // Axis-aligned blur per face — the engine's gaussianBlur(rect)
    // does the work in one pass per face.
    for (const f of faces) {
        const { cx, cy, hw, hh } = regionOf(f);
        const x = Math.max(0, cx - hw);
        const y = Math.max(0, cy - hh);
        const w = Math.min(img.width  - x, hw * 2);
        const h = Math.min(img.height - y, hh * 2);
        img.gaussianBlur(SIGMA, [
            x / img.width,  y / img.height,
            w / img.width,  h / img.height,
        ]);
    }
} else if (ACTION === 'blur' && faces.length > 0) {
    // Circle blur: build a luminance mask (white circles on a
    // transparent canvas → applyMask reads RGB as alpha), blur a
    // full clone, mask it, and composite Over the original. One
    // pass over the whole image instead of N rect-blurs — slower
    // for many faces, but gives circular edges natively.
    const maskCanvas = Engine.createCanvas(img.width, img.height);
    maskCanvas.fill('#ffffff');
    for (const f of faces) {
        const { cx, cy, r } = regionOf(f);
        maskCanvas.drawPath(Engine.createPath().circle(cx, cy, r));
    }
    const mask = maskCanvas.toImage();

    const working = img.clone().gaussianBlur(SIGMA);
    working.applyMask(mask);
    img.blendAt(working, new Px(0, 0), 1.0, BlendMode.Over);

    working.free();
    mask.free();
}

// 4) Optional ring marker — drawn on top of the effect so the user
// sees where the operation landed. Stroke-only, so the colour shows
// only as a thin ring; transparent everywhere else.
//
// MARK_COLOR accepts 8-digit hex (#rrggbbaa) for translucent rings —
// e.g. "#ff303080" gives a half-opacity red. 6-digit hex (#rrggbb) is
// fully opaque.
//
// When ACTION=magnify the ring tracks the *magnified* region (radius
// × MAGNIFY_SCALE), so it reads as a true magnifying-glass rim around
// the enlarged content rather than around the original face.
if (MARK_RING && faces.length > 0) {
    const ringScale = ACTION === 'magnify' ? MAGNIFY_SCALE : 1;
    const overlay = Engine.createCanvas(img.width, img.height);
    overlay.pen(MARK_COLOR, MARK_RING_PX);
    for (const f of faces) {
        const { cx, cy, hw, hh, r } = regionOf(f);
        if (SHAPE === 'rect') {
            // Path API ships a native rect(x, y, w, h) — no need to
            // emulate via moveTo/lineTo. (close() is the actual
            // close-path method; closePath() does not exist.)
            const rhw = hw * ringScale;
            const rhh = hh * ringScale;
            const box = Engine.createPath().rect(cx - rhw, cy - rhh, rhw * 2, rhh * 2);
            overlay.drawPath(box, true);
        } else {
            overlay.drawPath(Engine.createPath().circle(cx, cy, r * ringScale), true);
        }
    }
    const overlayImg = overlay.toImage();
    img.blendAt(overlayImg, new Px(0, 0), 1.0, BlendMode.Over);
    overlayImg.free();
}

img.save(OUTPUT);
img.free();

// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0