Javascript chroma key threshold and how to code it? [duplicate] - javascript

I just read this tutorial and tried this example. So I downloaded a video from web for my own testing. All I have to do is tweak rgb values in if conditions
HERE is the sample code from example
computeFrame: function() {
this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
let frame = this.ctx1.getImageData(0, 0, this.width, this.height);
let l = frame.data.length / 4;
for (let i = 0; i < l; i++) {
let r = frame.data[i * 4 + 0];
let g = frame.data[i * 4 + 1];
let b = frame.data[i * 4 + 2];
if (g > 100 && r > 100 && b < 43)
frame.data[i * 4 + 3] = 0;
}
this.ctx2.putImageData(frame, 0, 0);
return;
}
In the tutorial example its filtering out yellow(not yellow I guess) color. The sample video I downloaded uses green background. So I tweaked rgb value in if condition to get desired results
After multiple tries, I managed to get this.
Now what I want to know is how can I accurately filter out green screen (or any other screen)perfectly without guessing. Or randomly tweaking values.
With just guessing it take hours to get it perfectly right. And this is just a sample with a real world application. It can take maybe more.
NOTE: The example is working in Firefox for now..

You probably just need a better algorithm. Here's one, it's not perfect, but you can tweak it a lot easier.
Basically you'll just need a colorpicker, and pick the lightest and darkest values from the video (putting the RGB values in the l_ and d_ variables respectively). You can adjust the tolerance a little bit if you need to, but getting the l_ and r_ values just right by picking different areas with the color picker will give you a better key.
let l_r = 131,
l_g = 190,
l_b = 137,
d_r = 74,
d_g = 148,
d_b = 100;
let tolerance = 0.05;
let processor = {
timerCallback: function() {
if (this.video.paused || this.video.ended) {
return;
}
this.computeFrame();
let self = this;
setTimeout(function () {
self.timerCallback();
}, 0);
},
doLoad: function() {
this.video = document.getElementById("video");
this.c1 = document.getElementById("c1");
this.ctx1 = this.c1.getContext("2d");
this.c2 = document.getElementById("c2");
this.ctx2 = this.c2.getContext("2d");
let self = this;
this.video.addEventListener("play", function() {
self.width = self.video.videoWidth;
self.height = self.video.videoHeight;
self.timerCallback();
}, false);
},
calculateDistance: function(c, min, max) {
if(c < min) return min - c;
if(c > max) return c - max;
return 0;
},
computeFrame: function() {
this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
let frame = this.ctx1.getImageData(0, 0, this.width, this.height);
let l = frame.data.length / 4;
for (let i = 0; i < l; i++) {
let _r = frame.data[i * 4 + 0];
let _g = frame.data[i * 4 + 1];
let _b = frame.data[i * 4 + 2];
let difference = this.calculateDistance(_r, d_r, l_r) +
this.calculateDistance(_g, d_g, l_g) +
this.calculateDistance(_b, d_b, l_b);
difference /= (255 * 3); // convert to percent
if (difference < tolerance)
frame.data[i * 4 + 3] = 0;
}
this.ctx2.putImageData(frame, 0, 0);
return;
}
};
// :/

