- 原文地址:How to Train an Object Detection Model with Keras
- 原文做者:Jason Brownlee
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:EmilyQiRabbit
- 校对者:Ultrasteve,zhmhhu
目标检测是一项颇有挑战性的计算机视觉类课题,它包括预测目标在图像中的位置以及确认检测到的目标是何种类型的物体。html
基于掩膜区域的卷积神经网络模型,或者咱们简称为 Mask R-CNN,是目标检测中最早进的方法之一。Matterport Mask R-CNN 项目为咱们提供了可用于开发与测试 Mask R-CNN 的 Keras 模型的库,咱们可用其来完成咱们本身的目标检测任务。尽管它利用了那些在很是具备挑战性的目标检测任务中训练出来的最佳模型,如 MS COCO,来供咱们进行迁移学习,可是对于初学者来讲,使用这个库可能有些困难,而且它还须要开发者仔细准备好数据集。前端
在这篇教程中,你将学习如何训练能够在照片中识别袋鼠的 Mask R-CNN 模型。python
在学完教程后,你将会知道:android
若是你还想知道如何创建图像分类、目标检测、人脸识别的模型等等,能够看看个人关于计算机视觉的新书,书中包括了 30 篇讲解细致的教程和全部源代码。ios
如今咱们开始吧。git
如何使用 R-CNN 模型以及 Keras 训练能够在照片中识别袋鼠的目标检测模型 照片来自 Ronnie Robertson,做者保留图像权利。github
本片教程能够分为五个部分,分别是:web
目标检测是计算机视觉中的一个课题,它包括在给定图像中识别特定内容是否存在,位置信息,以及一个或多个对象所属的类别。算法
这是一个颇有挑战性的问题,涵盖了目标识别(例如,找到目标在哪里),目标定位(例如,目标所处位置的范围),以及目标分类(例如,目标是哪一类物体)这三个问题的模型构建方法。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 文件中的安装说明。
这一步很是简单,只须要在命令行运行下面的命令:
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
复制代码
仓库能够经过 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’元素,而后遍历每一个元素来提取出用于定义每一个边框的 x、y,、min 和 max 的值。
元素内的文字也能够被解析为整数值。
# 提取出每一个 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')
复制代码
每一个边框均可以用图像框的 min、max、x 和 y 坐标定义。
这些值能够直接用于定义数组中值为 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)
复制代码
图像的绘制图会在第一个掩膜重叠的状况下一块儿被建立出来。
这时,咱们就能够看到图像中出现了一只带有掩膜覆盖其边界的袋鼠。
带有目标检测掩膜覆盖的袋鼠图像
咱们能够对数据集中的前 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()
复制代码
运行示例代码咱们能够看到,图像被正确的加载了,同时这些包含多个目标的图像也被正肯定义了各自的掩膜。
绘制训练集中的前 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’类。
展现目标检测掩膜、边框和类标签的图像
如今,咱们很是确认数据集可以被正确加载,咱们可使用它来拟合 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_loss 和 val_mrcnn_class_loss),还有训练和验证集的边框输出(mrcnn_bbox_loss 和 val_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’后生成的。
将模型文件从配置目录拷贝到当前的工做目录。咱们将会在接下来的章节中使用它进行模型的评估,并对未知图片做出预测。
结果显示,也许更多的训练次数可以让模型性能更好,或许能够微调模型中全部层的参数;这个思路也许能够是本文一个有趣的扩展。
下面让咱们一块儿来看看这个模型的性能评估。
目标识别目标的模型的性能一般使用平均绝对精度来衡量,即 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)
复制代码
运行示例代码,将会建立一个显示训练集中前五张图像的绘图,并列的两张图像中分别包含了真实和预测的边框。
咱们能够看到,在这些示例中,模型的性能良好,它可以找出全部的袋鼠,甚至在包含两个或三个袋鼠的单张图像中也是如此。右侧一列第二张图出现了一个小错误,模型在同一个袋鼠上预测出了两个边框。
绘制训练集中带有真实和预测边框的袋鼠图像
建立的第二张图显示了测试集中带有真实和预测边框的五张图像。
这些图像在训练的过程当中没有出现果,一样的,模型在每一张图像中都检测到了袋鼠。咱们能够发现,在最后两张照片中有两个小错误。具体来讲,同一个袋鼠被检测到了两次。
毫无疑问,这些差别在屡次训练后能够被忽略,也许使用更大的数据集以及数据扩充,可让模型将检测到的人物做为背景,而且不会重复检测出袋鼠。
绘制测试集中带有真实和预测边框的袋鼠图像
这一章提供了与目标检测相关的更多资源,若是你想要更深刻的学习,能够阅读它们。
在这篇教程中,咱们共同探索了如何研发用于在图像中检测袋鼠目标的 Mask R-CNN 模型。
具体来说,你的学习内容包括:
你还有其余任何的疑问吗? 在下面的评论区写下你的问题,我将会尽量给你最好的解答。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。