本系列爲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);
}
}
}
}