.. _histogram_calculation: 直方图计算 ********************* 目标 ===== 本文档尝试解答如下问题: .. container:: enumeratevisibleitemswithsquare * 如何使用OpenCV函数 :split:`split <>` 将图像分割成单通道数组。 * 如何使用OpenCV函数 :calc_hist:`calcHist <>` 计算图像阵列的直方图。 * 如何使用OpenCV函数 :normalize:`normalize <>` 归一化数组。 .. note:: 在上一篇中 (:ref:`histogram_equalization`) 我们介绍了一种特殊直方图叫做 *图像直方图* 。现在我们从更加广义的角度来考虑直方图的概念,继续往下读! 什么是直方图? -------------------- .. container:: enumeratevisibleitemswithsquare * 直方图是对数据的集合 *统计* ,并将统计结果分布于一系列预定义的 *bins* 中。 * 这里的 *数据* 不仅仅指的是灰度值 (如上一篇您所看到的), 统计数据可能是任何能有效描述图像的特征。 * 先看一个例子吧。 假设有一个矩阵包含一张图像的信息 (灰度值 :math:`0-255`): .. image:: images/Histogram_Calculation_Theory_Hist0.jpg :align: center * 如果我们按照某种方式去 *统计* 这些数字,会发生什么情况呢? 既然已知数字的 *范围* 包含 256 个值, 我们可以将这个范围分割成子区域(称作 **bins**), 如: .. math:: \begin{array}{l} [0, 255] = { [0, 15] \cup [16, 31] \cup ....\cup [240,255] } \\ range = { bin_{1} \cup bin_{2} \cup ....\cup bin_{n = 15} } \end{array} 然后再统计掉入每一个 :math:`bin_{i}` 的像素数目。采用这一方法来统计上面的数字矩阵,我们可以得到下图( x轴表示 bin, y轴表示各个bin中的像素个数)。 .. image:: images/Histogram_Calculation_Theory_Hist1.jpg :align: center * 以上只是一个说明直方图如何工作以及它的用处的简单示例。直方图可以统计的不仅仅是颜色灰度, 它可以统计任何图像特征 (如 梯度, 方向等等)。 * 让我们再来搞清楚直方图的一些具体细节: a. **dims**: 需要统计的特征的数目, 在上例中, **dims = 1** 因为我们仅仅统计了灰度值(灰度图像)。 b. **bins**: 每个特征空间 **子区段** 的数目,在上例中, **bins = 16** c. **range**: 每个特征空间的取值范围,在上例中, **range = [0,255]** * 怎样去统计两个特征呢? 在这种情况下, 直方图就是3维的了,x轴和y轴分别代表一个特征, z轴是掉入 :math:`(bin_{x}, bin_{y})` 组合中的样本数目。 同样的方法适用于更高维的情形 (当然会变得很复杂)。 OpenCV的直方图计算 ----------------------- OpenCV提供了一个简单的计算数组集(通常是图像或分割后的通道)的直方图函数 :calc_hist:`calcHist <>` 。 支持高达 32 维的直方图。下面的代码演示了如何使用该函数计算直方图! 源码 ==== .. container:: enumeratevisibleitemswithsquare * **本程序做什么?** .. container:: enumeratevisibleitemswithsquare * 装载一张图像 * 使用函数 :split:`split <>` 将载入的图像分割成 R, G, B 单通道图像 * 调用函数 :calc_hist:`calcHist <>` 计算各单通道图像的直方图 * 在一个窗口叠加显示3张直方图 * **下载代码**: 点击 `这里 `_ * **代码一瞥:** .. code-block:: cpp #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include #include using namespace std; using namespace cv; /** @函数 main */ int main( int argc, char** argv ) { Mat src, dst; /// 装载图像 src = imread( argv[1], 1 ); if( !src.data ) { return -1; } /// 分割成3个单通道图像 ( R, G 和 B ) vector rgb_planes; split( src, rgb_planes ); /// 设定bin数目 int histSize = 255; /// 设定取值范围 ( R,G,B) ) float range[] = { 0, 255 } ; const float* histRange = { range }; bool uniform = true; bool accumulate = false; Mat r_hist, g_hist, b_hist; /// 计算直方图: calcHist( &rgb_planes[0], 1, 0, Mat(), r_hist, 1, &histSize, &histRange, uniform, accumulate ); calcHist( &rgb_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, &histRange, uniform, accumulate ); calcHist( &rgb_planes[2], 1, 0, Mat(), b_hist, 1, &histSize, &histRange, uniform, accumulate ); // 创建直方图画布 int hist_w = 400; int hist_h = 400; int bin_w = cvRound( (double) hist_w/histSize ); Mat histImage( hist_w, hist_h, CV_8UC3, Scalar( 0,0,0) ); /// 将直方图归一化到范围 [ 0, histImage.rows ] normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() ); normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() ); normalize(b_hist, b_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() ); /// 在直方图画布上画出直方图 for( int i = 1; i < histSize; i++ ) { line( histImage, Point( bin_w*(i-1), hist_h - cvRound(r_hist.at(i-1)) ) , Point( bin_w*(i), hist_h - cvRound(r_hist.at(i)) ), Scalar( 0, 0, 255), 2, 8, 0 ); line( histImage, Point( bin_w*(i-1), hist_h - cvRound(g_hist.at(i-1)) ) , Point( bin_w*(i), hist_h - cvRound(g_hist.at(i)) ), Scalar( 0, 255, 0), 2, 8, 0 ); line( histImage, Point( bin_w*(i-1), hist_h - cvRound(b_hist.at(i-1)) ) , Point( bin_w*(i), hist_h - cvRound(b_hist.at(i)) ), Scalar( 255, 0, 0), 2, 8, 0 ); } /// 显示直方图 namedWindow("calcHist Demo", CV_WINDOW_AUTOSIZE ); imshow("calcHist Demo", histImage ); waitKey(0); return 0; } 解释 =========== #. 创建一些矩阵: .. code-block:: cpp Mat src, dst; #. 装载原图像 .. code-block:: cpp src = imread( argv[1], 1 ); if( !src.data ) { return -1; } #. 使用OpenCV函数 :split:`split <>` 将图像分割成3个单通道图像: .. code-block:: cpp vector rgb_planes; split( src, rgb_planes ); 输入的是要被分割的图像 (这里包含3个通道), 输出的则是Mat类型的的向量。 #. 现在对每个通道配置 **直方图** 设置, 既然我们用到了 R, G 和 B 通道, 我们知道像素值的范围是 :math:`[0,255]` a. 设定bins数目 (5, 10...): .. code-block:: cpp int histSize = 255; b. 设定像素值范围 (前面已经提到,在 0 到 255之间 ) .. code-block:: cpp /// 设定取值范围 ( R,G,B) ) float range[] = { 0, 255 } ; const float* histRange = { range }; c. 我们要把bin范围设定成同样大小(均一)以及开始统计前先清除直方图中的痕迹: .. code-block:: cpp bool uniform = true; bool accumulate = false; d. 最后创建储存直方图的矩阵: .. code-block:: cpp Mat r_hist, g_hist, b_hist; e. 下面使用OpenCV函数 :calc_hist:`calcHist <>` 计算直方图: .. code-block:: cpp /// 计算直方图: calcHist( &rgb_planes[0], 1, 0, Mat(), r_hist, 1, &histSize, &histRange, uniform, accumulate ); calcHist( &rgb_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, &histRange, uniform, accumulate ); calcHist( &rgb_planes[2], 1, 0, Mat(), b_hist, 1, &histSize, &histRange, uniform, accumulate ); 参数说明如下: .. container:: enumeratevisibleitemswithsquare + **&rgb_planes[0]:** 输入数组(或数组集) + **1**: 输入数组的个数 (这里我们使用了一个单通道图像,我们也可以输入数组集 ) + **0**: 需要统计的通道 (*dim*)索引 ,这里我们只是统计了灰度 (且每个数组都是单通道)所以只要写 0 就行了。 + **Mat()**: 掩码( 0 表示忽略该像素), 如果未定义,则不使用掩码 + **r_hist**: 储存直方图的矩阵 + **1**: 直方图维数 + **histSize:** 每个维度的bin数目 + **histRange:** 每个维度的取值范围 + **uniform** 和 **accumulate**: bin大小相同,清楚直方图痕迹 #. 创建显示直方图的画布: .. code-block:: cpp // 创建直方图画布 int hist_w = 400; int hist_h = 400; int bin_w = cvRound( (double) hist_w/histSize ); Mat histImage( hist_w, hist_h, CV_8UC3, Scalar( 0,0,0) ); #. 在画直方图之前,先使用 :normalize:`normalize <>` 归一化直方图,这样直方图bin中的值就被缩放到指定范围: .. code-block:: cpp /// 将直方图归一化到范围 [ 0, histImage.rows ] normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() ); normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() ); normalize(b_hist, b_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() ); 该函数接受下列参数: .. container:: enumeratevisibleitemswithsquare + **r_hist:** 输入数组 + **r_hist:** 归一化后的输出数组(支持原地计算) + **0** 及 **histImage.rows**: 这里,它们是归一化 **r_hist** 之后的取值极限 + **NORM_MINMAX:** 归一化方法 (例中指定的方法将数值缩放到以上指定范围) + **-1:** 指示归一化后的输出数组与输入数组同类型 + **Mat():** 可选的掩码 #. 请注意这里如何读取直方图bin中的数据 (此处是一个1维直方图): .. code-block:: cpp /// 在直方图画布上画出直方图 for( int i = 1; i < histSize; i++ ) { line( histImage, Point( bin_w*(i-1), hist_h - cvRound(r_hist.at(i-1)) ) , Point( bin_w*(i), hist_h - cvRound(r_hist.at(i)) ), Scalar( 0, 0, 255), 2, 8, 0 ); line( histImage, Point( bin_w*(i-1), hist_h - cvRound(g_hist.at(i-1)) ) , Point( bin_w*(i), hist_h - cvRound(g_hist.at(i)) ), Scalar( 0, 255, 0), 2, 8, 0 ); line( histImage, Point( bin_w*(i-1), hist_h - cvRound(b_hist.at(i-1)) ) , Point( bin_w*(i), hist_h - cvRound(b_hist.at(i)) ), Scalar( 255, 0, 0), 2, 8, 0 ); } 使用了以下表达式: .. code-block:: cpp r_hist.at(i) :math:`i` 指示维度,假如我们要访问2维直方图,我们就要用到这样的表达式: .. code-block:: cpp r_hist.at( i, j ) #. 最后显示直方图并等待用户退出程序: .. code-block:: cpp namedWindow("calcHist Demo", CV_WINDOW_AUTOSIZE ); imshow("calcHist Demo", histImage ); waitKey(0); return 0; 结果 ====== #. 使用下图作为输入图像: .. image:: images/Histogram_Calculation_Original_Image.jpg :align: center #. 产生以下直方图: .. image:: images/Histogram_Calculation_Result.jpg :align: center 翻译者 ================= niesu@ `OpenCV中文网站 `_