مقدمة عن معالجة الصور بيثون في التصوير الحسابي

نشرت: 2022-03-11

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

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

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

التصوير في الإضاءة المنخفضة

لنفترض أننا نريد التقاط صورة ضوئية منخفضة لمشهد ما ، لكن الكاميرا بها فتحة عدسة صغيرة (عدسة) ووقت تعريض محدود. هذه حالة نموذجية لكاميرات الهاتف المحمول والتي ، في ظل الإضاءة المنخفضة ، يمكن أن تنتج صورة مثل هذه (تم التقاطها بكاميرا iPhone 6):

صورة لزوجين من الألعاب في بيئة منخفضة الإضاءة

إذا حاولنا تحسين التباين ، فإن النتيجة هي التالية ، وهي أيضًا سيئة للغاية:

نفس الصورة أعلاه ، أكثر إشراقًا ولكن مع تشويش بصري مشتت للانتباه

ماذا يحدث؟ من أين تأتي كل هذه الضوضاء؟

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

سيظل قدر من الضوضاء موجودًا دائمًا في جهاز التصوير ؛ ومع ذلك ، إذا كانت الإشارة (المعلومات المفيدة) ذات كثافة عالية ، فستكون الضوضاء ضئيلة (نسبة إشارة إلى ضوضاء عالية). عندما تكون الإشارة منخفضة - مثل الإضاءة المنخفضة - ستبرز الضوضاء (إشارة منخفضة للضوضاء).

ومع ذلك ، يمكننا التغلب على مشكلة الضوضاء ، حتى مع كل قيود الكاميرا ، من أجل الحصول على لقطات أفضل من تلك المذكورة أعلاه.

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

لذلك ، إذا كنا متوسط ​​عدد الصور التي تم التقاطها بمرور الوقت ، فسيتم إلغاء الضوضاء بينما لن تتأثر الإشارة.

يوضح الرسم التوضيحي التالي مثالًا مبسطًا: لدينا إشارة (مثلث) متأثرة بالضوضاء ، ونحاول استرداد الإشارة عن طريق حساب متوسط ​​حالات متعددة للإشارة نفسها متأثرة بضوضاء مختلفة.

عرض توضيحي من أربع لوحات للمثلث ، صورة مبعثرة تمثل المثلث مع ضوضاء إضافية ، نوع من المثلث المسنن يمثل متوسط ​​50 حالة ، ومتوسط ​​1000 حالة ، والتي تبدو متطابقة تقريبًا مع المثلث الأصلي.

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

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

المزيد من اللقطات يعني بشكل عام جودة أفضل ، لكن العدد الدقيق يعتمد على الموقف: مقدار الضوء الموجود ، ومدى حساسية الكاميرا ، وما إلى ذلك. يمكن أن يكون النطاق الجيد في أي مكان بين 10 و 100.

بمجرد حصولنا على هذه الصور (بتنسيق خام إن أمكن) ، يمكننا قراءتها ومعالجتها في Python.

بالنسبة لأولئك الذين ليسوا على دراية بمعالجة الصور في Python ، يجب أن نذكر أن الصورة يتم تمثيلها كمصفوفة ثنائية الأبعاد لقيم البايت (0-255) - أي لصورة أحادية اللون أو ذات تدرج رمادي. يمكن اعتبار الصورة الملونة على أنها مجموعة من ثلاث صور ، واحدة لكل قناة ألوان (R ، G ، B) ، أو بشكل فعال مصفوفة ثلاثية الأبعاد مفهرسة بالموضع العمودي والموضع الأفقي وقناة الألوان (0 ، 1 ، 2) .

