mmdetection 中 Mask Rcnn检测结果可视化(DICE计算、PR曲线绘制等)

这篇具有很好参考价值的文章主要介绍了mmdetection 中 Mask Rcnn检测结果可视化(DICE计算、PR曲线绘制等)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

mmdetection中的Mask Rcnn是一个很不错的检测网络,既可以实现目标检测,也可以实现语义分割。官方也有很详细的doc指导,但是对新手来说并不友好,刚好之前笔者写的mmlab系列里面关于可视化都还没有一个详细的文档,也在此一并介绍。

具体怎么制作自己的数据集和训练自己的模型教程如下:
mmdetect2d训练自己的数据集(一)—— labelme数据处理
mmdetect2d训练自己的数据集(二)—— 模型训练

通过上述两个教程,可以训练得到自己的config文件和checkpoints文件以及训练日志 xxx.log.json文件。接下来的可视化就是要用到这三个东西。

注:运行mmdetection的时候,我是用pycharm,这里有一个比较奇葩的bug,就是有时候我直接在terminal里面运行程序,会出现bug比如mmcv版本不对等,但是在RUN里面跑又可以了(别问怎么知道的,问就是卡了一周…)。所以出现bug的时候不要急着否定自己,多研究一下。

模型检测部署(检测结果可视化)

模型检测结果可视化有多种方式,这里介绍两种方法:1.调用api直接检测;2.用test.py进行检测。

1.使用test.py

运行test.py 必须要有config文件和checkpoint文件,其他为可选参数,具体如下:

python tools/test.py configs/myconfig.py checkpoints/last.pth \
--show --out result_file.pkl --show-dir result/test_result --eval segm

# --show 决定是否现实图片
# --out 将结果输出为pkl格式的文件
# --show-dir 将测试得到的文件存到目标文件夹下
# --eval 选择需要评估的指标,比如segm是分割的情况,这是mask rcnn网络会有这个结果,还有bbox等

2.调用API

在mmdet里面集成了很多现成的api也可以直接用来查看检测结果,这里写一个简单的调用方法。

from mmdet.apis import init_detector, inference_detector, show_result_pyplot
import cv2
import numpy as np

# config文件地址
config_file = 'others_ct/ct_head_mask_rcnn_r50_rpn_100_coco/mask_rcnn_r50_fpn_1x_coco.py'

# checkpoint文件地址
checkpoint_file = 'others_ct/ct_head_mask_rcnn_r50_rpn_100_coco/latest.pth'

# 选择使用的显卡
device = 'cuda:0'

# 模型载入
model = init_detector(config_file, checkpoint_file, device=device)

# 待检测的图片地址
img = 'data/coco_ct/val2017/Snipaste_2022-07-28_15-48-49.jpg'

# 检测结果输出
result = inference_detector(model, img)
#bbox_result, mask_result = result

# 现实检测结果
show_result_pyplot(model, img, result, score_thr=0.3)

3.对比自己的验证集标注

我之前都是用labelme直接看的,当然就显得b格不太够,mmdetection里面也可以直接看,用的browse_dataset.py,这个可以看训练集的,当然如果要看测试集的话,可以将config文件里面的data这个dict里面的train的dict中的ann_file和img_profix路径改成和val那个dict中的一样就好了mmdetection 可视化结果,mmlab学习系列,人工智能,深度学习,计算机视觉
运行的时候,需要输入参数有config文件,以及--show # 如果你要现实每一张的话 --output-dir ../mydir # 浏览的图片可以保存到该地址下
这种现实办法有一个好处就是可以得到比较干净的对比图片,就是test.py得到预测的图片,这个得到标注的图片。但是如果想要将结果放在同一张图上进行展示,及标注和预测放在同一张图上的话,mmdetection也提供了对应的代码。使用tools/analyze_results.py即可。使用方法如下:

python tools/analysis_tools/analyze_results.py \
      ${CONFIG} \  
      ${PREDICTION_PATH} \
      ${SHOW_DIR} \
      [--show] \
      [--wait-time ${WAIT_TIME}] \
      [--topk ${TOPK}] \
      [--show-score-thr ${SHOW_SCORE_THR}] \
      [--cfg-options ${CFG_OPTIONS}]

