به نام خدا : در این مطلب میخوام درباره ماژول Image Filtering که خودش زیر مجموعه ای از ماژول Image Processing هستش صحبت کنیم؛ تقریبا هر تابع کار جداگانه ای رو انجام میده و توابع ربطی به هم نداره، فلذا بعد هر یک یا چند تابع، یه کد نمونه احتمالا میزارم و ته مطلب هم یه مثال برا تست تمام توابع ( فیلترها )؛ هر چی فکر کردم چه عکسی برا این مطلب قرار بدم چیز خاصی به ذهنم نرسید، سر همین تصویر اهرام مصر رو گزاشتم ( چون تو این مطلب درباره هرم / Pyramid صحبت هایی کردم تو فیلترهای pyrDown و pyrUp )
1) فرق بین type ( نوع ) و depth ( عمق ) و channel ( کانال ) در تصاویر OpenCV چیست؟
type تصویر، depth تصویر، channels تصویر؛ به کمک توابع زیر میتونید مقدار موارد فوق رو برای هر تصویر بخونید :
1 2 3 |
int type = image_input.type(); int depth = image_input.depth(); int channels = image_input.channels(); |
تصاویر از پیکسل ساخته شدن و پیکسل ها از ترکیب چندین کانال ( مثلا تصویر RGB، از ترکیب کانال های R و G و B ایجاد میشه، مثل عکس زیر؛ یا تصاویر RGBA، که یه کانال با نام A، که مخفف ALPHA هستش ساخته شدن که این کانال A، این امکان رو میده که شفافیت پیکس رو تعیین کنیم، قطعا تصاویری رو دیدید که بخشی ازش شفاف/Transparent هستش، مثل آب و یا شیشه! و شونصد مدل کانال دیگه )؛ حالا معمولا تصاویر رنگی 3 کانال دارن ( ممکنه تعداد کانال ها بیشتر هم باشه )، و تصاویر خاکستری ( Grayscale ) یک کانال دارند.
خب حالا تصویر رو یه ماتریس در نظر بگیرید، ماتریسی حاوی داده های پیکسل ها، حالا داده های عددی هر خونه این ماتریس میتونه متفاوت باشه و فرمت های مختلفی داشته باشه، 8 بیتی، 16 بیتی، 32 و 64 بیتی و یا نوعش unsigned int یا signed int و یا float باشه ( به این چیزا میگن depth تصویر )
1) channels : مقدارش بین 1 تا n هستش.
2) depth : در زیر لیست تمام عمق ها رو قرار دادم :
1 2 3 4 5 6 7 8 |
#define CV_8U 0 #define CV_8S 1 #define CV_16U 2 #define CV_16S 3 #define CV_32S 4 #define CV_32F 5 #define CV_64F 6 #define CV_16F 7 |
3) type : خب نوع تصویر در واقع اطلاعات کاملی از تصویر میده، چرا که در واقع type = depth + channels، یعنی type هم درباره depth به ما اطلاعات میده و هم channels!، با ترکیب این دو، انواع مختلف type برا تصاویر ایجاد میشه که در زیر لیستشونو مشاهده میکنید ( برا جزئیات بیشتر به فایل interface.h مراجعه کنید )؛ میبینید که با ترکیب depth ها و تعداد کانال ها، define های جدیدی ایجاد کردن که بهش میگن type :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
#define CV_8UC1 CV_MAKETYPE(CV_8U,1) #define CV_8UC2 CV_MAKETYPE(CV_8U,2) #define CV_8UC3 CV_MAKETYPE(CV_8U,3) #define CV_8UC4 CV_MAKETYPE(CV_8U,4) #define CV_8UC(n) CV_MAKETYPE(CV_8U,(n)) #define CV_8SC1 CV_MAKETYPE(CV_8S,1) #define CV_8SC2 CV_MAKETYPE(CV_8S,2) #define CV_8SC3 CV_MAKETYPE(CV_8S,3) #define CV_8SC4 CV_MAKETYPE(CV_8S,4) #define CV_8SC(n) CV_MAKETYPE(CV_8S,(n)) #define CV_16UC1 CV_MAKETYPE(CV_16U,1) #define CV_16UC2 CV_MAKETYPE(CV_16U,2) #define CV_16UC3 CV_MAKETYPE(CV_16U,3) #define CV_16UC4 CV_MAKETYPE(CV_16U,4) #define CV_16UC(n) CV_MAKETYPE(CV_16U,(n)) #define CV_16SC1 CV_MAKETYPE(CV_16S,1) #define CV_16SC2 CV_MAKETYPE(CV_16S,2) #define CV_16SC3 CV_MAKETYPE(CV_16S,3) #define CV_16SC4 CV_MAKETYPE(CV_16S,4) #define CV_16SC(n) CV_MAKETYPE(CV_16S,(n)) #define CV_32SC1 CV_MAKETYPE(CV_32S,1) #define CV_32SC2 CV_MAKETYPE(CV_32S,2) #define CV_32SC3 CV_MAKETYPE(CV_32S,3) #define CV_32SC4 CV_MAKETYPE(CV_32S,4) #define CV_32SC(n) CV_MAKETYPE(CV_32S,(n)) #define CV_32FC1 CV_MAKETYPE(CV_32F,1) #define CV_32FC2 CV_MAKETYPE(CV_32F,2) #define CV_32FC3 CV_MAKETYPE(CV_32F,3) #define CV_32FC4 CV_MAKETYPE(CV_32F,4) #define CV_32FC(n) CV_MAKETYPE(CV_32F,(n)) #define CV_64FC1 CV_MAKETYPE(CV_64F,1) #define CV_64FC2 CV_MAKETYPE(CV_64F,2) #define CV_64FC3 CV_MAKETYPE(CV_64F,3) #define CV_64FC4 CV_MAKETYPE(CV_64F,4) #define CV_64FC(n) CV_MAKETYPE(CV_64F,(n)) #define CV_16FC1 CV_MAKETYPE(CV_16F,1) #define CV_16FC2 CV_MAKETYPE(CV_16F,2) #define CV_16FC3 CV_MAKETYPE(CV_16F,3) #define CV_16FC4 CV_MAKETYPE(CV_16F,4) #define CV_16FC(n) CV_MAKETYPE(CV_16F,(n)) |
راستی، تصویری که در بالا گزاشتم ( استخراج کانال های تصویر )، به کمک کد زیر ایجاد شدن :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
cv::Mat image_input = cv::imread("C:/Users/Mahdi/Desktop/shahid_ehsan_fathi.jpg"); std::vector<cv::Mat> image_input_channelsB, image_input_channelsG, image_input_channelsR; cv::split(image_input, image_input_channelsB); cv::split(image_input, image_input_channelsG); cv::split(image_input, image_input_channelsR); //--- cv::Size image_input_size = image_input.size(); //--- image_input_channelsB[1] = cv::Mat::zeros(image_input_size, image_input_channelsB[0].type()); image_input_channelsB[2] = cv::Mat::zeros(image_input_size, image_input_channelsB[0].type()); cv::Mat image_input_B; cv::merge(image_input_channelsB, image_input_B); //--- image_input_channelsG[0] = cv::Mat::zeros(image_input_size, image_input_channelsG[0].type()); image_input_channelsG[2] = cv::Mat::zeros(image_input_size, image_input_channelsG[0].type()); cv::Mat image_input_G; cv::merge(image_input_channelsG, image_input_G); //--- image_input_channelsR[0] = cv::Mat::zeros(image_input_size, image_input_channelsR[0].type()); image_input_channelsR[1] = cv::Mat::zeros(image_input_size, image_input_channelsR[0].type()); cv::Mat image_input_R; cv::merge(image_input_channelsR, image_input_R); cv::imwrite("C:/Users/Mahdi/Desktop/B.BMP", image_input_B); cv::imwrite("C:/Users/Mahdi/Desktop/G.BMP", image_input_G); cv::imwrite("C:/Users/Mahdi/Desktop/R.BMP", image_input_R); |
correlation vs convolution
image size vs resolution
overlap
کتابخانه مورد نیاز : برای استفاده از توابع این ماژول، کتابخونه زیر رو باید در پروژتون فراخونی کنید :
1 |
#include <opencv2/imgproc.hpp> |
پارامتر ddepth : در این مطلب، پارامتر ddepth، عمق ( depth ) تصویر خروجی هستش، اگه مقدار -1 بهش بدید، از عمق تصویر ورودی استفاده میکنه و عمق تصویر خروجی باید برابر یا بزرگتر از تصویر ورودی باشه ( کوچکتر نباید باشه )، که در زیر انواع حالتشو میبینید :
پارامتر borderType : در قسمت توضیحات Kernel، توضیحات مختصری درباره این مورد دادم، توضیح کاملش نه جاش تو این مطلبه و نه در حوصله این مطلبه؛ خلاصه این که وقتی فیلترهایی که استفاده میکنیم، میخوان هسته ( Kernel ) رو در تصویر ورودی اعمال کنن، نیاز دارن که یه حاشیه ایی به اندازه 1 پیکسل اطراف عکس ایجاد کنن تا محاسبات رو انجام بدن؛ در کل بزارید همیشه رو همون مقادر پیشفرض بمونه، تو خروجی تاثیرش فقط تو پیکسل های حاشیه ( حاشیه ای به اندازه 1 پیکسل اطراف تصویر ) تصویر خروجی هستش، فلذا متوجه تغییرات نمیشید.
پارامتر anchor : مقدار پیش فرض این پارامتر Point(-1,-1) هستش که یعنی که لنگر در مرکز K ( هسته ) قرار داره؛ ولی خب میتونید تغییرش بدید اگه نیاز بود!
توجه : لینک مربوط به این مطلب ( که انتهای مطلب قرارش دادم )، تابع getGaborKernel رو معرفی کرده، که به کمکش هسته Gabor رو محاسه میکنیم، ولی تابعی برای استفاده از این هسته ارائه نداده، احتمالا در تابع filter2D ازش استفاده میکنیم ( مطمئن نیستم )، برای مطلعه بیشتر دربارش به این لینک ( Gabor Filter ) مراجعه کنید.
1) تابع bilateralFilter : اعمال فیلتر bilateral بر روی تصویر ورودی؛ برای توضیحات کامل درباره این فیلتر، به این لینک ( Bilateral Filtering for Gray and Color Images ) مراجعه کنید؛ این فیلتر می تواند نویزهای ناخواسته را به خوبی کاهش دهد در حالی که لبه ها را نسبتاً تیز نگه می دارد، در مقایسه با اکثر فیلترها بسیار کند است؛ برای سادگی کار میتونید دو پارامتر sigma رو یکسان مقدار دهی کنید؛ برای برنامه های بلادرنگ ( نظیر وبکم ) بهتره که اندازه فیلتر ( d ) برابر با 5 باشه و برای برنامه های آفلاین ( نظیر ویدئو ) مقدار 9 مناسب هستش؛ تعریف این تابع به صورت زیر هستش :
1 2 3 |
CV_EXPORTS_W void bilateralFilter( InputArray src, OutputArray dst, int d, double sigmaColor, double sigmaSpace, int borderType = BORDER_DEFAULT ); |
توضیح پارامترها :
- src : تصویر ورودی 8-بیتی یا float، یک کاناله یا 3 کاناله.
- dst : تصویر خروجی، با اندازه و نوعی برابر با تصویر ورودی.
- d : ( اندازه فیلتر ) قطر همسایگی هر پیکسلی که در هنگام فیلتر کردن استفاده میشه؛ اگر مقدارش منفی باشه، اندازه فیلتر به کمک پارامتر sigmaScape محاسبه میشه!
- sigmaColor : فیلتر سیگما در فضای رنگ؛ مقدار بزرگتر پارامتر به این معنی است که رنگهای دورتر در همسایگی پیکسل ( به پارامتر sigmaSpace مراجعه کنید ) با هم ترکیب میشوند و در نتیجه مناطق بزرگتری از رنگهای نیمه مساوی ( semi-equal ) ایجاد میشود.
- sigmaSpace : فیلتر سیگما در فضای مختصات؛ مقدار بزرگتر پارامتر به این معنی است که پیکسل های دورتر روی یکدیگر تأثیر می گذارند تا زمانی که رنگ آنها به اندازه کافی نزدیک باشد ( به sigmaColor مراجعه کنید ).
- borderType : مورد BORDER_WRAP پشتیبانی نمیشه.
نمونه تصویر از عملکرد فیلتر bilateral :
OpenCV برا بلور / تار کردن تصاویر، فیلترهای blur و boxFilter و GaussianBlur و medianBlur رو ارائه داده است.
2) فیلتر blur : بلور / تار کردن تصویر به کمک متغییر K ( در ادامه بهش پرداختم ) محاسبه و ایجاد میشه؛ تعریف این تابع به صورت زیر هستش :
1 2 3 |
CV_EXPORTS_W void blur( InputArray src, OutputArray dst, Size ksize, Point anchor = Point(-1,-1), int borderType = BORDER_DEFAULT ); |
توضیح پارامترها :
- src : تصویر ورودی؛ هر تعداد کانال میتونه داشته باشه که کانال ها به صورت جداگونه پردازش میشن؛ نوع تصویر باید یکی از این موارد ( CV_8U, CV_16U, CV_16S, CV_32F یا CV_64F ) باشد.
- dst : تصویر خروجی؛ با اندازه و نوعی برابر با تصویر ورودی.
- ksize : به کمک این پارامتر اندازه هسته K ( kernel / هسته ) رو تعیین میکنیم که فرمولش رو در زیر مشاهده میکنید.
- borderType : مورد BORDER_WRAP پشتیبانی نمیشه.
\( \Large{ \texttt{K} = \frac{1}{\texttt{ksize.width * ksize.height}} \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \\ 1 & 1 & 1 & \cdots & 1 \\ \vdots & \vdots & \vdots & \ddots &\vdots \\ 1 & 1 & 1 & \cdots & 1 \\ \end{bmatrix} } \)
دو کد زیر با هم برابر هستن ( فلواقع تابع blur زیر مجموعه ای از تابع boxFilter هستش! ) :
1 2 |
blur(src, dst, ksize, anchor, borderType) boxFilter(src, dst, src.type(), ksize, anchor, true, borderType) |
نمونه تصویر از عملکرد فیلتر blur :
3) فیلتر boxFilter : بلور / تار کردن تصویر، به کمک هسته K ( که فرمولشو در زیر مشاهده میکنید )؛ این فیلتر، زمانی که پارامتر normalize = false میباشد، برای محاسبه ویژگیهای انتگرال مختلف بر روی هر همسایگی پیکسل، مانند ماتریسهای کوواریانس ( covariance matrices ) مشتقات تصویر ( image derivatives ) مفید است ( استفاده شده در الگوریتمهای "جریان نوری متراکم / Dense Optical Flow" و ... )؛ تعریف این تابع به صورت زیر هستش :
1 2 3 4 |
CV_EXPORTS_W void boxFilter( InputArray src, OutputArray dst, int ddepth, Size ksize, Point anchor = Point(-1,-1), bool normalize = true, int borderType = BORDER_DEFAULT ); |
توضیح پارامترها :
- src و dst : تصویر ورودی و خروجی، نوع و اندازه تصویر خروجی برابر با تصویر ورودی هستش.
- ddepth : depth تصویر خروجی ( اگر مقدار -1 بهش بدید، از depth تصویر ورودی استفاده میکنه )
- ksize : اندازه هسته کرنل رو تعیین میکنیم به کمکش ( طبق فرمول زیر )
- normalize : تعیین این که آیا هسته بر اساس مساحت آن نرمال بشه یا نه؛ اگه این پارامتر true باشه و ddepth = src.type() باشه، این فیلتر در واقع همان فیلتر Blur هستش! ( که قبلا صحبتشو کردیم! )
- borderType : مورد BORDER_WRAP پشتیبانی نمیشه.
\( \Large{ \texttt{K} = \alpha \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \\ 1 & 1 & 1 & \cdots & 1 \\ \vdots & \vdots & \vdots & \ddots &\vdots \\ 1 & 1 & 1 & \cdots & 1 \\ \end{bmatrix} } \)
\( \Large{ \alpha = \begin{cases} \frac{1}{\texttt{ksize.width*ksize.height}} & \texttt{when } \texttt{normalize=true} \\1 & \texttt{otherwise}\end{cases} } \)
نمونه تصویر از عملکرد فیلتر box :
4) فیلتر GaussianBlur : بلور / تار کردن تصویر؛ تعریف این تابع به صورت زیر هستش :
1 2 3 |
CV_EXPORTS_W void GaussianBlur( InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY = 0, int borderType = BORDER_DEFAULT ); |
توضیح پارامترها :
- src و dst : تصویر ورودی و خروجی، تعداد کانال تصویر ورودی مهم نی، اما عمقش باید یکی از این موارد ( CV_8U, CV_16U, CV_16S, CV_32F یا CV_64F ) باشه، اندازه و نوع تصویر خروجی برابر با تصویر ورودی هستش.
- ksize : مقدار ksize.width و ksize.height باید مثبت و فرد باشه ( وگرنه تابع محاسبه هسته، cv::createGaussianKernels، خطا میده، ما با این تابع کاری نداریم، تابع GaussianBlur ازش استفاده میکنه! )؛ اگه مقدارش 0 باشه، مقدارش به کمک پارامترهای sigma محاسبه میشه.
- sigmaX : انحراف استاندارد هسته گاوسی در جهت X.
- sigmaY : انحراف استاندارد هسته گاوسی در جهت Y؛ اگه مقدار sigmaY=0 باشه، مقدار sigmaX به sigmaY داده میشه؛ اگه هر دو 0 باشن، از ksize.width و ksize.height برای محاسبه این دو استفاده میکنه! ( برا توضیحات بیشتر به تابع cv::getGaussianKernel مراجعه کنید )
- borderType : مورد BORDER_WRAP، پشتیبانی نمیشه.
نمونه تصویر از عملکرد فیلتر GaussianBlur :
5) فیلتر medianBlur : بلور / تار کردن تصویر؛ این تابع با استفاده از فیلتر میانه با دیافراگم ksize×ksize یک تصویر را صاف/بلور میکند؛ کانال های تصویر ورودی، به صورت جداگانه پردازش میشوند؛ تعریف این تابع به صورت زیر هستش :
1 |
CV_EXPORTS_W void medianBlur( InputArray src, OutputArray dst, int ksize ); |
توضیح پارامترها :
- src : تصویر ورودی باید 1، 3 و یا 4 کاناله باشه؛ وقتی ksize برابر با 3 یا 5 است، عمق تصویر باید CV_8U، CV_16U یا CV_32F باشد، برای اندازههای دیافراگم بزرگتر، فقط میتواند CV_8U باشد.
- dst : اندازه و نوع تصویر خروجی برابر با تصویر ورودی هستش.
- ksize : اندازه خطی دیافراگم؛ باید فرد و بزرگتر از 1 باشد.
نمونه تصویر از عملکرد فیلتر medianBlur :
OpenCV برا عملیات های مورفولوژیکی، تابع morphologyEx رو ارائه داده که توابع dilate و erode، فیلتر های اساسی ( و پایه ییش ) هستند.
Morphological Operations ( عملیات های مورفولوژیکی ) چیست؟
مجموعه ای از عملیات ها که تصاویر را بر اساس اشکال ( shapes ) پردازش میکند؛ عملیات مورفولوژیکی یک عنصر ساختاری ( structuring element ) را به یک تصویر ورودی اعمال میکند و یک تصویر خروجی تولید میکند؛ اساسی ترین عملیات های مورفولوژیکی عبارتند از : Erosion ( فرسایش ) و Dilation ( اتساع )؛ که کاربردهای زیادی دارند، به عنوان مثال :
- حذف نویز
- جداسازی عناصر منفرد ( Isolation of individual elements ) و الحاق عناصر متفاوت ( joining disparate elements ) در یک تصویر.
- یافتن برجستگی های شدت ( intensity bumps ) یا حفره ها ( holes ) در یک تصویر.
مثالی از Erosion و Dilation : تصاویر زیر رو ببینید که عملیات های اتساع و فرسایش چه بلایی سر تصویر ورودی ابردن.
مثالی از morphologyEx : مدهای Dilation و Erosion تنها رو در تصویر زیر دیگه قرار ندادم، مثل تصویر بالا میشن؛ در زیر عملیات های مختلف morphologyEx رو مشاهده میکنید ( برای هر مد، تصویر متناسب با اون مد ایجاد شده تا با عملکرد دقیق اون مد آشنا بشید )
عنصر ساختاری ( structuring element ) : بعد از این که عنصر-ساختاری رو به کمک تابع getStructuringElement ایجاد کردید، باید عنصر-ساختاری رو روی تصویر ورودی اعمال کنیم؛ که خب هر کدوم از توابع dilate و erode، فرمول خاص خودشونو دارن که ذکر مثال از محاسبات برای یه تصویر ساده، خارج از حوصله من هستش! ویدئوهای زیر رو ببینید :
1) ویدئویی از اعمال Dilation ( منبع )
2) ویدئویی از اعمال Erosion ( منبع )
6) تابع getStructuringElement : به کمک این تابع میتونید عنصر-ساختاری مورد نیاز برا فیلتر های dilate و erode و morphologyEx رو ایجاد کنید، تعریف این تابع به صورت زیر هستش :
1 |
CV_EXPORTS_W Mat getStructuringElement(int shape, Size ksize, Point anchor = Point(-1,-1)); |
توضیح پارامترها :
- shape : شکل عنصر-ساختاری؛ به کمک نوع شمارشی cv::MorphShapes مقدار دهی میشود.
- ksize : اندازه عنصر-ساختاری.
- anchor : موقعیت لنگر در عنصر-ساختاری؛ که مقدار پیش فرضش (-1, -1) هستش که یعنی لنگر در مرکز عنصر-ساختاری قرار دارد؛ لنگر فقط در حالت MORPH_CROSS روی داده های عنصر-ساختاری تاثیر گذار هستش ( فلذا در حالت MORPH_RECT و MORPH_ELLIPSE، تغییر لنگر تاثیری رو داده های عنصر-ساختاری نمیذاره )؛ میتونید shape های مختلف رو تست کنید و لنگر رو تغیر بدید و خروجی تابع که در واقع یک تصویر ( MAT ) هستش رو بررسی کنید!؛ از تاثیر لنگر رو داده های عنصر-ساختاری بگذریم، اصل کار لنگر در محاسبات ( اعمال عنصر-ساختاری به تصویر ورودی ) هستش و اونجا تاثیر خودشو در خروجی میذاره.
نوع شمارشی MorphShapes : تعریف این enum به صورت زیر هستش :
1 2 3 4 5 6 |
enum MorphShapes { MORPH_RECT = 0, MORPH_CROSS = 1, MORPH_ELLIPSE = 2 }; |
توضیح گزینه ها :
- MORPH_RECT : عنصر-ساختاری یی که تمام پیکسل هاش 1 هستند.
- MORPH_CROSS : عنصر-ساختاری یی که تمام پیکسل هاش 0 هستند مگر پیکسل هایی که موقعیت سطر یا ستونشون برابر با anchor باشه. ( که خب اگه ksize رو مربعی تنظیم کنید، شکلی حاوی مقادیر 1 که شبیه + هستند ایجاد میشه و اگه مستطیلی تنظیم کنید، مقدار 1 شکل ما، شبیح صلیب میشوند )
- MORPH_ELLIPSE : یک عنصر-ساختاری یی بیضوی شکل ایجاد میکنه ( پیکسل های داخل بیضی، 1 و بقیه پیکسل ها 0 هستند )
کد نمونه : برای ایجاد عناصر-ساختاری میتونید از کد زیر استفاده کنید و با پارامترهای ورودی تابع بازی کنید تا شکل ایجاد شده در حالت های مختلف رو ببینید و متوجه توضیحات من بشید :
1 2 3 4 |
cv::Mat structuring_element = cv::getStructuringElement(cv::MorphShapes::MORPH_CROSS, cv::Size(5,5), cv::Point(-1,-1)); cv::namedWindow("dst", cv::WindowFlags::WINDOW_GUI_EXPANDED); cv::resizeWindow("dst", 350, 400); cv::imshow("dst", structuring_element); |
چند تا تصویر میزارم از حالت های مختلف MorphShapes و anchor ( تغییری تو اندازه عنصر-ساختاری نمیدم ) :
توجه : شاید براتون سوال باشه که چطوری تصاویر رو اینطوری نشون میدم که میتونیم مقدار پیکسل ها رو هم ببینیم؛ برای اینکار باید QT رو به OpenCV اضافه کنید، که آموزششو در این مطلب ( آموزش نصب QT در OpenCV با CMake در ویندوز ) قرار دادم قبلا.
7) فیلتر dilate : گشاد ( dilate ) کردن تصویر ورودی؛ این فیلتر را میتوان چندین دفعه روی تصویر اعمال کرد ( iterations )؛ در تصاویر چند کاناله، هر کانال به طور مستقل پردازش میشه؛ تعریف این تابع به صورت زیر هستش :
1 2 3 4 |
CV_EXPORTS_W void dilate( InputArray src, OutputArray dst, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar& borderValue = morphologyDefaultBorderValue() ); |
توضیح پارامترها ( توضیح پارامترهای فیلترهای dilate و erode یکسان هستش ) :
- src و dst : تصویر ورودی و خروجی؛ تعداد کانال های تصویر ورودی مهم نی، اما عمق تصویر ورودی باید یکی از این موارد ( CV_8U, CV_16U, CV_16S, CV_32F یا CV_64F ) باشه؛ نوع و اندازه تصویر خروجی برابر با تصویر ورودی هستش.
- kernel : این پارامتر هسته فیلر فوق هستش؛ این پارامتر از نوع cv::Mat هستش که به کمک تابع cv::getStructuringElement محاسبه میشه.
- iterations : تعداد دفعاتی که فیلتر فیلتر dilate اعمال میشه رو تصویر ورودی.
- borderType : مورد BORDER_WRAP پشتیبانی نمیشه.
- borderValue : مقدار border، برا حالتی که مقدار borderType رو روی BORDER_CONSTANT تنظیم کردید؛ برای محاسبه این پارامتر، از تابع cv::morphologyDefaultBorderValue میتونید استفاده کنید.
نمونه تصویر از عملکرد فیلتر dilate :
8) فیلتر erode : فرسایش ( erode ) دادن تصویر ورودی؛ این فیلتر را میتوان چندین دفعه روی تصویر اعمال کرد ( iterations )؛ در تصاویر چند کاناله، هر کانال به طور مستقل پردازش میشه؛ تعریف این تابع به صورت زیر هستش :
1 2 3 4 |
CV_EXPORTS_W void erode( InputArray src, OutputArray dst, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar& borderValue = morphologyDefaultBorderValue() ); |
پارامترهای این تابع، عینا مثل تابع dilate هستش.
نمونه تصویر از عملکرد فیلتر erode :
9) فیلتر morphologyEx : تبدیل های مورفولوژیکی پیشرفته را انجام میدهد؛ این تابع می تواند تبدیلات مورفولوژیکی پیشرفته را با استفاده از erosion و dilation به عنوان عملیات اساسی انجام دهد؛ در مورد تصاویر چند کاناله، هر کانال به طور مستقل پردازش می شود؛ تعریف این تابع به صورت زیر هستش :
1 2 3 4 5 |
CV_EXPORTS_W void morphologyEx( InputArray src, OutputArray dst, int op, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar& borderValue = morphologyDefaultBorderValue() ); |
توضیح پارامترها :
- src : تصویر ورودی؛ تعداد کانال مهم نی، اما عمق باید یکی از این موارد ( CV_8U، CV_16U، CV_16S، CV_32F یا CV_64F ) باشه.
- dst : تصویر خروجی؛ اندازه و نوع برابر با تصویر ورودی هستش.
- op : نوع عملیات مورفولوژیکی، به نوع-شمارشی cv::MorphTypes مراجعه کنید.
- kernel : عنصر ساختاری؛ می توان آن را با استفاده از تابع getStructuringElement ایجاد کرد.
- iterations : تعداد دفعاتی که erosion و dilation اعمال میشود.
MorphTypes : تعریف نوع-شمارشی cv::MorphTypes به صورت زیر هستش :
1 2 3 4 5 6 7 8 9 10 11 |
enum MorphTypes { MORPH_ERODE = 0, MORPH_DILATE = 1, MORPH_OPEN = 2, MORPH_CLOSE = 3, MORPH_GRADIENT = 4, MORPH_TOPHAT = 5, MORPH_BLACKHAT = 6, MORPH_HITMISS = 7 }; |
توضیح آیتم ها :
MORPH_ERODE : فیلتر erode روی تصویر اعمال شود.
MORPH_DILATE : فیلتر dilate روی تصویر اعمال شود.
MORPH_OPEN : ابتدا فیلتر erode و سپس فیلتر dilate روی تصویر اعمال شود.
\( \large{ \texttt{dst} = \mathrm{open} ( \texttt{src} , \texttt{element} )= \mathrm{dilate} ( \mathrm{erode} ( \texttt{src} , \texttt{element} )) } \)
MORPH_CLOSE : ابتدا فیلتر dilate و سپس فیلتر erode روی تصویر اعمال شود.
\( \large{ \texttt{dst} = \mathrm{close} ( \texttt{src} , \texttt{element} )= \mathrm{erode} ( \mathrm{dilate} ( \texttt{src} , \texttt{element} )) } \)
MORPH_GRADIENT : حاصل تفریق erode از dilate.
\( \large{ \texttt{dst} = \mathrm{morph\_grad} ( \texttt{src} , \texttt{element} )= \mathrm{dilate} ( \texttt{src} , \texttt{element} )- \mathrm{erode} ( \texttt{src} , \texttt{element} ) } \)
MORPH_TOPHAT : حاصل تفریق open از تصویر ورودی.
\( \large{ \texttt{dst} = \mathrm{tophat} ( \texttt{src} , \texttt{element} )= \texttt{src} - \mathrm{open} ( \texttt{src} , \texttt{element} ) } \)
MORPH_BLACKHAT : حاصل تفریق تصویر ورودی از close.
\( \large{ \texttt{dst} = \mathrm{blackhat} ( \texttt{src} , \texttt{element} )= \mathrm{close} ( \texttt{src} , \texttt{element} )- \texttt{src} } \)
MORPH_HITMISS : فقط برای تصاویر باینری CV_8UC1 پشتیانی میشود؛ برا مطالعه بیشتر به این مطلب ( Hit-or-Miss ) مراجعه کنید.
توجه : در این لینک ( Types of Morphological Operations ) مثال هایی از مدهای مختلف این فیلتر مشاهده میکنید.
OpenCV برای ایجاد یه فیلتر خطی دلخواه، تابع filter2D رو ارائه داده، فقط کافیه Kernel ( هسته ) مورد نظرمون رو طراحی کنیم و به تابع فوق بدیم.
Kernel ( هسته ) چیست؟
در پردازش تصویر به ماتریس 2 بعدی که روی تصویر اعمال میشود، Kernel ( هسته، کرنل )، convolution matrix ( ماتریس کانولوشن )، convolution kernel ( هسته کانولوشن ) گفته میشود، که خب بیشتر معروفه به Kernel ( هسته )؛ Kernel یک ماتریس M×N ( معمولا 3×3 یا 5×5 ) است ( M و N، اعدادی فرد هستند، 1، 3، 5 و... ).
توجه 1 : یه ماتریس 2 بعدی دیگه با نام mask هم داریم که هم اندازه تصویر ورودی هستش و رو تصاویر اعمال میشه که خب بحثش جداس.
توجه 2 : هسته 3 بعدی هم میتونیم داشته باشیم که کاربردهای خاص خودشو داره و ربطی به این مطلب نداره.
anchor ( لنگر ) چیست؟
به نقطه ای از هسته که محاسبات حول اون نقطه انجام میشه، لنگر ( anchor ) میگن، که خب معولا ( تو این مطلب همیشه، مگه این که تغییرش بدید ) در وسط هسته قرار داره.
نحوه اعمال Kernel به تصویر ورودی؟ ( به این عملیات میگن convolution / کانولوشن ! )
ابتدا سلول ها را نظیر به نظیر در هم ضرب میکنیم، سپس مقدار تمام سلول ها رو با هم جمع میکنیم که میشه مقدار خونه جدید ( میدونم که متوجه نشدید با این توضیحات بد من، فلذا عکس های زیر رو ایجاد کردم، امیدوارم که متوجه بشید )؛ برای تمام سلول ها این کار رو میکنیم، الا سلول های حاشیه تصویر ( در حالت افقی : A00 تا A07 و A70 تا A77؛ در حالت عمودی : A00 تا A70 و A07 تا A77 ) که به روشی که در عکس های زیر میبینید، قابل محاسبه نیست.
توجه 1 : برا بحث ماتریس ها، معمولا از کلمه «درایه» استفاده میکنن تا «سلول»، در هر صورت فرقی زیاد نداره. ( به هر یک از عناصر ماتریس، درایه / سلول میگیم؛ پیکسل هم میتونیم بگیم در واقع، چون ماتریس فوق حاوی داده های پیکسل ها هستش دیگه! )
توجه 2 : هسته ای که در زیر میبینید، من در آوردی هستش!؛ در انتهای آموزش Kernel، لیستی از هسته های معروفو قرار میدم.
توجه 3 : در تصاویر زیر، در ماتریس Kernel، لنگر ( anchor ) رو با رنگ زرد مشاهده میکنید ( ماتریس وسط تصاویر منظورمه، بالاش هم نوشتم Kernel !!! )؛ یکم فکر کنید که اگه موقعیت لنگر رو تغییر بدید، تو بحث ایجاد border ( حاشیه؛ که در ادامه مطلب دربارش صحبت کردم ) باید چیکار کنیم تا تو محاسبات به مشکل بر نخوریم!
محاسبه سلول A11 :
محاسبه سلول A12 :
محاسبه سلول A66 :
خب حالا مقدار سلول های کناری که در بالا لیستشونو قرار داد رو چطوری محاسبه کنیم؟ خب قبل از اینکار یه توضیحی درباره تابع copyMakeBorder بدم؛ به کمک این تابع میتونیم یه border ( حاشیه ) به اندازه پیکسلی دلخواه ایجاد کنیم ( در اینجا ما نیاز به 1 پیکل حاشیه داریم ) که خب مقدار پیکسل های فوق رو به کمک نوع شمارشی cv::BorderTypes تعیین میکنیم که چطوری محاسبه بشن؛ توضیحات بیشتر نیازی نی! ( برای مطالعه بیشتر درباره تابع copyMakeBorder، به این مطلب مراجعه کنید : آموزش ماژول Operations on arrays در OpenCV )
حالا به کمک تابع copyMakeBorder میایم 1 پیکسل حاشیه اطراف تصویر ورودی ایجاد میکنیم تا محاسبات مربوط به پیکسل های حاشیه ای رو طبق روال قبل انجام بدیم؛ بعد که محاسبات رو انجام دادیم، 1 پیکسل حاشیه ایجاد شده در تصویر ورودی رو حذف میکنیم. ( چون دیگه نیازی بهشون نداریم )
در بالا گفتم که برای ایجاد border ( حاشیه ) چندین روش وجود داره که لیستشون رو در نوع شمارشی cv::BorderTypes میتونید ببینید؛ تعریف این enum به صورت زیر هستش :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//! Various border types, image boundaries are denoted with `|` //! @see borderInterpolate, copyMakeBorder enum BorderTypes { BORDER_CONSTANT = 0, //!< `iiiiii|abcdefgh|iiiiiii` with some specified `i` BORDER_REPLICATE = 1, //!< `aaaaaa|abcdefgh|hhhhhhh` BORDER_REFLECT = 2, //!< `fedcba|abcdefgh|hgfedcb` BORDER_WRAP = 3, //!< `cdefgh|abcdefgh|abcdefg` BORDER_REFLECT_101 = 4, //!< `gfedcb|abcdefgh|gfedcba` BORDER_TRANSPARENT = 5, //!< `uvwxyz|abcdefgh|ijklmno` BORDER_REFLECT101 = BORDER_REFLECT_101, //!< same as BORDER_REFLECT_101 BORDER_DEFAULT = BORDER_REFLECT_101, //!< same as BORDER_REFLECT_101 BORDER_ISOLATED = 16 //!< do not look outside of ROI }; |
بعد از این که تصمیم گرفتید که از کدوم BorderTypes استفاده کنید و مقدار پیکسل های Border رو محاسبه کردید، میاید همون عملیاتی که دید ( تو اون 3 تا عکسی که گزاشتم ) رو اجرا میکنید؛ خب حالا چطوری مقدار پیکسل های Border رو محاسبه کنیم؟ نحوه اینکار در توضیحات enum فوق گفته شده ( اگه متوجه نشدید که باید صبر کنید تا مطلب Operations on arrays رو در سایت قرار بدم و تابع copyMakeBorder رو کامل توضیح بدم و لینکشو بزارم اینجا! )
در مثالی که میخوام برا Kernel قرار بدم، از تابع filter2D استفاده میکنم که Kernel رو بهش میدیم و نوع BorderTypes رو هم براش تعیین میکنیم و تصویر خروجی رو محاسبه میکنه و به ما میده ( filter2D یه فیلتر ساده که به کمکش هر فیلتری میتونیم بسازیم! که خروجی فقط بستگی به هسته ای که طراحی کردیم داره و البته مقدار BorderTypes !!! ).
کد نمونه : تصاویری که در بالا دید، به کمک کد زیر ایجاد شدن :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
// create image! [ float = CV_32FC1, uchar = CV_8UC1 ] float src_data[8][8] = { {3,0,1,5,0,3,0,3}, {2,6,2,4,3,0,3,0}, {2,4,1,0,6,1,4,1}, {3,0,1,5,0,3,0,2}, {2,6,2,4,3,2,3,0}, {2,4,1,0,6,2,1,1}, {2,6,2,4,4,0,3,6}, {2,4,1,0,6,1,6,1}, }; cv::Mat src = cv::Mat(8, 8, CV_32FC1, src_data); //--- cv::imshow("src", src); //--- for(int i=0; i<src.cols; i++) { QString row = ""; for(int j=0; j<src.rows; j++) { row += QString::number(src.at<float>(i, j)) + ","; } qDebug() << row; } qDebug() << "------------------------"; float kernel_data[3][3] = { {0,1,0}, {1,0,1}, {0,1,0}, }; cv::Mat kernel = cv::Mat(3, 3, CV_32FC1, kernel_data); //--- int borderType = cv::BorderTypes::BORDER_DEFAULT; cv::Mat dst; cv::filter2D(src, dst, -1, kernel, cv::Point(-1, -1), 0, borderType); //--- cv::imshow("dst", dst); //--- for(int i=0; i<dst.cols; i++) { QString row = ""; for(int j=0; j<dst.rows; j++) { row += QString::number(dst.at<float>(i, j)) + ","; } qDebug() << row; } qDebug() << "------------------------"; |
سایت جالب : خو یه سایت جالبی پیدا کردم که همین چیزایی که تو عکس های بالا نشون دادم ( کانولوشن ) رو به صورت آنلاین اومده اجرا کرده و کدنویسی کرده : Image Kernels
کاربرد Kernel در این مطلب؟
تنها تابعی که اجازه میده هسته براش تعیین کنیم، تابع filter2D هستش؛ البته توابع dilate و erode هم این امکان رو به ما میدن که خب اونا تابع مجزا خودشون رو برای محاسبه هسته دارن و نیازی نی ما دستی هسته ای براشون تعریف کنیم.
10) فیلتر filter2D : به کمک این تابع، میتونیم یه فیلتر خطی دلخواه ایجاد کنیم!؛ درباره نحوه کار این تابع، توضیحات مربوط به Kernel که اول مطلب دادم رو بخونید، این تابع میاد تمام اون محاسبات مربوط به اعمال هسته به تصویر ورودی و محاسبه تصویر خروجی ( عملیات convolution / کانولوشن ) رو برامون انجام میده.
تعریف این تابع به صورت زیر هستش :
1 2 3 |
CV_EXPORTS_W void filter2D( InputArray src, OutputArray dst, int ddepth, InputArray kernel, Point anchor = Point(-1,-1), double delta = 0, int borderType = BORDER_DEFAULT ); |
توضیح پارامترها :
- src و dst : تصویر ورودی و خروجی.
- ddepth : عمق تصویر خروجی ( اگر مقدار -1 بهش بدید، از عمق تصویر ورودی استفاده میکنه )
- kernel : هسته convolution ( یا بهتره بگیم : هسته correlation )؛ یک ماتریس تک کانال از نوع اعشاری؛ اگر میخواید هسته های متفاوتی رو در هر کانال تصویر اعمال کنید، میتویند کانال های تصویر رو از همدیگه جدا کنید ( به کمک تابع split ) و...
- delta : مقدار اختیاری به پیکسل های فیلتر شده قبل از ذخیره آنها در تصویر خروجی اضافه می شود.
- borderType : مورد BORDER_WRAP، پشتیبانی نمیشه.
نمونه تصویر از عملکرد فیلتر filter2D : چون تعداد تصاویر زیاده دیگه تصویر قرار نمیدم ( بخوام هم اندازشونو کوچیک کنم، برا بعضی هسته ها، متوجه تفاوت تصویر ورودی و خروجی نمیشید )، به کدهایی که برا این تابع نوشتم مراجعه کنید ( انتهای مطلب قرار میدم )
لیستی از هسته های موجود در اینترنت! : تو این لینک ( Kernel ) چند Kernel موجود داره که برا کد این قسمت ازشون استفاده کردم.
OpenCV فیلترهای pyrDown و pyrUp را برای محاسبه، هرم گاوسی ( Gaussian Pyramid ) تصاویر ارائه داده؛ تابع buildPyramid هم برای محاسبه کل هرم بکار میره. ( فیلترهای فوق برای محاسبه 1 سطح بکار میرن که باید چندین بار فراخونی بشن تا کل سطوح یه هرم کامل تشکیل بشه، که تابع buildPyramid هم دقیقا همین کارو میکنه )
تئوری : در بحث پردازش تصویر، معمولا با تصاویر با اندازه ثابت کار میکنیم؛ اما در برخی موارد، باید با تصاویر ( یکسان ) با وضوح ( resolution ) متفاوت کار کنیم؛ به عنوان مثال، هنگام جستجوی چیزی در یک تصویر ( مثلا چهره )، مطمئن نیستیم که شی در چه اندازه ای در تصویر مذکور وجود خواهد داشت؛ در این صورت، باید مجموعه ای از تصاویر با وضوح های مختلف ایجاد کنیم و در همه آنها شی رو جستجو کنیم ( همشون 1 تصویر هستن، فقط وضوحشون با هم فرق داره! )؛ این مجموعه از تصاویر با وضوح های مختلف رو هرم تصویر ( Image Pyramid ) میگوند.
تغییر وضوح ( resolution ) : خب تا اینجا دیدیم که بعضا نیاز داریم که resolution تصویر رو تغییر بدیم، برای این کار 2 گزینه داریم :
- بزرگ نمایی : برای این حالت از upsample ( نمونه بالا، فرا نمونه برداری، نمونه برداری افزایشی ) استفاده میکنیم که تابع pyrUp برای اینکار هستش.
- کوچک نمایی : برای این حالت از downsample ( نمونه پایین، فرو نمونه برداری، نمونه برداری کاهشی ) استفاده میکنیم که تابع pyrDown برای اینکار هستش.
توجه 1 : البته حالت های دیگه ای برای تغیر اندازه تصاویر داریم ( مثلا توابع توضیح داده شده داخل این مطلب : آموزش ماژول Geometric Image Transformations در OpenCV ) که خب ربطی به این قسمت نداره و کاری بهشون نداریم.
توجه 2 : یکی از 2 تا مطلب آموزش این قسمت در سایت OpenCV این بحث بزرگ نمایی از zoom in و برا کوچک نمایی از zoom out استفاده کردم که زیاد صحیح نی بنظرم ( مطمئن نیستم )، چون در حالت upsample، پیکسل هایی به تصویر اضافه میکنیم و تو حالت downsample، پیکسل هایی رو حذف میکنیم؛ تغییری در اندازه پیکسل ها نمیدیم، چون تو حالت zoom in و zoom out، چیزی از تصویر کم یا بهش اضافه نمیشه بلکه اندازه پیکسل ها بزرگتر و یا کوچکتر میشن فک کنم، نمیدونم شاید دارم اشتباه میگم...
هرم تصویر ( Image Pyramid ) : هرم تصویر مجموعه ای از تصاویر هستش؛ که تمام این تصاویر، از تصویر اصلی ایجاد شدن؛ که به طور متوالی downsample میشوند تا زمانی که به نقطه توقف دلخواه برسیم. ( البته همیشه تصویر اصلی، بزرگترین تصویر نیست، منظورم اینه که تصویر اصلی همیشه در پایین ترین سطح هرم قرار نداره؛ بعضا تصویر اصلی در وسطای هرم قرار میگیره که خب نیازه تصاویر سطوح بالاتر از تصویر اصلی، downsample بشن و اندازشون کوچکتر از تصویر اصلی بشه و تصاویر سطوح پایینتر از تصویر اصلی، upsample بشن و اندازشون بزرگتر از تصویر اصلی بشه )
انواع اهرام تصویری : دو نوع متداول از اهرام تصویری وجود دارد :
- هرم Gaussian : توابع pyrDown و pyrUp، از این روش استفاده میکنند.
- هرم Laplacian
هرم Gaussian : هرم را مجموعه ای از لایه ها تصور کنید که هر چه لایه بالاتر باشد، اندازه آن کوچکتر است.
لایه ها، از پایین هرم به سمت انتهای هرم، شماره گزاری میشن؛ بنابراین لایه \( (i+1) \) که با \( G_{i+1} \) نمایش داده میشود، کوچکتر از لایه \( G_{i} \) هستش.
downsample : برای تولید لایه \( (i+1) \) در هرم Gaussian، دو مورد زیر را انجام میدهیم :
1) \( G_{i} \) را با هسته Gaussian در هم بپیچید ( Convolve ) :
\( \Large{ \frac{1}{256} \begin{bmatrix} 1 & 4 & 6 & 4 & 1 \\ 4 & 16 & 24 & 16 & 4 \\ 6 & 24 & 36 & 24 & 6 \\ 4 & 16 & 24 & 16 & 4 \\ 1 & 4 & 6 & 4 & 1 \end{bmatrix} } \)
2) هر سطر و ستون با شماره زوج را حذف کنید؛ اندازه تصویر جدید به صورت زیر محاسبه میشه :
1 |
cv::Size dstsize = cv::Size((src.cols + 1) / 2, (src.rows + 1) / 2); |
به راحتی می توانید متوجه شوید که تصویر حاصل دقیقاً یک چهارم مساحت تصویر قبلی خود خواهد بود؛ تکرار این فرآیند روی تصویر ورودی \( G_{0} \) ( تصویر اصلی ) کل هرم را تولید میکند.
upsample : روش بالا برای downsample از یک تصویر مفید بود؛ اگر بخواهیم آن را بزرگتر کنیم چه می شود؟
1) اندازه تصویر رو در هر بعد 2 برابر کنید، با ردیف های زوج جدید! ( که خب مقدارشون 0 هستش )؛ اندازه تصویر جدید به صورت زیر محاسبه میشه :
1 |
cv::Size dstsize = cv::Size(src.cols * 2, src.rows * 2); |
2) یک کانولوشن با همان هسته نشان داده شده در بالا ( ضرب در 4 ) انجام دهید تا مقادیر "پیکسل های از دست رفته" به صورت تقریبی محاسبه بشن.
توجه : وقتی اندازه یک تصویر را کاهش می دهیم، در واقع بخشی از اطلاعات تصویر را از دست میدهیم! فلذا وقتی عمل بزرگ و کوچیک کردن تصویر رو به این قسمت انجام بدید، بنظر میاد که تصویر کیفیت اولیه خودشو نداره.
توابع : این دو رویه ( downsampling و upsampling همانطور که در بالا توضیح داده شد ) توسط توابع pyrUp و pyrDown پیاده سازی میشوند.
11) فیلتر pyrDown : تعریف این تابع به صورت زیر هستش :
1 2 3 4 5 |
CV_EXPORTS_W void pyrDown( InputArray src, OutputArray dst, const Size& dstsize = Size(), int borderType = BORDER_DEFAULT ); |
توضیح پارامترها :
- src : تصویر ورودی.
- dst : تصویر خروجی؛ نوعش برابر با تصویر ورودی هستش.
- dstsize : اندازه تصویر خروجی؛ درباره اندازه تصویر خروجی در بالا صحبت کردیم ( بهتره از این پارامتر استفاده نکنید! )
- پارامتر borderType : مورد BORDER_CONSTANT پشتیبانی نمیشود.
12) فیلتر pyrUp : این تابع مرحله upsampling از ساخت هرم Gaussian را انجام می دهد، اگرچه در واقع می توان از آن برای ساخت هرم Laplacian استفاده کرد؛ ابتدا، تصویر منبع را با تزریق صفر در ردیف و ستون های زوج، upsamples می کند و سپس نتیجه را با همان هسته در pyrDown ضرب در 4 میکند؛ تعریف این تابع به صورت زیر هستش :
1 2 3 4 5 |
CV_EXPORTS_W void pyrUp( InputArray src, OutputArray dst, const Size& dstsize = Size(), int borderType = BORDER_DEFAULT ); |
توضیح پارامترها :
- src : تصویر ورودی.
- dst : تصویر خروجی؛ نوعش برابر با تصویر ورودی هستش.
- dstsize : اندازه تصویر خروجی؛ درباره اندازه تصویر خروجی در بالا صحبت کردیم. ( بهتره از این پارامتر استفاده نکنید! )
- borderType : فقط مورد BORDER_DEFAULT پشتیبانی میشود!
کد نمونه : کد نمونه ای برای توابع pyrDown و pyrUp
1 2 3 4 5 6 7 8 9 10 |
cv::Mat src = cv::imread("C:/Users/Mahdi/Desktop/Shahid_Ehsan_Fathi.jpg"); cv::imshow("1) src [ G_{i} ]", src); cv::Mat pyrdown_dst; cv::pyrDown(src, pyrdown_dst); cv::imshow("2) pyrDown [ G_{i-1} ]", pyrdown_dst); cv::Mat pyrup_dst; cv::pyrUp(src, pyrup_dst); cv::imshow("3) pyrup_src [ G_{i+1} ]", pyrup_dst); |
نتیجه کد بالا : ابعاد تصویر pyrup_dst خیلی بزرگ میشه، بهتره خودتون تست کنید، دیگه خبری از نتیجه نی ^_^
13) تابع buildPyramid : هرم گاوسی ( Gaussian pyramid ) رو برای تصویر ورودی ایجاد میکنه؛ این تابع، با تکرار چندین باره تابع pyrDown، هرم فوق رو ایجاد میکنه ( که خب تصویر ورودی هر مرحله، در واقع تصویر خروجی مرحله قبل هستش )
1 2 3 4 5 |
CV_EXPORTS void buildPyramid( InputArray src, OutputArrayOfArrays dst, int maxlevel, int borderType = BORDER_DEFAULT ); |
توضیح پارامترها :
- src : تصویر ورودی.
- dst : تصاویر خروجی؛ تعداد تصاویر برابر با « maxlevel+1 » هستش؛ اولین تصویر خروجی ( خونه 0 ام! )، درواقع همان تصویر ورودی هستش ( تصویر ورودی، اولین سطح هرم ما هستش دیگه! )
- maxlevel : تعداد لایه های هرم.
- borderType : مورد BORDER_CONSTANT پشتیبانی نمیشه.
کد نمونه : این کد هم برا تست تابع buildPyramid ( با فشردن کلید «N» تصویر خروجی بعدی، نمایش داده میشه. ) :
1 2 3 4 5 6 7 8 9 10 |
cv::Mat src = cv::imread("C:/Users/Mahdi/Desktop/Shahid_Ehsan_Fathi.jpg"); std::vector<cv::Mat> dst; cv::buildPyramid(src, dst, 5); for (int i=0; i<dst.size(); i++) { //cv::imshow("dst [ G_{" + std::to_string(i) + "} ]", dst[i]); cv::imwrite("C:/Users/Mahdi/Desktop/dst [ G_{" + std::to_string(i) + "} ].jpg", dst[i]); } |
نتیجه کد بالا :
14) فیلتر pyrMeanShiftFiltering : مرحله اولیه meanshift segmentation یک تصویر را انجام میدهد؛ توضیحات کامل....
تعریف این تابع به صورت زیر هستش :
1 2 3 |
CV_EXPORTS_W void pyrMeanShiftFiltering( InputArray src, OutputArray dst, double sp, double sr, int maxLevel = 1, TermCriteria termcrit=TermCriteria(TermCriteria::MAX_ITER+TermCriteria::EPS,5,1) ); |
توضیح پارامترها :
- src : تصویر ورودی؛ 8-بیتی و 3 کاناله.
- dst : تصویر خروجی؛ عمق و اندازش مثل تصویر ورودی هستش.
- sp : شعاع پنجره فضایی ( spatial window radius ).
- sr : شعاع پنجره رنگی ( color window radius ).
- maxLevel : حداکثر سطح هرم برای تقسیم بندی.
- termcrit : معیارهای خاتمه؛ چه زمانی باید تکرارهای meanshift را متوقف کرد.
نمونه تصویر از عملکرد فیلتر pyrMeanShiftFiltering :
OpenCV سه نوع از فیلترهای gradient یا فیلترهای High-pass ( بالا گذر ) : Sobel، Scharr و Laplacian ارائه داده است؛ که برا پیدا کردن لبه ها ( edges ) و… بکار میروند. ( فیلتر spatialGradient هم زیر مجموعه ای از فیلتر Sobel هستش فلواقع؛ هرچند فیلتر Scharr هم یه جورایی زیر مجموعه Sobel هستش! )
15) فیلتر Scharr : اولین مشتق x یا y تصویر را محاسبه میکند؛ تعریف این تابع به صورت زیر هستش :
1 2 3 |
CV_EXPORTS_W void Scharr( InputArray src, OutputArray dst, int ddepth, int dx, int dy, double scale = 1, double delta = 0, int borderType = BORDER_DEFAULT ); |
توضیح پارامترها :
- src : تصویر ورودی.
- dst : تصویر خروجی؛ اندازه و تعداد کانال مثل تصویر ورودی هستش.
- ddepth : عمق تصویر خروجی.
- dx و dy : ترتیب مشتق x و y؛ مقدار هر یک از این دو پارامتر یا 0 و یا 1 باید باشه و مجموعشون 1 باید باشه ( یعنی همیشه یکیشون باید 1 باشه )
- scale : ضریب مقیاس اختیاری برای مقادیر مشتق محاسبه شده؛ به طور پیش فرض، هیچ مقیاسی اعمال نمی شود. ( برای جزئیات بیشتر به تابع cv::getDerivKernels مراجعه کنید. )
- delta : مقدار دلتا اختیاری که به نتایج قبل از ذخیره آنها در dst اضافه می شود.
- borderType : آیتم BORDER_WRAP پشتیبانی نمیشه.
دو کد زیر با هم برابر هستند ( که یعنی تابع Scharr زیر مجموعه ای از تابع Sobel هستش! ) :
1 2 |
Scharr(src, dst, ddepth, dx, dy, scale, delta, borderType) Sobel (src, dst, ddepth, dx, dy, FILTER_SCHARR, scale, delta, borderType) |
نمونه تصویر از عملکرد فیلتر Scharr :
16) فیلتر Sobel : محاسبه مشتقات اول، دوم، سوم و یا ترکیبی برای تصویر ورودی؛ در همه موارد به جز یک مورد، از هسته قابل تفکیک ksize×ksize برای محاسبه مشتق استفاده میشود؛ وقتی ksize = 1، هسته 3×1 یا 1×3 استفاده می شود ( یعنی صاف کردن گاوسی انجام نمی شود )؛ ksize = 1 را فقط می توان برای اولین یا دومین مشتق x یا y استفاده کرد؛ همچنین مقدار ویژه ksize = -1 ( مقدار enum اش میشه FILTER_SCHARR ) وجود دارد که مربوط به فیلتر Scharr 3×3 است که ممکن است نتایج دقیق تری نسبت به Sobel 3×3 ارائه دهد.
تعریف این تابع به صورت زیر هستش :
1 2 3 4 |
CV_EXPORTS_W void Sobel( InputArray src, OutputArray dst, int ddepth, int dx, int dy, int ksize = 3, double scale = 1, double delta = 0, int borderType = BORDER_DEFAULT ); |
توضیح پارامترها :
- src : تصویر ورودی.
- dst : تصویر خروجی؛ با اندازه و تعداد کانالی برابر با تصویر ورودی.
- dx و dy : ترتیب مشتق x و y ( order of the derivative x/y )؛ هر دو مقدار باید بزرگتر مساوی 0 باشن و مجموعشون بزرگتر مساوی 1 ( dx >= 0 && dy >= 0 && dx+dy > 0 )
- ksize : اندازه هسته Sobel؛ یکی از مقادیر 1، 3، 5 و یا 7 میتونه باشه.
- scale : ضریب مقیاس اختیاری برای مقادیر مشتق محاسبه شده؛ به طور پیش فرض، هیچ مقیاسی اعمال نمی شود ( برای جزئیات بیشتر به تابع cv::getDerivKernels مراجعه کنید. )
- delta : مقداری اختیاری که به نتایج قبل از ذخیره شدن در تصویر خروجی، اضافه میشود.
- borderType : آیتم BORDER_WRAP پشتیبانی نمیشه.
نمونه تصویر از عملکرد فیلتر Sobel :
17) فیلتر Laplacian : محاسبه لاپلاس تصویر ورودی؛ لاپلاس تصویر ورودی، با جمع کردن مشتقات x و y دوم محاسبه شده با استفاده از تابع Sobel محاسبه میشود ( که فرمولش به صورت زیر هستش ) :
\( \Large{ dst = \Delta src = \frac{\partial^2 \texttt{src}}{\partial x^2} + \frac{\partial^2 \texttt{src}}{\partial y^2} } \)
زمانی از فرمول فوق استفاده می شود که ksize > 1 باشد؛ اما وقتی ksize = 1، لاپلاسین با فیلتر کردن تصویر با دیافراگم 3×3 زیر محاسبه میشود :
\( \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix} \)
تعریف این تابع به صورت زیر هستش :
1 2 3 |
CV_EXPORTS_W void Laplacian( InputArray src, OutputArray dst, int ddepth, int ksize = 1, double scale = 1, double delta = 0, int borderType = BORDER_DEFAULT ); |
توضیح پارامترها :
- src و dst : تصویر ورودی و خروجی، اندازه و تعداد کانال تصویر خروجی برابر با تصویر ورودی هستش.
- ksize : اندازه Aperture، برای محاسبه فیلترهای مشتق دوم استفاده می شود؛ برای جزئیات بیشتر به تابع cv::getDerivKernels مراجعه کنید؛ اندازه باید مثبت و فرد باشد.
- scale : ضریب مقیاس ( scale factor ) اختیاری برای مقادیر لاپلاس محاسبه شده؛ به طور پیش فرض، هیچ مقیاسی اعمال نمی شود؛ برای جزئیات بیشتر به تابع cv::getDerivKernels مراجعه کنید.
- delta : مقداری اختیاری که قبل از ذخیره نتایج در dst به نتایج اضافه می شود.
- borderType : مورد BORDER_WRAP، پشتیبانی نمیشه.
نمونه تصویر از عملکرد فیلتر Laplacian :
18) فیلتر spatialGradient : مشتق مرتبه اول تصویر را در x و y با استفاده از عملگر Sobel محاسبه میکند؛ این تابع معادل 2 کد زیر هستش :
1 2 |
Sobel( src, dx, CV_16SC1, 1, 0, 3 ); Sobel( src, dy, CV_16SC1, 0, 1, 3 ); |
تعریف این تابع به صورت زیر هستش :
1 2 3 |
CV_EXPORTS_W void spatialGradient( InputArray src, OutputArray dx, OutputArray dy, int ksize = 3, int borderType = BORDER_DEFAULT ); |
توضیح پارامترها :
- src : تصویر ورودی.
- dx : تصویر خروجی با مشتق مرتبه اول در x.
- dy : تصویر خروجی با مشتق مرتبه اول در y.
- ksize : اندازه هسته Sobel؛ باید 3 باشه.
- borderType : فقط گزینه های BORDER_DEFAULT = BORDER_REFLECT_101 و BORDER_REPLICATE پشتیبانی میشود.
نمونه تصویر از عملکرد فیلتر spatialGradient ( تصویر ورودی رو به gray تبدیل کردم، حوصله محاسبه فیلتر فوق برا تک تک کانالا رو نداشتم ) :
19) فیلتر sqrBoxFilter : مجموع نرمال شده ( normalized ) مربع های مقادیر پیکسلی که روی فیلتر همپوشانی ( overlapping ) دارند را محاسبه میکند؛ برای هر پیکسل (x، y) در تصویر منبع، تابع مجموع مربع های آن مقادیر پیکسل همسایه را محاسبه می کند که روی فیلتر قرار داده شده روی پیکسل (x، y) همپوشانی دارند؛ فیلتر جعبه مربعی ( square box filter ) نرمال نشده ( unnormalized ) می تواند در محاسبه آمار تصویر محلی مانند واریانس محلی ( local variance ) و انحراف استاندارد ( standard deviation ) در اطراف همسایگی ( neighborhood ) یک پیکسل مفید باشد.
تعریف این تابع به صورت زیر هستش :
1 2 3 4 |
CV_EXPORTS_W void sqrBoxFilter( InputArray src, OutputArray dst, int ddepth, Size ksize, Point anchor = Point(-1, -1), bool normalize = true, int borderType = BORDER_DEFAULT ); |
توضیح پارامترها :
- src : تصویر ورودی.
- dst : تصویر خروجی؛ با اندازه و نوعی برابر با تصویر ورودی.
- ddepth : عمق تصویر خروجی.
- ksize : اندازه هسته.
- normalize : مشخص می کند که آیا هسته باید بر اساس ناحیه آن نرمال شود یا خیر.
- borderType : گزینه BORDER_WRAP پشتیبانی نمیشود.
نمونه تصویر از عملکرد فیلتر sqrBoxFilter : در دو حالت فعال و غیر فعال بودن پرچم normalize ( خروجی به صورت زیر میشه، البته اگه درست کدشو زده باشم، که خب احتمالا زیاد یه جای کارم میلنگه، نمیدونم چرا کد نمونه ای چیزی ازش تو نت و یا کدهای نمونه خود OpenCV پیدا نکردم! )
تصویر استفاده شده در این مطلب :
منابع :
- Image Filtering
- Image Pyramids Cpp
- Image Pyramids Python
- Making your own linear filters
- Image Gradients
- Eroding and Dilating
- Morphological Transformations
مطالعه بیشتر :
- Filtering using F-transform
- Out-of-focus Deblur Filter
- Periodic Noise Removing Filter
- Motion Deblur Filter
- Filters – Extended Image Processing
این مطلب رو به مرور زمان تکمیل و اصلاح میکنم، فعلا بیشتر از این حوصلم نمیشه.
برای شادی روح تمام شهدا، مخصوصا این شهید بزرگوار ( احسان فتحی ) و امام شهدا، صلواتی بفرستید؛ تا مطلب بعد یا علی.