图像增强-直方图均衡化
首先,直方图均衡化之前,我们需要计算一幅图像中,各个像素的所占比例,也就是概率分布,计算出图像在[0, 255]中所有像素的直方图(绘制),通过计算出来的直方图去做直方图均衡化的CDF概率计算。所以我们这里讲一下怎么计算直方图,我会介绍两种方法,分别是手动底层计算直方图和调用OpenCV的API去计算直方图,建议大家学习一下如何底层计算。
一、直方图计算
首先,直方图均衡化之前,我们需要计算一幅图像中,各个像素的所占比例,也就是概率分布,计算出图像在[0, 255]中所有像素的直方图(绘制),通过计算出来的直方图去做直方图均衡化的CDF概率计算。所以我们这里讲一下怎么计算直方图,我会介绍两种方法,分别是手动底层计算直方图和调用OpenCV的API去计算直方图,建议大家学习一下如何底层计算。
1.1 手动计算直方图
#include <iostream>
#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
std::vector<int> mannualCalHist(const cv::Mat& grayImage)
{
CV_Assert(grayImage.channels() == 1); // 判断是否为单通道图像
// 初始化直方图
std::vector<int> Hist(256, 0); // 将像素全部初始化为0
// 统计各个像素的数量
for(int i = 0; i < grayImage.rows; ++i)
{
const uchar* pixelRow = grayImage.ptr<uchar>(i)
for(int j = 0; j < grayImage.cols; ++j)
{
Hist[pixelRow[j]];
}
}
}
1.2 OpenCV计算直方图
#include <iostream>
#include <opencv2/opencv.hpp>
#include <string>
#include <vector>
std::vector<int> OpencvCalHist(const cv::Mat& grayImage)
{
std::vector<int> HistSize = {256};
std::vector<float> ranges = {0, 256};
cv::Mat Hist;
cv::calcHist(std::vector<cv::Mat>{grayImage}, std::vector<int>{0}, cv::Mat(), Hist, HistSize, ranges, false);
// 统一转换为数组类型,用于后续与手动计算直方图比较
std::vector<int> histCal(256, 0);
for(int i = 0; i < 256; ++i)
{
// 因为就算出来的直方图是1×MN的,所以可以直接取出数据即可
histCal[Hist.at<uchar>(i)]++;
}
return histCal;
}
1.3 手动计算直方图与OpenCV计算直方图比较
#include <iostream>
#include <opencv2/opencv.hpp>
#include <string>
#include <vector>
void manualAndOpencvCompare()
{
// 读取图像 OpenCV读取图像为BGR,video期望的是RGB
cv::Mat image = cv::imread("C:/Users/jiang/Desktop/data/0250.jpg", -1);
cv::Mat gray_image;
cv::cvtColor(image, gray_image, cv::COLOR_BGR2GRAY);
// 手动计算直方图
std::vector<int> manualHistogramResult = manualHistogramCalculation(gray_image);
// OpnCV计算直方图
std::vector<int> histSize = { 256 }; // 构建直方图的区间
std::vector<float> ranges = { 0, 256 }; // 区间像素范围分布
cv::Mat hist_opencv; // 用于OpenCV计算直方图
cv::calcHist(std::vector<cv::Mat>{gray_image}, std::vector<int>{0}, cv::Mat(), hist_opencv, histSize, ranges, false);
// 对手动计算的和opencv计算的直方图进行比较
bool isCorrect = true;
for (int i = 0; i < 256; ++i)
{
if (manualHistogramResult[i] != static_cast<int>(hist_opencv.at<float>(i)))
{
isCorrect = false;
break;
}
}
if (isCorrect)
{
std::cout << "手动计算的直方图数据和opencv计算的直方图数据完全相同!" << std::endl;
}
// 定义直方图的显示框的长和宽以及每个像素分布的宽度
int hist_height = 400;
int hist_width = 512;
double bin_w = round((double)hist_width / 256);
// 创建显示框
cv::Mat HistImage(cv::Size(hist_width, hist_height), CV_8UC3, cv::Scalar(255, 255, 255));
// 手动归一化
int max_value = *max_element(manualHistogramResult.begin(), manualHistogramResult.end());
// 绘制手动显示框
for (int i = 0; i < 255; ++i)
{
int height = cvRound(manualHistogramResult[i] * hist_height / static_cast<double>(max_value));
// 开始绘制
cv::line(HistImage, cv::Point(bin_w * i, hist_height - height), cv::Point(bin_w * (i + 1), height), cv::Scalar(255, 0, 0), 2, 8, 0);
}
// 展示原图和灰度图
// 创建一个可以调整大小的窗口,并设置初始尺寸
cv::namedWindow("Image Window", cv::WINDOW_NORMAL);
cv::resizeWindow("Image Window", image.cols / 2, image.rows/2); // 设置初始宽高
cv::imshow("Image Window", image);
cv::namedWindow("Gray Window", cv::WINDOW_NORMAL);
cv::resizeWindow("Gray Window", gray_image.cols / 2, gray_image.rows / 2);
cv::imshow("Gray Window", gray_image);
// 显示直方图绘制结果
cv::imshow("manualCalHistImage", HistImage);
cv::waitKey(0);
cv::destroyAllWindows();
}
结果如下,其手动计算的直方图结果与OpenCV计算的直方图结果是完全一样的。

