Non-Local Deep Features for Salient Object Detection


前言:

這篇論文是2017年cvpr頂會的論文,該論文文筆很優美,讀起來很舒服。這篇論文的作者是廈門大學的Zhiming Luo。

 

摘要:

作者指出:對於處理具有復雜背景的圖像,傳統的方法做的不好,而對於現在的深度學習方法,結構上有點過度的復雜並且處理圖像的速度並不能實時。因此,作者提出了他的解決方案:提出一個基於vgg16簡單的,結合4x5網格的多分辨率局部和全局特征信息的端到端的神經網絡。作者並沒有使用CRF或者是超像素,而是使用一個懲罰邊界錯誤的loss項來約束空間一致性。超像素預處理和CRF后處理過程,都是比較費時間的。該方案在達到和最好的效果同等水平的同時,在推斷處理的速度上也接近實時。

 

介紹:

典型的傳統方法是提取基於像素或基於區域的局部特征,然后與全局特征對比,會得到顯著圖。而深度學習相對於傳統的方法的好處在於,他們能夠使用局部和深度特征的結合,通過簡單優化函數來端到端的訓練。目前針對解決顯著性目標檢測特制的CNN架構,在速度上還有很大的空間可以提升。

作者提出的模型是4*5網格的卷積塊和反卷積塊組成。網格的每一列提取特定分辨率的特征,在每一列中局部對比處理模塊強化了局部特征的對比,局部特征和全局特征通過一個score處理模塊組合在一起,最終輸出輸入圖片一半分辨率大小的顯著圖。

簡單的網絡架構流程描述:

該深度卷積網絡架構是基於vgg-16的,我們知道vgg-16是有五個池化層的,而本文保留了vgg-16的前13層(即到第五個池化層),后面的三個全連接層去除。由上圖可知,是分別從每一個池化層后,將池化后的特征圖提取出來,分別作為該分辨率的特征圖,這也就是設置五列(五種不同的分辨率特征圖)的原因。這里有四行,第一行是vgg-16的前13層,第二行是分別處理五種不同的分辨率特征圖的卷積層,目的是通過對來自池化層的特征圖學習,得到不同分辨率(多個尺度的)特征圖,第三行是為了捕獲前景和背景這種差異的信息,增加了局部對比特征。第四行,是為了匯總每一列的局部特征,但由於每一列的局部特征分辨率不同,所以增加了反卷積模塊,從分辨率小的局部特征(X5,X5c)從后往前傳遞,最后通過一個卷積層來得到最后的局部特征。在第一行的最后,通過vgg-16第五個池化層得到的特征圖,通過三個卷積層來得到最后的全局特征,最后,局部特征和全局特征分別經過一個卷積層后,再相加得到最終包含局部和全局的特征。這整個網絡結構就是一個特征提取器,提取了更為全面的局部和全局特征信息,然后,經過一個softmax函數得到預測的輸出。

具體的流程描述:

首先,我們來確定一下輸入和輸出分別是什么,在本論文中,無論是訓練還是測試,輸入:都是將原圖片空間大小(也就是分辨率h*w)直接通過cv2.resize(src,(352,352))這個方法強制轉換為352*352這個分辨率大小的圖片,當然通道數還是不變,然后通過reshape((1,img_size,img_size,3))函數將圖片的輸入格式變為(1,352,352,3),也就是常見的處理格式(batch_size,height,weight,channel)格式,這就是我們的輸入啦! 輸出:我們先了解真值圖的類型,真值圖也是經過和輸入一樣的方法將其變為176*176分辨率大小(通道數為1)的圖片,然后,通過np.stack(label,(1-label),axis=2)的函數和np.reshape(label,[-1,2])將其真值圖變為兩個維度的(176*176,2)大小的ndarray類型數組,第一個維度是原本label值,第二個維度是其1-label值。要處理圖片輸入到該網絡結構后的特征(self.Score)維度信息是(1,176,176,2),經過reshape(self.Score[-1,2])變成(176*176,2)大小的類型。好了,在搞清楚了輸入和輸出是什么的情況下,那么我們帶着一張大小為357*357空間分辨率大小的三通道彩色圖片來依次去探尋這個網絡架構的每一個模塊是對它進行了那些操作吧!在開始探尋之前,我們來直觀的感受一下圖片的樣貌吧

    

 第一個圖片是最原始的輸入要處理的圖片(400*300),第二個是對應原始圖片的真值圖(400*300),第三個是將空間分辨率強制改為352*352的圖片,第四個是將第三個圖片的三個通道(BGR,因為cv2.imread讀入的是這三個通道)分別減去VGG16_MEAN=[103.939, 116.779, 123.68]這幾個平均值得到的圖片,這個才是真正的輸入到網絡結構的圖片,第五個是對應網絡輸出的空間分辨率為176*176的真值圖。好了,有了直觀的認識后,我們來開始帶着圖四去探尋吧!

 

