首页
/ OpenCV 直方图入门教程:用 cv.calcHist、np.histogram 与 Matplotlib 寻找、绘制和分析图像直方图

OpenCV 直方图入门教程:用 cv.calcHist、np.histogram 与 Matplotlib 寻找、绘制和分析图像直方图

2026-09-06 13:41:01作者:霍妲思

本文基于 OpenCV 官方 Python 教程中的 "Histograms - 1: Find, Plot, Analyze" 一文整理并深入扩充。直方图是理解图像亮度分布、对比度与色调构成最基础也最实用的工具。读完本篇,你将掌握:用 cv.calcHist() 和 Numpy(np.histogram()np.bincount())计算直方图、理解 histSize(BINS)、dims(DIMS)、ranges(RANGE)三个术语、用 Matplotlib 与 OpenCV 两种方式绘制直方图、以及通过 mask 只对图像局部区域统计直方图,并了解 calcHist 在 C++ 源码层面的完整参数语义。

直方图示例:图像与其灰度直方图

直方图是什么:强度分布的另一种"读图"方式

直方图(Histogram)是一种描述图像强度分布的统计图:横轴(X 轴)是像素值(通常为 0 到 255,并非总是如此),纵轴(Y 轴)是图像中对应像素值的像素数量。它是理解图像的另一重方式——通过观察直方图,你可以对图像的对比度、亮度、强度分布形成直觉判断。如今几乎所有图像处理工具都提供直方图功能。

从上方的示例图中可以看到一幅灰度图及其直方图(注意:该直方图是针对灰度图像绘制的,不是彩色图像)。直方图的左侧区域表示图中暗像素的数量,右侧区域表示亮像素的数量。从图中可以读出:暗区像素多于亮区像素,而中间调(midtone,即像素值处于中间范围、比如 127 附近)的像素数量很少。

计算直方图前的三个术语:BINS、DIMS 与 RANGE

在调用 OpenCV 或 Numpy 的内置函数之前,需要先理解直方图的三个术语:

  • BINS(柱数):最基本的直方图会给出 0 到 255 每个像素值对应的像素数量,即用 256 个数值表示整个直方图。但很多时候我们不需要逐个像素值统计,而是想统计某个像素值区间内的像素数量,例如统计 0~15、16~31、…、240~255 每个区间里的像素个数,此时只需要 16 个数值就能表示直方图。做法是把整个直方图切分为 16 个子区间,每个子区间的值等于其中所有像素计数之和。每个这样的子区间就称为一个 BIN。在前一种情况下 bin 数为 256(每个像素值一个),后一种情况下只有 16。BINS 在 OpenCV 文档中对应术语 histSize
  • DIMS(维度):即我们收集数据的参数个数。这里只针对强度值这一个量收集数据,因此 DIMS 为 1。
  • RANGE(范围):要测量的强度值范围,通常是 [0, 256],即全部强度值。

方法一:使用 cv.calcHist() 计算直方图

函数签名如下:

cv.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]])

各参数说明(完整继承自原文档,并结合 C++ 头文件补充):

  • images:源图像,类型必须是 uint8 或 float32,且必须以方括号包裹传入,例如 [img]
  • channels:同样以方括号给出。它指定要计算直方图的通道索引。若输入是灰度图,传 [0];对彩色图,可以分别传 [0][1][2] 来计算蓝(B)、绿(G)、红(R)通道的直方图。
  • mask:掩码图像。若要统计整幅图像的直方图,传 None;若只想统计图像的某个区域,需要为该区域创建掩码图像并作为 mask 传入(下文有完整示例)。
  • histSize:表示 BIN 的数量,需以方括号传入。全尺度统计时传 [256]
  • ranges:即 RANGE,通常为 [0, 256]。
  • accumulate(可选):累加标志。置位时,直方图在分配时不会被清零,从而可以对多组数组累加出同一个直方图,或随时间动态更新直方图。

最小可运行示例:以灰度模式加载图像,计算完整直方图:

