Oil-paint look via Kuwahara — one call
`img.kuwahara(radius)` is the canonical edge-preserving oil-paint filter — each pixel's neighbourhood is split into four overlapping quadrants, the smoothest one wins and its mean colour becomes the output. Flat regions stay flat, edges snap, the image takes on a real painterly "brush-zone" look in one call. A small saturation/sharpen touch-up keeps the result lively. Tweak `RADIUS` for the look you want: 4 = sketchy hint, 8-10 = clear painting, 16-24 = broad strokes. For comparison: the older `demo_oilpaint` builds an approximation from existing primitives (bilateral stack + posterize + saturation + sharpen) — useful as a "no new op" demo, but the result has the cubist colour-flattening that posterize introduces. Kuwahara avoids that entirely.
// Painterly look via classical Kuwahara — single-call oil paint
// demo_kuwahara_paint.js
//!INPUT: INPUT
//!OUTPUT: OUTPUT
//!PARAM: RADIUS:number=10,min=2,max=30
//!PARAM: SATURATION_BOOST:number=1.10,min=0.5,max=2.0
//!PARAM: SHARPEN_AMOUNT:number=0.25,min=0,max=2
// `img.kuwahara(radius)` is the canonical edge-preserving oil-paint
// filter: each pixel's surrounding (2·radius+1)² window is split
// into four overlapping quadrants, the one with the lowest
// luminance variance wins, and its mean RGB becomes the output.
// Result: flat regions stay flat, edges snap, the image gets a
// real painterly "brush-zone" look in one call — no bilateral
// stack, no posterize, no manual tuning chain.
//
// This demo runs the filter, gives a small saturation/sharpen
// touch-up to keep the result lively (raw Kuwahara can read a
// little flat), and saves. Tweak guide:
//
// RADIUS = 4 → sketchy hint
// RADIUS = 8-10 → clear painterly look (default)
// RADIUS = 16-24 → broad strokes / poster style
//
// Cost is O(W·H·radius²); a 1500×1000 image at radius=10 runs in
// roughly 1-2 s on a modern CPU.
const img = Engine.loadImage(INPUT);
img.kuwahara(RADIUS);
if (SATURATION_BOOST !== 1.0) {
img.saturation(SATURATION_BOOST);
}
if (SHARPEN_AMOUNT > 0) {
img.sharpen(SHARPEN_AMOUNT);
}
img.save(OUTPUT);
img.free();
// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0