Faster_RCNN 血液细胞检测项目实战(一)

1.数据集介绍

本项目所使用的数据集是BCCD数据集。

数据集包含三类血细胞照片:WBC(白细胞)、RBC(红细胞)、Platelets(血小板)

可以在这里下载:BCCD Object Detection Dataset

本文所使用的是2021.2.25版本的。包含847张图片,每张图像大小为416x416,图像格式为.jpg

该版本以源图像进行了图像增强,包括:

  • 50% 的概率进行水平翻转
  • 50% 的概率进行垂直翻转
  • 以等概率选择以下其中一种 90 度的旋转方式:不旋转、顺时针旋转、逆时针旋转、上下翻转
  • 随机裁剪图像,裁剪的部分占图像的 0% 到 15%
  • 随机亮度调整,亮度调整范围为 -15% 到 +15%
  • 随机曝光调整,曝光调整范围为 -20% 到 +20%

 所以在后面阶段不需要对图像进行增强操作。

当然新版本或者其他版本不同也不影响使用,一些版本图像大小不同,一些对图像并未做预处理,后期可能需要。

如果下载的数据集内容不一致,感兴趣的可以在这里下载,https://download.csdn.net/download/qq_43616651/88496033我已经上传。

该数据集已经分好test,train,vaild文件。

train 包含801张图片与801个包含图像标签与boxes的xml文件。

test  包含36张图片与36个包含图像标签与boxes的xml文件。

valid 包含73张图片与73个包含图像标签与boxes的xml文件。

其中每张图片的标签信息存放在xml文件中,可以用浏览器打开查看。

其中xml书写是对称的写法,比如size类是size开头,size结尾,中间包含width,height,depth三个参数。

对我们最有用的就是object类,其中对于神经网络使用的则是name与bndbox,表示图像中某个位置处细胞的名称,及其标注框bndbox。

其中bndbox中包含xmin,xmax,ymin,ymax元素。这里(xmin,ymin)组成标注框的左上角点位,(xmax,ymax)组成标注框右下角的点位。

对于检测框位置的表示有两种方式,一种是上述表述,给出标注框的左上角,与右下角。一种是给出标注框的中心位置与标注框的大小(即长宽),二者可以转换,这里就不细述。

以上就是数据集的介绍。

2.xml文件的读取

为了对xml的内容读取,这里使用的库是glob和xml.etree.ElemenTree

glob 是一个用于文件匹配的 Python 标准库模块,它可以帮助你在指定目录中查找文件和文件夹,以及根据文件名的模式匹配进行筛选。

glob的用法:

#导入glob库
import glob

#1.查找特定目录下的所有文件
file_list = glob.glob('/path/to/directory/*')
#这将返回 /path/to/directory 目录下的所有文件的列表。

#2.使用通配符来筛选文件
txt_files = glob.glob('/path/to/directory/*.txt')
#这将返回 /path/to/directory 目录下所有扩展名为 .txt 的文件。

#3.查找所有子目录中的文件
all_files = glob.glob('/path/to/directory/**/*.txt', recursive=True)
#这将返回 /path/to/directory 目录及其子目录中的所有 .txt 文件。

#4.使用多个通配符:
files = glob.glob('/path/to/directory/*.[jpg,png]')
这将返回 /path/to/directory 目录下所有扩展名为 .jpg 或 .png 的文件。

"""
glob 支持的通配符包括 *(匹配任何字符或字符序列)、
?(匹配任何单个字符)、[...](匹配字符集中的任何字符)、
[!...](不匹配字符集中的任何字符)等。

"""

glob可以快速实现文件,图像的批处理操作,数据集加载,图像文件批量处理。

xml.etree.ElementTree 是 Python 标准库中用于解析和处理 XML 数据的模块。

xml.etree.ElementTree的用法:

#导入模块
import xml.etree.ElementTree as ET

#解析parse XML文档
tree = ET.parse('example.xml')  # 从文件中解析XML文档
root = tree.getroot()  # 获取根元素

#或者,你也可以直接从XML字符串解析:
xml_data = '<root><element>data</element></root>'
root = ET.fromstring(xml_data)

#遍历XML树,通过遍历树来访问XML文档中的元素:
for child in root:
    print(child.tag, child.text)