探尋:

首先明確的一點是,輸入的數據格式為[batch_size,height,weight,channel],在本論文中,batch_size設置為1,所以,輸入為數據格式為[1,352,352,3],數據類型為numpy.ndarray的float64。

好了,啰嗦了大半天,開始吧!

我們以input作為輸入到網絡結構的圖片,先看一下經過vgg16的8個卷積層和5個池化層的變化(如上圖4*5結構的第一行):

vgg16的第一個模塊2個卷積核大小為3*3通道數為64的步長為1的padding為SAME的卷積層(conv1-1,conv1-2)和一個2*2步長為2的,padding為SAME最大池化層(pool1),input經過conv1-1,變為了[1,352,352,64],經過conv1-2,變為[1,352,352,64],再經過pool1,變為了[1,176,176,64] (記為input_pool1)的特征圖,將這個input_pool1的特征圖拿出來放在第一列中處理,接下來討論;

然后繼續經過vgg16的第二個模塊2個卷積核大小為3*3通道數為128的步長為1,padding為SAME的卷積層(conv2-1,conv2-2)和一個2*2步長為2的,padding為SAME的最大池化層(pool2),input_pool1經過conv2-1,變為[1,176,176,128],經過conv2-2,變為了[1,176,176,128],經過pool2,變為了[1,88,88,128](記為input_pool2)的特征圖,將這個input_pool2的特征圖拿出來放在第二列中處理,稍后討論;

然后繼續經過vgg16的第三個模塊3個卷積核大小為3*3通道數為256的步長為1的padding為SAME的卷積層(conv3-1,conv3-2,conv3-3)和一個2*2步長為2的padding為SAME的最大池化層(pool3),input_pool2經過conv3_1,變為了[1,88,88,256],經過conv3_2,變為了[1,88,88,256],經過conv3_3,變為了[1,88,88,256],經過pool3,變為了[1,44,44,256](記為input_pool3)的特征圖,將這個input_pool3的特征圖拿出來放在第三列中處理,一會討論;

然后繼續經過vgg16的第四個模塊3個卷積核大小為3*3通道數為512步長為1的padding為SAME卷積層(conv4-1,conv4-2,conv4-3)和一個2*2步長為2的padding為SAME的最大池化層(pool4),input_pool3經過conv4-1,變為[1,44,44,512],經過conv4-2,變為[1,44,44,512],經過conv4-3,變為[1,44,44,512],經過pool4變為[1,22,22,512](記為input_pool4)的特征圖,將這個input_pool4的特征圖拿出來放在第四列,下面會討論;

然后繼續經過vgg16第五個模塊3個卷積核大小為3*3通道數為512步長為1的padding為SAME卷積層(conv5-1,conv5-2,conv5-3)和一個2*2步長為2的padding為SAME的最大池化層(pool5),input_pool5經過conv5-1,變為[1,22,22,512],經過conv5-2,變為[1,22,22,512],經過conv5-3,變為[1,22,22,512],經過pool5變為[1,11,11,512](記為input_pool5)的特征圖,將這個input_pool5的特征圖拿出來放在第五列,稍后討論;

此時,vgg16完成了它的使命,接下來的工作都是本論文作者構建的層啦。

