تصویر و ماتریس در OpenCV – پویش ماتریس

گاهی اوقات لازم است تا برای یک هدف یا کاربرد، ماتریس را پویش (Scan) کنیم. به طور کلی OpenCV دو راهکار را برای پویش و دسترسی به درایه های ماتریس (یا همان پیکسل های تصویر) فراهم کرده است، یا باید به صورت مستقیم به موقعیت یا محل مورد نظر دسترسی پیدا کنیم و یا باید از طریق تکرار این کار را انجام دهیم.

لطفا پیش از خواندن ادامه، مطلب های زیر را بخوانید تا با مفهوم تصاویر خاکستری و رنگی (تک کانالی و سه کانالی) و چگونگی ذخیره سازی ماتریس تصویر آشنا شوید.

آشنایی با مفاهیم ابتدایی در OpenCV – تصویر خاکستری و رنگی

ماتریس تصویر چگونه در حافظه ذخیره می شود

خواندن و نمایش و ذخیره سازی تصویر در OpenCV

استفاده از تابع الگو <>at

برای پردازش کردن تصاویر، مهم است که بدانیم و بتوانیم چگونه به هر پیکسل به صورت مستقیم دسترسی داشته باشیم. OpenCV چندین روش برای این کار فراهم کرده است. اولین روش استفاده از تابع الگو (Template Function) به نام <>at است.

توجه کنید که هر تصویر به صورت ماتریسی از اعداد است که هر عدد بیانگر یک پیکسل است. OpenCV از ساختار داده ای به نام Mat برای ذخیره سازی تصویر استفاده می کند. همچنین توضیح دادیم به صورت پیشفرض پیسکل ها به صورت مقادیر ۸ بیتی بدون علامت (CV_8U) ذخیره می شوند ولی ممکن است که از انواع دیگر باشندبنابراین در زمان استفاده از تابع الگو <>at باید نوع مقدار (j, i) در ماتریس را تعیین کنیم.

در قطعه کد زیر ابتدا یک ماتریس همانی (Identity Matrix) به ابعاد ۴۰ در ۳۰ ایجاد می کنیم. سپس توسط خط ۱۳ و توسط تابع <>at به درایه سطر ۲۵ و ستون ۱۵ دسترسی خواهیم داشت. توجه کنید چون در خط ۱۰ نوع درایه های ماتریس را CV_F32C1 در نظر گرفتیم، پس نوع درایه ها ۳۲ بیتی ممیز شناور (Floating Point) است. همچنین C1 در CV_32FC1 به معنی تک کانالی بودن ماتریس و قاعدتا تصویر است.

حال فرض کنید می خواهیم ماتریس همانی بسازیم که درایه های آن به صورت مقادیر ۳۲ بیتی ممیز شناور دو کانالی هستند، پس باید از نوع CV_32FC2 در زمان ایجاد ماتریس استفاده کنیم. توجه کنید وقتی می گوییم سه کانالی، به این معنی است که هر درایه به صورت یک بردار دو تایی است. پس مطابق خطوط تا باید در تابع <>at از Vec2f استفاده کنیم.

اما همانطور که پیش از این توضیح دادیم در OpenCV و در تصاویر رنگی، سیستم RGB به صورت معکوس، یعنی BGR تفسیر می شود. در کد زیر می خواهیم تصویر زیر را بخوانیم در برنامه بارگذاری کنیم و سپس به پیکسل گوشه بالا (عنصر 0, 0) دسترسی داشته باشیم.

در خط ۱۰ از کد بالا، ابتدا تصویر را به صورت IMREAD_GRAYSCALE خوانده و درون ماتریس image1 بارگذاری کرده ایم، پس ماتریس image1 به صورت تک کانالی ۸ بیتی بی علامت است. برای همین در خط ۱۱ و توسط متد <>at، عنصر (0,0) را درون متغیر pixel1 از نوع ucahr ریخته ایم.

توجه کنید چون ماتریس image1 به صورت تک کانالی ۸ بیتی بی علامت است، پس از نوع uchar در تابع <>at استفاده کرده ایم. توجه کنید که ۸ بیتی بی علامت (یا CV_8U یا CV_8UC1) معادل یک کاراکتر است که همسان با یک کاراکتر یا نوع uchar در سی پلاس پلاس است. نوع CV_8U می تواند اعداد صفر تا ۲۵۵ را در خود نگه دارد که صفر معادل سیاه (تاریک ترین رنگ در طیف خاکستری (Grayscale)) و عدد ۲۵۵ معادل سفید (روشن ترین رنگ در طیف خاکستری) است.