#查找元素
#使用 find 和 findall 方法来查找元素
element = root.find('element')  # 查找第一个匹配的元素
elements = root.findall('element')  # 查找所有匹配的元素

#访问元素属性:
element = root.find('element')
attribute_value = element.get('attribute_name')

#创建新元素:
new_element = ET.Element('new_element')
new_element.text = 'data'
root.append(new_element)

#保存XML文档:
tree.write('new_example.xml')

#它还提供了许多其他功能,例如修改XML文档、删除元素、处理命名空间等。

基于以上库,实现读取train/test/valid数据集下的图像与xml文件,并将图像中的target信息以及bndbox从xml文件中解析出来。

以下是代码:

path = './data/train/'  #设置需要爬取的文件路径,当前文件下data下的train文件
xml_file = glob.glob(path+'*.xml') #通过glob来爬取文件中.xml结尾的文件,文件名
img_file = glob.glob(path+'*.jpg') #通过glob来爬取文件中.jpg结尾的图片,文件名

#乱序文件调整顺序
xml_list = []  #列表申请
img_list = []

for i in xml_file:
    img = i[:-3]+'jpg'    #将xml文件名后三位改成jpg,就是图像的文件名
    if img in img_file:   #判断该文件名是不是在img_file中,如果在,那就加入img_list
        img_list.append(img)
        xml_list.append(i)

#以上只是读取图像文件名,xml文件名

an_file =open(xml_file[0],encoding='utf-8') #用utf-8的格式打开该文件
tree = ET.parse(an_file) #用ET来解析an_file,得到文件内容树格式
root = tree.getroot()  #获取树的根目录,抓取xml中的数据,与html的爬取是一样的
#root findall 可以找目标标签,比如xml里的size,object等元素的内容

bndbox = [] #列表
for object in root.findall('object'):
    cell = object.find('name').text #同理拿到object的name属性,可以得到细胞的类名
    xmin = object.find('bndbox').find('xmin').text  #拿到候选框的位置
    ymin = object.find('bndbox').find('ymin').text
    xmax = object.find('bndbox').find('xmax').text
    ymax = object.find('bndbox').find('ymax').text
    bndbox.append([cell,xmin,ymin,xmax,ymax])

#以上是打开xml文件,并解析文件内容,我们想要的只有object下的name,与bndbox的
# xmin,ymin,xmax,ymax,最后将这些元素通过append加入到bndbox列表中。

最后输出bndbox,可以得到:

即细胞类名和标注框的位置。

3.xml信息解析

在2中,我们将xml中的所需信息读取出来并存放在bndbox中,但是这并不能在网络使用。首先我们的任务是血细胞目标检测任务,这个任务可以主要分为细胞种类的分类或者识别,以及对于某种细胞的检测框预测。

对于分类任务,不能直接向网络直接输入类别,需要对类别编码。

这里将三类细胞,WBC,RBC,Platelets,利用字典将类别名转化为对应编码(0,1,2)。

对于检测框的真实数据,由于在标注时有些检测框位置位于边界,或者某个方向的候选框重叠等问题,需要将这些有问题的标注框进行筛选。

下面就基于2中的代码进行改写,将文件信息分类label与bbox,其中label存放细胞种类标签,而bbox存放标注框的位置。

import glob
import xml.etree.ElementTree as ET

#定义类别字典
#分为3种血细胞,标号为0,1,2
class_idx = {'WBC':0,'RBC':1,'Platelets':2}

def get_LabelFromXml(xml_file):
    an_file = open(xml_file, encoding='utf-8')  # 用utf-8的格式打开该文件
    tree = ET.parse(an_file)  # 用ET来解析an_file,得到文件内容树格式
    root = tree.getroot()  # 获取树的根目录,抓取xml中的数据,与html的爬取是一样的
    label = []
    bbox_list = []
    for object in root.findall('object'):
        cell = object.find('name').text  # 同理拿到object的name属性,可以得到细胞的类名
        cell_id = class_idx[cell]  #根据字典将类名变成类序号
        xmin = object.find('bndbox').find('xmin').text  # 拿到候选框的位置
        ymin = object.find('bndbox').find('ymin').text
        xmax = object.find('bndbox').find('xmax').text
        ymax = object.find('bndbox').find('ymax').text
        #1 位于边界的框筛选不要
        #2 边界框无大小的筛选不邀
        if int(xmin)== 0 or int(xmax)== 0 or(ymin)== 0 or(ymax)== 0:
            pass #或者continue
        elif int(xmin) ==int(xmax)== 0 or(ymin) == (ymax)== 0:
            pass
        else:
            label.append(cell_id)   #保存标签名
            bbox_list.append([int(xmin),int(ymin),int(xmax),int(ymax)])  #保存候选框位置 左上角(x1,y1)和右下角(x2,y2)

    return  label,bbox_list


