🐍 Effortless Image Cropping with Python: Automate Your Workflow in Minute

Tom Smykowski
8 min readNov 11, 2024

--

Cropping hundreds of images by hand? No thanks! With Python and OpenCV, you can automatically detect the focus of an image and crop it to a perfect 16:9 in seconds. This guide shows you how to save hours on editing — just a few lines of code, and you’re set!

Since Python deck is currently also in Sci-Fi style, I had to prepare new illustrations. The work involves many steps that I try to automate as much as possible. Today I’d like to tell you about one particular step that is surprisingly easy to automate with Python. Namely, cropping an image.

Let’s say we have this image:

What I’d like to have is a 16:9 image that is focused on the rider, preserving the width of the image. It’s complicated to do, well you can use Photoshop if you like expensive solutions, or Krita, Paint.NET or IrfanView to do it.

But what if you have 1 000 files to process? 1000x5 minutes = a lot of time.

So let’s automate it. The initial idea of mine was as follows:

Use automatic crop, crop in set place:

Smile, rise eyebrows, stop smiling — model face

Perfect!

My next idea had to exceed possibilities, because I didn’t know how to implement it:

  1. Find where is the most important part of the image
  2. Crop 16:9 around it

The latter one is easy, the first one, not really? But there’s Python, and numpy, OpenCV, PIL so why not try it?

Required libraries:

  • OpenCV: This library handles image processing and will be our main tool for manipulating images. Install it using pip install opencv-python.
  • PIL (or Pillow): The Python Imaging Library is used for additional image handling tasks. Install it with pip install pillow.
  • NumPy: A powerful library for numerical computing, which integrates seamlessly with OpenCV. Install it using pip install numpy.

Basically Photoshop for free :)

After some research I’ve found you can do it by blurring stuff, greyscaling it, detecting edges, finding contours and identifying the largest one.

Let’s go over the most important lines of the code, the final script will be at the end of this article:

cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

That line converts image to greyscale:

Next, we have to blur it a little bit, so the latter processing is less CPU intense:

blurred = cv2.GaussianBlur(gray, (11, 11), 0)

Here a word of notice, by increasing (11, 11) you can increase blur, making the script better or worse in finding center of the image. But these numbers have to be odd.

Next step, we detect edges:

edges = cv2.Canny(blurred, 50, 150)

Now, we find contours. A contour is like a shape made from edges. So you can imagine that a triangle has 3 edges forming one contour:

cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

When we have contours we just get the biggest one, surprisingly:

Yeah, it occurs the longest contour is between horses legs. So we draw a bounding rectangle:

Voila — the most important element on the image (sort of).

I also crop it to 16:9 and use the rule of thirds, but that’s just math, so I won’t bore you with the details. Essentially, you take the center of the main subject, draw a 16:9 rectangle around it, and crop the image while aligning it with the photography rule of thirds to make it look impressive.

A Sci-Fi Python card game, there is also fantasy and neutral version. Ready made or and digital printables

In the same way, my Python Deck zeroes in on the essential concepts of Python — capturing the “center” of what’s most important, then positioning each principle with precision so you can learn effectively. Like framing a great shot, it’s about putting Python’s most powerful ideas right where they make the biggest impact.

Just for a reference, here’s how two thirds look like:

The rule of thirds is a photography guideline that divides an image into a 3x3 grid, helping you place important elements along these lines or at their intersections. By aligning your main subject with these points, you create a balanced, visually appealing composition that naturally draws the viewer’s eye.

And the calculated cropping area:

Result:

Beautiful right?

If we’ll try it on other images, we’ll get mixed results:

It’s not like it will always work as you think, because it’s not clear what is the most important part. It’s just a mathematical assumption:

But it works quite well, especially when you play with the numbers. So, if you have a lot of files and want to crop them automatically — preserving the photographic rule of thirds and focusing on the most important part — it’s perfectly doable.

One of the desk mats I’m writing about

And if you’re into tech tools that make your workspace more productive, check out my desk mats. They’re packed with programming shortcuts and tips, designed to keep the essentials right at your fingertips — just like this script does with image cropping!

Ah, here’s the script:

import cv2
from PIL import Image
import numpy as np
from pathlib import Path

# Define source and destination folders
input_folder = "demo" # Source folder for images
output_folder = "demo/output" # Destination folder for processed images

def crop_important_region(image_path, output_path):
# Load image
image = cv2.imread(str(image_path))
if image is None:
raise ValueError(f"Unable to load image: {image_path}")

# Set up file name prefix for saving steps
base_name = image_path.stem # Base name without extension

# Step 1-1: Save original image
cv2.imwrite(str(output_path / f"{base_name}_1-1-original.jpg"), image)

# Get image dimensions
img_height, img_width = image.shape[:2]

# Calculate the height for a 16:9 aspect ratio based on the image width
target_height = int(img_width * 9 / 16)

# Step 1-2: Convert to grayscale and save
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv2.imwrite(str(output_path / f"{base_name}_1-2-grayscale.jpg"), gray)

# Step 1-3: Apply a stronger Gaussian blur to reduce noise and detail, and save
blurred = cv2.GaussianBlur(gray, (11, 11), 0) # Increased kernel size for stronger blur
cv2.imwrite(str(output_path / f"{base_name}_1-3-blurred.jpg"), blurred)

