به نام خدا : تو این مطلب میخوام درباره چند تابع توضیح بدم، که مربوط هستند به این مطلب ( آموزش ماژول Operations on arrays در OpenCV )؛ که خب چون توضیحات این چندتا تابع طولانی شد، گفتم که تو یه مطلب جداگونه منتشرشون کنم؛ این مطلب مربوطه به تبدیل فوریه و تابع اصلی این مطلب هم cv::dft هستش؛ توجه داشته باشید که در این مطلب، کلمه complex به معنی “مختلط” بکار رفته و نه “پیچیده”، مختلط کلمه صحیح تری هستش در این مطلب تا پیچیده.
تبدیل فوریه
تبدیل فوریه ( Fourier Transform ) یک تصویر را به اجزای سینوسی و کسینوسی آن تجزیه میکند؛ به عبارت دیگر، یک تصویر را از حوزه فضایی ( spatial domain ) خود به حوزه فرکانس ( frequency domain ) خود تبدیل میکند؛ ایده این است که هر تابعی را می توان دقیقاً با مجموع توابع بی نهایت سینوس و کسینوس تقریب زد؛ تبدیل فوریه راهی برای انجام این کار است.
از نظر ریاضی تبدیل فوریه تصاویر دو بعدی عبارت است از :
$$ \Large{ F(k,l) = \displaystyle\sum\limits_{i=0}^{N-1}\sum\limits_{j=0}^{N-1} f(i,j)e^{-i2\pi(\frac{ki}{N}+\frac{lj}{N})} } $$
$$ \Large{ e^{ix} = \cos{x} + i\sin {x} } $$
در اینجا f مقدار تصویر در حوزه فضایی و F در حوزه فرکانس آن است؛ نتیجه تبدیل، اعدادی مختلط ( complex numbers ) است؛ نمایش این امر یا از طریق یک تصویر واقعی ( real image ) و یک تصویر مختلط ( complex image ) یا از طریق یک تصویر بزرگی و فاز ( magnitude and a phase image ) امکان پذیر است؛ با این حال، در سراسر الگوریتم های پردازش تصویر، تنها تصویر بزرگی ( magnitude image ) جالب است، زیرا این شامل تمام اطلاعات مورد نیاز ما در مورد ساختار هندسی تصاویر است؛ با این اوصاف، اگر قصد دارید برخی اصلاحات را در این فرم ها در تصویر ایجاد کنید و سپس باید آن را تبدیل مجدد ( retransform ) کنید، باید هر دوی این موارد را حفظ کنید.
تبدیل فوریه ( Fourier Transform ) برای تجزیه و تحلیل ویژگی های فرکانسی فیلترهای مختلف استفاده میشود؛ برای تصاویر، تبدیل فوریه گسسته ( DFT ) 2-بعدی برای یافتن دامنه ( domain ) فرکانسی استفاده میشود؛ یک الگوریتم سریع به نام تبدیل فوریه سریع ( FFT ) برای محاسبه DFT استفاده میشود.
برای یک سیگنال سینوسی، \( x(t) = A \sin(2 \pi ft) \)، میتوان گفت \( f \) فرکانس سیگنال است، و اگر دامنه ( domain ) فرکانسی آن گرفته شود، میتوانیم یک spike در \( f \) ببینیم؛ اگر سیگنال برای تشکیل یک سیگنال گسسته نمونه برداری شود، دامنه ( domain ) فرکانسی یکسانی را دریافت میکنیم، اما در محدوده \( [- \pi, \pi] \) یا \( [0,2\pi] \) ( یا \( [0,N] \) برای DFT نقطه N ) تناوبی است؛ شما می توانید یک تصویر را به عنوان یک سیگنال در نظر بگیرید که در دو جهت نمونه برداری میشود؛ بنابراین تبدیل فوریه در هر دو جهت X و Y به شما نمایش فرکانسی تصویر را میدهد.
به طور واضح تر، برای سیگنال سینوسی، اگر دامنه ( amplitude ) آنقدر سریع در مدت زمان کوتاه تغییر کند، میتوان گفت که یک سیگنال با فرکانس بالا است؛ اگر به آرامی تغییر کند، سیگنال فرکانس پایین است؛ می توانید همین ایده را به تصاویر نیز تعمیم دهید؛ در کجا، دامنه ( amplitude ) در تصاویر به شدت متفاوت است؟ در نقاط لبه ( edge ) و یا noise ها؛ بنابراین میتوان گفت لبه ها و نویزها محتوای فرکانس بالا در یک تصویر هستند؛ اگر تغییرات زیادی در دامنه ( amplitude ) وجود نداشته باشد، جزء فرکانس پایین است.
کاربردهای تبدیل فوریه در پردازش تصویر
تجزیه و تحلیل تصویر، فیلتر کردن تصویر، بازسازی تصویر و فشرده سازی تصویر از جمله برنامه هایی هستند که از تبدیل فوریه استفاده میکنند.
تکمیل کردن این قسمت...
نحوه پیدا کردن تبدیل فوریه در OpenCV : برای این کار، OpenCV تابع cv::dft ( و cv::idft، که باهاش کاری نداریم! ) رو ارائه داده است، که نتیجه ای دو کاناله را بر میگرداند، کانال اول قسمت واقعی نتیجه و کانال دوم قسمت مختلط نتیجه را خواهد داشت.
1) تابع dft : این تابع، تبدیل فوریه گسسته مستقیم و معکوس را انجام میدهد ( که مستقیم یا معکوس بودنش، به کمک پارامتر flags تعیین میشه )؛ البته یه تابع دیگه با نام idft داریم که مختص تبدیل فوریه گسسته معکوس هستش که خب ازش استفاده نمیکنیم و برا تبدیل فوریه گسسته مستقیم و معکوس، از همین تابع dft استفاده میکنیم؛ ورودی این تابع میتونه 1-کاناله ( کانال واقعی فقط ) و یا 2-کاناله ( کانال واقعی + کانال مختلط ) باشه؛ خروجیش هم همینطور، میشه تعیین کرد ( به کمک پارامتر flags ) که 1-کاناله باشه یا 2-کاناله.
تعریف تابع :
1 |
CV_EXPORTS_W void dft(InputArray src, OutputArray dst, int flags = 0, int nonzeroRows = 0); |
توضیح پارامترها :
- src : آرایه ورودی که میتواند واقعی ( real ) یا مختلط ( complex ) باشد؛ نوع آرایه ورودی باید CV_32FC1 یا CV_32FC2 یا CV_64FC1 یا CV_64FC2 باشد.
- dst : آرایه خروجی که اندازه و نوع آن به پارامتر flags بستگی دارد.
- flags : پرچم های تبدیل؛ که به کمک نوع شمارشی DftFlags، مقدار دهی میشه و میتونه ترکیبی از مقادیر DftFlags باشه.
- nonzeroRows : ؟؟؟؟؟ ( نیاز به تست در عمل!!! )
نوع شمارشی DftFlags :
1 2 3 4 5 6 7 8 9 10 11 12 |
enum DftFlags { DFT_INVERSE = 1, DFT_SCALE = 2, DFT_ROWS = 4, DFT_COMPLEX_OUTPUT = 16, DFT_REAL_OUTPUT = 32, DFT_COMPLEX_INPUT = 64, DCT_INVERSE = DFT_INVERSE, DCT_ROWS = DFT_ROWS }; |
توضیح گزینه ها :
- DFT_INVERSE : اگر این پرچم فعال باشد تبدیل فوریه گسسته معکوس وگرنه تبدیل فوریه گسسته مستقیمِ رو محاسبه میکنه.
- DFT_SCALE : اگر این پرچم فعال شود، تابع، پس از تبدیل، نتیجه را مقیاس میکند ( آن را بر تعداد عناصر آرایه تقسیم کنید )؛ این پرچم، معمولاً با DFT_INVERSE ترکیب میشود.
- DFT_ROWS : ؟؟؟؟؟ ( نیاز به تست در عمل!!! )
- DFT_COMPLEX_OUTPUT : ؟؟؟؟؟ ( نیاز به تست در عمل!!! )
- DFT_REAL_OUTPUT : اگر پرچم DFT_INVERSE فعال شده باشد ( که خب یعنی، تبدیل معکوس داریم انجام میدیم )، حالا اگه پرچم DFT_REAL_OUTPUT رو فعال کنید، خروجی یک آرایه 1-کاناله واقعی هستش و اگه رو فعال نکنید، که خب خروجی یک آرایه 2-کاناله ( کانال واقعی + کانال مختلط ) خواهد بود.
- DFT_COMPLEX_INPUT : ؟؟؟؟؟ ( نیاز به تست در عمل!!! )
2) تابع idft : تبدیل فوریه گسسته معکوس یک آرایه 1-بعدی یا 2-بعدی را محاسبه میکند.
کد idft(src, dst, flags) معادل کد dft(src, dst, flags | DFT_INVERSE) هستش، فلذا تو این مطلب کاری به تابع idft ندارم و برا تبدیل فوریه گسسته مستقیم و یا معکوس از تابع dft استفاده میکنیم و مستقیم و یا معکوس بودن تبدیل رو هم به کمک پارامتر flags تعیین میکنیم.
3) تابع getOptimalDFTSize : اندازه DFT بهینه را برای یک اندازه برداری معین برمی گرداند.
عملکرد DFT یک تابع یکنوا ( monotonic function ) در اندازه برداری نیست؛ بنابراین، هنگامی که همگشت ( convolution ) دو آرایه را محاسبه میکنید یا تحلیل طیفی ( spectral analysis ) یک آرایه را انجام میدهید، معمولا منطقی است که داده های ورودی را با صفر اضافه کنید تا آرایه کمی بزرگتر به دست آید که می تواند بسیار سریعتر از آرایه اصلی تبدیل شود ( عملکرد محاسبات DFT در برخی از اندازه های آرایه بهتر است )؛ آرایه هایی که اندازه آنها توانی از دو است ( 2، 4، 8، 16، 32، ... ) سریعترین پردازش را دارند؛ اگرچه، آرایه هایی که اندازه آنها حاصل ضرب 2، 3 و 5 است ( مثلاً 300 = 5*5*3*2*2 ) نیز کاملاً کارآمد پردازش میشوند.
تعریف تابع : اندازه آرایه ورودی رو به این تابع میدید و اندازه بهینه رو تحویل میگیرید؛ که خب یکبار برای اندازه بهینه طول آرایه ورودی و یکبار برای اندازه بهینه عرض آرایه ورودی باید از این تابع استفاده کنید؛ عددی که در خروجی میگیرید بزرگتر یا مساوی با vecsize هستش ( کوچکتر نیست! )
1 |
CV_EXPORTS_W int getOptimalDFTSize(int vecsize); |
توجه : خب حالا که به کمک تابع getOptimalDFTSize، اندازه بهینه برای آرایه ورودی رو رو گرفتیم، چطوری اندازه آرایه ورودی رو تغییر بدیم ( بزرگتر کنیم ) و در پیکسل های جدید، 0 قرار بدیم؟ برای این کار میتونیم از تابع copyMakeBorder استفاده کنیم؛ که قبلا در این مطلب ( آموزش ماژول Operations on arrays در OpenCV ) بهش پرداختم.
کد نمونه :
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 57 58 59 60 61 62 |
void operations_on_arrays_example1::dft_test1() { cv::Mat src = cv::imread("C:/Users/Mahdi/Desktop/butterfly.jpg", cv::IMREAD_GRAYSCALE); // Find the optimal size of the src array for the DFT function. int optimalRows = cv::getOptimalDFTSize(src.rows); int optimalCols = cv::getOptimalDFTSize(src.cols); // Resize the input array and fill the new pixels with 0 cv::Mat padded; cv::copyMakeBorder(src, padded, 0, optimalRows - src.rows, 0, optimalCols - src.cols, cv::BORDER_CONSTANT, cv::Scalar::all(0)); // Change padded type to CV_32F padded.convertTo(padded, CV_32F); std::vector<cv::Mat> planes = { padded, cv::Mat::zeros(padded.size(), padded.type()) }; cv::Mat complexI; cv::merge(planes, complexI); // in-place dft transform cv::dft(complexI, complexI); // compute the magnitude and switch to logarithmic scale cv::Mat mag; cv::split(complexI, planes); cv::magnitude(planes[0], planes[1], mag); cv::add(mag, cv::Scalar::all(1), mag); cv::log(mag, mag); // crop the spectrum, if it has an odd number of rows or columns // [ In some cases, the getOptimalDFTSize function returns an odd number ] mag = mag( cv::Rect(0, 0, mag.cols & -2, mag.rows & -2) ); // rearrange the quadrants of Fourier image, so that the origin is at the image center int cx = mag.cols / 2; int cy = mag.rows / 2; cv::Mat tmp; //--- cv::Mat TopLeft = mag( cv::Rect(0, 0, cx, cy) ); // quadrant top-left cv::Mat TopRight = mag( cv::Rect(cx, 0, cx, cy) ); // quadrant top-right cv::Mat BotLeft = mag( cv::Rect(0, cy, cx, cy) ); // quadrant bot-left cv::Mat BotRight = mag( cv::Rect(cx, cy, cx, cy) ); // quadrant bot-right // exchange TopLeft and BotRight quadrants TopLeft.copyTo(tmp); BotRight.copyTo(TopLeft); tmp.copyTo(BotRight); // exchange TopRight and BotLeft quadrants TopRight.copyTo(tmp); BotLeft.copyTo(TopRight); tmp.copyTo(BotLeft); // The pixel value of CV_32S type image ranges from 0 to 1. cv::normalize(mag, mag, 0, 1, cv::NORM_MINMAX); cv::imshow("src", src); cv::imshow("spectrum magnitude", mag); } |
توضیح کد بالا :
خط 3 : یه تصویری رو در مد GRAYSCALE میخونیم. ( 1-کاناله )
خط 6 و 7 : اندازه بهینه برای آرایه ورودی رو به کمک تابع getOptimalDFTSize پیدا میکنیم ( به توضیحات تابع getOptimalDFTSize در همین مطلب، مراجعه کنید. )
خط 11 : اندازه آرایه ورودی رو به اندازه بهینه تغییر میدیدم و در پیکسل های جدید مقدار 0 قرار میدیم.
خط 14 : آرایه ورودی رو به نوع CV_32F تبدیل میکنیم.
خط 16 تا 22 : نتیجه تبدیل فوریه مختلط است؛ که یعنی برای هر مقدار تصویر، نتیجه دو مقدار تصویر است!؛ علاوه بر این، محدوده دامنه فرکانس بسیار بزرگتر از همتای فضایی آن است؛ بنابراین، ما معمولاً اینها را حداقل در قالب اعشاری ذخیره میکنیم؛ بنابراین تصویر ورودی 1-کاناله خود را به 2-کاناله تبدیل میکنیم تا کانال جدید، مقادیر مختلط را نگه دارد.
خط 25 : تبدیل dft رو انجام میدیم.
خط 28 تا 30 : نتایج یک DFT اعدادی مختلط هستند؛ یک عدد مختلط یک قسمت واقعی ( Re ) و یک قسمت مختلط ( خیالی - Im ) داره ( که هر کدوم در یک کانال، از آرایه خروجی قرار دارند )؛ فلذا اومدیم کانال ها رو استخراج کردیم و بعد مقادیر واقعی و مختلط را به قدر ( magnitude ) تبدیل کردیم؛ فرمول محاسبه magnitude به صورت زیر هستش :
\( M = \sqrt[2]{ {Re(DFT(I))}^2 + {Im(DFT(I))}^2} \)
خط 32 و 33 : از مقیاس خطی به مقیاس لگاریتمی میریم تا خروجی ما روی صفحه نمایش، قابل نمایش دادن بشه.
\( mag = \log{(1 + mag)} \)
خط 37 : در خط 11، اندازه آرایه ورودی رو تغییر دادیم، فلذا طول و عرض آرایه ورودی میتونه فرد و یا زوج باشه؛ در ادامه پروژه نیاز داریم که این اندازه عددی زوج باشه فلذا اندازه آرایه رو زوج میکنیم و آرایه رو برش میدیم؛ این & کردن طول و عرض mag با -2، برای زوج کردنشون بکار میره؛ در این مطلب ( زوج یا فرد کردن اعداد در C++ ) در این باره صحبت کردم.
خط 40 تا 55 : برای تجسم بهتر، ربعهای نتیجه را دوباره مرتب میکنیم، به طوری که مبدا (0، 0) با مرکز تصویر مطابقت داشته باشد.
خط 58 : نرمالیزه کردن خروجی و بردن داده ها به محدوده 0 تا 1، تا قابل نمایش باشه خروجی.
خط 60 و 61 : نمایش ورودی و خروجی.
نتیجه کد بالا :
توجه 1 : البته برای سادگی میشد کد زیر رو :
1 2 3 4 5 6 7 |
padded.convertTo(padded, CV_32F); std::vector<cv::Mat> planes = { padded, cv::Mat::zeros(padded.size(), padded.type()) }; |
به صورت زیر نوشت، تا کدها کوتاه تر بشن :
1 2 3 4 5 |
std::vector<cv::Mat> planes = { cv::Mat_<float>(padded), // Change padded type to CV_32F cv::Mat::zeros(padded.size(), CV_32F) }; |
توجه 2 : به تابع dft یه ورودی 2 کاناله دادیم، پرچم ها رو هم تنظیم نکردیم ( که براش چیزی رو تعیین کنیم )، فلذا خروجیش هم 2 کاناله خواهد بود؛ اما خب میخوام کدمو ساده تر کنم، ورودی رو 1 کاناله میدم و خروجی رو 2 کاناله میگیرم! ( برای این کار باید از پارامتر flags کمک میگیرم )؛ تو همچین وضعیتی باید پرچم DFT_COMPLEX_OUTPUT رو استفاده کنم تا تابع dft، خروجی 1 کاناله بهم نده ( چون ورودی 1 کاناله میخوایم بهش بدم، قطعا خروجی 1 کاناله هم بهمون میده! اگه از پرچم DFT_COMPLEX_OUTPUT استفاده نکنم! )؛ فلذا کد زیر :
1 2 3 4 5 6 7 8 9 10 |
std::vector<cv::Mat> planes = { cv::Mat_<float>(padded), // Change padded type to CV_32F cv::Mat::zeros(padded.size(), CV_32F) }; cv::Mat complexI; cv::merge(planes, complexI); // dft transform cv::dft(complexI, complexI); |
به صورت زیر ساده سازی میشه : متغییر planes رو هم حذف نکردم چون تو پروژه ازش استفاده کردم!
1 2 3 4 |
// dft transform std::vector<cv::Mat> planes; cv::Mat complexI; cv::dft(cv::Mat_<float>(padded), complexI, cv::DftFlags::DFT_COMPLEX_OUTPUT); |
یه کد ساده که یه تصویر ورودی میگیره، تبدیل فوریه گسسته مستقیم ( dft ) رو انجام میده و بعد تبدیل فوریه گسسته معکوس ( idft ) رو انجام میده، توقع داریم که تصویر اولیه با تصویر نهایی یکسان باشه، کد زیر رو میتونید تست کنید، نتیجه ورودی و خروجی یکسان هستش؛ با توجه به این که ما اندازه آرایه ورودی رو بهینه میکنیم و تعییر میدیم، فلذا اندازه تصویر ورودی خروجی یکسان نی ( تصویر خروجی یکم بزرگتره ) که خب اگه میخواید هر دو برابر باشن، میتونید اون کدهای بهینه کدرن اندازه آرایه ورودی رو حذف کنید.
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 |
void operations_on_arrays_example1::dft_test2() { cv::Mat src = cv::imread("C:/Users/Mahdi/Desktop/butterfly.jpg", cv::IMREAD_GRAYSCALE); int optimalRows = cv::getOptimalDFTSize(src.rows); int optimalCols = cv::getOptimalDFTSize(src.cols); cv::Mat padded = src.clone(); cv::copyMakeBorder(src, padded, 0, optimalRows - src.rows, 0, optimalCols - src.cols, cv::BORDER_CONSTANT, cv::Scalar::all(0)); padded.convertTo(padded, CV_32F); std::vector<cv::Mat> planes = { padded, cv::Mat::zeros(padded.size(), padded.type()) }; cv::Mat complexI; cv::merge(planes, complexI); cv::dft(complexI, complexI); // dft // cv::DFT_INVERSE | cv::DFT_REAL_OUTPUT | cv::DFT_SCALE cv::dft(complexI, complexI, cv::DFT_INVERSE); // idft cv::split(complexI, planes); cv::Mat src2 = planes[0].clone(); //src2.convertTo(src2, CV_8U); // method1 [ if you want use this code, you must use this flag DFT_SCALE in dft or idft ] normalize(src2, src2, 0, 1, cv::NormTypes::NORM_MINMAX); // method2 cv::imshow("src", src); cv::imshow("src2", src2); } |
توجه : هیچ یک از توابع dft و idft به طور پیش فرض نتیجه را مقیاس نمیکنند؛ بنابراین، شما باید DFT_SCALE را به یکی از dft یا idft به صراحت ارسال کنید تا این تبدیل ها به صورت متقابل معکوس شوند؛ وقتی تبدیل dft و بعد idft استفاده میکنیم، اگر از پرچم DFT_SCALE استفاده کرده باشیم، خروجی idft رو میشه نوعشو به CV_8U تبدیل کرد ( به کمک تابع convertTo ) و بعد نمایش داد و یا از تابع normalize استفاده کرد و بعد نمایش داد، تغییر در تصویر ورودی اولیه و نهایی ایجاد نمیشه؛ اما اگه از پرچم DFT_SCALE استفاده نکرده باشیم، خروجی idft رو فقط میشه به تابع normalize داد و خروجی تابع normalize رو نمایش داد ( نمیتونید از تابع convertTo استفاده کنید ).
تصویری به کد زیر بدید که مضربی از 2 باشه، و طول و عرضش فرد نباشن، چون قسمت بهینه کردن اندازه آرایه رو حذف کردم تا کد زیر ساده بشه؛ اومدم تبدیل فوریه مستقیم رو حساب کردم، نتیجه magnitude رو ویرایش کردم و بعد تبدیل فوریه معکوس اعمال کردم، اینطوری تصویر اصلی رو ویرایش کردم، یه جورایی انگار خطوط عمودی رو از تصویر ورودی حذف کردم؛ یکم بازی با توابع dft و idft، تا کار باهاش دستتون بیاد.
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
void operations_on_arrays_example1::dft_test3() { cv::Mat src = cv::imread("C:/Users/Mahdi/Desktop/Morabaraba.png", cv::IMREAD_GRAYSCALE); cv::Mat padded = src.clone(); // Change padded type to CV_32F padded.convertTo(padded, CV_32F); std::vector<cv::Mat> planes = { padded, cv::Mat::zeros(padded.size(), padded.type()) }; cv::Mat complexI; cv::merge(planes, complexI); // in-place dft transform cv::dft(complexI, complexI); // compute the magnitude and switch to logarithmic scale cv::Mat mag, angle; cv::split(complexI, planes); cv::cartToPolar(planes[0], planes[1], mag, angle); // rearrange the quadrants of Fourier image, so that the origin is at the image center int cx = mag.cols / 2; int cy = mag.rows / 2; cv::Mat tmp; //--- cv::Mat TopLeft = mag( cv::Rect(0, 0, cx, cy) ); // quadrant top-left cv::Mat TopRight = mag( cv::Rect(cx, 0, cx, cy) ); // quadrant top-right cv::Mat BotLeft = mag( cv::Rect(0, cy, cx, cy) ); // quadrant bot-left cv::Mat BotRight = mag( cv::Rect(cx, cy, cx, cy) ); // quadrant bot-right // exchange TopLeft and BotRight quadrants TopLeft.copyTo(tmp); BotRight.copyTo(TopLeft); tmp.copyTo(BotRight); // exchange TopRight and BotLeft quadrants TopRight.copyTo(tmp); BotLeft.copyTo(TopRight); tmp.copyTo(BotLeft); // create a copy for showing mag to user [ mag before edit ] cv::Mat mag2 = mag.clone(); //--- cv::add(mag2, cv::Scalar::all(1), mag2); cv::log(mag2, mag2); //--- cv::normalize(mag2, mag2, 0, 1, cv::NORM_MINMAX); // edit mag for (int r = 0; r < mag.rows; ++r) { for (int c = mag.cols/2 - 10; c <= mag.cols/2 + 10; ++c) { mag.at<float>(r,c) = 0; } } // create a copy for showing mag to user [ mag after edit ] cv::Mat mag3 = mag.clone(); //--- cv::add(mag3, cv::Scalar::all(1), mag3); cv::log(mag3, mag3); //--- cv::normalize(mag3, mag3, 0, 1, cv::NORM_MINMAX); // rearrange the quadrants of Fourier image, so that the origin is at the image center TopLeft = mag( cv::Rect(0, 0, cx, cy) ); // quadrant top-left TopRight = mag( cv::Rect(cx, 0, cx, cy) ); // quadrant top-right BotLeft = mag( cv::Rect(0, cy, cx, cy) ); // quadrant bot-left BotRight = mag( cv::Rect(cx, cy, cx, cy) ); // quadrant bot-right // exchange TopLeft and BotRight quadrants TopLeft.copyTo(tmp); BotRight.copyTo(TopLeft); tmp.copyTo(BotRight); // exchange TopRight and BotLeft quadrants TopRight.copyTo(tmp); BotLeft.copyTo(TopRight); tmp.copyTo(BotLeft); cv::polarToCart(mag, angle, planes[0], planes[1]); cv::merge(planes, complexI); cv::dft(complexI, complexI, cv::DftFlags::DFT_INVERSE); cv::split(complexI, planes); cv::Mat src2 = planes[0].clone(); normalize(src2, src2, 0, 1, cv::NormTypes::NORM_MINMAX); cv::imshow("src before edit", src); cv::imshow("mag before edit", mag2); cv::imshow("mag after edit", mag3); cv::imshow("src after edit", src2); } |
نتیجه کد بالا :
توجه 1 : خطوط 91 تا 93 ( که تبدیل معکوس انجام میدیم و ... ) به صورت زیر هستش :
1 2 3 4 5 6 |
cv::dft(complexI, complexI, cv::DftFlags::DFT_INVERSE); cv::split(complexI, planes); cv::Mat src2 = planes[0].clone(); normalize(src2, src2, 0, 1, cv::NormTypes::NORM_MINMAX); |
تو تبدیل معکوس، دنبال کانال واقعی هستیم، کاری به کانال مختلط نداریم، فلذا بهتره که از پرچم DFT_REAL_OUTPUT استفاده کنیم، تا آرایه خروجی 1 کاناله باشه ( فقط کانال واقعی؛ اگه خروجی 2 کاناله باشه یعنی هم کانال واقعی رو داریم و هم کانال مختلط رو )؛ فلذا دیگه نیازی به استفاده از تابع split نداریم تا کانال ها رو استخراج کنه برامون!؛ فلذا ساده شده کد بالا، به صورت زیر میشه :
1 2 3 4 |
cv::dft(complexI, complexI, cv::DftFlags::DFT_INVERSE | cv::DFT_REAL_OUTPUT); cv::Mat src2 = complexI.clone(); normalize(src2, src2, 0, 1, cv::NormTypes::NORM_MINMAX); |
توجه 2 : کد 3 رو میشه تغییر داد، اون قسمتی که میایم تصویر mag رو ویرایش میکنیم ( خط 54 تا 61 )
حالت 1) کد زیر رو در پروژه 3 استفاده کردم، همونطور که قبلا گفتم، انگار خطوط عمودی رو از تصویر ورودی حذف میکنه، این کد.
1 2 3 4 5 6 7 8 |
// edit mag [ remove vertical lines! ] for (int r = 0; r < mag.rows; ++r) { for (int c = mag.cols/2 - 10; c <= mag.cols/2 + 10; ++c) { mag.at<float>(r,c) = 0; } } |
حالت 2) اگر بجای کدهای فوق، از کد زیر استفاده کنیم، میشه تبدیل LPF :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// edit mag [ LPF, HPF ] for (int r = 0; r < mag.rows; ++r) { for (int c = 0; c <= mag.cols; ++c) { cv::Point a(r,c); cv::Point b(mag.rows/2, mag.cols/2); double dis = cv::norm(a-b); if(dis > 50) mag.at<float>(r,c) = 0; // LPF //if(dis < 50) mag.at<float>(r,c) = 0; // HPF } } |
حالت 3) اگه بجای کدهای فوق، از کد زیر استفاده کنیم، میشه تبدیل HPF :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// edit mag [ LPF, HPF ] for (int r = 0; r < mag.rows; ++r) { for (int c = 0; c <= mag.cols; ++c) { cv::Point a(r,c); cv::Point b(mag.rows/2, mag.cols/2); double dis = cv::norm(a-b); //if(dis > 50) mag.at<float>(r,c) = 0; // LPF if(dis < 50) mag.at<float>(r,c) = 0; // HPF } } |
نتیجه حالت 2 و 3 :
mag before edit | SRC |
mag after edit _ HPF | mag after edit - LPF |
src after edit _ HPF | src after edit - LPF |
توجه : اگه در منابع دو کد بالا، میبینید که تصویر HPF تیره هستش و تو این مطلب، تصویر HPF روشن هستش، به این دلیله که اونا کد زیر رو قبل از normalize آرایه src2؛ کد زیر رو استفاده میکنند :
1 |
src2 = cv::abs(src2); |
منبع حالت های 1 و 2 و 3 :
یه الگوریتم ساده برای تشخیص تاری تصاویر به کمک تبدیل فوریه.
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 57 58 59 60 61 62 |
void operations_on_arrays_example1::dft_isBlured() { int blockRadius = 60; double thresh = 10; cv::Mat src = cv::imread("C:/Users/Mahdi/Desktop/Shahid_Soleimani/blur10x10.jpg", cv::IMREAD_GRAYSCALE); // FFT cv::Mat fft; cv::dft(cv::Mat_<float>(src), fft, cv::DFT_COMPLEX_OUTPUT); // rearrange the quadrants of Fourier image, so that the origin is at the image center int cx = src.cols / 2; int cy = src.rows / 2; cv::Mat tmp; //--- cv::Mat TopLeft = fft( cv::Rect(0, 0, cx, cy) ); // quadrant top-left cv::Mat TopRight = fft( cv::Rect(cx, 0, cx, cy) ); // quadrant top-right cv::Mat BotLeft = fft( cv::Rect(0, cy, cx, cy) ); // quadrant bot-left cv::Mat BotRight = fft( cv::Rect(cx, cy, cx, cy) ); // quadrant bot-right // exchange TopLeft and BotRight quadrants TopLeft.copyTo(tmp); BotRight.copyTo(TopLeft); tmp.copyTo(BotRight); // exchange TopRight and BotLeft quadrants TopRight.copyTo(tmp); BotLeft.copyTo(TopRight); tmp.copyTo(BotLeft); // Block the low frequencies fft(cv::Rect(cx-blockRadius, cy-blockRadius, 2*blockRadius, 2*blockRadius)).setTo(0); // rearrange the quadrants of Fourier image, so that the origin is at the image center TopLeft = fft( cv::Rect(0, 0, cx, cy) ); // quadrant top-left TopRight = fft( cv::Rect(cx, 0, cx, cy) ); // quadrant top-right BotLeft = fft( cv::Rect(0, cy, cx, cy) ); // quadrant bot-left BotRight = fft( cv::Rect(cx, cy, cx, cy) ); // quadrant bot-right // exchange TopLeft and BotRight quadrants TopLeft.copyTo(tmp); BotRight.copyTo(TopLeft); tmp.copyTo(BotRight); // exchange TopRight and BotLeft quadrants TopRight.copyTo(tmp); BotLeft.copyTo(TopRight); tmp.copyTo(BotLeft); // IFFT cv::Mat ifft; cv::dft(fft, ifft, cv::DFT_INVERSE | cv::DFT_REAL_OUTPUT | cv::DFT_SCALE); ifft = cv::abs(ifft); cv::log(ifft, ifft); ifft *= 20; double mean = cv::mean(ifft)[0]; std::cout << "mean = " << mean << std::endl; if(mean <= thresh) std::cout << "The image is blurry!" << std::endl; else std::cout << "The image is not blurry!" << std::endl; } |
نتیجه کد بالا :
توجه : اگه تصاویر تار ( blur ) دم دست ندارید، میتونید برای ایجاد تصاویری blur، به این مطلب ( آموزش ماژول Image Filtering در OpenCV ) مراجعه کنید.
مطالعه بیشتر : برا کسایی که دوس دارن در این زمینه مطالعه بیشتری بکنن، میتونید به فایل زیر مراجعه کنید، اطلاعات مفیدی باید داشته باشه!
منبع :
منابع :
- Fourier Transform [ OpenCV.js Tutorials ]
- Discrete Fourier Transform [ OpenCV Tutorials ]
- Fourier series & synthesis
تا مطلب بعد اگه زنده بودیم و قسمت شد، یا علی.