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

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 선명도 필터입니다. 중앙 픽셀에 가중치를 어떻게 맞추는지 확인하세요. 이미지의 밝기를 유지하려면 행렬 값의 합계가 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 프래그먼트 셰이더를 사용하여 이미지 처리를 수행하도록 필터를 포팅할 수 있습니다. 셰이더를 사용하면 대부분의 간단한 필터를 실시간으로 실행할 수 있으므로 동영상 및 애니메이션의 후처리에 사용할 수 있습니다.