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

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

תמונות מעניינות

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

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. אפשר לראות איך המערכת מתמקדת בפיקסל במרכז. כדי לשמור על הבהירות של התמונה, הסכום של ערכי המטריצה צריך להיות אחד.

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

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

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 באמצעות תג הקנבס של HTML. מומלץ להטמיע עוד מסנני תמונות, זה ממש כיף!

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