UP | HOME
جناوي

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

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

Software engineering student - interested in graphics and audio engineering

البكسل من وجهة نظر المبرمج وليس الخيال العلمي
نشر في Nov 19, 2020 بقلم السيد مذهل.

مقدمة

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

في هذه المقالة سوف نشرح طريقة كتابة وقراءة أبسط صيغ البكسل (غير المضغوطة) وسنركز على صيغة netpbm وبعمق لوني 8 بت وكأداة سنستخدم لغة البرمجة C.

الرواية المشهورة

تتكون الصورة من نقط أو مربعات (وفي أغلب الشاشات مستطيلات ولكن لا يهمنا كيفية عرض البكسل في هذه المقالة) ولكن ما هي تلك النقاط؟ هي مجرد أرقام وحسب

zoomncolor.png

شكل 1: نظرة مقربة على صورة نقطية

فبالصورة الملونة كل مربع يتكون من ثلاث أرقام غالباً وبعض الأحيان يضاف لون رابع للشفافية وهذا ما هو شائع ولكن توجد هناك نماذج لونية مختلفة ولكن ما يهمنا هو النموذج اللوني أحمر-أخضر-أزرق RGB (Red-Green-Blue) والذي هو مستوحى من الألوان الأساسية وبمزج قيمها يمكننا الحصول على بقية الألوان.

صيغة netpbm

تعتبر صيغة netpbm من أبسط وأسهل الصيغ لأنها لا غير مضغوطة وفي رأس الملف نكتب معلومات الصورة (metadata) كعمقها اللوني والأبعاد بأستخدام النص العادي وبالأدق معيار ASCII (عدد محدود من الروموز اللاتينة والأنجليزية سهل المعالجة بسبب تكون كل رمز من بايت byte واحد) وهذه الصيغة تشمل العديد من الأمتدادات ولها طريقتان الأولى كتابة الملف بالـ ASCII كنص عادي (وهذا جيد للتعلم لكن غير عملي للصور الكبيرة) والأخرى وهى الطريقة الطبيعية والأمثل كتابة الصورة بالصيغة الثنائية binary mode وهذا ما نسعى لفهمه ولكن قبل هذا لنرى الطريقة الأولى عبر ملف نكتبه يدوياً:

P1
# This is an example bitmap of the harif alef
# by the way # mean comment line
5 11
0 0 0 0 0
0 0 1 1 0
0 0 1 0 0
0 1 1 1 0
0 0 0 0 0
0 0 1 0 0
0 0 1 0 0
0 0 1 0 0
0 0 1 0 0
0 0 1 0 0
0 0 0 0 0

alef.png

شكل 2: مقاسات وأبعاد الصورة أقرب من ما تبدو عليه بالواقع

هذا المثال البدائي يستخدم أول أمتداد لمجموعة صيغ netpbm وهو pbm. وفيه عمق لوني 1 بت بمعنى لونان فقط الأسود الكلي والأبيض الكلي ولنشرح ماذا جرى يمكننا القول بالبداية كتبنا الرقم السحري (Magic Number) P1 وهو ما يحدد نوع الصيغة وبعدها كما تلاحظون سطران يوضحان كيفية كتابة التعليقات وبعدهما سطر يوضح أبعاد الصورة 5 بكسل عرضاً و11 بكسل طولاً ومن ثمة حددنا النقاط فالأبيض هو 0 والأسود 1 وأخيراً يمكنك نسخ مثال الألف بإي محرر نصوص وحفظه كـ alef.pbm ويمكنك فتح لاحقاً بإي برنامج عرض صور يدعم صيغة netpbm ولكن بما أن الصورة صغيرة جداً فأفضل أن تفتحها ببرنامج جيمب GIMP المتقدم لتعديل الصور لأن أغلب برامج عرض الصور والمتصفحات عندما تكون الصورة صغيرة تتطبق تأثيرات التمويه عند التقريب لكي لا تظهر الحواف الحادة.

والأن لننتقل إلى مثال أكثر إشراقاً قليلاً وهو يحوي اللونان الأبيض والأسود بتدرجات تميل من رقم 0 إلى 255 وهذا هو حد الـ 8 بت للبكسل الواحد (وهذا ما سنشرحه لاحقاً بقسم الـBinary mode):

P2
# ali.pgm
12 12
4
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 2 0 0 0 0 0
0 0 0 0 0 0 2 0 0 0 0 0
0 0 0 0 0 0 2 0 0 0 0 0
0 0 0 0 0 0 2 0 1 1 1 0
0 0 0 0 0 0 2 0 1 0 0 0
0 3 0 0 3 2 2 2 1 1 1 0
0 3 0 0 3 0 0 0 0 0 0 0
0 3 3 3 3 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 4 0 4 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0

ali.png

