OpenCV 直方图入门教程:用 cv.calcHist、np.histogram 与 Matplotlib 寻找、绘制和分析图像直方图
本文基于 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() 计算直方图
函数签名如下:
各参数说明(完整继承自原文档,并结合 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_8U、CV_16U或CV_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.calcHist 的 ranges 参数被简化为"扁平的 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 长路径
绘制直方图有两种方式:
- 短路径(Short Way):使用 Matplotlib 的绑图函数;
- 长路径(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 的普通 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()
结果如下:
从上图可以推断:图像中蓝色通道在高端存在较高数值的区域(这显然是天空造成的)。
方式二:使用 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()
运行结果如下:直方图绑线中,蓝线表示整幅图像的直方图,绿线表示掩码区域的直方图:
从源码侧印证: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)。
- 本文对应仓库文档源文件:doc/py_tutorials/py_imgproc/py_histograms/py_histogram_begins/py_histogram_begins.markdown
- 直方图系列教程目录:doc/py_tutorials/py_imgproc/py_histograms
calcHist/calcBackProject官方 C++ 声明与文档:modules/imgproc/include/opencv2/imgproc.hpp- 官方 C++ 直方图示例:samples/cpp/demhist.cpp
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00



