使用CenterNet2训练自己的数据集

Author Avatar
NENEIIII Apr 09, 2022
  • Read this article on other devices

参考

CenterNet2实战:手把手带你实现使用CenterNet2训练自定义数据集_AI浩的博客-CSDN博客_centernet2

环境构建

创建虚拟环境

建议是每个项目都创建一个单独的环境这样避免了很多不兼容和包冲突啥的,,

我自己没有单独创建,就用之前的一个pytorch环境,然后加了点包,果然后面有点问题反而耽误时间了


具体操作搬运原博:

操作系统:win10、Cuda11.0。

创建虚拟环境,并激活环境:

conda create --name centernet2 python=3.7
activate centernet2
conda install pytorch==1.7.1 torchvision==0.8.2 torchaudio==0.7.2 cudatoolkit=11.0 -c pytorch

安装apex

cd C:\Users\WH\Downloads\apex-master #进入apex目录
pip install -r requirements.txt

这个apex库我没有安装,看了一下是用来减小显存占用的应该对我有帮助😥

python setup.py install

安装其它库

根据pycharm提示,缺什么补什么。。

测试环境

新建imgs和imgout文件夹,imgs文件夹存放待测试的图片

执行命令:

python projects/CenterNet2/demo.py --config-file projects/CenterNet2/configs/CenterNet2_R50_1x.yaml --input imgs/ --output imgout --opts MODEL.WEIGHTS projects/CenterNet2/CenterNet2_R50_1x.pth

这块当时没成功,但我觉得我环境没问题提示的是路径有问题?结果我就没管了继续往下做的,,路径问题是后面解决的,是代码里有个部分不太对

制作数据集

==重点之一==

centernet2使用的是coco格式数据集,具体长这样:

coco

– annotations

​ – instances_train2017.json

​ – instances_val2017.json

– train2017

​ – xx.jpg

​ …

– val2017

​ – xx.jpg

​ …

而我使用labelImg标注的数据集是VOC格式的:

voc

– Annotations

​ – xx.xml

​ …

– ImageSets

​ – Main

​ train.txt

​ trainval.txt

​ val.txt

​ test.txt

– JPEGImages

​ – xx.jpg

​ …

因此需要进行转化,区别在于coco的标注格式是json,并且是train全部放在一个文件里,val全部放在一个文件里。以及coco的train和val的图片是分开的

思路就是根据voc的annotations和txt文件进行转化和划分图片:

  • 生成两个json文件

    需要修改voc_clses

    执行两次,每次修改test_txtjson_name对应train和val

    # coding=utf-8
    import xml.etree.ElementTree as ET
    import os
    import json
    
    # 此处修改成自己的类别
    voc_clses = ['圆孔', '矩形梅花孔', '环形梅花孔', '中心圆梅花孔',
        '矩形1', '圆环']
    
    
    categories = []
    for iind, cat in enumerate(voc_clses):
        cate = {}
        cate['supercategory'] = cat
        cate['name'] = cat
        # 类别要从1开始 不然后面会报错
        cate['id'] = iind + 1 
        categories.append(cate)
    
    def getimages(xmlname, id):
        sig_xml_box = []
        tree = ET.parse(xmlname)
        root = tree.getroot()
        images = {}
        for i in root:  # 遍历一级节点
            if i.tag == 'filename':
                # 统一改为jpg格式(修改了图片没有修改xml文件)
                file_name = i.text[:-3] + "jpg" # 0001.jpg
                # print('image name: ', file_name)
                images['file_name'] = file_name
            if i.tag == 'size':
                for j in i:
                    if j.tag == 'width':
                        width = int(j.text)
                        images['width'] = width
                    if j.tag == 'height':
                        height = int(j.text)
                        images['height'] = height
            if i.tag == 'object':
                for j in i:
                    if j.tag == 'name':
                        cls_name = j.text
                    cat_id = voc_clses.index(cls_name) + 1
                    if j.tag == 'bndbox':
                        bbox = []
                        xmin = 0
                        ymin = 0
                        xmax = 0
                        ymax = 0
                        for r in j:
                            if r.tag == 'xmin':
                                xmin = eval(r.text)
                            if r.tag == 'ymin':
                                ymin = eval(r.text)
                            if r.tag == 'xmax':
                                xmax = eval(r.text)
                            if r.tag == 'ymax':
                                ymax = eval(r.text)
                        bbox.append(xmin)
                        bbox.append(ymin)
                        bbox.append(xmax - xmin)
                        bbox.append(ymax - ymin)
                        bbox.append(id)   # 保存当前box对应的image_id
                        bbox.append(cat_id)
                        # annotations area
                        bbox.append((xmax - xmin) * (ymax - ymin) - 10.0)   # bbox的ares
                        # coco中的ares数值是 < w*h 的, 因为它其实是按segmentation的面积算的,所以我-10.0一下...
                        sig_xml_box.append(bbox)
                        # print('bbox', xmin, ymin, xmax - xmin, ymax - ymin, 'id', id, 'cls_id', cat_id)
        images['id'] = id
        # print ('sig_img_box', sig_xml_box)
        return images, sig_xml_box
    
    
    
    def txt2list(txtfile):
        f = open(txtfile)
        l = []
        for line in f:
            l.append(line[:-1])
        return l
    
    
    # voc2007xmls = 'anns'
    voc2007xmls = 'D:/pycharm/WorkSpace/毕设可能用得上/projects/CenterNet2/datasets/voc/Annotations'
    # test_txt = 'voc2007/test.txt'
    test_txt = 'D:/pycharm/WorkSpace/毕设可能用得上/projects/CenterNet2/datasets/voc/ImageSets/Main/' \
               'val.txt'
    xml_names = txt2list(test_txt)
    xmls = []
    bboxes = []
    ann_js = {}
    for ind, xml_name in enumerate(xml_names):
        xmls.append(os.path.join(voc2007xmls, xml_name + '.xml'))
    json_name = 'D:/pycharm/WorkSpace/毕设可能用得上/projects/CenterNet2/datasets/coco/annotations/' \
                'instances_val2017.json'
    images = []
    for i_index, xml_file in enumerate(xmls):
        image, sig_xml_bbox = getimages(xml_file, i_index)
        images.append(image)
        bboxes.extend(sig_xml_bbox)
    ann_js['images'] = images
    ann_js['categories'] = categories
    annotations = []
    for box_ind, box in enumerate(bboxes):
        anno = {}
        anno['image_id'] =  box[-3]
        anno['category_id'] = box[-2]
        anno['bbox'] = box[:-3]
        anno['id'] = box_ind
        anno['area'] = box[-1]
        anno['iscrowd'] = 0
        annotations.append(anno)
    ann_js['annotations'] = annotations
    json.dump(ann_js, open(json_name, 'w'), indent=4)  # indent=4 更加美观显示
    
  • 划分图片,生成train和val文件夹

    import csv
    import shutil
    import os
    # 根据自己的路径进行修改
    txt_path = 'D:/pycharm/WorkSpace/毕设可能用得上/projects/CenterNet2/datasets/voc/ImageSets/Main/' \
               'test.txt'
    target_path = 'D:/pycharm/WorkSpace/毕设可能用得上/projects/CenterNet2/datasets/' \
                  'coco/test2017/'
    original_path = 'D:/pycharm/WorkSpace/毕设可能用得上/projects/CenterNet2/datasets/voc/JPEGImages/'
    with open(txt_path,"rt", encoding="utf-8") as csvfile:
        for row in csvfile:
            row = row.strip('\n')
            if os.path.exists(target_path+row):
                print("已存在文件")
            else:
                full_path = original_path+row+'.jpg'   #还没有
                shutil.copy(full_path,target_path+row+".jpg")
    

