【演算法隨記五】使用FFT變換自動去除影像中嚴重的網紋。
- 2019 年 10 月 8 日
- 筆記
這個課題在很久以前就已經有所接觸,不過一直沒有用程式碼去實現過。最近買了一本《機器視覺演算法與應用第二版》書,書中再次提到該方法:使用傅里葉變換進行濾波處理的真正好處是可以通過使用訂製的濾波器來消除影像中某些特定頻率,例如這些特定頻率可能代表著影像中重複出現的紋理。
在網路上很多的PS教程中,也有提到使用FFT來進行去網紋的操作,其中最為廣泛的是使用PS小插件FOURIER TRANSFORM,使用過程為:打開影像–進行FFT RGB操作,然後定位到紅色通道,選取通道中除了最中心處的之外的白點區域,然後填充黑色,在返回綜合通道,點擊IFFT RGB,就OK了,
原圖 FFR RGB 頻譜圖
用於消除與紋理對應的頻率的濾波器 IFFT RGB處理的結果圖
針對這一幅,我曾嘗試在PS中用其他的方法來去背景紋理,可是一般去網的同時也把相片模糊了,只有FFT去網紋插件能完美去掉相片的網紋而且不損傷畫質。
這個插件有個特性,他要求輸入必須是3通道或者4通道的圖,但是用他處理完成後的圖雖然表面上看還是3通道還是4通道的,但是他已經失去了彩色資訊了,我們注意到他在進行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變換的複數數據 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變換的結果乘以用於消除與紋理對應的頻率的濾波器 { 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++) // 轉換成影像 { 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個獨立的通道,然後調用上述單通道的處理方法,然後在合成。
不過這個方法還是有限制的,他能處理的對象是有非常嚴重網紋的影像,我們測試過對於普通的身份證照片、摩爾紋等是起不到去除作用的,從頻譜上來說,就是要在頻譜上能看到分布在四周處有一些很明顯的獨立的亮點。這些亮點就對應著紋理的頻率。
上面的過程需要人工的參與,我們這裡進行一下擴展,嘗試下對這類影像進行自動的紋理去除。這裡的核心是找到紋理的頻率,也就是那些白色獨立的亮點。
我們看上面的FFT頻譜圖,這種顯示基本上都是對直接進行FFT變換後的浮點數據進行對數變換後,在線性映射到0到255範圍內的,有進行了log操作,數據壓縮了很多,導致頻譜圖的對比度不是很強,也不利於我們分隔出那些亮點,如果我們不記性這種操作,而是直接絕對值Clamp顯示,大概能得到下面的效果:
這種效果的FFT圖很明顯更有利於紋理特徵的提取。
下面的步驟就是:OSTU二值化 — 》膨脹 –》 腐蝕 — 》 反色 —》中心核保留 — 》中值 得到紋理頻率的濾波器。整個效果如下圖:
二值化 膨脹(半徑2) 腐蝕(半徑2)
反色 保留中心區域 中值(半徑1)
稍微分析下原理吧(也不一定科學)。
首先二值化,沒啥好說的。 二值後,我們看到白色部分有很多零碎的部分,特別是影像的中心區域的零碎化對最後的效果有非常不好的影響(我們必須保持中心部分沒啥變化),所以後續使用了開操作來改善效果,先膨脹後腐蝕。 接著我們反色一下,因為後續的濾波器是非中心區域的白色部分是要變為黑色的,第五步,也是比較核心的步驟,我們需要把中心部分的黑色部分變為白色,因為這部分保留著影像的大部分資訊, 這裡我們可以採用基於4領域的區域生長法,因為在頻譜中的中心點,這一點二值後肯定是白色的,在反色後就是白色,就以這一點為種子點,向四周進行區域生長,這樣就可以把中心處的黑色反色過來,而其他地方的黑色保持不變。
第五步的中值,或者可以用其他模糊來代替,也是有點必要的,對於有些影像,經過前面的處理後,有些核心的線(垂直或者水平方向)也被標記為黑色的了,正在處理完成的影像中會帶來原本沒有的新條紋。
原圖 頻譜圖
去除中值濾波後的濾波器 對應的結果(有瑕疵)
增加中值後的濾波器 對應的結果
上述過程先關的函數如下所示:
// 根據頻譜圖預估紋理的頻譜蒙版區域,支援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); // 先膨脹下(最大值),注意膨脹和腐蝕函數不支援InPlace操作 if (Status != IM_STATUS_OK) goto FreeMemory; Status = IM_Erode(Dest, Temp, Width, Height, Stride, 2, false); // 然後在腐蝕(最小值),恢復原來的差不多大小,但是這樣中心區域不相鄰的點就少了很多 if (Status != IM_STATUS_OK) goto FreeMemory; Status = IM_Invert(Temp, Dest, Width, Height, Stride); // 這個時候的圖,紋理的頻譜和其他核心能量區域都還是白色,為後續的處理需要先反色 if (Status != IM_STATUS_OK) goto FreeMemory; Status = IM_InvertCenter(Dest, Temp, Width, Height, Stride); // 把中心的能量區域保留(白色),其他的紋理的頻譜刪除(黑色) if (Status != IM_STATUS_OK) goto FreeMemory; Status = IM_MedianBlur(Temp, Dest, Width, Height, Stride, 1, 50); // 執行半徑為1的中值,這樣可能可以減少部分垂直或者水平的核心能力被刪除 if (Status != IM_STATUS_OK) goto FreeMemory; FreeMemory: if (Temp != NULL) free(Temp); return Status; }
我們注意到,上面的操作對紋理處頻率處對應的濾波器係數都為0了,也就是這一塊的資訊全部被消除了,當然實際操作時也可以稍微羽化一下,對最後的結果影響不大。
《任何未通知的轉載或轉發,都是豬狗不如的作為》。
根據上述的步驟,有選擇性的處理了幾幅圖,結果如下所示:
可以看出,雖然能再一定程度上去除網紋,但是也就有一些去除的不完全,這主要還是因為自動提取的濾波器還是不夠準確,要想獲取更為理想的結果,必須手動的予以修繕。
對於常規的圖片,或者說紋理資訊不明顯的圖,及時執行了上面的去紋理,圖片也基本上沒有什麼變化,因為按照上述方法得到的濾波器基本都為白色。
本文演算法的測試常式見 : http://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,位於菜單FFT–>TextureRemoval下。