轉載自知乎目標檢測|YOLO原理與實現

目標檢測|YOLO原理與實現

最新的YOLOv2和YOLOv3:

小白將:目標檢測|YOLOv2原理與實現(附YOLOv3)zhuanlan.zhihu.com圖示

前言

當我們談起計算機視覺時,首先想到的就是影象分類,沒錯,影象分類是計算機視覺最基本的任務之一,但是在影象分類的基礎上,還有更復雜和有意思的任務,如目標檢測,物體定位,影象分割等,見圖1所示。其中目標檢測是一件比較實際的且具有挑戰性的計算機視覺任務,其可以看成影象分類與定位的結合,給定一張圖片,目標檢測系統要能夠識別出圖片的目標並給出其位置,由於圖片中目標數是不定的,且要給出目標的精確位置,目標檢測相比分類任務更復雜。目標檢測的一個實際應用場景就是無人駕駛,如果能夠在無人車上裝載一個有效的目標檢測系統,那麼無人車將和人一樣有了眼睛,可以快速地檢測出前面的行人與車輛,從而作出實時決策。

圖1 計算機視覺任務(來源: cs231n)

近幾年來,目標檢測演算法取得了很大的突破。比較流行的演算法可以分為兩類,一類是基於Region Proposal的R-CNN系演算法(R-CNN,Fast R-CNN, Faster R-CNN),它們是two-stage的,需要先使用啟發式方法(selective search)或者CNN網路(RPN)產生Region Proposal,然後再在Region Proposal上做分類與迴歸。而另一類是Yolo,SSD這類one-stage演算法,其僅僅使用一個CNN網路直接預測不同目標的類別與位置。第一類方法是準確度高一些,但是速度慢,但是第二類演算法是速度快,但是準確性要低一些。這可以在圖2中看到。本文介紹的是Yolo演算法,其全稱是You Only Look Once: Unified, Real-Time Object Detection,其實個人覺得這個題目取得非常好,基本上把Yolo演算法的特點概括全了:You Only Look Once說的是隻需要一次CNN運算,Unified指的是這是一個統一的框架,提供end-to-end的預測,而Real-Time體現是Yolo演算法速度快。這裡我們談的是Yolo-v1版本演算法,其效能是差於後來的SSD演算法的,但是Yolo後來也繼續進行改進,產生了Yolo9000演算法。本文主要講述Yolo-v1演算法的原理,特別是演算法的訓練與預測中詳細細節,最後將給出如何使用TensorFlow實現Yolo演算法。

圖2 目標檢測演算法進展與對比

滑動視窗與CNN

在介紹Yolo演算法之前,首先先介紹一下滑動視窗技術,這對我們理解Yolo演算法是有幫助的。採用滑動視窗的目標檢測演算法思路非常簡單,它將檢測問題轉化為了影象分類問題。其基本原理就是採用不同大小和比例(寬高比)的視窗在整張圖片上以一定的步長進行滑動,然後對這些視窗對應的區域做影象分類,這樣就可以實現對整張圖片的檢測了,如下圖3所示,如DPM就是採用這種思路。但是這個方法有致命的缺點,就是你並不知道要檢測的目標大小是什麼規模,所以你要設定不同大小和比例的視窗去滑動,而且還要選取合適的步長。但是這樣會產生很多的子區域,並且都要經過分類器去做預測,這需要很大的計算量,所以你的分類器不能太複雜,因為要保證速度。解決思路之一就是減少要分類的子區域,這就是R-CNN的一個改進策略,其採用了selective search方法來找到最有可能包含目標的子區域(Region Proposal),其實可以看成採用啟發式方法過濾掉很多子區域,這會提升效率。

圖3 採用滑動視窗進行目標檢測(來源:deeplearning.ai)

