fix: 推理端三项修复 — 光谱断崖 + 全零拦截 + MNF 加载诊断
【修复 1】np.interp 边缘断崖 (3 处) - _preprocess_dual_stream + preprocess_spectra 主/兼容重采样路径 - left/right 从 np.nan 改为 y_vals[0]/y_vals[-1] 恒定外推 - 彻底杜绝 NaN→0.0 造成的光谱曲线两端零值断崖 【修复 2】全零光谱像素 NaN 拦截 - 新增 _mask_zero_spectra_pixels() 静态方法 - 检测 NDWI 掩膜外的全零陆地像素,强制预测值为 NaN - 覆盖 inference_pipeline / batch_inference / batch_inference_multi_data - Kriging/IDW 插值天然忽略 NaN,制图时陆地直接留白 【新增】MNF 诊断信息在模型加载时自动输出 - _print_mnf_info_if_available() 递归查找 Pipeline 中的 MNFTransformer - load_best_model / load_specific_model 加载后自动打印精简 3 行诊断
This commit is contained in:
@ -361,6 +361,9 @@ class WaterQualityInference:
|
||||
if 'test_rmse' in metadata:
|
||||
print(f" 测试集RMSE: {metadata['test_rmse']:.4f}")
|
||||
|
||||
# ★ 加载时打印 MNF 波段选择信息(从已训练的 Pipeline 中提取)
|
||||
self._print_mnf_info_if_available()
|
||||
|
||||
def load_specific_model(self, model_file_path: str):
|
||||
"""
|
||||
加载指定的模型文件
|
||||
@ -381,6 +384,9 @@ class WaterQualityInference:
|
||||
print(f" 模型名称: {self.loaded_model_data['model_name']}")
|
||||
print(f" 模型类型: {type(self.loaded_model_data['model'])}")
|
||||
|
||||
# ★ 加载时打印 MNF 波段选择信息
|
||||
self._print_mnf_info_if_available()
|
||||
|
||||
def _auto_detect_train_wavelengths(self):
|
||||
"""自动获取训练波长:优先级 json/txt > 工作目录 CSV > None"""
|
||||
import os as _os, json as _json, glob as _glob
|
||||
@ -461,14 +467,15 @@ class WaterQualityInference:
|
||||
pass
|
||||
|
||||
# np.interp 重采样:308/113/任意波段 → 模型训练波长
|
||||
# ★ 边缘填充:left/right 使用当前行首尾有效值,杜绝 NaN→0.0 断崖
|
||||
spec_data = spectra[spec_cols].values.astype(np.float64)
|
||||
src_wl = np.array([float(c) for c in spec_cols], dtype=np.float64)
|
||||
dst_wl = np.array(train_wl, dtype=np.float64)
|
||||
resampled = np.zeros((spec_data.shape[0], len(dst_wl)), dtype=np.float64)
|
||||
for i in range(spec_data.shape[0]):
|
||||
resampled[i] = np.interp(dst_wl, src_wl, spec_data[i],
|
||||
left=np.nan, right=np.nan)
|
||||
resampled = np.nan_to_num(resampled, nan=0.0)
|
||||
y_vals = spec_data[i]
|
||||
resampled[i] = np.interp(dst_wl, src_wl, y_vals,
|
||||
left=y_vals[0], right=y_vals[-1])
|
||||
|
||||
result = pd.DataFrame(resampled,
|
||||
columns=[f'{wl:.6f}' for wl in train_wl])
|
||||
@ -572,28 +579,12 @@ class WaterQualityInference:
|
||||
resampled = np.zeros((spectral_data.shape[0], len(train_wavelengths)),
|
||||
dtype=np.float64)
|
||||
for i in range(spectral_data.shape[0]):
|
||||
y_vals = spectral_data[i]
|
||||
resampled[i] = np.interp(
|
||||
train_wl_arr, target_wl_arr, spectral_data[i],
|
||||
left=np.nan, right=np.nan
|
||||
train_wl_arr, target_wl_arr, y_vals,
|
||||
left=y_vals[0], right=y_vals[-1]
|
||||
)
|
||||
|
||||
# NaN 填充(外推区域):用最近有效值填充
|
||||
nan_mask = np.isnan(resampled)
|
||||
if nan_mask.any():
|
||||
print(f"[光谱重采样] {nan_mask.sum()} 个 NaN (外推区域) → 最近邻填充")
|
||||
for i in range(resampled.shape[0]):
|
||||
row_nan = np.isnan(resampled[i])
|
||||
if row_nan.any():
|
||||
valid = ~row_nan
|
||||
if valid.any():
|
||||
valid_idx = np.where(valid)[0]
|
||||
resampled[i, row_nan] = np.interp(
|
||||
np.where(row_nan)[0], valid_idx,
|
||||
resampled[i, valid]
|
||||
)
|
||||
else:
|
||||
resampled[i] = 0.0
|
||||
|
||||
# 4) 重组为 DataFrame(列名 = 训练波长字符串)
|
||||
wl_col_names = [f"{wl:.6f}" for wl in train_wavelengths]
|
||||
resampled_df = pd.DataFrame(resampled, columns=wl_col_names,
|
||||
@ -669,24 +660,11 @@ class WaterQualityInference:
|
||||
_resampled = np.zeros((_spec_data.shape[0], expected_features),
|
||||
dtype=np.float64)
|
||||
for i in range(_spec_data.shape[0]):
|
||||
_y_vals = _spec_data[i]
|
||||
_resampled[i] = np.interp(
|
||||
fallback_wl, _target_arr, _spec_data[i],
|
||||
left=np.nan, right=np.nan
|
||||
fallback_wl, _target_arr, _y_vals,
|
||||
left=_y_vals[0], right=_y_vals[-1]
|
||||
)
|
||||
# NaN 填充
|
||||
_nan = np.isnan(_resampled)
|
||||
if _nan.any():
|
||||
for i in range(_resampled.shape[0]):
|
||||
row_nan = np.isnan(_resampled[i])
|
||||
if row_nan.any():
|
||||
valid = ~row_nan
|
||||
if valid.any():
|
||||
vi = np.where(valid)[0]
|
||||
_resampled[i, row_nan] = np.interp(
|
||||
np.where(row_nan)[0], vi, _resampled[i, valid]
|
||||
)
|
||||
else:
|
||||
_resampled[i] = 0.0
|
||||
|
||||
# 重组 DataFrame
|
||||
_wl_cols = [f"{w:.1f}" for w in fallback_wl]
|
||||
@ -837,6 +815,72 @@ class WaterQualityInference:
|
||||
f"({(n_clipped_lo + n_clipped_hi) / len(predictions) * 100:.1f}%)")
|
||||
return np.clip(predictions, lo, hi)
|
||||
|
||||
def _print_mnf_info_if_available(self):
|
||||
"""从已加载的 Pipeline 模型中提取 MNFTransformer 并打印波段选择信息。"""
|
||||
if self.loaded_model_data is None:
|
||||
return
|
||||
model = self.loaded_model_data.get('model')
|
||||
if model is None:
|
||||
return
|
||||
|
||||
from sklearn.pipeline import Pipeline, FeatureUnion
|
||||
if not isinstance(model, Pipeline):
|
||||
return
|
||||
|
||||
# 递归查找 MNFTransformer
|
||||
def _find_mnf(step):
|
||||
if step.__class__.__name__ == 'MNFTransformer':
|
||||
return step
|
||||
if isinstance(step, FeatureUnion):
|
||||
for _name, _trans in step.transformer_list:
|
||||
found = _find_mnf(_trans)
|
||||
if found is not None:
|
||||
return found
|
||||
if isinstance(step, Pipeline):
|
||||
for _sub_name, _sub_step in step.steps:
|
||||
found = _find_mnf(_sub_step)
|
||||
if found is not None:
|
||||
return found
|
||||
return None
|
||||
|
||||
mnf = _find_mnf(model)
|
||||
if mnf is not None and hasattr(mnf, 'eigvals_w_'):
|
||||
target_name = self.loaded_model_data.get('target_column_name', '')
|
||||
if not target_name and 'metadata' in self.loaded_model_data:
|
||||
target_name = self.loaded_model_data['metadata'].get('target_column_name', '')
|
||||
if target_name:
|
||||
print(f"\n [MNF 波段信息] 目标指数: {target_name}")
|
||||
mnf._print_band_selection()
|
||||
|
||||
@staticmethod
|
||||
def _mask_zero_spectra_pixels(spectra: pd.DataFrame,
|
||||
predictions: np.ndarray) -> np.ndarray:
|
||||
"""将全零光谱行(NDWI 掩膜外的陆地像素)的预测值强制设为 NaN。
|
||||
|
||||
GUI 制图 / Kriging / IDW 插值天然忽略 NaN,这是最安全的掩膜继承方式。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
spectra : pd.DataFrame
|
||||
原始光谱 DataFrame(shape: n_pixels × n_bands)
|
||||
predictions : np.ndarray
|
||||
模型预测值数组(shape: n_pixels)
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.ndarray
|
||||
处理后的预测值数组
|
||||
"""
|
||||
all_zero = (spectra == 0).all(axis=1)
|
||||
n_zero = int(all_zero.sum())
|
||||
if n_zero == 0:
|
||||
return predictions
|
||||
predictions = predictions.astype(np.float64, copy=True)
|
||||
predictions[all_zero.values] = np.nan
|
||||
print(f"[全零拦截] 检测到 {n_zero} 个全零光谱像素 "
|
||||
f"({n_zero / len(predictions) * 100:.1f}%),预测值已设为 NaN")
|
||||
return predictions
|
||||
|
||||
def save_predictions(self, coords: pd.DataFrame, predictions: np.ndarray,
|
||||
output_path: str, prediction_column: str = 'prediction',
|
||||
wqi_columns: Optional[pd.DataFrame] = None):
|
||||
@ -947,6 +991,9 @@ class WaterQualityInference:
|
||||
print("-" * 40)
|
||||
predictions = self.predict(spectra_processed)
|
||||
|
||||
# ★ 全零光谱拦截:NDWI 掩膜外的陆地像素 → NaN
|
||||
predictions = self._mask_zero_spectra_pixels(spectra, predictions)
|
||||
|
||||
# 5. 保存预测结果(透传 WQI 列至最终输出文件)
|
||||
print("\n步骤5: 保存预测结果")
|
||||
print("-" * 40)
|
||||
@ -1024,6 +1071,7 @@ class WaterQualityInference:
|
||||
coords, spectra, wqi_df = self.load_sampling_data(str(csv_file))
|
||||
spectra_processed = self.preprocess_spectra(spectra)
|
||||
predictions = self.predict(spectra_processed)
|
||||
predictions = self._mask_zero_spectra_pixels(spectra, predictions)
|
||||
result_df = self.save_predictions(coords, predictions, str(output_file),
|
||||
prediction_column, wqi_df)
|
||||
|
||||
@ -1223,6 +1271,7 @@ class WaterQualityInference:
|
||||
coords, spectra, wqi_df = self.load_sampling_data(str(csv_file))
|
||||
spectra_processed = self.preprocess_spectra(spectra)
|
||||
predictions = self.predict(spectra_processed)
|
||||
predictions = self._mask_zero_spectra_pixels(spectra, predictions)
|
||||
result_df = self.save_predictions(coords, predictions, str(output_file),
|
||||
prediction_column, wqi_df)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user