内容部分修改

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,9 +5,10 @@ import sys
def _safe_add(path: str) -> None:
if not path or not os.path.isdir(path):
return
try:
if hasattr(os, "add_dll_directory"):
try:
os.add_dll_directory(path)
return
except Exception:
pass
try:
@ -22,4 +23,3 @@ if base:
_safe_add(base)
_safe_add(os.path.join(base, "lib-dynload"))
_safe_add(os.path.join(base, "DLLs"))

View File

@ -31,7 +31,9 @@ def get_cpu_id() -> Optional[str]:
capture_output=True,
text=True,
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()
if cpu_id:
@ -57,7 +59,9 @@ def get_motherboard_uuid() -> Optional[str]:
capture_output=True,
text=True,
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 = 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"],
capture_output=True,
text=True,
timeout=5
timeout=5,
stdin=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if result.returncode == 0:
return result.stdout.strip()

View File

@ -40,6 +40,10 @@ CB_AVAILABLE = False # 注释掉catboost
import sys
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
@ -643,7 +647,7 @@ class WaterQualityModelingBatch:
config['params'],
cv=cv_strategy,
scoring=scoring,
n_jobs=-1,
n_jobs=safe_n_jobs,
verbose=1
)

View File

@ -40,16 +40,19 @@ class WaterQualityInference:
self.best_model_info = 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:
csv_path: CSV文件路径,前两列为经纬度,其余列为光谱数据
csv_path: CSV文件路径
旧版x_coord,y_coord,pixel_x,pixel_y,波长...
新版x_coord,y_coord,WQI_...,波长...
Returns:
coords: 经纬度数据 (DataFrame)
spectra: 光谱数据 (DataFrame)
coords: 经纬度数据 (DataFrame, 2列)
spectra: 光谱数据 (DataFrame, 跳过 WQI 列)
wqi_df: WQI 指数列 (DataFrame, 0或45列)
"""
print(f"正在加载采样数据: {csv_path}")
@ -71,15 +74,35 @@ class WaterQualityInference:
coords = data.iloc[:, :2].copy()
coords.columns = ['longitude', 'latitude']
# 从第5列开始为光谱数据跳过第2、3、4列的其他信息
spectra = data.iloc[:, 4:].copy()
# 动态识别光谱列(兼容 sampling_spectra.csv 列顺序变更
# 列名约定:波长为纯数字字符串如 "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" 光谱数据形状: {spectra.shape}")
print(f" 光谱数据形状: {spectra.shape} (自动识别波长列,排除 {len(wqi_col_indices)} 个WQI列)")
print(f" 经纬度范围: 经度[{coords['longitude'].min():.6f}, {coords['longitude'].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):
"""
@ -519,6 +542,69 @@ class WaterQualityInference:
print(f"正在应用预处理方法: {actual_preprocess_method}")
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:
# 应用预处理
spectra_processed = Preprocessing(actual_preprocess_method, spectra)
@ -573,7 +659,8 @@ class WaterQualityInference:
raise
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: 预测结果
output_path: 输出文件路径
prediction_column: 预测列名称
wqi_columns: Optional[pd.DataFrame] = None
"""
print(f"正在保存预测结果到: {output_path}")
# 创建结果DataFrame
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
# 确保输出目录存在
@ -659,10 +750,10 @@ class WaterQualityInference:
else:
self.load_best_model(metric=metric)
# 2. 加载采样数据
# 2. 加载采样数据coords=坐标, spectra=纯光谱, wqi_df=45个WQI指数列
print("\n步骤2: 加载采样数据")
print("-" * 40)
coords, spectra = self.load_sampling_data(sampling_csv_path)
coords, spectra, wqi_df = self.load_sampling_data(sampling_csv_path)
# 3. 数据预处理
print("\n步骤3: 数据预处理")
@ -674,10 +765,11 @@ class WaterQualityInference:
print("-" * 40)
predictions = self.predict(spectra_processed)
# 5. 保存预测结果
# 5. 保存预测结果(透传 WQI 列至最终输出文件)
print("\n步骤5: 保存预测结果")
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("推理流程完成!")
@ -747,10 +839,11 @@ class WaterQualityInference:
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)
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] = {
'output_file': str(output_file),
@ -908,10 +1001,11 @@ class WaterQualityInference:
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)
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] = {
'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
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'):
# 打包后,文件会被平铺或按 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)
# 兼容 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) 的绝对路径进行回溯
# 当前在 src/gui/panels/,目标在 src/gui/model/
base_dir = Path(__file__).resolve().parent.parent / "model"

View File

@ -5,6 +5,12 @@
GUI for Water Quality Inversion Pipeline
"""
# ==============================================================================
# 🚀 终极防御:必须在全宇宙第一行强制载入 GDAL 底层 DLL绝对杜绝 0xC0000005 内存崩溃
# ==============================================================================
import osgeo
from osgeo import gdal, ogr
import os
import json
import copy
@ -31,8 +37,23 @@ from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPalette, QColor, QPixmap
import sys
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:
"""获取资源的绝对路径,适配 PyInstaller 打包环境。
打包后资源位于 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):
print("\n" + "="*50)
print("【严重错误拦截 - PyQt 崩溃死因】")
traceback.print_exception(exc_type, exc_value, exc_traceback)
print("="*50 + "\n")
err_lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
err_msg = "".join(err_lines)
dump_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "crash_dump.txt")
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 静默闪退
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.report_generation_panel import ReportGenerationPanel
# Matplotlib相关导入
# 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 NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
@ -1278,7 +1324,17 @@ class WaterQualityGUI(QMainWindow):
"""水质参数反演分析系统主窗口"""
def __init__(self):
# 1. 🚀 强制设置任务栏图标(解决任务栏图标默认是 Python 黄蓝图标的问题)
# 为当前进程设置独立的 AppUserModelID
my_appid = u'mycompany.megacube.waterquality.v1'
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(my_appid)
super().__init__()
# 2. 设置窗口图标(指向你的 .ico 文件)
icon_path = get_resource_path("data/icons-1/uitubiao.ico")
self.setWindowIcon(QIcon(icon_path))
self.pipeline = None
self.worker = None
self.config_file = None
@ -3002,8 +3058,23 @@ class WaterQualityGUI(QMainWindow):
def main():
"""主函数"""
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:
from src.auth.license_manager import verify_license
from src.auth.license_dialog import LicenseDialog
@ -3018,27 +3089,36 @@ def main():
_is_license_valid, _license_msg = verify_license()
if not _is_license_valid:
_license_app = QApplication(sys.argv)
_license_app.setApplicationName("WaterQuality")
_dialog = LicenseDialog()
_dialog.exec_()
_license_app.quit()
sys.exit(0)
# 授权通过,正常载入主程序
app = QApplication(sys.argv)
app.setApplicationName("Mega Water")
app.setOrganizationName("WaterQuality")
# 4. 授权通过,正常载入主程序主界面
window = WaterQualityGUI()
window.show()
sys.exit(app.exec_())
# ==============================================================================
# 全宇宙最底部程序入口
# ==============================================================================
if __name__ == "__main__":
# 必须紧跟在 if __name__ == "__main__": 下面第一行
import sys
import multiprocessing
# 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()

View File

@ -11,6 +11,7 @@ os.environ['GDAL_FILENAME_IS_UTF8'] = 'YES'
os.environ['SHAPE_ENCODING'] = 'UTF-8'
import numpy as np
import pandas as pd
from osgeo import gdal, ogr
import spectral
from scipy import ndimage
@ -354,6 +355,45 @@ def get_spectral_sampling_points_chunked(bil_file, water_mask_shp, severe_glint=
if f:
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)
except Exception as e: