Python (Pillow) PIL 图片处理:读写/创建/显示、剪切/粘贴/拼接、颜色/几何变换、滤镜(过滤器)、绘图(ImageDraw)

原文链接:https://xiets.blog.csdn.net/article/details/130936331

版权声明:原创文章禁止转载

专栏目录:Python 专栏(总目录)

PIL(Python Imaging Library)是 Python 的图片处理模块。Python3 中没有包含 PIL 模块,Pillow 是 PIL 的分支。

Pillow 提供了相当强大的图像处理能力,几乎支持所有常见图片格式,包括多帧动画图片格式(JPG 、PNG、GIF、WebP、BMP、ICO、TIFF 等等)的编码和解码。

Pillow 支持读取图片、创建图片、显示图片。支持图片剪切、图片粘贴、图片拼接。支持颜色变换,图片滤镜(过滤器)。支持几何变换:缩放图片、旋转图片、转置图片。支持通道拆分,移除 alpha 通道,把 RGBA 图片转换成 RGB 图片。其中 PIL.ImageDraw 支持绘制各种图形:绘制点、线段、矩形、多边形、弧线、弦、扇形、椭圆、图片、文本。总之一个 Pillow 库提供了图像处理所需要的几乎所有功能。

安装 Pillow:

python3 -m pip install Pillow

Pillow 主要的模块和类:PIL.ImagePIL.Image.Image

1. 读写图片

PIL.Image.Image 表示一个图像,可以通过从文件加载图像、处理其他图像或从头开始创建图像实例。

1.1 读取图片

读取图片,创建 PIL.Image.Image 对象的函数:

PIL.Image.open(fp, mode='r', formats=None) -> PIL.Image.Image

"""
打开并识别给定的图片文件。
这是一个惰性操作, 函数标识的文件保持打开状态, 并且在 尝试处理数据 或 调用 load() 方法之前
不会从文件中读取实际图片数据, 但会立即读取前 16 Byte 的元数据。

尝试处理数据时会先调用 load() 方法。

参数:
    fp          文件名(str), pathlib.Path对象 或 文件对象。
                如果是 文件对象, 则对象必须是以二进制模式打开, 并且实现了 file.read(), file.seek() 和 file.tell() 方法。

    mode        打开模式, 必须是固定的 "r"

    formats     list/tuple, 加载文件时尝试使用的解码格式。这可用于检查图片的格式集。
                formats=None 表示尝试所有支持的格式。可运行 PIL.features.pilinfo() 函数打印支持的格式集。
"""

1.1.1 Image.open() 读取图片

PIL.Image.open() 函数读取图片示例:

# 1. 从文件读取
with Image.open("test.png") as im:
    print(im.size, im.mode, im.format)  # (219, 82) RGBA PNG

# 2. 从 pathlib.Path 读取
with Image.open(pathlib.Path("test.png")) as im:
    print(im.size, im.mode, im.format)

# 3. 从二进制打开的文件对象读取
with open("test.png", "rb") as f:
    # f 实现了 read(), seek() 和 tell() 方法
    with Image.open(f) as im:
        print(im.size, im.mode, im.format)

# 4. 从二进制数据读取
bufr = None
with open("test.png", "rb") as f:
    bufr = io.BytesIO(f.read())
    # bufr 实现了 read(), seek() 和 tell() 方法
with Image.open(bufr) as im:
    print(im.size, im.mode, im.format)
bufr.close()

# 5. 从 URL 读取
url = "https://python-pillow.org/images/pillow-logo.png"
with urllib.request.urlopen(url) as resp:
    # resp 实现了 read(), seek() 和 tell() 方法
    with Image.open(resp) as im:
        print(im.size, im.mode, im.format)

# 6. 从 zip 文件中读取
zf = zipfile.ZipFile("test.zip")
bytes_data = zf.read("test.png")
zf.close()
bufr = io.BytesIO(bytes_data)
with Image.open(bufr) as im:
    print(im.size, im.mode, im.format)
bufr.close()

1.1.2 读取图片并显示

读取图片并显示:

with Image.open("test.jpg") as im:
    print(im.mode)                  # 图片的模式, "RGB"
    print(im.format)                # 图片的格式, "JPEG"
    print(im.getexif())             # Exif信息, Image.Exif 类型 (继承自 MutableMapping)
    
    # im.getpixel((x, y))           # 获取某个像素点的像素值, L灰度图返回int, RGB图返回(int, int, int), RGBA图返回(int, int, int, int)
    # im.putpixel((x, y), value)    # 设置某个像素点的像素值 (像素坐标原点在图片左上角)

    im.show()                       # 使用系统默认的图片查看器打开图片 (不阻塞)

Exif 相关信息的获取,参考:PIL.Image.ExifExifTags Module

1.1.3 读取多帧(GIF)图片

读取多帧(GIF)图片:

with Image.open("test.gif") as im:
    print(im.mode)          # P
    print(im.format)        # GIF
    print(im.info)          # {'loop': 0, 'duration': 30, ...}
    print(im.is_animated)   # 是否是动画(多帧图像)
    print(im.n_frames)      # 图像的帧数

    # im.info["loop"]       表示循环次数, 0 表示无限循环
    # im.info["duration"]   表示每一帧的播放时长, 单位为 毫秒

    # 保存 GIF 图片的每一帧
    for frame in range(im.n_frames):
        im.seek(frame)                          # 设置当前帧
        im.save(f"test_{frame:02d}.png")        # 保存当前帧

1.1.4 读取带方向/镜像属性的图片

有些手机或相机拍的照片,带有方向、镜像等属性,这些属性值保存在 Exif 信息中。如果不处理 Exif 属性,直接读取出来的图片方向和在手机电脑上预览的方向可能不一样。可以通过 Image.getexif() 方法获取对应 Exif 属性的值然后手动转换原图数据。手动处理 Exif 信息比较麻烦,PIL 提供了 ImageOps.exif_transpose() 函数处理 Exif 信息转换的问题。

from PIL import Image, ImageOps

def main():
    img_file = "Camera-IMAGE.JPG"       # 一张拍摄时相机旋转了 90° 的照片
    with Image.open(img_file) as img:
        print("origin_size:", img.size)
        img = ImageOps.exif_transpose(img)
        print("transposed_size:", img.size)

if __name__ == "__main__":
    main()

输出:

origin_size: (2560, 1440)
transposed_size: (1440, 2560)

图片在手机和电脑显示的是 竖图(1440x2560),PIL 解析后得到的图片是没有处理方向属性的 横图(1440x2560),然后通过 Exif 转换后得到和手机电脑预览效果一致的图片。

1.2 Image 对象的生命周期

参考:Pillow 中的文件处理

  • PIL.Image.open():如果传的是 文件名 或 Path对象,则作为文件打开。从打开的文件中立即读取元数据,然后该文件保持打开状态以供后续使用。
  • PIL.Image.Image.load():当前需要图片的像素数据时,调用 load() 方法,当前帧将被读入内存,现在可以独立于底层图片文件使用图片数据。
    • Pillow 任何基于现有 Image 对象创建新 Image 对象的方法,内部都将先调用 load() 方法加载原始图片并读取数据, 新 Image 对象实例不会与原 Image 对象(包括底层文件)有任何关联。
    • 打开文件时,如果传递 文件名 或 Path对象 给 PIL.Image.open(),则文件由 Pillow 打开,并被认为由 Pillow 独占使用。如果图片是单帧的,load() 方法会读取完一帧后关闭文件。如果图片是多帧的(如 多页TIFF、GIF动画),则文件保存打开状态,以便 PIL.Image.Image.seek() 可以加载适合的帧。
  • Image.Image.close():关闭文件(img.fp属性变为None),并销毁核心图片对象。如果使用 PIL.Image.Image 对象的上下文管理器,退出后,则只会关闭文件,不会销毁核心图片对象(如果图片帧已加载,则还可以操作图片数据)。例如:
with Image.open("test.jpg") as img:
    img.load()
assert img.fp is None
img.save("test.png")

单帧图片的生命周期相对简单,只需要在调用 load()close() 函数 或 上下文管理器退出前,文件保持打开状态即可。

多帧图片更复杂,调用 load() 方法后,文件还需要保持打开状态。通常,在明确调用 close() 方法之前,Pillow 不知道是否还会有任何额外的数据请求。

1.3 保存图片

1.3.1 保存图片函数

保存图片的函数:

PIL.Image.save(fp, format=None, **params)

"""
将此图像保存在给定的文件名下。如果未指定格式, 则使用的格式由文件扩展名确定 (如果可能)。

可以使用文件对象而不是文件名, 在这种情况下, 必须始终指定格式。
文件对象必须实现 seek()、tell() 和 write() 方法, 并以二进制模式打开。

如果是多帧图片, 则默认只保存由 seek(frame) 设置的当前帧。
如果需要保存多帧, 需要传递图像编码器的额外参数 params。

参数:
    fp          文件名(str)、pathlib.Path对象 或 文件对象

    formats     可选的格式覆盖。如果省略, 则使用的格式由文件扩展名确定。
                如果使用文件对象而不是文件名, 则应始终使用此参数。
                可运行 PIL.features.pilinfo() 函数打印支持的格式集。

    params      图像编码器的额外参数 (不同的格式有不同的参数, 如果不传, 均有默认值)
"""

保存图片格式的编码器参数 **params 参考:Image file formats

1.3.2 保存图片示例

保存图片示例:

# webp -> jpg
with Image.open("test.webp") as im:
    im.save("test.jpg")
    # 可以加上编码器参数, quality 值范围 0(worst) to 95(best), 默认为 75
    # im.save("test.jpg", quality=80)


# 保存到二进制打开的文件
with Image.open("test.webp") as im:
    with open("test.png", "wb") as f:
        im.save(f, format="PNG")


# 先保存到缓冲区, 再保存到文件
buf = io.BytesIO()
with Image.open("test.webp") as im:
    im.save(buf, format="JPEG")
with open("test.jpg", "wb") as f:
    f.write(buf.getvalue())


# PIL 支持的 format 格式可运行下面代码查询
Image.init()
print(Image.SAVE)
print(Image.SAVE_ALL)

添加 Exif 信息,Exif 相关文档参考:PIL.Image.ExifExifTags Module

from PIL import Image, ExifTags

with Image.open("test.jpg") as im:
    exif = im.getexif()     # 获取 Exif 信息
    print(im.info)
    print(type(exif))       # <class 'PIL.Image.Exif'>, Image.Exif 继承自 MutableMapping, 是一个字典

    print(exif[ExifTags.Base.Make])         # 设备制造商
    print(exif[ExifTags.Base.Model])        # 设备型号

    # 修改 Exif 参数
    exif[ExifTags.Base.Make] = "HUAWEI"
    exif[ExifTags.Base.Model] = "LIO-AN00"

    # 保存图片, 指定 Exif 参数
    im.save("demo.jpg", exif=exif)

1.3.3 保存多帧图片(动画)

保存 GIF 动画:

from PIL import Image

# 读取出所有图片
images = []
for img_f in ["test_01.png", "test_02.png", "test_03.png"]:
    with Image.open(img_f) as im:
        im.load()   # 必须要先加载
        images.append(im)

# 以首张图片为基础保存图片: 保存全部帧, 添加剩余的图片, 无限循环, 每帧1000毫秒
images[0].save("test.gif", save_all=True, append_images=images[1:], loop=0, duration=1000)

# 保存的图片尺寸与 images[0] 基础图片相同, 添加的图片尺寸过小不会放大, 尺寸过大会裁剪

保存 WebP 动画:

from PIL import Image

# 读取 GIF 图片, 保存为 WebP 动画
with Image.open("test.gif") as im:
    im.save("test.webp", save_all=True, loop=im.info["loop"], duration=im.info["duration"])

1.4 创建图片

1.4.1 创建图片函数

创建图片 的函数:Constructing images

PIL.Image.new(mode, size, color=0)
"""
创建具有给定模式和大小的新图像, 返回一个 Image 对象

参数:
    mode    新图片的模式, 可取值:
                "1"         1 位像素, 黑白, 每字节存储一个像素
                "L"         8 位像素, 灰度
                "P"         8 位像素, 使用调色板映射到任何其他模式
                "RGB"       3x8 位像素, 真彩色
                "RGBA"      4x8 位像素,带透明蒙版的真彩色
                "CMYK"      4x8 位像素, 分色
                "YCbCr"     3x8 位像素, 彩色视频格式 (注意: 这是指 JPEG, 而不是 ITU-R BT.2020 标准)
                "LAB"       3x8 位像素, L*a*b 颜色空间
                "HSV"       3x8 位像素、色调、饱和度、值颜色空间 (0-255的色调范围 是 0度<=色调<360度 的 缩放版本)
                "I"         32 位有符号整数像素
                "F"         32 位浮点像素

    size    图片大小, 二元组 (width, height)

    color   图片的初始化颜色, 默认为黑色。
            如果是单通道图片, 可以是一个 int/float。如果是多通道图片, 则为 元祖(每个通道一个值) 或 颜色字符串。
            如: 
                mode="L", color=255                 灰度图, 初始颜色为白色
                mode="RGB", color=(255, 0, 0)       RGB彩色图, 初始颜色为红色
                mode="RGBA", color="#FF00007F"      RGBA待透明度的彩色图, 初始颜色为半透明红色

构建图片示例:
    img = Image.new(mode="RGBA", size=(300, 200), color=(0xFF, 0x00, 0x00, 0x7F))
    img.show()
"""


PIL.Image.fromarray(obj, mode=None)
"""
从导出数组接口的对象创建图像内存 (使用缓冲协议), 返回一个 Image 对象

参数:
    obj     多维数组, 一般是 numpy 数组, 每个数组颜色值范围 0~255
            如果是 灰度图, 则数组形状为 (width, height)
            如果是 RGB 图, 则数组形状为 (width, height, 3)
            如果是 RGBA 图, 则数组形状为 (width, height, 4)

    mode    新图片的模式, None 表示自动从 obj 多维数组中识别

ndarray 与 Image 相互转换:
    Image -> ndarray        numpy.asarray(image)
    ndarray -> Image        Image.fromarray(ndarray)
"""


PIL.Image.frombytes(mode, size, data, decoder_name='raw', *args)
"""
从缓冲区中的像素数据创建图像内存的副本, 返回一个 Image 对象。

此函数仅解码像素数据, 而不是整个图像。如果 data 是整个图片文件数据, 请将其包装在一个 BytesIO 对象中, 然后用 Image.open() 加载它。

参数:
    mode            新图片的模式
    size            图片大小
    data            包含给定模式的原始数据的字节缓冲区
    decoder_name    要使用的解码器
    *args           给定解码器的附加参数
"""


PIL.Image.frombuffer(mode, size, data, decoder_name='raw', *args)
"""
在字节缓冲区中创建引用像素数据的图像内存, 返回一个 Image 对象。此函数类似于 frombytes(), 但尽可能使用字节缓冲区中的数据。

参数:
    mode            新图片的模式
    size            图片大小
    data            包含给定模式的原始数据的字节或其他缓冲区对象
    decoder_name    要使用的解码器
    *args           给定解码器的附加参数
"""

1.4.2 Image 与 numpy 数组的转换

import numpy as np
from PIL import Image

# Image对象 转换为 numpy数组
with Image.open("test.jpg") as img:
    # Image 对象转换为 ndarray
    arr = np.asarray(img)
    # 如果是 灰度图, arr 的形状为 (width, height)
    # 如果是 RGB 图, arr 的形状为 (width, height, 3)
    # 如果是 RGBA 图, arr 的形状为 (width, height, 4)

# numpy数组 转换为 Image对象
img = Image.fromarray(arr)
img.show()

1.4.3 彩色图 转 灰度图

构建图片 代码示例:

import numpy as np
from PIL import Image

# Image对象 转换为 numpy数组
with Image.open("test.jpg") as img:
    # Image 对象转换为 ndarray
    arr = np.asarray(img)       # (width, height, 3)

# 计算多个通道的均值, 转换为灰度图
if len(arr.shape) == 3:
    # 沿通道数轴, 求每个像素点的多个通道的均值
    gray_arr = arr.mean(axis=2).astype(dtype=np.uint8)
    print(gray_arr.shape)       # (width, height)
    img = Image.fromarray(gray_arr, mode="L")
    img.show()

# 实际上直接使用 convert() 方法转换即可
with Image.open("test.jpg") as img:
    gray_img = img.convert("L")
    gray_img.show()

彩色原图 与 转换后的灰度图:

pil_scenery_water pil_gray_out

1.5 Image 模块

PIL.Image 模块的函数:PIL.Image

# 打开图片
PIL.Image.open(fp, mode='r', formats=None)  # 打开并识别给定的图像文件。

# 图片处理
PIL.Image.alpha_composite(im1, im2)         # alpha 合成, im2 在 im1 上
PIL.Image.blend(im1, im2, alpha)            # 通过使用常量 alpha 在两个输入图像之间进行插值来创建新图像
PIL.Image.composite(image1, image2, mask)   # 通过使用透明蒙版混合图像来创建合成图像
PIL.Image.eval(image, *args)                # 将函数 (应采用一个参数) 应用于给定图像中的每个像素
PIL.Image.merge(mode, bands)                # 将一组单波段图像合并为一个新的多波段图像

# 构建图片
PIL.Image.new(mode, size, color=0)          # 创建具有给定模式和大小的新图像
PIL.Image.fromarray(obj, mode=None)         # 从导出数组接口的对象创建图像内存 (使用缓冲协议)
PIL.Image.frombytes(mode, size, data, decoder_name='raw', *args)    # 从缓冲区中的像素数据创建图像内存的副本
PIL.Image.frombuffer(mode, size, data, decoder_name='raw', *args)   # 在字节缓冲区中创建引用像素数据的图像内存

# 生成图片
PIL.Image.effect_mandelbrot(size, extent, quality)  # 生成覆盖给定范围的 Mandelbrot 集
PIL.Image.effect_noise(size, sigma)                 # 生成以 128 为中心的高斯噪声
PIL.Image.linear_gradient(mode)                     # 生成从黑到白、从上到下的 256x256 线性渐变
PIL.Image.radial_gradient(mode)                     # 生成从黑到白、中心到边缘的 256x256 径向渐变

# 注册插件 (这些函数供插件作者使用。应用程序作者可以忽略它们)
PIL.Image.register_open(id, factory, accept=None)   # 注册图像文件插件。此函数不应在应用程序代码中使用。
PIL.Image.register_mime(id, mimetype)               # 注册图像 MIME 类型。此函数不应在应用程序代码中使用。
PIL.Image.register_save(id, driver)                 # 注册图像保存功能。此函数不应在应用程序代码中使用。
PIL.Image.register_save_all(id, driver)             # 注册一个图像函数来保存多帧格式的所有帧。此函数不应在应用程序代码中使用。
PIL.Image.register_extension(id, extension)         # 注册图像扩展。此函数不应在应用程序代码中使用。
PIL.Image.register_extensions(id, extensions)       # 注册图像扩展。此函数不应在应用程序代码中使用。
PIL.Image.registered_extensions()                   # 返回包含属于已注册插件的所有文件扩展名的字典
PIL.Image.register_decoder(name, decoder)           # 注册图像解码器。此函数不应在应用程序代码中使用。
PIL.Image.register_encoder(name, encoder)           # 注册图像编码器。此函数不应在应用程序代码中使用。

1.6 Image.Image 类

PIL.Image.Image 类的属性和方法:PIL.Image.Image

Image 相关概念:Concepts - Bands, Modes, Size, Info, Orientation, Filters, …

Image.filename: str             # 源文件的文件名或路径。只有使用文件名或路径传递给 open() 函数打开的图片对象才有值; 如果传的是文件对象, 则文件名属性为 ""。

Image.format: Optional[str]     # 源文件的文件格式
Image.mode                      # 图像模式。指定图像使用的像素格式。可能的值: "1", "L", "P", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"。

Image.size: tuple[int, int]     # 图像大小 (width, height), 以像素为单位。
Image.width: int                # 图像宽度, 以像素为单位。
Image.height: int               # 图像高度, 以像素为单位。

Image.palette: Optional[PIL.ImagePalette.ImagePalette]  # 调色板表 (如果有)
Image.info: dict                # 保存与图像关联数据的字典。
Image.is_animated               # 是否是多帧图片 (并非所有 Image 对象都有此属性)
Image.n_frames                  # 图片的帧数 (并非所有 Image 对象都有此属性)


Image.alpha_composite(im, dest=(0, 0), source=(0, 0))   # alpha 合成, 将图像合成到此图像上
Image.apply_transparency(im1, im2, alpha)               # 应用透明度
Image.convert(mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256)     # 转换图像
Image.copy()                    # 复制图像。如果您希望将内容粘贴到图像中但仍保留原始图像, 请使用此方法。
Image.crop(box=None)            # 裁剪图像
Image.draft(mode, size)         # 配置图像文件加载器
Image.effect_spread(distance)   # 随机分布图像中的像素
Image.entropy(mask=None, extrema=None)              # 计算并返回图像的熵
Image.filter(filter)                                # 使用给定的过滤器过滤此图像
Image.frombytes(data, decoder_name='raw', *args)    # 使用来自字节对象的像素数据加载此图像
Image.getbands()                # 返回一个元组, 其中包含此图像中每个波段的名称。例如: getbands 在 RGB 图像上返回 ("R", "G", "B")
Image.getbbox()                 # 计算图像中非零区域的边界框
Image.getchannel(channel)       # 返回包含源图像的单个通道的图像
Image.getcolors(maxcolors=256)  # 返回此图像中使用的颜色列表
Image.getdata(band=None)        # 将此图像的内容作为包含像素值的序列对象返回 (序列对象被展平)
Image.getexif()                 # 从图像中获取 EXIF 数据
Image.getextrema()              # 获取图像中每个波段的最小和最大像素值
Image.getpalette(rawmode='RGB') # 将图像调色板作为列表返回
Image.getpixel(xy)              # 返回给定位置的像素值
Image.getprojection()           # 获取到 x 和 y 轴的投影
Image.histogram(mask=None, extrema=None)    # 返回图像的直方图。直方图作为像素计数列表返回, 源图像中的每个像素值对应一个像素计数。
Image.paste(im, box=None, mask=None)        # 将另一个图像粘贴到此图像中
Image.point(lut, mode=None)                 # 通过查找表或函数映射此图像
Image.putalpha(alpha)                       # 添加或替换此图像中的 alpha 层
Image.putdata(data, scale=1.0, offset=0.0)  # 将展平序列对象中的像素数据复制到图像中
Image.putpalette(data, rawmode='RGB')       # 将调色板附加到此图像。图片必须是 "P"、"PA"、"L" 或 "LA" 图片。
Image.putpixel(xy, value)                   # 修改给定位置的像素
Image.quantize(colors=256, method=None, kmeans=0, palette=None, dither=Dither.FLOYDSTEINBERG) # 将图像转换为具有指定颜色数的 "P" 模式
Image.reduce(factor, box=None)              # 返回图像减少次数的副本factor
Image.remap_palette(dest_map, source_palette=None)  # 重写图像以重新排序调色板
Image.resize(size, resample=None, box=None, reducing_gap=None)  # 返回此图像的调整大小的副本
Image.rotate(angle, resample=Resampling.NEAREST, expand=0, center=None, translate=None, fillcolor=None) # 返回此图像的旋转副本
Image.save(fp, format=None, **params)       # 将此图像保存在给定的文件名下
Image.seek(frame)                           # 在此序列文件中寻找给定的帧, 后续的获取数据操作将在此帧上操作
Image.show(title=None)                      # 显示此图像 
Image.split()                               # 将此图像拆分为单独的波段
Image.tell()                                # 返回当前帧号, seek() 用于设置帧号
Image.thumbnail(size, resample=Resampling.BICUBIC, reducing_gap=2.0)    # 将此图像制作成缩略图
Image.tobitmap(name='image')                # 返回转换为 X11 位图的图像
Image.tobytes(encoder_name='raw', *args)    # 将图像作为字节对象返回
Image.transform(size, method, data=None, resample=Resampling.NEAREST, fill=1, fillcolor=None)   # 转换此图像
Image.transpose(method)                     # 转置图像 (翻转或旋转 90 度)
Image.verify()  # 验证文件的内容。对于从文件中读取的数据,此方法会尝试确定文件是否损坏,而不实际解码图像数据
Image.load()    # 为图像分配存储空间并加载像素数据。一般情况下不需要调用该方法, 因为Image类会在第一次访问图片像素数据时自动加载打开的图片。
Image.close()   # 关闭文件指针(如果可能), 并销毁核心图片对象

2. 剪切、粘贴 和 拼接图片

剪切、粘贴 和 合并图片,参考:Cutting, pasting, and merging images

Image 对象相关方法:

Image.crop(box=None) -> Image
"""
裁剪图片

参数:
    box     裁剪区域, 四元组(左, 上, 右, 下) 像素区域
"""


Image.paste(im, box=None, mask=None)
"""
粘贴图片, 将另一个图片粘贴到此图片中

参数:
    im      源图片(Image) 或 像素值(int/tuple)

    box     在此图片中的粘贴位置。
            如果是 二元组(x, y) 则为 im 左上角在此图片中的坐标, 默认 box=None 相当于 box=(0, 0)。
            如果是 四元组(左, 上, 右, 下) 则为 im 在此图片中的像素区域 (四元组区域必须与 im 大小匹配)。

            如果图片模式不匹配, 则使用 convert() 将粘贴的图片(im)转换为此图片的模式。

            im 可以是包含像素值的 整数/元祖, 如果是, 则用该像素值填充 box 区域。

    mask    可选的掩码图像
"""


Image.split() -> Tuple[Image, ...]
"""
拆分此图片的所有通道。

例如拆分一个 RGBA 模式的 Image, 将返回 (Image_R, Image_G, Image_B, Image_A) 的副本, 
元组中的每一个 Image 的 mode 都是 "L"。

如果只需要一个通道, 可以使用 Image.getchannel(channel) -> Image 方法, channel 为通道名称。
可以调用 Image.getbands() -> Tuple[str, ...] 获取图片的所有通道名称, 
如 model 为 "L" 的图片返回 ("L",), model 为 "RGBA" 的图片返回 ("R", "G", "B", "A")。
"""

PIL.Image 模块相关方法:

PIL.Image.merge(mode, bands) -> Image
"""
将一组单通道图像合并为一个新的多通道图像

参数:
    mode        用于输出图片的模式
    bands       包含输出图片中每个通道的一个单通道图片的序列, 所有通道的大小必须相同
"""

2.1 图片的裁剪与粘贴

from PIL import Image

img1 = Image.open("test_01.jpg")
img2 = Image.open("test_02.jpg")

# 裁剪 img1 指定区域
region_img = img1.crop(box=(0, 0, 50, 100))

# 粘贴到 img2 的指定位置
img2.paste(region_img, box=(10, 20))

# 在 img2 的指定区域填充 像素值(R, G, B)
img2.paste((255, 0, 0), box=(10, 20, 100, 100))

# 显示图片
img2.show()

img1.close()
img2.close()

2.2 拼接图片

from PIL import Image

img1 = Image.open("test_01.jpg")
img2 = Image.open("test_02.jpg")

# 水平合并两张图片
# 构建一张图片, 宽度是两张图片宽度之和, 高度为两张图片高度较大者
img = Image.new("RGB", size=(img1.width + img2.width, max(img1.height, img2.height)))

# 粘贴第一张图片
img.paste(img1, box=(0, 0))
# 粘贴第二张图片
img.paste(img2, box=(img1.width, 0))

img.save("test.jpg")

img1.close()
img2.close()

2.3 图片通道拆分与合并

移除 alpha 通道,把 RGBA 图片转换成 RGB 图片:

from PIL import Image

rgba_img = Image.open("test.png")

print(rgba_img.getbands())      # 所有通道名称, 输出: ('R', 'G', 'B', 'A')

# 拆分所有通道, 返回各单通道图片组成的元组, 元组每一个元素都是 model 为 "L" 的 Image 对象
r, g, b, a = rgba_img.split()

# 也可以通过 getchannel() 方法获取指定通道名称的 单通道图片
# r = rgba_img.getchannel("R")

# 把 r, g, b 三个单通道图片合并成一个 RGB 图片
rgb_img = Image.merge("RGB", (r, g, b))
rgb_img.save("test.jpg")

rgb_img.close()
rgba_img.close()

3. 几何变换

几何变换,参考:Geometrical transforms

缩放、旋转、转置:

Image.resize(size, resample=None, box=None, reducing_gap=None) -> Image
"""
返回此图片调整大小后的副本(Image), 图片会强制压缩/拉伸到指定大小, 不裁剪。

参数:
    size            以像素为单位的调整大小, 二元组 (width, height)
    resample        一个可选的重采样过滤器。
    box             一个可选的四元组浮点数 (左, 上, 右, 下), 提供要缩放的源图像区域, 默认为整个源图。
    reducing_gap    通过分两步调整图像大小来应用优化
"""


Image.rotate(angle, resample=Resampling.NEAREST, expand=0, center=None, translate=None, fillcolor=None) -> Image
"""
返回此图像旋转后的副本(Image)

参数:
    angle           旋转角度, 逆时针为正, 顺时针为负。
    resample        可选的重采样过滤器。
    expand          扩展标志。如果为 True, 则扩展输出图片以使其足够大以容纳整个旋转图像(内切), 输出图片会变大(原图部分大小不变, 不剪切)。
                    如果为 False 或 省略, 则使输出图片与源图片大小相同, 输出图片大小不变 (旋转后的原图有部分可能被剪切)。
    center          旋转中心二元组, 原点在左上角, 默认是原图片的中心。
    translate       旋转后的平移距离, 一个二元组(x, y)
    fillcolor       旋转图片外部区域的可选颜色。
"""


Image.transpose(method) -> Image
"""
转置图像 (翻转 或 旋转)

method 可取值:
    Transpose.FLIP_LEFT_RIGHT       左右翻转
    Transpose.FLIP_TOP_BOTTOM       上下翻转
    Transpose.ROTATE_90             逆时针旋转90度
    Transpose.ROTATE_180            逆时针旋转180度
    Transpose.ROTATE_270            逆时针旋转270度
    Transpose.TRANSPOSE             转置 (左右翻转, 再逆时针旋转90度)
    Transpose.TRANSVERSE            横向 (上下翻转, 再逆时针旋转90度)
"""

