首页 » Python编程快速上手 » Python编程快速上手全文在线阅读

《Python编程快速上手》第17章 操作图像

关灯直达底部

如果你有一台数码相机,或者只是将照片从手机上传到Facebook,你可能随时都会偶然遇到数字图像文件。你可能知道如何使用基本的图形软件,如Microsoft Paint或Paintbrush,甚至更高级的应用程序,如Adobe Photoshop。但是,如果需要编辑大量的图像,手工编辑可能是漫长、枯燥的工作。

请用Python。Pillow是一个第三方Python模块,用于处理图像文件。该模块包含一些函数,可以很容易地裁剪图像、调整图像大小,以及编辑图像的内容。可以像Microsoft Paint或Adobe Photoshop 一样处理图像,有了这种能力,Python 可以轻松地自动编辑成千上万的图像。

17.1 计算机图像基础

为了处理图像,你需要了解计算机如何处理图像中的顔色和坐标的基本知识,以及如何在Pillow中处理颜色和坐标。但在继续探讨之前,先要安装pillow模块。安装第三方模块请见附录A。

17.1.1 颜色和RGBA值

计算机程序通常将图像中的颜色表示为RGBA值。RGBA值是一组数字,指定顔色中的红、绿、蓝和alpha(透明度)的值。这些值是从0(根本没有)到255(最高)的整数。这些RGBA值分配给单个像素,像素是计算机屏幕上能显示一种顔色的最小点(你可以想到,屏幕上有几百万像素)。像素的RGB设置准确地告诉它应该显示哪种颜色的色彩。图像也有一个alpha值,用于生成RGBA值。如果图像显示在屏幕上,遮住了背景图像或桌面墙纸,alpha值决定了“透过”这个图像的象素,你可以看到多少背景。

在Pillow中,RGBA值表示为四个整数值的元组。例如,红色表示为(255,0,0,255)。这种颜色中红的值为最大,没有绿和蓝,并且alpha值最大,这意味着它完全不透明。绿色表示为(0,255,0,255),蓝色是(0,0,255,255)。白色是各种颜色的组合,即(255,255,255,255),而黑色没有任何颜色,是(0,0,0,255)。

如果颜色的alpha值为0,不论RGB值是什么,该颜色是不可见的。毕竟,不可见的红色看起来就像不可见的黑色一样。

Pillow使用了HTML使用的标准颜色名称。表17-1列出了一些标准颜色的名称和值。

表17-1 标准颜色名称及其RGB值

名称

RGBA值

名称

RGBA值

White

(255, 255, 255, 255)

Red

(255, 0, 0, 255)

Green

(0, 128, 0, 255)

Blue

(0, 0, 255, 255)

Gray

(128, 128, 128, 255)

Yellow

(255, 255, 0, 255)

Black

(0, 0, 0, 255)

Purple

(128, 0, 128, 255)

Pillow提供ImageColor.getcolor函数,所以你不必记住想用的顔色的RGBA值。该函数接受一个颜色名称字符串作为第一个参数,字符串'RGBA'作为第二个参数,返回一个RGBA元组。

要了解该函数的工作方式,就在交互式环境中输入以下代码:

❶ >>> 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导入ImageColor模块❶(不是从Pillow,稍后你就会明白为什么)。传递给ImageColor.getcolor的颜色名称字符串是不区分大小写的,所以传入'red'❷和传入'RED'❸将得到同样的RGBA元组。还可以传递更多的不常见的顔色名称,如'chocolate'和'Cornflower Blue'。

Pillow支持大量的颜色名称,从'aliceblue'到'whitesmoke'。在http://nostarch. com/automatestuff/的资源中,可以找到超过100种标准颜色名称的完整列表。

17.1.2 坐标和Box元组

图像像素用x和y坐标指定,分别指定像素在图像中的水平和垂直位置。原点是位于图像左上角的像素,用符号(0,0)指定。第一个0表示x坐标,它以原点处为0,从左至右增加。第二个0表示y坐标,它以原点处为0,从上至下增加。这值得重复一下:y坐标向下走增加,你可能还记得数学课上使用的y坐标,与此相反。图17-1展示了这个坐标系统的工作方式。

图17-1 27×26的图像的x和y坐标,某种古老的数据存储装置

CMYK和RGB着色

