Filtry obrazów w obszarze roboczym

Ilmari Heikkinen

Wprowadzenie

Element HTML5 canvas można używać do tworzenia filtrów obrazu. Musisz tylko narysować obraz na obszarze roboczym, odczytać piksele obszaru roboczego i użyć na nich filtra. Następnie możesz zapisać wynik na nowym płótnie (lub wykorzystać stare).

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

Oryginalne zdjęcie testowe
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;
};

Musimy znaleźć sposób filtrowania 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;
};

Wyznaczenie wartości obrazu dla obrazu też jest całkiem 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 Convolution to bardzo przydatne ogólne filtry do przetwarzania obrazów. Ogólnie chodzi o to, aby przyjąć wyważoną sumę pikseli prostokąta z obrazu źródłowego i użyć jej jako wartości wyjściowej. Filtry Convolution mogą służyć m.in. do zamazywania, wyostrzania, wytłaczania, wykrywania progó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. W tym celu należy utworzyć macierz splotu o rozmiarze NxN, w której każda z wag to 1 / (NxN). W ten sposób każdy piksel w matrycy ma taki sam udział w obrazie wyjściowym, 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 obrazów, łą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 celu stosujemy filtr Sobela, najpierw wyszarzając obraz, a następnie zbierając gradienty poziome i pionowe, a następnie łącząc je w taki sposób, by uzyskać ostateczny obraz.

Jeśli chodzi o terminologię, „gradient” oznacza tu zmianę wartości piksela w danej pozycji obrazu. Jeśli piksel ma sąsiad po lewej stronie o wartości 20, a o wartości 50 po prawej, gradient poziomy tego piksela wynosi 30. Ten gradient jest taki sam jak w przypadku, ale występuje w nim sąsiednie elementy powyżej i pod nimi.

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 np. zaimplementować filtr Laplace'a w powyższej zabawce splotowej i zobacz, jaki efekt uzyskasz.

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 filtry wymagają lepszej wydajności, zwykle można je przenieść do przetwarzania obrazu za pomocą cieniowania fragmentów WebGL. 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.