介紹
實例分割(instance segmentation)的目的是從圖像中分割出每個目標實例的掩模(mask)。與語義分割相比,實例分割不但要區分不同的類別,還要區分出同一種類別下的不同目標實例。如圖13-1所示
語義分割的結果中,不同的羊對應的標簽是一樣的,即都被分配為“羊”的類別,而實例分割的結果中,不同的羊的類別標簽一樣,但是會有不同的實例號:與目標檢測相比,實例分割需要在每個包圍盒內再進一步將目標的掩模分割出來。因此,實例分割任務是語義分割任務與目標檢測任務的結合。一句話,劃分的更加仔細了。
在本章中,我們將介紹經典的基于深度學習的實例分割方法Mask R-CNN,并動手實現相應的實例分割框架。
數據集和度量
由于實例分割與目標檢測之間有著緊密的聯系,用于這兩種任務的數據集和度量也很類似。 MS COCO是最常用的實例分割數據集,mAP是實例分割的評價指標。與目標檢測不同的是,這里的mAP是通過Mask IoU進行度量。同目標檢測中的Box IoU類似,而不是AP的平均,對于圖像中的一個目標,令其真實的掩模內部的像素集合為A,一個預測的掩模內部的像素集合為B,Mask真實掩模預測掩模 IoU通過這兩個掩模內像素集合的IoU來度量兩個掩模的重合度:
圖13-2中黃色部分的面積除以紅色的面積。與目標檢測中計算mAP的過程一樣,對每種類別,通過調節Mask IoU的閾值,可以得到該類掩模交集掩模并集別實例分割結果的PR曲線,PR曲線下方的面積即為該類別實例分割結果的AP,所有類別實例分割結果的AP的平均即為mAP。
Mask R-CNN
既然實例分割結合了目標檢測和語義分割,那么實現實例分割的一個很直接的思路就是先進行目標檢測,然后在每個預測包圍盒里做語義分割。著名的Mask R-CNN模型采用的就是這一思路,從模型的命名可以看出,這是R-CNN系列工作的延續。Mask R-CNN也是由何愷明、Georgia Gkioxari、Piotr Dollar和Ross Girshick提出的,獲得了2017年國際計算機視覺大會(International Conference on Computer Vision,ICCV)最佳論文獎一馬爾獎(Marr Prize). Mask R-CNN的結構如圖I3-3所示。
不難發現,Mask R-CNN是Faster R-CNN的拓展,它們之間的主要區別有3點:
(1)引入特征金字塔網絡(feature pyramid network,FPN)與ResNet一起作為主干網絡。 FPN作為一種融合多尺度特征信息的手段,在視覺領域有著非常廣泛的應用;
(2)提出感興趣區域對齊(ROI align)取代Faster R-CNN中的ROI池化作為目標區域的特征提取器,用于提取整個候選區域特征并將候選區域特征圖尺寸歸一化到統一大小,用于后續的預測;
(3)增加了一個用于預測掩模的分支,該分支與分類頭和回歸頭并行。
特征金字塔網絡(FPN)
由于圖像中的目標大小各不相同,為了能夠檢測到不同大小的目標,目標檢測模型通常需要多尺度檢測架構。
圖13-4展示了4種不同的用于目標檢測的特征提取框架。
圖13-4(a)這種架構只在主干網絡的最后一層卷積層輸出的特征圖上做目標檢測,故該架構最大的問題是對小目標的檢測效果不理想。這是因為小尺度目標的特征會隨著主干網絡逐層下采樣逐漸消失,在最后一層僅有少量的特征支持小目標的精準檢測。
圖13-4(b)是早期的目標檢測算法常用的圖像金字塔結構,它通過將輸入圖像縮放到不同尺度來構建圖像金字塔,隨后主干網絡通過處理這些不同尺度的圖像來得到不同尺度的特征圖,最后分別在每個尺度的特征圖上做目標檢測。不難看出,使用圖像金字塔最大的問題是主干網絡的多次推理會帶來計算開銷的成倍增長。
圖13-4(c)展示的網絡架構從主干網絡的多個層級都輸出特征圖,然后在不同空間分辨率的特征圖上進行獨立的目標檢測。顯然,這種結構并沒有進行不同層之間的特征交互,既沒有給深層特征賦予淺層特征擅長定位小目標的能力,也沒有給淺層的特征賦予深層蘊含的語義信息,因此對目標檢測效果的提升有限。
圖13-4(d)展示了FPN的架構。FPN的主要思想是構建一個特征金字塔,將不同層級的特征圖融合到一起,從而實現不同大小目標的檢測。
具體而言,類似圖13-4(C),FPN先使用一個主干網絡得到每個層級的特征圖,從下到上分別命名為C2、C3、C4,其中的數字2、3 、4(直到 l),代表特征圖空間分辨率為原圖的1/2,由此得到的深層的特征圖空間分辨率小,淺層的特征圖空間分辨率大,這一過程稱為自底向上。在此之后,FPN構建了一個自頂向下的路徑來對不同空間分辨率的特征圖進行融合。首先FPN直接輸出空間分辨率最小的特征圖[圖13-4(d)右邊第3層,頂層],記為P4,然后對P4進行2倍的上采樣,使其空間分辨率大小和C3特征圖的空間分辨率保持一致,再通過1×1卷積調整C3的通道數,使其與P4的通道數一致,并將兩者進行相加,得到融合的特征圖P3[圖13-4(d)右邊第2層],這一過程稱為橫向連接。這種融合方式迭代地在每層執行,上一層不斷與下一層融合,從而實現自頂向下的效果。通過這種方式,FPN可以獲取多個尺度的特征,并將這些特征融合在一起,形成了P2、P3、P4,從而提高對多尺度目標檢測的準確性和效率。
FPN代碼實現(基于PyTorch)
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision.models.resnet import resnet50class FPN(nn.Module):def __init__(self, backbone=resnet50(pretrained=True)):super(FPPN, self).__init__()self.backbone = backbone# Lateral connectionsself.lateral5 = nn.Conv2d(2048, 256, 1)self.lateral4 = nn.Conv2d(1024, 256, 1)self.lateral3 = nn.Conv2d(512, 256, 1)self.lateral2 = nn.Conv2d(256, 256, 1)# Smooth layersself.smooth5 = nn.Conv2d(256, 256, 3, padding=1)self.smooth4 = nn.Conv2d(256, 256, 3, padding=1)self.smooth3 = nn.Conv2d(256, 256, 3, padding=1)self.smooth2 = nn.Conv2d(256, 256, 3, padding=1)# Extra layersself.conv6 = nn.Conv2d(2048, 256, 3, stride=2, padding=1)self.conv7 = nn.Conv2d(256, 256, 3, stride=2, padding=1)def forward(self, x):# Bottom-up pathway (backbone)c1 = self.backbone.conv1(x)c1 = self.backbone.bn1(c1)c1 = self.backbone.relu(c1)c1 = self.backbone.maxpool(c1)c2 = self.backbone.layer1(c1)c3 = self.backbone.layer2(c2)c4 = self.backbone.layer3(c3)c5 = self.backbone.layer4(c4)# Top-down pathwayp5 = self.lateral5(c5)p4 = self.lateral4(c4) + F.interpolate(p5, scale_factor=2, mode='nearest')p3 = self.lateral3(c3) + F.interpolate(p4, scale_factor=2, mode='nearest')p2 = self.lateral2(c2) + F.interpolate(p3, scale_factor=2, mode='nearest')# Smoothp5 = self.smooth5(p5)p4 = self.smooth4(p4)p3 = self.smooth3(p3)p2 = self.smooth2(p2)# Extra pyramid levelsp6 = self.conv6(c5)p7 = self.conv7(F.relu(p6))return p2, p3, p4, p5, p6, p7
感興趣區域對齊(ROI)
在ROI池化層中,為了得到固定空間分辨率的ROI特征圖(如7像素×7像素)并將其送給后續的預測分支,需要將每個ROI的包圍盒頂點坐標取整。在將該ROI切分為7×7共49個子窗口的過程中也需要將每個子窗口的坐標取整,如圖13-5所示。
在上述過程中,之所以需要取整,是因為將原始圖像的候選區域映射到圖像特征圖中對應的ROI后,ROI頂點坐標可能是非整數,每一個子窗口的坐標也可能是非整數,而提取的特征圖上每個位置坐標為整數。因此,通常需要將浮點數坐標取整到最近的整數坐標。但既然是近似,就會引入誤差,從而影響檢測算法的性能。為了解決這個問題,ROI對齊不再使用取整來處理非整數坐標,而是直接使用這些坐標,如圖13-6所示。
那么如何處理非整數坐標并得到特征圖上對應位置的值呢?回憶一下之前學過的雙線性插值,對于特征圖中的任意一個位置,都可以利用雙線性插值的方式得到其對應的數值。在這個過程中不會用到取整操作,因此不會引入因坐標取整導致的偏移誤差,這有利于實例分割的精度提升。同ROI池化一樣,在ROI對齊中,每個ROI將被均分成M×N個子窗口(圖中為7×7個)。隨后,將每個子窗口再均分成4個小的子窗口,取這4個小子窗口的中心點,根據每個點在特征圖中最近的整數坐標對應的特征值,通過插值來計算各自的特征值,再對這4個值進行最大池化(max pooling)或平均池化(average pooling),得到該子窗口的特征值,最終便可以得到空間分辨率為M像素×N像素的ROI特征圖。下面我們將介紹如何用代碼實現ROI對齊。
ROI對齊的代碼實現
import cv2
import numpy as npdef align_roi(image, roi_coords, output_size=(100, 100)):"""對齊ROI區域到指定尺寸"""x, y, w, h = roi_coordsroi = image[y:y+h, x:x+w]aligned = cv2.resize(roi, output_size, interpolation=cv2.INTER_AREA)return aligned
在利用ROI對齊層得到了固定空間分辨率的ROI特征圖之后,便可以利用這些特征圖進后續的任務。同Faster R-CNN一樣,Mask R-CNN在分類和回歸分支利用全連接層對ROI進目標類別分類及包圍盒回歸。與Faster R-CNN不同的是,Mask R-CNN還增加了一條專門用預測掩模的分支
在這一分支中,ROI特征圖的大小是14像素×14像素,首先會經過4層卷積層來適應分割任務,隨后做2倍上采樣得到空間分辨率為28像素×28像的特征圖。這是因為空間分辨率大的特征圖對于分割精度的提升有正向影響。最后,對該特征圖使用1×1卷積層來預測最后的分割結果。
Mask R-CNN
Mask R-CNN的損失函數由三部分組成:分類損失
、包圍盒回歸損失
和掩模分割損失
。公式如下:
分類損失
采用交叉熵損失,用于衡量預測類別與真實類別的差異:
包圍盒回歸損失
采用平滑L1損失,用于優化邊界框坐標
掩模分割損失
采用逐像素的二值交叉熵損失,用于優化目標的二值掩模
Mask R-CNN的測試階段
在測試階段,Mask R-CNN執行以下操作:
- 對候選區域生成包圍盒預測和類別預測。
- 對每個包圍盒內的目標預測其掩模。
- 對預測的包圍盒進行非極大值抑制(NMS),去除冗余的包圍盒。
NMS的公式如下:
對于每個類別,按置信度排序,保留最高置信度的包圍盒,并移除與其交并比(IoU)超過閾值的其他包圍盒:
其中和
為兩個包圍盒,
為預設閾值(通常為0.5)。
Mask R-CNN代碼實現
由于Mask R-CNN代碼架構規模比較大,我們將直接調用相關接口進行效果展示。先導入必要的包。
由于Mask R-CNN 和舊版 Keras/TensorFlow 在?Python 3.6 或 3.7?上運行更穩定,所以建議你安裝對應的py版本來避免環境問題
下面代碼都是在.ipynb文件中運行不同以往
先安裝Mask_RCNN
# 導入必要的包
!git clone https://github.com/matterport/Mask_RCNN.git
!cd Mask_RCNN/
!python setup.py install
導入和配置
import ospy
import sys
import random
import math
import numpy as np
import skimage.io
import matplotlib
import matplotlib.pyplot as plt# Root directory of the project
ROOT_DIR = os.path.abspath("../")# Import Mask RCNN
sys.path.append(ROOT_DIR) # To find local version of the library
from mrcnn import utils
import mrcnn.model as modellib
from mrcnn import visualize
# Import COCO config
sys.path.append(os.path.join(ROOT_DIR, "samples/coco/")) # To find local version
import coco%matplotlib inline # Directory to save logs and trained model
MODEL_DIR = os.path.join(ROOT_DIR, "logs")# Local path to trained weights file
COCO_MODEL_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5")
# Download COCO trained weights from Releases if needed
if not os.path.exists(COCO_MODEL_PATH):utils.download_trained_weights(COCO_MODEL_PATH)# Directory of images to run detection on
IMAGE_DIR = os.path.join(ROOT_DIR, "images")
Configurations??配置項
我們將使用基于 MS-COCO 數據集訓練的模型。該模型的配置參數存放在?coco.py
?文件的?CocoConfig
?類中。
為了進行推理任務,需要稍微調整配置以適應需求。具體做法是繼承?CocoConfig
?類并重寫需要修改的屬性。
class InferenceConfig(coco.CocoConfig):# Set batch size to 1 since we'll be running inference on# one image at a time. Batch size = GPU_COUNT * IMAGES_PER_GPUGPU_COUNT = 1IMAGES_PER_GPU = 1config = InferenceConfig()
config.display()
output:
Configurations:
BACKBONE_SHAPES [[256 256][128 128][ 64 64][ 32 32][ 16 16]]
BACKBONE_STRIDES [4, 8, 16, 32, 64]
BATCH_SIZE 1
BBOX_STD_DEV [ 0.1 0.1 0.2 0.2]
DETECTION_MAX_INSTANCES 100
DETECTION_MIN_CONFIDENCE 0.5
DETECTION_NMS_THRESHOLD 0.3
GPU_COUNT 1
IMAGES_PER_GPU 1
IMAGE_MAX_DIM 1024
IMAGE_MIN_DIM 800
IMAGE_PADDING True
IMAGE_SHAPE [1024 1024 3]
LEARNING_MOMENTUM 0.9
LEARNING_RATE 0.002
MASK_POOL_SIZE 14
MASK_SHAPE [28, 28]
MAX_GT_INSTANCES 100
MEAN_PIXEL [ 123.7 116.8 103.9]
MINI_MASK_SHAPE (56, 56)
NAME coco
NUM_CLASSES 81
POOL_SIZE 7
POST_NMS_ROIS_INFERENCE 1000
POST_NMS_ROIS_TRAINING 2000
ROI_POSITIVE_RATIO 0.33
RPN_ANCHOR_RATIOS [0.5, 1, 2]
RPN_ANCHOR_SCALES (32, 64, 128, 256, 512)
RPN_ANCHOR_STRIDE 2
RPN_BBOX_STD_DEV [ 0.1 0.1 0.2 0.2]
RPN_TRAIN_ANCHORS_PER_IMAGE 256
STEPS_PER_EPOCH 1000
TRAIN_ROIS_PER_IMAGE 128
USE_MINI_MASK True
USE_RPN_ROIS True
VALIDATION_STEPS 50
WEIGHT_DECAY 0.0001
創建模型并加載已訓練權重
# Create model object in inference mode.
model = modellib.MaskRCNN(mode="inference", model_dir=MODEL_DIR, config=config)# Load weights trained on MS-COCO
model.load_weights(COCO_MODEL_PATH, by_name=True)
Class Names??類名稱
模型會對物體進行分類并返回類別 ID,這些整數值用于標識每個類別。某些數據集會為類別分配整數值,有些則不會。例如在 MS-COCO 數據集中,'person'類別對應 1,'teddy bear'對應 88。這些 ID 通常是連續的,但并非始終如此——比如 COCO 數據集中就有對應 70 和 72 的類別,但 71 卻不存在。
為了提高一致性,并支持同時從多個數據源進行訓練,我們的?Dataset
?類會為每個類分配自有的順序整數 ID。例如,若使用我們的?Dataset
?類加載 COCO 數據集,'person'類別將獲得 class ID=1(與 COCO 相同),而'teddy bear'類別則為 78(與 COCO 不同)。在將類別 ID 映射到類別名稱時需注意這一點。
要獲取類名列表,您可以加載數據集后使用?class_names
?屬性,如下所示。
# Load COCO dataset
dataset = coco.CocoDataset()
dataset.load_coco(COCO_DIR, "train")
dataset.prepare()# Print class names
print(dataset.class_names)
我們不想讓你為了運行這個演示就必須下載 COCO 數據集,所以下面提供了類別名稱列表。列表中每個類別名稱的索引代表其 ID(第一個類別是 0,第二個是 1,第三個是 2,...以此類推)。
# COCO Class names
# Index of the class in the list is its ID. For example, to get ID of
# the teddy bear class, use: class_names.index('teddy bear')
class_names = ['BG', 'person', 'bicycle', 'car', 'motorcycle', 'airplane','bus', 'train', 'truck', 'boat', 'traffic light','fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird','cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear','zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie','suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball','kite', 'baseball bat', 'baseball glove', 'skateboard','surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup','fork', 'knife', 'spoon', 'bowl', 'banana', 'apple','sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza','donut', 'cake', 'chair', 'couch', 'potted plant', 'bed','dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote','keyboard', 'cell phone', 'microwave', 'oven', 'toaster','sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors','teddy bear', 'hair drier', 'toothbrush']
運行目標檢測
# Load a random image from the images folder
file_names = next(os.walk(IMAGE_DIR))[2]
image = skimage.io.imread(os.path.join(IMAGE_DIR, random.choice(file_names)))# Run detection
results = model.detect([image], verbose=1)# Visualize results
r = results[0]
visualize.display_instances(image, r['rois'], r['masks'], r['class_ids'], class_names, r['scores'])
Processing 1 images image shape: (476, 640, 3) min: 0.00000 max: 255.00000 molded_images shape: (1, 1024, 1024, 3) min: -123.70000 max: 120.30000 image_metas shape: (1, 89) min: 0.00000 max: 1024.00000
Mask-RCNN還要很對應用,具體可見matterport/Mask_RCNN: Mask R-CNN for object detection and instance segmentation on Keras and TensorFlow
本文介紹了實例分割的基本概念及其經典方法Mask R-CNN。實例分割結合了目標檢測和語義分割,不僅能區分不同類別,還能識別同一類別下的不同實例。Mask R-CNN在Faster R-CNN基礎上引入特征金字塔網絡(FPN)和ROI對齊(ROIAlign)技術,并增加掩模預測分支,顯著提升了分割精度。文章詳細闡述了FPN的多尺度特征融合機制和ROIAlign避免坐標取整誤差的優勢,并提供了基于PyTorch的FPN實現代碼。最后演示了使用預訓練Mask R-CNN模型進行實例分割的完整流程,包括環境配置。