
第021期 | 图像处理算法入门系列 | 2026-08-30
关键词:直方图、cv2.calcHist、灰度直方图、多通道直方图、归一化、CDF、直方图均衡化
一、引言
第020期我们讲了"如何把灰度图一刀切成两半"------二值化。但是在切之前,必须先回答一个问题:刀要落在哪里?
答案藏在图像的 像素分布 里。把一张图所有像素按灰度值数一遍,画成柱状图,就是直方图(Histogram)。
直方图是图像处理里出场率第二高的工具,仅次于二值化。它是:
- 看图的眼睛:一眼判断偏暗 / 偏亮 / 低对比度 / 高对比度
- 选阈值的尺子:双峰直方图的"谷底"就是二值化的最佳阈值(接续 020 期)
- 算法的前奏:直方图均衡化(022 期)、CLAHE(023 期)、Otsu 自动阈值(076 期)、直方图匹配、色彩迁移、图像检索、相机白平衡......几乎所有需要"理解图像统计规律"的算法,第一步都是直方图
text
直方图的本质:把 [0, 255] 切成 256 个 bin,数每个 bin 里有多少像素。
像素值 k (0-255) → 第 k 个 bin 高度 +1
看似一行图能讲清的事,里面藏着 5 个关键概念:
① 直方图定义与计算 ② 多通道叠加 ③ 归一化(PDF) ④ 累积分布 CDF ⑤ 性能陷阱
本期我们就把直方图从定义到实现一次性讲透,最后引出下期"直方图均衡化"。
二、核心概念与原理
2.1 直方图的数学定义
设图像为 I(x, y) ∈ [0, L-1](灰度图 L=256),灰度级 k 的概率密度为:
text
p(k) = n_k / N
其中:
n_k : 灰度值 = k 的像素个数
N : 图像总像素数
p(k) : 归一化后的概率密度(PDF),所有 p(k) 之和 = 1
由此衍生两个关键函数:
text
概率密度函数(PDF): p(k) = n_k / N ← 归一化直方图
累积分布函数(CDF): c(k) = Σ_{i=0..k} p(i) ← 022 期均衡化的核心
2.2 cv2.calcHist 函数签名
python
hist = cv2.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]])
参数:
images : 输入图像列表(用 [img] 包裹),单张灰度图也行,彩色图也行
channels : 统计哪些通道
- 灰度图:[0]
- 彩色图:可以 [0](只算 B),[1](只算 G),[2](只算 R)
也可以 [0,1] / [0,1,2](多维直方图)
mask : 掩膜,None 表示全图统计;非 None 时只统计 mask>0 的区域
histSize : 每个通道的分箱数(Bins),灰度图常用 [256]
ranges : 每个通道的取值范围,灰度图 [0, 256],彩色图 [0,256, 0,256, 0,256]
返回值:
hist : 形状为 (256, 1) 的 numpy 数组
2.3 灰度直方图 vs 多通道直方图
text
灰度直方图: 1 个通道 → 1 条曲线 → 反映整体亮度分布
多通道直方图: 3 个通道 → 3 条曲线叠加 → 反映色彩平衡(B/G/R 各占多少)
N 维直方图: N 通道联合 → 高维矩阵 → 反映色彩出现频率(图像检索/分割常用)
2.4 归一化的意义
text
原始直方图: 纵轴是"像素个数"------受图像分辨率影响(1M 像素 vs 1K 像素差 1000 倍)
归一化直方图:纵轴是"概率密度"------和分辨率无关,可以跨图比较
两套做法等价:
① 手动:hist_norm = hist / hist.sum()
② OpenCV:cv2.normalize(hist, None, 0, 1, cv2.NORM_MINMAX)
三、实验:6 个动手环节
实验 1:灰度直方图基础
合成一张"高对比度双峰"灰度图(30 与 220),绘制直方图:
python
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 合成测试图:30% 暗像素 + 70% 亮像素,叠加少量噪声
size = (360, 540)
rng = np.random.default_rng(7)
high = np.zeros(size, dtype=np.uint8)
mask = rng.random(size) > 0.5
high[mask] = 220
high[~mask] = 30
high = np.clip(high.astype(np.int16) + rng.integers(-8, 8, size), 0, 255).astype(np.uint8)
# 计算并绘制直方图
hist = cv2.calcHist([high], [0], None, [256], [0, 256]) # 形状 (256, 1)
plt.figure(figsize=(8, 4))
plt.plot(hist, color='#2c3e50', lw=1.5)
plt.fill_between(np.arange(256), hist.ravel(), alpha=0.25, color='#3498db')
plt.axvline(30, color='#e74c3c', ls='--', label='左峰 ≈ 30')
plt.axvline(220, color='#27ae60', ls='--', label='右峰 ≈ 220')
plt.xlim(0, 256)
plt.xlabel('灰度级'); plt.ylabel('像素计数')
plt.title('灰度直方图(256 bins)')
plt.grid(True, alpha=0.3); plt.legend()
plt.show()
运行结果:

可以清楚看到两个尖峰:左峰在 30 附近(背景区),右峰在 220 附近(前景区),中间几乎为 0------这就是典型的"双峰直方图",是 020 期选阈值的依据。
实验 2:多通道直方图
彩色图像分 B/G/R 三个通道统计,颜色与通道对应:
python
colorful = cv2.imread('colorful.jpg') # BGR 顺序
colors = ('b', 'g', 'r') # OpenCV 用 BGR
for i, c in enumerate(colors):
hist = cv2.calcHist([colorful], [i], None, [256], [0, 256])
plt.plot(hist, color=c, label=f'通道 {i} ({c.upper()})', lw=1.6)
plt.xlim(0, 256)
plt.xlabel('像素值'); plt.ylabel('像素计数')
plt.title('BGR 三通道直方图')
plt.legend(); plt.grid(True, alpha=0.3)
运行结果:

可以看到蓝/绿/红三块区域的直方图分别集中在不同像素值段,没有任何交叉------这正是彩色直方图判断"图像颜色偏向"的最常用方法。
实验 3:归一化直方图与统计量
直方图本身是绝对计数,但跨图比较时需要归一化:
python
gray = cv2.imread('low_contrast.jpg', cv2.IMREAD_GRAYSCALE)
hist = cv2.calcHist([gray], [0], None, [256], [0, 256])
# 两种归一化做法
hist_manual = hist / hist.sum() # 手动除以总数
hist_cv = cv2.normalize(hist, None, 0, 1, cv2.NORM_MINMAX) # OpenCV 归一到 [0,1]
# 直方图统计量
print(f'像素总数: {gray.size:,}')
print(f'最小值 : {gray.min()}')
print(f'最大值 : {gray.max()}')
print(f'均值 : {gray.mean():.2f}')
print(f'中位数 : {np.median(gray):.1f}')
print(f'标准差 : {gray.std():.2f}')
运行结果:

| 指标 | 数值 |
|---|---|
| 形状 | (360, 540) |
| 像素总数 | 194,400 |
| 最小值 | 15 |
| 最大值 | 241 |
| 均值 | 127.51 |
| 中位数 | 127.0 |
| 标准差 | 24.97 |
观察发现:均值和中位数几乎相等(都是 127 左右),说明这张图的分布大致对称,标准差 24.97 偏小------典型的"低对比度"特征。
实验 4:四类典型图像的直方图对比
最有价值的不是单张图的直方图,而是多张图横向对比:
python
samples = {
'dark': (cv2.imread('dark.jpg', 0), '偏暗图'),
'bright': (cv2.imread('bright.jpg', 0), '偏亮图'),
'low_contrast': (cv2.imread('low_contrast.jpg', 0), '低对比度'),
'high_contrast': (cv2.imread('high_contrast.jpg', 0), '高对比度'),
}
fig, axes = plt.subplots(2, 4, figsize=(15, 7))
for col, (key, (img, title)) in enumerate(samples.items()):
axes[0, col].imshow(img, cmap='gray')
axes[0, col].set_title(title); axes[0, col].axis('off')
hist = cv2.calcHist([img], [0], None, [256], [0, 256])
axes[1, col].plot(hist)
axes[1, col].axvline(img.mean(), color='r', ls='--')
axes[1, col].set_xlabel('灰度级')
运行结果:

