欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

SOLOV2源码解析

时间:2023-08-19
一、 train部分 1.1 总体框架(主要介绍detectron2框架)

执行train_net.py-->

args = default_argument_parser().parse_args()加载参数-->

launch函数判断是否为多gpu训练,从launch末尾进入train_net的main函数-->

main函数主要是读取参数,同时判断是否先进入test(不进入)。之后根据从trainer = Trainer(cfg)进入Trainer类,同样是理解读取参数,具体操作包括加载数据集,模型,优化器等,

之后回到main函数-->

根据return trainer.train()函数进入Trainer类里的train函数-->

从self.train函数进入train_net.py的self.train_loop函数(开始训练)-->

训练分为三部分,如下:

self.before_step()

self.run_step()

self.after_step()

其中,hook函数只放在before和after中,而run函数专门负责训练,即执行如下语句

self._trainer.run_step() #位于defaults.py第495行

值得一提的是,根据公约,计算比较复杂的操作一般放在after_step()函数中,而不放在before_step中。

1.2 参数读取

        训练所需参数读取自/AdelaiDet/configs/SOLOv2/R50_3x.yaml文件,同时这个文件还继承自/AdelaiDet/configs/SOLOv2/base-SOLOv2.yaml文件。后者中与前者相同的信息会被前者覆盖。但是实际训练用到的参数远多于这些,这些没有在上文提及的参数取自文件/AdelaiDet/adet/config/defaults.py。

1.3 Trainer(cfg)部分

trainer=Trainer(cfg)是将定义在train_net.py的Trainer类实例化的过程。这个类是继承自DefaultTrainer类,又继承自Trainerbase类(这两个被集成的类都有__init__方法而Trainer类没有)。因此会先执行他们的__init__方法。

在DefaultTrainer(&Trainerbase)类的__init__方法会进行一些必要的操作,包括上文提到的,根据cfg加载数据集,模型,优化器,保存权重轮数,钩子等。如下:

def __init__(self, cfg): """ Args: cfg (CfgNode): """ super().__init__() logger = logging.getLogger("detectron2") if not logger.isEnabledFor(logging.INFO): # setup_logger is not called for d2 setup_logger() cfg = DefaultTrainer.auto_scale_workers(cfg, comm.get_world_size()) # Assume these objects must be constructed in this order. model = self.build_model(cfg) # 得到SOLOV2的模型,搭积木核心 optimizer = self.build_optimizer(cfg, model) # 选择优化器 data_loader = self.build_train_loader(cfg) #数据加载器,保存了图片的信息。 训练时直接传入这三个参数即可 model = create_ddp_model(model, broadcast_buffers=False) #如果是多进程,则对model进行修改。这里默认是不修改(return model) self._trainer = (AMPTrainer if cfg.SOLVER.AMP.ENABLED else SimpleTrainer)( #执行trainer的初始化,也就是run.step函数所在类的init部分 model, data_loader, optimizer ) self.scheduler = self.build_lr_scheduler(cfg, optimizer) #从配置中调度lr程序。 self.checkpointer = DetectionCheckpointer( # Assume you want to save checkpoints together with logs/statistics model, cfg.OUTPUT_DIR, trainer=weakref.proxy(self), ) # self.checkpointer.save("model_999") self.start_iter = 0 self.max_iter = cfg.SOLVER.MAX_ITER #最大轮数 self.cfg = cfg self.register_hooks(self.build_hooks())

Trainer(cfg)有几个值得一提的函数,接下来将详细介绍他们。

1.3.0 self.build_model(cfg)函数

这句话是读取参数的核心,用于构建模型。内容较多,之后在soloV2.py一节有详细阐述。

1.3.1 build_lr_scheduler函数

这个函数的意义是从配置中构建LR调度程序,语句如下:

self.scheduler = self.build_lr_scheduler(cfg, optimizer)

这个函数定义在build.py,主要分为两部分。

1.3.1.1 sched = MultiStepParamScheduler函数

第一部分为sched = MultiStepParamScheduler()部分,这部分根据输入的各部分轮数(非base.yaml)得到了每部分的步长。在原版里,输入的是(210000,250000),270000(MAX),各部分间步长为1,0.1,0.01。(但我没在yaml文件中找到0.1的参数,可能是默认的)

sched = MultiStepParamScheduler( #得到每一段的步长。信息输入取自R50_3x.yaml配置文件。 文件中三个数210000,250000,270000,三段间步长分别为1,0.1,0.01 这个步长我没在yaml文件中找到,可能是默认的 values=[cfg.SOLVER.GAMMA ** k for k in range(len(steps) + 1)], # 每一段为0.1的n次方。n为段数,也就是1,0.1,0.01 milestones=steps, num_updates=cfg.SOLVER.MAX_ITER, )

1.3.1.2sched = WarmupParamScheduler()函数

第二部分为sched = WarmupParamScheduler()部分,这部分根据输入的各部分预热轮数(base.yaml)得到了各预热部分的步长,并对sched进行修改(相当于把前面那一小点儿变为预热训练)。

class WarmupParamScheduler(CompositeParamScheduler): """ Add an initial warmup stage to another scheduler. """ def __init__( self, scheduler: ParamScheduler, warmup_factor: float, #预热的初始值 warmup_length: float, #预热长度所占比例(乘以总长度即为预热长度) warmup_method: str = "linear", #预热方法:线性 ): """ Args: scheduler: warmup will be added at the beginning of this scheduler warmup_factor: the factor w.r.t the initial value of ``scheduler``, e.g、0.001 warmup_length: the relative length (in [0, 1]) of warmup steps w.r.t the entire training, e.g、0.01 warmup_method: one of "linear" or "constant" """ end_value = scheduler(warmup_length) # 预热结束时步长(不确定是实际步长还是步长与正常步长比值)the value to reach when warmup ends start_value = warmup_factor * scheduler(0.0) # 预热开始时步长 if warmup_method == "constant": warmup = ConstantParamScheduler(start_value) elif warmup_method == "linear": warmup = LinearParamScheduler(start_value, end_value) else: raise ValueError("Unknown warmup method: {}".format(warmup_method)) super().__init__( [warmup, scheduler], interval_scaling=["rescaled", "fixed"], lengths=[warmup_length, 1 - warmup_length], )

1.3.2 register_hooks函数

DefaultTrainer类的__init__方法最后一句为登记钩子,即:

self.register_hooks(self.build_hooks())

DefaultTrainer类有自己的build_hooks()方法,但我们在Trainer类中重写了这个方法,所以程序会执行我们重写的方法(但我们重写的这个方法有继承自原来的方法,所以从结果上看是两个方法都执行了)

在DefaultTrainer类中,程序默认注册了六个钩子,如下:

hooks.IterationTimer(), #计算花费时间并在训练结束时打印总结 hooks.LRScheduler(), #torch内置LR调度程序 hooks.PreciseBN( # Run at the same freq as (but before) evaluation. cfg.TEST.eval_PERIOD, self.model, # Build a new data loader to not affect training self.build_train_loader(cfg), cfg.TEST.PRECISE_BN.NUM_ITER, ) #据说和BN层有关,有啥关我没太看懂 每period执行一轮(但是默认不加载这个hook) hooks.PeriodicCheckpointer(self.checkpointer, cfg.SOLVER.CHECKPOINT_PERIOD) #定期保存检查点(权重) 每period(默认5000)执行一轮 或者大于等于所设置的最高轮数时保存 # 这里我为了方便观察先将5000改为了1000 hooks.evalHook(cfg.TEST.eval_PERIOD, test_and_save_results) #定期进行评价每period(默认0轮,即不执行)执行一轮 hooks.PeriodicWriter(self.bu,ld_writers(), period=20) #定期将事件写入EventStorage 就是输出的loss等信息(调用write.write函数) #每period(默认20轮)输出一次信息

1.4 run_step部分

        这一部分对应4.1.1中提到的语句

def run_step(self): self._trainer.iter = self.iter self._trainer.run_step() #位于defaults.py第495行

其核心语句self._trainer.run_step()如下

def run_step(self): """ Implement the standard training logic described above. """ assert self.model.training, "[SimpleTrainer] model was changed to eval mode!" #判断是否处于training模式,否则输出: "[SimpleTrainer] model was changed to eval mode!" start = time.perf_counter() """ If you want to do something with the data, you can wrap the dataloader. """ data = next(self._data_loader_iter) #得到图片信息(路径,长宽,像素信息等) data_time = time.perf_counter() - start """ If you want to do something with the losses, you can wrap the model. """ loss_dict = self.model(data) #得到loss if isinstance(loss_dict, torch.Tensor): losses = loss_dict loss_dict = {"total_loss": loss_dict} else: losses = sum(loss_dict.values()) """ If you need to accumulate gradients or do something similar, you can wrap the optimizer with your custom `zero_grad()` method. """ self.optimizer.zero_grad() #清空原梯度 losses.backward() # 反向传播 self._write_metrics(loss_dict, data_time) #输出loss信息 """ If you need gradient clipping/scaling or other processing, you can wrap the optimizer with your custom `step()` method、But it is suboptimal as explained in https://arxiv.org/abs/2006.15704 Sec 3.2.4 """ self.optimizer.step()

二、solov2.py 2.1 总体流程

放上一位大佬绘制的流程框图(红笔为笔者写的注释):

其中backbone包括resnet部分和FPN部分。结束时输出FPN的五层。

三条支路从左到右分别对应kernel预测(卷积核),cate预测(类别)和mask预测(实例)。

训练过程中分支分为ins分支和mask分支,ins分支得到的是kernel预测和cate预测,mask分支得到的是mask预测(而在loss输入里,又把mask预测看做ins预测)(注意,ins分支和ins预测没有任何关系)

最终cate预测得到类别信息(loss_cate),kernel预测和mask预测(即mask预测得到特征图,经过kernel一次卷积得到结果)共同得到掩码信息(loss_ins)

 参考文献:

solov2网络结构图_Jumi爱笑笑的博客-CSDN博客_solov2网络结构

2.2 具体流程

    在solov2.py中,一共定义了三个类,分别是SOLOv2,SOLOv2InsHead,SOLOv2MaskHead。其中SOLOV2类内容较多,包括损失函数等。而其余两类只有__init__方法和forward方法。接下来将他们三个的介绍分别写在三个小节里(2.3-2.5)。具体的运算,难点等将在各自的模块中提及。本小节只负责介绍在训练过程中数据流动的总体流程,并简要概括一些操作。

2.2.1 __init__部分

        在执行1.3.0 self.build_model(cfg)函数时,会对SOLOv2类进行初始化(__init__)操作。具体各参数的注释写在了代码上。在初始化solov2类的过程中,有如下几行代码

# build the ins head. instance_shapes = [backbone_shape[f] for f in self.instance_in_features] self.ins_head = SOLOv2InsHead(cfg, instance_shapes) # build the mask head. mask_shapes = [backbone_shape[f] for f in self.mask_in_features] self.mask_head = SOLOv2MaskHead(cfg, mask_shapes)

        在调用他们的过程中,同时对剩下两个类SOLOv2InsHead和SOLOv2MaskHead进行了初始化。

2.2.2 forward部分

        之后程序一直没有走进solov2.py,直到执行到函数1.4 run_step时,标志着训练正式开始。此时步入SOLOV2类的forward方法。

 2.3 class:SOLOv2(核心) 2.3.1 __init__方法

        读取一些cfg中的参数,并通过函数

self.backbone = build_backbone(cfg)

构建主干网络 。

2.3.2 forward方法

第一步:对图像进行预处理,即执行如下语句:       

images = self.preprocess_image(batched_inputs)

         其中,输入的batched_inputs包括了图片全部信息,而images是一个元素为tensor的list。通过此方法得到了图片预处理(包括归一化,转化均值方差等操作)过后的像素信息。

第二步:通过如下语句:

if "instances" in batched_inputs[0]: gt_instances = [x["instances"].to(self.device) for x in batched_inputs] elif "targets" in batched_inputs[0]: log_first_n( logging.WARN, "'targets' in the model inputs is now renamed to 'instances'!", n=10 ) gt_instances = [x["targets"].to(self.device) for x in batched_inputs] else: gt_instances = None

 将图片的标注信息(如类别,gt框等)保存到gt_instances中。

第三步:通过如下语句得到p2-p6五个头

features = self.backbone(images.tensor)

第四步:ins分支(其实是cate和kernel部分)

ins分支执行如下语句

# ins branch ins_features = [features[f] for f in self.instance_in_features] ins_features = self.split_feats(ins_features) cate_pred, kernel_pred = self.ins_head(ins_features)

通过函数self.split_feats()对FPN得到的五层中最上层进行下采样,最下层进行上采样,如下(不同图片尺寸对应数据可能不同):

( 1,256,176,240)-->( 1,256,88,120)

( 1,256,88,120)-->( 1,256,88,120)

( 1,256,44,60)-->( 1,256,44,60)

( 1,256,22,30)-->( 1,256,22,30)

( 1,256,11,15)-->( 1,256,22,30)

然后通过self.ins_head()语句得到对种类(cate)和卷积核(kernel)的预测(详见2.4.2 )

第五步:mask分支

mask分支执行如下语句

# mask branch mask_features = [features[f] for f in self.mask_in_features] mask_pred = self.mask_head(mask_features)

通过self.mask_head得到对掩码(mask)的预测(详见2.5.2 )

第六步:判断模型是训练还是评价。如果是训练则计算损失函数并返回loss,如果是评价则进行inference并返回result。

 2.4 class:SOLOv2InsHead 2.4.1 __init__方法

这里__init__方法的作用就是构建网络。在调用__init__ 方法时,用到了如下语句

head_configs = {"cate": (cfg.MODEL.SOLOV2.NUM_INSTANCE_CONVS, #创建一个字典 cfg.MODEL.SOLOV2.USE_DCN_IN_INSTANCE, False), "kernel": (cfg.MODEL.SOLOV2.NUM_INSTANCE_CONVS, cfg.MODEL.SOLOV2.USE_DCN_IN_INSTANCE, cfg.MODEL.SOLOV2.USE_COORD_CONV) }

创造了一个字典。在之后又用了如下语句

for head in head_configs:

为两个分支(cate和kernel)添加卷积层。添加的结果如下

cate:256-->512-->512-->512-->80 kernel:258-->512-->512-->512-->256 (这里的258应该是256后面加入了xy坐标)

之后都是些辅助性步骤,对初始权重进行填充。构建出的网络如下(应该分别对应上图的中间支路和左支路)

SOLOv2InsHead(
  (cate_tower): Sequential(
    (0): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (1): GroupNorm(32, 512, eps=1e-05, affine=True)
    (2): ReLU(inplace=True)
    (3): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (4): GroupNorm(32, 512, eps=1e-05, affine=True)
    (5): ReLU(inplace=True)
    (6): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (7): GroupNorm(32, 512, eps=1e-05, affine=True)
    (8): ReLU(inplace=True)
    (9): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (10): GroupNorm(32, 512, eps=1e-05, affine=True)
    (11): ReLU(inplace=True)
  )
  (kernel_tower): Sequential(
    (0): Conv2d(258, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (1): GroupNorm(32, 512, eps=1e-05, affine=True)
    (2): ReLU(inplace=True)
    (3): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (4): GroupNorm(32, 512, eps=1e-05, affine=True)
    (5): ReLU(inplace=True)
    (6): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (7): GroupNorm(32, 512, eps=1e-05, affine=True)
    (8): ReLU(inplace=True)
    (9): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
    (10): GroupNorm(32, 512, eps=1e-05, affine=True)
    (11): ReLU(inplace=True)
  )
  (cate_pred): Conv2d(512, 80, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (kernel_pred): Conv2d(512, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)

2.4.2 forward方法

从2.3.2 第四步 ins分支导入。方法都比较简单,每一步注释都写在后面了:

def forward(self, features): """ Arguments: features (list[Tensor]): FPN feature map tensors in high to low resolution. Each tensor in the list correspond to different feature levels. Returns: pass """ cate_pred = [] kernel_pred = [] for idx, feature in enumerate(features): # feature返回每个元素,idx返回每个元素的索引 ins_kernel_feat = feature # concat coord x_range = torch.linspace(-1, 1, ins_kernel_feat.shape[-1], device=ins_kernel_feat.device) #等间距生成从-1到1的120个数(即对应层宽度) y_range = torch.linspace(-1, 1, ins_kernel_feat.shape[-2], device=ins_kernel_feat.device) #等间距生成从-1到1的88个数(即对应层高度) y, x = torch.meshgrid(y_range, x_range) #用于划分单元网格,生成shape为(88,120),各行列分别被二者(y_range, x_range)对应元素填充的tensor 参考文献:https://blog.csdn.net/weixin_39504171/article/details/106356977 y = y.expand([ins_kernel_feat.shape[0], 1, -1, -1]) x = x.expand([ins_kernel_feat.shape[0], 1, -1, -1]) # 在最前面加两个一维,即(88,120)->(1,1,88,120) coord_feat = torch.cat([x, y], 1) # (1,1,88,120)-->(1,2,88,120) ins_kernel_feat = torch.cat([ins_kernel_feat, coord_feat], 1)# (1,1,88,120)-->(1,258,88,120) 这一段的作用就是将坐标xy添加到最后两个通道 # individual feature. kernel_feat = ins_kernel_feat seg_num_grid = self.num_grids[idx] #self.num_grids是一个列表,这里根据idx索引找到列表对应元素 应该是对不同层设置的grid个数 kernel_feat = F.interpolate(kernel_feat, size=seg_num_grid, mode='bilinear') #双线性插值,(1,1,88,120)-->(1,1,40,40) cate_feat = kernel_feat[:, :-2, :, :] #抓出前256维,即(1,256,40,40) # kernel kernel_feat = self.kernel_tower(kernel_feat) kernel_pred.append(self.kernel_pred(kernel_feat)) #执行针对kernel的卷积并保存结果 # cate cate_feat = self.cate_tower(cate_feat) cate_pred.append(self.cate_pred(cate_feat)) #执行针对cate的卷积并保存结果 return cate_pred, kernel_pred

2.5 class:SOLOv2MaskHead 2.4.1 __init__方法

类似的,这里只用到了P2-P5四层,通过如下代码段构建神经网络层

for i in range(self.num_levels):

这里一共两个循环(i,j),创建了三个存储神经网络的列表,分别是

conv_tower:每个小循环刷新 (list类型)convs_per_level:每个大循环刷新 (sequential类型)self.convs_all_levels:不刷新 (modulelist类型)(最终保存的)

j循环比i循环小1;

当i=0时或j=0时,都进行一些相对特殊的操作;

此外,当i=3时,输入通道+2。

总之,通过这段代码构建出了如下网络(应该对应上图最右支路)

ModuleList(
  (0): Sequential(
    (conv0): Sequential(
      (0): Conv2d(256, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (1): GroupNorm(32, 128, eps=1e-05, affine=True)
      (2): ReLU()
    )
  )
  (1): Sequential(
    (conv0): Sequential(
      (0): Conv2d(256, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (1): GroupNorm(32, 128, eps=1e-05, affine=True)
      (2): ReLU()
    )
    (upsample0): Upsample(scale_factor=2.0, mode=bilinear)
  )
  (2): Sequential(
    (conv0): Sequential(
      (0): Conv2d(256, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (1): GroupNorm(32, 128, eps=1e-05, affine=True)
      (2): ReLU()
    )
    (upsample0): Upsample(scale_factor=2.0, mode=bilinear)
    (conv1): Sequential(
      (0): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (1): GroupNorm(32, 128, eps=1e-05, affine=True)
      (2): ReLU()
    )
    (upsample1): Upsample(scale_factor=2.0, mode=bilinear)
  )
  (3): Sequential(
    (conv0): Sequential(
      (0): Conv2d(258, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (1): GroupNorm(32, 128, eps=1e-05, affine=True)
      (2): ReLU()
    )
    (upsample0): Upsample(scale_factor=2.0, mode=bilinear)
    (conv1): Sequential(
      (0): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (1): GroupNorm(32, 128, eps=1e-05, affine=True)
      (2): ReLU()
    )
    (upsample1): Upsample(scale_factor=2.0, mode=bilinear)
    (conv2): Sequential(
      (0): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (1): GroupNorm(32, 128, eps=1e-05, affine=True)
      (2): ReLU()
    )
    (upsample2): Upsample(scale_factor=2.0, mode=bilinear)
  )
)

2.4.2 forward方法

从2.3.2 第五步 mask分支导入。

如上图所说,先对FPN输出的前四层各自卷积,并上采样到跟第一层一样的大小(1,128,176,240)然后相加(应该是对应元素相加,不是concat) 。

def forward(self, features): """ Arguments: features (list[Tensor]): FPN feature map tensors in high to low resolution. Each tensor in the list correspond to different feature levels. Returns: pass """ assert len(features) == self.num_levels, print("The number of input features should be equal to the supposed level.") # bottom features first.,对FPN输出的前四层各自卷积,并上采样到跟第一层一样的大小(1,128,176,240)然后相加(应该是对应元素相加,不是concat) feature_add_all_level = self.convs_all_levels[0](features[0]) # 第一层初始(1,256,176,240) 注意这里和ins分支不同,因为没有经过spilt_feats的下采样 for i in range(1, self.num_levels): # 2-4层初始分别为(...,88,120,(...,44,60),(...,22,30) mask_feat = features[i] # 值得一提的是,第四层卷积前还加入了xy两个通道(第四层设计的卷积输入本来就是258,不影响输出), if i == 3: # add for coord. x_range = torch.linspace(-1, 1, mask_feat.shape[-1], device=mask_feat.device) y_range = torch.linspace(-1, 1, mask_feat.shape[-2], device=mask_feat.device) y, x = torch.meshgrid(y_range, x_range) y = y.expand([mask_feat.shape[0], 1, -1, -1]) x = x.expand([mask_feat.shape[0], 1, -1, -1]) coord_feat = torch.cat([x, y], 1) mask_feat = torch.cat([mask_feat, coord_feat], 1) # add for top features. feature_add_all_level += self.convs_all_levels[i](mask_feat) mask_pred = self.conv_pred(feature_add_all_level) return mask_pred

4.2 test部分 4.1.1 数据流动

执行train_net.py-->

args = default_argument_parser().parse_args()加载参数-->

launch函数判断是否为多gpu训练,从launch末尾进入train_net的main函数-->

main函数主要是读取参数,同时判断是否先进入test(进入)-->

通过Trainer.build_model函数加载被评价的model信息和超参数信息(比如各个层是干什么的,不包括加载权重参数weights)-->

通过AdetCheckpointer函数向model加载权重信息-->

之后由main函数进入python3.7/site-packages/detectron2/engine/defaults.py的test函数,根据输入的model,cfg和评价器类型(默认NONE)进行评价-->

如果评价器为NONE,则会进入train_net.py的build_evaluatior函数帮你构建评价器。这个函数会根据你在builtin中的注册,来选择合适的评价器(所以千万不要注册错)

之后进入detectron2/evalution/evaluator.py的inference_on_dataset函数,通过model函数运行得到分割结果并进入评价阶段-->

评价阶段用的是evaluator.process(inputs, outputs) 函数,定义在adet/evalution/text_evaluation.py中。其中input是原照片的信息,output是预测分割的结果(为什么没有原图分割信息?)-->

之后从这个函数进入同py文件中的instances_to_coco_json函数。这个函数作用应该是理解得到的参数。但是会报bug3.2.2。因为

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。