# Step 1-4: Detect edges using the Canny edge detector and save
edges = cv2.Canny(blurred, 50, 150)
edges_colored = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR) # Convert edges to BGR for better visualization
edges_colored[np.where((edges_colored == [255, 255, 255]).all(axis=2))] = [0, 0, 255] # Make edges red
cv2.imwrite(str(output_path / f"{base_name}_1-4-edges.jpg"), edges_colored)

# Step 1-5: Find contours
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# Find the largest contour (assuming it’s the main object)
largest_contour = max(contours, key=cv2.contourArea)

# Visualize only the largest edge on the original image with a thicker line for high-res visibility
largest_edge_image = image.copy()
cv2.drawContours(largest_edge_image, [largest_contour], -1, (0, 255, 0), 10) # Green color, 10px thickness
cv2.imwrite(str(output_path / f"{base_name}_1-5-largest-edge.jpg"), largest_edge_image)

# Get the bounding box around the largest contour
x, y, w, h = cv2.boundingRect(largest_contour)

# Calculate the center of the largest contour
center_x = x + w // 2
center_y = y + h // 2

# Visualize the bounding box on the original image, filled with blue and with cross lines at the center
bounding_box_image = image.copy()
cv2.rectangle(bounding_box_image, (x, y), (x + w, y + h), (255, 0, 0), -1) # Fill bounding box with blue
cv2.line(bounding_box_image, (center_x, y), (center_x, y + h), (255, 0, 0), 10) # Vertical line in center
cv2.line(bounding_box_image, (x, center_y), (x + w, center_y), (255, 0, 0), 10) # Horizontal line in center
cv2.imwrite(str(output_path / f"{base_name}_1-6-bounding-box.jpg"), bounding_box_image)

# Step 1-7: Draw rule of thirds grid and show two-thirds overlay
thirds_image = image.copy()
# Draw vertical thirds (yellow color, thicker line for visibility)
cv2.line(thirds_image, (img_width // 3, 0), (img_width // 3, img_height), (0, 255, 255), 8)
cv2.line(thirds_image, (2 * img_width // 3, 0), (2 * img_width // 3, img_height), (0, 255, 255), 8)
# Draw horizontal thirds
cv2.line(thirds_image, (0, img_height // 3), (img_width, img_height // 3), (0, 255, 255), 8)
cv2.line(thirds_image, (0, 2 * img_height // 3), (img_width, 2 * img_height // 3), (0, 255, 255), 8)
# Overlay two-thirds of the image with semi-transparent green
overlay = thirds_image.copy()
cv2.rectangle(overlay, (0, 0), (img_width, int(2 * img_height // 3)), (0, 255, 0), -1)
cv2.addWeighted(overlay, 0.3, thirds_image, 0.7, 0, thirds_image)
cv2.imwrite(str(output_path / f"{base_name}_1-7-thirds-and-two-thirds.jpg"), thirds_image)

# Calculate the y-coordinate for the crop to center the important area within the lower third of the 16:9 crop
crop_y = max(0, center_y - (2 * target_height) // 3)
crop_y = min(crop_y, img_height - target_height) # Ensure crop is within image bounds

# Step 1-8: Show crop area on the original image
crop_area_image = image.copy()
cv2.rectangle(crop_area_image, (0, crop_y), (img_width, crop_y + target_height), (255, 0, 255), 8) # Magenta border
cv2.imwrite(str(output_path / f"{base_name}_1-8-crop-area.jpg"), crop_area_image)

# Step 1-9: Crop the image to the specified width and target height
cropped_image = image[crop_y:crop_y + target_height, 0:img_width]

# Save the final cropped image with a suffix
output_image_path = output_path / f"{base_name}_1-9-cropped.jpg"
output_image = Image.fromarray(cv2.cvtColor(cropped_image, cv2.COLOR_BGR2RGB))
output_image.save(output_image_path)

print(f"Processed and saved steps for {image_path.name}")

def process_all_images_in_folder():
# Set up the input and output directories
input_dir = Path(input_folder)
output_dir = Path(output_folder)
output_dir.mkdir(exist_ok=True)

# Process each image file in the input directory
for image_path in input_dir.glob("*.*"):
if image_path.is_file() and image_path.suffix.lower() in {'.jpg', '.jpeg', '.png', '.webp'}:
try:
crop_important_region(image_path, output_dir)
except Exception as e:
print(f"Error processing {image_path.name}: {e}")

# Run the script on all files in the input folder
process_all_images_in_folder()

Thank you for joining me on this journey into the world of image processing! If this article brought value to your work, please give it a few claps to let me know — it helps others find this content, too.

For those ready to level up their coding skills, my exclusive Python Deck is crafted just for you. Available as a convenient printable or in a beautifully designed printed edition, it’s perfect for gaining deeper insights and hands-on practice with Python’s essential concepts.

Want to see more articles like this? Follow me for updates, and feel free to share this with fellow Python enthusiasts to support future content. Let’s keep pushing the boundaries of what we can achieve in code! 🐍💡

--

--

Tom Smykowski
Tom Smykowski

Written by Tom Smykowski

🚀 Senior/Lead Frontend Engineer | Angular · Vue.js · React | Design Systems, UI/UX | Looking for a new project! 📩 contact@tomasz-smykowski.com

No responses yet