直方图计算与可视化:从数据到图形
直方图这东西,说白了就是一张统计图。它把图像里每个像素的亮度值数一遍,然后告诉你:这张图里有多少像素是暗的,多少是亮的。我刚开始学图像处理时,总觉得直方图就是个辅助工具,后来做项目才发现——它其实是图像分析的「第一道安检」。
直方图统计方法:灰度级与频数
一张灰度图像,每个像素的取值在 0 到 255 之间。直方图要做的就两件事:
- 统计每个灰度级出现的像素个数(频数)
- 把频数画成柱状图,横轴是灰度级,纵轴是频数
举个例子,一张 4×4 的 8 位灰度图:
| 像素值 | 50 | 50 | 100 | 100 |
|---|---|---|---|---|
| 像素值 | 50 | 100 | 150 | 150 |
| 像素值 | 100 | 100 | 150 | 200 |
| 像素值 | 200 | 200 | 200 | 255 |
统计结果就是:灰度 50 出现 3 次,100 出现 5 次,150 出现 3 次,200 出现 4 次,255 出现 1 次。就这么简单。
我的习惯:拿到一张图,我第一件事就是看它的直方图。如果直方图集中在左侧,说明图像偏暗;集中在右侧,说明偏亮;中间隆起但两边塌陷,说明对比度低。这比肉眼判断快多了。
OpenCV/Python 绘制直方图:三种常用方法
Python 里画直方图,我常用的工具是 OpenCV 配合 Matplotlib。下面给出三种写法,从最基础的到最实用的。
方法一:用 OpenCV 的 calcHist
import cv2
import matplotlib.pyplot as plt
# 读取灰度图
img = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE)
# 计算直方图
hist = cv2.calcHist([img], [0], None, [256], [0, 256])
# 绘制
plt.plot(hist, color='black')
plt.xlabel('灰度级')
plt.ylabel('像素数')
plt.title('灰度直方图')
plt.show()
calcHist 的参数含义:
[img]:输入图像列表[0]:通道索引,灰度图只有 0 通道None:掩膜,None 表示全图统计[256]:bin 的数量,256 表示每个灰度级一个 bin[0, 256]:灰度范围
方法二:用 Matplotlib 的 hist 直接画
import cv2
import matplotlib.pyplot as plt
img = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE)
# 将图像展平为一维数组
pixels = img.ravel()
# 直接画直方图
plt.hist(pixels, bins=256, range=[0, 256], color='gray', alpha=0.7)
plt.xlabel('灰度级')
plt.ylabel('像素数')
plt.title('灰度直方图(Matplotlib)')
plt.show()
这个方法更简洁,但底层也是先统计再绘图。我个人习惯用 calcHist,因为后续做直方图均衡化时可以直接复用统计结果。
方法三:彩色图像的三通道直方图
import cv2
import matplotlib.pyplot as plt
img = cv2.imread('example.jpg')
colors = ('b', 'g', 'r')
for i, color in enumerate(colors):
hist = cv2.calcHist([img], [i], None, [256], [0, 256])
plt.plot(hist, color=color, label=f'通道 {color}')
plt.xlabel('灰度级')
plt.ylabel('像素数')
plt.legend()
plt.title('彩色图像三通道直方图')
plt.show()
我曾经踩过的坑:OpenCV 读取彩色图像默认是 BGR 顺序,不是 RGB。画直方图时通道索引和颜色名称一定要对应好,否则画出来的红蓝通道是反的。我刚开始就因为这个查了半天 bug。
直方图归一化:让统计结果可比较
不同尺寸的图像,像素总数不一样。直接比较频数没有意义。比如一张 100×100 的图和一张 1000×1000 的图,同样灰度 128 出现 500 次,前者占比 5%,后者只有 0.05%。
归一化就是把频数转换成概率密度,公式很简单:
p(rk) = nk / N
其中 nk 是灰度级 rk 的像素数,N 是总像素数。
OpenCV 里归一化直方图有两种方式:
import cv2
import numpy as np
img = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE)
hist = cv2.calcHist([img], [0], None, [256], [0, 256])
# 方式一:手动归一化
hist_norm = hist / (img.shape[0] * img.shape[1])
# 方式二:用 normalize 函数
hist_cv_norm = cv2.normalize(hist, hist, alpha=0, beta=1, norm_type=cv2.NORM_MINMAX)
注意这两种方式的区别:
| 方式 | 含义 | 适用场景 |
|---|---|---|
| 手动归一化(除以 N) | 概率密度,总和为 1 | 概率分析、熵计算 |
| cv2.normalize(NORM_MINMAX) | 线性缩放到 [0,1] 区间 | 可视化对比、特征归一化 |
核心理解:归一化后的直方图,纵轴不再是「多少个像素」,而是「每个灰度级出现的概率」。这样不同尺寸的图像之间才能公平比较。
本章知识体系
下面这张图把直方图计算与可视化的核心流程串起来了:
从输入图像开始,先统计每个灰度级的像素频数,然后分两条路走:一条是直接可视化绘制直方图,另一条是归一化处理。归一化后的结果既可以用于概率分析,也可以作为后续直方图均衡化、匹配等增强技术的输入。
避坑指南:我曾经在对比两张不同分辨率的图像时,直接比较原始频数直方图,结果完全看不出规律。后来归一化后再看,两张图的分布形态其实非常相似。记住:归一化是直方图分析的「标准动作」,不是可选项。