OpenCV 笔记(33):二维离散傅里叶变换及其性质

1. 二维离散傅里叶变换

DFT 是 Discrete Fourier Transform 即离散傅里叶变换的简称。二维离散傅里叶变换(2D Discrete Fourier Transform,简称 2D DFT)是将二维离散信号(例如数字图像)从空间域变换到频率域的一种数学工具。

1.1 定义

二维离散傅里叶变换的定义如下:

设 f(x,y) 是一个 M×N 的图像,其中 x=0,1,...,M−1 和 y=0,1,...,N−1。则其二维离散傅里叶变换 F(u,v) 定义为:

<math xmlns="http://www.w3.org/1998/Math/MathML"> F ( u , v ) = ∑ x = 0 M − 1 ∑ y = 0 N − 1 f ( x , y ) e − j 2 π ( u x / M + v y / N ) F(u, v) = \sum_{x = 0}^{M - 1} \sum_{y = 0}^{N - 1} f(x, y) e^{-j2\pi(ux/M + vy/N)} </math>F(u,v)=∑x=0M−1∑y=0N−1f(x,y)e−j2π(ux/M+vy/N)

其中,u=0,1,...,M−1 和 v=0,1,...,N−1。

二维离散傅里叶逆变换:

<math xmlns="http://www.w3.org/1998/Math/MathML"> f ( x , y ) = 1 M N ∑ u = 0 M − 1 ∑ v = 0 N − 1 F ( u , v ) e j 2 π ( u x / M + v y / N ) f(x, y) = \frac{1}{MN} \sum_{u = 0}^{M - 1} \sum_{v = 0}^{N - 1} F(u, v) e^{j2\pi(ux/M + vy/N)} </math>f(x,y)=MN1∑u=0M−1∑v=0N−1F(u,v)ej2π(ux/M+vy/N)

1.2 矩阵乘法表示

二维离散傅里叶变换可以表示为矩阵乘法,这是一种更直观、更易于理解的表示方式。

<math xmlns="http://www.w3.org/1998/Math/MathML"> F = W f \mathbf{F} = \mathbf{W} \mathbf{f} </math>F=Wf

其中,

  • F 是 M×N 的傅里叶变换矩阵,代表变换后的图像在频率域的数据。

  • f 是 M×N 的图像矩阵。

  • W 是 M×N 的傅里叶变换基矩阵,由傅里叶变换系数构成。

傅里叶变换基矩阵 W 的元素定义为: <math xmlns="http://www.w3.org/1998/Math/MathML"> W x y = e − j 2 π ( u x / M + v y / N ) W_{xy} = e^{-j2\pi(ux/M + vy/N)} </math>Wxy=e−j2π(ux/M+vy/N)

矩阵乘法表示傅里叶变换的过程,是将原始图像每个像素值与基矩阵中对应元素进行乘积并求和,得到变换后的每个频率分量。

二维离散傅里叶逆变换使用矩阵乘法表示:

<math xmlns="http://www.w3.org/1998/Math/MathML"> f = W H F \mathbf{f} = \mathbf{W}^H \mathbf{F} </math>f=WHF

其中, <math xmlns="http://www.w3.org/1998/Math/MathML"> W H \mathbf{W}^H </math>WH是 W 的共轭转置。

矩阵乘法表示的优势:

  • 简洁直观:用矩阵乘法表示二维离散傅里叶变换,可以直观地理解变换过程,并将计算过程转化为矩阵运算,方便计算机实现。
  • 易于编程:矩阵乘法是编程中常用的操作,可以用各种编程语言方便地实现二维离散傅里叶变换。
  • ⾼效计算:基于矩阵乘法的快速傅里叶变换(FFT)算法,可以显著提高计算效率,是图像处理中常用的傅里叶变换实现方法。
  • 便于扩展:矩阵乘法可以扩展到更高维度的傅里叶变换。

