Visual Servoing Platform  version 3.6.1 under development (2024-04-19)
Tutorial: Brightness and contrast adjustment

Introduction

While the ViSP library is not intended to be an image processing library or replace a raster graphics editor, some easy image processing techniques can be used to improve or adjust the brightness and the contrast of an image.

The different techniques used in this tutorial are:

  • brightness and contrast adjustment using a linear function
  • gamma correction
  • histogram equalization
  • Retinex algorithm

The following example also available in tutorial-brightness-adjustment.cpp will demonstrate on a real underexposed photo the result of each of these methods:

#include <cstdlib>
#include <iostream>
#include <visp3/core/vpImage.h>
#include <visp3/gui/vpDisplayGDI.h>
#include <visp3/gui/vpDisplayOpenCV.h>
#include <visp3/gui/vpDisplayX.h>
#include <visp3/io/vpImageIo.h>
#if defined(VISP_HAVE_MODULE_IMGPROC)
#include <visp3/imgproc/vpImgproc.h>
#endif
int main(int argc, const char **argv)
{
#if defined(VISP_HAVE_MODULE_IMGPROC) && \
(defined(VISP_HAVE_X11) || defined(VISP_HAVE_GDI) || defined(VISP_HAVE_OPENCV)) && \
(defined(VISP_HAVE_PNG) || defined(VISP_HAVE_OPENCV))
std::string input_filename = "Sample_low_brightness.png";
double alpha = 10.0, beta = 50.0;
double gamma = 3.5;
int scale = 240, scaleDiv = 3, level = 0, kernelSize = -1;
double dynamic = 3.0;
for (int i = 1; i < argc; i++) {
if (std::string(argv[i]) == "--input" && i + 1 < argc) {
input_filename = std::string(argv[i + 1]);
}
else if (std::string(argv[i]) == "--alpha" && i + 1 < argc) {
alpha = atof(argv[i + 1]);
}
else if (std::string(argv[i]) == "--beta" && i + 1 < argc) {
beta = atof(argv[i + 1]);
}
else if (std::string(argv[i]) == "--gamma" && i + 1 < argc) {
gamma = atof(argv[i + 1]);
}
else if ((std::string(argv[i]) == "--gamma-color-handling") && ((i + 1) < argc)) {
++i;
colorHandling = vp::vpGammaColorHandlingFromString(argv[i]);
}
else if ((std::string(argv[i]) == "--gamma-method") && ((i + 1) < argc)) {
++i;
method = vp::vpGammaMethodFromString(argv[i]);
}
else if (std::string(argv[i]) == "--scale" && i + 1 < argc) {
scale = atoi(argv[i + 1]);
}
else if (std::string(argv[i]) == "--scaleDiv" && i + 1 < argc) {
scaleDiv = atoi(argv[i + 1]);
}
else if (std::string(argv[i]) == "--level" && i + 1 < argc) {
level = atoi(argv[i + 1]);
}
else if (std::string(argv[i]) == "--kernelSize" && i + 1 < argc) {
kernelSize = atoi(argv[i + 1]);
}
else if (std::string(argv[i]) == "--dynamic" && i + 1 < argc) {
dynamic = atof(argv[i + 1]);
}
else if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") {
std::cout << "Usage: " << argv[0]
<< " [--input <input image>]"
" [--alpha <alpha for vp::adjust()>] [--beta <beta for "
"vp::adjust()>]"
" [--gamma <gamma for vp::gammaCorrection()>]"
" [--gamma-color-handling " << vp::vpGammaColorHandlingList() << "]"
" [--gamma-method " << vp::vpGammaMethodList() << "]"
" [--scale <scale for vp::retinex()> [--scaleDiv for "
"vp::retinex()]"
" [--level <level for vp::retinex()> [--kernelSize "
"<kernelSize for vp::retinex()>]"
" [--dynamic <dynamic for vp::retinex()>] [--help]"
<< std::endl;
return EXIT_SUCCESS;
}
}
vpImage<vpRGBa> I_color;
vpImageIo::read(I_color, input_filename);
vpImageConvert::convert(I_color, I_gray);
vpImage<vpRGBa> I_color_res(I_color.getHeight(), 2 * I_color.getWidth());
I_color_res.insert(I_color, vpImagePoint());
vpImage<unsigned char> I_gray_res(I_gray.getHeight(), 2 * I_gray.getWidth());
I_gray_res.insert(I_gray, vpImagePoint());
#ifdef VISP_HAVE_X11
vpDisplayX d_gray(I_gray_res);
vpDisplayX d(I_color_res);
#elif defined(VISP_HAVE_GDI)
vpDisplayGDI d_gray(I_gray_res);
vpDisplayGDI d(I_color_res);
#elif defined(HAVE_OPENCV_HIGHGUI)
vpDisplayOpenCV d_gray(I_gray_res);
vpDisplayOpenCV d(I_color_res);
#endif
vpImage<vpRGBa> I_color_adjust;
vp::adjust(I_color, I_color_adjust, alpha, beta);
I_color_res.insert(I_color_adjust, vpImagePoint(0, I_color.getWidth()));
std::stringstream ss;
ss << "Sample_low_brightness_alpha=" << alpha << "_beta=" << beta << ".png";
vpImageIo::write(I_color_res, ss.str());
vpDisplay::display(I_color_res);
vpDisplay::displayText(I_color_res, 20, 20, "Brightness and contrast adjustment. Click to continue.", vpColor::red);
vpDisplay::flush(I_color_res);
vpDisplay::getClick(I_color_res);
if (method != vp::GAMMA_MANUAL) {
// If the user wants to use an automatic method, the gamma factor must be negative.
gamma = -1.;
}
if (gamma > 0.) {
// If the user wants to set a constant user-defined gamma factor, the method must be set to manual.
method = vp::GAMMA_MANUAL;
}
vpImage<unsigned char> I_gray_gamma_correction;
vp::gammaCorrection(I_gray, I_gray_gamma_correction, static_cast<float>(gamma), method);
vpImage<vpRGBa> I_color_gamma_correction;
vp::gammaCorrection(I_color, I_color_gamma_correction, static_cast<float>(gamma), colorHandling, method);
I_gray_res.insert(I_gray_gamma_correction, vpImagePoint(0, I_gray.getWidth()));
ss.str("");
ss << "Sample_low_brightness_gray.png";
vpImageIo::write(I_gray_res, ss.str());
vpDisplay::display(I_gray_res);
vpDisplay::displayText(I_gray_res, 20, 20, "Gamma correction on gray image. Click to continue.", vpColor::red);
vpDisplay::flush(I_gray_res);
vpDisplay::getClick(I_gray_res);
I_color_res.insert(I_color_gamma_correction, vpImagePoint(0, I_color.getWidth()));
ss.str("");
ss << "Sample_low_brightness_gamma=" << gamma << ".png";
vpImageIo::write(I_color_res, ss.str());
vpDisplay::display(I_color_res);
vpDisplay::displayText(I_color_res, 20, 20, "Gamma correction. Click to continue.", vpColor::red);
vpDisplay::flush(I_color_res);
vpDisplay::getClick(I_color_res);
vpImage<vpRGBa> I_color_equalize_histogram;
vp::equalizeHistogram(I_color, I_color_equalize_histogram);
I_color_res.insert(I_color_equalize_histogram, vpImagePoint(0, I_color.getWidth()));
ss.str("");
ss << "Sample_low_brightness_eqHist.png";
vpImageIo::write(I_color_res, ss.str());
vpDisplay::display(I_color_res);
vpDisplay::displayText(I_color_res, 20, 20, "Histogram equalization. Click to continue.", vpColor::red);
vpDisplay::flush(I_color_res);
vpDisplay::getClick(I_color_res);
vpImage<vpRGBa> I_color_retinex;
vp::retinex(I_color, I_color_retinex, scale, scaleDiv, level, dynamic, kernelSize);
I_color_res.insert(I_color_retinex, vpImagePoint(0, I_color.getWidth()));
ss.str("");
ss << "Sample_low_brightness_scale=" << scale << "_scaleDiv=" << scaleDiv << "_level=" << level
<< "_dynamic=" << dynamic << "_kernelSize=" << kernelSize << ".png";
vpImageIo::write(I_color_res, ss.str());
vpDisplay::display(I_color_res);
vpDisplay::displayText(I_color_res, 20, 20, "Retinex. Click to quit.", vpColor::red);
vpDisplay::flush(I_color_res);
vpDisplay::getClick(I_color_res);
#else
(void)argc;
(void)argv;
#endif
return EXIT_SUCCESS;
}
static const vpColor red
Definition: vpColor.h:211
Display for windows using GDI (available on any windows 32 platform).
Definition: vpDisplayGDI.h:128
The vpDisplayOpenCV allows to display image using the OpenCV library. Thus to enable this class OpenC...
Use the X11 console to display images on unix-like OS. Thus to enable this class X11 should be instal...
Definition: vpDisplayX.h:128
static bool getClick(const vpImage< unsigned char > &I, bool blocking=true)
static void display(const vpImage< unsigned char > &I)
static void flush(const vpImage< unsigned char > &I)
static void displayText(const vpImage< unsigned char > &I, const vpImagePoint &ip, const std::string &s, const vpColor &color)
static void convert(const vpImage< unsigned char > &src, vpImage< vpRGBa > &dest)
static void read(vpImage< unsigned char > &I, const std::string &filename, int backend=IO_DEFAULT_BACKEND)
Definition: vpImageIo.cpp:143
static void write(const vpImage< unsigned char > &I, const std::string &filename, int backend=IO_DEFAULT_BACKEND)
Definition: vpImageIo.cpp:287
Class that defines a 2D point in an image. This class is useful for image processing and stores only ...
Definition: vpImagePoint.h:82
unsigned int getWidth() const
Definition: vpImage.h:245
unsigned int getHeight() const
Definition: vpImage.h:184
VISP_EXPORT void adjust(vpImage< unsigned char > &I, double alpha, double beta)
Definition: vpImgproc.cpp:177
VISP_EXPORT void gammaCorrection(vpImage< unsigned char > &I, const float &gamma, const vpGammaMethod &method=vp::GAMMA_MANUAL, const vpImage< bool > *p_mask=nullptr)
Definition: vpImgproc.cpp:601
VISP_EXPORT void equalizeHistogram(vpImage< unsigned char > &I, const vpImage< bool > *p_mask=nullptr)
Definition: vpImgproc.cpp:220
VISP_EXPORT void retinex(vpImage< vpRGBa > &I, int scale=240, int scaleDiv=3, int level=RETINEX_UNIFORM, double dynamic=1.2, int kernelSize=-1)
Definition: vpRetinex.cpp:244
vpGammaMethod
Gamma Correction automatic methods.
Definition: vpImgproc.h:99
@ GAMMA_MANUAL
Definition: vpImgproc.h:100
VISP_EXPORT vpGammaColorHandling vpGammaColorHandlingFromString(const std::string &name)
Cast a string into a vp::vpGammaColorHandling.
Definition: vpImgproc.cpp:160
vpGammaColorHandling
How to handle color images when applying Gamma Correction.
Definition: vpImgproc.h:148
@ GAMMA_HSV
Definition: vpImgproc.h:150
VISP_EXPORT std::string vpGammaColorHandlingList(const std::string &pref="<", const std::string &sep=" , ", const std::string &suf=">")
Get the list of available vpGammaColorHandling.
Definition: vpImgproc.cpp:129
VISP_EXPORT std::string vpGammaMethodList(const std::string &pref="<", const std::string &sep=" , ", const std::string &suf=">")
Get the list of available vpGammaMethod.
Definition: vpImgproc.cpp:69
VISP_EXPORT vpGammaMethod vpGammaMethodFromString(const std::string &name)
Cast a string into a vp::vpGammaMethod.
Definition: vpImgproc.cpp:112