#检验函数
path = './data/train/'  #设置需要爬取的文件路径,当前文件下data下的train文件
xml_file = glob.glob(path+'*.xml') #通过glob来爬取文件中.xml结尾的文件,文件名
img_file = glob.glob(path+'*.jpg') #通过glob来爬取文件中.jpg结尾的图片,文件名

#乱序文件调整顺序
xml_list = []
img_list = []

for i in xml_file:
    img = i[:-3]+'jpg'    #将xml文件名后三位改成jpg,就是图像的文件名
    if img in img_file:   #判断该文件名是不是在img_file中,如果在,那就加入img_list
        img_list.append(img)
        xml_list.append(i)


#批量处理
for i in xml_list:
    label1,bbox1 = get_LabelFromXml(i)
    print(label1)
    print(bbox1)
#所有文件的数据爬取出来

#由于每张图片的类别数量不同,后续还需特殊处理

以上就将数据集train文件的信息全部处理好了。

但是由于上述代码对检测框进行了筛选,所以不同图像中的label,bbox的大小是不同的,即每幅图中所包含的细胞数量、候选框数量时不同的,这点在进行神经网络批处理的时候存在问题。后面处理。下面是label与bbox的数据显示

4.train_dataset的制作

有人可能想问为什么我这不是已经将数据准保好了,为什么还要将数据封装成data_set。

这里给出回答,封装数据集为自定义类,主要目的是为了更好的组织和管理数据,并于深度学习框架的数据加载器(‘DataLoader’)兼容。方便对图像的批处理,比如图像增强,将数据传入cuda等。方便数据加载,预处理。

这里使用torch.utils.data中的Dataset类。我们封装数据集类基础Dataset类的属性。

例如以下代码示例:

import torch
from torch.utils.data import Dataset
from PIL import Image

class CustomDataset(Dataset):
    def __init__(self, data_dir, transform=None):
        self.data_dir = data_dir
        self.transform = transform
        self.data = []  # 数据集的样本列表,包括图像和标签
        
        # 在构造函数中加载数据并填充到self.data中

    def __len__(self):
        return len(self.data)

    def __getitem__(self, index):
        image_path, label = self.data[index]
        image = Image.open(image_path)
        
        if self.transform:
            image = self.transform(image)
        
        return image, label

torch.utils.data.Dataset 类的常见方法和用法:

  1. __init__(self, ...): 构造函数,用于初始化数据集的属性或参数。你可以在这里传递数据集的文件路径、转换等信息。

  2. __len__(self): 返回数据集的样本数量,通常用于确定数据集的大小。

  3. __getitem__(self, index): 根据给定的索引 index 返回数据集中的一个样本。在这个方法中,你可以加载图像、标签,进行数据预处理等操作。

  4. 自定义数据加载和预处理:你可以在 __getitem__ 方法中定义如何加载和处理数据。这通常包括从磁盘加载图像、对图像进行缩放、归一化、数据增强等操作。

  5. 数据切分:你可以将数据集划分为训练、验证和测试集,以便用于模型训练和评估。

其中可以利用transform对数据进行预处理,数据增强等。

比如:

from torchvision import transforms


# 数据预处理操作,例如缩放、归一化等
transform = transforms.Compose([
    transforms.Resize((256, 256)),
    transforms.ToTensor(),
])

这样,你就可以使用 data_loader 对数据进行批处理并传递给神经网络进行训练。

来看看本项目如何制作数据集data_set。

from PIL import Image
import torch
from torch.utils.data import Dataset,DataLoader
from torchvision import transforms
import xml.etree.ElementTree as ET
import glob

