首页
/ OpenCV 二维直方图实战:用 cv.calcHist 与 np.histogram2d 计算并可视化 HSV 色彩直方图

OpenCV 二维直方图实战:用 cv.calcHist 与 np.histogram2d 计算并可视化 HSV 色彩直方图

2026-09-06 13:36:36作者:尤峻淳Whitney

本文围绕 OpenCV 教程《Histograms - 3 : 2D Histograms》展开,系统讲解如何计算和绘制二维直方图(2D Histogram)。读完本篇,你将掌握三个核心技能:用 cv.calcHist() 对 HSV 图像同时统计 Hue(色相)与 Saturation(饱和度)两个特征;用 np.histogram2d() 在纯 NumPy 环境中完成同样的统计;以及三种不同的二维直方图可视化方案——cv.imshow()、Matplotlib 的 imshow() 和带真实色彩标注的"OpenCV 示例风格"彩色直方图。这些能力是理解后续直方图反向投影(Histogram Back-Projection)等主题的基础。

输入图像及其颜色直方图:X 轴为饱和度 S 值,Y 轴为色相 Hue 值

为什么需要二维直方图

在前一篇教程(一维直方图)中,我们只考虑单一特征——像素的灰度强度值,因此得到一维直方图。而二维直方图同时考虑两个特征,最典型的应用是颜色直方图(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=30s_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),xbinsybins 是各维度的 bin 边界数组。与 cv.calcHist 相比,NumPy 版本的直方图数值语义一致,但绘制时需要自行处理轴方向与边界;OpenCV 版本则与后续 cv.calcBackProjectcv.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;然后把直方图图像与该颜色映射图逐像素相乘。同时使用若干预处理步骤去除孤立的小像素点,最终得到漂亮的直方图。

这种方法的精髓可以概括为三步:

  1. 构造一张与直方图同尺寸(180 × 256)的"色谱图",横坐标位置 s 的像素取饱和度 s、纵坐标位置 h 的像素取色相 h,整体转换到 BGR;
  2. 将归一化后的直方图与该色谱图逐元素相乘,使每个 bin 位置呈现"密度 × 颜色";
  3. 用形态学运算(如开运算)剔除孤立的微小亮点,减少视觉噪声。

教程给出的同一输入图像的输出结果如下,可以清晰地看到蓝色、黄色的存在,以及因棋盘格产生的白色成分:

OpenCV 示例风格的彩色直方图输出,峰值位置带有真实颜色标注

如果你想自己复现这套效果,核心骨架大致是:

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)) 矩阵 + 两个边界数组

再强调两条易错点:

  1. Hue 的上界是 180 而不是 360。OpenCV 的 8 位图像 HSV 转换中,H 被压缩到 0–180 区间(cv.COLOR_BGR2HSV 的约定),这与度制色相 0–360 相差 2 倍,range 写错会导致统计结果整体为零;
  2. bins 与 range 必须按维度顺序一一对应[180, 256][0, 180, 0, 256] 中,前 180 属于 H,后 256 属于 S,顺序颠倒会静默地得到错误直方图而不报错。

一维直方图教程的官方代码 calcHist_Demo.py 展示了 histSizehistRangeaccumulate 等参数在单通道场景下的用法,与本文的双通道用法一脉相承:单通道时 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.pycalcBackProject_Demo2.py(二维直方图 → 反向投影 → 相似区域检索)、直方图比较 compareHist_Demo.pycv.compareHist 的多种度量方法)、以及直方图均衡化 EqualizeHist_Demo.py。教程系列的完整入口见 py_tutorials 目录

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