You can edit the below JavaScript code to customize the image tool.
Apply Changes
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;
}
Apply Changes