如果你使用的是CNN分類器,那麼滑動視窗是非常耗時的。但是結合卷積運算的特點,我們可以使用CNN實現更高效的滑動視窗方法。這裡要介紹的是一種全卷積的方法,簡單來說就是網路中用卷積層代替了全連線層,如圖4所示。輸入圖片大小是16x16,經過一系列卷積操作,提取了2x2的特徵圖,但是這個2x2的圖上每個元素都是和原圖是一一對應的,如圖上藍色的格子對應藍色的區域,這不就是相當於在原圖上做大小為14x14的視窗滑動,且步長為2,共產生4個字區域。最終輸出的通道數為4,可以看成4個類別的預測概率值,這樣一次CNN計算就可以實現視窗滑動的所有子區域的分類預測。這其實是overfeat演算法的思路。之所可以CNN可以實現這樣的效果是因為卷積操作的特性,就是圖片的空間位置資訊的不變性,儘管卷積過程中圖片大小減少,但是位置對應關係還是儲存的。說點題外話,這個思路也被R-CNN借鑑,從而誕生了Fast R-cNN演算法。

圖4 滑動視窗的CNN實現(來源:deeplearning.ai)

上面儘管可以減少滑動視窗的計算量,但是隻是針對一個固定大小與步長的視窗,這是遠遠不夠的。Yolo演算法很好的解決了這個問題,它不再是視窗滑動了,而是直接將原始圖片分割成互不重合的小方塊,然後通過卷積最後生產這樣大小的特徵圖,基於上面的分析,可以認為特徵圖的每個元素也是對應原始圖片的一個小方塊,然後用每個元素來可以預測那些中心點在該小方格內的目標,這就是Yolo演算法的樸素思想。下面將詳細介紹Yolo演算法的設計理念。

設計理念

整體來看,Yolo演算法採用一個單獨的CNN模型實現end-to-end的目標檢測,整個系統如圖5所示:首先將輸入圖片resize到448x448,然後送入CNN網路,最後處理網路預測結果得到檢測的目標。相比R-CNN演算法,其是一個統一的框架,其速度更快,而且Yolo的訓練過程也是end-to-end的。

圖5 Yolo檢測系統

具體來說,Yolo的CNN網路將輸入的圖片分割成 S\times S 網格,然後每個單元格負責去檢測那些中心點落在該格子內的目標,如圖6所示,可以看到狗這個目標的中心落在左下角一個單元格內,那麼該單元格負責預測這個狗。每個單元格會預測 B 個邊界框(bounding box)以及邊界框的置信度(confidence score)。所謂置信度其實包含兩個方面,一是這個邊界框含有目標的可能性大小,二是這個邊界框的準確度。前者記為 Pr(object) ,當該邊界框是背景時(即不包含目標),此時 Pr(object)=0 。而當該邊界框包含目標時, Pr(object)=1 。邊界框的準確度可以用預測框與實際框(ground truth)的IOU(intersection over union,交併比)來表徵,記為 \text{IOU}^{truth}_{pred} 。因此置信度可以定義為 Pr(object)*\text{IOU}^{truth}_{pred} 。很多人可能將Yolo的置信度看成邊界框是否含有目標的概率,但是其實它是兩個因子的乘積,預測框的準確度也反映在裡面。邊界框的大小與位置可以用4個值來表徵: (x, y,w,h) ,其中 (x,y) 是邊界框的中心座標,而 wh 是邊界框的寬與高。還有一點要注意,中心座標的預測值 (x,y) 是相對於每個單元格左上角座標點的偏移值,並且單位是相對於單元格大小的,單元格的座標定義如圖6所示。而邊界框的 wh 預測值是相對於整個圖片的寬與高的比例,這樣理論上4個元素的大小應該在 [0,1] 範圍。這樣,每個邊界框的預測值實際上包含5個元素: (x,y,w,h,c) ,其中前4個表徵邊界框的大小與位置,而最後一個值是置信度。

圖6 網格劃分

還有分類問題,對於每一個單元格其還要給出預測出 C 個類別概率值,其表徵的是由該單元格負責預測的邊界框其目標屬於各個類別的概率。但是這些概率值其實是在各個邊界框置信度下的條件概率,即 Pr(class_{i}|object) 。值得注意的是,不管一個單元格預測多少個邊界框,其只預測一組類別概率值,這是Yolo演算法的一個缺點,在後來的改進版本中,Yolo9000是把類別概率預測值與邊界框是繫結在一起的。同時,我們可以計算出各個邊界框類別置信度(class-specific confidence scores): Pr(class_{i}|object)*Pr(object)*\text{IOU}^{truth}_{pred}=Pr(class_{i})*\text{IOU}^{truth}_{pred}

邊界框類別置信度表徵的是該邊界框中目標屬於各個類別的可能性大小以及邊界框匹配目標的好壞。後面會說,一般會根據類別置信度來過濾網路的預測框。

總結一下,每個單元格需要預測 (B*5+C) 個值。如果將輸入圖片劃分為 S\times S 網格,那麼最終預測值為 S\times S\times (B*5+C) 大小的張量。整個模型的預測值結構如下圖所示。對於PASCAL VOC資料,其共有20個類別,如果使用 S=7,B=2 ,那麼最終的預測結果就是 7\times 7\times 30 大小的張量。在下面的網路結構中我們會詳細講述每個單元格的預測值的分佈位置。

圖7 模型預測值結構

網路設計

Yolo採用卷積網路來提取特徵,然後使用全連線層來得到預測值。網路結構參考GooLeNet模型,包含24個卷積層和2個全連線層,如圖8所示。對於卷積層,主要使用1x1卷積來做channle reduction,然後緊跟3x3卷積。對於卷積層和全連線層,採用Leaky ReLU啟用函式: max(x, 0.1x) 。但是最後一層卻採用線性啟用函式。

圖8 網路結構

可以看到網路的最後輸出為 7\times 7\times 30 大小的張量。這和前面的討論是一致的。這個張量所代表的具體含義如圖9所示。對於每一個單元格,前20個元素是類別概率值,然後2個元素是邊界框置信度,兩者相乘可以得到類別置信度,最後8個元素是邊界框的 (x, y,w,h) 。大家可能會感到奇怪,對於邊界框為什麼把置信度 c(x, y,w,h) 都分開排列,而不是按照 (x, y,w,h,c) 這樣排列,其實純粹是為了計算方便,因為實際上這30個元素都是對應一個單元格,其排列是可以任意的。但是分離排布,可以方便地提取每一個部分。這裡來解釋一下,首先網路的預測值是一個二維張量 P ,其shape為 [batch, 7\times 7\times 30] 。採用切片,那麼 P_{[:,0:7*7*20]} 就是類別概率部分,而 P_{[:,7*7*20:7*7*(20+2)]} 是置信度部分,最後剩餘部分 P_{[:,7*7*(20+2):]} 是邊界框的預測結果。這樣,提取每個部分是非常方便的,這會方面後面的訓練及預測時的計算。

圖9 預測張量的解析

網路訓練

在訓練之前,先在ImageNet上進行了預訓練,其預訓練的分類模型採用圖8中前20個卷積層,然後新增一個average-pool層和全連線層。預訓練之後,在預訓練得到的20層卷積層之上加上隨機初始化的4個卷積層和2個全連線層。由於檢測任務一般需要更高清的圖片,所以將網路的輸入從224x224增加到了448x448。整個網路的流程如下圖所示:

圖10 Yolo網路流程

下面是訓練損失函式的分析,Yolo演算法將目標檢測看成迴歸問題,所以採用的是均方差損失函式。但是對不同的部分採用了不同的權重值。首先區分定位誤差和分類誤差。對於定位誤差,即邊界框座標預測誤差,採用較大的權重 \lambda _{coord}=5 。然後其區分不包含目標的邊界框與含有目標的邊界框的置信度,對於前者,採用較小的權重值 \lambda _{noobj}=0.5 。其它權重值均設為1。然後採用均方誤差,其同等對待大小不同的邊界框,但是實際上較小的邊界框的座標誤差應該要比較大的邊界框要更敏感。為了保證這一點,將網路的邊界框的寬與高預測改為對其平方根的預測,即預測值變為 (x,y,\sqrt{w}, \sqrt{h})

另外一點時,由於每個單元格預測多個邊界框。但是其對應類別只有一個。那麼在訓練時,如果該單元格內確實存在目標,那麼只選擇與ground truth的IOU最大的那個邊界框來負責預測該目標,而其它邊界框認為不存在目標。這樣設定的一個結果將會使一個單元格對應的邊界框更加專業化,其可以分別適用不同大小,不同高寬比的目標,從而提升模型效能。大家可能會想如果一個單元格記憶體在多個目標怎麼辦,其實這時候Yolo演算法就只能選擇其中一個來訓練,這也是Yolo演算法的缺點之一。要注意的一點時,對於不存在對應目標的邊界框,其誤差項就是隻有置信度,座標項誤差是沒法計算的。而只有當一個單元格內確實存在目標時,才計算分類誤差項,否則該項也是無法計算的。

綜上討論,最終的損失函式計算如下:

其中第一項是邊界框中心座標的誤差項, 1^{obj}_{ij} 指的是第 i 個單元格存在目標,且該單元格中的第 j 個邊界框負責預測該目標。第二項是邊界框的高與寬的誤差項。第三項是包含目標的邊界框的置信度誤差項。第四項是不包含目標的邊界框的置信度誤差項。而最後一項是包含目標的單元格的分類誤差項, 1^{obj}_{i} 指的是第 i 個單元格存在目標。這裡特別說一下置信度的target值 C_i ,如果是不存在目標,此時由於 Pr(object)=0,那麼 C_i=0 。如果存在目標, Pr(object)=1 ,此時需要確定 \text{IOU}^{truth}_{pred} ,當然你希望最好的話,可以將IOU取1,這樣 C_i=1 ,但是在YOLO實現中,使用了一個控制引數rescore(預設為1),當其為1時,IOU不是設定為1,而就是計算truth和pred之間的真實IOU。不過很多復現YOLO的專案還是取 C_i=1 ,這個差異應該不會太影響結果吧。

網路預測

在說明Yolo演算法的預測過程之前,這裡先介紹一下非極大值抑制演算法(non maximum suppression, NMS),這個演算法不單單是針對Yolo演算法的,而是所有的檢測演算法中都會用到。NMS演算法主要解決的是一個目標被多次檢測的問題,如圖11中人臉檢測,可以看到人臉被多次檢測,但是其實我們希望最後僅僅輸出其中一個最好的預測框,比如對於美女,只想要紅色那個檢測結果。那麼可以採用NMS演算法來實現這樣的效果:首先從所有的檢測框中找到置信度最大的那個框,然後挨個計算其與剩餘框的IOU,如果其值大於一定閾值(重合度過高),那麼就將該框剔除;然後對剩餘的檢測框重複上述過程,直到處理完所有的檢測框。Yolo預測過程也需要用到NMS演算法。

圖11 NMS應用在人臉檢測

下面就來分析Yolo的預測過程,這裡我們不考慮batch,認為只是預測一張輸入圖片。根據前面的分析,最終的網路輸出是 7\times 7 \times 30 ,但是我們可以將其分割成三個部分:類別概率部分為 [7, 7, 20] ,置信度部分為 [7,7,2] ,而邊界框部分為 [7,7,2,4] (對於這部分不要忘記根據原始圖片計算出其真實值)。然後將前兩項相乘(矩陣 [7, 7, 20] 乘以 [7,7,2] 可以各補一個維度來完成 [7,7,1,20]\times [7,7,2,1] )可以得到類別置信度值為 [7, 7,2,20] ,這裡總共預測了 7*7*2=98 個邊界框。

所有的準備資料已經得到了,那麼我們先說第一種策略來得到檢測框的結果,我認為這是最正常與自然的處理。首先,對於每個預測框根據類別置信度選取置信度最大的那個類別作為其預測標籤,經過這層處理我們得到各個預測框的預測類別及對應的置信度值,其大小都是 [7,7,2] 。一般情況下,會設定置信度閾值,就是將置信度小於該閾值的box過濾掉,所以經過這層處理,剩餘的是置信度比較高的預測框。最後再對這些預測框使用NMS演算法,最後留下來的就是檢測結果。一個值得注意的點是NMS是對所有預測框一視同仁,還是區分每個類別,分別使用NMS。Ng在deeplearning.ai中講應該區分每個類別分別使用NMS,但是看了很多實現,其實還是同等對待所有的框,我覺得可能是不同類別的目標出現在相同位置這種概率很低吧。

上面的預測方法應該非常簡單明瞭,但是對於Yolo演算法,其卻採用了另外一個不同的處理思路(至少從C原始碼看是這樣的),其區別就是先使用NMS,然後再確定各個box的類別。其基本過程如圖12所示。對於98個boxes,首先將小於置信度閾值的值歸0,然後分類別地對置信度值採用NMS,這裡NMS處理結果不是剔除,而是將其置信度值歸為0。最後才是確定各個box的類別,當其置信度值不為0時才做出檢測結果輸出。這個策略不是很直接,但是貌似Yolo原始碼就是這樣做的。Yolo論文裡面說NMS演算法對Yolo的效能是影響很大的,所以可能這種策略對Yolo更好。但是我測試了普通的圖片檢測,兩種策略結果是一樣的。

圖12 Yolo的預測處理流程

演算法效能分析

這裡看一下Yolo演算法在PASCAL VOC 2007資料集上的效能,這裡Yolo與其它檢測演算法做了對比,包括DPM,R-CNN,Fast R-CNN以及Faster R-CNN。其對比結果如表1所示。與實時性檢測方法DPM對比,可以看到Yolo演算法可以在較高的mAP上達到較快的檢測速度,其中Fast Yolo演算法比快速DPM還快,而且mAP是遠高於DPM。但是相比Faster R-CNN,Yolo的mAP稍低,但是速度更快。所以。Yolo演算法算是在速度與準確度上做了折中。

表1 Yolo在PASCAL VOC 2007上與其他演算法的對比

為了進一步分析Yolo演算法,文章還做了誤差分析,將預測結果按照分類與定位準確性分成以下5類:

  • Correct:類別正確,IOU>0.5;(準確度)
  • Localization:類別正確,0.1 < IOU<0.5(定位不準);
  • Similar:類別相似,IOU>0.1;
  • Other:類別錯誤,IOU>0.1;
  • Background:對任何目標其IOU<0.1。(誤把背景當物體)

Yolo與Fast R-CNN的誤差對比分析如下圖所示:

圖13 Yolo與Fast R-CNN的誤差對比分析

可以看到,Yolo的Correct的是低於Fast R-CNN。另外Yolo的Localization誤差偏高,即定位不是很準確。但是Yolo的Background誤差很低,說明其對背景的誤判率較低。Yolo的那篇文章中還有更多效能對比,感興趣可以看看。

現在來總結一下Yolo的優缺點。首先是優點,Yolo採用一個CNN網路來實現檢測,是單管道策略,其訓練與預測都是end-to-end,所以Yolo演算法比較簡潔且速度快。第二點由於Yolo是對整張圖片做卷積,所以其在檢測目標有更大的視野,它不容易對背景誤判。其實我覺得全連線層也是對這個有貢獻的,因為全連線起到了attention的作用。另外,Yolo的泛化能力強,在做遷移時,模型魯棒性高。

最後不得不談一下Yolo的缺點,首先Yolo各個單元格僅僅預測兩個邊界框,而且屬於一個類別。對於小物體,Yolo的表現會不如人意。這方面的改進可以看SSD,其採用多尺度單元格。也可以看Faster R-CNN,其採用了anchor boxes。Yolo對於在物體的寬高比方面泛化率低,就是無法定位不尋常比例的物體。當然Yolo的定位不準確也是很大的問題。

演算法的TF實現

Yolo的原始碼是用C實現的,但是好在Github上有很多開源的TF復現。這裡我們參考gliese581gg的YOLO_tensorflow的實現來分析Yolo的Inference實現細節。我們的程式碼將構建一個end-to-end的Yolo的預測模型,利用的已經訓練好的權重檔案,你將可以用自然的圖片去測試檢測效果。

首先,我們定義Yolo的模型引數:

class Yolo(object):
    def __init__(self, weights_file, verbose=True):
        self.verbose = verbose
        # detection params
        self.S = 7  # cell size
        self.B = 2  # boxes_per_cell
        self.classes = ["aeroplane", "bicycle", "bird", "boat", "bottle",
                        "bus", "car", "cat", "chair", "cow", "diningtable",
                        "dog", "horse", "motorbike", "person", "pottedplant",
                        "sheep", "sofa", "train","tvmonitor"]
        self.C = len(self.classes) # number of classes
        # offset for box center (top left point of each cell)
        self.x_offset = np.transpose(np.reshape(np.array([np.arange(self.S)]*self.S*self.B),
                                              [self.B, self.S, self.S]), [1, 2, 0])
        self.y_offset = np.transpose(self.x_offset, [1, 0, 2])
    <span class="bp">self</span><span class="o">.</span><span class="n">threshold</span> <span class="o">=</span> <span class="mf">0.2</span>  <span class="c1"># confidence scores threhold</span>
    <span class="bp">self</span><span class="o">.</span><span class="n">iou_threshold</span> <span class="o">=</span> <span class="mf">0.4</span>
    <span class="c1">#  the maximum number of boxes to be selected by non max suppression</span>
    <span class="bp">self</span><span class="o">.</span><span class="n">max_output_size</span> <span class="o">=</span> <span class="mi">10</span>

    <span class="bp">self</span><span class="o">.</span><span class="n">sess</span> <span class="o">=</span> <span class="n">tf</span><span class="o">.</span><span class="n">Session</span><span class="p">()</span>
    <span class="bp">self</span><span class="o">.</span><span class="n">_build_net</span><span class="p">()</span>
    <span class="bp">self</span><span class="o">.</span><span class="n">_build_detector</span><span class="p">()</span>
    <span class="bp">self</span><span class="o">.</span><span class="n">_load_weights</span><span class="p">(</span><span class="n">weights_file</span><span class="p">)</span>

然後是我們模型的主體網路部分,這個網路將輸出[batch,7730]的張量:

def _build_net(self):
“”“build the network”""
if self.verbose:
print(“Start to build the network …”)
self.images = tf.placeholder(tf.float32, [None, 448, 448, 3])
net = self._conv_layer(self.images, 1, 64, 7, 2)
net = self._maxpool_layer(net, 1, 2, 2)
net = self._conv_layer(net, 2, 192, 3, 1)
net = self._maxpool_layer(net, 2, 2, 2)
net = self._conv_layer(net, 3, 128, 1, 1)
net = self._conv_layer(net, 4, 256, 3, 1)
net = self._conv_layer(net, 5, 256, 1, 1)
net = self._conv_layer(net, 6, 512, 3, 1)
net = self._maxpool_layer(net, 6, 2, 2)
net = self._conv_layer(net, 7, 256, 1, 1)
net = self._conv_layer(net, 8, 512, 3, 1)
net = self._conv_layer(net, 9, 256, 1, 1)
net = self._conv_layer(net, 10, 512, 3, 1)
net = self._conv_layer(net, 11, 256, 1, 1)
net = self._conv_layer(net, 12, 512, 3, 1)
net = self._conv_layer(net, 13, 256, 1, 1)
net = self._conv_layer(net, 14, 512, 3, 1)
net = self._conv_layer(net, 15, 512, 1, 1)
net = self._conv_layer(net, 16, 1024, 3, 1)
net = self._maxpool_layer(net, 16, 2, 2)
net = self._conv_layer(net, 17, 512, 1, 1)
net = self._conv_layer(net, 18, 1024, 3, 1)
net = self._conv_layer(net, 19, 512, 1, 1)
net = self._conv_layer(net, 20, 1024, 3, 1)
net = self._conv_layer(net, 21, 1024, 3, 1)
net = self._conv_layer(net, 22, 1024, 3, 2)
net = self._conv_layer(net, 23, 1024, 3, 1)
net = self._conv_layer(net, 24, 1024, 3, 1)
net = self._flatten(net)
net = self._fc_layer(net, 25, 512, activation=leak_relu)
net = self._fc_layer(net, 26, 4096, activation=leak_relu)
net = self._fc_layer(net, 27, self.Sself.S(self.C+5self.B))
self.predicts = net

接下來,我們要去解析網路的預測結果,這裡採用了第一種預測策略,即判斷預測框類別,再NMS,多虧了TF提供了NMS的函式tf.image.non_max_suppression,其實實現起來很簡單,所有的細節前面已經交代了:

