OpenCV-Python (官方)中文教程(部分一)_Day22

发布于:2025-05-09 ⋅ 阅读:(13) ⋅ 点赞:(0)

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组合出现频率越高。


网站公告

今日签到

点亮在社区的每一天
去签到