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).
// 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