Painterly look that follows image structure — anisotropic Kuwahara
`img.anisotropicKuwahara(radius)` is the structure-aware cousin of `kuwahara(radius)`. Same four-quadrant "smoothest-wins" core, but each pixel's quadrants are rotated to align with the local edge direction (computed from the smoothed structure tensor of the luminance gradient). Brush strokes follow image texture — hair runs along hair, fabric ridges along ridges — instead of all being the same little square everywhere. Cost is ~3× the classical version. Use it when structure matters: portraits with prominent hair, landscapes with flowing lines. For very flat content the difference vs `kuwahara` is subtle. `RADIUS` 4-6 = fine sketch, 8-12 = clear painting (default), 16-20 = broad strokes.
INPUT
Anisotropic Kuwahara
JavaScript
// Painterly look that follows image structure — anisotropic Kuwahara
// demo_anisotropic_kuwahara.js
//!INPUT: INPUT
//!OUTPUT: OUTPUT
//!PARAM: RADIUS:integer=10,min=2,max=24
//!PARAM: SATURATION_BOOST:number=1.10,min=0.5,max=2.0
//!PARAM: SHARPEN_AMOUNT:number=0.20,min=0,max=2
// `img.anisotropicKuwahara(radius)` orients its four Kuwahara
// quadrants by the local edge direction — derived from the
// smoothed structure tensor of the luminance gradient. Compared
// to the classical `kuwahara(radius)` (axis-aligned quadrants),
// this version's brush strokes follow image texture: hair runs
// along hair, fabric ridges along ridges, instead of all being
// the same little square everywhere.
//
// Cost is ~3× the classical Kuwahara at the same radius (extra
// gradient + tensor smoothing pass), but the result reads as
// closer to a real painting in regions with strong directional
// structure. For very flat content (clear sky, plain skin) the
// difference vs. classical is subtle.
//
// Tweak guide:
// RADIUS = 4-6 → fine sketch / watercolour
// RADIUS = 8-12 → clear painterly look (default)
// RADIUS = 16-20 → broad strokes / gouache style
const img = Engine.loadImage(INPUT);
img.anisotropicKuwahara(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