sift的java实现解述

来源:互联网 发布:小凯老师淘宝 编辑:程序博客网 时间:2024/05/01 19:23

代码已经开源到github上,https://github.com/alibaba/simpleimage项目,其中的 analyze模块中。

原始图片为:


主要调用方法:

[java] view plain copy
  1. BufferedImage img = ImageIO.read(logoFile);  
  2.  RenderImage ri = new RenderImage(img);  
  3.  SIFT sift = new SIFT();  
  4.  sift.detectFeatures(ri.toPixelFloatArray(null));  
  5.  List<KDFeaturePoint> al = sift.getGlobalKDFeaturePoints();  


同样可以再读另一个张图得到另一个 List<KDFeaturePoint> al1,然后两个List进行match

[java] view plain copy
  1. List<Match> ms = MatchKeys.findMatchesBBF(al, al1);  
  2. ms = MatchKeys.filterMore(ms);  

先从上面的调用入口,详细讲解图sift的特征点生,至于match,有空再说。其中最难理解的是极值点的查找,主要对这部分讲解。


一.构建尺度空间,检测极值点,获得尺度不变性


ImagePixelArray就是保存一张图片的象素点进行灰度化后的数组。在RenderImage的toPixelFloatArray方法中默认灰度处理只是简单将rgb的值换成r,g,b的平均值并做归一化处理(除以255);

从调用入口我们进入主类SIFT的用户接口detectFeatures方法,它实际调用了detectFeaturesDownscaled(ImageMap img, int bothDimHi, double startScale) ,我们看随了开始的几行init工作,最后对图片使用preprocSigma(1.5)的参数进行高斯模数预处理。

[java] view plain copy
  1.         if (preprocSigma > 0.0) {  
  2.             GaussianArray gaussianPre = new GaussianArray(preprocSigma);  
  3.             img = gaussianPre.convolve(img);  
  4.         }  
进行高斯模糊的目的是为了使大片的灰度相近的点连成一片,而使一些比较突出的点更加区别于其它点,就象我们把一张灰度图片使用“版画”效果会把大量的连片点去掉只留下轮廓。

经过上面的预处理效果为:



现在我们还看不出强烈的效果,仅是pre的处理了一下。再接着使用Pyramid的buildOctaves方法构建8度金字塔,我们跟踪到方法内。

[java] view plain copy
  1. public int buildOctaves(ImagePixelArray source, float scale, int levelsPerOctave, float octaveSigm, int minSize) {  
  2.     this.octaves = new ArrayList<OctaveSpace>();  
  3.     OctaveSpace downSpace = null;  
  4.     ImagePixelArray prev = source;  
  5.   
  6.     while (prev != null && prev.width >= minSize && prev.height >= minSize) {  
  7.         OctaveSpace osp = new OctaveSpace();  
  8.   
  9.         // Create both the gaussian filtered images and the DOG maps  
  10.         osp.makeGaussianImgs(prev, scale, levelsPerOctave, octaveSigm); //构建当前8度空间的高斯模糊图像  
  11.         osp.makeGaussianDiffImgs();  
  12.         octaves.add(osp);  
  13.         prev = osp.getLastGaussianImg().halved(); //下一个8度空间的原始图象  
  14.         if (downSpace != null) downSpace.up = osp;  
  15.         osp.down = downSpace;  
  16.         downSpace = osp;  
  17.         scale *= 2.0;  
  18.     }  
  19.     return (octaves.size());  
  20. }  


先不看            

            osp.makeGaussianImgs(prev, scale, levelsPerOctave, octaveSigm);
            osp.makeGaussianDiffImgs();

 

整个方法就是以原图为基础不断地构造图层,这里也是高斯金字塔最微妙的地方。尺度空间概念不是简单的不同大小尺寸组成的尖塔,也不是相同大小不同模糊因子处理过的直方塔,其实说是金字塔不太准确,更象中国传统的宝塔。

它首先由原始图象根据不同的模糊因子进行模糊(说成是平滑更确切,就是把比较相近的颜色值让他们更相近以便突出反差很强的点),这是在同一尺寸上做的。这些相同尺寸不同高斯模糊因子处理过的图像集合叫一个8度空间。相当于宝塔中的一层,然后再向下采样,即以其中一幅进行1/2缩小作为原图再进行另一个8度空间的高斯模糊处理,直到图层的width或hight小于minSize(32),这样不断模糊并向下采样的构成的所有8度空间的集合才叫高斯金字塔(高斯宝塔?)。这是为了能检测到原图的某一点在不同尺度上都有稳定的特征。

            然后我们回头来看osp.makeGaussianImgs方法。对1个8度空间原始图象,以不同的 sigma 参数构建多张高斯模糊图。因为最底层的原图最大,我选了塔中scale为2的那一个8度间空的smoothedImgs.


