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
INPUT — Oil-painting / impressionist look — without a Kuwahara filter
Oil paint
Oil paint — Oil-painting / impressionist look — without a Kuwahara filter
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