Обработка изображений / Пиксельные искажения с билинейной фильтрацией в HTML5 canvas

в 16:31, , рубрики: canvas, fisheye, html5, image processing, javascript, zoom, обработка изображений, метки: , , , , , ,

Обработка изображений / Пиксельные искажения с билинейной фильтрацией в HTML5 canvas
В данном посте я хочу описать простую методику пиксельного искажения изображения на «чистом» javascript в 2D-Canvas без использования специальных библиотек и шейдеров, путём прямого доступа к пикселям изображения. Надеюсь, это будет интересно и полезно как для общего развития, так и для решения каких-то задач.

Canvas и пиксели

Я не буду описывать полностью объект Canvas, для этого есть документация. Остановимся на том, что нам нужно. Во-первых, это получение 2D-контекста:

var context = canvas.getContext('2d'); 

Этот контекст умеет многое делать с двухмерной графикой, в том числе получать прямой доступ к пиекселям в заданной области:

var pixels = context.getImageData(x, y, width, height); context.putImageData(pixels, x, y); 

Вот эти пиксели нам и предстоит изменять. Мы будем рассматривать только 32-битные изображения. Каждый пиксель такого изображения представляет собой четыре байта, по байту на канал (R,G,B,A). Пиксели представляют собой одномерный массив из этих байт. Доступ к ним осуществляется через поле data (x,y — координаты, c — канал, b — значение):

pixels.data[(x+y*height)*4+c] = b; 

Функция искажения

Искажение изображения, которое мы рассматриваем, представляет собой функцию, параметрами которой являются кооринаты получаемого изображения (далее будем называть их пиксели), а результатом — координаты исходного изображения (далее будем называть их текселы, так как фактически исходное изображение — это текстура, а координаты — это числа с плавающей точкой). Таким образом, функция для увеличения изображения имеет примерно следующий вид:
Обработка изображений / Пиксельные искажения с билинейной фильтрацией в HTML5 canvas

var zoom = function(px, py) {     return {         'x': (px+width/2)*0.5,         'y': (py+height/2)*0.5     } }   

Составим еще несколько функций для других искажений. Описывать каждый алгоритм я не вижу смысла, математика довольно простая и говорит сама за себя.
Обработка изображений / Пиксельные искажения с билинейной фильтрацией в HTML5 canvas

var twirl = function(px, py) {     var x = px-width/2;     var y = py-height/2;     var r = Math.sqrt(x*x+y*y);     var maxr = width/2;     if (r>maxr) return {         'x':px,         'y':py     }     var a = Math.atan2(y,x);     a += 1-r/maxr;     var dx = Math.cos(a)*r;     var dy = Math.sin(a)*r;     return {         'x': dx+width/2,         'y': dy+height/2     } } 

Обработка изображений / Пиксельные искажения с билинейной фильтрацией в HTML5 canvas

var reflect = function(px, py) {     if (py<height/2) return {         'x': px,          'y': py     }     var dx = (py-height/2)*(-px+width/2)/width;     return {         'x': px+dx,         'y': height-py     } } 

Обработка изображений / Пиксельные искажения с билинейной фильтрацией в HTML5 canvas

var spherize = function(px,py) {     var x = px-width/2;     var y = py-height/2;     var r = Math.sqrt(x*x+y*y);     var maxr = width/2;     if (r>maxr) return {         'x':px,         'y':py     }     var a = Math.atan2(y,x);     var k = (r/maxr)*(r/maxr)*0.5+0.5;     var dx = Math.cos(a)*r*k;     var dy = Math.sin(a)*r*k;     return {         'x': dx+width/2,         'y': dy+height/2     } }

Хэш-таблица

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

// Параметром является функция искажения. Если это строка, функция устанавливается из имеющихся в объекте. var setTranslate = function(translator) {     if (typeof translator === 'string') translator = this[translator];     for (var y=0; y<height; y++) {         for (var x=0; x<width; x++) {             var t = translator(x, y);             map[(x+y*height)*2+0] = Math.max(Math.min(t.x, width-1), 0);             map[(x+y*height)*2+1] = Math.max(Math.min(t.y, height-1), 0);         }     } } 

Билинейная фильтрация

Чтобы при искажениях резкие границы нам не портили настроение, применим классический алгоритм билинейной фильтрации. Он подробно описан в Википедии. Суть алгоритма заключается в нахождении цвета пикселя в зависимости от четырёх ближайших текселей. В нашем случае, алгоритм выглядеть будет так:

var colorat = function(x, y, channel) {     return texture.data[(x+y*height)*4+channel]; } for (var j=0; j<height; j++) {     for (var i=0; i<width; i++) {         var u = map[(i+j*height)*2];         var v = map[(i+j*height)*2+1];         var x = Math.floor(u);         var y = Math.floor(v);         var kx = u-x;         var ky = v-y;         for (var c=0; c<4; c++) {             bitmap.data[(i+j*height)*4+c] =                 (colorat(x, y  , c)*(1-kx) + colorat(x+1, y  , c)*kx) * (1-ky) +                 (colorat(x, y+1, c)*(1-kx) + colorat(x+1, y+1, c)*kx) * (ky);         }     } } 

Заключение

Вот, собственно и всё. Осталось обернуть это в отдельный объект, добавить его в код и посмотреть, что получится.
Поиграться в реальном времени на JSFiddle.
Работает в Chrome и Firefox. В других не могу пока проверить, если не работает, напишите в личку.
Спасибо за внимание.

Автор: Stdit


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js