文章目录

一、简介

关于傅里叶变换【点击查看】​, 讲的通俗易懂。
关于傅里叶变换讲解 ​​​【点击查看】​​​。
拓展阅读 ​penCV—python 图像矫正(基于傅里叶变换—基于透视变换)

对一张图像使用傅立叶变换就是将它分解成正弦和余弦两部分。也就是将图像从空间域(spatial domain)转换到频域(frequency domain)。

这一转换的理论基础来自于以下事实:任一函数都可以表示成无数个正弦和余弦函数的和的形式。傅立叶变换就是一个用来将函数分解的工具。 2维图像的傅立叶变换可以用以下数学公式表达:
OpenCV + CPP 系列(卅一)离散傅立叶变换_数组
式中 OpenCV + CPP 系列(卅一)离散傅立叶变换_opencv_02空间域(spatial domain)值OpenCV + CPP 系列(卅一)离散傅立叶变换_频域_03 则是频域(frequency domain)值。 转换之后的频域值是复数, 因此,显示傅立叶变换之后的结果需要使用实数图像(real image) 加虚数图像(complex image), 或者幅度图像(magitude image)加相位图像(phase image)。
在实际的图像处理过程中,仅仅使用了幅度图像,因为幅度图像包含了原图像的几乎所有我们需要的几何信息。 我们可以通过修改幅度图像或者相位图像的方法来间接修改原空间图像。

由于数字图像的离散性,像素值的取值范围也是有限的。比如在一张灰度图像中,像素灰度值一般在0到255之间。 因此,我们这里讨论的也仅仅是离散傅立叶变换(DFT)。

二、函数介绍

  • intgetOptimalDFTSize(int vecsize) 返回给定向量尺寸的最佳傅立叶变换尺寸

InputArray src,      源图像

OutputArray dst,   目标图像


int top,        上:图像每侧边框的长度(以像素为单位)


int bottom,       下


int left,        左


int right,        右


int borderType,    定义要应用的边框类型


const Scalar& value = Scalar()  填充边框像素的值


)



InputArray src,        输入矩阵,可以为实数或者虚数
OutputArray dst,     输出矩阵
int flags = 0,       转换的标识符,默认0即 DFT_COMPLEX_OUTPUT,详情说明如下

  • DFT_INVERSE 执行逆向 1D 或 2D 变换,而不是默认的正向变换。
  • DFT_SCALE 缩放结果:将其除以数组元素的数量。通常与DFT_INVERSE结合使用。
  • DFT_ROWS 对输入矩阵的每一行执行正向或逆向变换。此标志使您能够同时变换多个向量,并可用于减少开销(有时比处理本身大数倍)以执行 3D 和更高维变换等。
  • DFT_COMPLEX_OUTPUT 执行一维或二维实数数组的前向变换。结果虽然是一个复数数组,但具有复共轭对称性。这样的数组可以打包成与输入大小相同的真实数组,这是最快的选项,也是函数默认执行的操作。但是,您可能希望获得一个完整的复杂阵列(用于更简单的频谱分析等)。传递标志以启用函数以生成完整大小的复数输出数组。
  • DFT_REAL_OUTPUT 执行一维或二维复数数组的逆变换。结果通常是相同大小的复杂数组。但是,如果源数组具有共轭复对称性(例如,它是带有DFT_COMPLEX_OUTPUT 标志的前向变换的结果 ),则输出为实数数组。虽然函数本身不检查输入是否对称,但您可以传递标志,然后函数将假定对称并生成实际输出数组。请注意,当输入被打包成一个实数数组并执行逆变换时,该函数将输入视为一个打包的复共轭对称数组。因此,输出也将是一个真正的数组。

int nonzeroRows = 0    当参数不为零时,函数假定只有输入数组的第一个 nonzeroRows行( 未设置DFT_INVERSE)或仅 输出数组的第一个 nonzeroRows( 设置了DFT _INVERSE)包含非零值。因此,该函数可以更有效地处理其余行并节省一些时间。这种技术对于使用DFT计算阵列互相关或卷积非常有用。
)


  • void log(InputArray src, OutputArray dst); 对数运算
  • OpenCV + CPP 系列(卅一)离散傅立叶变换_opencv_04