上小学时你学过,混合红、黄、蓝三种顔料可以得到其他颜色。例如,可以混合蓝色和黄色,得到绿色顔料。这就是所谓的减色模型,它适用于染料、油墨和颜料。这就是为什么彩色打印机有的CMYK墨盒:青色(蓝色)、品红色(红色)、黄色和黑色墨水可以混合在一起,形成任何颜色。

然而,光的物理使用所谓的加色模型。如果组合光(例如由计算机屏幕发出的光),红、绿和蓝光可以组合形成其他颜色。这就是为什么在计算机程序中使用RGB值表示颜色。

许多Pillow函数和方法需要一个矩形元组参数。这意味着Pillow需要一个四个整坐标的元组,表示图像中的一个矩形区域。四个整数按顺序分别是:

  • 左:该矩形的最左边的x坐标。
  • 顶:该矩形的顶边的y坐标。
  • 右:该矩形的最右边右面一个像素的x坐标。此整数必须比左边整数大。
  • 底:该矩形的底边下面一个像素的y坐标。此整数必须比顶边整数大。

注意,该矩形包括左和顶坐标,直到但不包括右和底坐标。例如,矩形元组(3, 1, 9, 6)表示图17-2中黑色矩形的所有像素。

图17-2 由矩形元组(3, 1, 9, 6)表示的区域

17.2 用Pillow操作图像

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

图17-3 我的猫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('C://folder_with_image_file')  

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

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

17.2.1 处理Image数据类型

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

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

 >>> from PIL import Image >>> catIm = Image.open('zophie.png') >>> catIm.size❶ (816, 1088)❷ >>> width, height = catIm.size❸ >>> width 816❹ >>> height 1088 >>> catIm.filename 'zophie.png' >>> catIm.format 'PNG' >>> catIm.format_description 'Portable network graphics'❺ >>> catIm.save('zophie.jpg')  

从Zophie.png得到一个Image对象并保存在catIm中后,我们可以看到该对象的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函数的返回值作为这个参数。另外,Image.new也支持传入标准颜色名称的字符串。

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

 >>> 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')  

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

17.2.2 裁剪图片

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

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

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

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

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

17.2.3 复制和粘贴图像到其他图像

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

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

catIm和catCopyIm变量包含了两个独立的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粘贴到catCopyIm。paste方法有两个参数:一个“源”Image对象,一个包含x和y坐标的元组,指明源Image对象粘贴到主Image对象时左上角的位置。这里,我们在catCopyIm上两次调用paste,第一次传入(0, 0),第二次传入(400, 500)。这将faceIm两次粘贴到catCopyIm:一次faceIm的左上角在(0, 0),一次faceIm的左上角在(400, 500)。最后,我们将修改后的catCopyIm存入pasted.png。pasted.png如图17-5所示。

图17-5 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 0 430 0 645 0 860 0 1075 230 0 230 215 --snip-- 690 860 690 1075 >>> catCopyTwo.save('tiled.png')  

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

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

17.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元组中的两个值赋给变量width和height❶。使用width和height,而不是catIm.size[0]和catIm.size[1],让接下来的代码更易读。

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

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

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

粘贴透明像素

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

17.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所示。

图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的右侧的图像)。

图17-8 图像普通旋转6度(左),以及使用expand=True(右)

利用transpose方法,还可以得到图像的“镜像翻转”。必须向transpose方法传入Image.FLIP_LEFT_RIGHT或Image.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所示。

图17-9 原始图像(左),水平翻转(中),垂直翻转(右)

17.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')  

在❶,我们得到一个新图像,这是一个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。

图17-10 putPixel.png中的图像

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

17.3 项目:添加徽标

假设你有一项无聊的工作,要调整数千张图片的大小,并在每张图片的角上增加一个小徽标水印。使用基本的图形程序,如Paintbrush或Paint,完成这项工作需要很长时间。像Photoshop这样神奇的应用程序可以批量处理,但这个软件要花几百美元。让我们写一个脚本来完成工作。

假定图 17-11 是要添加到每个图像右下角的标识:带有白色边框的黑猫图标,图像的其余部分是透明的。

图17-11 添加到图像中的徽标

总的来说,程序应该完成下面的事:

  • 载入徽标图像。
  • 循环遍历工作目标中的所有.png和.jpg文件。
  • 检查图片是否宽于或高于300像素。
  • 如果是,将宽度或高度中较大的一个减小为300像素,并按比例缩小的另一维度。
  • 在角上粘贴徽标图像。
  • 将改变的图像存入另一个文件夹。

这意味着代码需要做到以下几点:

  • 打开catlogo.png文件作为Image对象。
  • 循环遍历os.listdir('.')返回的字符串。
  • 通过size属性取得图像的宽度和高度。
  • 计算调整后图像的新高度和宽度。
  • 调用resize方法来调整图像大小。
  • 调用paste方法来粘贴徽标。
  • 调用save方法保存更改,使用原来的文件名。

第1步:打开徽标图像

针对这个项目,打开一个新的文件编辑器窗口,输入以下代码,并保存为resizeAndAddLogo.py:

 #! python3 # resizeAndAddLogo.py - Resizes all images in current working directory to fit # in a 300x300 square, and adds catlogo.png to the lower-right corner. import os from PIL import Image❶ SQUARE_FIT_SIZE = 300❷ LOGO_FILENAME = 'catlogo.png'❸ logoIm = Image.open(LOGO_FILENAME)❹ logoWidth, logoHeight = logoIm.size # TODO: Loop over all files in the working directory. # TODO: Check if 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对象从Image.open返回❸。为了增强可读性,logoWidth和logoHeight被赋予logoIm.size中的值❹。

该程序的其余部分目前是TODO注释,说明了程序的骨架。

第2步:遍历所有文件并打开图像

现在,需要找到当前工作目录中的每个PNG文件和.jpg文件。请注意,你不希望将徽标图像添加到徽标图像本身,所以程序应该跳过所有像LOGO_FILENAME这样的图像文件名。在程序中添加以下代码:

 #! python3 # resizeAndAddLogo.py - Resizes all images in current working directory to fit # in a 300x300 square, and adds catlogo.png to the lower-right 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已存在时抛出异常。在用os.listdir('.')遍历工作目录中的所有文件时❶,较长的if语句❷检查每个filename是否以.png或.jpg结束。如果不是,或者该文件是徽标图像本身,循环就跳过它,使用continue❸去处理下一个文件。如果filename确实以'.png'或'.jpg'结束(而且不是徽标文件),可以将它打开为一个Image对象❹,并设置width和height。

第3步:调整图像的大小

只在有宽或高超过SQUARE_FIT_SIZE时(在这个例子中,是300像素),该程序才应该调整图像的大小,所以将所有大小调整的代码放在一个检查width和height变量的if语句内。在程序中添加以下代码:

 #! python3 # resizeAndAddLogo.py - Resizes all images in current working directory to fit # in a 300x300 square, and adds catlogo.png to the lower-right corner. import os from PIL import Image --snip--     # Check if 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('Resizing %s...' % (filename))❸  im = im.resize((width, height)) --snip--  

如果图像确实需要调整大小,就需要弄清楚它是太宽还是太高。如果width大于height,则高度应该根据宽度同比例减小❶。这个比例是当前宽度除以SQUARE_ FIT_SIZE的值。新的高度值是这个比例乘以当前高度值。由于除法运算符返回一个浮点值,而resize要求的尺寸是整数,所以要记得将结果用int函数转换成整数。最后,新的width值就设置为SQUARE_FIT_SIZE。

如果height大于或等于width(这两种情况都在else子句中处理),那么进行同样的计算,只是交换height和width变量的位置❷。

在width和height包含新图像尺寸后,将它们传入resize方法,并返回的Image对象保存在im中❸。

第4步:添加徽标,并保存更改

不论图像是否调整大小,徽标仍应粘贴到右下角。徽标粘贴的确切位置取决于图像的大小和徽标的大小。图 17-12 展示了如何计算粘贴的位置。粘贴徽标的左坐标将是图像宽度减去徽标宽度,顶坐标将是图像高度减去徽标高度。

图17-12 在右下角放置徽标的左坐标和顶坐标,应该是图像的宽度/高度减去徽标宽度/高度

代码将徽标粘贴到图像中后,应保存修改后的Image对象。将以下代码添加到程序中:

 #! python3 # resizeAndAddLogo.py - Resizes all images in current working directory to fit # in a 300x300 square, and adds catlogo.png to the lower-right corner. import os from PIL import Image --snip--     # Check if image needs to be resized.     --snip--     # Add the logo.❶     print('Adding logo to %s...' % (filename))❷     im.paste(logoIm, (width - logoWidth, height - logoHeight), logoIm)     # Save changes.❸     im.save(os.path.join('withLogo', filename))  

新的代码输出一条消息,告诉用户徽标已被加入❶,将logoIm粘贴到im中计算的坐标处❷,并将变更保存到withLogo目录的filename中❸。如果运行这个程序,zophie.png文件是工作目录中唯一的图像,输出会是这样:

Resizing zophie.png...Adding logo to zophie.png...  

图像zophie.png将变成225×300像素的图像,如图17-13所示。请记住,如果没有传入logoIm作为第三个参数,paste方法不会粘贴透明的像素。这个程序可以在短短几分钟内自动调整几百幅图像,并“加上徽标”。

图17-13 图像zophie.png调整了大小并加上了徽标(左)。如果忘记了第三个参数,
徽标中透明的像素将被复制为不透明的白色像素(右)

第5步:类似程序的想法

能够批量合成图像或修改图像大小,在许多应用中都有用。可以编写类似的程序,完成以下任务:

  • 为图像添加文字或网站URL。
  • 为图像添加时间戳。
  • 根据图像的大小,将图像复制或移动到不同的文件夹中。
  • 为图像添加一个几乎透明的水印,防止他人复制。

17.4 在图像上绘画

如果需要在图像上画线、矩形、圆形或其他简单形状,就用Pillow的ImageDraw模块。在交互式环境中输入以下代码:

>>> from PIL import Image, ImageDraw>>> im = Image.new('RGBA', (200, 200), 'white')>>> draw = ImageDraw.Draw(im)  

首先,我们导入Image和ImageDraw。然后,创建一个新的图像,在这个例子中,是200×200的白色图像,将这个Image对象保存在Im中。我们将该Image对象传入ImageDraw.Draw函数,得到一个ImageDraw对象。这个对象有一些方法,可以在Image对象上绘制形状和文字。将ImageDraw对象保存在变量draw中,这样就能在接下来的例子中方便地使用它。

17.4.1 绘制形状

下面的ImageDraw方法在图像上绘制各种形状。这些方法的fill和outline参数是可选的,如果未指定,默认为白色。

point(xy, fill)方法绘制单个像素。xy参数表示要画的点的列表。该列表可以是x和y坐标的元组的列表,例如[(x, y), (x, y), ...],或是没有元组的x和y坐标的列表,例如[x1, y1, x2, y2, ...]。fill参数是点的颜色,要么是一个RGBA元组,要么是颜色名称的字符串,如'red'。fill参数是可选的。

线

line(xy, fill, width)方法绘制一条线或一系列的线。xy要么是一个元组的列表,例如[(x, y), (x, y), ...],要么是一个整数列表,例如[x1, y1, x2, y2, ...]。每个点都是正在绘制的线上的一个连接点。可选的fill参数是线的颜色,是一个RGBA元组或颜色名称。可选的width参数是线的宽度,如果未指定,缺省值为1。

矩形

rectangle(xy, fill, outline)方法绘制一个矩形。xy参数是一个矩形元组,形式为(left, top, right, bottom)。left和top值指定了矩形左上角的x和y坐标,right和bottom指定了矩形的右下角。可选的fill参数是颜色,将填充该矩形的内部。可选的outline参数是矩形轮廓的颜色。

椭圆

ellipse(xy, fill, outline)方法绘制一个椭圆。如果椭圆的宽度和高度一样,该方法将绘制一个圆。xy参数是一个矩形元组(left, top, right, bottom),它表示正好包含该椭圆的矩形。可选的fill参数是椭圆内的颜色,可选的outline参数是椭圆轮廓的颜色。

多边形

polygon(xy, fill, outline)方法绘制任意的多边形。xy参数是一个元组列表,例如[(x, y), (x, y), ...],或者是一个整数列表,例如[x1, y1, x2, y2, ...],表示多边形边的连接点。最后一对坐标将自动连接到第一对坐标。可选的fill参数是多边形内部的颜色,可选的outline参数是多边形轮廓的颜色。

