手撕OpenCV源码之filter2D(一)

opencv中的filter2D

函数原型

  • 官方解释

网站

CV_EXPORTS_W void filter2D( InputArray src, OutputArray dst, int ddepth,
                            InputArray kernel, Point anchor=Point(-1,-1),
                            double delta=0, int borderType=BORDER_DEFAULT );
  • ddepth是目标图像深度:每个像素所用的位数
    当前支持的图像深度为:
src.depth() = CV_8U, ddepth = -1/CV_16S/CV_32F/CV_64F
    src.depth() = CV_16U/CV_16S, ddepth = -1/CV_32F/CV_64F
    src.depth() = CV_32F, ddepth = -1/CV_32F/CV_64F
    src.depth() = CV_64F, ddepth = -1/CV_64F

ddepth为-1时,图像深度与源图像深度保持一致.

如果了解深度学习可能知道,深度学习中图像的直接卷积计算有4个参数,whck,但是在opencv
当前只支持k==1,c==1;如果图像为多通道,每个通道使用不同的kernel,则需要使用split()
函数将各通道拆开.

  • Point anchor:这个参数直接翻译叫做锚点,普通意义上,我们在计算卷积的时候,锚点是kernel的中心.中心与图像需要卷积的点对齐,计算改点及周围8个点的乘累加和,替代中心点.在OpenCV中可以设置其他点为锚点,这样就是以锚点和卷积的点对其,计算9个点.但是在Opencv中,对于anchor point做了计算,所以锚点的实际值与设置的值稍有差别,下文会详细介绍.

  • dalta:额外加的数值,就是在卷积过程中该数值会添加到每个像素上.

  • int borderType:表示对边界的处理方式.主要有以下几种:
/*
 Various border types, image boundaries are denoted with '|'
 * BORDER_REPLICATE:     aaaaaa|abcdefgh|hhhhhhh
 * BORDER_REFLECT:       fedcba|abcdefgh|hgfedcb
 * BORDER_REFLECT_101:   gfedcb|abcdefgh|gfedcba
 * BORDER_WRAP:          cdefgh|abcdefgh|abcdefg
 * BORDER_CONSTANT:      iiiiii|abcdefgh|iiiiiii  with some specified 'i'
 */

这是opencv中对5种宏的解释,例子描述的是比较形象的.
BORDER_REPLICATE: 表示对边界重复,来填充边界的扩展.
BORDER_REFLECTBORDER_REFLECT_101都是对边界的对称扩展,但是具体
方式不同,从例子可以很容易看出来.

Point anchor详解

关于Point anchor我在网上查了很多资料,解释都不够清楚,或者说在OpenCV中,按照常规解释,难以获得和OpenCV相同的计算结果.最终在OpenCV的源代码中找到了答案.先上源代码:
(opencv-2.4.9/modules/imgproc/src/filter.cpp)

void cv::filter2D( InputArray _src, OutputArray _dst, int ddepth,
                    InputArray _kernel, Point anchor,
                    double delta, int borderType )
{
     Mat src = _src.getMat(), kernel = _kernel.getMat();

     if( ddepth < 0 )
         ddepth = src.depth();

 #if CV_SSE2
     int dft_filter_size = ((src.depth() == CV_8U && (ddepth == CV_8U || ddepth == CV_16S)) ||
         (src.depth() == CV_32F && ddepth == CV_32F)) && checkHardwareSupport(CV_CPU_SSE3)? 130 : 50;
 #else
     int dft_filter_size = 50;
 #endif

     _dst.create( src.size(), CV_MAKETYPE(ddepth, src.channels()) );
     Mat dst = _dst.getMat();
     anchor = normalizeAnchor(anchor, kernel.size());//对anchor做了处理

 #ifdef HAVE_TEGRA_OPTIMIZATION
     if( tegra::filter2D(src, dst, kernel, anchor, delta, borderType) )
         return;
 #endif

     if( kernel.cols*kernel.rows >= dft_filter_size )
     {
         Mat temp;
         if( src.data != dst.data )
             temp = dst;
         else
             temp.create(dst.size(), dst.type());
         crossCorr( src, kernel, temp, src.size(),
                    CV_MAKETYPE(ddepth, src.channels()),
                    anchor, delta, borderType );
         if( temp.data != dst.data )
             temp.copyTo(dst);
         return;
     }

     Ptr f = createLinearFilter(src.type(), dst.type(), kernel,
                                              anchor, delta, borderType & ~BORDER_ISOLATED );
     f->apply(src, dst, Rect(0,0,-1,-1), Point(), (borderType & BORDER_ISOLATED) != 0 );
 }

