Filtry obrazów w obszarze roboczym

Ilmari Heikkinen

Wprowadzenie

Element HTML5 canvas możesz wykorzystać do tworzenia filtrów obrazów. Musisz narysować obraz na płótnie, odczytać piksele płótna i zastosować do nich filtr. Następnie możesz zapisać wynik na nowym płótnie (lub wykorzystać stare).

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

Oryginalny obraz testowy
Oryginalny obraz testowy

Przetwarzanie pikseli

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

Następnie musimy znaleźć sposób na filtrowanie obrazów. A może metoda filterImage, która przyjmuje filtr i obraz, a zwraca przefiltrowane 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

Teraz, gdy mamy już gotowy przetwarzanie pikseli, nadszedł czas na napisanie prostych filtrów. Na początek przekształcimy obraz w 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;
};

Ustawienie progu dla obrazu jest też bardzo proste. Wystarczy porównać wartość szarości piksela z wartością progową i ustawić kolor na podstawie tego porównania:

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

Konwoluowanie obrazów

Filtry konwolucyjne to bardzo przydatne ogólne filtry do przetwarzania obrazu. Podstawowy pomysł polega na tym, że bierze się ważoną sumę prostokąta pikseli z obrazu źródłowego i używa się jej jako wartości wyjściowej. Filtry konwolucyjne można stosować do rozmywania, wyostrzania, tłoczenia, wykrywania krawędzi i 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 wyostrzenia 3 x 3. Zobacz, jak skupia ono 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 konwolucji, czyli zamglenia prostokątnego. Funkcja boxblur zwraca średnią wartości pikseli wewnątrz macierzy konwekcji. Aby to zrobić, należy utworzyć macierz sprzężenia zwrotnego o rozmiarach NxN, w której każda z wag wynosi 1 / (NxN). W ten sposób każdy piksel w ramach macierzy wnosi równą ilość do 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 ]
);

Możemy tworzyć bardziej złożone filtry zdjęć, łącząc istniejące filtry. Na przykład: załóżmy, że chcemy napisać filtr Sobela. Filtr Sobela oblicza gradienty pionowy i poziomy obrazu oraz łączy obliczone obrazy, aby znaleźć krawędzie. W tym przypadku filtr Sobela jest implementowany przez przekształcenie obrazu na skalę szarości, a następnie przez zastosowanie gradientów poziomego i pionowego oraz połączenie ich w jeden obraz.

Jeśli chodzi o terminologię, „gradient” oznacza tu zmianę wartości piksela w danej pozycji obrazu. Jeśli piksele mają sąsiada z lewej strony o wartości 20 i sąsiada z prawej strony o wartości 50, gradient poziomy w przypadku tego piksela będzie wynosił 30. Gradient pionowy działa na podobnej zasadzie, ale wykorzystuje sąsiadujące piksele 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
}

Dostępnych jest też mnóstwo innych fajnych filtrów konwolucyjnych, które tylko czekają na odkrycie. Spróbuj na przykład zastosować filtr Laplace’a w zaimplementowanym powyżej filtrze sprzężonym i zobacz, jak działa.

Podsumowanie

Mam nadzieję, że ten krótki artykuł pomógł Ci w zapoznaniu się z podstawowymi koncepcjami tworzenia filtrów obrazów w JavaScript za pomocą tagu HTML canvas. Zachęcam do zaimplementowania większej liczby filtrów zdjęć. To całkiem fajna zabawa.

Jeśli chcesz uzyskać większą wydajność filtrów, możesz je przeportować, aby używały shaderów fragmentów WebGL do przetwarzania obrazu. Dzięki shaderom możesz uruchamiać większość prostych filtrów w czasie rzeczywistym, co pozwala na ich używanie do postprodukcji filmów i animacji.