绘制示例

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

 >>> 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对象,将它保存在draw中,可以对draw调用绘图方法。这里,我们在图像边缘画上窄的黑色轮廓❶;一个蓝色的矩形,左上角在(20, 30),右下角在(60, 60)❷;一个红色的椭圆,由(120, 30)到(160, 60)的矩形来定义❸;一个棕色的多边形,有五个顶点❹,以及一些绿线的图案,用for循环绘制❺。得到的drawing.png文件如图17-14所示。

图17-14 得到的图像drawing.png

ImageDraw对象还有另外几个绘制形状的方法。完整的文档在http://pillow. readthedocs.org/en/latest/reference/ImageDraw.html。

17.4.2 绘制文本

ImageDraw对象还有text方法,用于在图像上绘制文本。text方法有4个参数:xy、text、fill和font。

  • xy参数是两个整数的元组,指定文本区域的左上角。
  • text参数是想写入的文本字符串。
  • 可选参数fill是文本的颜色。
  • 可选参数font是一个ImageFont对象,用于设置文本的字体和大小。下一节中更详细地介绍了这个参数。

因为通常很难预先知道一块文本在给定的字体下的大小,所以ImageDraw模块也提供了textsize方法。它的第一个参数是要测量的文本字符串,第二个参数是可选的ImageFont对象。textsize方法返回一个两整数元组,表示如果以指定的字体写入图像,文本的宽度和高度。可以利用这个宽度和高度,帮助你精确计算文本放在图像上的位置。

text的前三个参数非常简单。在用text向图像绘制文本之前,让我们来看看可选的第四个参数,即ImageFont对象。

text和textsize都接受可选的ImageFont对象,作为最后一个参数。要创建这种对象,先执行以下命令:

>>> from PIL import ImageFont  

既然已经导入Pillow的ImageFont模块,就可以调用ImageFont.truetype函数,它有两个参数。第一个参数是字符串,表示字体的TrueType文件,这是硬盘上实际的字体文件。TrueType字体文件具有.TTF文件扩展名,通常可以在以下文件夹中找到:

  • 在Windows上:C:/Windows/Fonts。
  • 在OS X上:/Library/Fonts and /System/Library/Fonts。
  • 在Linux上:/usr/share/fonts/truetype。

实际上并不需要输入这些路径作为TrueType字体文件的字符串的一部分,因为Python知道自动在这些目录中搜索字体。如果无法找到指定的字体,Python会显示错误。

ImageFont.truetype的第二个参数是一个整数,表示字体大小的点数(而不是像素)。请记住,Pillow创建的PNG图像默认是每英寸72像素,一点是1/72英寸。

在交互式环境中输入以下代码,用你的操作系统中实际的文件夹名称替换FONT_FOLDER:

 >>> 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') >>> fontsFolder = 'FONT_FOLDER' # e.g. ‘/Library/Fonts’❹ >>> arialFont = ImageFont.truetype(os.path.join(fontsFolder, 'arial.ttf'), 32)❺ >>> draw.text((100, 150), 'Howdy', fill='gray', font=arialFont) >>> im.save('text.png')  

导入Image、ImageDraw、ImageFont和os后,我们生成一个Image对象,是新的200×200白色图像❶,并通过这个Image对象得到一个ImageDraw对象❷。我们使用text在(20, 150)以紫色绘制Hello❸。在这次text调用中,我们没有传入可选的第四个参数,所以这段文本的字体和大小没有定制。

要设置字体和大小,我们首先将文件夹名称(如/Library/Fonts)保存在fontsFolder中。然后调用ImageFont.truetype,传入我们想要的字体的.TTF文件,之后是表示字体大小的整数❹。将ImageFont.truetype返回的Font对象保存在arialFont这样的变量中,然后将该变量传入text,作为最后的关键字参数。❺行的text调用绘制了Howdy,采用灰色、32点Arial字体。

得到的text.png文件如图17-15所示。

图17-15 得到的图像text.png

17.5 小结

图像由像素的集合构成,每个像素具有表示颜色的RGBA值,可以通过x和y坐标的定位。两种常见的图像格式是JPEG和PNG。Pillow模块可以处理这两种图像格式和其他格式。

当图像被加载为Image对象时,它的宽度和高度作为两整数元组,保存在size属性中。Image数据类型的对象也有一些方法,实现常见的图像处理:crop、copy、paste、resize、rotate和transpose。要将Image对象保存为图像文件,就调用save方法。

