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

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

תמונות convolve

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