UP | HOME
جناوي

السيد مذهل
mr. fantastic

طالب هندسة برمجيات - مهتم بالرسوميات والهندسة الصوتية

Software engineering student - interested in graphics and audio engineering

وصفات وخلطات مزج الصور
نشر في Mar 05, 2021 بقلم السيد مذهل.

كثيراً ما نرى ونسمع عن وضعيات الطبقات (Layer modes, Blending modes) الصورية ببرامج معالجة الصور المتقدمة مثل جيمب (Gimp) وغيره من البرامج التي تمتلك تراسنة من الخصائص المتقدمة وهنا ومن حيث المبدئ ما نتحدث عنه لا يختلف كثيراً عن خلطات ووصفات البهارات الشرقية إلا من حيث أن المادة المراد مزجها هي صورة أخرى أو طبقة مع الأصل.

وللتنويه لقد أشرنا سابقاً بهذه المدونة عن أساسيات التعامل مع الصور الرقمية وكيفية تكوينها في مقالة البكسل من وجهة نظر المبرمج وليس الخيال العلمي.

herbs.png

شكل 1: ما تلذ وتشتهي الأعين

من خلال أمثلتنا الأتية سنعمل بأستعمال صورتين المصدر أو الأصل والقناع

jailboat.png

شكل 2: صورة المصدر

mask.png

شكل 3: القناع

وهنا القناع مجرد تدرج لون بين الأبيض والأسود وهو ما يمكننا توليده بواسطة برنامج بسيط أو بأستخدام أداة التدرجات في جيمب.

ملاحظة هامة: الصورة المعروضة هي بصيغة PNG وبرنامجنا لا يقرأ سوى صيغة PPM ولذلك فعليك بتحويلها إلى PPM بأستخدام إي أداة كجيمب مثلاً وما دعانا لأستخدام PNG هو عدم دعم الكثير من المتصفحات لصيغة PPM.

وأما الأن فسوف نضع بين إيديكم هذا البرنامج التوضيحي البسيط ولندع الشفرة تتحدث:

// -*- compile-command: "gcc -o main main.c && ./main jailboat.ppm mask.ppm && eom multiply.ppm"; -*-
// replace eom with your favorite image viewer
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string.h>

// blend modes {lighten, subtract, addition, darken, multiply, screen, overlay}
// Images should be from the same size

#define MAX(n,x) x > n ? x : n
#define MIN(n,x) x > n ? n : x

unsigned char *img;
unsigned char *mask;
unsigned int w, h, d;

unsigned char *load_image (char *file_name)
{
  FILE *fp = fopen(file_name, "rb");
  char *line = NULL; // initializing it to NULL is very important for memory safty
  size_t len;
  getline (&line, &len, fp);
  if (strcmp ("P6\n", line) != 0)
    { printf("This is not valid P6 ppm file\n"); fclose (fp); exit (1); }
  getline (&line, &len, fp);
  while (strstr(line, "#") != NULL) getline (&line, &len, fp);
  sscanf (line, "%u %u", &w, &h);
  getline (&line, &len, fp);
  sscanf (line, "%u", &d);
  unsigned char *data = malloc (w*h*3);
  fread (data, w*h*3, 1, fp);
  fclose (fp);

  return data;
}

void addition ()
{
  FILE *fp = fopen("addition.ppm", "w+b");
  fprintf(fp, "P6\n%d %d\n255\n", w,h);

  unsigned char *img_cpy = malloc(w * h * 3);
  memcpy (img_cpy, img, w*h*3);

  unsigned char *img_ptr = img_cpy;
  unsigned char *mask_ptr = mask;

  for (int i = 0; i < w*h; ++i)
    {
      *img_ptr = ((*img_ptr + *mask_ptr) >= 255) ? 255 : (*img_ptr + *mask_ptr);
      img_ptr++;
      mask_ptr++;

      *img_ptr = ((*img_ptr + *mask_ptr) >= 255) ? 255 : (*img_ptr + *mask_ptr);
      img_ptr++;
      mask_ptr++;

      *img_ptr = ((*img_ptr + *mask_ptr) >= 255) ? 255 : (*img_ptr + *mask_ptr);
      img_ptr++;
      mask_ptr++;
    }


  fwrite (img_cpy, w*h*3, 1, fp);
  fclose (fp);
  free (img_cpy);
}


