【目标检测数据处理】数据集标注文件处理的相关脚本

一、标注格式转换

1.voc2yolo

import os
import xml.etree.ElementTree as ET

# VOC格式标注文件夹路径
voc_annotations_folder = "E:\\Data_set\\on_water\\Changjiangdata\\Changjiangdata\\voc\\ann1_c2"
# YOLO格式标注保存路径
yolo_annotations_folder = "E:\\Data_set\\on_water\\Changjiangdata\\Changjiangdata\\voc\\label1_c2"

# 类别映射字典,根据自己的数据集修改
class_map = {
    "passenger_ship": 0,
    "cargo_ship": 1,
    # "class3": 2,
    # Add more classes as needed
}

def convert_coordinates(size, box):
    dw = 1.0 / size[0]
    dh = 1.0 / size[1]
    x = (box[0] + box[1]) / 2.0
    y = (box[2] + box[3]) / 2.0
    w = box[1] - box[0]
    h = box[3] - box[2]
    x = x * dw
    w = w * dw
    y = y * dh
    h = h * dh
    return x, y, w, h

def convert_voc_to_yolo(voc_annotation_path, yolo_annotation_path, class_map):
    tree = ET.parse(voc_annotation_path)
    root = tree.getroot()

    size = root.find('size')
    width = float(size.find('width').text)
    height = float(size.find('height').text)

    yolo_lines = []

    for obj in root.iter('object'):
        difficult = obj.find('difficult').text
        cls = obj.find('name').text
        if cls not in class_map or int(difficult) == 1:
            continue
        cls_id = class_map[cls]
        xmlbox = obj.find('bndbox')
        xmin = float(xmlbox.find('xmin').text)
        xmax = float(xmlbox.find('xmax').text)
        ymin = float(xmlbox.find('ymin').text)
        ymax = float(xmlbox.find('ymax').text)
        b = (xmin, xmax, ymin, ymax)
        bb = convert_coordinates((width, height), b)
        yolo_lines.append(f"{cls_id} {' '.join([str(a) for a in bb])}")

    with open(yolo_annotation_path, 'w') as f:
        for line in yolo_lines:
            f.write(line + '\n')

if not os.path.exists(yolo_annotations_folder):
    os.makedirs(yolo_annotations_folder)

# 遍历VOC格式标注文件夹
for filename in os.listdir(voc_annotations_folder):
    if filename.endswith('.xml'):
        voc_annotation_path = os.path.join(voc_annotations_folder, filename)
        # 构建对应的YOLO格式标注文件名
        yolo_annotation_path = os.path.join(yolo_annotations_folder, os.path.splitext(filename)[0] + ".txt")
        convert_voc_to_yolo(voc_annotation_path, yolo_annotation_path, class_map)

print("Conversion completed.")

2.yolo2voc

from xml.dom.minidom import Document
import os
import cv2