缩放、旋转、转置 代码示例:

from PIL import Image

# 缩小一半, 然后水平翻转, 然后旋转45度并向右平移一段距离
with Image.open("sky_300.webp") as img:
    print(img.mode)     # RGB

    # 缩小一半
    img = img.resize((img.width // 2, img.height // 2))

    # 水平翻转
    img = img.transpose(Image.FLIP_LEFT_RIGHT)

    # 旋转45度, 扩展输出图片大小(内切), 向右平移50像素, 旋转后的空白区域使用灰色填充
    img = img.rotate(45, expand=True, translate=(10, 0), fillcolor="#888888")

    img.save("sky_out.webp")

原图与处理后的图片:

pil_sky_300 pil_sky_out

4. 颜色变换

颜色变换,参考:Color transforms

Image.convert(mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256)
"""
颜色类型转换

参数:
    mode        转换的目标颜色类型, 如: "1", "L", "P", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"
    matrix      转换矩阵
    dither      抖动方法
    palette     从模式 "“RGB”" 转换为 "P" 时使用的调色板
    colors      色板使用的颜色数 Palette.ADAPTIVE, 默认为 256
"""

彩色图转换为灰度图:

from PIL import Image

with Image.open("rgb_img.jpg") as img:
    gray_img = img.convert("L")
    gray_img.save("gray_out.jpg")

5. 图片过滤器

图像增强,参考:Image enhancement

PIL 提供了许多可用于增强图像的方法和模块。如 ImageFilter 过滤器模块,包含了许多可与 filter() 方法一起使用的预定义增强过滤器。

PIL 预定义的一些过滤器实例:

ImageFilter.BLUR                    # 模糊
ImageFilter.CONTOUR                 # 轮廓
ImageFilter.DETAIL                  # 细节
ImageFilter.EDGE_ENHANCE            # 边缘增强
ImageFilter.EDGE_ENHANCE_MORE       # 边缘增强_更多
ImageFilter.EMBOSS                  # 浮雕
ImageFilter.FIND_EDGES              # 寻找边缘
ImageFilter.SHARPEN                 # 锐化
ImageFilter.SMOOTH                  # 光滑的
ImageFilter.SMOOTH_MORE             # 平滑_更多

PIL 预定义的一些过滤器类,需要传递参数实例化,所有的过滤器类都继承自 ImageFilter.Filter

ImageFilter.Color3DLUT(size, table, channels=3, target_mode=None, **kwargs)     # 三维颜色查找表
ImageFilter.BoxBlur(radius)                                                     # 方框模糊
ImageFilter.GaussianBlur(radius=2)                                              # 高斯模糊
ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3)                     # 不锐化蒙版滤镜
ImageFilter.Kernel(size, kernel, scale=None, offset=0)                          # 创建一个卷积核
ImageFilter.RankFilter(size, rank)                                              # 创建排名过滤器
ImageFilter.MedianFilter(size=3)                                                # 创建一个中值过滤器
ImageFilter.MinFilter(size=3)                                                   # 创建一个最小过滤器
ImageFilter.MaxFilter(size=3)                                                   # 创建一个最大过滤器
ImageFilter.ModeFilter(size=3)                                                  # 创建模式过滤器

下面是一张用来测试过滤器的原图:pil_scenery.webp

pil_scenery

应用 浮雕(ImageFilter.EMBOSS) 过滤器:

from PIL import Image, ImageFilter

with Image.open("pil_scenery.webp") as img:
    # 浮雕 过滤器
    emboss_img = img.filter(ImageFilter.EMBOSS)
    emboss_img.save("pil_emboss_out.webp")

pil_scenery_emboss

应用 轮廓(ImageFilter.CONTOUR) 过滤器:

from PIL import Image, ImageFilter

with Image.open("pil_scenery.webp") as img:
    # 轮廓 过滤器
    contour_img = img.filter(ImageFilter.CONTOUR)
    contour_img.save("pil_scenery_contour.webp")

pil_scenery_contour

应用 寻找边缘(ImageFilter.FIND_EDGES) 过滤器:

from PIL import Image, ImageFilter

with Image.open("pil_scenery.webp") as img:
    # 寻找边缘 过滤器
    find_edges_img = img.filter(ImageFilter.FIND_EDGES)
    find_edges_img.save("pil_scenery_find_edges.webp")

pil_scenery_find_edges

应用 边缘增强(ImageFilter.EDGE_ENHANCE) 过滤器:

from PIL import Image, ImageFilter

with Image.open("pil_scenery.webp") as img:
    # 边缘增强 过滤器
    edge_enhance_img = img.filter(ImageFilter.EDGE_ENHANCE)
    edge_enhance_img.save("pil_scenery_edge_enhance.webp")

pil_scenery_edge_enhance

6. 绘图

ImageDraw 模块提供了绘图功能,可以新建或在现有图片上绘制各种图形、文本 等。

绘图简单示例:新建图像,画个矩形和一个文本

from PIL import Image, ImageDraw

# 创建一个图片, 默认使用半透明红色填充
img = Image.new("RGBA", size=(300, 200), color="#FF00007F")

# 创建 ImageDraw, 指定一个图片作为绘制的底层容器, 最终所有绘制的图形都将绘制到该图片
draw = ImageDraw.Draw(img)

# 绘制矩形
draw.rectangle((30, 30, 200, 100), width=2)
# 绘制文本
draw.text((100, 150), "Hello World")

# 显示图片
img.show()
img.close()

结果展示:

my_pil_draw_demo

6.1 概念

坐标

绘制的画板坐标使用与 PIL 本身相同的坐标系,即左上角为原点(0, 0)。在图片边界之外绘制的任何像素都将被丢弃。

颜色

要指定颜色,可以使用 数字、字符串 或 元祖。如果是单通道图片,一般可以只用一个整数表示颜色值。如果是多通道图片,如 “RGBA” 图片,可以使用 "#RGBA" 形式的字符串表示,也可以使用 (R, G, B, A) 形式的元祖表示。

字体

PIL 可以使用 位图字体 或 OpenType/TrueType 字体。位图字体以 PIL 自己的格式存储,其中每种字体通常由两个文件组成,一个名为 .pil,另一个通常名为 .pbm,前者包含字体规格,后者包含光栅数据。

要加载位图字体,需使用 ImageFont 模块中的加载函数。

要加载 OpenType/TrueType 字体,需使用 ImageFont 模块中的 truetype() 函数(此功能依赖于第三方库,可能并非在所有 PIL 构建中都可用)。

6.2 ImageDraw 绘图对象

PIL.ImageDraw 模块中的函数:

PIL.ImageDraw.Draw(im, mode=None) -> PIL.ImageDraw.ImageDraw
"""
创建一个可以在给定图片对象中绘制图形的对象 (画布/画板), 返回 ImageDraw 对象。

参数:
    im      接收绘制结果的图片
    mode    用于颜色值的可选模式。对于 RGB 图像, 此参数可以是 RGB 或 RGBA (将绘图混合到图像中)。
            对于所有其他模式, 此参数必须与图像模式相同。如果省略, 模式默认为图像的模式。
"""

PIL.ImageDraw.ImageDraw 对象属性:

ImageDraw.fill              # bool, 选择是否 ImageDraw.ink 应该用作填充颜色或轮廓颜色
ImageDraw.font              # 当前的默认字体 (可以为每个实例设置不同的默认字体)
ImageDraw.fontmode          # 当前的字体绘制模式, 设置为 "1" 表示禁用抗锯齿, 设置为 "L" 表示启用抗锯齿
ImageDraw.ink               # 当前默认颜色的内部表示


ImageDraw.getfont() -> ImageFont.ImageFont
"""
获取当前默认的字体, 如果没有则会加载默认字体并保存到 ImageDraw.font 属性
"""

6.2.1 点、线段、矩形、多边形

绘制 线段矩形多边形

ImageDraw.point(xy, fill=None)
"""
点, 在给定坐标处绘制点 (单个像素)。
xy 的格式为 [(x0, y0), (x1, y1), ...] 或 [x0, y0, x1, y1, ...]。
fill 表示用于点的颜色。
"""

ImageDraw.line(xy, fill=None, width=0, joint=None)
"""
线段, 在 xy 列表中的坐标之间画一条线, 如果坐标点超过 2 个则绘制一条折线。
参数:
    xy      格式为 [(x0, y0), (x1, y1), ...] 或 [x0, y0, x1, y1, ...]
    fill    用于线条的颜色
    width   线宽, 以像素为单位
    joint   一系列线段之间的连接类型。对于圆边, 它可以是 "curve", 或者 None
"""

ImageDraw.rectangle(xy, fill=None, outline=None, width=1)
"""
矩形, 绘制一个矩形。xy 表示定义边界框的两个点, 格式为 [(x0, y0), (x1, y1)] 或 [x0, y0, x1, y1] (x1 >= x0 and y1 >= y0)
fill 表示填充的颜色, outline 表示边框颜色, width 表示边框宽度。
"""

ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1)
"""
圆角矩形, 绘制一个圆角矩形。与 rectangle() 类似, radius 表示圆角半径。
"""