可以看到,代码对anchor做了规范化处理.继续看源代码(opencv-2.4.9/modules/imgproc/src/precomp.hpp)

static inline Point normalizeAnchor( Point anchor, Size ksize )
  {
      if( anchor.x == -1 )
          anchor.x = ksize.width/2;
      if( anchor.y == -1 )
          anchor.y = ksize.height/2;
      CV_Assert( anchor.inside(Rect(0, 0, ksize.width, ksize.height)) );
      return anchor;
  }

很容易看到,(-1,-1)的时候,其实对应的是(ksize.width/2,ksize.height/2),也就是kernel的中心,所以默认情况为(-1,-1)时,是kernel center.
最后一句是判断语句,CV_Assert(val),若val为假则报错,inside函数是判断anchor是否在矩形内部,即anchor是不是within the kernel. 搞明白这一点以后,问题就简单了.
需要注意的是,src的填充与anchor有关.

0 1 2 3 4
0 1 0 1 0 1
1 2 2 2 2 2
2 0 1 3 1 3
3 3 5 4 2 1
4 1 3 3 1 2
0 1 2
0 1 1 1
1 1 1 1
2 1 1 1

假设以上分别为src(上)和kernel(下).首先来看常用的情况,anchor = (-1,-1),容易知道,其实是(1,1).也就是src的(0,0)与kernel的(1,1)对齐进行计算.这样需要对src进行填充,按照前文介绍的填充规则,填充后的数据是:

0 1 2 3 4 5 6
0 2 2 2 2 2 2 2
1 0 1 0 1 0 1 0
2 2 2 2 2 2 2 2
3 1 0 1 3 1 3 1
4 5 3 5 4 2 1 2
5 3 1 3 3 1 2 1
6 5 3 5 4 2 1 2

计算的结果如下:

0 1 2 3 4
0 13 14 13 14 13
1 9 12 12 15 12
2 21 22 22 20 16
3 22 23 23 20 14
4 33 31 29 20 14

以上是经常用到的计算方式,结果中加粗的部分是在不进行填充的时候求得的结果(5×5的src,3×3
的kernel,得到3×3的计算结果).
接下来计算anchor=(-1,2)的情况,其实是(1,2),也就是kernel的(1,2)与src的(0,0)对齐进行计算,那么填充的结果为:

0 1 2 3 4 5 6
0 1 0 1 3 1 3 1
1 2 2 2 2 2 2 2
2 0 1 0 1 0 1 0
3 2 2 2 2 2 2 2
4 1 0 1 3 1 3 1
5 5 3 5 4 2 1 2
6 3 1 3 3 1 2 1

计算的结果为:

0 1 2 3 4
0 9 12 12 15 12
0 13 14 13 14 13
1 9 12 12 15 12
2 21 22 22 20 16
3 22 23 23 20 14

以上是两种情况,其他情况类推.一共是9中情况,因为kernel有9个点,每个点都可以作为锚点;但是考虑到-1是1,所以在坐标设置上一共有16中情况,其计算结果是anchor解析 需要注意的是文章中的填充只是一种情况,根据anchor的不同需要进行不同的填充.

测试代码

输入数据跟试验不匹配,若用到自己修改

#include 
#include 
#include 
#include 

using namespace std;
using namespace cv;

int main()
{
    Mat srcImage = (Mat_<double>(5,5)<<1,2,3,4,5,   
                                       6,7,8,9,10,
                                       11,12,13,14,15,
                                       16,17,18,19,20,
                                       21,22,23,24,25);


   Mat kern = (Mat_<double>(3,3) << 1, 2, 3,
                                    1, 1, 1,
                                    1, 1, 1);
  Mat dstImage;

  int point_x,point_y;
  for(point_x = -2; point_x <= 2; point_x++){
      for(point_y = -2; point_y <= 2; point_y++){
              filter2D(srcImage,dstImage, -1 ,kern, Point(point_x,point_y),0,BORDER_DEFAULT);
              cout << "(" << point_x <<","<")" <for(int i = 0; i < 5; i++){
                  for(int j = 0; j < 5; j++){
                      double temp = dstImage.at<double>(i,j);
                      if((j+1)%5)
                         cout << temp <<" ";
                      else
                         cout << temp << endl;
                  }
              }
              cout << endl;
       }

  }

      return 0;
}

你可能感兴趣的:(OpenCV源码杂记)