内容部分修改

This commit is contained in:
DXC
2026-05-11 17:38:29 +08:00
parent bf4237b160
commit 170d347e21
8 changed files with 284 additions and 47 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

View File

@ -5,11 +5,12 @@ import sys
def _safe_add(path: str) -> None: def _safe_add(path: str) -> None:
if not path or not os.path.isdir(path): if not path or not os.path.isdir(path):
return return
try: if hasattr(os, "add_dll_directory"):
if hasattr(os, "add_dll_directory"): try:
os.add_dll_directory(path) os.add_dll_directory(path)
except Exception: return
pass except Exception:
pass
try: try:
os.environ["PATH"] = path + os.pathsep + os.environ.get("PATH", "") os.environ["PATH"] = path + os.pathsep + os.environ.get("PATH", "")
except Exception: except Exception:
@ -21,5 +22,4 @@ base = getattr(sys, "_MEIPASS", None)
if base: if base:
_safe_add(base) _safe_add(base)
_safe_add(os.path.join(base, "lib-dynload")) _safe_add(os.path.join(base, "lib-dynload"))
_safe_add(os.path.join(base, "DLLs")) _safe_add(os.path.join(base, "DLLs"))

View File

@ -31,7 +31,9 @@ def get_cpu_id() -> Optional[str]:
capture_output=True, capture_output=True,
text=True, text=True,
timeout=5, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
) )
cpu_id = result.stdout.strip().split("\n")[-1].strip() cpu_id = result.stdout.strip().split("\n")[-1].strip()
if cpu_id: if cpu_id:
@ -57,7 +59,9 @@ def get_motherboard_uuid() -> Optional[str]:
capture_output=True, capture_output=True,
text=True, text=True,
timeout=5, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
) )
board_uuid = result.stdout.strip().split("\n")[-1].strip() board_uuid = result.stdout.strip().split("\n")[-1].strip()
board_uuid = re.sub(r'[^a-zA-Z0-9\-]', '', board_uuid) board_uuid = re.sub(r'[^a-zA-Z0-9\-]', '', board_uuid)
@ -68,7 +72,9 @@ def get_motherboard_uuid() -> Optional[str]:
["cat", "/sys/class/dmi/id/product_uuid"], ["cat", "/sys/class/dmi/id/product_uuid"],
capture_output=True, capture_output=True,
text=True, text=True,
timeout=5 timeout=5,
stdin=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
) )
if result.returncode == 0: if result.returncode == 0:
return result.stdout.strip() return result.stdout.strip()

View File

@ -40,6 +40,10 @@ CB_AVAILABLE = False # 注释掉catboost
import sys import sys
import os import os
# PyInstaller 打包环境感知EXE 模式下强制单核,防止 Windows 派生无限重启
is_frozen_env = getattr(sys, 'frozen', False)
safe_n_jobs = 1 if is_frozen_env else -1
from src.preprocessing.spectral_Preprocessing import Preprocessing from src.preprocessing.spectral_Preprocessing import Preprocessing
@ -643,7 +647,7 @@ class WaterQualityModelingBatch:
config['params'], config['params'],
cv=cv_strategy, cv=cv_strategy,
scoring=scoring, scoring=scoring,
n_jobs=-1, n_jobs=safe_n_jobs,
verbose=1 verbose=1
) )

View File

