干貨 | 使用FFT變換自動去除圖像中嚴重的網(wǎng)紋
點擊上方“小白學視覺”,選擇加"星標"或“置頂”
重磅干貨,第一時間送達
本文轉(zhuǎn)自:opencv學堂
這個課題在很久以前就已經(jīng)有所接觸,不過一直沒有用代碼去實現(xiàn)過。最近買了一本《機器視覺算法與應用第二版》書,書中再次提到該方法:使用傅里葉變換進行濾波處理的真正好處是可以通過使用定制的濾波器來消除圖像中某些特定頻率,例如這些特定頻率可能代表著圖像中重復出現(xiàn)的紋理。
在網(wǎng)絡上很多的PS教程中,也有提到使用FFT來進行去網(wǎng)紋的操作,其中最為廣泛的是使用PS小插件FOURIER TRANSFORM,使用過程為:打開圖像--進行FFT RGB操作,然后定位到紅色通道,選取通道中除了最中心處的之外的白點區(qū)域,然后填充黑色,在返回綜合通道,點擊IFFT RGB,就OK了,


針對這一幅,我曾嘗試在PS中用其他的方法來去背景紋理,可是一般去網(wǎng)的同時也把相片模糊了,只有FFT去網(wǎng)紋插件能完美去掉相片的網(wǎng)紋而且不損傷畫質(zhì)。
這個插件有個特性,他要求輸入必須是3通道或者4通道的圖,但是用他處理完成后的圖雖然表面上看還是3通道還是4通道的,但是他已經(jīng)失去了彩色信息了,我們注意到他在進行FFT RGB操作后,RGB三個通道中,R通道保存了頻譜圖,G通道了保存了相位圖,B通道為固定值128,頻譜和相位組合在一起,只能回復一個通道的信息,因此處理后的圖也只能是一個顏色了,這是這個插件的缺陷或者說作為插件的必然性。
按照這個思路,如果用戶提供了用于消除與紋理對應的頻率的濾波器,則該過程的一個大概算法流程如下所示:
int IM_TextureRemoval(unsigned char *Src, unsigned char *Mask, unsigned char *Dest, int Width, int Height, int Stride)
{
int Channel = Stride / Width;
if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= 0) || (Height <= 0)) return IM_STATUS_INVALIDPARAMETER;
if ((Channel != 1) && (Channel != 3)) return IM_STATUS_INVALIDPARAMETER;
if (Channel == 1)
{
Complex *Data = (Complex*)malloc(Width * Height * sizeof(Complex));
if (Data == NULL) return IM_STATUS_OUTOFMEMORY;
for (int Y = 0; Y < Height; Y++)
{
unsigned char *LinePS = Src + Y * Stride; // 填充FFT變換的復數(shù)數(shù)據(jù)
Complex *LinePD = Data + Y * Width;
for (int X = 0; X < Width; X++)
{
LinePD[X].Real = LinePS[X];
LinePD[X].Imag = 0;
}
}
IM_FFT2D(Data, Data, Width, Height, false, 0, 0); // FFT變換
IM_FFTShift(Data, Data, Width, Height); // 平移中心到圖像的中心
for (int Y = 0; Y < Height; Y++) // FFT變換的結(jié)果乘以用于消除與紋理對應的頻率的濾波器
{
unsigned char *LinePS = Mask + Y * Stride;
Complex *LinePD = Data + Y * Width;
for (int X = 0; X < Width; X++)
{
LinePD[X].Real *= LinePS[X] * IM_INV255;
LinePD[X].Imag *= LinePS[X] * IM_INV255;
}
}
IM_IFFTShift(Data, Data, Width, Height); // 在反中心化
IM_FFT2D(Data, Data, Width, Height, true, 0, 0); // FFT逆變換
for (int Y = 0; Y < Height; Y++) // 轉(zhuǎn)換成圖像
{
Complex *LinePS = Data + Y * Width;
unsigned char *LinePD = Dest + Y * Stride;
for (int X = 0; X < Width; X++)
{
LinePD[X] = IM_ClampToByte(LinePS[X].Real);
}
}
free(Data);
}
else
{
}
return IM_STATUS_OK;
}這個過程也是非常簡單的。
對于彩色的圖像,可以把他們先劈成3個獨立的通道,然后調(diào)用上述單通道的處理方法,然后在合成。
不過這個方法還是有限制的,他能處理的對象是有非常嚴重網(wǎng)紋的圖像,我們測試過對于普通的身份證照片、摩爾紋等是起不到去除作用的,從頻譜上來說,就是要在頻譜上能看到分布在四周處有一些很明顯的獨立的亮點。這些亮點就對應著紋理的頻率。
上面的過程需要人工的參與,我們這里進行一下擴展,嘗試下對這類圖像進行自動的紋理去除。這里的核心是找到紋理的頻率,也就是那些白色獨立的亮點。
我們看上面的FFT頻譜圖,這種顯示基本上都是對直接進行FFT變換后的浮點數(shù)據(jù)進行對數(shù)變換后,在線性映射到0到255范圍內(nèi)的,有進行了log操作,數(shù)據(jù)壓縮了很多,導致頻譜圖的對比度不是很強,也不利于我們分隔出那些亮點,如果我們不記性這種操作,而是直接絕對值Clamp顯示,大概能得到下面的效果:

