An image histogram counts how many pixels fall into each intensity range. Histograms are useful for diagnosing exposure, comparing image distributions, selecting thresholds, and improving contrast.
This example calculates 256 bins over the complete intensity range of an 8-bit grayscale image:
using OpenCvSharp;
using var source = Cv2.ImRead("input.jpg", ImreadModes.Grayscale);
if (source.Empty())
{
throw new FileNotFoundException("Could not decode input.jpg.");
}
using var histogram = new Mat();
Cv2.CalcHist(
images: [source],
channels: [0],
mask: default,
hist: histogram,
dims: 1,
histSize: [256],
ranges: [new Rangef(0, 256)]);
float darkPixelCount = histogram.At<float>(32);
Console.WriteLine($"Pixels with intensity 32: {darkPixelCount}");The result is a CV_32FC1 matrix with one row per bin. The range end is exclusive, so [0, 256) covers every possible value in an 8-bit image.
Normalize the bin counts to the plot height before drawing them:
const int plotWidth = 512;
const int plotHeight = 300;
using var normalized = new Mat();
Cv2.Normalize(
histogram,
normalized,
alpha: 0,
beta: plotHeight - 1,
normType: NormTypes.MinMax);
using var plot = new Mat(
rows: plotHeight,
cols: plotWidth,
type: MatType.CV_8UC3,
s: Scalar.White);
int binWidth = plotWidth / 256;
for (int bin = 1; bin < 256; bin++)
{
int previousHeight = (int)Math.Round(normalized.At<float>(bin - 1));
int currentHeight = (int)Math.Round(normalized.At<float>(bin));
Cv2.Line(
plot,
new Point((bin - 1) * binWidth, plotHeight - 1 - previousHeight),
new Point(bin * binWidth, plotHeight - 1 - currentHeight),
Scalar.Black,
thickness: 2);
}
Cv2.ImWrite("histogram.png", plot);Normalization here is only for display. Preserve the original histogram when its counts or proportions will be used in later calculations.
CalcHist accepts an 8-bit single-channel mask. Nonzero mask pixels select the source pixels to include:
using var mask = new Mat(source.Size(), MatType.CV_8UC1, Scalar.Black);
Cv2.Rectangle(
mask,
new Rect(50, 40, 200, 150),
Scalar.White,
thickness: -1);
using var regionHistogram = new Mat();
Cv2.CalcHist(
images: [source],
channels: [0],
mask: mask,
hist: regionHistogram,
dims: 1,
histSize: [256],
ranges: [new Rangef(0, 256)]);The mask must have the same width and height as the source image.
Global histogram equalization redistributes intensities across the complete image:
using var equalized = new Mat();
Cv2.EqualizeHist(source, equalized);It works on an 8-bit single-channel source. It can over-amplify noise or produce unnatural results when illumination varies across the image.
Contrast Limited Adaptive Histogram Equalization (CLAHE) processes local tiles and limits amplification:
using var clahe = Cv2.CreateCLAHE(
clipLimit: 2.0,
tileGridSize: new Size(8, 8));
using var improved = new Mat();
clahe.Apply(source, improved);
Cv2.ImWrite("improved.png", improved);Treat clipLimit and tileGridSize as parameters to tune against representative images. Smaller tiles adapt to more local variation but can emphasize noise and tile boundaries.
For a BGR image, channel index 0 is blue, 1 is green, and 2 is red. Separate BGR channel histograms describe the storage channels, but they are often poor measures of perceived color similarity. Convert to a color space such as HSV or Lab first when hue, saturation, or perceptual brightness is the quantity of interest.
- Match the histogram range to the source depth and the intended values;
[0, 256)is specific to an 8-bit intensity channel. - Do not compare raw bin counts from images or masks containing different numbers of pixels. Normalize the histograms first.
- Histogram similarity does not preserve spatial layout. Two visually different images can have the same histogram.
- Use a mask for a region of interest when the surrounding background would dominate the distribution.
- Cv2.CalcHist
- Cv2.EqualizeHist
- CLAHE