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

Ilmari Heikkinen

Giới thiệu

Bạn có thể dùng phần tử canvas HTML5 để viết 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 pixel canvas và chạy bộ lọc trên canvas đó. Sau đó, bạn có thể ghi kết quả vào một canvas mới (hoặc chỉ sử dụng lại canvas cũ).

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

Hình ảnh kiểm thử ban đầu
Hình ảnh kiểm thử ban đầu

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 một bộ lọc và một hình ảnh rồ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

Bây giờ, 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 các 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;
};

Việc đặt 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 một pixel với giá trị ngưỡng rồi đặ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;
};

Kết hợp hình ảnh

Bộ lọc tích chập là các bộ lọc chung rất hữu ích để xử lý hình ảnh. Ý tưởng cơ bản là bạ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 tổng đó 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, tạo nổi, phát hiện cạnh và làm nhiều việc 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;
};

Dưới đây là bộ lọc làm sắc nét 3x3. Hãy xem cách hàm này tập trung trọng số vào pixel ở giữa. Để duy trì độ sáng của hình ảnh, tổng các giá trị ma trận phải là một.

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

Sau đây là một ví dụ khác về bộ lọc tích chập, đó là hiệu ứng làm mờ hộp. Hiệu ứng làm mờ hộp sẽ xuất 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 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 đó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 tôi 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 một bộ lọc Sobel. Bộ lọc Sobel tính toán độ dốc theo chiều dọc và chiều ngang của hình ảnh, đồng thờ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 là điều chỉnh tỷ lệ màu xám của hình ảnh, sau đó lấy các hiệu ứng chuyển màu theo chiều ngang và chiều dọc, cuối cùng là kết hợp các hình ảnh chuyển màu để tạo thành hình ảnh cuối cùng.

Về mặt thuật ngữ, "độ dốc" ở đây có nghĩa là sự thay đổi về giá trị pixel tại một vị trí hình ảnh. Nếu một pixel có pixel bên trái có giá trị 20 và pixel 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ũng có cùng ý tưởng nhưng sử dụng các pixel liền kề ở trên và 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 luỹ 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 để xem bộ lọc đó có chức năng gì.

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ề cách viết bộ lọc hình ảnh trong JavaScript bằng thẻ canvas HTML. Bạn nên triển khai thêm một số bộ lọc hình ảnh khác, việc này khá thú vị!

Nếu cần có hiệu suất tốt hơn từ các bộ lọc của mình, bạn thường có thể chuyển chúng sang 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 hầu hết các bộ lọc đơn giản theo thời gian thực để sử dụng các bộ lọc này để xử lý hậu kỳ cho video và ảnh động.