def _build_detector(self):
“”“Interpret the net output and get the predicted boxes”""
# the width and height of orignal image
self.width = tf.placeholder(tf.float32, name=“img_w”)
self.height = tf.placeholder(tf.float32, name=“img_h”)
# get class prob, confidence, boxes from net output
idx1 = self.S self.S self.C
idx2 = idx1 + self.S self.S * self.B
# class prediction
class_probs = tf.reshape(self.predicts[0, :idx1], [self.S, self.S, self.C])
# confidence
confs = tf.reshape(self.predicts[0, idx1:idx2], [self.S, self.S, self.B])
# boxes -> (x, y, w, h)
boxes = tf.reshape(self.predicts[0, idx2:], [self.S, self.S, self.B, 4])
    <span class="c1"># convert the x, y to the coordinates relative to the top left point of the image</span>
    <span class="c1"># the predictions of w, h are the square root</span>
    <span class="c1"># multiply the width and height of image</span>
    <span class="n">boxes</span> <span class="o">=</span> <span class="n">tf</span><span class="o">.</span><span class="n">stack</span><span class="p">([(</span><span class="n">boxes</span><span class="p">[:,</span> <span class="p">:,</span> <span class="p">:,</span> <span class="mi">0</span><span class="p">]</span> <span class="o">+</span> <span class="n">tf</span><span class="o">.</span><span class="n">constant</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">x_offset</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">tf</span><span class="o">.</span><span class="n">float32</span><span class="p">))</span> <span class="o">/</span> <span class="bp">self</span><span class="o">.</span><span class="n">S</span> <span class="o">*</span> <span class="bp">self</span><span class="o">.</span><span class="n">width</span><span class="p">,</span>
                      <span class="p">(</span><span class="n">boxes</span><span class="p">[:,</span> <span class="p">:,</span> <span class="p">:,</span> <span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="n">tf</span><span class="o">.</span><span class="n">constant</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">y_offset</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">tf</span><span class="o">.</span><span class="n">float32</span><span class="p">))</span> <span class="o">/</span> <span class="bp">self</span><span class="o">.</span><span class="n">S</span> <span class="o">*</span> <span class="bp">self</span><span class="o">.</span><span class="n">height</span><span class="p">,</span>
                      <span class="n">tf</span><span class="o">.</span><span class="n">square</span><span class="p">(</span><span class="n">boxes</span><span class="p">[:,</span> <span class="p">:,</span> <span class="p">:,</span> <span class="mi">2</span><span class="p">])</span> <span class="o">*</span> <span class="bp">self</span><span class="o">.</span><span class="n">width</span><span class="p">,</span>
                      <span class="n">tf</span><span class="o">.</span><span class="n">square</span><span class="p">(</span><span class="n">boxes</span><span class="p">[:,</span> <span class="p">:,</span> <span class="p">:,</span> <span class="mi">3</span><span class="p">])</span> <span class="o">*</span> <span class="bp">self</span><span class="o">.</span><span class="n">height</span><span class="p">],</span> <span class="n">axis</span><span class="o">=</span><span class="mi">3</span><span class="p">)</span>

    <span class="c1"># class-specific confidence scores [S, S, B, C]</span>
    <span class="n">scores</span> <span class="o">=</span> <span class="n">tf</span><span class="o">.</span><span class="n">expand_dims</span><span class="p">(</span><span class="n">confs</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">tf</span><span class="o">.</span><span class="n">expand_dims</span><span class="p">(</span><span class="n">class_probs</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>

    <span class="n">scores</span> <span class="o">=</span> <span class="n">tf</span><span class="o">.</span><span class="n">reshape</span><span class="p">(</span><span class="n">scores</span><span class="p">,</span> <span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">C</span><span class="p">])</span>  <span class="c1"># [S*S*B, C]</span>
    <span class="n">boxes</span> <span class="o">=</span> <span class="n">tf</span><span class="o">.</span><span class="n">reshape</span><span class="p">(</span><span class="n">boxes</span><span class="p">,</span> <span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span&