OpenCV
开源计算机视觉库
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
直方图计算

上一篇教程: 直方图均衡化
下一篇教程: 直方图比较

原作者Ana Huamán
兼容性OpenCV >= 3.0

目标

在本教程中,您将学习如何:

  • 使用 OpenCV 函数 cv::split 将图像分割成其对应的平面。
  • 使用 OpenCV 函数 cv::calcHist 计算图像数组的直方图。
  • 使用函数 cv::normalize 归一化数组。
注意
在上一篇教程 (直方图均衡化) 中,我们讨论了一种特殊的直方图,称为 *图像直方图*。现在我们将从更一般的概念来考虑它。继续阅读!

什么是直方图?

  • 直方图是将数据组织到一组预定义的 *区间* 中的 *计数* 的集合。
  • 当我们说 *数据* 时,我们并没有将其限制为强度值(正如我们在上一篇教程 直方图均衡化 中看到的)。收集的数据可以是您发现对描述图像有用的任何特征。
  • 让我们来看一个例子。假设一个矩阵包含图像的信息(例如,范围为 0255 的强度)。
  • 如果我们想以一种有组织的方式 *计数* 此数据会发生什么?由于我们知道在这种情况下信息值的 *范围* 是 256 个值,我们可以将我们的范围细分为子部分(称为 **区间**),例如

    [0,255]=[0,15][16,31]....[240,255]range=bin1bin2....binn=15

    我们可以计算落在每个 bini 范围内的像素数量。将其应用于上面的示例,我们得到下面的图像(x 轴表示区间,y 轴表示每个区间中的像素数量)。

  • 这只是一个关于直方图如何工作以及为什么它有用的简单示例。直方图不仅可以统计颜色强度,还可以统计我们想要测量的任何图像特征(例如,梯度、方向等)。
  • 让我们确定直方图的一些部分
    1. **dims**:您想要收集数据的参数数量。在我们的示例中,**dims = 1**,因为我们只统计每个像素的强度值(在灰度图像中)。
    2. **bins**:这是每个 dim 中的 **子分区** 数量。在我们的示例中,**bins = 16**
    3. **range**:要测量的值的限制。在这种情况下:**range = [0,255]**
  • 如果您想计算两个特征怎么办?在这种情况下,您的结果直方图将是一个 3D 绘图(其中 x 和 y 将是每个特征的 binxbiny,而 z 将是每个 (binx,biny) 组合的计数数量)。对于更多特征也是如此(当然会更棘手)。

OpenCV 提供了什么

为了简单起见,OpenCV 实现了函数 cv::calcHist,它计算一组数组(通常是图像或图像平面)的直方图。它最多可以操作 32 个维度。我们将在下面的代码中看到它!

代码

  • 这个程序做什么?
    • 加载图像
    • 使用函数 cv::split 将图像分割成其 R、G 和 B 平面。
    • 通过调用函数 cv::calcHist 计算每个 1 通道平面的直方图。
    • 在一个窗口中绘制三个直方图。
  • 可下载代码: 点击 这里
  • 代码概览
    #include <iostream>
    using namespace std;
    using namespace cv;
    int main(int argc, char** argv)
    {
    CommandLineParser parser( argc, argv, "{@input | lena.jpg | input image}" );
    Mat src = imread( samples::findFile( parser.get<String>( "@input" ) ), IMREAD_COLOR );
    if( src.empty() )
    {
    return EXIT_FAILURE;
    }
    vector<Mat> bgr_planes;
    split( src, bgr_planes );
    int histSize = 256;
    float range[] = { 0, 256 }; //the upper boundary is exclusive
    const float* histRange[] = { range };
    bool uniform = true, accumulate = false;
    Mat b_hist, g_hist, r_hist;
    calcHist( &bgr_planes[0], 1, 0, Mat(), b_hist, 1, &histSize, histRange, uniform, accumulate );
    calcHist( &bgr_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, histRange, uniform, accumulate );
    calcHist( &bgr_planes[2], 1, 0, Mat(), r_hist, 1, &histSize, histRange, uniform, accumulate );
    int hist_w = 512, hist_h = 400;
    int bin_w = cvRound( (double) hist_w/histSize );
    Mat histImage( hist_h, hist_w, CV_8UC3, Scalar( 0,0,0) );
    normalize(b_hist, b_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
    normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
    normalize(r_hist, r_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(b_hist.at<float>(i-1)) ),
    Point( bin_w*(i), hist_h - cvRound(b_hist.at<float>(i)) ),
    Scalar( 255, 0, 0), 2, 8, 0 );
    line( histImage, Point( bin_w*(i-1), hist_h - cvRound(g_hist.at<float>(i-1)) ),
    Point(bin_w*(i), hist_h - cvRound(g_hist.at<float>(i)) ),
    Scalar( 0, 255, 0), 2, 8, 0 );
    line(histImage, Point(bin_w*(i-1), hist_h - cvRound(r_hist.at<float>(i-1)) ),
    Point(bin_w*(i), hist_h - cvRound(r_hist.at<float>(i)) ),
    Scalar( 0, 0, 255), 2, 8, 0 );
    }
    imshow("Source image", src );
    imshow("calcHist Demo", histImage );
    return EXIT_SUCCESS;
    }
    用于命令行解析。
    **定义:** utility.hpp:890
    n维密集数组类
    **定义:** mat.hpp:829
    _Tp & at(int i0=0)
    返回对指定数组元素的引用。
    bool empty() const
    如果数组没有元素,则返回true。
    std::string String
    **定义:** cvstd.hpp:151
    #define CV_8UC3
    **定义:** interface.h:90
    int cvRound(double value)
    将浮点数四舍五入到最接近的整数。
    **定义:** fast_math.hpp:200
    void imshow(const String &winname, InputArray mat)
    在指定的窗口中显示图像。
    int waitKey(int delay=0)
    等待按键按下。
    int main(int argc, char *argv[])
    **定义:** highgui_qt.cpp:3
    **定义:** core.hpp:107
    STL 命名空间。

