Automate the Boring Stuff with Python
Page 41
Pillow also provides the Image.new() function, which returns an Image object—much like Image.open(), except the image represented by Image.new()’s object will be blank. The arguments to Image.new() are as follows:
The string 'RGBA', which sets the color mode to RGBA. (There are other modes that this book doesn’t go into.)
The size, as a two-integer tuple of the new image’s width and height.
The background color that the image should start with, as a four-integer tuple of an RGBA value. You can use the return value of the ImageColor.getcolor() function for this argument. Alternatively, Image.new() also supports just passing the string of the standard color name.
For example, enter the following into the interactive shell:
>>> 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')
Here we create an Image object for an image that’s 100 pixels wide and 200 pixels tall, with a purple background ➊. This image is then saved to the file purpleImage.png. We call Image.new() again to create another Image object, this time passing (20, 20) for the dimensions and nothing for the background color ➋. Invisible black, (0, 0, 0, 0), is the default color used if no color argument is specified, so the second image has a transparent background; we save this 20×20 transparent square in transparentImage.png.
Cropping Images
Cropping an image means selecting a rectangular region inside an image and removing everything outside the rectangle. The crop() method on Image objects takes a box tuple and returns an Image object representing the cropped image. The cropping does not happen in place—that is, the original Image object is left untouched, and the crop() method returns a new Image object. Remeber that a boxed tuple—in this case, the cropped section—includes the left column and top row of pixels but only goes up to and does not include the right column and bottom row of pixels.
Enter the following into the interactive shell:
>>> croppedIm = catIm.crop((335, 345, 565, 560)) >>> croppedIm.save('cropped.png')
This makes a new Image object for the cropped image, stores the object in croppedIm, and then calls save() on croppedIm to save the cropped image in cropped.png. The new file cropped.png will be created from the original image, like in Figure 17-4.
Figure 17-4. The new image will be just the cropped section of the original image.
Copying and Pasting Images onto Other Images
The copy() method will return a new Image object with the same image as the Image object it was called on. This is useful if you need to make changes to an image but also want to keep an untouched version of the original. For example, enter the following into the interactive shell:
>>> catIm = Image.open('zophie.png') >>> catCopyIm = catIm.copy()
The catIm and catCopyIm variables contain two separate Image objects, which both have the same image on them. Now that you have an Image object stored in catCopyIm, you can modify catCopyIm as you like and save it to a new filename, leaving zophie.png untouched. For example, let’s try modifying catCopyIm with the paste() method.
The paste() method is called on an Image object and pastes another image on top of it. Let’s continue the shell example by pasting a smaller image onto 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')
First we pass crop() a box tuple for the rectangular area in zophie.png that contains Zophie’s face. This creates an Image object representing a 230×215 crop, which we store in faceIm. Now we can paste faceIm onto catCopyIm. The paste() method takes two arguments: a “source” Image object and a tuple of the x- and y-coordinates where you want to paste the top-left corner of the source Image object onto the main Image object. Here we call paste() twice on catCopyIm, passing (0, 0) the first time and (400, 500) the second time. This pastes faceIm onto catCopyIm twice: once with the top-left corner of faceIm at (0, 0) on catCopyIm, and once with the top-left corner of faceIm at (400, 500). Finally, we save the modified catCopyIm to pasted.png. The pasted.png image looks like Figure 17-5.
Figure 17-5. Zophie the cat, with her face pasted twice
Note
Despite their names, the copy() and paste() methods in Pillow do not use your computer’s clipboard.
Note that the paste() method modifies its Image object in place; it does not return an Image object with the pasted image. If you want to call paste() but also keep an untouched version of the original image around, you’ll need to first copy the image and then call paste() on that copy.
Say you want to tile Zophie’s head across the entire image, as in Figure 17-6. You can achieve this effect with just a couple for loops. Continue the interactive shell example by entering the following:
>>> 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')
Here we store the width of height of catIm in catImWidth and catImHeight. At ➊ we make a copy of catIm and store it in catCopyTwo. Now that we have a copy that we can paste onto, we start looping to paste faceIm onto catCopyTwo. The outer for loop’s left variable starts at 0 and increases by faceImWidth(230) ➋. The inner for loop’s top variable start at 0 and increases by faceImHeight(215) ➌. These nested for loops produce values for left and top to paste a grid of faceIm images over the catCopyTwo Image object, as in Figure 17-6. To see our nested loops working, we print left and top. After the pasting is complete, we save the modified catCopyTwo to tiled.png.
Figure 17-6. Nested for loops used with paste() to duplicate the cat’s face (a duplicat, if you will).
Pasting Transparent Pixels
Normally transparent pixels are pasted as white pixels. If the image you want to paste has transparent pixels, pass the Image object as the third argument so that a solid rectangle isn’t pasted. This third argument is the “mask” Image object. A mask is an Image object where the alpha value is significant, but the red, green, and blue values are ignored. The mask tells the paste() function which pixels it should copy and which it should leave transparent. Advanced usage of masks is beyond this book, but if you want to paste an image that has transparent pixels, pass the Image object again as the third argument.
Resizing an Image
The resize() method is called on an Image object and returns a new Image object of the specified width and height. It accepts a two-integer tuple argument, representing the new width and height of the returned image. Enter the following into the interactive shell:
➊ >>> 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')
Here we assign the two values in the catIm.size tuple to the variables width and height ➊. Using width and height instead of catIm.size[0] and catIm.size[1] makes the rest of the code more readable.
The first resize() call passes int(width / 2) for the new width and int(height / 2) for the new height ➋, so the Image object returned from resize() will be half the length and width of the original image, or one-quarter of the original image size overall. The resize() method accepts only integers in its tuple argument, which is why you needed to wrap both divisions by 2 in an int() call.
This resizing keeps the same proportions for the width and height. But the new width and height passed to resize() do not have to be proportional to the original image. The svelteIm variable contains an Image object that has the original width but a height that is 300 pixels taller ➌, giving Zophie a more slender look.
Note that the resize() method does not edit the Image object in place but instead returns a new Image object.
Rotating and Flipping Images
Images can be rotated with the rotate() method, which returns a new Image object of the rotated image and leaves the original Image object unchanged. The argument to rotate() is a single integer or float representing the number of degrees to rotate the image counterclockwise. Enter the following into the interactive shell:
>>> catIm.rotate(90).save('rotated90.png') >>> catIm.rotate(180).save('rotated180.png') >>> catIm.rotate(270).save('rotated270.png')
Note how you can chain method calls by calling save() directly on the Image object returned from rotate(). The first rotate() and save() call makes a new Image object representing the image rotated counterclockwise by 90 degrees and saves the rotated image to rotated90.png. The second and third calls do the same, but with 180 degress and 270 degress. The results look like Figure 17-7.
Figure 17-7. The original image (left) and the image rotated counterclockwise by 90, 180, and 270 degrees
Notice that the width and height of the image change when the image is rotated 90 or 270 degrees. If you rotate an image by some other amount, the original dimensions of the image are maintained. On Windows, a black background is used to fill in any gaps made by the rotation, like in Figure 17-8. On OS X, transparent pixels are used for the gaps instead.
The rotate() method has an optional expand keyword argument that can be set to True to enlarge the dimensions of the image to fit the entire rotated new image. For example, enter the following into the interactive shell:
>>> catIm.rotate(6).save('rotated6.png') >>> catIm.rotate(6, expand=True).save('rotated6_expanded.png')
The first call rotates the image 6 degrees and saves it to rotate6.png (see the image on the left of Figure 17-8). The second call rotates the image 6 degrees with expand set to True and saves it to rotate6_expanded.png (see the image on the right of Figure 17-8).
Figure 17-8. The image rotated 6 degrees normally (left) and with expand=True (right)
You can also get a “mirror flip” of an image with the transpose() method. You must pass either Image.FLIP_LEFT_RIGHT or Image.FLIP_TOP_BOTTOM to the transpose() method. Enter the following into the interactive shell:
>>> catIm.transpose(Image.FLIP_LEFT_RIGHT).save('horizontal_flip.png') >>> catIm.transpose(Image.FLIP_TOP_BOTTOM).save('vertical_flip.png')
Like rotate(), transpose() creates a new Image object. Here was pass Image.FLIP_LEFT_RIGHT to flip the image horizontally and then save the result to horizontal_flip.png. To flip the image vertically, we pass Image.FLIP_TOP_BOTTOM and save to vertical_flip.png. The results look like Figure 17-9.
Figure 17-9. The original image (left), horizontal flip (center), and vertical flip (right)
Changing Individual Pixels
The color of an individual pixel can be retrieved or set with the getpixel() and putpixel() methods. These methods both take a tuple representing the x- and y-coordinates of the pixel. The putpixel() method also takes an additional tuple argument for the color of the pixel. This color argument is a four-integer RGBA tuple or a three-integer RGB tuple. Enter the following into the interactive shell:
➊ >>> 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')
At ➊ we make a new image that is a 100×100 transparent square. Calling getpixel() on some coordinates in this image returns (0, 0, 0, 0) because the image is transparent ➋. To color pixels in this image, we can use nested for loops to go through all the pixels in the top half of the image ➌ and color each pixel using putpixel() ➍. Here we pass putpixel() the RGB tuple (210, 210, 210), a light gray.
Say we want to color the bottom half of the image dark gray but don’t know the RGB tuple for dark gray. The putpixel() method doesn’t accept a standard color name like 'darkgray', so you have to use ImageColor.getcolor() to get a color tuple from 'darkgray'. Loop through the pixels in the bottom half of the image ➎ and pass putpixel() the return value of ImageColor.getcolor() ➏, and you should now have an image that is light gray in its top half and dark gray in the bottom half, as shown in Figure 17-10. You can call getpixel() on some coordinates to confirm that the color at any given pixel is what you expect. Finally, save the image to putPixel.png.
Figure 17-10. The putPixel.png image
Of course, drawing one pixel at a time onto an image isn’t very convenient. If you need to draw shapes, use the ImageDraw functions explained later in this chapter.
Project: Adding a Logo
Say you have the boring job of resizing thousands of images and adding a small logo watermark to the corner of each. Doing this with a basic graphics program such as Paintbrush or Paint would take forever. A fancier graphics application such as Photoshop can do batch processing, but that software costs hundreds of dollars. Let’s write a script to do it instead.
Say that Figure 17-11 is the logo you want to add to the bottom-right corner of each image: a black cat icon with a white border, with the rest of the image transparent.
Figure 17-11. The logo to be added to the image.
At a high level, here’s what the program should do:
Load the logo image.
Loop over all .png and.jpg files in the working directory.
Check whether the image is wider or taller than 300 pixels.
If so, reduce the width or height (whichever is larger) to 300 pixels and scale down the other dimension proportionally.
Paste the logo image into the corner.
Save the altered images to another folder.
This means the code will need to do the following:
Open the catlogo.png file as an Image object.
Loop over the strings returned from os.listdir('.').
Get the width and height of the image from the size attribute.
Calculate the new width and height of the resized image.
Call the resize() method to resize the image.
Call the paste() method to paste the logo.
Call the save() method to save the changes, using the original filename.
Step 1: Open the Logo Image
For this project, open a new file editor window, enter the following code, and save it as 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.
By setting up the SQUARE_FIT_SIZE ➊ and LOGO_FILENAME ➋ constants at the start of the program, we’ve made it easy to change the program later. Say the logo that you’re adding isn’t the cat icon, or say you’re reducing the output images’ largest dimension to something other than 300 pixels. With these constants at the start of the program, you can just open the code, change those values once, and you’re done. (Or you can make it so that the values for these constants are taken from the command line arguments.) Without these constants, you’d instead have to search the code for all instances of 300 and 'catlogo.png' and replace them with the values for your new project. In short, using constants makes your program more generalized.
The logo Image object is returned from Image.open() ➌. For readability, logoWidth and logoHeight are assigned to the values from logoIm.size ➍.
The rest of the program is a skeleton of TODO comment
s for now.
Step 2: Loop Over All Files and Open Images
Now you need to find every .png file and .jpg file in the current working directory. Note that you don’t want to add the logo image to the logo image itself, so the program should skip any image with a filename that’s the same as LOGO_FILENAME. Add the following to your code:
#! 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--