4. 直方图与对比度增强:让图像的“脾气”一目了然

各位同学,欢迎来到第四章。

前面我们聊了像素操作、空间滤波,算是把图像的“微观世界”摸了一遍。今天要讲的,是图像的“宏观统计”——直方图。说白了,直方图就是一张图里像素值的分布情况。你想想看,一张图是偏亮还是偏暗,对比度高不高,直方图上一眼就能看出来。

我个人习惯,拿到一张图第一件事就是看它的直方图。这就像医生看病人的血常规报告,很多问题在直方图上一目了然。这一章,我们就来彻底搞懂直方图,以及如何用它来给图像“提神”——也就是对比度增强。

4.1 直方图概念:图像的“人口普查”

直方图,本质上就是一个统计图。横轴是像素值(0-255),纵轴是该像素值出现的次数(或概率)。

举个例子,一张纯黑的图,直方图就只有一根柱子立在0的位置;一张纯白的图,柱子就立在255的位置。一张正常的风景照,直方图通常像一座连绵起伏的山脉,中间高两边低。

核心公式(理解即可):

对于灰度图,直方图定义为:H(r_k) = n_k,其中 r_k 是第k个灰度级,n_k 是该灰度级的像素个数。

归一化直方图:p(r_k) = n_k / N,N是总像素数。归一化后,纵轴就变成了概率,方便不同尺寸的图像进行比较。

在OpenCV里,计算直方图就一行代码的事。但我建议你,刚开始学的时候,自己用Python写一遍循环,感受一下像素是怎么被“数”出来的。

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

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

# 方法一:OpenCV 官方函数
hist = cv2.calcHist([img], [0], None, [256], [0, 256])

# 方法二:自己动手算(理解原理)
# hist_manual = np.zeros((256,), dtype=np.int32)
# for pixel in img.flatten():
#     hist_manual[pixel] += 1

# 画图
plt.plot(hist)
plt.title('灰度直方图')
plt.xlabel('像素值')
plt.ylabel('频数')
plt.show()

小技巧: cv2.calcHist 的参数里,[0] 表示通道索引(灰度图只有1个通道),[256] 表示bin的数目。如果你处理的是彩色图,可以分别计算R、G、B三个通道的直方图。

4.2 直方图均衡化:给图像“拉一把”

什么是直方图均衡化?

想象一下,一张图整体偏暗,它的直方图就都挤在左边。均衡化,就是把这些挤在一起的像素值,像拉手风琴一样,均匀地“拉开”到整个0-255区间。结果就是,暗的地方变亮了,亮的地方可能稍微暗一点,整体对比度大幅提升。

我记得有一次做水下图像增强,拍出来的鱼都是灰蒙蒙的。直方图一看,像素值全挤在50-100之间。用均衡化一处理,鱼的纹理和颜色瞬间就出来了。效果立竿见影。

OpenCV实现非常简单:

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

# 对比显示
res = np.hstack((img, equ))
cv2.imshow('Original vs Equalized', res)
cv2.waitKey(0)

但是,这里有个坑。全局直方图均衡化,它是对整张图做同一个映射。如果图像本身有很亮和很暗的区域,均衡化后,亮区可能会过曝,暗区可能会变成一片死黑。为什么会这样?因为它是全局的,它不会区分“天空”和“地面”。

避坑指南: 我曾经在一个医疗影像项目里,直接用全局均衡化处理X光片。结果骨骼边缘是清楚了,但软组织区域出现了大量噪声和伪影。后来我才意识到,对于这类图像,全局方法太“粗暴”了。这时候,就需要我们的主角——自适应直方图均衡化(CLAHE)登场了。

4.3 自适应直方图均衡化(CLAHE):精细化的“局部手术”

CLAHE,全称是 Contrast Limited Adaptive Histogram Equalization。名字很长,但核心思想很简单:

  1. 分块处理: 把图像分成一个个小方块(tile),比如8x8的网格。
  2. 局部均衡: 对每个小方块单独做直方图均衡化。
  3. 对比度限制: 这是最关键的一步。它会设定一个阈值,如果某个小方块内的直方图峰值太高(意味着噪声多),就把它“剪掉”,把剪掉的部分均匀分布到其他bin上。这就防止了噪声被过度放大。
  4. 双线性插值: 为了避免块与块之间出现明显的边界,最后会用插值算法平滑过渡。