void subtract ()
{
  FILE *fp = fopen("subtract.ppm", "w+b");
  fprintf(fp, "P6\n%d %d\n255\n", w,h);

  unsigned char *img_cpy = malloc(w * h * 3);
  memcpy (img_cpy, img, w*h*3);

  unsigned char *img_ptr = img_cpy;
  unsigned char *mask_ptr = mask;

  for (int i = 0; i < w*h; ++i)
    {
      *img_ptr = ((*img_ptr - *mask_ptr) < 0) ? 0 : (*img_ptr - *mask_ptr);
      img_ptr++;
      mask_ptr++;

      *img_ptr = ((*img_ptr - *mask_ptr) < 0) ? 0 : (*img_ptr - *mask_ptr);
      img_ptr++;
      mask_ptr++;

      *img_ptr = ((*img_ptr - *mask_ptr) < 0) ? 0 : (*img_ptr - *mask_ptr);
      img_ptr++;
      mask_ptr++;
    }


  fwrite (img_cpy, w*h*3, 1, fp);
  fclose (fp);
  free (img_cpy);
}


void lighten ()
{
  FILE *fp = fopen("lighten.ppm", "w+b");
  fprintf(fp, "P6\n%d %d\n255\n", w,h);

  unsigned char *img_cpy = malloc(w * h * 3);
  memcpy (img_cpy, img, w*h*3);

  unsigned char *img_ptr = img_cpy;
  unsigned char *mask_ptr = mask;

  for (int i = 0; i < w*h; ++i)
    {
      *img_ptr = MAX(*img_ptr, *mask_ptr);
      img_ptr++;
      mask_ptr++;

      *img_ptr = MAX(*img_ptr, *mask_ptr);
      img_ptr++;
      mask_ptr++;

      *img_ptr = MAX(*img_ptr, *mask_ptr);
      img_ptr++;
      mask_ptr++;
    }


  fwrite (img_cpy, w*h*3, 1, fp);
  fclose (fp);
  free (img_cpy);
}


void darken ()
{
  FILE *fp = fopen("darken.ppm", "w+b");
  fprintf(fp, "P6\n%d %d\n255\n", w,h);

  unsigned char *img_cpy = malloc(w * h * 3);
  memcpy (img_cpy, img, w*h*3);

  unsigned char *img_ptr = img_cpy;
  unsigned char *mask_ptr = mask;

  for (int i = 0; i < w*h; ++i)
    {
      *img_ptr = MIN(*img_ptr, *mask_ptr);
      img_ptr++;
      mask_ptr++;

      *img_ptr = MIN(*img_ptr, *mask_ptr);
      img_ptr++;
      mask_ptr++;

      *img_ptr = MIN(*img_ptr, *mask_ptr);
      img_ptr++;
      mask_ptr++;
    }


  fwrite (img_cpy, w*h*3, 1, fp);
  fclose (fp);
  free (img_cpy);
}

void multiply ()
{
  FILE *fp = fopen("multiply.ppm", "w+b");
  fprintf(fp, "P6\n%d %d\n255\n", w,h);

  unsigned char *img_cpy = malloc(w * h * 3);
  memcpy (img_cpy, img, w*h*3);

  unsigned char *img_ptr = img_cpy;
  unsigned char *mask_ptr = mask;

  for (int i = 0; i < w*h; ++i)
    {
      float fval = ((float) *img_ptr) / 255.0;
      float mfval = ((float) *mask_ptr) / 255.0;
      *img_ptr = (unsigned char) ((fval*mfval)*255.0);
      img_ptr++;
      mask_ptr++;

      fval = ((float) *img_ptr) / 255.0;
      mfval = ((float) *mask_ptr) / 255.0;
      *img_ptr = (unsigned char) ((fval*mfval)*255.0);
      img_ptr++;
      mask_ptr++;

      fval = ((float) *img_ptr) / 255.0;
      mfval = ((float) *mask_ptr) / 255.0;
      *img_ptr = (unsigned char) ((fval*mfval)*255.0);
      img_ptr++;
      mask_ptr++;
    }


  fwrite (img_cpy, w*h*3, 1, fp);
  fclose (fp);
  free (img_cpy);
}

