Visual Servoing Platform  version 3.6.1 under development (2024-07-23)
Tutorial: Contrast and image sharpening techniques


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 the contrast and the sharpness of an image.

The different methods presented are:

The first two methods consist of stretching the histogram of an image to make it use the entire range of values. It is more or less similar to the histogram equalization technique (presented in Histogram equalization). The stretching will act like a direct mapping between the old and new intensity values whereas the histogram equalization will linearize the cumulative histogram distribution to try to make each intensity values the same weighting in the image. The CLAHE algorithm will limit the contrast enhancement using a maximum slope value to avoid to amplify too much the image noise. It will also compute the cumulative histogram locally by sliding a window around the current pixel location.

Example code

The following example also available in tutorial-contrast-sharpening.cpp will show the result of each of these methods on a low contrast image.

#include <cstdlib>
#include <iostream>
#include <visp3/core/vpConfig.h>
#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>
#include <visp3/imgproc/vpImgproc.h>
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))
using namespace VISP_NAMESPACE_NAME;
std::string input_filename = "Crayfish-low-contrast.png";
int blockRadius = 150;
int bins = 256;
float slope = 3.0f;
float sigma = 2.0f;
double weight = 0.5;
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]) == "--blockRadius" && i + 1 < argc) {
blockRadius = atoi(argv[i + 1]);
else if (std::string(argv[i]) == "--bins" && i + 1 < argc) {
bins = atoi(argv[i + 1]);
else if (std::string(argv[i]) == "--slope" && i + 1 < argc) {
slope = (float)atof(argv[i + 1]);
else if (std::string(argv[i]) == "--sigma" && i + 1 < argc) {
sigma = (float)atof(argv[i + 1]);
else if (std::string(argv[i]) == "--weight" && i + 1 < argc) {
weight = atof(argv[i + 1]);
else if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") {
std::cout << "Usage: " << argv[0]
<< " [--input <input image>]"
" [--blockRadius <block radius for CLAHE>] "
" [--bins <nb histogram bins for CLAHE>] [--slope <slope for CLAHE>]"
" [--sigma <Gaussian kernel standard deviation>] [--weight <unsharp mask weighting>]"
" [--help] [-h]"
<< std::endl;
vpImage<vpRGBa> I_color;
vpImageIo::read(I_color, input_filename);
#ifdef VISP_HAVE_X11
vpDisplayX d, d2, d3, d4, d5, d6;
#elif defined(VISP_HAVE_GDI)
vpDisplayGDI d, d2, d3, d4, d5, d6;
#elif defined(HAVE_OPENCV_HIGHGUI)
vpDisplayOpenCV d, d2, d3, d4, d5, d6;
d.init(I_color, 0, 0, "Input color image");
vpImage<vpRGBa> I_stretch;
d2.init(I_stretch, I_color.getWidth(), 10, "Stretch contrast");
vpImage<vpRGBa> I_stretch_hsv;
VISP_NAMESPACE_NAME::stretchContrastHSV(I_color, I_stretch_hsv);
d3.init(I_stretch_hsv, 0, I_color.getHeight() + 80, "Stretch contrast HSV");
vpImage<vpRGBa> I_hist_eq;
d4.init(I_hist_eq, I_color.getWidth(), I_color.getHeight() + 80, "Histogram equalization");
vpImage<vpRGBa> I_clahe;
VISP_NAMESPACE_NAME::clahe(I_color, I_clahe, blockRadius, bins, slope);
d5.init(I_clahe, 0, 2 * I_color.getHeight() + 80, "CLAHE");
vpImage<vpRGBa> I_unsharp;
VISP_NAMESPACE_NAME::unsharpMask(I_clahe, I_unsharp, sigma, weight);
d6.init(I_unsharp, I_color.getWidth(), 2 * I_color.getHeight() + 80, "Unsharp mask");
vpDisplay::displayText(I_unsharp, 20, 20, "Click to quit.", vpColor::red);
static const vpColor red
Definition: vpColor.h:217
Display for windows using GDI (available on any windows 32 platform).
Definition: vpDisplayGDI.h:130
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:135
void init(vpImage< unsigned char > &I, int win_x=-1, int win_y=-1, const std::string &win_title="") VP_OVERRIDE
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 read(vpImage< unsigned char > &I, const std::string &filename, int backend=IO_DEFAULT_BACKEND)
Definition: vpImageIo.cpp:147
unsigned int getWidth() const
Definition: vpImage.h:242
unsigned int getHeight() const
Definition: vpImage.h:181
VISP_EXPORT void clahe(const VISP_NAMESPACE_ADDRESSING vpImage< unsigned char > &I1, VISP_NAMESPACE_ADDRESSING vpImage< unsigned char > &I2, int blockRadius=150, int bins=256, float slope=3.0f, bool fast=true)
VISP_EXPORT void stretchContrast(VISP_NAMESPACE_ADDRESSING vpImage< unsigned char > &I)
VISP_EXPORT void equalizeHistogram(VISP_NAMESPACE_ADDRESSING vpImage< unsigned char > &I, const VISP_NAMESPACE_ADDRESSING vpImage< bool > *p_mask=nullptr)
VISP_EXPORT void unsharpMask(VISP_NAMESPACE_ADDRESSING vpImage< unsigned char > &I, float sigma, double weight=0.6)

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

#include <visp3/imgproc/vpImgproc.h>

The first step is to read the input image:

vpImage<vpRGBa> I_color;
vpImageIo::read(I_color, input_filename);

The low contrast color image used in this tutorial can be downloaded here (By Biem (Own work) [Public domain], via Wikimedia Commons):

Input low contrast color image

The figure below represents the histogram and the cumulative histogram of the low contrast image. Most of the histogram bins are approximately in the [80 - 140] range, resulting in an image with low dynamic.

Histogram and cumulative histogram of the input image

The histogram stretching can be done with:

vpImage<vpRGBa> I_stretch;

The result is:

Histogram stretching

The corresponding histogram and cumulative histogram are the following:

Histogram and cumulative histogram of the stretched histogram image

This method stretches the histogram with a direct mapping between the old and the new intensity values. The histogram bins are more spread out and the image gains some dynamic. It will not change the intensity distribution as the histogram equalization method could do.

The histogram stretching on HSV colorspace can be done with:

vpImage<vpRGBa> I_stretch_hsv;
VISP_NAMESPACE_NAME::stretchContrastHSV(I_color, I_stretch_hsv);
Histogram stretching on HSV colorspace

The main difference is that this method will stretch the Saturation and Value components and preserve the Hue channel.

From the Gimp documentation:

it works in HSV color space, rather than RGB color space, and it preserves the Hue. Thus, it independently stretches the ranges of the Hue, Saturation and Value components of the colors. Occasionally the results are good, often they are a bit odd.

Histogram and cumulative histogram of the stretched histogram image in HSV colorspace

The histogram and cumulative histogram are similar to the previous method as expected.

To improve the image contrast using the histogram equalization method:

The result is:

Histogram equalization

If we look at the histogram and the cumulative histogram:

Histogram and cumulative histogram of the histogram equalized image

The cumulative histogram is more linear which can be related to a more equal distribution of the pixel intensities in the image.

To use the CLAHE algorithm:

vpImage<vpRGBa> I_clahe;
VISP_NAMESPACE_NAME::clahe(I_color, I_clahe, blockRadius, bins, slope);

The CLAHE method avoid the over amplification of the noise compared to the histogram equalization method:

Contrast limited adaptive histogram equalization (blockRadius=150, bins=256, slope=3)

The parameters are:

  • the block radius: the size (2*blockRadius+1) of the neighborhood to consider around the current pixel location
  • the number of bins for the histogram computation
  • the maximum slope to limit the contrast enhancement

The histogram of the corrected image is stretched and the local processing plus the limitation of the contrast enhancement avoid the over boosting of the contrast as in the histogram equalization case.

Histogram and cumulative histogram after using the CLAHE method

The unsharp masking will sharpen the edges in an image:

vpImage<vpRGBa> I_unsharp;
VISP_NAMESPACE_NAME::unsharpMask(I_clahe, I_unsharp, sigma, weight);

It is applied here on the image after using the CLAHE algorithm:

Unsharp masking (weight=0.5, Gaussian blur size=11) on the processed image after CLAHE

Two parameters can be modified:

  • the size of the Gaussian kernel, see vpImageFilter::gaussianBlur(const vpImage<double> &, vpImage<double> &, unsigned int, double, bool)
  • the unsharp masking weighting: $ I_{sharpen} = \frac{\left( I_{original} - weight \times I_{blurred} \right)}{\left( 1 - weight \right)} $

To summarize, the techniques presented to improve the contrast of an image can do a good job in some situations and not in another. The CLAHE algorithm and the unsharp masking can be tuned (but the default values should be good enough in most of the situation).

Next tutorial

You can now read the Tutorial: Automatic thresholding, to learn how to automatically threshold / binarise a grayscale image.