[譯] 如何使用 Keras 訓練目標檢測模型

如何使用 Keras 訓練目標檢測模型

目標檢測是一項頗有挑戰性的計算機視覺類課題,它包括預測目標在圖像中的位置以及確認檢測到的目標是何種類型的物體。html

基於掩膜區域的卷積神經網絡模型,或者咱們簡稱爲 Mask R-CNN,是目標檢測中最早進的方法之一。Matterport Mask R-CNN 項目爲咱們提供了可用於開發與測試 Mask R-CNN 的 Keras 模型的庫,咱們可用其來完成咱們本身的目標檢測任務。儘管它利用了那些在很是具備挑戰性的目標檢測任務中訓練出來的最佳模型,如 MS COCO,來供咱們進行遷移學習,可是對於初學者來講,使用這個庫可能有些困難,而且它還須要開發者仔細準備好數據集。前端

在這篇教程中,你將學習如何訓練能夠在照片中識別袋鼠的 Mask R-CNN 模型。python

在學完教程後,你將會知道:android

  • 如何爲訓練 R-CNN 模型準備好目標檢測數據集。
  • 如何使用遷移學習在新的數據集上訓練目標檢測模型。
  • 如何在測試數據集上評估 Mask R-CNN,以及如何在新的照片上做出預測。

若是你還想知道如何創建圖像分類、目標檢測、人臉識別的模型等等,能夠看看個人關於計算機視覺的新書,書中包括了 30 篇講解細緻的教程和全部源代碼。ios

如今咱們開始吧。git

How to Train an Object Detection Model to Find Kangaroos in Photographs (R-CNN with Keras)

如何使用 R-CNN 模型以及 Keras 訓練能夠在照片中識別袋鼠的目標檢測模型 照片來自 Ronnie Robertson,做者保留圖像權利。github

教程目錄

本片教程能夠分爲五個部分,分別是:web

  1. 如何爲 Keras 安裝 Mask R-CNN
  2. 如何爲目標檢測準備數據集
  3. 如何訓練檢測袋鼠的 Mask R-CNN 模型
  4. 如何評估 Mask R-CNN 模型
  5. 如何在新照片中檢測袋鼠

如何爲 Keras 安裝 Mask R-CNN

目標檢測是計算機視覺中的一個課題,它包括在給定圖像中識別特定內容是否存在,位置信息,以及一個或多個對象所屬的類別。算法

這是一個頗有挑戰性的問題,涵蓋了目標識別(例如,找到目標在哪裏),目標定位(例如,目標所處位置的範圍),以及目標分類(例如,目標是哪一類物體)這三個問題的模型構建方法。macos

基於區域的卷積神經網絡,即 R-CNN,是卷積神經網絡模型家族中專爲目標檢測而設計的,它的開發者是 Ross Girshick 等人。這種方法大約有四個主要的升級變更,結果就是造成了目前最優的 Mask R-CNN。2018 年的文章「Mask R-CNN」提出的 Mask R-CNN 是基於區域的卷積神經網絡的模型家族中最新的版本,可以同時支持目標檢測與目標分割。目標分割不只包括了目標在圖像中的定位,而且包括指定圖像的掩膜,以及準確指示出圖像中的哪些像素屬於該對象。

與簡單模型,甚至最早進的深度卷積神經網絡模型相比,Mask R-CNN 是一個應用複雜的模型。與其要從頭開始開發 R-CNN 或者 Mask R-CNN 模型應用,不如使用一個可靠的基於 Keras 深度學習框架的第三方應用。

目前最好的 Mask R-CNN 的第三方應用是 Mask R-CNN Project,其研發者爲 Matterport。該項目是擁有許可證的開源項目(例如 MIT license),它的代碼已經被普遍的應用於各類不一樣的項目以及 Kaggle 競賽中。

第一步是安裝該庫。

到本篇文章寫就爲止,該庫並無發行版,因此咱們須要手動安裝。可是好消息是安裝很是簡單。

安裝步驟包括拷貝 GitHub 倉庫而後在工做區下運行安裝腳本,若是你在該過程當中遇到了困難,能夠參見倉庫 readme 文件中的安裝說明

第一步,克隆 GitHub 上的 Mask R-CNN 倉庫

這一步很是簡單,只須要在命令行運行下面的命令:

git clone https://github.com/matterport/Mask_RCNN.git
複製代碼

這段代碼將會在本地建立一個新的名爲 Mask_RCNN 的目錄,目錄結構以下:

Mask_RCNN
├── assets
├── build
│   ├── bdist.macosx-10.13-x86_64
│   └── lib
│       └── mrcnn
├── dist
├── images
├── mask_rcnn.egg-info
├── mrcnn
└── samples
    ├── balloon
    ├── coco
    ├── nucleus
    └── shapes
複製代碼

第二步,安裝 Mask R-CNN 庫

倉庫能夠經過 pip 命令安裝。

將路徑切換至 Mask_RCNN 而後運行安裝腳本。

在命令行中輸入:

cd Mask_RCNN
python setup.py install
複製代碼

在 Linux 或者 MacOS 系統上,你也許須要使用 sudo 來容許軟件安裝;你也許會看到以下的報錯:

error: can't create or remove files in install directory 複製代碼

這種狀況下,使用 sudo 安裝軟件:

sudo python setup.py install
複製代碼

若是你在使用 Python 的虛擬環境(virtualenv),例如 EC2 深度學習的 AMI 實例(推薦用於本教程),你可使用以下命令將 Mask_RCNN 安裝到你的環境中:

sudo ~/anaconda3/envs/tensorflow_p36/bin/python setup.py install
複製代碼

這樣,該庫就會直接開始安裝,你將會看到安裝成功的消息,並如下面這條結束:

...
Finished processing dependencies for mask-rcnn==2.1
複製代碼

這條消息表示你已經成功安裝了該庫的最新 2.1 版本。

第三步,確認庫已經安裝完成

確認庫已經正確安裝永遠是一個良好的習慣。

你能夠經過 pip 命令來請求庫來確認它是否已經正確安裝;例如:

pip show mask-rcnn
複製代碼

你應該能夠看到告知你版本號和安裝地址的輸出信息;例如:

Name: mask-rcnn
Version: 2.1
Summary: Mask R-CNN for object detection and instance segmentation
Home-page: https://github.com/matterport/Mask_RCNN
Author: Matterport
Author-email: waleed.abdulla@gmail.com
License: MIT
Location: ...
Requires:
Required-by:
複製代碼

咱們如今已經準備好,能夠開始使用這個庫了。

如何爲目標檢測準備數據集

接下來,咱們須要爲模型準備數據集。

在本篇教程中,咱們將會使用袋鼠數據集,倉庫的做者是 experiencor 即 Huynh Ngoc Anh。數據集包括了 183 張包含袋鼠的圖像,以及一些 XML 註解文件,用來提供每張照片中袋鼠所處的邊框信息。

人們設計出的 Mask R-CNN 能夠學習並同時預測出目標的邊界以及檢測目標的掩膜,然而袋鼠數據集並不提供掩膜信息。所以咱們使用這個數據集來完成學習袋鼠目標檢測的任務,同時忽略掉掩膜,咱們不關心模型的圖像分割能力。

在準備訓練模型的數據集以前,還須要幾個步驟,這些步驟咱們將會在這一章中逐個完成,包括下載數據集,解析註解文件,創建可用於 Mask_RCNN 庫的袋鼠數據集對象,而後還要測試數據集對象,以確保咱們可以正確的加載圖像和註解文件。

安裝數據集

第一步是將數據集下載到當前的工做目錄中。

經過將 GitHub 倉庫直接拷貝下來便可完成這一步,運行以下命令:

git clone https://github.com/experiencor/kangaroo.git
複製代碼

此時會建立一個名爲 「kangaroo」 的新目錄,其包含一個名爲 ‘images/’ 的子目錄,子目錄中包含了全部的袋鼠 JPEG 圖像,以及一個名爲 ‘annotes/’ 的子目錄,其中的 XML 文件描述了每張照片中袋鼠的位置信息。

kangaroo
├── annots
└── images
複製代碼

讓咱們查看一下每一個子目錄,能夠看到圖像和註解文件都遵循了一致的命名約定,即五位零填充編號系統(5-digit zero-padded numbering system);例如:

images/00001.jpg
images/00002.jpg
images/00003.jpg
...
annots/00001.xml
annots/00002.xml
annots/00003.xml
...
複製代碼

這種命名方式讓圖像和其註解文件可以很是容易的匹配在一塊兒。

咱們也能看到,編號系統的數字並不連續,一些照片沒有出現,例如,沒有名爲 ‘00007’ 的 JPG 或者 XML 文件。

這意味着,咱們應該直接加載目錄下的實際文件列表,而不是利用編號系統加載文件。

解析註解文件

下一步是要搞清楚如何加載註解文件。

首先,咱們打開並查看第一個註解文件(annots/00001.xml);你會看到:

<annotation>
	<folder>Kangaroo</folder>
	<filename>00001.jpg</filename>
	<path>...</path>
	<source>
		<database>Unknown</database>
	</source>
	<size>
		<width>450</width>
		<height>319</height>
		<depth>3</depth>
	</size>
	<segmented>0</segmented>
	<object>
		<name>kangaroo</name>
		<pose>Unspecified</pose>
		<truncated>0</truncated>
		<difficult>0</difficult>
		<bndbox>
			<xmin>233</xmin>
			<ymin>89</ymin>
			<xmax>386</xmax>
			<ymax>262</ymax>
		</bndbox>
	</object>
	<object>
		<name>kangaroo</name>
		<pose>Unspecified</pose>
		<truncated>0</truncated>
		<difficult>0</difficult>
		<bndbox>
			<xmin>134</xmin>
			<ymin>105</ymin>
			<xmax>341</xmax>
			<ymax>253</ymax>
		</bndbox>
	</object>
</annotation>
複製代碼

咱們能夠看到,註解文件包含一個用於描述圖像大小的「size」元素,以及一個或多個用於描述袋鼠對象在圖像中位置的邊框的「object」元素。

大小和邊框是每一個註解文件中所需的最小信息。咱們能夠仔細一點、寫一些 XML 解析代碼來處理這些註解文件,這對於生產環境的系統是頗有幫助的。而在開發過程當中,咱們將會縮減步驟,直接使用 XPath 從每一個文件中提取出咱們須要的數據,例如,//size 請求能夠從文件中提取出 size 元素,而 //object 或者 //bndbox 請求能夠提取出 bounding box 元素。

Python 爲開發者提供了 元素樹 API,可用於加載和解析 XML 文件,咱們可使用 find()findall() 函數對已加載的文件發起 XPath 請求。

首先,註解文件必需要被加載並解析爲 ElementTree 對象。

# load and parse the file
tree  =  ElementTree.parse(filename)
複製代碼

加載成功後,咱們能夠取到文檔的根元素,並能夠對根元素髮起 XPath 請求。

# 獲取文檔根元素
root  =  tree.getroot()
複製代碼

咱們可使用帶‘.//bndbox’參數的 findall() 函數來獲取全部‘bndbox’元素,而後遍歷每一個元素來提取出用於定義每一個邊框的 xy,minmax 的值。

元素內的文字也能夠被解析爲整數值。

# 提取出每一個 bounding box 元素
for  box in  root.findall('.//bndbox'):
	xmin  =  int(box.find('xmin').text)
	ymin  =  int(box.find('ymin').text)
	xmax  =  int(box.find('xmax').text)
	ymax  =  int(box.find('ymax').text)
	coors  =  [xmin,  ymin,  xmax,  ymax]
複製代碼

接下來咱們就能夠將全部邊框的定義值整理爲一個列表。

圖像的尺寸也一樣頗有用,它能夠經過直接請求取得。

# 提取出圖像尺寸
width  =  int(root.find('.//size/width').text)
height  =  int(root.find('.//size/height').text)
複製代碼

咱們能夠將上面這些代碼合成一個函數,它以註解文件做爲入參,提取出邊框和圖像尺寸等細節信息,並將這些值返回給咱們使用。

以下的 extract_boxes() 函數就是上述功能的實現。

# 從註解文件中提取邊框值的函數
def extract_boxes(filename):
	# 加載並解析文件
	tree = ElementTree.parse(filename)
	# 獲取文檔根元素
	root = tree.getroot()
	# 提取出每一個 bounding box 元素
	boxes = list()
	for box in root.findall('.//bndbox'):
		xmin = int(box.find('xmin').text)
		ymin = int(box.find('ymin').text)
		xmax = int(box.find('xmax').text)
		ymax = int(box.find('ymax').text)
		coors = [xmin, ymin, xmax, ymax]
		boxes.append(coors)
	# 提取出圖像尺寸
	width = int(root.find('.//size/width').text)
	height = int(root.find('.//size/height').text)
	return boxes, width, height
複製代碼

如今能夠測試這個方法了,咱們能夠將目錄中第一個註解文件做爲函數參數進行測試。

完整的示例以下。

# 從註解文件中提取邊框值的函數
def extract_boxes(filename):
	# 加載並解析文件
	tree = ElementTree.parse(filename)
	# 獲取文檔根元素
	root = tree.getroot()
	# 提取出每一個 bounding box 元素
	boxes = list()
	for box in root.findall('.//bndbox'):
		xmin = int(box.find('xmin').text)
		ymin = int(box.find('ymin').text)
		xmax = int(box.find('xmax').text)
		ymax = int(box.find('ymax').text)
		coors = [xmin, ymin, xmax, ymax]
		boxes.append(coors)
	# 提取出圖像尺寸
	width = int(root.find('.//size/width').text)
	height = int(root.find('.//size/height').text)
	return boxes, width, height
複製代碼

運行上述示例代碼,函數將會返回一個包含了註解文件中每一個邊框元素信息,以及每張圖像的寬度和高度的列表。

[[233, 89, 386, 262], [134, 105, 341, 253]] 450 319
複製代碼

如今咱們學會了如何加載註解文件,下面咱們將學習如何使用這個功能,來建立一個數據集對象。

建立袋鼠數據集對象

mask-rcnn 須要 mrcnn.utils.Dataset 對象來管理訓練、校驗以及測試數據集的過程。

這就意味着,新建的類必需要繼承 mrcnn.utils.Dataset 類,並定義一個加載數據集的函數,這個函數能夠任意命名,例如能夠是 load_dataset(),它會重載用於加載掩膜的函數 load_mask() 以及用於加載圖像引用(路徑或者 URL)的函數 image_reference()

# 用於定義和加載袋鼠數據集的類
class KangarooDataset(Dataset):
	# 加載數據集定義
	def load_dataset(self, dataset_dir, is_train=True):
		# ...

	# 加載圖像掩膜
	def load_mask(self, image_id):
		# ...

	# 加載圖像引用
	def image_reference(self, image_id):
		# ...
複製代碼

爲了可以使用類 Dataset 的對象,它必需要先進行實例化,而後必須調用你的自定義加載函數,最後內建的 prepare() 函數纔會被調用。

例如,咱們將要建立一個名爲 KangarooDataset 的類,它將會以以下這樣的方式使用:

# 準備數據集
train_set  =  KangarooDataset()
train_set.load_dataset(...)
train_set.prepare()
複製代碼

自定義的加載函數,即 load_dataset(),同時負責定義類以及定義數據集中的圖像。

經過調用內建的函數 add_class() 能夠定義類,經過函數的參數能夠指定數據集名稱‘source’,類的整型編號‘class_id’(例如,1 代指第一個類,不要使用 0,由於 0 已經保留用於背景類),以及‘class_name’(例如‘kangaroo’)。

# 定義一個類
self.add_class("dataset",  1,  "kangaroo")
複製代碼

經過調用內建的 add_image() 函數能夠定義圖像對象,經過函數的參數能夠指定數據集名稱‘source’,惟一的‘image_id’(例如,形如‘00001’這樣沒有擴展的文件名),以及圖像加載的位置(例如‘kangaroo/images/00001.jpg’)。

這樣,咱們就爲圖像定義了一個「image info」字典結構,因而圖像就能夠經過它加入數據集的索引或者序號被檢索到。你也能夠定義其餘的參數,它們也一樣會被加入到字典中去,例如用於定義註解文件的‘annotation’參數。

# 添加到數據集
self.add_image('dataset',  image_id='00001',  path='kangaroo/images/00001.jpg',  annotation='kangaroo/annots/00001.xml')
複製代碼

例如,咱們能夠運行 load_dataset() 函數,並將數據集字典的地址做爲參數傳入,那麼它將會加載全部數據集中的圖像。

注意,測試代表,編號‘00090’的圖像存在一些問題,因此咱們將它從數據集中移除。

# 加載數據集定義
def load_dataset(self, dataset_dir):
	# 定義一個類
	self.add_class("dataset", 1, "kangaroo")
	# 定義數據所在位置
	images_dir = dataset_dir + '/images/'
	annotations_dir = dataset_dir + '/annots/'
	# 定位到全部圖像
	for filename in listdir(images_dir):
		# 提取圖像 id
		image_id = filename[:-4]
		# 略過不合格的圖像
		if image_id in ['00090']:
			continue
		img_path = images_dir + filename
		ann_path = annotations_dir + image_id + '.xml'
		# 添加到數據集
		self.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)
複製代碼

咱們能夠更進一步,爲函數增長一個參數,這個參數用於定義 Dataset 的實例是用於訓練、測試仍是驗證。咱們有大約 160 張圖像,因此咱們可使用其中的大約 20%,或者說最後的 32 張圖像做爲測試集或驗證集,將開頭的 131 張,或者說 80% 的圖像做爲訓練集。

可使用文件名中的數字編號來完成圖像的分類,圖像編號在 150 以前的圖像將會被用於訓練,等於或者大於 150 的將用於測試。更新後的 load_dataset() 函數能夠支持訓練和測試數據集,其代碼以下:

# 加載數據集定義
def load_dataset(self, dataset_dir, is_train=True):
	# 定義一個類
	self.add_class("dataset", 1, "kangaroo")
	# 定義數據所在位置
	images_dir = dataset_dir + '/images/'
	annotations_dir = dataset_dir + '/annots/'
	# 定位到全部圖像
	for filename in listdir(images_dir):
		# 提取圖像 id
		image_id = filename[:-4]
		# 略過不合格的圖像
		if image_id in ['00090']:
			continue
		# 若是咱們正在創建的是訓練集,略過 150 序號以後的全部圖像
		if is_train and int(image_id) >= 150:
			continue
		# 若是咱們正在創建的是測試/驗證集,略過 150 序號以前的全部圖像
		if not is_train and int(image_id) < 150:
			continue
		img_path = images_dir + filename
		ann_path = annotations_dir + image_id + '.xml'
		# 添加到數據集
		self.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)
複製代碼

接下來,咱們須要定義函數 load_mask(),用於爲給定的‘image_id’加載掩膜。

這時‘image_id’是數據集中圖像的整數索引,該索引基於加載數據集時,圖像經過調用函數 add_image() 加入數據集的順序。函數必須返回一個包含一個或者多個與 image_id 關聯的圖像掩膜的數組,以及每一個掩膜的類。

咱們目前尚未 mask,可是咱們有邊框,咱們能夠加載給定圖像的邊框而後將其做爲 mask 返回。接下來庫將會從「掩膜」推斷出邊框信息,由於它們的大小是相同的。

咱們必須首先加載註解文件,獲取到 image_id。獲取的步驟包括,首先獲取包含 image_id 的‘image info’字典,而後經過咱們以前對 add_image() 的調用獲取圖像的加載路徑。接下來咱們就能夠在調用 extract_boxes() 的時候使用該路徑,這個函數是在前一章節中定義的,用於獲取邊框列表和圖像尺寸。

# 獲取圖像詳細信息
info = self.image_info[image_id]
# 定義盒文件位置
path = info['annotation']
# 加載 XML
boxes, w, h = self.extract_boxes(path)
複製代碼

如今咱們能夠爲每一個邊框定義一個掩膜,以及一個相關聯的類。

掩膜是一個和圖像維度同樣的二維數組,數組中不屬於對象的位置值爲 0,反之則值爲 1。

經過爲每一個未知大小的圖像建立一個全 0 的 NumPy 數組,併爲每一個邊框建立一個通道,咱們能夠完成上述的目標:

# 爲全部掩膜建立一個數組,每一個數組都位於不一樣的通道
masks  =  zeros([h,  w,  len(boxes)],  dtype='uint8')
複製代碼

每一個邊框均可以用圖像框的 minmaxxy 座標定義。

這些值能夠直接用於定義數組中值爲 1 的行和列的範圍。

# 建立掩膜
for i in range(len(boxes)):
	box = boxes[i]
	row_s, row_e = box[1], box[3]
	col_s, col_e = box[0], box[2]
	masks[row_s:row_e, col_s:col_e, i] = 1
複製代碼

在這個數據集中,全部的對象都有相同的類。咱們能夠經過‘class_names’字典獲取類的索引,而後將索引和掩膜一併添加到須要返回的列表中。

self.class_names.index('kangaroo')
複製代碼

將這幾步放在一塊兒進行測試,最終完成的 load_mask() 函數以下。

# 加載圖像掩膜
def load_mask(self, image_id):
	# 獲取圖像詳細信息
	info = self.image_info[image_id]
	# 定義盒文件位置
	path = info['annotation']
	# 加載 XML
	boxes, w, h = self.extract_boxes(path)
	# 爲全部掩膜建立一個數組,每一個數組都位於不一樣的通道
	masks = zeros([h, w, len(boxes)], dtype='uint8')
	# 建立掩膜
	class_ids = list()
	for i in range(len(boxes)):
		box = boxes[i]
		row_s, row_e = box[1], box[3]
		col_s, col_e = box[0], box[2]
		masks[row_s:row_e, col_s:col_e, i] = 1
		class_ids.append(self.class_names.index('kangaroo'))
	return masks, asarray(class_ids, dtype='int32')
複製代碼

最後,咱們還必須實現 image_reference() 函數,

這個函數負責返回給定‘image_id’的路徑或者 URL,也就是‘image info’字典的‘path’屬性。

# 加載圖像引用
def image_reference(self, image_id):
	info = self.image_info[image_id]
	return info['path']
複製代碼

好了,這樣就完成了。咱們已經爲袋鼠數據集的 mask-rcnn 庫成功的定義了 Dataset 對象。

包含類與建立訓練數據集和測試數據集的完整列表以下。

# 將數據分爲訓練和測試集
from os import listdir
from xml.etree import ElementTree
from numpy import zeros
from numpy import asarray
from mrcnn.utils import Dataset

# 用於定義和加載袋鼠數據集的類
class KangarooDataset(Dataset):
	# 加載數據集定義
	def load_dataset(self, dataset_dir, is_train=True):
		# 定義一個類
		self.add_class("dataset", 1, "kangaroo")
		# 定義數據所在位置
		images_dir = dataset_dir + '/images/'
		annotations_dir = dataset_dir + '/annots/'
		# 定位到全部圖像
		for filename in listdir(images_dir):
			# 提取圖像 id
			image_id = filename[:-4]
			# 略過不合格的圖像
			if image_id in ['00090']:
				continue
			# 若是咱們正在創建的是訓練集,略過 150 序號以後的全部圖像
			if is_train and int(image_id) >= 150:
				continue
			# 若是咱們正在創建的是測試/驗證集,略過 150 序號以前的全部圖像
			if not is_train and int(image_id) < 150:
				continue
			img_path = images_dir + filename
			ann_path = annotations_dir + image_id + '.xml'
			# 添加到數據集
			self.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)

	# 從註解文件中提取邊框值
	def extract_boxes(self, filename):
		# 加載並解析文件
		tree = ElementTree.parse(filename)
		# 獲取文檔根元素
		root = tree.getroot()
		# 提取出每一個 bounding box 元素
		boxes = list()
		for box in root.findall('.//bndbox'):
			xmin = int(box.find('xmin').text)
			ymin = int(box.find('ymin').text)
			xmax = int(box.find('xmax').text)
			ymax = int(box.find('ymax').text)
			coors = [xmin, ymin, xmax, ymax]
			boxes.append(coors)
		# 提取出圖像尺寸
		width = int(root.find('.//size/width').text)
		height = int(root.find('.//size/height').text)
		return boxes, width, height

	# 加載圖像掩膜
	def load_mask(self, image_id):
		# 獲取圖像詳細信息
		info = self.image_info[image_id]
		# 定義盒文件位置
		path = info['annotation']
		# 加載 XML
		boxes, w, h = self.extract_boxes(path)
		# 爲全部掩膜建立一個數組,每一個數組都位於不一樣的通道
		masks = zeros([h, w, len(boxes)], dtype='uint8')
		# 建立掩膜
		class_ids = list()
		for i in range(len(boxes)):
			box = boxes[i]
			row_s, row_e = box[1], box[3]
			col_s, col_e = box[0], box[2]
			masks[row_s:row_e, col_s:col_e, i] = 1
			class_ids.append(self.class_names.index('kangaroo'))
		return masks, asarray(class_ids, dtype='int32')

	# 加載圖像引用
	def image_reference(self, image_id):
		info = self.image_info[image_id]
		return info['path']

# 訓練集
train_set = KangarooDataset()
train_set.load_dataset('kangaroo', is_train=True)
train_set.prepare()
print('Train: %d' % len(train_set.image_ids))

# 測試/驗證集
test_set = KangarooDataset()
test_set.load_dataset('kangaroo', is_train=False)
test_set.prepare()
print('Test: %d' % len(test_set.image_ids))
複製代碼

正確的運行示例代碼將會加載並準備好訓練和測試集,並打印出每一個集合中圖像的數量。

Train: 131
Test: 32
複製代碼

如今,咱們已經定義好了數據集,咱們還須要確認一下是否對圖像、掩膜以及邊框進行了正確的處理。

測試袋鼠數據集對象

第一個有用的測試是,確認圖像和掩膜是否可以正確的加載。

建立一個數據集,以 image_id 爲參數調用 load_image() 函數加載圖像,而後以同一個 image_id 爲參數調用 load_mask() 函數加載掩膜,經過這樣的步驟,咱們能夠完成測試。

# 加載圖像
image_id = 0
image = train_set.load_image(image_id)
print(image.shape)
# 加載圖像掩膜
mask, class_ids = train_set.load_mask(image_id)
print(mask.shape)
複製代碼

接下來,咱們可使用 Matplotlib 提供的 API 繪製出圖像,而後使用 alpha 值繪製出頂部的第一個掩膜,這樣下面的圖像依舊能夠看到。

# 繪製圖像
pyplot.imshow(image)
# 繪製掩膜
pyplot.imshow(mask[:, :, 0], cmap='gray', alpha=0.5)
pyplot.show()
複製代碼

完整的代碼示例以下。

# 繪製一幅圖像及掩膜
from os import listdir
from xml.etree import ElementTree
from numpy import zeros
from numpy import asarray
from mrcnn.utils import Dataset
from matplotlib import pyplot

# 定義並加載袋鼠數據集的類
class KangarooDataset(Dataset):
	# 加載數據集定義
	def load_dataset(self, dataset_dir, is_train=True):
		# 定義一個類
		self.add_class("dataset", 1, "kangaroo")
		# 定義數據所在位置
		images_dir = dataset_dir + '/images/'
		annotations_dir = dataset_dir + '/annots/'
		# 定位到全部圖像
		for filename in listdir(images_dir):
			# 提取圖像 id
			image_id = filename[:-4]
			# 略過不合格的圖像
			if image_id in ['00090']:
				continue
			# 若是咱們正在創建的是訓練集,略過 150 序號以後的全部圖像
			if is_train and int(image_id) >= 150:
				continue
			# 若是咱們正在創建的是測試/驗證集,略過 150 序號以前的全部圖像
			if not is_train and int(image_id) < 150:
				continue
			img_path = images_dir + filename
			ann_path = annotations_dir + image_id + '.xml'
			# 添加到數據集
			self.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)

	# 從註解文件中提取邊框值
	def extract_boxes(self, filename):
		# 加載並解析文件
		tree = ElementTree.parse(filename)
		# 獲取文檔根元素
		root = tree.getroot()
		# 提取出每一個 bounding box 元素
		boxes = list()
		for box in root.findall('.//bndbox'):
			xmin = int(box.find('xmin').text)
			ymin = int(box.find('ymin').text)
			xmax = int(box.find('xmax').text)
			ymax = int(box.find('ymax').text)
			coors = [xmin, ymin, xmax, ymax]
			boxes.append(coors)
		# 提取出圖像尺寸
		width = int(root.find('.//size/width').text)
		height = int(root.find('.//size/height').text)
		return boxes, width, height

	# 加載圖像掩膜
	def load_mask(self, image_id):
		# 獲取圖像詳細信息
		info = self.image_info[image_id]
		# 定義盒文件位置
		path = info['annotation']
		# 加載 XML
		boxes, w, h = self.extract_boxes(path)
		# 爲全部掩膜建立一個數組,每一個數組都位於不一樣的通道
		masks = zeros([h, w, len(boxes)], dtype='uint8')
		# 建立掩膜
		class_ids = list()
		for i in range(len(boxes)):
			box = boxes[i]
			row_s, row_e = box[1], box[3]
			col_s, col_e = box[0], box[2]
			masks[row_s:row_e, col_s:col_e, i] = 1
			class_ids.append(self.class_names.index('kangaroo'))
		return masks, asarray(class_ids, dtype='int32')

	# 加載圖像引用
	def image_reference(self, image_id):
		info = self.image_info[image_id]
		return info['path']

# 訓練集
train_set = KangarooDataset()
train_set.load_dataset('kangaroo', is_train=True)
train_set.prepare()
# 加載圖像
image_id = 0
image = train_set.load_image(image_id)
print(image.shape)
# 加載圖像掩膜
mask, class_ids = train_set.load_mask(image_id)
print(mask.shape)
# 繪製圖像
pyplot.imshow(image)
# 繪製掩膜
pyplot.imshow(mask[:, :, 0], cmap='gray', alpha=0.5)
pyplot.show()
複製代碼

運行示例代碼,首先將會打印出圖像尺寸以及掩膜的 NumPy 數組。

咱們能夠肯定這兩個具備一樣的長度和寬度,僅在通道的數量上不一樣。咱們也能夠看到在此場景下,第一張圖像(也就是 image_id = 0 的圖像)僅有一個掩膜。

(626, 899, 3)
(626, 899, 1)
複製代碼

圖像的繪製圖會在第一個掩膜重疊的狀況下一塊兒被建立出來。

這時,咱們就能夠看到圖像中出現了一隻帶有掩膜覆蓋其邊界的袋鼠。

Photograph of Kangaroo With Object Detection Mask Overlaid

帶有目標檢測掩膜覆蓋的袋鼠圖像

咱們能夠對數據集中的前 9 張圖像作相同的操做,將每一張圖像做爲總體圖的子圖繪製出來,而後繪製出每一張圖像的全部掩膜。

# 繪製最開始的幾張圖像
for i in range(9):
	# 定義子圖
	pyplot.subplot(330 + 1 + i)
	# 繪製原始像素數據
	image = train_set.load_image(i)
	pyplot.imshow(image)
	# 繪製全部掩膜
	mask, _ = train_set.load_mask(i)
	for j in range(mask.shape[2]):
		pyplot.imshow(mask[:, :, j], cmap='gray', alpha=0.3)
# 展現繪製結果
pyplot.show()
複製代碼

運行示例代碼咱們能夠看到,圖像被正確的加載了,同時這些包含多個目標的圖像也被正肯定義了各自的掩膜。

Plot of First Nine Photos of Kangaroos in the Training Dataset With Object Detection Masks

繪製訓練集中的前 9 幅帶有目標檢測掩膜的袋鼠圖像

另外一個頗有用的調試步驟是加載數據集中全部的‘image info’對象,並將它們在控制檯輸出。

這能夠幫助咱們確認,全部在 load_dataset() 函數中對 add_image() 函數的調用都按照預期運做。

# 枚舉出數據集中全部的圖像
for image_id in train_set.image_ids:
	# 加載圖像信息
	info = train_set.image_info[image_id]
	# 在控制檯展現
	print(info)
複製代碼

在加載的訓練集上運行此代碼將會展現出全部的‘image info’字典,字典中包含數據集中每張圖像的路徑和 id。

{'id': '00132', 'source': 'dataset', 'path': 'kangaroo/images/00132.jpg', 'annotation': 'kangaroo/annots/00132.xml'}
{'id': '00046', 'source': 'dataset', 'path': 'kangaroo/images/00046.jpg', 'annotation': 'kangaroo/annots/00046.xml'}
{'id': '00052', 'source': 'dataset', 'path': 'kangaroo/images/00052.jpg', 'annotation': 'kangaroo/annots/00052.xml'}
...
複製代碼

最後,mask-rcnn 庫提供了顯示圖像和掩膜的工具。咱們可使用一些內建的方法來確認數據集運做正常。

例如,mask-rcnn 提供的 mrcnn.visualize.display_instances() 函數,能夠用於顯示包含邊框、掩膜以及類標籤的圖像。可是須要邊框已經經過 extract_bboxes() 方法從掩膜中提取出來。

# 定義圖像 id
image_id = 1
# 加載圖像
image = train_set.load_image(image_id)
# 加載掩膜和類 id
mask, class_ids = train_set.load_mask(image_id)
# 從掩膜中提取邊框
bbox = extract_bboxes(mask)
# 顯示帶有掩膜和邊框的圖像
display_instances(image, bbox, mask, class_ids, train_set.class_names)
複製代碼

爲了讓你對整個流程有完成的認識,全部代碼都在下面列出。

# 顯示帶有掩膜和邊框的圖像
from os import listdir
from xml.etree import ElementTree
from numpy import zeros
from numpy import asarray
from mrcnn.utils import Dataset
from mrcnn.visualize import display_instances
from mrcnn.utils import extract_bboxes

# 定義並加載袋鼠數據集的類
class KangarooDataset(Dataset):
	# 加載數據集定義
	def load_dataset(self, dataset_dir, is_train=True):
		# 定義一個類
		self.add_class("dataset", 1, "kangaroo")
		# 定義數據所在位置
		images_dir = dataset_dir + '/images/'
		annotations_dir = dataset_dir + '/annots/'
		# 定位到全部圖像
		for filename in listdir(images_dir):
			# 提取圖像 id
			image_id = filename[:-4]
			# 略過不合格的圖像
			if image_id in ['00090']:
				continue
			# 若是咱們正在創建的是訓練集,略過 150 序號以後的全部圖像
			if is_train and int(image_id) >= 150:
				continue
			# 若是咱們正在創建的是測試/驗證集,略過 150 序號以前的全部圖像
			if not is_train and int(image_id) < 150:
				continue
			img_path = images_dir + filename
			ann_path = annotations_dir + image_id + '.xml'
			# 添加到數據集
			self.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)

	# 從註解文件中提取邊框值
	def extract_boxes(self, filename):
		# 加載並解析文件
		tree = ElementTree.parse(filename)
		# 獲取文檔根元素
		root = tree.getroot()
		# 提取出每一個 bounding box 元素
		boxes = list()
		for box in root.findall('.//bndbox'):
			xmin = int(box.find('xmin').text)
			ymin = int(box.find('ymin').text)
			xmax = int(box.find('xmax').text)
			ymax = int(box.find('ymax').text)
			coors = [xmin, ymin, xmax, ymax]
			boxes.append(coors)
		# 提取出圖像尺寸
		width = int(root.find('.//size/width').text)
		height = int(root.find('.//size/height').text)
		return boxes, width, height

	# 加載圖像掩膜
	def load_mask(self, image_id):
		# 獲取圖像詳細信息
		info = self.image_info[image_id]
		# 定義盒文件位置
		path = info['annotation']
		# 加載 XML
		boxes, w, h = self.extract_boxes(path)
		# 爲全部掩膜建立一個數組,每一個數組都位於不一樣的通道
		masks = zeros([h, w, len(boxes)], dtype='uint8')
		# 建立掩膜
		class_ids = list()
		for i in range(len(boxes)):
			box = boxes[i]
			row_s, row_e = box[1], box[3]
			col_s, col_e = box[0], box[2]
			masks[row_s:row_e, col_s:col_e, i] = 1
			class_ids.append(self.class_names.index('kangaroo'))
		return masks, asarray(class_ids, dtype='int32')

	# 加載圖像引用
	def image_reference(self, image_id):
		info = self.image_info[image_id]
		return info['path']

# 訓練集
train_set = KangarooDataset()
train_set.load_dataset('kangaroo', is_train=True)
train_set.prepare()
# 定義圖像 id
image_id = 1
# 加載圖像
image = train_set.load_image(image_id)
# 加載掩膜和類 id
mask, class_ids = train_set.load_mask(image_id)
# 從掩膜中提取邊框
bbox = extract_bboxes(mask)
# 顯示帶有掩膜和邊框的圖像
display_instances(image, bbox, mask, class_ids, train_set.class_names)
複製代碼

運行這段示例代碼,將會建立出用不一樣的顏色標記每一個目標掩膜的圖像。

從程序設計開始,邊框和掩膜就是能夠相互精確匹配的,在圖像中它們用虛線外邊框標記出來。最後,每一個對象也會被類標籤標記,在這個例子中就是‘kangaroo’類。

Photograph Showing Object Detection Masks, Bounding Boxes, and Class Labels

展現目標檢測掩膜、邊框和類標籤的圖像

如今,咱們很是確認數據集可以被正確加載,咱們可使用它來擬合 Mask R-CNN 模型了。

如何訓練檢測袋鼠的 Mask R-CNN 模型

Mask R-CNN 模型能夠從零開始擬合,可是和其餘計算機視覺應用同樣,經過使用遷移學習的方法能夠節省時間並提高性能。

Mask R-CNN model 在 MS COCO 目標檢測的預先擬合能夠用做初始模型,而後對於特定的數據集再作適配,在本例中也就是袋鼠數據集。

第一步須要先爲預先擬合的 Mask R-CNN 模型下載模型文件(包括結構和權重信息)。權重信息能夠在 Github 項目中下載,文件大約 250 MB。

將模型權重加載到工做目錄內的文件‘mask_rcnn_coco.h5’中。

接下來,必需要爲模型定義一個配置對象。

這個新的類繼承了 mrcnn.config.Config 類,定義了須要預測的內容(例如類的名字和數量)和訓練模型的算法(例如學習速率)。

配置對象必須經過‘NAME’屬性定義配置名,例如‘kangaroo_cfg’,在項目運行時,它將用於保存詳細信息和模型到文件中。配置對象也必須經過‘NUM_CLASSES’屬性定義預測問題中類的數量。在這個例子中,儘管背景中有不少其餘的類,但咱們只有一個識別目標,那就是袋鼠。

最後咱們還要定義每輪訓練中使用的樣本(圖像)數量。這也就是訓練集中圖像的數量,即 131。

將這些內容組合在一塊兒,咱們自定義的 KangarooConfig 類的定義以下。

# 定義模型配置
class KangarooConfig(Config):
	# 給配置對象命名
	NAME = "kangaroo_cfg"
	# 類的數量(背景中的 + 袋鼠)
	NUM_CLASSES = 1 + 1
	# 每輪訓練的迭代數量
	STEPS_PER_EPOCH = 131

# 準備好配置信息
config = KangarooConfig()
複製代碼

下面,咱們能夠定義模型了。

經過建立類 mrcnn.model.MaskRCNN 的實例咱們能夠建立模型,經過將‘mode’屬性設置爲‘training’,特定的模型將能夠用於訓練。

必須將‘config_’參數賦值爲咱們的 KangarooConfig 類。

最後,須要一個目錄來存儲配置文件以及每輪訓練結束後的模型檢查點。咱們就使用當前的工做目錄吧。

# 定義模型
model  =  MaskRCNN(mode='training',  model_dir='./',  config=config)
複製代碼

接下來,須要加載預約義模型的結構和權重。經過在模型上調用 load_weights() 函數便可,同時要記得指定保存了下載數據的‘mask_rcnn_coco.h5’文件的地址。

模型將按照原樣使用,可是指定了類的輸出層將會被移除,這樣新的輸出層才能夠被定義和訓練。這要經過指定‘exclude’參數,並在模型加載後列出全部須要從模型移除的輸出層來完成。這包括分類標籤、邊框和掩膜的輸出層。

# 加載 mscoco 權重信息
model.load_weights('mask_rcnn_coco.h5', by_name=True, exclude=["mrcnn_class_logits", "mrcnn_bbox_fc",  "mrcnn_bbox", "mrcnn_mask"])
複製代碼

下面,經過調用 train() 函數並將訓練集和驗證集做爲參數傳遞進去,模型將開始在訓練集上進行擬合。咱們也能夠指定學習速率,配置默認的學習速率是 0.001。

咱們還能夠指定訓練哪一個層。在本文的例子中,咱們只訓練頭部,也就是模型的輸出層。

# 訓練權重(輸出層,或者說‘頭部’)
model.train(train_set, test_set, learning_rate=config.LEARNING_RATE, epochs=5, layers='heads')
複製代碼

咱們能夠在後續的訓練中重複這樣的訓練步驟,微調模型中的權重。經過使用更小的學習速率並將‘layer’參數從‘heads’修改成‘all’便可實現。

完整的在袋鼠數據集訓練 Mask R-CNN 模型的代碼以下。

就算將代碼在性能不錯的硬件上運行,也可能須要花費一些時間。因此我建議在 GPU 上運行它,例如 Amazon EC2,在 P3 類型的硬件上,代碼在五分鐘內便可運行完成。

# 在袋鼠數據集上擬合 mask rcnn 模型
from os import listdir
from xml.etree import ElementTree
from numpy import zeros
from numpy import asarray
from mrcnn.utils import Dataset
from mrcnn.config import Config
from mrcnn.model import MaskRCNN

# 定義並加載袋鼠數據集的類
class KangarooDataset(Dataset):
	# 加載數據集定義
	def load_dataset(self, dataset_dir, is_train=True):
		# 定義一個類
		self.add_class("dataset", 1, "kangaroo")
		# 定義數據所在位置
		images_dir = dataset_dir + '/images/'
		annotations_dir = dataset_dir + '/annots/'
		# 定位到全部圖像
		for filename in listdir(images_dir):
			# 提取圖像 id
			image_id = filename[:-4]
			# 略過不合格的圖像
			if image_id in ['00090']:
				continue
			# 若是咱們正在創建的是訓練集,略過 150 序號以後的全部圖像
			if is_train and int(image_id) >= 150:
				continue
			# 若是咱們正在創建的是測試/驗證集,略過 150 序號以前的全部圖像
			if not is_train and int(image_id) < 150:
				continue
			img_path = images_dir + filename
			ann_path = annotations_dir + image_id + '.xml'
			# 添加到數據集
			self.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)

	# 從註解文件中提取邊框值
	def extract_boxes(self, filename):
		# 加載並解析文件
		tree = ElementTree.parse(filename)
		# 獲取文檔根元素
		root = tree.getroot()
		# 提取出每一個 bounding box 元素
		boxes = list()
		for box in root.findall('.//bndbox'):
			xmin = int(box.find('xmin').text)
			ymin = int(box.find('ymin').text)
			xmax = int(box.find('xmax').text)
			ymax = int(box.find('ymax').text)
			coors = [xmin, ymin, xmax, ymax]
			boxes.append(coors)
		# 提取出圖像尺寸
		width = int(root.find('.//size/width').text)
		height = int(root.find('.//size/height').text)
		return boxes, width, height

	# 加載圖像掩膜
	def load_mask(self, image_id):
		# 獲取圖像詳細信息
		info = self.image_info[image_id]
		# 定義盒文件位置
		path = info['annotation']
		# 加載 XML
		boxes, w, h = self.extract_boxes(path)
		# 爲全部掩膜建立一個數組,每一個數組都位於不一樣的通道
		masks = zeros([h, w, len(boxes)], dtype='uint8')
		# 建立掩膜
		class_ids = list()
		for i in range(len(boxes)):
			box = boxes[i]
			row_s, row_e = box[1], box[3]
			col_s, col_e = box[0], box[2]
			masks[row_s:row_e, col_s:col_e, i] = 1
			class_ids.append(self.class_names.index('kangaroo'))
		return masks, asarray(class_ids, dtype='int32')

	# 加載圖像引用
	def image_reference(self, image_id):
		info = self.image_info[image_id]
		return info['path']

# 定義模型配置
class KangarooConfig(Config):
	# 定義配置名
	NAME = "kangaroo_cfg"
	# 類的數量(背景中的 + 袋鼠)
	NUM_CLASSES = 1 + 1
	# 每輪訓練的迭代數量
	STEPS_PER_EPOCH = 131

# 準備訓練集
train_set = KangarooDataset()
train_set.load_dataset('kangaroo', is_train=True)
train_set.prepare()
print('Train: %d' % len(train_set.image_ids))
# 準備測試/驗證集
test_set = KangarooDataset()
test_set.load_dataset('kangaroo', is_train=False)
test_set.prepare()
print('Test: %d' % len(test_set.image_ids))
# 準備配置信息
config = KangarooConfig()
config.display()
# 定義模型
model = MaskRCNN(mode='training', model_dir='./', config=config)
# 加載 mscoco 權重信息,排除輸出層
model.load_weights('mask_rcnn_coco.h5', by_name=True, exclude=["mrcnn_class_logits", "mrcnn_bbox_fc",  "mrcnn_bbox", "mrcnn_mask"])
# 訓練權重(輸出層,或者說‘頭部’)
model.train(train_set, test_set, learning_rate=config.LEARNING_RATE, epochs=5, layers='heads')
複製代碼

運行示例代碼將會使用標準 Keras 進度條報告運行進度。

咱們能夠發現,每一個網絡的輸出頭部,都報告了不一樣的訓練和測試的損失分數。注意到這些損失分數,會讓人以爲很困惑。

在本文的例子中,咱們感興趣的是目標識別而不是目標分割,因此我建議應該注意訓練集和驗證集分類輸出的損失(例如 mrcnn_class_lossval_mrcnn_class_loss),還有訓練和驗證集的邊框輸出(mrcnn_bbox_lossval_mrcnn_bbox_loss)。

Epoch 1/5
131/131 [==============================] - 106s 811ms/step - loss: 0.8491 - rpn_class_loss: 0.0044 - rpn_bbox_loss: 0.1452 - mrcnn_class_loss: 0.0420 - mrcnn_bbox_loss: 0.2874 - mrcnn_mask_loss: 0.3701 - val_loss: 1.3402 - val_rpn_class_loss: 0.0160 - val_rpn_bbox_loss: 0.7913 - val_mrcnn_class_loss: 0.0092 - val_mrcnn_bbox_loss: 0.2263 - val_mrcnn_mask_loss: 0.2975
Epoch 2/5
131/131 [==============================] - 69s 526ms/step - loss: 0.4774 - rpn_class_loss: 0.0025 - rpn_bbox_loss: 0.1159 - mrcnn_class_loss: 0.0170 - mrcnn_bbox_loss: 0.1134 - mrcnn_mask_loss: 0.2285 - val_loss: 0.6261 - val_rpn_class_loss: 8.9502e-04 - val_rpn_bbox_loss: 0.1624 - val_mrcnn_class_loss: 0.0197 - val_mrcnn_bbox_loss: 0.2148 - val_mrcnn_mask_loss: 0.2282
Epoch 3/5
131/131 [==============================] - 67s 515ms/step - loss: 0.4471 - rpn_class_loss: 0.0029 - rpn_bbox_loss: 0.1153 - mrcnn_class_loss: 0.0234 - mrcnn_bbox_loss: 0.0958 - mrcnn_mask_loss: 0.2097 - val_loss: 1.2998 - val_rpn_class_loss: 0.0144 - val_rpn_bbox_loss: 0.6712 - val_mrcnn_class_loss: 0.0372 - val_mrcnn_bbox_loss: 0.2645 - val_mrcnn_mask_loss: 0.3125
Epoch 4/5
131/131 [==============================] - 66s 502ms/step - loss: 0.3934 - rpn_class_loss: 0.0026 - rpn_bbox_loss: 0.1003 - mrcnn_class_loss: 0.0171 - mrcnn_bbox_loss: 0.0806 - mrcnn_mask_loss: 0.1928 - val_loss: 0.6709 - val_rpn_class_loss: 0.0016 - val_rpn_bbox_loss: 0.2012 - val_mrcnn_class_loss: 0.0244 - val_mrcnn_bbox_loss: 0.1942 - val_mrcnn_mask_loss: 0.2495
Epoch 5/5
131/131 [==============================] - 65s 493ms/step - loss: 0.3357 - rpn_class_loss: 0.0024 - rpn_bbox_loss: 0.0804 - mrcnn_class_loss: 0.0193 - mrcnn_bbox_loss: 0.0616 - mrcnn_mask_loss: 0.1721 - val_loss: 0.8878 - val_rpn_class_loss: 0.0030 - val_rpn_bbox_loss: 0.4409 - val_mrcnn_class_loss: 0.0174 - val_mrcnn_bbox_loss: 0.1752 - val_mrcnn_mask_loss: 0.2513
複製代碼

每輪訓練結束後會建立並保存一個模型文件於子目錄中,文件名以‘kangaroo_cfg’開始,後面是隨機的字符。

使用的時候,咱們必需要選擇一個模型;在本文的例子中,每輪訓練都會讓邊框選擇的損失遞減,因此咱們將使用最終的模型,它是在運行‘mask_rcnn_kangaroo_cfg_0005.h5’後生成的。

將模型文件從配置目錄拷貝到當前的工做目錄。咱們將會在接下來的章節中使用它進行模型的評估,並對未知圖片做出預測。

結果顯示,也許更多的訓練次數可以讓模型性能更好,或許能夠微調模型中全部層的參數;這個思路也許能夠是本文一個有趣的擴展。

下面讓咱們一塊兒來看看這個模型的性能評估。

如何評估 Mask R-CNN 模型

目標識別目標的模型的性能一般使用平均絕對精度來衡量,即 mAP。

咱們要預測的是邊框位置,因此咱們能夠用預測邊框與實際邊框的重疊程度來決定預測是否準確。經過將邊框重疊的區域除以兩個邊框的總面積能夠用來計算準確度,或者說是交叉面積除以總面積,又稱爲「intersection over union,」 或者 IoU。最完美的邊框預測的 IoU 值應該爲 1。

一般狀況下,若是 IoU 的值大於 0.5,咱們就能夠認爲邊框預測的結果良好,也就是,重疊部分佔總面積的 50% 以上。

準確率指的是正確預測的邊框(即 IoU > 0.5 的邊框)佔總邊框的百分比。召回率指的是正確預測的邊框(即 IoU > 0.5 的邊框)佔全部圖片中對象的百分比。

隨着咱們做出更屢次的預測,召回率將會升高,可是準確率可能會因爲咱們開始過擬合而降低或者波動。能夠根據準確率(y)繪製召回率(x),每一個精確度的值均可以繪製出一條曲線或直線。咱們能夠最大化曲線上的每一個點的值,並計算準確率的平均值,或者每一個召回率的 AP。

注意:AP 如何計算有不少種方法,例如,普遍使用的 PASCAL VOC 數據集和 MS COCO 數據集計算的方法就是不一樣的。

數據集中全部圖片的平均準確度的平均值(AP)被稱爲平均絕對精度,即 mAP。

mask-rcnn 庫提供了函數 mrcnn.utils.compute_ap,用於計算 AP 以及給定圖片的其餘指標。數據集中全部的 AP 值能夠被集合在一塊兒,而且計算均值可讓咱們瞭解模型在數據集中檢測目標的準確度如何。

首先咱們必須定義一個 Config 對象,它將用於做出預測,而不是用於訓練。咱們能夠擴展以前定義的 KangarooConfig 來複用一些參數。咱們將定義一個新的屬性值都相等的對象來讓代碼保持簡潔。配置必須修改一些使用 GPU 進行預測時的默認值,這和在訓練模型的時候的配置是不一樣的(那時候不用管你是在 GPU 或者 CPU 上運行代碼的)。

# 定義預測配置
class PredictionConfig(Config):
	# 定義配置名
	NAME = "kangaroo_cfg"
	# 類的數量(背景中的 + 袋鼠)
	NUM_CLASSES = 1 + 1
	# 簡化 GPU 配置
	GPU_COUNT = 1
	IMAGES_PER_GPU = 1
複製代碼

接下來咱們就可使用配置定義模型了,而且要將參數‘mode’從‘training’改成‘inference’。

# 建立配置
cfg = PredictionConfig()
# 定義模型
model = MaskRCNN(mode='inference', model_dir='./', config=cfg)
複製代碼

下面,咱們能夠從保存的模型中加載權重。

經過指定模型文件的路徑便可完成這一步。在本文的例子中,模型文件就是當前工做目錄下的‘mask_rcnn_kangaroo_cfg_0005.h5’。

# 加載模型權重
model.load_weights('mask_rcnn_kangaroo_cfg_0005.h5',  by_name=True)
複製代碼

接下來,咱們能夠評估模型了。這包括列舉出數據集中的圖片,做出預測,而後在預測全部圖片的平均 AP 以前計算用於預測的 AP 值。

第一步,根據指定的 image_id 從數據集中加載出圖像和真實掩膜。經過使用 load_image_gt() 這個便捷的函數便可完成這一步。

# 加載指定 image id 的圖像、邊框和掩膜
image, image_meta, gt_class_id, gt_bbox, gt_mask = load_image_gt(dataset, cfg, image_id, use_mini_mask=False)
複製代碼

接下來,必須按照與訓練數據相同的方式縮放已加載圖像的像素值,例如居中。經過使用 mold_image() 便捷函便可完成這一步。

# 轉換像素值(例如居中)
scaled_image  =  mold_image(image,  cfg)
複製代碼

而後,圖像的維度須要在數據集中擴展爲一個樣本,它將做爲模型預測的輸入。

sample = expand_dims(scaled_image, 0)
# 做出預測
yhat = model.detect(sample, verbose=0)
# 爲第一個樣本提取結果
r = yhat[0]
複製代碼

接下來,預測值能夠和真實值做出比對,並使用 compute_ap() 函數計算指標。

# 統計計算,包括計算 AP
AP, _, _, _ = compute_ap(gt_bbox, gt_class_id, gt_mask, r["rois"], r["class_ids"], r["scores"], r['masks'])
複製代碼

AP 值將會被加入到一個列表中去,而後計算平均值。

將上面這些組合在一塊兒,下面的 evaluate_model() 函數就是整個過程的實現,並在給定數據集、模型和配置的前提下計算出了 mAP。

# 計算給定數據集中模型的 mAP
def evaluate_model(dataset, model, cfg):
	APs = list()
	for image_id in dataset.image_ids:
		# 加載指定 image id 的圖像、邊框和掩膜
		image, image_meta, gt_class_id, gt_bbox, gt_mask = load_image_gt(dataset, cfg, image_id, use_mini_mask=False)
		# 轉換像素值(例如居中)
		scaled_image = mold_image(image, cfg)
		# 將圖像轉換爲樣本
		sample = expand_dims(scaled_image, 0)
		# 做出預測
		yhat = model.detect(sample, verbose=0)
		# 爲第一個樣本提取結果
		r = yhat[0]
		# 統計計算,包括計算 AP
		AP, _, _, _ = compute_ap(gt_bbox, gt_class_id, gt_mask, r["rois"], r["class_ids"], r["scores"], r['masks'])
		# 保存
		APs.append(AP)
	# 計算全部圖片的平均 AP
	mAP = mean(APs)
	return mAP
複製代碼

如今咱們能夠計算訓練集和數據集上模型的 mAP。

# 評估訓練集上的模型
train_mAP = evaluate_model(train_set, model, cfg)
print("Train mAP: %.3f" % train_mAP)
# 評估測試集上的模型
test_mAP = evaluate_model(test_set, model, cfg)
print("Test mAP: %.3f" % test_mAP)
複製代碼

完整的代碼以下。

# 評估袋鼠數據集上的 mask rcnn 模型
from os import listdir
from xml.etree import ElementTree
from numpy import zeros
from numpy import asarray
from numpy import expand_dims
from numpy import mean
from mrcnn.config import Config
from mrcnn.model import MaskRCNN
from mrcnn.utils import Dataset
from mrcnn.utils import compute_ap
from mrcnn.model import load_image_gt
from mrcnn.model import mold_image

# 定義並加載袋鼠數據集的類
class KangarooDataset(Dataset):
	# 加載數據集定義
	def load_dataset(self, dataset_dir, is_train=True):
		# 定義一個類
		self.add_class("dataset", 1, "kangaroo")
		# 定義數據所在位置
		images_dir = dataset_dir + '/images/'
		annotations_dir = dataset_dir + '/annots/'
		# 定位到全部圖像
		for filename in listdir(images_dir):
			# 提取圖像 id
			image_id = filename[:-4]
			# 略過不合格的圖像
			if image_id in ['00090']:
				continue
			# 若是咱們正在創建的是訓練集,略過 150 序號以後的全部圖像
			if is_train and int(image_id) >= 150:
				continue
			# 若是咱們正在創建的是測試/驗證集,略過 150 序號以前的全部圖像
			if not is_train and int(image_id) < 150:
				continue
			img_path = images_dir + filename
			ann_path = annotations_dir + image_id + '.xml'
			# 添加到數據集
			self.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)

	# 從註解文件中提取邊框值
	def extract_boxes(self, filename):
		# 加載並解析文件
		tree = ElementTree.parse(filename)
		# 獲取文檔根元素
		root = tree.getroot()
		# 提取出每一個 bounding box 元素
		boxes = list()
		for box in root.findall('.//bndbox'):
			xmin = int(box.find('xmin').text)
			ymin = int(box.find('ymin').text)
			xmax = int(box.find('xmax').text)
			ymax = int(box.find('ymax').text)
			coors = [xmin, ymin, xmax, ymax]
			boxes.append(coors)
		# 提取出圖像尺寸
		width = int(root.find('.//size/width').text)
		height = int(root.find('.//size/height').text)
		return boxes, width, height

	# 加載圖像掩膜
	def load_mask(self, image_id):
		# 獲取圖像詳細信息
		info = self.image_info[image_id]
		# 定義盒文件位置
		path = info['annotation']
		# 加載 XML
		boxes, w, h = self.extract_boxes(path)
		# 爲全部掩膜建立一個數組,每一個數組都位於不一樣的通道
		masks = zeros([h, w, len(boxes)], dtype='uint8')
		# 建立掩膜
		class_ids = list()
		for i in range(len(boxes)):
			box = boxes[i]
			row_s, row_e = box[1], box[3]
			col_s, col_e = box[0], box[2]
			masks[row_s:row_e, col_s:col_e, i] = 1
			class_ids.append(self.class_names.index('kangaroo'))
		return masks, asarray(class_ids, dtype='int32')

	# 加載圖像引用
	def image_reference(self, image_id):
		info = self.image_info[image_id]
		return info['path']

# 定義預測配置
class PredictionConfig(Config):
	# 定義配置名
	NAME = "kangaroo_cfg"
	# 類的數量(背景中的 + 袋鼠)
	NUM_CLASSES = 1 + 1
	# 簡化 GPU 配置
	GPU_COUNT = 1
	IMAGES_PER_GPU = 1

# 計算給定數據集中模型的 mAP
def evaluate_model(dataset, model, cfg):
	APs = list()
	for image_id in dataset.image_ids:
		# 加載指定 image id 的圖像、邊框和掩膜
		image, image_meta, gt_class_id, gt_bbox, gt_mask = load_image_gt(dataset, cfg, image_id, use_mini_mask=False)
		# 轉換像素值(例如居中)
		scaled_image = mold_image(image, cfg)
		# 將圖像轉換爲樣本
		sample = expand_dims(scaled_image, 0)
		# 做出預測
		yhat = model.detect(sample, verbose=0)
		# 爲第一個樣本提取結果
		r = yhat[0]
		# 統計計算,包括計算 AP
		AP, _, _, _ = compute_ap(gt_bbox, gt_class_id, gt_mask, r["rois"], r["class_ids"], r["scores"], r['masks'])
		# 保存
		APs.append(AP)
	# 計算全部圖片的平均 AP
	mAP = mean(APs)
	return mAP

# 加載訓練集
train_set = KangarooDataset()
train_set.load_dataset('kangaroo', is_train=True)
train_set.prepare()
print('Train: %d' % len(train_set.image_ids))
# 加載測試集
test_set = KangarooDataset()
test_set.load_dataset('kangaroo', is_train=False)
test_set.prepare()
print('Test: %d' % len(test_set.image_ids))
# 建立配置
cfg = PredictionConfig()
# 定義模型
model = MaskRCNN(mode='inference', model_dir='./', config=cfg)
# 加載模型權重
model.load_weights('mask_rcnn_kangaroo_cfg_0005.h5', by_name=True)
# 評估訓練集上的模型
train_mAP = evaluate_model(train_set, model, cfg)
print("Train mAP: %.3f" % train_mAP)
# 評估測試集上的模型
test_mAP = evaluate_model(test_set, model, cfg)
print("Test mAP: %.3f" % test_mAP)
複製代碼

運行示例代碼將會爲訓練集和測試集中的每張圖片做出預測,並計算每次預測的 mAP。

90% 或者 95% 以上的 mAP 就是一個不錯的分數了。咱們能夠看到,在兩個數據集上 mAP 分數都不錯,而且在測試集而不是訓練集上可能還要更好一些。

這多是由於測試集比較小,或者是由於模型在進一步訓練中變得更加準確了。

Train mAP: 0.929
Test mAP: 0.958
複製代碼

如今咱們確信模型是合理的,咱們可使用它做出預測了。

如何在新照片中檢測袋鼠

咱們能夠在新的圖像,特別是那些指望有袋鼠的圖像中使用訓練過的模型來檢測袋鼠。

首先,咱們須要一張新的袋鼠圖像

咱們能夠到 Flickr 上隨機的選取一張有袋鼠的圖像。或者也可使用測試集中沒有用來訓練模型的圖像。

在前幾個章節中,咱們已經知道如何對圖像做出預測。具體來講,須要縮放圖像的像素值,而後調用 model.detect() 函數。例如:

# 作預測的例子
...
# 加載圖像
image = ...
# 轉換像素值(例如居中)
scaled_image = mold_image(image, cfg)
# 將圖像轉換爲樣本
sample = expand_dims(scaled_image, 0)
# 做出預測
yhat = model.detect(sample, verbose=0)
...
複製代碼

咱們來更進一步,對數據集中多張圖像做出預測,而後將帶有實際邊框和預測邊框的圖像依次繪製出來。這樣咱們就能直接看出模型預測的準確性如何。

第一步,從數據集中加載圖像和掩膜。

# 加載圖像和掩膜
image = dataset.load_image(image_id)
mask, _ = dataset.load_mask(image_id)
複製代碼

下一步,咱們就能夠對圖像做出預測了。

# 轉換像素值(例如居中)
scaled_image = mold_image(image, cfg)
# 將圖像轉換爲樣本
sample = expand_dims(scaled_image, 0)
# 做出預測
yhat = model.detect(sample, verbose=0)[0]
複製代碼

接下來,咱們能夠爲包含真實邊框位置的圖像建立一個子圖,並將其繪製出來。

# 定義子圖
pyplot.subplot(n_images, 2, i*2+1)
# 繪製原始像素數據
pyplot.imshow(image)
pyplot.title('Actual')
# 繪製掩膜
for j in range(mask.shape[2]):
	pyplot.imshow(mask[:, :, j], cmap='gray', alpha=0.3)
複製代碼

接下來咱們能夠在第一個子圖旁邊建立第二個子圖,並繪製第一幅圖,這一次要將帶有預測邊框位置的圖像繪製出來。

# 獲取繪圖框的上下文
pyplot.subplot(n_images, 2, i*2+2)
# 繪製原始像素數據
pyplot.imshow(image)
pyplot.title('Predicted')
ax = pyplot.gca()
# 繪製每一個圖框
for box in yhat['rois']:
	# 獲取座標
	y1, x1, y2, x2 = box
	# 計算繪圖框的寬度和高度
	width, height = x2 - x1, y2 - y1
	# 建立形狀對象
	rect = Rectangle((x1, y1), width, height, fill=False, color='red')
	# 繪製繪圖框
	ax.add_patch(rect)
複製代碼

咱們能夠將製做數據集,模型,配置信息,以及繪製數據集中前五張帶有真實和預測邊框的圖像,這些內容全都整合放在一個函數裏面。

# 繪製多張帶有真實和預測邊框的圖像
def plot_actual_vs_predicted(dataset, model, cfg, n_images=5):
	# 加載圖像和掩膜
	for i in range(n_images):
		# 加載圖像和掩膜
		image = dataset.load_image(i)
		mask, _ = dataset.load_mask(i)
		# 轉換像素值(例如居中)
		scaled_image = mold_image(image, cfg)
		# 將圖像轉換爲樣本
		sample = expand_dims(scaled_image, 0)
		# 做出預測
		yhat = model.detect(sample, verbose=0)[0]
		# 定義子圖
		pyplot.subplot(n_images, 2, i*2+1)
		# 繪製原始像素數據
		pyplot.imshow(image)
		pyplot.title('Actual')
		# 繪製掩膜
		for j in range(mask.shape[2]):
			pyplot.imshow(mask[:, :, j], cmap='gray', alpha=0.3)
		# 獲取繪圖框的上下文
		pyplot.subplot(n_images, 2, i*2+2)
		# 繪製原始像素數據
		pyplot.imshow(image)
		pyplot.title('Predicted')
		ax = pyplot.gca()
		# 繪製每一個繪圖框
		for box in yhat['rois']:
			# 獲取座標
			y1, x1, y2, x2 = box
			# 計算繪圖框的寬度和高度
			width, height = x2 - x1, y2 - y1
			# 建立形狀對象
			rect = Rectangle((x1, y1), width, height, fill=False, color='red')
			# 繪製繪圖框
			ax.add_patch(rect)
	# 顯示繪製結果
	pyplot.show()
複製代碼

完整的加載訓練好的模型,並對訓練集和測試集中前幾張圖像做出預測的代碼以下。

# 使用 mask rcnn 模型在圖像中檢測袋鼠
from os import listdir
from xml.etree import ElementTree
from numpy import zeros
from numpy import asarray
from numpy import expand_dims
from matplotlib import pyplot
from matplotlib.patches import Rectangle
from mrcnn.config import Config
from mrcnn.model import MaskRCNN
from mrcnn.model import mold_image
from mrcnn.utils import Dataset

# 定義並加載袋鼠數據集的類
class KangarooDataset(Dataset):
	# 加載數據集定義
	def load_dataset(self, dataset_dir, is_train=True):
		# 定義一個類
		self.add_class("dataset", 1, "kangaroo")
		# 定義數據所在位置
		images_dir = dataset_dir + '/images/'
		annotations_dir = dataset_dir + '/annots/'
		# 定位到全部圖像
		for filename in listdir(images_dir):
			# 提取圖像 id
			image_id = filename[:-4]
			# 略過不合格的圖像
			if image_id in ['00090']:
				continue
			# 若是咱們正在創建的是訓練集,略過 150 序號以後的全部圖像
			if is_train and int(image_id) >= 150:
				continue
			# 若是咱們正在創建的是測試/驗證集,略過 150 序號以前的全部圖像
			if not is_train and int(image_id) < 150:
				continue
			img_path = images_dir + filename
			ann_path = annotations_dir + image_id + '.xml'
			# 添加到數據集
			self.add_image('dataset', image_id=image_id, path=img_path, annotation=ann_path)

	# 從圖片中加載全部邊框信息
	def extract_boxes(self, filename):
		# 加載並解析文件
		root = ElementTree.parse(filename)
		boxes = list()
		# 提取邊框信息
		for box in root.findall('.//bndbox'):
			xmin = int(box.find('xmin').text)
			ymin = int(box.find('ymin').text)
			xmax = int(box.find('xmax').text)
			ymax = int(box.find('ymax').text)
			coors = [xmin, ymin, xmax, ymax]
			boxes.append(coors)
		# 提取出圖像尺寸
		width = int(root.find('.//size/width').text)
		height = int(root.find('.//size/height').text)
		return boxes, width, height

	# 加載圖像掩膜
	def load_mask(self, image_id):
		# 獲取圖像詳細信息
		info = self.image_info[image_id]
		# 定義盒文件位置
		path = info['annotation']
		# 加載 XML
		boxes, w, h = self.extract_boxes(path)
		# 爲全部掩膜建立一個數組,每一個數組都位於不一樣的通道
		masks = zeros([h, w, len(boxes)], dtype='uint8')
		# 建立掩膜
		class_ids = list()
		for i in range(len(boxes)):
			box = boxes[i]
			row_s, row_e = box[1], box[3]
			col_s, col_e = box[0], box[2]
			masks[row_s:row_e, col_s:col_e, i] = 1
			class_ids.append(self.class_names.index('kangaroo'))
		return masks, asarray(class_ids, dtype='int32')

	# 加載圖像引用
	def image_reference(self, image_id):
		info = self.image_info[image_id]
		return info['path']

# 定義預測配置
class PredictionConfig(Config):
	# 定義配置名
	NAME = "kangaroo_cfg"
	# 類的數量(背景中的 + 袋鼠)
	NUM_CLASSES = 1 + 1
	# 簡化 GPU 配置
	GPU_COUNT = 1
	IMAGES_PER_GPU = 1

# 繪製多張帶有真實和預測邊框的圖像
def plot_actual_vs_predicted(dataset, model, cfg, n_images=5):
	# 加載圖像和掩膜
	for i in range(n_images):
		# 加載圖像和掩膜
		image = dataset.load_image(i)
		mask, _ = dataset.load_mask(i)
		# 轉換像素值(例如居中)
		scaled_image = mold_image(image, cfg)
		# 將圖像轉換爲樣本
		sample = expand_dims(scaled_image, 0)
		# 做出預測
		yhat = model.detect(sample, verbose=0)[0]
		# 定義子圖
		pyplot.subplot(n_images, 2, i*2+1)
		# 繪製原始像素數據
		pyplot.imshow(image)
		pyplot.title('Actual')
		# 繪製掩膜
		for j in range(mask.shape[2]):
			pyplot.imshow(mask[:, :, j], cmap='gray', alpha=0.3)
		# 獲取繪圖框的上下文
		pyplot.subplot(n_images, 2, i*2+2)
		# 繪製原始像素數據
		pyplot.imshow(image)
		pyplot.title('Predicted')
		ax = pyplot.gca()
		# 繪製每一個繪圖框
		for box in yhat['rois']:
			# 獲取座標
			y1, x1, y2, x2 = box
			# 計算繪圖框的寬度和高度
			width, height = x2 - x1, y2 - y1
			# 建立形狀對象
			rect = Rectangle((x1, y1), width, height, fill=False, color='red')
			# 繪製繪圖框
			ax.add_patch(rect)
	# 顯示繪製結果
	pyplot.show()

# 加載訓練集
train_set = KangarooDataset()
train_set.load_dataset('kangaroo', is_train=True)
train_set.prepare()
print('Train: %d' % len(train_set.image_ids))
# 加載測試集
test_set = KangarooDataset()
test_set.load_dataset('kangaroo', is_train=False)
test_set.prepare()
print('Test: %d' % len(test_set.image_ids))
# 建立配置
cfg = PredictionConfig()
# 定義模型
model = MaskRCNN(mode='inference', model_dir='./', config=cfg)
# 加載模型權重
model_path = 'mask_rcnn_kangaroo_cfg_0005.h5'
model.load_weights(model_path, by_name=True)
# 繪製訓練集預測結果
plot_actual_vs_predicted(train_set, model, cfg)
# 繪製測試集訓練結果
plot_actual_vs_predicted(test_set, model, cfg)
複製代碼

運行示例代碼,將會建立一個顯示訓練集中前五張圖像的繪圖,並列的兩張圖像中分別包含了真實和預測的邊框。

咱們能夠看到,在這些示例中,模型的性能良好,它可以找出全部的袋鼠,甚至在包含兩個或三個袋鼠的單張圖像中也是如此。右側一列第二張圖出現了一個小錯誤,模型在同一個袋鼠上預測出了兩個邊框。

Plot of Photos of Kangaroos From the Training Dataset With Ground Truth and Predicted Bounding Boxes

繪製訓練集中帶有真實和預測邊框的袋鼠圖像

建立的第二張圖顯示了測試集中帶有真實和預測邊框的五張圖像。

這些圖像在訓練的過程當中沒有出現果,一樣的,模型在每一張圖像中都檢測到了袋鼠。咱們能夠發現,在最後兩張照片中有兩個小錯誤。具體來講,同一個袋鼠被檢測到了兩次。

毫無疑問,這些差別在屢次訓練後能夠被忽略,也許使用更大的數據集以及數據擴充,可讓模型將檢測到的人物做爲背景,而且不會重複檢測出袋鼠。

Plot of Photos of Kangaroos From the Training Dataset With Ground Truth and Predicted Bounding Boxes

繪製測試集中帶有真實和預測邊框的袋鼠圖像

擴展閱讀

這一章提供了與目標檢測相關的更多資源,若是你想要更深刻的學習,能夠閱讀它們。

論文

項目

API

文章

總結

在這篇教程中,咱們共同探索瞭如何研發用於在圖像中檢測袋鼠目標的 Mask R-CNN 模型。

具體來說,你的學習內容包括:

  • 如何爲訓練 R-CNN 模型準備好目標檢測數據集。
  • 如何使用遷移學習在新的數據集上訓練目標檢測模型。
  • 如何在測試數據集上評估 Mask R-CNN,以及如何在新的照片上做出預測。

你還有其餘任何的疑問嗎? 在下面的評論區寫下你的問題,我將會盡量給你最好的解答。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