第十章 深度学习中的图像数据扩增(Data Augmentations)(工具)

117 篇文章 2 订阅
57 篇文章 0 订阅

数据增强介绍

1. 前言

这篇文章主要参考 A survey on Image Data Augmentation for Deep Learning, 总结了常用的传统扩增方法及其应用时的注意事项。这里的传统方法指不包括基于深度学习(比如 GAN)等新的扩增方法。

另外需要注意的是,虽然对于不同的任务,比如对于分类,检测任务,不同的任务在采用某一个具体的扩增方法的时候会有所不同,比如对于检测任务需要考虑对 bounding box 进行相应的操作,但是这里仅仅从扩增方法的角度来说是没有区别的。

最后, 数据扩增的具体方法非常多,而且除了各个训练框架提供的方法之外还有很多第三方库,这里仅仅是整理了一些比较常见的扩增方法。更多的扩增方法可以参考第三方库 imgaug, albumentations 等。

在这里插入图片描述

2. 数据扩增

数据扩增是对数据进行扩充的方法的总称。数据扩增可以增加训练集的样本,可以有效缓解模型过拟合的情况,也可以给模型带来的更强的泛化能力。怎么理解呢?

数据扩增的目的就是使得训练数据尽可能的接近测试数据,从而提高预测精度。另外数据扩增可以迫使网络学习到更鲁棒性的特征,从而使模型拥有更强的泛化能力,比如对图像进行一定程度的遮挡。

通常在进行数据扩增操作的时候应该保持图像原本的标签不变,比如对于猫狗分类任务,rotate 或者 flip ,一般对标签是没有影响的,但是对于手写数字识别,比如 9 和 6 就不适用了。当然如果能相应的修改标签,对于网络训练来说是有益的,但是这将又是一个麻烦的费时费力的过程。所以通常来说,数据扩增应该在不改变标签的前提下进行。

3. 常用数据扩增方法

3.1 基于几何变换的扩增方法

基于几何变换的方法可以消除测试集和训练集的位置差异,尺度差异,视角差异等。

3.1.1 Flipping

水平翻转通常比竖直翻转更通用,但是对于字符识别任务,通常不适用。

3.1.2 Cropping

裁剪是一个比较常用,也是一个长点比较明显的数据增强的方式。
img

3.1.3 Rotation

旋转的角度关系到标签是否安全,比如对于 MNIST,1° 到 20° 或者 -1° 到 -20° 之间的轻微旋转是没有问题的,如果更大幅度的旋转可能会影响到标签。

3.1.4 Translation

平移(向左、向右、向上或向下移动)图像是一个非常有用的变换,可以避免数据中的位置偏差。例如,如果数据集中的所有图像都是居中的(这在人脸识别数据集中很常见),这就要求模型也要在完全居中的图像上进行测试。当原始图像被平移后造成的空白区域,可以用一个常数值填充,如 0 或 255 ,也可以用随机或高斯噪声填充。这种填充可以保留图像增强后的空间尺寸。

3.1.5 Noise injection

添加噪声一般可以应对噪声干扰,或者成像异常等特殊情况。

增加高斯噪声是比较常用的操作,增加噪声可以帮助 CNNs 学习到更 robust feature。

对于训练数据中存在的位置偏差,几何变换是非常好的解决方案。有许多潜在的偏差来源,可以将训练数据与测试数据的分布分开。如果存在位置偏差,例如在人脸识别数据集中,每个人脸都是完全居中的,几何变换是一个很好的解决方案。除了克服位置偏差的强大能力之外,几何变换也很有用,因为它们很容易实现。有很多成像处理库,可以让水平翻转和旋转等操作轻松上手。几何变换的一些缺点包括额外的内存、变换计算成本和额外的训练时间。一些几何变换,如平移或随机裁剪等几何变换必须手动观察,以确保它们没有改变图像的标签。最后,在所涉及的许多应用领域,如医学图像分析,训练数据与测试数据之间的偏差比位置偏差和平移偏差更复杂。因此,几何变换也不一定总是能带来明显的效果。

3.2 颜色空间变换