شكل 3: مقاسات وأبعاد الصورة أقرب من ما تبدو عليه بالواقع

نلحظ هنا أن هناك رقماً يلي الأبعاد وهو يرمز إلى كم درجة من الرمادي لدينا وهنا لدينا 4 وأما فبعد هذا المثال سنأخذ صورة ملونة:

P3
# colorboard.ppm
3 5
255

255 0 0         0 255 0         0 0 255
255 0 255       0 255 255       0 0 255
255 0 0         255 255 0       255 0 255
255 255 0       0 255 0         0 255 255
255 255 255     140 0 0         0 0 0

colorboard.png

شكل 4: مقاسات وأبعاد الصورة أقرب من ما تبدو عليه بالواقع

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

بأستخدام الصيغة الثنائية Binary mode

ماذا نعني هنا؟

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

كل لغة برمجة فيها دوال ومكتبات وطرق لكتابة ملفات الصيغة الثنائية ففي السي يمكننا أستخدام أما open أو fopen وسنعمل على الأخير لذا يجب عليك تضمين مكتبة <stdio.h> وقبل أن نبدأ يجب علينا أستيعاب بعض المفاهيم.

أولاً كل نوع بيانات في السي يشغل حيز من البايتات وهنا حفنة من الأنواع مع أحجامها موضحة كالأتي:

حرف أو رموز واحد (char): عندما نستخدم هذا النوع لوضع حرف نقوم بوضه ضمن علامتين أقتباس مفردتين مثل 'c' وبما أن حجم هذا النوع بايت واحد فأنه لا يقبل غير الرموز الأنجليزية واللاتينة بمعيار ASCII ولكن هذا ليس موضوعنا لذا سأتخطاه وكما يشرح معيار اللغة سي فأنه سيبقى دوماً بايت واحد مهما تغيرات المعالجات ومعمارياتها لأنه في بعض الأنواع يختلف الحجم حسب نوع معمارية المعالج (CPU Architecture) ففي معمارية 32 بت بعض الأنواع تختلف عن معمارية 64 بت وعموماً هذه يعطينا أرقام من 128- إلى 127.

رمز (char) بدون إشارة (unsigned) [unsigned char]: عندما نقول بدون إشارة فهذا يعني أن العدد دوماً موجب وهنا نحصل على مجال من 0 إلى 255 رقماً يمكن أن نستخدمه مع unsigned char وهذا سنستخدمه كثيراً في أمثلتنا لأنه 8 بت.

عدد قصير (short): عدد بحجم 16 بت من ما يعني أن مجاله من 32,768- إلى 32,767.

عدد قصير بدون إشارة (unsigned short): الحجم 2 بايت (16 بت) والمجال 0 إلى 65,535.

عدد صحيح عادي (int): يساوي 32 بت ومجاله يمتد من 2,147,483,648- إلى 2,147,483,647.

عدد صحيح عادي بدون إشارة (unsigned int): يساوي 32 بت ومجاله يمتد من 0 إلى 4,294,967,295.

عدد طويل (long): هنا الموضوع يختلف بين أجهزة 32 بت وأجهزة 64 بت فتأكد من حاسوبك لتعلم كيف يكون وزن هذا المتغير عندك ففي أجهزة 32 هذا المتغير يساوي 32 بت ومجاله من 2,147,483,648- إلى 2,147,483,647 كأنه عدد صحيح عاد وأما بأجهزة معمارية 64 بت فهو يساوي 64 بت ومجاله من 9,223,372,036,854,775,808- إلى 9,223,372,036,854,775,807.

عدد طويل بدون إشارة (unsigned long): في أجهزة 32 بت يساوي 32 بت ومجاله من 0 إلى 4,294,967,295 وأما أجهزة 64 بت (الحديثة والشائعة حالياً) فيساوي 64 بت بمجال 0 إلى 18,446,744,073,709,551,615.

عدد طويل طويل (long long): هنا الموضوع يختلف فهذا العدد ثابت الحجم بكلاَ المعماريتين فهو يساوي 64 بت ومجاله من 9,223,372,036,854,775,808- إلى 9,223,372,036,854,775,807.

عدد طويل طويل بدون إشارة (unsigned long long): 64 بت حجماً يمتد من 0 إلى 18,446,744,073,709,551,615.

ماذا يحصل لو تخطينا المجال أو صرنا خلفه؟ عن تخطي المجال سيأخذ المُجمع (compiler) الأرقام الزايدة ويضيفها على الحد الأدنى والعكس صحيح عن التراجع خلف المجال فعندما نضع القيمة 256 في unsigned char سيعود إلى 0 صفر وهكذا.

