מסנני תמונות עם קנבס

אילמרי הייקינן

מבוא

ניתן להשתמש באלמנט הקנבס של 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
}

ויש עוד הרבה מסננים מגניבים אחרים שממתינים שתגלה אותם. לדוגמה, נסו להטמיע מסנן לקנבס במודל הקונבולוציה שלמעלה, ולראות מה הוא עושה.

סיכום

אני מקווה שמאמר קטן זה עזר לכם להציג את המושגים הבסיסיים של כתיבת מסנני תמונות ב-JavaScript באמצעות תג הקנבס ב-HTML. אני ממליץ לך ליישם עוד כמה מסנני תמונות, זה די כיף!

אם אתם צריכים ביצועים טובים יותר מהמסננים, בדרך כלל אפשר להעביר אותם להשתמש בהצללה של קטעי WebGL כדי לבצע את עיבוד התמונה. באמצעות תוכנות הצללה (shader) תוכלו להפעיל את רוב המסננים הפשוטים בזמן אמת, ולהשתמש בהם ליצירת אנימציות וסרטונים לאחר העיבוד.