وصفات وخلطات مزج الصور
نشر في Mar 05, 2021 بقلم السيد مذهل.
كثيراً ما نرى ونسمع عن وضعيات الطبقات (Layer modes, Blending modes) الصورية ببرامج معالجة الصور المتقدمة مثل جيمب (Gimp) وغيره من البرامج التي تمتلك تراسنة من الخصائص المتقدمة وهنا ومن حيث المبدئ ما نتحدث عنه لا يختلف كثيراً عن خلطات ووصفات البهارات الشرقية إلا من حيث أن المادة المراد مزجها هي صورة أخرى أو طبقة مع الأصل.
وللتنويه لقد أشرنا سابقاً بهذه المدونة عن أساسيات التعامل مع الصور الرقمية وكيفية تكوينها في مقالة البكسل من وجهة نظر المبرمج وليس الخيال العلمي.
شكل 1: ما تلذ وتشتهي الأعين
من خلال أمثلتنا الأتية سنعمل بأستعمال صورتين المصدر أو الأصل والقناع
شكل 2: صورة المصدر
شكل 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 بت فعندها نضع القيمة العليا وأن قلت نضع القيمة الدُنيا
شكل 4: نتيجة الإضافة
ويمكننا روئية هذا واضحاً جلياً في داخل دالة addition
حيث تكرر العملية لكل قناة لونية
void addition () { ... *img_ptr = ((*img_ptr + *mask_ptr) >= 255) ? 255 : (*img_ptr + *mask_ptr); img_ptr++; mask_ptr++; ... }
وهنا نرى أننا هنا أستخدمنا مؤشرين واحد للقناع وواحد للمصدر وبنفس المفهوم تسري العلميات الأخرى
شكل 5: الطرح
شكل 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) لكل بكسلة مرادفة من الصورتين المراد دمجهما
شكل 7: (أحمر r, أخضر g, أزرق b)
شكل 8: دالة التخفيف أو التفتيح
من الملحوظ أن لو كانت قيم القناع أقل تطرفاً لحصلنا على نتائج أفضل وهذا ما سنلحظه بمثالنا القادم بدالة التعتيم darken
وهي عكس دالة التفتيح فبدل الأخذ بالقيم الأكبر من الصورتين نأخذ القيم الأصغر الأقرب للصفر
شكل 9: دالة التعتيم
وأما بالنسبة لخوارزمية الغشاء overlay
فالدالة المستخدمة تأخذ بعين الأعتبار مجالين الأول ما قبل النصف والثاني ما بعده
شكل 10: دالة الغشاء
وهذا بالطبع مع تطبيع القيم إلى وإعادتها إلى إلى مجال ما بين 0 إلى 255 كما فعلنا بدالة الضرب
شكل 11: دالة الغشاء
وأما تأثير الشاشة فيأتي تباعاً
شكل 12: خوارزمية تأثير الشاشة
بحيث أن a هي المصدر و b هو القناع
شكل 13: تأثير الشاشة
مصادر وملاحظات ختامية
بهذه المقالة تم أختيار عدة خوارزميات من مقالة أوضاع المزج من ويكيبيديا الموسوعة الحرة وكانوا أسهلها وذلك لأغراض توضيحية ويمكنكم إيضاً أن تتطلعوا على الشفرة المصدرية لجيمب بهذا الملف
وهذا المجلد
https://gitlab.gnome.org/GNOME/gimp/-/tree/master/app/operations/layer-modes
ومن الأشياء الملوحظة هو أن عملية القسمة بها بعض التفاصيل التي تجعلها آمنة وصحيحة ولهذا لم نتناولها بمقالتنا.
وكما أننا لم نناقش الوضع الطبيعي normal mode
لأن سيقوم بأستبدال كل ما في الطبقة الأولى بما هو موجود بالطبقة الثانية وشيءٌ أخر وهو أننا لم نتكلم عن تركيب الألفا (أو تركيب الطبقة الشفافة) وهذا لأن صيغة ppm لا تدعم الشفافية ولكن صيغة PNM الشبيهة بها تقوم بالمطلوب.
ويجدر بنا الذكر بأن بالإمكان أستخدامه قناع حلزوني أو قُطري أو صورة عادية قناع ملونة أو رمادية
وهذا قد يعطي تأثير شبيه بتركيز عدسة الكاميرا في بعض الأوضاع.
صورة سوق البهارات مأخوذة من ويكيبيديا من المستخدم Bertrand Devouard تحت رخصة جنو للوثائق الحرة.
لقطة سفينة الجالبوت من ويكيميديا بواسطة المصور توماس جارفيز عام 2006 تحت بنود رخصة المشاع الأبداعي المشاركة بالمثل.