Плохие результаты при альфа-смешивании с исходным кодом (холст HTML5) - PullRequest
7 голосов
/ 19 марта 2011

Редактировать: Мне не обязательно нужно решение этой проблемы - скорее, я бы хотел понять , почему это происходит. Я не понимаю, почему я должен получить странные результаты ниже ...

Хотя этот вопрос направлен на решение проблемы, возникшей у меня с приложением HTML5 canvas, я думаю, что проблема менее специфична.

У меня есть приложение HTML5 canvas, которое позволяет вам печатать изображения на экране. Эти изображения являются 32-битными PNG, поэтому я работаю с прозрачностью. Если я много раз штампую очень прозрачное изображение в одном и том же месте (примерно 100), я получаю совершенно ужасный результат:

http://i.imgur.com/qYY5n.png

Цвет изображения, которое я использую в качестве штампа, - RGB(167, 22, 22), а фон, на котором я штампую, - RGB(255, 255, 255). Вот исходное изображение, если кому-то интересно:

http://i.stack.imgur.com/Wm9Dx.png

Как вы можете сказать, изображение имеет очень низкие уровни альфа. Вероятно, около 2/255 to 5/255 или около того. Я ожидаю, что произойдет, если вы неоднократно примените штамп изображения к холсту достаточно раз, вы получите пиксели цвета RGBA(167, 22, 22, 255). К сожалению, я получаю смешанную сумку цветов, включая некоторые очень странные области серого со значением RGB(155, 155, 155).

Я только что загрузил Excel и подключил уравнение для альфа-смешения с исходным кодом ( ссылка на Википедию ), и мне кажется, что после достаточного количества итераций я стремлюсь к RGB(167, 22, 22). Вероятно, я упускаю что-то фундаментальное в операциях альфа-смешивания и в том, как на холсте HTML5 реализовано наложение исходного кода ... Кто-нибудь может мне помочь?

Спасибо!

Примечание: этот вопрос похож на мою проблему, но я не совсем понимаю, почему я получаю результаты, которые я разместил здесь.

1 Ответ

5 голосов
/ 22 марта 2011

Точность и правила округления внутренних элементов холста по математике в основном не определены, поэтому трудно точно сказать, что здесь происходит. Все, что мы действительно знаем, это то, что пиксели являются байтами без знака, а альфа предварительно умножается.

Однако мы можем получить некоторую информацию, используя getImageData для проверки пикселей при рисовании штампа, например:

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';

Образец в px = 75, py = 100 находится прямо в середине серого шарика. После нанесения штампа на белый холст, журнал гласит:

[254, 254, 254, 255]

При px = 120, py = 150 образец находится в середине красной области. После нанесения штампа один раз журнал гласит:

[254, 253, 253, 255]

Итак, похоже, что холст был изменен на (-1, -1, -1) для серого пикселя и (-1, -2, -2) для красного пикселя.

Выборка этих же пикселей в изображении штампа с использованием RMagick дает:

[167, 22, 22, 1]  // x = 75, y = 100
[167, 22, 22, 2]  // x = 120, y = 150

Работая по математике, используя стандартное уравнение альфа-смешивания, вы можете проверить каждое из значений цвета:

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...]

Исходя из этого, мы можем догадаться, что код наложения холста фактически представляет результаты, а не округляет их. Это даст вам результат [254, 254, 254] и [254, 253, 253], как мы видели на холсте. Скорее всего, они вообще не выполняют никакого округления, и оно неявно сбрасывается при возвращении к неподписанному байту.

Именно поэтому в другом посте рекомендуется хранить данные изображения в виде массива с плавающей точкой, самостоятельно выполнять математические вычисления, а затем обновлять холст с результатом. Таким образом вы получаете больше точности и можете управлять такими вещами, как округление.

Редактировать: На самом деле, эта функция blend() не совсем правильная, даже когда результаты получены, поскольку значения пикселей холста для 120, 150 стабилизируются на [127, 0, 0], и эта функция стабилизируется на [167, 22, 22]. Точно так же, когда я рисовал изображение только один раз в прозрачном холсте, getImageData на пикселе в 120, 150 было [127, 0, 0, 2]. Что?!

Оказывается, это вызвано предварительным умножением, которое, похоже, применяется к загруженным элементам изображения. См. this jsFiddle для примера.

Предварительно умноженные пиксели сохраняются как:

// 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;

Они распаковываются позже как:

inv = 1.0 / (a / 255);
r = Math.floor(dst.r * inv);
g = Math.floor(dst.g * inv);
b = Math.floor(dst.b * inv);

Запуск этого пакета / распаковки против [167, 22, 22, 2] показывает:

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
...