Gabor变换的本质实际上还是对二维图像求卷积。因此二维卷积运算的效率就直接决定了Gabor变换的效率。在这里我先说说二维卷积运算以及如何通过二维傅立叶变换提高卷积运算效率。在下一步分内容中我们将此应用到Gabor变换上,抽取笔迹纹理的特征。
关于离散二维叠加和卷积的运算介绍的书籍比较多,我这里推荐William K. Pratt著,邓鲁华 张延恒 等译的《数字图像处理(第3版)》,其中第7章介绍的就是这方面的运算。为了便于理解,我用下面几个图来说明离散二维叠加和卷积的求解过程。
A可以理解成是待处理的笔迹纹理,B可以理解成Gabor变换的核函数,现在要求A与B的离散二维叠加卷积,我们首先对A的右边界和下边界填充0(zero padding),然后将B进行水平翻转和垂直翻转,如下图:
然后用B中的每个值依次乘以A中相对位置处的值并进行累加,结果填入相应位置处(注意红圈位置)。通常二维卷积的结果比A、B的尺寸要大。如下图所示:
根据傅立叶变换理论,对图像进行二维卷积等价于对图像的二维傅立叶变换以及核函数的二维傅立叶变换在频域求乘法。通过二维傅立叶变换可以有效提高卷积的运算效率。但在进行傅立叶变换时一定要注意“卷绕误差效应”,只有正确对原有图像以及卷积核填补零后,才能得到正确的卷积结果。关于这部分内容可以参考William K. Pratt著,邓鲁华 张延恒 等译的《数字图像处理(第3版)》第9章的相关内容,此处就不再赘述。
目前网上可以找到开源C#版的快速傅立叶变换代码(Exocortex.DSP),我使用的是1.2版,2.0版似乎只能通过CVS从SourceForge上签出, 并且功能没有什么太大改变。将Exocortex.DSP下载下来后,将源代码包含在自己的项目中,然后就可以利用它里面提供的复数运算以及傅立叶变换功能了。为了测试通过傅立叶变换求卷积的有效性,特编写以下代码:
using System;
using Exocortex.DSP;
class MainEntry
{
static void
Main
()
{
fftConv
2 c
= new fftConv2();
c.DoFFTConv2();
}
}
public class fftConv2
{
double[,] kernel = {{-1, 1},
{0, 1}};
double[,] data = {{10,5,20,20,20},
{10,5,20,20,20},
{10,5,20,20,20},
{10,5,20,20,20},
{10,5,20,20,20}};
Complex[] Kernel = new Complex[8*8];
Complex[] Data = new Complex[8*8];
Complex[] Result = new Complex[8*8];
private void Init()
{
for(int y=0; y<2; y++)
for(int x=0; x<2; x++)
Kernel[y*8+x].Re = kernel[y,x];
for(int y=0; y<5; y++)
for(int x=0; x<5; x++)
Data[y*8+x].Re = data[y,x];
}
public void DoFFTConv2()
{
Init();
Fourier.FFT2(Data, 8, 8, FourierDirection.Forward);
Fourier.FFT2(Kernel, 8, 8, FourierDirection.Forward);
for(int i=0; i<8*8; i++)
Result[i] = Data[i] * Kernel[i] / (8*8);
Fourier.FFT2(Result, 8, 8, FourierDirection.Backward);
for(int y=0; y<6; y++)
{
for(int x=0; x<6; x++)
Console.Write("{0,8:F2}", Result[y*8+x].Re);
Console.WriteLine();
}
}
}
程序的运行结果与离散二维叠加和卷积的运算结果完全相同。
由于卷积结果与原始输入图片的大小是不一样的,存在着所谓“边界”,在我的实际应用程序中,为了避免这些“边界”对结果过多的影响,我采用的是居中阵列定义,并且从卷积结果中只截取需要的那部分内容,确保和原始图片的大小完全一致,如下图:
这就需要对卷积的傅立叶求法做些微小的调整,具体调整办法就不说了,主要是坐标的变换,将示例代码贴上来供大家参考:
using System;
using Exocortex.DSP;
class MainEntry
{
static void
Main
()
{
CenterfftConv2 s = new CenterfftConv2();
s.CommonMethod();
s.DoFFTConv2();
}
}
public class CenterfftConv2
{
double[,] kernel = {{0, 1, 0},
{1, 2, 0},
{0, 0, 3}};
double[,] data = new double[12,12];
Complex[] Kernel = new Complex[16*16];
Complex[] Data = new Complex[16*16];
Complex[] Result = new Complex[16*16];
public CenterfftConv2()
{
Random r = new Random();
for(int y=0; y<12; y++)
for(int x=0; x<12; x++)
data[y,x] = r.NextDouble();
for(int y=0; y<3; y++)
for(int x=0; x<3; x++)
Kernel[y*16+x].Re = kernel[y,x];
for(int y=1; y<13; y++)
for(int x=1; x<13; x++)
Data[y*16+x].Re = data[y-1,x-1];
}
public void DoFFTConv2()
{
Console.WriteLine(" ========= By FFT2Conv2 Method =========");
Fourier.FFT2(Data, 16, 16, FourierDirection.Forward);
Fourier.FFT2(Kernel, 16, 16, FourierDirection.Forward);
for(int i=0; i<16*16; i++)
Result[i] = Data[i] * Kernel[i] / (16*16);
Fourier.FFT2(Result, 16, 16, FourierDirection.Backward);
for(int y=2; y<14; y++)
{
for(int x=2; x<14; x++)
Console.Write("{0,5:F2}", Result[y*16+x].GetModulus());
Console.WriteLine();
}
}
public void CommonMethod()
{
double real = 0;
Console.WriteLine(" ========== Direct Transform ===========");
for(int y=0; y < 12; y++)
{
for(int x=0; x < 12; x++)
{
for(int y1=0; y1 < 3; y1++)
for(int x1=0; x1 < 3; x1++)
{
// (kernel.Length-1)/2 = 1
if(((y - 1 + y1)>=0) &&
((y - 1 + y1)<12) &&
((x - 1 + x1)>=0) &&
((x - 1 + x1)<12))
{
real += data[y - 1 + y1, x - 1 + x1] * kernel[2 - x1, 2 - y1];
}
}
Console.Write("{0,5:F2}", real);
real=0;
}
Console.WriteLine();
}
Console.WriteLine("\n");
}
}
有了此部分的基础知识后,我们就要步入笔迹识别中最核心的部分Gabor变换,提取笔迹的特征了。
Gabor变换属于加窗傅立叶变换,Gabor函数可以在频域不同尺度、不同方向上提取相关的特征。另外Gabor函数与人眼的生物作用相仿,所以经常用作纹理识别上,并取得了较好的效果。二维Gabor函数可以表示为:
其中:
v的取值决定了Gabor滤波的波长,u的取值表示Gabor核函数的方向,K表示总的方向数。参数
图片来源:GaborFilter.html
图片来源:http://www.bmva.ac.uk/bmvc/1997/papers/033/node2.html
Gabor函数是复值函数,因此在运算过程中要分别计算其实部和虚部。代码如下:
private void CalculateKernel(int Orientation, int Frequency)
{
double real, img;
for(int x = -(GaborWidth-1)/2; x<(GaborWidth-1)/2+1; x++)
for(int y = -(GaborHeight-1)/2; y<(GaborHeight-1)/2+1; y++)
{
real = KernelRealPart(x, y, Orientation, Frequency);
img = KernelImgPart(x, y, Orientation, Frequency);
KernelFFT2[(x+(GaborWidth-1)/2) + 256 * (y+(GaborHeight-1)/2)].Re = real;
KernelFFT2[(x+(GaborWidth-1)/2) + 256 * (y+(GaborHeight-1)/2)].Im = img;
}
}
private double KernelRealPart(int x, int y, int Orientation, int Frequency)
{
double U, V;
double Sigma, Kv, Qu;
double tmp1, tmp2;
U = Orientation;
V = Frequency;
Sigma = 2 * Math.PI * Math.PI;
Kv = Math.PI * Math.Exp((-(V+2)/2)*Math.Log(2, Math.E));
Qu = U * Math.PI / 8;
tmp1 = Math.Exp(-(Kv * Kv * ( x*x + y*y)/(2 * Sigma)));
tmp2 = Math.Cos(Kv * Math.Cos(Qu) * x + Kv * Math.Sin(Qu) * y) - Math.Exp(-(Sigma/2));
return tmp1 * tmp2 * Kv * Kv / Sigma;
}
private double KernelImgPart(int x, int y, int Orientation, int Frequency)
{
double U, V;
double Sigma, Kv, Qu;
double tmp1, tmp2;
U = Orientation;
V = Frequency;
Sigma = 2 * Math.PI * Math.PI;
Kv = Math.PI * Math.Exp((-(V+2)/2)*Math.Log(2, Math.E));
Qu = U * Math.PI / 8;
tmp1 = Math.Exp(-(Kv * Kv * ( x*x + y*y)/(2 * Sigma)));
tmp2 = Math.Sin(Kv * Math.Cos(Qu) * x + Kv * Math.Sin(Qu) * y) - Math.Exp(-(Sigma/2));
return tmp1 * tmp2 * Kv * Kv / Sigma;
}
有了Gabor核函数后就可以采用前文中提到的“离散二维叠加和卷积”或“快速傅立叶变换卷积”的方法求解Gabor变换,并对变换结果求均值和方差作为提取的特征。32个Gabor核函数对应32次变换可以提取64个特征(包括均值和方差)。由于整个变换过程代码比较复杂,这里仅提供测试代码供下载。该代码仅计算了一个101×101尺寸的Gabor函数变换,得到均值和方差。代码采用两种卷积计算方式,从结果中可以看出,快速傅立叶变换卷积的效率是离散二维叠加和卷积的近50倍。
(程序代码在Visual Studio .net 2003下调试通过)。