در خط ۱۴ مجدد همان تصویر شکل زیر را به صورت عادی و رنگی خوانده و در متغیر image بارگذاری کرده ایم. چون تصویر سه کانالی است، پس عنصر (0,0) به صورت بردار سه تایی است و چون هر درایه به صورت ۸ بیتی بی علامت است، پس از نوع Vec3b در تابع <>at استفاده کرده ایم. همچنین نوع متغیر pixel2 نیز از نوع Vec3b است.

اگر به شکل بالا نگاه کنید می بینید رنگ گوشه سمت بالا رنگی به نام Navy که کد آن به صورت است(RGB(0,0,128 ولی کد خروجی خط ۱۶ به صورت معکوس، یعنی (RGB(0,0, 128 است که کد RGB رنگ Maroon است. (به دو شکل زیر و کد و رنگ هر یک نگاه کنید) در واقع می خواهیم بگوییم خط ۱۶ معکوس RGB یعنی BGR را چاپ کرده است. برای رفع مشکل در خط ۱۷ خروجی را به صورت معکوس چاپ کرده ایم.

مثال – ایجاد نویزهای سفید و سیاه

در این بخش به عنوان یک مثال ساده از کاربرد تابع <>at و برای درک بهتر پویش پیکسل ها، می خواهیم نویزی را که به نویز سفید و سیاه (یا نویز نمک و فلفل – Salt and Pepper Noise) معرف است را بر روی تصاویر ایجاد کنیمهدف از این مطلب این است که تصویر را بخوانیم و سپس به صورت تصادفی برخی از پیسکل ها را به پیسکل های سفید و سیاه تبدیل کنیم.

نویز سیاه و سفید، باعث می شود مقدار برخی از پیسکل ها با سفید و مقدار برخی با سیاه جایگزین (عوض) شوند. هر نویزی می تواند در حین انتقال تصویر و در اثر خطای ارتباطی بر روی تصویر رخ دهد. به عبارت ساده نویز باعث عوض شدن مقدار یک پیسکل می شود.

در خط ۸ تابعی به نام saltPepperNoise را اعلان کردیم که دو ورودی دریافت می کند. ورودی اول که به صورت Call by Reference است یک متغیر از نوع کلاس Mat و متغیر دوم تعداد تکرار حلقه است. خود تابع در خطوط ۳۲ تا ۵۰ تعریف شده است.

در خط ۳۴ یک حلقه for به اندازه ورودی دوم تابع (یعنی پارامتر int n) تکرار می شود. در خط های ۳۶ و ۳۷ توسط تابع ()rand از کتابخانه OpenCV و با استفاده از تعداد ستون ها و تعداد سطرهای تصویر ورودی (پارامتر اول)، در هر مرتبه از تکرار دو عدد صحیح ایجاد و در متغیرهای h و w ذخیره می شوند. در واقع h و w مختصات یک پیکسل در تصویر هستند.

در خط های ۳۹ تا ۴۲ بررسی می شود که آیا تصویر ورودی به صورت تک کانالی (خاکستری – Grayscale) است. اگر این طور باشد، توسط خط ۴۱ مقدار یک پیکسل تصادفی تغییر می کند. در خط های ۴۳ تا ۴۸ نیز همین مورد اتفاق می افتد ولی اگر تصویر به صورت سه کانالی (رنگی – RGB) باشد.

برای امتحان برنامه می توانید یکبار یک تصویر رنگی (مانند تصویر بالا) و یکبار تصویر خاکستری (مانند تصویر زیر) را به برنامه بدهید. توجه کنید اگر ابعاد تصویر بزرگتر باشد، شما باید در خط ۱۲ برنامه عددی بزرگتر را به ورودی بدهید.

در خط ۲۲ تابع saltPepperNoise فراخوانی شده است. پیش از آن و در خط ۱۲ متغیر num مقدار دهی شده که به عنوان آرگومان دوم به تابع ارسال می شود و در خط ۱۴ تصویر خوانده و ماتریس آن در متغیر image بارگذازی می شود. توجه کنید که پارامتر اول تابع به صورت فراخوانی با ارجاع (Call by Reference) است، بنابراین پس از اجرای تابع، متغیر image تغییر و به تصویر دارای نویز اشاره می کند. در نهایت در خط ۲۴ توسط تابع ()imshow تصویر نمایش داده می شود و برنامه به خاطر تابع waitKey که با عدد ۳۰۰۰۰ فراخوانی شده است، به مدت ۳۰ ثانیه صبر می کند و سپس پنجره تصویر از بین می رود.