转:以Python Imaging Library 进行影像资料处理


author: Yung-Yu Chen (yungyuc) http://blog.seety.org/everydaywork/ < yyc@seety.org >
copyright: Copyright 2006, all rights reserved

1影像与图形资料的处理

上一回我们谈过了图形介面程式的撰写,这一次我们要讨论图形(影像) 本身的处理,而讨论的内容将会集中在Python Imaging Library (PIL) 这一套程式库上。

PIL 是Python 下最有名的影像处理套件,由许多不同的模组所组成,并且提供了许多的处理功能,允许我们在简单的Python 程式里进行影像的处理。 使用像PIL 许样的程式库套件可以帮助我们把精力集中在影像处理的工作本身,避免迷失在底层的演算法里面。

由于影像处理牵涉到了大量的数学运算,因此PIL 中有许多的模组是用C 语言所写成的,以提升处理的效率。 不过,在使用的时候,我们当然不必在意这样的问题,只管放心地用就是了。

1.1 PIL能为你作的事

PIL 具备(但不限于) 以下的能力:

  • 数十种图档格式的读写能力。 常见的JPEG, PNG, BMP, GIF, TIFF 等格式,都在PIL 的支援之列。 另外,PIL 也支援黑白、灰阶、自订调色盘、RGB true color、带有透明属性的RBG true color、CMYK 及其它数种的影像模式。 相当齐全。
  • 基本的影像资料操作:裁切、平移、旋转、改变尺寸、调置(transpose)、剪下与贴上等等。
  • 强化图形:亮度、色调、对比、锐利度。
  • 色彩处理。
  • PIL 提​​供十数种滤镜(filter)。 当然,这个数目远远不能与Photoshop® 或GIMP® 这样的专业特效处理软体相比;但PIL 提​​供的这些滤镜可以用在Python 程式里面,提供批次化处理的能力。
  • PIL 可以在影像中绘图制点、线、面、几何形状、填满、文字等等。

接下来,我们开始一步一步地对Python/PIL 的影像处理程式设计进行讨论。

2转换图档格式

市面上有许多影像处理程式,一般人最常用它们来处理的工作大概就是图档格式转换了;这是影像处理软体最基本的功能,PIL 当然也要支援。

假设我们有一个JPG档案,名字叫作sample01.jpg ,那么,以下的程式会把这个档案载入Python:

 >>> import Image
 >>> im = Image.open( "sample01.jpg" )

im这个物件是由Image.open()方法所产生出来的Image物件。 我们可以用Image物件内的属性来查询关于此档案的资讯:

 >>> print im.format, im.size, im.mode
 JPEG (2288, 1712) RGB

格式字串放在format属性里,尺寸放在size属性里,而(调色盘)模式 ​​放在mode属性里。 从以上的执行结果,可以看出来我们读的确实是一个JPEG 档案,档案的尺寸是2288 像素宽、1712 像素高,调色盘是RGB 全彩模式。

既然我们已经把图档读入了Python,要处理它就简单了。 利用Image类别的save()方法,可以把档案储存成PIL支援的格式:

 >>> im.save( "fileout.png" )

如果图档很大,这会花上一点时间。 Image.save()方法会根据欲存档的副档名,自动判断要存图档的格式(刚刚我们用的open()函式也会这样作)。

save()可以指定存档格式。 在以下的例子里,我们把存档格式指定为JPEG:

 >>> im.save( "fileout.png", "JPEG" )

这时候副档名是无所谓的。

只处理一两个档案的时候,使用Python 直译器就相当合适。 然而若要处理一大群档案,譬如把一整个目录的JPEG 档转换为PNG 档,那么写成一个程式档会比较方便,例如:

 #!/usr/bin/env python

 from glob import glob
 from os.path import splitext
 import Image

 jpglist = glob( "python_imaging_pix/*.[jJ][pP][gG]" )

 for jpg in jpglist:
     im = Image.open(jpg)
     png = splitext(jpg)[0]+".png"
     im.save(png)
     print png

