看标题就知道跟我的上一篇博客差不多了,本来没打算写这篇的,因为之前在网上搜过,有现成的OpenCV实现fft2和ifft2代码,而且我也试过fft2确实和Matlab计算的结果一致。但是项目后来又用到了ifft2,这次发现计算结果跟Matlab怎么都对不上,实在没办法,我只能自己摸索,后面也对网上的fft2代码进行了完善,现在一起发出来。
要用OpenCV实现傅里叶变换,首先要知道复数在OpenCV中是如何表示的,然后才能理解fft2和ifft2的实现。
OpenCV的mat类没有专门的复数数据类型,可以将两个二维的Mat,一个存储实部,一个存储虚部,merge在一起,形成存储虚数的Mat。或者直接定义类似CV_64FC2格式的Mat,如下所示:
// 第一种方式,定义实部和虚部
Mat real = Mat::zeros(3, 3, CV_64F);
Mat imag = Mat::zeros(3, 3, CV_64F);
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
real.at(i, j) = i * 3 + j;
imag.at(i, j) = i * 3 + j;
}
}
Mat planes[2] = { real, imag };
Mat complex;
merge(planes, 2, complex);
// 第二种方式,直接定义“复数”格式的Mat
Mat complex2 = Mat::zeros(3, 3, CV_64FC2);
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
complex2.at(i, j)[0] = i * 3 + j;
complex2.at(i, j)[1] = i * 3 + j;
}
}
虽然第二种方式更简捷,但OpenCV的Mat矩阵运算基本上都不支持复数格式的数据,往往需要把复数拆分成实部和虚部,再分别进行计算,所以第二种还是需要转换成第一种的形式。另外再补充一下,输出复数的两种方法,这样就能方便地和Matlab比较结果了。
// 第一种输出实部、虚部、复数的方式(点坐标)
cout << complex2.at(i, j)[0];
cout << complex2.at(i, j)[1];
cout << complex2.at(i, j);
// 第二种输出实部、虚部、复数的方式(复数格式)
cout << complex2.at<std::complex<double>>(i, j).real();
cout << complex2.at<std::complex<double>>(i, j).imag();
cout << complex2.at<std::complex<double>>(i, j);
注意at指针后面<>里面的内容,本文统一采用的是double格式数据。其实CV_64FC2的每个元素就是点坐标,点坐标的第一个值对应实部,第二个对应虚部。比较起来第二种方式更直观,但是写法稍复杂一点。另外第一种和第二种输出复数的表现形式有一点差别,如下图所示:
OpenCV有自带的实现离散傅里叶函数:dft,但如果直接使用,往往得到的结果不太正确。此时我们可以参考官方给出的案例,具体就是,当输入的Mat数据格式是CV_64F时,需要将其转换为CV_64FC2格式。所以这里我们就要做个判断,根据输入的Mat数据格式,选择不同的处理方式。判断数据格式可以采用Mat自带的type函数,当格式为CV_8U~CV_64F时,该函数返回值为0~6,当格式为CV_8UC2~CV_64FC2时,返回值为8~14。至此,下面给出OpenCV实现fft2函数的代码。
void fft2(const Mat &src, Mat &Fourier)
{
int mat_type = src.type();
assert(mat_type<15); //不支持的数据格式
if (mat_type < 7)
{
Mat planes[] = { Mat_<double>(src), Mat::zeros(src.size(),CV_64F) };
merge(planes, 2, Fourier);
dft(Fourier, Fourier);
}
else // 7
{
Mat tmp;
dft(src, tmp);
vector planes;
split(tmp, planes);
magnitude(planes[0], planes[1], planes[0]); //将复数转化为幅值
Fourier = planes[0];
}
}
ifft2和fft2十分类似,唯一的不同就是调用dft函数时使用的参数,一定要加上DFT_SCALE,对结果进行缩放,这样才能达到与Matlab的ifft2函数相同的效果。下面是具体代码。
void ifft2(const Mat &src, Mat &Fourier)
{
int mat_type = src.type();
assert(mat_type<15); //不支持的数据格式
if (mat_type < 7)
{
Mat planes[] = { Mat_<double>(src), Mat::zeros(src.size(),CV_64F) };
merge(planes, 2, Fourier);
dft(Fourier, Fourier, DFT_INVERSE + DFT_SCALE, 0);
}
else // 7
{
Mat tmp;
dft(src, tmp, DFT_INVERSE + DFT_SCALE, 0);
vector planes;
split(tmp, planes);
magnitude(planes[0], planes[1], planes[0]); //将复数转化为幅值
Fourier = planes[0];
}
}
需要注意的是,fft2和ifft2返回的结果都是CV_64F(double)类型的,自己可以根据需要改成float型。同样地,相关的代码已经更新到了我的Github上,欢迎提意见。另外再多说几句,目前这已经是第四篇个人原创技术博客了,可是四篇的浏览量加起来才100出头,简直是在自言自语,略尴尬。自我感觉写的东西还是有点用的,特别是Matlab代码转OpenCV的时候,希望等百度能搜到我的博客后,情况会好吧。