لم نتطرق للأعداد الكسرية (float, Double) لأن الأكثر شيوعاً أستخدام الأعداد الصحيحة لأنها أصغراً مساحة وأسرع في المعالجة ولكن للإشارة السريعة لحجومهم الـ float بوازي 32 بت (4 بايت) والـ double هو 64 بت (8 بايت).

قد يكون من الغريب على بعض المبتدئين تخزين عدد بدلاً من رمز وهذا لأنهم لا يعلمون أن الرموز تحدد بعدد معين فإذا عرفنا متغير char letter = 'g' وقمنا بطباعة رقمه على الشاشة بأستخدام هذا الأمر printf("%d", letter) فستكون النتيجة 103.

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

حتى نضمن أن الأعداد ستكون ذاتها في كل حاسوب هناك مكتبة معيارية في أنواع بيانات ثابتة للأعداد الصحيحة بجميع الحواسيب تسمى <stdint.h> وفيها على سبيل المثال int8_t و int16_t و int32_t و int64_t وهناك إيضاً للأعداد بغير إشارة uint8_t و uint16_t ….. وإلخ.

تسمى بعض الصيغ الملونة التي تأخذ أرقام 8 بت بـ 24 لأنها تحوي بالبسكل الواحد ثلاث أرقام كل واحد منها ضمن حدود 8 بت (8x3 = 24) وعندما تحوي شفافية تسمى صيغة 32 بت وهذا لإضافة قناة الشفافية ورغم أن التسمية لا أرى أنها دقيقة لأن كل لون يكون ضمن حدود 8 بت لذلك فهي صيغة 8 بت ولكن هذه التسمية متداولة لذلك لا تستغرب عندما تقرأ هكذا أشياء مع العلم بوجود صيغ 16 بت و 32 بت لكل قناة لونية وهذه الصيغة تأخذ مساحة أكبر بكثير وتعطي جودة أعلى.

هذا الكلام جميل ولكن تبقت مشكلة واحد لنستوعبها وهي أن هناك نوعان من الحواسيب التي تحسب العدد من البتات على اليمين (little endian) والتي تحسب العدد من بتاته من اليسار (big endian) وهذا التصنيف غير مرتبط بأن يكون الجهاز 32 أو 64 بت فيمكن أن يكون ببعض الأجهزة من هذا النوع وذاك النوع ولكن الأشهر هو الحواسيب التي تحسب العدد من اليمين (little endian)

endians.png

شكل 5: رسم يوضح طرق العد والأرقام ظاهرة بالصيغة الستعشرية (ويكيميديا بواسطة R. S. Shaw, ملكية عامة)

فماذا يعني هذا لنا ولماذا نهتم إذا كان هذا أمراً داخلياً بالحاسوب؟ هذا يعني أن الحاسوب يكتب البتات بحسب طريقته للعد فهذا يعني أختلاف الملفات المكتوبة من حاسوب لأخر لهذا السبب وضعت المعايير وحددت الصيغ إي الطرق تستعمل ففي جهاز (little endian) إذا أردنا كتابة البتس لصيغة PNG (وهي تستخدم طريقة big endian) وجب علينا تحول الأرقام واحداً بواحد ولكن في شرحنا هذا قلنا سنستخدم أبسط الصيغ وهي تستخدم طريقة (little endian) كما أغلب حواسيب المستخدمين.

لنأخذ مثلاً العدد 133 ونرى كيف يتم تمثيله:

Little endian: 0x000085

Big endian: 0x850000

ولكن لو كتبنا الرقم الخاص بـ big endian وأعدنا قرائته بجهاز little endian فسوف يصبح 8716288 وليس 133 وهكذا دواليك.

ملاحظة: لتحويل الرقم من العد الإيسر للعد الإيمن يمكن أستخدام دالة htonl(num) وللعكس ntohl من مكتبة <arpa/inet.h> أو أستخدام بعض حيل الـ bit shifiting.

وبعد كل هذا التفصيل الممل لنأخذ مثلاً لكتابة صورة بالصيغة الثنائية:

// -*- compile-command: "gcc -o red-gradient red-gradient.c && ./red-gradient"; -*-
#include <stdio.h>
#include <stdlib.h>

#define WIDTH  400
#define HEIGHT 200

int main(int argc, char **argv)
{
  FILE *fp = fopen("red-gradient.ppm", "w+b");
  fprintf(fp, "P6\n%d %d\n255\n", WIDTH, HEIGHT);

  float inc = (float) (256.0/WIDTH);

  float k = inc;
  for (float i = 0; i < HEIGHT; ++i)
    {
      for (int j = 0; j < WIDTH; ++j)
        {
          putc ((char) k, fp);
          putc (0, fp);
          putc (0, fp);
          k+=inc;
        }
      k = inc;
    }

  fclose (fp);
  return 0;
}

red-gradient.png

شكل 6: تدرج اللون الأحمر

