The histogram alignment operation (increasing contrast) is often used to increase image quality. Within the framework of this article, the concept of a histogram , the theory of an algorithm for its alignment, and practical examples of the operation of this algorithm will be considered . To facilitate image processing, we will use the OpenCV library . Also, we will make a comparative analysis of the results of our algorithm and the algorithm that is already built into OpenCV .

The histogram is a function h (x) , which returns the total number of pixels whose brightness is equal to x .
Histogram h of halftone image I is given by
, where m corresponds to the intervals of brightness values
Below is the pseudo-code for calculating the histogram.
procedure histogram(I,H);
{
for i := 0 to MaxVal
H[i] := 0;
for L := 0 to MaxRow
for P := 0 to MaxCol {
grayval := I[r,c];
H[grayval] := H[grayval] + 1;
};
}
Visually, the histogram is a rectangle, the width of which is equal to the maximum possible value of the brightness of a point in the original image. For grayscale images, we will work with a range of brightness of points from 0 to 255, which means that the width of the histogram will be 256. The height of the histogram can be any, but for clarity, we will work with rectangular histograms.
, — 256 ( ), . .
1. h(x)

. — cdf(x) .
, OpenCV , , . , , . F# OpenCVSharp4.
, Visual Studio, . , MacOS, , "runtimes/osx-x64/native/libOpenCvSharpExtern.dylib" , .
Unable to load shared library 'OpenCvSharpExtern' or one of its dependencies
,
open OpenCvSharp
open Microsoft.FSharp.NativeInterop
// mat - Mat OpenCV
let getHistogram (mat: Mat) =
//
let hx = Array.zeroCreate<int> 256
//
mat.ForEachAsByte(fun value _ ->
let v = int (NativePtr.get value 0)
hx.[v] <- hx.[v] + 1)
//
hx
, . X
, X. 1 ( ).
, cdf(x)
let getCdx hx = // hx -
//
hx |> Array.mapi (fun x _ -> if x > 0 then hx.[0..(x-1)] |> Array.sum else 0)
1 , . ( ), . , , .
2.
2.

, .
:
cdf(x) — X
cdf_min — ,
pixels —
255 — . 255
round —
F# :
// cdf_min
let cdxMin = cdx |> Array.filter (fun v -> v > 0) |> Array.min
//
let totalPixels = src.Rows * src.Cols // src - Mat
for y in 0..src.Rows do
for x in 0..src.Cols do
//
let s = int(src.At<byte>(y, x))
//
let fx = (float(cdx.[s]) - float(cdxMin))/(float(totalPixels - 1))*255.
//
equalizeImage.Circle(x, y, 1, new Scalar(double(fx)))
, . . , OpenCV .
// Learn more about F# at http://fsharp.org
open OpenCvSharp
open Microsoft.FSharp.NativeInterop
//
let getHistogram (mat: Mat) =
let hx = Array.zeroCreate<int> 256
mat.ForEachAsByte(fun value _ ->
let v = int (NativePtr.get value 0)
hx.[v] <- hx.[v] + 1)
hx
//
let getCdx hx =
hx |> Array.mapi (fun x _ -> if x > 0 then hx.[0..(x-1)] |> Array.sum else 0)
//
let drawHistogramAndCdx hx cdx (mat: Mat) =
let histoWidth = 256
let histoHeight = 256
//
let cdxMax = cdx |> Array.max
// ( )
//
let cdxK = float(histoHeight)/float(cdxMax)
let histMax = hx |> Array.max
let histK = float(histoHeight)/float(histMax)
let histMat = new Mat(histoWidth, histoHeight, MatType.CV_8UC4)
hx
|> Array.iteri (fun x v ->
let histDy = int(float(v)*histK)
let cdxDy = int(float(cdx.[x])*cdxK)
// h(x)
mat.Line(x, histoHeight-1, x, histoHeight-1-histDy, Scalar.White)
// cdx(x)
mat.Circle(x, histoHeight-cdxDy, 1, Scalar.Blue))
[<EntryPoint>]
let main argv =
let histoWidth = 256
let histoHeight = 256
let src = Cv2.ImRead("cat.jpg", ImreadModes.Grayscale)
let equalizeImage = new Mat(src.Rows, src.Cols, MatType.CV_8UC1)
// calculate histogram h(x)
let hx = getHistogram src
// calculate cdf(x) = h(0) + h(1) + .. + h(x)
let cdx = getCdx hx
// draw histogram
let histMat = new Mat(histoWidth, histoHeight, MatType.CV_8UC4)
drawHistogramAndCdx hx cdx histMat
// equalize the histogram
let cdxMin = cdx |> Array.filter (fun v -> v > 0) |> Array.min
let totalPixels = src.Rows * src.Cols
for y in 0..src.Rows do
for x in 0..src.Cols do
let s = int(src.At<byte>(y, x))
let fx = (float(cdx.[s]) - float(cdxMin))/(float(totalPixels - 1))*255.
//equalizeImage.Set<Scalar>(y, x, new Scalar(double(fx)))
equalizeImage.Circle(x, y, 1, new Scalar(double(fx)))
// calculate equalize histogram
let hx2 = getHistogram equalizeImage
let cdx2 = getCdx hx2
let histMat2 = new Mat(histoWidth, histoHeight, MatType.CV_8UC4)
drawHistogramAndCdx hx2 cdx2 histMat2
// opencv equalize histogram
let opencCVImage = new Mat(src.Rows, src.Cols, MatType.CV_8UC1)
let in1 = InputArray.Create(src)
let in2 = OutputArray.Create(opencCVImage)
Cv2.EqualizeHist(in1, in2)
// get opencv histogram
let hx3 = getHistogram opencCVImage
let cdx3 = getCdx hx3
let histMat3 = new Mat(histoWidth, histoHeight, MatType.CV_8UC4)
drawHistogramAndCdx hx3 cdx2 histMat3
// show results
use w1 = new Window("original image", src)
use w2 = new Window("original histogram", histMat)
use w3 = new Window("custom equalize image", equalizeImage)
use w4 = new Window("custom equalize histogram", histMat2)
use w5 = new Window("opencv equalize image", opencCVImage)
use w6 = new Window("opencv equalize histogram", histMat3)
Cv2.WaitKey() |> ignore
0 // return an integer exit code
3.
, , OpenCV.
( ), — , — OpenCV.




The author of the article is far from original and did not come up with anything new, presenting to the public another story about how the histogram alignment algorithm is implemented and why it is needed. The main purpose of the article was not only to talk about the algorithm, but also to give specific practical examples and source codes that you can copy and verify yourself, and possibly experiment. Of course, the histogram algorithm is not the only one used to filter images. But this is the topic of other issues.