二、直方图均衡化
直方图均衡化,其实就是在直方图计算的结果上进行离散像素概率统计,进行归一化后再根据像素累计重新分配像素,使其像素分配均匀的情况。我们下面举一个例子说明,以下这个例子来源于萨冈雷斯图像处理第四版,供大家学习。首先我们先讲解一下直方图均衡化的计算公式,再根据公式讲解下面这个例子,这样大家就比较清晰了,对于离散的像素值,我们用概率与求和来代替概率密度函数与积分(本质是一样的),所以数字图像中出现灰度级rk的概率近似为

其中nk为某一像素值的统计,MN为像素总和(宽*高),可以得到某一个像素的概率分布(归一化),然后根据直方图均衡化的变换离散公式计算,就可以将图像中的输入灰度级为rk的每个像素映射为图像灰度级为sk的对应像素了。得到的输出图像就是直方图均衡化的图像。我们举例说明。


我们通过萨冈雷斯图像处理第四版这个典型案例来说明直方图均衡化的具体流程。假设图像大小为64*64,MN = 4096的3比特图像(L = 8),为什么3比特是L = 8呢?首先一副灰度图是8比特图像,可以让我 S = r7r6r5r4r3r2r1r0来表示,3比特即2^3 = 8,具体详解可以参考书籍或者资料,具体就不悉数了,通过下面的计算我们可以大概知道下面那些数值是怎么来的,这里的离散型概率求和就等同于概率密度,因为像素值是离散的,所以我们采用概率求和计算来求解每个像素的概率密度即可。求出输入级像素通过直方图均衡化后的输出灰度级像素,因为它们是通过求概率值的和生成的,因此需要将他们四舍五入到接近的整数值,由下面的数值可以得出均衡化后的直方图的值,由原来的7个灰度级最后生成了5个灰度级,并通过折线图可以看出,像素分配更加均匀了。