说明

  • 加载源图像

    CommandLineParser parser( argc, argv, "{@input | lena.jpg | input image}" );
    Mat src = imread( samples::findFile( parser.get<String>( "@input" ) ), IMREAD_COLOR );
    if( src.empty() )
    {
    return EXIT_FAILURE;
    }
  • 将源图像分离成三个R、G和B平面。为此,我们使用OpenCV函数cv::split

    vector<Mat> bgr_planes;
    split( src, bgr_planes );

    我们的输入是要分割的图像(本例中包含三个通道),输出是Mat的向量。

  • 现在我们准备开始为每个平面配置**直方图**。由于我们使用的是B、G和R平面,因此我们知道我们的值将在区间[0,255]内。
  • 确定bin的数量(5、10……)

    int histSize = 256;
  • 设置值的范围(如前所述,在0到255之间)

    float range[] = { 0, 256 }; //the upper boundary is exclusive
    const float* histRange[] = { range };
  • 我们希望我们的bin大小相同(uniform),并在开始时清除直方图,因此

    bool uniform = true, accumulate = false;
  • 我们使用OpenCV函数cv::calcHist来计算直方图

    Mat b_hist, g_hist, r_hist;
    calcHist( &bgr_planes[0], 1, 0, Mat(), b_hist, 1, &histSize, histRange, uniform, accumulate );
    calcHist( &bgr_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, histRange, uniform, accumulate );
    calcHist( &bgr_planes[2], 1, 0, Mat(), r_hist, 1, &histSize, histRange, uniform, accumulate );
  • 参数为(**C++代码**)
    • **&bgr_planes[0]:** 源数组
    • **1:** 源数组的数量(在本例中我们使用1。我们也可以在这里输入数组列表)
    • **0:** 要测量的通道(_dim_)。在本例中,它只是强度(每个数组都是单通道),因此我们只写0。
    • **Mat():** 要用于源数组的掩码(零表示要忽略的像素)。如果未定义,则不使用。
    • **b_hist:** 将存储直方图的Mat对象
    • **1:** 直方图维度。
    • **histSize:** 每个维度使用的bin的数量
    • **histRange:** 每个维度要测量的值范围
    • **uniform** 和 **accumulate:** bin大小相同,并且直方图在开始时被清除。
  • 创建图像以显示直方图

    int hist_w = 512, hist_h = 400;
    int bin_w = cvRound( (double) hist_w/histSize );
    Mat histImage( hist_h, hist_w, CV_8UC3, Scalar( 0,0,0) );
  • 请注意,在绘制之前,我们首先cv::normalize直方图,以便其值落在输入参数指示的范围内。

    normalize(b_hist, b_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
    normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
    normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1, Mat() );
  • 此函数接收以下参数(**C++代码**)
    • **b_hist:** 输入数组
    • **b_hist:** 输出归一化数组(可以相同)
    • **0** 和 **histImage.rows:** 对于此示例,它们是将**r_hist**的值归一化到的下限和上限。
    • **NORM_MINMAX:** 指示归一化类型的参数(如上所述,它将值调整到之前设置的两个限制之间)
    • **-1:** 表示输出归一化数组将与输入类型相同。
    • **Mat():** 可选掩码
  • 观察访问bin的方法(在本例的一维直方图中)

    for( int i = 1; i < histSize; i++ )
    {
    line(histImage, Point(bin_w*(i-1), hist_h - cvRound(b_hist.at<float>(i-1))),
    Point( bin_w*(i), hist_h - cvRound(b_hist.at<float>(i)) ),
    Scalar( 255, 0, 0), 2, 8, 0 );
    line(histImage, Point(bin_w*(i-1), hist_h - cvRound(g_hist.at<float>(i-1))),
    Point(bin_w*(i), hist_h - cvRound(g_hist.at<float>(i)) ),
    Scalar( 0, 255, 0), 2, 8, 0 );
    line(histImage, Point(bin_w*(i-1), hist_h - cvRound(r_hist.at<float>(i-1))),
    Point(bin_w*(i), hist_h - cvRound(r_hist.at<float>(i)) ),
    Scalar( 0, 0, 255), 2, 8, 0 );
    }

    我们使用表达式(**C++代码**)

    b_hist.at<float>(i)

    其中i表示维度。如果是二维直方图,我们会使用类似这样的表达式:

    b_hist.at<float>(i, j)
  • 最后,我们显示直方图并等待用户退出。

    imshow("Source image", src );
    imshow("calcHist Demo", histImage );

结果

  1. 使用如下所示的图像作为输入参数
  1. 会生成以下直方图