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.