@ -40,16 +40,19 @@ class WaterQualityInference:
self.best_model_info = None self.best_model_info = None
self.loaded_model_data = None self.loaded_model_data = None
def load_sampling_data(self, csv_path: str) -> Tuple[pd.DataFrame, pd.DataFrame]: def load_sampling_data(self, csv_path: str) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
""" """
加载sampling生成的CSV数据 加载sampling生成的CSV数据(兼容 WQI 增强版 CSV
Args: Args:
csv_path: CSV文件路径,前两列为经纬度,其余列为光谱数据 csv_path: CSV文件路径
旧版x_coord,y_coord,pixel_x,pixel_y,波长...
新版x_coord,y_coord,WQI_...,波长...
Returns: Returns:
coords: 经纬度数据 (DataFrame) coords: 经纬度数据 (DataFrame, 2列)
spectra: 光谱数据 (DataFrame) spectra: 光谱数据 (DataFrame, 跳过 WQI 列)
wqi_df: WQI 指数列 (DataFrame, 0或45列)
""" """
print(f"正在加载采样数据: {csv_path}") print(f"正在加载采样数据: {csv_path}")
@ -71,15 +74,35 @@ class WaterQualityInference:
coords = data.iloc[:, :2].copy() coords = data.iloc[:, :2].copy()
coords.columns = ['longitude', 'latitude'] coords.columns = ['longitude', 'latitude']
# 从第5列开始为光谱数据跳过第2、3、4列的其他信息 # 动态识别光谱列(兼容 sampling_spectra.csv 列顺序变更
spectra = data.iloc[:, 4:].copy() # 列名约定:波长为纯数字字符串如 "374.285004"WQI 为 "WQI_xxx" 前缀
# 旧版 CSV无WQIx_coord,y_coord,pixel_x,pixel_y,波长... → 取 [4:]
# 新版 CSV有WQIx_coord,y_coord,WQI_...,波长... → 过滤 WQI 列后取光谱
all_cols = list(data.columns)
spectral_col_indices = []
wqi_col_indices = []
for i, col in enumerate(all_cols):
col_str = str(col)
if col_str.startswith('WQI_'):
wqi_col_indices.append(i)
elif col_str.replace('.', '').lstrip('-').isdigit():
# 波长列:纯数字字符串
spectral_col_indices.append(i)
else:
# 其他元数据列x_coord/y_coord/pixel_x/pixel_y由 coords 接收
pass
# 光谱列 = 纯数字列WQI 已被排除)
spectra = data.iloc[:, spectral_col_indices].copy() if spectral_col_indices else data.iloc[:, 4:].copy()
# WQI 列(用于追加到预测结果输出)
wqi_df = data.iloc[:, wqi_col_indices].copy() if wqi_col_indices else pd.DataFrame()
print(f" 经纬度数据形状: {coords.shape}") print(f" 经纬度数据形状: {coords.shape}")
print(f" 光谱数据形状: {spectra.shape}") print(f" 光谱数据形状: {spectra.shape} (自动识别波长列,排除 {len(wqi_col_indices)} 个WQI列)")
print(f" 经纬度范围: 经度[{coords['longitude'].min():.6f}, {coords['longitude'].max():.6f}], " print(f" 经纬度范围: 经度[{coords['longitude'].min():.6f}, {coords['longitude'].max():.6f}], "
f"纬度[{coords['latitude'].min():.6f}, {coords['latitude'].max():.6f}]") f"纬度[{coords['latitude'].min():.6f}, {coords['latitude'].max():.6f}]")
return coords, spectra return coords, spectra, wqi_df
def random(self, data, label, test_ratio=0.2, random_state=123): def random(self, data, label, test_ratio=0.2, random_state=123):
""" """
@ -519,6 +542,69 @@ class WaterQualityInference:
print(f"正在应用预处理方法: {actual_preprocess_method}") print(f"正在应用预处理方法: {actual_preprocess_method}")
print(f"原始光谱数据形状: {spectra.shape}") print(f"原始光谱数据形状: {spectra.shape}")
# ---- 自动特征补全50 光谱 → 补全至模型训练时的 95 维WQI 指数) ----
# 触发条件:模型期望 n_features_in_ 个特征,但当前 spectra 列数不足
# 原因training_spectra.csv 含 50 光谱 + 45 WQIsampling_spectra.csv 只有 50 光谱
# 做法与训练端calculate_all_indices完全一致的算法列表实时补全缺失的 45 个 WQI 列
model = self.loaded_model_data['model']
expected_features = getattr(model, 'n_features_in_', None)
# ---- 自动特征补全50 光谱 → 补全至模型训练时的 n_features_in_ 维WQI 指数) ----
if expected_features is not None and spectra.shape[1] < expected_features:
print(f"[特征补全] 检测到特征缺口:当前 {spectra.shape[1]} 列 < 模型期望 {expected_features} 列,"
f"正在从光谱数据实时计算 WQI 指数...")
try:
from src.utils.water_index import WaterQualityIndexCalculator
calc = WaterQualityIndexCalculator()
# 提取纯计算方法(排除 find_closest_wavelength 和 calculate_all_indices
# 以及不返回 Series 的辅助方法)
algorithm_methods = []
for m in dir(calc):
if m.startswith('_'):
continue
if m in ['find_closest_wavelength', 'calculate_all_indices']:
continue
attr = getattr(calc, m)
if callable(attr):
algorithm_methods.append(m)
original_col_count = spectra.shape[1]
for algo_name in algorithm_methods:
try:
algo_func = getattr(calc, algo_name)
result = algo_func(spectra)
# 只追加返回 Series 且长度为样本数的合法结果
if isinstance(result, pd.Series) and len(result) == len(spectra):
spectra[algo_name] = result.values
else:
spectra[algo_name] = np.nan
except Exception:
spectra[algo_name] = np.nan
print(f"[特征补全] 完成!光谱列已扩充至 {spectra.shape[1]}"
f"(追加了 {spectra.shape[1] - original_col_count} 个 WQI 指数)")
except Exception as e:
print(f"[特征补全] 失败,将使用原始光谱特征: {e}")
# ---- 防线 1强制维度对齐物理截断----
if expected_features is not None and spectra.shape[1] > expected_features:
print(f"[精准对齐] 正在将 {spectra.shape[1]} 维特征截断为模型要求的 {expected_features}")
spectra = spectra.iloc[:, :expected_features]
elif expected_features is not None and spectra.shape[1] < expected_features:
# 维度不足时填充 0
padding_cols = expected_features - spectra.shape[1]
for i in range(padding_cols):
spectra[f'_padding_{i}'] = 0.0
print(f"[精准对齐] 特征不足,填充 {padding_cols} 列 0")
# ---- 防线 2彻底清洗无穷大数值----
# 防止 WQI 计算中除零/溢出产生 np.inf / -np.inf 导致预处理崩溃
spectra = spectra.replace([np.inf, -np.inf], np.nan)
spectra = spectra.fillna(0)
print(f"[特征对齐] 最终输入维度: {spectra.shape}")
try: try:
# 应用预处理 # 应用预处理
spectra_processed = Preprocessing(actual_preprocess_method, spectra) spectra_processed = Preprocessing(actual_preprocess_method, spectra)
@ -573,7 +659,8 @@ class WaterQualityInference:
raise raise
def save_predictions(self, coords: pd.DataFrame, predictions: np.ndarray, def save_predictions(self, coords: pd.DataFrame, predictions: np.ndarray,
output_path: str, prediction_column: str = 'prediction'): output_path: str, prediction_column: str = 'prediction',
wqi_columns: Optional[pd.DataFrame] = None):
""" """
保存预测结果 保存预测结果
@ -582,11 +669,15 @@ class WaterQualityInference:
predictions: 预测结果 predictions: 预测结果
output_path: 输出文件路径 output_path: 输出文件路径
prediction_column: 预测列名称 prediction_column: 预测列名称
wqi_columns: Optional[pd.DataFrame] = None
""" """
print(f"正在保存预测结果到: {output_path}") print(f"正在保存预测结果到: {output_path}")
# 创建结果DataFrame # 创建结果DataFrame
result_df = coords.copy() result_df = coords.copy()
# 追加 WQI 水质指数列(如 sampling_spectra.csv 注入了 45 列指数)
if wqi_columns is not None and not wqi_columns.empty:
result_df = pd.concat([result_df, wqi_columns.reset_index(drop=True)], axis=1)
result_df[prediction_column] = predictions result_df[prediction_column] = predictions
# 确保输出目录存在 # 确保输出目录存在
@ -659,10 +750,10 @@ class WaterQualityInference:
else: else:
self.load_best_model(metric=metric) self.load_best_model(metric=metric)
# 2. 加载采样数据 # 2. 加载采样数据coords=坐标, spectra=纯光谱, wqi_df=45个WQI指数列
print("\n步骤2: 加载采样数据") print("\n步骤2: 加载采样数据")
print("-" * 40) print("-" * 40)
coords, spectra = self.load_sampling_data(sampling_csv_path) coords, spectra, wqi_df = self.load_sampling_data(sampling_csv_path)
# 3. 数据预处理 # 3. 数据预处理
print("\n步骤3: 数据预处理") print("\n步骤3: 数据预处理")
@ -674,10 +765,11 @@ class WaterQualityInference:
print("-" * 40) print("-" * 40)
predictions = self.predict(spectra_processed) predictions = self.predict(spectra_processed)
# 5. 保存预测结果 # 5. 保存预测结果(透传 WQI 列至最终输出文件)
print("\n步骤5: 保存预测结果") print("\n步骤5: 保存预测结果")
print("-" * 40) print("-" * 40)
result_df = self.save_predictions(coords, predictions, output_csv_path, prediction_column) result_df = self.save_predictions(coords, predictions, output_csv_path,
prediction_column, wqi_df)
print("\n" + "=" * 80) print("\n" + "=" * 80)
print("推理流程完成!") print("推理流程完成!")
@ -747,10 +839,11 @@ class WaterQualityInference:
output_file = output_path / f"prediction_{csv_file.name}" output_file = output_path / f"prediction_{csv_file.name}"
# 执行推理 # 执行推理
coords, spectra = self.load_sampling_data(str(csv_file)) coords, spectra, wqi_df = self.load_sampling_data(str(csv_file))
spectra_processed = self.preprocess_spectra(spectra) spectra_processed = self.preprocess_spectra(spectra)
predictions = self.predict(spectra_processed) predictions = self.predict(spectra_processed)
result_df = self.save_predictions(coords, predictions, str(output_file), prediction_column) result_df = self.save_predictions(coords, predictions, str(output_file),
prediction_column, wqi_df)
results[csv_file.name] = { results[csv_file.name] = {
'output_file': str(output_file), 'output_file': str(output_file),
@ -908,10 +1001,11 @@ class WaterQualityInference:
output_file = output_path / f"{file_stem}{file_ext}" output_file = output_path / f"{file_stem}{file_ext}"
# 执行推理 # 执行推理
coords, spectra = self.load_sampling_data(str(csv_file)) coords, spectra, wqi_df = self.load_sampling_data(str(csv_file))
spectra_processed = self.preprocess_spectra(spectra) spectra_processed = self.preprocess_spectra(spectra)
predictions = self.predict(spectra_processed) predictions = self.predict(spectra_processed)
result_df = self.save_predictions(coords, predictions, str(output_file), prediction_column) result_df = self.save_predictions(coords, predictions, str(output_file),
prediction_column, wqi_df)
results[file_stem] = { results[file_stem] = {
'input_file': str(csv_file), 'input_file': str(csv_file),

View File

@ -13,11 +13,24 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet from src.gui.styles import ModernStylesheet
def get_resource_path(relative_path: str) -> str: def get_resource_path(relative_path: str) -> str:
"""适配开发与 PyInstaller 环境的路径获取逻辑""" """适配开发与 PyInstaller 环境的路径获取逻辑
支持两种打包模式:
1. --onedir 模式:文件在 exe_root/_internal/ 下 → 检查 _internal 目录
2. --onefile 模式:文件在 sys._MEIPASS 平铺目录
"""
# 优先检查 PyInstaller onefile 模式(文件平铺在 _MEIPASS 下)
if hasattr(sys, '_MEIPASS'): if hasattr(sys, '_MEIPASS'):
# 打包后,文件会被平铺或按 tree 结构放入临时目录 internal_path = os.path.join(sys._MEIPASS, '_internal', relative_path)
if os.path.exists(internal_path):
return internal_path
return os.path.join(sys._MEIPASS, relative_path) return os.path.join(sys._MEIPASS, relative_path)
# 兼容 PyInstaller onedir 模式的 _internal 目录exe 同级目录下)
exe_dir = os.path.dirname(sys.executable)
internal_path = os.path.join(exe_dir, '_internal', relative_path)
if os.path.exists(internal_path):
return internal_path
# 开发环境下:基于当前文件 (step5_5_panel.py) 的绝对路径进行回溯 # 开发环境下:基于当前文件 (step5_5_panel.py) 的绝对路径进行回溯
# 当前在 src/gui/panels/,目标在 src/gui/model/ # 当前在 src/gui/panels/,目标在 src/gui/model/
base_dir = Path(__file__).resolve().parent.parent / "model" base_dir = Path(__file__).resolve().parent.parent / "model"

View File

@ -5,6 +5,12 @@
GUI for Water Quality Inversion Pipeline GUI for Water Quality Inversion Pipeline
""" """
# ==============================================================================
# 🚀 终极防御:必须在全宇宙第一行强制载入 GDAL 底层 DLL绝对杜绝 0xC0000005 内存崩溃
# ==============================================================================
import osgeo
from osgeo import gdal, ogr
import os import os
import json import json
import copy import copy
@ -31,8 +37,23 @@ from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPalette, QColor, QPixmap
import sys import sys
import traceback import traceback
import multiprocessing
import ctypes
# ==============================================================================
# 🚀 终极防御置顶:在载入任何自定义面板、样式或子模块之前,强制提前创建 QApplication
# 彻底杜绝 import 时期载入类属性 (如 QFont/QIcon/QPixmap) 触发的 QWidget 崩溃
# ==============================================================================
if multiprocessing.current_process().name == 'MainProcess':
if not QApplication.instance():
try:
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
except Exception:
pass
_global_app = QApplication(sys.argv)
# 👇 全局异常钩子(保持不变)
def get_resource_path(relative_path: str) -> str: def get_resource_path(relative_path: str) -> str:
"""获取资源的绝对路径,适配 PyInstaller 打包环境。 """获取资源的绝对路径,适配 PyInstaller 打包环境。
打包后资源位于 sys._MEIPASS解压临时目录开发环境则基于 __file__ 向上三级。 打包后资源位于 sys._MEIPASS解压临时目录开发环境则基于 __file__ 向上三级。
@ -45,10 +66,31 @@ def get_resource_path(relative_path: str) -> str:
def global_exception_handler(exc_type, exc_value, exc_traceback): def global_exception_handler(exc_type, exc_value, exc_traceback):
print("\n" + "="*50) err_lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
print("【严重错误拦截 - PyQt 崩溃死因】") err_msg = "".join(err_lines)
traceback.print_exception(exc_type, exc_value, exc_traceback) dump_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "crash_dump.txt")
print("="*50 + "\n") try:
with open(dump_path, "a", encoding="utf-8") as f:
f.write(f"\n{'='*60}\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]\n")
f.write(err_msg)
except Exception:
pass
try:
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtCore import Qt
msg = (
"【严重错误 - 程序即将退出】\n\n"
"错误类型: {}\n\n"
"错误信息: {}\n\n"
"详细信息已写入:\n{}".format(
exc_type.__name__,
str(exc_value),
dump_path,
)
)
QMessageBox.critical(None, "程序崩溃", msg)
except Exception:
pass
# 挂载全局异常钩子,阻止 PyQt 静默闪退 # 挂载全局异常钩子,阻止 PyQt 静默闪退
sys.excepthook = global_exception_handler sys.excepthook = global_exception_handler
@ -89,9 +131,13 @@ from src.gui.panels.step9_panel import Step9Panel
from src.gui.panels.visualization_panel import VisualizationPanel from src.gui.panels.visualization_panel import VisualizationPanel
from src.gui.panels.report_generation_panel import ReportGenerationPanel from src.gui.panels.report_generation_panel import ReportGenerationPanel
# Matplotlib相关导入 # Matplotlib相关导入 (推迟并加入底层防爆保护)
import matplotlib import matplotlib
matplotlib.use('Qt5Agg') try:
# 确保只在主线程且安全的环境下绑定后端
matplotlib.use('Qt5Agg', force=False)
except Exception:
pass
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure from matplotlib.figure import Figure
@ -1278,7 +1324,17 @@ class WaterQualityGUI(QMainWindow):
"""水质参数反演分析系统主窗口""" """水质参数反演分析系统主窗口"""
def __init__(self): def __init__(self):
# 1. 🚀 强制设置任务栏图标(解决任务栏图标默认是 Python 黄蓝图标的问题)
# 为当前进程设置独立的 AppUserModelID
my_appid = u'mycompany.megacube.waterquality.v1'
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(my_appid)
super().__init__() super().__init__()
# 2. 设置窗口图标(指向你的 .ico 文件)
icon_path = get_resource_path("data/icons-1/uitubiao.ico")
self.setWindowIcon(QIcon(icon_path))
self.pipeline = None self.pipeline = None
self.worker = None self.worker = None
self.config_file = None self.config_file = None
@ -3002,8 +3058,23 @@ class WaterQualityGUI(QMainWindow):
def main(): def main():
"""主函数""" """主函数"""
import sys import sys
import multiprocessing
from PyQt5.QtWidgets import QApplication
# 离线授权验证拦截(必须在业务窗口创建前执行) # 1. 多进程 Fork 环境隔离
if multiprocessing.current_process().name != 'MainProcess':
sys.exit(0)
# 2. 🚀 终极防御:必须在全宇宙第一行强制创建 QApplication 实例!
# 绝对杜绝任何后续验签模块或弹窗过早调用 QWidget 导致的崩溃
app = QApplication.instance()
if not app:
app = QApplication(sys.argv)
app.setApplicationName("Mega Water")
app.setOrganizationName("WaterQuality")
# 3. 安全载入离线授权验证拦截
try: try:
from src.auth.license_manager import verify_license from src.auth.license_manager import verify_license
from src.auth.license_dialog import LicenseDialog from src.auth.license_dialog import LicenseDialog
@ -3018,27 +3089,36 @@ def main():
_is_license_valid, _license_msg = verify_license() _is_license_valid, _license_msg = verify_license()
if not _is_license_valid: if not _is_license_valid:
_license_app = QApplication(sys.argv)
_license_app.setApplicationName("WaterQuality")
_dialog = LicenseDialog() _dialog = LicenseDialog()
_dialog.exec_() _dialog.exec_()
_license_app.quit()
sys.exit(0) sys.exit(0)
# 授权通过,正常载入主程序 # 4. 授权通过,正常载入主程序主界面
app = QApplication(sys.argv)
app.setApplicationName("Mega Water")
app.setOrganizationName("WaterQuality")
window = WaterQualityGUI() window = WaterQualityGUI()
window.show() window.show()
sys.exit(app.exec_()) sys.exit(app.exec_())
# ==============================================================================
# 全宇宙最底部程序入口
# ==============================================================================
if __name__ == "__main__": if __name__ == "__main__":
# 必须紧跟在 if __name__ == "__main__": 下面第一行 import sys
import multiprocessing import multiprocessing
multiprocessing.freeze_support()
# 1. 极其强硬的底层防御:
# 如果当前进程明确是 PyInstaller 派生的后台计算子进程,强行静默退出!
# 彻底绕过多进程钩子在尝试解包 sys.argv 时引发的 ValueError 崩溃
if multiprocessing.current_process().name != 'MainProcess':
sys.exit(0)
# 2. 安全调用 freeze_support带防爆气囊
try:
multiprocessing.freeze_support()
except Exception:
pass # 哪怕底层钩子参数解包失败,也强行保住主进程平稳过关
# 3. 正常拉起主业务逻辑
main() main()

View File

@ -11,6 +11,7 @@ os.environ['GDAL_FILENAME_IS_UTF8'] = 'YES'
os.environ['SHAPE_ENCODING'] = 'UTF-8' os.environ['SHAPE_ENCODING'] = 'UTF-8'
import numpy as np import numpy as np
import pandas as pd
from osgeo import gdal, ogr from osgeo import gdal, ogr
import spectral import spectral
from scipy import ndimage from scipy import ndimage
@ -354,6 +355,45 @@ def get_spectral_sampling_points_chunked(bil_file, water_mask_shp, severe_glint=
if f: if f:
f.close() f.close()
# ==============================================================================
# 🚀 终极手术植入点:带强行环境净化的特征引擎挂载
# ==============================================================================
# 2. 安全校验路径落盘状态
if output_csvpath and os.path.exists(str(output_csvpath)):
try:
from src.utils.water_index import WaterQualityIndexCalculator
print("\n[特征引擎挂载] 正在为采样点自动追加 45 个水质指数衍生特征...")
# 读取基础底座50列光谱
base_df = pd.read_csv(output_csvpath)
# 实例化计算器
calc = WaterQualityIndexCalculator()
# 提取有效算法
algorithm_methods = [
m for m in dir(calc)
if not m.startswith('_') and m not in ['find_closest_wavelength', 'calculate_all_indices']
]
# 就地追加 45 列衍生指数
for algo_name in algorithm_methods:
try:
algo_func = getattr(calc, algo_name)
base_df[algo_name] = algo_func(base_df)
except Exception:
base_df[algo_name] = np.nan
# 覆盖重写最终结果!
base_df.to_csv(output_csvpath, index=False, encoding='utf-8-sig')
print(f"✓ 特征扩充大功告成!当前文件总维度完美适配模型: {base_df.shape}")
except Exception as e:
print(f"⚠ 警告:追加特征失败,保留原基础光谱。死因: {e}")
# ==============================================================================
return x_out, y_out, np.array(spectral_out) return x_out, y_out, np.array(spectral_out)
except Exception as e: except Exception as e: