60分钟吃掉detectron2

本范例演示使用非常有名的目标检测框架detectron2 🤗🤗

在自己的数据集(balloon数据)上训练实例分割模型MaskRCNN的方法。

detectron2框架的设计有以下一些优点:

  • 1,强大:提供了包括目标检测、实例分割、全景分割等非常广泛的视觉任务模型库。

  • 2,灵活:可以通过注册机制自定义模块或模型结构,从而进行扩展和改进。

  • 3,易用:通过list of dict格式定义自己的数据集, 简单好用。

公众号算法美食屋后台回复关键词: torchkeras,获取本文源代码和balloon数据集下载链接。

我们首先需要安装并导入detectron库~

!pip install 'git+https://github.com/facebookresearch/detectron2.git'
!pip install torchkeras
import numpy as np
import os, json, cv2, random
from PIL import Image 

import torch 

import detectron2
from detectron2.config import get_cfg
from detectron2.utils.visualizer import Visualizer
from detectron2.data import MetadataCatalog, DatasetCatalog
from detectron2 import model_zoo
from detectron2.engine import DefaultPredictor

#from detectron2.utils.logger import setup_logger
#setup_logger()

def cv2_show(arr):
    img = Image.fromarray(cv2.cvtColor(arr, cv2.COLOR_BGR2RGB))
    return img

0,预训练模型

from torchkeras import data 
#下载测试图片
img = data.get_example_image('park.jpg')
img.save('park.jpg')
cfg = get_cfg()
cfg.merge_from_file(model_zoo.get_config_file("COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml"))
cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.5  # set threshold for this model
cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url("COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml")
predictor = DefaultPredictor(cfg)
im = cv2.imread("park.jpg")
outputs = predictor(im)
print(outputs["instances"].pred_classes)
print(outputs["instances"].pred_boxes)
v = Visualizer(im[:, :, ::-1], 
            MetadataCatalog.get(cfg.DATASETS.TRAIN[0]), scale=1.2)
out = v.draw_instance_predictions(outputs["instances"].to("cpu"))
cv2_show(out.get_image()[:, :, ::-1])

98799cea10afaddc3fb0ebc83032f022.png

一,准备数据

detectron准备数据集,需要先注册。

如果是coco格式的数据,可以用以下方法快速注册:

from detectron2.data.datasets import register_coco_instances
register_coco_instances("my_dataset_train",{},"json_annotation_train.json","path/to/image/dir")
register_coco_instances("my_dataset_val", {}, "json_annotation_val.json","path/to/image/dir")

非coco格式的数据,可以用以下步骤进行注册:

  • 1,先将数据集整理成字典组成的列表形式

  • 2,使用DatasetCatalog注册数据集

from detectron2.structures import BoxMode

def get_balloon_dicts(img_dir):
    json_file = os.path.join(img_dir, "via_region_data.json")
    with open(json_file) as f:
        imgs_anns = json.load(f)

    dataset_dicts = []
    for idx, v in enumerate(imgs_anns.values()):
        record = {}
        
        filename = os.path.join(img_dir, v["filename"])
        height, width = cv2.imread(filename).shape[:2]
        
        record["file_name"] = filename
        record["image_id"] = idx
        record["height"] = height
        record["width"] = width
      
        annos = v["regions"]
        objs = []
        for _, anno in annos.items():
            assert not anno["region_attributes"]
            anno = anno["shape_attributes"]
            px = anno["all_points_x"]
            py = anno["all_points_y"]
            poly = [(x + 0.5, y + 0.5) for x, y in zip(px, py)]
            poly = [p for x in poly for p in x]

            obj = {
                "bbox": [np.min(px), np.min(py), np.max(px), np.max(py)],
                "bbox_mode": BoxMode.XYXY_ABS,
                "segmentation": [poly],
                "category_id": 0,
            }
            objs.append(obj)
        record["annotations"] = objs
        dataset_dicts.append(record)
    return dataset_dicts



try:
    #DatasetCatalog.remove('balloon_train')
    #DatasetCatalog.remove('balloon_val')
    
    DatasetCatalog.register("balloon_train", lambda : get_balloon_dicts("./data/balloon/train"))
    MetadataCatalog.get("balloon_train" ).set(thing_classes=["balloon"])
    
    DatasetCatalog.register("balloon_val", lambda : get_balloon_dicts("./data/balloon/val"))
    MetadataCatalog.get("balloon_val" ).set(thing_classes=["balloon"])
    
except Exception as err:
    pass 
    
balloon_metadata = MetadataCatalog.get("balloon_train")

我们来可视化一下数据,看看是否正确。

dicts_train = DatasetCatalog.get('balloon_train') #get_balloon_dicts("./data/balloon/train")  
dicts_val = DatasetCatalog.get('balloon_val') #get_balloon_dicts("./data/balloon/val")
dic = dicts_train[3]
img = cv2.imread(dic["file_name"])
visualizer = Visualizer(img[:, :, ::-1], metadata=balloon_metadata, scale=0.5)
out = visualizer.draw_dataset_dict(dic)
cv2_show(out.get_image()[:, :, ::-1])

84b94e0020b5cf04514e999919639e8d.png

二,定义模型

detectron2通过配置文件定义模型。可以查看 detectron2目录下的configs路径,有各种各样功能的模型配置文件可以使用。

包括:Detection(检测), InstanceSegmentation(实例分割), Keypoints(关键点检测), Panoptic(全景分割) 等各种类型

cfg = get_cfg()

cfg.merge_from_file(model_zoo.get_config_file(
    "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml"))
cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url(
   "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml")  


cfg.MODEL.ROI_HEADS.BATCH_SIZE_PER_IMAGE = 128   # The "RoIHead batch size". 128 is faster, and good enough for this toy dataset (default: 512)
cfg.MODEL.ROI_HEADS.NUM_CLASSES = 1  # only has one class (ballon). 


#model = detectron2.modeling.build_model(cfg) # This way do not load pretrained weights

predictor = DefaultPredictor(cfg)
model = predictor.model

三,训练模型

以下代码使用detectron2原生的DefaultTrainer进行训练,比较简单。

但是这个DefaultTrainer灵活性一般,当你想在训练循环中加入自己想要的功能时比较麻烦,并且日志输出不够直观。

此外也没有earlystopping,不能够保存验证集上最优的权重。

from detectron2.engine import DefaultTrainer

cfg.DATASETS.TRAIN = ("balloon_train",)
cfg.DATASETS.TEST = ("balloon_val",)
cfg.DATALOADER.NUM_WORKERS = 2
cfg.SOLVER.IMS_PER_BATCH = 4

os.makedirs(cfg.OUTPUT_DIR, exist_ok=True)

    
cfg.DATALOADER.NUM_WORKERS = 2
cfg.SOLVER.IMS_PER_BATCH = 2  # This is the real "batch size" commonly known to deep learning people
cfg.SOLVER.BASE_LR = 0.00025  # pick a good LR
cfg.SOLVER.MAX_ITER = 600    
cfg.SOLVER.STEPS = []        # do not decay learning rate

os.makedirs(cfg.OUTPUT_DIR, exist_ok=True)

trainer = DefaultTrainer(cfg) 
trainer.resume_or_load(resume=False)
trainer.train()

d986b45953d50c6357601eb663823b8d.png

下面使用我们的 梦中情炉 ~ torchkeras ~ 实现最优雅的训练循环~  😋😋

ds_train = detectron2.data.DatasetFromList(dicts_train)
ds_val = detectron2.data.DatasetFromList(dicts_val)

mp = detectron2.data.DatasetMapper(cfg,is_train=True)
batch_size = 16
dl_train = detectron2.data.build_detection_train_loader(ds_train,
        mapper=mp,total_batch_size=batch_size,num_workers=2)
dl_train.size = len(ds_train)//batch_size 

dl_val = detectron2.data.build_detection_train_loader(ds_val,
        mapper=mp,total_batch_size=1,num_workers=2)
dl_val.size = len(dicts_val)
for batch in dl_val:
    break
from torchkeras import KerasModel 
from tqdm import tqdm 
from detectron2.utils.events import EventStorage 

