Iterative Mehrpass-Schärfung — jede Stufe sichtbar
Wendet 4 Schärf-+Weichzeichnen-Durchgänge mit pro Runde steigender Schärfe an und zeigt das kumulative Ergebnis nach jedem Durchgang in einem 2×3-Raster: - **original** — Kontrollpanel (unbearbeiteter Input) - **pass 1…N** — `sharpen(SHARP_BASE + i*SHARP_STEP)` gefolgt von `gaussianBlur(SOFTEN_SIGMA)` — der Weichzeichn-Pass verhindert, dass die Schärfung in Halos kippt - **final** — der finale Look: kumulative Pässe plus `contrast`, `saturation`, `vignette` Beobachte, wie der lokale Mikro-Kontrast über die Durchgänge aufgebaut wird, ohne Lichter oder Schatten auszufressen — Schleifen + berechnete Parameter pro Pass treiben die Sequenz.
INPUT
Stage-by-stage sharpening
JavaScript
// Iterative multi-pass sharpening — visualised stage by stage
// demo_progressive_enhance.js
//!INPUT: INPUT
//!OUTPUT: OUTPUT
//!PARAM: PASSES:integer=4,min=2,max=6
//!PARAM: SHARP_BASE:number=0.20,min=0.05,max=0.5
//!PARAM: SHARP_STEP:number=0.08,min=0.0,max=0.2
//!PARAM: SOFTEN_SIGMA:number=0.25,min=0.0,max=1.0
//!PARAM: FINAL_CONTRAST:number=1.2,min=0.5,max=2.0
//!PARAM: FINAL_SATURATION:number=1.15,min=0.0,max=2.0
//!PARAM: FINAL_VIGNETTE:number=0.35,min=0.0,max=1.0
//!PARAM: TILE_W:integer=512,min=256,max=1024
//!PARAM: LABEL_SIZE:number=18,min=10,max=64
// Multi-pass sharpening, every stage visible in a 2×3 grid:
//
// ┌──────────┬──────────┬──────────┐
// │ original │ pass 1 │ pass 2 │
// ├──────────┼──────────┼──────────┤
// │ pass 3 │ pass 4 │ + final │
// └──────────┴──────────┴──────────┘
//
// Each pass = sharpen(amount) → gaussianBlur(sigma) where amount
// grows linearly with i (SHARP_BASE + i * SHARP_STEP). The last
// panel adds a contrast/saturation/vignette pass on top of the
// fully sharpened image — the published "final look".
//
// Watch how local micro-contrast builds up across the passes
// without blowing out highlights or shadows.
// Load and downscale: a 6-panel grid at full resolution becomes
// huge fast. Cap each tile at TILE_W on its long side.
const src = Engine.loadImage(INPUT);
{
const ow = src.width, oh = src.height;
const long = Math.max(ow, oh);
if (long > TILE_W) {
const s = TILE_W / long;
src.resize(Math.round(ow * s), Math.round(oh * s), Interp.Bilinear);
}
}
const W = src.width;
const H = src.height;
// Cap visible pass panels at 4 so the grid stays human-readable
// even when PASSES is cranked high. Anything past pass 4 still
// applies to the accumulator below — it just doesn't get its own
// snapshot panel.
const MAX_PASS_PANELS = 4;
const passesShown = Math.min(PASSES, MAX_PASS_PANELS);
// Grid sized to exactly the cells we'll actually render:
// 1 (original) + passesShown + 1 (final)
// Picking COLS dynamically avoids the black-panel gap that used
// to appear when PASSES < 4 with a fixed 3×2 layout. The COLS
// table keeps the grid as close to square as possible for the
// common cellsUsed values:
// 3 → 3×1 (PASSES=1)
// 4 → 2×2 (PASSES=2) ← perfect, no empty cells
// 5 → 3×2 (PASSES=3) ← one empty cell at bottom-right
// 6 → 3×2 (PASSES≥4, capped) ← perfect
const cellsUsed = 1 + passesShown + 1;
const COLS = cellsUsed <= 3 ? cellsUsed
: cellsUsed === 4 ? 2
: 3;
const ROWS = Math.ceil(cellsUsed / COLS);
const FINAL_SLOT = cellsUsed - 1; // always the last filled cell
const bandH = LABEL_SIZE + 8;
const cellW = W;
const cellH = bandH + H;
const out = Engine.createImage(1, 1);
out.setPixel(px(0, 0), new Pixel(0.05, 0.05, 0.07, 1.0));
out.resize(cellW * COLS, cellH * ROWS);
function panel(label, image, idx) {
const col = idx % COLS;
const row = Math.floor(idx / COLS);
const x0 = col * cellW;
const y0 = row * cellH;
out.drawText(label, x0 + 16, y0 + LABEL_SIZE - 2, {
size: LABEL_SIZE, color: NamedColor.white,
});
out.blendAt(image, px(x0, y0 + bandH), 1.0, BlendMode.Over);
}
// Panel 0 — original (control)
panel('0) original', src.clone(), 0);
// Working buffer that accumulates the iterations. Snapshot
// after each pass so we can show the cumulative effect.
const acc = src.clone();
src.free();
// One progress bar per pass — useful when PASSES is high and the
// gaussianBlur sigma is wide enough to make each pass take real time.
const p = Engine.progress("Sharpening passes", { total: PASSES });
for (let i = 0; i < PASSES; i++) {
const sharpAmount = SHARP_BASE + i * SHARP_STEP;
acc.sharpen(sharpAmount).gaussianBlur(SOFTEN_SIGMA);
if (i < passesShown) {
const snap = acc.clone();
panel(
`${i + 1}) pass ${i + 1} sharpen(${sharpAmount.toFixed(2)})`,
snap, i + 1
);
snap.free();
}
p.step(1, `sharpen(${sharpAmount.toFixed(2)}) + blur(${SOFTEN_SIGMA})`);
}
p.done(`✓ ${PASSES} passes applied`);
// Last panel — apply the published "final look" on top of the
// fully sharpened image: contrast, saturation, vignette.
{
const finalImg = acc.clone();
finalImg
.contrast(FINAL_CONTRAST)
.saturation(FINAL_SATURATION)
.vignette(FINAL_VIGNETTE);
panel(
`${FINAL_SLOT + 1}) final c=${FINAL_CONTRAST.toFixed(2)} s=${FINAL_SATURATION.toFixed(2)} v=${FINAL_VIGNETTE.toFixed(2)}`,
finalImg, FINAL_SLOT
);
finalImg.free();
}
acc.free();
out.save(OUTPUT);
out.free();
// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0