في هذا المثال لم نستخدم إي مصفوفة (array) أو نخصص مساحة من الذاكرة بأستخدام malloc ولكن كتبنا البايتات مباشرة بالملف ومن الملاحظ عندما فتحنا الملف مررنا خيرا w+b حيث b ترمز إلى binary وهكذا نستطيع كتابة البايتات مباشرة وليس الـ Ascii وكما نرى فقط وضعت سطر البناء بأعلى الملف كل ما عليك هو نسخ هذا النص ووضع بملف مسمى red-gradient.c وبنائه بالأمر أعلاه وإذا كنت من مستخدمين إيماكس فتستطيع بمجرد الدخول على الملف مرة أخرى كتابة هذا الأمر M-x compile.

أما الأن لننتقل لمثال أخر حيث نستعمل فيه مصفوفة:

// -*- compile-command: "gcc -o red-white red-white.c && ./red-white"; -*-
#include <stdio.h>
#include <stdlib.h>

#define WIDTH 400
#define HEIGHT 200

int main(int argc, char **argv)
{
  FILE *fp = fopen("red-white.ppm", "w+b");
  fprintf(fp, "P6\n%d %d\n255\n", WIDTH, HEIGHT);

  unsigned char pixels[HEIGHT][WIDTH][3];
  float inc = (float) (256.0/WIDTH);
  float k = 255;

  for (int i = 0; i < HEIGHT; ++i)
    {
      for (int j = 0; j < WIDTH; ++j)
        {
          pixels[i][j][0] = 255;
          pixels[i][j][1] = (unsigned char) k;
          pixels[i][j][2] = (unsigned char) k;
          k-=inc;
        }
      k=255;
    }

  fwrite(pixels, HEIGHT*WIDTH*3, 1, fp);
  fclose (fp);
  return 0;
}

red-white.png

شكل 7: من أبيض يساراً إلى أحمر يميناً

وهنا يجب الإشارة أنه ليس لزاماً أستعمال مصفوفة ثلاثية الأبعاد فيمكن الأكتفاء بالأحادية على أن يكون حجمها pixels[HEIGHT*WIDTH*3] وهنا ستختلف طريقة معالجتها قليلاً هذا وبعد كنا بالمثال الأول قد أنتجنا تدرج من الأسود إلى الأحمر والأن من الأبيض إلى الأسود ونستطيع أن نلاحظ أن الخانة الأولى (وهي خانة اللون الأحمر) دوماً مملوئة لأخر حد (255) بينما الأخريات ينمون تصاعدياً من الصفر إلى 255 وهذا معكوس المثال الأسبق.

لا حاجة لتنظيف الذاكرة بأستخدام free(pixels) لأن هذه مصفوفة (array) وليس مؤشر (pointer) لذا سوف يتم تنظيفها بعد الخروج من نطاقها تلقائياً.

الأن في مثالنا الأتي سوف نستخدم المؤشرات (pointers) وسوف نخصص قطعة من الذاكرة في وقت التشغيل مع malloc:

// -*- compile-command: "gcc -o ptr-eg ptr-eg.c && ./ptr-eg"; -*-
#include <stdio.h>
#include <stdlib.h>

#define WIDTH 400
#define HEIGHT 200

int main(int argc, char **argv)
{
  FILE *fp = fopen("ptr-eg.ppm", "w+b");
  fprintf(fp, "P6\n%d %d\n255\n", WIDTH, HEIGHT);

  unsigned char *pixels = malloc(sizeof(unsigned char) * WIDTH * HEIGHT * 3);
  float inc = (float) (256.0/WIDTH);
  float k = 255;
  int ten_percent_w = WIDTH*0.1;
  int ten_percent_h = HEIGHT*0.1;

  unsigned char *iter = pixels;
  for (int i = 0; i < HEIGHT; ++i)
    {
      for (int j = 0; j < WIDTH; ++j)
        {
          if (j % ten_percent_w == 0 && i % ten_percent_h == 0)
            {
              *iter++ = 255;
              *iter++ = 0;
              *iter++ = 0;
            }
          else if (i % ten_percent_h == 0)
            {
              *iter++ = 255;
              *iter++ = 0;
              *iter++ = 0;
            }
          else if (j % ten_percent_h == 0)
            {
              *iter++ = 255;
              *iter++ = 0;
              *iter++ = 0;
            }
          else
            {
              *iter++ = k;
              *iter++ = k/2 ;
              *iter++ = i+j << 2;
            }

          k-=inc;
        }
      k=255;
    }

  fwrite(pixels, HEIGHT*WIDTH*3, 1, fp);
  free (pixels);
  fclose (fp);
  return 0;
}

ptr-eg.png

شكل 8: مثال المؤشر

