캔버스를 사용한 이미지 필터

일마리 헤이키넨

소개

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 선명하게 필터입니다. 중앙 픽셀에 가중치가 어떻게 집중되는지 확인합니다. 이미지의 밝기를 유지하려면 행렬 값의 합계가 1이어야 합니다.

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

컨볼루션 필터의 또 다른 예시로 상자 블러가 있습니다. 상자 블러는 컨볼루션 행렬 내 픽셀 값의 평균을 출력합니다. 그러려면 각 가중치가 1 / (NxN)인 NxN 크기의 컨볼루션 행렬을 만드세요. 이런 식으로 행렬 내의 각 픽셀은 출력 이미지에 동일한 양을 기여하고 가중치의 합이 1이 됩니다.

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

기존 필터를 조합하여 더 복잡한 이미지 필터를 만들 수 있습니다. 예를 들어 Sobel 필터를 작성해 보겠습니다. Sobel 필터는 이미지의 세로 및 가로 경사를 계산하고 계산된 이미지를 결합하여 이미지의 가장자리를 찾습니다. 여기서 Sobel 필터를 구현하는 방법은 먼저 이미지를 그레이 스케일링한 다음 가로 및 세로 그라데이션을 취한 다음 그라데이션 이미지를 결합하여 최종 이미지를 구성하는 것입니다.

용어와 관련하여 여기서 '그라데이션'은 이미지 위치에서 픽셀 값의 변화를 의미합니다. 픽셀에 값이 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
}

이 외에도 다른 멋진 컨볼루션 필터가 많이 있습니다. 예를 들어 위의 컨볼루션 장난감에서 라플라스 필터를 구현해 보고 어떤 작업을 수행하는지 확인해 보세요.

결론

이 간단한 문서가 HTML 캔버스 태그를 사용하여 JavaScript에서 이미지 필터를 작성하는 기본 개념을 소개하는 데 도움이 되었기를 바랍니다. 이미지 필터를 더 많이 구현해 보시기 바랍니다. 꽤 재미있습니다.

필터의 성능을 개선해야 하는 경우 일반적으로 WebGL 프래그먼트 셰이더를 사용하여 이미지를 처리하도록 포팅하면 됩니다. 셰이더를 사용하면 가장 간단한 필터를 실시간으로 실행할 수 있으므로 동영상 및 애니메이션 후처리에 이 필터를 사용할 수 있습니다.