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