The Python Pillow Image Library part 2
In a previous post I introduced the Pillow imaging library and demonstrated some of its core functionality. In this article I’ll show a few more features including the seemingly Dark Art of getting decent black and white images from a colour photo.
This article covers the following topics:
Converting an image to black and white the wrong way
Converting an image to black and white the right way
Improving a flat and dull B&W image by increasing its contrast
Splitting an image into its RGB channels, editing them, and then gluing them back together
Showing how to save images at various qualities
The Files
The files for this article are in the same Github repository as those for Part 1. I have used the same photo which is shown below but you’ll probably want to run the code with your own.
Imports, main and image information
This is the first part of pillowintro2.py which is a recap of the first part of my previous post, and simply opens an image file and shows some information about it. Most of the function calls are commented out so we can uncomment and run them one at a time.
pillowintro2.py part 1
import PIL
from PIL import Image
from PIL import ImageEnhance
def main():
print(”-----------------”)
print(”| codedrome.com |”)
print(”| Pillow 2 |”)
print(”-----------------\n”)
openfilepath = “photo.jpg”
show_image_info(openfilepath)
# The desaturate function is the wrong way to change an image
# to black and white and is included here with show_image_info
# to demonstrate that it leaves the image with a mode of RGB
# desaturate(openfilepath, “photo_desaturated.jpg”)
# show_image_info(”photo_desaturated.jpg”)
# This is the correct way to convert an image to B&W.
# Calling show_image_info will show a mode of L
# mode_L(openfilepath, “photo_mode_L.jpg”)
# show_image_info(”photo_mode_L.jpg”)
# contrast(”photo_mode_L.jpg”, “photo_mode_L_contrast.jpg”, 1.5)
# bands_brightness(openfilepath, “photo_bands_brightness.jpg”, 1.2, 1.0, 1.0)
# quality_demo(openfilepath)
def show_image_info(openfilepath: str) -> None:
“”“
Open an image and show a few attributes
“”“
try:
image = Image.open(openfilepath)
print(”filename: {}”.format(image.filename))
print(”size: {}”.format(image.size))
print(”width: {}”.format(image.width))
print(”height: {}”.format(image.height))
print(”format: {}”.format(image.format))
print(”format description: {}”.format(image.format_description))
print(”mode: {}\n”.format(image.mode))
except IOError as ioe:
print(ioe)If you want to use one of your own photos edit the line assigning a value to openfilepath, and then run the program with the following command:
python3 pillowintro2.py
This will give us the following output. Note in particular that the mode is RGB.
Converting an Image to Black and White
I mentioned in the previous post that reducing the saturation to 0 has the effect of converting the image to black and white. The first of the following functions does that, and the second does the same job but by using the convert method with an argument of “L”. (Despite extensive Googling I have been unable to find out what “L” stands for.)
pillowintro2.py part 2
def desaturate(openfilepath: str, savefilepath: str) -> None:
“”“
Convert an image to black and white the wrong way.
This method still leaves the image with a colour
depth of 24 bit RGB.
The correct method is to use convert(”L”)
“”“
try:
image = Image.open(openfilepath)
enhancer = ImageEnhance.Color(image)
image = enhancer.enhance(0.0)
image.save(savefilepath)
print(”Image desaturated”)
except IOError as ioe:
print(ioe)
except ValueError as ve:
print(ve)
def mode_L(openfilepath: str, savefilepath: str) -> None:
“”“
The correct way to convert an image to black and white.
Do not use ImageEnhance.Color to reduce saturation to 0
as that leaves the colour depth at 24 bit.
“”“
try:
image = Image.open(openfilepath)
image = image.convert(”L”)
image.save(savefilepath)
print(”Mode changed to L”)
except IOError as ioe:
print(ioe)
except ValueError as ve:
print(ve)Uncomment desaturate and mode_L in main as well as the two calls to show_image_info. Run the program again which will give us the following output.
If you go to the folder where you have your source code and images you’ll find a couple more files have been created which are visually identical despite their technical differences. The first, photo_desaturated.jpg, was created by the desaturate function and appears to be black and white but as you can see from the above output it is technically an RGB image which happens to contain only shades of grey.
The second function, mode_L, does the conversion properly and creates photo_mode_L.jpg which does actually have an 8-bit colour depth or a mode of L - this is that image.
Improving Contrast
The above image doesn’t look too bad but a very common problem is that images converted from colour to black and white look rather flat and boring. To solve this we need to increase the contrast, often by quite a lot.
The following function does this, and is a more general-purpose version of the contrast function in the previous post. Instead of having the contrast amount hard-coded for demo purposes it takes a value as an argument.
pillowintro2.py part 3
def contrast(openfilepath: str, savefilepath: str, amount: float) -> None:
“”“
A general-purpose function to change the contrast
by the specified amount and save the image.
“”“
try:
image = Image.open(openfilepath)
enhancer = ImageEnhance.Contrast(image)
image = enhancer.enhance(amount)
image.save(savefilepath)
print(”Contrast changed”)
except IOError as ioe:
print(ioe)
except ValueError as ve:
print(ve) Uncomment the call to contrast in main and run the program. It will create this image which is a lot punchier.
Splitting and Editing Colour Bands
The three colour channels (or bands to use Pillow’s terminology) of an RGB image can be separated, edited individually, and then glued back together. Most of the time you’ll want to edit the whole image but editing individual channels allows you to alter the colour balance, and the following function does that by altering the brightnesses of the red, green and blue channels by the specified amounts.
After opening the image it calls the split() method. This returns a tuple of three images but as we need to overwrite them the tuple is cast to a list.
It then uses ImageEnhance.Brightness which I introduced in the earlier post, but on the three colour channels individually. You could also use ImageEnhance.Color or ImageEnhance.Contrast, or even a combination of all three. Finally we stick the channels back together into a single image using merge() and then save that image.
pillowintro2.py part 4
def bands_brightness(openfilepath: str, savefilepath: str, r: float, g: float, b: float) -> None:
“”“
Split the image into colour channels (bands).
Change the brightness of each by the specified amount (1 = no change).
Merge the channels and save the image.
“”“
try:
image = Image.open(openfilepath)
# image.split() returns a tuple so we need to convert
# it to a list so we can overwrite the bands.
bands = list(image.split())
enhancer = ImageEnhance.Brightness(bands[0])
bands[0] = enhancer.enhance(r)
enhancer = ImageEnhance.Brightness(bands[1])
bands[1] = enhancer.enhance(g)
enhancer = ImageEnhance.Brightness(bands[2])
bands[2] = enhancer.enhance(b)
image = PIL.Image.merge(”RGB”, bands)
image.save(savefilepath)
print(”Band brightnesses changed”)
except IOError as ioe:
print(ioe)
except ValueError as ve:
print(ve)In main I have called bands_brightness with values of 1.2 for red and 1.0 for green and blue, the latter two indicating no change. This should give the photo a warmer look.
Uncomment the function call in main and run the program. This is the result. Even a fairly modest 1.2 value for the red channel is too much, but at least it demonstrates the process.
Saving Images at Various Qualities
Finally, lets look at changing the quality of files while saving them. The save method has an optional argument called quality which can be any value between 1 and 100, the higher the number the lower the level of compression and therefore the better the quality. Note however that it is widely accepted that values over 95 significantly increase file sizes with negligible improvement.
The following function demonstrates this by saving the supplied image at 25, 50, 75 and 100 using a loop, and with the qualities as file names.
pillowintro2.py part 5
def quality_demo(openfilepath: str) -> None:
“”“
Save the specified image at several different quality levels
for demonstration purposes.
Quality can be any value between 1 (awful) to 100 (best).
Anything < 50 is unlikely to be acceptable.
“”“
try:
image = Image.open(openfilepath)
for q in range(25, 101, 25):
filename = f”quality {q}.jpg”
image.save(filename, quality=q)
print(f”Image saved at quality {q}”)
except IOError as ioe:
print(ioe)
except ValueError as ve:
print(ve)If you run it from main you’ll get four new files of various sizes, the smallest and worst being this one at 25.
I tried saving the image with a quality of 1 but the result was too dreadful to inflict on anyone, and even 25 is pretty poor as you can see.
The image saved at 75 was very good and acceptable for use on a web site but in an age of huge and cheap storage and fast internet connections I don’t think there is really any need to trade quality for file size. If you really need to cut down on file sizes I think most people would rather see physically smaller images in good quality than larger images of poor quality.
Please let me know if you have any comments or suggestions about this article or Pillow in general.
No AI was used in the creation of the code, text or images in this article.










