直方图(Histogram)最开始在统计学中被提出,由一系列高度不等的纵向条纹或线段表示数据分布的情况。 一般用横轴表示数据类型,纵轴表示分布情况。在图像领域,直方图用来更直观地展现各像素值出现的频率,例如常用的灰度直方图反映的是一幅图像中各灰度像素值(0-255)出现的频率之间的关系
图像的直方图表示图像像素灰度值的统计特性,因此可以通过比较两张图像的直方图特性比较两张图像的相似程度。从一定程度上来讲,虽然两张图像的直方图分布相似不代表两张图像相似,但是两张图像相似则两张图像的直方图分布一定相似。例如通过插值对图像进行放缩后图像的直方图虽然不会与之前完全一致,但是两者一定具有很高的相似性,因而可以通过比较两张图像的直方图分布相似性对图像进行初步的筛选与识别。
目前OpenCV中只提供了图像直方图的统计函数calcHist(),该函数能够统计出图像中每个灰度值的个数,但是对于直方图的绘制需要我们自行解决。
void cv::calcHist(const Mat* images, // 图像或图像集合,集合内所有的图像应具有相同的尺寸和数据类型,并且数据类型只能是CV_8U、CV_16U和CV_32F三种中的一种,但是不同图像的通道数可以不同。
int nimages, // 输入图像的数量(当处理多幅图像时使用)
const int* channels, // 需要统计的通道索引数组,第一个图像的通道索引从0到images[0].channels()-1(灰度图设置为[0]),第二个图像通道索引从images[0].channels()到images[0].channels()+ images[1].channels()-1,以此类推
InputArray mask, // 可选的操作掩码,如果是空矩阵则表示图像中所有位置的像素都计入直方图中,如果矩阵不为空,则必须与输入图像尺寸相同且数据类型为CV_8U
OutputArray hist, // 输出的统计直方图结果,是一个dims维度的数组,cv::Mat形式
int dims, // 直方图的维数。对于灰度图像,默认为1;对于彩色图像,默认为3(每个颜色通道一个维度)
const int* histSize, // 存放每个维度直方图的数组的尺寸,即像素最小值和最小值的差距
const float** ranges, // 每个图像通道中灰度值的取值范围
bool uniform = true, // 直方图是否均匀的标志符,默认状态下为均匀
bool accumulate = false // 是否累积统计直方图的标志,如果累积(true),则统计新图像的直方图时之前图像的统计结果不会被清除,该功能主要用于统计多个图像整体的直方图
);
上面参数中需要注意有些参数是指针,下面是用数组来定义的方式:
cv::Mat image = cv::imread("C:/Users/Opencv/temp/lena.png");
if (image.empty()) {
cout << "打开图片失败" <<endl;
return -1;
}
cv::Mat gray;
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
//定义直方图参数来统计
cv::Mat hist; // 存放直方图结果
const int channels[] = { 0 }; // 通道索引
const int histsize[] = { 256 }; // 直方图的维度,即像素最小值和最大值的差距
float inrange[] = { 0,255 };
const float* ranges[] = {inrange}; // 像素灰度值范围
cv::calcHist(&gray, 1, channels, cv::Mat(), hist, 1, histsize, ranges);
一张很大的图像,某些像素点的值可能有成百上千个,某些像素点可能为0,彼此差距较大。所以需要进行归一化处理,在特定范围内对像素数据进行缩放。OpenCV4提供了normalize()函数实现多种形式的归一化功能。
void normalize(InputArray src, OutputArray dst, double alpha = 1.0, double beta = 0.0,
int norm_type = NORM_L2, int dtype = -1, InputArray mask = noArray());
norm_type(归一化类型):NORM_INF, NORM_L1, NORM_L2
dtype:输出数据类型选择标志,如果为负数,则输出数据与src拥有相同的类型
在一个全黑的图片上绘制直方图
// 绘制直方图
int histH = 500;
int histW = 600;
int width = cvRound(histW / histsize[0]);
cv::Mat histImg(histH, histW, CV_8UC1, cv::Scalar(0, 0, 0));
//归一化
cv::normalize(hist, hist, 1, 0, cv::NORM_INF, -1, cv::Mat());
//cv::normalize(hist, hist, 1, 0, cv::NORM_L1, -1, cv::Mat());
//cv::normalize(hist, hist, 1, 0, cv::NORM_L2, -1, cv::Mat());
for (int i = 1; i < hist.rows; i++)
{
cv::rectangle(histImg, cv::Point(width * (i - 1), histH - 1),
cv::Point(width * i - 1, histH - cvRound(histH * hist.at<float>(i - 1)) - 1),
cv::Scalar(255, 255, 255), -1);
}
从上面的结果可以看出,当图像亮度比较暗的时候,其直方图就会集中在低像素区域,同理,原图较亮就集中在高像素区域。当都集中在中间值100到150之间,则整个图像想会给人一种模糊的感觉,看不清图中的内容
这种像素集中在某个区域的图像,其整体对比度较小,不利于纹理的识别(如像素灰度值集中在10,11,12这些相邻区域,肉眼都很难区分这几种像素值的差别)。为此,需要增大对比度,可通过映射关系,将图像中灰度值的范围扩大,增加原来两个灰度值之间的差值,这个过程称为图像直方图均衡化,也就是图像增强中的对比度增强。
Opencv中使用equalizeHist()函数对单通道图像进行直方图均衡化操作,参数只有两个:输入图和输出图
void drawHist(cv::Mat &hist, const int* histsize, string name)
{
// 绘制直方图
int histH = 500;
int histW = 600;
int width = cvRound(histW / histsize[0]);
cv::Mat histImg(histH, histW, CV_8UC1, cv::Scalar(0, 0, 0));
//归一化
cv::normalize(hist, hist, 1, 0, cv::NORM_INF, -1, cv::Mat());
//cv::normalize(hist, hist, 1, 0, cv::NORM_L1, -1, cv::Mat());
//cv::normalize(hist, hist, 1, 0, cv::NORM_L2, -1, cv::Mat());
for (int i = 1; i < hist.rows; i++)
{
cv::rectangle(histImg, cv::Point(width * (i - 1), histH - 1),
cv::Point(width * i - 1, histH - cvRound(histH * hist.at<float>(i - 1)) - 1),
cv::Scalar(255, 255, 255), -1);
}
cv::imshow(name, histImg);
}
int main()
{
cv::Mat image = cv::imread("C:/Users/Opencv/temp/hist.png");
if (image.empty()) {
cout << "打开图片失败" <<endl;
return -1;
}
cv::Mat gray, equImg;
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
cv::equalizeHist(gray, equImg);
//定义直方图参数
cv::Mat hist1, hist2; // 存放直方图结果
const int channels[] = { 0 }; // 通道索引
const int histsize[] = { 256 }; // 直方图的维度,即像素最小值和最大值的差距
float inrange[] = { 0,255 };
const float* ranges[] = {inrange}; // 像素灰度值范围
cv::calcHist(&gray, 1, channels, cv::Mat(), hist1, 1, histsize, ranges);
cv::calcHist(&equImg, 1, channels, cv::Mat(), hist2, 1, histsize, ranges);
drawHist(hist1, histsize, "原图直方图");
drawHist(hist2, histsize, "均值化后直方图");
cv::imshow("gray", gray);
cv::imshow("equ", equImg);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
注:直方图均衡化只能在单通道图片进行
直方图匹配(Histogram Matching),也称为直方图规定化或直方图规范化。给定一个参考图像,在某些特定的条件下需要将原图直方图映射成指定的分布形式。它通过调整原始图像的像素值,使其直方图与参考图像的直方图相似,从而达到两幅图像之间颜色和对比度的匹配。
匹配的原理不解释了,OpenCV中没有直接进行匹配的函数,可自行使用LUT来进行相关的映射来改变像素值。大致步骤:1)计算两张图像直方图的累积概率。2)构建累积概率误差矩阵。3)生成LUT映射表
代码如下(示例):
#include
#include
#include
using namespace std;
void drawHist(cv::Mat &hist, const int* histsize, string name)
{
// 绘制直方图
int histH = 500;
int histW = 600;
int width = cvRound(histW / histsize[0]);
cv::Mat histImg(histH, histW, CV_8UC1, cv::Scalar(0, 0, 0));
//归一化
cv::normalize(hist, hist, 1, 0, cv::NORM_INF, -1, cv::Mat());
//cv::normalize(hist, hist, 1, 0, cv::NORM_L1, -1, cv::Mat());
//cv::normalize(hist, hist, 1, 0, cv::NORM_L2, -1, cv::Mat());
for (int i = 1; i < hist.rows; i++)
{
cv::rectangle(histImg, cv::Point(width * (i - 1), histH - 1),
cv::Point(width * i - 1, histH - cvRound(histH * hist.at<float>(i - 1)) - 1),
cv::Scalar(255, 255, 255), -1);
}
cv::imshow(name, histImg);
}
int main()
{
cv::Mat image1 = cv::imread("C:/Users/Opencv/temp/hist.png");
cv::Mat image2 = cv::imread("C:/Users/Opencv/temp/yuan.png");
if (image1.empty() || image2.empty()) {
cout << "打开图片失败" <<endl;
return -1;
}
cv::Mat gray1, gray2, equImg;
cv::cvtColor(image1, gray1, cv::COLOR_BGR2GRAY);
cv::cvtColor(image2, gray2, cv::COLOR_BGR2GRAY);
cv::equalizeHist(gray1, equImg);
//定义直方图参数
cv::Mat hist1, hist2, hist3; // 存放直方图结果
const int channels[] = { 0 }; // 通道索引
const int histsize[] = { 256 }; // 直方图的维度,即像素最小值和最大值的差距
float inrange[] = { 0,255 };
const float* ranges[] = {inrange}; // 像素灰度值范围
// 计算两张图像直方图
//cv::calcHist(&image1, 1, channels, cv::Mat(), hist1, 1, histsize, ranges);
//cv::calcHist(&image2, 1, channels, cv::Mat(), hist2, 1, histsize, ranges);
cv::calcHist(&gray1, 1, channels, cv::Mat(), hist1, 1, histsize, ranges);
cv::calcHist(&gray2, 1, channels, cv::Mat(), hist2, 1, histsize, ranges);
cv::calcHist(&equImg, 1, channels, cv::Mat(), hist3, 1, histsize, ranges);
// 归一化
drawHist(hist1, histsize, "原图直方图");
drawHist(hist2, histsize, "模板直方图");
drawHist(hist3, histsize, "均值化后直方图");
//1.计算两张图像直方图的累积概率
float hist1_cdf[256] = { hist1.at<float>(0) };
float hist2_cdf[256] = { hist2.at<float>(0) };
for (int i = 1; i < 256; i++)
{
hist1_cdf[i] = hist1_cdf[i - 1] + hist1.at<float>(i);
hist2_cdf[i] = hist2_cdf[i - 1] + hist2.at<float>(i);
}
//2.构建累积概率误差矩阵
float diff_cdf[256][256];
for (int i = 0; i < 256; i++)
{
for (int j = 0; j < 256; j++)
{
diff_cdf[i][j] = fabs(hist1_cdf[i] - hist2_cdf[j]);
}
}
//3.生成LUT映射表
cv::Mat lut(1, 256, CV_8U);
for (int i = 0; i < 256; i++)
{
// 查找源灰度级为i的映射灰度
// 和i的累积概率差值最小的规定化灰度
float min = diff_cdf[i][0];
int index = 0;
//寻找累积概率误差矩阵中每一行中的最小值
for (int j = 1; j < 256; j++)
{
if (min > diff_cdf[i][j])
{
min = diff_cdf[i][j];
index = j;
}
}
lut.at<uchar>(i) = (uchar)index;
}
cv::Mat matchImg;
//cv::LUT(image1, lut, matchImg);
cv::LUT(gray1, lut, matchImg);
//cv::imshow("原图", image1);
//cv::imshow("模板图", image2);
cv::imshow("原图", gray1);
cv::imshow("模板图", gray2);
cv::imshow("匹配图", matchImg);
cv::imshow("equ", equImg);
cv::Mat hist4;
cv::calcHist(&matchImg, 1, channels, cv::Mat(), hist4, 1, histsize, ranges);
drawHist(hist4, histsize, "匹配直方图");
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
直方图均值化的equalizeHist()函数没有任何可调参数,故只有一种结果——图像直方图必然是均匀分布的。直方图匹配更加灵活,根据模板图片的不同,能实现不同区域分布的直方图,能够实现增强某个灰度区间。
个人理解:像素之间的个数差距是不变的,即直方图中的高点和低点的趋势不变。直方图均值化是让其均匀分布,直方图匹配是根据一定的映射关系来改变分布区域(如像素值18的个数最多,为最高点,即使映射成其它数值,它还是最高点)。
此外,直方图均衡化的目标是增强图像的全局对比度,使得图像更加鲜明、清晰。通过重新分布图像的像素值,使得直方图在整个灰度范围内尽可能平均分布,其equalizeHist函数只能对灰度图进行操作。而直方图匹配没有这个限制,在对彩色图像进行操作时,通过匹配图像的颜色特性可以实现视觉一致性,可用于颜色转换、风格迁移等应用中。