二维离散傅里叶变换按照定义的公式来计算,其计算量非常大。在实际应用中,通常使用快速傅里叶变换(FFT)算法来计算二维离散傅里叶变换。FFT 算法可以将二维离散傅里叶变换的计算量从 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( M 2 N 2 ) O(M^2N^2) </math>O(M2N2) 降低到 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( M N log ⁡ ( M N ) ) O(MN\log(MN)) </math>O(MNlog(MN))。

2. 二维离散傅里叶变换的性质

2.1 尺度变换

如果图像在空间域中进行尺度变换,则其傅里叶变换在频率域中会发生相应的尺度反变换。

f(x,y) 将其沿着 x 方向缩放 s 倍,或者沿着 y 方向缩放 t 倍,则其傅里叶变换 F(u,v) 沿着 u 方向缩放 1/s 倍,或者沿着 v 方向缩放 1/t 倍。

2.2 平移性

如果 f(x,y) 沿着 x 方向平移 p 个单位,或者沿着 y 方向平移 q 个单位,则其傅里叶变换 F(u,v) 沿着 u 方向平移 −p 个单位,或者沿着 v 方向平移 −q 个单位。

该性质意味着图像在空间域中平移,其傅里叶变换在频率域中对应方向上发生平移。

2.3 旋转不变性

如果 f(x,y) 绕原点旋转 θ 角度,则其傅里叶变换 F(u,v) 绕原点旋转 −θ 角度。

该性质意味着图像在空间域中旋转,其傅里叶变换在频率域中对应方向上旋转。

2.4 周期性

二维离散傅里叶变换的周期性体现在以下两个方面:

  • 水平方向周期性:将二维离散傅里叶变换的结果沿水平方向平移 M 个单位(即图像宽度),则结果将与原始结果相同。即: <math xmlns="http://www.w3.org/1998/Math/MathML"> F ( u + M , v ) = F ( u , v ) F(u + M, v) = F(u, v) </math>F(u+M,v)=F(u,v)
  • 垂直方向周期性:将二维离散傅里叶变换的结果沿垂直方向平移 N 个单位(即图像高度),则结果将与原始结果相同。即: <math xmlns="http://www.w3.org/1998/Math/MathML"> F ( u , v + N ) = F ( u , v ) F(u, v + N) = F(u, v) </math>F(u,v+N)=F(u,v)

二维离散傅里叶变换的周期性可以用以下公式来表示:

<math xmlns="http://www.w3.org/1998/Math/MathML"> F ( u + k M , v + l N ) = F ( u , v ) F(u + kM, v + lN) = F(u, v) </math>F(u+kM,v+lN)=F(u,v)

其中,k 和 l 是任意整数。

2.5 共轭对称性

二维离散傅里叶变换的实部和虚部具有共轭对称性。即

<math xmlns="http://www.w3.org/1998/Math/MathML"> F ( u , v ) = F ∗ ( − u , − v ) F(u,v)= \mathbf{F}^*(−u,−v) </math>F(u,v)=F∗(−u,−v)

该性质意味着二维离散傅里叶变换的频谱在实轴和虚轴对称。

2.6 傅里叶频谱和相角

二维离散傅里叶变换通常是复函数,因此可以用极坐标形式表示:

<math xmlns="http://www.w3.org/1998/Math/MathML"> F ( u , v ) = R ( u , v ) + j I ( u , v ) = ∣ F ( u , v ) ∣ e j ϕ ( u , v ) F(u,v)= R(u,v)+jI(u,v) = |F(u,v)|e^{j\phi(u,v)} </math>F(u,v)=R(u,v)+jI(u,v)=∣F(u,v)∣ejϕ(u,v)

幅度 :

<math xmlns="http://www.w3.org/1998/Math/MathML"> ∣ F ( u , v ) ∣ = [ R 2 ( u , v ) + I 2 ( u , v ) ] 1 / 2 |F(u,v)| = [R^2(u,v)+I^2(u,v)]^{1/2} </math>∣F(u,v)∣=[R2(u,v)+I2(u,v)]1/2 称为傅里叶频谱(频谱)。