生成的coco放在`projects/CenterNet2/datasets/下

配置训练环境

==重点之二==

更改预训练模型的size

在projects/CenterNet2目录,新建change_model_size.py文件(名称不重要)

import torch
import numpy as np
import pickle
# 根据自己的修改
num_class = 6
pretrained_weights  = torch.load('models/CenterNet2_R50_1x.pth')
pretrained_weights['iteration']=0
pretrained_weights['model']["roi_heads.box_predictor.0.cls_score.weight"].resize_(num_class+1,1024)
pretrained_weights['model']["roi_heads.box_predictor.0.cls_score.bias"].resize_(num_class+1)
pretrained_weights['model']["roi_heads.box_predictor.1.cls_score.weight"].resize_(num_class+1,1024)
pretrained_weights['model']["roi_heads.box_predictor.1.cls_score.bias"].resize_(num_class+1)
pretrained_weights['model']["roi_heads.box_predictor.2.cls_score.weight"].resize_(num_class+1,1024)
pretrained_weights['model']["roi_heads.box_predictor.2.cls_score.bias"].resize_(num_class+1)
torch.save(pretrained_weights, "models/CenterNet2_%d.pth"%num_class)

这里有个点我一开始忽略了,,执行完这个文件后,就会生成一个类似这样命名的模型CenterNet2_6.pth,然后接下来的修改都应该用这个,而不是原先的CenterNet2_R50_1x.pth,因为输出size不对应会报错

修改config参数

路径:“detectron2/engine/defaults.py”

–config-file:模型的配置文件,CenterNet2的模型配置文件放在“projects/CenterNet2/configs”下面。名字和预训练模型对应。

parser.add_argument("--config-file", default="./configs/CenterNet2_R50_1x.yaml", metavar="FILE", help="path to config file")

resume 是否再次,训练,如果设置为true,则接着上次训练的结果训练。所以第一次训练不用设置

parser.add_argument(
    "--resume",
    action="store_true",
    help="Whether to attempt to resume from the checkpoint directory. "
    "See documentation of `DefaultTrainer.resume_or_load()` for what it means.",
)

这里注意,如果第一次训练就直接在命令行 python xx.py

如果中途断了,要继续从上次的checkpoint开始,就需要 python xx.py --resume

–num-gpus,gpu的个数,如果只有一个设置为1,如果有多个,可以自己设置想用的个数

parser.add_argument("--num-gpus", type=int, default=1, help="number of gpus *per machine*")

修改类别,文件路径“projects/CenterNet2/centernet/config.py”

_C.MODEL.CENTERNET.NUM_CLASSES = 6

修改yaml文件参数

Base-CenterNet2.yaml中修改预训练模型的路径。

WEIGHTS: "CenterNet2_6.pth"

BASE_LR:设置学习率。

STEPS:设置训练多少步之后调整学习率。

MAX_ITER:最大迭代次数。

CHECKPOINT_PERIOD:设置迭代多少次保存一次模型

要根据自己的计算能力来设置,比如batchsize iter等

SOLVER:
  IMS_PER_BATCH: 4
  BASE_LR: 0.01
  REFERENCE_WORLD_SIZE: 0
  STEPS: (40, 70)
  MAX_ITER: 100
  CHECKPOINT_PERIOD: 20
  WARMUP_ITERS: 40
  WARMUP_FACTOR: 0.00025
  CLIP_GRADIENTS:
    ENABLED: True

==为什么是Base-CenterNet2.yaml这个文件呢==

其实因为上面我们选的是CenterNet2_R50_1x.yaml这个文件,点进去:

_BASE_: "Base-CenterNet2.yaml"

修改train_net.py

主要修改该setup函数,增加数据集注册

NUM_CLASSES=6

def setup(args):
    """
    Create configs and perform basic setups.
    """
    register_coco_instances("train", {}, "datasets/coco/annotations/instances_train2017.json",
                            "datasets/coco/train2017")
    register_coco_instances("test", {}, "datasets/coco/annotations/instances_val2017.json", "datasets/coco/val2017")

    cfg = get_cfg()

    add_centernet_config(cfg)
    cfg.merge_from_file(args.config_file)
    cfg.merge_from_list(args.opts)

    cfg.DATASETS.TRAIN = ("train",)
    cfg.DATASETS.TEST = ("test",)
    cfg.MODEL.CENTERNET.NUM_CLASSES = NUM_CLASSES
    cfg.MODEL.ROI_HEADS.NUM_CLASSES = NUM_CLASSES

    if '/auto' in cfg.OUTPUT_DIR:
        file_name = os.path.basename(args.config_file)[:-5]
        cfg.OUTPUT_DIR = cfg.OUTPUT_DIR.replace('/auto', '/{}'.format(file_name))
        logger.info('OUTPUT_DIR: {}'.format(cfg.OUTPUT_DIR))
    cfg.freeze()
    default_setup(cfg, args)
    return cfg

还要修改detectron2/engine/launch.py,在launch函数下面增加一句

dist.init_process_group('gloo', init_method='file://tmp/somefile', rank=0, world_size=1)

def launch(
    main_func,
    num_gpus_per_machine,
    num_machines=1,
    machine_rank=0,
    dist_url=None,
    args=(),
    timeout=DEFAULT_TIMEOUT,
):
   ...
    ...
    else:
        dist.init_process_group('gloo', init_method='file://tmp/somefile', rank=0, world_size=1)

        main_func(*args)

这句话的作用是初始化分布式训练,因为我们没有使用分布式,所以没有初始化,但是不初始化就会报错,所以加上这句

训练

第一种,命令行:进入“projects/CenterNet2/”目录下,执行:

python train_net.py

当然,,是不会这么顺利

错误解决历程》》

  • KeyError: “Encountered category_id=6 but this id does not exist in ‘categories’ of the json file.

    修改json文件categories下的id:从1开始的 (前面提供的生成json文件是正确版本的,只是这里说下是怎么改正的)

    cate['id'] = iind + 1

    另外还有一个要改的是长宽要是数值型

                    if j.tag == 'width':
                        width = int(j.text)
                        images['width'] = width
                    if j.tag == 'height':
                        height = int(j.text)
                        images['height'] = height
    
  • 页面太小 | brokenpipe

    我改了两个地方:

    detectrom2>engine>defaults.py 修改

    cfg.DATALOADER.NUM_WORKERS = 0不使用多线程

    增加虚拟内存

    windows10系统如何增加虚拟内存-win7之家 (win7zhijia.cn)

    一开始还是不行,,后来再增加虚拟内存的大小就好了,,

  • cuda out of memory

    因为之前我训练其它模型就遇到过毕竟我的显卡只有2g,,咱本来也不打游戏谁知道以后我会干这个😥

    所以解决之前我就在想估计解决不了因为根本原因是内存太小,然后这个网络也不是什么轻量级的有500多M呢。所以新的问题是如何使用CPU训练–》

    修改detectron2 >config>defaults文件:_C.MODEL.DEVICE = "cpu"

关于训练的其它小事情:

可以修改下输出频次和checkpoints的保存频率,原来是20iter输出一次,毕竟人家要训练1000次,而我一共100次,所以改成5次一输出

(iteration % 5 == 0 or iteration == max_iter):

修改checkpoints文件的保存频率为20iter存一次:

在Base_CenterNet2.yaml中:CHECKPOINT_PERIOD: 20


第二种,直接在pycharm 直接运行train_net.py.

但是我会报错ImportError: cannot import name '_C' from 'detectron2'

反正第一种可以用所以我没管了,,

训练结果

为撒效果不好,,我的数据集确实一般,不过之前在SSD上都还不错,这个之后再研究

预测

修改projects/CenterNet2/demo.py

修改setup_cfg函数

cfg.MODEL.CENTERNET.NUM_CLASSES = NUM_CLASSES

cfg.MODEL.ROI_HEADS.NUM_CLASSES = NUM_CLASSES

执行命令:

python projects/CenterNet2/demo.py --config-file projects/CenterNet2/configs/CenterNet2_R50_1x.yaml --input imgs/ --output imgout --opts MODEL.WEIGHTS projects/CenterNet2/output/CenterNet2/CenterNet2_R50_1x/model_final.pth

报错:

File “D:\ANACONDA\envs\Pytorch\lib\site-packages\detectron2-0.6-py3.7-win-amd64.egg\detectron2\utils\video_
visualizer.py”, line 111, in draw_instance_predictions
else [y[0] for y in filter(lambda x: x[1], zip(labels, visibilities))]
TypeError: zip argument #2 must support iteration

好像是因为原先创建的是video格式的可视化对象,但我们的是图片 因此把对应部分删掉,因为之后的函数如果传入的是空 会重新创建一个的,具体如图:

# visualizer = VideoVisualizer(
        #     MetadataCatalog.get(
        #         cfg.DATASETS.TEST[0] if len(cfg.DATASETS.TEST) else "__unused"
        #     ),
        #     instance_mode=ColorMode.IMAGE)
        for path in tqdm.tqdm(args.input, disable=not args.output):
            print("path1:",path)
            # 以下部分也注释掉了 因为我发现路径好像不太对
            # path = os.path.join(args.input, str(path))
            # use PIL, to be consistent with evaluation
            # print("path2:",path)
            img = read_image(path, format="BGR")
            start_time = time.time()
            # visualizer.metadata.thing_classes[:10] = ["圆孔", "矩形梅花孔",
            #                                           "环形梅花孔", "中心圆梅花孔", "矩形1",
            #                                           "圆环"]
            predictions, visualized_output = demo.run_on_image(
                img, visualizer=None)

然后在projects>CenterNet2>predictor.py中添加自己的类别名称:(记得不要有中文)

visualizer.metadata.thing_classes[:10] = ["yuankong", "juxingmeihuakong",
                                                      "huanxingmeihuakong", "zhongxinyuan", "juxing",
                                                      "yuanhuan"]

参考:[Detectron2框架 CenterNet2 demo报错解决 video_visualizer.py .TypeError: ‘NoneType‘ object is not iterable]_哦对的对的的博客-CSDN博客

  • 最后一个问题,调用cv2.imshow()报错??The function is not implemented. Rebuild the library with Windows, GTK+ 2.x or Carbon support

    因为cv2用过很多次了,但这回出问题??我想是不是配环境的时候搞了什么,,所以单独再创一个环境真的有必要,,

    解决方法10个有9个都说是

    pip install opencv-contrib-python

    但是我已经安装了😥

    然后调用cv2.getBuildInformation()查看编译信息显示 GUI:NO

    最后是把这个环境下的cv相关的全部卸载了,然后直接一个pip install opencv-python

    解决了 重装大法还是好用