Hi,
I recently needed to use the keyOut
function in BITMAP, but I ran into an issue because the images I was working with weren’t the best quality. The green backgrounds often contained multiple shades, so using a single color for keying didn’t produce good results.
I tried passing an array of greenish colors to handle the variations, but that didn’t work as expected either.
Eventually, I wrote a custom function (with help from Claude) that handles multiple colors and allows for more flexibility. I think it could be a great enhancement if keyOut
supported something like this natively.
Here’s the function we came up with:
function removeGreenScreen(bitmap) {
// Default values for green screen detection
const hueMin = 80; // Minimum hue value (0-360) for green detection - sets the lower bound of green hues
const hueMax = 160; // Maximum hue value (0-360) for green detection - sets the upper bound of green hues
const satMin = 30; // Minimum saturation value (0-100) - avoids detecting desaturated/grayish pixels as green
const lightMin = 15; // Minimum lightness value (0-100) - avoids detecting very dark pixels as green
const smoothing = 0; // Edge smoothing factor (0-10) - higher values create smoother edges around subjects
// Create temporary canvas for image processing
const canvas = document.createElement('canvas');
const width = bitmap.width;
const height = bitmap.height;
canvas.width = width;
canvas.height = height;
// Draw the bitmap onto the canvas
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap.image, 0, 0);
// Get pixel data
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
// Function to convert RGB to HSL
function rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0; // gray
} 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: h * 360, // convert to degrees
s: s * 100, // percentage
l: l * 100 // percentage
};
}
// Process each pixel
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Convert to HSL for easier green detection
const hsl = rgbToHsl(r, g, b);
// Check if pixel is in the defined green range
let isGreen = (hsl.h >= hueMin && hsl.h <= hueMax &&
hsl.s >= satMin &&
hsl.l >= lightMin);
// Set transparency with edge smoothing
let alpha = 255; // opaque by default
if (isGreen) {
// Completely transparent pixel
alpha = 0;
} else if (smoothing > 0) {
// Check if pixel is close to green range - for edge smoothing
const hueDiff = Math.min(
Math.abs(hsl.h - hueMin),
Math.abs(hsl.h - hueMax)
);
if (hueDiff < smoothing * 5) {
// Reduce opacity gradually based on proximity to green
const factor = hueDiff / (smoothing * 5);
alpha = Math.min(255, Math.max(0, Math.round(factor * 255)));
}
}
// Save pixel with updated transparency
data[i + 3] = alpha;
}
// Update image data
ctx.putImageData(imageData, 0, 0);
// Create new ZIM Bitmap from canvas
const resultBitmap = new Bitmap(canvas);
return resultBitmap;
}