These functions are provided in a vp:: namespace and accessible using this include:

#include <visp3/imgproc/vpImgproc.h>

Brightness and contrast adjustment

The brightness and the contrast of an image can be adjusted using a linear function:

\[I_{res}\left ( i,j \right ) = \alpha \cdot I_{src}\left ( i,j \right ) + \beta\]

The $\alpha$ value will behave as a gain factor and the $\beta$ value as an offset.

The code to use is straightforward:

vpImage<vpRGBa> I_color_adjust;
vp::adjust(I_color, I_color_adjust, alpha, beta);

The result image is the following:

Left: underexposed image - Right: image adjusted with alpha=10, beta=50

Gamma correction

Gamma correction is a simple technique allowing to correct an image using a non-linear operation. The formula used is:

\[I_{res}\left ( i,j \right ) = \left ( \frac{I_{src}\left ( i,j \right )}{255} \right )^{\frac{1}{\gamma}} \cdot 255\]

The image below shows in x the input pixel values and in y the output pixel values as they would be transformed by a gamma correction function according to different gamma values.

Visualization of the gamma correction function

The result image is the following:

Left: underexposed image - Right: image corrected with gamma=3.5

ViSP proposes the implementation of several automatic computation of the gamma factor. Most of these methods are designed for gray-shade images, so ViSP proposes different way of handling the colors.