{CONFIG}: 是config文件路径
{PREDICTION_PATH}: 是test.py得到的pkl文件路径
{SHOW_DIR}: 是绘制的到的图片存放的目录
--show: 决定是否显示,不指定的话,默认为不显示
--wait-time时间的间隔,若为 0 表示持续显示
--topk: 根据最高或最低 topk 概率排序保存的图片数量,若不指定,默认设置为 20
--show-score-thr: 能够展示的概率阈值,默认为 0
--cfg-options: 如果指定,可根据指定键值对覆盖更新配置文件的对应选项

DICE(DSC)计算

我使用mask-rcnn这个网络的,想要计算语义分割结果理论上是可以直接用mmdetection里面的dice loss计算,但是我找了好几天没找到,主要是不知道怎么输出,所以就手动写了一个。主要原理是挨个计算标注的mask的面积、预测的mask的面积,以及二者的iou。
语义分割里 Dice = 2 * (A∩B) /(|A| + |B|)
但是这里有一点需要注意,我做的是单类别检测,且每张图最多会存在一个检测结果,如果有多类别的话,可能就需要对代码进行修改了,后续如果我用到的话也会更新的。
所以我是将mmdetection里面mask部分的numpy数组输出保存了。具体在image.py里面找到def imshow_det_bboxes函数,在判定segms的部分加上输出的指令:


if segms is not None:
	##############在原代码中的if语句里的开头部分添加该部分代码#################
	# 获取输出的的图片名称,这里一定要在test.py 和 browse_dataset.py的时候,选择保存文件,即必须有--show-dir和路径
	file_tmp = str(out_file)
	# 设置npy文件的名称
	file_name = file_tmp[:-3] + 'npy'
	# 将segms的numpy数组保存为npy文件
	np.save(file_name, segms)
	####################################################################

然后在运行上述的test.py文件和browse_dataset.py时,会在–show-dir下面,生成与图片同名的npy文件。计算dice就是要用到这部分文件。

因此具体代码如下:

import numpy as np
import os
# 存放预测结果的路径(前面test.py结果的--show-dir)
pred_root_path = '/home/kevin/mmlab/mmdetection/tools/result004'
# 存放标签的路径(前面dataset.py结果的--show-dir)
label_root_path = '/home/kevin/mmlab/mmdetection/tools/misc/ct_results'

# 读取预测结果路径下的文件
file_list_tmp = os.listdir(pred_root_path)
file_list = []
dice_list = []

for i in file_list_tmp:
	# 判定是否为npy文件
    if i[-3:] == 'npy':  
    	# 获取预测的npy文件路径
        pred_path = pred_root_path + '/' + i
        # 获取同名的标签文件路径
        label_path = label_root_path + '/' + i
        print(pred_path)
        print(label_path)

        # 导入ndarray文件
        pred = np.load(pred_path)
        label = np.load(label_path)

        # 标签文件中的数组是Ture和False组成,需要变成1、0
        pred_1 = pred + 0
        # 因为browse得到的标签文件会有0.5的比例Flip来达到数据增强的目的,即图像会水平翻转
        # 所以产生的mask文件也有一半的概率是水平翻转过的,需要将其翻转回来
        pred_2 = pred_1[:,:,::-1]
        # 有时候检测为空但还是会有pred的npy文件,原因我也不清楚,所以这里是加了个判定,如果输出为空则直接跳过。
        # 预测的mask文件形状都是(n, 512, 512)形状的,n取决于预测的个数,我是单目标,所以最多只有一个。
        if pred_1.shape == (0, 512, 512):
            continue
        # 同上
        label = label + 0


        # 将 (1, 512, 512) 展平为 (262144,),目的是方便计算
        b1_1 = pred_1.flatten()
        b1_2 = pred_2.flatten()
        b2 = label.flatten()

        # 分别计算预测的和标签的两个mask部分的面积,因为翻转对面积不影响,这里只计算一次
        pred_area = np.sum(b1_1)
        label_area = np.sum(b2)

        # 计算IoU
        iou_pred_label_1 = b1_1 * b2
        iou_pred_label_2 = b1_2 * b2

        iou_area_1 = np.sum(iou_pred_label_1)
        iou_area_2 = np.sum(iou_pred_label_2)
        # print(iou_area_1, iou_area_2)
		
		# IoU小说明该图被翻转过了,因此选大的那个
        iou_area = max(iou_area_1, iou_area_2)
        print(iou_area)

		# 防止为空文件
        if (pred_area + label_area) > 0:
            dice = (2 * iou_area) / (pred_area + label_area)
        else:
            dice = 0
		# 将得到的dice加入list
        dice_list.append(dice)

