Python-自动化指南-繁琐工作自动化-第三版-十三-
Python 自动化指南(繁琐工作自动化)第三版(十三)
原文:
automatetheboringstuff.com/译者:飞龙
21 制作图表和操作图像

如果你拥有数码相机或从手机上传照片到社交媒体网站,你可能经常遇到数字图像文件。你可能知道如何使用基本的图形软件,如 Microsoft Paint 或 Paintbrush,甚至更高级的应用程序,如 Adobe Photoshop。但是,如果你需要编辑大量图像,手动修改它们可能是一项耗时且无聊的工作。
进入 Pillow,这是一个用于与图像文件交互的第三方 Python 包。此包具有几个函数,使裁剪、调整大小和编辑图像内容变得容易。本章介绍了使用 Pillow 使 Python 能够轻松自动编辑数百或数千张图像。
本章还介绍了 Matplotlib,这是一个用于制作专业外观图表的流行库。Matplotlib 功能丰富,可自定义选项众多,有许多书籍完全致力于它。在这里,我们将介绍使用 Matplotlib 生成图表图像的基础知识。
计算机图像基础
要操作图像,你必须了解如何在 Pillow 中处理颜色和坐标。你可以通过附录 A 中的说明安装 Pillow 的最新版本。
颜色和 RGBA 值
计算机程序通常将图像中的颜色表示为RGBA 值,这是一组指定要包含的红色、绿色、蓝色和alpha(或透明度)数量的数字。这些组件值都是整数,范围从 0(完全没有)到 255(最大值)。这些 RGBA 值属于单个像素,这是计算机屏幕可以显示的单色最小点。像素的 RGB 设置精确地告诉它应该显示什么颜色的阴影。如果屏幕上的图像叠加在背景图像或桌面壁纸之上,alpha 值决定了你可以通过图像的像素“看到”多少背景。
Pillow 使用四个整数的元组来表示 RGBA 值。例如,它用(255, 0, 0, 255)表示红色。这种颜色具有最大量的红色,没有绿色或蓝色,并且具有最大的 alpha 值,意味着它是完全不透明的。Pillow 用(0, 255, 0, 255)表示绿色,用(0, 0, 255, 255)表示蓝色。白色是所有颜色的组合,表示为(255, 255, 255, 255),而黑色,没有任何颜色,表示为(0, 0, 0, 255)。
如果一个颜色的 alpha 值为0,它是不可见的,RGB 值实际上并不重要。毕竟,不可见的红色看起来和不可见的黑色一样。
Pillow 使用与 HTML 相同的标准颜色名称。表 21-1 列出了标准颜色名称及其值。
表 21-1:标准颜色名称及其 RGBA 值
| 名称 | RGBA 值 | 名称 | RGBA 值 | |
|---|---|---|---|---|
| 白色 | (255, 255, 255, 255) |
红色 | (255, 0, 0, 255) |
|
| 绿色 | (0, 255, 0, 255) |
蓝色 | (0, 0, 255, 255) |
|
| 灰色 | (128, 128, 128, 255) |
黄色 | (255, 255, 0, 255) |
|
| 黑色 | (0, 0, 0, 255) |
紫色 | (128, 0, 128, 255) |
Pillow 提供了ImageColor.getcolor()函数,这样你就不必记住你想要使用的颜色的 RGBA 值。此函数将其第一个参数作为颜色名称字符串,第二个参数为字符串'RGBA',并返回一个 RGBA 元组。要查看此函数的工作方式,请在交互式 shell 中输入以下内容:
>>> from PIL import ImageColor # ❶
>>> ImageColor.getcolor('red', 'RGBA') # ❷
(255, 0, 0, 255)
>>> ImageColor.getcolor('RED', 'RGBA') # ❸
(255, 0, 0, 255)
>>> ImageColor.getcolor('Black', 'RGBA')
(0, 0, 0, 255)
>>> ImageColor.getcolor('chocolate', 'RGBA')
(210, 105, 30, 255)
>>> ImageColor.getcolor('CornflowerBlue', 'RGBA')
(100, 149, 237, 255)
首先,从PIL(而不是从 Pillow,因为命名历史超出了本书的范围)导入ImageColor模块❶。传递给ImageColor.getcolor()的颜色名称字符串不区分大小写,因此'red'❷和'RED'❸会给你相同的 RGBA 元组。你也可以传递更不寻常的颜色名称,如'chocolate'和'CornflowerBlue'。
Pillow 支持大量的颜色名称,从'aliceblue'到'yellowgreen'。在交互式 shell 中输入以下内容以查看颜色名称:
>>> from PIL import ImageColor
>>> list(ImageColor.colormap)
['aliceblue', 'antiquewhite', 'aqua', ... 'yellow', 'yellowgreen']
你可以在ImageColor.colormap字典的键中找到超过 100 个标准颜色名称的完整列表。
坐标和矩形元组
图像像素通过 x 和 y 坐标来定位,分别指定像素在图像中的水平和垂直位置。原点是图像左上角的像素,用表示法(0, 0)指定。第一个零代表 x 坐标,它从原点的零开始,从左到右增加。第二个零代表 y 坐标,它从原点的零开始,沿着图像向下增加。这一点需要重复强调:y 坐标向下增加,这与你在数学课上可能记住的 y 坐标的使用方式相反。图 21-1 演示了这种坐标系是如何工作的。

图 21-1:某种古代数据存储设备的 28×27 图像的 x 和 y 坐标
Pillow 的许多函数和方法都接受一个矩形元组参数。这意味着 Pillow 期望一个包含四个整数坐标的元组,这些坐标代表图像中的一个矩形区域。这四个整数按顺序如下:
左 矩形框最左边边缘的 x 坐标。
顶 矩形框顶部边缘的 y 坐标。
右 矩形框最右边边缘右侧一个像素点的 x 坐标。这个整数必须大于左侧整数值。
底 矩形框底部边缘下方一个像素点的 y 坐标。这个整数必须大于顶部整数值。
注意,该盒包括左上角坐标,并延伸到但不包括右下角坐标。例如,盒元组 (3, 1, 9, 6) 表示图 21-2 中黑色框内的所有像素。

图 21-2:由盒元组(3, 1, 9, 6)表示的区域
现在你已经知道了在 Pillow 中颜色和坐标是如何工作的,让我们用 Pillow 来操作一张图片。
使用 Pillow 操作图像
为了练习使用 Pillow,我们将使用图 21-3 中显示的 zophie.png 图像文件。你可以从本书的在线资源中下载它,网址为 nostarch.com/automate-boring-stuff-python-3rd-edition。将文件保存在你的当前工作目录中,然后按照如下方式将图像加载到 Python 中:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> cat_im.show()
从 Pillow 导入 Image 模块,并调用 Image.open(),传递图像的文件名。然后你可以将加载的图像存储在变量 cat_im 中。Pillow Image 对象有一个 show() 方法,可以在窗口中打开图像。这在调试程序并需要识别 Image 对象中的图像时非常有用。

图 21-3:我的猫,Zophie
如果图像文件不在当前工作目录中,可以通过调用 os.chdir() 函数将工作目录更改为包含图像文件的文件夹:
>>> import os
>>> os.chdir('C:\\folder_with_image_file')
Image.open() 函数返回一个 Image 对象数据类型的值,Pillow 使用它来表示图像作为 Python 值。你可以通过传递文件名字符串给 Image.open() 函数来从任何格式的图像文件中加载 Image 对象。你可以使用 save() 方法将你对 Image 对象所做的任何更改保存到图像文件(也是任何格式)中。所有的旋转、调整大小、裁剪、绘制和其他图像操作都将通过在 Image 对象上调用方法来完成。
为了缩短本章的示例,我将假设你已经导入了 Pillow 的 Image 模块,并将 Zophie 图像存储在名为 cat_im 的变量中。确保 zophie.png 文件位于当前工作目录中,这样 Image.open() 函数才能找到它。否则,你必须在函数的字符串参数中指定完整的绝对路径。
使用图像数据类型
一个 Image 对象有几个有用的属性,可以提供从加载的图像文件中获取的基本信息:其宽度和高度、文件名以及图形格式(如 JPEG、WebP、GIF 或 PNG)。例如,在交互式外壳中输入以下内容:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> cat_im.size
(816, 1088) # ❶
>>> width, height = cat_im.size # ❷
>>> width # ❸
816
>>> height # ❹
1088
>>> cat_im.filename
'zophie.png'
>>> cat_im.format
'PNG'
>>> cat_im.format_description
'Portable network graphics'
>>> cat_im.save('zophie.jpg') # ❺
在您从zophie.png创建Image对象并将其存储在cat_im中之后,该对象的size属性包含一个表示图像宽度和高度的像素值的元组 ❶。您可以将元组中的值分配给width和height变量 ❷,以便单独访问宽度 ❸和高度 ❹。filename属性描述了原始文件的名称。format和format_description属性是描述原始文件图像格式的字符串(其中format_description更为详细)。
最后,调用save()方法并传入'zophie.jpg'将一个新的图像文件以文件名zophie.jpg保存到您的硬盘上 ❺。Pillow 会检查文件扩展名为.jpg,并自动使用 JPEG 图像格式保存图像。现在您应该在硬盘上拥有两个图像,zophie.png和zophie.jpg。虽然这些文件基于相同的图像,但它们并不相同,因为它们的格式不同。
Pillow 还提供了Image.new()函数,它返回一个Image对象——类似于Image.open(),但Image.new()表示的对象中的图像将是空白的。Image.new()的参数如下:
-
字符串
'RGBA',将颜色模式设置为 RGBA。(本书未涉及其他模式。) -
新图像的宽度和高度作为两个整数的元组。
-
图像应开始的背景颜色,作为包含 RGBA 值的四个整数的元组。您可以使用
ImageColor.getcolor()函数的返回值作为此参数。或者,您可以将标准颜色名称作为字符串传递。
例如,将以下内容输入到交互式壳中:
>>> from PIL import Image
>>> im = Image.new('RGBA', (100, 200), 'purple') # ❶
>>> im.save('purpleImage.png')
>>> im2 = Image.new('RGBA', (20, 20)) # ❷
>>> im2.save('transparentImage.png')
在这里,我们创建了一个宽度为 100 像素、高度为 200 像素的Image对象,背景为紫色 ❶。然后我们将此图像保存到文件purpleImage.png中。我们再次调用Image.new()来创建另一个Image对象,这次传递(20, 20)作为尺寸,不传递背景颜色 ❷。不可见的黑色(0, 0, 0, 0)是未指定颜色参数时的默认颜色,因此第二个图像具有透明背景。我们将这个 20×20 的透明正方形保存到transparentImage.png中。
裁剪图像
裁剪图像意味着在图像内部选择一个矩形区域并移除矩形外的所有内容。Image对象的crop()方法接受一个矩形元组并返回一个表示裁剪图像的Image对象。裁剪不会在原地发生——也就是说,原始的Image对象保持不变,crop()方法返回一个新的对象。记住,矩形元组(在这种情况下,裁剪部分)包括像素的左侧列和顶部行,但不包括右侧列和底部行。
将以下内容输入到交互式壳中:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> cropped_im = cat_im.crop((335, 345, 565, 560))
>>> cropped_im.save('cropped.png')
此代码为裁剪图像创建一个新的 Image 对象,将对象存储在 cropped_im 中,然后对 cropped_im 调用 save() 以将裁剪图像保存到 cropped.png,如图 21-4 所示。

图 21-4:新图像是原始图像的裁剪部分。
裁剪会从原始图像创建新文件。
在其他图像上粘贴图像
copy() 方法将返回一个包含与被调用 Image 对象相同图像的新 Image 对象。如果你需要修改图像但又想保留原始图像的未修改版本,这很有用。例如,在交互式外壳中输入以下内容:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> cat_copy_im = cat_im.copy()
cat_im 和 cat_copy_im 变量包含两个不同的 Image 对象,它们上面都有相同的图像。现在你已经在 cat_copy_im 中存储了一个 Image 对象,你可以随意修改 cat_copy_im 并将其保存到新的文件名中,而 cat_im 保持不变。
当在 Image 对象上调用时,paste() 方法会在其上方粘贴另一个图像。让我们继续外壳示例,将一个较小的图像粘贴到 cat_copy_im 上:
>>> face_im = cat_im.crop((335, 345, 565, 560))
>>> face_im.size
(230, 215)
>>> cat_copy_im.paste(face_im, (0, 0))
>>> cat_copy_im.paste(face_im, (400, 500))
>>> cat_copy_im.save('pasted.png')
首先,我们将一个矩形区域元组传递给 crop(),该区域在 zophie.png 中包含 Zophie 的脸部。这个方法调用创建了一个代表 230×215 裁剪的 Image 对象,我们将其存储在 face_im 中。现在我们可以将 face_im 粘贴到 cat_copy_im 上。paste() 方法接受两个参数:一个源 Image 对象和一个包含我们想要粘贴源 Image 对象左上角坐标的元组。在这里,我们在 cat_copy_im 上两次调用 paste(),将两个 face_im 的副本粘贴到 cat_copy_im 上。最后,我们将修改后的 cat_copy_im 保存到 pasted.png,如图 21-5 所示。
注意
尽管它们的名称如此,Pillow 中的 copy() 和 paste() 方法并不使用你的计算机剪贴板。
paste() 方法会修改其 Image 对象 原地;它不会返回一个包含粘贴图像的 Image 对象。如果你想调用 paste() 但又想保留原始图像的未修改版本,你需要首先复制图像,然后在该副本上调用 paste()。

图 21-5:Zophie 猫,脸部粘贴了两次
假设你想要将 Zophie 的头部在整个图像上平铺,如图 21-6 所示。

图 21-6:使用 paste() 的嵌套循环可以复制猫的脸(如果你愿意,可以称之为“复制品”)。
你可以用几个 for 循环实现这种效果。通过在交互式外壳中输入以下内容继续示例:
>>> cat_im_width, cat_im_height = cat_im.size
>>> face_im_width, face_im_height = face_im.size
>>> cat_copy_im = cat_im.copy() # ❶
>>> for left in range(0, cat_im_width, face_im_width): # ❷
... for top in range(0, cat_im_height, face_im_height): # ❸
... print(left, top)
... cat_copy_im.paste(face_im, (left, top))
...
0 0
0 215
0 430
0 645
0 860
0 1075
230 0
230 215
# --snip--
690 860
690 1075
>>> cat_copy_im.save('tiled.png')
我们将原始图片的宽度和高度存储在 cat_im_width 和 cat_im_height 中。接下来,我们复制图片并将其存储在 cat_copy_im 中 ❶。现在我们可以循环,将 face_im 粘贴到副本上。外部 for 循环的 left 变量从 0 开始,每次增加 face_im_width ❷。内部 for 循环的 top 变量从 0 开始,每次增加 face_im_height ❸。这些嵌套的 for 循环产生 left 和 top 的值,将 face_im 图片的网格粘贴到 Image 对象上,如图 21-6 所示。为了看到嵌套循环的工作情况,我们打印 left 和 top。粘贴完成后,我们将修改后的 cat_copy_im 保存为 tiled.png。
如果你粘贴带有透明度的图片,你需要将图片作为可选的第三个参数传递,这告诉 Pillow 原始图片的哪些部分需要粘贴。否则,原始图片中的透明像素将在粘贴的图片中显示为白色像素。我们将在第 507 页的“项目 16:添加徽标”中更详细地探讨这种做法。
调整图片大小
当在 Image 对象上调用时,resize() 方法返回一个指定宽度和高度的新的 Image 对象。它接受一个表示新尺寸的两个整数的元组参数。在交互式外壳中输入以下内容:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> width, height = cat_im.size # ❶
>>> quarter_sized_im = cat_im.resize((int(width / 2), int(height / 2))) # ❷
>>> quarter_sized_im.save('quartersized.png')
>>> svelte_im = cat_im.resize((width, height + 300)) # ❸
>>> svelte_im.save('svelte.png')
我们将 cat_im.size 元组中的两个值分配给变量 width 和 height ❶。使用这些变量而不是 cat_im.size[0] 和 cat_im.size[1] 使得其余的代码更易于阅读。
第一次调用 resize() 时,将 int(width / 2) 作为新的宽度,将 int(height / 2) 作为新的高度 ❷,因此 resize() 返回的 Image 对象将是原始图片宽度高度的一半,或者总体上是原始图片大小的四分之一。resize() 方法只接受其元组参数中的整数,这就是为什么你需要将两个除以 2 的操作都包裹在 int() 调用中。
这种调整大小保持了原始图片的比例,但新的宽度和高度值不必保持这些比例。svelte_im 变量包含一个 Image 对象,它具有原始宽度,但高度比原始图片高 300 像素 ❸,使佐菲看起来更加苗条。
注意,resize() 方法不会原地编辑 Image 对象,而是返回一个新的 Image 对象。
旋转和翻转图片
要旋转图片,请使用 rotate() 方法,该方法返回一个新的 Image 对象,而原始图片保持不变。该方法接受一个整数或浮点数,表示逆时针旋转图片的度数。在交互式外壳中输入以下内容:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> cat_im.rotate(90).save('rotated90.png')
>>> cat_im.rotate(180).save('rotated180.png')
>>> cat_im.rotate(270).save('rotated270.png')
注意,你可以通过直接在rotate()返回的Image对象上调用save()来链式调用方法。第一个rotate()和save()链将图像逆时针旋转 90 度并保存到rotated90.png。第二个和第三个调用执行相同的操作,除了它们分别将图像旋转 180 度和 270 度。结果看起来像图 21-7。

图 21-7:原始图像(左)和逆时针旋转 90 度、180 度和 270 度的图像
旋转后的图像将与原始图像具有相同的高度和宽度。在 Windows 上,旋转产生的任何间隙将由黑色背景填充,如图 21-8 所示。在 macOS 和 Linux 上,透明像素将填充这些间隙。
rotate()方法有一个可选的expand关键字参数,可以设置为True以扩大图像的尺寸以适应整个旋转后的新图像。例如,在交互式外壳中输入以下内容:
>>> cat_im.rotate(6).save('rotated6.png')
>>> cat_im.rotate(6, expand=True).save('rotated6_expanded.png')
第一次调用将图像旋转六度并保存为rotated6.png。(见图 21-8 左边的图像。)第二次调用将图像旋转六度,将expand设置为True,并将图像保存为rotated6_expanded.png。(见图 21-8 右边的图像。)

图 21-8:图像以六度正常旋转(左)和expand=True时的旋转(右)
如果你将图像旋转 90 度、180 度或 270 度,并且使用expand=True,旋转后的图像将不会有黑色或透明背景。
你还可以使用transpose()方法进行镜像翻转,如图 21-9 所示。

图 21-9:原始图像(左)、水平翻转的图像(中)和垂直翻转的图像(右)
在交互式外壳中输入以下内容:
>>> cat_im.transpose(Image.FLIP_LEFT_RIGHT).save('horizontal_flip.png')
>>> cat_im.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。
修改单个像素
getpixel()方法可以检索单个像素的颜色,而putpixel()方法还可以更改该颜色。这两种方法都接受一个表示像素 x 和 y 坐标的元组。putpixel()方法还接受一个额外的参数,用于指定像素的新颜色,可以是表示 RGBA 的四整数元组或表示 RGB 的三整数元组。在交互式 shell 中输入以下内容:
>>> from PIL import Image
>>> im = Image.new('RGBA', (100, 100)) # ❶
>>> im.getpixel((0, 0)) # ❷
(0, 0, 0, 0)
>>> for x in range(100): # ❸
... for y in range(50):
... for top in range(0, cat_im_height, face_im_height): # ❸
...
>>> from PIL import ImageColor
>>> for x in range(100): # ❺
... for y in range(50, 100):
... for top in range(0, cat_im_height, face_im_height): # ❸
...
>>> im.getpixel((0, 0))
(210, 210, 210, 255)
>>> im.getpixel((0, 50))
(169, 169, 169, 255)
>>> im.save('putPixel.png')
我们创建一个新的图像,它是一个 100×100 的透明正方形 ❶。在这个图像中的坐标上调用getpixel()会返回(0, 0, 0, 0),因为图像是透明的 ❷。为了给像素上色,我们使用嵌套的for循环遍历图像上半部分的像素 ❸,并将表示浅灰色的 RGB 元组传递给putpixel() ❹。
假设我们想要将图像的下半部分染成深灰色,但不知道深灰色的 RGB 元组。putpixel()方法不接受像'darkgray'这样的标准颜色名称,所以我们使用ImageColor.getcolor()来获取相应的颜色元组 ❻。我们遍历图像下半部分的像素 ❺,并将这个调用的返回值传递给putpixel(),从而生成一个上半部分为浅灰色,下半部分为深灰色的图像,如图 21-10 所示。我们可以对任何坐标调用getpixel()来确认给定像素的颜色是否符合预期。最后,我们将图像保存为putPixel.png*。

图 21-10:putPixel.png 图像
当然,一次只绘制图像的一个像素并不方便。如果你需要绘制形状,请使用“在图像上绘制”部分中解释的ImageDraw函数,该部分位于第 512 页。
项目 16:添加标志
假设你有一个无聊的工作,就是调整数千张图片的大小,并在每张图片的角落添加一个小标志水印。使用像 Paintbrush 或 Paint 这样的基本图形程序来做这件事会花费很长时间。一个更复杂的图形应用程序,如 Photoshop,可以进行批量处理,但该软件的价格高达数百美元。让我们编写一个脚本来完成这项工作。
想象一下图 21-11 是你要添加到每个图像右下角的标志:一个黑色猫图标,带有白色边框和透明背景。你可以使用自己的标志图像或下载本书在线资源中包含的标志。

图 21-11:要添加到图像中的标志
从高层次来看,程序应该执行以下操作:
-
加载标志图像。
-
遍历工作目录中的所有.png和.jpg文件。
-
检查图像是否比 300 像素宽和高。
-
如果是这样,将宽度或高度(取较大的那个)减少到 300 像素,并按比例缩小另一个维度。
-
将标志图像粘贴到角落。
-
将修改后的图像保存到另一个文件夹。
这意味着代码需要执行以下操作:
-
将
catlogo.png*文件作为Image对象打开。 -
遍历
os.listdir('.')返回的字符串。 -
从
size属性获取图像的宽度和高度。 -
计算调整大小后图像的新宽度和高度。
-
调用
resize()方法来调整图像大小。 -
调用
paste()方法将标志粘贴到右下角。 -
调用
save()方法来保存更改,使用原始文件名。
第 1 步:打开标志图像
打开一个新的文件编辑标签,输入以下代码,并将其保存为 resizeAndAddLogo.py:
# Resizes images to fit in a 300x300 square with a logo in the corner
import os
from PIL import Image
SQUARE_FIT_SIZE = 300 # ❶
LOGO_FILENAME = 'catlogo.png' # ❷
logo_im = Image.open(LOGO_FILENAME) # ❸
logo_width, logo_height = logo_im.size # ❹
# TODO: Loop over all files in the working directory.
# TODO: Check if the image needs to be resized.
# TODO: Calculate the new width and height to resize to.
# TODO: Resize the image.
# TODO: Add the logo.
# TODO: Save changes.
通过在程序开始时设置 SQUARE_FIT_SIZE ❶ 和 LOGO_FILENAME ❷ 常量,我们使得以后更改程序变得容易。比如说,你添加的标志不是猫图标,或者你将输出图像的最大尺寸减少到 300 像素以外的其他值。你可以直接打开代码并更改这些值。你也可以通过接受命令行参数来设置这些常量的值。如果没有这些常量,你将不得不在代码中搜索所有 300 和 'catlogo.png' 的实例,并将它们替换为新值。
Image.open() 方法返回标志 Image 对象 ❸。为了可读性,我们将标志的宽度和高度分配给变量 ❹。程序的其他部分是 TODO 注释的框架。
第 2 步:遍历所有文件
现在你需要找到当前工作目录中的所有 .png 文件和 .jpg 文件。你不希望将标志添加到标志图像本身,所以程序应该跳过任何文件名与 LOGO_FILENAME 相同的图像。将以下内容添加到你的代码中:
# Resizes images to fit in a 300x300 square with a logo in the corner
import os
from PIL import Image
# --snip--
os.makedirs('withLogo', exist_ok=True)
# Loop over all files in the working directory.
for filename in os.listdir('.'): # ❶
if not (filename.endswith('.png') or filename.endswith('.jpg')) \ # ❷
or filename == LOGO_FILENAME:
continue # Skip non-image files and the logo file itself. # ❸
im = Image.open(filename) # ❹
width, height = im.size
# --snip--
首先,os.makedirs() 调用会在一个名为 withLogo 的文件夹中创建一个文件夹来存储修改后的图像,而不是覆盖原始图像文件。exist_ok=True 关键字参数将防止 os.makedirs() 在 withLogo 已存在时抛出异常。当代码遍历工作目录中的所有文件 ❶ 时,一个长的 if 语句会检查文件名不以 .png 或 .jpg 结尾 ❷。如果找到任何,或者文件是标志图像本身,循环应该跳过它并使用 continue 跳到下一个文件 ❸。如果 filename 以 '.png' 或 '.jpg' 结尾并且不是标志文件,代码会将其作为 Image 对象打开 ❹ 并保存其宽度和高度。
第 3 步:调整图像大小
程序应该只在宽度或高度大于 SQUARE_FIT_SIZE(在本例中为 300 像素)时调整图像大小,所以你应该将调整大小代码放在一个检查 width 和 height 变量的 if 语句中。将以下代码添加到你的程序中:
# Resizes images to fit in a 300x300 square with a logo in the corner
import os
from PIL import Image
# --snip--
# Check if the image needs to be resized.
if width > SQUARE_FIT_SIZE and height > SQUARE_FIT_SIZE:
# Calculate the new width and height to resize to.
if width > height:
height = int((SQUARE_FIT_SIZE / width) * height) # ❶
width = SQUARE_FIT_SIZE
else:
width = int((SQUARE_FIT_SIZE / height) * width) # ❷
height = SQUARE_FIT_SIZE
# Resize the image.
print(f'Resizing {filename}...')
im = im.resize((width, height)) # ❸
# --snip--
如果需要调整图片大小,你必须确定它是宽图还是高图。如果width的值大于height,代码应该按照与宽度相同的比例减少高度 ❶。这个比例是SQUARE_FIT_SIZE值除以当前宽度,因此代码将新的height值设置为这个比例乘以当前height值。因为除法运算符返回一个浮点值,而resize()方法需要整数维度,你必须记得使用int()函数将结果转换为整数。最后,代码将新的width值设置为SQUARE_FIT_SIZE。
如果height大于或等于width,else子句执行相同的计算,但交换height和width变量 ❷。一旦这些变量包含新的图像尺寸,代码将它们传递给resize()方法,并存储返回的Image对象 ❸。
第 4 步:添加标志并保存更改
无论你是否调整了图片大小,你应该将标志粘贴到其右下角。确切的位置取决于图片和标志的大小。图 21-12 显示了如何计算粘贴位置。粘贴标志的左坐标是图片宽度减去标志宽度,粘贴标志的顶坐标是图片高度减去标志高度。

图 21-12:标志的左上坐标是图片宽度/高度减去标志宽度/高度。
在你的代码将标志粘贴到图片上之后,它应该保存修改后的Image对象。在你的程序中添加以下内容:
# Resizes images to fit in a 300x300 square with a logo in the corner
import os
from PIL import Image
# --snip--
# Check if the image needs to be resized.
# --snip--
# Add the logo.
print(f'Adding logo to {filename}...') # ❶
im.paste(logo_im, (width – logo_width, height – logo_height), logo_im) # ❷
# Save changes.
im.save(os.path.join('withLogo', filename)) # ❸
新的代码打印一条消息,告诉用户正在添加标志 ❶,在计算出的坐标上粘贴logo_im到im上 ❷,并将更改保存到withLogo目录下的文件名中 ❸。当你运行这个程序时,在当前工作目录中的zophie.png和其他图像文件,输出将如下所示:
Resizing zophie.png...
Adding logo to zophie.png...
Resizing zophie_xmas_tree.png...
Adding logo to zophie_xmas_tree.png...
Resizing me_and_zophie.png...
Adding logo to me_and_zophie.png...
程序将zophie.png转换为 225×300 像素的图像,看起来像图 21-13。

图 21-13:程序调整了 zophie.png 的大小并添加了标志(左)。如果你忘记了第三个参数,标志中的透明像素将显示为实心白色像素(右)。
记住,除非你将logo_im作为第三个参数传递,否则paste()方法不会粘贴透明像素。这个程序可以在几分钟内自动调整数百张图片的大小并添加标志。
类似程序的思路
能够批量构建复合图像或修改图片大小对于许多应用来说很有用。你可以编写类似的程序来完成以下任务:
-
在图像上添加文本或网站 URL。
-
在图像上添加时间戳。
-
根据图像大小将图像复制或移动到不同的文件夹中。
-
在图像上添加一个几乎透明的水印以防止他人复制。
从图像中绘制
如果你需要在图像上绘制线条、矩形、圆形或其他简单形状,请使用 Pillow 的 ImageDraw 模块。例如,在交互式外壳中输入以下内容:
>>> from PIL import Image, ImageDraw
>>> im = Image.new('RGBA', (200, 200), 'white')
>>> draw = ImageDraw.Draw(im)
首先,我们导入 Image 和 ImageDraw。然后,我们创建一个 200×200 的白色图像并将其存储在 im 中。我们将此 Image 对象传递给 ImageDraw.Draw() 函数以接收一个 ImageDraw 对象。此对象具有用于绘制形状和文本的几个方法。将新对象存储在变量如 draw 中,以便你可以在以下示例中轻松使用它。
形状
以下 ImageDraw 方法在图像上绘制各种形状。这些方法的 fill 和 outline 参数是可选的,如果未指定,则默认为白色。
点
point(xy, fill) 方法绘制单个像素。xy 参数表示要绘制的点的列表。该列表可以包含 x 和 y 坐标元组,例如 [(x, y), (x, y), ...],或者没有元组的 x 和 y 坐标,例如 [x1, y1, x2, y2, ...]。fill 参数为点着色,可以是 RGBA 元组或字符串,例如 'red'。fill 参数是可选的。“point”名称在这里指的是像素,而不是字体大小单位。
线条
line(xy, fill, width) 方法绘制线条或线条系列。xy 参数可以是元组列表,例如 [(x, y), (x, y), ...],或者整数列表,例如 [x1, y1, x2, y2, ...]。每个点都是你正在绘制的线条上的连接点。可选的 fill 参数指定线条的颜色,作为 RGBA 元组或颜色名称。可选的 width 参数确定线条的宽度,如果未指定,则默认为 1。
矩形
rectangle(xy, fill, outline, width) 方法绘制矩形。xy 参数是形式为 (left, top, right, bottom) 的框元组。左和上值指定矩形左上角的 x 和 y 坐标,而右和下指定右下角的坐标。可选的 fill 参数是矩形的内部颜色。可选的 outline 参数是矩形轮廓的颜色。可选的 width 参数表示线条的宽度,如果未指定,则默认为 1。
椭圆
ellipse(xy, fill, outline, width)方法绘制一个椭圆。如果椭圆的宽度和高度相同,此方法将绘制一个圆。xy 参数是一个表示精确包含椭圆的矩形的元组(左,上,右,下)。可选的 fill 参数是椭圆内部的颜色,可选的 outline 参数是椭圆轮廓的颜色。可选的 width 参数是线的宽度,如果未指定,则默认为1。
Polygons
polygon(xy, fill, outline, width)方法绘制一个任意多边形。xy 参数是一个元组列表,例如[(x, y), (x, y), ...],或者整数,例如[x1, y1, x2, y2, ...],代表多边形边的连接点。最后一对坐标将自动连接到第一对坐标。可选的 fill 参数是多边形内部的颜色,可选的 outline 参数是多边形轮廓的颜色。可选的 width 参数是线的宽度,如果未指定,则默认为1。
A Drawing Example
要练习使用这些方法,请在交互式外壳中输入以下内容:
>>> from PIL import Image, ImageDraw
>>> im = Image.new('RGBA', (200, 200), 'white')
>>> draw = ImageDraw.Draw(im)
>>> draw.line([(0, 0), (199, 0), (199, 199), (0, 199), (0, 0)], fill='black') ❶
>>> draw.rectangle((20, 30, 60, 60), fill='blue') ❷
>>> draw.ellipse((120, 30, 160, 60), fill='red') ❸
>>> draw.polygon(((57, 87), (79, 62), (94, 85), (120, 90), (103, 113)), fill='brown') ❹
>>> for i in range(100, 200, 10): ❺
... draw.line([(i, 0), (200, i - 100)], fill='green')
>>> im.save('drawing.png')
在创建一个 200×200 的白色图像的Image对象后,将其传递给ImageDraw.Draw()以获取ImageDraw对象,并将ImageDraw对象存储在draw中,我们就可以在draw上调用绘图方法。在这里,我们在图像的边缘绘制了一个细的黑色轮廓 ❶,一个左上角在(20, 30),右下角在(60, 60)的蓝色矩形 ❷,一个由(120, 30)到(160, 60)的矩形定义的红椭圆 ❸,一个有五个顶点的棕色多边形 ❹,以及用for循环绘制的绿色线条图案 ❺。生成的drawing.png文件将类似于图 21-14(尽管这本书中没有打印颜色)。

图 21-14:生成的 drawing.png 图像
您可以在ImageDraw对象上使用其他几个形状绘制方法。完整文档可在pillow.readthedocs.io/en/latest/reference/ImageDraw.html找到。
Text
ImageDraw对象还有一个text()方法用于在图像上绘制文本。此方法接受四个参数:
xy 指定文本框左上角的两个整数的元组
text 您想要写入的文本字符串
fill 文本的颜色
font 用于设置文本字体和大小的一个可选的ImageFont对象
在我们使用text()在图像上绘制文本之前,让我们更详细地讨论可选的字体参数。此参数是一个ImageFont对象,您可以通过运行以下代码来获取:
>>> from PIL import ImageFont
一旦你导入了 Pillow 的ImageFont模块,可以通过调用ImageFont.truetype()函数来访问字体,该函数接受两个参数。第一个参数是一个表示字体TrueType文件的字符串,即实际存在于你的硬盘上的字体文件。TrueType 文件具有.ttf文件扩展名,通常位于 Windows 的C:\Windows\Fonts,macOS 的/Library/Fonts和/System/Library/Fonts,以及 Linux 的/usr/share/fonts/truetype。你不需要将这些路径作为 TrueType 文件字符串的一部分输入,因为 Pillow 会自动搜索这些目录,但如果它无法找到你指定的字体,将会显示错误。
ImageFont.truetype()的第二个参数是一个整数,表示以点为单位的字体大小(而不是像素)。Pillow 默认创建每英寸 72 像素的 PNG 图像,而一个点是 1/72 英寸。为了练习,在交互式 shell 中输入以下内容:
>>> from PIL import Image, ImageDraw, ImageFont
>>> import os
>>> im = Image.new('RGBA', (200, 200), 'white') # ❶
>>> draw = ImageDraw.Draw(im) # ❷
>>> draw.text((20, 150), 'Hello', fill='purple') # ❸
>>> arial_font = ImageFont.truetype('arial.ttf', 32) # ❹
>>> draw.text((100, 150), 'Howdy', fill='gray', font=arial_font) # ❺
>>> im.save('text.png')
在导入Image、ImageDraw、ImageFont和os之后,我们创建了一个新的 200×200 白色图像的Image对象❶,并从Image对象创建了一个ImageDraw对象❷。我们使用text()在(20, 150)位置用紫色书写Hello❸。在这个调用中,我们没有传递可选的第四个参数,因此文本的字体和大小没有被定制。
接下来,为了设置字体和大小,我们调用ImageFont.truetype(),传递所需的.ttf字体文件,然后是一个整数字体大小❹。我们将返回的Font对象存储在一个变量中,然后将该变量传递给text()方法的最后一个关键字参数。这个方法调用在(100, 150)位置用灰色绘制了 32 点的 Arial 字体Howdy❺。生成的text.png文件看起来像图 21-15。

图 21-15:生成的 text.png 图像
如果你对使用 Python 创建计算机生成的艺术感兴趣,可以查看 Tristan Bunn(No Starch Press,2021 年)的Learn Python Visually或我的书The Recursive Book of Recursion(No Starch Press,2022 年)。
将图像复制和粘贴到剪贴板
正如第三方pyperclip模块允许你将文本字符串复制和粘贴到剪贴板一样,pyperclipimg模块可以复制和粘贴 Pillow Image对象。要安装pyperclipimg,请参阅附录 A 中的说明。
pyperclipimg.copy()函数接受一个 Pillow Image对象,并将其放置在你的操作系统的剪贴板上。然后你可以将其粘贴到图形或图像处理程序中,例如 MS Paint。pyperclipimg.paste()函数返回剪贴板上的图像内容,作为一个Image对象。在当前工作目录中存在zophie.png时,在交互式 shell 中输入以下内容:
>>> from PIL import Image
>>> im = Image.open('zophie.png')
>>> import pyperclipimg
>>> pyperclipimg.copy(im)
>>> pasted_im = pyperclipimg.paste() # Now copy a new image to the clipboard.
>>> # Paste the clipboard contents to a graphics program.
>>> pasted_im.show() # Shows the image from the clipboard
在此代码中,我们首先将 zophie.png 图像作为 Image 对象打开,然后将其传递给 pyperclipimg.copy() 以将其复制到剪贴板。您可以通过将图像粘贴到图形程序中来验证复制是否成功。接下来,从图形程序中复制一张新图像,或者通过在您的网页浏览器中右键单击图像并复制它。调用 pyperclipimg.paste() 将此图像作为 Image 对象返回到 pasted_im 变量中。您可以通过使用 pasted_im.show() 来查看它以验证粘贴是否成功。
pyperclipimg 模块可以作为让用户将图像数据输入和输出到您的 Python 程序的一种方式。
使用 Matplotlib 创建图表
使用 Pillow 绘制自己的图表是可能的,但需要大量工作。Matplotlib 库为专业出版物创建了许多种类的图表。在本章中,我们将创建基本的折线图、条形图、散点图和饼图,但 Matplotlib 也能创建更复杂的 3D 图表。您可以在 matplotlib.org 找到完整的文档。按照附录 A 中的说明安装 Matplotlib。
折线图和散点图
让我们从创建一个二维折线图开始,该图有两个轴,x 和 y。折线图非常适合显示一个量随时间的变化。在 Matplotlib 中,术语 plot、graph 和 chart 常常可以互换使用,而术语 figure 指的是包含一个或多个图表的窗口。在交互式外壳中输入以下内容:
>>> import matplotlib.pyplot as plt ❶
>>> x_values = [0, 1, 2, 3, 4, 5]
>>> y_values1 = [10, 13, 15, 18, 16, 20]
>>> y_values2 = [9, 11, 18, 16, 17, 19]
>>> plt.plot(x_values, y_values1) ❷
[<matplotlib.lines.Line2D object at 0x000002501D9A7D10>]
>>> plt.plot(x_values, y_values2)
[<matplotlib.lines.Line2D object at 0x00000250212AC6D0>]
>>> plt.savefig('linegraph.png') # Saves the plot as an image file
>>> plt.show() # Opens a window with the plot
>>> plt.show() # Does nothing
我们将 matplotlib.pyplot 导入为 plt ❶ 以便于输入其函数。接下来,为了将数据点绘制到二维图中,我们必须调用 plt.plot() 函数。我们首先在 x_values 中保存一个整数或浮点数的列表用于 x 轴,然后在 y_values1 中保存一个整数或浮点数的列表用于 y 轴 ❷。x 轴和 y 轴列表中的第一个值相互关联,两个列表中的第二个值相互关联,依此类推。在用这些值调用 plt.plot() 后,我们再次用 x_values 和 y_values2 调用它,以在图表中添加第二条线。
Matplotlib 将自动为线条选择颜色并为图表选择合适的尺寸。我们可以通过调用 plt.savefig('linegraph.png') 将默认图表保存为 PNG 图像。
Matplotlib 具有预览功能,可以在窗口中显示图表,类似于 Pillow 有 show() 方法用于预览 Image 对象。调用 plt.show() 以在窗口中打开图表。它将看起来像图 21-16。

图 21-16:使用 plt.show() 显示的折线图
plt.show()创建的窗口是交互式的:你可以移动图表或放大或缩小。左下角的房子图标重置视图,软盘图标允许你将图表保存为图像文件。如果你正在实验数据,plt.show()是一个方便的可视化工具。plt.show()函数调用将阻塞,直到用户关闭此窗口。
当你关闭plt.show()方法创建的窗口时,你也会重置图表数据。再次调用plt.show()要么不起作用,要么显示一个空窗口。你必须再次调用plt.plot()和任何其他与绘图相关的函数来重新创建图表。要保存图表的图像文件,必须在调用plt.show()之前调用plt.savefig()。
要创建相同数据的散点图,将 x 轴和 y 轴的值传递给plt.scatter()函数:
>>> import matplotlib.pyplot as plt
>>> x_values = [0, 1, 2, 3, 4, 5]
>>> y_values1 = [10, 13, 15, 18, 16, 20]
>>> y_values2 = [9, 11, 18, 16, 17, 19]
>>> plt.scatter(x_values, y_values1)
<matplotlib.collections.PathCollection object at 0x00000250212CBAD0>
>>> plt.scatter(x_values, y_values2)
<matplotlib.collections.PathCollection object at 0x000002502132DC10>
>>> plt.savefig('scatterplot.png')
>>> plt.show()
当你调用plt.show()时,Matplotlib 会在图 21-17 中显示该图表。创建散点图的代码与创建折线图的代码相同,只是函数调用不同。

图 21-17:使用 plt.show()显示的散点图
如果你将此图与图 21-16 中的折线图进行比较,你会看到数据是相同的,尽管散点图使用点而不是连接的线条。
条形图和饼图
让我们使用 Matplotlib 创建一个基本的条形图。条形图用于比较不同类别中的相同类型数据。与折线图不同,类别的顺序并不重要,尽管它们通常按字母顺序列出。在交互式 shell 中输入以下内容:
>>> import matplotlib.pyplot as plt
>>> categories = ['Cats', 'Dogs', 'Mice', 'Moose']
>>> values = [100, 200, 300, 400]
>>> plt.bar(categories, values)
<BarContainer object of 4 artists>
>>> plt.savefig('bargraph.png')
>>> plt.show()
这段代码创建了图 21-18 中显示的条形图。我们将 x 轴上的类别列表作为plt.bar()的第一个列表参数传递,并将每个类别的值作为第二个列表参数传递。

图 21-18:使用 plt.show()显示的条形图
记住,关闭plt.show()窗口会重置图表数据。
要创建饼图,调用plt.pie()函数。与类别和值不同,饼图有标签和切片。在交互式 shell 中输入以下内容:
>>> import matplotlib.pyplot as plt
>>> slices = [100, 200, 300, 400] # The size of each slice
>>> labels = ['Cats', 'Dogs', 'Mice', 'Moose'] # The name of each slice
>>> plt.pie(slices, labels=labels, autopct='%.1f%%')
(<matplotlib.patches.Wedge object at 0x00000218F32BA950>,
# --snip--
>>> plt.savefig('piechart.png')
>>> plt.show()
当你为饼图调用plt.show()时,Matplotlib 会在一个窗口中显示它,如图 21-19 所示。plt.pie()函数接受一个切片大小列表和每个切片的标签列表。
autopct参数指定了每个切片的百分比标签的精度。该参数是一个格式说明符字符串;'%.1f%%'字符串指定数字应显示小数点后一位数字。如果你在函数调用中省略此关键字参数,饼图将不会列出百分比文本。
![一个饼图,每个切片都有百分比,标签为“狗”、“猫”、“麋鹿”和“老鼠”。
图 21-19:使用 plt.show()显示的饼图
Matplotlib 会自动为每个切片选择颜色,但你可以自定义这种行为,以及你创建的图表的许多其他方面。
附加组件
我们在上一节中创建的图表相当基础。Matplotlib 拥有大量的附加功能,足以填满一本自己的书,所以我们只看最常见的组件。让我们向我们的图表添加数据点标记、自定义颜色和标签。在交互式外壳中输入以下内容:
>>> import matplotlib.pyplot as plt
>>> x_values = [0, 1, 2, 3, 4, 5]
>>> y_values1 = [10, 13, 15, 18, 16, 20]
>>> y_values2 = [9, 11, 18, 16, 17, 19]
>>> plt.plot(x_values, y_values1, marker='o', color='b', label='Line 1') # ❶
[<matplotlib.lines.Line2D object at 0x000001BC339D2F90>]
>>> plt.plot(x_values, y_values2, marker='s', color='r', label='Line 2')
[<matplotlib.lines.Line2D object at 0x000001BC339D1A90>]
>>> plt.legend() # ❷
<matplotlib.legend.Legend object at 0x000001BC20915B90>
>>> plt.xlabel('X-axis Label') # ❸
Text(0.5, 0, 'X-axis Label')
>>> plt.ylabel('Y-axis Label')
Text(0, 0.5, 'Y-axis Label')
>>> plt.title('Graph Title')
Text(0.5, 1.0, 'Graph Title')
>>> plt.grid(True) # ❹
>>> plt.show()
运行此代码后,Matplotlib 显示一个类似于图 21-20 的窗口。它包含之前创建的相同线图,但我们向plt.plot()函数调用中添加了marker、color和label关键字参数❶。标记为每个数据点在线上创建一个点。'o'值使点成为 O 形圆圈,而's'使其成为正方形。'b'和'r'颜色参数分别将线条设置为蓝色和红色。我们为每条线提供一个标签,以便在调用plt.legend()❷时创建的图例中使用。
我们还通过调用plt.xlabel()、plt.ylabel()和plt.title()❸为 x 轴、y 轴和整个图表本身创建标签,将标签文本作为字符串传递。最后,将True传递给plt.grid()❹启用带有 x 轴和 y 轴值的线条的网格。

图 21-20:带有附加组件的示例线图
这只是 Matplotlib 提供功能的小样本。你可以在在线文档中了解其他功能。
摘要
图像由像素集合组成,每个像素都有一个 RGBA 值用于表示其颜色,以及一组 x 和 y 坐标表示其位置。两种常见的图像格式是 JPEG 和 PNG。Pillow 可以处理这两种图像格式以及其他格式。
当程序将图像加载到Image对象中时,其宽度和高度维度以两个整数的元组形式存储在size属性中。Image数据类型的对象还具有用于常见图像操作的方法:crop()、copy()、paste()、resize()、rotate()和transpose()。要将Image对象保存到图像文件,请调用save()方法。
如果你想让你的程序在图像上绘制形状,请使用ImageDraw方法绘制点、线、矩形、椭圆和多边形。该模块还提供了以你选择的字体和字号绘制文本的方法。
虽然 Pillow 库允许您绘制形状和单个像素,但使用 Matplotlib 库生成图表更容易。您可以使用 Matplotlib 的默认设置创建线图、条形图和饼图,或者您可以进行特定的自定义设置。show() 方法将在您的屏幕上显示图表以供预览,而 save() 方法将生成可以包含在文档或电子表格中的图像文件。库的在线文档可以告诉您更多关于其丰富功能的信息。
尽管像 Photoshop 这样的高级(且昂贵)的应用程序提供了自动批量处理功能,但您可以使用 Python 脚本来免费进行许多相同的修改。在前几章中,您编写了 Python 程序来处理纯文本文件、电子表格、PDF 文件和其他格式。使用 Pillow,您已经扩展了您的编程能力,现在也可以处理图像了!
练习问题
-
RGBA 值是什么?
-
您如何从 Pillow 模块获取
'CornflowerBlue'的 RGBA 值? -
什么是 box 元组?
-
哪个函数可以返回名为 zophie.png 的图像文件的
Image对象? -
如何获取
Image对象图像的宽度和高度? -
您可以调用哪个方法来获取 100×100 图像左下角的
Image对象? -
在对
Image对象进行更改后,您如何将其保存为图像文件? -
哪个模块包含 Pillow 的形状绘图代码?
-
Image对象没有绘图方法。哪种对象有?您如何获取这种对象? -
哪些 Matplotlib 函数可以创建折线图、散点图、条形图和饼图?
-
您如何将 Matplotlib 图表保存为图像?
-
plt.show()函数做什么,为什么不能连续调用两次?
练习程序
为了练习,编写程序来完成以下任务。
拼贴制作器
编写一个程序,从单个图像生成拼贴图像,就像图 21-6 中的猫脸拼贴一样。您的程序应该有一个 make_tile() 函数,该函数有三个参数:一个图像文件名字符串、一个整数表示水平拼接的次数,以及一个整数表示垂直拼接的次数。make_tile() 函数应该返回一个更大的拼贴图像 Image 对象。您将使用 paste() 方法作为此函数的一部分。
例如,如果 zophie_the_cat.jpg 是一个 20×50 像素的图像,调用 make_tile('zophie_the_cat.jpg', 6, 10) 应该返回一个 120×500 像素的图像,总共有 60 个拼贴。作为加分项,尝试在粘贴到较大图像时随机翻转或旋转图像。此拼贴制作器最适合较小的图像进行拼接。看看您可以用此代码创建什么样的抽象艺术。
在硬盘上识别照片文件夹
我有一个坏习惯,就是将文件从数码相机转移到硬盘上某个临时文件夹,然后忘记这些文件夹。写一个能够扫描整个硬盘并找到这些遗留的图片文件夹的程序会很好。
编写一个程序,遍历你的硬盘上的每一个文件夹,并找到潜在的图片文件夹。当然,首先你将需要定义你认为是“图片文件夹”的是什么;让我们假设它是指任何文件中超过一半是照片的文件夹。那么你如何定义哪些文件是照片呢?首先,照片文件必须有文件扩展名 .png 或 .jpg。此外,照片是大型图像;照片文件的宽度和高度都必须大于 500 像素。这是一个安全的赌注,因为大多数数码相机的照片宽度和高度都是几千像素。
作为提示,这里有一个这个程序可能的大致框架:
# Import modules and write comments to describe this program.
for folder_name, subfolders, filenames in os.walk('C:\\'):
num_photo_files = 0
num_non_photo_files = 0
for filename in filenames:
# Check if the file extension isn't .png or .jpg.
if TODO:
num_non_photo_files += 1
continue # Skip to the next filename.
# Open image file using Pillow.
# Check if the width & height are larger than 500.
if TODO:
# Image is large enough to be considered a photo.
num_photo_files += 1
else:
# Image is too small to be a photo.
num_non_photo_files += 1
# If more than half of files were photos,
# print the absolute path of the folder.
if TODO:
print(TODO)
当程序运行时,它应该将任何图片文件夹的绝对路径打印到屏幕上。
创建自定义座位卡
在第十七章的练习程序中,你从 plaintext 文件中的客人名单创建自定义邀请。作为一个额外的项目,使用 Pillow 创建将作为客人自定义座位卡的图像。对于书中在线资源中 guests.txt 文件列出的每位客人,生成一个包含客人姓名和一些花卉装饰的图像文件。书中资源中还有一个公共领域的花卉图像。
为了确保每张座位卡的大小相同,在邀请图片的边缘添加一个黑色矩形;这样,当你打印图片时,你将有一个裁剪的指南。Pillow 生成的 PNG 文件设置为每英寸 72 像素,所以一个 4×5 英寸的卡片将需要一个 288×360 像素的图像。 ### 计算机图像基础
要操作图像,你必须了解如何在 Pillow 中处理颜色和坐标。你可以通过遵循附录 A 中的说明来安装 Pillow 的最新版本。
颜色和 RGBA 值
计算机程序通常将图像中的颜色表示为 RGBA 值,一组数字,指定要包含的红、绿、蓝和 alpha(或透明度)的数量。这些组件值中的每一个都是一个介于 0(完全没有)到 255(最大)之间的整数。这些 RGBA 值属于单个 像素,这是计算机屏幕可以显示的单色最小点。一个像素的 RGB 设置精确地告诉它应该显示什么颜色的阴影。如果一个屏幕上的图像叠加在背景图像或桌面壁纸之上,alpha 值决定了你可以通过图像的像素“看到”多少背景。
Pillow 使用四个整数的元组来表示 RGBA 值。例如,它用(255, 0, 0, 255)表示红色。这种颜色具有最大量的红色,没有绿色或蓝色,并且具有最大的 alpha 值,这意味着它是完全不透明的。Pillow 用(0, 255, 0, 255)表示绿色,用(0, 0, 255, 255)表示蓝色。所有颜色的组合,即白色,是(255, 255, 255, 255),而没有任何颜色的黑色是(0, 0, 0, 255)。
如果一个颜色的 alpha 值为0,它是不可见的,RGB 值实际上并不重要。毕竟,不可见的红色看起来和不可见的黑色一样。
Pillow 使用与 HTML 相同的标准颜色名称。表 21-1 列出了标准颜色名称及其值的选取。
表 21-1:标准颜色名称及其 RGBA 值
| 名称 | RGBA 值 | 名称 | RGBA 值 | |
|---|---|---|---|---|
| 白色 | (255, 255, 255, 255) |
红色 | (255, 0, 0, 255) |
|
| 绿色 | (0, 255, 0, 255) |
蓝色 | (0, 0, 255, 255) |
|
| 灰色 | (128, 128, 128, 255) |
黄色 | (255, 255, 0, 255) |
|
| 黑色 | (0, 0, 0, 255) |
紫色 | (128, 0, 128, 255) |
Pillow 提供了ImageColor.getcolor()函数,这样你就不必记住你想要使用的颜色的 RGBA 值。这个函数将其第一个参数作为颜色名称字符串,第二个参数作为字符串'RGBA',并返回一个 RGBA 元组。要查看这个函数的工作方式,请在交互式 shell 中输入以下内容:
>>> from PIL import ImageColor # ❶
>>> ImageColor.getcolor('red', 'RGBA') # ❷
(255, 0, 0, 255)
>>> ImageColor.getcolor('RED', 'RGBA') # ❸
(255, 0, 0, 255)
>>> ImageColor.getcolor('Black', 'RGBA')
(0, 0, 0, 255)
>>> ImageColor.getcolor('chocolate', 'RGBA')
(210, 105, 30, 255)
>>> ImageColor.getcolor('CornflowerBlue', 'RGBA')
(100, 149, 237, 255)
首先,从PIL(而不是从 Pillow,因为命名历史超出了本书的范围)导入ImageColor模块 ❶。传递给ImageColor.getcolor()的颜色名称字符串不区分大小写,所以'red' ❷和'RED' ❸给你相同的 RGBA 元组。你也可以传递更多不寻常的颜色名称,如'chocolate'和'CornflowerBlue'。
Pillow 支持大量颜色名称,从'aliceblue'到'yellowgreen'。在交互式 shell 中输入以下内容以查看颜色名称:
>>> from PIL import ImageColor
>>> list(ImageColor.colormap)
['aliceblue', 'antiquewhite', 'aqua', ... 'yellow', 'yellowgreen']
你可以在ImageColor.colormap字典的键中找到超过 100 种标准颜色名称的完整列表。
坐标和框元组
图像像素通过 x 和 y 坐标来定位,分别指定像素在图像中的水平和垂直位置。原点是图像左上角的像素,用表示法(0, 0)指定。第一个零代表 x 坐标,它从原点的零开始,从左到右增加。第二个零代表 y 坐标,它从原点的零开始,沿着图像向下增加。这一点需要重复:y 坐标向下增加,这与你在数学课上可能记住的 y 坐标的使用方式相反。图 21-1 展示了这个坐标系是如何工作的。

图 21-1:某种古代数据存储设备的 28×27 图像的 x 和 y 坐标
Pillow 的许多函数和方法都接受一个盒子元组参数。这意味着 Pillow 期望一个包含四个整数坐标的元组,这些坐标代表图像中的一个矩形区域。这四个整数按顺序如下:
左侧 箱子最左侧边缘的 x 坐标。
顶部 箱子顶部边缘的 y 坐标。
正确 箱子右侧边缘右侧一个像素的 x 坐标。这个整数必须大于左侧整数。
底部 箱子底部边缘下方一个像素的 y 坐标。这个整数必须大于顶部整数。
注意,盒子包括左上坐标,并延伸到但不包括右下坐标。例如,盒子元组(3, 1, 9, 6)表示图 21-2 中黑色盒子中的所有像素。

图 21-2:由盒子元组(3, 1, 9, 6)表示的区域
现在你已经知道了在 Pillow 中颜色和坐标是如何工作的,让我们使用 Pillow 来操作图像。
颜色和 RGBA 值
计算机程序通常将图像中的颜色表示为RGBA 值,一组指定要包含的红色、绿色、蓝色和alpha(或透明度)数量的数字。这些组件值是介于 0(完全没有)到 255(最大值)之间的整数。这些 RGBA 值属于单个像素,这是计算机屏幕可以显示的单色最小点。像素的 RGB 设置精确地告诉它应该显示什么颜色的阴影。如果屏幕上的图像叠加在背景图像或桌面壁纸之上,alpha 值决定了你可以通过图像的像素“看到”多少背景。
Pillow 使用四个整数的元组来表示 RGBA 值。例如,它用(255, 0, 0, 255)表示红色。这种颜色具有最大量的红色,没有绿色或蓝色,并且具有最大的 alpha 值,这意味着它是完全不透明的。Pillow 用(0, 255, 0, 255)表示绿色,用(0, 0, 255, 255)表示蓝色。白色,所有颜色的组合,是(255, 255, 255, 255),而黑色,完全没有颜色,是(0, 0, 0, 255)。
如果一个颜色具有0的 alpha 值,它是不可见的,RGB 值实际上并不重要。毕竟,不可见的红色看起来和不可见的黑色一样。
Pillow 使用与 HTML 相同的标准颜色名称。表 21-1 列出了标准颜色名称及其值的选择。
表 21-1:标准颜色名称及其 RGBA 值
| 名称 | RGBA 值 | 名称 | RGBA 值 | |
|---|---|---|---|---|
| 白色 | (255, 255, 255, 255) |
红色 | (255, 0, 0, 255) |
|
| 绿色 | (0, 255, 0, 255) |
蓝色 | (0, 0, 255, 255) |
|
| 灰色 | (128, 128, 128, 255) |
黄色 | (255, 255, 0, 255) |
|
| 黑色 | (0, 0, 0, 255) |
紫色 | (128, 0, 128, 255) |
Pillow 提供了ImageColor.getcolor()函数,这样你就不必记住你想要使用的颜色的 RGBA 值。此函数将其第一个参数作为颜色名称字符串,第二个参数作为字符串'RGBA',并返回一个 RGBA 元组。要查看此函数的工作方式,请在交互式 shell 中输入以下内容:
>>> from PIL import ImageColor # ❶
>>> ImageColor.getcolor('red', 'RGBA') # ❷
(255, 0, 0, 255)
>>> ImageColor.getcolor('RED', 'RGBA') # ❸
(255, 0, 0, 255)
>>> ImageColor.getcolor('Black', 'RGBA')
(0, 0, 0, 255)
>>> ImageColor.getcolor('chocolate', 'RGBA')
(210, 105, 30, 255)
>>> ImageColor.getcolor('CornflowerBlue', 'RGBA')
(100, 149, 237, 255)
首先,从PIL(不是从 Pillow,因为命名历史超出了本书的范围)导入ImageColor模块❶。传递给ImageColor.getcolor()的颜色名称字符串不区分大小写,所以'red'❷和'RED'❸都会给你相同的 RGBA 元组。你也可以传递更不寻常的颜色名称,如'chocolate'和'CornflowerBlue'。
Pillow 支持从'aliceblue'到'yellowgreen'的无数颜色名称。在交互式 shell 中输入以下内容以查看颜色名称:
>>> from PIL import ImageColor
>>> list(ImageColor.colormap)
['aliceblue', 'antiquewhite', 'aqua', ... 'yellow', 'yellowgreen']
你可以在ImageColor.colormap字典的键中找到超过 100 个标准颜色名称的完整列表。
坐标和盒子元组
图像像素通过 x 和 y 坐标来定位,分别指定像素在图像中的水平和垂直位置。原点是图像左上角的像素,用表示法(0, 0)指定。第一个零代表 x 坐标,它从原点的零开始,向右增加。第二个零代表 y 坐标,它从原点的零开始,向下增加。这一点需要重复强调:y 坐标向下增加,这与你在数学课上可能记住的 y 坐标使用方式相反。图 21-1 展示了这个坐标系是如何工作的。

图 21-1:某种古代数据存储设备的 28×27 图像的 x 和 y 坐标
Pillow 的许多函数和方法都接受一个盒子元组参数。这意味着 Pillow 期望一个包含四个整数坐标的元组,这些坐标代表图像中的一个矩形区域。这四个整数按顺序如下:
左 箱子最左边边缘的 x 坐标。
顶 箱子顶部边缘的 y 坐标。
Right 框最右边一像素的 x 坐标。这个整数必须大于左边的整数。
Bottom 框底边下一像素的 y 坐标。这个整数必须大于顶部的整数。
注意,该框包括左上和左下坐标,但不包括右上和右下坐标。例如,元组 (3, 1, 9, 6) 表示图 21-2 中黑色框内的所有像素。

图 21-2:由元组(3, 1, 9, 6)表示的区域
现在你已经知道了在 Pillow 中颜色和坐标是如何工作的,让我们使用 Pillow 来操作一个图像。
使用 Pillow 操作图像
为了练习使用 Pillow,我们将使用图 21-3 中显示的 zophie.png 图像文件。你可以从本书的在线资源中下载它,网址为 nostarch.com/automate-boring-stuff-python-3rd-edition。将文件保存在你的当前工作目录中,然后按照如下方式将图像加载到 Python 中:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> cat_im.show()
从 Pillow 中导入 Image 模块并调用 Image.open(),传递图像的文件名。然后你可以将加载的图像存储在一个变量中,比如 cat_im。Pillow 的 Image 对象有一个 show() 方法,可以在窗口中打开图像。这在调试你的程序并需要识别 Image 对象中的图像时非常有用。

图 21-3:我的猫,Zophie
如果图像文件不在当前工作目录中,可以通过调用 os.chdir() 函数将工作目录更改为包含图像文件的文件夹:
>>> import os
>>> os.chdir('C:\\folder_with_image_file')
Image.open() 函数返回一个 Image 对象的数据类型,这是 Pillow 用于将图像表示为 Python 值的方式。你可以通过传递文件名字符串给 Image.open() 函数来从任何格式的图像文件中加载一个 Image 对象。你可以通过 save() 方法将你对 Image 对象所做的任何更改保存到图像文件(也是任何格式)中。所有的旋转、调整大小、裁剪、绘制和其他图像操作都将通过在这个 Image 对象上的方法调用进行。
为了缩短本章的示例,我将假设你已经导入了 Pillow 的 Image 模块并将 Zophie 图像存储在一个名为 cat_im 的变量中。确保 zophie.png 文件位于当前工作目录中,这样 Image.open() 函数就可以找到它。否则,你必须在函数的字符串参数中指定完整的绝对路径。
使用图像数据类型
Image对象有几个有用的属性,可以为您提供从其中加载的图像文件的基本信息:其宽度和高度、文件名以及图形格式(如 JPEG、WebP、GIF 或 PNG)。例如,在交互式外壳中输入以下内容:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> cat_im.size
(816, 1088) # ❶
>>> width, height = cat_im.size # ❷
>>> width # ❸
816
>>> height # ❹
1088
>>> cat_im.filename
'zophie.png'
>>> cat_im.format
'PNG'
>>> cat_im.format_description
'Portable network graphics'
>>> cat_im.save('zophie.jpg') # ❺
在您从zophie.png创建Image对象并将其存储在cat_im中之后,该对象的size属性包含一个元组,其中包含图像的宽度和高度(以像素为单位)❶。您可以将元组中的值分配给width和height变量❷,以便单独访问宽度❸和高度❹。filename属性描述了原始文件的名称。format和format_description属性是描述原始文件图像格式的字符串(其中format_description更为详细)。
最后,调用save()方法并传入'zophie.jpg'将一个新的图像文件以zophie.jpg为文件名保存到您的硬盘上 ❺。Pillow 会检查文件扩展名为.jpg,并自动使用 JPEG 图像格式保存图像。现在您应该在硬盘上拥有两个图像,zophie.png和zophie.jpg。虽然这些文件基于同一图像,但它们并不相同,因为它们的格式不同。
Pillow 还提供了Image.new()函数,该函数返回一个Image对象——类似于Image.open(),但Image.new()表示的对象中的图像将是空白的。Image.new()的参数如下:
-
字符串
'RGBA',将颜色模式设置为 RGBA。(本书未涉及其他模式。) -
新图像的宽度和高度作为两个整数的元组。
-
图像应开始的背景颜色,作为包含 RGBA 值的四个整数的元组。您可以使用
ImageColor.getcolor()函数的返回值作为此参数。或者,您也可以传递标准颜色名称作为字符串。
例如,在交互式外壳中输入以下内容:
>>> from PIL import Image
>>> im = Image.new('RGBA', (100, 200), 'purple') # ❶
>>> im.save('purpleImage.png')
>>> im2 = Image.new('RGBA', (20, 20)) # ❷
>>> im2.save('transparentImage.png')
在这里,我们创建了一个宽度为 100 像素、高度为 200 像素的图像Image对象,背景为紫色❶。然后我们将此图像保存到文件purpleImage.png中。我们再次调用Image.new()以创建另一个Image对象,这次传递(20, 20)作为尺寸,并传递空值作为背景颜色❷。如果未指定颜色参数,则默认使用不可见的黑色(0, 0, 0, 0),因此第二个图像具有透明背景。我们将这个 20×20 的透明正方形保存到transparentImage.png中。
裁剪图像
裁剪图像意味着在图像内部选择一个矩形区域并删除矩形外的所有内容。Image对象的crop()方法接受一个框元组并返回一个代表裁剪图像的Image对象。裁剪不是原地发生的——也就是说,原始Image对象保持未修改,crop()方法返回一个新的对象。记住,框元组(在这种情况下,裁剪部分)包括像素的左侧列和顶部行,但不包括右侧列和底部行。
将以下内容输入到交互式壳中:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> cropped_im = cat_im.crop((335, 345, 565, 560))
>>> cropped_im.save('cropped.png')
此代码为裁剪图像创建一个新的Image对象,将对象存储在cropped_im中,然后对cropped_im调用save()以将裁剪图像保存到cropped.png,如图 21-4 所示。

图 21-4:新图像是原始图像的裁剪部分。
裁剪是从原始图像中创建新文件。
在其他图像上粘贴图像
copy()方法将返回一个包含与被调用Image对象相同图像的新Image对象。如果你需要修改图像但还想保留原始图像的未修改版本,这很有用。例如,将以下内容输入到交互式壳中:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> cat_copy_im = cat_im.copy()
cat_im和cat_copy_im变量包含两个独立的Image对象,它们上面都有相同的图像。现在你已经在cat_copy_im中存储了一个Image对象,你可以按你喜欢的方式修改cat_copy_im并将其保存到新的文件名中,而cat_im保持不变。
当在Image对象上调用时,paste()方法将另一个图像粘贴到其上方。让我们通过将一个较小的图像粘贴到cat_copy_im上来继续 shell 示例:
>>> face_im = cat_im.crop((335, 345, 565, 560))
>>> face_im.size
(230, 215)
>>> cat_copy_im.paste(face_im, (0, 0))
>>> cat_copy_im.paste(face_im, (400, 500))
>>> cat_copy_im.save('pasted.png')
首先,我们传递一个矩形区域框元组给crop()函数,该区域在zophie.png中包含 Zophie 的脸。这个方法调用创建了一个代表 230×215 裁剪的Image对象,我们将其存储在face_im中。现在我们可以将face_im粘贴到cat_copy_im上。paste()方法接受两个参数:一个源Image对象和一个元组,表示我们想要将源Image对象的左上角粘贴到主Image对象上的 x 和 y 坐标。在这里,我们在cat_copy_im上调用paste()两次,将两个face_im的副本粘贴到cat_copy_im上。最后,我们将修改后的cat_copy_im保存到pasted.png,如图 21-5 所示。
注意
尽管它们的名称如此,Pillow 中的 copy()和 paste()方法并不使用你的计算机剪贴板。
paste()方法在原地修改其Image对象;它不返回一个带有粘贴图像的Image对象。如果你想调用paste()但还想保留原始图像的未修改版本,你需要首先复制图像,然后在该副本上调用paste()。

图 21-5:猫佐菲,她的脸部粘贴了两次
假设你想要将佐菲的头像铺满整个图像,如图 21-6 所示。

图 21-6:使用 paste() 方法时嵌套的循环可以复制猫的脸(如果你愿意,可以称之为“双猫”)。
你可以通过几个 for 循环实现这种效果。通过在交互式外壳中输入以下内容继续交互式外壳示例:
>>> cat_im_width, cat_im_height = cat_im.size
>>> face_im_width, face_im_height = face_im.size
>>> cat_copy_im = cat_im.copy() # ❶
>>> for left in range(0, cat_im_width, face_im_width): # ❷
... for top in range(0, cat_im_height, face_im_height): # ❸
... print(left, top)
... cat_copy_im.paste(face_im, (left, top))
...
0 0
0 215
0 430
0 645
0 860
0 1075
230 0
230 215
# --snip--
690 860
690 1075
>>> cat_copy_im.save('tiled.png')
我们将原始图像的宽度和高度存储在 cat_im_width 和 cat_im_height 中。接下来,我们复制图像并将其存储在 cat_copy_im 中 ❶。现在我们可以循环,将 face_im 粘贴到副本上。外层 for 循环的 left 变量从 0 开始,每次增加 face_im_width ❷。内层 for 循环的 top 变量从 0 开始,每次增加 face_im_height ❸。这些嵌套的 for 循环产生 left 和 top 的值,将 face_im 图像的网格粘贴到 Image 对象上,如图 21-6 所示。为了看到嵌套循环的工作情况,我们打印 left 和 top。粘贴完成后,我们将修改后的 cat_copy_im 保存为 tiled.png。
如果你正在粘贴带有透明度的图像,你需要将图像作为可选的第三个参数传递,这将告诉 Pillow 原始图像的哪些部分需要粘贴。否则,原始图像中的透明像素将作为粘贴图像中的白色像素出现。我们将在第 507 页的“项目 16:添加徽标”中更详细地探讨这种做法。
调整图像大小
当对 Image 对象调用 resize() 方法时,它返回一个指定宽度和高度的新的 Image 对象。它接受一个表示新尺寸的两个整数的元组参数。在交互式外壳中输入以下内容:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> width, height = cat_im.size # ❶
>>> quarter_sized_im = cat_im.resize((int(width / 2), int(height / 2))) # ❷
>>> quarter_sized_im.save('quartersized.png')
>>> svelte_im = cat_im.resize((width, height + 300)) # ❸
>>> svelte_im.save('svelte.png')
我们将 cat_im.size 元组中的两个值分配给变量 width 和 height ❶。使用这些变量而不是 cat_im.size[0] 和 cat_im.size[1] 使得其余的代码更易于阅读。
第一次 resize() 调用将 int(width / 2) 作为新的宽度,将 int(height / 2) 作为新的高度 ❷,因此 resize() 返回的 Image 对象的宽度和高度将是原始图像的一半,或者总体图像大小的四分之一。resize() 方法只接受其元组参数中的整数,这就是为什么你需要将两个除以 2 的操作都包裹在一个 int() 调用中。
这种调整大小保持了原始图像的比例,但新的宽度和高度值不必保持这些比例。svelte_im 变量包含一个 Image 对象,它具有原始宽度,但高度比原始图像高 300 像素 ❸,使佐菲看起来更加苗条。
注意,resize() 方法不会原地编辑 Image 对象,而是返回一个新的 Image 对象。
旋转和翻转图像
要旋转图像,请使用rotate()方法,该方法返回一个新的Image对象,并保持原始图像不变。该方法接受一个整数或浮点数,表示逆时针旋转图像的度数。在交互式外壳中输入以下内容:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> cat_im.rotate(90).save('rotated90.png')
>>> cat_im.rotate(180).save('rotated180.png')
>>> cat_im.rotate(270).save('rotated270.png')
注意,你可以通过在rotate()方法返回的Image对象上直接调用save()来链式调用方法。第一个rotate()和save()链将图像逆时针旋转 90 度并保存为rotated90.png。第二个和第三个调用执行相同的操作,只是它们分别将图像旋转 180 度和 270 度。结果看起来像图 21-7。

图 21-7:原始图像(左)和逆时针旋转 90 度、180 度和 270 度的图像
旋转后的图像将与原始图像具有相同的高度和宽度。在 Windows 上,黑色背景将填充旋转产生的任何间隙,如图 21-8 所示。在 macOS 和 Linux 上,透明像素将填充这些间隙。
rotate()方法有一个可选的expand关键字参数,可以设置为True以扩大图像尺寸以适应整个旋转后的新图像。例如,在交互式外壳中输入以下内容:
>>> cat_im.rotate(6).save('rotated6.png')
>>> cat_im.rotate(6, expand=True).save('rotated6_expanded.png')
第一次调用将图像旋转六度并保存为rotated6.png。(见图 21-8 左边的图像。)第二次调用将图像旋转六度,将expand设置为True,并将图像保存为rotated6_expanded.png。(见图 21-8 右边的图像。)

图 21-8:图像正常旋转六度(左)和expand=True时的图像(右)
如果你将图像旋转 90 度、180 度或 270 度,并且expand=True,旋转后的图像将不会有黑色或透明背景。
你还可以使用transpose()方法进行镜像翻转,如图 21-9 所示。

图 21-9:原始图像(左)、水平翻转的图像(中)和垂直翻转的图像(右)
在交互式外壳中输入以下内容:
>>> cat_im.transpose(Image.FLIP_LEFT_RIGHT).save('horizontal_flip.png')
>>> cat_im.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。
修改单个像素
getpixel() 方法可以检索单个像素的颜色,而 putpixel() 方法可以进一步改变该颜色。这两种方法都接受一个表示像素的 x 和 y 坐标的元组。putpixel() 方法还接受一个额外的参数,用于指定像素的新颜色,可以是表示 RGBA 的四个整数的元组,也可以是表示 RGB 的三个整数的元组。在交互式外壳中输入以下内容:
>>> from PIL import Image
>>> im = Image.new('RGBA', (100, 100)) # ❶
>>> im.getpixel((0, 0)) # ❷
(0, 0, 0, 0)
>>> for x in range(100): # ❸
... for y in range(50):
... for top in range(0, cat_im_height, face_im_height): # ❸
...
>>> from PIL import ImageColor
>>> for x in range(100): # ❺
... for y in range(50, 100):
... for top in range(0, cat_im_height, face_im_height): # ❸
...
>>> im.getpixel((0, 0))
(210, 210, 210, 255)
>>> im.getpixel((0, 50))
(169, 169, 169, 255)
>>> im.save('putPixel.png')
我们创建一个新的图像,它是一个 100×100 的透明正方形 ❶。在这个图像中的坐标上调用 getpixel() 返回 (0, 0, 0, 0),因为图像是透明的 ❷。为了给像素上色,我们使用嵌套的 for 循环遍历图像上半部分的像素 ❸,并将表示浅灰色的 RGB 元组传递给 putpixel() ❹。
假设我们想要将图像的下半部分染成深灰色,但不知道深灰色的 RGB 元组。putpixel() 方法不接受像 'darkgray' 这样的标准颜色名称,所以我们使用 ImageColor.getcolor() 来获取相应的颜色元组 ❻。我们遍历图像下半部分的像素 ❺,并将此调用的返回值传递给 putpixel(),生成一个上半部分为浅灰色,下半部分为深灰色的图像,如图 21-10 所示。我们可以在任何坐标上调用 getpixel() 来确认给定像素的颜色是我们预期的。最后,我们将图像保存到 putPixel.png。

图 21-10:putPixel.png 图像
当然,一次只绘制图像的一个像素不是很方便。如果你需要绘制形状,请使用“在图像上绘制”页面 512 上解释的 ImageDraw 函数。
项目 16:添加标志
假设你有一个无聊的工作,需要调整数千个图像并在每个图像的角落添加一个小标志水印。使用像 Paintbrush 或 Paint 这样的基本图形程序来做这件事会花费很长时间。一个更复杂的图形应用程序,如 Photoshop,可以进行批处理,但该软件的费用高达数百美元。让我们编写一个脚本来完成这项工作。
想象一下图 21-11 是你想要添加到每个图像右下角的标志:一个黑色猫图标,带有白色边框和透明背景。你可以使用自己的标志图像或下载本书在线资源中包含的标志。

图 21-11:要添加到图像中的标志
从高层次来看,程序应该执行以下操作:
-
加载标志图像。
-
遍历工作目录中的所有 .png 和 .jpg 文件。
-
检查图像是否比 300 像素宽和高。
-
如果是这样,将宽度或高度(较大的那个)减小到 300 像素,并按比例缩小另一个维度。
-
将标志图像粘贴到角落。
-
将修改后的图像保存到另一个文件夹中。
这意味着代码需要执行以下操作:
-
将 catlogo.png 文件作为
Image对象打开。 -
遍历由
os.listdir('.')返回的字符串。 -
从
size属性获取图像的宽度和高度。 -
计算调整大小后的图像的新宽度和高度。
-
调用
resize()方法调整图像大小。 -
调用
paste()方法将标志粘贴到右下角。 -
调用
save()方法以原始文件名保存更改。
第 1 步:打开标志图像
打开一个新的文件编辑标签,输入以下代码,并将其保存为 resizeAndAddLogo.py:
# Resizes images to fit in a 300x300 square with a logo in the corner
import os
from PIL import Image
SQUARE_FIT_SIZE = 300 # ❶
LOGO_FILENAME = 'catlogo.png' # ❷
logo_im = Image.open(LOGO_FILENAME) # ❸
logo_width, logo_height = logo_im.size # ❹
# TODO: Loop over all files in the working directory.
# TODO: Check if the image needs to be resized.
# TODO: Calculate the new width and height to resize to.
# TODO: Resize the image.
# TODO: Add the logo.
# TODO: Save changes.
通过在程序开始时设置 SQUARE_FIT_SIZE ❶ 和 LOGO_FILENAME ❷ 常量,我们使程序稍后易于更改。比如说,你添加的标志不是猫图标,或者说你将输出图像的最大尺寸减小到 300 像素以外的值。你可以直接打开代码并更改这些值。你也可以通过接受命令行参数来设置这些常量的值。如果没有这些常量,你将不得不在代码中搜索所有 300 和 'catlogo.png' 的实例,并将它们替换为新值。
Image.open() 方法返回标志 Image 对象 ❸。为了可读性,我们将标志的宽度和高度分配给变量 ❹。程序的其他部分是 TODO 注释的框架。
第 2 步:遍历所有文件
现在,你需要找到当前工作目录中的每个 .png 文件和 .jpg 文件。你不希望将标志添加到标志图像本身,因此程序应跳过任何文件名与 LOGO_FILENAME 相同的图像。将以下内容添加到你的代码中:
# Resizes images to fit in a 300x300 square with a logo in the corner
import os
from PIL import Image
# --snip--
os.makedirs('withLogo', exist_ok=True)
# Loop over all files in the working directory.
for filename in os.listdir('.'): # ❶
if not (filename.endswith('.png') or filename.endswith('.jpg')) \ # ❷
or filename == LOGO_FILENAME:
continue # Skip non-image files and the logo file itself. # ❸
im = Image.open(filename) # ❹
width, height = im.size
# --snip--
首先,os.makedirs() 调用在存储修改后的图像的 withLogo 文件夹中创建,而不是覆盖原始图像文件。exist_ok=True 关键字参数将防止 os.makedirs() 在 withLogo 已存在时引发异常。当代码遍历工作目录中的所有文件 ❶ 时,一个长的 if 语句检查不以 .png 或 .jpg 结尾的文件名 ❷。如果找到任何,或者如果文件是标志图像本身,则循环应跳过它并使用 continue 转到下一个文件 ❸。如果 filename 以 '.png' 或 '.jpg' 结尾且不是标志文件,则代码将其作为 Image 对象打开 ❹ 并保存其宽度和高度。
第 3 步:调整图像大小
程序应该仅在宽度或高度大于 SQUARE_FIT_SIZE(在本例中为 300 像素)时调整图像大小,因此你应该将调整大小的代码放在一个检查 width 和 height 变量的 if 语句中。将以下代码添加到你的程序中:
# Resizes images to fit in a 300x300 square with a logo in the corner
import os
from PIL import Image
# --snip--
# Check if the image needs to be resized.
if width > SQUARE_FIT_SIZE and height > SQUARE_FIT_SIZE:
# Calculate the new width and height to resize to.
if width > height:
height = int((SQUARE_FIT_SIZE / width) * height) # ❶
width = SQUARE_FIT_SIZE
else:
width = int((SQUARE_FIT_SIZE / height) * width) # ❷
height = SQUARE_FIT_SIZE
# Resize the image.
print(f'Resizing {filename}...')
im = im.resize((width, height)) # ❸
# --snip--
如果需要调整图像大小,您必须确定它是宽图像还是高图像。如果width大于height,代码应该按照与宽度相同的比例减少高度 ❶。这个比例是SQUARE_FIT_SIZE值除以当前宽度,因此代码将新的height值设置为这个比例乘以当前height值。因为除法运算符返回一个浮点值,而resize()方法需要整数维度,您必须记得使用int()函数将结果转换为整数。最后,代码将新的width值设置为SQUARE_FIT_SIZE。
如果height大于或等于width,则else子句执行相同的计算,但交换height和width变量 ❷。一旦这些变量包含新的图像尺寸,代码将它们传递给resize()方法,并存储返回的Image对象 ❸。
第 4 步:添加标志并保存更改
无论您是否调整了图像大小,都应该将标志粘贴到其右下角。确切的位置取决于图像和标志的大小。图 21-12 展示了如何计算粘贴位置。粘贴标志的左坐标是图像宽度减去标志宽度,粘贴标志的顶坐标是图像高度减去标志高度。

图 21-12:标志的左和顶坐标是图像宽度/高度减去标志宽度/高度。
在您的代码将标志粘贴到图像中之后,它应该保存修改后的Image对象。请将以下内容添加到您的程序中:
# Resizes images to fit in a 300x300 square with a logo in the corner
import os
from PIL import Image
# --snip--
# Check if the image needs to be resized.
# --snip--
# Add the logo.
print(f'Adding logo to {filename}...') # ❶
im.paste(logo_im, (width – logo_width, height – logo_height), logo_im) # ❷
# Save changes.
im.save(os.path.join('withLogo', filename)) # ❸
新的代码打印一条消息,告知用户正在添加标志 ❶,在计算出的坐标上粘贴logo_im到im上 ❷,并将更改保存到withLogo目录下的文件名中 ❸。当您在当前工作目录中运行此程序,并带有zophie.png和其他图像文件时,输出将如下所示:
Resizing zophie.png...
Adding logo to zophie.png...
Resizing zophie_xmas_tree.png...
Adding logo to zophie_xmas_tree.png...
Resizing me_and_zophie.png...
Adding logo to me_and_zophie.png...
程序将zophie.png转换为 225×300 像素的图像,看起来像图 21-13。

图 21-13:程序调整了 zophie.png 的大小并添加了标志(左)。如果您忘记了第三个参数,标志中的透明像素将显示为实心白色像素(右)。
记住,除非您将logo_im作为第三个参数传递,否则paste()方法不会粘贴透明像素。此程序可以在几分钟内自动调整大小并将标志添加到数百张图像中。
相似程序的思路
能够批量构建复合图像或修改图像大小对于许多应用都很有用。您可以编写类似的程序来完成以下操作:
-
在图片上添加文本或网站 URL。
-
在图片上添加时间戳。
-
根据图片大小将图片复制或移动到不同的文件夹中。
-
在图片上添加一个主要透明的水印以防止他人复制。
处理图像数据类型
Image对象具有几个有用的属性,可以为您提供有关从其中加载的图像文件的基本信息:其宽度和高度、文件名和图形格式(如 JPEG、WebP、GIF 或 PNG)。例如,在交互式外壳中输入以下内容:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> cat_im.size
(816, 1088) # ❶
>>> width, height = cat_im.size # ❷
>>> width # ❸
816
>>> height # ❹
1088
>>> cat_im.filename
'zophie.png'
>>> cat_im.format
'PNG'
>>> cat_im.format_description
'Portable network graphics'
>>> cat_im.save('zophie.jpg') # ❺
在您从*zophie.png*创建Image对象并将其存储在cat_im中之后,对象的size属性包含一个元组,其中包含以像素为单位的图片宽度和高度❶。您可以将元组中的值分配给width和height变量❷,以便单独访问宽度❸和高度❹。filename属性描述了原始文件名。format和format_description属性是描述原始文件图像格式的字符串(其中format_description更为详细)。
最后,调用save()方法并传递'zophie.jpg',将新图像以文件名*zophie.jpg*保存到您的硬盘❺。Pillow 看到文件扩展名为.jpg,并自动使用 JPEG 图像格式保存图片。现在您应该在硬盘上拥有两个图像,zophie.png和zophie.jpg。虽然这些文件基于相同的图像,但它们并不相同,因为它们的格式不同。
Pillow 还提供了Image.new()函数,该函数返回一个Image对象——类似于Image.open(),但Image.new()对象表示的图片将是空白的。Image.new()的参数如下:
-
字符串
'RGBA',用于设置颜色模式为 RGBA。(本书未涉及其他模式。) -
新图片的尺寸,作为包含新图片宽度和高度的整数元组。
-
图片应开始的背景颜色,作为包含 RGBA 值的四个整数的元组。您可以使用
ImageColor.getcolor()函数的返回值作为此参数。或者,您可以将标准颜色名称作为字符串传递。
例如,在交互式外壳中输入以下内容:
>>> from PIL import Image
>>> im = Image.new('RGBA', (100, 200), 'purple') # ❶
>>> im.save('purpleImage.png')
>>> im2 = Image.new('RGBA', (20, 20)) # ❷
>>> im2.save('transparentImage.png')
在这里,我们为宽度为 100 像素、高度为 200 像素且背景为紫色的图片创建了一个Image对象❶。然后我们将此图片保存到文件*purpleImage.png*中。我们再次调用Image.new()来创建另一个Image对象,这次传递(20, 20)作为尺寸,并为背景颜色留空❷。不可见的黑色(0, 0, 0, 0)是未指定颜色参数时的默认颜色,因此第二个图片具有透明背景。我们将这个 20×20 的透明正方形保存到*transparentImage.png*。
裁剪图片
裁剪图像意味着在图像内部选择一个矩形区域并移除矩形外的所有内容。Image 对象的 crop() 方法接受一个矩形元组并返回一个代表裁剪图像的 Image 对象。裁剪不是就地发生的——也就是说,原始 Image 对象保持未修改,crop() 方法返回一个新的对象。记住,矩形元组(在这种情况下,裁剪部分)包括像素的左侧列和顶部行,但不包括右侧列和底部行。
在交互式外壳中输入以下内容:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> cropped_im = cat_im.crop((335, 345, 565, 560))
>>> cropped_im.save('cropped.png')
此代码为裁剪图像创建一个新的 Image 对象,将其存储在 cropped_im 中,然后对 cropped_im 调用 save() 以将裁剪图像保存为 cropped.png,如图 21-4 所示。

图 21-4:新图像是原始图像的裁剪部分。
裁剪从原始文件创建新文件。
在其他图像上粘贴图像
copy() 方法将返回一个新的 Image 对象,其中包含与被调用时所在的 Image 对象相同的图像。如果你需要修改图像但还想保留原始图像的未修改版本,这很有用。例如,在交互式外壳中输入以下内容:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> cat_copy_im = cat_im.copy()
cat_im 和 cat_copy_im 变量包含两个独立的 Image 对象,它们上面都有相同的图像。现在你已经在 cat_copy_im 中存储了一个 Image 对象,你可以随意修改 cat_copy_im 并将其保存到新的文件名中,而 cat_im 保持不变。
当在 Image 对象上调用时,paste() 方法会在其上方粘贴另一个图像。让我们通过在 cat_copy_im 上粘贴一个较小的图像来继续外壳示例:
>>> face_im = cat_im.crop((335, 345, 565, 560))
>>> face_im.size
(230, 215)
>>> cat_copy_im.paste(face_im, (0, 0))
>>> cat_copy_im.paste(face_im, (400, 500))
>>> cat_copy_im.save('pasted.png')
首先,我们将一个矩形区域元组传递给 crop(),该区域在 zophie.png 中包含 Zophie 的脸。这个方法调用创建了一个代表 230×215 裁剪的 Image 对象,我们将其存储在 face_im 中。现在我们可以将 face_im 粘贴到 cat_copy_im 上。paste() 方法接受两个参数:一个源 Image 对象和一个包含我们想要粘贴源 Image 对象左上角坐标的元组。在这里,我们在 cat_copy_im 上两次调用 paste(),将两个 face_im 的副本粘贴到 cat_copy_im 上。最后,我们将修改后的 cat_copy_im 保存到 pasted.png,如图 21-5 所示。
注意
尽管它们的名称如此,Pillow 中的 copy() 和 paste() 方法并不使用你的计算机剪贴板。
paste() 方法会就地修改其 Image 对象;它不会返回一个包含粘贴图像的 Image 对象。如果你想调用 paste() 同时保留原始图像的未修改版本,你需要首先复制图像,然后在该副本上调用 paste()。

图 21-5:Zophie 猫,她的脸粘贴了两次
假设你想要将 Zophie 的头在整个图像上平铺,如图 21-6 所示。

图 21-6:使用 paste()方法与嵌套 for 循环可以复制猫的脸(如果你愿意,可以称之为复制品猫)。
你可以用几个for循环实现这种效果。通过在交互式 shell 中输入以下内容继续示例:
>>> cat_im_width, cat_im_height = cat_im.size
>>> face_im_width, face_im_height = face_im.size
>>> cat_copy_im = cat_im.copy() # ❶
>>> for left in range(0, cat_im_width, face_im_width): # ❷
... for top in range(0, cat_im_height, face_im_height): # ❸
... print(left, top)
... cat_copy_im.paste(face_im, (left, top))
...
0 0
0 215
0 430
0 645
0 860
0 1075
230 0
230 215
# --snip--
690 860
690 1075
>>> cat_copy_im.save('tiled.png')
我们将原始图像的宽度和高度存储在cat_im_width和cat_im_height中。接下来,我们创建图像的一个副本并将其存储在cat_copy_im中 ❶。现在我们可以循环,将face_im粘贴到副本上。外层for循环的left变量从0开始,每次增加face_im_width ❷。内层for循环的top变量从0开始,每次增加face_im_height ❸。这些嵌套的for循环产生left和top的值,将face_im图像的网格粘贴到Image对象上,如图 21-6 所示。为了看到嵌套循环的工作情况,我们打印left和top。粘贴完成后,我们将修改后的cat_copy_im保存为tiled.png。
如果你正在粘贴带有透明度的图像,你需要将图像作为可选的第三个参数传递,这告诉 Pillow 粘贴原始图像的哪些部分。否则,原始图像中的透明像素将在粘贴的图像中显示为白色像素。我们将在第 507 页的“项目 16:添加徽标”中更详细地探讨这种做法。
调整图像大小
当在Image对象上调用resize()方法时,它返回一个指定宽度和高度的新的Image对象。它接受一个表示新尺寸的两个整数的元组参数。在交互式 shell 中输入以下内容:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> width, height = cat_im.size # ❶
>>> quarter_sized_im = cat_im.resize((int(width / 2), int(height / 2))) # ❷
>>> quarter_sized_im.save('quartersized.png')
>>> svelte_im = cat_im.resize((width, height + 300)) # ❸
>>> svelte_im.save('svelte.png')
我们将cat_im.size元组中的两个值赋给变量width和height ❶。使用这些变量而不是cat_im.size[0]和cat_im.size[1]可以使代码的其余部分更容易阅读。
第一次resize()调用将int(width / 2)作为新的宽度,将int(height / 2)作为新的高度 ❷,因此resize()返回的Image对象将是原始图像宽度高度的一半,或者总体上是原始图像大小的四分之一。resize()方法只接受其元组参数中的整数,这就是为什么你需要将两个除以2的操作都包裹在int()调用中。
这种调整大小保持了原始图像的比例,但新的宽度和高度值不必保持这些比例。svelte_im变量包含一个Image对象,其宽度与原始图像相同,但高度比原始图像高 300 像素 ❸,使 Zophie 看起来更苗条。
注意,resize()方法不会就地编辑Image对象,而是返回一个新的Image对象。
旋转和翻转图像
要旋转图像,请使用rotate()方法,该方法返回一个新的Image对象,而原始图像保持不变。该方法接受一个整数或浮点数,表示逆时针旋转图像的度数。在交互式 shell 中输入以下内容:
>>> from PIL import Image
>>> cat_im = Image.open('zophie.png')
>>> cat_im.rotate(90).save('rotated90.png')
>>> cat_im.rotate(180).save('rotated180.png')
>>> cat_im.rotate(270).save('rotated270.png')
注意,您可以通过直接在rotate()方法返回的Image对象上调用save()来链式调用方法。第一个rotate()和save()链将图像逆时针旋转 90 度并保存为rotated90.png。第二个和第三个调用执行相同的操作,除了它们分别旋转图像 180 度和 270 度。结果看起来像图 21-7。

图 21-7:原始图像(左)和逆时针旋转 90 度、180 度和 270 度的图像
旋转后的图像将与原始图像具有相同的高度和宽度。在 Windows 上,黑色背景将填充旋转产生的任何间隙,如图 21-8 所示。在 macOS 和 Linux 上,透明像素将填充这些间隙。
rotate()方法有一个可选的expand关键字参数,可以设置为True以扩大图像的尺寸以适应整个旋转后的新图像。例如,在交互式 shell 中输入以下内容:
>>> cat_im.rotate(6).save('rotated6.png')
>>> cat_im.rotate(6, expand=True).save('rotated6_expanded.png')
第一次调用将图像旋转六度并保存为rotated6.png。(见图 21-8 左边的图像。)第二次调用将图像旋转六度,将expand设置为True,并将图像保存为rotated6_expanded.png。(见图 21-8 右边的图像。)

图 21-8:图像正常旋转六度(左)和 expand=True 时的图像(右)
如果您使用expand=True旋转图像 90 度、180 度或 270 度,旋转后的图像将不会有黑色或透明背景。
您还可以使用transpose()方法进行镜像翻转,如图 21-9 所示。

图 21-9:原始图像(左)、水平翻转的图像(中)和垂直翻转的图像(右)
在交互式 shell 中输入以下内容:
>>> cat_im.transpose(Image.FLIP_LEFT_RIGHT).save('horizontal_flip.png')
>>> cat_im.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。
修改单个像素
getpixel()方法可以检索单个像素的颜色,而putpixel()方法可以进一步改变该颜色。这两种方法都接受一个表示像素 x 和 y 坐标的元组。putpixel()方法还接受一个额外的参数,用于指定像素的新颜色,可以是表示 RGBA 的四整数元组或表示 RGB 的三整数元组。在交互式 shell 中输入以下内容:
>>> from PIL import Image
>>> im = Image.new('RGBA', (100, 100)) # ❶
>>> im.getpixel((0, 0)) # ❷
(0, 0, 0, 0)
>>> for x in range(100): # ❸
... for y in range(50):
... for top in range(0, cat_im_height, face_im_height): # ❸
...
>>> from PIL import ImageColor
>>> for x in range(100): # ❺
... for y in range(50, 100):
... for top in range(0, cat_im_height, face_im_height): # ❸
...
>>> im.getpixel((0, 0))
(210, 210, 210, 255)
>>> im.getpixel((0, 50))
(169, 169, 169, 255)
>>> im.save('putPixel.png')
我们创建一个新的图像,它是一个 100×100 的透明正方形 ❶。在这个图像的坐标上调用getpixel()会返回(0, 0, 0, 0),因为图像是透明的 ❷。为了给像素上色,我们使用嵌套的for循环遍历图像上半部分的像素 ❸,并将代表浅灰色的 RGB 元组传递给putpixel() ❹。
假设我们想要将图像的下半部分染成深灰色,但不知道深灰色的 RGB 元组。putpixel()方法不接受像'darkgray'这样的标准颜色名称,所以我们使用ImageColor.getcolor()来获取相应的颜色元组 ❻。我们遍历图像下半部分的像素 ❺,并将此调用的返回值传递给putpixel(),从而生成上半部分为浅灰色、下半部分为深灰色的图像,如图 21-10 所示。我们可以在任何坐标上调用getpixel()来确认给定像素的颜色是否符合预期。最后,我们将图像保存到putPixel.png。

图 21-10:putPixel.png 图像
当然,一次绘制图像的一个像素并不方便。如果你需要绘制形状,请使用“在图像上绘制”中解释的ImageDraw函数,该函数位于第 512 页。
项目 16:添加标志
假设你有一个无聊的工作,就是调整数千张图片的大小,并在每张图片的角落添加一个小标志水印。使用像 Paintbrush 或 Paint 这样的基本图形程序来做这件事会花费很长时间。一个更复杂的图形应用程序,如 Photoshop,可以进行批量处理,但该软件的价格高达数百美元。让我们编写一个脚本来完成这项工作。
想象一下图 21-11 是你想要添加到每个图像右下角的标志:一个黑色猫图标,带有白色边框和透明背景。你可以使用自己的标志图像或下载本书在线资源中包含的标志。

图 21-11:要添加到图像中的标志
在高层次上,程序应该执行以下操作:
-
加载标志图像。
-
遍历工作目录中的所有.png和.jpg文件。
-
检查图像是否比 300 像素宽和高。
-
如果需要,将宽度或高度(较大的那个)减少到 300 像素,并按比例缩小另一个维度。
-
将 Logo 图像粘贴到角落。
-
将修改后的图像保存到另一个文件夹中。
这意味着代码需要执行以下操作:
-
将catlogo.png文件作为
Image对象打开。 -
遍历
os.listdir('.')返回的字符串。 -
从
size属性获取图像的宽度和高度。 -
计算调整大小后图像的新宽度和高度。
-
调用
resize()方法调整图像大小。 -
调用
paste()方法将 Logo 粘贴到右下角。 -
调用
save()方法保存更改,使用原始文件名。
第 1 步:打开 Logo 图像
打开一个新的文件编辑标签,输入以下代码,并将其保存为resizeAndAddLogo.py:
# Resizes images to fit in a 300x300 square with a logo in the corner
import os
from PIL import Image
SQUARE_FIT_SIZE = 300 # ❶
LOGO_FILENAME = 'catlogo.png' # ❷
logo_im = Image.open(LOGO_FILENAME) # ❸
logo_width, logo_height = logo_im.size # ❹
# TODO: Loop over all files in the working directory.
# TODO: Check if the image needs to be resized.
# TODO: Calculate the new width and height to resize to.
# TODO: Resize the image.
# TODO: Add the logo.
# TODO: Save changes.
通过在程序开始时设置SQUARE_FIT_SIZE ❶ 和 LOGO_FILENAME ❷ 常量,我们使得以后更改程序变得容易。比如说,你添加的 Logo 不是猫图标,或者你将输出图像的最大维度减少到 300 像素以外的值。你可以直接打开代码并更改这些值。你也可以通过接受命令行参数来设置这些常量的值。如果没有这些常量,你将不得不在代码中搜索所有300和'catlogo.png'的实例,并将它们替换为新的值。
Image.open()方法返回 Logo Image对象 ❸。为了可读性,我们将 Logo 的宽度和高度分配给变量 ❹。程序的其他部分是TODO注释的框架。
第 2 步:遍历所有文件
现在需要找到当前工作目录中的每个.png文件和.jpg文件。你不希望将 Logo 图像添加到 Logo 图像本身,因此程序应该跳过任何文件名与LOGO_FILENAME相同的图像。将以下内容添加到你的代码中:
# Resizes images to fit in a 300x300 square with a logo in the corner
import os
from PIL import Image
# --snip--
os.makedirs('withLogo', exist_ok=True)
# Loop over all files in the working directory.
for filename in os.listdir('.'): # ❶
if not (filename.endswith('.png') or filename.endswith('.jpg')) \ # ❷
or filename == LOGO_FILENAME:
continue # Skip non-image files and the logo file itself. # ❸
im = Image.open(filename) # ❹
width, height = im.size
# --snip--
首先,os.makedirs()调用创建一个withLogo文件夹来存储修改后的图像,而不是覆盖原始图像文件。exist_ok=True关键字参数将防止os.makedirs()在withLogo已存在时抛出异常。当代码遍历工作目录中的所有文件时 ❶,一个长的if语句检查不以.png或.jpg结尾的文件名 ❷。如果找到任何,或者如果文件是 Logo 图像本身,循环应该跳过它并使用continue跳到下一个文件 ❸。如果filename以'.png'或'.jpg'结尾且不是 Logo 文件,代码将其打开为Image对象 ❹并保存其宽度和高度。
第 3 步:调整图像大小
程序应该只在宽度或高度大于SQUARE_FIT_SIZE(在本例中为 300 像素)时调整图像大小,因此你应该将调整大小的代码放在一个检查width和height变量的if语句中。将以下代码添加到你的程序中:
# Resizes images to fit in a 300x300 square with a logo in the corner
import os
from PIL import Image
# --snip--
# Check if the image needs to be resized.
if width > SQUARE_FIT_SIZE and height > SQUARE_FIT_SIZE:
# Calculate the new width and height to resize to.
if width > height:
height = int((SQUARE_FIT_SIZE / width) * height) # ❶
width = SQUARE_FIT_SIZE
else:
width = int((SQUARE_FIT_SIZE / height) * width) # ❷
height = SQUARE_FIT_SIZE
# Resize the image.
print(f'Resizing {filename}...')
im = im.resize((width, height)) # ❸
# --snip--
如果需要调整图像大小,你必须找出它是宽图像还是高图像。如果宽度大于高度,则代码应按与宽度相同的比例减少高度 ❶。这个比例是SQUARE_FIT_SIZE值除以当前宽度,因此代码将新的高度值设置为这个比例乘以当前高度值。因为除法运算符返回一个浮点值,而resize()需要整数维度,你必须记住使用int()函数将结果转换为整数。最后,代码将新的宽度值设置为SQUARE_FIT_SIZE。
如果高度大于或等于宽度,则else子句执行相同的计算,但交换高度和宽度变量 ❷。一旦这些变量包含新的图像尺寸,代码将它们传递给resize()方法,并存储返回的Image对象 ❸。
第 4 步:添加徽标并保存更改
无论你是否调整了图像大小,你应该将徽标粘贴到其右下角。确切的位置取决于图像和徽标的大小。图 21-12 显示了如何计算粘贴位置。粘贴徽标的左坐标是图像宽度减去徽标宽度,粘贴徽标的顶坐标是图像高度减去徽标高度。

图 21-12:徽标的左上坐标是图像宽度/高度减去徽标宽度/高度。
在你的代码将徽标粘贴到图像中之后,它应该保存修改后的Image对象。将以下内容添加到你的程序中:
# Resizes images to fit in a 300x300 square with a logo in the corner
import os
from PIL import Image
# --snip--
# Check if the image needs to be resized.
# --snip--
# Add the logo.
print(f'Adding logo to {filename}...') # ❶
im.paste(logo_im, (width – logo_width, height – logo_height), logo_im) # ❷
# Save changes.
im.save(os.path.join('withLogo', filename)) # ❸
新代码打印一条消息,告知用户正在添加徽标 ❶,将logo_im粘贴到im计算出的坐标上 ❷,并将更改保存到withLogo目录下的文件名中 ❸。当你运行此程序时,工作目录中的zophie.png和其他图像文件,输出将如下所示:
Resizing zophie.png...
Adding logo to zophie.png...
Resizing zophie_xmas_tree.png...
Adding logo to zophie_xmas_tree.png...
Resizing me_and_zophie.png...
Adding logo to me_and_zophie.png...
程序将zophie.png转换为 225×300 像素的图像,看起来像图 21-13。

图 21-13:程序调整了 zophie.png 的大小并添加了徽标(左)。如果你忘记了第三个参数,徽标中的透明像素将显示为实心白色像素(右)。
请记住,paste()方法不会粘贴透明像素,除非你将logo_im作为第三个参数传递。此程序可以在几分钟内自动调整大小并“添加徽标”数百张图片。
类似程序的思路
能够批量构建复合图像或修改图像大小对于许多应用都很有用。你可以编写类似的程序来完成以下任务:
-
向图像添加文本或网站 URL。
-
向图像添加时间戳。
-
根据图像的大小将图像复制或移动到不同的文件夹中。
-
在图像上添加一个几乎透明的水印以防止他人复制。
利用图像
如果需要在图像上绘制线条、矩形、圆形或其他简单形状,请使用 Pillow 的 ImageDraw 模块。例如,在交互式外壳中输入以下内容:
>>> from PIL import Image, ImageDraw
>>> im = Image.new('RGBA', (200, 200), 'white')
>>> draw = ImageDraw.Draw(im)
首先,我们导入 Image 和 ImageDraw。然后,我们创建一个 200×200 的白色图像并将其存储在 im 中。我们将此 Image 对象传递给 ImageDraw.Draw() 函数以接收一个 ImageDraw 对象。此对象具有用于绘制形状和文本的几个方法。将新对象存储在变量如 draw 中,以便您可以在以下示例中轻松使用它。
形状
以下 ImageDraw 方法在图像上绘制各种形状。这些方法的 fill 和 outline 参数是可选的,如果未指定,则默认为白色。
点
point(xy, fill) 方法绘制单个像素。xy 参数表示要绘制的点的列表。该列表可以包含 x 和 y 坐标元组,例如 [(x, y), (x, y), ...],或者没有元组的 x 和 y 坐标,例如 [x1, y1, x2, y2, ...]。fill 参数为点着色,可以是 RGBA 元组或字符串,如 'red'。fill 参数是可选的。这里的“point”名称指的是像素,而不是字体大小的单位。
线条
line(xy, fill, width) 方法绘制线条或线条系列。xy 参数是元组列表,例如 [(x, y), (x, y), ...],或者整数列表,例如 [x1, y1, x2, y2, ...]。每个点是你正在绘制的线条上的连接点。可选的 fill 参数指定线条的颜色,作为 RGBA 元组或颜色名称。可选的 width 参数确定线条的宽度,如果未指定,则默认为 1。
矩形
rectangle(xy, fill, outline, width) 方法绘制矩形。xy 参数是形式为 (left, top, right, bottom)`` 的框元组。左和顶值指定矩形左上角的 x 和 y 坐标,而右和底指定右下角的坐标。可选的 fill 参数是矩形的内部颜色。可选的 outline 参数是矩形轮廓的颜色。可选的 width 参数表示线条的宽度,如果未指定,则默认为 1。
椭圆
ellipse(xy, fill, outline, width)方法绘制一个椭圆。如果椭圆的宽度和高度相同,此方法将绘制一个圆。xy 参数是一个表示精确包含椭圆的矩形的元组(左,顶,右,底)。可选的 fill 参数是椭圆内部的颜色,可选的 outline 参数是椭圆轮廓的颜色。可选的 width 参数是线的宽度,如果未指定,默认为1。
多边形
polygon(xy, fill, outline, width)方法绘制一个任意多边形。xy 参数是一个元组列表,例如[(x, y), (x, y), ...],或者整数,例如[x1, y1, x2, y2, ...],代表多边形边的连接点。最后一对坐标将自动连接到第一对坐标。可选的 fill 参数是多边形内部的颜色,可选的 outline 参数是多边形轮廓的颜色。可选的 width 参数是线的宽度,如果未指定,默认为1。
绘图示例
为了练习使用这些方法,请在交互式外壳中输入以下内容:
>>> from PIL import Image, ImageDraw
>>> im = Image.new('RGBA', (200, 200), 'white')
>>> draw = ImageDraw.Draw(im)
>>> draw.line([(0, 0), (199, 0), (199, 199), (0, 199), (0, 0)], fill='black') ❶
>>> draw.rectangle((20, 30, 60, 60), fill='blue') ❷
>>> draw.ellipse((120, 30, 160, 60), fill='red') ❸
>>> draw.polygon(((57, 87), (79, 62), (94, 85), (120, 90), (103, 113)), fill='brown') ❹
>>> for i in range(100, 200, 10): ❺
... draw.line([(i, 0), (200, i - 100)], fill='green')
>>> im.save('drawing.png')
在创建一个 200×200 的白色图像的Image对象后,将其传递给ImageDraw.Draw()以获取一个ImageDraw对象,并将ImageDraw对象存储在draw中,我们就可以在draw上调用绘图方法。在这里,我们在图像的边缘绘制一个细的黑色轮廓 ❶,一个左上角在(20, 30)、右下角在(60, 60)的蓝色矩形 ❷,一个由(120, 30)到(160, 60)的矩形定义的红椭圆 ❸,一个由五个点组成的棕色多边形 ❹,以及用for循环绘制的绿色线条图案 ❺。生成的drawing.png文件将类似于图 21-14(尽管这本书中没有打印颜色)。

图 21-14:生成的 drawing.png 图像
你可以在ImageDraw对象上使用几个其他形状绘制方法。完整文档可在pillow.readthedocs.io/en/latest/reference/ImageDraw.html找到。
Text
ImageDraw对象还有一个用于在图像上绘制文本的text()方法。此方法接受四个参数:
xy 一个由两个整数组成的元组,指定文本框的左上角
text 你想写入的文本字符串
fill 文本的颜色
font 一个可选的ImageFont对象,用于设置文本的字形和大小
在我们使用text()在图像上绘制文本之前,让我们更详细地讨论可选的字体参数。此参数是一个ImageFont对象,你可以通过运行以下代码来获取:
>>> from PIL import ImageFont
一旦导入了 Pillow 的 ImageFont 模块,通过调用 ImageFont.truetype() 函数来访问字体,该函数接受两个参数。第一个参数是一个表示字体的 TrueType 文件的字符串,即实际存在于你的硬盘上的字体文件。TrueType 文件具有 .ttf 文件扩展名,通常位于 Windows 的 C:\Windows\Fonts,macOS 的 /Library/Fonts 和 /System/Library/Fonts,以及 Linux 的 /usr/share/fonts/truetype。你不需要将这些路径作为 TrueType 文件字符串的一部分输入,因为 Pillow 会自动搜索这些目录,但如果它找不到你指定的字体,将会显示错误。
ImageFont.truetype() 函数的第二个参数是一个表示点大小的整数(而不是像素)。Pillow 默认创建每英寸 72 像素的 PNG 图像,而一个 点 等于 1/72 英寸。为了练习,请在交互式外壳中输入以下内容:
>>> from PIL import Image, ImageDraw, ImageFont
>>> import os
>>> im = Image.new('RGBA', (200, 200), 'white') # ❶
>>> draw = ImageDraw.Draw(im) # ❷
>>> draw.text((20, 150), 'Hello', fill='purple') # ❸
>>> arial_font = ImageFont.truetype('arial.ttf', 32) # ❹
>>> draw.text((100, 150), 'Howdy', fill='gray', font=arial_font) # ❺
>>> im.save('text.png')
在导入 Image、ImageDraw、ImageFont 和 os 之后,我们创建一个 200×200 白色图像的 Image 对象❶,并从 Image 对象创建一个 ImageDraw 对象❷。我们使用 text() 在 (20, 150) 处用紫色写上 Hello❸。我们没有在这个调用中传递可选的第四个参数,所以文本的字型和大小没有被定制。
接下来,为了设置字型和大小,我们调用 ImageFont.truetype(),传递所需字体的 .ttf 文件,然后是一个整数字体大小❹。我们将返回的 Font 对象存储在一个变量中,然后将该变量传递给 text() 方法的最后一个关键字参数。这个方法调用在 (100, 150) 处用灰色绘制了 32 点 Arial 的 Howdy❺。生成的 text.png 文件看起来像图 21-15。

图 21-15:生成的 text.png 图像
如果你对使用 Python 创建计算机生成的艺术感兴趣,可以查看由 Tristan Bunn 编著的 Learn Python Visually(No Starch Press,2021)或我的书 The Recursive Book of Recursion(No Starch Press,2022)。
形状
以下 ImageDraw 方法在图像上绘制各种形状。这些方法的 fill 和 outline 参数是可选的,如果未指定,将默认为白色。
点
point(xy, fill) 方法绘制单个像素。xy 参数表示要绘制的点的列表。该列表可以包含 x 和 y 坐标元组,例如 [(x, y), (x, y), ...],或者没有元组的 x 和 y 坐标,例如 [x1, y1, x2, y2, ...]。fill 参数为点着色,可以是 RGBA 元组或字符串,例如 'red'。fill 参数是可选的。这里的“point”名称指的是像素,而不是字体大小单位。
行
line(xy, fill, width)方法绘制一条线或一系列线。xy 参数是一个元组列表,例如[(x, y), (x, y), ...],或者是一个整数列表,例如[x1, y1, x2, y2, ...]。每个点是你正在绘制的线上的连接点。可选的 fill 参数指定线的颜色,作为一个 RGBA 元组或颜色名称。可选的 width 参数确定线的宽度,如果未指定,则默认为1。
矩形
rectangle(xy, fill, outline, width)方法绘制一个矩形。xy 参数是一个形式为(left, top, right, bottom)的矩形元组。left 和 top 值指定矩形左上角的 x 和 y 坐标,而 right 和 bottom 指定右下角的坐标。可选的 fill 参数是矩形内部的颜色。可选的 outline 参数是矩形轮廓的颜色。可选的 width 参数代表线的宽度,如果未指定,则默认为1。
椭圆
ellipse(xy, fill, outline, width)方法绘制一个椭圆。如果椭圆的宽度和高度相同,此方法将绘制一个圆。xy 参数是一个表示精确包含椭圆的矩形的元组(left, top, right, bottom)。可选的 fill 参数是椭圆内部的颜色,可选的 outline 参数是椭圆轮廓的颜色。可选的 width 参数是线的宽度,如果未指定,则默认为1。
多边形
polygon(xy, fill, outline, width)方法绘制一个任意多边形。xy 参数是一个元组列表,例如[(x, y), (x, y), ...],或者是一个整数列表,例如[x1, y1, x2, y2, ...],代表多边形边的连接点。最后一对坐标将自动连接到第一对坐标。可选的 fill 参数是多边形内部的颜色,可选的 outline 参数是多边形轮廓的颜色。可选的 width 参数是线的宽度,如果未指定,则默认为1。
绘图示例
为了练习使用这些方法,请在交互式 shell 中输入以下内容:
>>> from PIL import Image, ImageDraw
>>> im = Image.new('RGBA', (200, 200), 'white')
>>> draw = ImageDraw.Draw(im)
>>> draw.line([(0, 0), (199, 0), (199, 199), (0, 199), (0, 0)], fill='black') ❶
>>> draw.rectangle((20, 30, 60, 60), fill='blue') ❷
>>> draw.ellipse((120, 30, 160, 60), fill='red') ❸
>>> draw.polygon(((57, 87), (79, 62), (94, 85), (120, 90), (103, 113)), fill='brown') ❹
>>> for i in range(100, 200, 10): ❺
... draw.line([(i, 0), (200, i - 100)], fill='green')
>>> im.save('drawing.png')
在创建一个 200×200 的白色图像的Image对象后,将其传递给ImageDraw.Draw()以获取一个ImageDraw对象,并将ImageDraw对象存储在draw中,我们就可以在draw上调用绘图方法。在这里,我们在图像的边缘绘制了一个细的黑色轮廓 ❶,一个左上角在(20, 30)、右下角在(60, 60)的蓝色矩形 ❷,一个由(120, 30)到(160, 60)的矩形定义的红椭圆 ❸,一个有五个顶点的棕色多边形 ❹,以及用for循环绘制的绿色线条图案 ❺。生成的drawing.png文件将类似于图 21-14(尽管这本书中没有打印颜色)。

图 21-14:生成的绘制.png 图像
你可以在ImageDraw对象上使用几种其他的形状绘制方法。完整的文档可以在pillow.readthedocs.io/en/latest/reference/ImageDraw.html找到。
点
point(xy, fill)方法用于绘制单个像素。xy 参数表示要绘制的点的列表。列表可以包含 x 和 y 坐标元组,例如[(x, y), (x, y), ...],或者没有元组的 x 和 y 坐标,例如[x1, y1, x2, y2, ...]。fill 参数为点着色,可以是 RGBA 元组或字符串,例如'red'。fill 参数是可选的。“point”在这里指的是像素,而不是字体大小的单位。
线条
line(xy, fill, width)方法用于绘制线条或一系列线条。xy 参数是一个元组列表,例如[(x, y), (x, y), ...],或者是一个整数列表,例如[x1, y1, x2, y2, ...]。每个点都是你绘制线条上的连接点。可选的 fill 参数指定线条的颜色,作为一个 RGBA 元组或颜色名称。可选的 width 参数确定线条的宽度,如果未指定,则默认为1。
矩形
rectangle(xy, fill, outline, width)方法用于绘制矩形。xy 参数是一个形式为(left, top, right, bottom)的矩形元组。左和顶的值指定了矩形左上角的 x 和 y 坐标,而右和底指定了右下角的坐标。可选的 fill 参数是矩形的内部颜色。可选的 outline 参数是矩形轮廓的颜色。可选的 width 参数代表线条的宽度,如果未指定,则默认为1。
椭圆
ellipse(xy, fill, outline, width)方法用于绘制椭圆。如果椭圆的宽度和高度相同,此方法将绘制一个圆。xy 参数是一个表示包含椭圆的矩形的元组(left, top, right, bottom)。可选的 fill 参数是椭圆内部的颜色,可选的 outline 参数是椭圆轮廓的颜色。可选的 width 参数是线条的宽度,如果未指定,则默认为1。
多边形
polygon(xy, fill, outline, width) 方法用于绘制任意多边形。xy 参数是一个元组列表,例如 [(x, y), (x, y), ...],或者整数,例如 [x1, y1, x2, y2, ...],代表多边形边的连接点。最后一对坐标将自动连接到第一对坐标。可选的 fill 参数是多边形内部的颜色,可选的 outline 参数是多边形轮廓的颜色。可选的 width 参数是线条的宽度,如果未指定,则默认为 1。
绘图示例
为了练习使用这些方法,请在交互式外壳中输入以下内容:
>>> from PIL import Image, ImageDraw
>>> im = Image.new('RGBA', (200, 200), 'white')
>>> draw = ImageDraw.Draw(im)
>>> draw.line([(0, 0), (199, 0), (199, 199), (0, 199), (0, 0)], fill='black') ❶
>>> draw.rectangle((20, 30, 60, 60), fill='blue') ❷
>>> draw.ellipse((120, 30, 160, 60), fill='red') ❸
>>> draw.polygon(((57, 87), (79, 62), (94, 85), (120, 90), (103, 113)), fill='brown') ❹
>>> for i in range(100, 200, 10): ❺
... draw.line([(i, 0), (200, i - 100)], fill='green')
>>> im.save('drawing.png')
在创建一个 200×200 的白色图像的 Image 对象后,将其传递给 ImageDraw.Draw() 以获取一个 ImageDraw 对象,并将 ImageDraw 对象存储在 draw 中,我们可以在 draw 上调用绘图方法。在这里,我们在图像的边缘创建了一个细黑轮廓 ❶,一个左上角在 (20, 30) 且右下角在 (60, 60) 的蓝色矩形 ❷,一个由 (120, 30) 到 (160, 60) 的矩形定义的红椭圆 ❸,一个有五个顶点的棕色多边形 ❹,以及用 for 循环绘制的绿色线条图案 ❺。生成的 drawing.png 文件将类似于图 21-14(尽管这本书中没有打印颜色)。

图 21-14:生成的 drawing.png 图像
您可以在 ImageDraw 对象上使用其他几个形状绘制方法。完整文档可在 pillow.readthedocs.io/en/latest/reference/ImageDraw.html 找到。
文本
ImageDraw 对象还有一个 text() 方法,用于在图像上绘制文本。此方法接受四个参数:
xy 一个由两个整数组成的元组,指定文本框的左上角
text 您想要写入的文本字符串
fill 文本的颜色
font 一个可选的 ImageFont 对象,用于设置文本的字形和大小
在我们使用 text() 在图像上绘制文本之前,让我们更详细地讨论可选的字体参数。此参数是一个 ImageFont 对象,您可以通过运行以下代码来获取:
>>> from PIL import ImageFont
一旦您导入了 Pillow 的 ImageFont 模块,通过调用 ImageFont.truetype() 函数来访问字体,该函数接受两个参数。第一个参数是一个表示字体的 TrueType 文件(即您硬盘上实际的字体文件)。TrueType 文件具有 .ttf 文件扩展名,通常位于 Windows 的 C:\Windows\Fonts,macOS 的 /Library/Fonts 和 /System/Library/Fonts,以及 Linux 的 /usr/share/fonts/truetype。您不需要将这些路径作为 TrueType 文件字符串的一部分输入,因为 Pillow 会自动搜索这些目录,但如果它无法找到您指定的字体,则会显示错误。
ImageFont.truetype() 的第二个参数是一个表示点大小的整数(而不是像素)。Pillow 默认创建每英寸 72 像素的 PNG 图像,而一个 点 是 1/72 英寸。为了练习,请在交互式外壳中输入以下内容:
>>> from PIL import Image, ImageDraw, ImageFont
>>> import os
>>> im = Image.new('RGBA', (200, 200), 'white') # ❶
>>> draw = ImageDraw.Draw(im) # ❷
>>> draw.text((20, 150), 'Hello', fill='purple') # ❸
>>> arial_font = ImageFont.truetype('arial.ttf', 32) # ❹
>>> draw.text((100, 150), 'Howdy', fill='gray', font=arial_font) # ❺
>>> im.save('text.png')
在导入 Image、ImageDraw、ImageFont 和 os 之后,我们创建了一个新的 200×200 白色图像的 Image 对象 ❶,并从 Image 对象创建了一个 ImageDraw 对象 ❷。我们使用 text() 在 (20, 150) 位置用紫色写入 Hello ❸。在这个调用中,我们没有传递可选的第四个参数,因此文本的字体和大小没有被定制。
接下来,为了设置字体和大小,我们调用 ImageFont.truetype(),将其所需的 .ttf 字体文件和一个整数字体大小 ❹ 传递给它。我们将返回的 Font 对象存储在一个变量中,然后将该变量传递给 text() 方法的最后一个关键字参数。这个方法调用将在 (100, 150) 位置用灰色绘制 32 点 Arial 字体的 Howdy ❺。生成的 text.png 文件看起来像图 21-15。

图 21-15:生成的 text.png 图像
如果你对使用 Python 创建计算机生成的艺术感兴趣,请查看特里斯坦·巴恩(No Starch Press,2021 年)的 Learn Python Visually 或我的书 The Recursive Book of Recursion(No Starch Press,2022 年)。
将图像复制和粘贴到剪贴板
正如第三方 pyperclip 模块允许你将文本字符串复制和粘贴到剪贴板一样,pyperclipimg 模块可以复制和粘贴 Pillow Image 对象。要安装 pyperclipimg,请参阅附录 A 中的说明。
pyperclipimg.copy() 函数接受一个 Pillow Image 对象并将其放在你的操作系统的剪贴板上。然后你可以将其粘贴到图形或图像处理程序,如 MS Paint。pyperclipimg.paste() 函数返回剪贴板上的图像内容,作为一个 Image 对象。在当前工作目录中包含 zophie.png,请在交互式外壳中输入以下内容:
>>> from PIL import Image
>>> im = Image.open('zophie.png')
>>> import pyperclipimg
>>> pyperclipimg.copy(im)
>>> pasted_im = pyperclipimg.paste() # Now copy a new image to the clipboard.
>>> # Paste the clipboard contents to a graphics program.
>>> pasted_im.show() # Shows the image from the clipboard
在此代码中,我们首先以 Image 对象的形式打开 zophie.png 图像,然后将其传递给 pyperclipimg.copy() 以将其复制到剪贴板。你可以通过将图像粘贴到图形程序中来验证复制是否成功。接下来,从图形程序复制一个新的图像,或者通过在网页浏览器中右键单击图像并复制它。调用 pyperclipimg.paste() 将返回一个包含在 pasted_im 变量中的 Image 对象。你可以通过使用 pasted_im.show() 来查看它以验证粘贴是否成功。
pyperclipimg 模块可以作为让用户将图像数据输入和输出到你的 Python 程序的一种方式。
使用 Matplotlib 创建图表
使用 Pillow 绘制自己的图表是可能的,但这需要大量的工作。Matplotlib 库为专业出版物创建了许多种类的图表。在本章中,我们将创建基本的折线图、柱状图、散点图和饼图,但 Matplotlib 也能创建更复杂的 3D 图表。您可以在 matplotlib.org 找到完整的文档。按照附录 A 中的说明安装 Matplotlib。
折线图和散点图
让我们从创建一个带有两个轴(x 和 y)的二维折线图开始。折线图非常适合展示一个量随时间的变化。在 Matplotlib 中,术语 plot、graph 和 chart 常常可以互换使用,而术语 figure 指的是包含一个或多个图表的窗口。将以下内容输入到交互式 shell 中:
>>> import matplotlib.pyplot as plt ❶
>>> x_values = [0, 1, 2, 3, 4, 5]
>>> y_values1 = [10, 13, 15, 18, 16, 20]
>>> y_values2 = [9, 11, 18, 16, 17, 19]
>>> plt.plot(x_values, y_values1) ❷
[<matplotlib.lines.Line2D object at 0x000002501D9A7D10>]
>>> plt.plot(x_values, y_values2)
[<matplotlib.lines.Line2D object at 0x00000250212AC6D0>]
>>> plt.savefig('linegraph.png') # Saves the plot as an image file
>>> plt.show() # Opens a window with the plot
>>> plt.show() # Does nothing
我们将 matplotlib.pyplot 以 plt 的名字导入❶,以便更容易地输入其函数。接下来,为了将数据点绘制到二维图中,我们必须调用 plt.plot() 函数。我们首先在 x_values 中保存一个整数或浮点数的列表用于 x 轴,然后在 y_values1 中保存一个整数或浮点数的列表用于 y 轴❷。x 轴和 y 轴列表中的第一个值相互关联,两个列表中的第二个值相互关联,依此类推。在用这些值调用 plt.plot() 之后,我们再次用 x_values 和 y_values2 调用它,以在图表中添加第二条线。
Matplotlib 会自动为线条选择颜色并为图表选择合适的大小。我们可以通过调用 plt.savefig('linegraph.png') 将默认图表保存为 PNG 图像。
Matplotlib 有一个预览功能,它在一个窗口中显示图表,就像 Pillow 有 show() 方法用于预览 Image 对象一样。调用 plt.show() 在窗口中打开图表。它看起来像图 21-16。

图 21-16:使用 plt.show() 显示的折线图
plt.show() 创建的窗口是交互式的:你可以移动图表或放大或缩小。左下角的房子图标重置视图,而软盘图标允许你将图表保存为图像文件。如果你正在实验数据,plt.show() 是一个方便的可视化工具。plt.show() 函数调用将阻塞,直到用户关闭此窗口才会返回。
当你关闭 plt.show() 方法创建的窗口时,你也会重置图表数据。再次调用 plt.show() 可能没有任何作用或显示一个空窗口。你必须再次调用 plt.plot() 和任何其他与绘图相关的函数来重新创建图表。要保存图表的图像文件,必须在调用 plt.show() 之前调用 plt.savefig()。
要创建相同数据的散点图,将 x 轴和 y 轴的值传递给 plt.scatter() 函数:
>>> import matplotlib.pyplot as plt
>>> x_values = [0, 1, 2, 3, 4, 5]
>>> y_values1 = [10, 13, 15, 18, 16, 20]
>>> y_values2 = [9, 11, 18, 16, 17, 19]
>>> plt.scatter(x_values, y_values1)
<matplotlib.collections.PathCollection object at 0x00000250212CBAD0>
>>> plt.scatter(x_values, y_values2)
<matplotlib.collections.PathCollection object at 0x000002502132DC10>
>>> plt.savefig('scatterplot.png')
>>> plt.show()
当你调用plt.show()时,Matplotlib 会在图 21-17 中显示图表。创建散点图的代码与创建线图的代码相同,只是函数调用不同。

图 21-17:使用 plt.show()显示的散点图
如果你将此图与图 21-16 中的线图进行比较,你会看到数据是相同的,尽管散点图使用点而不是连接的线。
柱状图和饼图
让我们使用 Matplotlib 创建一个基本的柱状图。柱状图用于比较不同类别中的相同类型的数据。与线图不同,类别的顺序并不重要,尽管它们通常按字母顺序列出。在交互式外壳中输入以下内容:
>>> import matplotlib.pyplot as plt
>>> categories = ['Cats', 'Dogs', 'Mice', 'Moose']
>>> values = [100, 200, 300, 400]
>>> plt.bar(categories, values)
<BarContainer object of 4 artists>
>>> plt.savefig('bargraph.png')
>>> plt.show()
此代码创建了图 21-18 中显示的柱状图。我们将 x 轴上的类别列表作为plt.bar()的第一个列表参数传递,并将每个类别的值作为第二个列表参数传递。

图 21-18:使用 plt.show()显示的柱状图
记住,关闭plt.show()窗口会重置图表数据。
要创建饼图,调用plt.pie()函数。与类别和值不同,饼图有标签和切片。在交互式外壳中输入以下内容:
>>> import matplotlib.pyplot as plt
>>> slices = [100, 200, 300, 400] # The size of each slice
>>> labels = ['Cats', 'Dogs', 'Mice', 'Moose'] # The name of each slice
>>> plt.pie(slices, labels=labels, autopct='%.1f%%')
(<matplotlib.patches.Wedge object at 0x00000218F32BA950>,
# --snip--
>>> plt.savefig('piechart.png')
>>> plt.show()
当你为饼图调用plt.show()时,Matplotlib 会在窗口中显示它,如图 21-19 所示。plt.pie()函数接受一个切片大小列表和每个切片的标签列表。
autopct参数指定了每个切片的百分比标签的精度。该参数是一个格式说明符字符串;'%.1f%%'字符串指定数字应显示小数点后一位数字。如果你在函数调用中省略此关键字参数,饼图将不会列出百分比文本。
![每个切片都有百分比和标签“dogs”,“cats”,“moose”和“mice”的饼图
图 21-19:使用 plt.show()显示的饼图
Matplotlib 会自动为每个切片选择颜色,但你可以自定义这种行为,以及你创建的图表的许多其他方面。
其他组件
我们在上一节中创建的图表相当基础。Matplotlib 有大量的附加功能,足以填满一本自己的书,所以我们只看最常见的组件。让我们向我们的图表添加数据点标记、自定义颜色和标签。在交互式外壳中输入以下内容:
>>> import matplotlib.pyplot as plt
>>> x_values = [0, 1, 2, 3, 4, 5]
>>> y_values1 = [10, 13, 15, 18, 16, 20]
>>> y_values2 = [9, 11, 18, 16, 17, 19]
>>> plt.plot(x_values, y_values1, marker='o', color='b', label='Line 1') # ❶
[<matplotlib.lines.Line2D object at 0x000001BC339D2F90>]
>>> plt.plot(x_values, y_values2, marker='s', color='r', label='Line 2')
[<matplotlib.lines.Line2D object at 0x000001BC339D1A90>]
>>> plt.legend() # ❷
<matplotlib.legend.Legend object at 0x000001BC20915B90>
>>> plt.xlabel('X-axis Label') # ❸
Text(0.5, 0, 'X-axis Label')
>>> plt.ylabel('Y-axis Label')
Text(0, 0.5, 'Y-axis Label')
>>> plt.title('Graph Title')
Text(0.5, 1.0, 'Graph Title')
>>> plt.grid(True) # ❹
>>> plt.show()
在运行此代码后,Matplotlib 显示一个类似于图 21-20 的窗口。它包含之前创建的相同线图,但我们已经向 plt.plot() 函数调用中添加了 marker、color 和 label 关键字参数 ❶。标记为每个数据点在线上创建一个点。'o' 值使点成为 O 形状的圆圈,而 's' 使其成为正方形。'b' 和 'r' 颜色参数将线条设置为蓝色和红色。我们为每条线提供一个标签,以便在调用 plt.legend() ❷ 创建的图例中使用。
我们通过调用 plt.xlabel()、plt.ylabel() 和 plt.title() ❸ 为 x 轴、y 轴和整个图表本身创建标签,传递标签文本作为字符串。最后,将 True 传递给 plt.grid() ❹ 启用带有 x 轴和 y 轴值的线条的网格。

图 21-20:带有附加组件的示例线图
这只是 Matplotlib 提供的功能的小样本。您可以在在线文档中了解其他功能。
线图和散点图
让我们从创建一个带有 x 和 y 轴的二维线图开始。线图非常适合显示一个量随时间的变化。在 Matplotlib 中,术语 plot、graph 和 chart 常常可以互换使用,而术语 figure 指的是包含一个或多个图表的窗口。在交互式 shell 中输入以下内容:
>>> import matplotlib.pyplot as plt ❶
>>> x_values = [0, 1, 2, 3, 4, 5]
>>> y_values1 = [10, 13, 15, 18, 16, 20]
>>> y_values2 = [9, 11, 18, 16, 17, 19]
>>> plt.plot(x_values, y_values1) ❷
[<matplotlib.lines.Line2D object at 0x000002501D9A7D10>]
>>> plt.plot(x_values, y_values2)
[<matplotlib.lines.Line2D object at 0x00000250212AC6D0>]
>>> plt.savefig('linegraph.png') # Saves the plot as an image file
>>> plt.show() # Opens a window with the plot
>>> plt.show() # Does nothing
我们将 matplotlib.pyplot 导入为 plt ❶ 以便更容易输入其函数。接下来,为了将数据点绘制到二维图中,我们必须调用 plt.plot() 函数。我们首先在 x_values 中保存一个整数或浮点数的列表用于 x 轴,然后在 y_values1 中保存一个整数或浮点数的列表用于 y 轴 ❷。x 轴和 y 轴列表中的第一个值相互关联,两个列表中的第二个值相互关联,依此类推。在用这些值调用 plt.plot() 后,我们再次用 x_values 和 y_values2 调用它,以在图表中添加第二条线。
Matplotlib 会自动选择线条颜色和图表的适当大小。我们可以通过调用 plt.savefig('linegraph.png') 将默认图表保存为 PNG 图像。
Matplotlib 具有一个预览功能,可以在窗口中显示图表,这与 Pillow 的 show() 方法用于预览 Image 对象类似。通过调用 plt.show() 在窗口中打开图表。它看起来像图 21-16。

图 21-16:使用 plt.show() 显示的线图
plt.show()创建的窗口是交互式的:你可以移动图表或放大或缩小。左下角的房子图标重置视图,软盘图标允许你将图表保存为图像文件。如果你正在实验数据,plt.show()是一个方便的可视化工具。plt.show()函数调用将阻塞,直到用户关闭此窗口。
当你关闭plt.show()方法创建的窗口时,你也会重置图表数据。第二次调用plt.show()要么没有任何作用,要么显示一个空窗口。你必须再次调用plt.plot()和任何其他与绘图相关的函数来重新创建图表。要保存图表的图像文件,必须在调用plt.show()之前调用plt.savefig()。
要创建相同数据的散点图,将 x 轴和 y 轴的值传递给plt.scatter()函数:
>>> import matplotlib.pyplot as plt
>>> x_values = [0, 1, 2, 3, 4, 5]
>>> y_values1 = [10, 13, 15, 18, 16, 20]
>>> y_values2 = [9, 11, 18, 16, 17, 19]
>>> plt.scatter(x_values, y_values1)
<matplotlib.collections.PathCollection object at 0x00000250212CBAD0>
>>> plt.scatter(x_values, y_values2)
<matplotlib.collections.PathCollection object at 0x000002502132DC10>
>>> plt.savefig('scatterplot.png')
>>> plt.show()
当你调用plt.show()时,Matplotlib 会在图 21-17 中显示该图表。创建散点图的代码与创建线图的代码相同,只是函数调用不同。

图 21-17:使用 plt.show()显示的散点图
如果你将此图与图 21-16 中的线图进行比较,你会看到数据是相同的,尽管散点图使用点而不是连接的线条。
条形图和饼图
让我们使用 Matplotlib 创建一个基本的条形图。条形图用于比较不同类别中的相同类型的数据。与线图不同,类别的顺序并不重要,尽管它们通常按字母顺序列出。将以下内容输入到交互式外壳中:
>>> import matplotlib.pyplot as plt
>>> categories = ['Cats', 'Dogs', 'Mice', 'Moose']
>>> values = [100, 200, 300, 400]
>>> plt.bar(categories, values)
<BarContainer object of 4 artists>
>>> plt.savefig('bargraph.png')
>>> plt.show()
此代码创建了图 21-18 中显示的条形图。我们将要在 x 轴上列出的类别作为plt.bar()的第一个列表参数传递,并将每个类别的值作为第二个列表参数传递。

图 21-18:使用 plt.show()显示的条形图
记住,关闭plt.show()窗口会重置图表数据。
要创建饼图,调用plt.pie()函数。与类别和值不同,饼图有标签和切片。将以下内容输入到交互式外壳中:
>>> import matplotlib.pyplot as plt
>>> slices = [100, 200, 300, 400] # The size of each slice
>>> labels = ['Cats', 'Dogs', 'Mice', 'Moose'] # The name of each slice
>>> plt.pie(slices, labels=labels, autopct='%.1f%%')
(<matplotlib.patches.Wedge object at 0x00000218F32BA950>,
# --snip--
>>> plt.savefig('piechart.png')
>>> plt.show()
当你为饼图调用plt.show()时,Matplotlib 会在窗口中显示它,如图 21-19 所示。plt.pie()函数接受一个切片大小列表和一个每个切片的标签列表。
autopct参数指定了每个切片的百分比标签的精度。该参数是一个格式说明符字符串;'%.1f%%'字符串指定数字应显示小数点后一位数字。如果你在函数调用中省略此关键字参数,饼图将不会列出百分比文本。
![每个切片中都有百分比和标签“dogs”、“cats”、“moose”和“mice”的饼图。
图 21-19:使用 plt.show()显示的饼图
Matplotlib 会自动为每个切片选择颜色,但你可以自定义这种行为,以及你创建的图表的许多其他方面。
其他组件
我们在上一节中创建的图表相当基础。Matplotlib 拥有大量的其他功能,足以填满一本自己的书,所以我们只查看最常见的组件。让我们向我们的图表添加数据点标记、自定义颜色和标签。在交互式外壳中输入以下内容:
>>> import matplotlib.pyplot as plt
>>> x_values = [0, 1, 2, 3, 4, 5]
>>> y_values1 = [10, 13, 15, 18, 16, 20]
>>> y_values2 = [9, 11, 18, 16, 17, 19]
>>> plt.plot(x_values, y_values1, marker='o', color='b', label='Line 1') # ❶
[<matplotlib.lines.Line2D object at 0x000001BC339D2F90>]
>>> plt.plot(x_values, y_values2, marker='s', color='r', label='Line 2')
[<matplotlib.lines.Line2D object at 0x000001BC339D1A90>]
>>> plt.legend() # ❷
<matplotlib.legend.Legend object at 0x000001BC20915B90>
>>> plt.xlabel('X-axis Label') # ❸
Text(0.5, 0, 'X-axis Label')
>>> plt.ylabel('Y-axis Label')
Text(0, 0.5, 'Y-axis Label')
>>> plt.title('Graph Title')
Text(0.5, 1.0, 'Graph Title')
>>> plt.grid(True) # ❹
>>> plt.show()
运行此代码后,Matplotlib 显示一个类似于图 21-20 的窗口。它包含之前创建的相同折线图,但我们已经向plt.plot()函数调用中添加了marker、color和label关键字参数 ❶。标记为线上的每个数据点创建一个点。'o'值使点成为一个 O 形圆圈,而's'则使其成为一个正方形。'b'和'r'颜色参数分别将线条设置为蓝色和红色。我们为每条线提供一个标签,以便在调用plt.legend() ❷时创建的图例中使用。
我们还通过调用plt.xlabel()、plt.ylabel()和plt.title() ❸为 x 轴、y 轴和整个图表本身创建标签,将标签文本作为字符串传递。最后,将True传递给plt.grid() ❹启用带有沿 x 轴和 y 轴值的线条的网格。

图 21-20:带有附加组件的示例折线图
这只是 Matplotlib 提供功能的小样本。你可以在在线文档中了解其他功能。
摘要
图像由像素集合组成,每个像素都有一个表示其颜色的 RGBA 值以及一组表示其位置的 x 和 y 坐标。两种常见的图像格式是 JPEG 和 PNG。Pillow 可以处理这两种图像格式以及其他格式。
当程序将图像加载到Image对象中时,其宽度和高度维度以两个整数的元组形式存储在size属性中。Image数据类型的对象还具有用于常见图像操作的方法:crop()、copy()、paste()、resize()、rotate()和transpose()。要将Image对象保存到图像文件,请调用save()方法。
如果你想让你的程序在图像上绘制形状,请使用ImageDraw方法绘制点、线、矩形、椭圆和多边形。该模块还提供了以你选择的字体和字号绘制文本的方法。
虽然 Pillow 库允许您绘制形状和单个像素,但使用 Matplotlib 库生成图表更容易。您可以使用 Matplotlib 的默认设置创建线图、条形图和饼图,或者您可以进行特定的自定义。show()方法在屏幕上显示图表以进行预览,而save()方法生成可以包含在文档或电子表格中的图像文件。库的在线文档可以告诉您更多关于其丰富功能的信息。
尽管像 Photoshop 这样的高级(且昂贵)的应用程序提供了自动批量处理功能,但您可以使用 Python 脚本来免费执行许多相同的修改。在前几章中,您编写了 Python 程序来处理纯文本文件、电子表格、PDF 和其他格式。使用 Pillow,您已经扩展了您的编程能力,可以处理图像了!
实践题目
1. 什么是 RGBA 值?
2. 您如何从 Pillow 模块中获取'CornflowerBlue'的 RGBA 值?
3. 什么是箱元组?
4. 哪个函数返回名为zophie.png的图像文件的Image对象?
5. 您如何找出Image对象图像的宽度和高度?
6. 您会称什么方法来获取一个 100×100 图像左下四分之一的Image对象?
7. 在修改Image对象后,您如何将其保存为图像文件?
8. 哪个模块包含 Pillow 的形状绘制代码?
9. Image对象没有绘图方法。哪种对象有?您如何获取这种对象?
10. 哪些 Matplotlib 函数可以创建折线图、散点图、条形图和饼图?
11. 您如何将 Matplotlib 图形保存为图像?
12. plt.show()函数的作用是什么,为什么不能连续调用两次?
实践程序
为了练习,编写程序来完成以下任务。
瓦片制作器
编写一个程序,从单个图像生成平铺图像,就像图 21-6 中的猫脸瓷砖一样。您的程序应该有一个make_tile()函数,该函数有三个参数:一个图像文件名的字符串,一个整数表示水平平铺的次数,以及一个整数表示垂直平铺的次数。make_tile()函数应该返回一个更大的平铺图像的Image对象。您将在这个函数中使用paste()方法。
例如,如果zophie_the_cat.jpg是一个 20×50 像素的图像,调用make_tile('zophie_the_cat.jpg', 6, 10)应该返回一个 120×500 像素的图像,总共有 60 个瓷砖。作为加分项,尝试在粘贴到较大图像时随机翻转或旋转图像。这种瓷砖制作器最适合较小的图像进行平铺。看看您可以用这段代码创建什么样的抽象艺术。
识别硬盘上的照片文件夹
我有一个坏习惯,就是将文件从我的数码相机传输到硬盘上某个地方的临时文件夹,然后忘记这些文件夹。写一个能够扫描整个硬盘并找到这些遗留照片文件夹的程序会很好。
编写一个程序,遍历你的硬盘上的每个文件夹,并找到潜在的图片文件夹。当然,首先你必须定义你认为是“图片文件夹”的是什么;让我们说,它是指超过一半的文件是照片的任何文件夹。那么你如何定义照片文件呢?首先,照片文件必须有文件扩展名.png或.jpg。此外,照片是大型图像;照片文件的宽度和高度都必须大于 500 像素。这是一个安全的赌注,因为大多数数码相机的照片宽度和高度都是几千像素。
作为提示,以下是这个程序可能的大致框架:
# Import modules and write comments to describe this program.
for folder_name, subfolders, filenames in os.walk('C:\\'):
num_photo_files = 0
num_non_photo_files = 0
for filename in filenames:
# Check if the file extension isn't .png or .jpg.
if TODO:
num_non_photo_files += 1
continue # Skip to the next filename.
# Open image file using Pillow.
# Check if the width & height are larger than 500.
if TODO:
# Image is large enough to be considered a photo.
num_photo_files += 1
else:
# Image is too small to be a photo.
num_non_photo_files += 1
# If more than half of files were photos,
# print the absolute path of the folder.
if TODO:
print(TODO)
当程序运行时,它应该在屏幕上打印任何照片文件夹的绝对路径。
创建自定义座位卡
在第十七章的练习程序中,你从 plaintext 文件中的客人列表创建自定义邀请。作为一个附加项目,使用 Pillow 创建将作为客人自定义座位卡的图像。对于书中在线资源中的guests.txt文件中列出的每位客人,生成一个包含客人姓名和一些花卉装饰的图像文件。书中资源中还有一个公共领域的花卉图像。
为了确保每张座位卡的大小相同,请在邀请图像的边缘添加一个黑色矩形;这样,当你打印图像时,你将有一个切割的指南。Pillow 生成的 PNG 文件设置为每英寸 72 像素,所以一个 4×5 英寸的卡片将需要一个 288×360 像素的图像。
瓦片制作器
编写一个程序,从单个图像生成瓦片图像,就像图 21-6 中的猫脸瓦片一样。你的程序应该有一个make_tile()函数,它有三个参数:图像文件名的字符串,一个表示水平瓦片次数的整数,以及一个表示垂直瓦片次数的整数。make_tile()函数应该返回一个更大的Image对象,表示瓦片图像。你将在这个函数中使用paste()方法。
例如,如果zophie_the_cat.jpg是一个 20×50 像素的图像,调用make_tile('zophie_the_cat.jpg', 6, 10)应该返回一个 120×500 像素的图像,总共有 60 个瓦片。作为加分项,尝试在粘贴到较大图像时随机翻转或旋转图像。这个瓦片制作器最适合较小的图像进行瓦片。看看你能用这段代码创造出什么样的抽象艺术。
在硬盘上识别照片文件夹
我有一个坏习惯,就是将文件从我的数码相机传输到硬盘上某个地方的临时文件夹,然后忘记这些文件夹。写一个能够扫描整个硬盘并找到这些遗留照片文件夹的程序会很好。
编写一个程序,遍历你的硬盘上的每个文件夹,并找到潜在的照片文件夹。当然,首先你必须定义你认为是“照片文件夹”的是什么;让我们说,它是任何文件中超过一半是照片的文件夹。那么你如何定义哪些文件是照片呢?首先,照片文件必须有文件扩展名.png或.jpg。此外,照片是大型图像;照片文件的宽度和高度都必须大于 500 像素。这是一个安全的赌注,因为大多数数码相机的照片宽度和高度都是几千像素。
作为提示,以下是这个程序可能的大致框架:
# Import modules and write comments to describe this program.
for folder_name, subfolders, filenames in os.walk('C:\\'):
num_photo_files = 0
num_non_photo_files = 0
for filename in filenames:
# Check if the file extension isn't .png or .jpg.
if TODO:
num_non_photo_files += 1
continue # Skip to the next filename.
# Open image file using Pillow.
# Check if the width & height are larger than 500.
if TODO:
# Image is large enough to be considered a photo.
num_photo_files += 1
else:
# Image is too small to be a photo.
num_non_photo_files += 1
# If more than half of files were photos,
# print the absolute path of the folder.
if TODO:
print(TODO)
当程序运行时,它应该在屏幕上打印任何照片文件夹的绝对路径。
创建定制座位卡
在第十七章的练习程序中,你从一份嘉宾名单的纯文本文件中创建了自定义邀请函。作为额外项目,使用 Pillow 创建将作为嘉宾定制座位卡的图片。对于书中在线资源中guests.txt文件列出的每位嘉宾,生成一个包含嘉宾姓名和一些花哨装饰的图片文件。书中资源中也有一个公共领域的花卉图片。
为了确保每张座位卡大小相同,在邀请函图片的边缘添加一个黑色矩形;这样,当你打印图片时,你将有一个裁剪的指南。Pillow 生成的 PNG 文件设置为每英寸 72 像素,所以一个 4×5 英寸的卡片将需要一个 288×360 像素的图片。


浙公网安备 33010602011771号