你想想看,这就像给图像做“局部手术”,哪里暗就提亮哪里,但又不让噪声“趁虚而入”。

# 创建CLAHE对象
# clipLimit: 对比度限制阈值,默认40。值越大,对比度增强越强。
# tileGridSize: 分块大小,默认(8,8)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))

# 应用CLAHE
clahe_img = clahe.apply(img)

# 对比三种效果
res = np.hstack((img, equ, clahe_img))
cv2.imshow('Original | Global Equalization | CLAHE', res)
cv2.waitKey(0)
方法 优点 缺点 适用场景
全局直方图均衡化 简单、速度快 容易过曝、放大噪声 整体偏暗或偏亮,且光照均匀的图像
CLAHE 局部自适应、抑制噪声、效果自然 速度稍慢、参数需要调 光照不均、医学影像、水下图像、夜间图像

调参经验: clipLimit 我一般从2.0开始试。如果图像噪声大,就调小到1.0或0.5;如果希望对比度更强烈,可以调到3.0或4.0。tileGridSize 越大,效果越接近全局均衡化;越小,局部细节越丰富,但计算量也越大。

4.4 直方图比较:判断两张图的“血缘关系”

学会了画直方图,我们还能用它来比较两张图是否相似。这在图像检索、目标跟踪、视频镜头切换检测里非常有用。

OpenCV提供了 cv2.compareHist 函数,支持多种比较方法:

方法 OpenCV常量 说明
相关性 (Correlation) cv2.HISTCMP_CORREL 值越大越相似(1为完全相似,-1为完全不相似)
卡方 (Chi-Square) cv2.HISTCMP_CHISQR 值越小越相似(0为完全相似)
交叉 (Intersection) cv2.HISTCMP_INTERSECT 值越大越相似(直接计算重叠部分)
巴氏距离 (Bhattacharyya) cv2.HISTCMP_BHATTACHARYYA 值越小越相似(0为完全相似,1为完全不相似)

我个人最常用的是 巴氏距离,因为它对噪声不敏感,而且结果在0-1之间,非常直观。

# 计算两张图的直方图
hist1 = cv2.calcHist([img1], [0], None, [256], [0, 256])
hist2 = cv2.calcHist([img2], [0], None, [256], [0, 256])

# 归一化(重要!比较前一定要归一化)
hist1 = cv2.normalize(hist1, hist1).flatten()
hist2 = cv2.normalize(hist2, hist2).flatten()

# 比较
d = cv2.compareHist(hist1, hist2, cv2.HISTCMP_BHATTACHARYYA)
print(f'巴氏距离: {d:.4f}')  # 越接近0,越相似

注意: 比较之前,一定要对直方图进行归一化!否则,两张尺寸不同的图,即使内容一样,直方图的数值也会差很多,比较结果就没有意义了。我曾经在这个细节上吃过亏,排查了半天才发现是归一化的问题。

4.5 本章知识体系图

下面这张图,帮你把本章的核心逻辑串起来:

直方图与对比度增强知识体系 直方图与对比度增强 直方图概念 统计像素分布 cv2.calcHist() 全局均衡化 cv2.equalizeHist() 简单但粗暴 CLAHE 分块 + 限制对比度 cv2.createCLAHE() 直方图比较 cv2.compareHist() 相关性 卡方 交叉 巴氏距离 核心:直方图是统计工具,均衡化是增强手段,CLAHE是精细化方案,比较是应用延伸。 实战中,CLAHE + 巴氏距离是我最常用的组合。

好了,这一章的内容就是这些。从直方图的概念,到全局均衡化,再到精细的CLAHE,最后是直方图比较。每一步都有它的应用场景和坑。希望你在自己的项目里,能灵活运用这些工具,让图像“开口说话”。