【算法随记五】使用FFT变换自动去除图像中严重的网纹。
这个课题在很久以前就已经有所接触,不过一直没有用代码去实现过。最近买了一本《机器视觉算法与应用第二版》书,书中再次提到该方法:使用傅里叶变换进行滤波处理的真正好处是可以通过使用定制的滤波器来消除图像中某些特定频率,例如这些特定频率可能代表着图像中重复出现的纹理。
在网络上很多的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了,也就是这一块的信息全部被消除了,当然实际操作时也可以稍微羽化一下,对最后的结果影响不大。
《任何未通知的转载或转发,都是猪狗不如的作为》。
根据上述的步骤,有选择性的处理了几幅图,结果如下所示:
可以看出,虽然能再一定程度上去除网纹,但是也就有一些去除的不完全,这主要还是因为自动提取的滤波器还是不够准确,要想获取更为理想的结果,必须手动的予以修缮。
对于常规的图片,或者说纹理信息不明显的图,及时执行了上面的去纹理,图片也基本上没有什么变化,因为按照上述方法得到的滤波器基本都为白色。
本文算法的测试例程见 : https://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,位于菜单FFT-->TextureRemoval下。