ImageDraw.polygon(xy, fill=None, outline=None, width=1)
"""
多边形, 绘制多边形。多边形轮廓由给定坐标之间的直线以及最后一个坐标和第一个坐标之间的直线组成。
xy 的格式为 [(x0, y0), (x1, y1), ...] 或 [x0, y0, x1, y1, ...]。
"""

ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None)
"""
圆内切正多边形, 绘制一个正多边形, 内接于 bounding_circle, 具有 n_sides, 并旋转了 rotation 度数。
参数:
    bounding_circle     边界圆, 由点和半径定义的元组, 格式为 (x, y, r) 或 ((x, y), r)
    n_sides             边数 (例如 n_sides=3 三角形、n_sides=6 六边形
    rotation            对多边形应用任意旋转 (例如 rotation=90, 应用 90 度旋转)
    fill                用于填充的颜色
    outline             用于边框的颜色
"""

6.2.2 弧线、弦、扇形、椭圆

绘制 弧线扇形椭圆

ImageDraw.arc(xy, start, end, fill=None, width=0)
"""
弧线, 在给定边界框内的起始角和结束角之间绘制弧线(圆轮廓的一部分)
参数:
    xy      定义边界框的两个点, 格式为 [(x0, y0), (x1, y1)] 或 [x0, y0, x1, y1] (x1 >= x0 and y1 >= y0)
    start   起始角度, 以度为单位。角度从 3 点开始测量, 顺时针增加
    end     结束角度,以度为单位
    fill    圆弧使用的颜色
    width   线宽, 以像素为单位
"""

ImageDraw.chord(xy, start, end, fill=None, outline=None, width=1)
"""
弦, 与 arc() 相同, 但用直线连接端点。fill 表示填充的颜色, outline 表示边框颜色, width 表示边框宽度。
"""

ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=1)
"""
扇形, 与 chord() 圆弧类似, 但也在边界框的端点和中心之间绘制直线
"""

ImageDraw.ellipse(xy, fill=None, outline=None, width=1)
"""
椭圆, 在给定的边界框内绘制一个椭圆。fill 表示填充的颜色, outline 表示边框颜色, width 表示边框宽度。
"""

6.2.3 图片

绘制 图片

ImageDraw.bitmap(xy, bitmap, fill=None)
"""
图片, 在给定位置绘制位图(遮罩), 使用非零部分的当前填充颜色。相当于 image.paste(xy, color, bitmap)。
"""

6.2.4 文本

