文章目录
FFT应用于卷积
卷积
卷积常用于描述线性时不变系统如何响应一个输入信号。
- 定义:
对于两个函数 f ( t ) f(t) f(t) 和 g ( t ) g(t) g(t),它们的卷积定义为:
( f ∗ g ) ( t ) = ∫ − ∞ ∞ f ( τ ) g ( t − τ ) d τ (f * g)(t) = \int_{-\infty}^{\infty} f(\tau) g(t - \tau) d\tau (f∗g)(t)=∫−∞∞f(τ)g(t−τ)dτ
- 物理解释:
考虑一个线性时不变系统,其冲激响应为 h ( t ) h(t) h(t)。冲激响应描述了系统对于一个单位冲激的响应。现在,假设我们有一个输入信号 x ( t ) x(t) x(t)。由于系统是线性时不变的,我们可以认为输入信号是由许多小的冲激组成的。每个这样的冲激会激发系统产生一个响应,这些响应会加在一起形成最终的输出。
- 推导:
-
假设输入信号 x ( t ) x(t) x(t) 在某个微小时间 Δ t \Delta t Δt 内的值为 x ( τ ) Δ t x(\tau)\Delta t x(τ)Δt。这微小的输入可以被看作一个微小的冲激。
-
由于系统的冲激响应为 h ( t ) h(t) h(t),这个微小的冲激将会导致一个微小的输出 x ( τ ) h ( t − τ ) Δ t x(\tau)h(t-\tau)\Delta t x(τ)h(t−τ)Δt
-
要得到系统在时间 t t t 的完整输出,我们需要将所有这些微小的输出加在一起:
y ( t ) = ∫ − ∞ ∞ x ( τ ) h ( t − τ ) d τ y(t) = \int_{-\infty}^{\infty} x(\tau) h(t - \tau) d\tau y(t)=∫−∞∞x(τ)h(t−τ)dτ
上述表达式描述了输入 x ( t ) x(t) x(t) 和系统的冲激响应 h ( t ) h(t) h(t) 的卷积。这就是为什么线性时不变系统的输出可以描述为输入和冲激响应的卷积。
此推导表明,对于任何给定的输入和一个线性时不变系统,输出总是输入和冲激响应的卷积。这为我们提供了一个强大的工具来分析和设计各种系统,从电子设备到声音处理等等。
卷积在FFT中推导
卷积定理是傅里叶变换的一个重要性质。它告诉我们,两个信号(或函数)在时域(或位置空间)中的卷积等于它们在频域中的乘积,反之亦然。
为了证明这一点,让我们从卷积的定义开始。对于两个函数 f ( t ) f(t) f(t) 和 g ( t ) g(t) g(t),它们的卷积定义为:
( f ∗ g ) ( t ) = ∫ − ∞ ∞ f ( τ ) g ( t − τ ) d τ (f * g)(t) = \int_{-\infty}^{\infty} f(\tau) g(t - \tau) d\tau (f∗g)(t)=∫−∞∞f(τ)g(t−τ)dτ
我们希望证明:
F { f ∗ g } = F { f } ⋅ F { g } \mathcal{F}\{f * g\} = \mathcal{F}\{f\} \cdot \mathcal{F}\{g\} F{
f∗g}=F{
f}⋅F{
g}
其中, F \mathcal{F} F 表示傅里叶变换。
证明:
-
假设:
F ( f ) = F { f } F(f) = \mathcal{F}\{f\} F(f)=F{ f}
G ( f ) = F { g } G(f) = \mathcal{F}\{g\} G(f)=F{ g} -
根据傅里叶变换的定义, F ( f ) F(f) F(f) 和 G ( f ) G(f) G(f) 分别为:
F ( f ) = ∫ − ∞ ∞ f ( t ) e − j 2 π f t d t F(f) = \int_{-\infty}^{\infty} f(t) e^{-j2\pi ft} dt F(f)=∫−∞∞f(t)e−j2πftdt
G ( f ) = ∫ − ∞ ∞ g ( t ) e − j 2 π f t d t G(f) = \int_{-\infty}^{\infty} g(t) e^{-j2\pi ft} dt G(f)=∫−∞∞g(t)e−j2πftdt -
考虑 f ( t ) ∗ g ( t ) f(t) * g(t) f(t)∗g(t) 的傅里叶变换:
F { f ∗ g } = ∫ − ∞ ∞ ( ∫ − ∞ ∞ f ( τ ) g ( t − τ ) d τ ) e − j 2 π f t d t \mathcal{F}\{f * g\} = \int_{-\infty}^{\infty} \left( \int_{-\infty}^{\infty} f(\tau) g(t - \tau) d\tau \right) e^{-j2\pi ft} dt F{ f∗g}=∫−∞∞(∫−∞∞f(τ)g(t−τ)dτ)e−j2πftdt -
交换积分的顺序:
F { f ∗ g } = ∫ − ∞ ∞ f ( τ ) ( ∫ − ∞ ∞ g ( t − τ ) e − j 2 π f t d t ) d τ \mathcal{F}\{f * g\} = \int_{-\infty}^{\infty} f(\tau) \left( \int_{-\infty}^{\infty} g(t - \tau) e^{-j2\pi ft} dt \right) d\tau F{ f∗g}=∫−∞∞f(τ)(∫−∞∞g(t−τ)e−j2πftdt)dτ -
将内部的积分变量 t t t 替换为 v = t − τ v = t - \tau v=t−τ,则 d t = d v dt = dv dt=dv,当 t t t 从 − ∞ -\infty −∞ 到 ∞ \infty ∞ 时, v v v 也是从 − ∞ -\infty −∞ 到 ∞ \infty ∞:
F { f ∗ g } = ∫ − ∞ ∞ f ( τ ) ( ∫ − ∞ ∞ g ( v ) e − j 2 π f ( v + τ ) d v ) d τ \mathcal{F}\{f * g\} = \int_{-\infty}^{\infty} f(\tau) \left( \int_{-\infty}^{\infty} g(v) e^{-j2\pi f(v+\tau)} dv \right) d\tau F{ f∗g}=∫−∞∞f(τ)(∫−∞∞g(v)e−j2πf(v+τ)dv)dτ -
由于 e − j 2 π f ( v + τ ) = e − j 2 π f v ⋅ e − j 2 π f τ e^{-j2\pi f(v+\tau)} = e^{-j2\pi fv} \cdot e^{-j2\pi f\tau} e−j2πf(v+τ)=e−j2πfv⋅e−j2πfτ,并且 e − j 2 π f τ e^{-j2\pi f\tau} e−j2πfτ 是常数关于 v v v 的,可以从内部积分中提出来:
F { f ∗ g } = ∫ − ∞ ∞ f ( τ ) e − j 2 π f τ ( ∫ − ∞ ∞ g ( v ) e − j 2 π f v d v ) d τ \mathcal{F}\{f * g\} = \int_{-\infty}^{\infty} f(\tau) e^{-j2\pi f\tau} \left( \int_{-\infty}^{\infty} g(v) e^{-j2\pi fv} dv \right) d\tau F{ f∗g}=∫−∞∞f(τ)e−j2πfτ(∫−∞∞g(v)e−j2πfvdv)dτ -
注意,内部的积分只是 g ( t ) g(t) g(t) 的傅里叶变换,即 G ( f ) G(f) G(f)。因此:
F { f ∗ g } = ∫ − ∞ ∞ f ( τ ) e − j 2 π f τ G ( f ) d τ \mathcal{F}\{f * g\} = \int_{-\infty}^{\infty} f(\tau) e^{-j2\pi f\tau} G(f) d\tau F{ f∗g}=∫−∞∞f(τ)e−j2πfτG(f)dτ -
这个表达式是 F ( f ) ⋅ G ( f ) F(f) \cdot G(f) F(f)⋅G(f) 的形式,因此我们证明了:
F { f ∗ g } = F ( f ) ⋅ G ( f ) \mathcal{F}\{f * g\} = F(f) \cdot G(f) F{ f∗g}=F(f)⋅G(f)
这就完成了证明。位置空间的卷积等于频率空间的直接乘法。
代码实现:
//FFT 使用Eigen库中的向量表示,方便二维计算
Eigen::VectorXcd FFT(const Eigen::VectorXcd& y, bool inv = false)
{
//数据的大小
int N = y.size();
// 确保输入的大小是2的幂
int nextPower = 1;
while (nextPower < N)
{
nextPower <<= 1;
}
// 使用0填充到下一个2的幂
Eigen::VectorXcd x = Eigen::VectorXcd::Zero(nextPower);
x.block(0, 0, N, 1) = y;
N = nextPower;
// 按位反转
for (int i = 1, j = 0; i < N; i++)
{
//bit = N/2
int bit = N >> 1;
/*
* 我们正在查看j的二进制表示中的每一位。
从左到右,我们检查每一位是否为1。
对于每一个为1的位,我们将其反转为0。
当我们遇到第一个为0的位时,循环终止。
*/
for (; j & bit; bit >>= 1)
{
j ^= bit;
}
//进行异或运算(XOR运算的工作原理是:当两个比较的位相同时,结果是0;当两个比较的位不同时,结果是1。)
j ^= bit;
//当 i >j 表示前面已经替换了位置, i==j,表示位置不用变
if (i < j)
{
std::swap(x[i], x[j]);
}
}
// 预先计算旋转因子
Eigen::VectorXcd w(N >> 1);
//逆变换 = 1;傅里叶变换 = -1;
double imag_i = inv ? 1.0 : -1.0;
std::complex<double> tempW = std::exp(std::complex<double>(0, imag_i * 2 * M_PI / N));
w[0] = 1;
for (int i = 1; i < (N >> 1); ++i)
{
// w[i] = std::polar(1.0, imag_i * 2 * M_PI * i / N);
//w[i] = std::pow(tempW, i);
w[i] = w[i - 1] * tempW;
}
// 迭代FFT
for (int len = 2; len <= N; len <<= 1)
{
int halfLen = len >> 1;
int step = N / len;
for (int i = 0; i < N; i += len)
{
for (int j = 0; j < halfLen; ++j)
{
std::complex<double> u = x[i + j];
std::complex<double> v = x[i + j + halfLen] * w[j * step];
x[i + j] = u + v;
x[i + j + halfLen] = u - v;
}
}
}
//使用逆变换时
if (inv)
{
for (std::complex<double>& a : x) {
a /= N;
}
}
return x;
}
// 离散傅里叶变换 - 二维
Eigen::MatrixXcd FFT2D(const Eigen::MatrixXcd& image, bool inv = false) {
int rows = image.rows();
int cols = image.cols();
// 确保输入的大小是2的幂
int newRows = 1, newCols = 1;
while (newRows < rows)
{
newRows <<= 1;
}
while (newCols < cols)
{
newCols <<= 1;
}
Eigen::MatrixXcd result = Eigen::MatrixXcd::Zero(newRows, newCols);
result.block(0, 0, image.rows(), image.cols()) = image;
//对于行填充为0的数据不做FFT,没必要。
for (int i = 0; i < image.rows(); i++)
{
result.row(i) = FFT(result.row(i).transpose(), inv).transpose();
}
for (int j = 0; j < newCols; j++)
{
result.col(j) = FFT(result.col(j), inv);
}
return result;
}
//fft 卷积 kernel 大小必须为奇数
Eigen::MatrixXd convolutionUsingFFT(const Eigen::MatrixXd& image, const Eigen::MatrixXd& kernel)
{
int augmentRows = kernel.rows() - 1;
int augmentCols = kernel.rows() - 1;
int newRows = image.rows() + augmentRows ;
int newCols = image.cols() + augmentCols ;
//边界用0填充
Eigen::MatrixXd paddedImage = Eigen::MatrixXd::Zero(newRows, newCols);
Eigen::MatrixXd paddedKernel = Eigen::MatrixXd::Zero(newRows, newCols);
// 将图像和核心填充到新的尺寸
paddedImage.block(0, 0, image.rows(), image.cols()) = image;
paddedKernel.block(0, 0, kernel.rows(), kernel.cols()) = kernel;
// FFT变换
Eigen::MatrixXcd imageFreq = FFT2D(paddedImage);
Eigen::MatrixXcd kernelFreq = FFT2D(paddedKernel);
// 频域中进行乘法操作
Eigen::MatrixXcd resultFreq = imageFreq.cwiseProduct(kernelFreq);
// 逆FFT转换以返回到空域
Eigen::MatrixXd result = FFT2D(resultFreq,true).real();
// 返回原始尺寸的结果
return result.block(augmentRows / 2 , augmentCols / 2, image.rows(), image.cols());
}
按块进行卷积,加快运算
//镜像填充
Eigen::MatrixXd MirrorPadding(const Eigen::MatrixXd& input, int padRows, int padCols,int augmentRows,int augmentCols)
{
// 创建一个新矩阵,大小为原始大小加上填充
Eigen::MatrixXd padded(input.rows() + 2 * augmentRows, input.cols() + 2 * augmentCols);
// 将原始矩阵的内容复制到新矩阵的中心
padded.block(padRows, padCols, input.rows(), input.cols()) = input;
// 填充上边界和下边界
for (int i = 0; i < padRows; ++i)
{
padded.row(i) = padded.row(padRows + padRows - 1 - i);
padded.row(padded.rows() - i - 1) = padded.row(padded.rows() - padRows - padRows + i);
}
// 填充左边界和右边界
for (int i = 0; i < padCols; ++i) {
padded.col(i) = padded.col(padCols + padCols - i - 1);
padded.col(padded.cols() - i - 1) = padded.col(padded.cols() - padCols - padCols + i);
}
// 填充四个角
for (int i = 0; i < padRows; ++i) {
for (int j = 0; j < padCols; ++j) {
padded(i, j) = input(padRows - i - 1, padCols - j - 1); // 左上角
padded(i, padded.cols() - j - 1) = input(padRows - i - 1, input.cols() - padCols + j); // 右上角
padded(padded.rows() - i - 1, j) = input(input.rows() - padRows + i, padCols - j - 1); // 左下角
padded(padded.rows() - i - 1, padded.cols() - j - 1) = input(input.rows() - padRows + i, input.cols() - padCols + j); // 右下角
}
}
return padded;
}
//零填充
Eigen::MatrixXd ZerosPadding(const Eigen::MatrixXd& input, int padRows, int padCols,int augmentRows,int augmentCols)
{
// 创建一个新矩阵,大小为原始大小加上填充
Eigen::MatrixXd padded = Eigen::MatrixXd::Zero(input.rows() + 2 * augmentRows, input.cols() + 2 * augmentCols);
// 将原始矩阵的内容复制到新矩阵的中心
padded.block(padRows, padCols, input.rows(), input.cols()) = input;
return padded;
}
Eigen::MatrixXd convolutionUsingFFT(const Eigen::MatrixXd& image,
const Eigen::MatrixXd& kernel,std::pair<int,int> point=std::pair<int,int>(-1,-1),
double delta = 0,int blockSize = 64);
//fft 卷积
Eigen::MatrixXd convolutionUsingFFT(const Eigen::MatrixXd& image, const Eigen::MatrixXd& kernel,std::pair<int,int> anchor,double delta,int blockSize)
{
//int blockSize = 64;
//块大小,必须是2的幂数。 每次FFT计算大小。用于节省计算。如果数据大小是1025,FFT就会扩展到2的幂数2048,防止没必要的计算。
int blockSizeRows = blockSize - anchor.second;
int blockSizeCols = blockSize - anchor.first;
if(kernel.rows()>blockSizeRows || kernel.cols()>blockSizeCols)
{
qDebug()<<"异常: kernel大小超出 block大小,请重新设置kernel大小或者block大小!";
return Eigen::MatrixXd();
}
//边界最大大小
int augmentRows = kernel.rows() - 1;
int augmentCols = kernel.cols() - 1;
//不能大于卷积核一半的大小
if(anchor.first > augmentCols || anchor.second > augmentRows)
{
return Eigen::MatrixXd();
}
// -1 默认是中间 augmentCols/2;
if(anchor.first == -1)
{
anchor.first = augmentCols/2;
}
if(anchor.second == -1)
{
anchor.second = augmentRows/2;
}
//扩充边界后最大大小
int newRows = image.rows() + augmentRows + augmentRows;
int newCols = image.cols() + augmentCols + augmentCols;
//计算卷积核频域 只需计算一次
Eigen::MatrixXd paddedKernel = Eigen::MatrixXd::Zero(blockSize, blockSize);
paddedKernel.block(0, 0, kernel.rows(), kernel.cols()) = kernel;
Eigen::MatrixXcd kernelFreq = FFT2D(paddedKernel);
//保存最终结果
Eigen::MatrixXd result(newRows,newCols);
//镜像填充
//Eigen::MatrixXd paddedImage = MirrorPadding(image,anchor.second,anchor.first,augmentRows,augmentCols);
//边界用0填充
Eigen::MatrixXd paddedImage = ZerosPadding(image,anchor.second,anchor.first,augmentRows,augmentCols);
for (int i = 0; i < newRows; i += (blockSize - augmentRows))//augmentRows :行数据最大重叠部分
{
for (int j = 0; j < newCols; j += (blockSize - augmentCols))//augmentCols :列数据最大重叠部分
{
// 创建一个大小为 blockSize x blockSize 的块并初始化为零
Eigen::MatrixXd block = Eigen::MatrixXd::Zero(blockSize, blockSize);
// 确保不超出矩阵的边界
int rowsToCopy = std::min(blockSize, newRows - i);
int colsToCopy = std::min(blockSize, newCols - j);
block.block(0, 0, rowsToCopy, colsToCopy) = paddedImage.block(i, j, rowsToCopy, colsToCopy);
// FFT变换
Eigen::MatrixXcd imageFreq = FFT2D(block);
// 频域中进行对位乘法操作
Eigen::MatrixXcd resultFreq = imageFreq.cwiseProduct(kernelFreq);
// 逆FFT转换以返回到时域
result.block(i, j, rowsToCopy-augmentRows, colsToCopy-augmentCols) = FFT2D(resultFreq,true).real()
.block(augmentRows, augmentCols, rowsToCopy-augmentRows, colsToCopy-augmentCols);
}
}
// 返回原始尺寸的结果
return result.block(0, 0, image.rows(), image.cols()).array() + delta;
}
int main()
{
cv::Mat img = cv::imread("face.jpg");
if (img.empty())
{
std::cout << "请确定是否输入正确的图像文件" << std::endl;
}
cv::Mat gray;
cvtColor(img, gray, cv::COLOR_BGR2GRAY);
//图像转换CV_32F储存
gray.convertTo(gray, CV_32F, 1 / 255.0, 0);
// 定义一个简单的 3x3 卷积核
cv::Mat kernel = (cv::Mat_<float>(5, 5) <<
1,0,-1,0,1,
-1, -1, -0, -1,-1,
1, 0, 4, 0,1,
-1, -1, 0, -1,-1,
1,0,-1,0,1 );
Eigen::MatrixXd eigenKernel =tMatrixXd(kernel);
//Mat 转 MatrixXd
Eigen::MatrixXd image = tMatrixXd(gray);
// 记录开始时间
auto start = std::chrono::high_resolution_clock::now();
Eigen::MatrixXd conv=convolutionUsingFFT(image,eigenKernel,std::pair<int,int>(4,4));
// 记录结束时间
auto stop = std::chrono::high_resolution_clock::now();
// 计算持续时间
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(stop - start);
qDebug() << "代码运行时长: " << duration.count() << " 微秒" ;
cv::Mat convz = tMat(conv);
cv::imshow("convz", convz);
// 记录开始时间
auto start1 = std::chrono::high_resolution_clock::now();
cv::Mat convolved;
cv::filter2D(gray, convolved, gray.depth(), kernel,Point(4,4),0,BORDER_CONSTANT);
// 记录结束时间
auto stop1 = std::chrono::high_resolution_clock::now();
// 计算持续时间
auto duration1 = std::chrono::duration_cast<std::chrono::microseconds>(stop1 - start1);
qDebug() << "代码运行时长: " << duration1.count() << " 微秒" ;
cv::imshow("conv1",convolved);
// // ... 这里进行结果比较 ...
for(int i=0;i<convz.rows;++i)
{
for(int j=0;j<convz.cols;++j)
{
if(std::abs( convz.at<float>(i,j) - convolved.at<float>(i,j))>1e-6)
{
qDebug()<<"不相等:"<<i<<","<<j<<" :"<<convz.at<float>(i,j)<<" : "<<convolved.at<float>(i,j);
}
}
}
qDebug() << "代码运行时长: " << duration1.count() << " 微秒";
}