وأما الأن سوف نأخذ مثال واحد على قراءة ملفات النصوص العادية (ASCII) لصيغة netpbm ويحولها إلى الصيغة الثنائية:

// -*- compile-command: "gcc -o ascii-parse ascii-parse.c && ./ascii-parse"; -*-
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct 
{
  char *magic, *width, *height, *depth;
} header;                       /* Image metadata, the section before data */

int main(int argc, char **argv)
{
  header file_header;

  FILE *fp = fopen("colorboard.ppm", "r");
  fseek(fp, 0, SEEK_END);
  unsigned int lastpos = ftell(fp);
  fseek(fp, 0, SEEK_SET);

  int numberscount = 0;         /* How many number in file without spaces and comments */
  int wordc = 0;                /* How many char the word takes */
 loop:
  while (ftell(fp) != lastpos)
    {
      char c = fgetc(fp);
      if (c == '#') while (c != '\n') c = fgetc(fp);      
      if (c == '\n' || c == '   ' || c == ' ')
        {
          if (wordc > 0)
            {
              numberscount++;
              wordc = 0;
            }
          goto loop;
        }
      else wordc++;
    }
  unsigned char pixels[numberscount-4]; // data without magic number, height, width and depth
  unsigned int positions[numberscount];
  int sizes[numberscount];

  fseek(fp, 0, SEEK_SET);
  int iter = 0;
  wordc = 0;
  loop2:
  while (ftell(fp) != lastpos)
    {
      char c = fgetc(fp);
      if (c == '#') while (c != '\n') c = fgetc(fp);      
      if (c == '\n' || c == '   ' || c == ' ')
        {
          if (wordc > 0)
            {
              positions[iter] = ftell(fp);
              sizes[iter] = wordc;
              iter++;
              wordc = 0;
            }
          goto loop2;
        }
      else wordc++;
    }

  int pixiter = 0;              /* Pixel iterator */
  for (int i = 0; i < numberscount; ++i)
    {
      fseek(fp, positions[i]-sizes[i]-1, SEEK_SET);
      char str[sizes[i]];
      for (int t = 0; t < sizes[i]; ++t) str[t] = fgetc(fp);
      if (i == 0) file_header.magic = strdup(str);
      else if (i == 1) file_header.width = strdup(str);
      else if (i == 2) file_header.height = strdup(str);
      else if (i == 3) file_header.depth = strdup(str);
      else
        {
          pixels[pixiter] = (unsigned char) atoi(str);
          pixiter++;
        }
    }
  // setting the last number manually because it is somewhat problematic
  fseek(fp, numberscount-sizes[numberscount-1], SEEK_SET);
  char last[1];
  for (int f = 0; f < sizes[numberscount-1]; ++f) last[f] = fgetc(fp);
  pixels[numberscount-4] = atoi(last);

  fclose(fp);

  FILE *fp2 = fopen("binary-colorboard.ppm", "w+b");
  fprintf(fp2, "P6\n%s %s\n%s\n", file_header.width,
          file_header.height, file_header.depth);
  fwrite(pixels, numberscount-3, 1, fp);
  fclose(fp);

  return 0;
}

size.png

شكل 9: كيف أختلف حجم الصورة

في هذا أستخدام صورة من أحد الأمثلة السابقة colorboard.ppm وعلى أفتراض أنها بنفس مجلد البرنامج وكما رأينا كيف تقلص حجم الصورة عند تحويلها من نص عادي إلى الصيغة الثنائية فكانت من 167 إلى 56 بايت.

تمارين:

1- أستبدل القيم الثابتة (Hard-coded) بمتغيرات وأجعل الشفرة تأخذ أسم الملف من المستخدم

2- حول إجراءات التحويل إلى دالة خارج الدالة الرئيسية

3- هذه الشفرة تعمل فقط على الصورة ذات ثلاث ألوان أجعلها تعمل على صور أبيض وأسود والتدرج الرمادي

4- عندما يعطي المستخدم البرنامج filename.ppm أجعل الملف المحول بأسم binary-filename.ppm

5- تخلص من goto

6- حاول أن تكتب خوارزمية أفضل أخف وأسرع

7- أكتب خوارزمية بديلة بأستخدام strtok و strtok_r من مكتبة <string.h> حتى تأخذ نص (string) وتفصل محتوياته بأستخدام المسافات والأسطر.

8- هذه الشفرة عندما تحول الصيغة لا تهتم لحفظ التعليقات التي تبتدأ بـ # فلا تنقلها, أجعل الشفرة تقوم بذلك

وأما الأن سنرى كيفية قراءة الملفات ذات الصيغة الثنائية وسنقوم ببعض المعالجة كذلك:

// -*- compile-command: "gcc -g -o flip flip.c -lm && ./flip dog.ppm  && eog flip-dog.ppm"; -*-
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <string.h>

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

  unsigned int w, h, d;
  FILE *fp = fopen(argv[1], "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);

  unsigned char newdata[w*h*3];
  char outstr[strlen (argv[1])+ strlen ("flip-")+1];
  snprintf (outstr, strlen (argv[1])+ strlen ("flip-")+1, "flip-%s", argv[1]);
  FILE *fpout = fopen(outstr, "wb");
  fprintf(fpout, "P6\n%u %u\n255\n", w, h);

  int ik = w*h*3;
  for(int cy = 0; cy < w*h*3; cy+=3)
    {
      newdata[cy+2] = data[--ik];
      newdata[cy+1] = data[--ik];
      newdata[cy] = data[--ik];
    }

  fwrite (newdata, sizeof newdata, 1, fpout);
  fclose (fpout);
  return 0;
}

flip.png

شكل 10: أنقلاب الصورة

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

$ gcc -g -o flip flip.c -lm && ./flip dog.ppm  && eog flip-dog.ppm

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

في هذا المثال أستخدمنا صورة وقلبناها بعد قراءتها من ملف صيغة الثنائية (binary file) ففي البداية قراءنا رأس الملف (file header) ومنه حددنا الطول والعرض والعمق فبمعرفة الطول والعرض والعمق نستطيع ضربهم معاً لنحدد حجم مصفوفة البكسل بالبايت فلدينا لكل بكسل ثلاثة ألوان لذا: الطول x العرض x ثلاثة = الحجم بالبايت وهكذا قمنا بنسخ المصفوفة وأنشاء أخرى حتى ندخل البيانات بالمقلوب.

هنا اللون يساوي بايت واحد وأما إذا كان يساوي أثنان (16 بت للعمق اللوني 8+8 = 16 بت) الطول x العرض x ثلاثة x أثنان, حيث أثنان هو وزن المتغير بالبايت ونحن لم نكن نضرب وزن المتغير بتللك المعادلة السابقة لأنه في حالة 8 بت يساوي بايت واحد وحسب فلا يغير النتيجة ومن هذا التوضيح يمكنك فهم طريقة حساب الأعماق اللونية الأكبر مثل 32 و 64.

والأن مع مثال مشابه ولكن لعكس الألوان بالصورة:

// -*- compile-command: "gcc -g -o invert invert.c -lm && ./invert dog.ppm  && eog invert-dog.ppm"; -*-
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <string.h>

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

  unsigned int w, h, d;
  FILE *fp = fopen(argv[1], "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);

  unsigned char newdata[w*h*3];
  char outstr[strlen (argv[1])+ strlen ("invert-")+1];
  snprintf (outstr, strlen (argv[1])+ strlen ("invert-")+1, "invert-%s", argv[1]);
  FILE *fpout = fopen(outstr, "wb");
  fprintf(fpout, "P6\n%u %u\n255\n", w, h);

  for(int cy = 0; cy < w*h*3; ++cy)
    {
      newdata[cy] = 255 - data[cy];
    }

  fwrite (newdata, sizeof newdata, 1, fpout);
  fclose (fpout);
  return 0;
}

invert.png

شكل 11: الألوان المعكوسة

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

وللعلم أن دالة getline موجودة بمكتبة Glibc في التوزيعات الجناوية وربما تتوفر على بعض الأنظمة الأخرى ولكن إن لم يعمل هذا المثال بنظامك فقم بأستخدام دالة بديلة وبدل المثال أو قم بأستخدام Gnulib.

صيغة targa و microsft BMP

هنا سندخل في شرح صيغ بسيطة ومشابهة لـ netpbm ومنها targa (.tga) و Microsoft BMP (Bit map image) ولكن دون التمعق فيهما لأننا عرجنا على الأساسيات للبكسل لذا فأن القارئ بوسعه الخوض في غمارها مع العلم أنهما يدعمان على حد علمي أبسط خوارزمية ضغط وهي ترميز طول التشغيل (RLE run length encoding).

مثال كتابة ملف بصيغة tga:

// -*- compile-command: "gcc -o write-tga write-tga.c && ./write-tga && eog file.tga"; -*-
#include <stdio.h>
#include <string.h>