# def makexml(txtPath, xmlPath, picPath):  # txt所在文件夹路径,xml文件保存路径,图片所在文件夹路径
def makexml(picPath, txtPath, xmlPath):  # txt所在文件夹路径,xml文件保存路径,图片所在文件夹路径
    """此函数用于将yolo格式txt标注文件转换为voc格式xml标注文件
    """
    dic = {'0': "cargo_ship",  # 创建字典用来对类型进行转换
           '1': "container_ship",  # 此处的字典要与自己的classes.txt文件中的类对应,且顺序要一致
           '2': "passenger_ship",
           '3':'speed_boat',
           '4':'buoy'
           }
    files = os.listdir(txtPath)
    for i, name in enumerate(files):
        xmlBuilder = Document()
        annotation = xmlBuilder.createElement("annotation")  # 创建annotation标签
        xmlBuilder.appendChild(annotation)
        txtFile = open(txtPath + name)
        txtList = txtFile.readlines()
        img = cv2.imread(picPath + name[0:-4] + ".jpg")
        Pheight, Pwidth, Pdepth = img.shape

        folder = xmlBuilder.createElement("folder")  # folder标签
        foldercontent = xmlBuilder.createTextNode("driving_annotation_dataset")
        folder.appendChild(foldercontent)
        annotation.appendChild(folder)  # folder标签结束

        filename = xmlBuilder.createElement("filename")  # filename标签
        filenamecontent = xmlBuilder.createTextNode(name[0:-4] + ".jpg")
        filename.appendChild(filenamecontent)
        annotation.appendChild(filename)  # filename标签结束

        size = xmlBuilder.createElement("size")  # size标签
        width = xmlBuilder.createElement("width")  # size子标签width
        widthcontent = xmlBuilder.createTextNode(str(Pwidth))
        width.appendChild(widthcontent)
        size.appendChild(width)  # size子标签width结束

        height = xmlBuilder.createElement("height")  # size子标签height
        heightcontent = xmlBuilder.createTextNode(str(Pheight))
        height.appendChild(heightcontent)
        size.appendChild(height)  # size子标签height结束

        depth = xmlBuilder.createElement("depth")  # size子标签depth
        depthcontent = xmlBuilder.createTextNode(str(Pdepth))
        depth.appendChild(depthcontent)
        size.appendChild(depth)  # size子标签depth结束

        annotation.appendChild(size)  # size标签结束

        for j in txtList:
            oneline = j.strip().split(" ")
            object = xmlBuilder.createElement("object")  # object 标签
            picname = xmlBuilder.createElement("name")  # name标签
            namecontent = xmlBuilder.createTextNode(dic[oneline[0]])
            picname.appendChild(namecontent)
            object.appendChild(picname)  # name标签结束

            pose = xmlBuilder.createElement("pose")  # pose标签
            posecontent = xmlBuilder.createTextNode("Unspecified")
            pose.appendChild(posecontent)
            object.appendChild(pose)  # pose标签结束

            truncated = xmlBuilder.createElement("truncated")  # truncated标签
            truncatedContent = xmlBuilder.createTextNode("0")
            truncated.appendChild(truncatedContent)
            object.appendChild(truncated)  # truncated标签结束

            difficult = xmlBuilder.createElement("difficult")  # difficult标签
            difficultcontent = xmlBuilder.createTextNode("0")
            difficult.appendChild(difficultcontent)
            object.appendChild(difficult)  # difficult标签结束

            bndbox = xmlBuilder.createElement("bndbox")  # bndbox标签
            xmin = xmlBuilder.createElement("xmin")  # xmin标签
            mathData = int(((float(oneline[1])) * Pwidth + 1) - (float(oneline[3])) * 0.5 * Pwidth)
            xminContent = xmlBuilder.createTextNode(str(mathData))
            xmin.appendChild(xminContent)
            bndbox.appendChild(xmin)  # xmin标签结束

            ymin = xmlBuilder.createElement("ymin")  # ymin标签
            mathData = int(((float(oneline[2])) * Pheight + 1) - (float(oneline[4])) * 0.5 * Pheight)
            yminContent = xmlBuilder.createTextNode(str(mathData))
            ymin.appendChild(yminContent)
            bndbox.appendChild(ymin)  # ymin标签结束

            xmax = xmlBuilder.createElement("xmax")  # xmax标签
            mathData = int(((float(oneline[1])) * Pwidth + 1) + (float(oneline[3])) * 0.5 * Pwidth)
            xmaxContent = xmlBuilder.createTextNode(str(mathData))
            xmax.appendChild(xmaxContent)
            bndbox.appendChild(xmax)  # xmax标签结束

            ymax = xmlBuilder.createElement("ymax")  # ymax标签
            mathData = int(((float(oneline[2])) * Pheight + 1) + (float(oneline[4])) * 0.5 * Pheight)
            ymaxContent = xmlBuilder.createTextNode(str(mathData))
            ymax.appendChild(ymaxContent)
            bndbox.appendChild(ymax)  # ymax标签结束

            object.appendChild(bndbox)  # bndbox标签结束

            annotation.appendChild(object)  # object标签结束

        f = open(xmlPath + name[0:-4] + ".xml", 'w')
        xmlBuilder.writexml(f, indent='\t', newl='\n', addindent='\t', encoding='utf-8')
        f.close()