绘制 文本

ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align='left', direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False)
ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align='left', direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False)
"""
在给定位置绘制字符串, 其中 multiline_text() 表示绘制多行文本, 。

参数:
    xy          文本的锚点坐标
    text        要绘制的字符串, 如果它包含任何换行符, 且调用的是 text(), 则将自动转为调用 multiline_text()
    fill        用于文本的颜色
    font        绘制使用的字体 (包括字体大小也在字体实例中设置), 一个 FreeTypeFont 实例
    anchor      文本锚对齐, 确定锚点与文本的相对位置, 默认对齐方式是左上角。有关有效值, 可参考官方文档。对于非 TrueType 字体, 此参数将被忽略。
    spacing     行间距像素数。文本传递给 multiline_text() 时有效。
    align       对齐方式。文本传递给 multiline_text() 有效, 可取值, "left", "center" 或 "right", 用于确定线的相对对齐方式, 使用 anchor 参数指定对齐到 xy。
    direction   文本的方向。可取值 "rtl"(从右到左)、"ltr"(从左到右) 或 "ttb"(从上到下)
    features    在文本布局期间使用的 OpenType 字体功能列表。
    language    文本的语言。不同的语言可能使用不同的字形形状或连字。
    stroke_width    文字描边的宽度。
    stroke_fill     用于文本描边的颜色。如果没有给出, 将默认为 fill 参数。
    embedded_color  是否使用字体嵌入颜色字形 (COLR、CBDT、SBIX)
"""

ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False)
"""
根据提供的方向、特征和语言的字体呈现时, 返回绘制给定文本所需要的长度 (以像素为单位, 精度为 1/64)。
文本边界框可能会超出某些字体的长度 (例如使用斜体或重音符号时)。
返回: 水平文本的宽度 或 垂直文本的高度
"""

ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align='left', direction=None, features=None, language=None, stroke_width=0, embedded_color=False)
ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align='left', direction=None, features=None, language=None, stroke_width=0, embedded_color=False)
"""
当以具有提供的方向、特征和语言的字体呈现时,返回给定文本相对于给定锚点的边界框(以像素为单位)。仅支持 TrueType 字体。
用于 textlength() 以 1/64 像素精度获取后续文本的偏移量。边界框包括一些字体的额外边距, 例如斜体或重音。
返回: (left, top, right, bottom) 边界框
"""

文本锚点(anchor)取值参考:Text anchors

6.3 字体:ImageFont

ImageFont 模块定义了一个同名的类,此类的实例存储位图字体,并与该 PIL.ImageDraw.ImageDraw.text() 方法一起使用。

ImageFont 加载字体的函数:

PIL.ImageFont.load(filename) -> PIL.ImageFont.ImageFont
"""
加载字体文件。该函数从给定的位图字体文件中加载一个字体对象, 并返回相应的字体对象。
"""

PIL.ImageFont.load_path(filename) -> PIL.ImageFont.ImageFont
"""
加载字体文件。与 load() 相同, 但会沿着 Python 路径搜索位图字体。
"""

PIL.ImageFont.truetype(font=None, size=10, index=0, encoding='', layout_engine=None) -> PIL.ImageFont.ImageFont
"""
从文件或类文件对象加载 TrueType 或 OpenType 字体, 并创建字体对象。

参数:
    font        包含 TrueType 字体的文件名或类似文件的对象。
                font 可以是一个字体文件的完整路径, 也可以是一个字体文件的文件名(可以不包括后缀名)。
                如果在此文件名中找不到该文件, 则加载程序还可能会在系统字体目录中搜索。
                例如: 
                    在 Windows 的 %WINDIR%/fonts 目录下搜索;
                    在 MacOS 的 /Library/Fonts/, /System/Library/Fonts/ 和 ~/Library/Fonts/ 目录下搜索;
                    在 Linux 的 $XDG_DATA_DIRS/fonts 或 /usr/share/fonts 目录下搜索。

    size        请求的大小, 以像素为单位。

    index       要加载的字体 (默认是第一个可用的字体)

    encoding    使用哪种字体编码 (默认为 Unicode)

    layout_engine   使用哪个布局引擎 (如果可用)
"""

PIL.ImageFont.load_default() -> PIL.ImageFont.ImageFont
"""
加载默认字体, 如果在绘制文本时, 没有指定字体, 会使用默认字体。
"""

对于 系统可用的字体文件,可以使用 matplotlib 模块执行下面代码查看:

import matplotlib.font_manager

fonts = matplotlib.font_manager.findSystemFonts()

for font in fonts:
    print(font)     # 字体文件路径 (.ttf, .ttc 等字体文件), 使用时只需要使用字体文件名的名称(可以不包括后缀)

在 Mac/Linux 系统上,可以使用 fc-list :lang=zhfc-list :lang=zh family 命令查看本机 支持中文的字体

也可以使用网上下载的字体文件,一些开源字体:

6.4 绘图代码示例

from PIL import Image, ImageDraw, ImageFont


def main():
    img = Image.new("RGB", size=(550, 500), color="#DDDDDD")

    draw = ImageDraw.Draw(img)

    # 划线: 三点连续, 线条红色, 线宽2
    draw.line([(50, 100), (125, 50), (200, 100)], fill="#5570C7", width=2)

    # 矩形: (左, 上, 右, 下) 坐标点, 不填充颜色, 边框半透明绿色, 线宽2
    draw.rectangle((50, 100, 200, 200), fill=None, outline="#3CA273", width=2)

    # 圆角矩形
    draw.rounded_rectangle((50, 250, 200, 320), radius=10, fill="#FAC859", outline="#9A60B4", width=2)

    # 圆内切正多边形
    draw.regular_polygon((125, 400, 50), n_sides=6, rotation=90, fill="#73C1DF")

    # 弧线
    draw.arc((50, 100, 200, 200), start=0, end=90, fill="#FFA801", width=2)

    # 弦
    draw.chord((300, 50, 500, 250), start=90, end=180, fill="#91CB74")

    # 扇形
    draw.pieslice(((300, 50), (500, 250)), start=270, end=360, fill="#FC8453")

    # 椭圆
    draw.ellipse([(300, 280), (500, 445)], outline="#5570C7", width=2)

    # 加载字体, 字体在本地必须存在
    font = ImageFont.truetype("Times New Roman.ttf", size=25)
    draw.text((190, 20), "xiets.blog.csdn.net", font=font, fill="#5570C7")

    # 加载字体, 字体在本地必须存在
    font = ImageFont.truetype("Songti.ttc", size=30)
    # 绘制包括换行符的文本
    draw.text((368, 320), "Hello\n世界", font=font, fill="#353535", spacing=10, stroke_width=1, stroke_fill="#FF0000")

    img.save("my_pil_draw.webp")
    img.close()


if __name__ == "__main__":
    main()

结果展示:

my_pil_draw

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

谢TS

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

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

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

打赏作者

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

抵扣说明:

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

余额充值