只要在一个放了*.jpg*.JPG档案的目录里面执行这个指令稿,它就会把所有的JPEG档转成PNG档案:

 $ ./convertdir.py
 file0001.png
 file0002.png
 .
 .
 file9999.png

既然PIL 会从档名侦测常用的档案格式,存档时我们通常都不会指定存档格式。

然而,依据档案格式的不同, save()方法提供了不同的选项参数。 以JPEG而言,它可以接受quality (从1到100的整数,预设为75)、 optimize (真假值)及progression (真假值)。 在以下的例子里,我们以100的quality来储存JPEG档案:

 >>> im.save( "quality100.jpg", quality=100 )

要诀

PIL 也支援EPS (Encapsulate PostScript) 格式的写入。 TeX 的使用者可以利用PIL 来简单地把图档转成EPS 以供TeX compiler 使用。

3改变影像与制作缩图

在了解了基本的图档转换之后,我们来看看如何对影像进行尺寸方面的修改。 PIL对Image物件提供了resize方法,以执行影像的缩放工作。 用我们的sample01.jpg档案来当例子:

 >>> im = Image.open( "sample01.jpg" )
 >>> print im.size
 (2288, 1712)
 >>> width = 400
 >>> ratio = float(width)/im.size[0]
 >>> height = int(im.size[1]*ratio)
 >>> nim = im.resize( (width, height), Image.BILINEAR )
 >>> print nim.size
 (400, 299)
 >>> nim.save( "resized.jpg" )

然后我们就会得到比较小的resized.jpg

resized

resize()这个方法会传回一个新的Image物件,所以旧的Image不会被更动。 resize()接受两个参数,第一个用来指定变更后的大小,是一个双元素tuple,分别用以指定影像的宽与高;第二个参数可以省略,是用来指定变更时使用的内插法,预设是Image.NEAREST (取最近点),这里我们指定为品质比较好的Image.BILINEAR

resize()可以把影像放大缩小,在使用时一定要传 ​​入宽与高。 上面的程式会先限定新影像的宽,再根据旧影像的长宽比例来算出新影像的高应该是多少,最后把尺寸值传入resize()去。 由此可知, resize()是允许我们不等比例缩放的:

 >>> width = 400
 >>> height = 100
 >>> nim2 = im.resize( (width, height), Image.BILINEAR )
 >>> nim2.save( "resize2wide.jpg" )

会得到形状奇怪的缩图:

resize2wide

我们可以任意改变新影像的尺寸值。

另一个常用的操作是旋转; rotate()方法可以用来旋转影像。 它取两个参数,第一个参数是一个逆时针的度数,第二个参数则也是影像处理时的内插法,可省略:

 >>> nim3 = nim.rotate( 45, Image.BILINEAR )
 >>> nim3.save( "rotated.jpg" )

rotate()并不会改变影像的尺寸(dimension),所以你会看到:

rotated

出现了黑边。 如果我们想要连影像尺寸一起变动,得要改用transpose()方法:

 >>> nim4 = nim.transpose( Image.ROTATE_90 )
 >>> nim4.save( "transposed90.jpg" )

结果是:

transposed90

transpose()方法接受Image.FLIP_LEFT_RIGHT , Image.FLIP_TOP_DOWN , ROTATE_90 , ROTATE_180 , ROTATE_270等五种参数;其中后三种的旋转均为逆时针。 rotate()方法会对像素资料进行内插;而transpose()则只是转置像素资料,所以没有内插参数可以设定,也不会影响影像的品质。

缩放与旋转是最常用的两个操作,而在其中,缩图的制作可能是特别常用的;PIL对缩图提供了一个方便的thumbnail()方法。 thumbnail()会直接修改Image物件本身,所以速度能比resize()更快,也消耗更少的记忆体。 它不接受指定内插法的参数,而且只能缩小影像,不能放大影像;用法是:

 >>> im = Image.open( "sample01.jpg" )
 >>> im.thumbnail( (400,100) )
 >>> im.save( "thumbnail.jpg" )
 >>> print im.size
 (133, 100)