颜色空间变换一般可以消除光照、亮度及色彩差异。

  • 对过亮或过暗的图像进行快速处理的方法是在图像中进行循环,并将像素值减少或增加一个恒定值。
  • 另一种快速的色彩空间处理方法是拼接出单个RGB颜色矩阵。
  • 另一种变换包括将像素值限制在一定的最小值或最大值。数字图像中颜色的内在表现形式,使其可以用于许多增强策略。
  • 通道分离
  • 亮度
  • 对比度
  • 饱和度
  • 基于直方图
  • 灰度图
    转换为灰度图后,计算量减少,但是精度通常会有所降低,有人在 ImageNet 上对比 RGB 和 灰度图,发现灰度图的精度下降大约 3%。

颜色空间变换也可以从图像编辑应用程序中得到。图像中每个RGB颜色通道中的像素值被聚合成一个颜色直方图。这个直方图可以被操纵,应用滤镜来改变图像的颜色空间特性。

颜色空间变化可以帮助客服光照差异,但是如果任务对颜色的依赖性很强,比如要分辨油漆,水和血液,可能红色是一个非常重要的信息,如果进行不当的颜色空间变换,可能适得其反。

3.3 Kernel filters

  • 模糊
  • 锐化
    直观地讲,为数据增强而对图像进行模糊化处理可能会导致测试过程中对运动模糊的抵抗力更强。此外,为数据增强而对图像进行锐化可能会导致对感兴趣对象的更多细节进行封装。另外由于 Kernel filters 的方式和 CNNs 类似,因此可以把该操作集成到 CNN 层。

3.4 Mixing images

在这里插入图片描述

Mixing images 被证明对小的数据集作用更明显。

  • 两幅图每个像素取平均
  • 两幅图线性叠加
  • 从两幅或多幅图中裁剪一个局部,然后将这些拼接

这种方法主要在目标检测中使用的比较多,效果比较好。

3.5 Random erasing

思想和 dropout 类似,不同的是 Random erasing 是在输入数据空间进行,而非是在网络结构中。这种方法也可以看着是在模拟遮挡的情况,以保证网络关注整个图像,而不是只关注其中的一个子集。

通常 earsing 的区域直接填充随机值效果更好。使用的时候需要注意是否标签安全,可能需要人为的加入一些限制,以保证标签的正确性。

albumentations介绍

1. Albumentations的自我介绍

  1. 我的官方地址在 github链接

  2. 我的API(文档)地址在 https://albumentations.ai/docs/

  3. 我是负责处理图像的一个库,可用于所有数据类型:图像(RBG图像,灰度图像,多光谱图像),分割mask,边界框和关键点

  4. 我大概有70多种不同的图像处理方法,相比torch自带的,这个库函数有更多的对图像的预处理的办法

  5. 我的特点就是:在相同的对图像的处理下,我的速度就是比其他处理方式更快👍
    img
    这个图中,可以看到albumentations的处理方法中,很多都是速度最快的1。

  6. 我可以与流行的深度学习框架(例如PyTorch和TensorFlow)一起使用。顺便说一句,我还是PyTorch生态系统的一部分

  7. 对Pytorch很友好,而且这个库函数是kaggle master制作的

  8. 广泛用于工业,深度学习研究,机器学习竞赛和开源项目。就是大佬都爱用的一个库,在kaggle比赛上都有我的身影。

2.案例欣赏

首先我们来欣赏一哈官方提供的案例。

  • Inria数据集上的语义分割 数据增强
    在这里插入图片描述
    ​​​​​​对原始图像进行了水平旋转,垂直旋转,旋转90度,行列转置,尺度缩放等
  • 医学影像 MRI
    在这里插入图片描述
  • Mapillary Vistas数据集上的对象检测和语义分割

在这里插入图片描述

  • 关键点增强
    在这里插入图片描述

弄个好用的函数库,我们一起来学习学习撒~~~~~~

3.如何安装

  1. pip 安装
    pip install albumentations
  2. 源码库安装
    pip install -U git+https://github.com/albumentations-team/albumentations

4.有哪些数据增强方法

数据增强方法分为像素级变换和空间级变换。像素级变换只针对图像,空间级变换同时作用于图像,mask(就是分割的标签,中文称为蒙版),检测框(BBoxes),关键点。–

4.1 像素级变换(Pixel-level transforms)

