直方图修正包括直方图均衡化和直方图规定化。首先我们来了解一下什么是直方图。
1. 直方图的概念
对数一幅图像由不同灰度值的像素组成,图像中灰度的分布情况是该图像的一个重要特征。图像的灰度直方图描述了图像中灰度的分布情况,能够直观地展示出图像中各个灰度级所占的比例。
图像的灰度直方图是灰度级的函数,描述的是图像中具有该灰度级的像素的个数。简单地说,就是把一幅图像中每一个像素出现的次数先统计出来,然后把每一个像素出现的次数除以总的像素个数,得到的就是这个像素出现的频率,再把像素与该像素出现的频率用图表示出来,得到的就是灰度直方图。
图5-14所示就是一个灰度图像的灰度直方图,横坐标代表像素的灰度值的范围0,255,灰度值越接近0,表示图像越黑;越接近255,表示图像越亮。纵坐标代表每一个灰度值在图像所有像素中出现的次数。
对数除此之外,直方图的纵轴还可以代表图像灰度概率密度,这种直方图被称为归一化直方图,它可以直接反映不同灰度级出现的比率(或叫概率)。此时会将纵坐标归一化到0,1区间内,也就是将灰度级出现的次数(频率或像素个数)除以图像中像素的总数。对于拥有256种灰度(灰度级是256)的图像,我们假设n为灰度值等于r的像素个数(即灰度r出现的次数),则该灰度出现的概率(灰度概率密度函数)可以用如下公式表示:


其中,n表示图像像素的总个数,它可以用图像的宽度和高度相乘来获得;n为灰度值为r的像素个数(灰度r出现的次数),比如n是所有像素中灰度r出现的次数;L是图像的灰度级数;r表示第k级灰度值;p®是各个r出现的概率,反映了图像的灰度值的分布情况。
2. OpenCV实现灰度直方图
直方图的计算很简单,无非是遍历图像的像素,统计每个灰度级的个数。在OpenCV中封装了直方图的计算函数calcHist,该函数能够同时计算多个图像、多个通道、不同灰度范围的灰度直方图。该函数声明如下:

【例5.9】得到某图像的灰度直方图
c
#include <iostream>
#include<opencv2/opencv.hpp>
#include <opencv2\imgproc\types_c.h> //for CV_RGB2GRAY
using namespace cv; //所有opencv类都在命名空间cv下
using namespace std;
//绘制灰度直方图
int main(int argc, char* argv[])
{
Mat src, gray;
if (argc == 1)
{
src = imread("gza.jpg"); //如果用户没有传图片名进来,就默认读取工程目录下的gza.jpg图片
}
else if (argc == 2)
{
src = imread(argv[1]);
}
if (src.empty()) //判断原图是否加载成功
{
cout << "图像加载失败" << endl;
return -1;
}
cvtColor(src, gray, CV_RGB2GRAY); //转换为灰度图
int bins = 256;
int hist_size[] = { bins };
float range[] = { 0, 256 };
const float* ranges[] = { range };
MatND hist;
int channels[] = { 0 };
//计算出灰度直方图
calcHist(&gray, 1, channels, Mat(), // do not use mask
hist, 1, hist_size, ranges,
true, // the histogram is uniform
false);
//画出直方图
double max_val;
minMaxLoc(hist, 0, &max_val, 0, 0); //定位矩阵中最小值、最大值的位置
int scale = 2;
int hist_height = 256;
Mat hist_img = Mat::zeros(hist_height, bins * scale, CV_8UC3); //创建一个全0的特殊矩阵
for (int i = 0; i < bins; i++)
{
float bin_val = hist.at<float>(i);
int intensity = cvRound(bin_val * hist_height / max_val);//要绘制的高度
rectangle(hist_img, Point(i * scale, hist_height - 1), //画矩形
Point((i + 1) * scale - 1, hist_height - intensity),
CV_RGB(255, 255, 255));
}
//显示原图和直方图
imshow("src", src);
imshow("Grayscale histogram", hist_img);//灰度直方图
waitKey(10000000000);
return 0;
}

值得关注的是"画出直方图"这一块的代码。这个直方图首先将每一个区间长度定为2个像素,直方图画布的长是:区间数×区间长度= 256×2 = 512,高为256,初始化为一幅黑色画布。然后进入画直方图阶段,简单来说,就是取出图像的灰度频率表(每个灰度值占整个图像的像素数)的每一个值,计算出绘制高度,根据这个绘制高度画线或者矩形。绘制高度是:矩形高度×画布高度/最大矩形高度。
3. 直方图均衡化
直方图均衡化是一种常见的增强图像对比度的方法,在数据较为相似的图像中作用更加明显。直方图均衡化处理的中心思想是把原始图像的灰度直方图从比较集中的某个灰度区间变成在全部灰度范围内的均匀分布。具体操作就是对图像进行非线性拉伸,重新分配图像像素值,使一定灰度范围内的像素数量大致相同,即将给定图像的直方图分布变
成"均匀"分布的直方图分布。
为什么要进行直方图均衡化呢?很多时候,我们的图片看起来不是那么清晰,这时可以对图像进行一些处理,以扩大图像像素值显示的范围。例如,有些图像整体像素值偏低,图像中的一些特征不是很清晰,只是隐约看到一些轮廓痕迹,这时可以通过图像直方图均衡化使得图像看起来亮一些,便于后续的处理。直方图均衡化是灰度变换的一个重要应用,它高效且易于实现,广泛应用于图像增强处理中。
直方图均衡化方法把原图像的直方图通过灰度变换函数修正为灰度均匀分布的直方图,然后按均衡直方图修正原图像。当图像的直方图均匀分布时,图像包含的信息量最大,图像看起来就很清晰。该方法以累计分布函数为基础,其变换函数取决于图像灰度直方图的累积分布函数。它对整幅图像进行同一个变换,因此也称为全局直方图均衡化。
在均衡化过程中,必须保证满足两个条件:
- ·像素无论怎么映射,一定要保证原来的大小关系不变,较亮的区域依旧较亮,较暗 的区域依旧较暗,只是对比度增大,绝对不能明暗颠倒。
- ·如果是8位图像,那么像素映射函数的值域应在0和255之间,不能越界。
综合以上两个条件,累积分布函数是一个好的选择,因为累积分布函数是单调增函数(控制大小关系),并且值域是0-1(控制越界问题)。下面通过一个例子来说明直方图均衡算法。比如灰度为0~7,对应的像素数如图所示。

