Python: Building an Image Manipulation Tool (Part 2)

A tutorial and case study on developing an image processing application with Python

Image for post
Image for post

Introduction

Source Files

$ git clone https://github.com/danebulat/py-image-manip-demo.git

1. Parsing Command Line Arguments

Creating an Argument Parser

namespace = parser.parse_arguments()
Creating an ArgumentParser object.

Positional Arguments

parser.add_argument('inputs', action='store', nargs='+',
help='A list of image files to process.')

Special Optional Arguments

parser.add_argument('-h', '--help', action='help',
default=argparse.SUPPRESS,
help="Show this help message and exit.")

parser.add_argument('--version', action='version',
version='%(prog)s ' + get_version_string(),
help="Show program's version number and exit.")

Boolean Optional Arguments

parser.add_argument('-i', '--info', action='store_true',
help='Display image properties.')

parser.add_argument('--thumbnail', action='store_true',
help='A thumbnail is generated.')

Single Value Optional Arguments

parser.add_argument(
'--factor', action='store', default=1.2, type=float,
help='Float to specify the strength of enhancers.')

parser.add_argument(
'--thumbnail-width', action='store', type=int, default=128,
help='Specify a width for the generated thumbnail.')

Optional Arguments with Pre-Determined Choices

parser.add_argument(
'--flip', action='store', nargs='+', choices=['horz', 'vert'],
help='Flip an image vertically or horizontally.')

parser.add_argument(
'--rotate', action='store', choices=[90, 180, 270], type=int,
help="Rotate an image in a 90 degree increment.")

Multiple Value Optional Arguments

parser.add_argument(
'-f', '--filter', action='store', nargs='+',
help='Apply filters to an images.', dest='filters')

parser.add_argument(
'-e', '--enhance', action='store', nargs='+',
help='Apply enhancers to an images.', dest='enhancers')

Mutually Exclusive Optional Arguments

Defining resize arguments in a mutually exclusive argument group.

Returning Arguments to the Application

args = parser.parse_args()
return args

2. Handling Parsed Arguments

for index, filename in enumerate(namespace.inputs):
try:
with Image.open(filename) as opened_image:
handle_arguments(namespace, opened_image, index)
except FileNotFoundError as err:
print(f"WARNING: {err.filename} not found. Skipping...")

Handling Arguments

from manipulator import ImageManipulator
def handle_arguments(namespace, image):
"""Function to handle command line arguments."""
manip = ImageManipulator(image, index)
Handling objects received from the command line.
# Save the new file if necessary
manip.save_image()

Handler Functions

Validation Functions

def _is_valid_filter(filter_string):
"""Validates a passed filter string."""

if filter_string in [
'BLUR', 'CONTOUR', 'DETAIL', 'EDGE_ENHANCE',
'EDGE_ENHANCE_MORE','EMBOSS', 'FIND_EDGES',
'SHARPEN', 'SMOOTH', 'SMOOTH_MORE']:
return True
return False
def _is_valid_enhancer(enhancer_string):
"""Validates a passed enhancer string."""
if enhancer_string in [
'BRIGHTNESS', 'COLOR', 'COLOUR', 'CONTRAST',
'SHARPNESS', 'GREYSCALE', 'GAUSSIANBLUR', 'BOXBLUR']:
return True
return False

3. Image Processing with PIL

from PIL import Image, ImageEnhance, ImageFilter
def __init__(self, image):
self.image = image
self.original_filename = image.filename
self.save_flag = False
self.index = index

Displaying Image Properties

def output_information(self):
"""Sends image properties to standard output."""
print("{0:<9}: {1}".format("Filename", self.image.filename))
print("{0:<9}: {1}".format("Size", self.image.size))
print("{0:<9}: {1}".format("Format", self.image.format))
print("{0:<9}: {1}\n".format("Bands", self.image.getbands()))

Applying Image Filters

def apply_filter_and_save(self, filter_string):
"""Apply a filter to a loaded image and save the new version."""

self.save_flag = True
self.image = self._apply_filter(filter_string)
print(f"{filter_string} filter applied to \
{self.original_filename}")
How a filter is applied to loaded image data.

Applying Image Enhancements

Creating enhancer objects based on an enhancer string.
def _apply_enhancer(self, enhancer, enhancer_string, factor):
"""Enhances an image and saves a new file."""

self.image = enhancer.enhance(factor)
print("{0} enhancer applied to {1} [factor = {2}]"
.format(enhancer_string, self.original_filename, factor))

Generating Greyscale Images

elif enhancer_string == 'GREYSCALE':
self.image = self.image.convert('L')
print("{0} enhancer applied to {1}"
.format(enhancer_string, self.original_filename))

Gaussian Blur and Box Blur Filters

elif enhancer_string in ['GAUSSIANBLUR', 'BOXBLUR']:

if enhancer_string == 'GAUSSIANBLUR':
self.image =
self.image.filter(ImageFilter.GaussianBlur(factor))
else:
self.image = self.image.filter(ImageFilter.BoxBlur(factor))

print("{0} filter applied to {1} [radius = {2}]".format(
enhancer_string, self.original_filename, factor))

Generating a Thumbnail

The method that handles thumbnail generation.

Flipping Images

def apply_flips(self, flips):
"""Flips the image horizontally or vertically."""

for flip in flips:
if flip.upper() == 'HORZ':
self.image = self.image.transpose(Image.FLIP_LEFT_RIGHT)
print(f"{self.original_filename} flipped horizontally.")

elif flip.upper() == 'VERT':
self.image = self.image.transpose(Image.FLIP_TOP_BOTTOM)
print(f"{self.original_filename} flipped vertically.")

self.save_flag = True

Resizing Images

def resize_image(self, size):
"""Resizes cached image to the X and Y values in the
passed tuple."""
self.image = self.image.resize(size)
print(f"{self.original_filename} resized to: {size}")

self.save_flag = True

Rotating Images

def rotate_image(self, increment):
"""Rotates the cached image counter-clockwise in a 90 degree
increment.
"""


if increment == 90:
self.image = self.image.transpose(Image.ROTATE_90)
elif increment == 180:
self.image = self.image.transpose(Image.ROTATE_180)
else:
self.image = self.image.transpose(Image.ROTATE_270)

print(f"{self.original_filename} rotated {increment} "
f"degrees CCW.")
self.save_flag = True

Saving the Image

def save_image(self):
"""Saves the image to a new file if save flag is set."""
if self.save_flag is True:
name, ext = os.path.splitext(self.original_filename)
out_filename = name + '-manip-' + str(self.index) + ext
self.image.save(out_filename)
print(f"New file saved as: {out_filename}")

In Conclusion — Part 2

MSc. Programmer and fan of open source software.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store