fix: 补齐缺失的 handler 文件 + IDW 插值退化检测 + V1 代码归档

This commit is contained in:
duxin
2026-07-06 15:57:29 +08:00
parent f3aca09df4
commit c46f78e69d
5 changed files with 2101 additions and 127 deletions

View File

@ -556,7 +556,14 @@ class ContentMapper:
return grid_content
def _perform_interpolation(self, points, values, grid_xx, grid_yy):
"""执行空间插值"""
"""三级降级插值策略:Kriging → IDW → 最近邻
2026-07-01 重构:
- Kriging 优先(自动拟合球形变异函数,不强制 nugget)
- Kriging 退化检测:若结果标准差接近 0(纯色图),自动回退 IDW
- IDW 作为首选回退:无需拟合变异函数,不会产生纯色图
- scipy linear/nearest 作为最后兜底
"""
print(f"插值输入检查:")
print(f" - 数据点数量: {len(points)}")
print(f" - 数据值范围: {values.min():.4f} - {values.max():.4f}")
@ -573,149 +580,145 @@ class ContentMapper:
if len(points) < 3:
raise ValueError(f"有效数据点不足3个(当前:{len(points)}个)")
# 优先使用Kriging插值
# ── 策略 0:值域极窄时直接跳过 Kriging ──
value_range = float(values.max() - values.min())
value_std = float(np.std(values))
# ═══════════════════════════════════════════════════════════
# 策略 1:Kriging(自动拟合球形变异函数)
# ═══════════════════════════════════════════════════════════
kriging_degraded = False
if PYKRIGE_AVAILABLE:
try:
print("正在使用Kriging插值(半变异函数模型,块金值=100%)...")
print("正在使用 Kriging 插值(球形模型,自动拟合 nugget)...")
grid_x = grid_xx[0, :]
grid_y = grid_yy[:, 0]
ok = OrdinaryKriging(
points[:, 0], points[:, 1], values,
variogram_model='spherical',
verbose=False,
enable_plotting=False
enable_plotting=False,
)
# ★ 局部邻域 Kriging:只参考最近的 15 个点,避免万阶矩阵求逆
z, _ = ok.execute('grid', grid_x, grid_y, backend='loop', n_closest_points=15)
z, ss = ok.execute('grid', grid_x, grid_y, backend='loop', n_closest_points=15)
grid_content = np.array(z)
valid_count = np.sum(~np.isnan(grid_content))
print(f"Kriging插值成功,有效点数: {valid_count} / {grid_content.size}")
if valid_count > 0:
return grid_content
else:
print("警告:Kriging插值结果为空,将回退到其他插值方法")
except Exception as e:
print(f"Kriging插值失败: {e},将回退到其他插值方法")
else:
print("警告:pykrige未安装,无法使用Kriging插值,将使用其他插值方法")
valid_mask = ~np.isnan(grid_content)
valid_count = int(np.sum(valid_mask))
if valid_count > 0:
# ★ 退化检测:若插值结果标准差 < 原始数据标准差的 5%,判定为纯色图
kriging_std = float(np.nanstd(grid_content))
degradation_ratio = kriging_std / max(value_std, 1e-12)
print(f"Kriging 完成: 有效点={valid_count}/{grid_content.size}, "
f"输出std={kriging_std:.6f}, 退化比={degradation_ratio:.3f}")
if degradation_ratio < 0.05 and value_range > 1e-8:
print(f"⚠ Kriging 严重退化(输出 std/输入 std={degradation_ratio:.3f}<5%),"
f"判定为纯色图,回退 IDW")
kriging_degraded = True
else:
print(f"Kriging 通过退化检测,直接使用")
return grid_content
else:
print("Kriging 结果全为 NaN,回退")
kriging_degraded = True
except Exception as e:
print(f"Kriging 失败: {e}")
kriging_degraded = True
else:
print("pykrige 未安装,跳过 Kriging")
kriging_degraded = True
# ═══════════════════════════════════════════════════════════
# 策略 2:IDW(反距离权重)— 不需要拟合变异函数,绝不纯色
# ═══════════════════════════════════════════════════════════
if kriging_degraded:
try:
print("正在使用 IDW 插值(反距离权重, power=2, neighbors=15)...")
grid_content = self._idw_interpolation(
points, values, grid_xx, grid_yy,
power=2, n_neighbors=min(15, len(points)),
)
valid_count = int(np.sum(~np.isnan(grid_content)))
if valid_count > 0:
idw_std = float(np.nanstd(grid_content))
print(f"IDW 完成: 有效点={valid_count}/{grid_content.size}, 输出std={idw_std:.6f}")
if idw_std > 0:
return grid_content
else:
print("IDW std=0(所有输入值完全相同),结果可用")
return grid_content
else:
print("IDW 结果全为 NaN,回退 scipy 插值")
except Exception as e:
print(f"IDW 失败: {e},回退 scipy 插值")
# ═══════════════════════════════════════════════════════════
# 策略 3:scipy 线性插值 + 最近邻填充(最终兜底)
# ═══════════════════════════════════════════════════════════
try:
# 首先尝试使用线性插值
print("正在尝试线性插值...")
print("正在尝试 scipy 线性插值...")
grid_content = griddata(
points, values, (grid_xx, grid_yy),
method='linear', fill_value=np.nan
)
# 检查线性插值结果
valid_linear = ~np.isnan(grid_content)
valid_count = np.sum(valid_linear)
print(f"线性插值结果:有效点数 {valid_count} / {grid_content.size}")
valid_count = int(np.sum(~np.isnan(grid_content)))
print(f"线性插值: 有效点={valid_count}/{grid_content.size}")
if valid_count > 0:
print(f"线性插值成功,有效区域覆盖率: {valid_count / grid_content.size * 100:.1f}%")
# 如果有NaN值,用最近邻插值填充
nan_count = np.sum(np.isnan(grid_content))
nan_count = int(np.sum(np.isnan(grid_content)))
if nan_count > 0:
print(f"正在用最近邻插值填充 {nan_count} 个缺失值...")
print(f"用最近邻填充 {nan_count} 个 NaN...")
grid_nearest = griddata(
points, values, (grid_xx, grid_yy),
method='nearest'
points, values, (grid_xx, grid_yy), method='nearest'
)
# 只填充线性插值的NaN区域
nan_mask = np.isnan(grid_content)
grid_content[nan_mask] = grid_nearest[nan_mask]
print("缺失值填充完成")
# 最终检查
final_valid = ~np.isnan(grid_content)
print(f"最终有效点数: {np.sum(final_valid)} / {grid_content.size}")
grid_content[np.isnan(grid_content)] = grid_nearest[np.isnan(grid_content)]
return grid_content
else:
print("线性插值失败,尝试最近邻插值...")
except Exception as e:
print(f"线性插值失败: {e}")
print("尝试最近邻插值...")
try:
# 使用最近邻插值作为备选方案
print("执行最近邻插值...")
grid_content = griddata(
points, values, (grid_xx, grid_yy),
method='nearest'
)
# ═══════════════════════════════════════════════════════════
# 策略 4:最近邻(绝对兜底)
# ═══════════════════════════════════════════════════════════
print("执行最近邻插值(最终兜底)...")
grid_content = griddata(
points, values, (grid_xx, grid_yy), method='nearest'
)
if np.sum(~np.isnan(grid_content)) == 0:
raise ValueError("所有插值方法均失败")
return grid_content
valid_count = np.sum(~np.isnan(grid_content))
print(f"最近邻插值成功,有效点数: {valid_count}")
@staticmethod
def _idw_interpolation(points, values, grid_xx, grid_yy,
power=2, n_neighbors=15):
"""IDW(反距离权重)插值 — 不需拟合模型,绝不产生纯色图。
if valid_count == 0:
raise ValueError("最近邻插值也没有产生有效结果")
Parameters:
points: (N, 2) 采样点坐标
values: (N,) 采样点值
grid_xx, grid_yy: meshgrid 网格
power: 距离衰减幂参数(默认 2)
n_neighbors: 每个网格点参考的最近邻数量
"""
from scipy.spatial import cKDTree
grid_shape = grid_xx.shape
grid_flat = np.column_stack((grid_xx.ravel(), grid_yy.ravel()))
values_flat = values.ravel()
return grid_content
except Exception as e:
print(f"最近邻插值也失败: {e}")
# 对于地理坐标系,尝试更简单的方法
if self.output_crs == 'EPSG:4326':
print("地理坐标系检测到,尝试简化插值...")
try:
# 创建一个基于距离的简单插值
grid_content = np.full(grid_xx.shape, np.nan)
# 为每个网格点找到最近的数据点
for i in range(grid_xx.shape[0]):
for j in range(grid_xx.shape[1]):
grid_x, grid_y = grid_xx[i, j], grid_yy[i, j]
# 计算到所有数据点的距离
distances = np.sqrt((points[:, 0] - grid_x) ** 2 + (points[:, 1] - grid_y) ** 2)
nearest_idx = np.argmin(distances)
# 如果距离不是太远,就使用该值
if distances[nearest_idx] < (grid_xx.max() - grid_xx.min()) * 0.1: # 10%的范围内
grid_content[i, j] = values[nearest_idx]
valid_count = np.sum(~np.isnan(grid_content))
print(f"简化插值完成,有效点数: {valid_count}")
if valid_count > 0:
return grid_content
else:
raise ValueError("简化插值也没有产生有效结果")
except Exception as e3:
print(f"简化插值失败: {e3}")
print("尝试立方插值作为最后手段...")
try:
# 最后尝试立方插值
grid_content = griddata(
points, values, (grid_xx, grid_yy),
method='cubic', fill_value=np.nan
)
# 如果立方插值有NaN,用最近邻填充
if np.any(np.isnan(grid_content)):
print("用最近邻插值填充立方插值的NaN值...")
grid_nearest = griddata(
points, values, (grid_xx, grid_yy),
method='nearest'
)
nan_mask = np.isnan(grid_content)
grid_content[nan_mask] = grid_nearest[nan_mask]
valid_count = np.sum(~np.isnan(grid_content))
print(f"立方插值成功,有效点数: {valid_count}")
return grid_content
except Exception as e4:
print(f"立方插值也失败: {e4}")
print(f"所有插值方法都失败")
raise ValueError("无法完成空间插值,请检查数据点的分布和数值")
tree = cKDTree(points)
k = min(n_neighbors, len(points))
distances, indices = tree.query(grid_flat, k=k)
# 防止距离为 0 的除零
distances = np.maximum(distances, 1e-12)
weights = 1.0 / (distances ** power)
# 归一化权重
weights /= weights.sum(axis=1, keepdims=True)
# 加权求和
neighbor_vals = values_flat[indices] if k == 1 else values_flat[indices]
if k == 1:
result = neighbor_vals
else:
result = np.sum(weights * neighbor_vals, axis=1)
return result.reshape(grid_shape)
def read_csv_data(self, csv_file, uncertainty_col=None):
"""
@ -2514,28 +2517,30 @@ class ContentMapper:
safe_h = min(float(figsize[1]), _max_inch)
fig, ax = plt.subplots(figsize=(safe_w, safe_h))
# 计算有效值统计(2σ 标准差拉伸,排除长尾异常值干扰)
valid = array[~np.isnan(array)]
# 1. 明确排除 NaN 以及 nodata_value(与函数参数保持一致)
nodata_val = nodata_value
valid = array[(~np.isnan(array)) & (array != nodata_val)]
if valid.size == 0:
raise ValueError("GeoTIFF 中没有有效数据(全部为 NoData)")
mean_val = float(np.nanmean(array))
std_val = float(np.nanstd(array))
vmin = max(float(np.nanmin(array)), mean_val - 2 * std_val)
vmax = min(float(np.nanmax(array)), mean_val + 2 * std_val)
# 2. 改用更鲁棒的 2%-98% 百分位拉伸(抗偏态分布)
vmin = float(np.percentile(valid, 2))
vmax = float(np.percentile(valid, 98))
if (vmax - vmin) < 1e-9:
center = mean_val
if (vmax - vmin) < 1e-9: # 防退化:区间过窄 → 取中心 ±1%
center = vmin
exp = max(abs(center) * 0.01, 1e-9)
vmin = center - exp
vmax = center + exp
print(f"[visualize_raster] 2σ 拉伸: vmin={vmin:.4f}, vmax={vmax:.4f},"
f"mean={mean_val:.4f}, std={std_val:.4f},有效像元: {valid.size}/{array.size}")
print(f"[visualize_raster] P2-P98 拉伸: vmin={vmin:.4f}, vmax={vmax:.4f},"
f"有效像元: {valid.size}/{array.size}")
# ── 栅格绘图 ─────────────────────────────────────────────────
# 使用 masked array:NaN 区域自动不显示
masked_data = np.ma.masked_invalid(array)
# 不仅要屏蔽 NaN,如果 array 中还有 nodata_value,也必须 mask 掉,否则出图时背景会被渲染
masked_data = np.ma.masked_where((np.isnan(array)) | (array == nodata_val), array)
# 【核心修复2】废弃错误的坐标映射逻辑。
# 直接使用原生的 imshow,明确告知 matplotlib 第0行在最上方(origin='upper')