22.3 2D直方图
在前面的部分我们介绍了如何绘制一维直方图,之所以称为一维,是因为我们只考虑了图像的一个特征:灰度值。但是在 2D 直方图中我们就要考虑 两个图像特征。对于彩色图像的直方图通常情况下我们需要考虑每个的颜色(Hue)和饱和度(Saturation)。根据这两个特征绘制 2D 直方图。
OpenCV 的官方文档中包含一个创建彩色直方图的例子。本节就是要和大 家一起来学习如何绘制颜色直方图,这会对我们下一节学习直方图投影有所帮 助。
OpenCV 中的 2D 直方图
使用函数 cv2.calcHist() 来计算直方图既简单又方便。如果要绘制颜色 直方图的话,我们首先需要将图像的颜色空间从 BGR 转换到 HSV。(记住, 计算一维直方图,要从 BGR 转换到 HSV)。计算 2D 直方图,函数的参数要 做如下修改:
• channels=[0,1] 因为我们需要同时处理 H 和 S 两个通道。
• bins=[180,256]H 通道为 180,S 通道为 256。
• range=[0,180,0,256]H 的取值范围在 0 到 180,S 的取值范围 在 0 到 256。
代码如下:
import cv2
import numpy as np
img = cv2.imread('home.jpg')
hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
hist = cv2.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
以下是使用 OpenCV 计算 2D 直方图(如基于颜色或梯度方向的联合分布)的完整代码示例:
import cv2
import numpy as np
from matplotlib import pyplot as plt
# 读取图像并转换到HSV颜色空间
img = cv2.imread('image.jpg')
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 定义2D直方图的参数
channels = [0, 1] # 使用H(色调)和S(饱和度)通道
h_bins = 180 # H通道的bin数量(0-180)
s_bins = 256 # S通道的bin数量(0-256)
hist_size = [h_bins, s_bins]
h_range = [0, 180] # H通道取值范围
s_range = [0, 256] # S通道取值范围
ranges = h_range + s_range # 合并范围
# 计算2D直方图
#images: 输入图像列表(需用 [] 包裹,如 [hsv])。
#channels: 要统计的通道索引(如 [0, 1] 表示H和S通道)。
#mask: 掩模图像(None 表示全图统计)。
#histSize: 每个维度的bin数量(如 [180, 256])。
#ranges: 每个通道的取值范围(如 [0, 180, 0, 256])。
hist = cv2.calcHist([hsv], channels, None, hist_size, ranges)
# 归一化直方图(可选,方便可视化)cv2.normalize() 将直方图缩放到 [0, 255],便于显示。
hist_norm = cv2.normalize(hist, None, 0, 255, cv2.NORM_MINMAX)
# 可视化
# 创建图形
plt.figure(figsize=(12, 6))
# 子图1:原始图像
plt.subplot(121)
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.title('Original Image')
plt.axis('off')
# 子图2:2D直方图热力图
plt.subplot(122)
plt.imshow(hist_norm, cmap='jet', interpolation='nearest')
plt.title('2D Histogram (H-S)')
plt.xlabel('Saturation')
plt.ylabel('Hue')
plt.colorbar()
plt.tight_layout()
plt.savefig('2d_histogram_result.png', dpi=300)
plt.show()
其他常见 2D 直方图类型
(1) 梯度方向 vs. 梯度幅值
# 计算梯度
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0) # x方向梯度
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1) # y方向梯度
mag, ang = cv2.cartToPolar(gx, gy) # 梯度幅值和方向(弧度)
ang_deg = ang * 180 / np.pi # 弧度转角度(0~180°)
# 计算2D直方图(方向 vs. 幅值)
hist = cv2.calcHist(
[ang_deg, mag], [0, 1], None,
[180, 256], [0, 180, 0, 256] # 方向分180bin,幅值分256bin
)
(2) BGR 双通道直方图
# 计算B和G通道的2D直方图
hist = cv2.calcHist(
[img], [0, 1], None,
[256, 256], [0, 256, 0, 256]
)
在图像处理中,梯度方向 vs. 梯度幅值和BGR 双通道直方图是两种完全不同的特征表示方法,它们的核心区别体现在统计对象、应用场景和物理意义上。以下是详细对比:
1. 梯度方向 vs. 梯度幅值直方图
统计对象
梯度方向(Orientation):图像中每个像素点的梯度角度(如边缘方向)。
梯度幅值(Magnitude):梯度强度的量化值(如边缘的明显程度)。
计算步骤
计算梯度:
gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0) # x方向梯度
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1) # y方向梯度
mag, ang = cv2.cartToPolar(gx, gy) # 幅值和方向(弧度转角度:ang = ang * 180 / np.pi)
2D直方图:
横轴:梯度方向(0°~180°,无符号梯度)或(0°~360°,有符号梯度)。
纵轴:梯度幅值(通常归一化到 [0, 256])。
物理意义
方向:描述边缘或纹理的走向(如水平、垂直、对角线)。
幅值:描述边缘的强度(值越大,边缘越明显)。
应用场景
边缘检测:增强对物体轮廓的敏感性。
纹理分析:区分不同纹理模式(如条纹 vs 斑点)。
HOG(方向梯度直方图):行人检测、物体识别。
可视化示例
plt.imshow(ang, cmap='hsv') # 用HSV颜色映射方向
plt.colorbar(label='Angle (degrees)')
2. BGR 双通道直方图
统计对象
B通道:图像的蓝色分量强度(0~255)。
G通道:图像的绿色分量强度(0~255)。
计算步骤
hist = cv2.calcHist([img], [0, 1], None, [256, 256], [0, 256, 0, 256])
物理意义
颜色分布:反映图像中蓝色和绿色的联合分布(如天空的蓝色 vs 草地的绿色)。
相关性:若直方图对角线密集,说明B和G通道高度相关(如青色区域)。
应用场景
颜色分割:区分不同颜色的物体(如交通标志识别)。
白平衡分析:检测图像色偏。
图像检索:基于颜色相似性搜索图片。
可视化示例
plt.imshow(hist, cmap='jet', extent=[0, 256, 0, 256])
plt.xlabel('Blue Channel')
plt.ylabel('Green Channel')
3. 核心区别总结
4. 选择依据
用梯度直方图:
需要分析图像的结构特征(如边缘、纹理)时,例如检测车辆轮廓或指纹识别。
用BGR直方图:
需要分析图像的颜色特征时,例如区分红色苹果和绿色背景。
以下是完整的代码对比,展示梯度方向-幅值直方图、BGR双通道直方图和原始图像的视觉效果,并生成对比图:
import cv2
import numpy as np
import matplotlib.pyplot as plt
def plot_comparison(image_path):
# 1. 读取图像
img = cv2.imread(image_path)
if img is None:
print("错误:图像加载失败!")
return
# 转换为RGB格式(用于Matplotlib显示)
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 2. 计算梯度方向-幅值直方图
gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0) # x方向梯度
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1) # y方向梯度
mag, ang = cv2.cartToPolar(gx, gy) # 梯度幅值和方向(弧度)
ang_deg = ang * 180 / np.pi # 弧度转角度(0~180°)
hist_gradient = cv2.calcHist(
[ang_deg, mag], [0, 1], None,
[180, 256], [0, 180, 0, 256] # 方向分180bin,幅值分256bin
)
hist_gradient_norm = cv2.normalize(hist_gradient, None, 0, 255, cv2.NORM_MINMAX)
# 3. 计算B-G双通道直方图
hist_bgr = cv2.calcHist(
[img], [0, 1], None, # 使用B和G通道
[256, 256], [0, 256, 0, 256] # 每个通道分256bin
)
hist_bgr_norm = cv2.normalize(hist_bgr, None, 0, 255, cv2.NORM_MINMAX)
# 4. 绘制对比图
plt.figure(figsize=(15, 5))
# 子图1:原始图像
plt.subplot(131)
plt.imshow(img_rgb)
plt.title('Original Image')
plt.axis('off')
# 子图2:梯度方向-幅值直方图
plt.subplot(132)
plt.imshow(hist_gradient_norm, cmap='jet', extent=[0, 256, 0, 180], aspect='auto')
plt.title('Gradient Orientation vs. Magnitude')
plt.xlabel('Magnitude')
plt.ylabel('Orientation (degrees)')
plt.colorbar()
# 子图3:B-G双通道直方图
plt.subplot(133)
plt.imshow(hist_bgr_norm, cmap='jet', extent=[0, 256, 0, 256], aspect='auto')
plt.title('B vs. G Channel Histogram')
plt.xlabel('Green Channel')
plt.ylabel('Blue Channel')
plt.colorbar()
plt.tight_layout()
plt.savefig('comparison_result.png', dpi=300, bbox_inches='tight')
plt.show()
print("对比图已保存为 comparison_result.png")
if __name__ == "__main__":
plot_comparison('image.jpg') # 替换为你的图像路径
关键区别对比
梯度直方图中,方向为0°通常对应垂直边缘,90°对应水平边缘。
B-G直方图中,对角线密集区域表示蓝色和绿色通道强相关(如天空或水面)。
在梯度方向-幅值直方图(Gradient Orientation vs. Magnitude Histogram)中,线条分布和颜色强度可以直接反映原始图像的边缘特征、纹理结构和方向性。以下是具体分析方法:
1. 直方图坐标轴含义
横轴(X轴):梯度幅值(Magnitude)
表示边缘的强度,值越大(右侧)对应原图中越明显的边缘。
纵轴(Y轴):梯度方向(Orientation)
表示边缘的角度(0°~180°),例如:
0°:垂直边缘(如建筑物的竖线)
90°:水平边缘(如地平线)
45°/135°:对角线边缘(如斜向纹理)
颜色强度(热力图颜色):
颜色越亮(如黄色/白色)表示该方向和幅值的边缘出现频率越高。
2. 如何从直方图反推原图特征?
(1) 观察线条聚集区域
案例1:垂直线条主导
直方图中Y=0°附近有亮线→ 原图有大量垂直边缘(如栅栏、高楼)。
示例:
# 生成测试图像(垂直线条)
img = np.zeros((100, 100), dtype=np.uint8)
img[:, 30:35] = 255 # 垂直白条
案例2:水平线条主导
直方图中Y=90°附近有亮线 → 原图有水平边缘(如海平面、书架)。
示例:
img[40:45, :] = 255 # 水平白条
(2) 分析幅值分布
高幅值集中(右侧亮):
原图有清晰锐利的边缘(如物体轮廓、文字)。
低幅值分散(左侧亮):
原图以柔和纹理为主(如云彩、模糊背景)。
(3) 多方向混合特征
亮斑分散在多个角度:
原图存在复杂纹理(如树叶、毛发)。
示例(树叶图像):
(4) 颜色强度变化
连续亮带:
表示边缘方向连续变化(如圆形物体的渐变边缘)。
离散亮点:
表示特定方向的孤立边缘(如人工规则图案)。
3. 总结
通过梯度直方图可以直观判断原图的:
边缘主导方向(垂直/水平/斜向)。
边缘清晰度(幅值高低)。
纹理复杂度(分散或集中的亮斑)。
实用技巧:
若直方图在 0°和90° 同时有高峰 → 图像包含网格状结构(如棋盘)。
若直方图在 所有方向均匀分布 → 图像可能是噪声或随机纹理(如砂纸)。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 1. 生成测试图像
img = np.zeros((200, 200), dtype=np.uint8)
img[20:100, 20:100] = 255 # 白色方块
img[120:180, 120:180] = 255 # 另一个白色方块
cv2.line(img, (0, 0), (200, 200), 255, 2) # 对角线白线
# 2. 计算梯度
gx = cv2.Scharr(img, cv2.CV_32F, 1, 0)
gy = cv2.Scharr(img, cv2.CV_32F, 0, 1)
mag, ang = cv2.cartToPolar(gx, gy)
ang_deg = ang * 180 / np.pi # 转换为角度
# 3. 归一化幅度
mag_norm = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_32F)
# 4. 确保数据是2D矩阵
ang_deg = np.squeeze(ang_deg)
mag_norm = np.squeeze(mag_norm)
# 5. 计算直方图 (修正了bins和范围)
hist = cv2.calcHist(
[ang_deg, mag_norm], # 输入
[0, 1], # 使用的通道
None, # 无掩模
[180, 32], # bins数量 [角度, 幅度]
[0, 180, 0, 256] # 范围
)
# 6. 可视化 (修正了显示方式)
plt.figure(figsize=(12, 5))
plt.subplot(131)
plt.imshow(img, cmap='gray')
plt.title('Test Image')
plt.subplot(132)
plt.imshow(mag_norm, cmap='jet')
plt.colorbar()
plt.title('Gradient Magnitude')
plt.subplot(133)
# 对直方图进行对数变换以便更好地显示
hist_log = np.log1p(hist)
plt.imshow(hist_log.T, cmap='jet', aspect='auto', extent=[0, 180, 0, 64])
plt.colorbar()
plt.xlabel('Angle (degrees)')
plt.ylabel('Magnitude')
plt.title('Gradient Histogram (log scale)')
plt.tight_layout()
plt.show()
在梯度直方图(Gradient Histogram)中,X轴和Y轴分别表示以下内容:
X轴(横轴):梯度方向(Gradient Angle)
单位:角度(degrees)
范围:0° ~ 180°(因为梯度方向是无符号的,即 0° 和 180° 代表相同的方向)
含义:
0° 表示垂直边缘(梯度方向向右,即从黑到白的过渡方向)
90° 表示水平边缘(梯度方向向上)
45° 表示对角线边缘(从左上到右下)
135° 表示另一条对角线边缘(从右上到左下)
Y轴(纵轴):梯度幅度(Gradient Magnitude)
单位:归一化后的像素强度(0~255)
范围:0 ~ 64(取决于 cv2.calcHist 的 bins 设置)
含义:
值越大,表示该方向的边缘越强(即梯度变化越剧烈)
值越小,表示该方向的边缘越弱(即梯度变化越平缓)
直方图颜色(Color):
颜色越亮(如黄色/白色),表示该角度和幅度的梯度出现频率越高。
颜色越暗(如蓝色/黑色),表示该角度和幅度的梯度出现频率越低。
示例解释(针对你的测试图像)
你的测试图像包含:
两个白色方块(20:100, 20:100 和 120:180, 120:180):
它们的边缘会产生水平(90°)和垂直(0°)的梯度。
一条对角线白线(从 (0,0) 到 (200,200)):
会产生45° 方向的梯度。
因此,在梯度直方图中,你应该看到:
X=0° 和 X=90° 附近有较强的响应(来自方块的边缘)。
X=45° 附近也有较强的响应(来自对角线白线)。
Y轴 的值较高,表示这些方向的梯度幅度较大。
Numpy中2D直方图
Numpy 同样提供了绘制 2D 直方图的函数:np.histogram2d()。(还记得吗,绘制 1D 直方图时我们使用的是 np.histogram())。
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('home.jpg')
hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
hist, xbins, ybins = np.histogram2d(h.ravel(),s.ravel(),[180,256],[[0,180],[0,256]])
第一个参数是 H 通道,第二个参数是 S 通道,第三个参数是 bins 的数 目,第四个参数是数值范围。
现在我们要看看如何绘制颜色直方图。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 1. 生成测试图像
img = np.zeros((200, 200), dtype=np.uint8)
img[20:100, 20:100] = 255 # 白色方块
img[120:180, 120:180] = 255 # 另一个白色方块
cv2.line(img, (0, 0), (200, 200), 255, 2) # 对角线白线
# 2. 计算梯度(使用Scharr算子)
gx = cv2.Scharr(img, cv2.CV_32F, 1, 0)
gy = cv2.Scharr(img, cv2.CV_32F, 0, 1)
mag, ang = cv2.cartToPolar(gx, gy)
ang_deg = ang * 180 / np.pi # 转换为角度(0°~180°)
# 3. 归一化梯度幅度(0~255)
mag_norm = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX).flatten()
# 4. 使用np.histogram2d计算2D直方图
hist, xedges, yedges = np.histogram2d(
ang_deg.flatten(), # 角度数据(X轴)
mag_norm, # 幅度数据(Y轴)
bins=[180, 64], # bins数量:[角度, 幅度]
range=[[0, 180], [0, 256]] # 范围:角度0~180°,幅度0~256
)
# 5. 可视化
plt.figure(figsize=(15, 5))
# 子图1:原图
plt.subplot(131)
plt.imshow(img, cmap='gray')
plt.title('Original Image')
# 子图2:梯度幅度图
plt.subplot(132)
plt.imshow(mag, cmap='jet')
plt.colorbar()
plt.title('Gradient Magnitude')
# 子图3:2D直方图(使用pcolormesh显示)
plt.subplot(133)
# 使用对数变换增强低值可见性
hist_log = np.log(hist.T + 1) # 转置并取对数
#使用 pcolormesh 绘制2D直方图,比 imshow 更精确(能正确显示bin边缘)
plt.pcolormesh(xedges, yedges, hist_log, cmap='jet')
plt.colorbar(label='Log Frequency')
plt.xlabel('Gradient Angle (degrees)')
plt.ylabel('Gradient Magnitude')
plt.title('2D Gradient Histogram (np.histogram2d)')
plt.xlim(0, 180)
plt.ylim(0, 256)
plt.tight_layout()
plt.show()
cv2.calcHist() 和 np.histogram2d() 都可以计算二维直方图,但它们在输入格式、计算方式、返回值结构以及与OpenCV/NumPy生态的兼容性上有显著区别。以下是详细对比:
1. 输入格式
函数 输入数据要求
cv2.calcHist() 输入是列表形式的数组(即使单通道也要包在列表中),例如 [ang_deg, mag_norm]。
np.histogram2d() 直接接受两个独立的NumPy数组(x和y),例如 ang_deg.flatten(), mag_norm。
示例:
# cv2.calcHist
hist_cv2 = cv2.calcHist([ang_deg, mag_norm], [0, 1], None, [180, 64], [0, 180, 0, 256])
# np.histogram2d
hist_np, xedges, yedges = np.histogram2d(ang_deg.flatten(), mag_norm, bins=[180, 64], range=[[0, 180], [0, 256]])
2. 返回值
函数 返回值
cv2.calcHist() 返回单一的直方图数组(形状为 (bins[0], bins[1], ...)),无边界信息。
np.histogram2d() 返回直方图数组 + 两个方向的bin边界数组(hist, xedges, yedges),便于精确绘制。
关键区别:
cv2.calcHist() 的返回值直接是直方图,适合快速可视化(如 imshow)。
np.histogram2d() 返回的 xedges 和 yedges 可以用于 pcolormesh,能更精确地显示bin的边界。
3. 数据类型与性能
函数 数据类型支持 计算效率
cv2.calcHist() 对OpenCV的 cv2.CV_32F 或 cv2.CV_8U 类型优化更好,适合图像数据。 高度优化,适合大规模图像数据。
np.histogram2d() 直接处理NumPy数组,支持任意数据类型(如 float64)。 通用性强,但可能稍慢于OpenCV。
4. 可视化适配性
函数 推荐可视化方法 适用场景
cv2.calcHist() plt.imshow(hist.T, extent=[0, 180, 0, 256])(需手动转置和设置范围)。 快速查看直方图分布。
np.histogram2d() plt.pcolormesh(xedges, yedges, hist.T)(自动对齐bin边界)。 需要精确显示bin边界的场景。
示例对比:
# 使用cv2.calcHist + imshow
plt.imshow(hist_cv2.T, cmap='jet', extent=[0, 180, 0, 256], aspect='auto')
# 使用np.histogram2d + pcolormesh
plt.pcolormesh(xedges, yedges, hist_np.T, cmap='jet')
5. 功能扩展性
函数 额外功能
cv2.calcHist() 支持掩模(mask参数)、多通道直方图(如RGB图像的3D直方图)。
np.histogram2d() 支持权重(weights参数)、密度归一化(density=True),适合统计分析。
6. 实际效果对比(以你的代码为例)
cv2.calcHist() 结果
直方图是一个 密集的二维数组,直接映射到像素坐标。
使用 imshow 时需手动调整 extent 和转置(hist.T)。
np.histogram2d() 结果
直方图 + bin边界信息,可通过 pcolormesh 精确绘制。
天然支持非均匀bin(通过自定义 xedges 和 yedges)。
总结:如何选择?
场景 推荐函数
图像处理(如梯度直方图) cv2.calcHist()
统计分析与非均匀bin np.histogram2d()
需要精确bin边界的可视化 np.histogram2d()
高性能计算(大规模数据) cv2.calcHist()
两种方法本质上是等价的(最终直方图一致),但接口和生态适配不同。如果已用OpenCV处理图像,优先用 cv2.calcHist;若需更灵活的统计功能,选 np.histogram2d。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 1. 生成测试图像
img = np.zeros((200, 200), dtype=np.uint8)
img[20:100, 20:100] = 255 # 白色方块
img[120:180, 120:180] = 255 # 另一个白色方块
cv2.line(img, (0, 0), (200, 200), 255, 2) # 对角线白线
# 2. 计算梯度(使用Scharr算子)
gx = cv2.Scharr(img, cv2.CV_32F, 1, 0)
gy = cv2.Scharr(img, cv2.CV_32F, 0, 1)
mag, ang = cv2.cartToPolar(gx, gy)
ang_deg = ang * 180 / np.pi # 转换为角度(0°~180°)
# 3. 归一化梯度幅度(0~255)
mag_norm = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX).flatten()
# 4. 使用np.histogram2d计算2D直方图
hist, xedges, yedges = np.histogram2d(
ang_deg.flatten(), # 角度数据(X轴)
mag_norm, # 幅度数据(Y轴)
bins=[180, 64], # bins数量:[角度, 幅度]
range=[[0, 180], [0, 256]] # 范围:角度0~180°,幅度0~256
)
# 5. 可视化(新增边界数组的可视化)
plt.figure(figsize=(18, 6))
# 子图1:原图
plt.subplot(141)
plt.imshow(img, cmap='gray')
plt.title('Original Image')
# 子图2:梯度幅度图
plt.subplot(142)
plt.imshow(mag, cmap='jet')
plt.colorbar()
plt.title('Gradient Magnitude')
# 子图3:2D直方图(显式标注边界数组)
plt.subplot(143)
hist_log = np.log(hist.T + 1) # 转置并取对数
plt.pcolormesh(xedges, yedges, hist_log, cmap='jet')
plt.colorbar(label='Log Frequency')
# 标注边界数组的关键点(红色虚线)
for i in [0, 90, 180]: # 角度边界示例
plt.axvline(x=xedges[i], color='red', linestyle='--', alpha=0.5)
for j in [0, 32, 63]: # 幅度边界示例
plt.axhline(y=yedges[j], color='red', linestyle='--', alpha=0.5)
plt.xlabel('Gradient Angle (degrees)\nRed Dashed: xedges')
plt.ylabel('Gradient Magnitude\nRed Dashed: yedges')
plt.title('2D Histogram with Bin Edges')
# 子图4:边界数组的数值展示
plt.subplot(144)
plt.axis('off') # 关闭坐标轴
text_content = (
"=== Boundary Arrays ===\n"
f"xedges (angle bins):\n{np.round(xedges[:5], 1)} ... {np.round(xedges[-5:], 1)}\n"
f"Shape: {xedges.shape}\n\n"
f"yedges (magnitude bins):\n{np.round(yedges[:5], 1)} ... {np.round(yedges[-5:], 1)}\n"
f"Shape: {yedges.shape}"
)
plt.text(0, 0.5, text_content, fontfamily='monospace', va='center')
plt.title('np.histogram2d() Output')
plt.tight_layout()
plt.show()
绘制2D直方图
方法 1:使用 cv2.imshow() 我们得到结果是一个 180x256 的两维数组。 所以我们可以使用函数 cv2.imshow() 来显示它。但是这是一个灰度图,除 非我们知道不同颜色 H 通道的值,否则我们根本就不知道那到底代表什么颜色。
关键点:
输出结果:
cv2.calcHist() 返回的直方图是一个 180x256 的二维数组(假设角度分180个bin,幅度分256个bin)。
这个数组的每个值表示 某个角度和幅度组合的频次(即统计次数)。
显示问题:
cv2.imshow() 会将其当作灰度图像显示,亮度越高表示频次越高。
缺点:
你无法直接知道 颜色(Hue)对应的实际角度值(例如,哪个灰度值代表45°?)。
缺乏直观的颜色映射(如 jet、viridis 等颜色条)。
示例代码:
hist = cv2.calcHist([ang_deg, mag_norm], [0, 1], None, [180, 256], [0, 180, 0, 256])
cv2.imshow("Histogram (Grayscale)", hist)
cv2.waitKey(0)
效果:
显示一个灰度图,亮度表示频次,但无法直接关联到角度和幅度。
方法 2:使用 Matplotlib() 我们还可以使用函数 matplotlib.pyplot.imshow() 来绘制 2D 直方图,再搭配上不同的颜色图(color_map)。这样我们会对每 个点所代表的数值大小有一个更直观的认识。但是跟前面的问题一样,你还是 不知道那个数代表的颜色到底是什么。虽然如此,我还是更喜欢这个方法,它 既简单又好用。
关键点:
优势:
Matplotlib 的 imshow() 支持 颜色映射(color_map),例如 jet、hot、viridis 等。
可以通过颜色条(colorbar)直观地看到数值大小对应的颜色。
插值参数 interpolation='nearest':
默认情况下,imshow() 会对图像进行平滑插值,可能导致直方图的bin边界模糊。
设置 interpolation='nearest' 可以保留原始bin的锐利边界,避免误导。
依然存在的问题:
虽然颜色图能显示数值大小,但 X/Y轴的刻度需要手动关联到实际的角度和幅度值(需通过 extent 参数设置)。
例如:extent=[0, 180, 0, 256] 表示X轴是角度(0°~180°),Y轴是幅度(0~256)。
示例代码:
plt.imshow(hist.T, cmap='jet', interpolation='nearest', extent=[0, 180, 0, 256])
plt.colorbar(label='Frequency')
plt.xlabel('Angle (degrees)')
plt.ylabel('Magnitude')
plt.title('2D Histogram with Color Map')
plt.show()
效果:
显示一个彩色直方图,颜色表示频次,X/Y轴标签明确角度和幅度范围。
注意:在使用这个函数时,要记住设置插值参数为 nearest。
为什么作者更喜欢Matplotlib?
直观性:颜色映射比灰度图更容易理解数值分布。
灵活性:支持调整坐标轴、添加标签、颜色条等。
易用性:适合嵌入到更复杂的图表中(如子图、叠加其他数据)。
关键总结
方法 优点 缺点 适用场景
cv2.imshow() 简单快速,适合OpenCV流程。 只能显示灰度图,无颜色映射和坐标轴标签。 快速调试,无需详细分析。
plt.imshow() 支持颜色映射、坐标轴标签、插值控制。 需手动设置 extent 和 interpolation。 需要直观展示和定量分析时。
如何改进?
如果希望直接关联颜色与角度,可以:
自定义颜色映射:将角度(Hue)映射到HSV颜色空间,再转换为RGB显示。
添加交互式工具:用 mplcursors 库实现鼠标悬停时显示角度和幅度值。
示例(HSV颜色映射):
hsv = np.zeros((*hist.shape, 3), dtype=np.uint8)
hsv[..., 0] = np.linspace(0, 180, 180).astype(np.uint8) # Hue = 角度
hsv[..., 1] = 255 # 饱和度固定
hsv[..., 2] = cv2.normalize(hist, None, 0, 255, cv2.NORM_MINMAX) # 亮度 = 频次
rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)
plt.imshow(rgb, extent=[0, 180, 0, 256])
plt.colorbar(label='Magnitude Frequency')
plt.show()
这样颜色直接代表角度,亮度代表频次,解决了“不知道颜色对应什么角度”的问题。
代码如下:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('home.jpg')
hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
hist = cv2.calcHist( [hsv], [0, 1], None, [180, 256], [0, 180, 0, 256] )
plt.imshow(hist,interpolation = 'nearest')
plt.show()
下面是输入图像和颜色直方图。X 轴显示 S 值,Y 轴显示 H 值。
在直方图中,你可以看到在 H=100,S=100 附近有比较高的值。这部分与天的蓝色相对应。同样另一个峰值在 H=25 和 S=100 附近。这一宫殿的黄 色相对应。你可用通过使用图像编辑软件(GIMP)修改图像,然后在绘制直方图看看我说的对不对。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 1. 生成测试图像(200x200的黑底,带两个白方块和一条对角线)
img = np.zeros((200, 200), dtype=np.uint8)
img[20:100, 20:100] = 255 # 第一个白方块
img[120:180, 120:180] = 255 # 第二个白方块
cv2.line(img, (0, 0), (200, 200), 255, 2) # 对角线
# 2. 计算梯度(使用Scharr算子)
gx = cv2.Scharr(img, cv2.CV_32F, 1, 0) # x方向梯度
gy = cv2.Scharr(img, cv2.CV_32F, 0, 1) # y方向梯度
mag, ang = cv2.cartToPolar(gx, gy) # 梯度幅值和角度
ang_deg = ang * 180 / np.pi # 弧度转角度(0°~180°)
# 3. 归一化梯度幅值到0~255范围,并统一数据类型
mag_norm = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
ang_deg = ang_deg.astype(np.float32) # 强制转为float32
mag_norm = mag_norm.astype(np.float32) # 强制转为float32
# 4. 检查数据尺寸和类型(关键调试步骤)
print("ang_deg shape:", ang_deg.shape, "dtype:", ang_deg.dtype) # 应为 (200,200) float32
print("mag_norm shape:", mag_norm.shape, "dtype:", mag_norm.dtype) # 应为 (200,200) float32
# 5. 计算2D直方图
hist = cv2.calcHist([ang_deg, mag_norm], [0, 1], None, [180, 256], [0, 180, 0, 256])
# 6. 使用对数变换增强可视化
hist_log = np.log(hist + 1)
hist_normalized = cv2.normalize(hist_log, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
cv2.imshow("Grayscale Histogram (Log Scale)", hist_normalized)
cv2.waitKey(2000)
# 7. 优化Matplotlib显示
plt.figure(figsize=(15, 5))
plt.subplot(131)
plt.imshow(img, cmap='gray')
plt.title("Original Image")
plt.subplot(132)
plt.imshow(hist_log.T, cmap='gray', interpolation='nearest', extent=[0, 180, 0, 256])
plt.colorbar()
plt.title("Log Histogram (Grayscale)")
plt.subplot(133)
plt.imshow(hist_log.T, cmap='jet', interpolation='nearest', extent=[0, 180, 0, 256],
vmax=np.max(hist_log)*0.5) # 调整vmax以突出高频部分
plt.colorbar()
plt.title("Log Histogram (Color)")
plt.tight_layout()
plt.show()
方法 3:OpenCV 风格 在官方文档中有一个关于颜色直方图的例子。运行 一下这个代码,你看到的颜色直方图也显示了对应的颜色。简单来说就是:输 出结果是一副由颜色编码的直方图。效果非常好(虽然要添加很多代码)。
在那个代码中,作者首先创建了一个 HSV 格式的颜色地图,然后把它转 换成 BGR 格式。再将得到的直方图与颜色直方图相乘。作者还用了几步来去 除小的孤立的的点,从而得到了一个好的直方图。
我把对代码的分析留给你们了,自己去玩一下把。下边是对上边的图运行 这段代码之后得到的结果:
从直方图中我们可以很清楚的看出它们代表的颜色,蓝色,黄色,还有棋盘带来的白色,漂亮!!
这段代码是一个基于 HSV 颜色空间 的 2D 直方图可视化 程序,主要用于分析视频帧的 色调(Hue) 和 饱和度(Saturation) 分布。
import numpy as np
import cv2
from time import clock # 用于计时(但实际未使用)
import sys
import video # video 模块也是 opencv 官方文档中自带的
if __name__ == '__main__': #确保代码仅在直接运行时执行,而不是被导入时执行。
# 构建 HSV 颜色地图
hsv_map = np.zeros((180, 256, 3), np.uint8)
# np.indices 可以返回由数组索引构建的新数组。
# 例如:np.indices( 3,2);其中(3,2)为原来数组的维度:行和列。
# 返回值首先看输入的参数有几维:(3,2)有2维,所以从输出的结果应该是[[a],[b]], 其中包含两个3行,2列数组。第二看每一维的大小,第一维为3,所以a中的值就0到2(最大索引数),a中的每一个值就是它的行索引;同样的方法得到 b(列索引)
# 结果就是: array([[[0, 0],[1, 1],[2, 2]], [[0, 1],0, 1],[0, 1]]])
h, s = np.indices(hsv_map.shape[:2]) #生成坐标网格,h 和 s 分别表示行和列的索引。
hsv_map[:, :, 0] = h # 色调(Hue,0-179)
hsv_map[:, :, 1] = s # 饱和度(Saturation,0-255)
hsv_map[:, :, 2] = 255 # 亮度(Value,固定为最大值)
hsv_map = cv2.cvtColor(hsv_map, cv2.COLOR_HSV2BGR) #将 HSV 转换为 BGR 格式,以便用 imshow 正确显示。
cv2.imshow('hsv_map', hsv_map)
cv2.namedWindow('hist', 0) # 0 表示窗口大小可调
hist_scale = 10 # 直方图缩放因子初始值
def set_scale(val):
global hist_scale
hist_scale = val ## 更新缩放因子
#添加滑动条,动态调整直方图的缩放因子(hist_scale)。
cv2.createTrackbar('scale', 'hist', hist_scale, 32, set_scale)
try:
fn = sys.argv[1] # 尝试从命令行参数获取视频文件路径
except:
fn = 0 # 默认使用摄像头(设备索引 0)
cam = video.create_capture(fn, fallback='synth:bg=../cpp/baboon.jpg:class=chess:noise=0.05')
while True:
flag, frame = cam.read() # 读取一帧
cv2.imshow('camera', frame) # 显示原始帧
# 图像金字塔
# 通过图像金字塔降低分辨率,但不会对直方图有太大影响。
# 但这种低分辨率,可以很好抑制噪声,从而去除孤立的小点对直方图的影响。
small = cv2.pyrDown(frame) # 降采样(缩小图像,减少计算量)
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # 转换到 HSV 空间
# 取 v 通道 (亮度) 的值。
# dark = hsv[...,2] < 32
# 此步操作得到的是一个布尔矩阵,小于 32 的为真,大于 32 的为假。
#目的:排除暗区(如阴影),因为它们对颜色分析无意义。
dark = hsv[:, :, 2] < 32 # 找到亮度 <32 的像素(暗区)
hsv[dark] = 0 # 将暗区的 HSV 值设为 0(忽略这些像素)
h = cv2.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
# numpy.clip(a, a_min, a_max, out=None)[source]
# 给定一个区间,区间外的值被裁剪到区间边缘。例如,如果指定的间隔为[0,1],小于0的值将变为0,大于1的值将变为1。
# >>> a = np.arange(10)
# >>> np.clip(a, 1, 8)
# array([1, 1, 2, 3, 4, 5, 6, 7, 8, 8])
#hist_scale:通过滑动条调整的缩放因子。clip:确保值在 [0,1] 范围内。
h = np.clip(h * 0.005 * hist_scale, 0, 1)
# 可以在切片语法中使用'newaxis'对象来创建长度为1的轴。也可以用None代替newaxis,效果完全一样
# h 从一维变成 3 维
#h[:, :, np.newaxis]:将直方图从 2D 扩展为 3D(与 hsv_map 相乘)。hsv_map * h:用直方图的值加权颜色地图,高频区域显示更亮。
vis = hsv_map * h[:, :, np.newaxis] / 255.0 # 将直方图映射到颜色空间
cv2.imshow('hist', vis)
ch = 0xFF & cv2.waitKey(1)
if ch == 27:
break
cv2.destroyAllWindows()
总结
功能:实时分析视频帧的 色调和饱和度分布,通过颜色地图直观显示。
关键点:
HSV 颜色空间更适合颜色分析。
2D 直方图(H+S)反映颜色的分布规律。
滑动条动态调整直方图缩放,增强交互性。
忽略低亮度区域,避免噪声干扰。
处理单张彩色图像:
import numpy as np
import cv2
import sys
def main():
# 1. 读取输入图像(替换为你的图片路径)
if len(sys.argv) > 1:
image_path = sys.argv[1]
else:
image_path = 'test.jpg' # 默认图像文件名
frame = cv2.imread(image_path)
if frame is None:
print("Error: 无法加载图像,请检查路径!")
return
# 2. 创建HSV颜色地图(与之前相同)
hsv_map = np.zeros((180, 256, 3), np.uint8)
h, s = np.indices(hsv_map.shape[:2])
hsv_map[:, :, 0] = h
hsv_map[:, :, 1] = s
hsv_map[:, :, 2] = 255
hsv_map = cv2.cvtColor(hsv_map, cv2.COLOR_HSV2BGR)
# 3. 创建可调参数的窗口
cv2.namedWindow('hist', cv2.WINDOW_NORMAL)
hist_scale = 10
def set_scale(val):
global hist_scale
hist_scale = val
cv2.createTrackbar('Scale', 'hist', hist_scale, 32, set_scale)
# 4. 图像处理函数
def update_hist():
# 转换为HSV
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
# 过滤暗区(亮度<32的像素)
dark = hsv[:, :, 2] < 32
hsv[dark] = 0
# 计算2D直方图(H和S通道)
hist = cv2.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
# 缩放直方图
hist = np.clip(hist * 0.005 * hist_scale, 0, 1)
# 可视化
vis = hsv_map * hist[:, :, np.newaxis] / 255.0
cv2.imshow('hist', vis)
# 初始显示
cv2.imshow('input', frame)
update_hist()
# 5. 交互循环
while True:
key = cv2.waitKey(10)
if key == 27: # ESC退出
break
update_hist() # 更新直方图(响应滑动条)
cv2.destroyAllWindows()
if __name__ == '__main__':
main()
如何使用?
准备图像:
将你的图像放在同一目录下(如 test.jpg),或通过命令行参数指定路径。
运行效果:
窗口 input 显示原始图像。
窗口 hist 显示 HSV 2D直方图,可通过滑动条调整亮度。
颜色含义:
X轴:色调(Hue,0°~180°对应红→绿→蓝→红)
Y轴:饱和度(Saturation,0~255 从灰到纯色)
亮度:颜色越亮表示该HSV组合出现频率越高。