【OpenCV】OpenCV基础教程(9)—— 傅里叶变换

9. 傅里叶变换

9.1 二维离散的傅里叶(逆)变换

9.1.1 离散傅里叶变换

二维离散傅里叶变换的原理略,具体见书P346。

image-20211022222622790

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);
9.1.2 快速傅里叶变换

image-20211022223942806

在 OpenCV 中实现的傅里叶变换的快速算法是针对行数和列数均满足可以分解为 2𝑝 × 3𝑞 × 5𝑟 的情况的。所以计算二维矩阵的快速傅里叶变换时需要先对原矩阵进行扩充,在矩阵的右侧和下侧补 0,以满足该规则。对于补多少行多少列的 0,可以使用函数:

int cv::getOptimalDFTSize(int vecsize)

该函数返回一个不小于 vecsize,且可以分解为 2𝑝 × 3𝑞 × 5𝑟 的整数。

利用getOptimalDFTSizedft函数可以完成图像矩阵的快速傅里叶变换。其中参数 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);

9.2 傅里叶幅度谱与相位谱

分别记Real为矩阵 F F F的实部,Imaginary为矩阵 F F F的虚部,即:
F = Real + i ∗ Imaginary F=\text{Real}+i*\text{Imaginary} F=Real+iImaginary
幅度谱通过以下公式计算:
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]

9.3 谱残差显著性检测

视觉显著性检测可以看作抽取信息中最具差异的部分或者最感兴趣或首先关注的部分,赋予对图像分析的选择性能力,对提高图像的处理效率是极为重要的。

该算法的步骤如下:

**第一步:**计算图像的快速傅里叶变换矩阵 F F F

**第二步:**计算傅里叶变换的幅度谱的灰度级 graySpectrum。

**第三步:**计算相位谱 phaseSpectrum,然后根据相位谱计算对应的正弦谱和余弦谱。

**第四步:**对第二步计算出的灰度级进行均值平滑,记为 𝑓mean(graySpectrum)。

**第五步:**计算谱残差(spectralResidual)。谱残差的定义是第二步得到的幅度谱的灰度级减去第四步得到的均值平滑结果,即:
spectralResidual = graySpectrum − f mean ( graySpectrum ) \text{spectralResidual}=\text{graySpectrum}-f_{\text{mean}}(\text{graySpectrum}) spectralResidual=graySpectrumfmean(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);

效果如下:

image-20211026104204732

9.4 卷积与傅里叶变换的关系

假设 I I I M M M N N N 列的图像矩阵, k k k m m m n n n 列的卷积核, I ★ k I \bigstar k Ik I I I k k kfull 卷积。这里在 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 Ik 的傅里叶变换就等于 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}} IkFTIp.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 Ik

9.5 通过快速傅里叶变换计算卷积

在图像处理中,为了不改变图像的尺寸,通常计算的是 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} 2m1 行,在左侧和右侧均补充 n − 1 2 \frac{n-1}{2} 2n1 列。扩充策略和卷积计算的一样,效果比较好的是对边界进行镜像扩充,扩充后的结果记为 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) (m1,n1) 开始裁剪到右下角 ( m − 1 + M , n − 1 + N ) (m-1+M,n-1+N) (m1+M,n1+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;
}
  • 2
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Quentin_HIT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值