本次解析src/im2col.h 与 src/im2col.c 两个。这块其实是卷积操作的底层实现

im2col主要是完成矩阵的向量转换为之后的 gemm.c 做矩阵乘法做准备,而im2col和gemm就是darknet卷积底层实现的核心,其实也是caffe卷积实现的核心。

img2col.h

img2col.h 中的包含的代码如下:主要就是一个函数im2col_cpu定义,在这里我们先不涉及gpu那块,先讲解cpu这块的矩阵的向量转换。

#ifndef IM2COL_H
#define IM2COL_H
//先重点关注此函数:
void im2col_cpu(float* data_im,
        int channels, int height, int width,
        int ksize, int stride, int pad, float* data_col);

#ifdef GPU

void im2col_gpu(float *im,
         int channels, int height, int width,
         int ksize, int stride, int pad,float *data_col);

#endif
#endif

im2col.c

缩写:
im2col = image to column

im2col.c 的详细分析如下:

#include "im2col.h"
#include <stdio.h>
/**
 * 从输入多通道数组im(存储图像数据)中获取指定行、列、通道数处的元素值
 * @param im  输入,所有数据都存成一个一维数据,例如对于3通道而言,每一个通道按行存储(每一通道所有行合并成一行),三通道依次再并成一行
 * @param height 每一通道的高度(即输入图像的真正高度,补0之前)
 * @param width
 * @param channels  输入im的通道数,比如彩色图为3通道,之后每一次卷积的输入的通道数等于上一卷积层核的个数
 * @param row 要提取的元素所在的行(二维图像补0之后的行数)
 * @param col
 * @param channel 要提取的元素所在的通道
 * @param pad 图像左右上下各补0的长度(四个方向补0的长度一样)
 * @return  float类型数据,为im中channel通道,row-pad行,col-pad列处的元素值,高,宽;
 *          而row与col则是补0之后,元素所在的行列,因此,要准确获取im中元素值,首先要减去pad以获取真实的行列数;
 */
float im2col_get_pixel(float *im, int height, int width, int channels,
                        int row, int col, int channel, int pad)
{
    // 减去补0的长度,得到真实的行数和列数
    row -= pad;
    col -= pad;

    // 如果行列数小于0,则返回0(刚好是补0的效果)
    if (row < 0 || col < 0 ||
        row >= height || col >= width) return 0;

    // im存储多通道二维图像的数据格式为:各通道所有行并后成一行,再多通道一次并成一行;
    // 所在指定通道所在行,再加上col移位到所在列
    return im[col + width*(row + height*channel)];
}

//From Berkeley Vision's Caffe!
//https://github.com/BVLC/caffe/blob/master/LICENSE

/**
 * 将图片转为便于计算的数组格式,这是直接从caffe移植过来的
 * @param data_im 输入图像
 * @param channels 输入图像的通道数(对于都一层,一般是颜色图,3通道,中间层通道数为上一层卷积核个数)
 * @param height 输入图像的高度
 * @param width
 * @param ksize 卷积核尺寸
 * @param stride 步幅
 * @param pad 补0的个数
 * @param data_col 相当于输出,为进行格式化重排后的输入图像数据
 *
 * 说明:输出data_col的元素个数与data_im元素个数补相等,一般比data_im的元素个数多。
 *      因为stride较小,各个卷积核之间很很多重叠,
 */
void im2col_cpu(float* data_im,
     int channels,  int height,  int width,
     int ksize,  int stride, int pad, float* data_col) 
{
    int c,h,w;
    // 计算该层神经网络的输出图像尺寸(其实没有必要进行计算,因为在构建卷积层时,make_convolutional_layer()函数
    // 已经调用convolutional_out_width(), convolutional_out_height()函数求取这两个参数,
    // 此处直接调用l.out_h, l.out_w即可,函数参数只要传入该层网络指针即可)
    int height_col = (height + 2*pad - ksize) / stride + 1;
    int width_col = (width + 2*pad - ksize) / stride + 1;

    // 卷积核大小:ksize*ksize是一个卷积核大小,之所有乘以通道数channels,是因为输入图像是多通道。每个卷积核在做卷积时
    // 是同时对同一位置多通道的图像进行卷积运算,这里为了实现这一目的,将三通道上的卷积核并在一起以便于进行计算,因此卷积核
    // 实际上并不是二维的,而是三维的。比如对于3通道图像,卷积核尺寸为3*3,该卷积核同时作用在三通道图像上,这样并起来就得到含有
    // 27个元素的卷积核,且这27个元素都是独立的需要训练的参数。所以在计算训练参数个数时,一定要注意每一个卷积核的实际训练参数需要
    // 乘以通道数


    //***********这三层循环之间的逻辑关系,决定了输入图像重排后的格式 *********

    // 外循环次数为一个卷积核的尺寸数,循环次数即为最终得到的data_col的总行数
    int channels_col = channels * ksize * ksize;//im2col后的矩阵行数,3*3*3 = 27
    for (c = 0; c < channels_col; ++c) {
        // 列偏移,卷积核是一个二维矩阵,并按行存储在一维数组中,利用求余运算获取对应在卷积核的列数,比如对于
        // 3*3 的卷积核(3通道),当c=0,显然在第一列,当c=5,显示在第2列,当c=9时,在第二通道的卷积核的第一列,
        // 当c=26,在第三列(第三通道)。
        int w_offset = c % ksize;

        // 行偏移,卷积核是一个二维矩阵,且是安装(卷积核所有行并成一行)存储在一位数组中的,比如对于3*3的卷积核
        // 处理3通道的图像,那么一个卷积核具有27个元素,每9个元素对应一个通道上卷积核(互为一样),每当c为3的倍数,就
        // 意味着卷积核换了一行,h_offset取值为0,1,2,对应3*3卷积核中的第1,2,3行

        int h_offset = (c / ksize) % ksize;

        // 通道偏移,channels_col是多通道的卷积核并在一起的,比如对于3通道,3*3卷积和,每过9个元素就要换一通道数,
        // 当c=0-8时候,c_im=0, c=9-17, c_im=1, c=18-26, c_im=2;
        int c_im = c / ksize / ksize; // 计算目前处理第几个通道的图像
        for (h = 0; h < height_col; ++h) {
            // 中循环次数等于该层输出图像函数height_col, 说明data_col中的每一行存储了一张特征图,这张特征图又是按行存储在data_col中某行中
            for (w = 0; w < width_col; ++w) {
                // 由上面可知,对于3*3的卷积核,h_offset取值为0,1,2,当h_offset=0时,会提取出所有与卷积核第一行元素进行运算的像素,
                // 依次类推;加上h×stride是对卷积核进行行移位操作,比如卷积核从图像(0,0)位置开始做卷积,那么最先涉及(0,0)——(3,3)
                // 之间的像素值,若stride=2,那么卷积核进行一次行移位时,下一行的卷积操作是从元素(2, 0)(2为图像行号,0为列号)开始
                int im_row = h_offset + h * stride;
                // 对于3*3的卷积核,w_offset取值也为0,1,2,当w_offset=1,会提取所有与卷积核中第2列元素进行运算的像素,
                // 比如前一次卷积其实像素元素(0,0),若stride=2,那么下次卷积元素是从元素(2,0)(0为行号,2为列号)
                int im_col = w_offset + w * stride;
                // col_index为重排后图像中的像素索引。等于c * height_col * width_col * w(还是按行存储,所有通道在合并成一行)
                // 对应第c通道,h行,w列元素
                int col_index = (c * height_col + h) * width_col + w;
                // im2col_get_pixel函数获取输入图像data_im,第c_im通道,im_row, im_col的像素值并赋值给重排后的图像,
                // 不是真实输入图像中行列号,因此需要减去pad获得真实的行列号
                data_col[col_index] = im2col_get_pixel(data_im, height, width, channels,
                        im_row, im_col, c_im, pad);
            }
        }
    }
}