# print("dice_list is:", dice_list)

# 求和前将list转为numpy数组
dice_np = np.array(dice_list)
dice_sum = np.sum(dice_np)

# 计算总共有多少张图片,因为一些检测为空,所以用的是browse的结果,由于结果包含图片和npy,所以除以2
pic_num = len(os.listdir(label_root_path)) / 2
print('num is:', pic_num)
# 计算平均Dice
Dice = dice_sum / pic_num
print('Dice is:', Dice)

训练日志可视化

这一部分主要是将mmdetection训练得到的json文件可视化,代码主要源于github,具体哪一个忘记了(readme里面没有原址…)是专门做的mmdetection 结果可视化的,非常强!!。使用时如果出现keyerror的话,将json文件中第一行的env_info删掉就可以了。

import json
import matplotlib.pyplot as plt
import sys
import os
from collections import OrderedDict

class visualize_mmdetection():
    def __init__(self, path):
        self.log = open(path)
        self.dict_list = list()
        self.loss_rpn_bbox = list()
        self.loss_rpn_cls = list()
        self.loss_bbox = list()
        self.loss_cls = list()
        self.loss = list()
        self.acc = list()

    def load_data(self):
        for line in self.log:
            info = json.loads(line)
            # print('info:', info)
            if info['mode'] == 'train':
                self.dict_list.append(info)

        for i in range(1, len(self.dict_list)):
            for value, key in dict(self.dict_list[i]).items():
                # 读取每一行的信息
                loss_rpn_cls_value = dict(self.dict_list[i])['loss_rpn_cls']
                loss_rpn_bbox_value = dict(self.dict_list[i])['loss_rpn_bbox']
                loss_bbox_value = dict(self.dict_list[i])['loss_bbox']
                loss_cls_value = dict(self.dict_list[i])['loss_cls']
                loss_value = dict(self.dict_list[i])['loss']
                acc_value = dict(self.dict_list[i])['acc']
                # 将其保存至对应列表中
                self.loss_rpn_cls.append(loss_rpn_cls_value)
                self.loss_rpn_bbox.append(loss_rpn_bbox_value)
                self.loss_bbox.append(loss_bbox_value)
                self.loss_cls.append(loss_cls_value)
                self.loss.append(loss_value)
                self.acc.append(acc_value)
        # 清除list中的重复项 
        self.loss_rpn_cls = list(OrderedDict.fromkeys(self.loss_rpn_cls))
        self.loss_rpn_bbox = list(OrderedDict.fromkeys(self.loss_rpn_bbox))
        self.loss_bbox = list(OrderedDict.fromkeys(self.loss_bbox))
        self.loss_cls = list(OrderedDict.fromkeys(self.loss_cls))
        self.loss = list(OrderedDict.fromkeys(self.loss))
        self.acc = list(OrderedDict.fromkeys(self.acc))

    def show_chart(self):
        plt.rcParams.update({'font.size': 15})

        plt.figure(figsize=(20, 20))

        plt.subplot(321, title='loss_rpn_cls', ylabel='loss')
        plt.plot(self.loss_rpn_cls)
        plt.subplot(322, title='loss_rpn_bbox', ylabel='loss')
        plt.plot(self.loss_rpn_bbox)

        plt.subplot(323, title='loss_cls', ylabel='loss')
        plt.plot(self.loss_cls)
        plt.subplot(324, title='loss_bbox', ylabel='loss')
        plt.plot(self.loss_bbox)
        plt.subplot(325, title='total loss', ylabel='loss')
        plt.plot(self.loss)
        plt.subplot(326, title='accuracy', ylabel='accuracy')
        plt.plot(self.acc)
        plt.suptitle((sys.argv[1][5:] + "\n training result"), fontsize=30)
        plt.savefig(('output/' + sys.argv[1][5:] + '_result.png'))