If performance does not matter, then you could work in another color space e.g. HSV. You could use the left top pixel as reference.
You compare the hue value of the reference point with hue value other pixels, and exclude all pixels that exceed a certain threshold and dark and light areas using saturation and value.
This how ever does not completely get rid of color bleeding, there you might need to do some color correct/desaturation.
function rgb2hsv () {
var rr, gg, bb,
r = arguments[0] / 255,
g = arguments[1] / 255,
b = arguments[2] / 255,
h, s,
v = Math.max(r, g, b),
diff = v - Math.min(r, g, b),
diffc = function(c){
return (v - c) / 6 / diff + 1 / 2;
};
if (diff == 0) {
h = s = 0;
} else {
s = diff / v;
rr = diffc(r);
gg = diffc(g);
bb = diffc(b);
if (r === v) {
h = bb - gg;
}else if (g === v) {
h = (1 / 3) + rr - bb;
}else if (b === v) {
h = (2 / 3) + gg - rr;
}
if (h < 0) {
h += 1;
}else if (h > 1) {
h -= 1;
}
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
v: Math.round(v * 100)
};
}
let processor = {
timerCallback: function() {
if (this.video.paused || this.video.ended) {
return;
}
this.computeFrame();
let self = this;
setTimeout(function () {
self.timerCallback();
}, 0);
},
doLoad: function() {
this.video = document.getElementById("video");
this.c1 = document.getElementById("c1");
this.ctx1 = this.c1.getContext("2d");
this.c2 = document.getElementById("c2");
this.ctx2 = this.c2.getContext("2d");
let self = this;
this.video.addEventListener("play", function() {
self.width = self.video.videoWidth / 2;
self.height = self.video.videoHeight / 2;
self.timerCallback();
}, false);
},
computeFrame: function() {
this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
let frame = this.ctx1.getImageData(0, 0, this.width, this.height);
let l = frame.data.length / 4;
let reference = rgb2hsv(frame.data[0], frame.data[1], frame.data[2]);
for (let i = 0; i < l; i++) {
let r = frame.data[i * 4 + 0];
let g = frame.data[i * 4 + 1];
let b = frame.data[i * 4 + 2];
let hsv = rgb2hsv(r, g, b);
let hueDifference = Math.abs(hsv.h - reference.h);
if( hueDifference < 20 && hsv.v > 50 && hsv.s > 50 ) {
frame.data[i * 4 + 3] = 0;
}
}
this.ctx2.putImageData(frame, 0, 0);
return;
}
};

Related

Best way to optimise canvas for high complexity drawings with React?