這種效果的FFT圖很明顯更有利于紋理特征的提取。
下面的步驟就是:OSTU二值化 -- 》膨脹 --》 腐蝕 -- 》 反色 ---》中心核保留 -- 》中值 得到紋理頻率的濾波器。整個效果如下圖:


稍微分析下原理吧(也不一定科學)。
首先二值化,沒啥好說的。二值后,我們看到白色部分有很多零碎的部分,特別是圖像的中心區(qū)域的零碎化對最后的效果有非常不好的影響(我們必須保持中心部分沒啥變化),所以后續(xù)使用了開操作來改善效果,先膨脹后腐蝕。接著我們反色一下,因為后續(xù)的濾波器是非中心區(qū)域的白色部分是要變?yōu)楹谏?,第五步,也是比較核心的步驟,我們需要把中心部分的黑色部分變?yōu)榘咨?,因為這部分保留著圖像的大部分信息, 這里我們可以采用基于4領(lǐng)域的區(qū)域生長法,因為在頻譜中的中心點,這一點二值后肯定是白色的,在反色后就是白色,就以這一點為種子點,向四周進行區(qū)域生長,這樣就可以把中心處的黑色反色過來,而其他地方的黑色保持不變。
第五步的中值,或者可以用其他模糊來代替,也是有點必要的,對于有些圖像,經(jīng)過前面的處理后,有些核心的線(垂直或者水平方向)也被標記為黑色的了,正在處理完成的圖像中會帶來原本沒有的新條紋。



上述過程先關(guān)的函數(shù)如下所示:
// 根據(jù)頻譜圖預估紋理的頻譜蒙版區(qū)域,支持InPlace操作
int IM_GetTextureMask(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride)
{
int Channel = Stride / Width;
if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= 0) || (Height <= 0)) return IM_STATUS_INVALIDPARAMETER;
if (Channel != 1) return IM_STATUS_INVALIDPARAMETER;
int Status = IM_STATUS_OK;
unsigned char *Temp = (unsigned char *)malloc(Height * Stride * sizeof(unsigned char));
if (Temp == NULL){ Status = IM_STATUS_OUTOFMEMORY; goto FreeMemory; }
int Threshold = 0;
Status = IM_GetOSTUThreshold(Src, Width, Height, Stride, Threshold); // 使用OSTU方法二值化
if (Status != IM_STATUS_OK) goto FreeMemory;
Status = IM_Threshold(Src, Temp, Width, Height, Stride, Threshold); // 二值化
if (Status != IM_STATUS_OK) goto FreeMemory;
Status = IM_Dilate(Temp, Dest, Width, Height, Stride, 2, false); // 先膨脹下(最大值),注意膨脹和腐蝕函數(shù)不支持InPlace操作
if (Status != IM_STATUS_OK) goto FreeMemory;
Status = IM_Erode(Dest, Temp, Width, Height, Stride, 2, false); // 然后在腐蝕(最小值),恢復原來的差不多大小,但是這樣中心區(qū)域不相鄰的點就少了很多
if (Status != IM_STATUS_OK) goto FreeMemory;
Status = IM_Invert(Temp, Dest, Width, Height, Stride); // 這個時候的圖,紋理的頻譜和其他核心能量區(qū)域都還是白色,為后續(xù)的處理需要先反色
if (Status != IM_STATUS_OK) goto FreeMemory;
Status = IM_InvertCenter(Dest, Temp, Width, Height, Stride); // 把中心的能量區(qū)域保留(白色),其他的紋理的頻譜刪除(黑色)
if (Status != IM_STATUS_OK) goto FreeMemory;
Status = IM_MedianBlur(Temp, Dest, Width, Height, Stride, 1, 50); // 執(zhí)行半徑為1的中值,這樣可能可以減少部分垂直或者水平的核心能力被刪除
if (Status != IM_STATUS_OK) goto FreeMemory;
FreeMemory:
if (Temp != NULL) free(Temp);
return Status;
}我們注意到,上面的操作對紋理處頻率處對應的濾波器系數(shù)都為0了,也就是這一塊的信息全部被消除了,當然實際操作時也可以稍微羽化一下,對最后的結(jié)果影響不大。
根據(jù)上述的步驟,有選擇性的處理了幾幅圖,結(jié)果如下所示:

可以看出,雖然能再一定程度上去除網(wǎng)紋,但是也就有一些去除的不完全,這主要還是因為自動提取的濾波器還是不夠準確,要想獲取更為理想的結(jié)果,必須手動的予以修繕。
對于常規(guī)的圖片,或者說紋理信息不明顯的圖,及時執(zhí)行了上面的去紋理,圖片也基本上沒有什么變化,因為按照上述方法得到的濾波器基本都為白色。
交流群
歡迎加入公眾號讀者群一起和同行交流,目前有SLAM、三維視覺、傳感器、自動駕駛、計算攝影、檢測、分割、識別、醫(yī)學影像、GAN、算法競賽等微信群(以后會逐漸細分),請掃描下面微信號加群,備注:”昵稱+學校/公司+研究方向“,例如:”張三 + 上海交大 + 視覺SLAM“。請按照格式備注,否則不予通過。添加成功后會根據(jù)研究方向邀請進入相關(guān)微信群。請勿在群內(nèi)發(fā)送廣告,否則會請出群,謝謝理解~