void screen ()
{
  FILE *fp = fopen("screen.ppm", "w+b");
  fprintf(fp, "P6\n%d %d\n255\n", w,h);

  unsigned char *img_cpy = malloc(w * h * 3);
  memcpy (img_cpy, img, w*h*3);

  unsigned char *img_ptr = img_cpy;
  unsigned char *mask_ptr = mask;

  for (int i = 0; i < w*h; ++i)
    {
      float fval = ((float) *img_ptr) / 255.0;
      float mfval = ((float) *mask_ptr) / 255.0;
      *img_ptr = (unsigned char) (( 1 - ((1-fval) * (1-mfval)) )*255.0);
      img_ptr++;
      mask_ptr++;

      fval = ((float) *img_ptr) / 255.0;
      mfval = ((float) *mask_ptr) / 255.0;
      *img_ptr = (unsigned char) (( 1 - ((1-fval) * (1-mfval)) )*255.0);
      img_ptr++;
      mask_ptr++;

      fval = ((float) *img_ptr) / 255.0;
      mfval = ((float) *mask_ptr) / 255.0;
      *img_ptr = (unsigned char) (( 1 - ((1-fval) * (1-mfval)) )*255.0);
      img_ptr++;
      mask_ptr++;
    }


  fwrite (img_cpy, w*h*3, 1, fp);
  fclose (fp);
  free (img_cpy);
}

void overlay ()
{
  FILE *fp = fopen("overlay.ppm", "w+b");
  fprintf(fp, "P6\n%d %d\n255\n", w,h);

  unsigned char *img_cpy = malloc(w * h * 3);
  memcpy (img_cpy, img, w*h*3);

  unsigned char *img_ptr = img_cpy;
  unsigned char *mask_ptr = mask;

  for (int i = 0; i < w*h; ++i)
    {
      float fval = ((float) *img_ptr) / 255.0;
      float mfval = ((float) *mask_ptr) / 255.0;
      if (fval < 0.5)
        {
          *img_ptr = (unsigned char) ((2*fval*mfval)*255.0);
        }
      else
        {
          *img_ptr = (unsigned char) (( 1 - (2 * (1-fval) * (1-mfval)) )*255.0);
        }
      img_ptr++;
      mask_ptr++;

      fval = ((float) *img_ptr) / 255.0;
      mfval = ((float) *mask_ptr) / 255.0;
      if (fval < 0.5)
        {
          *img_ptr = (unsigned char) ((2*fval*mfval)*255.0);
        }
      else
        {
          *img_ptr = (unsigned char) (( 1 - (2 * (1-fval) * (1-mfval)) )*255.0);
        }
      img_ptr++;
      mask_ptr++;

      fval = ((float) *img_ptr) / 255.0;
      mfval = ((float) *mask_ptr) / 255.0;
      if (fval < 0.5)
        {
          *img_ptr = (unsigned char) ((2*fval*mfval)*255.0);
        }
      else
        {
          *img_ptr = (unsigned char) (( 1 - (2 * (1-fval) * (1-mfval)) )*255.0);
        }
      img_ptr++;
      mask_ptr++;
    }


  fwrite (img_cpy, w*h*3, 1, fp);
  fclose (fp);
  free (img_cpy);
}



int main(int argc, char **argv)
{
  if (argc < 3)
    {
      printf("Usage: %s img.ppm mask.ppm\n", argv[0]);
      exit (0);
    }

  img = load_image (argv[1]);
  mask = load_image (argv[2]);

  lighten ();
  subtract ();
  addition ();
  darken ();
  multiply ();
  screen ();
  overlay ();

  free (img);
  free (mask);
  return 0;
}

بالبداية أود أن أشير إلى أستخدامنا إلى دالة مختصرة (Macro function) لمعرفة أصغير متغير من بين متغيرين وفيها نسخة مختصرة إيضاً من جملة if else بأستخدام ? و : وهي نسخة قليلة الأستخدام من هذه الجملة فلذلك يجهلها الكثير من المبرمجين

#define MAX(n,x) x > n ? x : n
#define MIN(n,x) x > n ? n : x

وكما نلحظ بأن البرنامج مقسم إلى عدة دوال حيث تقوم كل دالة بتطبيق وضع معين وهذا عبر أخذ بكسلات الصورة الأولى والصورة الثانية واحد فمثلاً إذا أخذنا وضع الإضافة إي بمعنى أن نضيف قيمة قنوات بكسلات القناع إلى مرادفاتها من صورة المصدر بشرط أن فاقت القيم القيمة العليا للعمق اللون 255 مثلاً بصورة 8 بت فعندها نضع القيمة العليا وأن قلت نضع القيمة الدُنيا

addition.png

شكل 4: نتيجة الإضافة

ويمكننا روئية هذا واضحاً جلياً في داخل دالة addition حيث تكرر العملية لكل قناة لونية

void addition ()
{
...

*img_ptr = ((*img_ptr + *mask_ptr) >= 255) ? 255 : (*img_ptr + *mask_ptr);
img_ptr++;
mask_ptr++;

...
}

