Core function: change the contrast and brightness of the image OpenCV v4.8.0

Previous tutorial: Adding (blending) two images using OpenCV

Next tutorial: Discrete Fourier Transform

Original author Ana Huamán
Compatibility OpenCV >= 3.0

Goals

In this tutorial you will learn how to

  • Access pixel values
  • Initialize matrix with zeros
  • Learn what cv::saturate_cast does and its usefulness
  • Get some cool information about pixel transforms
  • Improve image brightness with practical examples

Theory

Comments
The following explanation belongs to the book “Computer Vision”: Algorithms and Applications.

Image processing

  • A general image processing operator is a function that receives one or more input images and produces an output image.
  • Image transformation can be divided into
    • Point operator (pixel transformation)
    • Neighborhood (region-based) operator

Pixel Transform

  • In this image processing transformation, the value of each output pixel depends only on the corresponding input pixel value (possibly plus some globally collected information or parameters).
  • Examples of such operators include brightness and contrast adjustments and color corrections and transformations.

Brightness and Contrast Adjustments

  • Two common point manipulations are multiplication and addition by constants:

g

(

x

)

=

α

f

(

x

)

+

β

g\left( x \right) =\alpha f\left( x \right) + \beta

g(x)=αf(x) + β

  • The parameters α>0 and β are often called gain and bias parameters; sometimes these parameters also control contrast and brightness, respectively.
  • You can think of f(x) as the source image pixel and g(x) as the output image pixel. Then, we can more conveniently write the expression as

g

(

i

,

j

)

=

α

?

f

(

i

,

j

)

+

β

g\left( i,j \right) =\alpha ?f\left( i,j \right) + \beta

g(i,j)=α?f(i,j) + β

where i and j represent the pixel located in the i-th row and j-th column.

Code

  • C++
  • Downloadable code: Click here
  • The following code implements the above formula
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
// We don't use "using namespace std;" here to avoid conflicting beta variables with std::beta in C++17
using std::cin;
using std::cout;
using std::endl;
using namespace cv;
int main(int argc, char** argv)
{<!-- -->
 CommandLineParser parser( argc, argv, "{@input | lena.jpg | input image}" );
 Mat image = imread( samples::findFile( parser.get<String>( "@input" ) ) );
 if(image.empty())
 {<!-- -->
 cout << "Could not open or find the image!\\
" << endl;
 cout << "Usage: " << argv[0] << " <Input image>" << endl;
 return -1;
 }
 Mat new_image = Mat::zeros( image.size(), image.type() );
 double alpha = 1.0; /*< Simple contrast control */
 int beta = 0; /*< Simple brightness control */
 cout << "Basic Linear Transforms " << endl;
 cout << "-------------------------" << endl;
 cout << "* Enter the alpha value [1.0-3.0]: "; cin >> alpha;
 cout << "* Enter the beta value [0-100]: "; cin >> beta;
 for( int y = 0; y < image.rows; y + + ) {<!-- -->
 for( int x = 0; x < image.cols; x + + ) {<!-- -->
 for( int c = 0; c < image.channels(); c + + ) {<!-- -->
 new_image.at<Vec3b>(y,x)[c] =
 saturate_cast<uchar>( alpha*image.at<Vec3b>(y,x)[c] + beta );
 }
 }
 }
 imshow("Original Image", image);
 imshow("New Image", new_image);
 waitKey();
 return 0;
}
  • Java
  • Downloadable code: Click here
  • The following code implements the above formula
