二维离散傅里叶变换的原理略,具体见书P346。
OpenCV实现傅里叶(逆)变换的函数:
void cv::dft(cv::InputArray src, cv::OutputArray dst, int flags = 0, int nonzeroRows = 0)
src:输入矩阵,只支持 CV_32F 或者 CV_64F 的单通道或双通道矩阵
dst:输出矩阵
flags:用于说明是傅里叶变换还是傅里叶逆变换
- DFT_COMPLEX_OUTPUT:输出复数形式
- DFT_REAL_OUTPUT:只输出实部
- DFT_INVERSE:傅里叶逆变换
- DFT_SCALE:是否除以 *
- DFT_ROWS:输入矩阵的每行进行傅里叶变换或者逆变换
如果输入矩阵 src 是单通道的,则代表实数矩阵;如果是双通道的,则代表复数矩阵。
输出矩阵 dst 是单通道的还是双通道的,则需要参数 flags 指定,其中 flags 的值可以组合使用,在进行傅里叶逆变换时,常用的组合为 DFT_INVERSE + DFT_SCALE + DFT_COMPLEX_OUTPUT。
用法如下:
//输入图像矩阵
Mat I = imread(argv[1], CV_LOAD_IMAGE_GRAYSCALE);
//数据类型:将CV_8U转换成CV_32F或者CV_64F
Mat fI;
I.convertTo(fI, CV_64F);
//傅里叶变换
Mat F;
dft(fI, F, DFT_COMPLEX_OUTPUT);
//傅里叶逆变换,只取实部
Mat iF;
dft(F, iF, DFT_REAL_OUTPUT + DFT_SCALE + DFT_INVERSE);
//计算的iF是浮点型,转换为CV_8U
Mat II;
iF.convertTo(II, CV_8U);
在 OpenCV 中实现的傅里叶变换的快速算法是针对行数和列数均满足可以分解为 2 × 3 × 5 的情况的。所以计算二维矩阵的快速傅里叶变换时需要先对原矩阵进行扩充,在矩阵的右侧和下侧补 0,以满足该规则。对于补多少行多少列的 0,可以使用函数:
int cv::getOptimalDFTSize(int vecsize)
该函数返回一个不小于 vecsize,且可以分解为 2 × 3 × 5 的整数。
利用getOptimalDFTSize
和dft
函数可以完成图像矩阵的快速傅里叶变换。其中参数 I I I为输入的矩阵,数据类型为浮点型;输出矩阵 F F F是复矩阵,存储为双通道,第一通道用于存储实部,第二通道用于存储虚部。
快速傅里叶变换的具体实现如下:
void fft2Image(Mat I, Mat &F)
{
//得到I的行数和列数
int rows = I.rows;
int cols = I.cols;
//满足快速傅里叶变换的最优行数和列数
int rPadded = getOptimalDFTSize(rows);
int cPadded = getOptimalDFTSize(cols);
//左侧和下侧补0
Mat f;
copyMakeBorder(I, f, 0, rPadded - rows, 0, cPadded - cols, BORDER_CONSTANT, Scalar::all(0));
//单通道转为双通道
Mat planes[] = {f, Mat::zeros(f.size(), CV_32F)};
merge(planes, 2, f);
//快速傅里叶变换(双通道,用于存储实部和虚部)
dft(f, F, DFT_COMPLEX_OUTPUT);
}
同理,在已知一幅图像的傅里叶变换的情况下,也可以恢复原图像,具体如下:
//傅里叶逆变换,并只取实部
dft(F, f, DFT_INVERSE + DFT_REAL_OUTPUT + DFT_SCALE);
//裁剪
I = f(Rect(0, 0, cols, rows));
通过以下主函数,可以测试从快速傅里叶变换到傅里叶逆变换的整个过程:
//输入图像矩阵
Mat I = imread(argv[1], CV_LOAD_IMAGE_GRAYSCALE);
if (!I.data)
return -1;
//数据类型转换:转换为浮点型
Mat fI;
img.convertTo(fI, CV_64FC1);
//快速傅里叶变换
Mat F;
fft2Image(fI, F);
//傅里叶逆变换,并只取实部
Mat iF;
cv::dft(F, iF, DFT_INVERSE + DFT_REAL_OUTPUT + DFT_SCALE);
//通过裁剪傅里叶逆变换的实部得到的i等于I
Mat i = I(Rect(0, 0, I.cols, I.rows)).clone();
//数据类型转换
i.convertTo(i, CV_8U);
分别记Real为矩阵 F F F的实部,Imaginary为矩阵 F F F的虚部,即:
F = Real + i ∗ Imaginary F=\text{Real}+i*\text{Imaginary} F=Real+i∗Imaginary
幅度谱通过以下公式计算:
Amplitude = Real 2 + Imaginary 2 \text{Amplitude}=\sqrt{\text{Real}^2+\text{Imaginary}^2} Amplitude=Real2+Imaginary2
注意:幅度谱在 (0, 0) 处的值等于输入矩阵 f f f 的所有值的和,这个值很大,是幅度谱中最大的值,它可能比其他项大几个数量级。
相位谱通过以下公式计算:
Phase = arctan ( Imaginary Real ) \text{Phase}=\arctan{(\frac{\text{Imaginary}}{\text{Real}})} Phase=arctan(RealImaginary)
下面对幅度谱和相位谱进行灰度级显示:
OpenCV中直接计算两个矩阵对应位置平方和的平方根:
void cv::magnitude(cv::InputArray x, cv::InputArray y, cv::OutputArray magnitude)
x:浮点型矩阵
y:浮点型矩阵
magnitude:幅度谱
因为对图像进行傅里叶变换后得到的是一个复数矩阵,保存在一个双通道 Mat 类中,所以在使用函数 magnitude
计算幅度谱时,需要利用 OpenCV 提供的函数 split
将傅里叶变换的实部和虚部分开。具体实现代码如下:
void amplitudeSpectrum(InputArray _srcFFT, OutputArray _dstSpectrum)
{
//判断傅里叶变换有两个通道
CV_Assert(_srcFFT.channels() == 2);
//分离通道
vector<Mat> FFT2Channel;
split(_srcFFT, FFT2Channel);
//计算傅里叶变换的幅度谱 sqrt(pow(R,2)+pow(I,2))
magnitude(FFT2Channel[0], FFT2Channel[1], _dstSpectrum);
}
对于傅里叶谱的灰度级显示,OpenCV 提供了函数 log
,该函数可以计算矩阵中每一个值的对数。进行归一化后,为了保存傅里叶谱的灰度级,有时需要将矩阵乘以 255,然后转换为 8 位图。具体代码如下:
Mat graySpectrum(Mat spectrum)
{
Mat dst;
log(spectrum + 1, dst);
//归一化
normalize(dst, dst, 0, 1, NORM_MINMAX);
//为了进行灰度级显示,做类型转换
dst.convertTo(dst, CV_8UC1, 255, 0);
return dst;
}
OpenCV提供的计算相位谱的函数:
void cv::phase(cv::InputArray x, cv::InputArray y, cv::OutputArray angle, bool angleInDegrees = false)
x:输入矩阵(傅里叶变换的实部矩阵)
y:输入矩阵(傅里叶变换的虚部矩阵)
angle:输出矩阵,且 angle = arctan ( y x ) \text{angle}=\arctan(\frac{y}{x}) angle=arctan(xy)
angleInDegrees:是否将角度转换到 [-180, 180]
视觉显著性检测可以看作抽取信息中最具差异的部分或者最感兴趣或首先关注的部分,赋予对图像分析的选择性能力,对提高图像的处理效率是极为重要的。
该算法的步骤如下:
**第一步:**计算图像的快速傅里叶变换矩阵 F F F。
**第二步:**计算傅里叶变换的幅度谱的灰度级 graySpectrum。
**第三步:**计算相位谱 phaseSpectrum,然后根据相位谱计算对应的正弦谱和余弦谱。
**第四步:**对第二步计算出的灰度级进行均值平滑,记为 mean(graySpectrum)。
**第五步:**计算谱残差(spectralResidual)。谱残差的定义是第二步得到的幅度谱的灰度级减去第四步得到的均值平滑结果,即:
spectralResidual = graySpectrum − f mean ( graySpectrum ) \text{spectralResidual}=\text{graySpectrum}-f_{\text{mean}}(\text{graySpectrum}) spectralResidual=graySpectrum−fmean(graySpectrum)
**第六步:**对谱残差进行幂指数运算 exp(spectralResidual),即对谱残差矩阵中的每一个值进行指数运算。
**第七步:**将第六步得到的幂指数作为新的“幅度谱”,仍然使用原图的相位谱,根据新的“幅度谱”和相位谱进行傅里叶逆变换,可得到一个复数矩阵。
**第八步:**对于第七步得到的复数矩阵,计算该矩阵的实部和虚部的平方和的开方,然后进行高斯平滑,最后进行灰度级的转换,即得到显著性。
OpenCV实现如下:
//读入图像(灰度化)
Mat image = imread(argv[1], CV_LOAD_IMAGE_GRAYSCALE);
if (!image.data)
return -1;
imshow("原图", image);
//转换为double类型
Mat fImage;
image.convertTo(fImage, CV_64FC1,1.0/255);
//快速傅里叶变换
Mat fft2;
fft2Image(fImage, fft2);
//幅度谱(又称傅里叶谱)
Mat amplitude;
amplitudeSpectrum(fft2, amplitude);
//对幅度谱进行对数运算
Mat logAmplitude;
cv::log(amplitude + 1.0, logAmplitude);
//均值平滑
Mat meanLogAmplitude;
cv::blur(logAmplitude, meanLogAmplitude, Size(3, 3),Point(-1,-1));
//谱残差
Mat spectralResidual = logAmplitude - meanLogAmplitude;
//相位谱
Mat phase = phaseSpectrum(fft2);
//余弦谱cos(phase)
Mat cosSpectrum(phase.size(), CV_64FC1);
//正弦谱sin(phase)
Mat sinSpectrum(phase.size(), CV_64FC1);
for (int r = 0; r < phase.rows; r++)
{
for (int c = 0; c < phase.cols; c++)
{
cosSpectrum.at<double>(r, c) = cos(phase.at<double>(r, c));
sinSpectrum.at<double>(r, c) = sin(phase.at<double>(r, c));
}
}
//指数运算
exp(spectralResidual, spectralResidual);
Mat real = spectralResidual.mul(cosSpectrum);
Mat imaginary = spectralResidual.mul(sinSpectrum);
vector<Mat> realAndImag;
realAndImag.push_back(real);
realAndImag.push_back(imaginary);
Mat complex;
merge(realAndImag, complex);
//快速傅里叶逆变换
Mat ifft2;
dft(complex, ifft2, DFT_COMPLEX_OUTPUT + DFT_INVERSE);
//傅里叶逆变换的幅度
Mat ifft2Amp;
amplitudeSpectrum(ifft2, ifft2Amp);
//平方运算
pow(ifft2Amp, 2.0, ifft2Amp);
//高斯平滑
GaussianBlur(ifft2Amp, ifft2Amp, Size(11,11), 2.5);
//显著性显示
normalize(ifft2Amp, ifft2Amp, 1.0, 0, NORM_MINMAX);
//提升对比度,进行伽马变换
pow(ifft2Amp, 0.5, ifft2Amp);
//数据类型转换
Mat saliencyMap;
ifft2Amp.convertTo(saliencyMap, CV_8UC1,255);
imshow("显著性", saliencyMap);
效果如下:
假设 I I I 是 M M M 行 N N N 列的图像矩阵, k k k 是 m m m 行 n n n 列的卷积核, I ★ k I \bigstar k I★k 是 I I I 与 k k k 的 full 卷积。这里在 full 卷积的运算过程中,采取 0 边界扩充的策略,扩充后的结果为 I padded I_{\text{padded}} Ipadded 和 k padded k_{\text{padded}} kpadded。
假设 F T Ip FT_{\text{Ip}} FTIp 和 F T kp FT_{\text{kp}} FTkp 分别是 I padded I_{\text{padded}} Ipadded 和 k padded k_{\text{padded}} kpadded 的傅里叶变换,那么 I ★ k I \bigstar k I★k 的傅里叶变换就等于 F T Ip . ∗ F T kp FT_{\text{Ip}} .* FT_{\text{kp}} FTIp.∗FTkp,即:
I ★ k ⇔ F T Ip . ∗ F T kp I \bigstar k \Leftrightarrow FT_{\text{Ip}} .* FT_{\text{kp}} I★k⇔FTIp.∗FTkp
其中,.*代表对应位置的元素相乘(同1.2.3中定义的点乘),即对应位置的两个复数相乘。该性质称为卷积定理。
计算步骤如下:
**第一步:**计算两个傅里叶变换 F T Ip FT_{\text{Ip}} FTIp 和 F T kp FT_{\text{kp}} FTkp 的点乘;
**第二步:**对点乘结果 F T Ip . ∗ F T kp FT_{\text{Ip}} .* FT_{\text{kp}} FTIp.∗FTkp 进行傅里叶逆变换;
**第三步:**取逆变换的实部,得到卷积结果 I ★ k I \bigstar k I★k。
在图像处理中,为了不改变图像的尺寸,通常计算的是 same 卷积,且采取其他比较理想的边界扩充策略。在上述实现中并没有采用快速傅里叶变换。下面就介绍如何利用快速傅里叶变换,且采用其他的边界扩充策略,从而得到 same 卷积结果。
计算步骤如下:
假设 I I I 是 M M M 行 N N N 列的图像矩阵, k k k 是 m m m 行 n n n 列的卷积核。
**第一步:**对 I I I 进行边界扩充,在上侧和下侧均补充 m − 1 2 \frac{m-1}{2} 2m−1 行,在左侧和右侧均补充 n − 1 2 \frac{n-1}{2} 2n−1 列。扩充策略和卷积计算的一样,效果比较好的是对边界进行镜像扩充,扩充后的结果记为 I padded I_{\text{padded}} Ipadded ,且行数为 + − 1,列数为 + − 1;
**第二步:**在 I padded I_{\text{padded}} Ipadded 和 k k k 的右侧和下侧扩充 0。为了利用快速傅里叶变换,将得到的结果记为 I padded_zeros I_{\text{padded\_zeros}} Ipadded_zeros 和 k zeros k_{\text{zeros}} kzeros;
**第三步:**计算 I padded_zeros I_{\text{padded\_zeros}} Ipadded_zeros 和 k zeros k_{\text{zeros}} kzeros 的傅里叶变换,分别记为 fft2 Ipz \text{fft2}_{\text{Ipz}} fft2Ipz 和 fft2 kz \text{fft2}_{\text{kz}} fft2kz;
**第四步:**计算上述两个复数矩阵(傅里叶变换的结果)的点乘:
fft2 Ipkz = fft2 Ipz . ∗ fft2 kz \text{fft2}_{\text{Ipkz}}=\text{fft2}_{\text{Ipz}}.*\text{fft2}_{\text{kz}} fft2Ipkz=fft2Ipz.∗fft2kz
**第五步:**计算 fft2 Ipkz \text{fft2}_{\text{Ipkz}} fft2Ipkz 的傅里叶逆变换,然后只取实部,得到的是 full 卷积的结果 fullConv \text{fullConv} fullConv;
**第六步:**裁剪。从 fullConv \text{fullConv} fullConv 的左上角 ( m − 1 , n − 1 ) (m-1,n-1) (m−1,n−1) 开始裁剪到右下角 ( m − 1 + M , n − 1 + N ) (m-1+M,n-1+N) (m−1+M,n−1+N) 的位置,该区域就是 same 卷积的结果。
OpenCV实现如下:
Mat fft2Conv(Mat I, Mat kernel, int borderType = BORDER_DEFAULT,Scalar value = Scalar())
{
// I的高、宽
int R = I.rows;
int C = I.cols;
// 卷积核kernel的高、宽均为奇数
int r = kernel.rows;
int c = kernel.cols;
// 卷积核的半径
int tb = (r - 1) / 2;
int lr = (c - 1) / 2;
/* 第一步:边界扩充 */
Mat I_padded;
copyMakeBorder(I, I_padded, tb, tb, lr, lr, borderType, value);
/* 第二步:在I_padded和kernel的右侧和下侧补0,以满足快速傅里叶变换的行数和列数 */
//满足二维快速傅里叶变换的行数、列数
int rows = getOptimalDFTSize(I_padded.rows + r -1);
int cols = getOptimalDFTSize(I_padded.cols + c - 1);
//补0
Mat I_padded_zeros, kernel_zeros;
copyMakeBorder(I_padded, I_padded_zeros, 0, rows - I_padded.rows, 0, cols - I_padded.cols, BORDER_CONSTANT, Scalar(0,0,0,0));
copyMakeBorder(kernel, kernel_zeros, 0, rows - kernel.rows, 0, cols - kernel.cols, BORDER_CONSTANT, Scalar(0,0,0,0));
/* 第三步:快速傅里叶变换 */
Mat fft2_Ipz,fft2_kz;
dft(I_padded_zeros, fft2_Ipz, DFT_COMPLEX_OUTPUT);
dft(kernel_zeros, fft2_kz, DFT_COMPLEX_OUTPUT);
/* 第四步:两个傅里叶变换点乘 */
Mat Ipz_kz;
mulSpectrums(fft2_Ipz, fft2_kz, Ipz_kz, DFT_ROWS);
/* 第五步:傅里叶逆变换,并只取实部 */
Mat ifft2;
dft(Ipz_kz, ifft2, DFT_INVERSE + DFT_SCALE + DFT_REAL_OUTPUT);
/* 第六步:裁剪,与所输入的图像矩阵的尺寸相同 */
Mat sameConv = ifft2(Rect(c - 1, r - 1, C + c - 1, R + r - 1));
return sameConv;
}