4. 单波段阈值法:近红外单波段阈值法、基于直方图分析的自动阈值选取(Otsu算法)

各位同学,今天我们来聊聊水体提取里最经典、也最基础的方法——单波段阈值法。

你可能会问,都什么年代了,还用这么简单的方法?

嗯,我个人的经验是:简单的方法往往最可靠。尤其是在做快速水质监测或者应急响应时,你根本没时间跑复杂的深度学习模型。这时候,单波段阈值法就是你的救命稻草。

4.1 为什么是近红外波段?

先说说原理。水体在近红外波段(NIR,约0.7-2.5μm)有一个非常明显的特征——几乎完全吸收

你想想看,太阳光打到水面上,近红外光基本被水分子吃掉了,反射回来的信号极弱。而植被、土壤、建筑物这些地物,在近红外波段反射率都很高。

这就形成了一个天然的对比度。说白了,就是水是黑的,其他东西是亮的

核心公式:

如果 NIR 反射率 < 阈值 T,则判定为水体;否则为非水体。

我在项目中遇到过这样的情况:用Landsat 8的Band 5(近红外)做太湖的水体提取,阈值设在0.1左右,效果出奇的好。当然,不同卫星、不同季节,阈值要微调。

4.2 手动阈值法:简单但需要经验

手动选阈值,说白了就是靠眼睛看、靠经验猜。

具体怎么做?

  1. 加载近红外波段影像
  2. 查看影像的灰度分布
  3. 找一个能区分水体和陆地的值
  4. 反复试,直到满意为止

听起来很粗糙对吧?但说实话,我做了十年遥感,有时候还是靠这招。尤其是在数据质量不好的时候,自动算法反而容易翻车。

我的小技巧:

先取一个水体样本点,记录它的NIR值。再取一个陆地样本点,记录它的NIR值。阈值就取这两个值的中间数。简单粗暴,但有效。

4.3 Otsu算法:让计算机帮你选阈值

手动选阈值太累了,而且不同影像要调不同参数。这时候,Otsu算法就派上用场了。

Otsu算法,也叫最大类间方差法。它的核心思想是:找到一个阈值,让水体和陆地的像素分布差异最大。

为什么会这样?

你想想看,如果阈值选得好,水体像素都集中在低值区,陆地像素都集中在高值区,两类之间的方差就很大。如果阈值选得不好,两类像素混在一起,方差就小。

Otsu算法就是遍历所有可能的阈值,找到那个让类间方差最大的值。

Otsu算法步骤:

  1. 计算影像的直方图(每个灰度级的像素个数)
  2. 遍历所有可能的阈值 t(从0到255)
  3. 对于每个 t,计算:
    • 水体类(灰度 < t)的像素比例 w0 和平均灰度 μ0
    • 陆地类(灰度 ≥ t)的像素比例 w1 和平均灰度 μ1
    • 类间方差 σ² = w0 × w1 × (μ0 - μ1)²
  4. 选择使 σ² 最大的 t 作为最优阈值

我记得第一次用Otsu算法时,心里还有点打鼓。结果跑出来一看,阈值选得比我自己调的还准。从那以后,我就成了Otsu的忠实用户。

4.4 代码实现:从理论到实战

光说不练假把式。我们来看看怎么用Python实现Otsu算法。

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

# 读取近红外波段影像(假设是单波段灰度图)
nir_image = cv2.imread('landsat_band5.tif', cv2.IMREAD_GRAYSCALE)

# 方法一:使用OpenCV内置的Otsu算法
# 注意:cv2.threshold要求输入是8位图像
_, otsu_threshold = cv2.threshold(
    nir_image, 0, 255, 
    cv2.THRESH_BINARY + cv2.THRESH_OTSU
)

print(f'Otsu算法自动选取的阈值: {_}')

# 方法二:手动实现Otsu算法(加深理解)
def otsu_threshold_manual(image):
    # 计算直方图
    hist, bins = np.histogram(image.flatten(), 256, [0, 256])
    
    # 归一化
    hist = hist / hist.sum()
    
    # 初始化
    best_threshold = 0
    best_variance = 0
    
    # 遍历所有可能的阈值
    for t in range(1, 255):
        # 水体类(低值区)
        w0 = hist[:t].sum()
        if w0 == 0:
            continue
        mu0 = np.sum(np.arange(t) * hist[:t]) / w0
        
        # 陆地类(高值区)
        w1 = hist[t:].sum()
        if w1 == 0:
            continue
        mu1 = np.sum(np.arange(t, 256) * hist[t:]) / w1
        
        # 计算类间方差
        variance = w0 * w1 * (mu0 - mu1) ** 2
        
        # 更新最优阈值
        if variance > best_variance:
            best_variance = variance
            best_threshold = t
    
    return best_threshold

manual_threshold = otsu_threshold_manual(nir_image)
print(f'手动实现的Otsu阈值: {manual_threshold}')

# 应用阈值
water_mask = nir_image < manual_threshold

# 可视化
plt.figure(figsize=(12, 4))

plt.subplot(1, 3, 1)
plt.imshow(nir_image, cmap='gray')
plt.title('近红外波段原始影像')
plt.axis('off')

plt.subplot(1, 3, 2)
plt.hist(nir_image.flatten(), bins=256, range=[0, 256])
plt.axvline(manual_threshold, color='r', linestyle='--', label=f'阈值={manual_threshold}')
plt.title('直方图与Otsu阈值')
plt.legend()

plt.subplot(1, 3, 3)
plt.imshow(water_mask, cmap='Blues')
plt.title('水体提取结果')
plt.axis('off')

plt.tight_layout()
plt.show()

避坑指南:

我曾经在山区项目里直接用Otsu算法提取水体,结果山体阴影全被识别成了水。为什么?因为阴影在近红外波段也很暗。后来我加了一个坡度掩膜,把坡度大于5度的区域排除掉,效果才好了。

所以记住:Otsu算法不是万能的,它只考虑灰度分布,不考虑空间上下文。复杂场景下需要结合其他信息。

4.5 单波段阈值法的局限性

说了这么多优点,也得讲讲缺点。单波段阈值法有几个硬伤:

问题 原因 解决办法
山体阴影误判 阴影在NIR波段反射率低,类似水体 结合DEM坡度数据
浑浊水体漏提 悬浮物增加NIR反射率 改用绿波段或SWIR波段
薄云干扰 云在NIR波段反射率高 先做云掩膜
阈值不通用 不同卫星、不同季节阈值不同 使用Otsu自适应阈值

嗯,这里要注意:单波段阈值法最适合的场景是:大面积、清洁、深水体的提取。比如大型湖泊、水库、海洋。如果是小河沟、浅滩、或者富营养化严重的水体,建议用多波段方法。

4.6 本章知识体系

为了让你更直观地理解本章的知识结构,我画了一张流程图:

单波段阈值法知识体系 单波段阈值法 水体在NIR波段强吸收 手动阈值 vs Otsu算法 水体提取与水质监测 水=黑,陆=亮 天然高对比度 手动选阈值 Otsu自动阈值 大面积水体 快速监测 核心要点 简单、快速、适合清洁深水体 局限性:阴影误判、浑浊水体漏提、阈值不通用

总结一下:

单波段阈值法,说白了就是利用水体在近红外波段的强吸收特性,用一个阈值把水陆分开。手动选阈值靠经验,Otsu算法靠数学。我个人建议:日常用Otsu,关键项目手动微调

记住,没有万能的方法。单波段阈值法虽然简单,但在很多场景下依然是最优解。尤其是当你需要快速出图、或者数据质量一般的时候,它往往比那些花里胡哨的算法更靠谱。


公众号:蓝海资料掘金营,微信deep3321