How can I use `putImageData` with transparency? - javascript

I am drawing a byte array of image data to a canvas with putImageData. The image data has some transparency, but when I draw multiple images to the canvas I don't see any overlap. In other words, I get this:
But I want this:
I am clearly not understanding something about how alpha compositing works in Javascript/HTML, and I hope someone can help me with this problem. Thanks!
Sample code:
const arr = new Uint8ClampedArray(4*100*100);
for (let i = 0; i < arr.length; i += 4) {
arr[i + 0] = 255;
arr[i + 1] = 0;
arr[i + 2] = 0;
arr[i + 3] = 100;
}
let imageData = new ImageData(arr, 100);
context.putImageData(imageData, 0, 0);
context.putImageData(imageData, 50, 50);

The thing is that putImageData (and to some extents getImageData too), is special: it won't care at all about the current context settings, that is, it will completely ignore the current context's transformation matrix, all its clipping regions, it will ignore the current context's filter, its shadowColor, its globalAlpha, etc. and back to our case, its globalCompositeOperation.
What it does is it sets the pixels on the canvas buffer at the untransformed coordinates to what is stored in the ImageData object.
So, yes, it will not composite your ImageData content with what was there, it will just replace it.
If you wish to draw your ImageData content as if it were a mere bitmap content, you can convert it to an ImageBitmap instead:
(async() => {
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
const arr = new Uint8ClampedArray(4*100*100);
for (let i = 0; i < arr.length; i += 4) {
arr[i + 0] = 255;
arr[i + 1] = 0;
arr[i + 2] = 0;
arr[i + 3] = 100;
}
let imageData = new ImageData(arr, 100);
const bmp = await createImageBitmap(imageData);
context.drawImage(bmp, 0, 0);
context.drawImage(bmp, 50, 50);
})();
<canvas></canvas>
Though beware this method is somehow async, so in an animation frame that might not be the best to use, and you may prefer falling back on using a second, offscreen, canvas, at the cost of a bigger memory impact.

Related

Find Hex Colors and X,Y in Images