從pool5出來的input_pool5,經過三個卷積層(global_conv-1,global_conv-2,global_conv-3),global_conv-1,卷積核大小為5*5,通道數為128,步長為1,padding為VALID;global_conv-2,卷積核大小為5*5,通道數為128,步長為1,padding為VALID;global_conv-3,卷積核大小為3*3,通道數為128,步長為1,padding為VALID;input_pool5經過global_conv-1,變為[1,7,7,128],經過global_conv-2變為[1,3,3,128],經過global_conv-3變為[1,1,1,128],最后在經過一個分數模塊(SCORE)的一個score_conv-G卷積核,這個卷積核大小為1*1,通道數為2,步長為1,padding為VALID,變為[1,1,1,2](記為input_gloal)。這個input_global就是我們的全局特征。

好了,現在開始討論第二行:

第二行中的每一個模塊都只有一個卷積層,並且這一行的每個模塊卷積層的設置是一樣的,即卷積核大小為3*3,通道數128,步長為1,padding為SAME(記為conv-6,conv-7,conv-8,conv-9,conv-10),input_pool1經過conv-6變為[1,176,176,128](記為X1);input_pool2經過conv-7,變為了[1,88,88,128](記為X2);input_pool3經過conv-8,變為了[1,44,44,128](記為X3);input_pool4經過conv-9,變為了[1,22,22,128](記為X4);input_pool5經過conv-10,變為了[1,11,11,128](記為X5)。

好了,現在開始討論第三行:

這一行的模塊,沒有參數;它的存在是計算在每一個特定的分辨率的局部特征的前景和背景對比的信息,計算方式為: Xi - AvgPool(Xi),Xi代表第二行的每一列的輸出,AvgPool(Xi)表示對Xi進行平均池化,大小為3*3,步長為1,padding為VALID,因為我們要保持AvgPool(Xi)的大小類型和Xi一致,所以,要對送入AvgPool的Xi做一個邊界補值,作者在代碼中,用的是tf.pad方法,模式為symmetric,將邊界補了一圈,如對X1而言,補值后,大小變為[1,178,178,128];所以,Xi - AvgPool(Xi)的大小仍然和Xi大小一樣,在這里記為(X1c,X2c,X3c,X4c,X5c)

到了最后一行啦:

這一行的模塊是卷積層和反卷積層,在這里的反卷積層是將小的空間分辨率特征圖,變為大一倍的大點的空間分辨率特征圖;對與卷積而言,我們只要知道卷積的對象是什么,卷積核大小,通道數,步長和pading類型即可推出卷積后的輸出,而反卷積要比卷積層需要多一個信息,就是我們的輸出數據格式是什么樣的,原因請百度查看。

在這一行的反卷積模塊設置是一樣的,即卷積核大小為5*5,步長為2,padding為SAME,通道數每一個反卷積模塊不同;從第四行,第五列到第二列以此記為(unpool-5,unpool-4,unpool-3,unpool-2)。

對於unpool-5這個模塊,輸入為X5,X5c,X5和X5c是級聯在一起,X5在上面,X5c在下面,那么輸入就變為了[1,11,11,128*2],設置輸出的通道數與X5保持相同,即[1,22,22,128](記為U5);

對於unpool-4這個模塊,輸入為X4,X4c和U5(同樣是級聯在一起,上到下的順序為X4,X4c,U5),那么輸入就變為[1,22,22,128*3],設置輸出的通道數為與X4和U5的通道數和保持一致,即[1,44,44,128*2](記為U4);

對於unpool-3這個模塊,輸入為X3,X3c和U4(同樣是級聯在一起,上到下的順序為X3,X3c,U4),那么輸入就變為[1,44,44,128*4],設置輸出的通道數為與X3和U4的通道數和保持一致,即[1,88,88,128*3](記為U3);

對於unpool-2這個模塊,輸入為X2,X2c和U3(同樣是級聯在一起,上到下的順序為X2,X2c,U3),那么輸入就變為[1,88,88,128*5],設置輸出的通道數為與X2和U3的通道數和保持一致,即[1,176,176,128*4](記為U2);

好了,到這里反卷積模塊就進行完了,

