How to do image processing in Python
Learn how to do image processing in Python. Discover different methods, tips, real-world applications, and how to debug common errors.

Python allows you to manipulate digital images for tasks from simple enhancements to complex data extraction. Its powerful libraries provide a straightforward way to automate these visual tasks with minimal code.
In this article, you'll explore essential techniques and practical tips. You'll also discover real-world applications and get advice on how to debug your code to solve common image manipulation challenges effectively.
Basic image processing with PIL/Pillow
from PIL import Image
img = Image.open('sample.jpg')
print(f"Format: {img.format}, Size: {img.size}, Mode: {img.mode}")
img.show() # Display the image--OUTPUT--Format: JPEG, Size: (800, 600), Mode: RGB
The Pillow library is the standard for image manipulation in Python. Your script starts by using Image.open() to load an image file into an object. This object holds not just the visual data but also crucial metadata you can use to guide your script's logic.
Before you manipulate an image, you can inspect its core properties:
format: Identifies the file type, like JPEG or PNG.size: Gives you the dimensions in pixels (width, height).mode: Defines the number and type of color channels.RGB, for instance, confirms a standard red, green, and blue color image.
Finally, the show() method provides a simple way to display the current state of the image, which is great for quick visual checks during development.
Common image manipulation techniques
Beyond just inspecting properties, Pillow lets you perform essential manipulations like resizing, rotating, applying filters, and converting between different color modes.
Resizing and rotating images with PIL
from PIL import Image
img = Image.open('sample.jpg')
resized_img = img.resize((400, 300))
rotated_img = img.rotate(45)
resized_img.save('resized.jpg')
rotated_img.save('rotated.jpg')--OUTPUT--# No output, but two new files are created
Altering an image's geometry is straightforward. The resize() and rotate() methods don't change your original image; instead, they return new image objects with the transformations applied.
resize()takes a tuple defining the new(width, height)in pixels.rotate()accepts an angle in degrees to turn the image counter-clockwise.
Once you have your new image object, you just call save() with a filename to write the result to a new file on your disk.
Applying filters and enhancing images
from PIL import Image, ImageFilter, ImageEnhance
img = Image.open('sample.jpg')
blurred = img.filter(ImageFilter.BLUR)
enhancer = ImageEnhance.Contrast(img)
enhanced = enhancer.enhance(1.5) # Increase contrast by 50%
enhanced.save('enhanced.jpg')--OUTPUT--# No output, but a new enhanced image file is created
Pillow's ImageFilter and ImageEnhance modules give you powerful tools for artistic effects and corrections. You can apply common filters or fine-tune properties like brightness and contrast with just a few lines.
- The
filter()method applies a predefined effect, such asImageFilter.BLUR, directly to the image. - For enhancements, you first create an
enhancerobject, likeImageEnhance.Contrast(img). Then, you call itsenhance()method with a factor. A factor of1.5increases the contrast by 50%, while1.0would return the original image.
Converting between color modes
from PIL import Image
img = Image.open('sample.jpg')
grayscale = img.convert('L')
binary = grayscale.point(lambda x: 0 if x < 128 else 255, '1')
grayscale.save('grayscale.jpg')
binary.save('binary.png')--OUTPUT--# No output, but two new files are created
Changing an image's color space is a common task, often done to simplify it for further processing. The convert('L') method transforms a color image into grayscale, where each pixel is represented by a single brightness value instead of three color channels.
- From there, you can create a binary image using the
point()method. It applies a function—in this case,lambda x: 0 if x < 128 else 255—to every pixel. - This function sets pixels darker than a threshold to black (0) and all others to white (255). The second argument,
'1', ensures the output is a true 1-bit binary image.
Advanced image processing techniques
While Pillow is excellent for foundational tasks, you'll need libraries like NumPy and OpenCV to manipulate pixels directly or perform more complex computer vision operations.
Using NumPy for direct pixel manipulation
import numpy as np
from PIL import Image
img = Image.open('sample.jpg')
img_array = np.array(img)
# Invert image colors
inverted_array = 255 - img_array
inverted_img = Image.fromarray(inverted_array)
inverted_img.save('inverted.jpg')--OUTPUT--# No output, but a new inverted image file is created
NumPy gives you direct access to an image's pixel data. By converting the Pillow object into a NumPy array with np.array(), you transform the image into a grid of numbers. This structure allows you to perform fast, mathematical operations across all pixels at once.
- The expression
255 - img_arrayis a vectorized operation. It subtracts each pixel's color value from 255, which effectively inverts the image's colors. - After manipulating the array, you use
Image.fromarray()to convert the numerical data back into a standard image object that can be saved.
Image processing with OpenCV
import cv2
img = cv2.imread('sample.jpg')
# Apply a Gaussian blur
blurred = cv2.GaussianBlur(img, (15, 15), 0)
# Add a weighted combination for sharpening
sharpened = cv2.addWeighted(img, 1.5, blurred, -0.5, 0)
cv2.imwrite('sharpened.jpg', sharpened)--OUTPUT--# No output, but a new sharpened image file is created
OpenCV is a go-to library for advanced computer vision tasks. It reads images directly into a format that's ready for complex mathematical operations, much like NumPy. This example demonstrates a common sharpening technique known as unsharp masking.
- First,
cv2.GaussianBlur()creates a smoothed-out version of the image. - Then,
cv2.addWeighted()combines the original and blurred images. It subtracts a portion of the blur from an amplified original, which enhances edges and details.
Finally, cv2.imwrite() saves the sharpened result to a new file.
Advanced feature detection with OpenCV
import cv2
img = cv2.imread('sample.jpg', 0) # Load as grayscale
edges = cv2.Canny(img, 100, 200) # Edge detection
contours, _ = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contour_img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
cv2.drawContours(contour_img, contours, -1, (0, 255, 0), 2)
cv2.imwrite('contours.jpg', contour_img)--OUTPUT--# No output, but a new image file with contours is created
OpenCV excels at identifying objects within an image. The process starts by loading the image in grayscale, which simplifies the data for analysis. The cv2.Canny() function then scans this simplified image to detect edges—the boundaries between different parts of the picture.
- With the edges identified,
cv2.findContours()traces them to find continuous outlines, or contours, of shapes. - Finally,
cv2.drawContours()visualizes these findings by drawing the outlines directly onto the image, making it easy to see the detected shapes.
Move faster with Replit
Replit is an AI-powered development platform that transforms natural language into working applications. Describe what you want to build, and Replit Agent creates it—complete with databases, APIs, and deployment.
You can use Replit Agent to turn the image processing concepts from this article into production-ready applications.
- Build a bulk image utility that automatically resizes and rotates a folder of images using Pillow's
resize()androtate()methods. - Create an automatic photo enhancer that applies filters and adjusts contrast with
ImageFilterandImageEnhance. - Deploy a contour detection tool that uses OpenCV's
cv2.Canny()andcv2.findContours()to identify and outline objects in uploaded images.
Describe your app idea, and Replit Agent will write the code, test it, and fix issues automatically, all in your browser.
Common errors and challenges
When you're working with images in Python, you'll inevitably run into a few common roadblocks that can trip you up.
A FileNotFoundError is one of the most frequent issues, and it simply means your script can't locate the image file you're trying to open. This usually happens because the path to the file is incorrect or the image isn't in the same directory as your script. To fix this, double-check for typos in the filename and consider using an absolute path for greater reliability.
Processing large images or many files at once can quickly consume your system's memory, causing your script to slow down or crash. The best way to handle this is by using a with statement when opening files. This practice ensures that Python automatically closes the file and releases its memory as soon as you're done with it, even if an error occurs.
You might also see errors when using the save() method, which often stem from format incompatibility. For example, you can't save an image with transparency as a JPEG. The solution is to either convert the image to a compatible mode, like RGB, before saving or to choose a different file format like PNG that supports the features you need.
Troubleshooting FileNotFoundError when opening images
This error is a classic 'file not found' problem, triggered when your script can't locate the specified image. The traceback is your friend here—it will point directly to the line with Image.open(), pinpointing the exact source of the failure. The following code demonstrates this common issue by attempting to open a file that doesn't exist.
from PIL import Image
img = Image.open('nonexistent_image.jpg')
img.show()
The code attempts to load 'nonexistent_image.jpg', but since the file isn't there, Python raises a FileNotFoundError. The next example demonstrates how to handle this situation so your script doesn't crash.
from PIL import Image
import os
image_path = 'nonexistent_image.jpg'
if os.path.exists(image_path):
img = Image.open(image_path)
img.show()
else:
print(f"Error: File {image_path} not found")
You can avoid this error by checking if a file exists before opening it. The os.path.exists() function confirms the file's presence, preventing a crash. By placing your Image.open() call inside an if block, the code only executes if the path is valid. This proactive check is crucial when dealing with files that might be moved or when paths are provided by users, ensuring your script handles missing files gracefully.
Fixing memory issues with the with statement
When you process images in a loop without explicitly closing them, your script's memory usage can skyrocket. Each open image consumes resources that aren't released, eventually causing your program to slow down or crash. The code below shows this problem in action.
from PIL import Image
img = Image.open('large_image.jpg')
# Processing without closing can lead to memory issues in loops
processed = img.resize((800, 600))
processed.save('resized_large.jpg')
The img object isn't closed after use, so it continues to occupy memory. While this is minor for one file, it becomes a major issue in larger scripts. The following example shows a more robust approach.
from PIL import Image
with Image.open('large_image.jpg') as img:
processed = img.resize((800, 600))
processed.save('resized_large.jpg')
# Image is automatically closed after the block
Using a with statement is the Pythonic way to handle files. When you open an image using with Image.open(...) as img, Python guarantees the file will be closed automatically once the code inside the block finishes. This is crucial for preventing memory leaks, especially when you're processing many images in a loop or working with very large files. It's a simple change that makes your code more robust and efficient by managing memory for you.
Resolving format compatibility issues with save()
You'll sometimes run into an error with the save() method if an image's mode is incompatible with the chosen file format. For instance, JPEGs don't support transparency, so saving an RGBA image as a .jpg will fail. The following code demonstrates this exact issue.
from PIL import Image
img = Image.open('sample.jpg')
# Converting to RGBA mode but saving as JPEG (which doesn't support alpha)
img_rgba = img.convert('RGBA')
img_rgba.save('transparent_image.jpg')
The script adds a transparency layer by converting the image to RGBA mode. Because the JPEG format can't handle transparency, the save() method fails. The following code demonstrates how to resolve this mismatch before saving.
from PIL import Image
img = Image.open('sample.jpg')
# Save RGBA images as PNG which supports transparency
img_rgba = img.convert('RGBA')
img_rgba.save('transparent_image.png')
The solution is to match the file format to the image's mode. Since JPEGs don't support transparency, saving the RGBA image as a .png resolves the error. The PNG format is built to handle alpha channels, so save('transparent_image.png') works perfectly. This issue often arises when you work with images that have special features like transparency or indexed color palettes, as not all formats are created equal.
Real-world applications
With a handle on the core techniques, you can now tackle practical workflows like automatically watermarking images or batch processing for social media.
Watermarking images with PIL
You can easily add a text watermark to your images using Pillow's ImageDraw module, which lets you draw text directly onto an image.
from PIL import Image, ImageDraw
img = Image.open('sample.jpg')
draw = ImageDraw.Draw(img)
watermark_text = '© 2023'
position = (img.width - 150, img.height - 50)
draw.text(position, watermark_text, fill=(255, 255, 255))
img.save('watermarked.jpg')
print("Watermark added to image")
This script overlays text onto an image by creating a drawing context with ImageDraw.Draw(). This object allows you to add elements like text directly to the pixel data.
- The
positionis calculated dynamically from the image's dimensions, ensuring the watermark appears in the bottom-right corner. - The
draw.text()method then renders the text, using thefillparameter to set its color to white.
The original image object is modified in place before being saved to a new file.
Batch processing images for social media
You can write a simple script to batch-process a single image, automatically creating optimized versions for every social media platform you use.
from PIL import Image
import os
img = Image.open('sample.jpg')
os.makedirs('social_media', exist_ok=True)
platforms = {
'instagram': (1080, 1080),
'twitter': (1200, 675),
'facebook': (1200, 630)
}
for platform, size in platforms.items():
resized = img.copy()
resized.thumbnail(size)
output_path = f"social_media/{platform}_image.jpg"
resized.save(output_path)
print(f"Created {platform} version: {size}")
This script automates creating multiple image versions from a single source. It loops through a dictionary that maps social media platforms to specific dimensions. For each platform, it creates a fresh copy of the source image using img.copy(), ensuring the original remains untouched.
- The
thumbnail()method resizes the copy. Unlikeresize(), it maintains the aspect ratio while ensuring the image fits within the target(width, height). - Each optimized image is then saved to a
social_mediadirectory with a unique filename, likeinstagram_image.jpg.
Get started with Replit
Turn these concepts into a real tool. Describe what you want to build to Replit Agent, like “a web app that batch-resizes images” or “a utility that applies a grayscale filter to a folder of photos.”
It will write the code, test for errors, and deploy your app from a single prompt. Start building with Replit and see your ideas come to life.
Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.
Create & deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.


.png)
.png)