像素级转换将仅更改输入图像,并且将使所有其他目标(例如蒙版,边界框和关键点)保持不变。像素级转换包括:模糊,色彩抖动,图像压缩,高斯噪声,倒置,归一化,随机雨,随机亮度对比,锐化,色相饱和度值等等。

其实用脑壳想一哈,这些变换只要是针对输入图像嘛,不可能对label或者mask,或者检测框进行模糊,加噪声这些嘛。选用的时候动哈脑筋就对了,不用看这个分类的。

4.2 空间级变换(Spatial-level transforms)

空间级变换将同时更改输入图像以及其他目标,例如蒙版,边界框和关键点。下表显示了每个转换支持哪些其他目标。
在这里插入图片描述
在使用空间变换的时候就要注意,图像转了,那蒙版,检测框啥的也要跟着转才对,而且设计到随机的变换,更要注意,图像和标签是否对齐。


说了那么多,还是不晓得咋个使用。别急,现在起讲用法,保证看完,妈妈都会了。

5.看完就会的使用方法

首先,来看看使用的正确姿势

import albumentations as A
trans = A.Compose([
        A.HorizontalFlip(p=0.5),
        A.VerticalFlip(p=0.5),
        A.OneOf([
            A.IAAAdditiveGaussianNoise(),   # 将高斯噪声添加到输入图像
            A.GaussNoise(),    # 将高斯噪声应用于输入图像。
        ], p=0.2),   # 应用选定变换的概率
        A.OneOf([
            A.MotionBlur(p=0.2),   # 使用随机大小的内核将运动模糊应用于输入图像。
            A.MedianBlur(blur_limit=3, p=0.1),    # 中值滤波
            A.Blur(blur_limit=3, p=0.1),   # 使用随机大小的内核模糊输入图像。
        ], p=0.2),
        A.ShiftScaleRotate(shift_limit=0.0625, scale_limit=0.2, rotate_limit=45, p=0.2),
        # 随机应用仿射变换:平移,缩放和旋转输入
        A.RandomBrightnessContrast(p=0.2),   # 随机明亮对比度
    ])

这里面特别要注意两个方法,很重要。Compose 和 OneOf。Compose大家都知道,torchvision里面有,就是把所有的变换放在一起,按顺序执行。而OneOf就厉害了,他可以选择性的执行包在它里面的变换。比如把都是模糊的变换(高斯模糊,模糊,运动模糊等等)放在OneOf里面,它就可以随机选择一种变换执行。

那接下来具体讲讲这两种

5.1 Compose

重点 :我不知道这个方法怎么用,去哪里搜索呢🤡
教你两招,一去官方APIhttps://albumentations.ai/docs/搜索,如下:
在这里插入图片描述
在这里插入图片描述
这里就可以看函数的作用,以及参数。从参数表里可以看到,compose也有概率参数p的,这是个细节。我们后面的代码里会用到这个参数。

这里组要是组合变换,代码上面已经给过了。

5.2 Oneof

在这里插入图片描述
它同Compose一样,都是做组合的,都有概率。区别就在于:Compose组合下的变换是要挨着顺序做的,而OneOf组合里面的变换是系统自动选择其中一个来做,而这里的概率参数p是指选定后的变换被做的概率。有点拗口,举个栗子。

A.OneOf([
            A.MotionBlur(p=0.2),   # 使用随机大小的内核将运动模糊应用于输入图像。
            A.MedianBlur(blur_limit=3, p=0.1),    # 中值滤波
            A.Blur(blur_limit=3, p=0.1),   # 使用随机大小的内核模糊输入图像。
        ], p=0.2)

这里有3个模糊方法,系统假设选择做A.Blur,那么做A.Blur的概率就是0.2。

5.3 我们再随便看看其他的一些方法。

  • A.resize()
    resize(img, height, width, interpolation=cv2.INTER_LINEAR)
    注意:这个函数如果单独使用需要提供img参数,如果放在Compose里面使用,就不提供img参数的。
import albumentations as A

from PIL import Image
import numpy as np
import matplotlib.pyplot as plt

image = Image.open('610235_7.jpg')
img_arr = np.array(image) # resize只支持array, 不支持jpg
resize = A.resize(img_arr, 256, 256)
transform = A.Compose([
    A.Resize(height=256, width=256, p=1.0),
], p=0.9)

img_trans = transform(image=np.array(image))  # image为图像地址'xxx.jpg'
  • HorizontalFlip 水平翻转
    围绕y轴水平翻转输入,需要实例化后在调用
flip = A.HorizontalFlip(p=1)  # 实例化,翻转概率为1,百分百翻转,测试使用
flipimg = flip(image=resize)
print(flipimg.keys()# dict_keys(['image'])
plt.figure(figsize=(8,8))
plt.subplot(121)
plt.title('resized image')
plt.imshow(resize, cmap='gray')
plt.subplot(122)
plt.title('fliped image')
plt.imshow(flipimg['image'], cmap='gray')

在这里插入图片描述
水平翻转是需要给定参数的,参数可以在编译器里面查看(如,pycharm)
在这里插入图片描述
不同目标,使用不同的参数。因为它会在内部根据参数调用不同的方法,如源码里面
在这里插入图片描述
这里HorizontalFlip是继承自DualTransform。

  • ShiftScaleRotate 随机应用仿射变换:平移,缩放和旋转输入
    在这里插入图片描述
shift = A.ShiftScaleRotate(shift_limit=0.0625, scale_limit=0.2, rotate_limit=45, p=1)
shift_img = shift(image=flipimg['image'])

plt.figure(figsize=(8,8))
plt.subplot(121)
plt.title('fliped image')
plt.imshow(flipimg['image'], cmap='gray')
plt.subplot(122)
plt.title('shift image')
plt.imshow(shift_img['image'], cmap='gray')

在这里插入图片描述

5.4 怎么把这些增强方法和pytorch结合

我们都知道pytorch模型接受的输入是Tensor格式,而albumentations库并没有totensor的方法,那我们怎么才能得到Tensor呢。

更正:albumentations库有totensor的方法, 如ToTensorV2。用ToTensorV2不用ToTensor的原因官方已经说明2
在这里插入图片描述

那如果不使用albumentations的totensor, 或者想和torchvision结合用,应该怎么操作呢?

假设用albumentations做好的transform如下:

trans =  A.Compose([
        A.HorizontalFlip(p=0.5),
        A.VerticalFlip(p=0.5),
        A.OneOf([
            A.IAAAdditiveGaussianNoise(),   # 将高斯噪声添加到输入图像
            A.GaussNoise(),    # 将高斯噪声应用于输入图像。
        ], p=0.2),   # 应用选定变换的概率
        A.OneOf([
            A.MotionBlur(p=0.2),   # 使用随机大小的内核将运动模糊应用于输入图像。
            A.MedianBlur(blur_limit=3, p=0.1),    # 中值滤波
            A.Blur(blur_limit=3, p=0.1),   # 使用随机大小的内核模糊输入图像。
        ], p=0.2),
        A.ShiftScaleRotate(shift_limit=0.0625, scale_limit=0.2, rotate_limit=45, p=0.2),
        # 随机应用仿射变换:平移,缩放和旋转输入
        A.RandomBrightnessContrast(p=0.2),   # 随机明亮对比度
    ])
trans_img = trans(image=img_arr)

plt.figure(figsize=(8,8))
plt.subplot(121)
plt.title('original image')
plt.imshow(img_arr, cmap='gray')
plt.subplot(122)
plt.title('albumentions image')
plt.imshow(trans_img['image'], cmap='gray')

在这里插入图片描述
现在使用torchvision 把做好了transform的数据转化成tensor

from torchvision import transforms as t
as_tensor = t.ToTensor()

img_tensor = as_tensor(trans_img['image'])

在这里插入图片描述
现在还有一个问题,虽然方法我都知道了,但是真正在建立dataset的时候应该怎么写dataset呢。

最后,就给大家看看一个范例。
在这里插入图片描述
这个范例是在做分割的时候写的。只关注transform部分就可以了。

Albumentations数据增强方法大全

本人根据非常棒的Albumentations数据增强库总结了常用的数据增强方法(本人能力有限,如有错误,请指出。有人使用Albumentations库的Blur, Flip, RandomBrightnessContrast, ShiftScaleRotate, ElasticTransform, Transpose, GridDistortion, HueSaturationValue, CLAHE, CoarseDropout在图像分类比赛中取得第二名,所以本人写了这篇文章)。
Albumentations官方手册

在这里插入图片描述

Blur 模糊

Blur(blur_limit = 7,always_apply = False,p = 0.5 )
图像均值平滑滤波。
在这里插入图片描述

VerticalFlip 水平翻转

VerticalFlip(always_apply = False,p = 0.5 )
在这里插入图片描述

HorizontalFlip 垂直翻转

HorizontalFlip(always_apply = False,p = 0.5 )
在这里插入图片描述

Flip 翻转

Flip(always_apply = False,p = 0.5 )
水平和垂直翻转
在这里插入图片描述

Normalize 归一化

Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225), max_pixel_value=255.0, always_apply=False, p=1.0)
将像素值除以255 = 2 ** 8 - 1,减去每个通道的平均值并除以每个通道的std

Transpose 转置

Transpose(always_apply=False, p=0.5)
将图像行和列互换
在这里插入图片描述

RandomCrop 随机裁剪

RandomCrop(height, width, always_apply=False, p=1.0)
随机从图像裁剪一块区域(参数是高宽,而且必须是整数,所以使用这个函数一定会裁剪一定区域的图片)
在这里插入图片描述

RandomGamma 随机Gamma

*RandomGamma(gamma_limit=(80, 120), eps=1e-07, always_apply=False, p=0.5)
随机伽马变换。在这里插入图片描述

RandomRotate90 随机旋转90度

RandomRotate90(always_apply=False, p=0.5)
随机旋转0个或多个90度。
在这里插入图片描述

Rotate旋转

Rotate(limit=90, interpolation=1, border_mode=4, value=None, mask_value=None, always_apply=False, p=0.5)
随机旋转图片(默认使用reflect方法扩充图片,可以改为参数等其他方法填充)。
在这里插入图片描述

ShiftScaleRotate 平移缩放旋转

ShiftScaleRotate(shift_limit=0.0625, scale_limit=0.1, rotate_limit=45, interpolation=1, border_mode=4, value=None, mask_value=None, always_apply=False, p=0.5)
随机平移、缩放、旋转图片。
在这里插入图片描述

CenterCrop 中心裁剪

CenterCrop(height, width, always_apply=False, p=1.0)
随机中心裁剪图片(参数为高宽,一定会进行裁剪,注意其输入为整数)。
在这里插入图片描述

OpticalDistortion 光学畸变

OpticalDistortion(distort_limit=0.05, shift_limit=0.05, interpolation=1, border_mode=4, value=None, mask_value=None, always_apply=False, p=0.5)
对图像进行光学畸变。
在这里插入图片描述

GridDistortion 网格失真

GridDistortion(num_steps=5, distort_limit=0.3, interpolation=1, border_mode=4, value=None, mask_value=None, always_apply=False, p=0.5)
对图像进行网格失真。
在这里插入图片描述

ElasticTransform 弹性变换

ElasticTransform(alpha = 1,sigma = 50,alpha_affine = 50,interpolation = 1,border_mode = 4,value = None,mask_value = None,always_apply = False,approximate = False,p = 0.5 )
随机对图像进行弹性变换。
在这里插入图片描述

RandomGridShuffle 随机网格洗牌

RandomGridShuffle(grid=(3, 3), always_apply=False, p=1.0)
参数:将图像以网格方式生成几块,并随机打乱。
在这里插入图片描述

HueSaturationValue 色调饱和度值

HueSaturationValue(hue_shift_limit=20, sat_shift_limit=30, val_shift_limit=20, always_apply=False, p=0.5)
参数:随机色调、饱和度、值变化。
在这里插入图片描述

PadIfNeeded 填充

PadIfNeeded(min_height=1024, min_width=1024, border_mode=4, value=None, mask_value=None, always_apply=False, p=1.0)
填充图像。
在这里插入图片描述

RGBShift RGB平移

RGBShift(r_shift_limit=20, g_shift_limit=20, b_shift_limit=20, always_apply=False, p=0.5)
参数:随机平移R、G、B通道值。
在这里插入图片描述

RandomBrightness 随机亮度

RandomBrightness(limit=0.2, always_apply=False, p=0.5)y = False,p = 0.5 )
随机亮度变化。
在这里插入图片描述

RandomContrast 随机对比度

RandomContrast(limit=0.2, always_apply=False, p=0.5)
随机对比度变化。
在这里插入图片描述

MotionBlur 运动模糊

MotionBlur(blur_limit=7, always_apply=False, p=0.5)
给图像加上运动模糊。运动模糊是景物图象中的移动效果。它比较明显地出现在长时间暴光或场景内的物体快速移动的情形里。
在这里插入图片描述

MedianBlur 中心模糊

MedianBlur(blur_limit=7, always_apply=False, p=0.5)
图像中值滤波。
在这里插入图片描述

GaussianBlur 高斯模糊

GaussianBlur(blur_limit=7, always_apply=False, p=0.5)
图像高斯平滑滤波。
在这里插入图片描述

GaussNoise 高斯噪声

*GaussNoise(var_limit=(10.0, 50.0), mean=None, always_apply=False, p=0.5)
给图像增加高斯噪声。
在这里插入图片描述

CLAHE 对比度受限自适应直方图均衡

CLAHE(clip_limit=4.0, tile_grid_size=(8, 8), always_apply=False, p=0.5)
将对比度受限的自适应直方图均衡化应用于输入图像。。
在这里插入图片描述

InvertImg 反转图像

InvertImg(always_apply=False, p=0.5)
通过用255减去像素值来反转输入图像。
在这里插入图片描述

ChannelShuffle 通道洗牌

ChannelShuffle(always_apply=False, p=0.5)
随机改变RGB三个通道的顺序。
在这里插入图片描述

Cutout

Cutout(num_holes=8, max_h_size=8, max_w_size=8, fill_value=0, always_apply=False, p=0.5)
在图像中生成正方形区域。
在这里插入图片描述

CoarseDropout

*CoarseDropout(max_holes=8, max_height=8, max_width=8, min_holes=None, min_height=None, min_width=None, fill_value=0, always_apply=False, p=0.5)
在图像上生成矩形区域。
在这里插入图片描述

ToFloat

ToFloat(max_value=None, always_apply=False, p=1.0)
图像除一个值,默认值为图像数据类型的最大值。

Crop 裁剪

Crop(x_min=0, y_min=0, x_max=1024, y_max=1024, always_apply=False, p=1.0)
裁剪图像,其与RandomCrop的区别是可以指定最小值和最大值,而RandomCrop只能指定宽高。
在这里插入图片描述

RandomScale 随机缩放

RandomScale(scale_limit = 0.1,interpolation = 1,always_apply = False,p = 0.5 )
随机缩放图像大小。

LongestMaxSize

LongestMaxSize(max_size = 1024,interpolation = 1,always_apply = False,p = 1 )
缩放图像,使最大边等于max_size,保持初始图像的纵横比。

SmallestMaxSize

SmallestMaxSize(max_size = 1024,interpolation = 1,always_apply = False,p = 1 )
缩放图像,使最小边等于max_size,保持初始图像的纵横比。## VerticalFlip 水平旋转。

Resize缩放

Resize(height,width,interpolation = 1,always_apply = False,p = 1 )
将输入图像调整为给定的高度和宽度。

RandomSizedCrop 随机裁剪缩放

RandomSizedCrop(min_max_height,height,width,w2h_ratio = 1.0,interpolation = 1,always_apply = False,p = 1.0 )
随机裁剪图像并缩放到固定大小。
在这里插入图片描述

RandomBrightnessContrast 随机亮度对比度

RandomBrightnessContrast(brightness_limit = 0.2,contrast_limit = 0.2,brightness_by_max = None,always_apply = False,p = 0.5 )
随机更改输入图像的亮度和对比度。

在这里插入图片描述

RandomCropNearBBox

RandomCropNearBBox(max_part_shift = 0.3,always_apply = False,p = 1.0 )
随机平移bbox的x,y坐标并从图像中裁剪。
在这里插入图片描述

ISONoise

ISONoise(color_shift=(0.01, 0.05), intensity=(0.1, 0.5), always_apply=False, p=0.5)
施加摄像头传感器噪音。
在这里插入图片描述

Solarize

Solarize(threshold=128, always_apply=False, p=0.5)
反转高于阈值的所有像素值。
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小酒馆燃着灯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值