一、前言
最近幾天接觸了圖像的傅里葉變換,數學原理依舊不是很懂,因此不敢在這里妄言。下午用Opencv代碼實現了這一變換,有一些經驗心得,願與大家分享。
二、關鍵函數解析
2.1copyMakeBorder() 擴展圖片尺寸
傅里葉變換的計算對圖像的尺寸有一定要求,尺寸不滿足要求的,可用copyMakeBorder() 函數進行擴展。函數定義如下:
void copyMakeBorder(InputArray src, //輸入圖像
OutputArray dst, //輸出圖像
int top, //上邊界添加的像素行數
int bottom, //下邊界添加的像素行數
int left, //左邊界添加的像素列數
int right, //右邊界添加的像素列數
int borderType, //表示邊界的類型
const Scalar& value=Scalar()//表示如果邊界的類型是BORDER_CONSTANT時邊界的顏色值 )
borderType邊界的類型有以下幾種:
1)BORDER_REPLICATE:重復: aaaaaa|abcdefgh|hhhhhhh
2)BORDER_REFLECT:反射: fedcba|abcdefgh|hgfedcb
3)BORDER_REFLECT_101:反射101: gfedcb|abcdefgh|gfedcba
4)BORDER_WRAP:外包裝: cdefgh|abcdefgh|abcdefg
5)BORDER_CONSTANT:常量復制: iiiiii|abcdefgh|iiiiiii(i的值由最后一個參數 const Scalar& value=Scalar()確定,如Scalar::all(0) )
2.2getOptimalDFTSize() 獲取最佳計算尺寸
離散傅里葉變換的計算速度與圖片的尺寸有關,當圖片的尺寸是2,3,5的倍數時,計算速度最快。常與函數copyMakeBorder() 配合使用,保證較快的計算速度。
int getOptimalDFTSize(int vecsize)
使用示例:
int m = getOptimalDFTSize(mImage.rows);//返回行的最佳尺寸
int n = getOptimalDFTSize(mImage.cols);//返回列的最佳尺寸
copyMakeBorder(mImage, mImage, 0, m - mImage.rows, 0, n - mImage.cols, BORDER_CONSTANT, Scalar(0));
2.3dft()傅里葉變換計算
傅里葉變換計算函數。
void dft(InputArray src, OutputArray dst, int flags=0, int nonzeroRows=0);
參數解釋:
InputArray src: 輸入圖像,可以是實數或虛數
OutputArray dst: 輸出圖像,其大小和類型取決於第三個參數flags
int flags = 0: 轉換的標識符,有默認值0。其可取的值如下所示:
1)DFT_INVERSE: 用一維或二維逆變換取代默認的正向變換 ;
2)DFT_SCALE: 縮放比例標識符,根據數據元素個數平均求出其縮放結果,如有N個元素,則輸出結果以1/N縮放輸出,常與DFT_INVERSE搭配使用;
3)DFT_ROWS: 對輸入矩陣的每行進行正向或反向的傅里葉變換;此標識符可在處理多種適量的的時候用於減小資源的開銷,這些處理常常是三維或高維變換等復雜操作;
4)DFT_COMPLEX_OUTPUT: 對一維或二維的實數數組進行正向變換,這樣的結果雖然是復數陣列,但擁有復數的共軛對稱性(CCS),可以以一個和原數組尺寸大小相同的實數數組進行填充,這是最快的選擇也是函數默認的方法。你可能想要得到一個全尺寸的復數數組(像簡單光譜分析等等),通過設置標志位可以使函數生成一個全尺寸的復數輸出數組;
5)DFT_REAL_OUTPUT: 對一維二維復數數組進行逆向變換,這樣的結果通常是一個尺寸相同的復數矩陣,但是如果輸入矩陣有復數的共軛對稱性(比如是一個帶有DFT_COMPLEX_OUTPUT標識符的正變換結果),便會輸出實數矩陣。
int nonzeroRows = 0: 當這個參數不為0,函數會假設只有輸入數組(沒有設置DFT_INVERSE)的第一行或第一個輸出數組(設置了DFT_INVERSE)包含非零值。這樣的話函數就可以對其他的行進行更高效的處理節省一些時間,這項技術尤其是在采用DFT計算矩陣卷積時非常有效。
2.4magnitude()計算二維矢量幅值
void magnitude(InputArray x, InputArray y, OutputArray magnitude);
參數解釋:
InputArray x: 浮點型數組的x坐標矢量,也就是實部
InputArray y: 浮點型數組的y坐標矢量,必須和x尺寸相同
OutputArray magnitude: 與x類型和尺寸相同的輸出數組
其計算公式如下:
2.5log()自然對數計算
log()函數的功能是計算每個數組元素絕對值的自然對數 。
void log(InputArray src,OutputArray dst)
參數解釋:
InputArray src:為輸入圖像
OutputArray dst:為得到的對數值
其原理如下:
2.6normalize()矩陣歸一化
歸一化就是把要處理的數據經過某種算法的處理限制在所需要的范圍內。首先歸一化是為了后面數據處理的方便,其次歸一化能夠保證程序運行時收斂加快。
void normalize(InputArray src, OutputArray dst,
double alpha=1, double beta=0, int norm_type=NORM_L2, int dtype=-1, InputArray mask=noArray() )
參數解釋:
InputArray src: 輸入圖像 ;
OutputArray dst: 輸出圖像,尺寸大小和src相同 ;
double alpha = 1: range normalization模式的最小值 ;
double beta = 0: range normalization模式的最大值,不用於norm normalization(范數歸一化)模式 ;
int norm_type = NORM_L2: 歸一化的類型,主要有 :
1)NORM_INF: 歸一化數組的C-范數(絕對值的最大值)
2)NORM_L1: 歸一化數組的L1-范數(絕對值的和)
3)NORM_L2: 歸一化數組的L2-范數(歐幾里得)
4)NORM_MINMAX: 數組的數值被平移或縮放到一個指定的范圍,線性歸一化,一般較常用。
int dtype = -1: 當該參數為負數時,輸出數組的類型與輸入數組的類型相同,否則輸出數組與輸入數組只是通道數相同,而depth = CV_MAT_DEPTH(dtype) ;
InputArray mask = noArray(): 操作掩膜版,用於指示函數是否僅僅對指定的元素進行操作。
歸一化公式:
1)norm_type!=NORM_MINMAX(線性函數轉換):
if mask(i,j)!=0
dst(i,j)=(src(i,j)-min(src))*(b'-a')/(max(src)-min(src))+ a'
else
dst(i,j)=src(i,j)
其中b'=MAX(a,b), a'=MIN(a,b);
2)norm_type!=NORM_MINMAX:
if mask(i,j)!=0
dst(i,j)=src(i,j)*a/norm (src,norm_type,mask)
else
dst(i,j)=src(i,j)
其中,函數norm的功能是計算norm(范數)的絕對值
三、代碼及結果分享
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
Mat mImage = imread("Lenna.jpg", 0);
if (mImage.data == 0)
{
cerr << "Image reading error" << endl;
system("pause");
return -1;
}
namedWindow("The original image", WINDOW_NORMAL);
imshow("The original image", mImage);
//Extending image
int m = getOptimalDFTSize(mImage.rows);
int n = getOptimalDFTSize(mImage.cols);
copyMakeBorder(mImage, mImage, 0, m - mImage.rows, 0, n - mImage.cols, BORDER_CONSTANT, Scalar(0));
//Fourier transform
Mat mFourier(mImage.rows + m, mImage.cols + n, CV_32FC2, Scalar(0, 0));
Mat mForFourier[] = { Mat_<float>(mImage), Mat::zeros(mImage.size(), CV_32F) };
Mat mSrc;
merge(mForFourier, 2, mSrc);
dft(mSrc, mFourier);
//channels[0] is the real part of Fourier transform,channels[1] is the imaginary part of Fourier transform
vector<Mat> channels;
split(mFourier, channels);
Mat mRe = channels[0];
Mat mIm = channels[1];
//Calculate the amplitude
Mat mAmplitude;
magnitude(mRe, mIm, mAmplitude);
//Logarithmic scale
mAmplitude += Scalar(1);
log(mAmplitude, mAmplitude);
//The normalized
normalize(mAmplitude, mAmplitude, 0, 255, NORM_MINMAX);
Mat mResult(mImage.rows, mImage.cols, CV_8UC1, Scalar(0));
for (int i = 0; i < mImage.rows; i++)
{
uchar* pResult = mResult.ptr<uchar>(i);
float* pAmplitude = mAmplitude.ptr<float>(i);
for (int j = 0; j < mImage.cols; j++)
{
pResult[j] = (uchar)pAmplitude[j];
}
}
Mat mQuadrant1 = mResult(Rect(mResult.cols / 2, 0, mResult.cols / 2, mResult.rows / 2));
Mat mQuadrant2 = mResult(Rect(0, 0, mResult.cols / 2, mResult.rows / 2));
Mat mQuadrant3 = mResult(Rect(0, mResult.rows / 2, mResult.cols / 2, mResult.rows / 2));
Mat mQuadrant4 = mResult(Rect(mResult.cols / 2, mResult.rows / 2, mResult.cols / 2, mResult.rows / 2));
Mat mChange1 = mQuadrant1.clone();
//mQuadrant1 = mQuadrant3.clone();
//mQuadrant3 = mChange1.clone();
mQuadrant3.copyTo(mQuadrant1);
mChange1.copyTo(mQuadrant3);
Mat mChange2 = mQuadrant2.clone();
//mQuadrant2 = mQuadrant4.clone();
//mQuadrant4 = mChange2.clone();
mQuadrant4.copyTo(mQuadrant2);
mChange2.copyTo(mQuadrant4);
namedWindow("The Fourier transform", WINDOW_NORMAL);
imshow("The Fourier transform", mResult);
waitKey();
destroyAllWindows();
return 0;
}
四、注意事項
4.1Mat對象類型問題
為方便運算,在計算過程中,Mat對象中數據都是float型,將其歸一化至0-255范圍后仍保留有小數部分,造成無法用imshow()正常顯示。最后結果必須用uchar強制轉換。源代碼中以下片段實現了這一過程:
normalize(mAmplitude, mAmplitude, 0, 255, NORM_MINMAX);
Mat mResult(mImage.rows, mImage.cols, CV_8UC1, Scalar(0));
for (int i = 0; i < mImage.rows; i++)
{
uchar* pResult = mResult.ptr<uchar>(i);
float* pAmplitude = mAmplitude.ptr<float>(i);
for (int j = 0; j < mImage.cols; j++)
{
pResult[j] = (uchar)pAmplitude[j];
}
}
4.2clone()與copyTo()的差異問題
clone 是完全的深拷貝,在內存中申請新的空間。copyTo 也是深拷貝,但是否申請新的內存空間,取決於dst矩陣頭中的大小信息是否與src一至,若一致則只深拷貝並不申請新的空間,否則先申請空間后再進行拷貝。
Mat A = Mat::ones(4,5,CV_32F);
Mat B = A.clone() //clone 是完全的深拷貝,在內存中申請新的空間,與A獨立
Mat C;
A.copyTo(C) //此處的C矩陣大小與A大小不一致,則申請新的內存空間,並完成拷貝,等同於clone()
Mat D = A.col(1);
A.col(0).copyTo(D) //此處D矩陣大小與A.col(0)大小一致,因此不會申請空間,而是直接進行拷貝,相當於把A的第1列賦值給第二列
因此在進行不同象限數據對換時,用copyTo()能成功對換數據,而用clone()則不能起到作用,原因就在於mQuadrant.clone()時重新開辟了內存空間,數據對換時對換的並非是原矩陣mResult中的數據。所以源代碼中,以下片段能夠正常對換數據,而注釋掉的部分不能正常工作。
Mat mChange1 = mQuadrant1.clone();
//mQuadrant1 = mQuadrant3.clone();
//mQuadrant3 = mChange1.clone();
mQuadrant3.copyTo(mQuadrant1);
mChange1.copyTo(mQuadrant3);
Mat mChange2 = mQuadrant2.clone();
//mQuadrant2 = mQuadrant4.clone();
//mQuadrant4 = mChange2.clone();
mQuadrant4.copyTo(mQuadrant2);
mChange2.copyTo(mQuadrant4);
注:錯誤之處,敬請雅正!