Using Python to Bulk Resize Images
This tutorial will cover a very simple process for resizing a collection of images. We will be using the Pillow module, which is a fork of the older PIL module. Pillow has been maintained more consistently and been kept up-to-date.
To begin, we will need to add the Pillow package using pip.
pip install Pillow
Once Pillow is installed, we will need to create our new Python file:
touch bulk_image_resize.py
At a minimum, we will need to add the following imports to our file:
bulk_image_resize.py
import os
from PIL import Image
In addition to importing Pillow dependencies, we will need the os
module to read and write files to our file system.
Next, we will add some variables in the conditional that checks if we’re running the script directly.
bulk_image_resize.py
...
if __name__ == "__main__":
output_dir = 'resized'
dir = os.getcwd()
input_dir = 'images'
full_input_dir = dir + '/' + input_dir
The output_dir
variable will be the name of the folder that we will add the re-sized images to. It will be relative to our current directory. Then, we created a dir
variable that gets our current working directory. For now, we will put all the images to be resized into an images
folder relative to the directory we’re currently in. We’ll improve on this later by allowing users to pass in different paths to change the input and output directories.
Since there is a chance that our “resized” directory doesn’t exist (probably because we haven’t run the script yet), let’s check if it exists. Then, create it if it doesn’t. We have already done the first step by importing the os
module. This module gives us the ability to both check if the path exists and create a new directory.
bulk_image_resize.py
...
if __name__ == "__main__":
output_dir = 'resized'
dir = os.getcwd()
input_dir = 'images'
full_input_dir = dir + '/' + input_dir
if not os.path.exists(os.path.join(dir, output_dir)):
os.mkdir(output_dir)
Now that we have resolved our output directory, we can iterate through the files that are in our images folder.
To get all the files in the input directory, we can use the listdir
method from the os
module. Since the user is deciding to run the script, we’ll optimistically assume the “images” directory exists, but, will wrap it in a try-except block just in case.
bulk_image_resize.py
...
if not os.path.exists(os.path.join(dir, output_dir)):
os.mkdir(output_dir)
try:
for file in os.listdir(full_input_dir):
print('file: {}'.format(file))
except OSError:
print('file not found')
We are merely printing the file since we do not have our resizing function setup yet. So, setting up this function would be a good next step.
Resizing the Images
Let’s modify our loop before creating the function:
bulk_image_resize.py
if __name__ == "__main__":
...
try:
for file in os.listdir(full_input_dir):
resize_image(input_dir, file, output_dir)
except OSError:
print('file not found')
We will be passing input_dir, file, and output_dir variables into our resize function.
bulk_image_resize.py
...
def resize_image(input_dir, infile, output_dir="resized", size=(320, 180)):
pass
...
Our resize_image
function will take the output_dir
and size
using keyword arguments with defaults. To avoid any potential conflicts, we will append “_resized” to our file names for any new images we generate. For this, we will need to use the os
module again, this time using the splitext
method so we can split the file from the file extension. Once we’ve add “resized” to the filename, we can re-add the file extension later.
bulk_image_resize.py
...
def resize_image(input_dir, infile, output_dir="resized", size=(320, 180)):
outfile = os.path.splitext(infile)[0] + "_resized"
extension = os.path.splitext(infile)[1]
...
In the snippet above, os.path.splitext(infile)[0]
will give us the file name, while os.path.splitext(infile)[1]
will give us just the file extension. Note also the 320 x 180 dimensions fit a 16:9 ratio. Later, we will add command-line arguments to override these values.
Next, we will use the Pillow Image
class to open and load the image from the file system. Then, we will attempt to resize the image. Once we have the image open, Pillow has a handy resize methods in which we only need to pass in the new dimensions, along with a resample filter argument. We’ll use a high-quality filter known as LANCZOS.
We should wrap this in a try-except block in case the file can’t be opened for whatever reason.
bulk_image_resize.py
...
def resize_image(input_dir, infile, output_dir="resized", size=(320, 180)):
outfile = os.path.splitext(infile)[0] + "_resized"
extension = os.path.splitext(infile)[1]
try:
img = Image.open(input_dir + '/' + infile)
img = img.resize((size[0], size[1]), Image.LANCZOS)
new_file = output_dir + "/" + outfile + extension
img.save(new_file)
except IOError:
print("unable to resize image {}".format(infile))
...
Running the script should successfully resize any images that have been placed in our images folder. We now have a working bulk image resize process.
That being said, in order to make this usable without having to change the script every time, we will use argparse
to allow passing in parameters through the command line. This will give use the ability to do things like pass in the directory in which our existing images are stored, and pass in another directory of where we would like our new, resized images to go. We can also include parameters for the height and width of our resized images.
Adding Command-line Arguments
We will need to import argparse
before setting up our command-line arguments:
bulk_image_resize.py
import os
import argparse
from PIL import Image
...
We also need to use argparse to check for any command-line arguments:
bulk_image_resize.py
...
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input_dir', help='Full Input Path')
parser.add_argument('-o', '--output_dir', help='Full Output Path')
args = parser.parse_args()
In order to make both the defaults and the arguments work, we need to do a refactoring that will only use full paths. We will first check to see if input_dir and/or output_dir are set.
For testing purposes, I’m going to copy my images
folder to a new folder called photos
. It will be in a folder relative to script we’re editing, but, for it to work properly, we will need to pass in the full path.
cp -R images photos
We need to adjust our script to handle passing in an input directory while still being able to use the relative images directory when nothing is being passed in.
bulk_image_resize.py
...
if __name__ == "__main__":
dir = os.getcwd()
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input_dir', help='Full Input Path')
parser.add_argument('-o', '--output_dir', help='Full Output Path')
args = parser.parse_args()
if args.input_dir:
input_dir = args.input_dir
else:
input_dir = dir + '/images'
...
Running the script by passing in our new “photos” folder should yield the same result if we copied the original files over:
python bulk_image_resize.py --input_dir=/path/to/photos
At this point, we haven’t touched our output_dir argument yet. There may be some other location on the file system where we want to place our files. Similar to “input_dir”, we can fully switch to using absolute paths.
bulk_image_resize.py
...
if __name__ == "__main__":
dir = os.getcwd()
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input_dir', help='Full Input Path')
parser.add_argument('-o', '--output_dir', help='Full Output Path')
args = parser.parse_args()
if args.input_dir:
input_dir = args.input_dir
else:
input_dir = dir + '/images'
if args.output_dir:
output_dir = args.output_dir
else:
output_dir = dir + '/resized'
if not os.path.exists(os.path.join(dir, output_dir)):
os.mkdir(output_dir)
try:
for file in os.listdir(input_dir):
resize_image(input_dir, file, output_dir)
except OSError:
print('file not found')
Now, we have the ability to place files anywhere we’d like.
python bulk_image_resize.py --input_dir=/path/to/photos --output_dir=/path/to/somewhere/else
The final feature we’d like to have is the ability to change the dimensions of the resized files. We will setup command-line arguments for the width and height.
Before adding the new arguments, we will add a new constant containing the default size at the top of the file. This way, it will only ever need to be defined once.
import os
import argparse
from PIL import Image
DEFAULT_SIZE = (320, 180)
def resize_image(input_dir, infile, output_dir="resized", size=DEFAULT_SIZE):
outfile = os.path.splitext(infile)[0] + "_resized"
extension = os.path.splitext(infile)[1]
...
In our resize_image
function, we have also switched to using the constant.
Inside our conditional, we will add command-line arguments for the width and height.
if __name__ == "__main__":
dir = os.getcwd()
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input_dir', help='Full Input Path')
parser.add_argument('-o', '--output_dir', help='Full Output Path')
parser.add_argument('-w', '--width', help='Resized Width')
parser.add_argument('-t', '--height', help='Resized Height')
args = parser.parse_args()
It’s important to note that the shorthand syntax for height will be “t” since -h
is reserved by argparse for help. Using -h will tell the user what parameters can be passed in:
python bulk_image_resize.py -h
usage: bulk_image_resize.py [-h] [-i INPUT_DIR] [-o OUTPUT_DIR] [-w WIDTH]
[-t HEIGHT]
optional arguments:
-h, --help show this help message and exit
-i INPUT_DIR, --input_dir INPUT_DIR
Full Input Path
-o OUTPUT_DIR, --output_dir OUTPUT_DIR
Full Output Path
-w WIDTH, --width WIDTH
Resized Width
-t HEIGHT, --height HEIGHT
Resized Height
We also need to check if both heigh and width are set. If not, we don’t want to risk changing the aspect ratio, so unless both arguments are passed in, we will stick with the defaults.
bulk_image_resize.py
...
if __name__ == "__main__":
dir = os.getcwd()
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input_dir', help='Full Input Path')
parser.add_argument('-o', '--output_dir', help='Full Output Path')
parser.add_argument('-w', '--width', help='Resized Width')
parser.add_argument('-t', '--height', help='Resized Height')
args = parser.parse_args()
if args.input_dir:
input_dir = args.input_dir
else:
input_dir = dir + '/images'
if args.output_dir:
output_dir = args.output_dir
else:
output_dir = dir + '/resized'
if args.width and args.height:
size = (int(args.width), int(args.height))
else:
size = DEFAULT_SIZE
if not os.path.exists(os.path.join(dir, output_dir)):
os.mkdir(output_dir)
try:
for file in os.listdir(input_dir):
resize_image(input_dir, file, output_dir, size=size)
except OSError:
print('file not found')
We should now be able to apply a new width and height:
python bulk_image_resize.py --width=640 --height=360
We are now able to easily bulk resize images. We have the ability to pass in directories for our inputs and outputs. We can also set our own size dimensions.
The full source code can be found below.
Posted in python