OpenCV入门(20):图像处理之直方图

在图像处理中,直方图是一种非常重要的工具,它可以帮助我们了解图像的像素分布情况。通过分析图像的直方图,我们可以进行图像增强、对比度调整、图像分割等操作。

一、什么是图像直方图?

图像直方图是图像像素强度分布的图形表示,对于灰度图像,直方图显示了每个灰度级(0 到 255)在图像中出现的频率,对于彩色图像,我们可以分别计算每个通道(如 R、G、B)的直方图。

  • 直方图: 表示图像中像素强度的分布情况,横轴表示像素强度值,纵轴表示该强度值的像素数量。
  • 灰度直方图: 针对灰度图像的直方图,表示每个灰度级的像素数量。
  • 颜色直方图: 针对彩色图像的直方图,分别表示每个颜色通道(如 BGR)的像素强度分布。

简单来说,直方图是一个统计图表,它显示了图像中每个像素强度值(比如亮度值0-255)出现的频率(即像素个数)。

  • 横坐标 (X轴):通常表示像素的强度值(例如,对于 8 位灰度图,就是 0 到 255)。
  • 纵坐标 (Y轴):表示具有该强度值的像素数量。

下面是一个一个典型的灰度图像及其直方图:

Blog_OpenCV_Learnl_103.png


二、直方图有什么用?

直方图非常有用,它可以告诉我们很多关于图像的信息:

  • 图像亮度与对比度
    • 如果直方图的峰值主要集中在左边(低亮度值),说明图像偏暗。
    • 如果峰值集中在右边(高亮度值),说明图像偏亮。
    • 如果峰值分布很窄,说明图像对比度较低。
    • 如果峰值分布很广,说明图像对比度较高。
  • 图像内容分析:通过直方图的形状,可以大致推断图像的内容。例如,一张大部分是天空的图片,其直方图的蓝色通道可能会在某个区域有高峰。
  • 图像阈值化:直方图可以帮助我们选择合适的阈值,用于将图像分割成前景和背景(例如,大津法就是基于直方图的)。
  • 图像均衡化:通过拉伸直方图,使其分布更均匀,可以增强图像的对比度,这就是“直方图均衡化”。
  • 图像匹配与检索:比较不同图像的直方图,可以作为一种简单的图像相似性度量方法。

OpenCV 提供了丰富的直方图计算和操作函数:

功能 函数 说明
计算直方图 cv::calcHist() 计算图像的直方图。
直方图均衡化 cv::equalizeHist() 增强图像的对比度。
直方图比较 cv::compareHist() 比较两个直方图的相似度。
绘制直方图 matplotlib.pyplot.plot() 使用 Matplotlib 绘制直方图。

三、计算直方图

3.1 API

OpenCV提供了一个非常方便的函数 cv::calcHist 来计算直方图。让我们看看它的基本用法。

void calcHist( InputArrayOfArrays images,
              const std::vector<int>& channels,
              InputArray mask, OutputArray hist,
              const std::vector<int>& histSize,
              const std::vector<float>& ranges,
              bool accumulate = false );

参数说明:

  • images:输入的图像列表,通常是一个包含单通道或多通道图像的列表。例如 [img]
  • channels:需要计算直方图的通道索引。对于灰度图像,使用 [0];对于彩色图像,可以使用 [0][1][2] 分别计算蓝色、绿色和红色通道的直方图。
  • mask:掩码图像。如果指定了掩码,则只计算掩码区域内的像素。如果不需要掩码,可以传入 None
  • hist:输出的直方图数组。
  • histSize:直方图的 bin 数量。对于灰度图像,通常设置为 [256],表示将灰度级分为 256 个 bin。
  • ranges:像素值的范围。对于灰度图像,通常设置为 [0, 256],表示像素值的范围是 0 到 255。
  • accumulate:是否累积直方图。如果设置为 True,则直方图不会被清零,而是在每次调用时累积。

3.2 示例:灰度图直方图

计算一张灰度图像的直方图:

#include<iostream>
#include<opencv2\opencv.hpp>

using namespace cv;
using namespace std;

