Фильтры изображений с холстом

Ilmari Heikkinen

Введение

Элемент холста HTML5 можно использовать для написания фильтров изображений. Что вам нужно сделать, это нарисовать изображение на холсте, считать пиксели холста и применить к ним фильтр. Затем вы можете записать результат на новый холст (или, черт возьми, просто повторно использовать старый).

Звучит просто? Хороший. Давайте потрепаемся!

Исходное тестовое изображение
Исходное тестовое изображение

Обработка пикселей

Сначала извлеките пиксели изображения:

Filters = {};
Filters.getPixels = function(img) {
var c = this.getCanvas(img.width, img.height);
var ctx = c.getContext('2d');
ctx.drawImage(img);
return ctx.getImageData(0,0,c.width,c.height);
};

Filters.getCanvas = function(w,h) {
var c = document.createElement('canvas');
c.width = w;
c.height = h;
return c;
};

Далее нам нужен способ фильтрации изображений. Как насчет метода filterImage , который принимает фильтр и изображение и возвращает отфильтрованные пиксели?

Filters.filterImage = function(filter, image, var_args) {
var args = [this.getPixels(image)];
for (var i=2; i<arguments.length; i++) {
args.push(arguments[i]);
}
return filter.apply(null, args);
};

Запуск простых фильтров

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

Filters.grayscale = function(pixels, args) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
var r = d[i];
var g = d[i+1];
var b = d[i+2];
// CIE luminance for the RGB
// The human eye is bad at seeing red and blue, so we de-emphasize them.
var v = 0.2126*r + 0.7152*g + 0.0722*b;
d[i] = d[i+1] = d[i+2] = v
}
return pixels;
};

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

Filters.brightness = function(pixels, adjustment) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
d[i] += adjustment;
d[i+1] += adjustment;
d[i+2] += adjustment;
}
return pixels;
};

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

Filters.threshold = function(pixels, threshold) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
var r = d[i];
var g = d[i+1];
var b = d[i+2];
var v = (0.2126*r + 0.7152*g + 0.0722*b >= threshold) ? 255 : 0;
d[i] = d[i+1] = d[i+2] = v
}
return pixels;
};

Свертывающиеся изображения

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

Filters.tmpCanvas = document.createElement('canvas');
Filters.tmpCtx = Filters.tmpCanvas.getContext('2d');

Filters.createImageData = function(w,h) {
return this.tmpCtx.createImageData(w,h);
};

Filters.convolute = function(pixels, weights, opaque) {
var side = Math.round(Math.sqrt(weights.length));
var halfSide = Math.floor(side/2);
var src = pixels.data;
var sw = pixels.width;
var sh = pixels.height;
// pad output by the convolution matrix
var w = sw;
var h = sh;
var output = Filters.createImageData(w, h);
var dst = output.data;
// go through the destination image pixels
var alphaFac = opaque ? 1 : 0;
for (var y=0; y<h; y++) {
for (var x=0; x<w; x++) {
  var sy = y;
  var sx = x;
  var dstOff = (y*w+x)*4;
  // calculate the weighed sum of the source image pixels that
  // fall under the convolution matrix
  var r=0, g=0, b=0, a=0;
  for (var cy=0; cy<side; cy++) {
    for (var cx=0; cx<side; cx++) {
      var scy = sy + cy - halfSide;
      var scx = sx + cx - halfSide;
      if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) {
        var srcOff = (scy*sw+scx)*4;
        var wt = weights[cy*side+cx];
        r += src[srcOff] * wt;
        g += src[srcOff+1] * wt;
        b += src[srcOff+2] * wt;
        a += src[srcOff+3] * wt;
      }
    }
  }
  dst[dstOff] = r;
  dst[dstOff+1] = g;
  dst[dstOff+2] = b;
  dst[dstOff+3] = a + alphaFac*(255-a);
}
}
return output;
};

Вот фильтр резкости 3x3. Посмотрите, как он фокусирует вес на центральном пикселе. Для сохранения яркости изображения сумма значений матрицы должна быть равна единице.

Filters.filterImage(Filters.convolute, image,
[  0, -1,  0,
-1,  5, -1,
  0, -1,  0 ]
);

Вот еще один пример фильтра свертки — размытие поля. Размытие поля выводит среднее значение пикселей внутри матрицы свертки. Способ сделать это — создать матрицу свертки размером NxN, где каждый из весов равен 1/(NxN). Таким образом, каждый из пикселей внутри матрицы вносит равный вклад в выходное изображение, а сумма весов равна единице.

Filters.filterImage(Filters.convolute, image,
[ 1/9, 1/9, 1/9,
1/9, 1/9, 1/9,
1/9, 1/9, 1/9 ]
);

Мы можем создавать более сложные фильтры изображений, комбинируя существующие фильтры. Например, напишем фильтр Собеля . Фильтр Собеля вычисляет вертикальные и горизонтальные градиенты изображения и объединяет вычисленные изображения для поиска краев изображения. Здесь мы реализуем фильтр Собеля: сначала масштабируем изображение в оттенках серого, затем используем горизонтальный и вертикальный градиенты и, наконец, объединяем изображения градиента для создания окончательного изображения.

Что касается терминологии, «градиент» здесь означает изменение значения пикселя в позиции изображения. Если у пикселя есть левый сосед со значением 20 и правый сосед со значением 50, горизонтальный градиент пикселя будет равен 30. Вертикальный градиент имеет ту же идею, но использует соседей сверху и снизу.

var grayscale = Filters.filterImage(Filter.grayscale, image);
// Note that ImageData values are clamped between 0 and 255, so we need
// to use a Float32Array for the gradient values because they
// range between -255 and 255.
var vertical = Filters.convoluteFloat32(grayscale,
[ -1, 0, 1,
-2, 0, 2,
-1, 0, 1 ]);
var horizontal = Filters.convoluteFloat32(grayscale,
[ -1, -2, -1,
  0,  0,  0,
  1,  2,  1 ]);
var final_image = Filters.createImageData(vertical.width, vertical.height);
for (var i=0; i<final_image.data.length; i+=4) {
// make the vertical gradient red
var v = Math.abs(vertical.data[i]);
final_image.data[i] = v;
// make the horizontal gradient green
var h = Math.abs(horizontal.data[i]);
final_image.data[i+1] = h;
// and mix in some blue for aesthetics
final_image.data[i+2] = (v+h)/4;
final_image.data[i+3] = 255; // opaque alpha
}

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

Заключение

Я надеюсь, что эта небольшая статья была полезна для ознакомления с основными понятиями написания фильтров изображений в JavaScript с использованием тега HTML Canvas. Я советую вам пойти и реализовать еще несколько фильтров изображений, это довольно весело!

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