این سه عدد به ترتیب سطر، ستون و کانال هستند. سطرها ارتفاع تصویرند و ستونها عرض آن؛ پس این عکس ۴۲۵ پیکسل ارتفاع و ۶۴۰ پیکسل عرض دارد. ویندوز همین عکس را «۶۴۰ × ۴۲۵» نشان میدهد، چون معمولاً اول عرض را میگوییم. حواستان باشد NumPy برعکس، اول ارتفاع را میدهد:
عدد سوم، ۳، تعداد کانالهای رنگ است: قرمز، سبز و آبی (RGB). هر رنگی که روی صفحه میبینید ترکیبی از این سه است. اگر در Paint یک رنگ نارنجی را با ابزار انتخاب رنگ بردارید، میبینید از ۲۵۵ واحد قرمز، ۱۲۷ واحد سبز و ۳۹ واحد آبی ساخته شده. هر کانال عددی بین ۰ تا ۲۵۵ است.
مقدار یک پیکسل:
a[250, 200] # array([94, 68, 33], dtype=uint8)
سطر ۲۵۰ و ستون ۲۰۰ ترکیبی از ۹۴ قرمز، ۶۸ سبز و ۳۳ آبی است؛ یک رنگ قهوهای. اگر همین سه عدد را در Paint وارد کنید، به همان قهوهای میرسید. شمارش سطر و ستون از صفر و از بالا چپ تصویر شروع میشود.
تصویر خاکستری (grayscale) به جای سه کانال یک کانال دارد: ۰ یعنی سیاه مطلق، ۲۵۵ یعنی سفید، و عددهای بین آن سطحهای مختلف خاکستریاند. سادهترین راه، میانگین سه کانال است:
// تقسیم صحیح است. NumPy عملیات را برداری انجام میدهد: سه ماتریس هماندازه را درایهبهدرایه جمع میکند و یک ماتریس هماندازه برمیگرداند؛ بدون حلقه.
cmap="gray" مهم است. Matplotlib برای رسم نمودار ساخته شده و یک آرایهی تککانالی را با یک نقشهی رنگ (colormap) رنگی نشان میدهد. با gray میگوییم کمترین مقدار را سیاه و بیشترین را سفید نشان بده.
جاهایی که روشن بودند تاریک شدهاند و برعکس. دلیلش نوع داده است:
a.dtype # dtype('uint8')
uint8 یعنی عدد صحیح بدون علامت ۸ بیتی. با ۸ بیت بیشترین عدد ممکن 11111111 در مبنای دو، یعنی ۲۵۵ است. اگر ۱ اضافه کنیم، ۲۵۶ در مبنای دو 100000000 میشود که ۹ بیت دارد؛ بیت اضافه دور ریخته میشود و ۸ بیت باقیمانده همه صفرند. یعنی عدد دور میزند (overflow):
x = np.array([250],dtype=np.uint8)
y = np.array([10],dtype=np.uint8)
x + y # array([4], dtype=uint8) چون 260 - 256 = 4
وقتی سه کانال یک پیکسل روشن را با هم جمع میکنیم، حاصل از ۲۵۵ رد میشود و دور میزند. نتیجه آنقدر خراب است که بیشترین مقدار در gray_wrong حداکثر ۸۵ (یعنی ۲۵۵ تقسیم بر ۳) است؛ Matplotlib همین ۸۵ را سفید نشان میدهد و تصویر کاملاً عوض میشود.
دو راه درست:
# ۱) اول تقسیم، بعد جمع (حداکثر 85 + 85 + 85 = 255)
روش اول به خاطر گرد کردن هر کانال، حداکثر دو واحد با میانگین واقعی فرق دارد که چشم متوجهش نمیشود. کتابخانههای حرفهای این جزئیات را خودشان رعایت میکنند، ولی وقتی خودتان روی پیکسلها حساب میکنید، همیشه به نوع داده فکر کنید.
تمرین تعاملیحل شده
سیاهوسفید کردن بدون سرریز
تابع to_gray(img) را بنویسید که تصویر رنگی uint8 با شکل (h, w, 3) را بگیرد و تصویر خاکستری با شکل (h, w) برگرداند؛
مقدار هر پیکسل میانگین سه کانال است.
مراقب باشید: جمع کردن مستقیم سه کانال uint8 از ۲۵۵ رد میشود و دور میزند. مثلاً 250 + 10 در uint8 میشود ۴.
راهنمایی
دو راه دارید: هر کانال را جدا بر ۳ تقسیم کنید و بعد جمع کنید، یا اول نوع داده را با img.astype(np.int32) بزرگ کنید، بعد جمع و تقسیم کنید.
OpenCV هم تصویر را به آرایهی NumPy تبدیل میکند و یک امکان راحت دارد: موقع خواندن میتواند مستقیم خاکستری بخواند.
import cv2
img = cv2.imread("images/elephant.jpg") # رنگی
gray = cv2.imread("images/elephant.jpg",0) # خاکستری؛ شکل (425, 640)
ولی یک تفاوت مهم دارد: OpenCV کانالها را به ترتیب BGR (آبی، سبز، قرمز) نگه میدارد، در حالی که Matplotlib، TensorFlow و بیشتر کتابخانهها RGB را فرض میکنند. اگر تصویری را که OpenCV خوانده مستقیم با Matplotlib نمایش دهید، قرمز و آبی جابهجا میشوند. راهحل، برعکس کردن ترتیب کانالهاست: