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) 定义为:

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

二维离散傅里叶逆变换:

1.2 矩阵乘法表示

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

其中,

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

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

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

傅里叶变换基矩阵 W 的元素定义为:

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

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

其中,是 W 的共轭转置。

矩阵乘法表示的优势:

  • 简洁直观:用矩阵乘法表示二维离散傅里叶变换,可以直观地理解变换过程,并将计算过程转化为矩阵运算,方便计算机实现。

  • 易于编程:矩阵乘法是编程中常用的操作,可以用各种编程语言方便地实现二维离散傅里叶变换。

  • ⾼效计算:基于矩阵乘法的快速傅里叶变换(FFT)算法,可以显著提高计算效率,是图像处理中常用的傅里叶变换实现方法。

  • 便于扩展:矩阵乘法可以扩展到更高维度的傅里叶变换。

二维离散傅里叶变换按照定义的公式来计算,其计算量非常大。在实际应用中,通常使用快速傅里叶变换(FFT)算法来计算二维离散傅里叶变换。FFT 算法可以将二维离散傅里叶变换的计算量从 降低到 。

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 个单位(即图像宽度),则结果将与原始结果相同。即:

  • 垂直方向周期性:将二维离散傅里叶变换的结果沿垂直方向平移 N 个单位(即图像高度),则结果将与原始结果相同。即:

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

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

2.5 共轭对称性

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

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

2.6 傅里叶频谱和相角

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

幅度 :

称为傅里叶频谱(频谱)。

而 称为相角(相位谱)。

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

2.7 二维离散卷积定理

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

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

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

根据卷积定理,有:

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

3.  示例

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

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

#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;
}
cde6b949e6d9822885fcb42df388bc38.jpeg
灰度图像和经过idft转换后的图像.png

3.2 添加盲水印

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

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

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

#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;
}
31687a98c976641602fce6f427adf608.jpeg
生成盲水印的过程.png

4.  总结

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

Java与Android技术栈】公众号

关注 Java/Kotlin 服务端、桌面端 、Android 、机器学习、端侧智能

更多精彩内容请关注:

  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这里提供一个基于OpenCV库的C语言代码,读取图像并进行二维离散傅立叶变换: ```c #include <opencv2/opencv.hpp> #include <iostream> #include <cmath> using namespace cv; using namespace std; int main(int argc, char** argv) { // 读取图像 Mat img = imread(argv[1], IMREAD_GRAYSCALE); if (img.empty()) { cout << "Could not read the image: " << argv[1] << endl; return -1; } // 双通道图像才能进行傅立叶变换 Mat img_float; img.convertTo(img_float, CV_32F); Mat planes[] = { img_float, Mat::zeros(img.size(), CV_32F) }; Mat complexImg; merge(planes, 2, complexImg); // 进行傅立叶变换 dft(complexImg, complexImg, DFT_COMPLEX_OUTPUT); // 将傅立叶变换结果分离成实部和虚部 split(complexImg, planes); Mat magImg; magnitude(planes[0], planes[1], magImg); // 对数变换,使结果更容易观察 magImg += Scalar::all(1); log(magImg, magImg); // 归一化到0~1之间 normalize(magImg, magImg, 0, 1, NORM_MINMAX); // 显示傅立叶变换的幅度谱 imshow("Magnitude spectrum", magImg); waitKey(); return 0; } ``` 这段代码首先使用OpenCV库中的`imread`函数读取图像,然后将图像转换为双通道浮点型图像,以便进行傅立叶变换。接着,使用`dft`函数对图像进行二维离散傅立叶变换,将得到的结果分离成实部和虚部,并计算出幅度谱。为了更容易观察幅度谱,对其进行对数变换,并将结果归一化到0~1之间。最后,使用`imshow`函数显示幅度谱,等待用户按下任意键后退出程序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值