if __name__ == "__main__":
    picPath = "D:/Data_set/on_water/Changjiangdata/Changjiangdata/images/val/"  # 图片所在文件夹路径,后面的/一定要带上
    txtPath = "D:/Data_set/on_water/Changjiangdata/Changjiangdata/labels/val/"  # txt所在文件夹路径,后面的/一定要带上
    xmlPath = "D:/Data_set/on_water/Changjiangdata/Changjiangdata/Annotations/"  # xml文件保存路径,后面的/一定要带上
    makexml(picPath, txtPath, xmlPath)

二、数据集划分

1.voc(xml)划分

import os
import random
import argparse

parser = argparse.ArgumentParser()
#xml文件的地址,根据自己的数据进行修改 xml一般存放在Annotations下
parser.add_argument('--xml_path',default='seaships7000/Annotations', type=str, help='input xml label path') # 路径自行修改
#数据集的划分,地址选择自己数据下的ImageSets/Main
parser.add_argument('--txt_path',default='seaships7000/ImageSets/Main',type=str, help='output txt label path')# 路径自行修改
opt = parser.parse_args()

trainval_percent = 1.0
train_percent = 0.7
xmlfilepath = opt.xml_path
txtsavepath = opt.txt_path
total_xml = os.listdir(xmlfilepath)
if not os.path.exists(txtsavepath):
    os.makedirs(txtsavepath)

num = len(total_xml)
list_index = range(num)
tv = int(num * trainval_percent)
tr = int(tv * train_percent)
trainval = random.sample(list_index, tv)
train = random.sample(trainval, tr)

file_trainval = open(txtsavepath + '/trainval.txt', 'w')
file_test = open(txtsavepath + '/test.txt', 'w')
file_train = open(txtsavepath + '/train.txt', 'w')
file_val = open(txtsavepath + '/val.txt', 'w')

for i in list_index:
    name = total_xml[i][:-4] + '\n'
    if i in trainval:
        file_trainval.write(name)
        if i in train:
            file_train.write(name)
        else:
            file_val.write(name)
    else:
        file_test.write(name)

file_trainval.close()
file_train.close()
file_val.close()
file_test.close()

2.yolo格式划分

import shutil
import random
import os

# 原始路径
image_original_path = "D:/Data_set/Fusion_Dataset/FusionDataset_20231127_1630/Img_20231127_1630/mydata/images/"
label_original_path = "D:/Data_set/Fusion_Dataset/FusionDataset_20231127_1630/Img_20231127_1630/mydata/labels/"

cur_path = os.getcwd()


# 划分后的数据集根目录
dataset_root = "D:/Data_set/Fusion_Dataset/FusionDataset_20231127_1630/Img_20231127_1630"

# 训练集路径
train_image_path = os.path.join(dataset_root, "images/train/")
train_label_path = os.path.join(dataset_root, "labels/train/")

# 验证集路径
val_image_path = os.path.join(dataset_root, "images/val/")
val_label_path = os.path.join(dataset_root, "labels/val/")

# 测试集路径
test_image_path = os.path.join(dataset_root, "images/test/")
test_label_path = os.path.join(dataset_root, "labels/test/")

# 训练集目录
list_train = os.path.join(dataset_root, "train.txt")
list_val = os.path.join(dataset_root, "val.txt")
list_test = os.path.join(dataset_root, "test.txt")
train_percent = 0.8
val_percent = 0.2
test_percent = 0


def del_file(path):
    for i in os.listdir(path):
        file_data = path + "\\" + i
        os.remove(file_data)


def mkdir():
    if not os.path.exists(train_image_path):
        os.makedirs(train_image_path)
    else:
        del_file(train_image_path)
    if not os.path.exists(train_label_path):
        os.makedirs(train_label_path)
    else:
        del_file(train_label_path)

    if not os.path.exists(val_image_path):
        os.makedirs(val_image_path)
    else:
        del_file(val_image_path)
    if not os.path.exists(val_label_path):
        os.makedirs(val_label_path)
    else:
        del_file(val_label_path)

    if not os.path.exists(test_image_path):
        os.makedirs(test_image_path)
    else:
        del_file(test_image_path)
    if not os.path.exists(test_label_path):
        os.makedirs(test_label_path)
    else:
        del_file(test_label_path)


def clearfile():
    if os.path.exists(list_train):
        os.remove(list_train)
    if os.path.exists(list_val):
        os.remove(list_val)
    if os.path.exists(list_test):
        os.remove(list_test)


