1. 程式人生 > >TensorFlow實戰:Chapter-8上(Mask R-CNN介紹與實現)

TensorFlow實戰:Chapter-8上(Mask R-CNN介紹與實現)

這裡寫圖片描述

簡介

程式碼源於matterport的工作組,可以在github上fork它們組的工作。

軟體必備

復現的Mask R-CNN是基於Python3,Keras,TensorFlow

  • Python 3.4+
  • TensorFlow 1.3+
  • Keras 2.0.8+
  • Jupyter Notebook
  • Numpy, skimage, scipy

建議配置一個高版本的Anaconda3+TensorFlow-GPU版本。

Mask R-CNN論文回顧

Mask R-CNN(簡稱MRCNN)是基於R-CNN系列、FPN、FCIS等工作之上的,MRCNN的思路很簡潔:Faster R-CNN針對每個候選區域有兩個輸出:種類標籤和bbox的偏移量。那麼MRCNN就在Faster R-CNN的基礎上通過增加一個分支進而再增加一個輸出,即物體掩膜(object mask

)。

先回顧一下Faster R-CNN, Faster R-CNN主要由兩個階段組成:區域候選網路(Region Proposal Network,RPN)和基礎的Fast R-CNN模型。

  • RPN用於產生候選區域
    這裡寫圖片描述

  • Fast R-CNN通過RoIPool層對每個候選區域提取特徵,從而實現目標分類和bbox迴歸
    這裡寫圖片描述

MRCNN採用和Faster R-CNN相同的兩個階段,具有相同的第一層(即RPN),第二階段,除了預測種類和bbox迴歸,並且並行的對每個RoI預測了對應的二值掩膜(binary mask)。示意圖如下:

這裡寫圖片描述

這樣做可以將整個任務簡化為mulit-stage pipeline,解耦了多個子任務的關係,現階段來看,這樣做好處頗多。

主要工作

損失函式的定義

依舊採用的是多工損失函式,針對每個每個RoI定義為

L=Lcls+Lbox+LmaskLclsLbox與Faster R-CNN的定義類似,這裡主要看Lmask

掩膜分支針對每個RoI產生一個Km2的輸出,即K個解析度為m×m的二值的掩膜K為分類物體的種類數目。依據預測類別分支預測的型別i,只將第i的二值掩膜輸出記為Lmask
掩膜分支的損失計算如下示意圖:

  1. mask branch 預測K個種類的m×m二值掩膜輸出
  2. 依據種類預測分支(Faster R-CNN部分)預測結果:當前RoI的物體種類為
    i
  3. i個二值掩膜輸出就是該RoI的損失Lmask

這裡寫圖片描述

對於預測的二值掩膜輸出,我們對每個畫素點應用sigmoid函式,整體損失定義為平均二值交叉損失熵。
引入預測K個輸出的機制,允許每個類都生成獨立的掩膜,避免類間競爭。這樣做解耦了掩膜和種類預測。不像是FCN的方法,在每個畫素點上應用softmax函式,整體採用的多工交叉熵,這樣會導致類間競爭,最終導致分割效果差。

掩膜表示到RoIAlign層

在Faster R-CNN上預測物體標籤或bbox偏移量是將feature map壓縮到FC層最終輸出vector,壓縮的過程丟失了空間上(平面結構)的資訊,而掩膜是對輸入目標做空間上的編碼,直接用卷積形式表示畫素點之間的對應關係那是最好的了。

輸出掩膜的操作是不需要壓縮輸出vector,所以可以使用FCN(Full Convolutional Network),不僅效率高,而且引數量還少。為了更好的表示出RoI輸入和FCN輸出的feature之間的畫素對應關係,提出了RoIAlign層。

先回顧一下RoIPool層:

其核心思想是將不同大小的RoI輸入到RoIPool層,RoIPool層將RoI量化成不同粒度的特徵圖(量化成一個一個bin),在此基礎上使用池化操作提取特徵。

下圖是SPPNet內對RoI的操作,在Faster R-CNN中只使用了一種粒度的特徵圖:

這裡寫圖片描述

平面示意圖如下:

這裡寫圖片描述

這裡面存在一些問題,在上面量操作上,實際計算中是使用的是[x/16]16的量化的步長,[·]是舍入操作(rounding)。這套量化舍入操作在提取特徵時有著較好的魯棒性(檢測物體具有平移不變性等),但是這很不利於掩膜定位,有較大負面效果。

針對這個問題,提出了RoIAlign層:避免了對RoI邊界或bin的量化操作,在擴充套件feature map時使用雙線性插值演算法。這裡實現的架構要看FPN論文:

這裡寫圖片描述

一開始的Faster R-CNN是基於最上層的特徵對映做分割和預測的,這會丟失高分辨下的資訊,直觀的影響就是丟失小目標檢測,對細節部分丟失不敏感。受到SSD的啟發,FPN也使用了多層特徵做預測。這裡使用的top-down的架構,是將高層的特徵反捲積帶到低層的特徵(即有了語義,也有精度),而在MRCNN論文裡面說的雙線性差值演算法就是這裡的top-down反捲積是用的插值演算法。

總結

MRCNN有著優異的效果,除去了掩膜分支的作用,很大程度上是因為基礎特徵網路的增強,論文使用的是ResNeXt101+FPN的top-down組合,有著極強的特徵學習能力,並且在實驗中夾雜這多種工程調優技巧。

但是吧,MRCNN的缺點也很明顯,需要大的計算能力並且速度慢,這離實際應用還是有很長的路,坐等大神們發力!

如何使用程式碼

  1. 滿足執行環境

    • Python 3.4+
    • TensorFlow 1.3+
    • Keras 2.0.8+
    • Jupyter Notebook
    • Numpy, skimage, scipy, Pillow(安裝Anaconda3直接完事)
    • cv2
  2. 下載程式碼

    • linux環境下直接clone到本地

      git clone https://github.com/matterport/Mask_RCNN.git
    • Windows下下載程式碼即可,地址在上面

  3. 下載模型在COCO資料集上預訓練權重(mask_rcnn_coco.h5),下載地址releasses Page.

  4. 如果需要在COCO資料集上訓練或測試,需要安裝pycocotoolsclone下來,make生成對應的檔案,拷貝下工程目錄下即可(方法可參考下面repos內的README.md檔案)。

  5. 如果使用COCO資料集,需要:

    • pycocotools (即第4條描述的)
    • COCO子資料集,5K的minival和35K的validation-minus-minival。(這兩個資料集下載比較慢,沒有貼原地址,而是我的CSDN地址,分不夠下載的可以私信我~)

下面的程式碼分析執行環境都是jupyter。

程式碼分析-資料預處理

導包

匯入的coco包需要從coco/PythonAPI上下載操作資料程式碼,並在本地使用make指令編譯.將生成的pycocotools拷貝至工程的主目錄下,即和該inspect_data.ipynb檔案同一目錄。

import os
import sys
import itertools
import math
import logging
import json
import re
import random
from collections import OrderedDict
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.lines as lines
from matplotlib.patches import Polygon

import utils
import visualize
from visualize import display_images
import model as modellib
from model import log

%matplotlib inline 

ROOT_DIR = os.getcwd()

# 選擇任意一個程式碼塊 
# import shapes
# config = shapes.ShapesConfig()    # 使用程式碼建立資料集,後面會有介紹

# MS COCO 資料集
import coco
config = coco.CocoConfig()
COCO_DIR = "/root/模型復現/Mask_RCNN-master/coco"  # COCO資料存放位置

載入資料集

COCO資料集的訓練集內有82081張圖片,共81類。

# 這裡使用的是COCO
if config.NAME == 'shapes':
    dataset = shapes.ShapesDataset()
    dataset.load_shapes(500, config.IMAGE_SHAPE[0], config.IMAGE_SHAPE[1])
elif config.NAME == "coco":
    dataset = coco.CocoDataset()
    dataset.load_coco(COCO_DIR, "train")

# Must call before using the dataset
dataset.prepare()

print("Image Count: {}".format(len(dataset.image_ids)))
print("Class Count: {}".format(dataset.num_classes))
for i, info in enumerate(dataset.class_info):
    print("{:3}. {:50}".format(i, info['name']))

>>>
>>>
loading annotations into memory...
Done (t=7.68s)
creating index...
index created!
Image Count: 82081
Class Count: 81
  0. BG                                                
  1. person                                            
  2. bicycle   
 ...
 77. scissors                                          
 78. teddy bear                                        
 79. hair drier                                        
 80. toothbrush

