Chapter 21 - Making Graphs and Manipulating Images (Python)
Here's a concise but thorough summary of Chapter 21, covering all sections and main ideas with small examples.
Big picture: Pillow + Matplotlib
- The chapter introduces Pillow for batch image manipulation (crop, resize, paste, draw, watermark, etc.) and Matplotlib for generating graphs as image files.
- You'll work with RGBA colors, pixel coordinates, Pillow's
Image/ImageDrawAPIs, and later basic plotting (line/bar charts) with Matplotlib.
Image fundamentals in Pillow
Colors and RGBA
- Colors are RGBA tuples
(red, green, blue, alpha), each 0–255; alpha is transparency (0 = fully transparent, 255 = opaque). - Examples: red
(255, 0, 0, 255), green(0, 255, 0, 255), blue(0, 0, 255, 255), white(255, 255, 255, 255), black(0, 0, 0, 255); alpha 0 makes them invisible.
Named colors:
from PIL import ImageColor
ImageColor.getcolor('red', 'RGBA') # (255, 0, 0, 255)
ImageColor.getcolor('chocolate', 'RGBA') # (210, 105, 30, 255)
list(ImageColor.colormap) # all standard names
Color names are case-insensitive.
Coordinates and box tuples
- Origin
(0, 0)is top-left; x grows to the right, y grows downward (opposite to math graphs). - Many functions take a box tuple
(left, top, right, bottom)meaning: includes left/top, up to but not including right/bottom.
Example: (3, 1, 9, 6) covers a rectangle from x=3..8 and y=1..5.
Basic Pillow usage
Assume zophie.png is in the working directory.
Loading, inspecting, saving, creating images
from PIL import Image
cat_im = Image.open('zophie.png')
cat_im.show() # open in viewer
cat_im.size # (816, 1088)
width, height = cat_im.size # 816, 1088
cat_im.filename # 'zophie.png'
cat_im.format # 'PNG'
cat_im.format_description # 'Portable network graphics'
cat_im.save('zophie.jpg') # same image, different format
Creating new blank images:
from PIL import Image, ImageColor
im = Image.new('RGBA', (100, 200), 'purple')
im.save('purpleImage.png')
im2 = Image.new('RGBA', (20, 20)) # default transparent black
im2.save('transparentImage.png')
Core image operations
Cropping
from PIL import Image
cat_im = Image.open('zophie.png')
cropped_im = cat_im.crop((335, 345, 565, 560))
cropped_im.save('cropped.png')
cropreturns a newImage; original remains unchanged.
Copying and pasting (with tiling)
cat_im = Image.open('zophie.png')
cat_copy_im = cat_im.copy()
face_im = cat_im.crop((335, 345, 565, 560))
cat_copy_im.paste(face_im, (0, 0))
cat_copy_im.paste(face_im, (400, 500))
cat_copy_im.save('pasted.png')
Tiling the face across the whole image:
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):
cat_copy_im.paste(face_im, (left, top))
cat_copy_im.save('tiled.png')
pastemodifies in place; use.copy()if you want to keep original.- For transparent images, you must pass the image as the third argument to preserve transparency (see watermark project later).
Resizing
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')
resizereturns a newImage; you must pass integers for dimensions.
Rotating and flipping
cat_im.rotate(90).save('rotated90.png')
cat_im.rotate(180).save('rotated180.png')
cat_im.rotate(270).save('rotated270.png')
Small angle + expand:
cat_im.rotate(6).save('rotated6.png')
cat_im.rotate(6, expand=True).save('rotated6_expanded.png')
Flips:
cat_im.transpose(Image.FLIP_LEFT_RIGHT).save('horizontal_flip.png')
cat_im.transpose(Image.FLIP_TOP_BOTTOM).save('vertical_flip.png')
rotateandtransposecreate new images;expand=Trueenlarges canvas to avoid clipped corners.
Per-pixel access
from PIL import Image, ImageColor
im = Image.new('RGBA', (100, 100))
im.getpixel((0, 0)) # (0, 0, 0, 0) transparent black
# top half light gray
for x in range(100):
for y in range(50):
im.putpixel((x, y), (210, 210, 210))
# bottom half dark gray using a named color
for x in range(100):
for y in range(50, 100):
im.putpixel((x, y), ImageColor.getcolor('darkgray', 'RGBA'))
im.save('putPixel.png')
getpixel((x, y))returns color;putpixel((x, y), color)sets it.
Project 16: Add a Logo (batch resize + watermark)
Goal: batch-process all .png / .jpg in current directory:
- If bigger than 300x300, shrink proportionally so max dimension is 300.
- Paste
catlogo.pngat bottom-right. - Save to
withLogo/folder, leaving originals untouched.
Setup and constants
# resizeAndAddLogo.py
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
os.makedirs('withLogo', exist_ok=True)
Loop files and open images
for filename in os.listdir('.'):
if not (filename.endswith('.png') or filename.endswith('.jpg')) \
or filename == LOGO_FILENAME:
continue # skip non-images and the logo itself
im = Image.open(filename)
width, height = im.size
Conditional resize while preserving aspect ratio
if width > SQUARE_FIT_SIZE and height > SQUARE_FIT_SIZE:
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
print(f'Resizing {filename}...')
im = im.resize((width, height))
- Scale the larger dimension down to 300, adjust the other proportionally.
Paste logo and save
print(f'Adding logo to {filename}...')
im.paste(logo_im, (width - logo_width, height - logo_height), logo_im)
im.save(os.path.join('withLogo', filename))
- Third argument
logo_imensures transparent background stays transparent; without it, transparency appears as white.
Ideas for similar programs:
- Add text or URL; add timestamps; move images by size; semi-transparent watermarks.
Drawing on images (ImageDraw)
Create an image and get a drawing context:
from PIL import Image, ImageDraw
im = Image.new('RGBA', (200, 200), 'white')
draw = ImageDraw.Draw(im)
Shape methods overview
All take optional fill, outline, and sometimes width:
draw.point(xy, fill)– draw individual pixels;xylist of points.draw.line(xy, fill, width)– one or more connected line segments.draw.rectangle(xy, fill, outline, width)–xyis a box tuple.draw.ellipse(xy, fill, outline, width)– ellipse/circle inside box.draw.polygon(xy, fill, outline, width)– arbitrary polygon; last point auto-connects to first.
Drawing example
from PIL import Image, ImageDraw
im = Image.new('RGBA', (200, 200), 'white')
draw = ImageDraw.Draw(im)
# border
draw.line([(0, 0), (199, 0), (199, 199), (0, 199), (0, 0)], fill='black')
# blue rectangle
draw.rectangle((20, 30, 60, 60), fill='blue')
# red ellipse
draw.ellipse((120, 30, 160, 60), fill='red')
# brown polygon
draw.polygon(((57, 87), (79, 62), (94, 85), (120, 90), (103, 113)), fill='brown')
# green diagonal lines
for i in range(100, 200, 10):
draw.line([(i, 0), (200, i - 100)], fill='green')
im.save('drawing.png')
You can combine these with text drawing (using ImageFont and draw.text) to annotate images, create charts, or generate simple graphics entirely from code.
Overall idea of the chapter
Chapter 21 covers image manipulation with Pillow: RGBA colors, box tuples for coordinates, loading/saving/creating images, cropping, copying/pasting (with tiling), resizing, rotating/flipping, per-pixel access, batch processing with logo watermarking (Project 16), and drawing shapes with ImageDraw (points, lines, rectangles, ellipses, polygons). Matplotlib handles graph generation for data visualization.