int main() {
    // 1. 加载图像
    Mat src = imread("lena.jpg", IMREAD_GRAYSCALE); // 以灰度模式加载
    if (src.empty()) {
        cout << "无法加载图像!" << endl;
        return -1;
    }

    // 2. 设置直方图参数
    Mat hist; // 用于存储直方图结果
    int histSize[] = { 256 }; // Bin的数量 (0-255,共256个)

    // 像素值范围 (对于8位图像是0-255)
    // 注意:上限是不包含的,所以是256
    float hranges[] = { 0.0f, 256.0f };
    const float* ranges[] = { hranges };

    int channels[] = { 0 }; // 我们只处理灰度图的第0个通道

    // 3. 计算直方图
    calcHist(&src,      // 输入图像 (注意是地址)
             1,          // 图像数量
             channels,   // 通道列表
             Mat(),      // 无掩码
             hist,       // 输出直方图
             1,          // 直方图维度 (1D)
             histSize,   // 每个维度的bin数量
             ranges      // 每个维度的取值范围
    );

    // (可选) 打印一些直方图的值(hist现在是一个256x1的Mat)
    // for (int i = 0; i < histSize[0]; i++) {
    //     cout << "灰度值 " << i << ": " << hist.at<float>(i, 0) << " 个像素" << endl;
    // }

    // 4. (重要!) 可视化直方图
    // `calcHist` 计算出来的是数值,我们还需要把它画出来才能直观看到。
    int hist_w = 512; // 直方图图像的宽度
    int hist_h = 400; // 直方图图像的高度
    int bin_w = cvRound((double)hist_w / histSize[0]); // 每个bin在图像中的宽度

    Mat histImage(hist_h, hist_w, CV_8UC3, Scalar(20, 20, 20)); // 创建一个黑色背景的图像用于绘制

    // 归一化直方图的值到 [0, hist_h] 区间,这样才能画在图上
    // `normalize` 函数会找到hist中的最大值,然后按比例缩放所有值
    normalize(hist, hist, 0, histImage.rows, NORM_MINMAX, -1, Mat());

    // 绘制直方图的每个bin
    for (int i = 1; i < histSize[0]; i++) {
        line(histImage,
             Point(bin_w * (i - 1), hist_h - cvRound(hist.at<float>(i - 1))), // 上一个点
             Point(bin_w * (i), hist_h - cvRound(hist.at<float>(i))),     // 当前点
             Scalar(200, 200, 200), // 线条颜色 (浅灰色)
             2, 8, 0);
    }

    // 5. 显示原图和直方图
    imshow("Src Image", src);
    imshow("Histogram Image", histImage);

    waitKey(0); // 等待按键
    return 0;
}

效果图如下所示:

Blog_OpenCV_Learnl_104.png


3.3 示例:彩色图直方图 (分别计算B, G, R通道)

对于彩色图(通常是BGR顺序),我们可以为每个颜色通道分别计算直方图:

#include<iostream>
#include<opencv2\opencv.hpp>

using namespace cv;
using namespace std;

int main() {
    // 1. 加载彩色图像
    Mat src = imread("lena.jpg", IMREAD_COLOR);
    if (src.empty()) {
        cout << "无法加载图像!" << endl;
        return -1;
    }

    // 2. 将图像分割成B, G, R三个通道
    vector<Mat> bgr_planes;
    split(src, bgr_planes); // bgr_planes[0] 是 B, bgr_planes[1] 是 G, bgr_planes[2] 是 R

    // 3. 设置直方图参数 (与灰度图类似)
    int histSize[] = { 256 };
    float range[] = { 0, 256 };
    const float* histRange[] = { range };
    bool uniform = true;
    bool accumulate = false;

    // 4. 分别计算B, G, R三个通道的直方图
    Mat b_hist, g_hist, r_hist;

    // 计算B通道直方图
    // 注意:split后的bgr_planes[0]已经是单通道图像,所以channels参数是{0}
    // 如果直接用src计算,那么channels可以是{0}代表B, {1}代表G, {2}代表R
    int b_channels[] = {0};
    calcHist(&bgr_planes[0], 1, b_channels, Mat(), b_hist, 1, histSize, histRange, uniform, accumulate);

    // 计算G通道直方图
    int g_channels[] = {0}; // 对于bgr_planes[1] (G通道图像) 来说,它自己的通道索引是0
    calcHist(&bgr_planes[1], 1, g_channels, Mat(), g_hist, 1, histSize, histRange, uniform, accumulate);

    // 计算R通道直方图
    int r_channels[] = {0}; // 对于bgr_planes[2] (R通道图像) 来说,它自己的通道索引是0
    calcHist(&bgr_planes[2], 1, r_channels, Mat(), r_hist, 1, histSize, histRange, uniform, accumulate);

    // 5. 绘制直方图 (与灰度图类似,但要画三条线)
    int hist_w = 512;
    int hist_h = 400;
    int bin_w = cvRound((double)hist_w / histSize[0]);

    Mat histImage(hist_h, hist_w, CV_8UC3, Scalar(20, 20, 20));

    // 归一化B, G, R直方图到 [0, histImage.rows]
    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[0]; 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); // 红色
    }

    // 6. 显示
    imshow("Src Image", src);
    imshow("Histogram Image (B, G, R)", histImage);

    waitKey(0);
    return 0;
}

在同一张 histImage 上用不同颜色(蓝、绿、红)绘制三个通道的直方图线条。效果图如下所示:

Blog_OpenCV_Learnl_105.png


四、二维直方图

二维直方图用于描述图像中两个通道之间的联合分布。例如,对于彩色图像中的 Hue(色调)和 Saturation(饱和度),我们可以构建一个二维直方图来反映颜色的联合统计信息,这在颜色分割、目标检测等任务中非常有用。


4.1 API

同样使用 cv::calcHist 函数,但需要同时传入两个通道的数据,并设置维数为 2

void calcHist( InputArrayOfArrays images,
              const std::vector<int>& channels,
              InputArray mask, OutputArray hist,
              const std::vector<int>& histSize,
              const std::vector<float>& ranges,
              bool accumulate = false );

4.2 示例代码

计算 HSV 图像中 Hue 和 Saturation 的二维直方图:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main() {
    // 读取彩色图像并转换到 HSV 空间
    Mat img = imread("lena.jpg");
    if (img.empty()) {
        cout << "加载图像失败!" << endl;
        return -1;
    }
    Mat hsv;
    cvtColor(img, hsv, COLOR_BGR2HSV);

    // 设置二维直方图参数
    int hBins = 50, sBins = 60;
    int histSize[2] = { hBins, sBins };
    // H 范围:0~180,S 范围:0~256
    float hRanges[] = { 0, 180 };
    float sRanges[] = { 0, 256 };
    const float* ranges[] = { hRanges, sRanges };
    int channels[] = { 0, 1 }; // 使用 H 和 S 通道

    // 计算二维直方图
    Mat hist;
    calcHist(&hsv, 1, channels, Mat(), hist, 2, histSize, ranges, true, false);

    // 归一化直方图到 [0, 255]
    normalize(hist, hist, 0, 255, NORM_MINMAX, -1, Mat());

    // 将二维直方图显示为图像
    int scale = 4;  // 调整显示图像的尺寸因子
    Mat histImg = Mat::zeros(sBins * scale, hBins * scale, CV_8UC3);

    // 注意:hist 的维度为 [hBins x sBins]
    // 这里遍历 hBins 和 sBins,绘制每个 bin 的强度
    for (int h = 0; h < hBins; h++) {
        for (int s = 0; s < sBins; s++) {
            float binVal = hist.at<float>(h, s);
            int intensity = cvRound(binVal);
            // 使用颜色填充直方图的每个小矩形区域
            rectangle(histImg,
                Point(h * scale, s * scale),
                Point((h + 1) * scale - 1, (s + 1) * scale - 1),
                Scalar(intensity, intensity, intensity),
                FILLED);
        }
    }
    // 转为彩色热力图
    applyColorMap(histImg, histImg, COLORMAP_JET);
    imshow("Src Image", img);
    imshow("Histogram Image (B, G, R)", histImg);

    waitKey(0);
    return 0;
}

此示例展示了如何计算 HSV 图像中 Hue 和 Saturation 的二维直方图,并将统计结果绘制为一幅热图,直观反映颜色分布情况。效果图如下所示:

Blog_OpenCV_Learnl_109.png


五、直方图均衡化

直方图均衡化是一种图像增强技术,其目标是改善图像对比度。通过重新分配像素的灰度值,使得直方图变得更均匀,达到突出细节和边缘的效果。该方法常用于灰度图像,但对于彩色图像,通常先将图像转换到 YCrCb 或 HSV 空间,仅对亮度通道进行均衡化,再转换回原空间。


5.1 API

OpenCV 提供了 cv::equalizeHist 函数,该函数针对单通道图像进行均衡化。

void equalizeHist( InputArray src, OutputArray dst );
  • src:源图像 Mat
  • src:目标图像 Mat

5.2 示例:灰度图像直方图均衡化

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main() {
    // 读取灰度图像
    Mat gray = imread("lena.jpg",IMREAD_GRAYSCALE);
    if (gray.empty()) {
        cerr << "加载图像失败!" << endl;
        return -1;
    }

    // 进行直方图均衡化
    Mat equalized;
    equalizeHist(gray, equalized);

    // 显示原图和均衡化后的图像
    imshow("Src Image", gray);
    imshow("Histogram Image", equalized);

    waitKey(0);
    return 0;
}

运行后右边图像的对比度明显增强,效果图如下所示:

Blog_OpenCV_Learnl_106.png


5.3 示例:彩色图像直方图均衡化

对于彩色图像,我们可以先转换为 YCrCb 空间,对 Y 通道进行均衡化,再转换回 BGR 空间。

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main() {
    // 读取彩色图像
    Mat img = imread("lena.jpg");
    if (img.empty()) {
        cerr << "加载图像失败!" << endl;
        return -1;
    }

    // 转换到 YCrCb 空间
    Mat img_ycrcb;
    cvtColor(img, img_ycrcb, COLOR_BGR2YCrCb);

    // 分离通道,均衡化 Y 通道
    vector<Mat> channels;
    split(img_ycrcb, channels);
    equalizeHist(channels[0], channels[0]);
    merge(channels, img_ycrcb);

    // 转换回 BGR 空间
    Mat img_equalized;
    cvtColor(img_ycrcb, img_equalized, COLOR_YCrCb2BGR);

    imshow("Src Image", img);
    imshow("Histogram Image", img_equalized);

    waitKey(0);
    return 0;
}

效果图如下所示:

Blog_OpenCV_Learnl_107.png


六、自适应直方图均衡

自适应直方图均衡(Adaptive Histogram Equalization, AHE)是一种改进的直方图均衡化方法,它不是对整幅图像进行全局均衡,而是将图像划分为多个小区域(称为子块窗口),在每个子块上分别执行直方图均衡化,以增强局部对比度。


6.1 概述

基本步骤:

  • 划分子块(Tiles): 将图像分成多个小的子区域(如 8×8 或 16×16);
  • 计算直方图均衡化: 对每个子块单独计算累积分布函数(CDF)并应用均衡化;
  • 插值平滑(Interpolation): 由于不同子块的均衡化结果可能不连续,采用双线性插值使其过渡平滑,避免块状效应;
  • 边界处理: 处理图像边缘区域,防止伪影出现。

对比普通直方图均衡化的优势:

  • 适用于局部光照不均的图像,增强细节;
  • 适用于对比度较低的区域,而不会过度增强对比度已高的区域;
  • 解决了全局均衡化可能导致的过度曝光或伪影问题。

改进方法:CLAHE(对比度受限自适应直方图均衡化)

CLAHE(Contrast Limited Adaptive Histogram Equalization)是在 AHE 基础上增加了对比度限制,防止某些局部区域对比度过高而产生噪声。其核心思想是对每个子块的直方图设定一个对比度限制阈值,若某个灰度级的频率超过阈值,则将超出的部分均匀分配到所有灰度级,最终进行均衡化。


6.2 示例代码

在 OpenCV 中,自适应直方图均衡化可通过 cv::createCLAHE() 实现,例如:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main() {
    // 读取彩色图像
    Mat img = imread("lena.jpg", IMREAD_GRAYSCALE);
    if (img.empty()) {
        cerr << "加载图像失败!" << endl;
        return -1;
    }

    // 自适应直方图均衡
    Ptr<CLAHE> clahe = createCLAHE(2.0, Size(8, 8));  // 限制对比度 2.0,窗口大小 8×8
    Mat dst;
    clahe->apply(img, dst);

    imshow("Original", img);
    imshow("CLAHE", dst);

    waitKey(0);
    return 0;
}

效果图如下所示:

Blog_OpenCV_Learnl_108.png


七、直方图的应用

  • 图像增强: 通过直方图均衡化,可以增强图像的对比度,使细节更加清晰。
  • 图像分割: 过分析直方图,可以确定阈值,用于图像分割。
  • 图像匹配: 通过比较直方图,可以判断两幅图像的相似度,用于图像匹配和检索。
  • 颜色分析: 通过颜色直方图,可以分析图像的颜色分布,用于颜色校正和风格化处理。

八、总结

本文详细介绍了 OpenCV C++ 中的直方图分析技术,包括:

  • 图像直方图:利用 cv::calcHist 计算灰度图像的直方图,并绘制统计结果。
  • 二维直方图:通过统计两个通道(如 HSV 中的 H 和 S)的联合分布,构建二维直方图热图,展示颜色分布信息。
  • 直方图均衡化:使用 cv::equalizeHist 对灰度图像进行均衡化,同时介绍了彩色图像均衡化的常用方法(基于 YCrCb 色彩空间).
  • 自适应均衡化:一种改进的直方图均衡化方法,它不是对整幅图像进行全局均衡,而是将图像划分为多个小区域(称为子块或窗口),在每个子块上分别执行直方图均衡化,以增强局部对比度

posted @ 2025-08-15 11:55  fengMisaka  阅读(8)  评论(0)    收藏  举报