如果希望程序在图像上绘制形状,就使用ImageDraw的方法绘制点、线、矩形、椭圆和多边形。该模块也提供了一些方法,用你选择的字体和大小绘制文本。

虽然像Photoshop这样高级(且昂贵)的应用程序提供了自动批量处理功能,但你可以用Python脚本,免费完成许多相同的修改。在前面的章节中,你编写Python程序来处理纯文本文件、电子表格、PDF和其他格式。利用Pillow模块,你已将编程能力扩展到处理图像!

17.6 习题

1.什么是RGBA值?

2.如何利用Pillow模块得到'CornflowerBlue'的RGBA值?

3.什么是矩形元组?

4.哪个函数针对名为sophie.png图像文件返回一个Image对象?

5.如何得到一个Image对象的图像的宽度和高度?

6.调用什么方法会得到一个100×100的图像的Image对象,但不包括它左下角的四分之一?

7.对Image对象修改后,如何将它保存为图像文件?

8.什么模块包含Pillow的形状绘制代码?

9.Image对象没有绘制方法。哪种对象有?如何获得这种类型的对象?

17.7 实践项目

作为实践,编程完成以下任务。

17.7.1 扩展和修正本章项目的程序

本章的resizeAndAddLogo.py程序使用PNG和JPEG文件,但Pillow还支持许多格式,不仅仅是这两个。扩展resizeAndAddLogo.py,让它也能处理GIF和BMP图像。

另一个小问题是,只有文件扩展名小写时,程序才修改PNG和JPEG文件。例如,它会处理zophie.png,但不处理zophie.PNG。修改代码,让文件扩展名检查不区分大小写。

最后,添加到右下角的徽标本来只是一个小标记,但如果该图像与徽标本身差不多大,结果将类似于图17-16。修改resizeAndAddLogo.py,使得图像必须至少是徽标的两倍的宽度和高度,然后才粘贴徽标。否则,它应该跳过添加徽标。

图17-16 如果图像不比徽标大很多,结果很难看。

17.7.2 在硬盘上识别照片文件夹

我有一个坏习惯,从数码相机将文件传输到硬盘的临时文件夹后,会忘记这些文件夹。编程扫描整个硬盘,找到这些遗忘的“照片文件夹”,就太好了。

编写一个程序,遍历硬盘上的每个文件夹,找到可能的照片文件夹。当然,首先你必须定义什么是“照片文件夹”。假定就是超过半数文件是照片的任何文件夹。你如何定义什么文件是照片?

首先,照片文件必须具有文件扩展名.png或.jpg。此外,照片是很大的图像。照片文件的宽度和高度都必须大于500像素。这是安全的假定,因为大多数数码相机照片,宽度和高度都是几千像素。

作为提示,下面是这个程序的粗略框架:

#! python3# Import modules and write comments to describe this program.for foldername, subfolders, filenames in os.walk('C://'):    numPhotoFiles = 0    numNonPhotoFiles = 0    for filename in filenames:# Check if file extension isn't .png or .jpg.if TODO:    numNonPhotoFiles += 1    continue     # skip to next filename# Open image file using Pillow.# Check if width & height are larger than 500.if TODO:    # Image is large enough to be considered a photo.    numPhotoFiles += 1else:    # Image is too small to be a photo.    numNonPhotoFiles += 1    # If more than half of files were photos,    # print the absolute path of the folder.    if TODO:print(TODO)  

程序运行时,它应该在屏幕上打印所有照片文件夹的绝对路径。

17.7.3 定制的座位卡

第13章包含了一个实践项目,利用纯文本文件的客人名单,创建定制的邀请函。作为附加项目,请使用 Pillow 模块,为客人创建定制的座位卡图像。从http://nostarch. com/automatestuff/下载资源文件guests.txt,对于其中列出的客人,生成带有客人名字和一些鲜花装饰的图像文件。在http://nostarch.com/automatestuff/的资源中,包含一个版权为公共领域的鲜花图像。

为了确保每个座位卡大小相同,在图像的边缘添加一个黑色的矩形,这样在图像打印出来时,可以沿线剪裁。Pillow生成的PNG文件被设置为每英寸72个像素,因此4×5英寸的卡片需要288×360像素的图像。