而 <math xmlns="http://www.w3.org/1998/Math/MathML"> ϕ ( u , v ) = arctan ⁡ [ I ( u , v ) R ( u , v ) ] \phi(u,v) = \arctan[\frac{I(u,v)}{R(u,v)}] </math>ϕ(u,v)=arctan[R(u,v)I(u,v)] 称为相角(相位谱)。

傅里叶频谱它通常用图像表示,其中亮度表示幅度的大小。傅里叶频谱可以用来分析图像的频率特征。

相角是二维离散傅里叶变换的相位值,它表示图像中各个频率分量的相位信息。相角可以用来分析图像的相位特征。

2.7 二维离散卷积定理

两个函数在空间域中卷积的结果的傅里叶变换等于这两个函数的傅里叶变换在频率域中的乘积。

设 f(x,y) 和 h(x,y) 是两个 M×N 的图像,则其卷积 g(x,y) 定义为:

<math xmlns="http://www.w3.org/1998/Math/MathML"> g ( x , y ) = ∑ k = 0 M − 1 ∑ l = 0 N − 1 f ( k , l ) h ( x − k , y − l ) g(x, y) = \sum_{k = 0}^{M - 1} \sum_{l = 0}^{N - 1} f(k, l) h(x - k, y - l) </math>g(x,y)=∑k=0M−1∑l=0N−1f(k,l)h(x−k,y−l)

其中,x=0,1,...,M−1 和 y=0,1,...,N−1。

根据卷积定理,有:

<math xmlns="http://www.w3.org/1998/Math/MathML"> G ( u , v ) = F ( u , v ) H ( u , v ) G(u, v) = F(u, v) H(u, v) </math>G(u,v)=F(u,v)H(u,v)

其中,G(u,v)、F(u,v) 和 H(u,v) 分别是 g(x,y)、f(x,y) 和 h(x,y) 的二维离散傅里叶变换。

3. 示例

3.1 将频率域图像转换成空间域的图像

下面的代码,展示了图像从空间域转换到频域,再转换回空间域的过程。

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>

using namespace std;
using namespace cv;

// fft 变换后进行频谱中心化
void fftshift(cv::Mat &plane0, cv::Mat &plane1)
{
    int cx = plane0.cols / 2;
    int cy = plane0.rows / 2;
    cv::Mat q0_r(plane0, cv::Rect(0, 0, cx, cy));  // 元素坐标表示为(cx, cy)
    cv::Mat q1_r(plane0, cv::Rect(cx, 0, cx, cy));
    cv::Mat q2_r(plane0, cv::Rect(0, cy, cx, cy));
    cv::Mat q3_r(plane0, cv::Rect(cx, cy, cx, cy));

    cv::Mat temp;
    q0_r.copyTo(temp);  //左上与右下交换位置(实部)
    q3_r.copyTo(q0_r);
    temp.copyTo(q3_r);

    q1_r.copyTo(temp);  //右上与左下交换位置(实部)
    q2_r.copyTo(q1_r);
    temp.copyTo(q2_r);

    cv::Mat q0_i(plane1, cv::Rect(0, 0, cx, cy));  //元素坐标(cx,cy)
    cv::Mat q1_i(plane1, cv::Rect(cx, 0, cx, cy));
    cv::Mat q2_i(plane1, cv::Rect(0, cy, cx, cy));
    cv::Mat q3_i(plane1, cv::Rect(cx, cy, cx, cy));

    q0_i.copyTo(temp);  //左上与右下交换位置(虚部)
    q3_i.copyTo(q0_i);
    temp.copyTo(q3_i);

    q1_i.copyTo(temp);  //右上与左下交换位置(虚部)
    q2_i.copyTo(q1_i);
    temp.copyTo(q2_i);
}

