Visual Servoing Platform  version 3.6.1 under development (2024-11-21)
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/vpConfig.h>
#include <visp3/core/vpImage.h>
#include <visp3/core/vpIoTools.h>
#include <visp3/gui/vpDisplayFactory.h>
#include <visp3/io/vpImageIo.h>
#if defined(VISP_HAVE_MODULE_IMGPROC)
#include <visp3/imgproc/vpImgproc.h>
#endif
#if ((__cplusplus >= 201103L) || (defined(_MSVC_LANG) && (_MSVC_LANG >= 201103L)))
#include <memory>
#endif
#ifdef ENABLE_VISP_NAMESPACE
using namespace VISP_NAMESPACE_NAME;
#endif
#if defined(VISP_HAVE_MODULE_IMGPROC) && defined(VISP_HAVE_DISPLAY) && \
(defined(VISP_HAVE_PNG) || defined(VISP_HAVE_OPENCV) || defined(VISP_HAVE_STBIMAGE) || defined(VISP_HAVE_SIMDLIB)) && \
((__cplusplus >= 201103L) || (defined(_MSVC_LANG) && (_MSVC_LANG >= 201103L)))
namespace
{
void display(vpImage<vpRGBa> &I_display, vpImage<vpRGBa> &I_color_res, const vpImage<vpRGBa> &I_color_adjust,
vpImage<unsigned char> &I_gray_res, vpImage<unsigned char> &I_gray_adjust, vpImage<vpRGBa> &I_gray_display,
const std::string &title, const std::string &filename_color, const std::string &filename_gray,
const std::string &title_2 = "")
{
I_color_res.insert(I_color_adjust, vpImagePoint(0, I_color_adjust.getWidth()));
I_display.insert(I_color_adjust, vpImagePoint(0, I_color_adjust.getWidth()));
I_gray_res.insert(I_gray_adjust, vpImagePoint(0, I_gray_adjust.getWidth()));
vpImageConvert::convert(I_gray_adjust, I_gray_display);
I_display.insert(I_gray_display, vpImagePoint(I_color_adjust.getHeight(), I_color_adjust.getWidth()));
vpImageIo::write(I_color_res, filename_color);
vpImageIo::write(I_gray_res, filename_gray);
vpDisplay::display(I_display);
vpDisplay::displayText(I_display, 20, 20, title, vpColor::red);
if (!title_2.empty()) {
vpDisplay::displayText(I_display, 40, static_cast<unsigned int>(I_color_adjust.getWidth()*0.85),
title_2, vpColor::green);
}
vpDisplay::flush(I_display);
vpDisplay::getClick(I_display);
}
}
#endif
int main(int argc, const char **argv)
{
#if defined(VISP_HAVE_MODULE_IMGPROC) && defined(VISP_HAVE_DISPLAY) && \
(defined(VISP_HAVE_PNG) || defined(VISP_HAVE_OPENCV) || defined(VISP_HAVE_STBIMAGE) || defined(VISP_HAVE_SIMDLIB)) && \
((__cplusplus >= 201103L) || (defined(_MSVC_LANG) && (_MSVC_LANG >= 201103L)))
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;
int scale_display = 2;
for (int i = 1; i < argc; i++) {
if (std::string(argv[i]) == "--input" && i + 1 < argc) {
++i;
input_filename = std::string(argv[i]);
}
else if (std::string(argv[i]) == "--alpha" && i + 1 < argc) {
++i;
alpha = atof(argv[i]);
}
else if (std::string(argv[i]) == "--beta" && i + 1 < argc) {
++i;
beta = atof(argv[i]);
}
else if (std::string(argv[i]) == "--gamma" && i + 1 < argc) {
++i;
gamma = atof(argv[i]);
}
else if ((std::string(argv[i]) == "--gamma-color-handling") && ((i + 1) < argc)) {
++i;
}
else if ((std::string(argv[i]) == "--gamma-method") && ((i + 1) < argc)) {
++i;
}
else if (std::string(argv[i]) == "--scale" && i + 1 < argc) {
++i;
scale = atoi(argv[i]);
}
else if (std::string(argv[i]) == "--scaleDiv" && i + 1 < argc) {
++i;
scaleDiv = atoi(argv[i]);
}
else if (std::string(argv[i]) == "--level" && i + 1 < argc) {
++i;
level = atoi(argv[i]);
}
else if (std::string(argv[i]) == "--kernelSize" && i + 1 < argc) {
++i;
kernelSize = atoi(argv[i]);
}
else if (std::string(argv[i]) == "--dynamic" && i + 1 < argc) {
++i;
dynamic = atof(argv[i]);
}
else if (std::string(argv[i]) == "--scale-display" && i + 1 < argc) {
++i;
scale_display = atoi(argv[i]);
}
else if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") {
std::cout << "Usage: " << argv[0]
<< " [--input <input image>]"
" [--alpha <alpha for adjust()>] [--beta <beta for adjust()>]"
" [--gamma <gamma for gammaCorrection()>]"
" [--gamma-color-handling " << VISP_NAMESPACE_NAME::vpGammaColorHandlingList() << "]"
" [--gamma-method " << VISP_NAMESPACE_NAME::vpGammaMethodList() << "]"
" [--scale <scale for retinex()> [--scaleDiv for retinex()]"
" [--level <level for retinex()> [--kernelSize <kernelSize for retinex()>]"
" [--dynamic <dynamic for retinex()>] "
" [--scale-display <display downscaling factor>] "
" [--help]"
<< std::endl;
return EXIT_SUCCESS;
}
}
// Filename without extension to save the results
const std::string input_name = vpIoTools::getNameWE(input_filename);
vpImage<vpRGBa> I_color;
vpImageIo::read(I_color, input_filename);
vpImageConvert::convert(I_color, I_gray);
vpImage<vpRGBa> I_gray_display;
vpImageConvert::convert(I_gray, I_gray_display);
// Side-by-side images
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());
// Side-by-side display for color (top) and gray (bottom) images
vpImage<vpRGBa> I_display(2 * I_color.getHeight(), 2 * I_color.getWidth());
I_display.insert(I_color, vpImagePoint());
I_display.insert(I_gray_display, vpImagePoint(I_color.getHeight(), 0));
std::shared_ptr<vpDisplay> d = vpDisplayFactory::createDisplay();
d->setDownScalingFactor(static_cast<vpDisplay::vpScaleType>(scale_display));
d->init(I_display, 10, 10, "Brightness adjustment results");
vpImage<vpRGBa> I_color_adjust;
VISP_NAMESPACE_NAME::adjust(I_color, I_color_adjust, alpha, beta);
vpImage<unsigned char> I_gray_adjust;
VISP_NAMESPACE_NAME::adjust(I_gray, I_gray_adjust, alpha, beta);
std::stringstream ss_color;
ss_color << input_name << "_adjust_alpha=" << alpha << "_beta=" << beta << ".png";
std::stringstream ss_gray;
ss_gray << input_name << "_adjust_alpha=" << alpha << "_beta=" << beta << "_gray.png";
display(I_display, I_color_res, I_color_adjust, I_gray_res, I_gray_adjust, I_gray_display,
"Brightness and contrast adjustment. Click to continue.", ss_color.str(), ss_gray.str());
// 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.
}
vpImage<vpRGBa> I_color_gamma_correction;
VISP_NAMESPACE_NAME::gammaCorrection(I_color, I_color_gamma_correction, static_cast<float>(gamma), colorHandling, method);
vpImage<unsigned char> I_gray_gamma_correction;
VISP_NAMESPACE_NAME::gammaCorrection(I_gray, I_gray_gamma_correction, static_cast<float>(gamma), method);
ss_color.str("");
ss_color << input_name << "_gamma=" << gamma << ".png";
ss_gray.str("");
ss_gray << input_name << "_gamma=" << gamma << "_gray.png";
display(I_display, I_color_res, I_color_gamma_correction, I_gray_res, I_gray_gamma_correction, I_gray_display,
"Gamma correction. Click to continue.", ss_color.str(), ss_gray.str());
// Display results for the different Gamma correction method
for (int gamma_idx = 0; gamma_idx < VISP_NAMESPACE_NAME::GAMMA_METHOD_COUNT; ++gamma_idx) {
gamma = -1.;
if (gamma_method == VISP_NAMESPACE_NAME::GAMMA_MANUAL) {
continue;
}
vpImage<vpRGBa> I_color_gamma_correction;
VISP_NAMESPACE_NAME::gammaCorrection(I_color, I_color_gamma_correction, static_cast<float>(gamma), colorHandling,
gamma_method);
vpImage<unsigned char> I_gray_gamma_correction;
VISP_NAMESPACE_NAME::gammaCorrection(I_gray, I_gray_gamma_correction, static_cast<float>(gamma), gamma_method);
const std::string gamma_name = VISP_NAMESPACE_NAME::vpGammaMethodToString(gamma_method);
ss_color.str("");
ss_color << input_name << "_" << gamma_name << ".png";
ss_gray.str("");
ss_gray << input_name << "_" << gamma_name << "_gray.png";
display(I_display, I_color_res, I_color_gamma_correction, I_gray_res, I_gray_gamma_correction, I_gray_display,
"Gamma correction. Click to continue.", ss_color.str(), ss_gray.str(), gamma_name);
}
vpImage<vpRGBa> I_color_equalize_histogram;
VISP_NAMESPACE_NAME::equalizeHistogram(I_color, I_color_equalize_histogram);
vpImage<unsigned char> I_gray_equalize_histogram;
VISP_NAMESPACE_NAME::equalizeHistogram(I_gray, I_gray_equalize_histogram);
ss_color.str("");
ss_color << input_name << "_eqHist.png";
ss_gray.str("");
ss_gray << input_name << "_eqHist_gray.png";
display(I_display, I_color_res, I_color_equalize_histogram, I_gray_res, I_gray_equalize_histogram, I_gray_display,
"Histogram equalization. Click to continue.", ss_color.str(), ss_gray.str());
vpImage<vpRGBa> I_color_retinex;
VISP_NAMESPACE_NAME::retinex(I_color, I_color_retinex, scale, scaleDiv, level, dynamic, kernelSize);
// Retinex uses color image as input
// Convert gray image into RGBa format for quick test
vpImage<vpRGBa> I_gray_color;
vpImageConvert::convert(I_gray, I_gray_color);
vpImage<vpRGBa> I_gray_color_retinex;
VISP_NAMESPACE_NAME::retinex(I_gray_color, I_gray_color_retinex, scale, scaleDiv, level, dynamic, kernelSize);
// Convert back to gray
vpImage<unsigned char> I_gray_retinex;
vpImageConvert::convert(I_gray_color_retinex, I_gray_retinex);
ss_color.str("");
ss_color << input_name << "_Retinex_scale=" << scale << "_scaleDiv=" << scaleDiv << "_level=" << level
<< "_dynamic=" << dynamic << "_kernelSize=" << kernelSize << ".png";
ss_gray.str("");
ss_gray << input_name << "_Retinex_scale=" << scale << "_scaleDiv=" << scaleDiv << "_level=" << level
<< "_dynamic=" << dynamic << "_kernelSize=" << kernelSize << "_gray.png";
display(I_display, I_color_res, I_color_retinex, I_gray_res, I_gray_retinex, I_gray_display,
"Retinex. Click to quit.", ss_color.str(), ss_gray.str());
#else
(void)argc;
(void)argv;
#endif
return EXIT_SUCCESS;
}
static const vpColor red
Definition: vpColor.h:217
static const vpColor green
Definition: vpColor.h:220
void init(vpImage< unsigned char > &I, int winx=-1, int winy=-1, const std::string &title="") VP_OVERRIDE
void setDownScalingFactor(unsigned int scale)
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:147
static void write(const vpImage< unsigned char > &I, const std::string &filename, int backend=IO_DEFAULT_BACKEND)
Definition: vpImageIo.cpp:291
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:242
void insert(const vpImage< Type > &src, const vpImagePoint &topLeft)
Definition: vpImage.h:637
unsigned int getHeight() const
Definition: vpImage.h:181
static std::string getNameWE(const std::string &pathname)
Definition: vpIoTools.cpp:1227
VISP_EXPORT void adjust(VISP_NAMESPACE_ADDRESSING vpImage< unsigned char > &I, double alpha, double beta)
VISP_EXPORT void gammaCorrection(VISP_NAMESPACE_ADDRESSING vpImage< unsigned char > &I, const float &gamma, const vpGammaMethod &method=GAMMA_MANUAL, const VISP_NAMESPACE_ADDRESSING vpImage< bool > *p_mask=nullptr)
VISP_EXPORT void equalizeHistogram(VISP_NAMESPACE_ADDRESSING vpImage< unsigned char > &I, const VISP_NAMESPACE_ADDRESSING vpImage< bool > *p_mask=nullptr)
VISP_EXPORT void retinex(VISP_NAMESPACE_ADDRESSING vpImage< VISP_NAMESPACE_ADDRESSING vpRGBa > &I, int scale=240, int scaleDiv=3, int level=RETINEX_UNIFORM, double dynamic=1.2, int kernelSize=-1)
VISP_EXPORT vpGammaMethod vpGammaMethodFromString(const std::string &name)
Cast a string into a vpGammaMethod.
Definition: vpImgproc.cpp:116
VISP_EXPORT vpGammaColorHandling vpGammaColorHandlingFromString(const std::string &name)
Cast a string into a vpGammaColorHandling.
Definition: vpImgproc.cpp:164
vpGammaColorHandling
How to handle color images when applying Gamma Correction.
Definition: vpImgproc.h:149
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:73
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:133
vpGammaMethod
Gamma Correction automatic methods.
Definition: vpImgproc.h:100
VISP_EXPORT std::string vpGammaMethodToString(const vpGammaMethod &type)
Cast a vpGammaMethod into a string, to know its name.
Definition: vpImgproc.cpp:87
std::shared_ptr< vpDisplay > createDisplay()
Return a smart pointer vpDisplay specialization if a GUI library is available or nullptr otherwise.

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;
VISP_NAMESPACE_NAME::adjust(I_color, I_color_adjust, alpha, beta);
vpImage<unsigned char> I_gray_adjust;
VISP_NAMESPACE_NAME::adjust(I_gray, I_gray_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.