22.直方图
22.1直方图的计算,绘制与分析
使用 OpenCV 或 Numpy 函数计算直方图
使用 Opencv 或者 Matplotlib 函数绘制直方图
将要学习的函数有:cv2.calcHist(),np.histogram()
什么是直方图呢?通过直方图你可以对整幅图像的灰度分布有一个整体的 了解。直方图的 x 轴是灰度值(0 到 255),y 轴是图片中具有同一个灰度值的 点的数目。
直方图其实就是对图像的另一种解释。一下图为例,通过直方图我们可以 对图像的对比度,亮度,灰度分布等有一个直观的认识。几乎所有的图像处理 软件都提供了直方图分析功能。下图来自Cambridge in Color website,强 烈推荐你到这个网站了解更多知识。
让我们来一起看看这幅图片和它的直方图吧。(要记住,直方图是根据灰度 图像绘制的,而不是彩色图像)。直方图的左边区域像是了暗一点的像素数量, 右侧显示了亮一点的像素的数量。从这幅图上你可以看到灰暗的区域比两的区 域要大,而处于中间部分的像素点很少。
(1).统计直方图
现在我们知道什么是直方图了, 那怎样获得一副图像的直方图呢? OpenCV和Numpy 都有内置函数做这件事。在使用这些函数之前我们有 必要想了解一下直方图相关的术语。
BINS:上面的直方图显示了每个灰度值对应的像素数。如果像素值为 0 到 255,你就需要 256 个数来显示上面的直方图。但是,如果你不需要知道每一个像素值的像素点数目的,而只希望知道两个像素值之间的像素点数目怎么办呢?举例来说,我们想知道像素值在 0 到 15 之间的像素点的数目,接着 是 16 到 31,....,240 到 255。我们只需要 16 个值来绘制直方图。OpenCV Tutorials on histograms中例子所演示的内容。
那到底怎么做呢?你只需要把原来的 256 个值等分成 16 小组,取每组的总和。而这里的每一个小组就被成为 BIN。第一个例子中有 256 个 BIN,第 二个例子中有 16个 BIN。在 OpenCV 的文档中用 histSize 表示 BINS。
DIMS:表示我们收集数据的参数数目。在本例中,我们对收集到的数据只考虑一件事:灰度值。所以这里就是 1。
RANGE:就是要统计的灰度值范围,一般来说为 [0,256],也就是说所有的灰度值
使用 OpenCV 统计直方图 函数 cv2.calcHist 可以帮助我们统计一幅图像的直方图。我们一起来熟悉一下这个函数和它的参数:
cv2.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]])
1. images: 原图像(图像格式为 uint8 或 float32)。当传入函数时应该 用中括号 [] 括起来,例如:[img]。
2. channels: 同样需要用中括号括起来,它会告诉函数我们要统计那幅图 像的直方图。如果输入图像是灰度图,它的值就是 [0];如果是彩色图像 的话,传入的参数可以是 [0],[1],[2] 它们分别对应着通道 B,G,R。
3. mask: 掩模图像。要统计整幅图像的直方图就把它设为 None。但是如 果你想统计图像某一部分的直方图的话,你就需要制作一个掩模图像,并 使用它。(后边有例子)
4. histSize:BIN 的数目。也应该用中括号括起来,例如:[256]。
5. ranges: 像素值范围,通常为 [0,256]
让我们从一副简单图像开始吧。以灰度格式加载一幅图像并统计图像的直方图。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 1. 读取图像(灰度模式)
img = cv2.imread('home.jpg', 0) # 参数0表示以灰度模式读取
# 检查图像是否成功加载
if img is None:
raise FileNotFoundError("无法加载图像,请检查路径")
# 2. 计算直方图
hist = cv2.calcHist(
[img], # 输入图像(必须用列表包裹)
[0], # 通道索引(灰度图是0)
None, # 掩膜(None表示整个图像)
[256], # 直方图大小(bin的数量)
[0, 256] # 像素值范围
)
# 3. 可视化直方图
plt.figure(figsize=(10, 5))
plt.title("Grayscale Histogram")
plt.xlabel("Pixel Value")
plt.ylabel("Frequency")
plt.xlim([0, 256]) # X轴范围
plt.plot(hist, color='black')
plt.grid(True, linestyle='--', alpha=0.7)
plt.show()
# 4. 可选:显示原图
cv2.imshow("Original Image", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
hist 是一个 256x1 的数组,每一个值代表了与次灰度值对应的像素点数目。
使用 Numpy 统计直方图 Numpy 中的函数 np.histogram() 也可以帮 我们统计直方图。你也可以尝试一下下面的代码:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 1. 读取图像(灰度模式)
img = cv2.imread('11111.png', 0) # 参数0表示以灰度模式读取
# 检查图像是否成功加载
if img is None:
raise FileNotFoundError("无法加载图像,请检查路径")
# 2. 使用numpy计算直方图(不需要中括号)
hist, bins = np.histogram(img.ravel(), # 将图像展平为一维数组
bins=256, # 直方图的bin数量
range=[0, 256]) # 像素值范围
# 3. 可视化直方图
plt.figure(figsize=(10, 5))
plt.title("Grayscale Histogram (numpy)")
plt.xlabel("Pixel Value")
plt.ylabel("Frequency")
plt.xlim([0, 256]) # X轴范围
# 注意:bins比hist多一个元素,需要调整显示
plt.bar(bins[:-1], # X轴坐标(bin的左边界)取bins数组除最后一个元素外的所有值(因为bins比hist多一个元素)
hist, # Y轴高度(频数)每个bin对应的像素频数
width=1, # 柱子的宽度 设置每个柱子的宽度为1(保证柱子之间无间隙)
color='black', # 柱子填充颜色 黑色
edgecolor='none') # 柱子边框颜色(无边框)
plt.grid(True, # 启用网格线
linestyle='--', # 虚线样式
alpha=0.7) # 透明度(0.7表示70%不透明)
plt.show()
# 4. 可选:显示原图
cv2.imshow("Original Image", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
hist 与上面计算的一样。但是这里的 bins 是 257,因为 Numpy 计算 bins 的方式为:0-0.99,1-1.99,2-2.99 等。所以最后一个范围是 255-255.99。 为了表示它,所以在 bins 的结尾加上了 256。但是我们不需要 256,到 255 就够了。
其 他:Numpy 还 有 一 个 函 数 np.bincount(), 它 的 运 行 速 度 是 np.histgram 的 十 倍。 所 以 对 于 一 维 直 方 图, 我 们 最 好 使 用 这 个 函 数。 使 用 np.bincount 时 别 忘 了 设 置 minlength=256。 例 如, hist=np.bincount(img.ravel(),minlength=256)
注 意:OpenCV的函数要比 np.histgram()快40倍.所以坚持使用OpenCV 函数.
(2).绘制直方图
有两种方法来绘制直方图:
1.Short Way(简单方法):使用 Matplotlib 中的绘图函数。
2.Long Way(复杂方法):使用 OpenCV 绘图函数
使用 Matplotlib 中有直方图绘制函数:matplotlib.pyplot.hist()
它可以直接统计并绘制直方图。你应该使用函数 calcHist() 或 np.histogram()
统计直方图。
以下是关于 matplotlib.pyplot.hist()、cv2.calcHist() 和 np.histogram() 三个函数的详细对比分析,包括它们的区别、优缺点和适用场景:
1. matplotlib.pyplot.hist()
特点:
一体化操作:直接统计并绘制直方图(计算+可视化一步完成)
接口简单:无需手动处理 bins 和 hist 的对应关系
可视化集成:自动添加坐标轴、标题等图形元素
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('drawing.png',0)
plt.hist(img.ravel(),256,[0,256])
plt.show()
2. cv2.calcHist()
特点:
OpenCV专属:针对图像数据高度优化
多通道支持:可同时计算多通道直方图
灵活掩膜:支持指定ROI区域计算
import cv2
img = cv2.imread('image.jpg', 0)
hist = cv2.calcHist([img], [0], None, [256], [0, 256])
# 需要手动绘制
plt.plot(hist, color='red')
plt.title('OpenCV Histogram')
plt.show()
3. np.histogram()
特点:
纯计算函数:只返回统计结果,不包含绘图
通用性强:适用于任何数值数据(不限于图像)
精确控制:可自定义 bins 和 range
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('image.jpg', 0)
hist, bins = np.histogram(img.ravel(), bins=256, range=[0, 256])
# 手动绘制(需处理bins边界)
plt.bar(bins[:-1], hist, width=1)
plt.title('NumPy Histogram')
plt.show()
如何选择?
1、需要快速可视化 → plt.hist()
plt.hist(img.ravel(), bins=256, range=[0,256], alpha=0.7)
2、OpenCV图像处理流程 → cv2.calcHist()
hist = cv2.calcHist([img], [0], mask, [256], [0,256])
3、精确控制或非图像数据 → np.histogram()
hist, bins = np.histogram(data, bins=50, range=[min_val, max_val])
4、多通道图像分析 → cv2.calcHist() + 循环
colors = ('b','g','r')
for i, col in enumerate(colors):
hist = cv2.calcHist([img], [i], None, [256], [0,256])
plt.plot(hist, color=col)
性能实测对比
import time
import matplotlib.pyplot as plt
import cv2
import numpy as np
img = cv2.imread('drawing.png', 0)
# 测试plt.hist
t1 = time.time()
plt.hist(img.ravel(), bins=256)
print(f"plt.hist: {time.time()-t1:.4f}s")
# 测试cv2.calcHist
t2 = time.time()
hist = cv2.calcHist([img], [0], None, [256], [0,256])
print(f"cv2.calcHist: {time.time()-t2:.4f}s")
# 测试np.histogram
t3 = time.time()
hist, bins = np.histogram(img.ravel(), bins=256)
print(f"np.histogram: {time.time()-t3:.4f}s")
典型输出(4000x3000像素图像):
plt.hist: 3.7026s
cv2.calcHist: 0.0060s
np.histogram: 0.0209s
或者你可以只使用 matplotlib 的绘图功能,这在同时绘制多通道(BGR) 的直方图,很有用。但是要告诉绘图函数你的直方图数据在哪里。运行 一下下面的代码:
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('drawing.png') #读取BGR格式的彩色图像(默认加载为3通道numpy数组)
if img is None:
raise FileNotFoundError("无法加载图像,请检查路径")
#元组存储三个颜色符号,对应Matplotlib的绘图颜色:
#'b': 蓝色(Blue,OpenCV的通道0)
#'g': 绿色(Green,通道1)
#'r': 红色(Red,通道2)
color = ('b', 'g', 'r')
#同时获取索引i(0,1,2)和颜色符号col('b','g','r')
for i, col in enumerate(color):
#[img]: 输入图像(必须放在列表中)[i]: 通道索引(0=Blue, 1=Green, 2=Red)
#None: 不使用掩膜(计算全图)[256]: 直方图bin数量(256级)[0,256]: 像素值范围(0~255)
histr = cv2.calcHist([img], [i], None, [256], [0,256])
#histr: 当前通道的直方图数据(256维向量)color=col: 使用对应通道的颜色(蓝/绿/红)
plt.plot(histr, color=col, label=f'{col.upper()} Channel') # 添加标签
plt.title('RGB Channel Histogram')
plt.xlabel('Pixel Value')
plt.ylabel('Frequency')
plt.xlim([0, 256]) #限制X轴显示范围为0~256(覆盖所有像素值)
plt.legend() # 显示图例
plt.grid(True, linestyle='--', alpha=0.5) # 添加网格线
plt.show()
输出效果
将显示一个包含三条曲线的直方图:
蓝色曲线: 蓝色通道的像素值分布
绿色曲线: 绿色通道的分布
红色曲线: 红色通道的分布
(X轴为像素值0~255,Y轴为对应像素值的出现频率)
关键点总结
enumerate(): 同时获取索引和值,避免手动计数
calcHist()的列表输入: 必须用[img]和[i]传递参数
通道顺序: OpenCV默认BGR,Matplotlib默认RGB,注意颜色对应关系
使用 OpenCV 自带函数绘制直方图比较麻烦,这里不作介 绍,有兴趣可以自己研究。可以参考 OpenCV-Python2 的官方示例。
(3).使用掩模
要统计图像某个局部区域的直方图只需构建一副掩模图像。将要统计的部分设置成白色,其余部分为黑色,就构成一副掩模图像。然后把这个掩模图像传给函数就可以了。
import matplotlib.pyplot as plt
import cv2
import numpy as np
img = cv2.imread('drawing.png',0)
# create a mask
#创建与图像尺寸相同的全黑掩膜(所有像素值为0)img.shape[:2]获取图像的高度和宽度(忽略通道数)
mask = np.zeros(img.shape[:2], np.uint8)
#将掩膜中y=100:300,x=100:400的矩形区域设为白色(255),该区域表示后续要分析的ROI
mask[100:300, 100:400] = 255
#对图像进行按位与操作,保留掩膜白色区域对应的原图像素
#参数说明:前两个img:输入图像(灰度图),mask=mask:指定掩膜区域
#效果:非ROI区域变为黑色(0),ROI区域保留原灰度值
masked_img = cv2.bitwise_and(img,img,mask = mask)
# Calculate histogram with mask and without mask
# Check third argument for mask
#计算全局和局部直方图
hist_full = cv2.calcHist([img],[0],None,[256],[0,256])
hist_mask = cv2.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')
#默认先画的hist_full为蓝色,后画的hist_mask为橙色
plt.subplot(224), plt.plot(hist_full), plt.plot(hist_mask)
plt.xlim([0,256])
plt.show()
结果如下,其中蓝线是整幅图像的直方图,橙色线是进行掩模之后的直方图。