p2b网络

把p2b的工作推广到p2rb
目的:学习目标检测,熟悉目标检测,为自己写论文打基础

我的碎碎念:真的是fuck了,自己这个东西整了这么久,还是没有整出来,从5月分我就开始了把。因为考试,因为自己喜欢玩游戏,因为我tm真的浪费了好多时间,像个废物,学校还封校&我很不爽。
就这样把,去死吧fuck day!!!

p2b模型为,从点标注实现框检测。是一个很神奇的网络。
本文的逻辑是:利用点标注,生成框标注,利用框,进行强监督。这就是为什么模型会有两个部分组成,并且有前后逻辑关系。那么这个模型能不能好,主要就是取决于点到框标注,生成的怎么样。
训练好网络之后,只需要只是用第二个模块,便是一个detection网络。

第一部分:p2b网络

整体逻辑

首先,经过了backbone和neck模块后,会根据stage的不同,做出不同的训练策略


for stage in range(self.num_stages):
if stage == 0:
    generate_proposals, proposals_valid_list = gen_proposals_from_cfg(gt_points, base_proposal_cfg,
                                                                      img_meta=img_metas)
    dynamic_weight = torch.cat(gt_labels).new_ones(len(torch.cat(gt_labels)))
    neg_proposal_list, neg_weight_list = None, None

    pseudo_boxes = generate_proposals

else:
    generate_proposals, proposals_valid_list = fine_proposals_from_cfg(pseudo_boxes, fine_proposal_cfg,
                                                                       img_meta=img_metas,
                                                                       stage=stage)
    neg_proposal_list, neg_weight_list = gen_negative_proposals(gt_points, fine_proposal_cfg,
                                                                generate_proposals,
                                                                img_meta=img_metas)


roi_losses, pseudo_boxes, dynamic_weight = self.roi_head.forward_train(stage, x, img_metas,
                                                                                   pseudo_boxes,
                                                                                   generate_proposals,
                                                                                   proposals_valid_list,
                                                                                   neg_proposal_list, neg_weight_list,
                                                                                   gt_true_bboxes, gt_labels,
                                                                                   dynamic_weight,
                                                                                   gt_bboxes_ignore, gt_masks,
                                                                                   **kwargs)

首先,stage==0,会根据设定好的大小和比例,在每一个gt_points为中心,生成一系列的框,更具体一点:
6种不同的尺寸4,8,16,32,64,128
7种不同的比例0.33,0.5,0.66,1,1.5,2,3
在一幅图片的7个gt_points附近,生成了(7*42,4)的数据

生成好初始的框之后,便进入了网络最重要的一部分:
self.roi_head.forward_train(*)

    def _bbox_forward_train(self, x, proposal_list_base, proposals_list, proposals_valid_list, neg_proposal_list,
                            neg_weight_list, gt_points,
                            gt_labels,
                            cascade_weight,
                            img_metas, stage):
        rois = bbox2roi(proposals_list)
        bbox_results = self._bbox_forward(x, rois, gt_points, stage)
        num_instance = bbox_results['num_instance']
        gt_labels = torch.cat(gt_labels)
        proposals_valid_list = torch.cat(proposals_valid_list).reshape(
            *bbox_results['cls_score'].shape[:2], 1)
            
        if neg_proposal_list is not None:
            neg_rois = bbox2roi(neg_proposal_list)
            neg_bbox_results = self._bbox_forward(x, neg_rois, None, stage)  ######stage
            neg_cls_scores = neg_bbox_results['cls_score']
            neg_weights = torch.cat(neg_weight_list)
        else:
            neg_cls_scores = None
            neg_weights = None
        reg_box = bbox_results['bbox_pred']
        if reg_box is not None:
            boxes_pred = self.bbox_head.bbox_coder.decode(torch.cat(proposals_list).reshape(-1, 4),
                                                          reg_box.reshape(-1, 4)).reshape(reg_box.shape)
        else:
            boxes_pred = None

        proposals_list_to_merge = proposals_list

        pseudo_boxes, mean_ious, filtered_boxes, filtered_scores, dynamic_weight = self.merge_box(bbox_results,
                                                                                                  proposals_list_to_merge,
                                                                                                  proposals_valid_list,
                                                                                                  gt_labels,
                                                                                                  gt_points,
                                                                                                  img_metas, stage)
        bbox_results.update(pseudo_boxes=pseudo_boxes)
        bbox_results.update(dynamic_weight=dynamic_weight.sum(dim=-1))
      
        pseudo_boxes = torch.cat(pseudo_boxes)
        if stage == self.num_stages - 1:
            retrain_weights = self._bac_assigner(torch.cat(proposals_list), torch.cat(proposal_list_base))
        else:
            retrain_weights = None
            
        loss_instance_mil = self.bbox_head.loss_mil(stage, bbox_results['cls_score'], bbox_results['ins_score'],
                                                    proposals_valid_list,
                                                    neg_cls_scores, neg_weights,
                                                    boxes_pred, gt_labels,
                                                    torch.cat(proposal_list_base), label_weights=cascade_weight,
                                                    retrain_weights=retrain_weights)
        loss_instance_mil.update({"mean_ious": mean_ious[-1]})
        loss_instance_mil.update({"s": mean_ious[0]})
        loss_instance_mil.update({"m": mean_ious[1]})
        loss_instance_mil.update({"l": mean_ious[2]})
        loss_instance_mil.update({"h": mean_ious[3]})

        bbox_results.update(loss_instance_mil=loss_instance_mil)

        return bbox_results

上面这一段函数,有一些非常重要的子函数,这里非常有必要好好介绍并解释:

  • bbox2roi :(294,4),(168,4) -->(462,5)这里是把两个初始坐标concatenate在一起,然后加入image_id以加以区分
  • bbox_forward:这是最最重要的函数其包含很多函数和模块,下面会详细介绍
  • merge_box
  • loss_mil
    上面的除了第一个都需要篇幅进行介绍。需要值得注意的是,这里的部分的代码都是stage0和1都需要跑的部分,这并不是代码stage0,1走的是一条路径,在每一个函数中,它们分别用了不同的处理。

经过了bbox_forward之后,会根据有无负例框,有无reg_box进行一些操作。而这些操作,在stage=0的时候都是略过的。在stage=1的时候,我们会一一介绍

然后便进入了merg_box函数–请从目录中进入
merge之后,stage0也就步入了尾声,最后计算loss

loss_mil—

pseudo_boxes, mean_ious, filtered_boxes, filtered_scores, dynamic_weight =
	self.merge_box(
	bbox_results,
	proposals_list_to_merge,
	proposals_valid_list,
	gt_labels,
	gt_points,
	img_metas, stage
	)

bbox_forward

_bbox_forward(x,rois,gt_points,stage)

这一段代码:根据roi从图片中提取特征,传入head模块提取分类得分,实例得分,回归框。
经过reshape之后传回一个字典
请添加图片描述

bbox_results = dict(cls_score=cls_score, ins_score=ins_score, bbox_pred=reg_box,
                 bbox_feats=bbox_feats, num_instance=num_gt)

merge_box

merge_box(self, bbox_results, proposals_list, proposals_valid_list, gt_labels, gt_bboxes, img_metas, stage)

还记得之前提到,在每一个gt点的附近生成了42个不同尺寸比例的框,这一步就是融合这些不同的框,使得每一个gt点只保留一个框,然后进行强监督训练。那么,如何决定最终保留的框呢?
这里采用根据分类得分预测的前7名,在通过加权平均的方式得到坐标。

这一部分的代码用到了非常巧妙的取切片的操作。
而其中分类得分的预测就在上一个函数中已经计算获得。

这部分代码的输出如下图:pseud_boxes:list2,tensor(7,4),tensor(4,4)
在这里插入图片描述


以上是宏观上,下面从代码层面细细分析一下。

