Jake Chu Jake Chu - 5 months ago 16
HTML Question

context.putImageData of HTML <canvas> set on wrong coordinate

I am trying to get each average color of 100 rectangle as pictures

enter image description here

As the picture show, the pasted imageData doesn't set suitably. But the all coordinate parameter of (

ctx.getImageData()
and
ctx.putImageData()
) is same as
console.log
picture I attached

enter image description here

Is it bug? Or did I miss something ?

convert(){
let canvas = this.$el.querySelector('#pixel-art-canvas'),
image = this.$el.querySelector('#upload-image'),
ctx = canvas.getContext('2d'),
degree = 10,
img = new Image,
tiles = Math.pow(degree,2),
eachWidth,eachHeight;

img.src = image.src;
ctx.drawImage(img,0,0);

eachWidth= canvas.width/degree;
eachHeight= canvas.height/degree;

for(let k = 0; k < tiles; k++) {
let imgd,x,y,
rgb = {r:0,g:0,b:0},
count = 0;

x = (k % degree) * eachWidth;
y = (k / degree) * eachHeight;
imgd = ctx.getImageData(x, y, eachWidth, eachHeight);
console.log('x: ' + x + ' , y:' +y+' , w: '+eachWidth + ' , h :' +eachHeight);

for (let i=0; i < imgd.data.length; i=i+4) {
rgb.r += imgd.data[i];
rgb.g += imgd.data[i+1];
rgb.b += imgd.data[i+2];
count++;
}

rgb.r = ~~(rgb.r/count);
rgb.g = ~~(rgb.g/count);
rgb.b = ~~(rgb.b/count);

for (let j=0; j < imgd.data.length; j=j+4) {
imgd.data[j] = rgb.r;
imgd.data[j+1] = rgb.g;
imgd.data[j+2] = rgb.b;
}

ctx.putImageData(imgd, x, y, 0, 0, eachWidth, eachHeight);
console.log('x: ' + x + ' , y:' +y+' , w: '+eachWidth + ' , h :' +eachHeight);
}//end for

}

Answer Source

The problem is that you are incorrectly calculating the y coordinate.

You have

y = (k / degree) * eachHeight;

Which will give a fractional result for (k/degree) you need to round (floor) the value before multiplying it.

// to fix the y coord.
y = Math.floor(k / degree)  * eachHeight;
// or 
y = ((k / degree) | 0) * eachHeight;

Also you are incorrectly getting the mean of the colors. RGB values represent the square root of the intensity of the pixel. Thus the difference in intensity between a RGB value of 128 and 256 is not 2 times but 4 times as bright.

If you take the mean by just summing the RGB values you end up with a result that is darker than it should be.

The correct method is to get the mean of the the square of the RGB values, then convert back to logarithmic RGB to put back onto the canvas.

Change you code

const rgb = {r:0,g:0,b:0};
var count = 0;

for (let i=0; i < imgd.data.length; i=i+4) {
    rgb.r += imgd.data[i] * imgd.data[i];   // Square the value
    rgb.g += imgd.data[i + 1] * imgd.data[i + 1];
    rgb.b += imgd.data[i + 2] * imgd.data[i + 1];
    count++;
}
// Get mean and convert back to logarithmic
// Also you do not need to floor the values with ~~(val) as
// the array is of type Uint8ClampedArray which will floor and clamp the
// values for you. 
// Also ~~(val) requires 2 operations, a quicker way the requires only one
// operation is (val) | 0
rgb.r = Math.sqrt(rgb.r/count);
rgb.g =  Math.sqrt(rgb.g/count);
rgb.b =  Math.sqrt(rgb.b/count);

for (let j=0; j < imgd.data.length; j=j+4) {
      imgd.data[j] = rgb.r;
      imgd.data[j+1] = rgb.g;
      imgd.data[j+2] = rgb.b;
  }

ctx.putImageData(imgd, x, y, 0, 0, eachWidth, eachHeight);

You can also optimise a little via.

const x = (k % degree) * eachWidth;
const y = ((k / degree) | 0) * eachHeight;
const imgd = ctx.getImageData(x, y, eachWidth, eachHeight);
const rgb = {r : 0, g : 0, b : 0};
const count = imgd.data.length / 4;
var i = 0;
while( i < imgd.data.length ) { 
    rgb.r += imgd.data[i] * imgd.data[i++];   // square the value
    rgb.g += imgd.data[i] * imgd.data[i++];
    rgb.b += imgd.data[i] * imgd.data[i++];
    i ++;
}
// need to round as we are not directly adding back to the buffer.
rgb.r = Math.sqrt(rgb.r / count) | 0;
rgb.g =  Math.sqrt(rgb.g / count) | 0;
rgb.b =  Math.sqrt(rgb.b / count) | 0;

// get a refer to 32 bit version of same data and set all values
// the 0xFF000000 sets the alpha to 255
// shift red 2 bytes (16 bits)
// shift green 1 byte (8 bit)
// blue is in the correct place.
new Uint32Array(imgd.data.buffer).fill(
    0xFF000000 + (rgb.r << 16) + (rgb.g << 8) + rgb.b
); 

// putImageData use 3 arguments imgd and the x, y if you are
// copying all the data back to the canvas.
ctx.putImageData(imgd, x, y);