img = cv.imread('home.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
hist = cv.calcHist([img], [0], None, [256], [0, 256])

返回的 hist 是一个 256×1 的数组,每个值对应图像中具有该像素值的像素数量。

源码纵深:C++ 侧 calcHist 的完整语义

Python 绑定的 cv.calcHist 对应 C++ 的 cv::calcHist,其声明与权威文档位于 modules/imgproc/include/opencv2/imgproc.hpp,头文件注释给出了比 Python 教程更完整的约束,值得补充:

  • 源数组(images)应具有相同的深度(CV_8UCV_16UCV_32F)和相同的尺寸,通道数可以任意;
  • dims(维度) 必须为正,且不超过 CV_MAX_DIMS(当前 OpenCV 版本中为 32)。多维直方图(如 2D 的 Hue-Saturation 直方图)正是通过 dims 与多组 ranges 组合实现的;
  • mask 若非空,必须是与 images 同尺寸的 8 位数组,非零元素标记参与统计的像素;
  • ranges:当直方图是均匀(uniform=true,即 Python 接口的默认情况)时,每个维度只需指定 0 号 bin 的下边界(含)与最后一个 bin 的上边界(不含),即每维 2 个元素的数组;当 non-uniform 时,ranges[i] 包含 histSize[i]+1 个元素,依次是各 bin 的边界。落在 [L0, U_last) 之外的像素值不会计入直方图——这解释了为什么灰度统计要写 [0, 256] 而不是 [0, 255];
  • 头文件注释还引用了官方 C++ 示例 samples/cpp/demhist.cpp,展示了创建图像直方图(含 2D 色相-饱和度直方图)的完整做法。原文档提到的 Python 版绘制示例 samples/python/hist.py 在当前仓库版本中已不再存在,如需参考绘制代码可直接查看该 C++ 示例。

需要说明:Python 侧 cv.calcHistranges 参数被简化为"扁平的 histSize.size()×2 个元素",即每对元素指定下、上边界,因此 cv.calcHist 的 Python 接口只支持均匀直方图(见 C++ 重载注释 "this variant supports only uniform histograms",位于 imgproc.hpp)。

方法二:使用 Numpy 计算直方图

Numpy 提供 np.histogram() 函数,可以替代 calcHist()

hist, bins = np.histogram(img.ravel(), 256, [0, 256])

hist 与之前用 cv.calcHist 算出的一致。但注意 bins 会有 257 个元素,因为 Numpy 把 bin 理解为 0-0.99、1-1.99、2-2.99……这样的半开区间,最终区间是 255-255.99,为了表示它会在 bins 末尾额外加上 256。实际使用时 255 就足够了,那个 256 并不需要。

原文档还给出两条重要的性能提示:

  • Numpy 另有一个函数 np.bincount(),比 np.histogram() 快大约 10 倍。对一维直方图,优先使用它,但不要忘记设置 minlength=256
hist = np.bincount(img.ravel(), minlength=256)
  • OpenCV 函数比 np.histogram() 快大约 40 倍,因此在 Python 中做直方图统计应坚持使用 cv.calcHist()

绘制直方图:Matplotlib 短路径与 OpenCV 长路径

绘制直方图有两种方式:

  1. 短路径(Short Way):使用 Matplotlib 的绑图函数;
  2. 长路径(Long Way):使用 OpenCV 的绘图函数。

方式一:使用 Matplotlib

Matplotlib 自带直方图绑图函数 matplotlib.pyplot.hist(),它会直接计算并绘制直方图,无需先调用 calcHist()np.histogram()

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

img = cv.imread('home.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
plt.hist(img.ravel(), 256, [0, 256]); plt.show()

输出效果如下:

Matplotlib 绘制的灰度直方图

也可以使用 Matplotlib 的普通 plot 函数,这对 BGR 三通道绑图更合适。此时需要先求出各通道的直方图数据:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

img = cv.imread('home.jpg')
assert img is not None, "file could not be read, check with os.path.exists()"
color = ('b', 'g', 'r')
for i, col in enumerate(color):
    histr = cv.calcHist([img], [i], None, [256], [0, 256])
    plt.plot(histr, color=col)
    plt.xlim([0, 256])
plt.show()

结果如下:

RGB 三通道直方图

从上图可以推断:图像中蓝色通道在高端存在较高数值的区域(这显然是天空造成的)。

方式二:使用 OpenCV 绘图函数

思路是:把直方图各柱的数值连同 bin 值一起换算成 x, y 坐标,然后用 cv.line()cv.polyline() 绘制出与 Matplotlib 相同效果的图形。原文档指出该实现已包含在 OpenCV-Python 官方示例中(路径 samples/python/hist.py);如前所述,该文件在当前仓库版本中已移除,可参考 samples/python 目录下的其他官方脚本(例如 samples/python/common.py 提供了窗口与命令行参数解析等公共工具)或 C++ 示例 samples/cpp/demhist.cpp 来理解其绘制逻辑。

进阶:应用 mask 对局部区域统计直方图

前面我们用 cv.calcHist() 计算了整幅图像的直方图。如果想计算图像某些区域的直方图怎么办?方法是:创建一个掩码图像,将想统计的区域涂白(255),其余部分涂黑(0),然后把它作为 mask 传入。

完整示例(逐行注释):

img = cv.imread('home.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"

# create a mask:创建与图像同尺寸的 0 掩码
mask = np.zeros(img.shape[:2], np.uint8)
# 将 (100,100)-(300,400) 矩形区域置为白色(255)
mask[100:300, 100:400] = 255
# 用掩码提取该区域,仅用于可视化
masked_img = cv.bitwise_and(img, img, mask=mask)

# Calculate histogram with mask and without mask
# 注意第三个参数:mask 位置
hist_full = cv.calcHist([img], [0], None, [256], [0, 256])  # 全图直方图
hist_mask = cv.calcHist([img], [0], mask, [256], [0, 256])  # 仅掩码区域

plt.subplot(221), plt.imshow(img, 'gray')       # 原图
plt.subplot(222), plt.imshow(mask, 'gray')     # 掩码
plt.subplot(223), plt.imshow(masked_img, 'gray')# 掩码提取出的区域
plt.subplot(224), plt.plot(hist_full), plt.plot(hist_mask)
plt.xlim([0, 256])

plt.show()

运行结果如下:直方图绑线中,蓝线表示整幅图像的直方图,绿线表示掩码区域的直方图:

Mask 局部区域直方图效果

从源码侧印证:C++ 头文件中对 mask 的定义是 "If the matrix is not empty, it must be an 8-bit array of the same size as images[i]. The non-zero mask elements mark the array elements counted in the histogram"(见 imgproc.hpp)——即掩码只需是 8 位同尺寸数组,任何非零值都会被计入统计,并非严格限定为 255,这给了掩码构造一定的自由度。

参数速查表

参数 含义 常用取值 备注
images 源图像(uint8/float32) [img] 必须以列表传入;C++ 侧要求同深度同尺寸
channels 通道索引 [0][0,1,2] 灰度图传 [0];彩色图 0/1/2 对应 B/G/R
mask 区域掩码 None 或 8 位同尺寸掩码 非零元素处参与统计
histSize BIN 数量 [256] 对应 BINS,即直方图分箱数
ranges 强度范围 [0, 256] 上界不含;区间外像素不计入
accumulate 累加标志 默认 False 置位则不清零,支持增量统计

小结与进一步学习

本篇覆盖了直方图入门的完整链路:从概念(强度分布图)、术语(BINS/DIMS/RANGE),到 cv.calcHist()np.histogram()np.bincount() 三种计算途径的对比与性能取舍,再到 Matplotlib 与 OpenCV 两种绘图路线和 mask 局部统计。直方图是后续高级操作的基石——例如直方图均衡化(cv.equalizeHist())、2D 直方图(Hue-Saturation 空间)与直方图反向投影(cv.calcBackProject,它是 CamShift 颜色追踪算法的核心步骤,相关说明见 imgproc.hpp)。

登录后查看全文
热门项目推荐
相关项目推荐