You can test the different methods using the --gamma-method option of the tutorial program and the different way of handling the colors using the --gamma-color-handling option.

Histogram equalization

Histogram equalization is an image processing method that will adjust the contrast of an image by stretching or shrinking the intensity distribution in order to have a linear cumulative histogram distribution.

In the next figure, you can observe the histogram for the original underexposed photo where most of the pixel intensities are located in the [0, 30] range. The cumulative histogram distribution has a strong slope for very low pixel intensities.

Histogram and normalized cumulative histogram of the underexposed photo

The histogram for the equalized photo is displayed in the next figure. This time, the bins are spread more uniformally along the intensity range and the cumulative histogram distribution presents a more linear shape.

Histogram and normalized cumulative histogram of the equalized photo

The result of the histogram equalized image is displayed below:

Left: underexposed image - Right: histogram equalized image

Retinex

The Retinex algorithm implemented is ported from the Retinex ImageJ plugin:

Retinex filtering is based on Land's theory of image perception, proposed to explain the perceived colour constancy of objects under varying illumination conditions. Several approaches exist to implement the retinex principles, among these the multiscale retinex with colour restoration algorithm (MSRCR) combines colour constancy with local contrast enhancement so images are rendered similarly to how human vision is believed to operate.

The original photo after the Retinex processing:

Left: underexposed image - Right: result of the Retinex algorithm with default parameters and dynamic=3

Next tutorial

You can now read the Tutorial: Contrast and image sharpening techniques, for additional contrast and sharpness improvement techniques.