class StepRunner:
    def __init__(self, net, loss_fn, accelerator, 
                 stage = "train", metrics_dict = None, 
                 optimizer = None, lr_scheduler = None
                 ):
        self.net,self.loss_fn,self.metrics_dict,self.stage = net,loss_fn,metrics_dict,stage
        self.optimizer,self.lr_scheduler = optimizer,lr_scheduler
        self.accelerator = accelerator
        
        if self.stage=='train':
            self.net.train() 
        else:
            self.net.train() 
    
    def __call__(self, batch):
        
        #loss
        with EventStorage() as event_storage:
            loss_dict = self.net(batch)
            
        loss = sum(loss_dict.values())
        
        #backward()
        if self.optimizer is not None and self.stage=="train":
            self.accelerator.backward(loss)
            self.optimizer.step()
            if self.lr_scheduler is not None:
                self.lr_scheduler.step()
            self.optimizer.zero_grad()
            
        all_loss = self.accelerator.gather(loss).sum()
        
        #losses
        step_losses = {self.stage+"_loss":all_loss.item()}
        
        #metrics
        step_metrics = {}
        
        if self.stage=="train":
            if self.optimizer is not None:
                step_metrics['lr'] = self.optimizer.state_dict()['param_groups'][0]['lr']
            else:
                step_metrics['lr'] = 0.0
        return step_losses,step_metrics
    
class EpochRunner:
    def __init__(self,steprunner,quiet=False):
        self.steprunner = steprunner
        self.stage = steprunner.stage
        self.accelerator = self.steprunner.accelerator
        self.quiet = quiet
        
    def __call__(self,dataloader):
        
        try:
            n = len(dataloader)
        except Exception as err:
            n = dataloader.size 
        loop = tqdm(enumerate(dataloader,start=1), 
                    total =n,
                    file=sys.stdout,
                    disable=not self.accelerator.is_local_main_process or self.quiet,
                    ncols = 100
                   )
        epoch_losses = {}
        for step, batch in loop: 
            step_losses,step_metrics = self.steprunner(batch)   
            step_log = dict(step_losses,**step_metrics)
            for k,v in step_losses.items():
                epoch_losses[k] = epoch_losses.get(k,0.0)+v
            if step<n:
                loop.set_postfix(**step_log)
            elif step==n:
                epoch_metrics = step_metrics
                epoch_metrics.update({self.stage+"_"+name:metric_fn.compute().item() 
                                 for name,metric_fn in self.steprunner.metrics_dict.items()})
                epoch_losses = {k:v/step for k,v in epoch_losses.items()}
                epoch_log = dict(epoch_losses,**epoch_metrics)
                loop.set_postfix(**epoch_log)
                for name,metric_fn in self.steprunner.metrics_dict.items():
                    metric_fn.reset()
            else:
                break 
        return epoch_log
    
    
KerasModel.StepRunner = StepRunner 
KerasModel.EpochRunner = EpochRunner
params = [p for p in model.parameters() if p.requires_grad]
optimizer = torch.optim.AdamW(params, lr=1e-4)

keras_model = KerasModel(model,
                         loss_fn = None,
                         metrics_dict=None,
                         optimizer= optimizer
                        )
ckpt_path = 'checkpoint.pt'
keras_model.fit(train_data=dl_train,val_data=dl_val,
    epochs=30,patience=10,
    monitor='val_loss',
    mode='min',
    ckpt_path =ckpt_path,
    plot=True
)

d20e28e90b0da6c1378d625a97abab6a.png

991498058a2c892eb34aefbccd1b9fcf.png

四,评估模型

from detectron2.evaluation import COCOEvaluator, inference_on_dataset
from detectron2.data import build_detection_test_loader


evaluator = COCOEvaluator("balloon_val", output_dir="./output")
dl_val = build_detection_test_loader(cfg, "balloon_val")
print(inference_on_dataset(model, dl_val, evaluator))

五,使用模型

from detectron2.engine import DefaultPredictor

#cfg.MODEL.WEIGHTS = os.path.join(cfg.OUTPUT_DIR, "model_final.pth") 
cfg.MODEL.WEIGHTS = ckpt_path
cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.7   # set a custom testing threshold
predictor = DefaultPredictor(cfg)
from detectron2.utils.visualizer import ColorMode

im = cv2.imread(dicts_val[10]['file_name'])
outputs = predictor(im)  # format is documented at https://detectron2.readthedocs.io/tutorials/models.html#model-output-format
vis = Visualizer(im[:, :, ::-1],
               metadata=balloon_metadata, 
               scale=0.5, 
               instance_mode=ColorMode.IMAGE_BW   # remove the colors of unsegmented pixels. This option is only available for segmentation models
)
out = vis.draw_instance_predictions(outputs["instances"].to("cpu"))
cv2_show(out.get_image()[:, :, ::-1])

3bb7048f1c15ce6e3bdec53bf9faa9ff.png

公众号算法美食屋后台回复关键词: torchkeras,获取本文源代码和balloon数据集下载链接。

万水千山总是情,点个赞赞行不行?😋😋

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值