import java.util.Scanner;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
class BasicLinearTransforms {<!-- -->
 private byte saturate(double val) {<!-- -->
 int iVal = (int) Math.round(val);
 iVal = iVal > 255 ? 255 : (iVal < 0 ? 0 : iVal);
 return (byte) iVal;
 }
 public void run(String[] args) {<!-- -->
 String imagePath = args.length > 0 ? args[0] : "../data/lena.jpg";
 Mat image = Imgcodecs.imread(imagePath);
 if (image.empty()) {<!-- -->
 System.out.println("Empty image: " + imagePath);
 System.exit(0);
 }
 Mat newImage = Mat.zeros(image.size(), image.type());
 double alpha = 1.0; /*< Simple contrast control */
 int beta = 0; /*< Simple brightness control */
 System.out.println(" Basic Linear Transforms ");
 System.out.println("-------------------------");
 try (Scanner scanner = new Scanner(System.in)) {<!-- -->
 System.out.print("* Enter the alpha value [1.0-3.0]: ");
 alpha = scanner.nextDouble();
 System.out.print("* Enter the beta value [0-100]: ");
 beta = scanner.nextInt();
 }
 byte[] imageData = new byte[(int) (image.total()*image.channels())];
 image.get(0, 0, imageData);
 byte[] newImageData = new byte[(int) (newImage.total()*newImage.channels())];
 for (int y = 0; y < image.rows(); y + + ) {<!-- -->
 for (int x = 0; x < image.cols(); x + + ) {<!-- -->
 for (int c = 0; c < image.channels(); c + + ) {<!-- -->
 double pixelValue = imageData[(y * image.cols() + x) * image.channels() + c];
 pixelValue = pixelValue < 0 ? pixelValue + 256 : pixelValue;
 newImageData[(y * image.cols() + x) * image.channels() + c]
 = saturate(alpha * pixelValue + beta);
 }
 }
 }
 newImage.put(0, 0, newImageData);
 HighGui.imshow("Original Image", image);
 HighGui.imshow("New Image", newImage);
 HighGui.waitKey();
 System.exit(0);
 }
}
public class BasicLinearTransformsDemo {<!-- -->
 public static void main(String[] args) {<!-- -->
 //Load the local OpenCV library
 System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
 new BasicLinearTransforms().run(args);
 }
}
  • Python
  • Downloadable code: Click here
  • The following code implements the above formula
from __future__ import print_function
from builtins import input
import cv2 as cv
import numpy as np
import argparse
# Read the image provided by the user
parser = argparse.ArgumentParser(description='Code for Changing the contrast and brightness of an image! tutorial.')
parser.add_argument('--input', help='Path to input image.', default='lena.jpg')
args = parser.parse_args()
image = cv.imread(cv.samples.findFile(args.input))
if image is None:
 print('Could not open or find the image: ', args.input)
 exit(0)
new_image = np.zeros(image.shape, image.dtype)
alpha = 1.0 # Simple contrast control
beta = 0 # Simple brightness control
#Initialize value
print(' Basic Linear Transforms ')
print('-------------------------')
try:
 alpha = float(input('* Enter the alpha value [1.0-3.0]: '))
 beta = int(input('* Enter the beta value [0-100]: '))
except ValueError:
 print('Error, not a number')
#Perform the operation new_image(i,j) = alpha*image(i,j) + beta
# We can simply use the following method to replace these "for " loops:
# new_image = cv.convertScaleAbs(image, alpha=alpha, beta=beta)
# But we want to show you how to access pixels :)
for y in range(image.shape[0]):
 for x in range(image.shape[1]):
 for c in range(image.shape[2]):
 new_image[y,x,c] = np.clip(alpha*image[y,x,c] + beta, 0, 255)
cv.imshow('Original Image', image)
cv.imshow('New Image', new_image)
# Wait for the user to press a key
cv.waitKey()

Explanation

  • We load the image using cv::imread and save it in a Mat object:
  • C++
 CommandLineParser parser( argc, argv, "{@input | lena.jpg | input image}" );
 Mat image = imread( samples::findFile( parser.get<String>( "@input" ) ) );
 if(image.empty())
 {<!-- -->
 cout << "Could not open or find the image!\\
" << endl;
 cout << "Usage: " << argv[0] << " <Input image>" << endl;
 return -1;
 }
  • Java
 String imagePath = args.length > 0 ? args[0] : "../data/lena.jpg";
 Mat image = Imgcodecs.imread(imagePath);
 if (image.empty()) {<!-- -->
 System.out.println("Empty image: " + imagePath);
 System.exit(0);
 }
  • Python
parser = argparse.ArgumentParser(description='Code for Changing the contrast and brightness of an image! tutorial.')
parser.add_argument('--input', help='Path to input image.', default='lena.jpg')
args = parser.parse_args()
image = cv.imread(cv.samples.findFile(args.input))
if image is None:
 print('Could not open or find the image: ', args.input)
 exit(0)
  • Now, since we are going to do some transformations on this image, we need a new Mat object to store it. Additionally, we want it to have the following characteristics:

    • The initial pixel value is equal to zero
    • Same size and type as original image
  • C++

 Mat new_image = Mat::zeros( image.size(), image.type() );
  • Java
 Mat newImage = Mat.zeros(image.size(), image.type());
 }
  • Python
new_image = np.zeros(image.shape, image.dtype)

