OpenCV 二维直方图实战:用 cv.calcHist 与 np.histogram2d 计算并可视化 HSV 色彩直方图
本文围绕 OpenCV 教程《Histograms - 3 : 2D Histograms》展开,系统讲解如何计算和绘制二维直方图(2D Histogram)。读完本篇,你将掌握三个核心技能:用 cv.calcHist() 对 HSV 图像同时统计 Hue(色相)与 Saturation(饱和度)两个特征;用 np.histogram2d() 在纯 NumPy 环境中完成同样的统计;以及三种不同的二维直方图可视化方案——cv.imshow()、Matplotlib 的 imshow() 和带真实色彩标注的"OpenCV 示例风格"彩色直方图。这些能力是理解后续直方图反向投影(Histogram Back-Projection)等主题的基础。
为什么需要二维直方图
在前一篇教程(一维直方图)中,我们只考虑单一特征——像素的灰度强度值,因此得到一维直方图。而二维直方图同时考虑两个特征,最典型的应用是颜色直方图(color histogram):每个像素有两个特征,即 HSV 颜色空间中的 Hue(色相)与 Saturation(饱和度)值。
一维灰度直方图只能回答"图像中什么灰度等级的像素多",无法回答"图像中是什么颜色占主导"。而色彩信息天然分布在 H 与 S 两个维度上,必须用二维统计才能完整表达。例如在教程使用的示例图像 home.jpg 中,可以看到 H ≈ 100、S ≈ 200 附近有明显高值峰,对应天空的蓝色;另一处 H ≈ 25、S ≈ 100 附近的峰对应宫殿的黄色。这种"坐标位置 → 实际颜色"的对应关系,正是二维直方图的核心价值,也可以用 GIMP 等图像编辑工具对原图取色加以验证。
用 cv.calcHist 计算二维直方图
OpenCV 中计算二维直方图与一维直方图使用同一个函数 cv.calcHist(),区别仅在于参数。对于颜色直方图,需要先把图像从 BGR 转换到 HSV(一维直方图时是从 BGR 转到 Grayscale)。参数修改方式如下:
- channels = [0, 1] —— 因为需要同时处理 H 平面(HSV 的第 0 通道)和 S 平面(第 1 通道);
- bins = [180, 256] —— H 平面取 180 个 bin,S 平面取 256 个 bin;
- range = [0, 180, 0, 256] —— OpenCV 中 8 位图像 HSV 转换后 Hue 的取值范围是 0–180,Saturation 的取值范围是 0–256。
完整代码:
import numpy as np
import cv2 as cv
img = cv.imread('home.jpg')
assert img is not None, "file could not be read, check with os.path.exists()"
hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)
hist = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
几点说明:
- 第 1 个参数
[hsv]是图像列表(列表形式是为了兼容多图像统计); - 第 2 个参数
[0, 1]指定参与统计的通道索引,0是 H、1是 S; - 第 3 个参数
None表示不使用掩码,统计全图。传入掩码时只统计掩码非零区域的像素; - 第 4 个参数
[180, 256]是各维度 bin 数,第 5 个参数[0, 180, 0, 256]是对应各维度的取值范围,按维度顺序依次拼接。
返回结果 hist 是一个 180 × 256 的二维数组,hist[h, s] 表示色相落在第 h 个 bin、饱和度落在第 s 个 bin 的像素个数。
这一参数模式在仓库官方教程代码中有真实印证。官方反向投影示例 calcBackProject_Demo2.py 中,正是用同样的"双通道 + 双维度 bins + 拼接 range"写法从 HSV 图像提取颜色直方图(该示例使用更细粒度的 h_bins=30、s_bins=32 以减少计算量):
# samples/python/tutorial_code/Histograms_Matching/back_projection/calcBackProject_Demo2.py
h_bins = 30
s_bins = 32
histSize = [h_bins, s_bins]
h_range = [0, 180]
s_range = [0, 256]
ranges = h_range + s_range # Concat list
channels = [0, 1]
# Get the Histogram and normalize it
hist = cv.calcHist([hsv], channels, mask, histSize, ranges, accumulate=False)
cv.normalize(hist, hist, alpha=0, beta=255, norm_type=cv.NORM_MINMAX)
# Get Backprojection
backproj = cv.calcBackProject([hsv], channels, hist, ranges, scale=1)
从这段源码可以确认:二维 HSV 直方图是反向投影(cv.calcBackProject)的直接输入——先统计出目标区域的直方图,再用它生成反向投影图做相似区域检索。这正是本文档末尾提到的"对后续主题有用"的具体落点。
用 np.histogram2d 在 NumPy 中计算
NumPy 提供了专用函数 np.histogram2d()(对应一维时使用的 np.histogram()),可以在不依赖 OpenCV 直方图接口的前提下完成同样的统计:
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()"
hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)
hist, xbins, ybins = np.histogram2d(h.ravel(), s.ravel(), [180, 256], [[0, 180], [0, 256]])
参数含义(原文档逐一点明):
- 第 1 个参数:H 平面数据,
h.ravel()把 H 通道展开为一维数组; - 第 2 个参数:S 平面数据,
s.ravel(); - 第 3 个参数:每个维度的 bin 数
[180, 256]; - 第 4 个参数:每个维度的取值范围
[[0, 180], [0, 256]]。
函数返回三个值:hist 为统计结果(形状为 (len(xbins), len(ybins)) 的二维数组,注意 NumPy 约定行轴对应第一个输入 x),xbins、ybins 是各维度的 bin 边界数组。与 cv.calcHist 相比,NumPy 版本的直方图数值语义一致,但绘制时需要自行处理轴方向与边界;OpenCV 版本则与后续 cv.calcBackProject、cv.compareHist 等接口天然兼容,工程链路上更顺。
小提示:若用
np.split(hsv, 3, axis=2)拆出h, s, v,可避免在示例代码中手写hsv[:,:,0]、hsv[:,:,1]这类索引。
二维直方图的三种可视化方法
二维直方图的结果是 180 × 256 的矩阵,"怎么画"直接影响可读性。教程给出了三种方案,按信息量递增排列。
方法一:直接使用 cv.imshow()
结果是一个 180 × 256 的二维数组,可以直接当作灰度图像显示:
cv.imshow('2D Histogram', hist)
cv.waitKey(0)
它的局限在于显示的是灰度图,除非你熟悉不同颜色对应的 Hue 数值区间,否则看不出图中峰值代表什么颜色。优点是零依赖、适合在无 GUI 绘图库的环境(如嵌入式、服务器)快速查看直方图形态。
方法二:使用 Matplotlib(推荐)
matplotlib.pyplot.imshow() 可以配合不同的 color map 来绘制二维直方图,能更直观地反映像素密度分布。同样地,它无法直接告诉你峰值处是什么颜色(除非你知道各 Hue 值对应的色相),但教程作者明确表示更推荐这种方式——简单且效果好。
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()"
hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)
hist = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
plt.imshow(hist, interpolation='nearest')
plt.show()
上面第一张配图(2dhist_matplotlib.jpg)即为该方法的输出。使用时的注意点:插值标志应设为 interpolation='nearest',以避免双线性插值模糊 bin 边界,得到更准确的密度呈现。
该图的坐标语义需要记住:X 轴是 S 值(0–255),Y 轴是 Hue(0–179)。图中可见两类显著高密度区:H ≈ 100、S ≈ 200 附近的蓝色(天空),以及 H ≈ 25、S ≈ 100 附近的黄色(宫殿主体)。
方法三:OpenCV 示例风格——输出彩色直方图
教程提到 OpenCV-Python 示例中有一个 samples/python/color_histogram.py,运行后可看到直方图中带有真实色彩标注(即"color coded histogram")。需要说明的是:在当前仓库版本中,该文件已不再位于 samples/python/ 目录下(仓库的 Python 示例已重组为 tutorial_code/ 目录结构),但教程对其实现原理的描述依然有参考价值:
作者先创建了一张 HSV 颜色映射图(color map),再将其转换为 BGR;然后把直方图图像与该颜色映射图逐像素相乘。同时使用若干预处理步骤去除孤立的小像素点,最终得到漂亮的直方图。
这种方法的精髓可以概括为三步:
- 构造一张与直方图同尺寸(180 × 256)的"色谱图",横坐标位置 s 的像素取饱和度 s、纵坐标位置 h 的像素取色相 h,整体转换到 BGR;
- 将归一化后的直方图与该色谱图逐元素相乘,使每个 bin 位置呈现"密度 × 颜色";
- 用形态学运算(如开运算)剔除孤立的微小亮点,减少视觉噪声。
教程给出的同一输入图像的输出结果如下,可以清晰地看到蓝色、黄色的存在,以及因棋盘格产生的白色成分:
如果你想自己复现这套效果,核心骨架大致是:
import numpy as np
import cv2 as cv
img = cv.imread('home.jpg')
assert img is not None, "file could not be read, check with os.path.exists()"
hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)
hist = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
cv.normalize(hist, hist, 0, 255, cv.NORM_MINMAX)
hist_img = np.uint8(hist)
# 构造与直方图同尺寸的 HSV 色谱图:S 随列变化,H 随行变化
color_map = np.zeros((180, 256, 3), dtype=np.uint8)
color_map[:, :, 0] = np.arange(256)[np.newaxis, :] # H 固定为 0 示例;
color_map[:, :, 1] = 255 # 实际应让 H 随行 h 变化
color_map[:, :, 2] = 0
# 正确的完整做法:H 平面 = 行索引 h,S 平面 = 列索引 s
hue_plane = np.tile(np.arange(180)[:, np.newaxis], (1, 256))
sat_plane = np.tile(np.arange(256)[np.newaxis, :], (180, 1))
color_map = np.dstack((hue_plane, sat_plane, np.full((180, 256), 128)))
color_map_bgr = cv.cvtColor(color_map, cv.COLOR_HSV2BGR)
# 直方图 × 颜色映射,再去除孤立点
colored_hist = (hist_img[..., np.newaxis] * color_map_bgr // 255)
colored_hist = cv.medianBlur(colored_hist, 3)
cv.imshow('Color Histogram', colored_hist)
cv.waitKey(0)
参数速查与源码印证
把本文涉及的参数集中对照如下,方便直接复制到项目中:
| 项目 | cv.calcHist | np.histogram2d |
|---|---|---|
| 输入数据 | [hsv](图像列表) |
h.ravel(), s.ravel()(两个一维数组) |
| 通道/维度 | [0, 1](H、S) |
第 1 个参数对应 H,第 2 个对应 S |
| bin 数 | [180, 256] |
[180, 256] |
| 取值范围 | [0, 180, 0, 256](拼接形式) |
[[0, 180], [0, 256]](嵌套形式) |
| 返回形状 | 180 × 256 矩阵 | (len(xbins), len(ybins)) 矩阵 + 两个边界数组 |
再强调两条易错点:
- Hue 的上界是 180 而不是 360。OpenCV 的 8 位图像 HSV 转换中,H 被压缩到 0–180 区间(
cv.COLOR_BGR2HSV的约定),这与度制色相 0–360 相差 2 倍,range 写错会导致统计结果整体为零; - bins 与 range 必须按维度顺序一一对应。
[180, 256]与[0, 180, 0, 256]中,前 180 属于 H,后 256 属于 S,顺序颠倒会静默地得到错误直方图而不报错。
一维直方图教程的官方代码 calcHist_Demo.py 展示了 histSize、histRange、accumulate 等参数在单通道场景下的用法,与本文的双通道用法一脉相承:单通道时 channels=[0]、bins=[256]、range=(0,256);双通道时维度翻倍,范围拼接为 [0, 180, 0, 256]。
小结与延伸
本篇覆盖的内容可以归纳为一句话:BGR 转 HSV → 双通道双维度统计 → 矩阵结果可视化。具体落点:
- 统计入口:
cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256]),或np.histogram2d(h.ravel(), s.ravel(), [180, 256], [[0,180],[0,256]]); - 可视化三法:
cv.imshow()(最快)、matplotlib.pyplot.imshow()(推荐,记得interpolation='nearest')、彩色映射相乘法(信息量最大); - 结果解读:X 轴为 S、Y 轴为 H,结合 Hue 数值区间判断颜色归属。
在此基础上可以继续深入仓库中的相关示例:反向投影 calcBackProject_Demo1.py 与 calcBackProject_Demo2.py(二维直方图 → 反向投影 → 相似区域检索)、直方图比较 compareHist_Demo.py(cv.compareHist 的多种度量方法)、以及直方图均衡化 EqualizeHist_Demo.py。教程系列的完整入口见 py_tutorials 目录。
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