离散傅立叶变换处理流程

  1. 将图像延扩到最佳尺寸. 离散傅立叶变换的运行速度与图片的尺寸息息相关。当图像的尺寸是2, 3,5的整数倍时,计算速度最快。 因此,为了达到快速计算的目的,经常通过添凑新的边缘像素的方法获取最佳图像尺寸。函数getOptimalDFTSize()返回最佳尺寸,使用函数 copyMakeBorder() 填充边缘像素
  2. 为傅立叶变换的结果(实部和虚部)分配存储空间. 傅立叶变换的结果是复数,这就是说对于每个原图像值,结果是两个图像值。 此外,频域值范围远远超过空间值范围, 因此至少要将频域储存在 float 格式中。 结果我们将输入图像转换成浮点类型,并多加一个额外通道来储存复数部分。
  3. 进行离散傅立叶变换. 支持图像原地计算 (输入输出为同一图像)。
  4. 将复数转换为幅度.复数包含实数部分(Re)和复数部分 (imaginary - Im)。 离散傅立叶变换的结果是复数,对应的幅度可以表示为:
    OpenCV + CPP 系列(卅一)离散傅立叶变换_傅立叶变换_05
  5. 对数尺度(logarithmic scale)缩放. 傅立叶变换的幅度值范围大到不适合在屏幕上显示。高值在屏幕上显示为白点,而低值为黑点,高低值的变化无法有效分辨。为了在屏幕上凸显出高低变化的连续性,我们可以用对数尺度来替换线性尺度: OpenCV + CPP 系列(卅一)离散傅立叶变换_傅立叶变换_06
  6. 剪切和重分布幅度图象限. 还记得我们在第一步时延扩了图像吗? 那现在是时候将新添加的像素剔除了。为了方便显示,我们也可以重新分布幅度图象限位置(注:将第五步得到的幅度图从中间划开得到四张1/4子图像,将每张子图像看成幅度图的一个象限,重新分布即将四个角点重叠到图片中心)。 这样的话原点(0,0)就位移到图像中心。
  7. 归一化. 这一步的目的仍然是为了显示。 现在我们有了重分布后的幅度图,但是幅度值仍然超过可显示范围[0,1] 。我们使用 normalize()函数将幅度归一化到可显示范围

头文件 ​​quick_opencv.h​​:声明类与公共函数

#pragma once
#include <opencv2\opencv.hpp>
using namespace cv;

class QuickDemo {
public:
...
void dft_demo(Mat& image);

};

主函数调用该类的公共成员函数

#include <opencv2\opencv.hpp>
#include <quick_opencv.h>
#include <iostream>
using namespace cv;


int main(int argc, char** argv) {
Mat src = imread("D:\\Desktop\\maomao.png");
if (src.empty()) {
printf("Could not load images...\n");
return -1;
}
namedWindow("input", WINDOW_NORMAL);
imshow("input", src);

QuickDemo qk;
qk.dft_demo(src);
waitKey(0);
destroyAllWindows();
return 0;
}

三、演示

源文件 ​​quick_demo.cpp​​:实现类与公共函数

void QuickDemo::dft_demo(Mat& image) {
cvtColor(image, image, COLOR_BGR2GRAY);

// 生成最优DFT图像尺寸;
Mat padded;
int h = getOptimalDFTSize(image.rows);
int w = getOptimalDFTSize(image.cols);
copyMakeBorder(image, padded, 0, h - image.rows, 0, w - image.cols, BORDER_CONSTANT, Scalar::all(0));

// 初始化矩阵数组用于存储实部与虚部;
Mat complexImage;
Mat planes_channel[] = { Mat_<float>(padded), Mat::zeros(padded.size(),CV_32F) };
merge(planes_channel, 2, complexImage);

// 离散傅里叶变换, -> 复数转换为幅度
dft(complexImage, complexImage);
split(complexImage, planes_channel);
magnitude(planes_channel[0], planes_channel[1], planes_channel[0]);
Mat magImage = planes_channel[0];

magImage += Scalar::all(1);
log(magImage, magImage);


magImage = magImage(Rect(0, 0, magImage.cols & -2, magImage.rows & -2));
int cx = magImage.cols / 2;
int cy = magImage.rows / 2;

Mat q0(magImage, Rect(0, 0, cx, cy)); // Top-Left
Mat q1(magImage, Rect(cx, 0, cx, cy)); // Top-Right
Mat q2(magImage, Rect(0, cy, cx, cy)); // Bottom-Left
Mat q3(magImage, Rect(cx, cy, cx, cy)); // Bottom-Right

Mat tmp; // swap quadrants (Top-Left with Bottom-Right)
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);

q1.copyTo(tmp); // swap quadrant (Top-Right with Bottom-Left)
q2.copyTo(q1);
tmp.copyTo(q2);

normalize(magImage, magImage, 0, 1, NORM_MINMAX);
imshow("spectrum magnitude", magImage);
}

OpenCV + CPP 系列(卅一)离散傅立叶变换_频域_07