隨機找幾張照片看看:

# 載入和展示隨機幾張照片和對應的mask
image_ids = np.random.choice(dataset.image_ids, 4)
for image_id in image_ids:
    image = dataset.load_image(image_id)
    mask, class_ids = dataset.load_mask(image_id)
    visualize.display_top_masks(image, mask, class_ids, dataset.class_names)

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

Bounding Boxes(bbox)

這裡我們不使用資料集本身提供的bbox座標資料,取而代之的是通過mask計算出bbox,這樣可以在不同的資料集下對bbox使用相同的處理方法。因為我們是從mask上計算bbox,相比與從圖片計算bbox轉換來說,更便於放縮,旋轉,裁剪影象。

# Load random image and mask.
image_id = random.choice(dataset.image_ids)
image = dataset.load_image(image_id)
mask, class_ids = dataset.load_mask(image_id)
# Compute Bounding box
bbox = utils.extract_bboxes(mask)

# Display image and additional stats
print("image_id ", image_id, dataset.image_reference(image_id))
log("image", image)
log("mask", mask)
log("class_ids", class_ids)
log("bbox", bbox)
# Display image and instances
visualize.display_instances(image, bbox, mask, class_ids, dataset.class_names)

>>>
>>>
image_id  41194 http://cocodataset.org/#explore?id=190360
image                    shape: (428, 640, 3)         min:    0.00000  max:  255.00000
mask                     shape: (428, 640, 5)         min:    0.00000  max:    1.00000
class_ids                shape: (5,)                  min:    1.00000  max:   59.00000
bbox                     shape: (5, 4)                min:    1.00000  max:  640.00000

這裡寫圖片描述

調整圖片大小

因為訓練時是批量處理的,每次batch要處理多張圖片,模型需要一個固定的輸入大小。故將訓練集的圖片放縮到一個固定的大小(1024×1024),放縮的過程要保持不變的寬高比,如果照片本身不是正方形,那邊就在邊緣填充0.(這在R-CNN論文裡面論證過)

需要注意的是:原圖片做了放縮,對應的mask也需要放縮,因為我們的bbox是依據mask計算出來的,這樣省了修改程式了~

# Load random image and mask.
image_id = np.random.choice(dataset.image_ids, 1)[0]
image = dataset.load_image(image_id)
mask, class_ids = dataset.load_mask(image_id)
original_shape = image.shape
# 調整到固定大小
image, window, scale, padding = utils.resize_image(
    image, 
    min_dim=config.IMAGE_MIN_DIM, 
    max_dim=config.IMAGE_MAX_DIM,
    padding=config.IMAGE_PADDING)
mask = utils.resize_mask(mask, scale, padding) # mask也要放縮
# Compute Bounding box
bbox = utils.extract_bboxes(mask)

# Display image and additional stats
print("image_id: ", image_id, dataset.image_reference(image_id))
print("Original shape: ", original_shape)
log("image", image)
log("mask", mask)
log("class_ids", class_ids)
log("bbox", bbox)
# Display image and instances
visualize.display_instances(image, bbox, mask, class_ids, dataset.class_names)

>>>
>>>
image_id:  6104 http://cocodataset.org/#explore?id=139889
Original shape:  (426, 640, 3)
image                    shape: (1024, 1024, 3)       min:    0.00000  max:  255.00000
mask                     shape: (1024, 1024, 2)       min:    0.00000  max:    1.00000
class_ids                shape: (2,)                  min:   24.00000  max:   24.00000
bbox                     shape: (2, 4)                min:  169.00000  max:  917.00000

原圖片從(426, 640, 3)放大到(1024, 1024, 3),圖片的上下兩端都填充了0(黑色的部分):

這裡寫圖片描述

Mini Mask

訓練高解析度的圖片時,表示每個目標的二值mask也會非常大。例如,訓練一張1024×1024的圖片,其目標物體對應的mask需要1MB的記憶體(用boolean變量表示單點),如果1張圖片有100個目標物體就需要100MB。講道理,如果是五顏六色就算了,但實際上表示mask的影象矩陣上大部分都是0,很浪費空間。

