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.
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
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
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