#定义xml提取函数
class_idx = {'WBC':0,'RBC':1,'Platelets':2}

def get_LabelFromXml(xml_file):
    an_file = open(xml_file, encoding='utf-8')  # 用utf-8的格式打开该文件
    tree = ET.parse(an_file)  # 用ET来解析an_file,得到文件内容树格式
    root = tree.getroot()  # 获取树的根目录,抓取xml中的数据,与html的爬取是一样的
    label = []
    bbox_list = []
    for object in root.findall('object'):
        cell = object.find('name').text  # 同理拿到object的name属性,可以得到细胞的类名
        cell_id = class_idx[cell]  #根据字典将类名变成类序号
        xmin = object.find('bndbox').find('xmin').text  # 拿到候选框的位置
        ymin = object.find('bndbox').find('ymin').text
        xmax = object.find('bndbox').find('xmax').text
        ymax = object.find('bndbox').find('ymax').text
        #1 位于边界的框筛选不要
        #2 边界框无大小的筛选不邀
        if int(xmin)== 0 or int(xmax)== 0 or(ymin)== 0 or(ymax)== 0:
            pass #或者continue
        elif int(xmin) ==int(xmax)== 0 or(ymin) == (ymax)== 0:
            pass
        else:
            label.append(cell_id)   #保存标签名
            bbox_list.append([int(xmin),int(ymin),int(xmax),int(ymax)])  #保存候选框位置 左上角(x1,y1)和右下角(x2,y2)

    return  label,bbox_list


#pytorch 数据增强时,resize,旋转之类的,目标位置,目标类别都需改变,
transformer = transforms.Compose([transforms.ToTensor(),])
class CellDetection(Dataset):
    def __init__(self,img,xml,transformer = None):
        self.img = img
        self.xml = xml
        self.transformer = transformer
    def __getitem__(self, index):
        img = self.img[index]
        xml = self.xml[index]

        img_open = Image.open(img)
        img_tensor = self.transformer(img_open)
        label, bbox = get_LabelFromXml(xml)
        #将列表转换为tensor   label int64   box float32
        bbox_tensor = torch.as_tensor(bbox,dtype=torch.float32)
        label_tensor = torch.as_tensor(label,dtype=torch.int64)
        #打包表为字典
        target = {}
        target['boxes'] = bbox_tensor
        target['labels'] = label_tensor

        return img_tensor,target
    def __len__(self):
        return len(self.img)

#检验函数
path = './data/train/'  #设置需要爬取的文件路径,当前文件下data下的train文件
xml_file = glob.glob(path+'*.xml') #通过glob来爬取文件中.xml结尾的文件,文件名
img_file = glob.glob(path+'*.jpg') #通过glob来爬取文件中.jpg结尾的图片,文件名

#乱序文件调整顺序
xml_list = []
img_list = []

for i in xml_file:
    img = i[:-3]+'jpg'    #将xml文件名后三位改成jpg,就是图像的文件名
    if img in img_file:   #判断该文件名是不是在img_file中,如果在,那就加入img_list
        img_list.append(img)
        xml_list.append(i)

train_data = CellDetection(img_list,xml_list,transformer)
#测试
print(train_data.xml[5])


从以上代码CellDetection可以看到,CellDetection类具有3个属性,img,xml以及transform。

在len中返回的时数据集的大小。而在getiem具有数据索引以及对数据进行了加载或者读取,对于img图像进行了transform,并将其与label,bbox转化为tensor张量并且是float类型。这是因为神经网络输入数据的类型是以tensor形式输入的,并且是浮点数。而我们之前3中对数据进行加载得到的都是整数int类型。所以这块需要进行转换。之后将label与bbox统一打包成target。对于该类初始化输入img文件名列表,以及xml文件名列表与自定义的tramsformer。这样就得到train_dataset。当然也可以基于此得到test数据集。

5.dataloader

到这一步就简单了。我们已经得到了train_dataset。接下来就是将数据集输入dataloader中,得到train的数据加载器。

首先还是介绍一下dataloader。

DataLoader 是 PyTorch 中的一个工具,用于从数据集中加载批次的数据以供训练。它负责多项任务,如数据分批、数据随机化、多线程加载等,以帮助你更有效地训练深度学习模型。