def merge_box(self, bbox_results, proposals_list, proposals_valid_list, gt_labels, gt_bboxes, img_metas, stage):
	# 7 42 81 shape of cls is same as ins_scores
    cls_scores = bbox_results['cls_score']
    ins_scores = bbox_results['ins_score']
    num_instances = bbox_results['num_instance']
   
    if stage < 1:
        cls_scores = cls_scores.softmax(dim=-1)
    else:
        cls_scores = cls_scores.sigmoid()
    ins_scores = ins_scores.softmax(dim=-2) * proposals_valid_list
    ins_scores = F.normalize(ins_scores, dim=1, p=1)
    cls_scores = cls_scores * proposals_valid_list
    dynamic_weight = (cls_scores * ins_scores)
    dynamic_weight = dynamic_weight[torch.arange(len(cls_scores)), :, gt_labels]
    cls_scores = cls_scores[torch.arange(len(cls_scores)), :, gt_labels]
    ins_scores = ins_scores[torch.arange(len(cls_scores)), :, gt_labels]
    # split batch
    batch_gt = [len(b) for b in gt_bboxes]
    cls_scores = torch.split(cls_scores, batch_gt)
    ins_scores = torch.split(ins_scores, batch_gt)
    gt_labels = torch.split(gt_labels, batch_gt)
    dynamic_weight_list = torch.split(dynamic_weight, batch_gt)
    if not isinstance(proposals_list, list):
        proposals_list = torch.split(proposals_list, batch_gt)
    stage_ = [stage for _ in range(len(cls_scores))]
    
    boxes, filtered_boxes, filtered_scores = multi_apply(self.merge_box_single, cls_scores, ins_scores,
                                                         dynamic_weight_list,
                                                         gt_bboxes,
                                                         gt_labels,
                                                         proposals_list,
                                                         img_metas, stage_)
    pseudo_boxes = torch.cat(boxes).detach()
    # mean_ious =torch.tensor(mean_ious).to(gt_point.device)
    iou1 = bbox_overlaps(pseudo_boxes, torch.cat(gt_bboxes), is_aligned=True)

    ### scale mean iou
    gt_xywh = bbox_xyxy_to_cxcywh(torch.cat(gt_bboxes))
    scale = gt_xywh[:, 2] * gt_xywh[:, 3]
    mean_iou_s = iou1[scale < 32 ** 2].sum() / (len(iou1[scale < 32 ** 2]) + 1e-5)
    mean_iou_m = iou1[(scale > 32 ** 2) * (scale < 64 ** 2)].sum() / (len(
        iou1[(scale > 32 ** 2) * (scale < 64 ** 2)]) + 1e-5)
    mean_iou_l = iou1[(scale > 64 ** 2) * (scale < 128 ** 2)].sum() / (len(
        iou1[(scale > 64 ** 2) * (scale < 128 ** 2)]) + 1e-5)
    mean_iou_h = iou1[scale > 128 ** 2].sum() / (len(iou1[scale > 128 ** 2]) + 1e-5)

    mean_ious_all = iou1.mean()
    mean_ious = [mean_iou_s, mean_iou_m, mean_iou_l, mean_iou_h, mean_ious_all]

    if self.test_mean_iou and stage == 1:
        self.sum_iou += iou1.sum()
        self.sum_num += len(iou1)
        # time.sleep(0.01)  # 这里为了查看输出变化,实际使用不需要sleep
        print('\r', self.sum_iou / self.sum_num, end='', flush=True)

    pseudo_boxes = torch.split(pseudo_boxes, batch_gt)
    return list(pseudo_boxes), mean_ious, list(filtered_boxes), list(filtered_scores), dynamic_weight.detach()

loss_mil

请添加图片描述
这里stage0只会用到mil1,也就是binary_cross_entropy损失,其实我不是很理解为什么名字为MIL损失,实际代码是把前面生成的42个框的结果加权求和之后,根据分类偏差计算损失。这么一分析好似和mil没有啥关系。

忘掉这些🍀蛋的事情吧,最后返回:
请添加图片描述

P2B_head模块最后输出:

请添加图片描述
计算完head部分的运算,返回P2BNet.py中,记录一下pseudo_boxes_out和loss,结束了stage=0的部分,
请添加图片描述
god damn

第二部分 p2b 的stage2

回顾一下stage1到底干啥了

  • 1,根据提传入的cfg,和图片中的真实标记,对每一个gt点附近,生成42个框
  • 2,bbox_forward这一部分干了相当多的事情,最重要的就是ROIAlign提取features,之后预测分类得分,实例得分(没有框回归的部分)
  • 3,merge部分,利用上一部分得到的得分,把每个gt点的42个框保留top7,进行加权平均,保证最后只得到一个框一个点的对应。
  • 4,如何更好的merge呢,这就需要引导网络对于实例得分的准确度的训练,但是又没有box标注,于是便充分利用到一个隐藏信息,那便是框得越好,检测出是目标的概率也就越大。使用分类损失来引导bbox_forward部分的预测结果。(但是毕竟二者不等价,所以效果一定会差一点,没法得到完美的框标注)