We note that cv::Mat::zeros returns a Matlab-style zero initializer based on image.size() and image.type() .

  • Now we ask the user to enter values for α and β:
  • C++
 double alpha = 1.0; /*< Simple contrast control */
 int beta = 0; /*< Simple brightness control */
 cout << "Basic Linear Transforms " << endl;
 cout << "-------------------------" << endl;
 cout << "* Enter the alpha value [1.0-3.0]: "; cin >> alpha;
 cout << "* Enter the beta value [0-100]: "; cin >> beta;
  • Java
 double alpha = 1.0; /*< Simple contrast control */
 int beta = 0; /*< Simple brightness control */
 System.out.println(" Basic Linear Transforms ");
 System.out.println("-------------------------");
 try (Scanner scanner = new Scanner(System.in)) {<!-- -->
 System.out.print("* Enter the alpha value [1.0-3.0]: ");
 alpha = scanner.nextDouble();
 System.out.print("* Enter the beta value [0-100]: ");
 beta = scanner.nextInt();
 }
 }
  • Python
alpha = 1.0 # Simple contrast control
beta = 0 # Simple brightness control
#Initialize values
print(' Basic Linear Transforms ')
print('-------------------------')
try:
 alpha = float(input('* Enter the alpha value [1.0-3.0]: '))
 beta = int(input('* Enter the beta value [0-100]: '))
except ValueError:
 print('Error, not a number')
  • Now, in order to execute

    g

    (

    i

    ,

    j

    )

    =

    α

    ?

    f

    (

    i

    ,

    j

    )

    +

    β

    g\left( i,j \right) =\alpha ?f\left( i,j \right) + \beta

    g(i,j)=α?f(i,j) + β
    operation, we will access every pixel in the image. Since we are using a BGR image, each pixel will have three values (B, G, and R), so we will access them separately as well. code show as below

  • C++

 for( int y = 0; y < image.rows; y + + ) {<!-- -->
 for( int x = 0; x < image.cols; x + + ) {<!-- -->
 for( int c = 0; c < image.channels(); c + + ) {<!-- -->
 new_image.at<Vec3b>(y,x)[c] =
 saturate_cast<uchar>( alpha*image.at<Vec3b>(y,x)[c] + beta );
 }
 }
 }
  • Java
 byte[] imageData = new byte[(int) (image.total()*image.channels())];
 image.get(0, 0, imageData);
 byte[] newImageData = new byte[(int) (newImage.total()*newImage.channels())];
 for (int y = 0; y < image.rows(); y + + ) {<!-- -->
 for (int x = 0; x < image.cols(); x + + ) {<!-- -->
 for (int c = 0; c < image.channels(); c + + ) {<!-- -->
 double pixelValue = imageData[(y * image.cols() + x) * image.channels() + c];
 pixelValue = pixelValue < 0 ? pixelValue + 256 : pixelValue;
 newImageData[(y * image.cols() + x) * image.channels() + c]
 = saturate(alpha * pixelValue + beta);
 }
 }
 }
 newImage.put(0, 0, newImageData);
  • Python
for y in range(image.shape[0]):
 for x in range(image.shape[1]):
 for c in range(image.shape[2]):
 new_image[y,x,c] = np.clip(alpha*image[y,x,c] + beta, 0, 255)

Please note the following (C++ code only):

  • To access each pixel in the image, we use the following syntax: image.at(y,x)[c], where y represents the row, x represents the column, and c represents B, G, or R (0, 1, or 2).

  • Since the operation α?p(i,j) + β may cause the value to be out of range or not an integer (if α is a floating point number), we use cv::saturate_cast to ensure that the value is valid.

  • Finally, we create the window and display the image as usual.

  • C++

 imshow("Original Image", image);
 imshow("New Image", new_image);
 waitKey();
  • Java
 HighGui.imshow("Original Image", image);
 HighGui.imshow("New Image", newImage);
 HighGui.waitKey();
  • Python
# Display content
cv.imshow('Original Image', image)
cv.imshow('New Image', new_image)
# Wait for the user to press a key
cv.waitKey()

Comments
Rather than using a for loop to access each pixel, we can use this command directly:

  • C++
image.convertTo(new_image, -1, alpha, beta);
  • Java
image.convertTo(newImage, -1, alpha, beta);
  • Python
new_image = cv.convertScaleAbs(image, alpha=alpha, beta=beta)

where cv::Mat::convertTo will effectively execute new_image = aimage + beta*. However, we want to show you how to access each pixel. Regardless, both methods give the same result, but convertTo is more optimized and runs faster.

Results

  • Run our code and use α=2.2 and β=50