if __name__ == '__main__':
    x = visualize_mmdetection(sys.argv[1])
    x.load_data()
    x.show_chart()

使用时直接是:

python visualize.py xxxx.json

xxxx.json是生成的json文件,结果如下:

mmdetection 可视化结果,mmlab学习系列,人工智能,深度学习,计算机视觉

PR曲线绘制

不太确定mmdetection里面有没有内置的绘制PR曲线的代码,这是参考其他一些博主写的代码

import os
import mmcv
import numpy as np
import matplotlib.pyplot as plt

from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval

from mmcv import Config
from mmdet.datasets import build_dataset

# config文件路径
CONFIG_FILE = '/home/kevin/mmlab/mmdetection/tools/others_ct/r18/mask_rcnn_r18_fpn_1x_coco.py'
# test.py得到的pkl文件路径
RESULT_FILE = '/home/kevin/mmlab/mmdetection/tools/r18_result.pkl'


## 对比不同网络之间的结果
#CONFIG_FILE_01 = '/home/kevin/mmlab/mmdetection/tools/others_ct/r18/mask_rcnn_r18_fpn_1x_coco.py'
#RESULT_FILE_01 = '/home/kevin/mmlab/mmdetection/tools/r18_result.pkl'

#CONFIG_FILE_02 = '/home/kevin/mmlab/mmdetection/others_ct/ct_head_mask_rcnn_r50_rpn_100_coco/mask_rcnn_r50_fpn_1x_coco.py'
#RESULT_FILE_02 = '/home/kevin/mmlab/mmdetection/tools/r50_result.pkl'

#CONFIG_FILE_03 = '/home/kevin/mmlab/mmdetection/tools/others_ct/ct_head_mask_rcnn_r101_rpn_100_coco/mask_rcnn_r101_fpn_1x_coco.py'
#RESULT_FILE_03 = '/home/kevin/mmlab/mmdetection/tools/r101_result.pkl'

# 绘制曲线
def plot_pr_curve(config_file, result_file, metric="bbox"):
    """plot precison-recall curve based on testing results of pkl file.

        Args:
            config_file (list[list | tuple]): config file path.
            result_file (str): pkl file of testing results path.
            metric (str): Metrics to be evaluated. Options are
                'bbox', 'segm'.
    """

    cfg = Config.fromfile(config_file)
    # turn on test mode of dataset
    if isinstance(cfg.data.test, dict):
        cfg.data.test.test_mode = True
    elif isinstance(cfg.data.test, list):
        for ds_cfg in cfg.data.test:
            ds_cfg.test_mode = True

    # build dataset
    dataset = build_dataset(cfg.data.test)
    # load result file in pkl format
    pkl_results = mmcv.load(result_file)
    # convert pkl file (list[list | tuple | ndarray]) to json
    json_results, _ = dataset.format_results(pkl_results)
    # initialize COCO instance
    coco = COCO(annotation_file=cfg.data.test.ann_file)
    coco_gt = coco
    coco_dt = coco_gt.loadRes(json_results[metric])
    # initialize COCOeval instance
    coco_eval = COCOeval(coco_gt, coco_dt, metric)
    coco_eval.evaluate()
    coco_eval.accumulate()
    coco_eval.summarize()
    # extract eval data
    precisions = coco_eval.eval["precision"]
    '''
    precisions[T, R, K, A, M]
    T: 是IOU的阈值,值为0-9,依次对应,[0.5 : 0.05 : 0.95],
    R: 召回率的阈值,[0 : 0.01 : 1], idx from 0 to 100
    K: 检测类别的索引,我只有1类,所以为0
    A: 检测的大小,对应 (all, small, medium, large), 索引值0-3 
    M: 最大检测数量, 对应(1, 10, 100), 索引值0-2
    '''
    pr_array1 = precisions[0, :, 0, 0, 2]
    pr_array2 = precisions[1, :, 0, 0, 2]
    pr_array3 = precisions[2, :, 0, 0, 2]
    pr_array4 = precisions[3, :, 0, 0, 2]
    pr_array5 = precisions[4, :, 0, 0, 2]
    pr_array6 = precisions[5, :, 0, 0, 2]
    pr_array7 = precisions[6, :, 0, 0, 2]
    pr_array8 = precisions[7, :, 0, 0, 2]
    pr_array9 = precisions[8, :, 0, 0, 2]
    pr_array10 = precisions[9, :, 0, 0, 2]

	# 计算平均值 iou@0.5:0.95
    pr_array = pr_array1 + pr_array2 +pr_array3 +pr_array4 + pr_array5 + \
        pr_array6 + pr_array7 + pr_array8 + pr_array9 +pr_array10
    print(pr_array/10)

    x = np.arange(0.0, 1.01, 0.01)
    # 绘制PR曲线
    plt.plot(x, pr_array1, label="iou=0.5")
    plt.plot(x, pr_array2, label="iou=0.55")
    plt.plot(x, pr_array3, label="iou=0.6")
    plt.plot(x, pr_array4, label="iou=0.65")
    plt.plot(x, pr_array5, label="iou=0.7")
    plt.plot(x, pr_array6, label="iou=0.75")
    plt.plot(x, pr_array7, label="iou=0.8")
    plt.plot(x, pr_array8, label="iou=0.85")
    plt.plot(x, pr_array9, label="iou=0.9")
    plt.plot(x, pr_array10, label="iou=0.95")

    plt.xlabel("recall")
    plt.ylabel("precison")
    plt.xlim(0, 1.0)
    plt.ylim(0, 1.01)
    plt.grid(True)
    plt.legend(loc="lower left")
    # 保存图像
    plt.savefig('PR_r18.png')
    plt.show()
	
	# 只绘制ap @ 0.5:0.95
    # return pr_array/10