即使没有那么完美,但是总比没有的好,所以,带着loss和前面通过融合得到的pseudo_boxes,进入了stage2
我是真的懒,然后读这个代码有时候战线拉的太长了,我常常读了一半,干了几天别的事情,忘了,然后又重新度。以我为戒吧,fuck!

stage2
请添加图片描述

fine prop from vfg和 gen negative prop

既然已经有了真实框,那么是不是就能开始像强监督一样的训练流程了呢?比如faster rcnn,但是这两个代码和我们前面的分析告诉我们,这么做可能效果不会太好。
想像一下训练初期,模型merge得到的框并不真实,用这种框,计算Assigner,效果当然不一定会好。
p2b使用了一种更为简单的办法,负例框直接生成,同时再fine之前merge得到的框。

请添加图片描述
fine prop from vfg
看一下函数的输入和输入包含的具体键值对,就知道它又要干一些事情了,比如从一个gt box得到一系列。
后面的shake ratio把图片中心点分别在上下左右移动了一点(移动的比例是一个超参数)
第一类操作:宽高比进行微调
第二类操作:中心点平移###question:为啥中心点不偏移的情况没有保留??(后来看代码,确实保留了)
第三类操作:计算和图片的iof,换句话说,把调整之后大部分区域已经不在图片内的框滤掉。
返回prop list, prop valid list(如果滤掉就是False,否则True)
把7,4 变成了 7,9,4 再变成7,9,5,4再变成315,4

那么现在就进入了下一个函数
gen negative prop
既然是强监督,而且不能通过iou方式判定一个框是负例框,那么就要手动生成,防止不准确。
这就好比是一个图片里面有一辆车,你把他标出来,作为正例——>
但是如果,车驶入了图片边缘,但是只驶入了1/3,你还会把它标注为正例吗?
如果是1/2,2/3呢?
总之,这类其实对实际应用不会有太大matter的事情,但是!我们一旦手动标注了,就不见的是一件对训练友好的事情。
我认为这就是为什么要手动生成负例的原因,我也可以大胆猜测:距离所有正例都很远的像素点是负例。当然这只是一个合理的猜测,下面看看代码:::

    num_neg_gen = proposal_cfg['gen_num_neg']
    if num_neg_gen == 0:
        return None, None
    neg_proposal_list = []
    neg_weight_list = []
    for i in range(len(gt_points)):
        pos_box = aug_generate_proposals[i]
        h, w, _ = img_meta[i]['img_shape']
        ## -0.1 w -> 1.1 w
        ## -0.1 h -> 1.1 h
        x1 = -0.2 * w + torch.rand(num_neg_gen) * (1.2 * w)
        y1 = -0.2 * h + torch.rand(num_neg_gen) * (1.2 * h)
        x2 = x1 + torch.rand(num_neg_gen) * (1.2 * w - x1)
        y2 = y1 + torch.rand(num_neg_gen) * (1.2 * h - y1)
        neg_bboxes = torch.stack([x1, y1, x2, y2], dim=1).to(gt_points[0].device)
        gt_point = gt_points[i]
        gt_min_box = torch.cat([gt_point - 10, gt_point + 10], dim=1)
        iou = bbox_overlaps(neg_bboxes, pos_box)
        neg_weight = ((iou < 0.1).sum(dim=1) == iou.shape[1])

        neg_proposal_list.append(neg_bboxes)
        neg_weight_list.append(neg_weight)

好吧我的预言是错误的,还真的用了IoU
这里的num_neg_gen=500,要生成500个负例框。
负例是从图片中随即生成的,torch.rand是0-1均匀分布。
当与positive_box的最大iou小于0.1,就是负例框
这里有句代码很有趣neg_weight=((iou<0.1).sum(dim=1)==iou.shape[1])可以自己尝试写一个看看到底作了啥

包含大量内容的bbox_forward_train(*)

请添加图片描述
请添加图片描述
box_forward
merge_box
loss_mil
他们在一阶段的时候都有,那么二阶段有什么不同呢?

