Please bookmark this page to avoid losing your image tool!

Image Expressionist Filter Applicator

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
async function processImage(originalImg, kuwaharaRadius = 2, saturationFactor = 1.2) {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d', { willReadFrequently: true }); // Optimization hint for frequent getImageData

    canvas.width = originalImg.naturalWidth || originalImg.width;
    canvas.height = originalImg.naturalHeight || originalImg.height;

    if (canvas.width === 0 || canvas.height === 0) {
        // Return an empty canvas or handle error, for now, return the 0x0 canvas
        return canvas;
    }

    ctx.drawImage(originalImg, 0, 0);

    // Ensure kuwaharaRadius is an integer.
    const rInt = Math.max(0, Math.floor(kuwaharaRadius));

    // If no processing is needed (radius 0 or less, saturation at 1.0), return the original drawn canvas
    if (rInt === 0 && saturationFactor === 1.0) {
        return canvas;
    }

    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const data = imageData.data; // This is a view, changes will reflect in imageData
    const width = imageData.width;
    const height = imageData.height;

    // --- Helper: RGB to HSL ---
    function rgbToHsl(r, g, b) {
        r /= 255; g /= 255; b /= 255;
        const max = Math.max(r, g, b), min = Math.min(r, g, b);
        let h, s, l = (max + min) / 2;

        if (max === min) {
            h = s = 0; // achromatic
        } else {
            const d = max - min;
            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
            switch (max) {
                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                case g: h = (b - r) / d + 2; break;
                case b: h = (r - g) / d + 4; break;
            }
            h /= 6;
        }
        return [h, s, l];
    }

    // --- Helper: HSL to RGB ---
    function hslToRgb(h, s, l) {
        let r, g, b;
        if (s === 0) {
            r = g = b = l; // achromatic
        } else {
            const hue2rgb = (p, q, t) => {
                if (t < 0) t += 1;
                if (t > 1) t -= 1;
                if (t < 1 / 6) return p + (q - p) * 6 * t;
                if (t < 1 / 2) return q;
                if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
                return p;
            };
            const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
            const p = 2 * l - q;
            r = hue2rgb(p, q, h + 1 / 3);
            g = hue2rgb(p, q, h);
            b = hue2rgb(p, q, h - 1 / 3);
        }
        return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
    }

    // --- Kuwahara Filter ---
    if (rInt > 0) {
        const r = rInt;
        // Create a copy of the data for reading, as Kuwahara needs original values
        const originalData = new Uint8ClampedArray(data); 
        const outputData = new Uint8ClampedArray(data.length); // Buffer for Kuwahara output

        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                const means = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]; // R, G, B sums for 4 quadrants
                const variances = [0, 0, 0, 0]; // Luminance variances for 4 quadrants
                const counts = [0, 0, 0, 0];    // Pixel counts for 4 quadrants

                // Quadrant definitions: [dx_start, dy_start, dx_end, dy_end] relative offsets
                // (x-r, y-r) to (x, y)     Q1 (Top-Left)
                // (x, y-r)   to (x+r, y)   Q2 (Top-Right)
                // (x-r, y)   to (x, y+r)   Q3 (Bottom-Left)
                // (x, y)   to (x+r, y+r)   Q4 (Bottom-Right)
                const quadRanges = [
                    [-r, -r, 0, 0], 
                    [0, -r, r, 0],  
                    [-r, 0, 0, r],  
                    [0, 0, r, r]    
                ];

                for (let q = 0; q < 4; q++) {
                    let sumLum = 0;
                    let sumLumSq = 0;

                    for (let qy_offset = quadRanges[q][1]; qy_offset <= quadRanges[q][3]; qy_offset++) {
                        for (let qx_offset = quadRanges[q][0]; qx_offset <= quadRanges[q][2]; qx_offset++) {
                            const currentX = Math.min(width - 1, Math.max(0, x + qx_offset));
                            const currentY = Math.min(height - 1, Math.max(0, y + qy_offset));
                            const idx = (currentY * width + currentX) * 4;

                            const R_val = originalData[idx];
                            const G_val = originalData[idx + 1];
                            const B_val = originalData[idx + 2];

                            means[q][0] += R_val;
                            means[q][1] += G_val;
                            means[q][2] += B_val;

                            const lum = 0.299 * R_val + 0.587 * G_val + 0.114 * B_val;
                            sumLum += lum;
                            sumLumSq += lum * lum;
                            counts[q]++;
                        }
                    }

                    if (counts[q] > 0) {
                        means[q][0] /= counts[q];
                        means[q][1] /= counts[q];
                        means[q][2] /= counts[q];
                        variances[q] = (sumLumSq / counts[q]) - Math.pow(sumLum / counts[q], 2);
                    } else {
                        variances[q] = Infinity; 
                    }
                }

                let minVariance = Infinity;
                let bestQuadrant = 0;
                for (let q = 0; q < 4; q++) {
                    if (variances[q] < minVariance) {
                        minVariance = variances[q];
                        bestQuadrant = q;
                    }
                }

                const outIdx = (y * width + x) * 4;
                outputData[outIdx] = means[bestQuadrant][0];
                outputData[outIdx + 1] = means[bestQuadrant][1];
                outputData[outIdx + 2] = means[bestQuadrant][2];
                outputData[outIdx + 3] = originalData[outIdx + 3]; // Preserve alpha
            }
        }
        // Copy Kuwahara processed data to the main 'data' array for subsequent saturation pass
        data.set(outputData);
    }

    // --- Saturation Adjustment ---
    // This operates on 'data', which is either original data or Kuwahara-filtered data.
    if (saturationFactor !== 1.0 && saturationFactor >= 0) { // Allow desaturation with factor < 1
        for (let i = 0; i < data.length; i += 4) {
            let r_val = data[i];
            let g_val = data[i + 1];
            let b_val = data[i + 2];

            const [h, s, l] = rgbToHsl(r_val, g_val, b_val);
            
            const newS = Math.max(0, Math.min(1, s * saturationFactor));
            
            const [newR, newG, newB] = hslToRgb(h, newS, l);

            data[i] = newR;
            data[i + 1] = newG;
            data[i + 2] = newB;
            // Alpha data[i+3] remains unchanged
        }
    }
    
    ctx.putImageData(imageData, 0, 0);
    return canvas;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Expressionist Filter Applicator is a web-based tool that applies artistic effects to images by utilizing a Kuwahara filter and saturation adjustments. Users can enhance their photos creatively, making it suitable for artists, graphic designers, and individuals looking to add a unique touch to their images. By adjusting the size of the Kuwahara filter and the level of color saturation, users can create visually striking results, making this tool ideal for artistic projects, social media posts, or personal photo collections.

Leave a Reply

Your email address will not be published. Required fields are marked *