還剩第一行,第四列的local_conv卷積層,該卷積層的卷積核大小為1*1,通道數為128*5(X1+U2的通道數和)步長為1,padding為VALID,對於這個模塊,輸入是X1,X1c,U2(同樣是級聯在一起,上到下的順序為X1,X1c,U2),那么輸入就變成了[1,176,176,128*6],經過local_conv,變為[1,176,176,128*5](記為XL),到這里第四行就結束啦。

最后局部特征XL還要經過一個Score模塊的conv-L卷積層,該卷積層大小為1*1,通道數為2,步長為1,padding為VALID,XL經過conv-L,變為[1,176,176,2](記為input_local),這個input_local就是我們的總局部特征啦!

到現在,我們得到了全局特征input_global,[1,1,1,2],和局部特征input_local,[1,176,176,2];此時,我們融合全局特征和局部特征,作者很簡單的將兩者加在了一起,有同學提問啦,這怎么加呀?特征的元素個數都不一致;作者代碼:self.Score = self.Local_Score + self.Global_Score ,這里input_global是self.Global_Score,input_local是self.Local_Score

答:利用了python的廣播機制。KO,那self.Local_Score的大小:[1,176,176,2]

最后,將self.Score做一個reshap, self.Score = tf.reshape(self.Score, [-1,2]),此時self.Score為[176*176,2],經過一個softmax(self.Score)輸出self.Prob(記為prediction_out)

這里的prediction_out就是我們的這個網絡的最后的輸出啦!到此將整個網絡結構詳細的分析了一下,接下來就是loss損失函數,真的是廢話一大篇,不知道您有看厭煩沒?哈哈哈,皮一下

 

LOSS 損失函數:(我們結合一下源碼來具體分析啦)

本論文中,loss損失函數由兩項組成,第一項為交叉熵損失函數,第二項是邊界重疊損失函數IoU Boundary Loss(借鑒醫療圖像處理中常用的評價指標):

 

交叉熵損失函數,這個調用一個tensorflow的函數即可,

tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=self.Score,labels=self.label_holder))了;self.Score,我們剛才分析過了,是網絡的輸出大小為[176*176,2],self.label_holder,是真值圖,大小也為[176*176,2]

tf.reduce:是求和然后平均

 

IoU Boundary Loss,就沒有那么簡單啦:

我們處理原始圖片如上圖,我們先對輸入的數據分析一下,通過網絡的輸出,我們得到了prediction_out,[176*176,2],這個里的值是接近0,或者接近1的:如下的形式

array([[2.9550231e-04, 9.9970442e-01],
[3.1208037e-04, 9.9968791e-01],
[2.9506793e-04, 9.9970490e-01],
...,
[7.4689655e-05, 9.9992526e-01],
[1.0897202e-04, 9.9989104e-01],
[2.2902120e-04, 9.9977094e-01]], dtype=float32)

我將這個prediction_out,可視化了一下,使用下面幾個函數

result = np.reshape(result,(l176,176,2)) : [1,176,176,2]

result = result[:,:,0] # [1,176,176]

result = np.squeeze(result) # 除去維度為1的那個維度 # [176,176]

cv2.imwrite('it_C.png',result)

 

通過self.Prob_C = tf.reshape(self.Prob, [1, 176, 176, 2]),這里的self.Prob_C,是我們的要處理的預測的:

我通過,cv2.imwrite('ccc1.png',(image_C[:,:,0]*255).astype(np.uint8));cv2.imwrite('ccc2.png',(image_C[:,:,1]*255).astype(np.uint8));這里的image_C就是self.Prob_C,寫成圖片,更為直觀一點

      

我們對真值圖,也做了形狀的轉換 

 self.label_C = tf.reshape(self.label_holder, [1, 176, 176, 2])

 

好了,到此,我們知道了,我們要處理的數據是什么樣的啦,預測的:self.Prob_C, 真值的:self.label_C ,大小都是[1,176,176,2]

 

下面我們開始了解怎樣對self.Prob_C和self.label_C進行邊界檢測的:

 

首先,我們需要簡單的了解一下Sobel算子,它是用來檢測圖像邊緣的,可以百度了解。

在水平方向上,Sobel算子的模板為:fx= np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]).astype(np.float32)