bbox_forward

1,还是像之前一样,先提取出特征
请添加图片描述
2,这里bbox_head居然和stage0完全一样的操作,不过只是在两个共享全连接层和对应的cls,ins,reg三个接下来的全连接层共享权重,最后一层把cls,ins,reg分别对应到各自的维度的全连接层,不共享权重。
请添加图片描述

input
shared_fc1&2
relu&fc
relu&fc
relu&fc
relu&fc^stage
relu&fc^stage
relu&fc^stage

计算完这些之后,负例框仍然需要走一遍bbox_forward,并且保留cls_scores以便于loss

merge_box(stage=1)

stage1和0唯一不同的地方就在于,cls_socres使用了sigmoid而不是softmax进行归一化。其余全部类似。

写到这里我其实是有点疑惑的,说他是强监督,其实也好像不全是吧。

bac_assigner(torch.cat(prop_list),torch.cat(prop_list_base))

输出0,1
维度11*45
根据prop_list与上一轮得到的prop_list_base计算iou作为依据
请添加图片描述

loss_mil请添加图片描述

这里也包含了几个部分,第一个部分就是计算损失(porp_list和stage=0得到的prop_list_base 之间的损失)请添加图片描述
可以看到使用的是fcoal loss
请添加图片描述
第二部分就和第一部分产生了不同,因为stage0只有一个损失,但是这里有三个部分的损失。
请添加图片描述

第二部分的损失,咋还是????fuck loss,这不是上一部分已经用过了吗,刚刚debug恰好出现了问题这部分还跳过了。算了就这样吧。

第三部分:负例的focal loss
请添加图片描述

反向传递完loss即完成了训练部分。
下面实际测试部分,emm暂时先拉下,过几天在写。

test

P2BNet.py simple_test()

def simple_test(self, img, img_metas, gt_bboxes, gt_anns_id, gt_true_bboxes, gt_labels,
                gt_bboxes_ignore=None, proposals=None, rescale=False):
    """Test without augmentation."""
    base_proposal_cfg = self.train_cfg.get('base_proposal',
                                           self.test_cfg.rpn)
    fine_proposal_cfg = self.train_cfg.get('fine_proposal',
                                           self.test_cfg.rpn)
    assert self.with_bbox, 'Bbox head must be implemented.'
    x = self.extract_feat(img)
    for stage in range(self.num_stages):

        gt_points = [bbox_xyxy_to_cxcywh(b)[:, :2] for b in gt_bboxes]
        if stage == 0:
            generate_proposals, proposals_valid_list = gen_proposals_from_cfg(gt_points, base_proposal_cfg,
                                                                              img_meta=img_metas)
        else:
            generate_proposals, proposals_valid_list = fine_proposals_from_cfg(pseudo_boxes, fine_proposal_cfg,
                                                                               img_meta=img_metas, stage=stage)

        test_result, pseudo_boxes = self.roi_head.simple_test(stage,
                                                              x, generate_proposals, proposals_valid_list,
                                                              gt_true_bboxes, gt_labels,
                                                              gt_anns_id,
                                                              img_metas,
                                                              rescale=rescale)
    return test_result
  • lets see self.roi_head.simple_test

P2B_Head.py simple_test()

def simple_test(self,
                stage,
                x,
                proposal_list,
                proposals_valid_list,
                gt_bboxes,
                gt_labels,
                gt_anns_id,
                img_metas,
                proposals=None,
                rescale=False):
    """Test without augmentation."""
    assert self.with_bbox, 'Bbox head must be implemented.'

    det_bboxes, det_labels, pseudo_bboxes = self.simple_test_bboxes(
        x, img_metas, proposal_list, proposals_valid_list, gt_bboxes, gt_labels, gt_anns_id, stage, self.test_cfg,
        rescale=rescale)

    bbox_results = [bbox2result(det_bboxes[i], det_labels[i], self.bbox_head.num_classes)
                    for i in range(len(det_bboxes))
    ]
    # pseudo_bboxes = [i[:, :4] for i in det_bboxes]
    return bbox_results, pseudo_bboxes
  • from here we can understand that it is composed of two function (simplt_test_bboxes & bbox2result)

P2B_Head.py simple_test_bboxes()