知识点:
卷积步长 https://www.softool.cn/blog-220.html

im2col()函数图解:

感觉直接讲解代码还是比较不直观,下面我们举个例子来说明im2col()到底是怎样工作的。

输入:data_im[] 是一个 5*5【实际】单通道的特征图,卷积核大小 3*3,卷积步长 stride 为 2,补0的个数 pad为 1,则传入参数如下:

data_im = {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};
pad = 1; stride = 2; ksize = 3; height = 5; width =5; chanels=1;

而data_im实际的逻辑结构如下,是5*5的二维矩阵。如下图所示:

05_im2col.h 和 im2col.c - 图1

补0的结果如下:

05_im2col.h 和 im2col.c - 图2

卷积后输出特征图的大小(行数,列数)计算公式为:

height_col = (height + 2*pad – ksize)/stride + 1 = (5 + 2*1 -3)/2 + 1 = 3
wight_col = (width + 2*pad – ksize)/stride + 1 = (5 + 2*1 -3)/2 + 1 = 3

输入特征图转换得到的输出矩阵data_col的行数:卷积核通道数×卷积核尺寸×卷积核尺寸,大家可以想想为什么行数是这么多???

chanenls_col = channels * ksize * ksize = 1*3*3 = 9

输入特征图转换得到的输出矩阵data_col的列数:卷积后输出特征图的宽度×卷积输出特征图的高度,这里也可想想为什么列数是这么多???

height_col*weight_col= 3*3 = 9

到这里我就知道输入特征图转换得到矩阵data_col的大小为 [channels_col, height_col * weight_col] = [9, 9]

那么接下来,我们就知道data_col矩阵每个元素是从输入矩阵data_im怎么样转换过来的。如下图所示:

05_im2col.h 和 im2col.c - 图3

以下 c,h,w 指上面程序中定义的 int c,h,w;

c = 0, h = 0, w = 0,1, 2的计算过程如下:黄色部分是对应到data_im坐标【row,col】,蓝色是data_col的编号【按行存储】,红色部分表示映射得到的值。

05_im2col.h 和 im2col.c - 图4


05_im2col.h 和 im2col.c - 图5


c = 0, h = 1, w = 0,1, 2的计算过程如下:黄色部分是对应到data_im坐标【row,col】,蓝色是data_col的编号【按行存储】,红色部分表示映射得到的值。

05_im2col.h 和 im2col.c - 图6


05_im2col.h 和 im2col.c - 图7


c = 0, h = 2, w = 0,1, 2的计算过程如下:黄色部分是对应到data_im坐标【row,col】,蓝色是data_col的编号【按行存储】,红色部分表示映射得到的值。

05_im2col.h 和 im2col.c - 图8


05_im2col.h 和 im2col.c - 图9


c = 1, h = 0, w = 0,1, 2的计算过程如下:黄色部分是对应到data_im坐标【row,col】,蓝色是data_col的编号【按行存储】,红色部分表示映射得到的值。

05_im2col.h 和 im2col.c - 图10


05_im2col.h 和 im2col.c - 图11


c = 1, h = 1, w = 0,1, 2的计算过程如下:黄色部分是对应到data_im坐标【row,col】,蓝色是data_col的编号【按行存储】,红色部分表示映射得到的值。

05_im2col.h 和 im2col.c - 图12


05_im2col.h 和 im2col.c - 图13


大家看看有没有找出规律。。。最终转换结果如下,data_col矩阵:大家data_col矩阵每一列与data_im中卷积核滑动过的位置。是不是有点明白了。

05_im2col.h 和 im2col.c - 图14