為了節省空間同時提升訓練速度,我們優化mask的表示方式,不直接儲存那麼多0,而是通過儲存有值座標的相對位置來壓縮表示資料的記憶體,原理和壓縮演算法差類似。

  • 我們儲存在物件邊界框內(bbox內)的mask畫素,而不是儲存整張圖片的mask畫素,大多數物體相對比於整張圖片是較小的,節省儲存空間是通過少儲存目標周圍的0實現的。
  • 將mask調整到小尺寸56×56,對於大尺寸的物體會丟失一些精度,但是大多數物件的註解並不是很準確,所以大多數情況下這些損失是可以忽略的。(可以在config類中設定mini mask的size。)

說白了就是在處理資料的時候,我們先利用標註的mask資訊計算出對應的bbox框,而後利用計算的bbox框反過來改變mask的表示方法,目的就是操作規範化,同時降低儲存空間和計算複雜度。

image_id = np.random.choice(dataset.image_ids, 1)[0]
# 使用load_image_gt方法獲取bbox和mask
image, image_meta, bbox, mask = modellib.load_image_gt(
    dataset, config, image_id, use_mini_mask=False)

log("image", image)
log("image_meta", image_meta)
log("bbox", bbox)
log("mask", mask)

display_images([image]+[mask[:,:,i] for i in range(min(mask.shape[-1], 7))])

>>>
>>>
image                    shape: (1024, 1024, 3)       min:    0.00000  max:  252.00000
image_meta               shape: (89,)                 min:    0.00000  max: 66849.00000
bbox                     shape: (1, 5)                min:   62.00000  max:  987.00000
mask                     shape: (1024, 1024, 1)       min:    0.00000  max:    1.00000

隨機選取一張圖片,可以看到圖片目標相對與圖片本身較小:

這裡寫圖片描述

visualize.display_instances(image, bbox[:,:4], mask, bbox[:,4], dataset.class_names)

這裡寫圖片描述

使用load_image_gt方法,傳入use_mini_mask=True實現mini mask操作:

# load_image_gt方法集成了mini_mask的操作
image, image_meta, bbox, mask = modellib.load_image_gt(
    dataset, config, image_id, augment=True, use_mini_mask=True)
log("mask", mask)
display_images([image]+[mask[:,:,i] for i in range(min(mask.shape[-1], 7))])

>>>
>>>
mask                     shape: (56, 56, 1)           min:    0.00000  max:    1.00000

這裡寫圖片描述

這裡為了展現效果,將mini_mask表示方法通過expand_mask方法擴大到大影象下的mask,再繪製試試:

mask = utils.expand_mask(bbox, mask, image.shape)
visualize.display_instances(image, bbox[:,:4], mask, bbox[:,4], dataset.class_names)

這裡寫圖片描述
可以看到邊界是鋸齒狀,這也是壓縮的副作用,總體來說效果還可以~

Anchors

Anchors是Faster R-CNN內提出的方法
模型在執行過程中有多層feature map,同時也會有非常多的Anchors,處理好Anchors的順序非常重要。例如使用anchors的順序要匹配卷積處理的順序等規則。

對於FPN網路,anchor的順序要與卷積層的輸出相匹配:

  • 先按金字塔等級排序,第一層的所有anchors,第二層所有anchors,etc..通過按層次可以很容易分開所有的anchors
  • 對於每個層,通過feature map處理序列來排列anchors,通常,一個卷積層處理一個feature map 是從左上角開始,向右一行一行來整
  • 對於feature map的每個cell,可為不同比例的Anchors採用隨意順序,這裡我們將採用不同比例的順序當引數傳遞給相應的函式

Anchor步長:在FPN架構下,前幾層的feature map是高解析度的。例如,如果輸入是1024×1024,那麼第一層的feature map大小為256×256,這會產生約200K的anchors(2562563),這些anchor都是32×32,相對於圖片畫素的步長為4(1024/256=4),這裡面有很多重疊,如果我們能夠為feature map的每個點生成獨有的anchor,就會顯著的降低負載,如果設定anchor的步長為2,那麼anchor的數量就會下降4倍。

這裡我們使用的strides為2,這和論文不一樣,在Config類中,我們配置了3中比例([0.5, 1, 2])的anchors,以第一層feature map舉例,其大小為256×256,故有