OpenCV计算图像直方图

1,382 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情

前言

一张图像由若干像素组成,每个像素如果包含一个值(一个通道),则可以组成一张灰度图像;或者如果每个像素包含三个值(三个通道),则可以组成一张彩色图像。每个通道的取值范围为 0 到 255。根据图像的内容,每个灰度值具有不同数量。

计算图像直方图

图像直方图是一种反映图像色调分布的直方图,其绘制每个色调值的像素数,每个色调值的像素数也称为频率 (frequency)。因此,灰度图像的直方图有 256 个条目(柱条或 bin)。 bin 0 表示值为 0 的像素数,bin 1 表示值为 1 的像素数,依此类推。显然,如果对直方图的所有 bin 求和,可以得到图像中的像素总数。直方图也可以归一化,使得 bin 的总和等于 1,在这种情况下,每个 bin 都表示图像中具有此色调值的像素数所占百分比。

使用 cv::calcHist() 函数可以方便的计算图像直方图。这是一个通用函数,可以计算任何像素值类型和范围的多通道图像的直方图。

本节中,我们通过为单通道灰度图像的情况创建一个直方图类来使其更易于使用。对于其他类型的图像,可以直接使用 cv::calcHist() 函数。

1. 首先,创建一个直方图类 Histogram1D

// 创建灰度图像直方图
class Histogram1D {
    private:
        int histSize[1];    // 直方图中 bin 的数量
        float hranges[2];   // 值的范围
        const float* ranges[1];     // 指向不同值范围的指针
        int channels[1];            // 通道数量
    public:
        Histogram1D() {
            // 默认参数
            histSize[0] = 256;      // 256 bins
            hranges[0] = 0.0;       // 从 0 开始
            hranges[1] = 256.0;     // 到 256 结束
            ranges[0] = hranges;
            channels[0] = 0;        // 使用通道 0
        }

2. 使用定义的成员变量,可以使用以下方法完成灰度直方图的计算,该方法在 Histogram1D 类中实现:

// 计算 1D 直方图
cv:: Mat getHistogram(const cv::Mat& image) {
    cv::Mat hist;
    cv::calcHist(&image,
                1,              // 仅使用1张图像计算直方图
                channels,       // 所用通道
                cv::Mat(),      // 不使用掩码
                hist,           // 直方图
                1,              // 1D 直方图
                histSize,       // bins 的数量
                ranges);        // 像素范围
    return hist;
}

3. 打开一个图像,创建一个 Histogram1D 实例,并调用 getHistogram 方法:

// 读取输入图像
cv::Mat image = cv::imread("1.png", 0);
Histogram1D h;
// 计算直方图
cv::Mat histo = h.getHistogram(image);

4. 这里的 histo 对象是一个简单的一维数组,有 256 个条目。因此,可以通过简单地循环遍历此数组来读取每个 bin

for (int i=0; i<256; i++) {
    cout << "Value" << i << " = " << histo.at<float>(i) << endl;
}

执行以上程序,某些像素值的像素数输出如下:

...

Value114 = 11
Value115 = 8
Value116 = 9
Value117 = 13
Value118 = 14
Value119 = 16
Value120 = 21
...

显然很难从这个值序列中提取任何直观的含义。因此,将直方图进行可视化有利于直观观察图像像素值分布。

5. 编写 getHistogramImage 方法来可视化直方图:

// 计算 1D 直方图并返回其图像
cv::Mat getHistogramImage(const cv::Mat& image, int zoom=1) {
    cv::Mat hist = getHistogram(image);
    return Histogram1D::getImageOfHistogram(hist, zoom);
}
// 不创建用于表示图像的直方图
static cv::Mat getImageOfHistogram(const cv::Mat& hist, int zoom) {
    double maxVal = 0;
    double minVal = 0;
    cv::minMaxLoc(hist, &minVal, &maxVal, 0, 0);
    int histSize = hist.rows;
    // 显示直方图
    cv::Mat histImg(histSize*zoom, histSize*zoom, CV_8U, cv::Scalar(255));
    // 设定图像高度
    int hpt = static_cast<int>(0.9*histSize);
    // 绘制每个 bin
    for (int h=0; h<histSize; h++) {
        float binVal = hist.at<float>(h);
        if (binVal>0) {
            int intensity = static_cast<int>(binVal*hpt/maxVal);
            cv::line(histImg, cv::Point(h*zoom, histSize*zoom),
                    cv::Point(h*zoom, (histSize-intensity)*zoom),
                    cv::Scalar(0), zoom);
        }
    }
    return histImg;
}

6. 使用 getImageOfHistogram 方法,可以以条形图的形式获取直方图的图像:

// 显示直方图
cv::namedWindow("Histogram");
cv::imshow("Histogram", h.getHistogramImage(image));

程序执行结果如下图所示:

直方图