Bộ lọc hình ảnh với canvas

Ilmari Heikkinen

Giới thiệu

Bạn có thể sử dụng phần tử canvas HTML5 để ghi bộ lọc hình ảnh. Việc bạn cần làm là vẽ hình ảnh lên canvas, đọc lại các pixel canvas và chạy bộ lọc trên các pixel đó. Sau đó, bạn có thể ghi kết quả lên canvas mới (hoặc vô cùng, chỉ cần sử dụng lại kết quả cũ.)

Có vẻ đơn giản phải không? Tốt quá. Chúng ta hãy bắt đầu!

Hình ảnh thử nghiệm ban đầu
Hình ảnh thử nghiệm gốc

Đang xử lý pixel

Trước tiên, hãy truy xuất các pixel hình ảnh:

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

Tiếp theo, chúng ta cần một cách để lọc hình ảnh. Còn phương thức filterImage lấy bộ lọc và hình ảnh đồng thời trả về các pixel đã lọc thì sao?

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

Chạy bộ lọc đơn giản

Giờ đây, khi chúng ta đã kết hợp quy trình xử lý pixel, đã đến lúc viết một số bộ lọc đơn giản. Để bắt đầu, hãy chuyển đổi hình ảnh sang thang màu xám.

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

Bạn có thể điều chỉnh độ sáng bằng cách thêm một giá trị cố định vào pixel:

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

Tạo ngưỡng cho hình ảnh cũng khá đơn giản. Bạn chỉ cần so sánh giá trị thang màu xám của pixel với giá trị ngưỡng và đặt màu dựa trên giá trị đó:

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

Hình ảnh chuyển đổi

Bộ lọc chuyển đổi là các bộ lọc chung rất hữu ích để xử lý hình ảnh. Về cơ bản, bạn cần lấy tổng trọng số của một hình chữ nhật pixel từ hình ảnh nguồn và sử dụng số đó làm giá trị đầu ra. Bạn có thể dùng bộ lọc Tích chập để làm mờ, làm sắc nét, làm nổi, phát hiện cạnh và nhiều chức năng khác.

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

Đây là bộ lọc làm sắc nét 3x3. Xem cách điện thoại tập trung trọng số vào pixel trung tâm. Để duy trì độ sáng của hình ảnh, tổng giá trị của ma trận phải bằng 1.

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

Đây là một ví dụ khác về bộ lọc tích chập, hộp mờ. Thao tác làm mờ hộp sẽ cho ra giá trị trung bình của các giá trị pixel bên trong ma trận tích chập. Cách để thực hiện việc đó là tạo một ma trận tích chập có kích thước NxN, trong đó mỗi trọng số là 1 / (NxN). Bằng cách đó, mỗi pixel bên trong ma trận sẽ đóng góp một lượng bằng nhau cho hình ảnh đầu ra và tổng trọng số là một.

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

Chúng ta có thể tạo các bộ lọc hình ảnh phức tạp hơn bằng cách kết hợp các bộ lọc hiện có. Ví dụ: hãy viết bộ lọc Sobel. Bộ lọc Sobel tính toán độ chuyển màu theo chiều dọc và chiều ngang của hình ảnh, rồi kết hợp các hình ảnh đã tính toán để tìm các cạnh trong hình ảnh. Cách chúng tôi triển khai bộ lọc Sobel ở đây là trước tiên sẽ chuyển đổi kích thước hình ảnh thành màu xám, sau đó lấy chuyển màu theo chiều ngang và chiều dọc, cuối cùng là kết hợp hình ảnh chuyển màu để tạo thành hình ảnh cuối cùng.

Về thuật ngữ, "gradient" (chuyển màu) ở đây có nghĩa là sự thay đổi về giá trị pixel ở một vị trí hình ảnh. Nếu một pixel có lân cận bên trái với giá trị 20 và một lân cận bên phải có giá trị 50, thì độ dốc theo chiều ngang tại pixel sẽ là 30. Độ dốc dọc có cùng ý tưởng nhưng sử dụng các thành phần lân cận ở trên và bên dưới.

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
}

Ngoài ra còn có rất nhiều bộ lọc tích chập thú vị khác đang chờ bạn khám phá. Ví dụ: hãy thử triển khai bộ lọc Laplace trong đồ chơi tích chập ở trên và xem chức năng của bộ lọc này.

Kết luận

Tôi hy vọng bài viết nhỏ này hữu ích trong việc giới thiệu các khái niệm cơ bản về việc viết bộ lọc hình ảnh trong JavaScript bằng thẻ canvas HTML. Tôi khuyến khích bạn triển khai thêm một số bộ lọc hình ảnh. Điều này khá thú vị!

Nếu cần hiệu suất tốt hơn từ các bộ lọc của mình, thông thường, bạn có thể chuyển các bộ lọc đó để sử dụng chương trình đổ bóng mảnh WebGL để xử lý hình ảnh. Với chương trình đổ bóng, bạn có thể chạy các bộ lọc đơn giản nhất theo thời gian thực, cho phép bạn sử dụng các bộ lọc này để xử lý hậu kỳ video và ảnh động.