وهنا نرى أننا هنا أستخدمنا مؤشرين واحد للقناع وواحد للمصدر وبنفس المفهوم تسري العلميات الأخرى

subtract.png

شكل 5: الطرح

multiply.png

شكل 6: الضرب

ولكن عند دالة الضرب الأمر يختلف قليلاً لأننا حولنا القيم من مقياس عادي إلى نسب مئوية من 0 إلى واحد ومن ثمة ضربناهم (قيم القناع والصورة المئوية) وضربناهم مع القيمة العليا لصور ثمانية بت (255) وهذا يضمن عدم تجاوز القيمة العليا ويعطي نتائج صحيحة

float fval = ((float) *img_ptr) / 255.0;
float mfval = ((float) *mask_ptr) / 255.0;
*img_ptr = (unsigned char) ((fval*mfval)*255.0);
img_ptr++;
mask_ptr++;

وهنا علينا أن نعرف أن بأستخدام قيمة كسرية من 0 إلى 1 أو من -1 إلى 1 تمكننا من صنع صيغة يمكنها التحول من عمق لون إلى أخر وهذا ما يتم أستخدامه في libcario و مكتبة OpenGL.

وأما دالة التخفيف lighten فتخفف قيم القنوات اللونية لكل بكسل عبر أختيار القيم الأكبر (الأكثر سطوعاً وأشراقاً والأقرب للـ 255) لكل بكسلة مرادفة من الصورتين المراد دمجهما

lightenfunc.png

شكل 7: (أحمر r, أخضر g, أزرق b)

lighten.png

شكل 8: دالة التخفيف أو التفتيح

من الملحوظ أن لو كانت قيم القناع أقل تطرفاً لحصلنا على نتائج أفضل وهذا ما سنلحظه بمثالنا القادم بدالة التعتيم darken وهي عكس دالة التفتيح فبدل الأخذ بالقيم الأكبر من الصورتين نأخذ القيم الأصغر الأقرب للصفر

darken.png

شكل 9: دالة التعتيم

وأما بالنسبة لخوارزمية الغشاء overlay فالدالة المستخدمة تأخذ بعين الأعتبار مجالين الأول ما قبل النصف والثاني ما بعده

overlayfunc.png

شكل 10: دالة الغشاء

وهذا بالطبع مع تطبيع القيم إلى وإعادتها إلى إلى مجال ما بين 0 إلى 255 كما فعلنا بدالة الضرب

overlay.png

شكل 11: دالة الغشاء

وأما تأثير الشاشة فيأتي تباعاً

screenfunc.png

شكل 12: خوارزمية تأثير الشاشة

بحيث أن a هي المصدر و b هو القناع

screen.png

شكل 13: تأثير الشاشة

مصادر وملاحظات ختامية

بهذه المقالة تم أختيار عدة خوارزميات من مقالة أوضاع المزج من ويكيبيديا الموسوعة الحرة وكانوا أسهلها وذلك لأغراض توضيحية ويمكنكم إيضاً أن تتطلعوا على الشفرة المصدرية لجيمب بهذا الملف

https://gitlab.gnome.org/GNOME/gimp/-/blob/master/app/operations/layer-modes/gimpoperationlayermode-blend.c

وهذا المجلد

https://gitlab.gnome.org/GNOME/gimp/-/tree/master/app/operations/layer-modes

ومن الأشياء الملوحظة هو أن عملية القسمة بها بعض التفاصيل التي تجعلها آمنة وصحيحة ولهذا لم نتناولها بمقالتنا.

وكما أننا لم نناقش الوضع الطبيعي normal mode لأن سيقوم بأستبدال كل ما في الطبقة الأولى بما هو موجود بالطبقة الثانية وشيءٌ أخر وهو أننا لم نتكلم عن تركيب الألفا (أو تركيب الطبقة الشفافة) وهذا لأن صيغة ppm لا تدعم الشفافية ولكن صيغة PNM الشبيهة بها تقوم بالمطلوب.

ويجدر بنا الذكر بأن بالإمكان أستخدامه قناع حلزوني أو قُطري أو صورة عادية قناع ملونة أو رمادية

radial.png

وهذا قد يعطي تأثير شبيه بتركيز عدسة الكاميرا في بعض الأوضاع.

صورة سوق البهارات مأخوذة من ويكيبيديا من المستخدم Bertrand Devouard تحت رخصة جنو للوثائق الحرة.

لقطة سفينة الجالبوت من ويكيميديا بواسطة المصور توماس جارفيز عام 2006 تحت بنود رخصة المشاع الأبداعي المشاركة بالمثل.

comments . التعليقات