Oil-painting / impressionist look — without a Kuwahara filter
Approximates a painted look from existing engine ops alone — no Kuwahara, no neural net. Multiple bilateral-blur passes flatten gradients into broad strokes, posterize collapses fine colour variations into discrete steps, and a small saturation boost + sharpen gives the result the vivid edge-contrast that reads as oil paint. Tweak PASSES + POSTERIZE_LEVELS to slide between photographic and abstract.
INPUT
Oil paint
JavaScript
// Oil-paint look via existing primitives — no new Rust op
// demo_oilpaint.js
//!INPUT: INPUT
//!OUTPUT: OUTPUT
//!PARAM: PASSES:integer=2,min=1,max=5
//!PARAM: BILATERAL_D:number=9,min=3,max=21
//!PARAM: BILATERAL_SC:number=0.30,min=0.05,max=1.0
//!PARAM: BILATERAL_SS:number=12,min=1,max=50
//!PARAM: POSTERIZE_LEVELS:number=8,min=3,max=24
//!PARAM: SATURATION_BOOST:number=1.20,min=0.5,max=2.0
//!PARAM: SHARPEN:number=0.4,min=0,max=2
// Approximates an oil-painting / impressionist look using only ops
// already shipped with the engine — no Kuwahara, no neural net.
//
// Pipeline:
// 1. bilateralBlur (×PASSES) — flattens gradients, keeps edges.
// Multiple passes with the same radius behave like a single
// pass with larger radius but smoother — closer to the way
// brush strokes blend pigment.
// 2. saturation(SATURATION_BOOST) — oil paint reads more vivid
// than a photograph; default 1.20 nudges the colours up.
// 3. posterize(POSTERIZE_LEVELS) — discretises the colour space
// into N steps per channel. The flatness this introduces is
// what reads as "brush strokes".
// 4. sharpen(SHARPEN) — adds a touch of edge contrast back; without
// it the result feels muddy. Default 0.4 is subtle.
//
// Tweak guide:
// • More 'painterly': raise PASSES to 3, BILATERAL_SC to 0.5
// • More 'cartoon': POSTERIZE_LEVELS down to 4–5
// • More 'photo': POSTERIZE_LEVELS up to 16, SATURATION_BOOST to 1.0
//
// When we ship a real img.kuwahara() Rust op later this script will
// stay as the "no-build-required" alternative — the look is similar,
// the ingredients are different.
const img = Engine.loadImage(INPUT);
for (let i = 0; i < PASSES; i++) {
img.bilateralBlur(BILATERAL_D, BILATERAL_SC, BILATERAL_SS);
}
img.saturation(SATURATION_BOOST);
img.posterize(POSTERIZE_LEVELS);
if (SHARPEN > 0) { img.sharpen(SHARPEN); }
img.save(OUTPUT);
img.free();
// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0