Skip to main content

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 / ImageDraw APIs, 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')
  • crop returns a new Image; 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')
  • paste modifies 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')
  • resize returns a new Image; 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')
  • rotate and transpose create new images; expand=True enlarges 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.png at 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_im ensures 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; xy list of points.
  • draw.line(xy, fill, width) – one or more connected line segments.
  • draw.rectangle(xy, fill, outline, width) – xy is 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.