基于MMDet的cascade MASKRCNN 入门

目录

目录

一、背景介绍

二、数据集制作

三、环境搭建

四、修改源码以训练自己的数据

五、训练

六、测试

七、可视化loss、acc

八、梯度爆炸

九、后记


一、背景介绍

       场景需要训练一个分割模型,参考这个场景大多数人的选择,决定用mmdet的maskrcnn,再进阶到cascade maskrcnn。

        实例分割Mask rcnn = 检测Faster rcnn + 语义分割FCN+ ROIAlign

        在Faster rcnn的cls+box分支基础上,增加了分割mask分支。ROIAlign替代ROIPool解决2次量化的影响。

        在mmdet中主要有以下几个方面的模型可供使用,具体算法列表查询readme即可

检测 Object Detection

实例分割Instance Segmentation

全景分割 Panoptic Segmentation

对比学习 Contrastive Learning

蒸馏 Distillation

感受野搜索 Receptive Field Search

二、数据集制作

        mmdet建议将数据转换成 COCO 格式,不要自己搞一套标注方式改解析标签的源码。

2.1 labelme多点标注

2.2 labelme2coco.py

    将labelme的标注json文件直接转为coco的格式,当前只需要训练集和验证集,分别生成 instances_val2017.json、instances_train2017.json,放置在annotations下;train2017、val2017文件夹下是jpg图片。 数据集层级关系如下

--coco

----annotations

----train2017

----val2017

----test2017

labelme2coco.py脚本如下:

# -*- coding:utf-8 -*-

import argparse
import json
import matplotlib.pyplot as plt
import skimage.io as io
import cv2
from labelme import utils
import numpy as np
import glob
import PIL.Image
from shapely.geometry import Polygon


