【darknet源碼解析-05】im2col.h 和 im2col.c 解析

本系列爲darknet源碼解析,本次解析src/im2col.h 與 src/im2col.c 兩個。這塊其實是卷積操作的底層實現。im2col主要是完成矩陣的向量轉換,爲了之後的gemm.c做矩陣乘法做準備,而im2col和gemm就是darknet卷積底層實現的核心。其實也是caffe卷積實現的核心。

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 的詳細分析如下:

#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);
            }
        }
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章