日萌社
人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新)
6.6 Mask RCNN
学习目标
- 目标
- 说明Mask RCNN的结构特点
- 掌握Mask RCNN的RoIAlign方法
- 掌握Mask RCNN的mask原理
- 了解Mask RCNN的主网络结构
- 了解Mask RCNN训练实验结果
- 应用
- 无
背景
目标检测和语义分割的效果在短时间内得到了很大的改善。在很大程度上,这些进步是由强大的基线系统驱动的,例如,分别用于目标检测和语义分割的Fast/Faster R-CNN和全卷积网络(FCN)框架。这些方法在概念上是直观的,提供灵活性和鲁棒性,以及快速的训练和推理。论文作者在这项工作中的目标是为目标分割开发一个相对有力的框架。
Mask RCNN主要解决的是实例分割,语义分割 (semantic segmentation) 指的是把一张图像的每一个像素进行分类, 比如把图像中所有的人分为一类. 而实例分割 (instance segmentation) 是指按照对象 (object) 进行分类, 那么不同的人就要分为不同的类别.
- deeplab:语义分割
- maskrcnn:实例分割
Mask R-CNN的输出见下图:
运行视频检测和分割的效果
YOLOv2 vs YOLOv3 vs Mask RCNN vs Deeplab Xception:https://www.bilibili.com/video/av63981949/
6.6.1 Mask RCNN介绍
Mask R-CNN是何凯明的力作,将Object Detection与Semantic Segmentation合在了一起做。Mask R-CNN是一个很多state-of-the-art算法的合成体,并非常巧妙的设计了这些模块的合成接口。
6.6.1.1 回顾
Faster R-CNN:我们首先简要回顾一下Faster R-CNN检测器。Faster R-CNN由两个阶段组成。
- 称为区域提议网络(RPN)的第一阶段提出候选目标边界框。
- 第二阶段,本质上是Fast R-CNN,使用RoIPool从每个候选框中提取特征,并进行分类和边界回归。两个阶段使用的特征可以共享,以便更快的推理。
对于FasterR-CNN来说,对于每个目标对象,它有两个输出,一个是类标签(classlabel),一个是边界框的偏移值(bounding-box offset)
6.6.1.2 Mask R-CNN
Mask R-CNN采用相同的两个阶段,在Faster R-CNN网络上的修改,具体包括:
1、采用ResNet-FPN的架构,并将ROI Pooling层替换成了ROIAlign
2、添加了并列的FCN层(Mask层)
- 第一阶段:只不过特征提取采用ResNet-FPN的架构,得到目标边界框。(FPN实际上是一种通用架构,可以结合各种骨架网络使用,比如VGG,ResNet等。Mask RCNN文章中使用了ResNNet-FPN网络结构)
- 多尺度检测在目标检测中变得越来越重要,对小目标的检测尤其如此。
- FasterRCNN的例子中对多级特征进行提取合并
第二阶段:通过一个RoIAlign除了预测类和预测框偏移,Mask R-CNN还为每个RoI输出二进制掩码。这与最近的其它系统相反,其分类取依赖于掩码预测。
-
Mask R-CNN方法增加了第三个分支的输出:进一步的是为每个RoI生成了一个目标掩码(二元掩码)。目标掩码与已有的class和box输出的不同在于它需要对目标的空间布局有一个更精细的提取。
掩码分支对于每个RoI的输出维度假设即K个分辨率为m×m的二进制掩码,每个类别一个,K表示类别数量。
6.6.1.3 RoIAlign
- 回顾:Faster-rcnn中的ROIPool是一种针对每一个在特征图中映射得到的Region of Interested的提取一个小尺度特征图(比如说7x7)的标准操作,它用以解决将不同尺度的ROI提取成相同尺度的特征大小的问题。
但是由于我们要实现像素级的 mask 图像, 所以如果仍然使用 Fast RCNN 中的 RoIPool 的话, 会出现一些误差:因为特征图上的 RoI 池化为固定大小 (比如 7×7) 的盒子 (bins) 时也可能存在取整操作。这些取整会使得 RoI 与提取的特征之间存在偏差, 这样小的偏差对分类基本没什么影像, 但是对像素级的分割必然会产生较大的负面影响。
- 原图800x800,经过VGG得到下采样32倍之后的25x25的特征图大小,RoI大小为665x665,所以转换到特诊途中为20x20(取整之后的),然后通过ROIPool的时候又要做7x7最大池化分成49块,显然,每个矩形块区域的边长为2.86,又含有小数,于是ROI Pooling 再次调整到2。
- 经过两次的调整得到候选区域已经出现了很大的误差了。
- 原因:为什么很大呢?
- 注意到特征图上的 1 个像素的误差会引起原始图像 16 个像素的误差, 如果池化了五次则会导致 32 个像素的误差
- 该层特征图上0.1个像素的偏差,缩放到原图就是3.2个像素。那么0.8的偏差,在原图上就是接近30个像素点的差别,影响还是很大的。
提出RoIAlign 的方法
主要包含三点:
- 1、预选框的大小保持浮点数状态不做取整
- 2、RoI 分割为 7×7 的单元时每个单元的边界不做取整
- 3、使用双线性内插法,在每个单元中采样四个固定位置的值进行池化
2、做法
1、假定原图中有一region proposal,还是大小为665x665,这样,映射到特征图中的大小:665/32=20.78,即20.78x20.78,此时,没有像RoiPooling那样就行取整操作,保留浮点数。
2、假定pooled_w=7,pooled_h=7,即pooling后固定成7x7大小的特征图,所以,将在 feature map上映射的20.78x20.78的region proposal 划分成49个同等大小的小区域,每个小区域的大小20.78/7=2.97,即2.97x2.97,得到下图共49个这样的大小的区域:
-
步骤 1:假定采样点数为4,即表示,对于每个2.97x2.97的小区域,平分四份,每一份取其中心点位置,而中心点位置的像素,采用双线性插值法进行计算。一个2.97 x2.97的区域就会得到四个点的像素值。
-
步骤2:取四个像素值中最大值作为这个小区域(即:2.97x2.97大小的区域)的像素值,如此类推,同样是49个小区域得到49个像素值,组成7x7大小的feature map
6.6.1.3 对比两种方式
- 对于检测图片中大目标物体时(VOC2007),两种方案的差别不大
- 而如果是图片中有较多小目标物体需要检测(COCO2017),则优先选择RoiAlign,更精准些。
- RoIAlign起到了很大的作用:论文中可以将掩码准确度提高0.1至0.5
6.6.1.4 主干架构(backbone architecture)的head结构(Mask预测部分)
论文中尝试的网络主干 (backbone) 结构有:
- 1、ResNet-50-C4
- 2、ResNet-101-C4
- 3、ResNeXt-101
- 4、ResNet-50-FPN
- 5、ResNet-101-FPN
注: Faster RCNN 使用 ResNets 的原始实现中从第四阶段的最后一次卷积之后提取特征, 我们称之为 C4. FPN 是指 Feature Pyramid Network
用于分类回归和分割的网络头部 (head) 结构(头部结构):
- 每个候选区域经过ROIAlign得到:分类和回归部分保持 Faster RCNN 中的结构不变, ResNet-C4 主干结构中额外添加 ResNet 的第五阶段
- 每个候选区域经过ROIAlign得到:分割部分使用全卷积作为 mask 预测分支
- 其实就是得到了图片中某个物体的像素属于哪个类别(0/1)
- 上图为两个结构对应的mask部分设置
- 数字表示分辨率或通道数,箭头表示卷积、反卷积、全连接层. 所有的卷积都是 3×3 的
- 1、输出层的卷积除外 (是 1×1 的)
- 2、反卷积是 2×2 的, 步长为 2, 使用 ReLU 激活
- 3、左边: 'res5' 表示 ResNet 的第五个阶段, 右边: '×4' 表示 4 个连续的卷积
6.6.1.4 训练细节
1、多任务损失函数
Mask RCNN 仍然使用两阶段方法, 在第二阶段添加了 mask 分支, 损失函数为:
注:分类损失Lcls和检测框损失Lbox与中定义的相同。其中
RPN网络
先对主干网络resnet101输出的每个特征图的像素生成3个候选框,然后通过RPN网络比较所有候选框与GT目标进行IoU判断,
标记256个正负样本继续用于RPN网络训练,RPN网络实际做的是softmax二分类层(包含目标/不包含目标的背景)实现cls层,
用logistic回归对正样本候选框和GT目标框继续进行偏移值计算最终回归输出预测框实现reg层。
最终RPN网络输出200个正负样本候选框(包含目标/不包含目标的背景)提供给mrcnn进行训练,正样本占其中全部样本的的比率是0.3。
mrcnn网络
200个正负样本候选框分别输入到分类cls层(每个ROI区域Softmax多分类分配GT目标类别)/逻辑回归reg层(检测框逻辑回归GT目标框)和mask分支,
mask通过像素级Sigmoid和二值化损失的手段对每个ROI区域的每个像素进行Sigmoid二分类输出,
判断每个像素是否属于ROI区域所分配的GT目标类别,即该ROI区域已经在分类cls层通过Softmax多分类分配了一个GT目标类别,
那么然后在mask分支中通过Sigmoid二分类对该ROI区域中的每个像素值进行判断该像素值是否属于该被赋予的GT目标类别。
- 假设最后得到的7x7的掩码,那么每个像素的对应是假设80个类别中的一个进行损失计算,不是有80个预测结果。
- 而是只有某个位置进行了sigmoid处理之后,得到的结果与GT的kk进行逻辑回归损失计算。其他的地方不贡献损失
- 为什么不算所有的:
- 因为是一个RoI区域对应一个类别,这个区域已经很小了,就只属于这个类别即可。可以看做这些像素是一个整体类别
这与FCN方法是不同,FCN是对每个像素进行多类别softmax分类,然后计算交叉熵损失,这种做法是会造成类间竞争的,而每个类别使用sigmoid输出并计算二值损失,可以避免类间竞争。实验表明,通过这种方法,可以较好地提升性能。
2、训练
- 1、正负样本:ground truth box 的 IoU 重合度超过 0.5 的 RoI 视为正例, 否则为反例.
- Mask 损失只定义在正例上. Mask 分支每个 RoI 可以预测 K (总类别数) 个 masks, 但我们只使用第 k 个, 这里的 k 是分类分支预测出的类别
- 2、图像 resize 到短边为 800 像素.
- 3、每个 mini-batch 每个 GPU 使用两张图, 每张图有 N 个采样的 RoIs, 正负样本数比例为 1:3
- C4 主干的 N=64, FPN 主干的 N=512.
- 4、参数:在 8 块 GPU 上训练 160k 步, 在第 120k 步的时候学习率从 0.02 下降到 0.002. 使用 0.0001 的权重衰减和 0.9 的动量.
- 5、RPN 的 anchor 使用了 5 种尺寸和 3 种比例, 与 FPN 中一致。一共15中类别的先验框
3、运行效果:
模型可以在GPU上以200毫秒每帧的速度运行,使用一台有8个GPU的机器,在COCO上训练需要一到两天的时间。
4、测试阶段
- 1、C4 的 proposals 的数量为 300, FPN 为 1000.
- 2、在所有的 proposals 上都进行 Bbox 回归, 最后应用 NMS
- 3、Mask 分支使用得分最高的 100 个检测框
- m×m 的浮点数的 mask 输出 resize 到 RoI 的大小, 然后应用 0.5 的阈值进行二值化。
6.6.4 效果
6.6.4.1 目标分割
Mask R-CNN超越了COCO实例分割任务上所有先前最先进的单一模型结果,其中包括COCO 2016挑战优胜者。作为副产品,我们的方法也优于COCO对象检测任务。
实例分割:MaskRCNN每个物体对象对应到一个类别,还有得到位置。所以能区分图片中同类别物体但是位置不一样
与以往实例分割算法比较
将Mask R-CNN与其它最先进的目标分割方法进行比较,如下表(表1)所示:(COCO test-dev上的目标分割掩码AP。 MNC和FCIS分别是COCO 2015和2016分割挑战的获胜者。Mask R-CNN优于更复杂的,包含多尺度训练和测试、水平翻转测试的FCIS+++,和OHEM。所有条目都是单模型的结果。)
对比:
FCIS+++对比 Mask R-CNN(ResNet-101-FPN)。 FCIS在重叠对象上有问题,Mask R-CNN没问题。
6.6.4.2 消融实验
-
1、结构上:表a显示了具有各种使用不同下层网络的Mask R-CNN。受益于更深层次的网络(50对比101)和高级设计,包括FPN和ResNeXt。我们注意到并不是所有的框架都会从更深层次的或高级的网络中自动获益
-
2、独立与非独立掩码:Mask R-CNN解耦了掩码和类预测:由于现有的检测框分支预测类标签,所以我们为每个类生成一个掩码,而不会在类之间产生竞争(通过像素级Sigmoid和二值化损失)。在表b中,这些方法将掩码和类预测的任务结合,导致了掩码AP(5.5个点)的严重损失。
-
3、RoIAlign:表c显示了对提出的RoIAlign层的评估。对于这个实验,使用的下层网络为ResNet-50-C4,其步进为16。RoIAlign相对RoIPool将AP提高了约3个点,在高IoU(AP75)结果中增益更多。
-
采用双线性采样的ROIAlign与提出的RoIWarp进行比较,RoIWarp仍然四舍五入了RoI,与输入失去了对齐。从表c可以看出,RoIWarp与RoIPool效果差不多,比RoIAlign差得多。这突出表明正确的对齐是关键。
-
4、掩码分支:
分割是一个像素到像素的任务,我们使用FCN来利用掩码的空间布局。在表e中,我们使用ResNet-50-FPN下层网络来比较多层感知机(MLP)和FCN。使用FCN可以提供超过MLP 2.1个点的AP增益。为了与与MLP进行公平的比较,FCN的上层网络的卷积层没有被预训练。
6.6.4.3 目标检测结果
在COCO数据集上将Mask R-CNN与其它最先进的目标检测方法进行比较,如下表所示:(目标检测结果(目标边界框AP),单模型,在test-dev上与其它最先进的技术对比。使用ResNet-101-FPN的Mask R-CNN优于所有先前最先进的模型的基本变体(实验中忽略了掩码输出)。Mask R-CNN超过12的增益来自使用RoIAlign(+1.1 APbb),多任务训练(+0.9 APbb)和ResNeXt-101(+1.6 APbb)。)
为了作进一步的比较,论文作者训练了一个没有掩码分支版本的Mask R-CNN,见表3中的“Faster R-CNN,RoIAlign”。由于RoIAlign,该模型的性能优于中提出的模型。但是比Mask R-CNN低0.9个点的AP。这个差距这是由于Mask R-CNN的多任务训练产生的。
6.6.4.4 Cityscapes上的实验
Cityscapes数据集的目标分割结果。该数据集具有精细标注的2975个训练图像,500个验证图像和1525个测试图像。它还有20k粗糙的训练图像,无精细标注,Cityscapes数据集的主要挑战是训练数据较少,特别是对于卡车,公共汽车和火车的类别,每个类别的训练样本大约有200-500个。所有图像的分辨率为2048 x 1024像素。目标分割任务涉及8个对象类别,其训练集中的总共目标数为:
人 | 骑手 | 小汽车 | 卡车 | 公交车 | 火车 | 摩托车 | 自行车 |
---|---|---|---|---|---|---|---|
17.9k | 1.8k | 26.9k | 0.5k | 0.4k | 0.2k | 0.7k | 3.7k |
该任务的目标分割性能由和COCO一样的AP(在IoU阈值上平均)来测量,也包括AP50(即,IoU为0.5的掩码AP)。
实现:
- 1、Mask R-CNN模型使用的下层网络是ResNet-FPN-50,也测试了对应的101层的网络,不过由于数据集比较小,性能相似。
- 2、将图像在[800,1024]像素范围内随机缩放(较短边)进行训练,从而减少过拟合。测试时则统一缩放到1024像素。
- 3、使用的批量大小为每个GPU 1个图像(实际上8个GPU上有8个),学习率为0.01,迭代次数为24k,在迭代次数达到18k时,学习率减少到0.001
结果:我们在测试集和验证集上,将我们的结果与其它主流方法进行了比较,使用预先训练好的COCO Mask R-CNN模型(骑手类别被随机初始化)。如下表(表7)所示:
- 1、对于人和小汽车类别,Cityscapes数据集包含了大量的类内重叠目标(每个图像平均6人和9辆小汽车)。类内重叠是目标分割的核心难点。论文的方法在这两个类别相对前最佳结果有大幅度改善(人相对提升了约85%,从16.5提高到30.5,小汽车相对提升了约30%,从35.7提高到46.9)。
- 2、使用COCO预训练的Mask R-CNN模型在测试集上达到了32.0 AP,比不预训练的模型提高了6个点。这表明足够的训练数据的重要性。
- 3、观察到测试集和训练集AP之间的偏差,偏差主要是由卡车,公共汽车和火车类别造成的,其中只使用精细标注训练数据的模型,在验证集和测试集上的AP分别为28.8/22.8,53.5/32.2和33.0/18.6。这表明这些训练数据很少的类别存在domain shift。 COCO预训练有助于改善这些类别上的结果,然而,domain shift依然存在,在验证集和测试集上的AP分别为38.0/30.1,57.5/40.9和41.2/30.9。
Cityscapes的结果示例如下图:
6.6.4.5 Mask R-CNN人体姿态估计(了解)
通过COCO关键点数据集上的人体姿态估计任务来展示论文框架的通用性。通过将每个关键点视为one-hot二进制掩码,只需要很少的修改,Mask R-CNN可以应用于人体关键点检测。不需要额外的技巧,Mask R-CNN超过了COCO 2016人体关键点检测比赛的冠军,同时运行速度可达5 FPS。因此,Mask R-CNN可以被更广泛地看作是用于目标级识别的灵活框架,并且可以容易地扩展到更复杂的任务。
6.6.5 小结
- 说明Mask RCNN的结构特点
- 掌握Mask RCNN的RoIAlign方法
- 掌握Mask RCNN的mask原理
- 了解Mask RCNN的主网络结构
- 了解Mask RCNN训练实验结果
6.8 Mask RCNN分割案例
学习目标
- 目标
- 知道分割数据集的读取处理方式
- 应用
- 应用完成数据集内容标签结果的读取
6.8.1 分割数据集介绍-气球分割数据集
气球分割数据集是一个小型的分割任务数据。目的是将气球从图片或者视频中分割出来。数据集有训练集和验证集
目录如下:
- 训练验证都含有.json标注文件以及jpg文件
- 大多数分割通过VIA tool标注工具可以生成每个图片的mask结果
- 注意:这里的标注数据中并没有提供检测框的标注信息,后期检测框的生成是动态根据mask结果生成的
- voc,coco等数据集中会提供了两者标注结果
其中json文件中的标注格式包含如下:
{
# 第一张图片的物体标记结果
"24631331976_defa3bb61f_k.jpg668058":{"fileref":"",
"size":668058,"filename":"24631331976_defa3bb61f_k.jpg",
"base64_img_data":"","file_attributes":{},
"regions":{"0":{"shape_attributes":{"name":"polygon","all_points_x":[916,913,905,889,868,836,809,792,789,784,777,769,767,777,786,791,769,739,714,678,645,615,595,583,580,584,595,614,645,676,716,769,815,849,875,900,916,916],"all_points_y":[515,583,616,656,696,737,753,767,777,785,785,778,768,766,760,755,755,743,728,702,670,629,588,539,500,458,425,394,360,342,329,331,347,371,398,442,504,515]},"region_attributes":{}}}},
# 第二张图片的物体标记
"16335852991_f55de7958d_k.jpg1767935":{"fileref":"","size":1767935,"filename":"16335852991_f55de7958d_k.jpg","base64_img_data":"","file_attributes":{},
"regions":{
"0":{"shape_attributes":{"name":"polygon","all_points_x":[588,617,649,673,692,708,722,730,737,718,706,699,697,676,650,613,580,552,534,520,513,513,521,526,541,560,588],"all_points_y":[173,168,172,182,197,216,237,260,283,312,341,367,390,369,349,337,337,347,361,332,296,266,243,225,205,187,173]},"region_attributes":{}},
"1":{"shape_attributes":{"name":"polygon","all_points_x":[845,861,880,892,902,910,889,869,844,813,785,762,745,739,731,746,767,790,821,845],"all_points_y":[219,229,242,260,275,299,277,263,254,250,255,265,279,283,258,241,225,216,213,219]},"region_attributes":{}},
"2":{"shape_attributes":{"name":"polygon","all_points_x":[931,928,920,913,897,872,840,811,789,768,754,730,726,724,718,698,698,707,721,734,746,769,794,822,845,865,889,910,921,929,931],"all_points_y":[378,402,435,454,475,460,450,449,450,460,469,489,486,459,426,390,367,335,306,290,278,261,252,250,254,261,277,299,323,354,378]},"region_attributes":{}},
"3":{"shape_attributes":{"name":"polygon","all_points_x":[927,946,968,989,992,985,975,957,937,913,889,862,852,876,897,910,925,933,939,939,935,927,910,900,927],"all_points_y":[486,498,516,553,593,630,649,668,686,700,707,707,708,691,675,656,635,610,587,562,538,512,492,480,486]},"region_attributes":{}},
"4":{"shape_attributes":{"name":"polygon","all_points_x":[704,692,690,691,699,711,723,742,766,785,807,839,865,887,904,923,933,939,939,931,920,905,885,861,839,808,786,769,754,748,746,738,738,729,722,718,704],"all_points_y":[664,631,604,580,545,521,498,480,461,452,449,449,457,469,484,506,532,565,584,620,643,662,682,701,713,719,723,728,733,731,738,737,729,720,708,690,664]},"region_attributes":{}},
"5":{"shape_attributes":{"name":"polygon","all_points_x":[526,509,497,493,490,493,501,512,526,546,573,603,626,662,688,709,721,724,724,719,704,694,691,691,682,683,687,688,684,682,679,676,664,648,620,587,564,548,526],"all_points_y":[551,526,498,470,444,422,398,381,365,351,340,338,340,357,381,408,438,466,493,504,531,568,584,604,609,612,610,617,625,625,619,616,620,619,609,599,585,573,551]},"region_attributes":{}},
"6":{"shape_attributes":{"name":"polygon","all_points_x":[594,579,567,563,564,568,579,605,631,656,671,676,682,684,687,687,684,684,691,691,694,702,711,719,722,729,737,738,746,749,756,765,757,728,714,683,654,623,594],"all_points_y":[735,712,691,659,631,612,596,605,613,621,618,616,625,625,616,612,612,608,605,619,637,656,678,692,706,719,727,737,739,731,734,730,741,762,766,772,769,757,735]},"region_attributes":{}}
}},
...
...
...
其中:"all_points_x":[588,617,649,673,692,708,722,730,737,718,706,699,697,676,650,613,580,552,534,520,513,513,521,526,541,560,588],"all_points_y":[173,168,172,182,197,216,237,260,283,312,341,367,390,369,349,337,337,347,361,332,296,266,243,225,205,187,173]}
表示该被标注物体所有像素点的坐标。
6.8.2 模型介绍
选用maskrcnn模型进行分割案例。maskrcnn的源码版本中选择最新的2.0版本。
github高星实现版本
- 高星版本:MaskRCNN。
- TensorFlow与keras实现的版本,代码只能在1.x版本运行,需要同时keras和TensorFlow两个库才能运行
使用版本是基于这个版本修改之后能在2.0环境下运行的maskrcnn源码。
并且预训练模型地址:maskrcnn迁移学习预训练模型。
- 可以使用多种预训练模型,这里提供Imagenet数据集训练的迁移模型
6.8.3 项目介绍
1、分割效果演示:
1、图片效果
2、视频分割效果
注:我们这里做的是直接将分割的结果显示原色,其他部分变成灰度图。
2、项目模块介绍
- ballon_dataset:项目的数据集
- logs:模型训练保存结果
- mrcnn:模型结构以及配置代码
- balloon_main:模型训练以及测试代码
其中Images:测试检测的图片或视频以及输出结果
6.8.4 项目训练过程实现
- 步骤
- 1、数据集读取处理和准备
- maskrcnn模型源码中Sequence封装数据集类使用
- 实现数据标签文件的读取
- 2、模型配置文件解析与修改、模型预训练模型加载、模型构建
- maskrcnn配置介绍
- 模型文件过程使用源码解析
- 3、模型训练过程实现
- 训练代码封装介绍
- 4、模型测试过程实现
- 图片预测结果处理
- 1、数据集读取处理和准备
6.8.4.1 数据集的读取处理
maskrcnn源码中utils.py文件封装了Dataset类,其中包含怎么获取分割数据集以及各式如何存储的方法。
class Dataset(object):
"""The base class for dataset classes.
To use it, create a new class that adds functions specific to the dataset
you want to use. For example:
See COCODataset and ShapesDataset as examples.
"""
可以通过编写自己的Dataset类以加载数据集进入的任何格式。
其中各个方法解释如下
-
def add_class(self, source, class_id, class_name):
- 添加类别信息,默认背景类别是第一个,记录在class_info中
- self.class_info = [{"source": "", "id": 0, "name": "BG"}]
-
def add_image(self, source, image_id, path, **kwargs):
- 添加图片信息
- self.image_info = { "id": image_id,"source": source,"path": path,}
-
def load_image(self, image_id): return image
- 加载指定图片id到[H,W,3]的numpy数组,并返回
- load_mask通过绘制多边形为图像中的每个对象生成位图蒙版(hitmap masks)。
- 加载图片id对应的mask,并且返回物体的mask [height, width, instance count]
- 以及物体类别id 1D array
- def prepare(self, class_map=None):
- 准备Dataset类数据使用
还有一个image_reference只是返回一个标识图像的字符串以进行调试。只是返回图像文件的路径。默认为空
1、Dataset的使用
- 使用过程
如下,需要继承重写load_mask方法,定义一个读取我们的气球数据的方法,添加到image_info当中
class CatsAndDogsDataset(Dataset):
"""
"""
def load_cats_and_dogs():
...
def load_mask(self, image_id):
...
注:通常我们可以自己实现数据读取处理的方法或者格式,如果有一些方便的通用工具也可以借鉴使用
- 比如:load_balloons读取JSON文件,提取注释,并迭代调用内部的add_class和add_image函数以构建数据集。
- load_mask:
2、获取结果展示数据
下面是我们定义获取数据过程和结果
dataset = balloon.BalloonDataset()
# 获取图片类别和图片其他信息
dataset.load_balloon(BALLOON_DIR, "train")
# 准备图片的dataset数据
dataset.prepare()
print("图片 数量: {}".format(len(dataset.image_ids)))
print("类别 数量: {}".format(dataset.num_classes))
for i, info in enumerate(dataset.class_info):
print("{:3}. {:50}".format(i, info['name']))
Image Count: 61
Class Count: 2
0.BG
1.balloon
展示样本的mask
可以使用模型中的visualize.display_top_masks(image, mask, class_ids, dataset.class_names)
image = dataset.load_image(image_id)
mask, class_ids = dataset.load_mask(image_id)
visualize.display_top_masks(image, mask, class_ids, dataset.class_names)
展示样本的bbox以及mask
没有bbox标记,通过utils.extract_bboxes对图片中的mask,计算出bbox位置
- 1、utils.extract_bboxes(mask):
- mask: [height, width, num_instances].mask的结果处理成 1 or 0.
- Returns: bbox array [num_instances, (y1, x1, y2, x2)].
image = dataset.load_image(image_id)
mask, class_ids = dataset.load_mask(image_id)
# 计算 Bounding box
bbox = utils.extract_bboxes(mask)
print("image_id ", image_id, dataset.image_reference(image_id))
# model中log方法
log("image", image)
log("mask", mask)
log("class_ids", class_ids)
log("bbox", bbox)
# 结果
image_id 1 /deepmatter/libs/mask_rcnn/datasets/balloon/train/25899693952_7c8b8b9edc_k.jpg
image shape: (1365, 2048, 3) min: 0.00000 max: 255.00000
mask shape: (1365, 2048, 1) min: 0.00000 max: 1.00000
class_ids shape: (1,) min: 1.00000 max: 1.00000
bbox shape: (1, 4) min: 116.00000 max: 965.00000
通过visualize.display_instances(image, bbox, mask, class_ids, dataset.class_names)展示
- 2、通过modellib.load_image_gt:传入dataset,配置、图片id
image, image_meta, class_ids, bbox, mask = modellib.load_image_gt(
dataset, config, image_id, use_mini_mask=False)
print("image", image)
print("image_meta", image_meta)
print("class_ids", class_ids)
print("bbox", bbox)
print("mask", mask)
mage shape: (1024, 1024, 3) min: 0.00000 max: 255.00000
image_meta shape: (10,) min: 0.00000 max: 1024.00000
class_ids shape: (2,) min: 1.00000 max: 1.00000
bbox shape: (2, 4) min: 181.00000 max: 1024.00000
mask shape: (1024, 1024, 2) min: 0.00000 max: 1.00000
6.8.4.2 数据集BalloonDataset实现
- 步骤
- 继承dataset类别
- 实现load_balloon方法
- 实现load_mask方法
这里我们创建一个utils文件夹作为训练数据集读取工具,其中编写一个balloon_dataset.py文件
import os
import json
import sys
import numpy as np
sys.path.append("../")
from mrcnn import utils, visualize
import skimage
class BalloonDataset(utils.Dataset):
"""气球分割数据集获取类
"""
def load_balloon(self, dataset_dir, subset):
pass
def load_mask(self, image_id):
pass
1、实现load_balloon方法
- 目的:添加每张图片的id、路径、长、宽、标注信息到selfi.mage_info字典中
- 1、读取标注json文件
- 2、获取标注区域
- 3、对每个图片,保存其中各个区域的相关信息,图片路径、长宽、filename
def load_balloon(self, dataset_dir, subset):
"""
加载数据集
:param dataset_dir: 数据集目录
:param subset: 训练集还是测试机
:return:
"""
# 添加数据集类别数量
self.add_class("balloon", 1, "balloon")
# 是否提供在训练或者验证集字符串
assert subset in ["train", "val"]
dataset_dir = os.path.join(dataset_dir, subset)
# Load annotations
# { 'filename': '28503151_5b5b7ec140_b.jpg',
# 'regions': {
# '0': {
# 'region_attributes': {},
# 'shape_attributes': {
# 'all_points_x': [...],
# 'all_points_y': [...],
# 'name': 'polygon'}},
# ... more regions ...
# },
# }
# 读取标注区域:
annotations = json.load(open(os.path.join(dataset_dir, "via_region_data.json")))
annotations = list(annotations.values())
# 如果annotations不存在直接跳过
annotations = [a for a in annotations if a['regions']]
# 添加每张图片的坐标
for a in annotations:
# 获取所有多边形的x, y 的所有点坐标,存储在shape_attributes
# 判断其中类型是否是字典,若果字典
if isinstance(a['regions'], dict):
polygons = [r['shape_attributes'] for r in a['regions'].values()]
else:
polygons = [r['shape_attributes'] for r in a['regions']]
# 读取图片内容获取长宽
image_path = os.path.join(dataset_dir, a['filename'])
image = skimage.io.imread(image_path)
height, width = image.shape[:2]
# 加入到image_info字典当中
self.add_image(
"balloon",
image_id=a['filename'],
path=image_path,
width=width, height=height,
polygons=polygons)
注:源码中大量使用skimage模块做图片读取处理
- 其中pil处理流程读取结果默认sRGB格式,不是rgb的,还需要转换成数组
image = pil.Image.open("")
image = image.convert('RGB')
arr = np.array(image)
- skimage.io.read()直接转换成array数组
2、实现load_mask方法
def load_mask(self, image_id):
"""加载图片中的mask返回每个图片的mask及其id
:param image_id: 图片ID
:return: masks: 一个实例的布尔形状 [height, width, instance count]
class_ids: 类别的 1D 数组
"""
# 如果不是balloon类别的图片数据,默认返回空
image_info = self.image_info[image_id]
if image_info["source"] != "balloon":
return super(self.__class__, self).load_mask(image_id)
# 将坐标转换成bitmap [height, width, instance_count]
info = self.image_info[image_id]
mask = np.zeros([info["height"], info["width"], len(info["polygons"])],
dtype=np.uint8)
for i, p in enumerate(info["polygons"]):
# Get indexes of pixels inside the polygon and set them to 1
# 获取图片像素中的这个mask多边形区域中像素下标,将其标记为1
rr, cc = skimage.draw.polygon(p['all_points_y'], p['all_points_x'])
mask[rr, cc, i] = 1
# 返回mask区域标记 [height, width, instance count]
# 以及mask物体的个数
return mask.astype(np.bool), np.ones([mask.shape[-1]], dtype=np.int32)
测试结果
if __name__ == '__main__':
dataset_train = BalloonDataset()
dataset_train.load_balloon("../balloon_dataset/", "train")
dataset_train.prepare()
可以做上述的测试
打印结果:
print("图片数量: {}".format(len(dataset_train.image_ids)))
print("类别数量: {}".format(dataset_train.num_classes))
for i, info in enumerate(dataset_train.class_info):
print("{}. {}".format(i, info['name']))
1、展示mask
# 1、随机选择部分图片进行展示mask区域
image_id = np.random.choice(dataset_train.image_ids, 1)[0]
image = dataset_train.load_image(image_id)
mask, class_ids = dataset_train.load_mask(image_id)
visualize.display_top_masks(image, mask, class_ids, dataset_train.class_names)
2、通过mask计算bbox区域,并进行展示
# 计算bbox
bbox = utils.extract_bboxes(mask)
from mrcnn.model import log
log("image", image)
log("mask", mask)
log("class_ids", class_ids)
log("bbox", bbox)
# 显示mask,以及bbox
visualize.display_instances(image, bbox, mask, class_ids, dataset_train.class_names)
结果
图片数量: 61
类别数量: 2
0. BG
1. balloon
image shape: (681, 1024, 3) min: 0.00000 max: 255.00000 uint8
mask shape: (681, 1024, 2) min: 0.00000 max: 1.00000 bool
class_ids shape: (2,) min: 1.00000 max: 1.00000 int32
bbox shape: (2, 4) min: 191.00000 max: 1024.00000 int32
6.9 Mask RCNN分割案例2
学习目标
- 目标
- 知道maskrcnn中的源码编译训练流程
- 知道maskrcnn中的anchor设置以及计算
- 掌握模型的训练预测流程
- 应用
- 应用完成maskrcnn指定气球分割数据集模型训练
- 应用完成maskrcnn指定图片或视频预测输出
6.9.1 项目步骤
步骤
- 1、数据集读取处理和准备
- 实现数据标签文件的读取
- 2、模型配置文件解析与修改、模型预训练模型加载、模型构建
- maskrcnn模型源码中Sequence封装数据集类使用
- maskrcnn配置介绍
- 模型文件过程使用源码解析
- 3、模型训练过程实现
- 训练代码封装介绍
- 4、模型测试过程实现
- 图片预测结果处理
6.9.1.1 模型分析以及模型训练流程实现
- 步骤分析
- 1、进行参数传入判断
- 2、配置模型的参数、数据集的训练读取配置
- 3、创建模型
- 4、训练测试逻辑实现
完整代码实现过程
args = parser.parse_args()
# 1、进行参数传入判断
if args.command == "train":
assert args.dataset, "指定训练的时候必须传入 --dataset数据目录"
elif args.command == "test":
assert args.image or args.video,\
"指定测试的时候必须提供图片或者视频"
# 2、配置模型的参数、数据集的训练读取配置
if args.command == "train":
config = BalloonConfig()
else:
# 测试的配置修改:设置batch_size为1,Batch size = GPU_COUNT * IMAGES_PER_GPU
class InferenceConfig(BalloonConfig):
GPU_COUNT = 1
IMAGES_PER_GPU = 1
config = InferenceConfig()
config.display()
# 3、创建模型
if args.command == "train":
model = maskrcnn.MaskRCNN(mode="training", config=config,
model_dir=args.logs)
else:
model = maskrcnn.MaskRCNN(mode="inference", config=config,
model_dir=args.logs)
# 4、训练测试逻辑实现
if args.command == "train":
# 选择加载的预训练模型类别并下载
if args.weights.lower() == "imagenet":
weights_path = model.get_imagenet_weights()
else:
raise ValueError("提供一种预训练模型种类")
# 加载预训练模型权重
print("Loading weights ", weights_path)
model.load_weights(weights_path, by_name=True)
# 进行训练
train(model)
elif args.command == "test":
model.load_weights(args.model, by_name=True)
# 进行检测
detect_and_draw_segmentation(args, model)
else:
print("'{}' 传入参数无法识别. "
"请使用 'train' or 'test'".format(args.command))
6.9.1.2 模型配置文件
- 其中config.py是模型的配置文件,我们可以根据自己的需求训练的需求进行修改
- maskrcnn参数等设置众多,通常提供一个参数配置更好
- 其中某些重要的配置我们进行介绍
- 1、训练参数配置
- 2、测试参数配置
- 3、学习率相关设置
class Config(object):
"""Base configuration class. For custom configurations, create a
sub-class that inherits from this one and override properties
that need to be changed.
"""
# 1、训练配置
# NUMBER OF GPUs to use. When using only a CPU, this needs to be set to 1.
# 如果设置大于1,多个GPU进行并行运算,源码中parallel_model.py使用1.xAPI进行多GPU计算
GPU_COUNT = 1
# 每个GPU训练的图片数量
# A 12GB GPU can typically handle 2 images of 1024x1024px.
# Adjust based on your GPU memory and image sizes. Use the highest
# number that your GPU can handle for best performance.
IMAGES_PER_GPU = 2
# 一个epoch的训练步数
# This doesn't need to match the size of the training set. Tensorboard
# updates are saved at the end of each epoch, so setting this to a
# smaller number means getting more frequent TensorBoard updates.
# Validation stats are also calculated at each epoch end and they
# might take a while, so don't set this too small to avoid spending
# a lot of time on validation stats.
STEPS_PER_EPOCH = 1000
# Number of validation steps to run at the end of every training epoch.
# A bigger number improves accuracy of validation stats, but slows
# down the training.
VALIDATION_STEPS = 50
# 主网络架构
# Supported values are: resnet50, resnet101.
# You can also provide a callable that should have the signature
# of model.resnet_graph. If you do so, you need to supply a callable
# to COMPUTE_BACKBONE_SHAPE as well
BACKBONE = "resnet101"
# 基于resnet101架构的图像金字塔FPN到达的每层 步长
BACKBONE_STRIDES = [4, 8, 16, 32, 64]
# Size of the fully-connected layers in the classification graph
FPN_CLASSIF_FC_LAYERS_SIZE = 1024
# Size of the top-down layers used to build the feature pyramid
TOP_DOWN_PYRAMID_SIZE = 256
# 总类别个数 (including background)
NUM_CLASSES = 1 # Override in sub-classes
# RPN anchor的面积根号设置
RPN_ANCHOR_SCALES = (32, 64, 128, 256, 512)
# anchor的比率用于设置长宽 (width/height)
# A value of 1 represents a square anchor, and 0.5 is a wide anchor
RPN_ANCHOR_RATIOS = [0.5, 1, 2]
# Anchor 设置步长,如果为2跳过一些特征层设置anchor
# If 1 then anchors are created for each cell in the backbone feature map.
# If 2, then anchors are created for every other cell, and so on.
RPN_ANCHOR_STRIDE = 1
# 过滤RPN proposals的NMS阈值,值越大产生更多的建议框
# You can increase this during training to generate more propsals.
RPN_NMS_THRESHOLD = 0.7
# 每张图产生多少anchors用于RPN training
RPN_TRAIN_ANCHORS_PER_IMAGE = 256
# 经过tf.nn.top_k筛选之后并且在 non-maximum suppression进行之前的ROIs 数量
PRE_NMS_LIMIT = 6000
# 在non-maximum suppression ROIs 的数量(training and inference)
POST_NMS_ROIS_TRAINING = 2000
POST_NMS_ROIS_INFERENCE = 1000
# Input image resizing,默认square模式,设置成[max_dim, max_dim]
# square: Resize and pad with zeros to get a square image
# of size [max_dim, max_dim].
IMAGE_RESIZE_MODE = "square"
IMAGE_MIN_DIM = 800
IMAGE_MAX_DIM = 1024
# 每个image提供给classifier/mask heads中的rois数量
# The Mask RCNN paper uses 512 but often the RPN doesn't generate
# enough positive proposals to fill this and keep a positive:negative
# ratio of 1:3. You can increase the number of proposals by adjusting
# the RPN NMS threshold.
TRAIN_ROIS_PER_IMAGE = 200
# ROIs 用于训练 classifier/mask heads的正样本比率
ROI_POSITIVE_RATIO = 0.33
# ROIs池化层大小
POOL_SIZE = 7
MASK_POOL_SIZE = 14
# 输出mask的大小
# To change this you also need to change the neural network mask branch
MASK_SHAPE = [28, 28]
# 每张图的GT实例数量的最大值
MAX_GT_INSTANCES = 100
# 2、检测的配置
# 最后测试检测的时候实例数量100
DETECTION_MAX_INSTANCES = 100
# Minimum probability value to accept a detected instance
# ROIs below this threshold are skipped
DETECTION_MIN_CONFIDENCE = 0.7
# 用于检测的Non-maximum suppression阈值
DETECTION_NMS_THRESHOLD = 0.3
# 3、学习率设置相关设置
# The Mask RCNN paper uses lr=0.02, but on TensorFlow it causes
# weights to explode. Likely due to differences in optimizer
# implementation.
LEARNING_RATE = 0.001
LEARNING_MOMENTUM = 0.9
# Weight decay regularization
WEIGHT_DECAY = 0.0001
# 损失计算公式分配权重
# Loss weights for more precise optimization.
# Can be used for R-CNN training setup.
LOSS_WEIGHTS = {
"rpn_class_loss": 1.,
"rpn_bbox_loss": 1.,
"mrcnn_class_loss": 1.,
"mrcnn_bbox_loss": 1.,
"mrcnn_mask_loss": 1.
}
# 梯度截断值
GRADIENT_CLIP_NORM = 5.0
其中有配置的几个方法,通过display显示模型的当前配置
def to_dict(self):
return {a: getattr(self, a)
for a in sorted(dir(self))
if not a.startswith("__") and not callable(getattr(self, a))}
def display(self):
"""Display Configuration values."""
print("\nConfigurations:")
for key, val in self.to_dict().items():
print(f"{key:30} {val}")
print("\n")
6.9.1.3 案例:气球数据集配置代码编写
在balloon_dataset中我们添加自己数据集需要的配置,如下
from mrcnn.config import Config
class BalloonConfig(Config):
"""继承MaskRCNN的模型配置信息
修改其中需要的训练集数据信息
"""
# 给配置一个名称
NAME = "balloon"
IMAGES_PER_GPU = 2
# 类别数量(包括背景),气球类别+1
NUM_CLASSES = 1 + 1
# 一个epoch的步数
STEPS_PER_EPOCH = 100
# 检测的时候过滤置信度的阈值
DETECTION_MIN_CONFIDENCE = 0.9
然后在训练过程balloon_main.py中加入以下获取配置等代码:
from utils.balloon_dataset import BalloonDataset, BalloonConfig
args = parser.parse_args()
# 1、进行参数传入判断
if args.command == "train":
assert args.dataset, "指定训练的时候必须传入 --dataset数据目录"
elif args.command == "test":
assert args.image or args.video,\
"指定测试的时候必须提供图片或者视频"
# 2、配置模型的参数、数据集的训练读取配置
if args.command == "train":
config = BalloonConfig()
else:
# 测试的配置修改:设置batch_size为1,Batch size = GPU_COUNT * IMAGES_PER_GPU
class InferenceConfig(BalloonConfig):
GPU_COUNT = 1
IMAGES_PER_GPU = 1
config = InferenceConfig()
# 显示配置
config.display()
其中初始导入包以及运行命令行参数如下:
import numpy as np
import skimage.draw
import argparse
from mrcnn import model as maskrcnn
from utils.balloon_dataset import BalloonDataset, BalloonConfig
import os
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
# 命令行参数
parser = argparse.ArgumentParser(
description='气球分割模型maskrcnn训练')
parser.add_argument("--command", type=str, default='test',
help="'train' or 'test' 训练还是进行测试")
parser.add_argument('--dataset', type=str, default='./balloon_data',
help='气球分割数据集目录')
parser.add_argument('--weights', type=str, default='imagenet',
help="预训练模型权重"
"imagenet:https://github.com/fchollet/"
"deep-learning-models/releases/"
"download/v0.2/resnet50_weights"
"_tf_dim_ordering_tf_kernels_notop.h5")
parser.add_argument('--logs', type=str, default='./logs/',
help='打印日志目录')
parser.add_argument('--image', type=str, default='./images/2917282960_06beee649a_b.jpg',
help='需要进行检测分割的图片目录')
parser.add_argument('--video', type=str, default='./images/v0200fd10000bq043q9pskdh7ri20vm0.MP4',
help='需要进行检测分割的视频目录')
parser.add_argument('--model', type=str, default='./logs/mask_rcnn_balloon.h5',
help='指定测试使用的训练好的模型文件')
6.9.1.4 模型使用介绍
模型使用比较简答,直接通过导入model即可,使用细节如下
from mrcnn import model as maskrcnn
# 1、选用训练模式,加入配置以及模型保存目录
model = maskrcnn.MaskRCNN(mode="training", config=config,
model_dir=args.logs)
# 2、选用测试推理模式,加入配置以及模型保存目录
model = maskrcnn.MaskRCNN(mode="inference", config=config,
model_dir=args.logs)
那么其中MaskRCNN类提供了建立模型的一套过程,主要源代码过程以下几个步骤:
- 模型过程:
- 输入构建、构建GT、RPN模型搭建输出、通过ProposalLayer(源码类)产生感兴趣区域
- 计算5种损失、构建模型输入输出
1、模型数据读取与训练源码顺序分析
模型对于训练过程封装较深,所以在这里需要对源码做出相应解释,主要介绍有几个重要函数
- self.train():模型的训练逻辑
- 1、class DataGenerator(KU.Sequence):数据准备阶段构建generator
- 设置RPN训练目标框
- 2、self.compile:模型编译阶段
- 设置损失计算、正则化化
- 3、self.fit训练
- 1、class DataGenerator(KU.Sequence):数据准备阶段构建generator
在训练的时候我们只需要调用maskcnn中的train函数即可,我们这里对源代码中的train函数进行分析:
- 1、def train(self, train_dataset, val_dataset, learning_rate, epochs, layers,
augmentation=None, custom_callbacks=None, no_augmentation_sources=None):
- train_dataset, val_dataset:训练、验证数据集dataset对象
- learning_rate:学习率
- epochs:迭代次数
- layers:选择那些层进行训练
- heads:RPN、classifier以及mask 网络进行训练
- custom_callbacks=None:自定义的训练回调函数
源码分析:
# 1、选择不同的层的名称,根据传入参数设置
layer_regex = {
# all layers but the backbone
"heads": r"(mrcnn\_.*)|(rpn\_.*)|(fpn\_.*)",
# From a specific Resnet stage and up
"3+": r"(res3.*)|(bn3.*)|(res4.*)|(bn4.*)|(res5.*)|(bn5.*)|(mrcnn\_.*)|(rpn\_.*)|(fpn\_.*)",
"4+": r"(res4.*)|(bn4.*)|(res5.*)|(bn5.*)|(mrcnn\_.*)|(rpn\_.*)|(fpn\_.*)",
"5+": r"(res5.*)|(bn5.*)|(mrcnn\_.*)|(rpn\_.*)|(fpn\_.*)",
# All layers
"all": ".*",
}
if layers in layer_regex.keys():
layers = layer_regex[layers]
# 2、Data获取,model中实现的 DataGenerator 类继承keras.utils.Sequence
train_generator = DataGenerator(train_dataset, self.config, shuffle=True,
augmentation=augmentation)
val_generator = DataGenerator(val_dataset, self.config, shuffle=True)
# Create log_dir if it does not exist
if not os.path.exists(self.log_dir):
os.makedirs(self.log_dir)
# Callbacks
callbacks = [
keras.callbacks.TensorBoard(log_dir=self.log_dir,
histogram_freq=0, write_graph=True, write_images=False),
keras.callbacks.ModelCheckpoint(self.checkpoint_path,
verbose=0, save_weights_only=True),
]
# Add custom callbacks to the list
if custom_callbacks:
callbacks += custom_callbacks
# 4、训练,指定训练的层,优化器等
log("\nStarting at epoch {}. LR={}\n".format(self.epoch, learning_rate))
log("Checkpoint Path: {}".format(self.checkpoint_path))
self.set_trainable(layers)
self.compile(learning_rate, self.config.LEARNING_MOMENTUM)
# Work-around for Windows: Keras fails on Windows when using
# multiprocessing workers. See discussion here:
# https://github.com/matterport/Mask_RCNN/issues/13#issuecomment-353124009
if os.name == 'nt':
workers = 0
else:
workers = multiprocessing.cpu_count()
self.keras_model.fit(
train_generator,
initial_epoch=self.epoch,
epochs=epochs,
steps_per_epoch=self.config.STEPS_PER_EPOCH,
callbacks=callbacks,
validation_data=val_generator,
validation_steps=self.config.VALIDATION_STEPS,
max_queue_size=100,
workers=workers,
use_multiprocessing=workers > 1,
)
self.epoch = max(self.epoch, epochs)
-
2、class DataGenerator(KU.Sequence):
-
初始化:def init(self, dataset, config, shuffle=True, augmentation=None,random_rois=0, detection_targets=False):
-
对于传入的Dataset对象建立序列数据,提供每批次数据给训练器
-
(1)主要根据配置文件产生RPN网络相应的anchor先验框的坐标(后面会调用显示)
-
self.backbone_shapes = compute_backbone_shapes(config, config.IMAGE_SHAPE) self.anchors = utils.generate_pyramid_anchors(config.RPN_ANCHOR_SCALES, config.RPN_ANCHOR_RATIOS, self.backbone_shapes, config.BACKBONE_STRIDES, config.RPN_ANCHOR_STRIDE)
-
(2)def getitem(self, idx):
- 产生第idx批次的数据。
-
return inputs, outputs。下面为返回的两个结果的解析
-
注:网络训练的时候只需要inputs即可。outputs输出默认为空,但是如果提供random_rois的值大于0的参数。那么DataGenerator将会过滤之后返回整个网络中第二阶段maskrcnn的需要的RoIs感兴趣框相关信息
- 因为训练期间网络在第一阶段产生bbox训练之后
-
# inputs返回值包含
- images: [batch, H, W, C]
- image_meta: [batch, (meta data)] Image details.
meta = np.array(
[image_id] + # size=1
list(original_image_shape) + # size=3
list(image_shape) + # size=3
list(window) + # size=4 (y1, x1, y2, x2) in image cooredinates
[scale] + # size=1
list(active_class_ids) # size=num_classes
)
- rpn_match: [batch, N] Integer (1=positive anchor, -1=negative, 0=neutral) # 政府严格不能
- rpn_bbox: [batch, N, (dy, dx, log(dh), log(dw))] Anchor bbox deltas.# anchor与GT进行偏移的值
- gt_class_ids: [batch, MAX_GT_INSTANCES] # GTclass IDs
- gt_boxes: [batch, MAX_GT_INSTANCES, (y1, x1, y2, x2)]# GT物体框位置
- gt_masks: [batch, height, width, MAX_GT_INSTANCES]. The height and width # GT mask目标值
are those of the image unless use_mini_mask is True, in which
case they are defined in MINI_MASK_SHAPE.
# outputs 默认为空,如果提供random_rois 大于0的参数
ouptuts=[batch_mrcnn_class_ids, batch_mrcnn_bbox, batch_mrcnn_mask]
其中在DataGenerator的返回批次数据中会调用下面函数。
(3)build_rpn_targets(image_shape, anchors, gt_class_ids, gt_boxes, config): anchor->bbox
- 对于目标GT以及RPN的众多acnhor,进行正负样本匹配,并且将转换极坐标到中心坐标
- 并且根据变换公式改变anchor,
# 返回结果
anchors: [num_anchors, (y1, x1, y2, x2)]
gt_class_ids: [num_gt_boxes] Integer class IDs.
gt_boxes: [num_gt_boxes, (y1, x1, y2, x2)]
Returns:
rpn_match: [N] (int32) matches between anchors and GT boxes.
1 = positive anchor, -1 = negative anchor, 0 = neutral
rpn_bbox: [N, (dy, dx, log(dh), log(dw))] Anchor bbox deltas.
# GT坐标变化公式
gt_h = gt[2] - gt[0]
gt_w = gt[3] - gt[1]
gt_center_y = gt[0] + 0.5 * gt_h
gt_center_x = gt[1] + 0.5 * gt_w
# Anchor的坐标变换
a_h = a[2] - a[0]
a_w = a[3] - a[1]
a_center_y = a[0] + 0.5 * a_h
a_center_x = a[1] + 0.5 * a_w
# 保存偏移结果
rpn_bbox[ix] = [
(gt_center_y - a_center_y) / a_h,
(gt_center_x - a_center_x) / a_w,
np.log(gt_h / a_h),
np.log(gt_w / a_w),
]
(4)build_detection_targets(rpn_rois, gt_class_ids, gt_boxes, gt_masks, config):(如果需要返回outputs就会调用该函数返回)
-
Generate targets for training Stage 2 classifier and mask heads.
-
# 训练期间不使用,是用作调试debug使用或者单独训练不带有RPN网络的maskrcnn结构时时使用 This is not used in normal training. It's useful for debugging or to train the Mask RCNN heads without using the RPN head.
# 输入Inputs:
rpn_rois: [N, (y1, x1, y2, x2)] proposal boxes.
gt_class_ids: [instance count] Integer class IDs
gt_boxes: [instance count, (y1, x1, y2, x2)]
gt_masks: [height, width, instance count] Ground truth masks. Can be full
size or mini-masks.
# 输出Returns:感兴趣区域以及感兴趣区域的框位置和mask
# 一张图片只产生200区域给maskrcnn训练
# Number of ROIs per image to feed to classifier/mask heads
# The Mask RCNN paper uses 512 but often the RPN doesn't generate
# enough positive proposals to fill this and keep a positive:negative
# ratio of 1:3. You can increase the number of proposals by adjusting
# the RPN NMS threshold.
#TRAIN_ROIS_PER_IMAGE = 200
rois: [TRAIN_ROIS_PER_IMAGE, (y1, x1, y2, x2)]
class_ids: [TRAIN_ROIS_PER_IMAGE]. Integer class IDs.
bboxes: [TRAIN_ROIS_PER_IMAGE, NUM_CLASSES, (y, x, log(h), log(w))]. Class-specific
bbox refinements.
masks: [TRAIN_ROIS_PER_IMAGE, height, width, NUM_CLASSES). Class specific masks cropped
to bbox boundaries and resized to neural network output size.
-
3、compile(self, learning_rate, momentum):
- Gets the model ready for training. Adds losses, regularization, and metrics. Then calls the Keras compile() function.
- 指定训练的学习率损失、添加五种损失最终结果计算、L2 Regularization正则化,并且会在函数中调用keras.compile()函数
- loss_names = ["rpn_class_loss", "rpn_bbox_loss","mrcnn_class_loss", "mrcnn_bbox_loss", "mrcnn_mask_loss"]
- 4、model.fit就是正常的训练函数
6.9.1.7 案例:网络数据Anchor以及目标值设置分析
为了更好理解maskrcnn中的RPN结构设置的acnhor。这里对于anchor做输出分析,这里使用上面dataset中使用的方法。在ballon_dataset.py文件中进行测试。
- 1、utils.generate_pyramid_anchors
- 需要rpn anchor的大小,rpnanchor的长宽比率等参数
# 需要创建一个配置来进行打印
# 3、计算anchor结果
config = BalloonConfig()
# 添加一个特征图大小属性(这里做测试需要设置一下才能用generate_pyramid_anchors),dataset中直接计算出来特征图大小
config.BACKBONE_SHAPES = [[256, 256], [128, 128], [64, 64], [32, 32], [16, 16]]
anchors = utils.generate_pyramid_anchors(config.RPN_ANCHOR_SCALES,
config.RPN_ANCHOR_RATIOS,
config.BACKBONE_SHAPES,
config.BACKBONE_STRIDES,
config.RPN_ANCHOR_STRIDE)
# 打印anchor相关信息
num_levels = len(config.BACKBONE_SHAPES)
anchors_per_cell = len(config.RPN_ANCHOR_RATIOS)
print("Count: ", anchors.shape[0])
print("Scales: ", config.RPN_ANCHOR_SCALES)
print("ratios: ", config.RPN_ANCHOR_RATIOS)
print("Anchors per Cell: ", anchors_per_cell)
print("Levels: ", num_levels)
anchors_per_level = []
for l in range(num_levels):
num_cells = config.BACKBONE_SHAPES[l][0] * config.BACKBONE_SHAPES[l][1]
anchors_per_level.append(anchors_per_cell * num_cells // config.RPN_ANCHOR_STRIDE ** 2)
print("Anchors in Level {}: {}".format(l, anchors_per_level[l]))
总共给结果:
# 总共anchor数量
Count: 261888
Scales: (32, 64, 128, 256, 512)
ratios: [0.5, 1, 2]
Anchors per Cell: 3
Levels: 5
# 第一层特征图的anchor数量
Anchors in Level 0: 196608
Anchors in Level 1: 49152
Anchors in Level 2: 12288
Anchors in Level 3: 3072
Anchors in Level 4: 768
2、数据集的准备阶段结果分析
- (1)对于RPN产生的261888去做感兴趣区域计算得到默认200个输入到msrcnn中。
- (2)对默认标记的RPN样本统计正负样本,正样本位置进行refine显示。总共256个
- (3)ROIs感兴趣区域会进行正负样本标记。总共200个
(1)获取anchor的代码以及进行获取感兴趣区域结果
- model.DataGenerator会在内部返回结果
# 4、anchor到rois感兴趣区域
from mrcnn import model
random_rois = 2000
# 获取4个数据测试看结果
g = model.DataGenerator(dataset_train, config,
shuffle=True,
random_rois=random_rois,
detection_targets=True)
# 针对数据集的GT计算得到rpn的预测框以及mrcnn的输出预测框
if random_rois:
[normalized_images, image_meta, rpn_match, rpn_bbox, gt_class_ids, gt_boxes, gt_masks, rpn_rois, rois], \
[mrcnn_class_ids, mrcnn_bbox, mrcnn_mask] = g.__getitem__(0)
# 打印rois以及mrcnn
log("rois", rois)
log("mrcnn_class_ids", mrcnn_class_ids)
log("mrcnn_bbox", mrcnn_bbox)
log("mrcnn_mask", mrcnn_mask)
# 打印GT结果
log("gt_class_ids", gt_class_ids)
log("gt_boxes", gt_boxes)
log("gt_masks", gt_masks)
log("rpn_match", rpn_match, )
log("rpn_bbox", rpn_bbox)
image_id = image_meta[0][0]
print("image_id: ", image_id)
打印输出结果
# 1、RPN的anchor过滤之后传入maskrcnn阶段感兴趣区域,这里2指的样本数默认最小返回数量
rois shape: (2, 200, 4) min: 0.00000 max: 1021.00000 int32
mrcnn_class_ids shape: (2, 200, 1) min: 0.00000 max: 1.00000 int32
mrcnn_bbox shape: (2, 200, 2, 4) min: -3.46591 max: 2.96960 float32
mrcnn_mask shape: (2, 200, 28, 28, 2) min: 0.00000 max: 1.00000 float32
# 2、msrcnn最终会100个框
gt_class_ids shape: (2, 100) min: 0.00000 max: 1.00000 int32
gt_boxes shape: (2, 100, 4) min: 0.00000 max: 985.00000 int32
gt_masks shape: (2, 56, 56, 100) min: 0.00000 max: 1.00000 bool
# 4、rpn的anchor标记结果
rpn_match shape: (2, 261888, 1) min: -1.00000 max: 1.00000 int32
# 每张图RPN 使用256个样本,
rpn_bbox shape: (2, 256, 4) min: -1.95943 max: 1.38107 float64
# 此图片ID
image_id: 17.0
(2)然后对于标记之后的结果,做正负样本数量统计,并且对于正样本的数据做微调之后结果打印在图片中。负样本同时也打印在图片中显示出来。
# 5、对于其中一张图片进行anchor的坐标转换显示
# 获取正负样本匹配结果
b = 0
positive_anchor_ids = np.where(rpn_match[b] == 1)[0]
print("Positive anchors: {}".format(len(positive_anchor_ids)))
negative_anchor_ids = np.where(rpn_match[b] == -1)[0]
print("Negative anchors: {}".format(len(negative_anchor_ids)))
neutral_anchor_ids = np.where(rpn_match[b] == 0)[0]
print("Neutral anchors: {}".format(len(neutral_anchor_ids)))
# 对于标记为正样本anchor进行位置refine计算
indices = np.where(rpn_match[b] == 1)[0]
refined_anchors = utils.apply_box_deltas(anchors[indices], rpn_bbox[b, :len(indices)] * config.RPN_BBOX_STD_DEV)
log("anchors", anchors)
log("refined_anchors", refined_anchors)
# 获取其中默认第一张图片的数据,打印正样本标记结果和负样本标记结果
sample_image = model.unmold_image(normalized_images[b], config)
# ROI的类别数量
for c, n in zip(dataset_train.class_names, np.bincount(mrcnn_class_ids[b].flatten())):
if n:
print("{:23}: {}".format(c[:20], n))
# 展示正样本输出结果
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, figsize=(16, 16))
visualize.draw_boxes(sample_image, boxes=anchors[positive_anchor_ids],
refined_boxes=refined_anchors, ax=ax)
输出结果:
# RPN标记正样本数量 10+246=256
Positive anchors: 10
# RPN标记负样本数量
Negative anchors: 246
# RPN标记无效框数量
Neutral anchors: 261632
# anchors总数量
anchors shape: (261888, 4) min: -362.03867 max: 1322.03867 float64
# 正样本进行refined之后的anchor数量
refined_anchors shape: (10, 4) min: 1.00000 max: 826.00000 float32
# 其中这200个bbox框的类别结果
# 背景数量
BG : 176
# 气球数量
balloon : 24
效果,这是我们对于训练数据中一张图片之后的筛选的结果
- 显示负样本标记的246个结果
# 展示负样本输出
visualize.draw_boxes(sample_image, boxes=anchors[negative_anchor_ids])
这里换了一张图(所以负样本数量不一定是上面的246)
其中没有标记的anchor是不会参与网络训练的
(3)Rois:msrcnn的感兴趣区域标记显示
print("Positive ROIs: ", mrcnn_class_ids[b][mrcnn_class_ids[b] > 0].shape[0])
print("Negative ROIs: ", mrcnn_class_ids[b][mrcnn_class_ids[b] == 0].shape[0])
print("Positive Ratio: {:.2f}".format(
mrcnn_class_ids[b][mrcnn_class_ids[b] > 0].shape[0] / mrcnn_class_ids[b].shape[0]))
结果为
Positive ROIs: 27
Negative ROIs: 173
Positive Ratio: 0.14
6.9.1.6 案例:模型加载训练过程代码编写
创建模型判断参数如果是训练,调用训练模型
# 3、创建模型
if args.command == "train":
model = maskrcnn.MaskRCNN(mode="training", config=config,
model_dir=args.logs)
else:
model = maskrcnn.MaskRCNN(mode="inference", config=config,
model_dir=args.logs)
# 4、训练测试逻辑实现
if args.command == "train":
# 选择加载的预训练模型类别并下载
if args.weights.lower() == "imagenet":
weights_path = model.get_imagenet_weights()
else:
raise ValueError("提供一种预训练模型种类")
# 加载预训练模型权重
print("Loading weights ", weights_path)
model.load_weights(weights_path, by_name=True)
# 进行训练
train(model)
(1)加载预训练模型,模型中提供了多种预训练读取使用方法,在这里我们使用imagenet的模型读取
- model.get_imagenet_weights():msrcnn模型中封装的此方法会指定模型下载
- 然后通过model.load_weights加载模型权重(此方法也是msrcnn模型本身封装的函数)
当指定好预训练模型的时候,会从一些官方释放的预训练模型路径中下载,下载到本地:/root/.keras/models/resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5路径中,所以确保家目录下的.keras有足够的空间存储模型。
Downloading data from https://github.com/fchollet/deep-learning-models/releases/download/v0.2/resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5
94658560/94653016 [==============================] - 472s 5us/step
Loading weights /root/.keras/models/resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5
注:或者指定COCO预训练路径
weights_path = COCO_WEIGHTS_PATH
通过msrcnn中的utils中下面方法获取权重加载到模型中
utils.download_trained_weights(weights_path)
(2)其中train函数中是数据读取和模型训练代码
def train(model):
"""训练模型逻辑
:param model: maskrcnn模型
:return:
"""
# 1、获取分割数据集
dataset_train = BalloonDataset()
dataset_train.load_balloon(args.dataset, "train")
dataset_train.prepare()
# 2、获取分割验证数据集
dataset_val = BalloonDataset()
dataset_val.load_balloon(args.dataset, "val")
dataset_val.prepare()
# 3、开始训练
print("开始训练网络:")
model.train(dataset_train, dataset_val,
learning_rate=config.LEARNING_RATE,
epochs=20,
layers='heads')
模型训练保存到./logs/中模型文件,这里提供了训练好的版本mask_rcnn_balloon.h5。方便进行测试使用。
6.9.1.7 案例:maskrcnn网络结构流程源码分析
其中源码中导入会有些库进行简写
import tensorflow.keras as keras
import tensorflow.keras.backend as K
import tensorflow.keras.layers as KL
import tensorflow.keras.layers as KE
import tensorflow.keras.utils as KU
import tensorflow.keras.models as KM
1、构造输出
input_image = KL.Input(
shape=[None, None, config.IMAGE_SHAPE[2]], name="input_image")
input_image_meta = KL.Input(shape=[config.IMAGE_META_SIZE],
name="input_image_meta")
2、如果训练的话,构造RPN层的anchor输入样本及其位置、构造masrcnn的GT输入,并对坐标进行normalize,如果使用了USE_MINI_MASK=True,那么input_gt_masks就必须是配置文件中的[56, 56]大小
# RPN GT
input_rpn_match = KL.Input(
shape=[None, 1], name="input_rpn_match", dtype=tf.int32)
input_rpn_bbox = KL.Input(
shape=[None, 4], name="input_rpn_bbox", dtype=tf.float32)
# Detection GT (class IDs, bounding boxes, and masks)
# 1. GT Class IDs (zero padded)
input_gt_class_ids = KL.Input(
shape=[None], name="input_gt_class_ids", dtype=tf.int32)
# 2. GT Boxes in pixels (zero padded)
# [batch, MAX_GT_INSTANCES, (y1, x1, y2, x2)] in image coordinates
input_gt_boxes = KL.Input(
shape=[None, 4], name="input_gt_boxes", dtype=tf.float32)
# Normalize coordinates
gt_boxes = KL.Lambda(lambda x: norm_boxes_graph(
x, K.shape(input_image)[1:3]))(input_gt_boxes)
# 3. GT Masks (zero padded)
# [batch, height, width, MAX_GT_INSTANCES]
if config.USE_MINI_MASK:
input_gt_masks = KL.Input(
shape=[config.MINI_MASK_SHAPE[0],
config.MINI_MASK_SHAPE[1], None],
name="input_gt_masks", dtype=bool)
else:
input_gt_masks = KL.Input(
shape=[config.IMAGE_SHAPE[0], config.IMAGE_SHAPE[1], None],
name="input_gt_masks", dtype=bool)
3、如果测试,直接构造anchors的输入
# Anchors in normalized coordinates
input_anchors = KL.Input(shape=[None, 4], name="input_anchors")
4、构造前面Resnet网络输入,输出
_, C2, C3, C4, C5 = resnet_graph(input_image, config.BACKBONE,
stage5=True, train_bn=config.TRAIN_BN)
5、Reset多级特征输出,经过FPN得到5层特征输出P2、P3、P4、P5、P6
# Top-down Layers
# TODO: add assert to varify feature map sizes match what's in config
P5 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c5p5')(C5)
P4 = KL.Add(name="fpn_p4add")([
KL.UpSampling2D(size=(2, 2), name="fpn_p5upsampled")(P5),
KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c4p4')(C4)])
P3 = KL.Add(name="fpn_p3add")([
KL.UpSampling2D(size=(2, 2), name="fpn_p4upsampled")(P4),
KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c3p3')(C3)])
P2 = KL.Add(name="fpn_p2add")([
KL.UpSampling2D(size=(2, 2), name="fpn_p3upsampled")(P3),
KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c2p2')(C2)])
# Attach 3x3 conv to all P layers to get the final feature maps.
P2 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p2")(P2)
P3 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p3")(P3)
P4 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p4")(P4)
P5 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p5")(P5)
# P6 is used for the 5th anchor scale in RPN. Generated by
# subsampling from P5 with stride of 2.
P6 = KL.MaxPooling2D(pool_size=(1, 1), strides=2, name="fpn_p6")(P5)
6、构造RPN的输入以及maskrcnn的输入
# Note that P6 is used in RPN, but not in the classifier heads.
rpn_feature_maps = [P2, P3, P4, P5, P6]
mrcnn_feature_maps = [P2, P3, P4, P5]
7、如果是训练,就获取每一层若干anchors合并(就是前面演示的结果),做形状改变。测试直接获取input_anchors
# Anchors
if mode == "training":
anchors = self.get_anchors(config.IMAGE_SHAPE)
# Duplicate across the batch dimension because Keras requires it
# TODO: can this be optimized to avoid duplicating the anchors?
anchors = np.broadcast_to(anchors, (config.BATCH_SIZE,) + anchors.shape)
# A hack to get around Keras's bad support for constants
anchors = KL.Lambda(lambda x: tf.Variable(anchors), name="anchors")(input_image)
else:
anchors = input_anchors
8、构造RPN网络,对每一个特征图,都做输入得到输出结果,最终得到RPN网络的输出概率、类别以及网络预测bbox框
# RPN Model
rpn = build_rpn_model(config.RPN_ANCHOR_STRIDE,
len(config.RPN_ANCHOR_RATIOS), config.TOP_DOWN_PYRAMID_SIZE)
config.TOP_DOWN_PYRAMID_SIZE)
# Loop through pyramid layers
layer_outputs = [] # list of lists
for p in rpn_feature_maps:
layer_outputs.append(rpn([p]))
output_names = ["rpn_class_logits", "rpn_class", "rpn_bbox"]
outputs = list(zip(*layer_outputs))
outputs = [KL.Concatenate(axis=1, name=n)(list(o))
for o, n in zip(outputs, output_names)]
rpn_class_logits, rpn_class, rpn_bbox = outputs
9、产生proposals建议框,根据anchors和网络预测输出,产生配置指定过滤到2000个
- ROIs kept after non-maximum suppression (training and inference)
- POST_NMS_ROIS_TRAINING = 2000
- POST_NMS_ROIS_INFERENCE = 1000
proposal_count = config.POST_NMS_ROIS_TRAINING if mode == "training"\
else config.POST_NMS_ROIS_INFERENCE
rpn_rois = ProposalLayer(
proposal_count=proposal_count,
nms_threshold=config.RPN_NMS_THRESHOLD,
name="ROI",
config=config)([rpn_class, rpn_bbox, anchors])
10、如果是训练过程
- DetectionTargetLayer(config, name="proposal_targets")
- 对于建议框,以及输入的GT结果,产生用于目标区域框、类别、位置、mask
- 设置成config.MASK_SHAPE=[28, 28]
if mode == "training":
....
# Generate detection targets
# Subsamples proposals and generates target outputs for training
# Note that proposal class IDs, gt_boxes, and gt_masks are zero
# padded. Equally, returned rois and targets are zero padded.
rois, target_class_ids, target_bbox, target_mask =\
DetectionTargetLayer(config, name="proposal_targets")([
target_rois, input_gt_class_ids, gt_boxes, input_gt_masks])
11、Network Heads(第二阶段的分类、回归、mask)
- 第二阶段maskrcnn的fpn_classifier_graph函数输入感兴趣区域进行计算分类回归输出
- Builds the computation graph of the feature pyramid network classifier and regressor heads.
- 第二阶段maskrcnn的mask分支,得到mrcnn_mask输出结果
- MASK_POOL_SIZE=[14, 14]
# TODO: verify that this handles zero padded ROIs
#Returns:
# logits: [batch, num_rois, NUM_CLASSES] classifier logits (before softmax)
# probs: [batch, num_rois, NUM_CLASSES] classifier probabilities
# bbox_deltas: [batch, num_rois, NUM_CLASSES, (dy, dx, log(dh), log(dw))] Deltas to apply to
# proposal boxes
mrcnn_class_logits, mrcnn_class, mrcnn_bbox =\
fpn_classifier_graph(rois, mrcnn_feature_maps, input_image_meta,
config.POOL_SIZE, config.NUM_CLASSES,
train_bn=config.TRAIN_BN,
fc_layers_size=config.FPN_CLASSIF_FC_LAYERS_SIZE)
# Builds the computation graph of the mask head of Feature Pyramid Network.
# Returns: Masks [batch, num_rois, MASK_POOL_SIZE, MASK_POOL_SIZE, NUM_CLASSES]
mrcnn_mask = build_fpn_mask_graph(rois, mrcnn_feature_maps,
input_image_meta,
config.MASK_POOL_SIZE,
config.NUM_CLASSES,
train_bn=config.TRAIN_BN)
12、损失计算以及模型最终构建
# TODO: clean up (use tf.identify if necessary)
output_rois = KL.Lambda(lambda x: x * 1, name="output_rois")(rois)
# Losses
rpn_class_loss = KL.Lambda(lambda x: rpn_class_loss_graph(*x), name="rpn_class_loss")(
[input_rpn_match, rpn_class_logits])
rpn_bbox_loss = KL.Lambda(lambda x: rpn_bbox_loss_graph(config, *x), name="rpn_bbox_loss")(
[input_rpn_bbox, input_rpn_match, rpn_bbox])
class_loss = KL.Lambda(lambda x: mrcnn_class_loss_graph(*x), name="mrcnn_class_loss")(
[target_class_ids, mrcnn_class_logits, active_class_ids])
bbox_loss = KL.Lambda(lambda x: mrcnn_bbox_loss_graph(*x), name="mrcnn_bbox_loss")(
[target_bbox, target_class_ids, mrcnn_bbox])
mask_loss = KL.Lambda(lambda x: mrcnn_mask_loss_graph(*x), name="mrcnn_mask_loss")(
[target_mask, target_class_ids, mrcnn_mask])
# Model
inputs = [input_image, input_image_meta,
input_rpn_match, input_rpn_bbox, input_gt_class_ids, input_gt_boxes, input_gt_masks]
if not config.USE_RPN_ROIS:
inputs.append(input_rois)
outputs = [rpn_class_logits, rpn_class, rpn_bbox,
mrcnn_class_logits, mrcnn_class, mrcnn_bbox, mrcnn_mask,
rpn_rois, output_rois,
rpn_class_loss, rpn_bbox_loss, class_loss, bbox_loss, mask_loss]
model = KM.Model(inputs, outputs, name='mask_rcnn')
6.9.2 模型预测流程
这里重点在于对图片的读取预测和显示。另外也提供了opencv读取视频分割的结果(了解流程即可)
完成过程代码如下
from utils.draw_segmention_utils import detect_and_draw_segmentation
elif args.command == "test":
model.load_weights(args.model, by_name=True)
# 进行检测
detect_and_draw_segmentation(model,
image_path=args.image,
video_path=args.video)
else:
print("'{}' 传入参数无法识别. "
"请使用 'train' or 'test'".format(args.command))
其中detect_and_draw_segmentation()方法中提供了对图片或者视频的预测标记显示过程。
我们在根目录中创建的utils目录中添加一个draw_segmention_utils.py的文件,用于预测流程中的绘制图片与视频的工具函数
- 主函数逻辑:
- 判断参数传入是否是图片还是视频,分别处理(使用skimage模块读取处理)
- 1、图片读取、检测结果、分割区域绘制、保存输出
- 2、视频的读取、读取每一帧绘制每一帧结果、返回数据到指定视频目录(了解过程)
import numpy as np
import skimage
def detect_and_draw_segmentation(args, model):
"""
检测结果并画出分割区域
:param args: 命令行参数
:param model: 模型
:return:
"""
if not args.image or not args.video:
raise ValueError("请提供要检测的图片或者视频路径之一")
# 传入的图片
if args.image:
print("正在分割图片:{}".format(args.image))
# 1、读取图片
image = skimage.io.imread(args.image)
# 2、模型检测返回结果
r = model.detect([image], verbose=1)[0]
# 3、画出分割区域
segmentation = draw_segmentation(image, r['masks'])
# 4、保存输出
file_name = "./images/segment_{}".format(args.image.split("/")[-1])
skimage.io.imsave(file_name, segmentation)
if args.video:
import cv2
# 1、获取视频的读取
vcapture = cv2.VideoCapture(args.video)
width = int(vcapture.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(vcapture.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = vcapture.get(cv2.CAP_PROP_FPS)
# 2、定义video writer后续写入
file_name = "./images/segmentation_{}".format(args.video.split("/")[-1])
vwriter = cv2.VideoWriter(file_name,
cv2.VideoWriter_fourcc(*'mp4v'),
fps, (width, height))
# 3、循环获取每帧数据进行处理,完成之后写入本地文件
count = 0
success = True
while success:
print("帧数: ", count)
# 读取图片
success, image = vcapture.read()
if success:
# OpenCV 返回的BGR格式转换成RGB
image = image[..., ::-1]
# 模型检测mask
r = model.detect([image], verbose=0)[0]
# 画出区域
segmentation = draw_segmentation(image, r['masks'])
# RGB -> BGR
segmentation = segmentation[..., ::-1]
# 添加这张图到video writer
vwriter.write(segmentation)
count += 1
vwriter.release()
print("保存到检测结果到路径文件:", file_name)
其中涉及到对每一张图片的分割区域以及图片绘制
def draw_segmentation(image, mask):
"""
对图片进行分割区域的标记
:param image: 输出图片 RGB image [height, width, 3]
:param mask: 分割区域[height, width, instance count]
:return: 返回黑白图片,并且将分割区域保留原来的颜色
"""
# 1、将彩色图片变成灰度图,并保留image以及同份灰色的图片
# 这里经过两次转变目的,gray必须有三个通道才能与后面np.where(mask, image, gray)进行设置得到segmentation
gray = skimage.color.gray2rgb(skimage.color.rgb2gray(image)) * 255
# 2、将彩色格式中mask部分保留其余部分都设置成gray
if mask.shape[-1] > 0:
# 如果多个物体,要将预测结果的多个物体的mask相加,得到一张mask
mask = (np.sum(mask, -1, keepdims=True) >= 1)
# 讲Mask中为1的设置成图片原色,0的设置成gray对应的
segmentation = np.where(mask, image, gray).astype(np.uint8)
else:
segmentation = gray.astype(np.uint8)
return segmentation
使用函数解释
from skimage import io, data, color
# 一张彩色图片转换为灰度图后,它的类型就由unit8变成了float
img_gray = color.rgb2gray(img)
# 再次转换会有损失的
image2 = color.gray2rgb(img_gray)
# API
skimage.color.gray2rgb(image, alpha=None)[source]
Create an RGB representation of a gray-level image.
Parameters
Input
image of shape (M[, N][, P]).
Returns
rgbndarray
RGB image of shape (M[, N][, P], 3).
- 图像数据类型
在skimage中,一张图片以numpy数组形式存储,数组的数据类型有很多中,相互之间可以转换,数据类型以及取值范围如下表所示
数据类型 数值范围
uint8 0 to 255
uint16 0 to 65535
float16 半精度浮点数:16位,正负号1位,指数5位,精度10位
float32 单精度浮点数:32位,正负号1位,指数8位,精度23位
float64 双精度浮点数:64位,正负号1位,指数11位,精度52位
我们读取提供的测试文件如项目image目录下两个文件
parser.add_argument('--image', type=str, default='./images/2917282960_06beee649a_b.jpg',
help='需要进行检测分割的图片目录')
parser.add_argument('--video', type=str, default='./images/v0200fd10000bq043q9pskdh7ri20vm0.MP4',
help='需要进行检测分割的视频目录')
最终测试结果如项目之前所示(当然如果有需要可以使用前面介绍的绘制bbox工具,将预测框也绘制出来):
6.9.3 小结
- maskrcnn中的源码编译训练流程
- maskrcnn中的anchor设置以及计算
- 模型的训练预测流程
- 完成maskrcnn指定气球分割数据集模型训练
- 完成maskrcnn指定图片或视频预测输出
====================================
=========== 训练配置 =========
====================================
#要使用的GPU数。仅使用CPU时,需要将其设置为1。
# 如果设置大于1,多个GPU进行并行运算,源码中parallel_model.py使用1.xAPI进行多GPU计算
GPU_COUNT = 1
#一个12GB的GPU通常可以处理两个1024x1024px的图像。
#根据GPU内存和图像大小进行调整。使用你的GPU可以处理的最高的数字来获得最佳性能。
# 每个GPU训练的图片数量
IMAGES_PER_GPU = 2
#这不需要匹配训练集的大小。
#Tensorboard更新保存在每个历元的末尾,
#因此,将此值设置为较小的数字意味着获得更频繁的TensorBoard更新。
#验证统计数据也在每个EPOCH结束时计算,它们可能需要一段时间,
#所以不要设置得太小,以免在验证统计数据上花费太多时间。
# 一个epoch的训练步数
STEPS_PER_EPOCH = 1000
#在每个训练阶段结束时要运行的验证步骤数。
#更大的数字可以提高验证统计的准确性,但会减慢训练速度。
VALIDATION_STEPS = 50
#支持的值为:resnet50、resnet101。
#您还可以提供一个应具有model.resnet_graph签名的可调用。
#如果这样做,则还需要提供一个可调用的计算主干形状
# 主网络架构
BACKBONE = "resnet101"
# 基于resnet101架构的图像金字塔FPN到达的每层 步长
BACKBONE_STRIDES = [4, 8, 16, 32, 64]
#分类图中完全连接层fully-connected layers的大小
FPN_CLASSIF_FC_LAYERS_SIZE = 1024
#用于构建特征top-down layers层的大小
TOP_DOWN_PYRAMID_SIZE = 256
# 总类别个数 (including background)
NUM_CLASSES = 1 # Override in sub-classes
# RPN anchor的面积根号设置
RPN_ANCHOR_SCALES = (32, 64, 128, 256, 512)
#值1表示方锚,值0.5表示宽锚
# anchor的比率用于设置长宽 (width/height)
RPN_ANCHOR_RATIOS = [0.5, 1, 2]
#如果为1,则为主干要素地图中的每个单元创建定位。
#如果为2,则为每个其他单元格创建定位,依此类推。
# Anchor 设置步长,如果为2跳过一些特征层设置anchor
#1表示每个像素设置Anchor
RPN_ANCHOR_STRIDE = 1
#你可以在训练中增加这个值来产生更多的建议框。
# 过滤RPN proposals的NMS阈值,值越大产生更多的建议框
RPN_NMS_THRESHOLD = 0.7
# 每张图产生多少anchors用于RPN training
RPN_TRAIN_ANCHORS_PER_IMAGE = 256
# NMS之前的感兴趣区域(ROI)的数量:经过tf.nn.top_k筛选之后并且在 non-maximum suppression进行之前的ROIs 数量
PRE_NMS_LIMIT = 6000
# NMS之后的感兴趣区域(ROI)的数量:在non-maximum suppression ROIs 的数量(training and inference)
POST_NMS_ROIS_TRAINING = 2000 #训练
POST_NMS_ROIS_INFERENCE = 1000 #预测
#square: 调整大小并用零填充以获得大小为[max_dim,max_dim]的方形图像。
# Input image resizing,默认square模式,设置成[max_dim, max_dim]
IMAGE_RESIZE_MODE = "square"
IMAGE_MIN_DIM = 800
IMAGE_MAX_DIM = 1024
#Mask RCNN paper使用512,但RPN通常没有产生足够的积极建议来填补这一点,并保持1:3的正负比。
#您可以通过调整RPN NMS阈值来增加建议数。
# 每个image提供给classifier/mask heads中的rois数量
TRAIN_ROIS_PER_IMAGE = 200
# ROIs 用于训练 classifier/mask heads的正样本比率
ROI_POSITIVE_RATIO = 0.33
# ROIs池化层大小
POOL_SIZE = 7
MASK_POOL_SIZE = 14
#要改变这一点,还需要改变神经网络mask掩码分支
# 输出mask的大小
MASK_SHAPE = [28, 28]
# 每张图的GT实例数量的最大值
MAX_GT_INSTANCES = 100
====================================
========= 检测的配置 =========
====================================
# 最后测试检测的时候实例数量100
DETECTION_MAX_INSTANCES = 100
#接受检测到的实例的最小概率值
#跳过低于此阈值的ROI
DETECTION_MIN_CONFIDENCE = 0.7
# 用于检测的Non-maximum suppression阈值
DETECTION_NMS_THRESHOLD = 0.3
# 3、学习率设置相关设置
LEARNING_RATE = 0.001
LEARNING_MOMENTUM = 0.9
WEIGHT_DECAY = 0.0001
# 损失计算公式分配权重
LOSS_WEIGHTS = {
"rpn_class_loss": 1.,
"rpn_bbox_loss": 1.,
"mrcnn_class_loss": 1.,
"mrcnn_bbox_loss": 1.,
"mrcnn_mask_loss": 1.
}
# 梯度裁剪:梯度截断值
GRADIENT_CLIP_NORM = 5.0
==========================================================================================
# 过滤RPN proposals的NMS阈值,值越大产生更多的建议框
# NMS阈值:0.7以上的锚框都会被丢弃掉,0.7以下的锚框进行下一轮比较,因此阈值越大,被丢弃的锚框就会越少,
# 即产生更多的建议框
RPN_NMS_THRESHOLD = 0.7
非极大值抑制(NMS)
1.输入数据:
通过SVM分类器对每个锚框分类好之后,每个锚框都带上了预测类别标签值和该预测类别的置信度score,最终每个锚框都放到对应的类别列表中。
2.迭代过程:
对每个分类列表中的锚框进行处理,比如对某个类别的列表中所有锚框根据其预测类别的置信度score按从大到小进行排序,
首先类别的列表中取出第一个score值最大的锚框放到输出列表中,然后类别的列表中剩余的所有锚框逐一和输出列表中第一个锚框进行计算IoU值(交并比),
把IoU值>0.5的锚框都丢弃掉,只留下IoU值<0.5的锚框继续进行下一轮比较。
下一轮比较中,仍然先把分类列表中剩余的(score值最大)第一个锚框放到输出列表中,
然后分类列表中剩余的所有锚框再和输出列表中最后添加进去的锚框进行计算IoU值(交并比),
同样的把IoU值>0.5的锚框都丢弃掉,只留下IoU值<0.5的锚框,以此类推继续进行下一轮比较。
==========================================================================================
class DataGenerator(KU.Sequence):
def init(self, dataset, config, shuffle=True, augmentation=None,random_rois=0, detection_targets=False):
对于传入的Dataset对象建立序列数据,提供每批次数据给训练器
主要根据配置文件产生RPN网络相应的anchor先验框的坐标
#1.通过generate_pyramid_anchors函数生成RPN网络众多的先验框,每个特征图每个像素点预测多少个先验框
#2.每个特征图每个像素点默认有:5种尺度(32, 64, 128, 256, 512),3种比率[0.5, 1, 2],一共15种类型大小的先验框
#3.resnet默认有5个特征图,每个特征图每个像素点默认预测3个先验框
# 产生第1层特征图anchor数量:196608
# 产生第2层特征图anchor数量:49152
# 产生第3层特征图anchor数量:12288
# 产生第4层特征图anchor数量:3072
# 产生第5层特征图anchor数量:768
self.anchors = utils.generate_pyramid_anchors(config.RPN_ANCHOR_SCALES,
config.RPN_ANCHOR_RATIOS,
self.backbone_shapes,
config.BACKBONE_STRIDES,
config.RPN_ANCHOR_STRIDE)
def getitem(self, idx):
#1.build_rpn_targets函数实现的内容:
# 根据上面resnet每层生成的众多anchor先验框输入到函数中,以及输入数据集的目标值(bbox),
# 进行正负样本分配,即得知某一个anchor先验框属于某一个类别,
# 然后还要根据上述分配好的正负样本 进行真实GT和anchor先验框(源码中叫bbox deltas)之间的偏移值计算,
# 即得出bbox框的偏移结果,然后把偏移值保存在rpn_bbox中,那么rpn_bbox保存了所有的bbox框的偏移结果,
# 最终这些数据作为RPN网络的输入之一,即作为getitem的返回值inputs所包含的数据之一。
#2.在做网络损失计算的时候,所使用的目标值并不是GT,也不是anchor先验框,而是GT和anchor先验框之间所计算的偏移值结果。
#对于目标GT以及RPN的众多acnhor,进行正负样本匹配,并且将转换极坐标到中心坐标
rpn_match, rpn_bbox = build_rpn_targets(image.shape, self.anchors, gt_class_ids, gt_boxes, self.config)
---------------------------------------
上述GT和anchor先验框之间的偏移值结果的计算公式流程如下
# GT坐标变化公式
gt_h = gt[2] - gt[0]
gt_w = gt[3] - gt[1]
gt_center_y = gt[0] + 0.5 * gt_h
gt_center_x = gt[1] + 0.5 * gt_w
# Anchor的坐标变换
a_h = a[2] - a[0]
a_w = a[3] - a[1]
a_center_y = a[0] + 0.5 * a_h
a_center_x = a[1] + 0.5 * a_w
# 保存偏移结果
rpn_bbox[ix] = [
(gt_center_y - a_center_y) / a_h,
(gt_center_x - a_center_x) / a_w,
np.log(gt_h / a_h),
np.log(gt_w / a_w),
]
---------------------------------------
#1.训练阶段:
# getitem在执行完build_rpn_targets函数之后就结束了,最终把inputs作为RPN网络的输入进行训练,outputs值为空。
# inputs(相当于目标值、作为RPN网络的输入) 还要和 RPN网络输出预测的bbox(真实GT和anchor先验框的偏移值)进行损失计算。
#2.测试使用
# 当传入random_rois是大于0的值,那么返回的outputs值不为空,即需要返回outputs时,
# 就会调用build_detection_targets函数返回感兴趣区域rois以及感兴趣区域的bboxes框位置和mask封装到outputs列表中。
# 注意训练阶段不会调用build_detection_targets。outputs用于提供给我们进行调试来看第二阶段maskrcnn的输入输出情况,
# 此时会调用build_detection_targets函数,函数输入rpn_rois区域,并根据参数配置的TRAIN_ROIS_PER_IMAGE = 200,
# 参数代表每张图提供给classifier分类器/mask heads中的rois数量,即一张图片只产生200个ROIs感兴趣区域给maskrcnn训练。
#
#1.inputs:输入到RPN网络进行训练
#2.outputs:random_rois默认为0,那么outputs默认返回为空。
# 测试使用(random_rois初始化值大于0):训练期间不使用,用作调试debug使用或者单独训练不带有RPN网络的maskrcnn结构时使用。
# outputs列表封装了build_detection_targets函数生成的感兴趣区域rois以及感兴趣区域的bboxes框位置和mask。
# outputs用作提供给我们进行调试来看第二阶段maskrcnn的输入输出情况。
return inputs, outputs
getitem函数返回值:
1.inputs:该数据输入到RPN网络
inputs返回值包含如下
- images: [batch, H, W, C]
- image_meta: [batch, (meta data)] Image details.
meta = np.array(
[image_id] + # size=1
list(original_image_shape) + # size=3
list(image_shape) + # size=3
list(window) + # size=4 (y1, x1, y2, x2) in image cooredinates
[scale] + # size=1
list(active_class_ids) # size=num_classes
)
- rpn_match: [batch, N] Integer (1=positive anchor, -1=negative, 0=neutral) #正负样本分类值
- rpn_bbox: [batch, N, (dy, dx, log(dh), log(dw))] Anchor bbox deltas # anchor与GT进行偏移的值
- gt_class_ids: [batch, MAX_GT_INSTANCES] # GTclass IDs
- gt_boxes: [batch, MAX_GT_INSTANCES, (y1, x1, y2, x2)] # GT物体框位置
- gt_masks: [batch, height, width, MAX_GT_INSTANCES]. # GT mask目标值
2.outputs:
# outputs 默认为空,如果提供random_rois 大于0的参数
ouptuts=[batch_mrcnn_class_ids, batch_mrcnn_bbox, batch_mrcnn_mask]
1.网络训练的时候只需要inputs即可。outputs输出默认为空,但是如果提供random_rois的值大于0的参数。
那么DataGenerator将会过滤之后返回整个网络中第二阶段maskrcnn的需要的RoIs感兴趣框相关信息
2.训练期间不使用,是用作调试debug使用或者单独训练不带有RPN网络的maskrcnn结构时时使用
如果需要返回outputs就会调用该函数返回:build_detection_targets(rpn_rois, gt_class_ids, gt_boxes, gt_masks, config):
# 输入 Inputs:
rpn_rois: [N, (y1, x1, y2, x2)] proposal boxes.
gt_class_ids: [instance count] Integer class IDs
gt_boxes: [instance count, (y1, x1, y2, x2)]
gt_masks: [height, width, instance count] Ground truth masks. Can be full
size or mini-masks.
#输出 Returns:
# 感兴趣区域以及感兴趣区域的框位置和mask
# 参数配置的TRAIN_ROIS_PER_IMAGE = 200,一张图片只产生200个ROIs感兴趣区域给maskrcnn训练
#要馈送到分类器classifier / mask heads的每个图像的ROIs数
#Mask RCNN paper 使用512,
#但RPN通常没有产生足够的positive proposals积极建议来填补这一点来保持1:3的positive:negative正负比。
#可以通过调整RPN NMS threshold阈值来增加建议数。
rois: [TRAIN_ROIS_PER_IMAGE, (y1, x1, y2, x2)]
class_ids: [TRAIN_ROIS_PER_IMAGE]. Integer class IDs.
bboxes: [TRAIN_ROIS_PER_IMAGE, NUM_CLASSES, (y, x, log(h), log(w))]. Class-specific
bbox refinements.
masks: [TRAIN_ROIS_PER_IMAGE, height, width, NUM_CLASSES). Class specific masks cropped
to bbox boundaries and resized to neural network output size.
==========================================================================================
RPN网络
先对主干网络resnet101输出的每个特征图的像素生成3个候选框,然后通过RPN网络比较所有候选框与GT目标进行IoU判断,
标记256个正负样本继续用于RPN网络训练,RPN网络实际做的是softmax二分类层(包含目标/不包含目标的背景)实现cls层,
用logistic回归对正样本候选框和GT目标框继续进行偏移值计算最终回归输出预测框实现reg层。
最终RPN网络输出200个正负样本候选框(包含目标/不包含目标的背景)提供给mrcnn进行训练,正样本占其中全部样本的的比率是0.3。
mrcnn网络
200个正负样本候选框分别输入到分类cls层(每个ROI区域Softmax多分类分配GT目标类别)/逻辑回归reg层(检测框逻辑回归GT目标框)和mask分支,
mask通过像素级Sigmoid和二值化损失的手段对每个ROI区域的每个像素进行Sigmoid二分类输出,
判断每个像素是否属于ROI区域所分配的GT目标类别,即该ROI区域已经在分类cls层通过Softmax多分类分配了一个GT目标类别,
那么然后在mask分支中通过Sigmoid二分类对该ROI区域中的每个像素值进行判断该像素值是否属于该被赋予的GT目标类别。