def simple_test_bboxes(self,
                       x,
                       img_metas,
                       proposals,
                       proposals_valid_list,
                       gt_bboxes,
                       gt_labels,
                       gt_anns_id,
                       stage,
                       rcnn_test_cfg,
                       rescale=False):
    img_shapes = tuple(meta['img_shape'] for meta in img_metas)
    scale_factors = tuple(meta['scale_factor'] for meta in img_metas)
    
    rois = bbox2roi(proposals)
    bbox_results = self._bbox_forward(x, rois, gt_bboxes, stage)
    proposals_valid_list = torch.cat(proposals_valid_list).reshape(
        *bbox_results['cls_score'].shape[:2], 1)

    pseudo_boxes, mean_ious, filtered_boxes, filtered_scores, dynamic_weight = self.merge_box(bbox_results,
                                                                                              proposals,
                                                                                              proposals_valid_list,
                                                                                              torch.cat(gt_labels),
                                                                                              gt_bboxes,
                                                                                              img_metas, stage)
    pseudo_boxes_out = copy.deepcopy(pseudo_boxes)

    det_bboxes, det_labels = self.pseudobox_to_result(pseudo_boxes, gt_labels, dynamic_weight, gt_anns_id,
                                                      scale_factors, rescale)

    return det_bboxes, det_labels, pseudo_boxes_out
  • from this function, everything before pseudobbox_to_result has already been explained. so we gonna explain pseudobbox_to_result

P2B_Head.py pseudobbox_to_result()

def pseudobox_to_result(self, pseudo_boxes, gt_labels, dynamic_weight, gt_anns_id, scale_factors, rescale):
    det_bboxes = []
    det_labels = []
    batch_gt = [len(b) for b in gt_labels]
    dynamic_weight = torch.split(dynamic_weight, batch_gt)
    for i in range(len(pseudo_boxes)):
        boxes = pseudo_boxes[i]
        labels = gt_labels[i]

        if rescale and boxes.shape[0] > 0:
            scale_factor = boxes.new_tensor(scale_factors[i]).unsqueeze(0).repeat(
                1,
                boxes.size(-1) // 4)
            boxes /= scale_factor

        boxes = torch.cat([boxes, dynamic_weight[i].sum(dim=1, keepdim=True)], dim=1)
        gt_anns_id_single = gt_anns_id[i]
        boxes = torch.cat([boxes, gt_anns_id_single.unsqueeze(1)], dim=1)
        det_bboxes.append(boxes)
        det_labels.append(labels)
    return det_bboxes, det_labels
  • it shows that pseudobox_to_result do nothing but rescale it back, and add two dimension represents dynamic_weight and gt_anns_id. to make its dimentsion like (n,6) instead of (n,4).
  • lets back to P2B_Head.py simple_test() and see another function bbox2result and then we can move on to stage 2
  • bbox_results = [bbox2result(det_bboxes[i], det_labels[i], self.bbox_head.num_classes) for i in range(len(det_bboxes))

core.bbox.transforms.py bbox2result(bboxes,labels,num_classes):

def bbox2result(bboxes, labels, num_classes):
    """Convert detection results to a list of numpy arrays.

    Args:
        bboxes (torch.Tensor | np.ndarray): shape (n, 5)
        labels (torch.Tensor | np.ndarray): shape (n, )
        num_classes (int): class number, including background class

    Returns:
        list(ndarray): bbox results of each class
    """
    if bboxes.shape[0] == 0:
        return [np.zeros((0, 5), dtype=np.float32) for i in range(num_classes)]
    else:
        if isinstance(bboxes, torch.Tensor):
            bboxes = bboxes.detach().cpu().numpy()
            labels = labels.detach().cpu().numpy()
        return [bboxes[labels == i, :] for i in range(num_classes)]
  • this bbox2result use classes to give these bboxes set a classification

  • as shown in P2BNet.py simple_test(), pseudo_boxes is the only parameter that used in stage2(which means test_result generated by stage1 is not used in stage2)
  • just as training , pseudo_boxes were used to generated fine_proposals and as the code shows ,generate_proposals were send into simple_test again

  • here i am not gonna show stage 2 again as these functions has already been explained in stage1
  • finally it returns test_results generated by bbox2result
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值