def main():
    mkdir()
    clearfile()

    file_train = open(list_train, 'w')
    file_val = open(list_val, 'w')
    file_test = open(list_test, 'w')

    total_txt = os.listdir(label_original_path)
    num_txt = len(total_txt)
    list_all_txt = range(num_txt)

    num_train = int(num_txt * train_percent)
    num_val = int(num_txt * val_percent)
    num_test = num_txt - num_train - num_val

    train = random.sample(list_all_txt, num_train)
    # train从list_all_txt取出num_train个元素
    # 所以list_all_txt列表只剩下了这些元素
    val_test = [i for i in list_all_txt if not i in train]
    # 再从val_test取出num_val个元素,val_test剩下的元素就是test
    val = random.sample(val_test, num_val)

    print("训练集数目:{}, 验证集数目:{}, 测试集数目:{}".format(len(train), len(val), len(val_test) - len(val)))
    for i in list_all_txt:
        name = total_txt[i][:-4]

        srcImage = image_original_path + name + '.jpg'
        srcLabel = label_original_path + name + ".txt"

        if i in train:
            dst_train_Image = train_image_path + name + '.jpg'
            dst_train_Label = train_label_path + name + '.txt'
            shutil.copyfile(srcImage, dst_train_Image)
            shutil.copyfile(srcLabel, dst_train_Label)
            file_train.write(dst_train_Image + '\n')
        elif i in val:
            dst_val_Image = val_image_path + name + '.jpg'
            dst_val_Label = val_label_path + name + '.txt'
            shutil.copyfile(srcImage, dst_val_Image)
            shutil.copyfile(srcLabel, dst_val_Label)
            file_val.write(dst_val_Image + '\n')
        else:
            dst_test_Image = test_image_path + name + '.jpg'
            dst_test_Label = test_label_path + name + '.txt'
            shutil.copyfile(srcImage, dst_test_Image)
            shutil.copyfile(srcLabel, dst_test_Label)
            file_test.write(dst_test_Image + '\n')

    file_train.close()
    file_val.close()
    file_test.close()

if __name__ == "__main__":
    main()


三、统计目标个数

1.voc标注格式(xml)

import os
import xml.etree.ElementTree as ET

# Path of xml files folder
xml_path = r"E:\\Data_set\\on_water\\Changjiangdata\\Changjiangdata\\voc\\ann1"
files = os.listdir(xml_path)

gt_dict = {}

if __name__ == '__main__':

    for xm in files:
        xmlfile = os.path.join(xml_path, xm)
        # Skip directories
        if not os.path.isfile(xmlfile):
            continue

        try:
            tree = ET.parse(xmlfile)
            root = tree.getroot()
            filenamelist = root.findall("filename")
            filename = filenamelist[0].text
            objectlist = root.findall("object")
            ##
            for objects in objectlist:
                namelist = objects.findall("name")
                objectname = namelist[0].text
                if objectname == '-':
                    print(filename)
                if objectname in gt_dict:
                    gt_dict[objectname] += 1
                else:
                    gt_dict[objectname] = 1
        except ET.ParseError:
            print(f"无法解析XML文件:{xmlfile}")

    dic = sorted(gt_dict.items(), key=lambda d: d[1], reverse=True)
    print(dic)
    print(len(dic))

2.YOLO格式

import os

label_folder = "D:/Data_set/on_water/Changjiangdata/Changjiangdata/labels/train"  # 标签文件夹路径
class_counts = {}  # 类别计数字典

label_list = os.listdir(label_folder)
for label_file in label_list:
    label_path = os.path.join(label_folder, label_file)
    with open(label_path, 'r') as file:
        lines = file.readlines()
        for line in lines:
            class_id = int(line.split()[0])
            class_counts[class_id] = class_counts.get(class_id, 0) + 1

# 输出类别计数结果
for class_id, count in class_counts.items():
    print("Class {}: {}".format(class_id, count))

四、YOLO标注可视化

1.全部可视化

import cv2
import os
from tqdm import tqdm


# 读取YOLO标注文件
def read_yolo_annotation(file_path):
    with open(file_path, 'r') as file:
        data = file.read().strip().split(' ')
        class_id = int(data[0])
        x, y, w, h = map(float, data[1:])
        return class_id, x, y, w, h