I am trying to find all the hex colors in an image and if possible, circle or highlight the X, Y position of where the hex color(s) are. My current code is attempting to find all colors and almost crashes my browser and my attempt to find the X,Y coordinates of each image isn't going good either.
I have two functions doing different things, it's what I have tried to work with to give an example of what has been attempted... Any help would be great!
Any assistance would be amazing!
<canvas id="myCanvas" width="240" height="297" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.
</canvas>
<img id="origImage" width="220" height="277" src="loggraph.PNG">
<script>
function getPixel(imgData, index) {
var i = index*4, d = imgData.data;
return [d[i],d[i+1],d[i+2],d[i+3]] // [R,G,B,A]
}
function getPixelXY(imgData, x, y) {
return getPixel(imgData, y*imgData.width+x);
}
function goCheck() {
var cvs = document.createElement('canvas'),
img = document.getElementsByTagName("img")[0];
cvs.width = img.width; cvs.height = img.height;
var ctx = cvs.getContext("2d");
ctx.drawImage(img,0,0,cvs.width,cvs.height);
var idt = ctx.getImageData(0,0,cvs.width,cvs.height);
console.log(getPixel(idt, 852)); // returns array [red, green, blue, alpha]
console.log(getPixelXY(idt,1,1)); // same pixel using x,y
}
function getColors(){
var canvas = document.getElementById("myCanvas");
var devices = canvas.getContext("2d");
var imageData = devices.getImageData(0, 0, canvas.width, canvas.height);
var data = imageData.data;
// iterate over all pixels
for(var i = 0, n = data.length; i < n; i += 4) {
var r = data[i];
var g = data[i + 1];
var b = data[i + 2];
var rgb = "("+r+","+g+","+b+")";
var incoming = i*4, d = imageData.data;
var bah = [d[incoming],d[incoming+1],d[incoming+2],d[incoming+3]];
$('#list').append("<li>"+rgb+"</li>");
colorList.push(rgb);
}
$('#list').append("<li>"+[d[incoming],d[incoming+1],d[incoming+2],d[incoming+3]]+"</li>");
}
}
Must check all pixels
To find a pixel that matches a color will require, in the worst case (pixel of that color not in image), that you step over every pixel in the image.
How not to do it
Converting every pixel to a DOM string is about the worst way to do it, as DOM string use a lot of memory and CPU overhead, especially if instantiated using jQuery (which has its own additional baggage)
Hex color to array
To find the pixel you need only check each pixels color data against the HEX value. You convert the hex value to an array of 3 Bytes.
The following function will convert from CSS Hex formats "#HHH" "#HHHH", "#HHHHHH" and "#HHHHHHHH" ignoring the alpha part if included, to an array of integers 0-255
const hex2RGB = h => {
if(h.length === 4 || h.length === 5) {
return [parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16), parseInt(h[3] + h[3], 16)];
}
return [parseInt(h[1] + h[2], 16), parseInt(h[3] + h[4], 16), parseInt(h[5] + h[6], 16)];
}
Finding the pixel
I do not know how you plan to use such a feature so the example below is a general purpose method that will help and can be modified as needed
It will always find a pixel if you let it even if there is no perfect match. It does this by finding the closest color to the color you are looking for.
The reason that of finds the closest match is that when you draw an image onto a 2D canvas the pixel values are modified slightly if the image has transparent pixels (pre-multiplied alpha)
The function finds the pixel by measuring the spacial distance between the pixel and the hex color (simple geometry Pythagoras). The closest color is the one that is the smallest distance.
It will return the object
{
x, // the x coordinate of the match
y, // the y coordinate of the match
distance, // how closely the color matches the requested color.
// 0 means a perfect match
// to 441 completely different eg black and white
// value is floored to an integer value
}
If the image is tainted (cross origin, local device storage), or you pass something that can not be converted to pixels the function will return undefined
The function keeps a canvas that it uses to get pixel data as it assumes that it will be use many times. If the image is tainted it will catch the error (add a warning to the console), cleanup the tainted canvas and be ready for another image.
Usage
To use the function add it to your code base, it will setup automatically.
Get an image and a hex value and call the function with the image, CSS hex color, and optionally the threshold distance for the color match.
Eg find exact match for #FF0000
const result = findPixel(origImage, "#FF0000", 0); // find exact match for red
if (result) { // only if found
console.log("Found color #FF0000 at pixel " + result.x + ", " + result.y);
} else {
console.log("The color #FF0000 is not in the image");
}
or find color close to
const result = findPixel(origImage, "#FF0000", 20); // find a match for red
// within 20 units.
// A unit is 1 of 256
if (result) { // only if found
console.log("Found closest color within " + result.distance + "units of #FF0000 at pixel " + result.x + ", " + result.y);
}
or find closest
// find the closest, no threshold ensures a result
const result = findPixel(origImage, "#FF0000");
console.log("Found closest color within " + result.distance + "units of #FF0000 at pixel " + result.x + ", " + result.y);
Code
The function is as follows.
const findPixel = (() => {
var can, ctx;
function createCanvas(w, h) {
if (can === undefined){
can = document.createElement("canvas");
ctx = can.getContext("2d");
}
can.width = w;
can.height = h;
}
function getPixels(img) {
const w = img.naturalWidth || img.width, h = img.naturalHeight || img.height;
createCanvas(w, h);
ctx.drawImage(img, 0, 0);
try {
const imgData = ctx.getImageData(0, 0, w, h);
can.width = can.height = 1; // make canvas as small as possible so it wont
// hold memory. Leave in place to avoid instantiation overheads
return imgData;
} catch(e) {
console.warn("Image is un-trusted and pixel access is blocked");
ctx = can = undefined; // canvas and context can no longer be used so dump them
}
return {width: 0, height: 0, data: []}; // return empty pixel data
}
const hex2RGB = h => { // Hex color to array of 3 values
if(h.length === 4 || h.length === 5) {
return [parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16), parseInt(h[3] + h[3], 16)];
}
return [parseInt(h[1] + h[2], 16), parseInt(h[3] + h[4], 16), parseInt(h[5] + h[6], 16)];
}
const idx2Coord = (idx, w) => ({x: idx % w, y: idx / w | 0});
return function (img, hex, minDist = Infinity) {
const [r, g, b] = hex2RGB(hex);
const {width, height, data} = getPixels(img);
var idx = 0, found;
while (idx < data.length) {
const R = data[idx] - r;
const G = data[idx + 1] - g;
const B = data[idx + 2] - b;
const d = R * R + G * G + B * B;
if (d === 0) { // found exact match
return {...idx2Coord(idx / 4, width), distance: 0};
}
if (d < minDist) {
minDist = d;
found = idx;
}
idx += 4;
}
return found ? {...idx2Coord(found / 4, width), distance: minDist ** 0.5 | 0 } : undefined;
}
})();
This function has been tested and works as described above.
Note Going by the code in the your question the alpha value of the image and CSS hex color is ignored.
Note that if you intend to find many colors from the same image this function is not the best suited for you needs. If this is the case let me know in the comment and I can make changes or instruct you how to optimism the code for such uses.
Note It is not well suited for single use only. However if this is the case change the line const findPixel = (() => { to var findPixel = (() => { and after you have used it remove the reference findpixel = undefined; and JS will clean up any resources it holds.
Note If you also want to get the actual color of the closest found color that is trivial to add as well. Ask in the comments.
Note It is reasonably quick (you will be hard pressed to get a quicker result) but be warned that for very large images 4K and above it may take a bit, and on very low end devices it may cause a out of memory error. If this is a problem then another solution is possible but is far slower.

Manually blending ImageData

I have a following task that I'm trying to accomplish the most efficient way possible: I have varying number of pictures of varying size as pixel arrays that I need to add to canvas pixel by pixel. Each pixel's value has to be added to canvas's ImageData so that the result is a blend of two or more images.
My current solution is to retrieve ImageData from the location where the picture needs to be blended with the size of the picture. Then I add the picture's ImageData to the retrieved ImageData and copy it back to the same location.
In a sense this is a manual implementation of canvas globalCompositeOperation "lighter".
"use strict";
let canvas = document.getElementById("canvas");
let width = canvas.width = window.innerWidth;
let height = canvas.height = window.innerHeight;
let ctx = canvas.getContext("2d");
ctx.fillStyle="black";
ctx.fillRect(0, 0, width, height);
let imageData = ctx.getImageData(0,0,width,height);
let data = imageData.data;
function random(min, max) {
let num = Math.floor(Math.random() * (max - min + 1)) + min;
return num;
}
function createColorArray(size, color) {
let arrayLength = (size*size)*4;
let array = new Uint8ClampedArray(arrayLength);
for (let i = 0; i < arrayLength; i+=4) {
switch (color) {
case 1:
array[i+0] = 255; // r
array[i+1] = 0; // g
array[i+2] = 0; // b
array[i+3] = 255; // a
break;
case 2:
array[i+0] = 0; // r
array[i+1] = 255; // g
array[i+2] = 0; // b
array[i+3] = 255; // a
break;
case 3:
array[i+0] = 0; // r
array[i+1] = 0; // g
array[i+2] = 255; // b
array[i+3] = 255; // a
}
}
return array;
}
function picture() {
this.size = random(10, 500);
this.x = random(0, width);
this.y = random(0, height);
this.color = random(1,3);
this.colorArray = createColorArray(this.size, this.color);
}
picture.prototype.updatePixels = function() {
let imageData = ctx.getImageData(this.x, this.y, this.size, this.size);
let data = imageData.data;
for (let i = 0; i < data.length; ++i) {
data[i]+=this.colorArray[i];
}
ctx.putImageData(imageData, this.x, this.y);
}
let pictures = [];
let numPictures = 50;
for (let i = 0; i < numPictures; ++i) {
let pic = new picture();
pictures.push(pic);
}
function drawPictures() {
for (let i = 0; i < pictures.length; ++i) {
pictures[i].updatePixels();
}
}
drawPictures();
<!DOCTYPE html>
<html>
<head>
<title>...</title>
<style type="text/css">
body {margin: 0px}
#canvas {position: absolute}
</style>
</head>
<body>
<div>
<canvas id="canvas"></canvas>
</div>
<script type="text/javascript" src="js\script.js"></script>
</body>
</html>
This solution works fine but it's very slow. I don't know if pixel by pixel blending can even be made very efficient, but one reason for slow performance might be that I need to get the ImageData and put it back each time a new image is blended into canvas.
Therefore the main question is how could I get whole canvas ImageData once in the beginning and then look correct pixels to update based on location and size of each picture that needs to blended into canvas and finally put updated ImageData back to canvas? Also, any other ideas on how to make blending more efficient are greatly appreciated.
Use the array methods.
The fastest way to fill an array is with the Array.fill function
const colors = new Uint32Array([0xFF0000FF,0xFF00FF00,0xFFFF00]); // red, green, blue
function createColorArray(size, color) {
const array32 = new Uint32Array(size*size);
array32.fill(colors[color]);
return array32;
}
Quick clamped add with |
If you are adding 0xFF to any channel and 0 to the others you can use | and a 32 bit array. For the updatePixels function
var imageData = ctx.getImageData(this.x, this.y, this.size, this.size);
var data = new Uint32Array(imageData.data.buffer);
var i = 0;
var pic = this.colorArray; // use a local scope for faster access
do{
data[i] |= pic[i] ; // only works for 0 and FF chanel values
}while(++i < data.length);
ctx.putImageData(imageData, this.x, this.y);
Bitwise or | is similar to arithmetic add and can be used to increase values using 32bit words. The values will be clamped as part of the bitwise operation.
// dark
var red = 0xFF000088;
var green = 0xFF008800;
var yellow = red | green; // 0xFF008888
There are many other ways to use 32bit operations to increase performance as long as you use only 1 or 2 operators. More and you are better off using bytes.
You can also add if you know that each channel will not overflow a bit
a = 0xFF101010; // very dark gray
b = 0xFF000080; // dark red
// non overflowing add
c = a + b; // result is 0xFF000090 correct
// as 0x90 + 0x80 will overflow = 0x110 the add will not work
c += b; // result overflows bit to green 0xFF000110 // incorrect
Uint8Array V Uint8ClampedArray
Uint8Array is slightly faster than Uint8ClampedArray as the clamping is skipped for the Uint8Array so use it if you don't need to clamp the result. Also the int typedArrays do not need you to round values when assigning to them.
var data = Uint8Array(1);
data[0] = Math.random() * 255; // will floor for you
var data = Uint8Array(1);
data[0] = 256; // result is 0
data[0] = -1; // result is 255
var data = Uint8ClampedArray(1);
data[0] = 256; // result is 255
data[0] = -1; // result is 0
You can copy data from array to array
var imageDataSource = // some other source
var dataS = new Uint32Array(imageData.data.buffer);
var imageData = ctx.getImageData(this.x, this.y, this.size, this.size);
var data = new Uint32Array(imageData.data.buffer);
data.set(dataS); // copies all data
// or to copy a row of pixels
// from coords
var x = 10;
var y = 10;
var width = 20; // number of pixels to copy
// to coords
var xx = 30
var yy = 30
var start = y * this.size + x;
data.set(dataS.subArray(start, start + width), xx + yy * this.size);
Dont dump buffers
Don't keep fetching pixel data if not needed. If it does not change between putImageData and getImageData then there is no need to get the data again. It is better to keep the one buffer than continuously creating a new one. This will also relieve the memory stress and reduce the workload on GC.
Are you sure you can not use the GPU
And you can perform a wide range of operations on pixel data using global composite operations. Add, subtract, multiply, divide, invert These are much faster and so far in your code I can see no reason why you need to access the pixel data.

How to change window center and width of an image on mouse move event

I have a DICOM image and i want to apply a W/L on the image. In normal terms, i am trying to change the color of the image as i drag the mouse over it. however it becomes completely white when i try to change it.
here's what I am doing.
this is just a part of the code.
var imageData = ctx.getImageData(0, 0, img.width, img.height);
var data = imageData.data;
var pixels = imageData.data,
len = pixels.length;
var a = 256 * slope / window_width,
b = 256 * ((intercept - window_level) / window_width);
for (i = 0; i < len; i += 4) {
//pixval = a * (pixels[i] * 256 + pixels[i + 1]) + b;
//pixels[i] = pixels[i + 1] = pixels[i + 2] = pixval
if (pixels[i + 3] == 0)
continue;
var pixelIndex = i;
var red = pixels[pixelIndex]; // red color
var green = pixels[pixelIndex + 1]; // green color
var blue = pixels[pixelIndex + 2]; // blue color
var alpha = pixels[pixelIndex + 3];
var pixelValue = a * (red * 256 + green + b);
pixels[i] = pixels[i + 1] = pixels[i + 2] = pixelValue;
pixels[i + 3] = 0xff;
//console.log(pixelValue == pixval);
}
here's the JSFIDDLE with the complete code.
The main problem is that you're changing your image data when drawing it. You need to store a raw version of your image data, then "apply" your window level dynamically when drawing. Otherwise, you're just compounding the changes on top of each other.
Try making two canvas objects: one (offscreen) to load and store your image data and the other to draw it. Making this relatively small change to your code, it appeared to work right.
There may be other issues with your starting values -- I think you may be missing rescale slope and intercept -- but fixing the first issue should make everything easier to figure out.
Here's the jsfiddle: https://jsfiddle.net/8ne1pnaj/5/
var imageData = ctxData.getImageData(0, 0, img.width, img.height);
// ...
ctx.putImageData(imageData, 0, 0)
ctxData is the original data. ctx is the drawing context.

sampling an image a tile at a time using canvas, getImageData and a Web Worker

I am attempting to build a simple HTML5 canvas based image processor that takes an image and generates a tiled version of it with each tile being the average color of the underlying image area.
This is easy enough to do outside the context of a Web Worker but I'd like to use a worker so as not to block the ui processing thread. The Uint8ClampedArray form the data takes is giving me a headache with regards to how to process it tile by tile.
Below is a plunk demonstrating what I've done so far and how it's not working.
http://plnkr.co/edit/AiHmLM1lyJGztk8GHrso?p=preview
The relevant code is in worker.js
Here it is:
onmessage = function (e) {
var i,
j = 0,
k = 0,
data = e.data,
imageData = data.imageData,
tileWidth = Math.floor(data.tileWidth),
tileHeight = Math.floor(data.tileHeight),
width = imageData.width,
height = imageData.height,
tile = [],
len = imageData.data.length,
offset,
processedData = [],
tempData = [],
timesLooped = 0,
tileIncremented = 1;
function sampleTileData(tileData) {
var blockSize = 20, // only visit every x pixels
rgb = {r:0,g:0,b:0},
i = -4,
count = 0,
length = tileData.length;
while ((i += blockSize * 4) < length) {
if (tileData[i].r !== 0 && tileData[i].g !== 0 && tileData[i].b !== 0) {
++count;
rgb.r += tileData[i].r;
rgb.g += tileData[i].g;
rgb.b += tileData[i].b;
}
}
// ~~ used to floor values
rgb.r = ~~(rgb.r/count);
rgb.g = ~~(rgb.g/count);
rgb.b = ~~(rgb.b/count);
processedData.push(rgb);
}
top:
for (; j <= len; j += (width * 4) - (tileWidth * 4), timesLooped++) {
if (k === (tileWidth * 4) * tileHeight) {
k = 0;
offset = timesLooped - 1 < tileHeight ? 4 : 0;
j = ((tileWidth * 4) * tileIncremented) - offset;
timesLooped = 0;
tileIncremented++;
sampleTileData(tempData);
tempData = [];
//console.log('continue "top" loop for new tile');
continue top;
}
for (i = 0; i < tileWidth * 4; i++) {
k++;
tempData.push({r: imageData.data[j+i], g: imageData.data[j+i+1], b: imageData.data[j+i+2], a: imageData.data[j+i+3]});
}
//console.log('continue "top" loop for new row per tile');
}
postMessage(processedData);
};
I'm sure there's a better way of accomplishing what I'm trying to do starting at the labeled for loop. So any alternative methods or suggestions would be much appreciated.
Update:
I've taken a different approach to solving this:
http://jsfiddle.net/TunMn/425/
Close, but no.
I know what the problem is but I have no idea how to go about amending it. Again, any help would be much appreciated.
Approach 1: Manually calculating average per tile
Here is one approach you can try:
There is only need for reading, update can be done later using HW acceleration
Use async calls for every row (or tile if the image is very wide)
This gives an accurate result but is slower and depends on CORS restrictions.
Example
You can see the original image for a blink below. This shows the asynchronous approach works as it allows the UI to update while processing the tiles in chunks.
window.onload = function() {
var img = document.querySelector("img"),
canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d"),
w = img.naturalWidth, h = img.naturalHeight,
// store average tile colors here:
tileColors = [];
// draw in image
canvas.width = w; canvas.height = h;
ctx.drawImage(img, 0, 0);
// MAIN CALL: calculate, when done the callback function will be invoked
avgTiles(function() {console.log("done!")});
// The tiling function
function avgTiles(callback) {
var cols = 8, // number of tiles (make sure it produce integer value
rows = 8, // for tw/th below:)
tw = (w / cols)|0, // pixel width/height of each tile
th = (h / rows)|0,
x = 0, y = 0;
(function process() { // for async processing
var data, len, count, r, g, b, i;
while(x < cols) { // get next tile on x axis
r = g = b = i = 0;
data = ctx.getImageData(x * tw, y * th, tw, th).data; // single tile
len = data.length;
count = len / 4;
while(i < len) { // calc this tile's color average
r += data[i++]; // add values for each component
g += data[i++];
b += data[i++];
i++
}
// store average color to array, no need to write back at this point
tileColors.push({
r: (r / count)|0,
g: (g / count)|0,
b: (b / count)|0
});
x++; // next tile
}
y++; // next row, but do an async break below:
if (y < rows) {
x = 0;
setTimeout(process, 9); // call it async to allow browser UI to update
}
else {
// draw tiles with average colors, fillRect is faster than setting each pixel:
for(y = 0; y < rows; y++) {
for(x = 0; x < cols; x++) {
var col = tileColors[y * cols + x]; // get stored color
ctx.fillStyle = "rgb(" + col.r + "," + col.g + "," + col.b + ")";
ctx.fillRect(x * tw, y * th, tw, th);
}
}
// we're done, invoke callback
callback()
}
})(); // to self-invoke process()
}
};
<canvas></canvas>
<img src="http://i.imgur.com/X7ZrRkn.png" crossOrigin="anonymous">
Approach 2: Letting the browser do the job
We can also let the browser do the whole job exploiting interpolation and sampling.
When the browser scales an image down it will calculate the average for each new pixel. If we then turn off linear interpolation when we scale up we will get each of those average pixels as square blocks:
Scale down image at a ratio producing number of tiles as number of pixels
Turn off image smoothing
Scale the small image back up to the desired size
This will be many times faster than the first approach, and you will be able to use CORS-restricted images. Just note it may not be as accurate as the first approach, however, it is possible to increase the accuracy by scaling down the image in several step, each half the size.
Example
window.onload = function() {
var img = document.querySelector("img"),
canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d"),
w = img.naturalWidth, h = img.naturalHeight;
// draw in image
canvas.width = w; canvas.height = h;
// scale down image so number of pixels represent number of tiles,
// here use two steps so we get a more accurate result:
ctx.drawImage(img, 0, 0, w, h, 0, 0, w*0.5, h*0.5); // 50%
ctx.drawImage(canvas, 0, 0, w*0.5, h*0.5, 0, 0, 8, 8); // 8 tiles
// turn off image-smoothing
ctx.imageSmoothingEnabled =
ctx.msImageSmoothingEnabled =
ctx.mozImageSmoothingEnabled =
ctx.webkitImageSmoothingEnabled = false;
// scale image back up
ctx.drawImage(canvas, 0, 0, 8, 8, 0, 0, w, h);
};
<canvas></canvas>
<img src="http://i.imgur.com/X7ZrRkn.png" crossOrigin="anonymous">

HTML5 Canvas, replacing colors in an image not working on some machines

I have a 2d RTS HTML5 / Javascript game. I use images to display the player's units and buildings. I provide the image and then use a script to replace certain colors in the images with other color, to get different versions of an image with different colors (so the soldier of player 1 has a red sword and the soldier of player 2 has a blue sword and so on...).
The problem is, for maybe ~20% of the users this replacing thing doesnt work and they see all units in the same (default) color. Im now wondering why this is. Heres the function i use to replayce the colors:
// returns a image with some colors replaced, specified by search and replace, which are arrays of color arrays ([[255, 255, 255], [...], ...], )
ImageTransformer.replaceColors = function(img, search, replace)
{
var canv = document.createElement('canvas');
canv.height = img.height;
canv.width = img.width
var ctx = canv.getContext('2d');
ctx.drawImage(img, 0, 0);
var imgData = ctx.getImageData(0, 0, canv.width, canv.height);
for(var i = 0; i < imgData.data.length; i += 4)
for(var k = 0; k < search.length; k++)
if(imgData.data[i] == search[k][0] && imgData.data[i + 1] == search[k][1] && imgData.data[i + 2] == search[k][2])
{
imgData.data[i] = replace[k][0];
imgData.data[i + 1] = replace[k][1];
imgData.data[i + 2] = replace[k][2];
}
ctx.putImageData(imgData, 0, 0);
return canv;
}
Browsers may or may not apply a gamma to the image prior to drawing them, the intent is to have more natural colors (...).
I bet this is the Browsers which apply a gama that fool your algorithm.
Rather than test for strict equality, you might use a color distance, and decide of a threshold to decide wether to switch or not :
var imgData = ctx.getImageData(0, 0, canv.width, canv.height);
var data = imgData.data, length = imgData.data.length ;
for(var k = 0; k < search.length; k++) {
var thisCol = search[k];
for(var i = 0; i < length; i += 4) {
var colDist = Math.abs(data[i] - thisCol[0] )
+ Math.abs(data[i+1] - thisCol[1] )
+ Math.abs(data[i+2] - thisCol[2] );
if( colDist < 5 )
{
data[i] = thisCol[0];
data[i + 1] = thisCol[1];
data[i + 2] = thisCol[2];
}
}
}
ctx.putImageData(imgData, 0, 0);
return canv;
(here i used as distance the sum of absolute differences in between r,g,b ; as #MarkE suggest, you can choose others, euclidian being this:
var colDist = sq(data[i] - thisCol[0] )
+ sq(data[i+1] - thisCol[1] )
+ sq(data[i+2] - thisCol[2] );
// notice this is the squared euclidian distance.
// whith function sq(x) { return x*x }
test several pictures / distances, and see what fits.
test several threshold also.
).

Categories

Resources