可以看出随着模糊因子变化模糊程度在加大。在塔中的每一个8度空间得到了一个smoothedImgs 数组。这里一共生成6幅图像,原因在方法内部有注释,我们最终要在3层的差分尺度中获取极值点,而每个尺度获取极值点都要在立体空间(这也是sift最区别于其它特征的革命性突破)上比较,即要比较它上一幅和下一幅的对应点,那么3层的尺度至少要五幅差分图像,而五幅差分图像至少要6幅高斯模糊图象才能生成。

然后对smoothedImgs中的图象通过osp.makeGaussianDiffImgs();依次求差放入diffImags数组中。

对于使用不同参数进行模糊的两张图片,象素相近的连片部分差值极小,只有边缘,转角等特征的点表现出较大的差值:


先不要在意连片的黑色,因为求差后的值很小并已经做了归一化,我把它还原到图片上时进行绝对值(可能为负)乘10然后模 255,以便清楚地显示出来。可以看出这些图片中特征强烈的点都是边缘,转角等地方。

二.特征点过滤并进行精确定位


现在回到detectFeaturesDownscaled,经过是面的处理,金字塔中每个8度空间上都有一个OctaveSpace对象保存着一个差分图象数组。下面的findPeaks其实就是从第2个图象开始到倒数第二个图象循环,当前图象上每一个点和周围的点比较,如果是最大值或是最小值就视为极值点。(这里的周围是立体的周围,不仅和当前图像上点周围的8个点比较,还要和他上一幅和下一幅对应的9个点比较)

[java] view plain copy
  1. checkMinMax(current, c, x, y, ref, true);  
  2. checkMinMax(below, c, x, y, ref, false);  
  3. checkMinMax(above, c, x, y, ref, false);  
  4. if (ref.isMin == false && ref.isMax == falsecontinue;  
  5. peaks.add(new ScalePoint(x, y, curLev));  

回到detectFeaturesDownscaled,下面的filterAndLocalizePeaks方法主要是根据原始论文的page12/13的方法进行过虑和精确定位。

isTooEdgelike是对太象边缘的点进行过虑,这个意思就是连续的线不是好的极值点,只有角点这样的孤立的点才是极值点,为什么“太象边缘”的连续的线不好呢?边缘点的特点是沿边缘方向的梯度很小,简单说一条线的点差别不大,而和它相切的方向梯度很大。由于梯度大它们很容易成为极值点,但因为沿线的两个点之间梯度又几乎没有区别,极值点本身是局部特征,所以边缘线上两个点对于某极值进行投射时根本无法区别是点1还是点2.所以要把这些“线性连续点去掉”。

在过虑掉“太象边缘”的点后,下面的localizeIsWeak就精确化每个尺度空间上的极值点,因为极值点是在连续的尺度空间中计算出来的非常精确化的坐标比如(0.12345678,0.23456789),而原始图象的点是以整数为坐标的,相对尺度空间的坐标而言是散列的。而极值点最终要映射到原始图像的整数坐标上,所以要有一个调整过程。

根据极值点的坐标和sigma参数,主要是三元一,二价导数和亚象素计算。这样精确匹配到原始点后会得到一个原始点的坐标,sigma参数和调整值 local.dValue,下面的过虑条件就简单了:

            if (Math.abs(peak.local.scaleAdjust) > scaleAdjustThresh) continue;

            if (Math.abs(peak.local.dValue) <= dValueLoThresh) continue;

简单说当匹配到原始点在某一范围之外的都过虑掉,注意原始论文上建议dValueLoThresh为0.03,这里实际是0.008。

三.为每个关键点指定方向参数

查找极值点的工作都完成了,下面就是对这些点和周转的点比较生成一些向量:

在生成关键的方向和梯度时,我们用一个pretreatMagnitudeAndDirectionImgs方法把差分图上所有点的梯度方向和梯度值先计算出来,因为特征点的方向最终是它周围的64个点的梯度方向梯值加权计算出来的,这样每一点可能被多个特征点作为“周围点”,如果当作为周围点才计算某点的梯度方向梯值加,很多点会被多次计算,这样计算的总次数会大于所有点计算一次。所以我们会把每个点先计算出来存在一个数组中。

详见 makeFeaturePoint的注释

四.生成关键点的描述子

createDescriptors中有详细注释。


from: http://blog.csdn.net/axman/article/details/9243681

0 0