Files
WQ_GUI/src/utils/lapulasi_otsu.py
2026-04-08 15:25:08 +08:00

418 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import numpy as np
import cv2
from osgeo import gdal
from collections import Counter
from typing import Optional, Union, Tuple
gdal.UseExceptions()
def laplacian_filter(image):
"""
拉普拉斯算子纹理提取
使用二阶偏微分对图像进行卷积,获取纹理图像
公式: η²g(m,n) = g(m,n+1) + g(m,n-1) + g(m+1,n) + g(m-1,n) - 4g(m,n)
参数:
image: 输入图像全波段叠加后的灰度图像F
返回:
L: 拉普拉斯滤波后的纹理图像
"""
# 拉普拉斯算子核4邻域中心5个像元
kernel = np.array([[0, 1, 0],
[1, -4, 1],
[0, 1, 0]], dtype=np.float32)
filtered_image = cv2.filter2D(image, -1, kernel)
return filtered_image
def apply_threshold(image, threshold):
"""
二值化处理(分割耀光区域)
公式:
- 对于L: W = {1, L>0; 0, L≤0}
- 对于F: S = {1, F>N; 0, F≤N}
参数:
image: 输入图像
threshold: 阈值
返回:
binary_image: 二值化图像0或1
"""
_, binary_image = cv2.threshold(image, threshold, 1, cv2.THRESH_BINARY)
return binary_image
def morphological_dilation(image, iterations=1):
"""
形态学处理(膨胀操作)
对耀光纹理区域进行扩展,使得检测到的耀光区域更加连贯
参数:
image: 输入二值图像
iterations: 膨胀迭代次数
返回:
dilated_image: 膨胀后的图像
"""
kernel = np.ones((3, 3), np.uint8)
dilated_image = cv2.dilate(image, kernel, iterations=iterations)
return dilated_image
def calculate_area_difference(W, S):
"""
计算面积差值
公式: rq = area(W) - area(S)
其中 area(W) 表示拉普拉斯纹理提取区域的面积(用于定位耀光位置)
area(S) 表示通过阈值分割全波段叠加图像得到的耀光区域面积
参数:
W: 纹理提取区域(二值图像)
S: 阈值分割区域(二值图像)
返回:
rq: 面积差值
area_W: 纹理提取区域面积
area_S: 阈值分割区域面积
"""
area_W = np.sum(W)
area_S = np.sum(S)
rq = area_W - area_S
return rq, area_W, area_S
def multi_band_weighted_sum(image_bands, water_mask):
"""
全波段叠加:将所有波段的遥感反射率加权叠加形成一个灰度图像
公式: F = Σ(i=1 to n) R(λi) × G
其中 G 为水体二值化图像F 为灰度图像
参数:
image_bands: 多波段图像数组,形状为 (rows, cols, bands)
water_mask: 水体二值化图像 G形状为 (rows, cols)值为0或1
返回:
F: 全波段加权叠加后的灰度图像
"""
# 确保water_mask是二值化的0或1
if water_mask.dtype != np.float32 and water_mask.dtype != np.float64:
water_mask = water_mask.astype(np.float32)
# 对每个波段进行加权叠加F = Σ R(λi) × G
F = np.zeros((image_bands.shape[0], image_bands.shape[1]), dtype=np.float32)
for band_idx in range(image_bands.shape[2]):
F += image_bands[:, :, band_idx] * water_mask
return F
def find_optimal_threshold(F, u=0.1, q=50, r=20, max_iterations=None):
"""
通过迭代找到最佳阈值
算法原理:
1. 使用拉普拉斯算子提取耀光纹理信息(用于定位耀光的位置)
2. 通过全波段叠加图像F进行阈值分割提取耀光面积
3. 当纹理提取区域的面积与通过阈值分割得到的耀光区域面积差最小时,确定最佳的阈值
步骤:
1. 对全波段叠加图像F进行拉普拉斯纹理提取得到L
2. 对L进行二值化W = {1, L>0; 0, L≤0}(用于定位耀光位置)
3. 对W和S进行形态学膨胀r次
4. 设定阈值N的初始值为F的最小值表示为Nf
5. 对初始值叠加数值u更新阈值迭代q次
6. 每次迭代记录W与S的面积差值到r行、q列的数组R
7. 寻找R中每列数组中最小值所在行数形成新的数组aind
8. 统计aind的众数得到M即为最佳的叠加次数
9. 最佳阈值Nf = min(F) + u·M
参数:
F: 全波段加权叠加后的灰度图像
u: 阈值更新步长默认0.1(论文参数)
q: 迭代次数默认50论文参数
r: 形态学膨胀次数默认20论文参数
max_iterations: 最大迭代次数如果指定则使用此值替代q
返回:
optimal_threshold: 最佳阈值Nf
optimal_S: 最佳阈值对应的耀光区域S通过阈值分割F得到
optimal_W: 最佳阈值对应的纹理区域W通过拉普拉斯提取得到
M: 最佳迭代次数索引
area_differences: 面积差值矩阵R (r行, q列)
thresholds: 每次迭代的阈值列表
"""
# 如果指定了max_iterations使用它替代q
if max_iterations is not None:
q = max_iterations
# 初始化阈值为影像非0最小值
F_nonzero = F[F > 0]
if len(F_nonzero) > 0:
min_value = np.min(F_nonzero)
else:
# 如果所有值都为0使用一个很小的正数作为最小值
min_value = np.finfo(np.float32).eps
print("警告: F中所有值都为0使用极小值作为最小值")
# 步骤1: 对全波段叠加图像F进行拉普拉斯纹理提取用于定位耀光位置
print("进行拉普拉斯纹理提取...")
L = laplacian_filter(F)
# 步骤2: 对L进行二值化得到纹理区域W
# W = {1, L>0; 0, L≤0}
W = apply_threshold(L, 0.0) # 阈值为0即L>0为1L≤0为0
# 步骤3: 对W进行形态学膨胀r次
print(f"对纹理区域W进行形态学膨胀{r}次...")
W_dilated = morphological_dilation(W, iterations=r)
# 存储每次迭代的面积差值r行q列
# 注意论文中r是膨胀次数但这里R矩阵的r行应该对应不同的膨胀次数
# 根据论文描述应该是迭代q次每次记录面积差值
# 但论文提到"叠加q次"和"一共迭代r次"这里理解为迭代q次每次对W和S都膨胀r次
area_differences = [] # 存储每次迭代的面积差值
thresholds = []
W_masks = []
S_masks = []
print(f"开始迭代计算最佳阈值(迭代{q}步长u={u}...")
# 迭代更新阈值迭代q次
for i in range(q):
# 当前阈值Nf = min(F) + u·(i+1)
current_threshold = min_value + u * (i + 1)
thresholds.append(current_threshold)
# 步骤4: 对全波段叠加图像F进行阈值分割得到耀光区域S用于提取耀光面积
# S = {1, F>N; 0, F≤N}
S = apply_threshold(F, current_threshold)
# 步骤5: 对S进行形态学膨胀r次
S_dilated = morphological_dilation(S, iterations=r)
# 步骤6: 计算面积差值:纹理提取区域面积 vs 阈值分割区域面积
rq, area_W, area_S = calculate_area_difference(W_dilated, S_dilated)
area_differences.append(rq)
W_masks.append(W_dilated.copy())
S_masks.append(S_dilated.copy())
if (i + 1) % 10 == 0:
print(f" 迭代 {i+1}/{q}: 阈值={current_threshold:.4f}, 面积差值={rq:.2f}")
# 步骤7: 寻找R中每列数组中最小值所在行数
# 注意:论文中提到"r行、q列的数组R"但根据算法描述应该是q次迭代
# 这里理解为将area_differences重新组织成矩阵形式如果需要
# 但根据论文描述,应该是直接找到最小面积差值对应的迭代次数
area_differences_array = np.array(area_differences)
# 步骤8: 找到最小面积差值所在的行数(索引)
# argmin_q(R(r,q)):找到最小面积差值所在的行数
min_indices = np.where(area_differences_array == np.min(area_differences_array))[0]
# 步骤9: 通过众数统计找到最频繁出现的行数确定最佳的叠加次数M
# Mode(aind):通过众数统计找到最频繁出现的行数
if len(min_indices) > 0:
# 如果最小值出现多次,使用众数统计
counter = Counter(min_indices)
most_common = counter.most_common(1)[0]
M = most_common[0] # 最佳迭代次数索引从0开始
else:
M = 0
# 步骤10: 计算最终阈值
# Nf = min(F) + u·M
# 注意M是索引从0开始所以实际迭代次数是M+1
optimal_threshold = min_value + u * (M + 1)
# 获取最佳阈值对应的掩膜
optimal_W = W_masks[M]
optimal_S = S_masks[M]
print(f"最佳迭代次数索引: M={M} (第{M+1}次迭代)")
print(f"最佳阈值: Nf={optimal_threshold:.4f}")
print(f"最小面积差值: {np.min(area_differences_array):.2f}")
return optimal_threshold, optimal_S, optimal_W, M, area_differences, thresholds
def generate_glint_mask(bsq_file, water_mask=None, u=0.1, q=50, r=20, max_iterations=None, output_file=None):
"""
生成耀光掩膜
算法流程:
1. 全波段叠加F = Σ(i=1 to n) R(λi) × GG为水体二值化图像
2. 拉普拉斯算子提取纹理信息(用于定位耀光位置)
3. 通过阈值分割全波段叠加图像提取耀光面积
4. 当纹理提取区域面积与阈值分割区域面积差最小时,确定最佳阈值
参数:
bsq_file: 输入的BSQ文件路径
water_mask: 水体二值化图像G可以是
- None: 自动生成基于所有像素即全为1的掩膜
- numpy数组: 直接使用数组作为掩膜,形状为 (rows, cols)值为0或1
- 文件路径: 栅格文件路径(.tif/.dat),将自动读取
u: 阈值更新步长默认0.1(论文参数)
q: 迭代次数默认50论文参数
r: 形态学膨胀次数默认20论文参数
max_iterations: 最大迭代次数如果指定则使用此值替代q
output_file: 输出文件路径如果为None则自动生成
返回:
tuple: (耀光掩膜文件路径, 纹理提取图像文件路径)
- 耀光掩膜文件路径: 通过阈值分割全波段叠加图像得到的最终掩膜S掩膜
- 纹理提取图像文件路径: 拉普拉斯纹理提取后的二值化图像W掩膜
"""
# 读取BSQ文件
bsq_dataset = gdal.Open(bsq_file)
if bsq_dataset is None:
raise ValueError(f"无法打开文件: {bsq_file}")
# 获取影像数据
bands = bsq_dataset.RasterCount
rows = bsq_dataset.RasterYSize
cols = bsq_dataset.RasterXSize
print(f"影像尺寸: {rows} x {cols}, 波段数: {bands}")
# 读取所有波段
print("正在读取所有波段数据...")
image_bands = np.zeros((rows, cols, bands), dtype=np.float32)
for band in range(bands):
image_bands[:, :, band] = bsq_dataset.GetRasterBand(band + 1).ReadAsArray().astype(np.float32)
if (band + 1) % 20 == 0:
print(f" 已读取 {band+1}/{bands} 个波段")
# 处理水体掩膜G
if water_mask is None:
# 如果没有提供水体掩膜使用全图所有像素为1
print("未提供水体掩膜,使用全图进行处理")
G = np.ones((rows, cols), dtype=np.float32)
elif isinstance(water_mask, np.ndarray):
# 如果直接提供了numpy数组
if water_mask.shape != (rows, cols):
raise ValueError(f"水体掩膜尺寸 {water_mask.shape} 与影像尺寸 {(rows, cols)} 不匹配")
G = water_mask.astype(np.float32)
# 确保是二值化的0或1
G = np.where(G > 0, 1.0, 0.0)
elif isinstance(water_mask, str):
# 如果是文件路径,读取文件
print(f"从文件读取水体掩膜: {water_mask}")
water_dataset = gdal.Open(water_mask)
if water_dataset is None:
raise ValueError(f"无法打开水体掩膜文件: {water_mask}")
if water_dataset.RasterXSize != cols or water_dataset.RasterYSize != rows:
raise ValueError(f"水体掩膜尺寸与影像尺寸不匹配")
G = water_dataset.GetRasterBand(1).ReadAsArray().astype(np.float32)
water_dataset = None
# 确保是二值化的0或1
G = np.where(G > 0, 1.0, 0.0)
else:
raise ValueError(f"不支持的水体掩膜类型: {type(water_mask)}")
print(f"水体掩膜统计: 水体像素数={np.sum(G)}, 总像素数={rows*cols}, 水体比例={np.sum(G)/(rows*cols)*100:.2f}%")
# 步骤1: 全波段叠加 F = Σ(i=1 to n) R(λi) × G
print("开始全波段叠加...")
F = multi_band_weighted_sum(image_bands, G)
print(f"全波段叠加完成F值范围: [{np.min(F):.4f}, {np.max(F):.4f}]")
print("开始计算最佳阈值...")
# 找到最佳阈值
optimal_threshold, glint_mask, texture_mask, optimal_iteration, area_diffs, thresholds = find_optimal_threshold(
F, u=u, q=q, r=r, max_iterations=max_iterations
)
print(f"\n=== 最佳阈值计算结果 ===")
print(f"最佳阈值: {optimal_threshold:.4f}")
print(f"最佳迭代次数: {optimal_iteration + 1}")
print(f"最小面积差值: {np.min(area_diffs):.4f}")
print(f"纹理提取区域面积: {np.sum(texture_mask)}")
print(f"阈值分割区域面积: {np.sum(glint_mask)}")
# 设置输出文件路径
if output_file is None:
output_file = 'glint_mask.tif'
# 生成纹理提取图像输出路径
texture_output_file = output_file.replace('.tif', '_texture.tif')
if texture_output_file == output_file: # 如果没有.tif扩展名
texture_output_file = output_file + '_texture.tif'
# 保存纹理提取图像W掩膜拉普拉斯提取的纹理区域
print(f"\n保存输出文件...")
driver = gdal.GetDriverByName('GTiff')
texture_dataset = driver.Create(texture_output_file, cols, rows, 1, gdal.GDT_Byte)
texture_dataset.SetGeoTransform(bsq_dataset.GetGeoTransform())
texture_dataset.SetProjection(bsq_dataset.GetProjection())
texture_mask_uint8 = (texture_mask * 255).astype(np.uint8)
texture_dataset.GetRasterBand(1).WriteArray(texture_mask_uint8)
texture_dataset = None
print(f"纹理提取图像已保存至: {texture_output_file}")
# 保存耀光掩膜S掩膜通过阈值分割全波段叠加图像得到
out_dataset = driver.Create(output_file, cols, rows, 1, gdal.GDT_Byte)
# 设置地理变换和投影信息
out_dataset.SetGeoTransform(bsq_dataset.GetGeoTransform())
out_dataset.SetProjection(bsq_dataset.GetProjection())
# 写入掩膜数据转换为0-255范围
glint_mask_uint8 = (glint_mask * 255).astype(np.uint8)
out_dataset.GetRasterBand(1).WriteArray(glint_mask_uint8)
# 关闭数据集
out_dataset = None
bsq_dataset = None
print(f"耀光掩膜已保存至: {output_file}")
return output_file, texture_output_file
# 使用示例
if __name__ == "__main__":
bsq_file = r"D:\BaiduNetdiskDownload\yaobao\test_glint.bsq" # 输入的BSQ文件
output_file = r'D:\BaiduNetdiskDownload\yaobao\glint\lapulas_otsu_glint_mask.tif'
# water_mask_file = r'path/to/water_mask.tif' # 可选:水体掩膜文件路径
# 示例1: 使用论文默认参数q=50, r=20, u=0.1
mask_file, texture_file = generate_glint_mask(
bsq_file,
water_mask=None,
u=0.1,
q=50,
r=20,
output_file=output_file
)
print(f"\n处理完成,耀光掩膜保存在: {mask_file}")
print(f"纹理提取图像保存在: {texture_file}")
# 示例2: 使用水体掩膜文件
# mask_file, texture_file = generate_glint_mask(
# bsq_file,
# water_mask=water_mask_file,
# u=0.1,
# q=50,
# r=20,
# output_file=output_file
# )
# 示例3: 使用numpy数组作为水体掩膜
# import numpy as np
# water_mask_array = np.ones((rows, cols), dtype=np.float32) # 示例全为1的掩膜
# mask_file, texture_file = generate_glint_mask(
# bsq_file,
# water_mask=water_mask_array,
# u=0.1,
# q=50,
# r=20,
# output_file=output_file
# )