enum
{
 width = 550,
 height = 400
};
/* to see the format spec see wikipedia */
/* https://en.wikipedia.org/wiki/Truevision_TGA */
/* this format is little-endian so no need to convert in normal x86 pc */
int main (void)
{
  FILE *fp = fopen("file.tga", "w+b");
  static unsigned char pixels[width * height * 3];
  static unsigned char tga[18+14];      /* HEADER static so all values initialized to zero */
  unsigned char *p;
  size_t x, y;

  /* all commented lines are unncessory because they are already initialized to zero */
  tga[0] = 14; // ID length: the length of comment section after header
  /* tga[1] = 0; */ // color map type: no color map (not indexed)
  tga[2] = 2; // Image type: uncompressed true-color image
  // color map specification
  /* tga[3] = 0; */ // First entry index (2 bytes): index of first
  /* tga[4] = 0; */ // color map entry that is included in the file
                    // it is zero because we didn't have colormap
  /* tga[5] = 0; */ // Color map length (2 bytes): number of entries
  /* tga[6] = 0; */ // of the color map that are included in the file
  /* tga[7] = 0; */ // Color map entry size (1 byte): number of bits per pixel

  tga[8] = 0; // X-origin (2 bytes): absolute coordinate of
  tga[9] = 0; // lower-left corner for displays where origin is
              // at the lower left  
  /* tga[10] = 0; */ // Y-origin (2 bytes): as for X-origin
  /* tga[11] = 0; */
  tga[12] = 255 & width;        /* width are stored in 2 bytes so this trick is needed */
  tga[13] = 255 & (width >> 8);
  tga[14] = 255 & height;       /* height are stored in 2 bytes so this trick is needed */
  tga[15] = 255 & (height >> 8);
  tga[16] = 24; // R 8 byte + G 8 byte + B 8 byte = 24 (without alpha)
  tga[17] = 32; // Image Descriptor see doc
  // comment section with word
  // happy, hacking
  tga[18] = 'h';
  tga[19] = 'a';
  tga[20] = 'p';
  tga[21] = 'p';
  tga[22] = 'y';
  tga[23] = ',';
  tga[24] = ' ';
  tga[25] = 'h';
  tga[26] = 'a';
  tga[27] = 'c';
  tga[28] = 'k';
  tga[29] = 'i';
  tga[30] = 'n';
  tga[31] = 'g';

  p = pixels; // p the iterator
  for (y = 0; y < height; y++)
    {
      for (x = 0; x < width; x++)
        {
          *p++ = 255 * ((float) y / height);
          *p++ = 255 * ((float) x / width);
          *p++ = 255 * ((float) y / height);
        }
    }
  fwrite (tga, sizeof (tga), 1, fp);
  fwrite (pixels, sizeof (pixels), 1, fp);
  fclose (fp);
  return 0;
}

tga.png

شكل 12: طيف ألوان

كتابة ملف بصيغة BMP:

// -*- compile-command: "gcc -g -o write-bmp-file write-bmp-file.c && ./write-bmp-file && eog written-file.bmp"; -*-
#include <stdio.h>

/* in bmp there are to headers : header (14 byte) and Dib header (22 byte) */
/* Color are not RGB but BGR */
/* Coordinates starts from bottem left */
#define WIDTH 400
#define HEIGHT 200
#define DEPTH 3
#define CDEPTH 24

int main(int argc, char **argv)
{
  FILE *fp = fopen("written-file.bmp", "w+b");

  //calculate padding
  int pad = 0, wcpy = WIDTH*DEPTH;
  while ((wcpy % 4) != 0) { wcpy++; pad++; }
  printf("pad = %u\n", pad);

  int rdata = (WIDTH*HEIGHT*DEPTH)+(HEIGHT*pad);
  printf("raw data = %u\n", rdata);
  int fsize = rdata+(14+40);
  printf("full file size = %u\n", fsize);

  fprintf(fp, "BM");
  // file size (4 byte to store the number that indicate file size)
  int i = fsize; // header size + Dib header size + raw data size (which include padding)
  fwrite (&i, 4, 1, fp);

  // optional application bit set to zero (2 byte)
  fputc(0, fp); fputc(0, fp);

  // another optional application bit set to zero (2 byte)
  fputc(0, fp); fputc(0, fp);

  // (4 byte) where pixel data start (offset) for us we have no  
  i = 54; // metadata other than header so 54 right after 53 (Dib header)
  fwrite (&i, 4, 1, fp);

  // Dib header //
  // 1: sizeof header (4 byte)
  i = 40;
  fwrite (&i, 4, 1, fp);

  // 2: image width (4 byte)
  i = WIDTH;
  fwrite (&i, 4, 1, fp);

  // 3: image height (4 byte)
  i = HEIGHT;
  fwrite (&i, 4, 1, fp);

  // 4: color plane must be 1 (according to wikipedia) (2 byte)
  short k = 1;
  fwrite (&k, 2, 1, fp);

  // 5: color depth  (2 byte)
  k = CDEPTH; // it is 24 because 8bit Blue + 8bit Green + 8bit Red
  fwrite (&k, 2, 1, fp);

  // 6: compression method (4 byte) (no compression 0)
  i = 0;
  fwrite (&i, 4, 1, fp);

  // 7: raw data size (4 byte)
  i = rdata;
  fwrite (&i, 4, 1, fp);

  // 8: horizontal resolution (4 byte)
  i = WIDTH;
  fwrite (&i, 4, 1, fp);

  // 9: vertical resolution (4 byte)
  i = HEIGHT;
  fwrite (&i, 4, 1, fp);

  // 10: number of colors in palette (4 byte)
  i = 0; // 0 for default 2^n
  fwrite (&i, 4, 1, fp);

  // 11: important colors (4 byte)
  i = 0; // ignored 0 (this what generally used)
  fwrite (&i, 4, 1, fp);

  float increment = (float) (256.0/WIDTH);
  float pkkk = 0;
  // after each row there must be padding bytes
  // to make each row multiple of 4 (if it is not )
  for (int i = 0; i < WIDTH; ++i)
    {
      // one row generated from each i iteration
      for (int jk = 0; jk < HEIGHT; ++jk)
        {
          fputc((unsigned char) pkkk, fp);      /* B */
          fputc(0, fp); /* G */
          fputc(0, fp); /* R */
        }
      for (int tk = 0; tk < pad; ++tk) fputc(0, fp); // Padding
      pkkk += increment;
    }

  fclose(fp);
  return 0;
}

