4. 直方图分析:灰度直方图概念、直方图绘制、直方图均衡化、直方图反向投影

直方图分析,说白了就是给图像的像素值「算账」。你想想看,一张灰度图有256个灰度级,每个灰度级上有多少个像素?把这些数字画成柱状图,就是直方图。我刚开始做图像处理时,觉得这玩意儿太简单了,不就是个统计图嘛。直到后来做工业检测,发现直方图能直接告诉我光照是否均匀、对比度够不够、甚至能判断产品有没有缺陷——嗯,从那以后我再也不敢小看它了。

4.1 灰度直方图概念

灰度直方图描述的是图像中每个灰度级出现的频率。横轴是灰度值(0~255),纵轴是像素个数(或归一化后的概率)。

数学上,对于灰度级 rk,其概率密度为:

P(r_k) = n_k / N

其中 nk 是灰度级 rk 的像素数,N 是总像素数。

我个人习惯把直方图分成三类来看:

直方图形态 图像特征 常见场景
窄峰集中在低灰度 整体偏暗,细节丢失 曝光不足、背光拍摄
窄峰集中在高灰度 整体偏亮,过曝 强光直射、反光表面
双峰或多峰 图像包含多个亮度区域 前景+背景分离、目标检测
均匀分布 对比度高,细节丰富 理想光照下的清晰图像
我的经验:在阈值检测中,直方图的双峰特性是自动阈值选取的基础。如果直方图只有一个峰,说明图像对比度太差,这时候做阈值分割基本是白费力气。先做直方图均衡化,再考虑阈值。

4.2 直方图绘制

用Python画直方图,我推荐两种方式:OpenCV的 calcHist 和 Matplotlib的 hist。前者更底层,后者更直观。

先看OpenCV版本:

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 读取灰度图
img = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE)

# 计算直方图
# 参数:图像、通道、掩膜、bin数、范围
hist = cv2.calcHist([img], [0], None, [256], [0, 256])

# 绘制
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.imshow(img, cmap='gray')
plt.title('原始图像')
plt.axis('off')

plt.subplot(1, 2, 2)
plt.plot(hist, color='black')
plt.title('灰度直方图')
plt.xlabel('灰度级')
plt.ylabel('像素数')
plt.grid(True, alpha=0.3)
plt.show()

再看Matplotlib直接绘制:

# 更简洁的方式
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.imshow(img, cmap='gray')
plt.title('原始图像')
plt.axis('off')

plt.subplot(1, 2, 2)
plt.hist(img.ravel(), bins=256, range=[0, 256], color='gray', alpha=0.7)
plt.title('灰度直方图')
plt.xlabel('灰度级')
plt.ylabel('像素数')
plt.grid(True, alpha=0.3)
plt.show()
注意:img.ravel() 将二维数组展平为一维,否则 hist 会报错。我曾经在这里卡了半小时,一直报维度错误,后来才发现是忘了展平。

绘制直方图时,我建议加上网格线,方便观察峰值位置。另外,如果图像很大(比如4K分辨率),hist 的bins可以适当减少到128或64,绘制速度会快很多。

4.3 直方图均衡化

直方图均衡化的目标,是把原始图像的直方图变换成近似均匀分布。说白了,就是把像素值重新映射,让亮的地方更亮、暗的地方更暗,拉开对比度。

算法步骤很简单:

  1. 计算原始直方图 P(rk)
  2. 计算累积分布函数 CDF:Sk = Σj=0k P(rj)
  3. 映射到新灰度级:sk = round(Sk × 255)
  4. 用映射表替换每个像素的灰度值

OpenCV一行搞定:

# 直方图均衡化
equalized = cv2.equalizeHist(img)

# 对比显示
plt.figure(figsize=(12, 6))
plt.subplot(2, 2, 1)
plt.imshow(img, cmap='gray')
plt.title('原始图像')
plt.axis('off')

plt.subplot(2, 2, 2)
plt.hist(img.ravel(), bins=256, range=[0, 256], color='gray', alpha=0.7)
plt.title('原始直方图')
plt.grid(True, alpha=0.3)

plt.subplot(2, 2, 3)
plt.imshow(equalized, cmap='gray')
plt.title('均衡化后图像')
plt.axis('off')

