Title: Detecting low contrast images with OpenCV, scikit-image, and Python - PyImageSearch

URL Source: https://pyimagesearch.com/2021/01/25/detecting-low-contrast-images-with-opencv-scikit-image-and-python/

Published Time: 2021-01-25T15:00:00+00:00

Markdown Content:
In this tutorial you will learn how to detect low contrast images using OpenCV and scikit-image.

![Image 1: opencv, scikit-image, image processing, video, video stream by Dr. Adrian Rosebrock on PyImageSearch](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20700%20499'%3E%3C/svg%3E)

Whenever I teach the fundamentals of computer vision and image processing to students eager to learn, one of the _first_ things I teach is:

> _“It’s far easier to write code for images captured in **controlled lighting conditions** than in **dynamic conditions with no guarantees.**”_

If you are able to control the environment and, most importantly, _the lighting_ when you capture an image, the easier it will be to write code to process the image.

With controlled lighting conditions you’re able to hard-code parameters, including:

*   Amount of blurring
*   Edge detection bounds
*   Thresholding limits
*   Etc.

Essentially, controlled conditions allow you to take advantage of your _a priori_ knowledge of an environment and then write code that handles that _specific_ environment rather than trying to handle _every_ edge case or condition.

Of course, controlling your environment and lighting conditions isn’t always possible …

… so what do you do then?

Do you try to code a super complex image processing pipeline that handles every edge case?

Well … you could do that — and probably waste weeks or months doing it and _still_ likely not capture every edge case.

**Or, you can instead _detect_ when low quality images, specifically _low contrast images_, are presented to your pipeline.**

If a low contrast image is detected, you can throw the image out or alert the user to capture an image in better lighting conditions.

Doing so will make it far easier for you to develop image processing pipelines (and reduce your headaches along the way).

**To learn how to detect low contrast images with OpenCV and scikit-image, _just keep reading._**

![Image 2](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20128%20128'%3E%3C/svg%3E)

#### Looking for the source code to this post?

[Jump Right To The Downloads Section](#download-the-code)

In the first part of this tutorial, we’ll discuss what low contrast images are, the problems they cause for computer vision/image processing practitioners, and how we can programmatically detect these images.

From there we’ll configure our development environment and review our project directory structure.

With our project structure reviewed, we’ll move on to coding two Python scripts:

1.  One to detect low contrast in static images
2.  And another to detect low contrast frames in real-time video streams

We’ll wrap up our tutorial with a discussion of our results.

### **What problems do low contrast images/frames create? And how can we detect them?**

![Image 3](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20700%20467'%3E%3C/svg%3E)

**Figure 1:** **Left:** Example of low contrast image where it would be hard to detect the outline of the card. **Right:** Higher contrast image where detecting the card would be far easier for a computer vision/image processing pipeline.

A low contrast image has very little difference between light and dark regions, making it hard to see where the boundary of an object begins and the background of the scene starts.

An example of a low contrast image is shown in **Figure 1 _(left)._** Here you can see a color matching/correction card on a background. Due to poor lighting conditions (i.e., not enough light), the boundaries of the card against the background are not well defined — by itself, an edge detection algorithm, such as the Canny edge detector, may struggle to detect the boundary of the card, _especially_ if the Canny edge detector parameters are hard-coded.

**Figure 1 _(right)_** shows an example image of “normal contrast”. We have more detail in this image due to better lighting conditions. Notice that the white of the color matching card sufficiently contrasts the background — it would be _far easier_ for an image processing pipeline to detect the edges of the color matching card (compared to the _right_ image).

**Whenever you’re tackling a computer vision or image processing problem, _always start with the environment the image/frame is captured in._** The more you can control and guarantee the lighting conditions, the _easier_ a time you will have writing code to process the scene.

However, there will be times when you _cannot_ control the lighting conditions and any parameters you hard-coded into your pipeline (ex., blur sizes, thresholding limits, Canny edge detection parameters, etc.) may result in incorrect/unusable output.

When that inevitably happens, don’t throw in the towel. And _certainly_ don’t start going down the rabbit hole of coding up complex image processing pipelines to handle every edge case.

**Instead, leverage low contrast image detection.**

Using low contrast image detection, you can programmatically detect images that are not sufficient for your image processing pipeline.

In the remainder of this tutorial, you’ll learn how to detect low contrast images in both static scenes and real-time video streams.

We’ll throw out images/frames that are low contrast and not suitable for our pipeline, while keeping only the ones that we know will produce usable results.

By the end of this guide, you’ll have a good understanding of low contrast image detection, and you’ll be able to apply it to your own projects, thereby making your own pipelines easier to develop and more stable in production.

### **Configuring your development environment**

In order to detect low contrast images, you need to have the [OpenCV library](https://opencv.org/) as well as [scikit-image](https://scikit-image.org/) installed.

Luckily, both of these are pip-installable:

$ pip install opencv-contrib-python
$ pip install scikit-image

**If you need help configuring your development environment for OpenCV and scikit-image, I _highly recommend_ that you read my _[pip install OpenCV guide](https://pyimagesearch.com/2018/09/19/pip-install-opencv/)_** — it will have you up and running in a matter of minutes.

### **Having problems configuring your development environment?**

[![Image 4](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20500%20334'%3E%3C/svg%3E)](https://pyimagesearch.com/pyimagesearch-plus/)

**Figure 2:** Having trouble configuring your dev environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join [PyImageSearch Plus](https://pyimagesearch.com/pyimagesearch-plus/) — you’ll be up and running with this tutorial in a matter of minutes.

All that said, are you:

*   Short on time?
*   Learning on your employer’s administratively locked system?
*   Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?
*   **Ready to run the code _right now_ on your Windows, macOS, or Linux system?**

Then join [PyImageSearch Plus](https://pyimagesearch.com/pyimagesearch-plus/) today!

**Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides that are _pre-configured_ to run on Google Colab’s ecosystem right in your web browser!** No installation required.

And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!

### **Project structure**

Before we get too far in this guide, let’s take a second to inspect our project directory structure.

Start by using the **_“Downloads”_** section of this tutorial to download the source code, example images, and sample video:

$ tree . --dirsfirst
.
├── examples
│   ├── 01.jpg
│   ├── 02.jpg
│   └── 03.jpg
├── detect\_low\_contrast\_image.py
├── detect\_low\_contrast\_video.py
└── example\_video.mp4

1 directory, 6 files

We have two Python scripts to review today:

1.  `detect_low_contrast_image.py`: Performs low contrast detection in static _images_ (i.e., images inside the `examples` directory)
2.  `detect_low_contrast_video.py`: Applies low contrast detection to _real-time video streams_ (in this case, `example_video.mp4`)

You can of course substitute in your own images and video files/streams as you see fit.

### **Implementing low contrast image detection with OpenCV**

Let’s learn how to detect low contrast images with OpenCV and scikit-image!

Open up the `detect_low_contrast_image.py` file in your project directory structure, and insert the following code.

\# import the necessary packages
from skimage.exposure import is\_low\_contrast
from imutils.paths import list\_images
import argparse
import imutils
import cv2

We start off on **Lines 2-6** importing our required Python packages.

Take special note of the `is_low_contrast` import from the [scikit-image library](https://scikit-image.org/). This function is used to detect low contrast images by examining an image’s histogram and then determining if the range of brightness spans less than a fractional amount of the full range.

We’ll see how to use the `is_low_contrast` function later in this example.

We then import `list_images` to grab the paths to our images in the `` `examples` `` directory, `argparse` for command line arguments, `imutils` for image processing routines, and `cv2` for our OpenCV bindings.

Let’s move on to parsing our command line arguments:

\# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add\_argument("-i", "--input", required=True,
	help="path to input directory of images")
ap.add\_argument("-t", "--thresh", type=float, default=0.35,
	help="threshold for low contrast")
args = vars(ap.parse\_args())

We have two command line arguments, the first of which is required and the second optional:

1.  `--input`: Path to our input image residing on disk
2.  `` `--thresh` ``: The threshold for low contrast

I’ve set the `--thresh` parameter to a default of `0.35`, implying that an image will be considered low contrast _“when the range of brightness spans less than this fraction of its data type’s full range”_ ([official scikit-image documentation](https://scikit-image.org/docs/dev/api/skimage.exposure.html#skimage.exposure.is_low_contrast)).

Essentially, what this means is that if less than 35% of the range of brightness occupies the full range of the data type, then the image is considered low contrast.

To make this a concrete example, consider that an image in OpenCV is represented by an unsigned 8-bit integer that has a range of values _\[0, 255\]._ If the distribution of pixel intensities occupies _less_ than 35% of this _\[0, 255\]_ range, then the image is considered low contrast.

You can of course tune the `--thresh` parameter to whatever percentage you deem fitting for your application, but I’ve found that 35% is a good starting point.

Moving on, let’s grab the image paths from our `--input` directory:

\# grab the paths to the input images
imagePaths = sorted(list(list\_images(args\["input"\])))

# loop over the image paths
for (i, imagePath) in enumerate(imagePaths):
	# load the input image from disk, resize it, and convert it to
	# grayscale
	print("\[INFO\] processing image {}/{}".format(i + 1,
		len(imagePaths)))
	image = cv2.imread(imagePath)
	image = imutils.resize(image, width=450)
	gray = cv2.cvtColor(image, cv2.COLOR\_BGR2GRAY)

	# blur the image slightly and perform edge detection
	blurred = cv2.GaussianBlur(gray, (5, 5), 0)
	edged = cv2.Canny(blurred, 30, 150)

	# initialize the text and color to indicate that the input image
	# is \*not\* low contrast
	text = "Low contrast: No"
	color = (0, 255, 0)

**Line 17** grabs the paths to our images in the `examples` directory. We then loop over each of these individual `imagePaths` on **Line 20**.

For each `imagePath` we proceed to:

1.  Load the `image` from disk
2.  Resize it to have a width of 450 pixels
3.  Convert the image to grayscale

From there we apply blurring (to reduce high frequency noise) and then apply the Canny edge detector (**Lines 30 and 31**) to detect edges in the input image.

**Lines 35 and 36** make the _assumption_ that the image is _not_ low contrast, setting the `text` and `color`.

The following code block handles the `if/else` condition if a low contrast image is detected:

	# check to see if the image is low contrast
	if is\_low\_contrast(gray, fraction\_threshold=args\["thresh"\]):
		# update the text and color
		text = "Low contrast: Yes"
		color = (0, 0, 255)

	# otherwise, the image is \*not\* low contrast, so we can continue
	# processing it
	else:
		# find contours in the edge map and find the largest one,
		# which we'll assume is the outline of our color correction
		# card
		cnts = cv2.findContours(edged.copy(), cv2.RETR\_EXTERNAL,
			cv2.CHAIN\_APPROX\_SIMPLE)
		cnts = imutils.grab\_contours(cnts)
		c = max(cnts, key=cv2.contourArea)

		# draw the largest contour on the image
		cv2.drawContours(image, \[c\], -1, (0, 255, 0), 2)

	# draw the text on the output image
	cv2.putText(image, text, (5, 25), cv2.FONT\_HERSHEY\_SIMPLEX, 0.8,
		color, 2)

	# show the output image and edge map
	cv2.imshow("Image", image)
	cv2.imshow("Edge", edged)
	cv2.waitKey(0)

**Line 39** makes a call to scikit-image’s `is_low_contrast` function to detect whether our `gray` image is low contrast or not. Note how we are passing in the `fraction_threshold`, which is our `--thresh` command line argument.

If the image is indeed low contrast, then we update our `text` and `color` variables (**Lines 41 and 42**).

Otherwise, the image is _not_ low contrast, so we can proceed with our image processing pipeline (**Lines 46-56**). Inside this code block we:

1.  Find contours in our edge map
2.  Find the largest contour in our `cnts` list (which we assume will be our card in the input image)
3.  Draw the outline of the card on the image

Finally, we draw the `text` on the `image` and display both the `image` and edge map to our screen.

### **Low contrast image detection results**

Let’s now apply low contrast image detection to our own images!

Start by using the **_“Downloads”_** section of this tutorial to download the the source code and example images:

$ python detect\_low\_contrast\_image.py --input examples
\[INFO\] processing image 1/3
\[INFO\] processing image 2/3
\[INFO\] processing image 3/3

![Image 5](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20700%20499'%3E%3C/svg%3E)

**Figure 3:** This example image is labeled as “low contrast”. Applying the Canny edge detector with hard-coded parameters shows that we _cannot_ detect the outline of the card in the image. **Ideally, we would discard this image from our pipeline due to its low quality.**

Our first image here is labeled as “low contrast”. As you can see, applying the Canny edge detector to the low contrast image results in us being unable to detect the outline of the card in the image.

If we tried to process this image further and detected the card itself, we would end up detecting some _other_ contour. Instead, by applying low contrast detection, we can simply ignore the image.

Our second image has sufficient contrast, and as such, we are able to accurately compute the edge map and extract the contour associated with the card outline:

![Image 6](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20700%20499'%3E%3C/svg%3E)

**Figure 4:** This image is labeled as sufficient contrast.

Our final image is also labeled as having sufficient contrast:

![Image 7](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20700%20499'%3E%3C/svg%3E)

**Figure 5:** Automatically detecting low contrast images with OpenCV and scikit-image.

We are again able to compute the edge map, perform contour detection, and extract the contour associated with the outline of the card.

### **Implementing low contrast frame detection in real-time video streams**

In this section you will learn how to implement low contrast frame detection in real-time video streams using OpenCV and Python.

Open up the `detect_low_contrast_video.py` file in your project directory structure, and let’s get to work:

\# import the necessary packages
from skimage.exposure import is\_low\_contrast
import numpy as np
import argparse
import imutils
import cv2

Our import statements here are near identical to our previous script. Note that again we are using scikit-image’s `is_low_contrast` function to detect low contrast frames.

We then have our command line arguments, both of which are optional:

\# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add\_argument("-i", "--input", type=str, default="",
	help="optional path to video file")
ap.add\_argument("-t", "--thresh", type=float, default=0.35,
	help="threshold for low contrast")
args = vars(ap.parse\_args())

The `--input` switch points to a (optional) video file on disk. By default this script will access your webcam, but if you want to supply a video file, you can do so here.

The `--thresh` parameter is identical to that of our previous script. This argument controls the `fraction_threshold` parameter to the `is_low_contrast` function. Refer to the _“Implementing low contrast image detection with OpenCV”_ for a detailed description of this parameter.

Let’s now access our video stream:

\# grab a pointer to the input video stream
print("\[INFO\] accessing video stream...")
vs = cv2.VideoCapture(args\["input"\] if args\["input"\] else 0)

# loop over frames from the video stream
while True:
	# read a frame from the video stream
	(grabbed, frame) = vs.read()

	# if the frame was not grabbed then we've reached the end of
	# the video stream so exit the script
	if not grabbed:
		print("\[INFO\] no frame read from stream - exiting")
		break

	# resize the frame, convert it to grayscale, blur it, and then
	# perform edge detection
	frame = imutils.resize(frame, width=450)
	gray = cv2.cvtColor(frame, cv2.COLOR\_BGR2GRAY)
	blurred = cv2.GaussianBlur(gray, (5, 5), 0)
	edged = cv2.Canny(blurred, 30, 150)

	# initialize the text and color to indicate that the current
	# frame is \*not\* low contrast
	text = "Low contrast: No"
	color = (0, 255, 0)

**Line 18** instantiates a point to our video stream. By default we’ll use our webcam; however, if you are a video file, you can supply the `--input` command line argument.

We then loop over frames from the video stream on **Line 21.** Inside the loop we:

1.  Read the next `frame`
2.  Detect whether we’ve reached the end of the video stream, and if so, `break` from the loop
3.  Preprocess the frame by converting it to grayscale, blurring it, and applying the Canny edge detector

We also initialize our `text` and `color` variables with the assumption that the image is _not_ low contrast.

Our next code block is essentially identical to our previous script:

	# check to see if the frame is low contrast, and if so, update
	# the text and color
	if is\_low\_contrast(gray, fraction\_threshold=args\["thresh"\]):
		text = "Low contrast: Yes"
		color = (0, 0, 255)

	# otherwise, the frame is \*not\* low contrast, so we can continue
	# processing it
	else:
		# find contours in the edge map and find the largest one,
		# which we'll assume is the outline of our color correction
		# card
		cnts = cv2.findContours(edged.copy(), cv2.RETR\_EXTERNAL,
			cv2.CHAIN\_APPROX\_SIMPLE)
		cnts = imutils.grab\_contours(cnts)
		c = max(cnts, key=cv2.contourArea)

		# draw the largest contour on the frame
		cv2.drawContours(frame, \[c\], -1, (0, 255, 0), 2)

**Lines 45-47** check to see if the image is low contrast, and if so, we update our `text` and `color` variables.

Otherwise, we proceed to:

1.  Detect contours
2.  Find the largest contour
3.  Draw the largest contour on the `frame`

Our final code block draws the `text` on the `output` frame:

	# draw the text on the output frame
	cv2.putText(frame, text, (5, 25), cv2.FONT\_HERSHEY\_SIMPLEX, 0.8,
		color, 2)

	# stack the output frame and edge map next to each other
	output = np.dstack(\[edged\] \* 3)
	output = np.hstack(\[frame, output\])

	# show the output to our screen
	cv2.imshow("Output", output)
	key = cv2.waitKey(1) & 0xFF

	# if the \`q\` key was pressed, break from the loop
	if key == ord("q"):
		break

We also stack the edge map and frame side-by-side so we can more easily visualize the output.

The `output` frame is then displayed to our screen.

### **Detecting low contrast frames in real-time**

We are now ready to detect low contrast images in real-time video streams!

Use the **_“Downloads”_** section of this tutorial to download the source code, example images, and sample video file.

From there, open up a terminal, and execute the following command:

$ python detect\_low\_contrast\_video.py --input example\_video.mp4
\[INFO\] accessing video stream...
\[INFO\] no frame read from stream - exiting

As our output shows, our low contrast frame detector is able to detect frames with low contrast and _prevent_ them from proceeding down the rest of our image processing pipeline.

Conversely, images with _sufficient contrast_ are allowed to proceed. We then apply edge detection to each of these frames, compute contours, and extract the contour/outline associated with the color correction card.

You can use low contrast detection in video streams in the same manner.

### What's next? We recommend [PyImageSearch University](https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend).

**Course information:**  
84 total classes • 114+ hours of on-demand code walkthrough videos • Last updated: February 2024  
★★★★★ 4.84 (128 Ratings) • 16,000+ Students Enrolled

**I strongly believe that if you had the right teacher you could _master_ computer vision and deep learning.**

Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?

That’s _not_ the case.

All you need to master computer vision and deep learning is for someone to explain things to you in _simple, intuitive_ terms. _And that’s exactly what I do_. My mission is to change education and how complex Artificial Intelligence topics are taught.

If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to _successfully_ and _confidently_ apply computer vision to your work, research, and projects. Join me in computer vision mastery.

**Inside PyImageSearch University you'll find:**

*   ✓ **84 courses** on essential computer vision, deep learning, and OpenCV topics
*   ✓ **84 Certificates** of Completion
*   ✓ **114+ hours** of on-demand video
*   ✓ **Brand new courses released _regularly_**, ensuring you can keep up with state-of-the-art techniques
*   ✓ **Pre-configured Jupyter Notebooks in Google Colab**
*   ✓ Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)
*   ✓ Access to **centralized code repos for _all_ 536+ tutorials** on PyImageSearch
*   ✓ **Easy one-click downloads** for code, datasets, pre-trained models, etc.
*   ✓ **Access** on mobile, laptop, desktop, etc.

[Click here to join PyImageSearch University](https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend)

**Summary**
-----------

In this tutorial you learned how to detect low contrast images in both static scenes and real-time video streams. We used both the OpenCV library and the scikit-image package to develop our low contrast image detector.

While simple, this method can be _extremely effective_ when used in computer vision and image processing pipelines.

One of the easiest ways to use this method is to provide feedback to your user. If a user provides your application with a low contrast image, alert them and request that they provide a higher-quality image.

Taking this approach allows you to place “guarantees” on the environment used to capture images that are ultimately presented to your pipeline. Furthermore, it helps the user understand that your application can only be used in certain scenarios and it’s on _them_ to ensure they conform to your standards.

**The gist here is to not overcomplicate your image processing pipelines.** It’s _far easier_ to write OpenCV code when you can place guarantees on the lighting conditions and environment — try to enforce these standards any way you can.

**To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), _simply enter your email address in the form below!_**

![Image 8](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20410%20567'%3E%3C/svg%3E)

#### Download the Source Code and FREE 17-page Resource Guide

Enter your email address below to get a .zip of the code and a **FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.** Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!