# 绘制标注框并保存
def draw_and_save_boxes(image_path, txt_path, output_folder):
    img = cv2.imread(image_path)
    class_id, x, y, w, h = read_yolo_annotation(txt_path)

    height, width, _ = img.shape
    x1 = int((x - w / 2) * width)
    y1 = int((y - h / 2) * height)
    x2 = int((x + w / 2) * width)
    y2 = int((y + h / 2) * height)

    cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)

    output_path = os.path.join(output_folder, os.path.basename(image_path))
    cv2.imwrite(output_path, img)


# 遍历文件夹中的图片和YOLO标注文件
def process_images_in_folders(image_folder, txt_folder, output_folder):
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)  # 创建输出文件夹

    image_files = [file for file in os.listdir(image_folder) if file.endswith(".jpg")]

    for file in tqdm(image_files, desc='Processing images'):
        image_path = os.path.join(image_folder, file)
        txt_file = os.path.splitext(file)[0] + ".txt"
        txt_path = os.path.join(txt_folder, txt_file)

        if os.path.exists(txt_path):
            draw_and_save_boxes(image_path, txt_path, output_folder)

# 示例使用
image_folder = "D:/Data_set/Fusion_Dataset/FusionDataset_20231127_1630/Img_20231127_1630/mydata/images"
txt_folder = "D:/Data_set/Fusion_Dataset/FusionDataset_20231127_1630/Img_20231127_1630/mydata/labels"
output_folder = "D:/Data_set/Fusion_Dataset/FusionDataset_20231127_1630/Img_20231127_1630/mydata/output_folder"

process_images_in_folders(image_folder, txt_folder, output_folder)

2.其他版本

import cv2
import os

def draw_box_in_single_image(image_path, txt_path):
    # 读取图像
    image = cv2.imread(image_path)

    # 读取txt文件信息
    def read_list(txt_path):
        pos = []
        with open(txt_path, 'r') as file_to_read:
            while True:
                lines = file_to_read.readline()  # 整行读取数据
                if not lines:
                    break
                # 将整行数据分割处理,如果分割符是空格,括号里就不用传入参数,如果是逗号, 则传入‘,'字符。
                p_tmp = [float(i) for i in lines.split(' ')]
                pos.append(p_tmp)  # 添加新读取的数据
                # Efield.append(E_tmp)
                pass
        return pos


    # txt转换为box
    def convert(size, box):
        xmin = (box[1]-box[3]/2.)*size[1]
        xmax = (box[1]+box[3]/2.)*size[1]
        ymin = (box[2]-box[4]/2.)*size[0]
        ymax = (box[2]+box[4]/2.)*size[0]
        box = (int(xmin), int(ymin), int(xmax), int(ymax))
        return box

    pos = read_list(txt_path)
    print(pos)
    tl = int((image.shape[0]+image.shape[1])/2)
    lf = max(tl-1,1)
    for i in range(len(pos)):
        label = str(int(pos[i][0]))
        print('label is '+label)
        box = convert(image.shape, pos[i])
        image = cv2.rectangle(image,(box[0], box[1]),(box[2],box[3]),(0,0,255),2)
        cv2.putText(image,label,(box[0],box[1]-2), 0, 1, [0,0,255], thickness=2, lineType=cv2.LINE_AA)
        pass

    if pos:
        cv2.imwrite('D:/Data_set/on_water/Changjiangdata/Changjiangdata/see_labels_images/{}.png'.format(image_path.split('\\')[-1][:-4]), image)
    else:
        print('None')


    print('D:/Data_set/on_water/Changjiangdata/Changjiangdata/see_labels_images/{}.png'.format(image_path.split('\\')[-1][:-4]))
    # cv2.imshow("images", image)
    # cv2.waitKey(0)
    # cv2.destroyAllWindows()


img_folder = "D:/Data_set/on_water/Changjiangdata/Changjiangdata/images/train"
img_list = os.listdir(img_folder)
img_list.sort()

label_folder = "D:/Data_set/on_water/Changjiangdata/Changjiangdata/labels/train"
label_list = os.listdir(label_folder)
label_list.sort()