سنستخدم مكتبتين: NumPy (http://www.numpy.org/) و OpenCV (https://opencv.org/). الأول يسمح لنا بإجراء عمليات حسابية على المصفوفات بشكل فعال للغاية (برمز قصير بشكل مدهش) ، بينما يتعامل OpenCV مع قراءة / كتابة ملفات الصور في هذه الحالة ، ولكنه أكثر قدرة بكثير ، ويوفر العديد من إجراءات الرسومات المتقدمة - سنقوم ببعض منها استخدم لاحقًا في المقالة.

 import os import numpy as np import cv2 folder = 'source_folder' # We get all the image files from the source folder files = list([os.path.join(folder, f) for f in os.listdir(folder)]) # We compute the average by adding up the images # Start from an explicitly set as floating point, in order to force the # conversion of the 8-bit values from the images, which would otherwise overflow average = cv2.imread(files[0]).astype(np.float) for file in files[1:]: image = cv2.imread(file) # NumPy adds two images element wise, so pixel by pixel / channel by channel average += image # Divide by count (again each pixel/channel is divided) average /= len(files) # Normalize the image, to spread the pixel intensities across 0..255 # This will brighten the image without losing information output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) # Save the output cv2.imwrite('output.png', output)

تظهر النتيجة (مع تطبيق التباين التلقائي) أن التشويش قد انتهى ، وهو تحسن كبير جدًا عن الصورة الأصلية.

الصورة الأصلية للألعاب ، هذه المرة أكثر إشراقًا ووضوحًا ، مع القليل جدًا من الضوضاء

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

لقطة مقربة للزاوية اليسرى العلوية للصورة أعلاه

لقطة مقرّبة للزاوية اليسرى العلوية ، تُظهر الإطار الأخضر ونمط الشبكة

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

لحسن الحظ ، هناك طريقة للتخلص من هذا النوع من الضوضاء أيضًا. يطلق عليه طرح الإطار المظلم.

للقيام بذلك ، نحتاج إلى صورة لضوضاء النمط نفسها ، ويمكن الحصول عليها إذا صورنا الظلام. نعم ، هذا صحيح - ما عليك سوى تغطية فتحة الكاميرا والتقاط الكثير من الصور (على سبيل المثال 100) بأقصى وقت تعريض وقيمة ISO ، ومعالجتها كما هو موضح أعلاه.

عند حساب المتوسط ​​على العديد من الإطارات السوداء (التي ليست سوداء في الواقع ، بسبب الضوضاء العشوائية) سننتهي بضوضاء النمط الثابت. يمكننا أن نفترض أن هذه الضوضاء الثابتة ستبقى ثابتة ، لذلك هذه الخطوة مطلوبة مرة واحدة فقط: يمكن إعادة استخدام الصورة الناتجة لجميع اللقطات المستقبلية في الإضاءة المنخفضة.

إليك كيف يبدو الجزء العلوي الأيمن من ضوضاء النمط (تعديل التباين) لجهاز iPhone 6:

ضوضاء النمط لجزء من الإطار المعروض في الصورة السابقة

مرة أخرى ، نلاحظ النسيج الشبيه بالشبكة ، وحتى ما يبدو أنه بكسل أبيض عالق.

بمجرد أن نحصل على قيمة ضوضاء الإطار المظلم (في average_noise ​​الضجيج) ، يمكننا ببساطة طرحها من اللقطة حتى الآن ، قبل التطبيع:

 average -= average_noise output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) cv2.imwrite('output.png', output)

ها هي صورتنا النهائية:

صورة أخرى للصورة ، هذه المرة بدون أي دليل على الإطلاق على التقاطها في الإضاءة المنخفضة

المدى الديناميكي العالي

من القيود الأخرى التي تمتلكها الكاميرا (المحمولة) الصغيرة نطاقها الديناميكي الصغير ، مما يعني أن نطاق شدة الضوء التي يمكنها من خلالها التقاط التفاصيل صغير نوعًا ما.

بمعنى آخر ، تستطيع الكاميرا التقاط نطاق ضيق فقط من شدة الضوء من مشهد ما ؛ تظهر الشدة أسفل هذا الشريط باللون الأسود الخالص ، بينما تظهر الشدة الموجودة فوقه بلون أبيض نقي ، وتضيع أي تفاصيل من تلك المناطق.

ومع ذلك ، هناك خدعة يمكن للكاميرا (أو المصور) استخدامها - وهي ضبط وقت التعرض (الوقت الذي يتعرض فيه المستشعر للضوء) من أجل التحكم في إجمالي كمية الضوء التي تصل إلى المستشعر ، بشكل فعال تحويل النطاق لأعلى أو لأسفل من أجل التقاط النطاق الأنسب لمشهد معين.

لكن هذا حل وسط. تفشل العديد من التفاصيل في الوصول إلى الصورة النهائية. في الصورتين أدناه ، نرى نفس المشهد تم التقاطه بأوقات تعريض مختلفة: تعريض قصير جدًا (1/1000 ثانية) ، وتعريض متوسط ​​(1/50 ثانية) ، وتعرض طويل (1/4 ثانية).

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

كما ترى ، لا تستطيع أي من الصورتين التقاط جميع التفاصيل المتاحة: خيوط المصباح مرئية فقط في اللقطة الأولى ، وبعض تفاصيل الزهرة مرئية إما في اللقطة الوسطى أو الأخيرة ولكن لا على حد سواء.