#include <iostream>
#include <opencv2/opencv.hpp>
#include <string>
#include <vector>
// 定于手动计算直方图函数
std::vector<int> manualCalHistFunc(const cv::Mat& grayImage)
{
// 对单通道图像进行直方图计算
CV_Assert(grayImage.channels() == 1);
// 初始化图像像素
std::vector<int> histToGram(256, 0);
// 获取图像的宽高
int width = grayImage.cols;
int height = grayImage.rows;
// 获取各个像素出现的次数
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
uchar pixelValue = grayImage.at<uchar>(i, j);
histToGram[pixelValue]++;
}
}
return histToGram;
}
// 定义手动直方图均衡化函数
cv::Mat manualHistogramEqualization(const cv::Mat& grayImage)
{
CV_Assert(grayImage.channels() == 1);
// 手动计算直方图
std::vector<int> manualHistoGramResult = manualCalHistFunc(grayImage);
// 计算图像总像素个数
int totalPixelValue = grayImage.rows * grayImage.cols;
// 计算累计分布函数cdf
std::vector<float> CDF(256, 0.0f);
CDF[0] = static_cast<float>(manualHistoGramResult[0]) / totalPixelValue;
for (int i = 1; i < 256; ++i)
{
CDF[i] = CDF[i - 1] + static_cast<float>(manualHistoGramResult[i]) / totalPixelValue;
}
// 手动创建lut映射表,用于直方图均衡化
std::vector<uchar> lut(256);
for (int i = 0; i < 256; ++i)
{
lut[i] = cv::saturate_cast<uchar>(255.0f * CDF[i]);
}
// 应用映射表
cv::Mat equalized = grayImage.clone();
for (int i = 0; i < equalized.rows; ++i)
{
for (int j = 0; j < equalized.cols; ++j)
{
// 改变图像的像素值,使其使用lut映射后的像素值
equalized.at<uchar>(i, j) = lut[grayImage.at<uchar>(i, j)];
}
}
return equalized;
}
// 定义直方图绘制函数
cv::Mat drawHistFunc(std::vector<int> histogram, const cv::Scalar& color)
{
// 定义图像显示框的宽高
int width = 400;
int height = 400;
// 修复:确保bin宽度计算准确
double bin_width = static_cast<double>(width) / 256;
int bin_w = cvRound(bin_width); // 四舍五入取整
// 初始化显示框
cv::Mat histImage(cv::Size(width, height), CV_8UC3, cv::Scalar(255, 255, 255));
// 手动归一化
int maxValue = *max_element(histogram.begin(), histogram.end());
// 如果最大值为0,避免除零错误
if (maxValue == 0) maxValue = 1;
// 修复:使用矩形绘制每个bin,确保占满整个宽度
for (int i = 0; i < 256; ++i)
{
// 计算当前bin的高度(从底部开始)
int hist_height = cvRound(static_cast<double>(histogram[i]) * height / maxValue);
// 计算当前bin的起始和结束x坐标
int x_start = cvRound(i * bin_width);
int x_end = cvRound((i + 1) * bin_width);
// 确保不会超出边界
if (x_end > width) x_end = width;
// 绘制矩形柱状图
if (hist_height > 0) {
cv::rectangle(histImage,
cv::Point(x_start, height - hist_height),
cv::Point(x_end - 1, height),
color,
cv::FILLED);
}
}
return histImage;
}
int main()
{
// 读取图像
cv::Mat image = cv::imread("C:/Users/jiang/Desktop/data/0250.jpg");
if (image.empty())
{
std::cout << "图像读取失败!" << std::endl;
return -1;
}
cv::Mat grayImage;
cv::cvtColor(image, grayImage, cv::COLOR_BGR2GRAY);
// 手动计算直方图
std::vector<int> manualCalHistRes = manualCalHistFunc(grayImage);
// opencv计算直方图
std::vector<int> histSize = { 256 };
std::vector<float> ranges = { 0, 256 };
cv::Mat opencvCalHistRes;
cv::calcHist(std::vector<cv::Mat>{grayImage}, std::vector<int>{0}, cv::Mat(), opencvCalHistRes, histSize, ranges);
// 进行比较
bool isCorrect = true;
for (int i = 0; i < 256; ++i)
{
if (manualCalHistRes[i] != static_cast<int>(opencvCalHistRes.at<float>(i)))
{
isCorrect = false;
}
}
if (!isCorrect)
{
std::cout << "手动计算的直方图与opencv计算的直方图不完全一致!" << std::endl;
}
// 手动直方图均衡化
cv::Mat manualEqualRes = manualHistogramEqualization(grayImage);
// opencv直方图均衡化
cv::Mat opencvEqualRes;
cv::equalizeHist(grayImage, opencvEqualRes);
// 比较手动直方图均衡化与opencv直方图均衡化
bool equalizationCorrect = true;
int diffCount = 0;
for (int i = 0; i < grayImage.rows; i++)
{
for (int j = 0; j < grayImage.cols; ++j)
{
if (manualEqualRes.at<uchar>(i, j) != opencvEqualRes.at<uchar>(i, j))
{
diffCount++;
// 只显示前五个的差异
if (diffCount < 5)
{
std::cout << "不同的像素点位置为:" << i << j << std::endl;
std::cout << "手动直方图均衡化的像素值为:" << manualEqualRes.at<uchar>(i, j) << std::endl;
std::cout << "opencv直方图均衡化的像素值为:" << opencvEqualRes.at<uchar>(i, j) << std::endl;
}
equalizationCorrect = false;
}
}
}
if (!equalizationCorrect)
{
std::cout << "手动直方图均衡化与opencv直方图均衡化不一致!" << std::endl;
}
// 显示原图和灰度图
cv::namedWindow("originalImage", cv::WINDOW_NORMAL);
cv::resizeWindow("originalImage", image.cols / 2, image.rows / 2);
cv::imshow("originalImage", image);
cv::namedWindow("grayImage", cv::WINDOW_NORMAL);
cv::resizeWindow("grayImage", image.cols / 2, image.rows / 2);
cv::imshow("grayImage", grayImage);
// 绘制手动直方图
cv::Mat manualHistImage = drawHistFunc(manualCalHistRes, cv::Scalar(255, 0, 0));
cv::imshow("manualHistImage", manualHistImage);
// 对于mat型的直方图定义lambda表达式进行转换
auto convert2vector = [](const cv::Mat& histogram)->std::vector<int>
{
std::vector<int> result(256);
for (int i = 0; i < 256; ++i)
{
result[i] = static_cast<int>(histogram.at<float>(i));
}
return result;
};
// 绘制opencv计算的直方图
std::vector<int> opencvCalHistRes_vector = convert2vector(opencvCalHistRes);
cv::Mat opencvHistImage = drawHistFunc(opencvCalHistRes_vector, cv::Scalar(0, 255, 0));
cv::imshow("OpencvHistImage", opencvHistImage);
auto calEquaHistogram = [](const cv::Mat& equalization) ->std::vector<int>
{
// 初始化像素值
std::vector<int> histogram(256, 0);
// 计算像素的概率分布
for (int i = 0; i < equalization.rows; ++i)
{
for (int j = 0; j < equalization.cols; ++j)
{
uchar pixelValue = equalization.at<uchar>(i, j);
histogram[pixelValue]++;
}
}
return histogram;
};
// 绘制手动均衡化直方图
std::vector<int> manualEquaRes_vec = calEquaHistogram(manualEqualRes);
cv::Mat manualEquaImage = drawHistFunc(manualEquaRes_vec, cv::Scalar(0, 0, 255));
cv::imshow("manualEquaImage", manualEquaImage);
// 绘制opencv均衡化直方图
std::vector<int> opencvEquaRes_vec = calEquaHistogram(opencvEqualRes);
cv::Mat opencvEquaImage = drawHistFunc(opencvEquaRes_vec, cv::Scalar(0, 0, 0));
cv::imshow("opencvEquaImage", opencvEquaImage);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}


魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)