bmp.png

شكل 13: تدرج أزرق

نلاحظ هنا أن الصيغة هذه تستخدم نموذج لوني مختلف قليلاً عن ما سبق فهنا نستخدم أزرق أخضر أحمر BGR بدل RGB والأحداثيات تبدأ من أدنى اليسار وليس أعلاه وإيضاً نضع فراغات (Padding) حتى نجعل كل صف من المصفوفة من أحد مضروبات أربعة وهذا ربما لدواعي الإداء وسرعة القراءة تم تصميم الصيغ هكذا لأن الحاسوب بإمكانه قراءة حفنة المعلومات الزوجية بشكل أفضل من الفردية ولكن اللَّه أعلم بما كان يجول بفكر مايكروسوفت عندما صممت الصيغة فقد علمت أنهم لم يصمموها لتكون صيغة مستقلة ومتنقلة بين الأجهزة والأنظمة والبرامج ولكن حتى تكون موجهة تجاه برامجهم بشكل خاص فأنا لا أعلم لماذا نحن مجبرون على جعل رأس الملف لا يقل عن 24 بايت وغالباً 54 بايت فهذه الصيغة أكثر تعقيداً من ما سبق شرحه.

تمارين:

1- أجعل البرنامج يأخذ أسم الملف, الطول, العرض, العمق اللوني, … إلخ.

2- أستبدل جمل fwrite و fputc المتكررة بكتابة جملة fwrite واحدة تضع فيها هيكل (struct) للـ header.

3- أستخدم مصفوفة (Array) بدل الكتابة مباشرة في الملف أو أستخدم تخصيص الذاكرة في وقت التشغيل

ملاحظات ختامية

لمزيد من المعلومات ومعرفة تفاصيل صيغة netpbm يمكنكم الإطلاع على المعيار الوثائق الرسمية وللحصول على مكتبات وأدوات وأمثلة يمكنكم إيضاً زيارة موقعهم الرسمي.

صيغة bmp يمكن معرفة تفاصيلها عبر الدليل الرسمي وأما صيغة tga فالشركة التي أبتكرتها لم يعد لها وجود إما أقفلت أو بيعت أو تغيرت لذا فالنسخة المؤرشفة موجودة بمكتبة الكونغرس الأمريكية وهنا الـpdf.

إذا كنت تملك معالج غير أعتيادي مثل Power9 وليس x86 وهو بوضع Big-endian فتأكد من تحويل الأرقام كما قلنا سابقاً لأن جميع الصيغ التي تم مناقشتها هي من نوع Little-endian وأما أنت كنت تستخدم معالج ARM فعلى الأغلب هو Little-endian ولكن هناك إيضاً ميزة بهذه المعالجات تتيح لك التبديل بين Big-endian و Little-endian بنفس المعالج في بعض الأجزاء (biendian).

تسمى طريقة Big-endian بالـ network order لربما لكثرة أستخدامها بالمعايير فهي الطريقة المفضلة والأكثر شيوعاً لذا وجب التنبيه.

صورة الثنائي المرن المعدلة مأخوذة من مؤسسة البرمجيات الحرة تحت رخصة جنو العمومية الثانية.

رغم أنه من المضحك عرض نتائج الشفرات بصيغة png في هذه المقالة بدلاً عن netpbm ولكن تم ذلك لضمان دعم كل المتصفحات.

يمكنك اللعب بالشفرة المصدرية وبالأخص تعديل الحلقة التكرارية حتى تجرب تعديل الخوارزمية لتحصل على نتائج مختلفة.

comments . التعليقات