int main()
{
    Mat src = imread(".../girl.jpg");
    imshow("src", src);

    Mat gray;
    cvtColor(src, gray, COLOR_BGR2GRAY);
    gray.convertTo(gray, CV_32FC1);

    cv::Mat planes[] = {Mat_<float>(gray), cv::Mat::zeros(src.size() , CV_32FC1) };

    cv::Mat complexI;
    cv::merge(planes, 2, complexI); // 合并通道 (把两个矩阵合并为一个2通道的Mat类容器)
    cv::dft(complexI, complexI); // 进行傅立叶变换

    // 分离通道(数组分离)
    cv::split(complexI, planes);

    // 计算幅值
    cv::Mat mag,mag_log,mag_nor;
    cv::magnitude(planes[0], planes[1], mag);

    // 幅值对数化:log(1+m)
    mag += Scalar::all(1);
    cv::log(mag, mag_log);
    cv::normalize(mag_log, mag_nor, 1,0, NORM_MINMAX);

    // 频谱中心化
    fftshift(planes[0], planes[1]);

    cv::Mat dst;

    // 再次频谱中心化
    fftshift(planes[0], planes[1]);
    cv::merge(planes, 2, dst); // 实部与虚部合并
    cv::idft(dst, dst);              // idft 结果也为复数
    dst = dst / dst.rows / dst.cols;
    cv::split(dst, planes);//分离通道,主要获取通道

    convertScaleAbs(gray,gray);
    convertScaleAbs(planes[0],planes[0]);

    imshow("gray", gray);
    imshow("result", planes[0]);
    waitKey(0);

    return 0;
}

3.2 添加盲水印

盲水印功能是指将水印以不可见的形式添加到图片中,这样的水印不会图片观感产生影响,同时也保证了图片的原创性。当发现图片被盗取后,可以通过提取盲水印,将不可见的水印提取出来,验证图片的归属。

下面的代码展示了添加盲水印的流程:

原图 -> 通过傅里叶变换,在频域上添加水印 -> 优化由 dft 操作产生的图像 -> 频域图 -> 傅里叶逆变换 -> 最终在空间域生成含有盲水印的图像

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>

using namespace std;
using namespace cv;

// 频谱中心化
void shiftDFT(cv::Mat &magnitudeImage)
{
    // 如果图像的尺寸是奇数的话对图像进行裁剪并重新排列(减去补充部分)
    magnitudeImage = magnitudeImage(cv::Rect(0, 0, magnitudeImage.cols & -2, magnitudeImage.rows & -2));

    // 重新排列图像的象限,使得图像的中心在象限的原点
    int cx = magnitudeImage.cols / 2;
    int cy = magnitudeImage.rows / 2;

    cv::Mat q0 = cv::Mat(magnitudeImage, cv::Rect(0, 0, cx, cy));    // 左上
    cv::Mat q1 = cv::Mat(magnitudeImage, cv::Rect(cx, 0, cx, cy));   // 右上
    cv::Mat q2 = cv::Mat(magnitudeImage, cv::Rect(0, cy, cx, cy));   // 左下
    cv::Mat q3 = cv::Mat(magnitudeImage, cv::Rect(cx, cy, cx, cy));  // 右下

    // 交换象限
    cv::Mat tmp = cv::Mat();

    // 左上与右下交换
    q0.copyTo(tmp);
    q3.copyTo(q0);
    tmp.copyTo(q3);

    // 右上与左下交换
    q1.copyTo(tmp);
    q2.copyTo(q1);
    tmp.copyTo(q2);
}

// 将频域的图像转换为空间域的图像
Mat transformImage(cv::Mat complexImage, vector<cv::Mat> allPlanes)
{
    cv::Mat invDFT;
    cv::idft(complexImage, invDFT, cv::DFT_SCALE | cv::DFT_REAL_OUTPUT, 0);

    cv::Mat restoredImage;
    invDFT.convertTo(restoredImage, CV_8U);

    // 合并多通道
    allPlanes.erase(allPlanes.begin());
    allPlanes.insert(allPlanes.begin(), restoredImage);

    cv::Mat dst;
    cv::merge(allPlanes, dst);

    return dst;
}