进行均衡化后,每个灰度所分配的像素数应该是5,即总像素数除以所有灰度之和,也就是40÷8=5。从原始图像的灰度值大的像素开始,每次取5个像素。从灰度为5的像素中选取一个像素有两种算法:随机法和从周围像素的平均灰度的较大像素中顺次选取。
后者比前者稍微复杂一些,但是后者所得结果的噪声比前者少。这里使用后者。接下来,在原始图像灰度为5的剩余8个像素中,用前面的方法选取5个像素,作为灰度6的像素数,以此类推,对所有像素重新进行灰度分配。
直方图均衡化的具体步骤如下:
步骤01: 计算每个灰度值的像素个数n,即每个灰度在所有像素中出现了几次。
步骤02: 计算原图像的灰度累积分布函数s,并求出灰度变换表。

步骤03 根据灰度变换表将原图像各灰度级映射为新的灰度级,即可完成直方图均衡化。
大多数自然图像由于其灰度分布集中在较窄的区间,导致图像细节不够清晰。采用直方图均衡化后,可使图像的灰度间距拉开或使灰度均匀分布,从而增大反差,使图像细节清晰,达到增强图像的目的。直方图均衡化方法有以下两个特点:
(1)根据各灰度级别出现频率的大小,对各个灰度级别进行相应程度的增强,即各
个级别之间的间距相应增大。
(2)可能减少原有图像灰度级别的个数,即对于出现频率过小的灰度级别可能出现简并现象。当满足公式时,第i+1个灰度才会映射到与第i个灰度不同的灰度级别上,即当第i+1个灰度出现频数小于n/256时,它可能与第i个灰度映射到同一个灰度级别上,这就是简并现象。

直方图均衡化的简并现象不仅使出现频数过大的灰度级别过度增强,还使所关注的目标细节信息丢失,未能达到预期增强的目的。目前,已有很多直方图均衡化的改进算法在一定程度上改善了这一缺点,例如基于幂函数的加权自适应直方图均衡化、平台直方图均衡化等。这些都可以作为读者以后从事图像工作的研究方向。
下面我们实现直方图均衡化来改善图像质量。通常有两种实现方法:一种不通过OpenCV函数,另一种通过OpenCV函数。
【例5.10】不用函数实现直方图均衡化
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int jh(char imgfn[])
{
Mat temp = imread(imgfn, 0);
if (temp.empty())
{
printf("open %s failed.", imgfn);
return -1;
} temp = imread("img.png", 0);
imshow("src", temp);
//equalizeHist(temp, temp);
int Total = temp.total();
//统计各像素数量
vector<int>nk(256, 0);
//执行第一个步骤
/*这个for循环统计每种灰度对应的像素数量,比如灰度是0的像素数量,灰度是255的像素数量,把每个nk计算出来 */
for (int i = 0; i < temp.total(); ++i)
nk[temp.data[i]]++; //temp.data[i]表示第i个像素的灰度值,范围是0~255
//++表示每个灰度值
//执行第二个步骤
for (int i = 1; i < 256; ++i) //这个for执行完毕后,nk[i]相当于公式中的累积分布函数sk,k和i是一个意思
nk[i] += nk[i - 1];
//第三步,重新建立映射关系
vector<int>a(256, 0);//均衡后像素值
for (int i = 0; i < 256; ++i) {
a[i] = (double(nk[i]) / Total) * 255;
}
for (int i = 0; i < temp.total(); ++i)
temp.data[i] = a[temp.data[i]];
imshow("After equilibrium", temp);
waitKey(-1); //按键后再继续
}
int main()
{
jh((char*)"img.png");
//jh((char*)"img2.png");
}

下面我们使用OpenCV提供的均衡化函数equalizeHist完成图像均衡化。
【例5.11】使用equalizeHist实现直方图均衡化

cpp
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char** argv)
{
Mat src = imread("img.png");
//Mat src2 = imread("img2.png");
if (src.empty() )
{
printf("不能加载图像!\n");
return -1;
}
Mat gray, dst;
cvtColor(src, gray, COLOR_BGR2GRAY);
//cvtColor(src2, gray2, COLOR_BGR2GRAY);
imshow("1--src", gray);
equalizeHist(gray, dst);
imshow("2-- Histogram equalization", dst);//显示直方图均衡化的结果图
waitKey(0);
return 0;
}