if __name__ == "__main__":
    plot_pr_curve(config_file=CONFIG_FILE, result_file=RESULT_FILE, metric="bbox")
    
    # pr_array_1 = plot_pr_curve(config_file=CONFIG_FILE_01, result_file=RESULT_FILE_01, metric="bbox")
    # pr_array_2 = plot_pr_curve(config_file=CONFIG_FILE_02, result_file=RESULT_FILE_02, metric="bbox")
    # pr_array_3 = plot_pr_curve(config_file=CONFIG_FILE_03, result_file=RESULT_FILE_03, metric="bbox")

    # x = np.arange(0.0, 1.01, 0.01)
    # plot PR curve
    # plt.plot(x, pr_array_1, label="r18")
    # plt.plot(x, pr_array_2, label="r50")
    # plt.plot(x, pr_array_3, label="r100")

    # plt.xlabel("recall")
    # plt.ylabel("precison")
    # plt.xlim(0, 1.0)
    # plt.ylim(0, 1.01)
    # plt.grid(True)
    # plt.legend(loc="lower left")
    # plt.savefig('PR_r18_r50_r101.png')
    # plt.show()

这边遇到了一个问题,就是使用plt保存图片时,必须要在ply.show前面,不然会保存为空白图像,因为它默认show完图像就被输出了,即没有可以保存的图像了。
得到结果图如下:
mmdetection 可视化结果,mmlab学习系列,人工智能,深度学习,计算机视觉

模型复杂度计算

这个官方文档就有python tools/analysis_tools/get_flops.py ${CONFIG_FILE} [--shape ${INPUT_SHAPE}]
得到结果如下:

==============================
Input shape: (3, 1280, 800)
Flops: 239.32 GFLOPs
Params: 37.74 M
==============================

对输出图像的处理

mmdetection检测或者预览的图像都是在image.py里面实现的,比如如果只是用mask rcnn来达到语义分割的效果,那么其实bbox和label输出就有点多余。同样是在def imshow_det_bboxes里面,把draw_bboxes和draw_labels注释掉就可以了文章来源地址https://www.toymoban.com/news/detail-523865.html

到了这里,关于mmdetection 中 Mask Rcnn检测结果可视化(DICE计算、PR曲线绘制等)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • PCL Visualizer可视化结果保存成图片、关闭可视化窗口

    内容: C++ PCLVisualizer可视化的结果保存成图片,并自动关闭可视化窗口。

    2024年02月13日
    浏览(26)
  • 【MMDetection3D】环境搭建,使用PointPillers训练&测试&可视化KITTI数据集

    2D卷不动了,来卷3D,之后更多地工作会放到3D检测上 本文将简单介绍什么是3D目标检测、KITTI数据集以及MMDetection3D算法库,重点介绍如何在MMDetection3D中,使用PointPillars算法训练KITTI数据集,并对结果进行测试和可视化。   对于一张输入图像,2D目标检测旨在给出物体类别并标

    2024年02月03日
    浏览(40)
  • 如何利用open3d对点云进行可视化并保存可视化结果

    来自open3d在GitHub上的这个issue 其中要用到的pcd的产生可以参考这篇帖子中用到的方式

    2024年02月10日
    浏览(39)
  • YOLOv5 使用tensorboard查看可视化训练结果

    1.1.找的models/yolo.py文件中,将最下面有关 Tensorboard 的注释打开 2.进入项目根目录 比如你训练的是第20个版本,那么 tensorboard --logdir=./runs/train/exp20 就可以查看当前训练的可视化结果了 3.通过浏览器查看可视化训练结果

    2024年02月16日
    浏览(42)
  • 使用Python和OpenCV批量可视化labelme分割标注结果

    【原创声明】 本文为博主原创文章,未经博主允许不得转载。 更多算法总结请关注我的博客:https://blog.csdn.net/suiyingy。         在计算机视觉领域中,图像分割是一项重要的任务,它可以将图像中的不同物体或区域进行像素级别的分割。而在图像分割任务中,人工标注数

    2024年04月16日
    浏览(23)
  • 使用 Scrapy 和 Selenium 爬取 Boss 直聘职位信息(可视化结果)

    在本博客中,我们将介绍如何使用 Scrapy 和 Selenium 来爬取 Boss 直聘 网站上的职位信息。Boss 直聘是一个广受欢迎的招聘平台,提供了大量的职位信息,以及公司和 HR 的联系信息。通过本文的指南,你将学会如何创建一个爬虫来抓取特定城市的 Python 职位信息。 在这个示例中,

    2024年02月08日
    浏览(30)
  • 【R语言】——基因GO/KEGG功能富集结果可视化(保姆级教程)

    上期“原来基因功能富集分析这么简单”介绍如何使用DAVID在线分析工具对基因进行GO/KEGG功能富集分析。本期则介绍使用R语言ggplot包对DAVID在线分析工具所获得的基因GO/KEGG功能富集结果进行可视化。 1 数据准备 数据输入格式(xlsx格式): 注:DAVID导出来的“%”这列为“Ge

    2024年02月13日
    浏览(26)
  • 【踩坑记录】colmap中的相机位姿的坐标系定义及其可视化结果的隐含转换

      这个问题来自于我想要使用colmap的稀疏重建结果,然后发现由于相机坐标系的定义没弄清楚,导致我获取的结果存在问题。    1 问题引出   下面先从我们还不知道坐标系定义的视角开始理解,引出问题所在。使用的是一份无人机影像数据,共有59张影像:   下图

    2024年02月06日
    浏览(122)
  • 三维目标检测之ROS可视化

    实验室有一个镭神C16的激光雷达,最近在我这,想拿来玩一玩。本意是做一个实时的检测,通过ROS获取激光雷达的激光点云,用pointpillars模型来进行实时的三维目标检测任务。但是镭神c16这一个激光雷达,不太好处理,目前只能用自带的驱动,进行一个实时的显示。所以下边

    2024年01月19日
    浏览(34)
  • 【Python小技巧】使用Gradio轻松部署AI算法结果可视化Web 应用(含图片转换、验证码识别完整源码)

    随着人工智能的不断发展,各种智能算法越来越普遍,但是这些算法结果通常显示在cmd命令窗口里。有没有一种方法可以动态展示,更具需要计算后动态展现? 答案是有! 下面让我了解一下Gradio库,只需寥寥几行代码就可以展现出chatGPT的对话窗口,是不是很nice! Gradio是一

    2024年02月15日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包