الخبر السار هو أن هناك شيئًا يمكننا القيام به حيال ذلك ، ومرة ​​أخرى يتضمن البناء على لقطات متعددة بقليل من كود Python.

النهج الذي سنتخذه يعتمد على عمل Paul Debevec وآخرون ، الذي يصف الطريقة في ورقته البحثية هنا. الطريقة تعمل مثل هذا:

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

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

 # Read all the files with OpenCV files = ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg'] images = list([cv2.imread(f) for f in files]) # Compute the exposure times in seconds exposures = np.float32([1. / t for t in [1000, 500, 100, 50, 10]]) # Compute the response curve calibration = cv2.createCalibrateDebevec() response = calibration.process(images, exposures)

يبدو منحنى الاستجابة شيئًا كالتالي:

رسم بياني يعرض منحنى الاستجابة كتعرض بكسل (سجل) فوق قيمة بكسل

على المحور الرأسي ، لدينا التأثير التراكمي لسطوع المشهد للنقطة ووقت التعرض ، بينما على المحور الأفقي لدينا القيمة (من 0 إلى 255 لكل قناة) سيكون للبكسل المقابل.

يسمح لنا هذا المنحنى بإجراء العملية العكسية (وهي الخطوة التالية في العملية) - نظرًا لقيمة البكسل ووقت التعرض ، يمكننا حساب السطوع الحقيقي لكل نقطة في المشهد. تسمى قيمة السطوع هذه بالإشعاع ، وهي تقيس كمية الطاقة الضوئية التي تسقط على وحدة منطقة المستشعر. على عكس بيانات الصورة ، يتم تمثيلها باستخدام أرقام الفاصلة العائمة لأنها تعكس نطاقًا أوسع بكثير من القيم (وبالتالي ، النطاق الديناميكي العالي). بمجرد أن نحصل على صورة الإشعاع (صورة HDR) ، يمكننا ببساطة حفظها:

 # Compute the HDR image merge = cv2.createMergeDebevec() hdr = merge.process(images, exposures, response) # Save it to disk cv2.imwrite('hdr_image.hdr', hdr)

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

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

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

يوفر OpenCV مجموعة من عوامل تعيين النغمة هذه ، مثل Drago أو Durand أو Mantiuk أو Reinhardt. فيما يلي مثال على كيفية استخدام أحد هذه العوامل (Durand) والنتيجة التي ينتجها.

 durand = cv2.createTonemapDurand(gamma=2.5) ldr = durand.process(hdr) # Tonemap operators create floating point images with values in the 0..1 range # This is why we multiply the image with 255 before saving cv2.imwrite('durand_image.png', ldr * 255) 

يتم عرض نتيجة الحساب أعلاه كصورة

باستخدام Python ، يمكنك أيضًا إنشاء عوامل التشغيل الخاصة بك إذا كنت بحاجة إلى مزيد من التحكم في العملية. على سبيل المثال ، هذه هي النتيجة التي تم الحصول عليها باستخدام عامل تشغيل مخصص يزيل الشدة الممثلة في عدد قليل جدًا من وحدات البكسل قبل تقليص نطاق القيمة إلى 8 بت (متبوعة بخطوة تباين تلقائي):

الصورة الناتجة عن اتباع العملية المذكورة أعلاه

وهنا رمز المشغل أعلاه:

 def countTonemap(hdr, min_fraction=0.0005): counts, ranges = np.histogram(hdr, 256) min_count = min_fraction * hdr.size delta_range = ranges[1] - ranges[0] image = hdr.copy() for i in range(len(counts)): if counts[i] < min_count: image[image >= ranges[i + 1]] -= delta_range ranges -= delta_range return cv2.normalize(image, None, 0, 1, cv2.NORM_MINMAX)

خاتمة

لقد رأينا كيف باستخدام القليل من Python وبعض المكتبات الداعمة ، يمكننا دفع حدود الكاميرا المادية من أجل تحسين النتيجة النهائية. يستخدم كلا المثالين اللذين ناقشناهما لقطات متعددة منخفضة الجودة لإنشاء شيء أفضل ، ولكن هناك العديد من الأساليب الأخرى للمشكلات والقيود المختلفة.

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

إذا كنت مهتمًا بحسابات الصور على جهاز محمول ، فراجع برنامج OpenCV التعليمي: اكتشاف الكائنات في الوقت الفعلي باستخدام MSER في iOS بواسطة زميل Toptaler ونخبة مطور OpenCV Altaibayar Tseveenbayar.