问题
Edit: I don't necessarily need a solution to this problem--rather I'd like to understand why it's occurring. I don't see why I should be getting the odd results below...
Although this question is directed towards an issue I'm having with an HTML5 canvas application, I think the problem is less specific.
I have an HTML5 canvas app that allows you to stamp images on the screen. These images are 32bit PNG's, so I'm working with transparency. If I stamp a highly transparent image in the same location many times (roughly 100), I end up with an absolutely terrible result:
The color of the image that I'm using as a stamp is RGB(167, 22, 22)
and the background that I'm stamping onto is RGB(255, 255, 255)
. Here's the source image, if anyone's interested:
As you can tell, the image has extremely low alpha levels. Likely about 2/255 to 5/255
or so. What I would expect to happen is that if you repeatedly apply the image stamp to the canvas enough times, you'll get pixels of color RGBA(167, 22, 22, 255)
. Unfortunately, I'm getting a mixed bag of colors including some very odd regions of gray with a value of RGB(155, 155, 155)
.
I just loaded up Excel and plugged in the equation for source-over alpha blending (Wikipedia reference) and I seem to be converging to RGB(167, 22, 22)
after enough iterations. I'm probably missing something fundamental about alpha blending operations and how the HTML5 canvas implements source-over compositing... can anyone help straighten me out?
Thanks!
Note: this question is similar to my issue, but I don't quite understand why I'm getting the results I've posted here.
回答1:
The precision and rounding rules of canvas math internals are mostly undefined, so it's hard to say exactly what's happening here. All we really know is that the pixels are unsigned bytes, and the alpha is premultiplied.
However, we can get some information by using getImageData
to inspect the pixels as the stamp is drawn, like so:
var px = 75;
var py = 100;
var stamp = new Image;
stamp.onload = function() {
for (var i = 0; i < 100; ++i) {
imageData = context.getImageData(px, py, 1, 1);
console.log(Array.prototype.slice.call(imageData.data, 0, 4));
context.drawImage(stamp, 0, 0);
}
};
stamp.src = 'stamp.png';
The sample at px = 75
, py = 100
is right in the middle of a gray blob. After drawing the stamp once on a white canvas, the log reads:
[254, 254, 254, 255]
At px = 120
, py = 150
, the sample is in the middle of a red area. After drawing the stamp once, the log reads:
[254, 253, 253, 255]
So, it looks like the canvas was modified by (-1, -1, -1) for the grey pixel, and (-1, -2, -2) for the red pixel.
Sampling these same pixels in the stamp image using RMagick gives:
[167, 22, 22, 1] // x = 75, y = 100
[167, 22, 22, 2] // x = 120, y = 150
Working through the math, using the standard alpha blending equation, you can test each of the color values:
function blend(dst, src) {
var a = src[3] / 255.0
return [
(1.0 - a) * dst[0] + a * src[0],
(1.0 - a) * dst[1] + a * src[1],
(1.0 - a) * dst[2] + a * src[2]
];
}
console.log(blend([255, 255, 255], [167, 22, 22, 1]));
// output: [254.6549..., 254.0862..., 254.0862...]
console.log(blend([255, 255, 255], [167, 22, 22, 2]));
// output: [254.3098..., 253.1725..., 253.1725...]
From this, we can guess that the canvas blending code is actually flooring the results, instead of rounding them. This would give you a result of [254, 254, 254]
and [254, 253, 253]
, like we saw from canvas. They're likely not doing any rounding at all, and it's being floored implicitly when cast back to an unsigned byte.
This is why the other post recommends storing the image data as an array of floats, doing the math yourself, and then updating the canvas with the result. You get more precision that way, and can control things like rounding.
Edit: In fact, this blend()
function isn't exactly right, even when the results are floored, as the canvas pixel values for 120, 150
stabilize at [127, 0, 0]
, and this function stabilizes at [167, 22, 22]
. Similarly, when I drew the image just once into a transparent canvas, getImageData
on the pixel at 120, 150
was [127, 0, 0, 2]
. What?!
It turns out that this is caused by premultiplication, which seems to be applied to loaded Image elements. See this jsFiddle for an example.
Premultiplied pixels are stored as:
// r, g, b are 0 to 255
// a is 0 to 1
// dst is all 0 to 255
dst.r = Math.floor(r * a);
dst.g = Math.floor(g * a);
dst.b = Math.floor(b * a);
dst.a = a * 255;
They are unpacked later as:
inv = 1.0 / (a / 255);
r = Math.floor(dst.r * inv);
g = Math.floor(dst.g * inv);
b = Math.floor(dst.b * inv);
Running this pack/unpack against [167, 22, 22, 2]
reveals:
a = 2 / 255; // 0.00784
inv = 1.0 / (2 / 255); // 127.5
r = Math.floor(Math.floor(167 * a) * inv); // 127
g = Math.floor(Math.floor(22 * a) * inv); // 0
b = Math.floor(Math.floor(22 * a) * inv); // 0
来源:https://stackoverflow.com/questions/5360699/poor-results-with-source-over-alpha-blending-html5-canvas