ITK
معمارية تطبيقات المصادر المفتوحة (المجلد الثاني)ITK
معمارية تطبيقات المصادر المفتوحة (المجلد الثاني) ITK
Luis Ibáñez وBrad King
إذا استمتعت بهذه الكتب، فقد تستمتع أيضًا بـ التصميم البرمجي بالأمثلة في Python والتصميم البرمجي بالأمثلة في JavaScript.
9.1. ما هي ITK؟
ITK، أي Insight Toolkit، هي مكتبة لتحليل الصور طوّرتها مبادرة المكتبة الوطنية للطب في الولايات المتحدة National Library of Medicine، التي وفّرت معظم تمويل هذه المبادرة. ويمكن النظر إلى ITK على أنها موسوعة عملية لخوارزميات تحليل الصور، وبخاصةٍ ما يتعلّق بترشيح الصور (image filtering) وتقطيعها (segmentation) وتسجيلها (registration). وقد طوّرت المكتبة تحالفٌ (consortium) يضمّ جامعات وشركات تجارية والعدّ من المساهمين الأفراد من مختلف أنحاء العالم. وبدأ تطوير ITK عام 1999، وبعد مرور عشر سنوات على ذلك بوقتٍ قريب خاضت المكتبة عملية إعادة هيكلة (refactoring) بقصد إزالة الشيفرة المتقادمة وإعادة تشكيلها للعقد القادم.
9.2. السمات المعمارية
لدى مجموعات الأدوات البرمجية علاقة تكاملية شديدة التآزر مع مجتمعاتها؛ إذ يشكّل كلٌّ منهما الآخر ضمن دورة تكرارية متواصلة. يُعدَّل البرنامج باستمرار حتى يلبّي احتياجات المجتمع، فيما تتكيّف سلوكيات المجتمع بدورها استنادًا إلى ما يسمح به البرنامج من صلاحيات أو ما يفرضه من قيود. ولأنّ فهم طبيعة معمارية ITK على نحو أفضل يقتضي فهم السياق، فإنّ من المفيد أن نكوّن فكرة واضحة عن نوع المشكلات التي يعالجها مجتمع ITK عادةً، وعن الطريقة التي يميل إلى حلّها بها.
طبيعة الوحش
إن لم تفهم طبيعة الوحوش، فلن ينفعك كثيرًا أن تعرف ميكانيكا تشريحها. Dee Hock، One from Many: VISA and the Rise of Chaordic Organization
في مسألة تحليل صور نموذجية، يأخذ باحث أو مهندس صورةً مدخلة، ويحسّن بعض خصائصها، كأن نقول بتقليل الضجيج أو زيادة التباين، ثم ينتقل إلى التعرّف على بعض السمات في الصورة، مثل الزوايا والحوافّ البالغة الوضوح. وهذا النوع من المعالجة يناسب بطبيعته معمارية خط أنابيب البيانات (data pipeline)، كما يبيّن الشكل 9.1.
الشكل 9.1: خط أنابيب معالجة الصور
ولتوضيح هذه النقطة، يعرض الشكل 9.2 صورةً لمخّ من صورة فُصلت بالتصوير بالرنين المغناطيسي (MRI)، ونتيجة معالجتها بمرشّح الوسيط (median filter) لخفض مستوى الضجيج فيها، وكذلك ناتج مرشّح لكشف الحوافّ استُخدم لتحديد حدود البنى التشريحية.
الشكل 9.2: صورة مخّ بالرنين المغناطيسي، مرشّح الوسيط، مرشّح كشف الحوافّ
طوّر مجتمع تحليل الصور، لكل واحدة من هذه المهام، مجموعةً متنوعة من الخوارزميات، ويستمرّ في تطوير خوارزميات جديدة. وقد تسأل: ولماذا يواصلون ذلك؟ والجواب أنّ معالجة الصور مزيج من العلم والهندسة والفن ومهارات «الطبخ». فادّعاء وجود مزيج خوارزمي يمثّل الإجابة «الصحيحة» لمسألة معالجة صور مضلِّل بقدر ما هو ادّعاء وجود نوع «صحيح» من حلوى الشوكولاتة يُقدَّم على العشاء. وبدلًا من السعي وراء الكمال، يسعى المجتمع إلى إنتاج مجموعة غنية من الأدوات تضمن ألّا تنفد الخيارات عند مواجهة تحدٍّ في معالجة الصور. ولهذا الحال ثمنٌ بطبيعة الحال. وتتمثّل التكلفة في أنّ محلّل الصور يضطلع بمهمة صعبة تتمثّل في الاختيار من بين عشرات الأدوات المختلفة التي يمكن تركيبها بصور متعدّدة لتحقيق نتائج متشابهة.
مجتمع تحليل الصور متكاملٌ عن قرب مع مجتمع البحث. ومن الشائع أن نرى مجموعات بحثية بعينها تتعلّق بعائلات الخوارزميات التي طوّرتها. ويؤدي هذا العرف في «العلامات التجارية»، وحتى «التسويق» إلى حدٍّ ما، إلى وضعٍ يصبح فيه أفضل ما يمكن لمجموعة الأدوات أن تفعله للمجتمع هو تقديم مجموعةٍ شديدة الاكتمال من التطبيقات الخوارزمية يجرّبونها، ثمّ يخلطون بينها ويوازنونها لتكوين «وصفة» تلبّي احتياجاتهم.
هذه بعض الأسباب التي دفعت إلى تصميم ITK وتنفيذها بوصفها مجموعةً كبيرة من الأدوات المستقلة نسبيًا والمتسقة فيما بينها، وهي مرشّحات الصور، وكثير منها يصلح لحلّ مسائل متشابهة. وفي هذا السياق فإنّ قدرًا من «التكرار»—مثل تقديم ثلاث تنفيذات مختلفة للمرشّح الغاوسي (Gaussian)—لا يُعدّ مشكلة بل يُعدّ ميزةً قيّمة، لأنّ التنفيذات المختلفة يمكن استعمالها بالتبادل لتلبية القيود واغتناز الكفاءات المتعلقة بحجم الصورة وعدد المعالجات وحجم النواة الغاوسية، والتي قد تكون خاصة بتطبيق تصوير معيّن.
وُصِّفت مجموعة الأدوات أيضًا بوصفها موردًا ينمو ويجدّد نفسه باستمرار مع توافر خوارزميات جديدة وتنفيذات أفضل تحلّ محلّ القائمة، ومع تطوير أدوات جديدة استجابةً للاحتياجات الناشئة عن تقنيات التصوير الطبي الجديدة.
وبفضل هذه اللمحة السريعة في يوميات محلّلي الصور داخل مجتمع ITK، يمكننا الآن الغوص في السمات الرئيسية للمعمارية:
- التعدّدية
- خط أنابيب البيانات
- المصانع
- مصانع الإدخال/الإخراج
- البثّ التدفقي
- إعادة الاستخدام
- قابلية الصيانة
التعدّدية
التعدّدية إحدى السمات الرئيسية لـ ITK. وهي متطلّب ينشأ عن الطريقة التي يعمل بها أفراد مجتمع تحليل الصور عند حلّ مشكلاتهم. فمعظم مشكلات تحليل الصور تمرّر صورةً مدخلة واحدة أو أكثر عبر مجموعة من مرشّحات المعالجة التي تعزّز معلومات معيّنة في الصور أو تستخلصها. وعليه لا توجد كائن معالجة كبير مفرد، عددٌ هائل من الكائنات الصغيرة. وطبيعة معالجة الصور البنيوية هذه تستلزم منطقيًا تنفيذ البرمجيات بوصفها مجموعةً كبيرة من مرشّحات معالجة الصور التي يمكن تركيبها بطرق متعدّدة.
كما أنّ بعض أنواع مرشّحات المعالجة تتجمّع في عائلات، داخلها يمكن تلخيص بعض سمات تنفيذها. ويؤدي ذلك إلى تجميع طبيعي لمرشّحات الصور في وحدات ومجموعات من الوحدات.
وعليه تقع التعدّدية في ITK على ثلاثة مستويات طبيعية:
- مستوى المرشّح
- مستوى عائلة المرشّحات
- مستوى مجموعة عائلات المرشّحات
على مستوى مرشّح الصورة، تضمّ ITK مجموعةً من نحو 700 مرشّح. وبما أنّ ITK مُنفَّذة بلغة C++، فإنّ هذا هو المستوى الطبيعي الذي يُنفَّذ عنده كل واحد من تلك المرشّحات بصنف C++ يتبع أنماط التصميم الكائني. وعلى مستوى عائلة المرشّحات، تُجمِّع ITK المرشّحات معًا بحسب طبيعة المعالجة التي يؤدّيها كلٌّ منها. فمثلًا، تُجمع كل المرشّحات المرتبطة بتحويلات فورييه (Fourier transforms) في وحدة واحدة (Module). وعلى مستوى C++، تقابل الوحداتُ الأدلةَ (directories) في شجرة المصدر، والمكتباتَ بعد أن يُصرَّف البرنامج إلى صيغته الثنائية. ويمتلك ITK نحو 120 من هذه الوحدات. وتحتوي كل وحدة على:
- شيفرة مصدر مرشّحات الصور التي تنتمي إلى تلك العائلة.
- مجموعة ملفات إعداد تصف كيفية بناء الوحدة وتسرد الاعتماديات بين هذه الوحدة والوحدات الأخرى.
- مجموعة اختبارات الوحدات المقابلة لكل مرشّح منها.
الشكل 9.3: البنية الهرمية للمجموعات والوحدات والأصناف
أمّا مستوى المجموعة، فيتمثّل في التقسيم التصوّري الذي رُسم فوق البرمجيات للمساعدة في تحديد مواقع المرشّحات ضمن شجرة المصدر. وترتبط المجموعات بمفاهيم عالية المستوى مثل الترشيح (Filtering) والتقطيع (Segmentation) والتسجيل (Registration) والإدخال/الإخراج (IO). ويوضّح الشكل 9.3 هذه البنية الهرمية. ويضمّ ITK حاليًا 124 وحدة، تُجمَّع بدورها في 13 مجموعة رئيسية. وتمتلك الوحدات مجموعةً متنوعة من الأحجام المختلفة. ويعرض الشكل 9.4 توزيع هذه الأحجام بالبايت.
الشكل 9.4: توزيع أحجام أكبر 50 وحدة في ITK بالكيلوبايت
ينطبق التسييح إلى وحدات في ITK أيضًا على مجموعة من المكتبات الخارجية (third-party) التي ليست جزءًا مباشرًا من مجموعة الأدوات، لكنها تعتمد عليها، وتوزَّع إلى جانب بقية الشيفرة لإتاحة الراحة للمستخدمين. ومن الأمثلة البارزة على هذه المكتبات الخارجية مكتبات صيغ ملفات الصور: HDF5 وPNG وTIFF وJPEG وOpenJPEG وغيرها. وقد أُبرِزت المكتبات الخارجية هنا لأنها تمثّل نحو 56 بالمئة من حجم ITK. وهذا يعكس الطابع المعتاد لتطبيقات المصادر المفتوحة التي تُبنى على منصّات قائمة. ولا يعكس توزيع أحجام المكتبات الخارجية بالضرورة التنظيم المعماري لـ ITK، لأنّنا اعتمدنا هذه المكتبات النافعة كما طوّرها أصحابها الأصليون. ومع ذلك، فإنّ شيفرة الجهات الخارجية تُوزَّع إلى جانب مجموعة الأدوات، وكان تقسيمها إلى وحدات أحد التوجيهات الرئيسية الحاكمة لعملية التسييح إلى وحدات.
يُعرض هنا توزيع أحجام الوحدات لأنه مقياسٌ لصحّة تسييح الشيفرة إلى وحدات. ويمكن النظر إلى تسييح الشيفرة بوصفه طيفًا متصلًا يمتدّ من طرفيه، وهما تجميع كل الشيفرة في وحدة واحدة أي النسخة الأحادية (monolithic)، وتقسيم الشيفرة إلى مجموعة ضخمة جدًا من الوحدات متساوية الحجم. واستُخدم توزيع الأحجام هذا أداةً لرصد سيرورة عملية التسييح، وبخاصةً لضمان ألّا تبقى كتلٌ كبيرة من الشيفرة في الوحدة الواحدة ما لم تكن هناك اعتماديات منطقية حقيقية تستدعي هذا التجميع.
تتيح المعمارية المعيارية لـ ITK وتُسهّل:
- تقليل الاعتماديات المتبادلة وتوضيحها
- ضمّ الشيفرة التي يساهم بها المجتمع
- تقييم مقاييس الجودة لكل وحدة (مثل تغطية الشيفرة)
- بناء مقتطفات مختارة من مجموعة الأدوات
- تحزيم مقتطفات مختارة من مجموعة الأدوات لإعادة توزيعها
- نموّ مستمر عبر الإضافة التدريجية لوحدات جديدة
جعلت عملية التسييح إلى وحدات ممكنةً التعرّفَ صراحةً على الاعتماديات بين الأجزاء المختلفة من مجموعة الأدوات وإعلانَها عند وضع كلٍّ منها في وحدة. وفي حالات كثيرة كشف هذا الجهد عن اعتماديات مصطنعة أو خاطئة أُدخلت في مجموعة الأدوات عبر الزمن، ومرّت دون أن تُلاحَظ حين كان معظم الشيفرة مجمَّعة في بضع مجموعات كبيرة.
لفائدة تقييم مقاييس الجودة لكل وحدة جانبان. أولًا، إنّه يجعل من الأسهل مساءلة المطوّرين عن الوحدات التي يتولّون صيانتها. وثانيًا، إنّه يتيح تنفيذ مبادرات تنظيف يركّز فيها عدد قليل من المطوّرين لفترات وجيزة على رفع جودة وحدة بعينها. فحين يتركّز الاهتمام على جزء صغير من مجموعة الأدوات، يصبح أثر الجهد أسهل في الرؤية، ويظلّ المطوّرون مشغولين ومتحمّسين.
ولإعادة التأكيد، نلاحظ أنّ بنية مجموعة الأدوات تعكس تنظيم المجتمع، وفي بعض الحالات تعكس العمليات التي اعتُمدت للنموّ المستمر في البرمجيات وضبط جودتها.
خط أنابيب البيانات
أدى الطابع المرحلي لمعظم مهام تحليل الصور طبيعيًا إلى اختيار معمارية خط أنابيب البيانات بوصفه البنية التحتية الأساسية لمعالجة البيانات. ويتيح خط أنابيب البيانات:
- تسلسل المرشّحات (Filter Concatenation): يمكن تسلسل مجموعة من مرشّحات الصور واحدًا تلو الآخر، لتُكوّن سلسلة معالجة تُطبَّق فيها سلسلة عمليات على الصور المدخلة.
- استكشاف المعاملات (Parameter Exploration): بمجرد تجميع سلسلة المعالجة، يصبح من السهل تغيير معاملات أي مرشّح في السلسلة، واستكشاف الآثار التي سيُحدثها هذا التغيير في صورة المخرج النهائية.
- البثّ من الذاكرة (Memory Streaming): يمكن التعامل مع الصور الكبيرة بمعالجة الكتل الفرعية من الصورة واحدةً في كل مرة. وبهذه الطريقة يصبح بالإمكان معالجة صور كبيرة كانت لن تتّسع في الذاكرة الرئيسية لولا ذلك.
عرض كلٌّ من الشكل 9.1 والشكل 9.2 تمثيلًا مبسّطًا لخط أنابيب البيانات من وجهة نظر معالجة الصور. تمتلك مرشّحات الصور عادةً معاملات عددية تُستعمل لضبط سلوك المرشّح. وفي كلّما مُرِّر تعديل على أحد المعاملات العددية، يضع خط أنابيب البيانات ناتجه «مُتّسخًا» (dirty) ويعلم أنّ هذا المرشّح بعينه، وكل ما يأتي بعده في السلسلة ويستهلك ناتجه، ينبغي إعادة تنفيذه. وتُسهّل هذه الخاصية في خط الأنابيب استكشاف فضاء المعاملات مع استعمال أقل قدر ممكن من القدرة المعالجة في كل نسخة من التجربة.
يمكن توجيه عملية تحديث خط الأنابيب بحيث لا تُعالَج سوى أجزاء فرعية من الصور في كل مرة. وهذه آلية لا بدّ منها لدعم وظيفة البثّ التدفقي. عمليًا، تُضبط العملية عبر تمرير مواصفة RequestedRegion داخليًا من مرشّح في اتجاه المجرى إلى مرشّح المزوّد المقابل له في اتجاه المصدر. ويتمّ هذا التواصل عبر واجهة برمجية داخلية (API) لا تُعرَض لمطوّري التطبيقات.
ولإعطاء مثال أكثر تحديدًا، إذا كان مرشّح تمويه غاوسي (Gaussian blur) يتوقّع أن يستخدم كمدخل صورةً بمقاس 100×100 بكسل يُنتجها مرشّح وسيط (median)، فيمكن للمرشّح التمويه أن يطلب من المرشّح الوسيط أن ينتج رُبع الصورة فقط، أي منطقة صورة بمقاس 100×25 بكسل. ويمكن أن يُنقل هذا الطلب إلى أعلى سلسلة المرشّحات نحو مرشّحات المصدر، على أن من الواجب أن يضيف كل مرشّح وسيط حدًّا إضافيًّا إلى مقاس منطقة الصورة ليتمكّن من إنتاج مقاس منطقة المخرج المطلوبة. وهناك المزيد عن البثّ التدفقي للبيانات لاحقًا.
سواء كان التغيير في معاملات مرشّح بعينه، أو في المنطقة المطلوبة تحديدًا التي يعالجها ذلك المرشّح، فإنّ الأثر هو وسم خط الأنابيب بأنّه «مُتّسخ» والإشارة إلى الحاجة إلى إعادة تنفيذ ذلك المرشّح عبر مرشّحات المجرى في خط الأنابيب.
كائنات المعالجة والبيانات
صُمِّم نوعان رئيسيان من الكائنات ليحملا البنية الأساسية لخط الأنابيب، وهما DataObject وProcessObject. ويمثّل DataObject تجريدًا (abstraction) لأصناف تحمل البيانات، مثل الصور والشبكات الهندسية. ويوفّر ProcessObject تجريدًا لمرشّحات الصور ومرشّحات الشبكات التي تعالج تلك البيانات. وتأخذ كائنات ProcessObject كائنات DataObject كمدخلات، وتطبّق عليها نوعًا ما من التحويل الخوارزمي، مثل التحويلات المبيّنة في الشكل 9.2.
تُنتَج كائنات DataObject بواسطة كائنات ProcessObject. وتبدأ هذه السلسلة عادةً بقراءة كائن DataObject من القرص، مثلًا باستخدام ImageFileReader وهو نوع من ProcessObject. وProcessObject الذي أنشأ كائن DataObject بعينه هو الوحيد الذي ينبغي أن يعدّل ذلك DataObject. وعادةً ما يُوصَل ناتج DataObject هذا كمدخل إلى ProcessObject آخر في اتجاه المجرى ضمن خط الأنابيب.
الشكل 9.5: العلاقة بين كائنات ProcessObject وكائنات DataObject
يوضّح الشكل 9.5 هذه التسلسلية. وقد يُمرَّر كائن DataObject نفسه كمدخل إلى عدّة كائنات ProcessObject، كما يبيّن الشكل، حيث ينتج قارئ الملفات في بداية خط الأنابيب كائن DataObject. وفي هذه الحالة بعينها يكون قارئ الملفات مثالًا (instance) من الصنف ImageFileReader، ويكون كائن DataObject الذي يُنتجه كمخرج مثالًا من الصنف Image. ومن الشائع أيضًا أن تطلب بعض المرشّحات كائنَي DataObject كمدخلين، كما في حالة مرشّح الطرح المشار إليه في يمين الشكل نفسه.
تُوصَل كائنات ProcessObject وDataObject ببعضها نتيجةً جانبية لبناء خط الأنابيب. ومن وجهة نظر مطوّر التطبيق، يُربط خط الأنابيب عبر تسلسل استدعاءات تتضمّن كائنات ProcessObject، مثل:
writer->SetInput( canny->GetOutput() );
canny->SetInput( median->GetOutput() );
median->SetInput( reader->GetOutput() );
لكنّ داخليًا لا يكون ما يُوصَل نتيجةً لهذه الاستدعاءات هو كائن ProcessObject إلى ProcessObject تالٍ، بل ProcessObject آخر في اتجاه المجرى إلى كائن DataObject الذي يُنتجه كائن ProcessObject في اتجاه المصدر.
تُمسك البنية المتسلسلة الداخلية لخط الأنابيب ثلاثة أنواع من الوصلات:
- يحتفظ كائن
ProcessObjectبقائمة من المؤشّرات إلى كائناتDataObjectالتي يُنتجها. أمّا كائناتDataObjectالناتجة فيملكهاProcessObjectالذي أنتجها ويتحكّم فيها. - يحتفظ كائن
ProcessObjectبقائمة من المؤشّرات إلى كائناتDataObjectالتي يتلقّاها كمدخلات. أمّا كائناتDataObjectالمدخلة فيملكها كائنProcessObjectفي اتجاه المصدر. - يحتفظ كائن
DataObjectبمؤشّر إلى كائنProcessObjectالذي أنتجه. ويكون ذلك بدوره هوProcessObjectالذي يملك هذاDataObjectويتحكّم فيه أيضًا.
تُستثمر مجموعة روابط الداخلية هذه لاحقًا لنقل الاستدعاءات إلى أعلى وإلى أسفل في خط الأنابيب. وخلال جميع هذه التفاعلات يحتفظ كائن ProcessObject بالسيطرة على كائن DataObject الذي يُنتجه وملكيته. وتكتسب المرشّحات في اتجاه المجرى القدرة على الوصول إلى المعلومات الخاصة بكائن DataObject بعينه عبر روابط المؤشّرات التي أُنشئت نتيجةً للاستدعاءات على الطريقتين SetInput() وGetOutput()، من دون أن تتسلّط يومًا على تلك البيانات المدخلة. ولأغراض عملية، ينبغي أن تتعامل المرشّحات مع بيانات مدخلها بوصفها كائنات للقراءة فقط. وهذا مُلزَم في الواجهة البرمجية باستخدام الكلمة المفتاحية const في C++ ضمن وسائط طُرق SetInput(). وقاعدةً عامّة، تبنّى ITK واجهةً برمجية خارجية صحيحة الثبات (const-correct)، وإنّ هذا الثبات يُتجاوَز داخليًا بفعل بعض عمليات خط الأنابيب.
التسلسل الهرمي لأصناف خط الأنابيب
الشكل 9.6: التسلسل الهرمي لكائنات ProcessObject وكائنات DataObject
استُمدّ التصميم والتنفيذ الأوّليان لخط أنابيب البيانات في ITK من مجموعة أدوات التصوّر (Visualization Toolkit، VTK)، وهي مشروع ناضج في الوقت الذي انطلق فيه تطوير ITK. (انظر معمارية تطبيقات المصادر المفتوحة، المجلد الأول.)
يبيّن الشكل 9.6 التسلسل الهرمي الكائني لكائنات خط الأنابيب في ITK. ولاحظ خصوصًا العلاقة بين الأصناف الأساسية Object وProcessObject وDataObject وبعض الأصناف في عائلة المرشّحات وعائلة البيانات. في هذا التجريد، ينبغي أن يرث أي كائن يُتوقَّع تمريره كمدخل إلى مرشّح، أو أن يُنتَج كمخرج من مرشّح، الصنف DataObject. ويُتوقَّع من جميع المرشّحات التي تُنتج البيانات وتستهلكها أن ترث ProcessObject. أمّا التفاوض على البيانات اللازم لنقلها عبر خط الأنابيب، فيُنفَّذ جزءًا منه في ProcessObject وجزءًا منه في DataObject.
تقع الأصناف LightObject وObject فوق التفرّع الثنائي بين ProcessObject وDataObject. وتوفّر الأصناف LightObject وObject وظائف مشتركة مثل الواجهة البرمجية لتبادل Events، ودعم تعدّد الخيوط (multi-threading).
الآليات الداخلية لخط الأنابيب
يعرض الشكل 9.7 مخطّط تسلسل UML يصف التفاعلات بين كائنات ProcessObject وكائنات DataObject في خط أنابيب مصغّر مكوَّن من ImageFileReader وMedianImageFilter وImageFileWriter.
يتكوّن التفاعل الكامل من أربع تمريرات (passes):
- تحديث معلومات المخرج (تسلسل استدعاءات في اتجاه المصدر)
- تحديث المنطقة المطلوبة (تسلسل استدعاءات في اتجاه المصدر)
- تحديث بيانات المخرج (تسلسل استدعاءات في اتجاه المصدر)
- توليد البيانات (تسلسل استدعاءات في اتجاه المجرى)
تُطلق العمليةُ كلها عندما يستدعي تطبيقٌ الطريقة Update() في آخر مرشّح ضمن خط الأنابيب، وفي هذا المثال المحدّد يكون ذلك ImageFileWriter. ويبدأ الاستدعاء Update() التمريرة الأولى التي تسير في اتجاه المصدر، أي من آخر مرشّح في خط الأنابيب نحو أول مرشّح فيه.
الهدف من هذه التمريرة الأولى طرح السؤال: «كم من البيانات يمكنك أن تولّد لي؟» وتُرمَّز هذه الصياغة في الطريقة UpdateOutputInformation(). وفي هذه الطريقة، يحسب كل مرشّح مقدار بيانات الصورة الذي يمكن إنتاجه كمخرج بالمقدار المتاح له من البيانات كمدخل. ولأنّ مقدار بيانات الإدخال يجب أن يُعرَف أولًا قبل أن يستطيع المرشّح الإجابة عن سؤال مقدار بيانات الإخراج، فلا بدّ من أن ينتشر السؤال إلى المرشّح في اتجاه المصدر حتى يبلغ مرشّح مصدر يستطيع الإجابة عن السؤال الأول وحده. وفي هذا المثال المحدّد يكون مرشّح المصدر هو ImageFileReader. فهذا المرشّح يستطيع تحديد حجم ناتجه عبر جمع معلومات من ملف الصورة الذي كُلِف بقراءته. وبمجرد أن يُجيب مرشّح خط الأنابيب الأول عن السؤال، تستطيع المرشّحات التالية في اتجاه المجرى أن تحسب كلٌّ منها مقدار ناتجه تباعًا، حتى يبلغ ذلك آخر مرشّح في خط الأنابيب.
أمّا التمريرة الثانية، التي تسير هي الأخرى في اتجاه المصدر، فتُعلم المرشّحات بمقدار المخرج المطلوب من كلٍّ منها خلال تنفيذ خط الأنابيب. ومفهوم المنطقة المطلوبة (Requested Region) جوهري في دعم قدرات البثّ التدفقي في ITK؛ فهو يتيح إخبار المرشّحات في خط الأنابيب بألّا تولّد الصورة بأكملها، وأن تركّز بدلًا من ذلك على منطقة فرعية منها هي المنطقة المطلوبة. وهذا مفيد جدًا حين تكون الصورة المعنيّة أكبر من الذاكرة العشوائية (RAM) المتاحة في النظام. وينتشر الاستدعاء من آخر مرشّح إلى أوله، وعند كل مرشّح وسيط يُعدَّل مقاس المنطقة المطلوبة بحيث يراعي أي حدود إضافية قد يحتاجها المرشّح في المدخل ليتمكّن من إنتاج مقاس منطقة معيّن كمخرج. وفي مثالنا المحدّد، سيضطر مرشّح الوسيط عادةً إلى إضافة حدّ عرضه 2 بكسل إلى مقاس مدخله الخاص. أي إذا طلب الكاتب من مرشّح الوسيط منطقة بمقاس 500 × 500 بكسل، فإنّ مرشّح الوسيط سيطلب بدوره من القارئ منطقة بمقاس 502 × 502 بكسل، لأنّ مرشّح الوسيط يحتاج افتراضيًا إلى منطقة جوار بمقاس 3 × 3 بكسل لحساب قيمة بكسل خرج واحد. وتُرمَّز هذه التمريرة في الطريقة PropagateRequestedRegion().
تهدف التمريرة الثالثة إلى إطلاق الحساب على البيانات الواقعة داخل المنطقة المطلوبة. وتسير هذه التمريرة أيضًا في اتجاه المصدر، وتُرمَّز في الطريقة UpdateOutputData(). ولأنّ كل مرشّح يحتاج إلى بيانات مدخله قبل أن يحسب بيانات ناتجه، فإنّ الاستدعاء يُمرَّر أولًا إلى مرشّح المصدر المقابل، ومن هنا يأتي انتشاره في اتجاه المصدر. وبعد عودة الاستدعاء، بالشروع فعليًا في حساب بياناته.
أمّا التمريرة الرابعة والأخيرة، فتسير في اتجاه المجرى، وتتمثّل في التنفيذ الفعلي للحساب من قِبل كل مرشّح. وتُرمَّز في الطريقة GenerateData(). وإنّ اتجاه المجرى ليس نتيجةً لأنّ مرشّحًا ما يُجري استدعاءات على شريكه في اتجاه المجرى، بل لأنّ استدعاءات UpdateOutputData() نُفِّذت بالترتيب من المرشّح الأول إلى المرشّح الأخير. أي أنّ التسلسل يجري في اتجاه المجرى بسبب توقيت الاستدعاءات، لا بسبب المرشّح الذي يقود الاستدعاءات. وهذا التوضيح مهمّ لأنّ خط أنابيب ITK بطبيعته خط أنابيب سحّاب (Pull Pipeline)، تُطلب فيه البيانات من النهاية، وتُضبط المنطق أيضًا من النهاية.
المصانع
أحد متطلّبات التصميم الجوهرية في ITK هو توفير الدعم لمنصّات متعدّدة. وينشأ هذا المتطلّب عن الرغبة في تعظيم أثر مجموعة الأدوات بجعلها قابلة للاستخدام من قبل مجتمع واسع بغضّ النظر عن المنصّة التي يختارها. وقد تبنّت ITK نمط التصميم المصنع (Factory) لمعالجة تحدّي دعم الفروق الجوهرية بين منصّات العتاد والبرمجيات العديدة، من دون أن تضحّي بملاءمة الحلّ لكل منصّة من تلك المنصّات على حِدة.
يستخدم نمط المصنع في ITK أسماء الأصناف مفاتيحًا لسجلٍّ من بانِي الأصناف (class constructors). ويجري تسجيل المصانع وقت التشغيل (run time)، ويمكن ذلك ببساطة بوضع مكتبات ديناميكية (dynamic libraries) في مجلّدات محدّدة تبحث فيها تطبيقات ITK عند بدء التشغيل. وتوفّر هذه الخاصية الأخيرة آليةً طبيعية لتنفيذ معمارية إضافات (plugin architecture) بصورة نظيفة وشفّافة. والنتيجة هي تيسير تطوير تطبيقات تحليل صور قابلة للتوسيع، تلبّي الحاجة إلى تقديم مجموعة قدرات تحليل صور لا تتوقّف عن النمو.
مصانع الإدخال/الإخراج
وتكتسب آلية المصانع أهمية خاصة عند تنفيذ الإدخال/الإخراج (IO).
احتضان التنوّع عبر الواجهات
طوّر مجتمع تحليل الصور مجموعةً ضخمة جدًا من صيغ الملفات لتخزين بيانات الصور. وقد صُمِّم كثيرٌ من هذه الصيغ ونُفِّذ بغرضات محدّدة، ولذلك هو مضبوط بدقة لأنواع صور بعينها. ونتيجةً لذلك، تُبتكر صيغ ملفات صور جديدة وتُروَّج لها عبر المجتمع على أساس منتظم. وإدراكًا لهذه الظاهرة، صمّم فريق تطوير ITK معمارية إدخال/إخراج ملائمة لسهولة التوسّع، بحيث يسهل إضافة دعم لعدد أكبر وأكبر من صيغ الملفات على نحو منتظم.
الشكل 9.8: اعتماديات مصانع الإدخال/الإخراج
بُنيت معمارية الإدخال/الإخراج القابلة للتوسّع هذه على آلية المصنع الموصوفة في القسم السابق. والفارق الرئيسي أنّه في حالة الإدخال/الإخراج، تُسجَّل مصانع الإدخال/الإخراج في سجلّ متخصّص تديره الصنف الأساس ImageIOFactory، المبيّن في الركن العلوي الأيسر من الشكل 9.8. أمّا الوظيفة الفعلية لقراءة البيانات من صيغ ملفات الصور وكتابتها فهي مُنفَّذة في مجموعة من أصناف ImageIO، المبيّنة في يمين الشكل 9.8. ومن المقصود أن تُنشَأ أصناف الخدمة هذه عند الطلب حين يطلب المستخدم قراءة صورة أو كتابتها. ولا تُعرَض أصناف الخدمة هذه لشيفرة التطبيق. بل يُتوقَّع من التطبيقات بدلًا من ذلك أن تتفاعل مع أصناف الواجهة (facade):
ImageFileReaderImageFileWriter
وهذان الصنفان هما ما يستدعي التطبيق عبره شيفرةً مثل:
reader->SetFileName("../image1.png");
reader->Update();
أو:
writer->SetFileName("../image2.jpg");
writer->Update();
في كلتا الحالتين، يُطلق الاستدعاء Update() تنفيذ خط الأنابيب في اتجاه المصدر الذي تكون متصلًا به كائنات ProcessObject هذه. ويتصرّف كلٌّ من القارئ والكاتب كمرشّح إضافي ضمن خط الأنابيب. وفي حالة القارئ بعينها، يُطلق الاستدعاء Update() قراءة ملف الصورة المقابل إلى الذاكرة. أمّا في حالة الكاتب، فيُطلق الاستدعاء Update() تنفيذ خط الأنابيب في اتجاه المصدر الذي يوفّر المدخل للكاتب، وينتهي ذلك في النهاية بكتابة صورة إلى القرص ضمن صيغة ملف بعينها.
تُخفي أصناف الواجهة هذه عن مطوّر التطبيق الفروق الداخلية الكامنة في خصوصيات كل صيغة ملف. بل إنّها تخفي حتى وجود صيغة الملف نفسها. وقد صُمِّمت الواجهات بحيث لا يحتاج مطوّرو التطبيقات في معظم الأحيان إلى معرفة ما هي صيغ الملفات التي يُتوقَّع أن يقرأها التطبيق. كل ما سيقوم به التطبيق المعتاد هو استدعاء شيفرةٍ مثل
std::string filename = this->GetFileNameFromGUI();
writer->SetFileName( filename );
writer->Update();
وستعمل هذه الاستدعاءات على ما يرام مهما كان محتوى المتغيّر filename أحد السلاسل التالية:
- image1.png
- image1.jpeg
- image1.tiff
- image1.dcm
- image1.mha
- image1.nii
- image1.nii.gz
حيث تُحدِّد امتدادات أسماء الملفات في كل حالة صيغة ملف صورة مختلفة.
اعرف نوع البكسل الخاص بك
على الرغم من المساعدة التي تقدّمها واجهتا القارئ والكاتب، فإنّ مسؤولية مطوّر التطبيق تبقى قائمة في أن يكون على عِلم بنوع البكسل الذي يحتاج التطبيق إلى معالجته. وفي سياق التصوير الطبي، من المعقول توقّع أن يعرف مطوّر التطبيق ما إذا كانت الصورة المدخلة ستحتوي على صورة بالرنين المغناطيسي (MRI) أو صورة ثدي (mammogram) أو مسح بالحوسبة المقطعية (CT scan)، وبالتالي ينتبه إلى اختيار نوع البكسل وأبعاد الصورة المناسبين لكلٍّ من هذه الوسائط التصويرية المختلفة. وقد لا يكون هذا التخصيص لنوع الصورة مريحًا في سياقات التطبيق التي يريد فيها المستخدمون قراءة أي نوع من الصور، وهي أكثر ما تظهر في سيناريوهات النمذجة السريعة (rapid prototyping) والتعليم. وفي سياق نشر تطبيق لصور طبية في بيئة سريرية، من المتوقّع على العكس أن يكون نوع البكسل وأبعاد الصور محدَّدين بوضوح ومُصرَّح بهما استنادًا إلى الوسيط التصويري المطلوب معالجته. وإليك مثالًا محدّدًا لتطبيق يدير مسوحات بالرنين المغناطيسي ثلاثية الأبعاد:
typedef itk::Image< signed short, 3 > MRImageType;
typedef itk::ImageFileWriter< MRImageType > MRIWriterType;
MRIWriterType::Pointer writer = MRIWriterType::New();
writer->Update();
غير أنّ هناك حدًّا لمدى إمكان إخفاء خصوصيات صيغ ملفات الصور عن مطوّر التطبيق. فمثلًا، عند قراءة صور من ملفات DICOM، أو عند قراءة صور RAW، قد يجد مطوّر التطبيق نفسه مضطرًّا إلى إدراج استدعاءات إضافية لتحديد خصائص صيغة الملف المعنيّة أكثر. وستكون ملفات DICOM هي الأكثر شيوعًا في البيئات السريرية، بينما تظلّ صور RAW شرًّا لازمًا لتبادل البيانات في بيئة البحث.
معًا لكن منفصلين
ينعكس طابع الاكتفاء الذاتي لكل مصنع إدخال/إخراج وكل صنف خدمة ImageIO أيضًا في التسييح إلى وحدات. فعادةً يعتمد صنف ImageIO على مكتبة متخصّصة مخصّصة لإدارة صيغة ملف بعينها، كما هي الحال مع PNG وJPEG وTIFF وDICOM مثلًا. وفي تلك الحالات، تُدار المكتبة الخارجية بوصفها وحدة مكتفية ذاتيًا، كما يوضع شيفرة ImageIO المتخصّصة التي تربط ITK بتلك المكتبة الخارجية في وحدة مستقلة. وبهذه الطريقة، يتيسّر للتطبيقات المعيّنة تعطيلُ صيغ ملفات كثيرة لا صلة لها بمجالها، والتركيزَ على تقديم صيغ الملفات المفيدة فقط لسيناريوهات التطبيق المتوقَّعة.
ومثل المصانع القياسية تمامًا، يمكن تحميل مصانع الإدخال/الإخراج وقت التشغيل من مكتبات ديناميكية. وييسّر هذا المرونةَ استعمال صيغ ملفات متخصّصة أو مطوّرة داخليًا دون اشتراط دمج كل تلك الصيغ مباشرةً في مجموعة أدوات ITK نفسها. وقد كانت مصانع الإدخال/الإخراج القابلة للتحميل إحدى أنجح الخصائص في التصميم المعماري لـ ITK؛ إذ جعلت من الممكن معالجة وضعٍ صعب بسهولة دون تحميل عبءٍ على الشيفرة أو تعتيم تنفيذها. وفي الآونة الأخيرة، تكيّفت معمارية الإدخال/الإخراج نفسها لإدارة عملية قراءة الملفات المحتوية على تحويلات مكانية (spatial transformations) ممثَّلة بعائلة الأصناف Transform وكتابةها.
البثّ التدفقي
صُوِّرت ITK في الأصل بوصفها مجموعة أدوات لمعالجة الصور التي يوفّرها مشروع الإنسان المرئي (Visible Human Project).في ذلك الحين كان واضحًا أنّ مجموعة بيانات بهذا الحجم لن تتّسع في الذاكرة العشوائية للحاسوب المتوفّرة عادةً لمجتمع البحث في التصوير الطبي. ولا يزال الأمر كذلك؛ إذ لن تتّسع مجموعة البيانات تلك في حواسيب سطح المكتب المعتادة التي نستعملها اليوم. ومن ثمّ كان أحد متطلّبات تطوير Insight Toolkit هو تمكين بثّ بيانات الصور عبر خط أنابيب البيانات. وبأدقّ العبارات، التمكّن من معالجة الصور الكبيرة عبر دفع الكتل الفرعية من الصورة عبر خط أنابيب البيانات، ثم تجميع الكتل الناتجة في جهة المخرج من خط الأنابيب.
الشكل 9.9: توضيح عملية البثّ التدفقي للصور
يوضّح الشكل 9.9 هذا التقسيم لمجال الصورة في مثال محدّد هو مرشّح الوسيط. يحسب مرشّح الوسيط قيمة بكسل خرج واحد بوصفها الوسيط الإحصائي لقيم البكسلات في الصورة المدخلة ضمن جوار حول ذلك البكسل. ومقاس ذلك الجوار معاملٌ عددي للمرشّح. وفي حالتنا هذه نضبطه على 2 بكسل، أي أنّنا سنأخذ جوارًا نصف قطره 2 بكسل حول بكسل خرجنا. ويؤدي ذلك إلى جوار بمقاس 5×5 بكسل يقع بكسل الخرج في وسطه، مع حدّ مستطيل عرضه 2 بكسل من حوله. ويشار إلى هذا عادةً بـ«نصف قطر مانهاتن». وحين يُطلب من مرشّح الوسيط حساب منطقة معيّنة مطلوبة من صورة الخرج، فإنّه ينعكس ويعود إلى مرشّحه في اتجاه المصدر ليطلب منطقة أكبر تشكّل المنطقة المطلوبة موسَّعة بحدّ عرضه، في هذه الحالة، 2 بكسل. وفي حالة الشكل 9.9 تحديدًا، عند طلب المنطقة 2 بمقاس 100×25 بكسل، يمرّر مرشّح الوسيط هذا الطلب إلى مرشّحه في اتجاه المصدر ليطلب منطقة بمقاس 100×29 بكسل. ويُحسب مقاس الـ 29 بكسل في الاتجاه العمودي بوصفه 25 بكسلًا زائدًا حدّين نصف قطر كلٍّ منهما 2 بكسل. ولاحظ أنّ البُعد الأفقي لا يُوسَّع في هذه الحالة لأنّه أصلًا عند الحدّ الأقصى الذي يمكن أن توفّره الصورة المدخلة؛ وعليه فإنّ الطلب الموسَّع البالغ 104 بكسلات (100 بكسل زائدًا حدّين عرض كلٍّ منهما 2 بكسل) يُقتطع إلى الحدّ الأقصى لحجم الصورة، وهو 100 بكسل في البُعد الأفقي.
تعالج مرشّحات ITK التي تعمل على الأحياء شروطَ الحدّ (boundary conditions) باستعمال أحد ثلاثة أساليب معتادة: اعتبار القيمة خارج الصورة معدومة، أو عكس قيم البكسلات عبر الحدّ، أو تكرار قيمة الحدّ في الخارج. وفي حالة مرشّح الوسيط، يُستخدم شرط حدّ نيومان عديم التدفّق (zero-flux Neumann)، وهو ما يعني ببساطة افتراض أنّ البكسلات الواقعة خارج حدّ المنطقة هي تكرار لقيم البكسلات الموجودة في آخر البكسلات داخل الحدّ.
ثمة سرّ صغير غير معلَن من أسرار أدبيات معالجة الصور، هو أنّ معظم صعوبات التنفيذ مع مرشّحات الصور تتعلّق بالإدارة السليمة لشروط الحدّ. وهذا عرضٌ بعينه من أعراض انفصال التكوين النظري الوارد في كثير من الكتب المدرسية عن الممارسة البرمجية في معالجة الصور. وقد عولج هذا في ITK بتنفيذ مجموعة من أصناف مكرّرات الصور (image iterator classes) وعائلة مرافقة من حاسبات شروط الحدّ. وتُخفي هاتان عائلتا الأصناف المساعدتان عن مرشّحات الصور تعقيدات إدارة شروط الحدّ في الأبعاد المتعدّدة (N-dimensions).
تُقاد عملية البثّ التدفقي من خارج المرشّح، عادةً بواسطة ImageFileWriter أو StreamingImageFilter. وتُنفّذ هاتان الصنفان وظيفة البثّ التدفقي بأخذ الحجم الكلي للصورة وتقسيمه إلى عدد من التقسيمات يطلبه مطوّر التطبيق. ثمّ في أثناء استدعائهما لـ Update() يدخلان في حلقة تكرار تطلب كلّ واحدة من القطع الوسيطة من الصورة. وعند تلك المرحلة يستفيدان من الواجهة البرمجية SetRequestedRegion() الموصوفة في الشكل 9.7. وهذا ما يقيّد حساب خط الأنابيب في اتجاه المصدر على منطقة فرعية من الصورة.
تبدو شيفرة التطبيق التي تقود عملية البثّ التدفقي مثل:
median->SetInput( reader->GetOutput() );
median->SetNeighborhoodRadius( 2 );
writer->SetInput( median->GetOutput() );
writer->SetFileName( filename );
writer->SetNumberOfStreamDivisions( 4 );
writer->Update();
حيث العنصر الجديد الوحيد هو الاستدعاء SetNumberOfStreamDivisions() الذي يعرّف عدد القطع التي ستُقسَّم إليها الصورة بغرض بثّها عبر خط الأنابيب. ولمطابقة مثال الشكل 9.9 استعملنا الرقم أربعة كعدد المناطق التي ستُقسَّم إليها الصورة. وهذا يعني أنّ writer سيُطلق تنفيذ مرشّح median أربع مرات، في كل مرة بمنطقة مطلوبة مختلفة.
توجد أوجه تشابه لافتة بين عملية البثّ التدفقي وعملية توازي تنفيذ مرشّح بعينه. فكلاهما يعتمد على إمكانية تقسيم عمل معالجة الصورة إلى كتلٍّ صور تُعالَج بصورة منفصلة. وفي حالة البثّ التدفقي، تُعالَج كتل الصورة عبر الزمن واحدةً تلو الأخرى، بينما في حالة التوازي تُسنَد كتل الصورة إلى خيوط (threads) مختلفة، التي تُسنَد بدورها إلى نوى معالجات منفصلة. وفي النهاية، فإنّ الطابع الخوارزمي للمرشّح هو ما يحدّد ما إذا كان بالإمكان تقسيم صورة الخرج إلى كتل يمكن حسابها بصورة مستقلة استنادًا إلى مجموعة كتل صور مقابلة من الصورة المدخلة. وفي ITK، يتعامد البثّ التدفقي والتوازي فعليًا، بمعنى أنّ هناك واجهة برمجية تعنى بعملية البثّ التدفقي، وواجهة برمجية منفصلة مخصّصة لدعم تنفيذ الحوسبة المتوازية بالاستناد إلى تعدّد الخيوط والذاكرة المشتركة.
للأسف، لا يمكن تطبيق البثّ التدفقي على جميع أنواع الخوارزميات. والحالات المحدّدة غير المناسبة للبثّ التدفقي هي:
- الخوارزميات التكرارية التي، لحساب قيمة بكسل في كل تكرار، تتطلّب كمدخلات قيم بكسلات جاراتها. وهذه هي حالة معظم الخوارزميات المبنية على حلّ المعادلات التفاضلية الجزئية (PDE)، مثل الانتشار اللامتجانس (anisotropic diffusion)، والتسجيل القابل للتشوّه بأسلوب demons، ومجموعات المستوى الكثيفة (dense level sets).
- الخوارزميات التي تتطلّب مجموعة قيم البكسلات المدخلة كاملةً حتى تحسب قيمة أحد بكسلات الخرج. ويُعدّ تحويل فورييه ومرشّحات الاستجابة النبضية اللانهائية (Infinite Impulse Response، IIR)، مثل مرشّح الغاوسي التكراري (Recursive Gaussian filter)، أمثلةً على هذه الفئة.
- خوارزميات انتشار المناطق أو انتشار الجبهات، التي يجري فيها تعديل البكسلات على نحو تكراري أيضًا، والتي لا يمكن تقسيم مواقع المناطق أو الجبهات فيها بصورة منتظمة إلى كتل بطريقة قابلة للتنبّؤ. ومن الأمثلة النموذجية هنا تقطيع المناطق بنموّها (Region growing segmentation)، ومجموعات المستوى المتناثرة (sparse level sets)، وبعض تطبيقات علم التشريح الرياضي (mathematical morphology)، وبعض أشكال خوارزمية الجبي (watersheds).
- خوارزميات تسجيل الصور، إذ تتطلّب الوصول إلى بيانات الصورة المدخلة كاملةً لحساب قيم المقاييس في كل تكرار من دورات تحسينها.
ولحسن الحظ، يمكّن من جهة أخرى بنية خط أنابيب البيانات في ITK من دعم البثّ التدفقي في مجموعة متنوعة من مرشّحات التحويل، وذلك بالاستفادة من أنّ جميع المرشّحات تُنشئ ناتجها الخاص، ولذلك فإنّها لا تكتب فوق ذاكرة الصورة المدخلة. ويأتي ذلك ثمنًا لاستهلاك الذاكرة، إذ يضطر خط الأنابيب إلى تخصيص الصورتين المدخلة والمخرجة في الذاكرة في الوقت نفسه. وتندرج مرشّحات مثل قلب الصورة (flipping) وتبديل المحاور وإعادة التشكيل الهندسي (geometric resampling) ضمن هذه الفئة. وفي هذه الحالات، يدير خط أنابيب البيانات مطابقة مناطق الإدخال بمناطق الإخراج عبر اشتراط أن يوفّر كل مرشّح طريقة اسمها GenerateInputRequestedRegion() تتلقّى كوسائط منطقة مستطيلة شكل الخرج. وتحسب هذه الطريقة منطقة الإدخال المستطيلة التي سيحتاجها هذا المرشّح لحساب منطقة الخرج المستطيلة المحدَّدة تلك. ويجعل هذا التفاوض المستمر داخل خط أنابيب البيانات من الممكن إسناد كل كتلة خرج إلى المقطع المقابل من الصورة المدخلة الذي يلزم للحساب.
ولنكن أدقّ هنا، علينا القول إذن بأنّ ITK تدعم البثّ التدفقي—لكن في الخوارزميات التي طبيعتها «قابلة للبثّ» (streamable) فحسب. ومع ذلك، وروحًا منّا في التعامل التقدّمي مع بقية الخوارزميات، ينبغي أن نؤطّر هذا القول لا بالادّعاء بأنّ «بثّ هذه الخوارزميات مستحيل»، بل بأنّ «منهجنا المعتاد في البثّ التدفقي غير مناسب لهذه الخوارزميات» في الوقت الحالي، مع الأمل في أن يتبيّن المجتمع في المستقبل تقنيات جديدة تعالج هذه الحالات.
9.3. الدروس المستفادة
قابلية إعادة الاستخدام
يمكن أيضًا قراءة مبدأ إعادة الاستخدام بوصفه «تجنّب التكرار» (avoidance of redundancy). وقد تحقّق ذلك في حالة ITK عبر مقاربة ذات ثلاثة فروع.
- أولًا، تبنّي البرمجة الكائنية، وبخاصةٍ الإنشاء السليم للتسلسلات الهرمية للأصناف حيث تُلخَّص الوظائف المشتركة في أصناف أساس.
- ثانيًا، تبنّي البرمجة العمومية (generic programming)، المنفَّذة عبر الاستعمال المكثّف لقوالب C++ (templates)، بتلخيص السلوكيات التي تُحدَّد بوصفها أنماطًا.
- ثالثًا، سمح الاستعمال السخي لماكروهات C++ أيضًا بإعادة استخدام مقتطفات شيفرة القياسية المطلوبة في مواضع لا تُحصى عبر مجموعة الأدوات.
قد تبدو هذه البنود بدايات جوهرية (platitudes) وتبدو بديهية اليوم، لكن حين انطلق تطوير ITK عام 1999 لم تكن بعضها بهذه البديهية. وبخاصةٍ في ذلك الحين لم يكن الدعم الذي تقدّمه معظم المترجِمات الخاصة بـ C++ للقوالب يتبع معيارًا متّسقًا تمامًا. وحتى اليوم، لا تزال قرارات مثل تبنّي البرمجة العمومية واستعمال تنفيذ مؤطَّر على نحو واسع موضع جدل في المجتمع. ويتجلّى ذلك في المجتمعات التي تفضّل استعمال ITK عبر طبقات التغليف (wrapping layers) نحو Python أو Tcl أو Java.
البرمجة العمومية
كان تبنّي البرمجة العمومية إحدى سمات التنفيذ المميِّزة لـ ITK. فقد كان قرارًا صعبًا في عام 1999 حين كان دعم المترجِمات لقوالب C++ مجزّأً إلى حدٍّ كبير، وكانت مكتبة القوالب القياسية (Standard Template Library، STL) لا تزال تُعدّ نوعًا ما من الغرائب.
أُخذت البرمجة العمومية في ITK بتبنّي استعمال قوالب C++ لتنفيذ تعميم المفاهيم، وبذلك زيادة إعادة استخدام الشيفرة. والمثال النموذجي على تخصيص قوالب C++ في ITK هو الصنف Image، الذي يمكن إنشاؤه بالطريقة التالية:
typedef unsigned char PixelType;
const unsigned int Dimension = 3;
typedef itk::Image< PixelType, Dimension > ImageType;
ImageType::Pointer image = ImageType::New();
في هذا التعبير، يختار مطوّر التطبيق النوع المستعمَل لتمثيل البكسلات في الصورة، وكذلك أبعاد الصورة بوصفها شبكةً في الفضاء. وفي هذا المثال تحديدًا، اخترنا بكسلًا بعرض 8-bit ممثَّلًا بالنوع unsigned char، لصورة ثلاثية الأبعاد. وبفضل التنفيذ العمومي القائم في الأساس، يتبيّن في ITK إنشاء صور من أي نوع بكسل وبأي أبعاد.
ولكي يتيسّر كتابة هذه التعبيرات، كان على مطوّري ITK تنفيذ الصنف Image مع التحفّظ التامّ في الافتراضات التي يجري اتخاذها حول نوع البكسل. وبعد أن يُنشئ مطوّر التطبيق نوع الصورة، يستطيع المطوّر إنشاء كائنات من ذلك النوع، أو المضيّ قُدُمًا لإنشاء مرشّحات صور تتعلّق أنواعها بدورها بنوع الصورة. فمثلًا:
typedef itk::MedianImageFilter< ImageType, ImageType> FilterType;
FilterType::Pointer median = FilterType::New();
قيود الطابع الخوارزمي الخاص بمرشّحات الصور المختلفة تحصر أنواع البكسلات الفعلية التي يمكنها دعمها. فمثلًا، تتوقّع بعض مرشّحات الصور أن يكون نوع بكسل الصورة نوعًا عدديًا مقيَّدًا (integer scalar)، بينما تتوقّع مرشّحات أخرى أن يكون نوع البكسل متجهًا من الأعداد العشرية (floating point). وعند إنشائها بأنواع بكسلات غير مناسبة، ستنتج هذه المرشّحات أخطاء في التصريف (compilation) أو ستُفضي إلى نتائج حساب خاطئة. ولمنع عمليات الإنشاء غير الصحيحة وتيسير تشخيص أخطاء التصريف، تبنّت ITK استعمال فحص المفاهيم (concept checking)، الذي يستند إلى إجبار الأنواع على اجتياز بعض السمات المتوقَّعة، بهدف إحداث إخفاقات مبكّرة مقترنة برسائل خطأ مقروءة من الإنسان.
تُستغل قوالب C++ أيضًا في أقسام معيّنة من مجموعة الأدوات على شكل برمجة قالبية عابرة (Template Metaprogramming)، بهدف زيادة سرعة تنفيذ الشيفرة في وقت التشغيل، وبخاصةً لأغراض فكّ الحلقات (loop unrolling) التي تتحكّم في حساب المتجهات والمصفوفات منخفضة الأبعاد. ومن المفارقة أنّنا اكتشفنا مع مرور الوقت أنّ بعض المترجِمات صارت أذكى في تحديد وقت فكّ الحلقات، ولم تعد تحتاج في بعض الحالات إلى مساعدة تعبيرات البرمجة القالبية العابرة.
معرفة متى تتوقّف
هناك أيضًا الخطر العامّ في الإفراط في «حسن الفعل»، أي خطر الإفراط في استعمال القوالب، أو الإفراط في استعمال الماكروهات. ومن السهل الانحراف والإفراط لينتهي المطوّر إلى إنشاء لغة جديدة فوق C++ تقوم في جوهرها على استعمال القوالب والماكروهات. وهذا خط رفيع، ويتطلّب اهتمامًا مستمرًا من فريق التطوير للتأكّد من أنّ مزايا اللغة تُستعمل استعمالًا سليمًا لا إفراطًا فيه.
ولنأخذ مثالًا محدّدًا، فقد أثبت الاستعمال الواسع لتسمية الأنواع صراحةً عبر typedefs في C++ أهميةً بالغة. وتؤدي هذه الممارسة دورين: من جهة فهي توفّر اسمًا معلوماتيًّا مقروءًا من الإنسان يصف طابع النوع والغرض منه؛ ومن جهة أخرى تضمن استعمال النوع على نحو متّسق عبر مجموعة الأدوات. فمثلًا، أثناء إعادة هيكلة مجموعة الأدوات لإصدارها 4.0، استُثمر جهد هائل في جمع المواضع التي استُعملت فيها أنواع C++ الصحيحة مثل int وunsigned int وlong وunsigned long، واستبدالها بأنواع سمّيت بحسب المفهوم الصحيح الذي كانت تمثّله المتغيّرات المرتبطة بها. وقد كان هذا أكثر أجزاء المهمة كلفةً من ناحية ضمان قدرة مجموعة الأدوات على الاستفادة من أنواع 64 بت لإدارة الصور الأكبر من أربعة غيغابايت على جميع المنصّات. وكانت هذه المهمة ذات أهمية قصوى للترويج لاستعمال ITK في مجالي المجهر (microscopy) والاستشعار عن بُعد (remote sensing)، حيث تكون الصور بعشرات الغيغابايتات شائعة.
قابلية الصيانة
تلبّي المعمارية القيود التي تقلّل تكلفة الصيانة.
- التعدّدية (على مستوى الصنف)
- كثرة الملفات الصغيرة
- إعادة استخدام الشيفرة
- تكرار الأنماط
تقلّل هذه الخصائص تكلفة الصيانة بالطرق التالية:
- التعدّدية (على مستوى الصنف) تجعل من الممكن فرض تقنيات التطوير القائم على الاختبارات على مستوى مرشّح الصورة، أو بصفة عامة على مستوى أصناف ITK. ولانضباط الاختبارات الصارم المطبَّق على أجزاء الشيفرة الصغيرة والمعيّارية ميزةٌ تقلّل برك الشيفرة التي يمكن أن تختبئ فيها الأخطاء؛ ومع الفصل الطبيعي الناجم عن التسييح إلى وحدات، يصبح تحديد العيوب وإزالتها أسهل بكثير.
- كثرة الملفات الصغيرة تيسّر إسناد أجزاء من الشيفرة إلى مطوّرين بعينهم، وتبسّط تتبّع العيوب عندما ترتبط بإيداعات (commits) محدّدة في نظام التحكّم بالإصدارات. ويؤدي انضباط الإبقاء على ملفات صغيرة أيضًا إلى فرض القاعدة الذهبية للدوالّ والأصناف: افعل شيئًا واحدًا، وافعله بإتقان.
- إعادة استخدام الشيفرة: عند إعادة استعمال الشيفرة (بدلًا من نسخها ولصقها وإعادة تنفيذها) تستفيد الشيفرة نفسها من المستوى الأعلى من التدقيق الذي ينتج عن تشغيلها في ظروف مختلفة كثيرة. فهي توجّه أنظارًا أكثر إلى الشيفرة، أو على الأقلّ إلى آثارها، وبذلك تستفيد الشيفرة من قانون لينوس: «مع ما يكفي من العيون، تصبح كل الأخطاء سطحية.»
- تكرار الأنماط يبسّط عمل الصيانة، وهي في الواقع تمثّل أكثر من 75 بالمئة من تكلفة تطوير البرمجيات على مدى عمر المشروع. فاستعمال أنماط برمجية تتكرّر على نحو متّسق في مواضع مختلفة من الشيفرة يجعل من اليسير جدًا أن يفتح المطوّر ملفًا ويفهم بسرعة ما الذي تفعله الشيفرة، أو ما الذي يُفترض أن تفعله.
وحين انخرط المطوّرون في أنشطة الصيانة الاعتيادية، تعرّضوا لبعض «الإخفاقات الشائعة»، وبخاصة:
- افتراضات يقدّمها بعض المرشّحات بشأن أنواع بكسلات محدّدة لصورها المدخلة أو الناتجة، دون أن تكون مفروضة عبر الأنواع أو عبر فحص المفاهيم، ودون أن تكون مذكورة في التوثيق.
- عدم الكتابة من أجل القابلية للقراءة. وهذه إحدى أكثر التحديات شيوعًا لأي برمجيات تنشأ تنفيذاتها الخوارزمية الجديدة من مجتمع البحث. فمن الشائع في تلك البيئة كتابة شيفرة «مجرد تعمل»، وأن يُنسى أنّ غرض الشيفرة ليس التنفيذ في وقت التشغيل فحسب، بل أيضًا أن يقرأها المطوّر التالي بسهولة. وقواعد كتابة «الشيفرة النظيفة» الجيدة عادةً—مثلًا، كتابة دوالّ صغيرة تؤدّي مهمةً واحدة لا غير (مبدأ المسؤولية المفردة، ومبدأ أقلّ المفاجآت)، والالتزام بتسمية سليمة للمتغيّرات والدوالّ—تميل إلى أن تُهمَل حين ينشغل الباحثون بالحصول على خوارزميّتهم اللامعة الجديدة لتعمل.
- إهمال حالات الفشل وإدارة الأخطاء. من الشائع التركيز على «الحالات السعيدة» من معالجة البيانات والإخفاق في توفير شيفرة تتدبّر جميع الحالات التي يمكن أن تسوء. ويصطدم متبنّو مجموعة الأدوات سريعًا بهذه الحالات ما إن يبدأون في تطوير تطبيقاتهم ونشرها في العالم الحقيقي.
- اختبارات غير كافية. يتطلّب التزام ممارسة التطوير القائم على الاختبارات انضباطًا كبيرًا، ولا سيما فكرة كتابة الاختبارات أولًا وعدم تنفيذ الوظائف إلا مع اختبارها. وفي أغلب الأحوال تكون الأخطاء في الشيفرة مختبئة خلف الحالات التي جرى تخطّيها أثناء تنفيذ شيفرة الاختبار.
وبفضل ممارسات التواصل في مجتمعات المصادر المفتوحة، ينكشف كثير من هذه البنود في النهاية عبر الأسئلة التي تُطرح عادةً في قوائم البريد، أو يُبلَّغ عنها مباشرةً بوصفها أخطاءً من المستخدمين. وبعد التعامل مع مسائل كثيرة من هذا النوع، يتعلّم المطوّرون كتابة شيفرة «صالحة للصيانة». وتنطبق بعض هذه السمات على أسلوب البرمجة وعلى التنظيم الفعلي للشيفرة معًا. ومن وجهة نظرنا، لا يبلغ المطوّر الإتقان إلا بعد أن يقضي مدّة من الزمن—سنة على الأقلّ—في الصيانة ويتعرّض لـ«كل ما يمكن أن يسوء».
اليد الخفية
ينبغي أن تبدو البرمجيات وكأنها كُتبت على يد شخص واحد. وأفضل المطوّرين هم من يكتبون شيفرة يمكن لأي شخص آخر أن يتولّاها إذا أصابتهم حافلة اليسخرية المعروفة (proverbial bus). وقد تعلّمنا أن نُدرك أنّ أي أثر لـ«لمسة شخصية» (personal touch) إنما هو مؤشّر على عيب أُدخل في البرمجيات.
ولضمان وحدة أسلوب الشيفرة وتعزيزها، أثبتت الأدوات التالية فاعلية كبيرة:
- KWStyle لفحص أسلوب الشيفرة المصدرية آليًا. وهذه محلّلات C++ مبسّطة تفحص أسلوب الترميز وتُبلّغ عن أي مخالفة.
- Gerrit لإجراء مراجعات الشيفرة بانتظام. وتؤدّي هذه الأداة غرضين: من جهة تمنع الشيفرة غير الناضجة من الدخول إلى قاعدة الشيفرة بتجريد أخطائها ونواقصها وعبوبها خلال دورات مراجعة تكرارية يشارك فيها مطوّرون آخرون في تحسين الشيفرة. ومن جهة أخرى توفّر معسكرًا تدريبًا افتراضيًا يتعلّم فيه المطوّرون الجدد من مطوّرين أكثر خبرة (اقرأ «خبرة» على أنها أنّهم ارتكبوا كل الأخطاء ويعرفون أين مدفونة الجثث…) كيفية تحسين الشيفرة وتجنّب المشكلات المعروفة التي رُصدت خلال دورات الصيانة.
- خطّافات Git (Git hooks) التي تفرض استعمال KWStyle وGerrit، والتي تجري أيضًا بعض فحوصاتها الخاصة. فمثلًا، تستعمل ITK خطّافات Git التي تمنع إيداع شيفرة تحوي مسافات بادئة على هيئة جدولة (tabs) أو مسافات فارغة في نهاية الأسطر.
- كما استكشف الفريق استعمال Uncrustify كأداة لفرض أسلوب متّسق.
تجدر التأكيد على أنّ وحدة الأسلوب ليست مسألة جاذبية جمالية فحسب، بل مسألة اقتصادية في المقام الأول. وقد قدّرت دراساتٌ حول إجمالي تكلفة الملكية (Total Cost of Ownership، TCO) لمشاريع البرمجيات أنّ تكلفة الصيانة في دورة حياة المشروع ستبلغ نحو 75 بالمئة من إجمالي التكلفة، وبما أنّ تكلفة الصيانة تُطبَّق على أساس سنوي فإنّها تتجاوز عادةً تكاليف التطوير الأوّلي في السنوات الخمس الأولى من دورة حياة مشروع برمجي. (انظر "Software Development Cost Estimating Handbook"، المجلد الأول، مركز تحليل التكاليف البحرية (Naval Center for Cost Analysis)، وكالة تحليل تكاليف القوات الجوية (Air Force Cost Analysis Agency)، 2008.) ويُقدَّر أنّ الصيانة تشكّل نحو 80 بالمئة مما يقوم به المطوّرون فعليًا، وعندما ينشغلون بهذا النشاط تكون الغالبية العظمى من وقت المطوّر مخصّصة لقراءة شيفرة شخص آخر، في محاولة لفهم ما كان ينبغي أن تفعله (انظر Clean Code, A Handbook of Agile Software Craftsmanship، Robert C. Martin، Prentice Hall، 2009). ويُحدث الأسلوب المتّسق أثرًا بالغًا في تقصير الوقت الذي يحتاجه المطوّرون ليغمسوا أنفسهم في ملف برمجي مفتوح حديثًا ويفهموا الشيفرة قبل أن يُجريوا عليها أي تعديل. وعلى المنوال نفسه، يقلّل من احتمالات أن يُسيء المطوّرون تفسير الشيفرة ويجريون تعديلات تنتهي بإدخال أخطاء جديدة وهم في الحقيقة يحاولون إصلاح أخطاء قديمة (The Art of Readable Code، Dustin Boswell، Trevor Foucher، O'Reilly، 2012).
المفتاح في جعل هذه الأدوات فاعلة هو التأكّد من أنها:
- متاحة لجميع المطوّرين، ولهذا نفضّل أدوات المصادر المفتوحة.
- تُنفَّذ على أساس منتظم. وفي حالة ITK، أُدمجت هذه الأدوات في بنيات ليلية ومستمرة (Nightly and Continuous Dashboard) يديرها CDash.
- تُنفَّذ أقرب ما يكون إلى الموضع الذي تُكتب فيه الشيفرة، بحيث يمكن إصلاح الانحرافات فورًا، ويتعلّم المطوّرون بسرعة أيّ الممارسات تكسر قواعد الأسلوب.
إعادة الهيكلة
انطلقت ITK عام 2000 ونمت باستمرار حتى عام 2010. وفي عام 2011، بفضل ضخّ استثمارات تمويل اتحادي، حصل فريق التطوير على فرصة فريدة حقًا للاضطلاع بجهد إعادة هيكلة. وقد وفّرت المكتبة الوطنية للطب هذا التمويل ضمن مبادرة قانون American Recovery and Reinvestment Act (ARRA). ولم تكن هذه مهمّة بسيطة. تخيّل أنّك تعمل على قطعة برمجيات منذ أكثر من عقد، ثمّ يُعرض عليك فرصة لتنظيفها؛ فما الذي كنت ستغيّره؟
هذه الفرصة لإعادة هيكلة واسعة النطاق نادرة جدًا. فخلال السنوات العشر السابقة، كنّا نعتمد على الجهد اليومي في إجراء عمليات إعادة هيكلة صغيرة موضعية، وتنظيف زوايا بعينها من مجموعة الأدوات عند مصادفتها. وتستفيد عملية التنظيف والتحسين المستمرة هذه من التعاون الواسع لمجتمعات المصادر المفتوحة، ويضطلع بها على نحو آمن بنية الاختبار التي يقودها CDash، والتي تجرّب بانتظام نحو 84 بالمئة من الشيفرة في مجموعة الأدوات. ولاحظ أنّ متوسط تغطية الشيفرة في اختبارات صناعة البرمجيات يُقدَّر بـ 50 بالمئة فقط، على النقيض من ذلك.
من بين الأشياء الكثيرة التي تغيّرت في جهد إعادة الهيكلة، أكثرها ارتباطًا بالمعمارية ما يلي:
- أُدخل التسييح إلى وحدات في مجموعة الأدوات
- وُحِّدت الأنواع الصحيحة
- صُحِّحت تعريفات الأنواع (typedefs) للسماح بإدارة صور أكبر من 4 غيغابايت على جميع المنصّات
- نُقِّح سير عمل البرمجيات: الانتقال من CVS إلى Git
- أُدخلت مراجعة الشيفرة باستخدام Gerrit
- أُدخل الاختبار عند الطلب باستخدام CDash@home
- حُسّنت طريقة تنزيل بيانات اختبارات الوحدات
أُلغي الدعم عن المترجِمات المهجورة. وحُسّن الدعم لكثير من صيغ ملفات صور الإدخال/الإخراج، ومنها:
- DICOM
- JPEG2000
- TIFF (BigTIFF)
- HDF5
أُدخل إطار عمل لدعم الحوسبة على وحدة معالجة الرسوميات (GPU). وأُدخل دعم لمعالجة الفيديو.
- أُضيف جسر (bridge) إلى OpenCV
- أُضيف جسر إلى VXL
تنجح الصيانة القائمة على تعديلات تزايديّة—من قبيل إضافة ميزات إلى مرشّح صور، أو تحسين أداء خوارزمية بعينها، أو معالجة بلاغات عن الأخطاء، أو تحسين توثيق مرشّحات صور محدّدة—في تحسين أصناف C++ بعينها على المستوى المحلّي. غير أنّ تعديلات البنية التحتية التي تمسّ عددًا كبيرًا من الأصناف على نحو شامل، مثل التعديلات المذكورة أعلاه، تستلزم إعادة هيكلة ضخمة. فمثلًا، كانت مجموعة التغييرات اللازمة لدعم صور أكبر من 4 غيغابايت على الأرجح إحدى أكبر الرقع التي طُبِّقت يومًا على ITK؛ فقد تطلّبت تعديل مئات الأصناف ولم يكن بالإمكان إنجازها تزايديًّا من دون أن يتحمّل المطوّرون عبئًا هائلًا من العسر. والتسييح إلى وحدات مثال آخر على مهمة لا يمكن إنجازها تزايديًّا؛ فقد أثّر فعليًا في تنظيم مجموعة الأدوات كلّه، وفي طريقة عمل بنية الاختبار فيها، وفي كيفية إدارة بيانات الاختبار، وفي كيفية تحزيم مجموعة الأدوات وتوزيعها، وفي كيفية تغليف المساهمات الجديدة كي تُضاف إلى مجموعة الأدوات في المستقبل.
قابلية إعادة الإنتاج
كان أحد أوائل الدروس المستفادة في ITK أنّ الأبحاث العديدة المنشورة في هذا المجال لم تكن بالسهولة التي قُدِّرنا إليها في تطبيقها. فمجال الحوسبة يميل إلى التهويل المبالغ فيه في الخوارزميات، وإلى الاستخفاف بالعمل العملي في كتابة البرمجيات باعتباره «مجرد تفصيل تنفيذي».
هذا الموقف الاستخفافي ضارّ بالمجال إلى حدٍّ كبير، إذ يقلّل من أهمية الخبرة العملية المباشرة مع الشيفرة واستعمالها السليم. وتتمثّل النتيجة في أنّ معظم الأبحاث المنشورة ببساطة غير قابلة لإعادة الإنتاج، وحين يحاول الباحثون والطلبة استعمال تقنيات من هذا النوع ينتهون إلى إنفاق وقت طويل في العملية ويقدّمون تنويعات للعمل الأصلي. ومن الصعب جدًا عمليًا التحقّق ممّا إذا كان تنفيذ ما يطابق ما ورد في بحث.
أحدثت ITK، لفائدة، اضطرابًا في تلك البيئة، وأعادت إلى مجالٍ اعتاد الاستدلال النظري وتعلّم الاستخفاف بالعمل التجريبي ثقافة «افعلها بنفسك» (DIY). والثقافة الجديدة التي جاء بها ITK ثقافة عملية واقعية تُحكَم فيها مزايا البرمجيات بنتائجها العملية، لا بمظهر التعقيد الذي يُمجَّد في بعض المنشورات العلمية. ويتبيّن عمليًا أنّ أنجع طرائق المعالجة هي تلك التي تبدو أبسط من أن تُقبل في بحث علمي.
ثقافة قابلية إعادة الإنتاج امتدادٌ لفلسفة التطوير القائم على الاختبارات، وتُفضي منظومًا إلى برمجيات أفضل: وضوحًا أعلى، وقابلية أعلى للقراءة، ومتانةً وتركيزًا أكبر.
ولسدّ الفجوة الناتجة عن نقص المنشورات القابلة لإعادة الإنتاج، أنشأ مجتمع ITK مجلة Insight Journal. وهي منشورٌ مفتوح الوصول يُنشر عبر الإنترنت بالكامل، تشترط فيه المساهمات تضمين الشيفرة والبيانات والمعاملات والاختبارات من أجل تمكين التحقّق عن طريق إعادة الإنتاج. وتُنشر المقالات عبر الإنترنت في أقل من 24 ساعة من تقديمها. ثمّ تُتاح للمراجعة من الأقران من قِبل أي عضو في المجتمع. ويحصل القرّاء على وصولٍ كامل إلى جميع المواد المرافقة للمقال، وهي تحديدًا شيفرة المصدر والبيانات والمعاملات وسكربتات الاختبار. وقد وفّرت المجلة مساحةً منتجة لمشاركة المساهمات الشيفرية الجديدة التي تجد طريقها من هناك إلى قاعدة الشيفرة. وقد استقبلت المجلة مؤخرًا مقالها الخامس مئة، ولا تزال تُستعمل بوصفها البوابة الرسمية لإضافة الشيفرة الجديدة إلى ITK.