$ ./BasicLinearTransforms lena.jpg
Basic Linear Transforms
-----------------------
* Enter the alpha value [1.0-3.0]: 2.2
* Enter the beta value [0-100]: 50

We get this:

Practical examples

In this section, we’ll apply what we’ve learned to correct an underexposed image by adjusting its brightness and contrast. We’ll also look at another technique for correcting the brightness of an image, gamma correction.

Brightness and Contrast Adjustments

Increasing (/decreasing) the beta value increases (/decreases) each pixel by a constant value. Pixel values outside the range [0 ; 255] will be saturated (i.e. pixel values above (/below) 255(/0) will be clamped to 255(/0)).


Light gray is the histogram of the original image, dark gray is the histogram when brightness = 80 in Gimp

The histogram represents each color level and the number of pixels with that color level. Dark images will have many pixels with low color values, so there will be a peak on the left side of the histogram. When adding a constant bias, the histogram shifts to the right because we add a constant bias to all pixels.

The parameter α changes how the color scale spreads. If α<1, the color levels will be compressed and the contrast of the image will be reduced.


Light gray is the histogram of the original image, dark gray when contrast < 0 in Gimp.
Please note that these histograms were drawn using the “Brightness-Contrast” tool in Gimp software. The brightness tool should be the same as the beta bias parameter, but the contrast tool seems to be different from the alpha gain, whose output range seems to be centered around Gimp (as shown in the previous histogram).

Using beta bias increases brightness, but at the same time makes the image slightly blurry due to reduced contrast. Using alpha gain can reduce this effect, but we will lose some detail in the originally bright areas due to saturation.

Gamma Correction

Gamma correction corrects the brightness of an image by performing a non-linear transformation between input values and mapped output values:

O

=

(

I

255

)

γ

×

255

O=\left( \frac{I}{255} \right) ^{\gamma}×255

O=(255I?)γ×255

Since this relationship is non-linear, the effect will not be the same for all pixels and will depend on their original values.


Graphs of different gamma values
When γ<1, the original dark area will be brighter and the histogram will move to the right. When γ>1, the situation is just the opposite.

Correct underexposed images

The following images have correction values of: α=1.3 and β=40.


Contributed by Visem (work) [CC BY-SA 3.0] via Wikimedia Commons

The overall brightness has improved, but you can notice that the clouds are now very saturated due to the implementation of numerical saturation (highlight clipping in photography).

The image below has been used with γ=0.4.


Contributed by Visem (work) [CC BY-SA 3.0] via Wikimedia Commons

Gamma correction should reduce saturation effects since the mapping is non-linear and numerical saturation is not possible like the previous method.


Left picture: Histogram after alpha and beta correction; Middle picture: Histogram of the original image; Right picture: Histogram after gamma correction
The above figure compares the histograms of three images (the three histograms have different y ranges). It can be seen that most of the pixel values of the original image are located in the lower part of the histogram. After alpha, beta correction, we can see that there is a larger peak at 255 due to saturation, moving to the right. After gamma correction, the histogram shifts to the right, but dark pixels move more than bright pixels (see gamma curve plot).

In this tutorial, you’ll see two simple ways to adjust the contrast and brightness of your images. They are basic technologies and cannot replace a raster graphics editor!

Code

Tutorial code is here.

Gamma correction code

  • C++
 Mat lookUpTable(1, 256, CV_8U);
 uchar* p = lookUpTable.ptr();
 for(int i = 0; i < 256; + + i)
 p[i] = saturate_cast<uchar>(pow(i / 255.0, gamma_) * 255.0);
 Mat res = img.clone();
 LUT(img, lookUpTable, res);
  • Java
 Mat lookUpTable = new Mat(1, 256, CvType.CV_8U);
 byte[] lookUpTableData = new byte[(int) (lookUpTable.total()*lookUpTable.channels())];
 for (int i = 0; i < lookUpTable.cols(); i + + ) {<!-- -->
 lookUpTableData[i] = saturate(Math.pow(i / 255.0, gammaValue) * 255.0);
 }
 lookUpTable.put(0, 0, lookUpTableData);
 Mat img = new Mat();
 Core.LUT(matImgSrc, lookUpTable, img);
  • Python
 lookUpTable = np.empty((1,256), np.uint8)
 for i in range(256):
 lookUpTable[0,i] = np.clip(pow(i / 255.0, gamma) * 255.0, 0, 255)
 res = cv.LUT(img_original, lookUpTable)

Additional Resources

  • Gamma correction in graphics rendering
  • Gamma correction and images displayed on CRT monitors
  • digital exposure technology