class labelme2coco(object):
    def __init__(self, labelme_json=[], save_json_path='./new.json'):
        '''
        :param labelme_json: 所有labelme的json文件路径组成的列表
        :param save_json_path: json保存位置
        '''
        self.labelme_json = labelme_json
        self.save_json_path = save_json_path
        self.images = []
        self.categories = []
        self.annotations = []
        # self.data_coco = {}
        self.label = []
        self.annID = 1
        self.height = 0
        self.width = 0

        # 初始化时就调用了save方法
        self.save_json()

    def data_transfer(self):
        for num, json_file in enumerate(self.labelme_json):
            with open(json_file, 'r') as fp:
                data = json.load(fp)  # 加载json文件
                self.images.append(self.image(data, num))     # 获取图像宽高索引文件名等信息
                if len(data['shapes'])==0:
                    print('shapes==0', json_file)
                for shapes in data['shapes']:
                    # label=shapes['label'].split('_')
                    # 标签处理
                    label = shapes['label']
                    # ################################# 费用清单标注的时候区分了有线表和无线表、少线表
                    label = 't'   # 统一处理为t
                    # print(json_file, label)
                    if label not in self.label:
                        self.categories.append(self.categorie(label))
                        self.label.append(label)
                    # 标注处理
                    points = shapes['points']
                    self.annotations.append(self.annotation(points, label, num, [data['imageWidth'], data['imageHeight']]))
                    self.annID += 1
        # 当前所有类别
        print(self.label)

    def image(self, data, num):
        """
        获取图像宽高索引文件名等信息
        :param data: labelme的信息
        :param num: 索引编号
        :return:
        """
        image = {}
        # base64 转cv2的array格式
        img = utils.img_b64_to_arr(data['imageData'])  # 解析原图片数据
        # img=io.imread(data['imagePath']) # 通过图片路径打开图片
        # img = cv2.imread(data['imagePath'], 0)
        height, width = img.shape[:2]
        img = None
        image['height'] = height
        image['width'] = width
        image['id'] = num + 1
        image['file_name'] = data['imagePath'].split('/')[-1].split('\\')[-1]
        # print(data['imagePath'], image['file_name'])

        self.height = height
        self.width = width

        return image

    def categorie(self, label):
        categorie = {}
        categorie['supercategory'] = label
        categorie['id'] = len(self.label) + 1  # 0 默认为背景
        categorie['name'] = label
        return categorie

    def annotation(self, points, label, num, img_size):
        # print('points=',points)
        # print('label=',label)
        # print('num=',num)
        """
        annotation['segmentation']= [[92.85714285714286, 1347.6190476190477, 1628.5714285714287, 1357.142857142857, 1629.7619047619048, 1576.1904761904761, 92.85714285714286, 1559.5238095238096]]
        poly= POLYGON ((92.85714285714286 1347.619047619048, 1628.571428571429 1357.142857142857, 1629.761904761905 1576.190476190476, 92.85714285714286 1559.52380952381, 92.85714285714286 1347.619047619048))
        area_= 331030.328798
        
        返回格式:
        segmentation 展品的点坐标
        area 面积
        iscrowd 固定值0
        image_id 从1开始的图像id
        bbox  边框,类似x0,y0,h,w,这种格式,具体排序没看
        category_id 类别索引
        id 第几个实例,不区分图像和类别,就是数据集里第几个实例
        """

        annotation = {}
        # 获取原始图片尺寸,对边缘点进行处理,cascade mask rcnn 训练时loss出现了nan
        # [h,w] = img_size
        tmp = [list(np.asarray(points).flatten())]
        # print(tmp)
        # print(img_size)

        # 把坐标限制在【1,max-2之间】
        # tmp1 = [[float(max(1, min(i, img_size[int(ind%2)]-2))) for ind,i in enumerate(tmp[0])]]
        # if tmp !=tmp1:
        #     print(tmp)
        #     print(tmp1)
        #     print(img_size)
        #     print('........')
        #     tmp = tmp1

        # 检查孤立点
        # if len(tmp[0]) < 8:
        #     print(tmp)
        #     print('........')


        # annotation['segmentation'] = [list(np.asarray(points).flatten())]
        annotation['segmentation'] = tmp
        poly = Polygon(points)
        area_ = round(poly.area, 6)
        annotation['area'] = area_
        annotation['iscrowd'] = 0
        annotation['image_id'] = num + 1
        # annotation['bbox'] = str(self.getbbox(points)) # 使用list保存json文件时报错(不知道为什么)
        # list(map(int,a[1:-1].split(','))) a=annotation['bbox'] 使用该方式转成list
        annotation['bbox'] = list(map(float, self.getbbox(points)))   # 边界框
        # 人工标注时,不小心点了一下?无效的小目标框
        if annotation['bbox'][2]<10 or annotation['bbox'][3]<10:
            print('人工标注时,不小心点了一下?无效的小目标框',annotation['bbox'])

        annotation['category_id'] = self.getcatid(label)   # 类别索引
        annotation['id'] = self.annID



        return annotation

    def getcatid(self, label):
        for categorie in self.categories:
            if label == categorie['name']:
                return categorie['id']
        return -1

    def getbbox(self, points):
        """
        由点画mask,再由mask反推其边框,不明白为啥不直接用min max的点值
        :param points:
        :return:
        """
        # img = np.zeros([self.height,self.width],np.uint8)
        # cv2.polylines(img, [np.asarray(points)], True, 1, lineType=cv2.LINE_AA) # 画边界线
        # cv2.fillPoly(img, [np.asarray(points)], 1) # 画多边形 内部像素值为1
        polygons = points
        mask = self.polygons_to_mask([self.height, self.width], polygons)
        return self.mask2box(mask)

    def mask2box(self, mask):
        '''从mask反算出其边框
        mask:[h,w] 0、1组成的图片
        1对应对象,只需计算1对应的行列号(左上角行列号,右下角行列号,就可以算出其边框)
        '''
        # np.where(mask==1)
        index = np.argwhere(mask == 1)
        rows = index[:, 0]
        clos = index[:, 1]
        # 解析左上角行列号
        left_top_r = np.min(rows)  # y
        left_top_c = np.min(clos)  # x

        # 解析右下角行列号
        right_bottom_r = np.max(rows)
        right_bottom_c = np.max(clos)

        # return [(left_top_r,left_top_c),(right_bottom_r,right_bottom_c)]
        # return [(left_top_c, left_top_r), (right_bottom_c, right_bottom_r)]
        # return [left_top_c, left_top_r, right_bottom_c, right_bottom_r]# [x1,y1,x2,y2]
        return [left_top_c, left_top_r, right_bottom_c - left_top_c,
                right_bottom_r - left_top_r]  # [x1,y1,w,h] 对应COCO的bbox格式

    def polygons_to_mask(self, img_shape, polygons):
        mask = np.zeros(img_shape, dtype=np.uint8)
        mask = PIL.Image.fromarray(mask)
        xy = list(map(tuple, polygons))
        PIL.ImageDraw.Draw(mask).polygon(xy=xy, outline=1, fill=1)
        mask = np.array(mask, dtype=bool)
        return mask

    def data2coco(self):
        data_coco = {}
        data_coco['images'] = self.images
        data_coco['categories'] = self.categories
        data_coco['annotations'] = self.annotations
        return data_coco

    def save_json(self):
        # 读取及处理
        self.data_transfer()
        #  生成一个大json
        self.data_coco = self.data2coco()
        """
        print(self.data_coco)
        print(self.data_coco['annotations'][0].keys())
        {'images': [{'height': 2304, 'width': 1728, 'id': 1, 'file_name': '..\\pic\\1562149745231b6cd7c4ee5.jpg'}], 'categories': [{'supercategory': 't', 'id': 1, 'name': 't'}], 'annotations': [{'segmentation': [[47.61904761904762, 1000.0, 39.285714285714285, 807.1428571428572, 41.66666666666667, 380.95238095238096, 552.3809523809524, 352.3809523809524, 996.4285714285714, 345.23809523809524, 1652.3809523809525, 348.8095238095238, 1634.5238095238096, 1226.1904761904761, 79.76190476190476, 1227.3809523809525]], 'area': 1393553.713152, 'iscrowd': 0, 'image_id': 1, 'bbox': [39.0, 345.0, 1613.0, 882.0], 'category_id': 1, 'id': 1}, {'segmentation': [[80.95238095238095, 1226.1904761904761, 1636.904761904762, 1227.3809523809525, 1627.3809523809525, 1355.952380952381, 92.85714285714286, 1347.6190476190477]], 'area': 193149.092971, 'iscrowd': 0, 'image_id': 1, 'bbox': [80.0, 1226.0, 1556.0, 129.0], 'category_id': 1, 'id': 2}, {'segmentation': [[92.85714285714286, 1347.6190476190477, 1628.5714285714287, 1357.142857142857, 1629.7619047619048, 1576.1904761904761, 92.85714285714286, 1559.5238095238096]], 'area': 331030.328798, 'iscrowd': 0, 'image_id': 1, 'bbox': [92.0, 1347.0, 1537.0, 229.0], 'category_id': 1, 'id': 3}]}
        dict_keys(['segmentation', 'area', 'iscrowd', 'image_id', 'bbox', 'category_id', 'id'])
        """

        # 保存json文件
        json.dump(self.data_coco, open(self.save_json_path, 'w'), indent=4)  # indent=4 更加美观显示


labelme_json = glob.glob('./Documents/数据集/labels/*json')
# labelme_json=['./Documents/useful_code/mask_rcnn_project/1562149745231b6cd7c4ee5.json']


# 测试
labelme2coco(labelme_json[:int(len(labelme_json)*0.1)], 'instances_val2017.json')
# 验证
# labelme2coco(labelme_json[int(len(labelme_json)*0.1):int(len(labelme_json)*0.2)], 'instances_val2017.json')
# 训练
labelme2coco(labelme_json[int(len(labelme_json)*0.1):], 'instances_train2017.json')

三、环境搭建

        需要pytorch1.5以上环境,mmdet的教程维护得很好。

3.1查看CUDApyTorch版本

import torch

print(torch.__version__)   # 1.7.0a0+8deb4fe

print(torch.version.cuda)   # 11.0

print(torch.backends.cudnn.version()) # 8002

3.2安装完整版支持CUDA的mmcv-full

        mmcv-full 的版本需要依赖cuda、pytorch、MMDetection。mmcv-full实际承担着训练、数据处理、注册机制的作用,是mmdet的灵魂伴侣,不是打辅助而已。

      需要下载正确版本的mmcv-full,版本间关系可参考:mmdetection/get_started.md at master · open-mmlab/mmdetection · GitHub

       mmcv的版本有多种方式,如:

a、pip install时指定对应的依赖“pip install mmcv-full==latest+torch1.1.0+cu901 -f https://download.openmmlab.com/mmcv/dist/index.html”。

b、下载后源码编译“ pip install -e . ”

c、下载正确的whl后安装(我采用的方法)

从https://download.openmmlab.com/mmcv/dist/cu110/torch1.7.0/index.html  下载

或所有whl查询 https://download.openmmlab.com/mmcv/dist/index.html

pip3 install mmcv_full-1.7.0-cp310-cp310-manylinux1_x86_64.whl

3.3 安装其他依赖

pip install pycocotools

3.4 安装mmdet

git clone https://github.com/open-mmlab/mmdetection.git

cd mmdetection-master_2022_11_11

pip install -r requirements/build.txt

pip install -v -e .

3.5 验证

from mmcv.ops import RoIPool

from mmdet.apis import init_detector, inference_detector

四、修改源码以训练自己的数据

        下面先以mask rcnn为例介绍怎么改代码,使其跑起来。如果是其他模型,大差不差,找一下指向的4个配置文件即可。

先看整体的配置文件

configs/mask_rcnn/mask_rcnn_r50_fpn_1x_coco.py文件内容如下,可见一次训练主要关联了4个配置文件,分别指向模型、数据、超参数的配置,可以把configs目录当成一个检索仓库。

_base_ = [

    '../_base_/models/mask_rcnn_r50_fpn.py',  # 模型结构的具体配置,注意num_class

    '../_base_/datasets/coco_instance.py',   #数据集配置

    '../_base_/schedules/schedule_1x.py', '../_base_/default_runtime.py’  # 训练策略配置,主要是学习率和优化器的配置

]

4.1、修改类别,注意class后要留逗号

vim mmdetection/mmdet/datasets/coco.py ,将CLASSES替换成自己的classes

vim mmdetection/mmdet/core/evaluation/class_names.py ,将coco_classes的return替换成自己的classes

vim mmdetection/configs/_base_/models/mask_rcnn_r50_fpn.py   将num_classes改成自己的类别数量

4.2 修改图像尺寸及数据集路径

vim mmdetection/configs/_base_/datasets/coco_instance.py ,注意里面的data_root,train、val、test的ann_file、img_prefix,img_scale

4.3 修改学习率lr、epoch数量max_epochs、学习率衰减方式 等训练参数

卡只有1、2张的情况下,注意改小学习率

vim mmdetection/configs/_base_/schedules/schedules_1x.py 修改训练参数

# optimizer
# 8张GPU ==> 0.02,4张GPU就0.01,两张就0.005

optimizer = dict(type='SGD', lr=0.002, momentum=0.9, weight_decay=0.0001)   # lr稳定后是0.02
optimizer_config = dict(grad_clip=None)
# learning policy
lr_config = dict(
    policy='step',  # 优化策略
    warmup='linear',   # 线性增长学习率,如0.0002、0.0004
    warmup_iters=500,   # 0~500 个batch size中逐渐增大学习率,即500iter后为指定的学习率0.02
    warmup_ratio=0.001,   # 有的博客说是起始学习率
    step=[8, 11])   # 在第8和11个epoch后降低学习率,可以按照max_epochs去调整衰减策略。在日志中epoch从1开始,因此第9个epoch降低为原来10%,第12个epoch降低为原来1%。注意这里不要改成len为3的列表,会导致学习率降低至0后面白训练。
runner = dict(type='EpochBasedRunner', max_epochs=12)   # 总共训练12个epochs

4.4 修改一些训练参数

vim mmdetection/configs/_base_/default_runtime.py

这里根据资源情况,改改base_batch_size就可以了,别的暂时不用改

checkpoint_config = dict(interval=1)   # 保存权重的间隔
# yapf:disable
log_config = dict(
    interval=50,   # 每多少batch打印一下loss
    hooks=[
        dict(type='TextLoggerHook'),
        # dict(type='TensorboardLoggerHook')
    ])
# yapf:enable
custom_hooks = [dict(type='NumClassCheckHook')]

dist_params = dict(backend='nccl')
log_level = 'INFO'
load_from = None   # 加载权重路径
resume_from = None
workflow = [('train', 1)]

# disable opencv multithreading to avoid system being overloaded
opencv_num_threads = 0
# set multi-process start method as `fork` to speed up the training
mp_start_method = 'fork'

# Default setting for scaling LR automatically
#   - `enable` means enable scaling LR automatically
#       or not by default.
#   - `base_batch_size` = (8 GPUs) x (2 samples per GPU).
auto_scale_lr = dict(enable=False, base_batch_size=16)   # 每个batch_size的图片数量

五、训练

python tools/train.py configs/mask_rcnn/mask_rcnn_r50_fpn_1x_coco.py --auto-resume

          执行上面的train命令就会提示如何下载预训练模型,并放到指定路径,如Downloading: "https://download.pytorch.org/models/resnet50-0676ba61.pth" to /root/.cache/torch/hub/checkpoints/resnet50-0676ba61.pth

--auto-resume 可以从断点继续训练,也可以用--resume-from从指定检查点继续

六、测试

python tools/test.py configs/mask_rcnn/mask_rcnn_r50_fpn_1x_coco.py  work_dirs/mask_rcnn_r50_fpn_1x_coco/latest.pth --show-dir work_dirs/result --eval segm

注意: 测试时要用配对的配置文件和模型文件,我不小心用mask rcnn的配置+cascade mask rcnn的模型,出来的推理结果非常差还都是孔洞

--show-dir  可视化结果

--show-score-thr 低于阈值的box不进行可视化

七、可视化loss、acc

python tools/analysis_tools/analyze_logs.py  plot_curve work_dirs/mask_rcnn_r50_fpn_1x_coco/20221124_125015.log.json work_dirs/mask_rcnn_r50_fpn_1x_coco/20221115_114758.log.json --keys acc --out loss.pdf

Mmdet的训练数据在work_dirs/***/***.log.json 文件中,可视化的脚本在tools/analysis_tools/analyze_logs.py。可以将多个json的信息进行拼接。keys可以通过看json文件里有什么决定

keys包括:

loss_rpn_cls  区域候选网络region proposal network分类损失(前景+背景)

loss_rpn_bbox   rpn网络目标框回归损失

loss_cls 属于mask_rcnn heads分类损失(真实类别+背景)

acc

loss_bbox 属于mask_rcnn heads回归损失

loss_mask 属于mask_rcnn heads分割损失

loss

time

八、梯度爆炸

         同样的标注数据,先训练了mask rcnn,没异常。再训练了cascade mask rcnn,一开始损失下降,后来突然nan了,也就是发生了梯度爆炸

        先说结论:这次损失变nan是数据标注问题+学习率太大双重问题引起的,导致的奇怪现象是mask rcnn不报错但cascade mask rcnn报错

以下记录解决过程,正确的操作加粗,其他未解决问题仅是记录思路。

-> 中断训练后从头开始

    失败,还是很早就nan了

-> 调小学习率,vim mmdetection/configs/_base_/schedules/schedules_1x.py

vim mmdetection/configs/_base_/schedules/schedules_1x.py 修改训练参数 缩小10倍

optimizer = dict(type='SGD', lr=0.002, momentum=0.9, weight_decay=0.0001)

调小学习率是对的,但数据有问题,所以失败了

->怀疑是标注问题,标注的点有边界点,触发了边缘问题,改一下labelme2coco.py,基于图像宽高,把坐标限制在【1,max-2之间】

 失败

->人工标注时,不小心点了一下?无效的小目标框

失败

->查询是哪张图引起的nan质变,源码是每50张打印一次loss,很不好找问题图片,按照如下修改,一张就打印一次loss,找loss哪里开始异常了。

vim /usr/local/lib/python3.7/dist-packages/mmcv/runner/epoch_based_runner.py  在def train 中 打印每张图的文件名

for i, data_batch in enumerate(self.data_loader):

            print('/usr/local/lib/python3.7/dist-packages/mmcv/runner/epoch_based_runner.py   data_batch=',data_batch['img_metas']._data[0][0]['filename'])

        这里的图像数据img_metas是一个DataContainer,定义在mmcv/parallel/data_container.py中。可通过._data获取值

vim configs/_base_/default_runtime.py 将interval改成1,每张图打印一下

一开始找到了一张问题标注图,修改标注。然后在300张左右又出现了nan,看来看去标注没啥问题,此时lr还在warm up增长阶段,于是将lr减小10倍试试

-> 调小学习率,vim mmdetection/configs/_base_/schedules/schedules_1x.py

vim mmdetection/configs/_base_/schedules/schedules_1x.py 修改训练参数 缩小10倍

optimizer = dict(type='SGD', lr=0.002, momentum=0.9, weight_decay=0.0001)

九、后记

        默认参数训练出来的Cascade Mask R-CNN模型在部分图片上表现不佳,出现不恰当重合或各种奇奇怪怪的边缘。对此,尝试了调整学习率、调整输入图像尺寸、重新设置anchor,都没有得到显著改善。其中,调整anchor后总损失从0.05降低到0.04,无望降低到0.03,效果也不咋的,经常漏或者大面积重合,搞不出来还是得及时换模型。

    loss及map的变化如下

模型优化点loss

segm_mAP_l

maskrcnn/

"loss_mask": 0.06503,

"loss": 0.17677,

0.834

cascade maskrcnn/

 "s2.loss_mask": 0.00476,

"loss": 0.09647

0.847

cascade maskrcnn调整输入尺寸

img_scale=(1920, 1280)

"s2.loss_mask": 0.00462,

"loss": 0.05722,

0.873

cascade maskrcnn

调整 anchor

调整输入尺寸

img_scale=(1920, 1280)

 推荐阅读

如何搭建环境及运行代码

ubuntu 18.04 mmdetection 训练mask_rcnn_萌新用户2.0的博客-CSDN博客_mmdetection训练mask

源码主流程梳理

mmdetection源码阅读笔记:概览 - 知乎

Faster rcnn

Faster RCNN 学习笔记 - 勇者归来 - 博客园

Faster-rcnn详解_技术挖掘者的博客-CSDN博客_faster r-cnn

Mask rcnn

Mask R-CNN详解_技术挖掘者的博客-CSDN博客_mask r-cnn

mmdet 官方教程

Welcome to MMDetection’s documentation! — MMDetection 2.26.0 文档

GitHub地址

GitHub - open-mmlab/mmdetection: OpenMMLab Detection Toolbox and Benchmark.

  • 5
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值