Me and a friend are playing around with fractals and wanted to make an interactive website where you can change values that generate the fractal, and you can see how its affected live. On small resolution tests, the website it quite responsive but still slow.
drawFractal = () => {
for (let x = 0; x < this.canvas.width; x++) {
for (let y = 0; y < this.canvas.height; y++) {
const belongsToSet = this.checkIfBelongsToMandelbrotSet(x / this.state.magnificationFactor - this.state.panX, y / this.state.magnificationFactor - this.state.panY);
if (belongsToSet === 0) {
this.ctx.clearRect(x,y, 1,1);
} else {
this.ctx.fillStyle = `hsl(80, 100%, ${belongsToSet}%)`;
// Draw a colorful pixel
this.ctx.fillRect(x,y, 1,1);
}
}
}
}
checkIfBelongsToMandelbrotSet = (x,y) => {
let realComponentOfResult = x;
let imaginaryComponentOfResult = y;
// Set max number of iterations
for (let i = 0; i < this.state.maxIterations; i++) {
const tempRealComponent = realComponentOfResult * realComponentOfResult - imaginaryComponentOfResult * imaginaryComponentOfResult + x;
const tempImaginaryComponent = this.state.imaginaryConstant * realComponentOfResult * imaginaryComponentOfResult + y;
realComponentOfResult = tempRealComponent;
imaginaryComponentOfResult = tempImaginaryComponent;
// Return a number as a percentage
if (realComponentOfResult * imaginaryComponentOfResult > 5) {
return (i / this.state.maxIterations * 100);
}
}
// Return zero if in set
return 0;
}
This is the algorithm that handles the generation of the fractal. However we iterate over every pixel of the canvas which is quite inefficient. As a result the whole website is really slow. I wanted to ask whether it's a good idea to use html canvas or are there more efficient alternatives? Or can I optimise the drawFractal() function to be able to be more efficient? I have no idea how to continue from this point as i am inexperienced and would appreciate any feedback!
Avoid painting operations as much as you can.
When you do fillRect(x, y, 1, 1) the browser has to go from the CPU to the GPU once per pixel, and that's very inefficient.
In your case, since you are drawing every pixels on their own, you can simply set all these pixels on an ImageBitmap and put the full image once per frame.
To improve a bit the color setting, I generated an Array of a hundred values before hand, you can make it more granular if you wish.
There might be improvements to do in your Mandelbrot, I didn't checked it, but this would be more suited to CodeReview than StackOverflow.
Here is a simple demo using a 800x600px canvas:
const state = {
magnificationFactor: 5000,
imaginaryConstant: 1,
maxIterations: 20,
panX: 1,
panY: 1
};
const canvas = document.getElementById('canvas');
const width = canvas.width = 800;
const height = canvas.height = 600;
const ctx = canvas.getContext('2d');
// the ImageData on which we will draw
const img = new ImageData( width, height );
// create an Uint32 view so that we can set one pixel in one op
const img_data = new Uint32Array( img.data.buffer );
const drawFractal = () => {
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const belongsToSet = checkIfBelongsToMandelbrotSet(x / state.magnificationFactor - state.panX, y / state.magnificationFactor - state.panY);
// setthe value in our ImageData's data
img_data[ y * width + x] = getColor( belongsToSet );
}
}
// only now we paint
ctx.putImageData( img, 0, 0 );
};
checkIfBelongsToMandelbrotSet = (x,y) => {
let realComponentOfResult = x;
let imaginaryComponentOfResult = y;
// Set max number of iterations
for (let i = 0; i < state.maxIterations; i++) {
const tempRealComponent = realComponentOfResult * realComponentOfResult - imaginaryComponentOfResult * imaginaryComponentOfResult + x;
const tempImaginaryComponent = state.imaginaryConstant * realComponentOfResult * imaginaryComponentOfResult + y;
realComponentOfResult = tempRealComponent;
imaginaryComponentOfResult = tempImaginaryComponent;
// Return a number as a percentage
if (realComponentOfResult * imaginaryComponentOfResult > 5) {
return (i / state.maxIterations * 100);
}
}
// Return zero if in set
return 0;
}
// we generate all the colors at init instead of generating every frame
const colors = Array.from( { length: 100 }, (_,i) => {
if( !i ) { return 0; }
return hslToRgb( 80/360, 100/100, i/100 );
} );
function getColor( ratio ) {
if( ratio === 0 ) { return 0; }
return colors[ Math.round( ratio ) ];
}
function anim() {
state.magnificationFactor -= 10;
drawFractal();
requestAnimationFrame( anim );
}
requestAnimationFrame( anim );
// original by mjijackson.com
// borrowed from https://stackoverflow.com/a/9493060/3702797
function hslToRgb(h, s, l){
var r, g, b;
if(s == 0){
r = g = b = l; // achromatic
}else{
var hue2rgb = function 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;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
// we want 0xAABBGGRR format
function toHex( val ) {
return Math.round( val * 255 ).toString(16);
}
return Number( '0xFF' + toHex(b) + toHex(g) + toHex(r) );
}
<canvas id="canvas"></canvas>

Having trouble changing shape of confetti

I've found this codepen that I'm integrating on a small website I have for a project. I don't want to put too much time into it, so I'll just be using a codepen I found. I'll do some adjustments to it, like the shape.
I managed to implement it, but now I'm having trouble to change the colors and the shape of the confetti.
The colors are completely random and I'd love for this to just be simple.
Also, I can't seem to be able to change the shape. I can only adjust the size.
If anyone here feels like giving me a nudge in the right direction, that'd be great!
'use strict';
// If set to true, the user must press
// UP UP DOWN ODWN LEFT RIGHT LEFT RIGHT A B
// to trigger the confetti with a random color theme.
// Otherwise the confetti constantly falls.
var onlyOnKonami = false;
$(function() {
// Globals
var $window = $(window)
, random = Math.random
, cos = Math.cos
, sin = Math.sin
, PI = Math.PI
, PI2 = PI * 2
, timer = undefined
, frame = undefined
, confetti = [];
// Settings
var konami = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]
, pointer = 0;
var particles = 150
, spread = 40
, sizeMin = 3
, sizeMax = 12 - sizeMin
, eccentricity = 10
, deviation = 100
, dxThetaMin = -.1
, dxThetaMax = -dxThetaMin - dxThetaMin
, dyMin = .13
, dyMax = .18
, dThetaMin = .4
, dThetaMax = .7 - dThetaMin;
var colorThemes = [
function() {
return color(200 * random()|0, 200 * random()|0, 200 * random()|0);
}, function() {
var black = 200 * random()|0; return color(200, black, black);
}, function() {
var black = 200 * random()|0; return color(black, 200, black);
}, function() {
var black = 200 * random()|0; return color(black, black, 200);
}, function() {
return color(200, 100, 200 * random()|0);
}, function() {
return color(200 * random()|0, 200, 200);
}, function() {
var black = 256 * random()|0; return color(black, black, black);
}, function() {
return colorThemes[random() < .5 ? 1 : 2]();
}, function() {
return colorThemes[random() < .5 ? 3 : 5]();
}, function() {
return colorThemes[random() < .5 ? 2 : 4]();
}
];
function color(r, g, b) {
return 'rgb(' + r + ',' + g + ',' + b + ')';
}
// Cosine interpolation
function interpolation(a, b, t) {
return (1-cos(PI*t))/2 * (b-a) + a;
}
// Create a 1D Maximal Poisson Disc over [0, 1]
var radius = 1/eccentricity, radius2 = radius+radius;
function createPoisson() {
// domain is the set of points which are still available to pick from
// D = union{ [d_i, d_i+1] | i is even }
var domain = [radius, 1-radius], measure = 1-radius2, spline = [0, 1];
while (measure) {
var dart = measure * random(), i, l, interval, a, b, c, d;
// Find where dart lies
for (i = 0, l = domain.length, measure = 0; i < l; i += 2) {
a = domain[i], b = domain[i+1], interval = b-a;
if (dart < measure+interval) {
spline.push(dart += a-measure);
break;
}
measure += interval;
}
c = dart-radius, d = dart+radius;
// Update the domain
for (i = domain.length-1; i > 0; i -= 2) {
l = i-1, a = domain[l], b = domain[i];
// c---d c---d Do nothing
// c-----d c-----d Move interior
// c--------------d Delete interval
// c--d Split interval
// a------b
if (a >= c && a < d)
if (b > d) domain[l] = d; // Move interior (Left case)
else domain.splice(l, 2); // Delete interval
else if (a < c && b > c)
if (b <= d) domain[i] = c; // Move interior (Right case)
else domain.splice(i, 0, c, d); // Split interval
}
// Re-measure the domain
for (i = 0, l = domain.length, measure = 0; i < l; i += 2)
measure += domain[i+1]-domain[i];
}
return spline.sort();
}
// Create the overarching container
var container = document.createElement('div');
container.style.position = 'fixed';
container.style.top = '0';
container.style.left = '0';
container.style.width = '100%';
container.style.height = '0';
container.style.overflow = 'visible';
container.style.zIndex = '9999';
// Confetto constructor
function Confetto(theme) {
this.frame = 0;
this.outer = document.createElement('div');
this.inner = document.createElement('div');
this.outer.appendChild(this.inner);
var outerStyle = this.outer.style, innerStyle = this.inner.style;
outerStyle.position = 'absolute';
outerStyle.width = (sizeMin + sizeMax * random()) + 'px';
outerStyle.height = (sizeMin + sizeMax * random()) + 'px';
innerStyle.width = '100%';
innerStyle.height = '100%';
innerStyle.backgroundColor = theme();
outerStyle.perspective = '50px';
outerStyle.transform = 'rotate(' + (360 * random()) + 'deg)';
this.axis = 'rotate3D(' +
cos(360 * random()) + ',' +
cos(360 * random()) + ',0,';
this.theta = 360 * random();
this.dTheta = dThetaMin + dThetaMax * random();
innerStyle.transform = this.axis + this.theta + 'deg)';
this.x = $window.width() * random();
this.y = -deviation;
this.dx = sin(dxThetaMin + dxThetaMax * random());
this.dy = dyMin + dyMax * random();
outerStyle.left = this.x + 'px';
outerStyle.top = this.y + 'px';
// Create the periodic spline
this.splineX = createPoisson();
this.splineY = [];
for (var i = 1, l = this.splineX.length-1; i < l; ++i)
this.splineY[i] = deviation * random();
this.splineY[0] = this.splineY[l] = deviation * random();
this.update = function(height, delta) {
this.frame += delta;
this.x += this.dx * delta;
this.y += this.dy * delta;
this.theta += this.dTheta * delta;
// Compute spline and convert to polar
var phi = this.frame % 7777 / 7777, i = 0, j = 1;
while (phi >= this.splineX[j]) i = j++;
var rho = interpolation(
this.splineY[i],
this.splineY[j],
(phi-this.splineX[i]) / (this.splineX[j]-this.splineX[i])
);
phi *= PI2;
outerStyle.left = this.x + rho * cos(phi) + 'px';
outerStyle.top = this.y + rho * sin(phi) + 'px';
innerStyle.transform = this.axis + this.theta + 'deg)';
return this.y > height+deviation;
};
}
function poof() {
if (!frame) {
// Append the container
document.body.appendChild(container);
// Add confetti
var theme = colorThemes[onlyOnKonami ? colorThemes.length * random()|0 : 0]
, count = 0;
(function addConfetto() {
if (onlyOnKonami && ++count > particles)
return timer = undefined;
var confetto = new Confetto(theme);
confetti.push(confetto);
container.appendChild(confetto.outer);
timer = setTimeout(addConfetto, spread * random());
})(0);
// Start the loop
var prev = undefined;
requestAnimationFrame(function loop(timestamp) {
var delta = prev ? timestamp - prev : 0;
prev = timestamp;
var height = $window.height();
for (var i = confetti.length-1; i >= 0; --i) {
if (confetti[i].update(height, delta)) {
container.removeChild(confetti[i].outer);
confetti.splice(i, 1);
}
}
if (timer || confetti.length)
return frame = requestAnimationFrame(loop);
// Cleanup
document.body.removeChild(container);
frame = undefined;
});
}
}
$window.keydown(function(event) {
pointer = konami[pointer] === event.which
? pointer+1
: +(event.which === konami[0]);
if (pointer === konami.length) {
pointer = 0;
poof();
}
});
if (!onlyOnKonami) poof();
});
Look inside Confetto() it's just using a <div> inside another <div> and giving it random width, height, 3D rotation, etc., you should be able to manipulate innerStyle and/or outerStyle to your liking there.
Try adding something like this to make it rounded:
innerStyle.borderRadius = '100%';
Try something like this to make it squared:
innerStyle.borderRadius = '0';
Or a combination:
innerStyle.borderRadius = '100% 0 0 100%';
You should be able to set any style property you need, by camel-casing the css name (.border for border, .borderBottom for border-bottom, .borderBottomColor for border-bottom-color, etc.). Check this list for ideas