在垂直方向上,Sobel算子的模板為: fy = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]]).astype(np.float32)

因為我們要處理的self.Prob_C和self.label_C是兩個圖,所以:我們將 fx,和 fy分別疊加:

fx = np.stack((fx, fx), axis=2)
fy = np.stack((fy, fy), axis=2)

並轉換一下形狀:

fx = np.reshape(fx, (3, 3, 2, 1))
fy = np.reshape(fy, (3, 3, 2, 1))

此時,我們需要將fx,fy與self.Prob_C和self.label_C進行卷積操作,然而tenforflow很擅長這件事情(卷積操作):

所以,將fx,fy轉為tf.Variable類型:

tf_fx = tf.Variable(tf.constant(fx))
tf_fy = tf.Variable(tf.constant(fy))

我們使用tensorflow提供的tf.nn.depthwise_conv2d這個方法進行卷積,在對self.Prob_C卷積之前,要進行一個tf.pad操作,擴充邊界值,從[1,176,176,2] -> [1,178,178,2],以免卷積后大小改變(卷積核為tf_fx,tf_fy3*3,步長為1,multi-channel為2,padding:VALID)

通過tf.nn.depthwise_conv2d這個方法卷積后,下面這個方法

def im_gradient(self, im):
  gx = tf.nn.depthwise_conv2d(tf.pad(im, [[0, 0], [1, 1], [1, 1], [0, 0]], 'SYMMETRIC'),self.sobel_fx, [1, 1, 1, 1], padding='VALID')
  gy = tf.nn.depthwise_conv2d(tf.pad(im, [[0, 0], [1, 1], [1, 1], [0, 0]], 'SYMMETRIC'),self.sobel_fy, [1, 1, 1, 1], padding='VALID')
  return tf.sqrt(tf.add(tf.square(gx), tf.square(gy)))

返回的數據格式為: [1,176,176,4]: 4 = 2*2 (第一個2是self.Prob_C的in_channel:2,第二個2是multi-channel,通過tf.nn.depthwise_conv2d方法,返回通道數為:in_channel*multi_channel)

通過對指定的維度reduction_indices=3,來求和,注意:keep_dims=True

self.Prob_Grad = tf.tanh(tf.reduce_sum(self.im_gradient(self.Prob_C), reduction_indices=3, keep_dims=True)) : [1,176,176,1],值在[0,1]之間

對於真值圖self.label_C:

self.contour_th = 1.5 閾值,大於1.5取True,否則取False,然后再轉化為1,或0

self.label_Grad = tf.cast(tf.greater(tf.reduce_sum(self.im_gradient(self.label_C),reduction_indices=3, keep_dims=True),self.contour_th), tf.float32) : [1,176,176,1] 值是1或者是0,我們來直觀的感受一下哈

   

我們得到了self.Prob_Grad的邊界圖和self.label_Grad邊界圖,接下來算兩個邊界圖的IOU,我們希望IOU越高越好,所以要最小化 這個項 1- IoU

inter: 表示IoU Loss 的分子,union 表示分母;注意到tf.square(pred),對pred求其平方,對gt平方,我能理解,因為gt值1或者0,平方后,值是不變的,那對pred平方呢?

我們來思考一下,我們要的是 pred邊界 和 gt邊界的重合情況,|Cj|:指的是gt的邊界像素點的強度之和,|Cj_hat|:指的是gt的邊界像素點的強度之和,

那么,如果沒有平方,pred里的像素值不僅邊界對其|Cj_hat|有影響,非邊界的像素點值對其也有影響,所以,引入平方后,降低非邊界像素點(假如,0.001平方后,接近0)對其的影響。

self.C_IoU_LOSS = self.Loss_IoU(self.Prob_Grad, self.label_Grad)

def Loss_IoU(self, pred, gt):
  inter = tf.reduce_sum(tf.multiply(pred, gt))
  union = tf.add(tf.reduce_sum(tf.square(pred)), tf.reduce_sum(tf.square(gt)))

  if inter == 0:
    return 0
  else:
    return 1 - (2*(inter+1)/(union + 1))

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM