赞
踩
目录
目录
场景需要训练一个分割模型,参考这个场景大多数人的选择,决定用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查看CUDA及pyTorch版本
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不进行可视化
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分类损失(真实类别+背景)
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
源码主流程梳理
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.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。