how to set contrast and saturation using fabric.js

I want to set contrast, saturation and hue in my image editor. for this i use fabric.js but it have only brightness option..
Here is the my fabric js code
(function() {
fabric.Object.prototype.transparentCorners = false;
var $ = function(id){return document.getElementById(id)};
console.log($);
function applyFilter(index, filter) {
console.log(filter);
var obj = canvas.getActiveObject();
obj.filters[index] = filter;
obj.applyFilters(canvas.renderAll.bind(canvas));
}
function applyFilterValue(index, prop, value) {
var obj = canvas.getActiveObject();
if (obj.filters[index]) {
obj.filters[index][prop] = value;
obj.applyFilters(canvas.renderAll.bind(canvas));
}
}
fabric.Object.prototype.padding = 5;
fabric.Object.prototype.transparentCorners = false;
var canvas = this.__canvas = new fabric.Canvas('c'),
f = fabric.Image.filters;
fabric.Image.fromURL('../lib/bg.png', function(img) {
canvas.backgroundImage = img;
canvas.backgroundImage.width = 400;
canvas.backgroundImage.height = 400;
});
canvas.on({
'object:selected': function() {
fabric.util.toArray(document.getElementsByTagName('input'))
.forEach(function(el){ el.disabled = false; })
var filters = ['brightness',];
// var filters = ['grayscale', 'invert', 'remove-white', 'sepia', 'sepia2',
// 'brightness', 'noise', 'gradient-transparency', 'pixelate',
// 'blur', 'sharpen', 'emboss', 'tint', 'multiply', 'blend'];
for (var i = 0; i < filters.length; i++) {
$(filters[i]).checked = !!canvas.getActiveObject().filters[i];
}
applyFilter(5, true && new f.Brightness({
brightness: parseInt($('brightness-value').value, 10)
}));
},
'selection:cleared': function() {
fabric.util.toArray(document.getElementsByTagName('input'))
.forEach(function(el){ el.disabled = true; })
}
});
fabric.Image.fromURL('../upload/Chrysanthemum.jpg', function(img) {
var oImg = img.set({ left: 50, top: 100, angle: 0 }).scale(0.9);
canvas.add(oImg).renderAll();
canvas.setActiveObject(oImg);
});
$('brightness').onclick = function () {
applyFilter(5, this.checked && new f.Brightness({
brightness: parseInt($('brightness-value').value, 10)
}));
};
$('brightness-value').onchange = function() {
applyFilterValue(5, 'brightness', parseInt(this.value, 10));
};
})();
Last year I wrote a HSL plugin which might be of use:
fabric.Image.filters.HSL = fabric.util.createClass(fabric.Image.filters.BaseFilter, {
type: 'HSL',
initialize: function(options) {
options || (options = {});
this.hue = options.hue || 0;
this.saturation = options.saturation || 0;
this.lightness = options.lightness || 0;
},
rgbToHsl: function(r, g, b) {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0;
} else {
var 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];
},
hslToRgb: function(h, s, l) {
var r, g, b;
if (s == 0) {
r = g = b = l;
} else {
function 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;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [r * 255, g * 255, b * 255];
},
applyTo: function(canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data;
for (var i=0; i<data.length; i+=4)
{
// Convert RGB to HSL
var hsl = this.rgbToHsl(data[i], data[i+1], data[i+2]);
// Apply HSL values
if (this.hue ) hsl[0] = this.hue;
if (this.saturation) hsl[1] = this.saturation;
if (this.lightness ) hsl[2] = this.lightness;
// Convert HSL back to RGB
var rgb = this.hslToRgb(hsl[0], hsl[1], hsl[2]);
// Update data
data[i] = rgb[0];
data[i+1] = rgb[1];
data[i+2] = rgb[2];
}
context.putImageData(imageData, 0, 0);
},
toObject: function() {
return extend(this.callSuper('toObject'), {
hue: this.hue,
saturation: this.saturation,
lightness: this.lightness
});
}
});
Add to your page after FabricJS has loaded and call like this:
var hue = 1; // Range: 0 to 1
var brightness = 1; // Range: 0 to 1
var lightness = 1; // Range: 0 to 1
var filterHSL = new img.filters.HSL({
hue: hue,
saturation: saturation,
lightness: lightness
});
img.filters = [filterHSL];
img.applyFilters(canvas.renderAll.bind(canvas));
...where canvas is your FabricJS canvas object.
I hope this helps.
You can now do this with the latest Fabricjs release, 1.6.6
fabric.Image.filters.Contrast and fabric.Image.filters.Saturate were added in this release.
I made a fiddle to show you how to achieve it (note: change the applyFilter() function to achieve different filters):
DEMO