plt.subplot(2, 2, 4)
plt.hist(equalized.ravel(), bins=256, range=[0, 256], color='gray', alpha=0.7)
plt.title('均衡化后直方图')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
核心结论:均衡化后的直方图并不是完全平坦的,而是近似均匀。因为灰度级是离散的,不可能做到完美均匀分布。但对比度提升效果非常明显,尤其适合低对比度图像。

不过,直方图均衡化也有坑。我做过一个项目,检测PCB板上的焊点缺陷。原始图像光照不均匀,左边暗右边亮。直接做全局均衡化,结果左边过亮、右边过暗,反而把缺陷信息淹没了。这时候需要用自适应直方图均衡化(CLAHE),限制对比度增幅,避免噪声放大。

# CLAHE 自适应直方图均衡化
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
clahe_img = clahe.apply(img)

# clipLimit 控制对比度限制,值越大增强越强
# tileGridSize 控制局部区域大小,一般8x8或16x16
我的建议:做阈值检测前,先看一眼直方图。如果直方图集中在很窄的范围内,先做CLAHE再阈值化,效果会好很多。clipLimit从2.0开始调,太大容易出噪声。

4.4 直方图反向投影

直方图反向投影,听起来高大上,其实原理很简单:用已知目标的颜色直方图,去原图中找相似区域。说白了,就是「按图索骥」。

应用场景:

  • 肤色检测:用肤色直方图在图像中找手、脸
  • 目标跟踪:用目标区域的直方图在后续帧中定位
  • 纹理识别:用纹理的灰度分布找相似区域

算法流程:

  1. 选取目标区域,计算其直方图(作为模板)
  2. 对原图每个像素,计算其灰度值在模板直方图中的概率
  3. 生成概率图,值越大表示越像目标
  4. 对概率图做阈值化,得到目标区域

OpenCV实现:

# 假设我们已经选定了目标区域 roi
roi = img[100:200, 150:250]  # 示例区域

# 计算目标直方图
roi_hist = cv2.calcHist([roi], [0], None, [256], [0, 256])
# 归一化到[0,1]区间
cv2.normalize(roi_hist, roi_hist, 0, 255, cv2.NORM_MINMAX)

# 反向投影
back_proj = cv2.calcBackProject([img], [0], roi_hist, [0, 256], 1)

# 显示结果
plt.figure(figsize=(12, 4))
plt.subplot(1, 3, 1)
plt.imshow(cv2.cvtColor(cv2.imread('example.jpg'), cv2.COLOR_BGR2RGB))
plt.title('原始图像')
plt.axis('off')

plt.subplot(1, 3, 2)
plt.imshow(back_proj, cmap='jet')
plt.title('反向投影概率图')
plt.axis('off')

plt.subplot(1, 3, 3)
# 阈值化得到二值图
_, mask = cv2.threshold(back_proj, 50, 255, cv2.THRESH_BINARY)
plt.imshow(mask, cmap='gray')
plt.title('阈值分割结果')
plt.axis('off')
plt.show()
避坑指南:我曾经用反向投影做肤色检测,结果把背景中的木纹也检测出来了。原因是木纹的灰度分布和肤色相似。解决办法是改用HSV空间的色调直方图,或者增加多个通道(如H+S)联合投影,能有效降低误检。

反向投影的阈值怎么选?我一般先看概率图的直方图,选一个能分离目标和背景的值。如果概率图峰值明显,取峰谷位置;如果分布平坦,用Otsu自动阈值。

4.5 本章知识体系

下面这张图总结了直方图分析的核心脉络,从概念到应用,每一步都有对应的技术手段:

直方图分析知识体系 灰度直方图概念 直方图绘制 直方图均衡化 直方图反向投影 calcHist plt.hist equalizeHist CLAHE calcBackProject 应用场景 光照校正 | 对比度增强 | 目标检测 | 肤色识别 | 缺陷检测 常见避坑 • 全局均衡化可能放大噪声 → 改用CLAHE • 单通道反向投影易误检 → 多通道联合投影

直方图分析是图像处理的基础工具,也是阈值检测的前置技能。掌握了直方图,你就能读懂图像的「脾气」——它偏暗还是偏亮?对比度够不够?有没有明显的目标和背景分离?这些信息,都会在直方图上一目了然。