Image Editing with Python
A common issue faced by many people, from the amateur photographer to the remote sensing data scientist, are images which need to be adjusted or edited. Whether its contrast, brightness, or sharpness, learning how to process many images quickly and efficiently using Python can be extremely useful. Whether you need to brighten up that overcast photoshoot, or enhance some satellite image chips to use as training data for your deep learning algorithm, this tutorial should get you started.
- The following tutorial will assume some basic understanding and knowledge of Python development, but you do not need to be an expert to follow along.
- You can find the full script on my GitHub page, at the following link:
https://github.com/daniel-clement/raster-processing/blob/main/Image_Editing_PIL.py
Test Images
Before we get into the meat of the tutorial, lets look at the test images we will be processing and adjusting. The images are cropped satellite images, or “chips”, which were delivered looking dark and washed out. The images did not have any contrast stretching or DRA applied from the factory and need some work to make them useful for use as training data in our deep learning model.
As you can see the images are really dark and are in need of both contrast adjustment and brightness to make them useful.
Import Packages
First, lets import the libraries and packages we will use to process the images. We need glob, tqdm, and Pillow, and will be going into more detail on these packages below.
Define Parameter Variables
In order to process the images, we need to define several variables and store their values as “parameters" which we will use later on in the script.
The inDir
and outDir
variables represent the folders which contain the input imagery and the folder where you want the edited images to be output to.
The contrast_Factor
, brightness_Factor
, and sharpness_Factor
variables allow you to fine tune the actual adjustments being made to the images. Details on the specifics of each option can be seen in the comments above each variable. Once you have the whole script running, it may take several iterations of different adjustment factors to get the parameter values right for your specific set of images. Trial and error will be required.
The input_format
and output_format
variables tell the script what the file format of the input images are, and what format you want the output adjusted images to be. You will see the options for JPG, TIFF, and PNG image formats have been included as inline comments, however, the Pillow package allows for numerous image formats to be read and written. More info on the other packages can be found here.
Create the Function to Process the Images
To process the images, we will use the Pillow Python library. This library allows an incredible amount of image processing capabilities through Python. Today, we will be focusing on Pillow’s Image Enhance module to adjust the images and Pillow’s Image module to read and write the images. We will walk through these processes one at a time, ultimately combining them into a function that will be used to process our images.
First, Image module will help us read the input image. Doing so is very simple and can be done with one simple line of code. We will create a variable that will store an image object created by using the Image.open()
function. The contents of the parenthesis in this case, input_image
represent a parameter of the function we will create in a later step, so dont worry about this right now, just be aware that what ever you put in the parenthesis of the Image.open()
function should represent the path to the image you want to open.
Next, we will use the Image Enhance module to actually adjust the images characteristics. To adjust the contrast, we create a variable named obj
and then set that variable to ie.Contrast(img)
. ie
is how we imported the Image Enhancement module, and Contrast(img)
represents the contrast function of that module. Next, we create a second variable named manualContrastImg
and set that to equal obj.enhance(contrast_factor)
. The contrast_factor
is the parameter we set at the beginning of our script, where 0.0 would give you a grey image (lowest contrast), 1.0 gives you the original image, and anything above 1.0 increases the contrast.
Next, we will also adjust the brightness and sharpness of the image. These adjustments will be constructed with the same method and schema of the contrast adjustment, except that we will iteratively perform the task on the last object we created. The contrast adjustment will be done on the obj
variable, the brightness adjustment will be performed on the obj2
variable and reference the manualContrastImg
variable, and the sharpness adjustment will be performed on obj3
and reference the manBrightImg
variable.
Once we have performed the sharpness enhancement, the manSharpImg
variable contains an object that has had its contrast, brightness, and sharpness adjusted, but the object only exists in memory, and has not been written to a file on the hard drive. In order to save this adjusted image as a file we first need to generate a path and filename for the output image. For this we will use several different functions of Pythons built in os
module.
First, we will use the os.path.basename()
function to determine the name of the input file. Next, we will use Pythons replace function to add “_adjusted” to the end of the input file name, as well as change, if necessary, the file extension (.jpg, .tif, .png). Finally, we will use the os.path.join()
function to join the output folder path with the newly created output file name, constructing the full output file path.
To save the image, we will use the Image module’s .save()
function. The value that goes inside of the parenthesis is the path of the output image, and the variable before the .save()
represents the object you are saving.
When you put all of this together, in the form of a function you get the the code seen below. In this function there are a number of required input parameters defined below:
- input_image: the input image
- contrast_factor: the contrast factor to adjust the image by
- brightness_factor: the brightness factor to adjust the image by
- sharpness_factor: the sharpness factor to adjust the image by
- out_dir: the output folder where you want the adjusted image written to
Almost There!
Now that you’ve defined your image processing function, you simply need to call that function. However, we are going to create a For Loop to enable our script to process multiple images located within a folder, without having to manually run the function on every single one. Using a for loop is a common programming technique which is important to master in order to avoid extremely tedious and time prohibitive work of manually processing hundreds or thousands of images. Speaking of the images we want to process, we will use Pythons glob
module to create a list of all the images in the input folder. To do so we set the value of a variable named inImgs
to make a list of the files matchings in inDir
that match the input format
We will also be using an awesome Python library called tqdm. This is a simple, yet powerful library that allows for the creation of progress bars that show the progress of our script while its running. To use tqdm, you simply create a normal for loop but add tqdm()
with the object you’re looping through inside the parenthesis, like this for image in tqdm(inImgs):
.
The adjustment_process()
function you created before, uses the various script parameters you created in the beginning of this tutorial as function parameters.
Last…We Deal With Errors
Good scripting practices will generally include error handling that will alert the user to any errors arising while the script is running. Error handling can quickly become very complex, and is mostly out of the scope of this tutorial. However, we will implement some basic error handling in order to ensure that all of our images were processed. This handling will simply make sure the number of output files matches the number of input files. To do this we will use Python’s len()
function, and some basic programmatic logic. First, we use glob
again to create a list of the images our script output. We then use the len()
function to return the number of items in both the output image list (outImg
) and the input image list (inImgs
).
Next we will use some basic logic to check if the number of output images is equal to the number of input images, which insinuates that all images were processed successfully. Otherwise, if the number of output images is less than the number of input images, print an error message.
Checking Out Our Results!
The last thing to do is check out our results! You can see that both of our test images have been made much easier to interpret.
That's It!
If you’ve stuck around this long, I hope you have learned a few Python tricks, and enough about some new libraries and modules to allow you to go forth and write your own code. If you have enjoyed this tutorial, please follow me on Medium. And if you would like to connect, you can find me on LinkedIn or my GitHub page.