for i in range(len(img_list)):
    image_path = img_folder + "\\" + img_list[i]
    txt_path = label_folder + "\\" + label_list[i]
    draw_box_in_single_image(image_path, txt_path)

五、标注修改

1.名称替换

import os
import xml.etree.ElementTree as ET

# 定义类别名称的替换映射
class_mapping = {
    # 'fishing_ship': 'cargo_ship',
    # 'Cargo_ship': 'cargo_ship',
    # 'Cargo_Ship': 'cargo_ship',
    # 'Fishing_ship': 'fishing_ship',
    # 'Passenger_ship': 'passenger_ship',
    # 'Container_ship': 'container_ship',
    # 'Buoy': 'buoy',
    # 'Speed_boat': 'speed_boat'
    'Sailing_boat': 'Passenger_ship'
}

# 指定包含XML标注文件的文件夹路径
folder_path = "D:/Data_set/haihang/分组/1"

# 遍历文件夹中的每个XML标注文件
for filename in os.listdir(folder_path):
    if not filename.endswith('.xml'):
        continue

    file_path = os.path.join(folder_path, filename)

    # 解析XML文件
    tree = ET.parse(file_path)
    root = tree.getroot()

    # 查找并替换类别名称
    for obj in root.iter('object'):
        name = obj.find('name').text
        if name in class_mapping:
            new_name = class_mapping[name]
            obj.find('name').text = new_name

    # 保存修改后的XML文件
    tree.write(file_path)

2.删除指定名称

import os
import xml.etree.ElementTree as ET

def remove_yes_boxes(xml_folder):
    for xml_file in os.listdir(xml_folder):
        if not xml_file.endswith('.xml'):
            continue

        xml_path = os.path.join(xml_folder, xml_file)
        tree = ET.parse(xml_path)
        root = tree.getroot()

        # 查找所有的 object 元素
        objects = root.findall('object')
        for obj in objects:
            # 查找名为 'name' 的子元素
            name_elem = obj.find('name')
            if name_elem is not None and name_elem.text == 'speed_boat':
                # 删除包含 'Yes' 的标注框
                root.remove(obj)
                # print('ok')

        # 保存修改后的 XML 文件
        tree.write(xml_path)

# 指定包含 XML 文件的文件夹路径
xml_folder = 'E:\\Data_set\\on_water\\Changjiangdata\\Changjiangdata\\voc\\ann1'

# 调用函数删除名为 'Yes' 的标注框
remove_yes_boxes(xml_folder)

3.删除小像素目标

import os
import xml.etree.ElementTree as ET

# 定义目标框的最小面积阈值
min_area = 64

# 指定包含XML标注文件的文件夹路径
folder_path = "D:\Data_set\on_water\Ship\labels"

# 遍历文件夹中的每个XML标注文件
for filename in os.listdir(folder_path):
    if not filename.endswith('.xml'):
        continue

    file_path = os.path.join(folder_path, filename)

    # 解析XML文件
    tree = ET.parse(file_path)
    root = tree.getroot()

    # 查找并删除面积小于最小阈值的目标框
    for obj in root.findall('object'):
        bbox = obj.find('bndbox')
        xmin = int(bbox.find('xmin').text)
        ymin = int(bbox.find('ymin').text)
        xmax = int(bbox.find('xmax').text)
        ymax = int(bbox.find('ymax').text)
        area = (xmax - xmin) * (ymax - ymin)
        if area < min_area:
            root.remove(obj)

    # 保存修改后的XML文件
    tree.write(file_path)

4.txt内容替换

def replace_text_in_file(file_path, old_text, new_text):
    # 读取原始文件内容
    with open(file_path, 'r') as file:
        file_content = file.read()

    # 替换文本内容
    new_content = file_content.replace(old_text, new_text)

    # 将替换后的内容写回文件
    with open(file_path, 'w') as file:
        file.write(new_content)

# 指定文件路径、要替换的旧文本和新文本
file_path = "val.txt"
old_text = 'yolov9' #旧文本
new_text = 'yolov5_5' #新文本

# 调用函数进行文本替换
replace_text_in_file(file_path, old_text, new_text)

print(f"文本替换完成。")

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值