#导入 DataLoader:
from torch.utils.data import DataLoader

#创建 DataLoader:
data_loader = DataLoader(dataset, batch_size=32, shuffle=True)

#dataset:要加载数据的数据集对象,通常是你自己创建的 torch.utils.data.Dataset 类的实例。
#batch_size:每个批次的样本数量。
#shuffle:是否对数据进行随机洗牌,通常在训练时使用以确保模型不会对样本的顺序产生依赖。


#遍历 DataLoader:
for inputs, labels in data_loader:
    # 在此处进行模型训练或其他操作

#DataLoader 还支持其他参数,如多线程数据加载、自定义批处理函数、持续加载等。
data_loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=4,collate_fn)
#num_workers:用于多线程数据加载的工作进程数量,可提高数据加载效率。
#collate_fn:自定义的数据批处理函数,用于对每个批次进行自定义处理。

上面3我们说到了不同图像中的label,bbox的大小是不同的,即每幅图中所包含的细胞数量、候选框数量时不同的。

这里需要自定义一个批处理函数。将它们合并成可以传递给神经网络的形式。

def detection_collate(x):
    return list(tuple(zip(*x)))

这里我们这样定义批处理函数。这个函数的主要目的是将一批数据中的图像、标签等信息从一个列表中提取出来并进行格式化,以便传递给神经网络。

在对象检测任务中,每个样本通常包含一幅图像和与该图像相关的目标(例如,边界框和类别标签)。每个样本的结构可能会有所不同,因此需要一个函数来将它们合并成可以传递给神经网络的形式。

detection_collate 函数中的 zip(*x) 用于将一批数据进行解压缩,然后 list 函数将结果转换为列表。这有助于将批处理的数据转换为一组图像和一组标签,以便进行后续的处理。

假设 x 是一批数据,其中每个元素是一个样本,每个样本由图像和标签组成,detection_collate 的目的是将它们分别提取出来,以便进行批处理。这个函数的具体行为可能会根据数据的结构和你的需求而有所不同。

这里提一嘴,我们的train_dataset,处理需要提取的label和xml,还有img,我们向网络中输入训练的就是图像,label与xml作为真实值计算损失函数。

除此之外dataloader的其他参数,batch_size,就是每次传进网络的批次大小,shuffle表示是否对数据打乱排序。

好了以上就是从数据集到dataloader。看看完整代码。

import matplotlib.pyplot as plt
from PIL import Image
import torch
from torch.utils.data import Dataset,DataLoader
from torchvision import transforms
import xml.etree.ElementTree as ET
import glob

#定义xml提取函数
class_idx = {'WBC':0,'RBC':1,'Platelets':2}

def get_LabelFromXml(xml_file):
    an_file = open(xml_file, encoding='utf-8')  # 用utf-8的格式打开该文件
    tree = ET.parse(an_file)  # 用ET来解析an_file,得到文件内容树格式
    root = tree.getroot()  # 获取树的根目录,抓取xml中的数据,与html的爬取是一样的
    label = []
    bbox_list = []
    for object in root.findall('object'):
        cell = object.find('name').text  # 同理拿到object的name属性,可以得到细胞的类名
        cell_id = class_idx[cell]  #根据字典将类名变成类序号
        xmin = object.find('bndbox').find('xmin').text  # 拿到候选框的位置
        ymin = object.find('bndbox').find('ymin').text
        xmax = object.find('bndbox').find('xmax').text
        ymax = object.find('bndbox').find('ymax').text
        #1 位于边界的框筛选不要
        #2 边界框无大小的筛选不邀
        if int(xmin)== 0 or int(xmax)== 0 or(ymin)== 0 or(ymax)== 0:
            pass #或者continue
        elif int(xmin) ==int(xmax)== 0 or(ymin) == (ymax)== 0:
            pass
        else:
            label.append(cell_id)   #保存标签名
            bbox_list.append([int(xmin),int(ymin),int(xmax),int(ymax)])  #保存候选框位置 左上角(x1,y1)和右下角(x2,y2)

    return  label,bbox_list