thumbnail()在接受尺寸参数的时候,行为与resize()不同; resize()允许我们不等比例进行缩放,但thumbnail()只能进行等比例缩小,并且是以长、宽中比较小的那一个值为基准。 因此,上面的程式所作出的thumbnail.jpg变成了133*100的小图片:

thumbnail

有了这些操作,我们可以很轻易地执行影像管理的任务。

4修改图形内容

除了可以针对图形的尺寸作变更之外,PIL 更提供我们变更影像内容的能力。 这样,我们就不只能对影像进行管理,而能更进一步地利用程式来把影像的内容改成我们想要的样子。

我们从「贴图」开始:

 >>> baseim = Image.open( "resized.jpg" )
 >>> floatim = Image.open( "thumbnail.jpg" )
 >>> baseim.paste( floatim, (150, 50) )
 >>> baseim.save( "pasted.jpg" )

利用paste()方法,把之前作的thumbnail.jpg贴到resized.jpg里面去:

pasted

此种用法的paste()方法要求两个参数,第一是要贴上的Image,第二是要贴上的位置。 第二个参数有三种指定的方式:

  • None :不指定位置与尺寸,那么pasted()会假设要贴上的Image与被贴上的Image的尺寸完全相同。
  • (left, upper) :双元素tuple。 pasted()会把要贴上的Image的左上角对齐在指定的位置。
  • (left, upper, right, lower) :四元素tuple。 paste()`除了会把Image的左上角对齐外,也会对齐右下角。 不过基本上这种写法和上面那一种一样,因为paste()要求要贴上的影像与这里指定的尺寸一致,所以不可能出现不同的两组right, lower。

除了「贴图」之外,我们还可以对影像的内容进行裁切:

 >>> im = Image.open( "sample01.jpg" )
 >>> nim = im.crop( (700, 300, 1500, 1300) )
 >>> nim.thumbnail( (400,400) )
 >>> nim.save( "croped.jpg" )

(因为裁切之后的图形还是大了点,所以再缩图一次) 得到的结果是:

croped

crop()接受的box参数指定要裁切的左、上、右、下四个边界值,形成一个矩形。

除了剪贴之外,PIL 还可以使用内建的滤镜(filter) 作一些特效处理。 这些滤镜都放在ImageFilter模组里面,使用前要先汇入这个模组:

 >>> import ImageFilter

我们用个例子,对刚刚裁切的"No Riding" 禁止牌作20 次blur (糊化),来看看PIL 滤镜的效果:

 >>> im = Image.open( "croped.jpg" )
 >>> nim = im
 >>> for i in range(20): nim = nim.filter( ImageFilter.BLUR )
 ...
 >>> nim.save( "blured.jpg" )

你应该看不出来它是"No Riding" 了吧:

blured

使用滤镜的基本语法是:

 newim = im.filter( ImageFilter.FILTERNAME )

其中FILTERNAME是 PIL中支援的滤镜名称,目前有:BLUR, CONTOUR, DETAIL, EDGE_ENHANCE, EDGE_ENHANCE_MORE, EMBOSS, FIND_EDGES, SMOOTH, SMOOTH_MORE, SHARPEN,此处就不一一介绍了,但建议你可以自己来把每一个滤镜都试试看。

利用滤镜,我们可以对同一类的影像进行相同的特效处理。 当然,影像特效需要很精细的调整,在自动​​化作业中通常只能达到很粗略的效果;但PIL 既然提供了,我们的自动程序就拥有更多的工具可以使用。

5用PIL制作新影像

除了对已存在的影像进行编修之外,从零开始建立新影像也是很重要的工作。 PIL中的ImageDraw模组提供给我们绘制影像内容的能力。 在使用ImageDraw之前,要先建立好空白的新影像:

 >>> import ImageDraw
 >>> im = Image.new( "RGB", (400,300) )
 >>> draw = ImageDraw.Draw( im )

最后建出来的draw是一个ImageDraw物件会提供各种绘制影像的方法。 针对几何图形, draw物件提供arc() (弧线)、 chord() (弦)、 line() (线段)、 ellipse() (椭圆)、 point() (点)、 rectangle() (矩形)与polygon () (多边形)。 不过,我们不准备讨论几何图形的绘制;相信这些方法的使用对一般人来说应该都很直觉才是。

要诀

你可以在指令行输入pydoc ImageDraw.ImageDraw.<<methodname>>来查询上述方法( <<methodname>> )的说明,譬如pydoc ImageDraw.ImageDraw.line

这里要介绍的不是几何图形,而是文字的绘制。 我们要再介绍一个模组: ImageFont ,并且以实例来说明如何用PIL 「写字」:

 >>> import Image, ImageDraw, ImageFont
 >>> font = ImageFont.truetype( \
 ... "/usr/share/fonts/truetype/freefont/FreeMono.ttf", 24 )
 >>> im = Image.new( "RGB", (400,300) )
 >>> draw = ImageDraw.Draw( im )
 >>> draw.text( (20,20), "TEXT", font=font )
 >>> im.save( "text.jpg" )

这样就在一个黑色底图上用白笔写了"TEXT" 四个大字:

text

接着一一说明刚刚作的动作。 首先我们用ImageFonttruetype()函式建立了一个TrueType字型,大小设定为16点。 truetype()函式的第一个参数必须是字型档的搜寻路径,第二个参数是字型的点数。 然后依序建立影像物件与draw物件。 写字的动作用draw物件的text()方法来完成,它接受两个参数,分别是文字的左上角点、字串,另外可以用font选项来指定所使用的字型(若不指定,便使用预设字型)。

在1.1.4 版之前,PIL 是只能使用点阵字型的。 现在PIL 加入了TrueType 向量字型的支援,对于要「写字」的人来说实在是一大福音。 对点阵字来说,想改变字型的大小得要更换字型才作得到,但TrueType 就没有这个限制。 如果我们想要写出两串不同大小的文字,这样作就可以了:

 >>> largefont = ImageFont.truetype( \
 ... "/usr/share/fonts/truetype/freefont/FreeMono.ttf", 48 )
 >>> smallfont = ImageFont.truetype( \
 ... "/usr/share/fonts/truetype/freefont/FreeMono.ttf", 24 )
 >>> im = Image.new( "RBG", (400,300) )
 >>> draw = ImageDraw.Draw( im )
 >>> draw.text( (20,20), "SmallTEXT", font=smallfont )
 >>> draw.text( (20,120), "LargeTEXT", font=largefont )
 >>> im.save( "multitext.jpg" )

结果如:

multitext

以上就是在PIL 里建立文字图形的方法。

最后,我们要说明如何改变绘制图形(文字)时的颜色;绘图时画笔的颜色是透过draw物件的ink属性来改变的:

 >>> draw.ink = 0 + 255*256 + 0*256*256

以上会把画笔设成绿色。 ink值必须要是一个整数,其值由色彩的RGB值算出。 举几个ink值的例子:

  • 红色的ink值应设为255(R) + 0(G)*256 + 0(B)*256*256
  • 蓝色的ink值应设为0(R) + 0(G)*256 + 255(B)*256*256
  • 靛色的ink值应设为0(R) + 255(G)*256 + 255(B)*256*256

所设定的ink会影响所有后续的绘图动作。

6结语

本文介绍了方便好用的PIL 套件,可以让我们用Python 撰写影像处理的程式。 我们对图档的格式处理、尺寸处理以及内容的编修都作了讨论,最后也说明如何从零开始创作一个影像。

对网页程式来说,动态产生简单的影像是特别有用的功能,可以用来补足HTML 与CSS 的不足之处。 利用PIL 来执行批次影像处理的工作,更能省去我们许多的操作时间。 相信读者能从其中发现它所提供的生产力。

在下一期的内容里,我们要开始介绍Python 的网页程式设计。

来源网址:http://tech.seety.org/python/python_imaging.html

Archives