Canvas change image background color but keep it's "effects"

I have an image where I need to change it's background color, but keep the "effects" on it (on the image the black dots, white lines etc.)
Here's the orginal image:
I managed to change the color, but also I keep removing those "effects". Preview:
Here's the code:
//let's say I want it to be red
var r = 255;
var g = 0;
var b = 0;
var imgElement = document.getElementById('img');
var canvas = document.getElementById('canvas');
canvas.width = imgElement.width;
canvas.height = imgElement.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(imgElement, 0, 0);
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
if (data[i + 3] !== 0) {
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
data[i + 3] = data[i + 3];
}
}
ctx.putImageData(imageData, 0, 0);
<img src="foo" id="img" />
<canvas id="canvas"></canvas>
How to prevent that?
For modern browsers except Internet Explorer, you can use compositing to change the hue of your original image while leaving the saturation & lightness unchanged. This will "recolor" your original image while leaving the contours intact.
Example code that works in modern browsers except Internet Explorer
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var img=new Image();
img.onload=start;
img.src="https://dl.dropboxusercontent.com/u/139992952/multple/M449a.png";
function start(){
// create an overlay with solid #00d9c6 color
var tempCanvas=document.createElement('canvas');
var tempctx=tempCanvas.getContext('2d');
canvas.width=tempCanvas.width=img.width;
canvas.height=tempCanvas.height=img.height;
tempctx.drawImage(img,0,0);
tempctx.globalCompositeOperation='source-atop';
tempctx.fillStyle='#00d9c6';
tempctx.fillRect(0,0,tempCanvas.width,tempCanvas.height);
//
canvas.width=img.width;
canvas.height=img.height;
// use compositing to change the hue of the original image
ctx.drawImage(img,0,0);
ctx.globalCompositeOperation='hue';
ctx.drawImage(tempCanvas,0,0);
// always clean up: reset compositing to its default
ctx.globalCompositeOperation='source-over';
}
#canvas{border:1px solid red; }
<canvas id="canvas" width=300 height=300></canvas>
Since Internet Explorer does not support Blend Compositing, you will have to do it manually.
Read the RGBA value of each pixel.
Convert that RGBA value to HSL.
Shift the hue value (the "H" in HSL) by the difference between your blue hue and your desired green hue.
Convert the hue-shifted HSL value to RGBA value.
Write the hue-shifted RGBA value back to the pixel.
Here's example code of manually shifting the hue:
Important note: This manual method works by manipulating pixels with .getImageData. Therefore you must make sure the original image is hosted on the same domain as the webpage. Otherwise, the canvas will become tainted for security reasons and you will not be able to use .getImageData.
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var img = new Image();
img.crossOrigin = "anonymous";
img.onload = start;
img.src = "https://dl.dropboxusercontent.com/u/139992952/multple/marioStanding.png";
function start() {
ctx.drawImage(img, 0, 0);
ctx.drawImage(img, 150, 0);
// shift blueish colors to greenish colors
recolorPants(-.33);
}
function recolorPants(colorshift) {
var imgData = ctx.getImageData(150, 0, canvas.width, canvas.height);
var data = imgData.data;
for (var i = 0; i < data.length; i += 4) {
red = data[i + 0];
green = data[i + 1];
blue = data[i + 2];
alpha = data[i + 3];
// skip transparent/semiTransparent pixels
if (alpha < 200) {
continue;
}
var hsl = rgbToHsl(red, green, blue);
var hue = hsl.h * 360;
// change blueish pixels to the new color
if (hue > 200 && hue < 300) {
var newRgb = hslToRgb(hsl.h + colorshift, hsl.s, hsl.l);
data[i + 0] = newRgb.r;
data[i + 1] = newRgb.g;
data[i + 2] = newRgb.b;
data[i + 3] = 255;
}
}
ctx.putImageData(imgData, 150, 0);
}
function rgbToHsl(r, g, b) {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var 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,
s: s,
l: l,
});
}
function hslToRgb(h, s, l) {
var r, g, b;
if (s == 0) {
r = g = b = l; // achromatic
} else {
function 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;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return ({
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255),
});
}
<p>Example shifting color Hue with .getImageData</p>
<p>(Original: left, Recolored: right)</p>
<canvas id="canvas" width=300 height=300></canvas>
You need to convert each pixel to the LSH colour space (lightness/luminance, hue, saturation). Then you set the Hue to the colour you want and keep the calculated luminance and saturation, then convert back to RGB and set the imageData to the new RGB value.
I have added my own code for conversions. There may be faster versions out there.
// returns RGB in an array on 3 numbers 0-255
var lshToRGB = function(ll,ss,hh){ //ll 0-255,ss 0-255, hh 0-360
var l = ll/255;
var s = ss/255;
var hhh = (hh/255)*360;
var C = (1 - Math.abs(2*l - 1)) * s;
var X = C*(1 - Math.abs(((hhh / 60)%2) - 1));
var m = l - C/2;
if(hhh < 60){
var r = C;
var g = X;
var b = 0;
}else
if(hhh < 120){
var r = X;
var g = C;
var b = 0;
}else
if(hhh < 180){
var r = 0;
var g = C;
var b = X;
}else
if(hhh < 240){
var r = 0;
var g = X;
var b = C;
}else
if(hhh < 300){
var r = X;
var g = 0;
var b = C;
}else{
var r = C;
var g = 0;
var b = X;
}
r += m;
g += m;
b += m;
// is there a need to clamp these ????)
r = Math.round(Math.min(255,Math.max(0,r*255)));
g = Math.round(Math.min(255,Math.max(0,g*255)));
b = Math.round(Math.min(255,Math.max(0,b*255)));
return [r,g,b];
}
// returns array of 3 numbers 0-255,0-255,0-360
var rgbToLSH = function(rr,gg,bb){ // could do without the conversion from 360 to 255 on hue
var r,
g,
b,
h,
s,
l,
min,
max,
d;
r = rr / 255;
g = gg / 255;
b = bb / 255;
max = Math.max(r, g, b);
min = Math.min(r, g, b);
l = (max + min) / 2;
if (max == min) {
h = 0;
s = 0; // achromatic
} else {
d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d;
break;
case g:
h = 2 + ((b - r) / d);
break;
case b:
h = 4 + ((r - g) / d);
break;
}
h *= 60;
if (h < 0) {
h += 360;
}
h = Math.round(h);
}
return [
Math.min(Math.round(l*255),255),
Math.min(Math.round(s*255),255),
Math.min(Math.round((h/360)*255),255)
];
}

