首页 > 代码库 > Caffe 中卷积运算的原理与实现

Caffe 中卷积运算的原理与实现

caffe中卷积运算设计的很巧妙,今天就来讨论一下caffe中卷积运算的原理,最后会给出一个自己的实现版本,便于初学者理解。


Caffe中卷积运算的原理

俗话说,一图胜千言,首先先给出原理示意图,为了方便,这里以二维核为例

技术分享

滑动窗口在图像中每滑动一个地方,将图像中该滑动窗口图像展开为一列,所有列组成图中的滑动窗口矩阵,这里假设pad=1,stride=1,K=3,则滑动窗口矩阵每行大小为W*H,一共K*K行.

每个核展开为一行,N个核形成的核矩阵大小为N*K*K。

最后将核矩阵和滑动窗口矩阵相乘,每一行就是一个特征图,N个卷积核形成N个特征图。

扩展到三维核

技术分享

技术分享

三维核就是多了一个通道的概念,原理与二维核一样。


Caffe中卷积运算的实现

caffe源码中的src\caffe\util\im2col.cpp中的im2col_cpu函数给出了实现,但是个人觉得并不利于初学者的理解,这里给出一个自己根据该原理实现的代码,这里只考虑二维核的情况

#define  SATURATE(x)   (uchar)(((x) & ~255) == 0 ? (x) : ~((x)>>31))

void Convolution1(const Mat &srcImage, const Mat &kernel, Mat &dstImage)
{
    CV_Assert(srcImage.type() == CV_8UC1&&kernel.type()==CV_32FC1);

    // 目标图像大小
    int stride = 1;// 默认滑动窗口步长为1
    int kernelSize = kernel.rows;
    int widthOfDst = (srcImage.cols + kernelSize - 1 - kernelSize) / stride + 1;
    int heightOfDst = (srcImage.rows + kernelSize - 1 - kernelSize) / stride + 1;
    dstImage.create(heightOfDst, widthOfDst,CV_8UC1);

    // 扩充原图
    Mat extendedImage;
    copyMakeBorder(srcImage, extendedImage, kernelSize / 2, kernelSize / 2, kernelSize / 2, kernelSize / 2, BORDER_DEFAULT);

    // 生成滑动窗口矩阵
    int numberOfElementOfKernel = kernelSize*kernelSize;
    int numberOfPixelsOfDst = widthOfDst*heightOfDst;
    Mat slidingWindowMat(numberOfElementOfKernel, numberOfPixelsOfDst, CV_8UC1);
    uchar *dataOfSlidingWindowMat = slidingWindowMat.data;
    uchar *dataOfExtendImage = extendedImage.data;

    // 获取卷积核每个元素在原图中滑动的时候对应的所有像素,形成滑动窗口矩阵中的一行(滑动窗口矩阵也可以称为patch矩阵)
    /* 卷积核的该元素在图像中滑动的时候对应的每个像素的偏移由两部分组成(这里只考虑单通道图像): 
        1. 该元素在滑动窗口内的偏移(每次遍历的时候是固定的),注:卷积核滑动的时候称为滑动窗口
        2. 滑动窗口在图像中的偏移(滑动窗口第一个元素在图像中的偏移) 
    */
    for (int c = 0; c <= numberOfElementOfKernel - 1; ++c)
    {
        // 计算该元素在滑动窗口中y方向上和x方向上的偏移
        int offsetOfY_1 = (c / kernelSize) % kernelSize;
        int offsetOfX_1 = c % kernelSize;

        // 计算滑动窗口在图像中的偏移
        for (int y = 0; y <= heightOfDst - 1; ++y)
        {
            // 滑动窗口在y方向上的偏移(滑动窗口第一个元素在y方向上的偏移)
            int offsetOfY_2 = y*stride;

            for (int x = 0; x <= widthOfDst - 1; ++x)
            {
                // 滑动窗口在方向上的偏移(滑动窗口第一个元素在x方向上的偏移)
                int offsetOfX_2 = x*stride;

                // 最后的偏移
                int offsetOfY = offsetOfY_1 + offsetOfY_2;
                int offsetOfX = offsetOfX_1 + offsetOfX_2;

                dataOfSlidingWindowMat[c*numberOfPixelsOfDst + y*widthOfDst + x] = dataOfExtendImage[offsetOfY*extendedImage.cols + offsetOfX];
            }
        }

    }

    // 卷积核与滑动窗口矩阵相乘,这部分可以使用矩阵运算加速(比如BLAS,Eigen)
    float *dataOfKernel = (float *)kernel.data;
    uchar *dataOfDst = dstImage.data;
    for (int x = 0; x <= slidingWindowMat.cols - 1; ++x)
    {
        float sum = 0; // 注意,这里不能使用int sum=0;每次都会有精度损失
        for (int y = 0; y <= numberOfElementOfKernel - 1; ++y)
        {
            sum += (dataOfKernel[y] * slidingWindowMat.at<uchar>(y, x));
        }

        // 卷积结果赋值为结果图像,注意溢出的处理!
        dataOfDst[x] = SATURATE((int)sum);
    }

} // Convolution1

下面是测试代码

void TestConvolution1()
{

    Mat image_Src=http://www.mamicode.com/imread("Test.jpg",-1);

    // edge detection
    Mat kernel = (Mat_<float>(3, 3) << -1., 0, 1.,
                                        -2., 0, 2.,
                                        -1., 0, 1.
                                        );
    // sharpen
    /*Mat kernel = (Mat_<float>(3, 3) << -1, -1, -1,
                                        -1, 9, -1,
                                        -1, -1, -1.
                                        );*/

    // blur
    /*Mat kernel = (Mat_<float>(3, 3) << 1.0 / 9, 1.0 / 9, 1.0 / 9,
                                    1.0 / 9, 1.0 / 9, 1.0 / 9,
                                    1.0 / 9, 1.0 / 9, 1.0 / 9
                                    );*/


    Mat image_Blur;
    Convolution1(image_Src, kernel, image_Blur);
    imwrite("Test.bmp",image_Blur);
}

原图
技术分享

结果图
技术分享

由于示例程序中的卷积核是垂直方向的Sobel算子,所以结果图像中对垂直方向的边缘比较敏感,而对水平方向的边缘不敏感。

2017-5-13 17:04:46


非常感谢您的阅读,如果您觉得这篇文章对您有帮助,欢迎扫码进行赞赏。
技术分享

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Caffe 中卷积运算的原理与实现