注:本文使用OpenCV版本:2.4.13
calcHist
OpenCV提供了计算图像直方图(Histogram)的接口calcHist:
void calcHist(const Mat* images,
int nimages,
const int* channels,
InputArray mask,
OutputArray hist,
int dims,
const int* histSize,
const float** ranges,
bool uniform=true,
bool accumulate=false);
其参数的涵义如下:
images
,源图像数组的指针。calcHist
可以计算多张同样大小、同样深度(CV_8U或CV_32F)、不同通道数的图像的多维直方图,所以要求传入一组图像的指针。如果我们只计算一张图像的直方图,传入该图像的指针即可。nimages
,源图像的个数。如果我们只计算一张图像的直方图,传入1即可。channels
,参与计算的通道的数组的指针,数组的长度与dims
相同。图像可能有多个通道,比如RGB图像就有3个通道。我们可以指定要参与计算的通道的索引。通道索引的规则如下:第一个图像的通道的索引为从0
到images[0].channels() - 1
,第二个图像的通道的索引为从images[0].channels()
到images[0].channels() + images[1].channels() - 1
,依此类推。比如我们有两张RGB图像,我们要计算第一张图像的B通道和第二张图像的B通道(注意OpenCV中RGB图像存储为BGR格式),则我们设置int channels[2] = {0, 3};
。需要注意的是,有时会看到有人将
channels
设置为0
或NULL
,通过查阅源代码,得知这样做的结果相当于只计算每个图像的第一个通道。不过在官方文档中并未作此说明,所以不提倡这样用。mask
,可选的掩模矩阵。如果掩模矩阵是空的,则不启用掩模操作;如果掩模矩阵不为空,则必须是与源图像大小相同的8位矩阵,非零值代表对应像素参与计算。hist
,输出的dims
维直方图。dims
,直方图的维数,必须为正数且小于等于CV_MAX_DIMS
(当前版本为32)。我们经常计算的是一维直方图,此时传入1即可。histSize
,直方图每一维度大小的数组的指针。每一维度的大小也就是bin
的大小。ranges
,直方图每一维度取值范围的数组的指针。当uniform
为true
时,ranges[i]
是一个包含两个元素的数组,第一个元素指定了第i
维的第0 bin
的取值下界(含),第二个元素指定了第i
维的第histSize[i] - 1 bin
的取值上界(不含),histSize[i]
个bin
均匀分割此取值范围。当uniform
为false
时,每一个ranges[i]
包含了histSize[i] + 1
个元素,它们作为每一bin
的边界。不包含在取值范围内的值不参与直方图计算。uniform
,指明直方图的取值是否均匀(参见ranges
参数的说明)。accumulate
,累积标识。如果为true
,初始时不会将传入的直方图清零,这样可以多次调用此函数来计算多个集合的图像的合并直方图。
直方图计算及绘制
使用calcHist
来计算一张单通道灰度图的直方图,代码如下:
//
// 计算一张单通道灰度图像的直方图
#include <iostream>
#include <iomanip>
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
// 使用直线绘制直方图
// 注意:因为反锯齿的原因,0值也会绘制出点来,可对line函数传入thickness=1来解决,
// 也可使用其他绘制函数
void drawHist_Line(const cv::Mat& hist, cv::Mat& canvas, const cv::Scalar& color)
{
CV_Assert(!hist.empty() && hist.cols == 1);
CV_Assert(hist.depth() == CV_32F && hist.channels() == 1);
CV_Assert(!canvas.empty() && canvas.cols >= hist.rows);
const int width = canvas.cols;
const int height = canvas.rows;
// 获取最大值
double dMax = 0.0;
cv::minMaxLoc(hist, nullptr, &dMax);
// 计算直线的宽度
float thickness = float(width) / float(hist.rows);
// 绘制直方图
for (int i = 0; i < hist.rows; ++i)
{
double h = hist.at<float>(i, 0) / dMax * 0.9 * height; // 最高显示为画布的90%
cv::line(canvas,
cv::Point(static_cast<int>(i * thickness), height),
cv::Point(static_cast<int>(i * thickness), static_cast<int>(height - h)),
color,
static_cast<int>(thickness));
}
}
// 使用Rect绘制直方图
void drawHist_Rect(const cv::Mat& hist, cv::Mat& canvas, const cv::Scalar& color)
{
CV_Assert(!hist.empty() && hist.cols == 1);
CV_Assert(hist.depth() == CV_32F && hist.channels() == 1);
CV_Assert(!canvas.empty() && canvas.cols >= hist.rows);
const int width = canvas.cols;
const int height = canvas.rows;
// 获取最大值
double dMax = 0.0;
cv::minMaxLoc(hist, nullptr, &dMax);
// 计算直线的宽度
float thickness = float(width) / float(hist.rows);
// 绘制直方图
for (int i = 1; i < hist.rows; ++i)
{
double h = hist.at<float>(i, 0) / dMax * 0.9 * height; // 最高显示为画布的90%
cv::rectangle(canvas,
cv::Point(static_cast<int>((i - 1) * thickness), height),
cv::Point(static_cast<int>(i * thickness), static_cast<int>(height - h)),
color,
static_cast<int>(thickness));
}
}
// 绘制折线直方图
void drawHist_Polyline(const cv::Mat& hist, cv::Mat& canvas, const cv::Scalar& color)
{
CV_Assert(!hist.empty() && hist.cols == 1);
CV_Assert(hist.depth() == CV_32F && hist.channels() == 1);
CV_Assert(!canvas.empty() && canvas.cols >= hist.rows);
const int width = canvas.cols;
const int height = canvas.rows;
// 获取最大值
double dMax = 0.0;
cv::minMaxLoc(hist, nullptr, &dMax);
// 计算直线的宽度
float thickness = float(width) / float(hist.rows);
// 绘制直方图
for (int i = 1; i < hist.rows; ++i)
{
double h = hist.at<float>(i - 1, 0) / dMax * 0.9 * height; // 最高显示为画布的90%
double h1 = hist.at<float>(i, 0) / dMax * 0.9 * height;
cv::line(canvas,
cv::Point(static_cast<int>((i-1) * thickness), static_cast<int>(height - h)),
cv::Point(static_cast<int>(i * thickness), static_cast<int>(height - h1)),
color);
}
}
int main()
{
// 读入图像,此时是3通道的RGB图像
cv::Mat image = cv::imread("D:/dataset/lena512.bmp");
if (image.empty())
{
return -1;
}
// 转换为单通道灰度图像
cv::Mat grayImage;
cv::cvtColor(image, grayImage, cv::COLOR_BGR2GRAY);
// 计算直方图
cv::Mat hist;
int channels[1] = { 0 };
int histSize = 256;
float range[2] = { 0, 256 };
const float* ranges[1] = { range };
cv::calcHist(&grayImage, // 只计算一个图像的直方图,所以传入此图像的指针
1, // 只计算一个图像的直方图,所以传入1
channels, // 图像只有一个通道,索引为0,所以传入只包含一个元素0的数组的指针
cv::Mat(), // 不进行掩模操作
hist, // 输出的一维直方图
1, // 计算的是一维直方图,所以传入1
&histSize, // 计算[0, 255]之中每个像素的个数,所以bin的个数为256
ranges, // 下一个参数设置为true,所以我们传入一个数组,其包含一个有两个元素的数组,这两个元素分别表示取值下界0和取值上界256(不含)
true, // 像素取值是在[0, 255]之间均匀分布的,所以设置为true
false); // 不做累积操作,所以设置为false
// 探寻计算结果
std::cout << "hist.width = " << hist.cols << ", hist.height = " << hist.rows << std::endl;
std::cout << "hist.depth = " << hist.depth() << std::endl;
std::cout.setf(std::ios::left);
for (int i = 0; i < hist.rows; ++i)
{
std::cout << std::setw(3) << i << ":" << std::setw(5) << hist.at<float>(i, 0) << " ";
if ((i + 1) % 10 == 0)
{
std::cout << std::endl;
}
}
// 显示图像
cv::imshow("Gray image", grayImage);
// 绘制并显示直方图
cv::Mat histLine(image.size(), CV_8UC3, cv::Scalar(255, 255, 255));
drawHist_Line(hist, histLine, cv::Scalar(255, 0, 0));
cv::imshow("histLine", histLine);
cv::Mat histRect(image.size(), CV_8UC3, cv::Scalar(255, 255, 255));
drawHist_Rect(hist, histRect, cv::Scalar(255, 0, 0));
cv::imshow("histRect", histRect);
cv::Mat histPolyline(image.size(), CV_8UC3, cv::Scalar(255, 255, 255));
drawHist_Polyline(hist, histPolyline, cv::Scalar(255, 0, 0));
cv::imshow("histPolyline", histPolyline);
cv::waitKey();
return 0;
}
计算结果如图所示:
可以看到,输出的直方图是一个256x1的矩阵,其深度为5,即CV_32F,矩阵的每个元素存储了对应亮度的像素的个数。
使用不同方式绘制的直方图如下所示:
绘制RGB图像的直方图
RGB图像有3个通道,绘制其直方图时,一般分别绘制R通道、G通道和B通道的直方图,可以参考如下链接:直方图计算,不再赘述。