OpenCV之图像直方图计算

注:本文使用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个通道。我们可以指定要参与计算的通道的索引。通道索引的规则如下:第一个图像的通道的索引为从0images[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设置为0NULL,通过查阅源代码,得知这样做的结果相当于只计算每个图像的第一个通道。不过在官方文档中并未作此说明,所以不提倡这样用。

  • mask,可选的掩模矩阵。如果掩模矩阵是空的,则不启用掩模操作;如果掩模矩阵不为空,则必须是与源图像大小相同的8位矩阵,非零值代表对应像素参与计算。
  • hist,输出的dims维直方图。
  • dims,直方图的维数,必须为正数且小于等于CV_MAX_DIMS(当前版本为32)。我们经常计算的是一维直方图,此时传入1即可。
  • histSize,直方图每一维度大小的数组的指针。每一维度的大小也就是bin的大小。
  • ranges,直方图每一维度取值范围的数组的指针。当uniformtrue时,ranges[i]是一个包含两个元素的数组,第一个元素指定了第i维的第0 bin的取值下界(含),第二个元素指定了第i维的第histSize[i] - 1 bin的取值上界(不含),histSize[i]bin均匀分割此取值范围。当uniformfalse时,每一个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通道的直方图,可以参考如下链接:直方图计算,不再赘述。

参考链接

  1. 官方注释
  2. 官方文档:直方图计算

(转载请保留作者信息)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值