| 类型 | 直方图形态 | 视觉特征 |
|---|---|---|
| 偏暗图 | 集中在 0-80 | 全图发暗,细节丢失在暗部 |
| 偏亮图 | 集中在 180-255 | 全图泛白,细节丢失在亮部 |
| 低对比度 | 集中在 100-150 | 灰蒙蒙,像隔着一层雾 |
| 高对比度 | 两端各一峰 | 黑白分明,过渡锐利 |
实操中拿到一张图,先画直方图再看图------能省 80% 的盲目调参时间。
实验 5:累积分布函数 CDF(铺垫下期)
把 PDF 累加起来就是 CDF,这是 022 期直方图均衡化的数学核心:
python
gray = cv2.imread('low_contrast.jpg', cv2.IMREAD_GRAYSCALE)
hist = cv2.calcHist([gray], [0], None, [256], [0, 256]).ravel()
pdf = hist / hist.sum()
cdf = np.cumsum(pdf) # 累积分布
fig, ax1 = plt.subplots(figsize=(8, 4))
ax1.plot(pdf, color='#3498db', label='PDF')
ax1.set_ylabel('PDF(归一化直方图)', color='#3498db')
ax2 = ax1.twinx()
ax2.plot(cdf, color='#e74c3c', label='CDF')
ax2.set_ylabel('CDF(累积分布)', color='#e74c3c')
ax2.set_ylim(0, 1.05)
运行结果:

CDF 的形态是单调不减的 S 形------中间陡(像素集中),两端缓(像素稀疏)。直方图均衡化的本质就是把 CDF 拉成一条直线 y=x,让每个像素值区间被等概率使用------这就把 022 期的入口打开了。
实验 6:性能基准测试
实测三种写法的耗时差异(1080×720 彩色图):
python
import time
big = cv2.imread('big.jpg') # 1080×720×3
# 方式 A:cv2.calcHist(三通道 256³)
t0 = time.perf_counter()
for _ in range(50):
cv2.calcHist([big], [0,1,2], None, [256,256,256], [0,256]*3)
t_cv = (time.perf_counter() - t0) / 50 * 1000
# 方式 B:np.bincount(单通道,最快)
t0 = time.perf_counter()
for _ in range(50):
np.bincount(big[:,:,0].ravel(), minlength=256)
t_np = (time.perf_counter() - t0) / 50 * 1000
# 方式 C:Python 双循环(极慢)
hist = np.zeros(256, dtype=np.int64)
for y in range(0, big.shape[0], 4): # 采样 1/16
for x in range(0, big.shape[1], 4):
hist[big[y, x, 0]] += 1
运行结果:

| 方式 | 耗时 | 适用场景 |
|---|---|---|
cv2.calcHist(三通道 256³) |
24.35 ms | 通用,特别是多通道 / 掩膜 |
np.bincount(单通道) |
4.45 ms | 单通道灰度直方图的最快方案 |
| Python 双循环 | 17.40 ms(仅 1/16 区域) | 教学演示,生产环境禁用 |
关键洞察:
np.bincount在单通道场景下比cv2.calcHist快约 5 倍- Python 双循环即使只采样 1/16 区域也比向量化方案慢------再次印证"必须向量化"
- 实际项目里灰度直方图直接用 np.bincount,多通道 / 掩膜场景用 cv2.calcHist
四、常见陷阱
陷阱 1:彩色图忘了拆通道
python
# 错误:直接对彩色图算"灰度直方图"
hist = cv2.calcHist([color_img], [0], None, [256], [0, 256]) # 只算了 B 通道
# 正确:三通道分别算,再叠加
for i in range(3):
hist_i = cv2.calcHist([color_img], [i], None, [256], [0, 256])
plt.plot(hist_i, color=('b','g','r')[i])
陷阱 2:直方图"假双峰"
python
# 反例:高斯模糊过度
high = cv2.GaussianBlur(high, (11, 11), 3) # 双峰被平滑成单峰!
# 正解:双峰图像不要做大核模糊
我第一次跑实验时就踩了这个坑------加了 (11,11) 高斯模糊,原本明显的双峰直接被磨平,标准差从 95 掉到 10,图就废了。
陷阱 3:bins 设错导致数据丢失
python
# 把 256 个 bin 缩成 16 个 bin:信息丢失严重
hist = cv2.calcHist([gray], [0], None, [16], [0, 256]) # 每个 bin 合并 16 个灰度级
# 通常 256 bins 是最直观的默认选择
陷阱 4:横轴范围错误
python
# OpenCV 范围是半开区间 [0, 256),不是 [0, 255]
hist = cv2.calcHist([gray], [0], None, [256], [0, 256]) # 正确
hist = cv2.calcHist([gray], [0], None, [256], [0, 255]) # 错误,丢掉灰度=255
五、产品推荐:照片分析与直方图工具
移动端(随手拍 + 现场分析)
| 应用 | 平台 | 直方图能力 | 特色 |
|---|---|---|---|
| Lightroom Mobile | iOS / Android | 实时 RGB 直方图 + 直方图裁剪警示 | Adobe 出品,专业摄影师首选 |
| Snapseed | iOS / Android | 调色面板内嵌直方图 | Google 出品,免费 + 局部调色 |
| VSCO | iOS / Android | 调色时显示 RGB / 亮度直方图 | 滤镜直方图参考 |
| 手机自带"照片信息" | iOS(点击 i)/ Android | 缩略图 + 直方图(部分厂商) | 零门槛,查看照片元数据 |
桌面端 / 专业级(修图 + 调色 + 科研)
| 工具 | 平台 | 直方图能力 | 特色 |
|---|---|---|---|
| RawTherapee | Windows / macOS / Linux | 全通道直方图 + Lab 色空间直方图 | 免费开源,RAW 处理神器 |
| Darktable | Windows / macOS / Linux | 实时直方图 + 波形图 + 矢量示波器 | 摄影师用的开源版 Lightroom |
| Adobe Lightroom Classic | Windows / macOS | 完整 RGB 直方图 + 直方图裁剪警告 | 业界标准,付费 |
| Capture One | Windows / macOS | 高级直方图 + 皮肤色调直方图 | 高端商业摄影常用 |
| ImageJ / FIJI | 全平台 | 完整直方图 + 任意 ROI 区域直方图 | 科研 / 医学图像分析标配 |
| GIMP | 全平台 | 基础直方图 | 免费开源,入门级 |
在线工具
| 工具 | 网址 | 特色 |
|---|---|---|
| Photopea | photopea.com | 浏览器内 PS,免费,支持直方图调色 |
| Pixlr X | pixlr.com | 简易在线编辑器,含直方图 |
六、本期小结
| 维度 | 内容 |
|---|---|
| 核心 API | cv2.calcHist(img, channels, mask, histSize, ranges) |
| 输出 | numpy 数组,形状 (bins, 1) |
| 归一化 | hist / hist.sum() 或 cv2.normalize(..., cv2.NORM_MINMAX) |
| 性能最优 | 单通道场景用 np.bincount,比 cv2.calcHist 快约 5 倍 |
| 形态识别 | 单峰位置(偏暗 / 偏亮)、峰宽(对比度)、双峰(高对比度 → 020 期选阈值) |
| 衍生函数 | CDF(累积分布)→ 铺垫 022 期直方图均衡化 |
七、下期预告
第022期:直方图均衡化(Histogram Equalization)------ cv2.equalizeHist 自动对比度增强
我们将学习:
- HE 数学原理:CDF 映射到均匀分布
- 全局 HE(cv2.equalizeHist) vs 自适应 HE(CLAHE,023 期)
- 暗光照片 / 雾天图像的对比度提升
- 过度均衡化的副作用与"伪影"
- 产品实践:手机相机的"夜景模式"和"HDR"算法底层
下期不见不散~
版权声明:本文为「图像处理算法入门」系列第021期,原创首发于 CSDN。代码与配图均开源,遵循 MIT 协议。引用或转载请保留原作者与链接。