HTML5 Canvas image segmentation

I'm trying to remove the white/gray pixels of background (a wall) from an image so that the foreground (a hand in my case) should be left.
I tried manipulating canvas pixels but the foreground extraction is not very nice because of shadow or other inconsistencies in background. I even tried with green colored background but it doesn't enhance the performance.
for (i = 0; i < imgData.width * imgData.height * 4; i += 4) {
var r = imgDataNormal.data[i + 0];
var g = imgDataNormal.data[i + 1];
var b = imgDataNormal.data[i + 2];
var a = imgDataNormal.data[i + 3];
// compare rgb levels for green and set alphachannel to 0;
selectedR = *a value 0 - 255*
selectedG = *a value 0 - 255*
selectedB = *a value 0 - 255*
if (r <= selectedR || g >= selectedG || b <= selectedB) {
a = 0;
}
}
Is there any sophisticated method or library to make background of an image transparent?
Converting the pixels from RGB to HSL and then running a threshold filter over the pixels to remove anything (by setting the pixel's alpha to be completely transparent) with a saturation less than 0.12 (measuring saturation on a scale of 0..1 - that value was found via trial-and-error).
JavaScript
function rgbToHsl( r, g, b ){
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var 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];
}
function thresholdHsl(pixels,lower,upper){
var d = pixels.data;
var createTest = function( lower, upper ){
return lower <= upper
? function(v){ return lower <= v && v <= upper; }
: function(v){ return lower <= v || v <= upper; };
}
var h = createTest( lower[0], upper[0] );
var s = createTest( lower[1], upper[1] );
var l = createTest( lower[2], upper[2] );
for (var i=0; i<d.length; i+=4) {
var hsl = rgbToHsl( d[i], d[i+1], d[i+2] );
if ( !h(hsl[0]) || !s(hsl[1]) || !l(hsl[2]) ){
d[i+3] = 0;
}
}
}
var img = new Image();
img.onload = function() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.drawImage(img,0,0);
var pixels = ctx.getImageData(0,0,canvas.width,canvas.height);
thresholdHsl(pixels,[0,0.12,0],[1,1,1]);
ctx.putImageData(pixels, 0, 0);
};
img.src = 'Hand.png';
HTML
<canvas id="myCanvas" width="638" height="475"></canvas>
Output
It could be further improved by removing noise from the picture to try to clean up the edges.
There are a few mistakes in your code :
The last line a = 0 does nothing but changing a local variable. You have to modify it directly in the array
After that, to see the result, you have to push back the pixel array to the context
Since you need (in that example) to check for white/gray background (rgb(255, 255, 255)), you need to check if the value of the pixel is superior, not inferior. And using a and operator instead of or allows you here to filter a bigger part of the shadow (which is gray so every colour component has almost the same value) without removing the hand itself, which is more coloured.
Here is an example :
var imgData = context.getImageData(0, 0, canvas.width, canvas.height);
for (i = 0; i < imgData.width * imgData.height * 4; i += 4) {
var r = imgData.data[i + 0];
var g = imgData.data[i + 1];
var b = imgData.data[i + 2];
var a = imgData.data[i + 3];
// compare rgb levels for green and set alphachannel to 0;
selectedR = 105;
selectedG = 105;
selectedB = 105;
if (r >= selectedR && g >= selectedG && b >= selectedB) {
imgData.data[i + 3] = 0;
}
}
context.putImageData(imgData, 0, 0);
That still isn't perfect because of the shadow, but it works.

Categories