int main()
{
    Mat src = imread(".../girl.jpg");
    imshow("src", src);

    Mat complexI;   // 傅里叶变换结果,复数
    vector<Mat> planes;
    vector<Mat> allPlanes;

    // 将多通道分为单通道(因为读入的是彩色图)
    cv::split(src, allPlanes);

    // 只获取 B 通道
    Mat padded = cv::Mat();
    if (allPlanes.size() > 1) {
        padded = allPlanes[0];
    }
    else {
        padded = src;
    }

    padded.convertTo(padded, CV_32F);

    // 将单通道扩展至双通道,以接收 DFT 的复数结果
    planes.push_back(padded);
    planes.push_back(cv::Mat::zeros(padded.size(), CV_32F));
    // 将 planes 数组组合合并成一个多通道 Mat
    cv::merge(planes, complexI);

    // 进行离散傅里叶变换
    cv::dft(complexI, complexI);

    // 添加文本水印
    string text = "tony test";
    Scalar scalar = cv::Scalar(0,0,0,0);
    Point point = cv::Point(300, 300);
    int fontFace = cv::FONT_HERSHEY_TRIPLEX;
    double fontScale = 8.0;
    putText(complexI, text, point, fontFace, fontScale, scalar);
    flip(complexI, complexI, -1);
    putText(complexI, text, point, fontFace, fontScale, scalar);
    flip(complexI, complexI, -1);

    planes.clear();

    cv::split(complexI, planes);
    // 计算幅值矩阵
    Mat mag;
    cv::magnitude(planes[0], planes[1], mag);

    mag += Scalar::all(1);
    // 转换到对数尺度
    cv::log(mag, mag);

    // 剪切和重分布幅度图象限
    shiftDFT(mag);

    // 归一化,用 0 到 255 之间的浮点值将矩阵变换为可视化的图像格式
    mag.convertTo(mag, CV_8UC1);
    normalize(mag, mag, 0, 255, cv::NORM_MINMAX, CV_8UC1);

    imshow("Frequency Domain Image", mag);

    Mat result = transformImage(complexI, allPlanes);
    planes.clear();
    imshow("Spatial Domain Image", result);

    waitKey(0);
    return 0;
}

4. 总结

在空间域中,我们只能看到图像的像素值;在频率域中,我们可以看到图像的不同频率分量。这对于理解图像的结构和特征非常有用。二维离散傅里叶变换可以用于各种图像处理和分析任务,例如图像增强、去噪、分割、特征提取和压缩等。

相关推荐
jndingxin36 分钟前
OpenCV相机标定与3D重建(3)校正鱼眼镜头畸变的函数calibrate()的使用
opencv·3d
只怕自己不够好5 小时前
《全面解析图像平滑处理:多种滤波方法及应用实例》
图像处理·python·opencv
SEVEN-YEARS6 小时前
使用OpenCV实现图像拼接
人工智能·opencv·计算机视觉
Mr.鱼10 小时前
opencv undefined reference to `cv::noarray()‘ 。window系统配置opencv,找到opencv库,但连接不了
人工智能·opencv·计算机视觉
SEVEN-YEARS11 小时前
使用OpenCV实现视频背景减除与目标检测
opencv·目标检测·音视频
弗锐土豆1 天前
工业生产安全-安全帽第二篇-用java语言看看opencv实现的目标检测使用过程
java·opencv·安全·检测·面部
如若1231 天前
利用 `OpenCV` 和 `Matplotlib` 库进行图像读取、颜色空间转换、掩膜创建、颜色替换
人工智能·opencv·matplotlib
威桑1 天前
CMake + mingw + opencv
人工智能·opencv·计算机视觉
大白要努力!1 天前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
只怕自己不够好1 天前
《OpenCV 图像基础操作全解析:从读取到像素处理与 ROI 应用》
人工智能·opencv·计算机视觉