#pytorch 数据增强时,resize,旋转之类的,目标位置,目标类别都需改变,
transformer = transforms.Compose([transforms.ToTensor(),])
class CellDetection(Dataset):
    def __init__(self,img,xml,transformer = None):
        self.img = img
        self.xml = xml
        self.transformer = transformer
    def __getitem__(self, index):
        img = self.img[index]
        xml = self.xml[index]

        img_open = Image.open(img)
        img_tensor = self.transformer(img_open)
        label, bbox = get_LabelFromXml(xml)
        #将列表转换为tensor   label int64   box float32
        bbox_tensor = torch.as_tensor(bbox,dtype=torch.float32)
        label_tensor = torch.as_tensor(label,dtype=torch.int64)
        #打包表为字典
        target = {}
        target['boxes'] = bbox_tensor
        target['labels'] = label_tensor

        return img_tensor,target
    def __len__(self):
        return len(self.img)

#检验函数
path = './data/train/'  #设置需要爬取的文件路径,当前文件下data下的train文件
xml_file = glob.glob(path+'*.xml') #通过glob来爬取文件中.xml结尾的文件,文件名
img_file = glob.glob(path+'*.jpg') #通过glob来爬取文件中.jpg结尾的图片,文件名

#乱序文件调整顺序
xml_list = []
img_list = []

for i in xml_file:
    img = i[:-3]+'jpg'    #将xml文件名后三位改成jpg,就是图像的文件名
    if img in img_file:   #判断该文件名是不是在img_file中,如果在,那就加入img_list
        img_list.append(img)
        xml_list.append(i)

train_data = CellDetection(img_list,xml_list,transformer)
#测试
print(train_data.xml[5])


#定义打包函数
def detection_collate(x):
    return list(tuple(zip(*x)))
print(detection_collate(train_data[1]))


dl_train = DataLoader(train_data,batch_size = 2,shuffle=True,collate_fn=detection_collate)
#数据加载器,加入train_data,因为本项目目标检测任务,其label,与bbox的数据长度不一致,所以不能完美的输入金网络
#多物体,多目标操作
#如果不对数据打包,则不能把数据形成一个batch放进网络
img,label = next(iter(dl_train))
label1 = label[0]
print(label1)
print(img[0])
print(img[0].shape)

plt.imshow(img[0].permute(1,2,0))
#permute 将tensor变量换通道. 图片转tensor 0通道是3,1是h,2是w
plt.show()
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 PyTorch 中使用 `faster_rcnn_resnet50_fpn` 模型,可以按照以下步骤进行: 1. 安装 PyTorch 和 TorchVision 库(如果未安装的话)。 2. 导入必要的库和模块: ```python import torch import torchvision from torchvision.models.detection.faster_rcnn import FastRCNNPredictor ``` 3. 加载预训练模型 `faster_rcnn_resnet50_fpn`: ```python model = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained=True) ``` 4. 修改模型的分类器,将其调整为适合你的任务。由于 `faster_rcnn_resnet50_fpn` 是一个目标检测模型,它的分类器通常是用来检测物体类别的。如果你的任务不需要检测物体类别,可以将分类器替换为一个只有一个输出的线性层: ```python num_classes = 1 # 只检测一个类别 in_features = model.roi_heads.box_predictor.cls_score.in_features model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes) ``` 5. 将模型转换为训练模式,并将其移动到所选设备(如GPU)上: ```python device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu') model.to(device) model.train() # 转换为训练模式 ``` 6. 训练模型,可以使用自己的数据集来训练模型,或者使用 TorchVision 中的数据集,如 Coco 或 Pascal VOC 数据集。 7. 在测试阶段,可以使用以下代码来检测图像中的物体: ```python # 定义图像 image = Image.open('test.jpg') # 转换为Tensor,并将其移动到设备上 image_tensor = torchvision.transforms.functional.to_tensor(image) image_tensor = image_tensor.to(device) # 执行推理 model.eval() with torch.no_grad(): outputs = model([image_tensor]) # 处理输出 boxes = outputs[0]['boxes'].cpu().numpy() # 物体框 scores = outputs[0]['scores'].cpu().numpy() # 物体分数 ``` 需要注意的是,`faster_rcnn_resnet50_fpn` 是一个较大的模型,需要较高的计算资源和训练时间。在训练和测试时,建议使用GPU来加速计算。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值