You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, targetColor = "#00FF00", similarity = 0.45, smoothness = 0.15, spillReduction = 1.0) {
const canvas = document.createElement('canvas');
canvas.width = originalImg.width;
canvas.height = originalImg.height;
const ctx = canvas.getContext('2d');
// Draw the image and extract pixel data
ctx.drawImage(originalImg, 0, 0);
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imgData.data;
// Parse target color
const hex = (targetColor || "#00FF00").replace(/^#/, '');
const tR = parseInt(hex.slice(0, 2), 16) || 0;
const tG = parseInt(hex.slice(2, 4), 16) || 255;
const tB = parseInt(hex.slice(4, 6), 16) || 0;
// Validate parameters
const sim = isNaN(Number(similarity)) ? 0.45 : Number(similarity);
const smooth = isNaN(Number(smoothness)) ? 0.15 : Number(smoothness);
const spillLimit = isNaN(Number(spillReduction)) ? 1.0 : Math.min(Math.max(Number(spillReduction), 0.0), 1.0);
// RGB to YCbCr conversion for accurate chrominance distance processing
// (luminance independent enough to handle shadows, while still protecting dark hair)
const rgbToYCbCr = (r, g, b) => {
const cb = 128 - 0.168736 * r - 0.331264 * g + 0.5 * b;
const cr = 128 + 0.5 * r - 0.418688 * g - 0.081312 * b;
return [cb, cr];
};
const [tCb, tCr] = rgbToYCbCr(tR, tG, tB);
// Determine if we need specialized green or blue spill suppression logic
const isGreenKey = (tG > tR + 20 && tG > tB + 20);
const isBlueKey = (tB > tR + 20 && tB > tG + 20);
// Process every pixel
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
let a = data[i + 3];
if (a === 0) continue; // Skip already transparent pixels
// 1. CHROMA KEY ALPHA EXTRACTION
const [cb, cr] = rgbToYCbCr(r, g, b);
// Euclidean distance in chrominance space (normalized roughly by typical max radius)
const dist = Math.sqrt(Math.pow(cb - tCb, 2) + Math.pow(cr - tCr, 2)) / 128.0;
let alpha = 1.0;
const t = (dist - sim) / (smooth + 0.0001); // avoid division by zero
// Calculate smoothstep interpolation for beautiful, natural hair edges
if (t <= 0.0) {
alpha = 0.0;
} else if (t >= 1.0) {
alpha = 1.0;
} else {
alpha = t * t * (3.0 - 2.0 * t);
}
if (alpha === 0.0) {
data[i + 3] = 0; // Completely transparent
continue;
}
// 2. GREEN SPILL ELIMINATION (DESPILLING)
let finalR = r;
let finalG = g;
let finalB = b;
if (spillLimit > 0) {
if (isGreenKey) {
// If it's a green screen, limit the green channel to max of red/blue
// This beautifully recovers hair colors completely free of green bounce reflection
let maxRB = Math.max(r, b);
if (g > maxRB) {
finalG = g * (1 - spillLimit) + maxRB * spillLimit;
}
} else if (isBlueKey) {
let maxRG = Math.max(r, g);
if (b > maxRG) {
finalB = b * (1 - spillLimit) + maxRG * spillLimit;
}
}
}
// 3. UN-PREMULTIPLY EDGES
// Fixes dark muddy hair lines where foreground was naturally optically blended into green
if (alpha < 1.0) {
// Un-premultiply scales the brightness back up to reverse camera background merge.
// Limited to 0.2 downscale to prevent extreme noise amplification/blowing up on subtle edges
let limit = Math.max(alpha, 0.2);
finalR = Math.min(255, finalR / limit);
finalG = Math.min(255, finalG / limit);
finalB = Math.min(255, finalB / limit);
}
// Apply back to image buffer
data[i] = finalR;
data[i + 1] = finalG;
data[i + 2] = finalB;
data[i + 3] = Math.round(a * alpha); // Blend with original alpha
}
// Write final pixels mapped to the canvas element
ctx.putImageData(imgData, 0, 0);
return canvas;
}
Apply Changes