内容部分修改
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 MiB |
@ -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"))
|
||||||
|
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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(无WQI):x_coord,y_coord,pixel_x,pixel_y,波长... → 取 [4:]
|
||||||
|
# 新版 CSV(有WQI):x_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 WQI;sampling_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),
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user