บทนำ
องค์ประกอบ Canvas ของ 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 ]
);
ต่อไปนี้เป็นตัวอย่างตัวกรองการกรอง นั่นคือการเบลอกล่อง การเบลอกล่องจะแสดงผลค่าเฉลี่ยของค่าพิกเซลภายในเมทริกซ์การกรอง วิธีดำเนินการคือสร้างเมทริกซ์การกรองขนาด NxN โดยน้ำหนักแต่ละรายการคือ 1 / (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
}
และยังมีตัวกรองคอนโวลูชันเจ๋งๆ อีกมากมายรอให้คุณค้นพบ เช่น ลองใช้ฟิลเตอร์ Laplace ในเครื่องมือเล่นคอนโวลูชันด้านบน แล้วดูผลลัพธ์
บทสรุป
เราหวังว่าบทความสั้นๆ นี้จะเป็นประโยชน์ในการแนะนำแนวคิดพื้นฐานเกี่ยวกับการเขียนตัวกรองรูปภาพใน JavaScript โดยใช้แท็ก Canvas ของ HTML ผมขอแนะนำว่าให้ใช้ฟิลเตอร์รูปภาพเพิ่มเติมนะ น่าจะสนุกมากทีเดียว
หากต้องการประสิทธิภาพที่ดีขึ้นจากฟิลเตอร์ โดยปกติแล้วคุณจะสามารถพอร์ตฟิลเตอร์ให้ใช้ WebGL Fragment Shader เพื่อประมวลผลรูปภาพได้ การใช้เชดเดอร์ช่วยให้คุณใช้ฟิลเตอร์แบบง่ายที่สุดแบบเรียลไทม์ได้ ซึ่งจะช่วยให้คุณใช้ฟิลเตอร์ดังกล่าวในขั้นตอนหลังการประมวลผลวิดีโอและภาพเคลื่อนไหวได้