Filtry obrazów w obszarze roboczym

Ilmari Heikkinen

Wstęp

Element canvas HTML5 może służyć do pisania filtrów obrazów. Musisz tylko narysować obraz na płótnie, odczytać jego piksele i włączyć na nich filtr. Wynik możesz zapisać na nowym arkuszu (lub lepiej użyć starego).

Brzmi prosto? Cieszę się. Do dzieła!

Oryginalny obraz testowy
Oryginalny obraz testowy

Piksele przetwarzania

Najpierw pobierz piksele obrazu:

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

Musimy też znaleźć sposób na filtrowanie obrazów. A co z metodą filterImage, która łączy filtr i obraz, a potem zwraca odfiltrowane piksele?

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);
};

Uruchamianie prostych filtrów

Po opracowaniu potoku przetwarzania pikseli czas napisać kilka prostych filtrów. Na początek przekonwertujmy obraz na tryb szarości.

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

Jasność można dostosować, dodając stałą wartość do pikseli:

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

Określanie progów dla obrazu również jest dość proste. Wystarczy, że porównujesz skalę szarości piksela z wartością progową i ustawiasz kolor na podstawie tego:

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

Obrazy nawijane

Filtry Convolution to bardzo przydatne filtry ogólne do przetwarzania obrazów. Podstawowym założeniem jest to, że ważysz sumę prostokątów pikseli z obrazu źródłowego i używasz jej jako wartości wyjściowej. Filtrów splotu można używać do rozmycia, wyostrzania, wytłaczania, wykrywania krawędzi i do wielu innych celów.

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

Oto filtr wyostrzania 3 x 3. Zobacz, jak ustawia wagę na środkowym pikselu. Aby zachować jasność obrazu, suma wartości macierzy powinna wynosić 1.

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

Oto kolejny przykład filtra splotowego – rozmycie ramki. Rozmycie ramki zwraca średnią wartości pikseli wewnątrz macierzy splotu. Można to zrobić, tworząc macierz splotu o rozmiarze NxN, w której każda waga ma wartość 1 / (NxN). W ten sposób każdy piksel w matrycy odpowiada w równej części obrazu wyjściowego, a suma wag wynosi 1.

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

Łącząc istniejące filtry, możemy tworzyć bardziej złożone filtry obrazów. Napiszmy na przykład filtr Sobel. Filtr Sobel oblicza gradienty pionowe i poziome obrazu, a następnie łączy obliczone obrazy, aby znaleźć krawędzie obrazu. Stosujemy tutaj filtr Sobel przez skalowanie szarości obrazu, następnie zastosowanie gradientów poziomych i pionowych, a na koniec łączenie obrazów w celu utworzenia ostatecznego obrazu.

Jeśli chodzi o terminologię, „gradient” oznacza tu zmianę wartości w pikselach w położeniu obrazu. Jeśli piksel ma sąsiadujący lewy o wartości 20 i prawy sąsiad o wartości 50, to gradient poziomy w tym elemencie będzie miał wartość 30. Gradient pionowy wygląda tak samo, ale wykorzystuje sąsiadów powyżej i poniżej.

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
}

Jest jeszcze cała masa innych fajnych filtrów z wykorzystaniem splotów, na które czekasz. Spróbuj np. zaimplementować filtr Laplace'a w powyższej zabawce z rozwojem i zobacz, jak to działa.

Podsumowanie

Mam nadzieję, że ten krótki artykuł pomoże Ci przybliżyć podstawowe pojęcia zapisywania filtrów obrazów w JavaScripcie przy użyciu tagu HTML canvas. Zachęcam do zastosowania dodatkowych filtrów graficznych. To całkiem fajne!

Jeśli filtry potrzebują większej skuteczności, można je zwykle przenieść do obróbki obrazu za pomocą cieniowania fragmentów WebGL. Programy do cieniowania pozwalają na uruchamianie najprostszych filtrów w czasie rzeczywistym, co pozwala na ich używanie do końcowego przetwarzania filmów i animacji.