>>> from env_helper import info; info()
页面更新时间: 2023-06-24 11:21:12
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-9-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

1.2. Pillow 操作图像

既然知道了 Pillow 中颜色和坐标的工作方式,就让我们用 Pillow 来处理图像。图17-3中的图像将用于本章中所有 交互式环境的例子。你可以从 http://nostarch.com/automatestuff/ 下载它。

我的猫Zophie。照片上看起来增加了10磅(对猫来说很多)

图 1.1 我的猫Zophie。照片上看起来增加了10磅(对猫来说很多)

我的猫Zophie。照片上看起来增加了10磅(对猫来说很多)

将图像文件 Zophie.png 放在当前工作目录中, 你就可以将 Zophie 的图像加载到 Python 中, 像这样:

>>> from PIL import Image
>>> catIm = Image.open('zophie.png')

要加载图像,就从Pillow导入Image模块, 并调用Image.open(),传入图像的文件名。然后, 可以将加载图像保存在CatIm这样的变量中。 Pillow的模块名称是PIL,这保持与老 模块Python Imaging Library向后兼容,这就是 为什么必须from PIL import Image,而不是 from Pillow import Image。由于Pillow的 创建者设计Pillow模块的方式,你必须使用 from PIL import Image形式的import语句, 而不是简单地import PIL

如果图像文件不在当前工作目录,就调用 os.chdir() 函数,将工作目录变为包含 图像文件的文件夹。

>>> import os
>>> os.chdir('.')

Image.open() 函数的返回值是Image 对象数据类型, 它是 Pillow 将图像表示为为Python 值的方法。 可以调用 Image.open() ,传入文件名字符串,从一个 图像文件(任何格式)加载一个 Image 对象。通过 save() 方法,对Image 对象的所有更改都可以保 存到图像文件中(也是任何格式)。所有的旋转、调整大小、 裁剪、绘画和其他图像操作,都通过这个 Image 对象 上的方法调用来完成。

为了让本章的例子更简短,我假定你已导入了 PillowImage 模块,并将 Zophie 的图像保存在变量 catIm 中。要确保 zophie.png 文件在当前工作目录中,让 Image.open() 函数能找到它。否则,必须在 Image.open() 的 字符串参数中指定完整的绝对路径。

1.2.1. 处理 Image 数据类型

Image 对象有一些有用的属性,提供了加载的图像文件的 基本信息:它的宽度和高度、文件名和图像格式 (如 JPEG 、GIF或PNG)。

例如,在交互式环境中输入以下代码:

>>> from PIL import Image
>>> catIm = Image.open('zophie.png')
>>> catIm.size
(225, 300)
>>> width, height=catIm.size
>>> width
225
>>> height
300
>>> catIm.filename
'zophie.png'
>>> catIm.format
'PNG'
>>> catIm.format_description
'Portable network graphics'
>>> catIm.save('zophie.jpg')

Zophie.png 得到一个 Image 对象并保存在 catIm 中后,我们可以看到该对象的 size 属性 是一个元组,包含该图像的宽度和高度的像素数。我们可以 将元组中的值赋给 widthheight 变量,以便 分别访问宽度和高度。 filename 属性描述了原始文件 的名称。 formatformat_description 属性是 字符串,描述了原始文件的图像格式 (format_description 比较详细)。

最后,调用 save() 方法,传入'zophie.jpg', 将新图像以文件名 zophie.jpg 保存到 硬盘上。 Pillow 看到文件扩展名是 jpg,就自动 使用 JPEG 图像格式来保存图像。 现在硬盘上应该有两个图像, zophie.pngzophie.jpg 。虽然这些文件都基于相同的 图像,但它们不一样,因为格式不同。

Pillow 还提供了Image.new()函数,它返回一个 Image 对象。这很像Image.open() ,不过 Image.new() 返回的对象表示空白的图像。 Image.new() 的参数如下:

  • 字符串’ RGBA’,将颜色模式设置为 RGBA (还有其他模式,但本书没有涉及)。

  • 大小,是两个整数元组,作为新图像的宽度和高度。

  • 图像开始采用的背景颜色,是一个表示 RGBA 值的 四整数元组。你可以用ImageColor.getcolor() 函数的 返回值作为这个参数。另外Image.new() 也支持传入标 准颜色名称的字符串。

例如,在交互式环境中输入以下代码:

>>> from PIL import Image
>>> im = Image.new('RGBA', (100, 200), 'purple')
>>> im.save('purplelmage.png')
>>> im2 = Image.new('RGBA', (20, 20))
>>> im2.save('transparentImage.png')

这里,我们创建了一个 Image 对象,它有100像素宽、200像素高,带有紫色背景。 然后,该图像存入文件 purplelmage.png 中。 我们再次调用 Image.new() ,创建另一个 Image 对象,这次传入(20,20)作为大小,没有指定背景色。 如果未指定颜色参数,默认的颜色是不可见的黑色(0, 0, 0, 0),因此第二个图像具有透明背景, 我们将这个 20×20 的透明正方形存入 transparentlmage.png

_images/transparentImage.png

1.2.2. 裁剪图片

裁剪图像是指在图像内选择一个矩形区域,并删除矩形之外的一切。 Image对象的crop()方法接受一个矩形元组,返回一个 Image对象,表示裁剪后的图像。裁剪不是在原图上发生的, 也就是说,原始的Image对象原封不动,crop()方法返回一 个新的Image对象。请记住,矩形元组(这里就是要裁剪的区域) 包括左列和顶行的像素,直至但不包括右列和底行的像素。

在交互式环境中输入以下代码:

>>> croppedIm = catIm.crop((335, 345, 565, 560))
>>> croppedIm.save('cropped.png')

这得到一个新的Image对象,是剪裁后的图像, 保存在croppedIm中,然后调用croppedImsave(),将裁剪后的图像存入cropped.png。 新文件cropped.png从原始图像创建,如图所示。

新图像只有原始图像剪裁后的部分

图 1.2 新图像只有原始图像剪裁后的部分

新图像只有原始图像剪裁后的部分

1.2.3. 复制和粘贴图像到其他图像

copy() 方法返回一个新的 Image 对象, 它和原来的 Image 对象具有一样的图像。 如果需要修改图像,同时也希望保持原有的版本 不变,这非常有用。例如,在交互式环境中输入 以下代码:

>>> catIm = Image.open('zophie.png')
>>> catCopyIm = catIm.copy()

catImcatCopyIm 变量包含了两个独立的 Image对象,它们的图像相同。既然 catCopyIm中保存 了一个Image对象,你可以随意修改catCopyIm ,将它 存入一个新的文件名,而zophie.png没有改变。例如, 尝试用paste()方法修改catCopyIm

paste()方法在Image对象调用,将另一个图像粘贴 在它上面。继续环境的例子环境的例子,将一个较小的图像 粘贴到catCopyIm()

>>> faceIm = catIm.crop((335, 345, 565, 560))
>>> faceIm.size
(230, 215)
>>> catCopyIm.paste(faceIm, (0, 0))
>>> catCopyIm.paste(faceIm, (400, 500))
>>> catCopyIm.save('pasted.png')

首先我们向crop()传入一个矩形元组,指定zophie.png 中的一个矩形区域,包含Zophie的脸。这将创建一个 Image对象,表示230*215的剪裁区域,保存在faceIm中。 现在,可以将faceIm粘贴到catCopyImImage 对象, 一个包含x和y坐标的元组,指明源 Image 对象粘贴到主 Image 对象时左上角的位置。这里,我们在 catCopyIm 上两次调用 paste() ,第一次传入(0,0), 第二次传入 (400,500)。这将 faceIm 两次粘贴到 catCopyIm: —次faceIm 的左上角在(0,0), —次faceIm 的左上角在 (400,500)。最后,我们将修改后的 catCopyIm存入 pasted.pngpasted.png 如图 17-5 所示。

_images/pasted.png

Zophie 猫,包含两次粘贴她的脸

注意

尽管名称是 copy()paste() ,但Pillow 中的 方法不使用计算机的剪贴板。

请注意, paste() 方法在原图上修改它的 Image 对象, 它不会返回粘贴后图像的Image 对象。如果想调用 paste() ,但还要保持原始图像的未修改版本,就需要先复 制图像,然后在副本上调用 paste()

假定要用 Zophie 的头平铺整个图像,如图17-6所示。 可以用两个 for 循环来实现这个效果。继续交互式 环境的例子,输入以下代码:

>>> catImWidth , catImHeight = catIm.size
>>> faceImWidth, faceImHeight = faceIm.size
>>> catCopyTwo = catIm.copy()
>>> for left in range(0, catImWidth, faceImWidth):
>>>     for top in range(0, catImHeight, faceImHeight):
>>>         print(left,top)
>>>         catCopyTwo.paste(faceIm,(left,top))
0 0
0 215
>>> catCopyTwo.save('tiled.png')

这里,我们将catIm的高度的宽度保存在catIm WidthcatIm Height中。在 O,我们得到了catIm的副本, 并保存在catCopyTwo。既然有了一个副本可以粘贴,我们就 开始循环,将faceIm粘贴到catCopyTwo。 外层 for循环的 left 变量从0开始,增量是faceImWidth (即230) 。内层for循环的top变量从0开始, 增量是faceImHeight (即 215) 。这些嵌套的for循环生成了 lefttop 的值,将faceIm图像按照网格粘贴 到Image对象catCopyTwo,如图17-6所示。为了看到 嵌套循环的工作,我们打印 了 lefttop。粘贴 完成后,我们将修改后的catCopyTwo保存到tiled.png

_images/tiled.png

图17-6嵌套的 for 循环与 paste() ,用于复制猫脸 (可以称之为dupli-cat)

1.2.4. 调整图像大小

resize()方法在 Image 对象上调用,返回指定宽度 和高度的一个新 Image 对象。它接受两个整数的元组作 为参数,表示返回图像的新高度和宽度。在交互式环境中 输入以下代码:

>>> width, height = catIm.size
>>> quartersizedIm = catIm.resize((int(width / 2), int(height / 2)))
>>> quartersizedIm.save('quartersized.png')
>>> svelteIm = catIm.resize((width, height + 300))
>>> svelteIm.save('svelte.png')

这里,我们将catIm.size元组中的两个值付给变量widthheight。使用widthheight,而不是 catIm.size[0]catIm.size[1],让接下来的代码更易读。

第一个 resize() 调用传入 int(width/2) 作为 新宽度, int(height/2) 作为新高度,所以 resize() 返回的 Image 对象具有原始图像的 一半长度和宽度,是原始图像大小的四分之一。 resize() 方法的元组参数中只允许整数,这就是 为什么需要用 into 调用对两个除以2的值取整。

这个大小调整保持了相同比例的宽度和高度。但传入 resize() 的新宽度和高度不必与原始图像成比例。 sveltelm 变量保存了一个 Image 对象, 宽度与原始图像相同,但高度增加了 300像素,让 Zophie 显得更苗条。

请注意, resize() 方法不会在原图上修改 Image 对象,而是返回一个新的 Image 对象。

粘贴透明像素

通常透明像素像白色像素一样粘贴。如果要粘贴图像有透明像素, 就将该图像作为第三个参数传入,这样就不会粘贴一个不透明的 矩形。这个第三参数是“遮罩” Image 对象。遮罩是一个 Image 对象,其中 alpha 值是有效的,但红、绿、 蓝值将被忽略。遮罩告诉 paste() 函数哪些像素应该复制, 哪些应该保持透明。遮罩的高级用法超出了本书的范围,但如果 你想粘贴有透明像素的图像,就再传入 该 Image 对象作为第三个参数。

1.2.5. 旋转和翻转图像

图像可以用 rotate() 方法旋转,该方法返回旋转后的新 Image 对象,并保持原始 Image 对象不变。rotate() 的参数是一个整数或浮点数,表示图像逆时针旋转的度数。 在交互式环境中输入以下代码:

>>> catIm.rotate(90).save('rotated90.png')
>>>
>>> catIm.rotate(180).save('rotated180.png')
>>>
>>> catIm.rotate(270).save('rotated270.png')

注意,可以连续调用方法,对 rotate() 返回的 Image 对象直接调用 save() 。第一个 rotate()save() 调用得到一个逆时针旋转90度的新 Image 对象,并将旋转后的图 像存入 rotated90.png 。第二和第三个调用做的事情一样, 但旋转了180度和270度。 结果如图17-7所示。

_images/imga_7.jpg

图17-7原始图像(左)和逆时针旋转90度、180度和270度的图像

注意,当图像旋转90度或270度时,宽度和高度会变化。 如果旋转其他角度,图像的原始尺寸会保持。在Windows上, 使用黑色的背景来填补旋转造成的缝隙,如图17-8所示。在OS X上, 使用透明的像素来填补缝隙。 rotate() 方法有一个可选的 expand 关键字参数, 如果设置为 True , 就会放大图像的尺寸,以适应整个旋转后的新图像。例如,在交互式环境中 输入以下代码:

>>> catIm.rotate(6).save('rotated6.png')
>>> catIm.rotate(6,expand=True).save('rotated6_expanded.png')

第一次调用将图像旋转6度,并存入rotate.png (参见图17-8的左边的图像)。第二次调用将图像旋转6度, expand 设置为 True ,并存入 rotate6_expanded.png (参见 图17-8的右侧的图像)。

_images/rotated6.png

图17-8图像普通旋转6度(左),

_images/rotated6_expanded.png

以及使用 expand=True (右)

利用 transpose() 方法,还可以得到图像的“镜像翻转”。 必须向 transpose() 方法传入 Image.FLIP_LEFT_RIGHTImage.FLIP_TOP_BOTTOM 。在交互式环境中输入以下代码:

>>> catIm.transpose(Image.FLIP_LEFT_RIGHT).save('horizontal_flip.png')
>>> catIm.transpose(Image.FLIP_TOP_BOTTOM).save('vertical_flip.png')

rotate()一样,transpose()会创建一个新 Image对象。这里我们传入Image.FLIP_LEFT_RIGHT, 让图像水平翻转,然后存入horizontal_flip.png。 要垂直翻转图像,传入Image.FLIP_TOP_BOTTOM, 并存入vertical_flip.png。结果如图 17-9 所示。

水平翻转

图 1.3 水平翻转

垂直翻转

图 1.4 垂直翻转

1.2.6. 更改单个像素

单个像素的颜色可以通过getpixel()putpixel()方法 取得和设置。它们都接受一个元组,表示像素的x和y坐标。 putpixel()方法还接受一个元组,作为该像素的颜色。 这个顔色参数是四整数 RGBA 元组或三整数 RGB 元组。 在交互式环境中输入以下代码:

>>> im = Image.new('RGBA', (100, 100))
>>> im.getpixel((0, 0))
(0, 0, 0, 0)
>>> for x in range(100):
>>>     for y in range(50):
>>>         im.putpixel((x, y), (210, 210, 210))
>>>
>>> from PIL import ImageColor
>>> for x in range(100):
>>>     for y in range(50, 100):
>>>         im.putpixel((x, y), ImageColor.getcolor('darkgray', 'RGBA'))
>>>
>>> im.getpixel((0, 0))
(210, 210, 210, 255)
>>> im.getpixel((0, 50))
(169, 169, 169, 255)
>>> im.save('putPixel.png')

在1中,我们得到一个新图像,这是一个100 ×100的透明正方形。 对一些坐标调用 getPixel() 将返回(0,0,0,0),因为图像 是透明的。要给图像中的像素上色,我们可以使用嵌套的 for 循环,遍历图像上半部分的所有像素,用 putpixel() 设置每个像素的顔色。这里我们向 putpixel() 传入RGB 元组(210, 210,210),即浅灰色。

假定我们希望图像下半部分是暗灰色,但不知道深灰色的 RGB 元组。 putpixel() 方法不接受'darkgray' 这样的标准颜色名称,所以必须使用ImageColor.getcolor() 来获得'darkgray'的颜色元组。循环遍历图像的下半部分 像素,向 putpixel() 传入ImageColor.getcolor() 的 返回值,你现在应该得到一个图像,上半部分是浅灰色,下半部分是 深灰色,如图17-10所示。可以对一些坐标调用 getPixel(), 确认指定像素的颜色符合你的期望。最后, 将图像存入 putPixel.png

putPixel中的图像

图 1.5 putPixel中的图像

当然,在图像上一次绘制一个像素不是很方便。如果需要绘制形状,就使用 ImageDraw 函数。