diff --git a/src/core/algorithms/__init__.py b/src/core/algorithms/__init__.py index ac04e68..efc2d87 100644 --- a/src/core/algorithms/__init__.py +++ b/src/core/algorithms/__init__.py @@ -16,6 +16,15 @@ from src.core.algorithms.glint_detection.detectors import ( remove_shoreline_buffer, calculate_glint_mask, ) +from src.core.algorithms.qaa.qaas_baseline import QAABaselineSolver +from src.core.algorithms.concentration_inversion import ( + ChlorophyllInversion, + CDOMInversion, + TurbidityInversion, + TotalNitrogenInversion, + TotalPhosphorusInversion, + ConcentrationPipeline, +) __all__ = [ # 插值 @@ -33,4 +42,13 @@ __all__ = [ 'create_shoreline_buffer', 'remove_shoreline_buffer', 'calculate_glint_mask', + # QAA + 'QAABaselineSolver', + # 浓度反演 + 'ChlorophyllInversion', + 'CDOMInversion', + 'TurbidityInversion', + 'TotalNitrogenInversion', + 'TotalPhosphorusInversion', + 'ConcentrationPipeline', ] diff --git a/src/core/algorithms/concentration_inversion.py b/src/core/algorithms/concentration_inversion.py new file mode 100644 index 0000000..b93d8f5 --- /dev/null +++ b/src/core/algorithms/concentration_inversion.py @@ -0,0 +1,662 @@ +# -*- coding: utf-8 -*- +""" +水质浓度反演模块 + +基于 QAA Step 8 输出的光谱吸收/散射系数 (a_lambda, bb_lambda), +通过生物光学模型反演水质参数浓度。 + +主要反演目标: + - 叶绿素 A (Chl-a):675nm 吸收峰法 + - 浊度 (Turbidity):后向散射系数法 + - CDOM 吸收系数 a_dg(440):指数衰减法 + - 总氮 (TN) / 总磷 (TP):光学代理回归框架 + +参考: + - Lee, Z.P. et al. (2002/2010/2014) QAA 系列 + - Bricaud, A. et al. (1998) Limnol. Oceanogr. — 叶绿素比吸收系数 + - Carder, K.L. et al. (1999) Marine Technology Society — CDOM 指数衰减 +""" + +from __future__ import annotations + +import os +from typing import Dict, List, Optional, Tuple, Union + +import numpy as np +import pandas as pd + + +# ------------------------------------------------------------------ +# 公共系数表(来自 Bricaud et al. 1998 等文献,内陆水体典型值) +# ------------------------------------------------------------------ + +# 叶绿素比吸收系数 a*_ph(675) 单位:m²/mg +# 随叶绿素浓度范围变化,Bricaud 经验值 +CHLA_SPECIFIC_ABSORPTION: Dict[str, float] = { + "low": 0.055, # 寡营养水体,Chla < 5 mg/m³ + "medium": 0.040, # 中营养,Chla 5-30 mg/m³ + "high": 0.028, # 富营养,Chla 30-100 mg/m³ + "bloom": 0.020, # 藻华,Chla > 100 mg/m³ +} + +# CDOM 指数衰减斜率 S(单位:nm⁻¹),内陆水体典型范围 0.010-0.025 +CDOM_S_LOOKUP: Dict[str, float] = { + "low_turbidity": 0.010, # 清澈寡营养 + "medium_turbidity": 0.015, # 中等浊度 + "high_turbidity": 0.020, # 高浊度富营养 + "bloom": 0.025, # 藻华主导 +} + +# 纯水吸收系数表(400-800nm,Babin et al. 2003 简化值,单位:m⁻¹) +PURE_WATER_A: Dict[int, float] = { + 400: 0.0064, 410: 0.0066, 420: 0.0068, 430: 0.0072, + 440: 0.0080, 450: 0.0092, 460: 0.0105, 470: 0.0120, + 480: 0.0135, 490: 0.0155, 500: 0.0175, 510: 0.0200, + 520: 0.0230, 530: 0.0270, 540: 0.0315, 550: 0.0370, + 560: 0.0435, 570: 0.0510, 580: 0.0600, 590: 0.0710, + 600: 0.0830, 610: 0.0960, 620: 0.1110, 630: 0.1280, + 640: 0.1470, 650: 0.1680, 660: 0.1920, 670: 0.2180, + 675: 0.2450, 680: 0.2750, 690: 0.3100, 700: 0.3500, + 710: 0.3950, 720: 0.4450, 730: 0.5000, 740: 0.5600, + 750: 0.6250, 760: 0.6950, 770: 0.7700, 780: 0.8500, + 790: 0.9300, 800: 1.0100, +} + + +def _interp_pure_water_a(wavelength: float) -> float: + """线性插值获取纯水吸收系数""" + wl_int = {k for k in PURE_WATER_A if k <= int(wavelength)} + if not wl_int: + return PURE_WATER_A[min(PURE_WATER_A.keys())] + k_low = max(wl_int) + k_high = min({k for k in PURE_WATER_A if k >= int(wavelength)} or {k_low}) + if k_low == k_high: + return float(PURE_WATER_A[k_low]) + w = (wavelength - k_low) / (k_high - k_low) + return float(PURE_WATER_A[k_low]) * (1 - w) + float(PURE_WATER_A[k_high]) * w + + +# ------------------------------------------------------------------ +# 叶绿素反演器 +# ------------------------------------------------------------------ + +class ChlorophyllInversion: + """ + 基于 675nm 吸收峰法的叶绿素 A 浓度反演。 + + 原理: + 总吸收 a(675) = a_w(675) + a_ph(675) + a_dg(675) + 其中 a_ph(675) 是叶绿素特征吸收峰, + a_dg(675) ≈ a_dg(440) * exp(-S * (675-440)) + + 步骤: + 1. 从 a(λ) 减去纯水吸收 a_w(λ) + 2. 用线性基线法估算 a_dg(675):baseline(675) = mean[a(665), a(685)] + 3. a_ph(675) = a(675) - a_w(675) - baseline(675) + 4. Chla = a_ph(675) / a*_ph(675) + + Parameters + ---------- + specific_absorption : float, optional + 叶绿素比吸收系数 a*_ph(675),单位 m²/mg。 + 若为 None,使用浓度自适应估算逻辑。 + lake_case : str, optional + 水体类型标识,用于自动选择比吸收系数, + 支持 "oligotrophic_clear" / "medium" / "bloom_dominant" / "turbid_mixed"。 + """ + + def __init__( + self, + specific_absorption: Optional[float] = None, + lake_case: Optional[str] = None + ): + self.specific_absorption = specific_absorption + self.lake_case = lake_case or "medium" + + def run_inversion( + self, + wavelengths: np.ndarray, + a_lambda: np.ndarray, + bb_lambda: Optional[np.ndarray] = None + ) -> Dict: + """ + 执行叶绿素 A 反演。 + + Parameters + ---------- + wavelengths : np.ndarray + 波长数组(nm),形状 (n_bands,)。 + a_lambda : np.ndarray + 总吸收系数 a(λ),形状 (n_bands,)。 + bb_lambda : np.ndarray, optional + 后向散射系数(暂未使用,保留扩展接口)。 + + Returns + ------- + dict + 包含键: + - chla_mg_m3 : 叶绿素 A 浓度(mg/m³) + - a_ph_675 : 675nm 处叶绿素吸收(m⁻¹) + - baseline_675 : 675nm 处 CDOM+NAP 基线(m⁻¹) + - a_w_675 : 纯水吸收(m⁻¹) + """ + wavelengths = np.asarray(wavelengths, dtype=np.float64) + a_lambda = np.asarray(a_lambda, dtype=np.float64) + + aw_675 = _interp_pure_water_a(675.0) + + wl_arr = wavelengths + a_arr = a_lambda + + a_665 = float(np.interp(665, wl_arr, a_arr, left=np.nan, right=np.nan)) + a_675 = float(np.interp(675, wl_arr, a_arr, left=np.nan, right=np.nan)) + a_685 = float(np.interp(685, wl_arr, a_arr, left=np.nan, right=np.nan)) + + if not np.isfinite(a_665) or not np.isfinite(a_675) or not np.isfinite(a_685): + return { + "chla_mg_m3": np.nan, + "a_ph_675": np.nan, + "baseline_675": np.nan, + "a_w_675": aw_675, + "warning": "675nm 波段缺失,无法进行叶绿素反演", + } + + baseline_675 = (a_665 + a_685) / 2.0 + a_ph_675 = max(a_675 - aw_675 - baseline_675, 0.0) + + if self.specific_absorption is not None: + a_star = self.specific_absorption + else: + a_star = self._adaptive_specific_absorption(a_ph_675) + + if a_star <= 0: + return { + "chla_mg_m3": np.nan, + "a_ph_675": a_ph_675, + "baseline_675": baseline_675, + "a_w_675": aw_675, + "warning": "比吸收系数为非正值", + } + + chla = a_ph_675 / a_star + return { + "chla_mg_m3": chla, + "a_ph_675": a_ph_675, + "baseline_675": baseline_675, + "a_w_675": aw_675, + } + + def _adaptive_specific_absorption(self, a_ph_675: float) -> float: + """根据 a_ph(675) 量级自适应选择比吸收系数""" + if a_ph_675 < 0.05: + return CHLA_SPECIFIC_ABSORPTION["low"] + elif a_ph_675 < 0.2: + return CHLA_SPECIFIC_ABSORPTION["medium"] + elif a_ph_675 < 0.5: + return CHLA_SPECIFIC_ABSORPTION["high"] + else: + return CHLA_SPECIFIC_ABSORPTION["bloom"] + + def invert_to_csv( + self, + input_csv: str, + output_csv: str, + sample_id_col: str = "sample_id" + ) -> str: + """ + 从 a_lambda_results.csv 批量反演叶绿素并保存结果。 + + Parameters + ---------- + input_csv : str + Step 8 输出的 a_lambda_results.csv 路径。 + output_csv : str + 保存路径。 + sample_id_col : str + 样本 ID 列名。 + + Returns + ------- + str + 输出文件路径。 + """ + df = pd.read_csv(input_csv, encoding="utf-8-sig") + df = df.sort_values([sample_id_col, "Wavelength"]) + + results = [] + for sid, group in df.groupby(sample_id_col, sort=False): + wl = group["Wavelength"].values.astype(np.float64) + a = group["a_lambda"].values.astype(np.float64) + res = self.run_inversion(wl, a) + res[sample_id_col] = sid + results.append(res) + + out_df = pd.DataFrame(results) + cols = [sample_id_col, "chla_mg_m3", "a_ph_675", "baseline_675", "a_w_675"] + cols = [c for c in cols if c in out_df.columns] + out_df = out_df[cols] + os.makedirs(os.path.dirname(output_csv) or ".", exist_ok=True) + out_df.to_csv(output_csv, index=False, float_format="%.6f") + return output_csv + + +# ------------------------------------------------------------------ +# CDOM 反演器 +# ------------------------------------------------------------------ + +class CDOMInversion: + """ + 基于指数衰减模型的 CDOM 吸收系数反演。 + + 原理: + a_dg(λ) = a_dg(λ₀) * exp(-S * (λ - λ₀)) + + 取 λ₀ = 440nm(蓝光峰),S 由水体类型决定, + 通过 a(550) ≈ a_w(550) + a_dg(550) 反推 a_dg(440)。 + + Parameters + ---------- + S : float, optional + CDOM 指数衰减斜率(nm⁻¹)。若为 None,根据 lake_case 自动选择。 + reference_wavelength : int + 参考波长,默认 440nm。 + """ + + def __init__( + self, + S: Optional[float] = None, + reference_wavelength: int = 440 + ): + self.S = S + self.ref_wl = reference_wavelength + + def run_inversion( + self, + wavelengths: np.ndarray, + a_lambda: np.ndarray + ) -> Dict: + """ + 执行 CDOM 反演。 + + Parameters + ---------- + wavelengths : np.ndarray + 波长数组。 + a_lambda : np.ndarray + 总吸收系数 a(λ)。 + + Returns + ------- + dict + 包含键: + - a_dg_440 : 440nm 处 CDOM 吸收(m⁻¹) + - S : 使用的衰减斜率 + """ + wavelengths = np.asarray(wavelengths, dtype=np.float64) + a_lambda = np.asarray(a_lambda, dtype=np.float64) + + if self.S is None: + S = CDOM_S_LOOKUP["medium_turbidity"] + else: + S = self.S + + a_440 = float(np.interp(440, wavelengths, a_lambda, left=np.nan, right=np.nan)) + a_550 = float(np.interp(550, wavelengths, a_lambda, left=np.nan, right=np.nan)) + aw_440 = _interp_pure_water_a(440.0) + aw_550 = _interp_pure_water_a(550.0) + + a_dg_550 = max(a_550 - aw_550, 0.0) + delta_wl = 550 - self.ref_wl + a_dg_440 = a_dg_550 * np.exp(S * delta_wl) + + return { + "a_dg_440": a_dg_440, + "a_dg_550": a_dg_550, + "S": S, + } + + def invert_to_csv( + self, + input_csv: str, + output_csv: str, + sample_id_col: str = "sample_id" + ) -> str: + """从 a_lambda_results.csv 批量反演 CDOM 并保存结果。""" + df = pd.read_csv(input_csv, encoding="utf-8-sig") + df = df.sort_values([sample_id_col, "Wavelength"]) + + results = [] + for sid, group in df.groupby(sample_id_col, sort=False): + wl = group["Wavelength"].values.astype(np.float64) + a = group["a_lambda"].values.astype(np.float64) + res = self.run_inversion(wl, a) + res[sample_id_col] = sid + results.append(res) + + out_df = pd.DataFrame(results) + cols = [sample_id_col, "a_dg_440", "a_dg_550", "S"] + cols = [c for c in cols if c in out_df.columns] + out_df = out_df[cols] + os.makedirs(os.path.dirname(output_csv) or ".", exist_ok=True) + out_df.to_csv(output_csv, index=False, float_format="%.6f") + return output_csv + + +# ------------------------------------------------------------------ +# 浊度反演器 +# ------------------------------------------------------------------ + +class TurbidityInversion: + """ + 基于后向散射系数的光学浊度反演。 + + 原理(简化模型): + Turbidity (NTU) ≈ k * b_b(550) + + 其中 b_b(550) 是 550nm 处的后向散射系数, + k 为经验系数(内陆水体典型值 1.0-3.0)。 + + Parameters + ---------- + k : float + 经验系数。默认值 2.0。 + reference_wavelength : int + 参考波段,默认 550nm。 + """ + + def __init__(self, k: float = 2.0, reference_wavelength: int = 550): + self.k = k + self.ref_wl = reference_wavelength + + def run_inversion( + self, + wavelengths: np.ndarray, + bb_lambda: np.ndarray + ) -> Dict: + """ + 执行浊度反演。 + + Parameters + ---------- + wavelengths : np.ndarray + 波长数组。 + bb_lambda : np.ndarray + 后向散射系数 b_b(λ)。 + + Returns + ------- + dict + 包含键: + - turbidity_ntu : 浊度(NTU) + - bb_ref : 参考波段处的 b_b 值 + """ + wavelengths = np.asarray(wavelengths, dtype=np.float64) + bb_lambda = np.asarray(bb_lambda, dtype=np.float64) + + bb_ref = float(np.interp( + self.ref_wl, wavelengths, bb_lambda, left=np.nan, right=np.nan + )) + turbidity = self.k * bb_ref + + return { + "turbidity_ntu": turbidity, + "bb_ref": bb_ref, + } + + def invert_to_csv( + self, + input_csv: str, + output_csv: str, + sample_id_col: str = "sample_id" + ) -> str: + """从 a_lambda_results.csv 批量反演浊度并保存结果。""" + df = pd.read_csv(input_csv, encoding="utf-8-sig") + if "bb_lambda" not in df.columns: + raise ValueError("输入 CSV 中缺少 bb_lambda 列") + df = df.sort_values([sample_id_col, "Wavelength"]) + + results = [] + for sid, group in df.groupby(sample_id_col, sort=False): + wl = group["Wavelength"].values.astype(np.float64) + bb = group["bb_lambda"].values.astype(np.float64) + res = self.run_inversion(wl, bb) + res[sample_id_col] = sid + results.append(res) + + out_df = pd.DataFrame(results) + cols = [sample_id_col, "turbidity_ntu", "bb_ref"] + cols = [c for c in cols if c in out_df.columns] + out_df = out_df[cols] + os.makedirs(os.path.dirname(output_csv) or ".", exist_ok=True) + out_df.to_csv(output_csv, index=False, float_format="%.6f") + return output_csv + + +# ------------------------------------------------------------------ +# 总氮 / 总磷反演器(光学代理回归框架) +# ------------------------------------------------------------------ + +class TotalNitrogenInversion: + """ + 总氮 (TN) 光学代理回归模型。 + + 框架说明: + TN 与 Chla 之间通常存在正相关(R² ≈ 0.5-0.7), + 本类提供回归框架,实际系数需由实测数据标定。 + + 公式(线性代理): + TN (mg/L) = α * Chla + β * Turbidity + γ + + Parameters + ---------- + alpha : float + Chla 系数。默认 0.05。 + beta : float + 浊度系数。默认 0.10。 + gamma : float + 截距。默认 0.20。 + """ + + def __init__( + self, + alpha: float = 0.05, + beta: float = 0.10, + gamma: float = 0.20 + ): + self.alpha = alpha + self.beta = beta + self.gamma = gamma + + def run_inversion( + self, + chla_mg_m3: float, + turbidity_ntu: float + ) -> Dict: + """执行总氮反演(光学代理法)。""" + tn = self.alpha * chla_mg_m3 + self.beta * turbidity_ntu + self.gamma + return {"tn_mg_L": tn} + + def calibrate( + self, + samples: List[Dict] + ) -> None: + """ + 用实测样本标定回归系数。 + + Parameters + ---------- + samples : list[dict] + 样本列表,每项包含 'chla', 'turbidity', 'tn' 键。 + """ + try: + import numpy as np + X = np.array([[s["chla"], s["turbidity"]] for s in samples]) + y = np.array([s["tn"] for s in samples]) + coeffs, _, _, _ = np.linalg.lstsq(X, y, rcond=None) + self.alpha, self.beta = coeffs + self.gamma = float(np.mean(y - self.alpha * X[:, 0] - self.beta * X[:, 1])) + except Exception as e: + raise RuntimeError(f"标定失败: {e}") + + +class TotalPhosphorusInversion: + """ + 总磷 (TP) 光学代理回归模型。 + + 框架说明: + TP 与 Chla / 浊度均相关(湖泊富营养化阶段尤为明显), + 提供双变量线性回归框架,实际系数需由实测数据标定。 + + 公式(线性代理): + TP (mg/L) = α * Chla + β * Turbidity + γ + + Parameters + ---------- + alpha : float + Chla 系数。默认 0.002。 + beta : float + 浊度系数。默认 0.005。 + gamma : float + 截距。默认 0.010。 + """ + + def __init__( + self, + alpha: float = 0.002, + beta: float = 0.005, + gamma: float = 0.010 + ): + self.alpha = alpha + self.beta = beta + self.gamma = gamma + + def run_inversion( + self, + chla_mg_m3: float, + turbidity_ntu: float + ) -> Dict: + """执行总磷反演(光学代理法)。""" + tp = self.alpha * chla_mg_m3 + self.beta * turbidity_ntu + self.gamma + return {"tp_mg_L": tp} + + def calibrate( + self, + samples: List[Dict] + ) -> None: + """用实测样本标定回归系数。""" + try: + import numpy as np + X = np.array([[s["chla"], s["turbidity"]] for s in samples]) + y = np.array([s["tp"] for s in samples]) + coeffs, _, _, _ = np.linalg.lstsq(X, y, rcond=None) + self.alpha, self.beta = coeffs + self.gamma = float(np.mean(y - self.alpha * X[:, 0] - self.beta * X[:, 1])) + except Exception as e: + raise RuntimeError(f"标定失败: {e}") + + +# ------------------------------------------------------------------ +# 一站式浓度反演流水线 +# ------------------------------------------------------------------ + +class ConcentrationPipeline: + """ + 整合 Chlorophyll / CDOM / Turbidity / TN / TP 反演的一站式流水线。 + + 接收 Step 8 输出的 a_lambda_results.csv, + 输出 final_concentrations.csv(含所有水质参数浓度列)。 + + Parameters + ---------- + lake_case : str, optional + 水体类型,用于 Chla 比吸收系数自适应选择。 + S_cdom : float, optional + CDOM 衰减斜率(若为 None,自动选择)。 + k_turbidity : float + 浊度经验系数。 + tn_params : dict, optional + 总氮反演初始参数。 + tp_params : dict, optional + 总磷反演初始参数。 + """ + + def __init__( + self, + lake_case: str = "medium", + S_cdom: Optional[float] = None, + k_turbidity: float = 2.0, + tn_params: Optional[Dict] = None, + tp_params: Optional[Dict] = None, + ): + self.lake_case = lake_case + self.chla_inv = ChlorophyllInversion(lake_case=lake_case) + self.cdom_inv = CDOMInversion(S=S_cdom) + self.turb_inv = TurbidityInversion(k=k_turbidity) + self.tn_inv = TotalNitrogenInversion(**(tn_params or {})) + self.tp_inv = TotalPhosphorusInversion(**(tp_params or {})) + + def run_pipeline( + self, + input_csv: str, + output_csv: str, + sample_id_col: str = "sample_id" + ) -> str: + """ + 执行完整浓度反演流水线。 + + Parameters + ---------- + input_csv : str + Step 8 输出的 a_lambda_results.csv 路径。 + output_csv : str + 输出 final_concentrations.csv 路径。 + sample_id_col : str + 样本 ID 列名。 + + Returns + ------- + str + 输出文件路径。 + """ + df = pd.read_csv(input_csv, encoding="utf-8-sig") + if "bb_lambda" not in df.columns: + df["bb_lambda"] = np.nan + + df = df.sort_values([sample_id_col, "Wavelength"]) + + results = [] + for sid, group in df.groupby(sample_id_col, sort=False): + wl = group["Wavelength"].values.astype(np.float64) + a = group["a_lambda"].values.astype(np.float64) + bb = group["bb_lambda"].values.astype(np.float64) \ + if "bb_lambda" in group.columns and group["bb_lambda"].notna().any() \ + else None + + chla_res = self.chla_inv.run_inversion(wl, a) + cdom_res = self.cdom_inv.run_inversion(wl, a) + if bb is not None and np.any(np.isfinite(bb)): + turb_res = self.turb_inv.run_inversion(wl, bb) + else: + turb_res = {"turbidity_ntu": np.nan, "bb_ref": np.nan} + + chla_val = chla_res.get("chla_mg_m3", np.nan) + turb_val = turb_res.get("turbidity_ntu", np.nan) + + tn_res = self.tn_inv.run_inversion(chla_val, turb_val) + tp_res = self.tp_inv.run_inversion(chla_val, turb_val) + + row = { + sample_id_col: sid, + "Chla_mg_m3": chla_val, + "a_ph_675_m1": chla_res.get("a_ph_675", np.nan), + "CDOM_a_dg_440_m1": cdom_res.get("a_dg_440", np.nan), + "Turbidity_NTU": turb_val, + "TN_mg_L": tn_res.get("tn_mg_L", np.nan), + "TP_mg_L": tp_res.get("tp_mg_L", np.nan), + } + results.append(row) + + out_df = pd.DataFrame(results) + os.makedirs(os.path.dirname(output_csv) or ".", exist_ok=True) + out_df.to_csv(output_csv, index=False, float_format="%.6f") + return output_csv \ No newline at end of file diff --git a/src/core/water_quality_inversion_pipeline_GUI.py b/src/core/water_quality_inversion_pipeline_GUI.py index d9e580a..ac02378 100644 --- a/src/core/water_quality_inversion_pipeline_GUI.py +++ b/src/core/water_quality_inversion_pipeline_GUI.py @@ -657,7 +657,7 @@ class WaterQualityInversionPipeline: self._notify("completed", f"训练光谱数据已保存: {result}") return result - def step8_water_quality_indices(self, + def step6_water_quality_indices(self, training_csv_path: Optional[str] = None, formula_csv_file: Optional[str] = None, formula_names: Optional[List[str]] = None, @@ -743,7 +743,116 @@ class WaterQualityInversionPipeline: self._record_step_time("步骤6: 训练机器学习模型", 0, 0) self._notify("completed", f"模型训练完成,结果保存在: {result}") return result - + + def step8_qaa_inversion(self, **config): + """步骤8: QAA 物理推导(非经验模型)""" + import numpy as np + import pandas as pd + from src.core.algorithms.qaa import QAABaselineSolver + from src.utils.water_owt_config import get_lambda_0 + + qaa_cfg = config.get('step8_qaa', {}) + lake_name = qaa_cfg.get('lake_name', 'Unknown') + lambda_0 = qaa_cfg.get('lambda_0', get_lambda_0(lake_name)) + output_dir = os.path.join(self.work_dir, "8_QAA_Inversion") + os.makedirs(output_dir, exist_ok=True) + output_path = qaa_cfg.get('output_path') or os.path.join(output_dir, "a_lambda_results.csv") + + spectrum_csv = qaa_cfg.get('spectrum_csv_path') + if not spectrum_csv: + spectrum_csv = config.get('training_csv_path') + if not spectrum_csv or not os.path.exists(spectrum_csv): + # 回退:扫描 work_dir 下 step5 的产物目录,找第一个 .csv + fallback_candidates = [] + step5_dir = os.path.join(self.work_dir, "5_Training_Spectra") + if os.path.isdir(step5_dir): + for f in sorted(os.listdir(step5_dir)): + if f.lower().endswith('.csv'): + fallback_candidates.append(os.path.join(step5_dir, f)) + if fallback_candidates: + spectrum_csv = fallback_candidates[0] + msg = f"[Step 8] spectrum_csv_path 为空,已自动回退到 step5 产物: {spectrum_csv}" + (self.logger.info if hasattr(self, 'logger') else print)(msg) + else: + msg = f"[Step 8] 训练光谱 CSV 不存在或路径为空: {spectrum_csv}" + (self.logger.info if hasattr(self, 'logger') else print)(msg) + return + + df = pd.read_csv(spectrum_csv, encoding="utf-8-sig") + col_names = df.columns.tolist() + + wavelength_col_idx = None + for i, col in enumerate(col_names): + try: + float(col) + wavelength_col_idx = i + break + except (ValueError, TypeError): + pass + + if wavelength_col_idx is None: + msg = "[Step 8] 无法从 CSV 列名中识别波长信息" + (self.logger.info if hasattr(self, 'logger') else print)(msg) + return + + wavelengths = np.array([float(c) for c in col_names[wavelength_col_idx:]], dtype=np.float64) + data_matrix = df.iloc[:, wavelength_col_idx:].values.astype(np.float64) + if data_matrix.ndim == 1: + data_matrix = data_matrix[np.newaxis, :] + + solver = QAABaselineSolver() + raw_result = solver.run_inversion(wavelengths, data_matrix, lambda_0) + + # run_inversion 返回:单样本 → dict,多样本 → list[dict] + if isinstance(raw_result, list): + sample_results = raw_result + else: + sample_results = [raw_result] + + rows_out = [] + for i, sample_result in enumerate(sample_results): + wl_arr = wavelengths + a_arr = sample_result['a_lambda'] + bb_arr = sample_result['bb_lambda'] + for j, wl in enumerate(wl_arr): + rows_out.append({ + 'sample_id': f"sample_{i}", + 'Wavelength': wl, + 'a_lambda': a_arr[j], + 'bb_lambda': bb_arr[j], + }) + + result_df = pd.DataFrame(rows_out) + result_df.to_csv(output_path, index=False, float_format='%.8f') + + msg = f"Step 8: QAA 反演完毕,水域={lake_name},λ₀={lambda_0}nm,结果保存于: {output_path}" + (self.logger.info if hasattr(self, 'logger') else print)(msg) + + def step9_concentration_inversion(self, **config): + """步骤9: 浓度反演(基于 QAA Step 8 输出的 a_lambda/bb_lambda)""" + from src.core.algorithms.concentration_inversion import ConcentrationPipeline + + conc_cfg = config.get('step9_concentration', {}) + input_csv = conc_cfg.get('input_csv') + output_csv = conc_cfg.get('output_csv') + lake_case = conc_cfg.get('lake_case', 'medium') + + if not input_csv or not os.path.exists(input_csv): + msg = f"[Step 9] QAA 结果文件不存在或路径为空: {input_csv}" + (self.logger.info if hasattr(self, 'logger') else print)(msg) + return + + if not output_csv: + output_dir = os.path.join(self.work_dir, "9_Concentration") + os.makedirs(output_dir, exist_ok=True) + output_csv = os.path.join(output_dir, "final_concentrations.csv") + + pipeline = ConcentrationPipeline(lake_case=lake_case) + result_csv = pipeline.run_pipeline(input_csv, output_csv) + + msg = f"Step 9: 浓度反演完毕,结果保存于: {result_csv}" + (self.logger.info if hasattr(self, 'logger') else print)(msg) + def step10_sampling(self, deglint_img_path: Optional[str] = None, interval: int = 50, sample_radius: int = 5, @@ -1521,13 +1630,13 @@ class WaterQualityInversionPipeline: else: self._notify("步骤5: 光谱提取", "skipped", "未配置") - # 步骤8: 计算水质指数 - if 'step8' in config: - self._notify("步骤8: 水质指数计算", "start") - self.step8_water_quality_indices(**config['step8']) - self._notify("步骤8: 水质指数计算", "completed", f"(输出: {self.indices_path})") + # 步骤6: 计算水质指数 + if 'step6' in config: + self._notify("步骤6: 水质光谱指数计算", "start") + self.step6_water_quality_indices(**config['step6']) + self._notify("步骤6: 水质光谱指数计算", "completed", f"(输出: {self.indices_path})") else: - self._notify("步骤8: 水质指数计算", "skipped", "未配置") + self._notify("步骤6: 水质光谱指数计算", "skipped", "未配置") # 步骤7: 训练模型 if 'step7' in config: @@ -1713,7 +1822,7 @@ class WaterQualityInversionPipeline: pipeline_info['step3'] = {'status': 'completed', 'output_file': str(self.deglint_img_path) if self.deglint_img_path else 'N/A'} pipeline_info['step4'] = {'status': 'completed', 'output_file': str(self.processed_csv_path) if self.processed_csv_path else 'N/A'} pipeline_info['step5'] = {'status': 'completed', 'output_file': str(self.training_csv_path) if self.training_csv_path else 'N/A'} - pipeline_info['step8'] = {'status': 'completed', 'output_file': str(self.indices_path) if self.indices_path else 'N/A'} + pipeline_info['step6'] = {'status': 'completed', 'output_file': str(self.indices_path) if self.indices_path else 'N/A'} pipeline_info['step7'] = {'status': 'completed', 'output_file': str(self.models_dir)} pipeline_info['step9'] = {'status': 'completed', 'output_file': str(self.custom_regression_path) if self.custom_regression_path else 'N/A'} pipeline_info['training_params'] = config.get('step7', {}) @@ -2158,7 +2267,7 @@ def main(): # 单步运行时建议显式指定;完整流程中可省略,将使用步骤2输出的耀斑掩膜 # 'glint_mask_path': r"path/to/severe_glint_area.dat", }, - 'step8': { + 'step6': { 'formula_csv_file': 'path/to/water_quality_formulas.csv', # 公式CSV文件路径 'formula_names': ['Al10SABI', 'TurbBe16RedOverViolet'], # 要计算的公式名称列表 'output_filename': 'water_quality_indices.csv', diff --git a/src/gui/core/worker_thread.py b/src/gui/core/worker_thread.py index 3c20f36..caf4661 100644 --- a/src/gui/core/worker_thread.py +++ b/src/gui/core/worker_thread.py @@ -325,9 +325,11 @@ class WorkerThread(QThread): 'step3': 'step3_remove_glint', 'step4': 'step4_process_csv', 'step5': 'step5_extract_training_spectra', - 'step8': 'step8_water_quality_indices', + 'step6': 'step6_water_quality_indices', 'step7': 'step7_ml_modeling', 'step8_non_empirical_modeling': 'step8_non_empirical_modeling', + 'step8_qaa': 'step8_qaa_inversion', + 'step9_concentration': 'step9_concentration_inversion', 'step9': 'step9_custom_regression', 'step10': 'step10_sampling', 'step11_ml': 'step11_ml_prediction', @@ -342,6 +344,19 @@ class WorkerThread(QThread): method_name = step_method_map[step_name] step_config = dict(config.get(step_name, {})) + # step8_qaa_inversion 内部使用 config.get('step8_qaa', {}) 读取内层, + # 必须透传完整 config dict(含外层 step_name key) + if step_name == 'step8_qaa': + method = getattr(self.pipeline, method_name) + result = method(**config) + return result + + # step9_concentration_inversion 同理,必须透传完整 config dict + if step_name == 'step9_concentration': + method = getattr(self.pipeline, method_name) + result = method(**config) + return result + # 透传面板顶层传入的外部预训练模型(GUI step11_prediction_panel 通过 config['_external_model'] 传入) # 非空才覆盖(遵循 feedback_never_overwrite_with_empty 原则) for key in ('_external_model', '_external_model_path', @@ -449,12 +464,12 @@ class WorkerThread(QThread): " → 请确认「流程步骤-阶段五」中已填写有效的边界 shp 路径。" ) - # ── 步骤8(水质指数):训练光谱 CSV ── - step8_cfg = config.get('step8', {}) - training_csv = step8_cfg.get('training_csv_path') + # ── 步骤6(水质光谱指数):训练光谱 CSV ── + step6_cfg = config.get('step6', {}) + training_csv = step6_cfg.get('training_csv_path') if training_csv and not os.path.isfile(training_csv): errors.append( - f"步骤 8(水质指数):训练光谱文件不存在:\n {training_csv}\n" + f"步骤 6(水质光谱指数):训练光谱文件不存在:\n {training_csv}\n" " → 请确认步骤 5 已成功运行并生成了训练光谱。" ) diff --git a/src/gui/panels/step9_concentration_panel.py b/src/gui/panels/step9_concentration_panel.py new file mode 100644 index 0000000..74f64b7 --- /dev/null +++ b/src/gui/panels/step9_concentration_panel.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Step9 面板 - 浓度反演(基于 QAA 物理反演的二次反演) +""" + +import os + +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QGroupBox, QFormLayout, QHBoxLayout, + QLabel, QCheckBox, QPushButton, QMessageBox, QComboBox, + QFileDialog, +) +from PyQt5.QtGui import QFont +from PyQt5.QtCore import Qt + +from src.gui.components.custom_widgets import FileSelectWidget +from src.gui.styles import ModernStylesheet + + +class Step9ConcentrationPanel(QWidget): + """步骤9:浓度反演(物理模型二次反演)""" + def __init__(self, parent=None): + super().__init__(parent) + self.init_ui() + + def init_ui(self): + layout = QVBoxLayout() + + title = QLabel("步骤9:浓度反演(物理模型二次反演)") + title.setFont(QFont("Arial", 12, QFont.Bold)) + layout.addWidget(title) + + # 输入 QAA 结果文件 + self.input_file = FileSelectWidget( + "QAA 结果文件:", + "CSV Files (*.csv);;All Files (*.*)" + ) + self.input_file.line_edit.setPlaceholderText( + "选择 Step 8 输出的 a_lambda_results.csv" + ) + layout.addWidget(self.input_file) + + # 输出路径 + self.output_file = FileSelectWidget( + "输出文件:", + "CSV Files (*.csv);;All Files (*.*)", + mode="save" + ) + self.output_file.line_edit.setPlaceholderText( + "自动生成到 9_Concentration,或手动指定..." + ) + layout.addWidget(self.output_file) + + # 选择反演指标 + indicators_group = QGroupBox("选择反演指标") + indicators_layout = QFormLayout() + + self.chla_check = QCheckBox("叶绿素 A (Chl-a)") + self.chla_check.setChecked(True) + self.cdom_check = QCheckBox("CDOM 吸收系数 a_dg(440)") + self.cdom_check.setChecked(True) + self.turbidity_check = QCheckBox("浊度 (Turbidity)") + self.turbidity_check.setChecked(True) + self.tn_check = QCheckBox("总氮 (TN)") + self.tn_check.setChecked(True) + self.tp_check = QCheckBox("总磷 (TP)") + self.tp_check.setChecked(True) + + chk_layout = QVBoxLayout() + chk_layout.setSpacing(6) + for cb in [self.chla_check, self.cdom_check, + self.turbidity_check, self.tn_check, self.tp_check]: + chk_layout.addWidget(cb) + + indicators_layout.addRow("水质参数:", chk_layout) + indicators_group.setLayout(indicators_layout) + layout.addWidget(indicators_group) + + # 水体类型(用于比吸收系数自适应) + lake_group = QGroupBox("水体类型") + lake_layout = QFormLayout() + self.lake_case_combo = QComboBox() + self.lake_case_combo.addItems([ + "通用 (medium)", + "oligotrophic_clear(寡营养清澈)", + "bloom_dominant(藻华主导)", + "turbid_mixed(高浊混合)", + ]) + self.lake_case_combo.setCurrentIndex(0) + lake_layout.addRow("水体类型:", self.lake_case_combo) + lake_group.setLayout(lake_layout) + layout.addWidget(lake_group) + + # 启用步骤 + self.enable_checkbox = QCheckBox("启用此步骤") + self.enable_checkbox.setChecked(False) + layout.addWidget(self.enable_checkbox) + + # 独立运行按钮 + self.run_btn = QPushButton("执行浓度反演") + self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success')) + self.run_btn.clicked.connect(self.run_step) + layout.addWidget(self.run_btn) + + layout.addStretch() + self.setLayout(layout) + + def _get_default_work_dir(self) -> str: + if hasattr(self, 'work_dir') and self.work_dir: + return str(self.work_dir) + mw = self.window() + if mw and hasattr(mw, 'work_dir') and mw.work_dir: + return str(mw.work_dir) + return "" + + def browse_output_path(self): + current = self.output_file.get_path().strip() + if current: + initial_dir = os.path.dirname(current) + initial_file = os.path.basename(current) + else: + initial_dir = "" + initial_file = "" + + if not initial_dir or not os.path.isdir(initial_dir): + work_dir = self._get_default_work_dir() + initial_dir = os.path.join(work_dir, "9_Concentration") if work_dir else "" + if initial_dir and not os.path.isdir(initial_dir): + os.makedirs(initial_dir, exist_ok=True) + + file_path, _ = QFileDialog.getSaveFileName( + self, "保存输出文件", + os.path.join(initial_dir, initial_file) if initial_file else initial_dir, + "CSV Files (*.csv);;All Files (*.*)" + ) + if file_path: + self.output_file.set_path(file_path) + + def get_config(self) -> dict: + enabled_indicators = [] + if self.chla_check.isChecked(): + enabled_indicators.append('chla') + if self.cdom_check.isChecked(): + enabled_indicators.append('cdom') + if self.turbidity_check.isChecked(): + enabled_indicators.append('turbidity') + if self.tn_check.isChecked(): + enabled_indicators.append('tn') + if self.tp_check.isChecked(): + enabled_indicators.append('tp') + + lake_case_map = { + 0: "medium", + 1: "oligotrophic_clear", + 2: "bloom_dominant", + 3: "turbid_mixed", + } + lake_case = lake_case_map.get(self.lake_case_combo.currentIndex(), "medium") + + return { + 'input_csv': self.input_file.get_path(), + 'output_csv': self.output_file.get_path(), + 'enabled_indicators': enabled_indicators, + 'lake_case': lake_case, + } + + def set_config(self, config: dict): + if 'input_csv' in config: + self.input_file.set_path(config['input_csv']) + if 'output_csv' in config: + self.output_file.set_path(config['output_csv']) + + def update_from_config(self, work_dir=None, pipeline=None): + if work_dir: + self.work_dir = work_dir + elif hasattr(self, 'work_dir') and self.work_dir: + pass + else: + self.work_dir = None + + if self.work_dir: + step8_dir = os.path.join(self.work_dir, "8_QAA_Inversion") + if os.path.isdir(step8_dir): + candidates = [] + for f in sorted(os.listdir(step8_dir)): + if f.lower().endswith('.csv'): + candidates.append(os.path.join(step8_dir, f)) + if candidates: + self.input_file.set_path(candidates[0]) + + conc_dir = os.path.join(self.work_dir, "9_Concentration") + os.makedirs(conc_dir, exist_ok=True) + output_path = os.path.join(conc_dir, "final_concentrations.csv").replace('\\', '/') + self.output_file.set_path(output_path) + + def run_step(self): + input_path = self.input_file.get_path() + if not input_path: + QMessageBox.warning(self, "输入错误", "请选择 QAA 结果文件!") + return + + main_window = self.window() + if hasattr(main_window, 'run_single_step'): + config = {'step9_concentration': self.get_config()} + main_window.run_single_step('step9_concentration', config) + else: + self._run_concentration_direct() + + def _run_concentration_direct(self): + from src.core.algorithms.concentration_inversion import ConcentrationPipeline + + input_path = self.input_file.get_path() + output_path = self.output_file.get_path() + + if not output_path: + work_dir = self._get_default_work_dir() + conc_dir = os.path.join(work_dir, "9_Concentration") if work_dir else "" + if conc_dir and not os.path.isdir(conc_dir): + os.makedirs(conc_dir, exist_ok=True) + output_path = os.path.join(conc_dir, "final_concentrations.csv").replace('\\', '/') + + lake_case_map = { + 0: "medium", + 1: "oligotrophic_clear", + 2: "bloom_dominant", + 3: "turbid_mixed", + } + lake_case = lake_case_map.get(self.lake_case_combo.currentIndex(), "medium") + + try: + pipeline = ConcentrationPipeline(lake_case=lake_case) + result_csv = pipeline.run_pipeline(input_path, output_path) + QMessageBox.information( + self, "执行成功", + f"浓度反演完成!\n结果已保存到:\n{result_csv}" + ) + except Exception as e: + QMessageBox.critical(self, "执行错误", f"浓度反演失败:\n{str(e)}") \ No newline at end of file diff --git a/src/gui/water_quality_gui.py b/src/gui/water_quality_gui.py index 6526866..46b45f3 100644 --- a/src/gui/water_quality_gui.py +++ b/src/gui/water_quality_gui.py @@ -119,14 +119,12 @@ from src.gui.panels.step2_panel import Step2Panel from src.gui.panels.step3_panel import Step3Panel from src.gui.panels.step4_panel import Step4Panel from src.gui.panels.step5_panel import Step5Panel -from src.gui.panels.step8_panel import Step8Panel # was step5_5_panel +from src.gui.panels.step6_panel import Step6Panel # was step8_panel from src.gui.panels.step7_panel import Step7Panel # was step6_panel -from src.gui.panels.step8_non_empirical_panel import Step8NonEmpiricalPanel # was step6_5_panel -from src.gui.panels.step9_panel import Step9Panel # was step6_75_panel +from src.gui.panels.step8_qaa_panel import Step8QAAPanel # QAA 物理反演(非经验模型) +from src.gui.panels.step9_concentration_panel import Step9ConcentrationPanel # 浓度反演 from src.gui.panels.step10_panel import Step10Panel # was step7_panel from src.gui.panels.step11_ml_panel import Step11MlPanel # ML prediction (step11_ml) -from src.gui.panels.step11_panel import Step11Panel # was step8_5_panel -from src.gui.panels.step12_panel import Step12Panel # was step8_75_panel from src.gui.panels.step14_panel import Step14Panel # was step9_panel from src.gui.dialogs import BandConfirmDialog, AISettingsDialog from src.gui.panels.visualization_panel import VisualizationPanel @@ -1390,7 +1388,7 @@ class WaterQualityGUI(QMainWindow): 'step5': { 'training_spectra': '5_training_spectra/training_spectra.csv' }, - 'step8': { + 'step6': { 'water_indices': '6_water_quality_indices/water_quality_indices.csv' }, 'step7': { @@ -1438,8 +1436,8 @@ class WaterQualityGUI(QMainWindow): 'boundary_mask_path': ('step1', 'water_mask', 'boundary_mask_file'), # 步骤5可选水体掩膜 'glint_mask_path': ('step2', 'glint_mask', 'glint_mask_file') # 步骤5可选耀斑掩膜 }, - 'step8': { - 'training_csv_path': ('step5', 'training_spectra', 'output_file') # 步骤8需要步骤5输出的训练光谱 + 'step6': { + 'training_csv_path': ('step5', 'training_spectra', 'output_file') # 步骤6需要步骤5输出的训练光谱 }, 'step7': { 'csv_path': ('step5', 'training_spectra', 'csv_file') # 步骤7需要训练光谱数据 @@ -1850,7 +1848,7 @@ class WaterQualityGUI(QMainWindow): "阶段二:样本数据准备 ": [ ("step4", "4. 数据标准化处理"), ("step5", "5. 光谱特征提取"), - ("step8", "6. 水质参数指数计算"), + ("step6", "6. 水质参数指数计算"), ], "阶段三:模型构建与训练": [ ("step7", "7. 机器学习模型训练"), @@ -1964,19 +1962,17 @@ class WaterQualityGUI(QMainWindow): self.step5_panel = Step5Panel() self.step_stack.addTab(self.create_scroll_area(self.step5_panel), QIcon(self.get_icon_path("5.png")), "特征构建") - self.step8_panel = Step8Panel() - self.step_stack.addTab(self.create_scroll_area(self.step8_panel), QIcon(self.get_icon_path("5.png")), "水质指数") + self.step6_panel = Step6Panel() + self.step_stack.addTab(self.create_scroll_area(self.step6_panel), QIcon(self.get_icon_path("6.png")), "水质光谱指数计算") self.step7_panel = Step7Panel() - self.step_stack.addTab(self.create_scroll_area(self.step7_panel), QIcon(self.get_icon_path("6.png")), "监督建模") + self.step_stack.addTab(self.create_scroll_area(self.step7_panel), QIcon(self.get_icon_path("7.png")), "监督建模") - self.step8_non_empirical_panel = Step8NonEmpiricalPanel() - self.step_stack.addTab(self.create_scroll_area(self.step8_non_empirical_panel), QIcon(self.get_icon_path("6.png")), "回归建模") - self.step_stack.tabBar().setTabVisible(7, False) # 隐藏回归建模 Tab + self.step8_qaa_panel = Step8QAAPanel() + self.step_stack.addTab(self.create_scroll_area(self.step8_qaa_panel), QIcon(self.get_icon_path("6.png")), "物理推导(非经验模型)") - self.step9_panel = Step9Panel() - self.step_stack.addTab(self.create_scroll_area(self.step9_panel), QIcon(self.get_icon_path("6.png")), "自定义回归建模") - self.step_stack.tabBar().setTabVisible(8, False) # 隐藏自定义回归建模 Tab + self.step9_concentration_panel = Step9ConcentrationPanel() + self.step_stack.addTab(self.create_scroll_area(self.step9_concentration_panel), QIcon(self.get_icon_path("6.png")), "浓度反演") self.step10_panel = Step10Panel() self.step_stack.addTab(self.create_scroll_area(self.step10_panel), QIcon(self.get_icon_path("7.png")), "采样点布设") @@ -1984,14 +1980,6 @@ class WaterQualityGUI(QMainWindow): self.step11_ml_panel = Step11MlPanel() # ML prediction panel (step11_ml) self.step_stack.addTab(self.create_scroll_area(self.step11_ml_panel), QIcon(self.get_icon_path("8.png")), "监督预测") - self.step11_panel = Step11Panel() - self.step_stack.addTab(self.create_scroll_area(self.step11_panel), QIcon(self.get_icon_path("8.png")), "回归预测") - self.step_stack.tabBar().setTabVisible(11, False) # 隐藏回归预测 Tab - - self.step12_panel = Step12Panel() - self.step_stack.addTab(self.create_scroll_area(self.step12_panel), QIcon(self.get_icon_path("8.png")), "自定义回归预测") - self.step_stack.tabBar().setTabVisible(12, False) # 隐藏自定义回归预测 Tab - self.step14_panel = Step14Panel() self.step_stack.addTab(self.create_scroll_area(self.step14_panel), QIcon(self.get_icon_path("10.png")), "专题图生成") @@ -2143,7 +2131,7 @@ class WaterQualityGUI(QMainWindow): 'step3': 2, 'step4': 3, 'step5': 4, - 'step8': 5, + 'step6': 5, 'step7': 6, 'step8_non_empirical_modeling': 7, 'step9': 8, @@ -2174,7 +2162,7 @@ class WaterQualityGUI(QMainWindow): 2: 'step3', 3: 'step4', 4: 'step5', - 5: 'step8', + 5: 'step6', 6: 'step7', 7: 'step8_non_empirical_modeling', 8: 'step9', @@ -2219,44 +2207,36 @@ class WaterQualityGUI(QMainWindow): elif index == 4: self.step5_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) - # Step8(水质指数)切换时自动填充输出路径 + # Step6(水质光谱指数)切换时自动填充输出路径 elif index == 5: - self.step8_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) + self.step6_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) # Step7(监督建模)切换时自动填充训练数据和输出路径 elif index == 6: self.step7_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) - # Step8非经验建模切换时自动填充训练数据和模型目录 + # Step8 QAA 物理反演切换时自动填充光谱数据和输出路径 elif index == 7: - self.step8_non_empirical_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) + self.step8_qaa_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) - # Step9(自定义回归建模)切换时自动填充训练数据和模型目录 + # Step9 浓度反演切换时自动填充 QAA 结果和输出路径 elif index == 8: - self.step9_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) + self.step9_concentration_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) # Step10(采样点布设)切换时自动填充掩膜和输出路径 elif index == 9: self.step10_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) - # Step8(机器学习预测)切换时自动填充采样光谱和模型目录 + # Step11(机器学习预测)切换时自动填充采样光谱和模型目录 elif index == 10: self.step11_ml_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) - # Step11(回归预测)切换时自动填充采样光谱和回归模型目录 - elif index == 11: - self.step11_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) - - # Step12(自定义回归预测)切换时自动填充采样光谱和自定义回归模型目录 - elif index == 12: - self.step12_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) - # Step14(专题图生成)切换时自动填充预测结果目录 - elif index == 13: + elif index == 11: self.step14_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) # 可视化分析面板切换时自动推断图像目录并加载目录树 - elif index == 14: + elif index == 12: self.viz_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) def apply_stylesheet(self): @@ -2300,20 +2280,14 @@ class WaterQualityGUI(QMainWindow): self.step4_panel.set_config(config['step4']) if 'step5' in config: self.step5_panel.set_config(config['step5']) - if 'step8' in config: - self.step8_panel.set_config(config['step8']) + if 'step6' in config: + self.step6_panel.set_config(config['step6']) if 'step7' in config: self.step7_panel.set_config(config['step7']) - if 'step8_non_empirical_modeling' in config: - self.step8_non_empirical_panel.set_config(config['step8_non_empirical_modeling']) - if 'step9' in config: - self.step9_panel.set_config(config['step9']) if 'step10' in config: self.step10_panel.set_config(config['step10']) if 'step11_ml' in config: self.step11_ml_panel.set_config(config['step11_ml']) - if 'step11' in config: - self.step11_panel.set_config(config['step11']) if 'step14' in config: self.step14_panel.set_config(config['step14']) if 'visualization' in config: @@ -2358,13 +2332,10 @@ class WaterQualityGUI(QMainWindow): 'step3': self.step3_panel.get_config(), 'step4': self.step4_panel.get_config(), 'step5': self.step5_panel.get_config(), - 'step8': self.step8_panel.get_config(), + 'step6': self.step6_panel.get_config(), 'step7': self.step7_panel.get_config(), - 'step8_non_empirical_modeling': self.step8_non_empirical_panel.get_config(), - 'step9': self.step9_panel.get_config(), 'step10': self.step10_panel.get_config(), 'step11_ml': self.step11_ml_panel.get_config(), - 'step11': self.step11_panel.get_config(), 'step14': self.step14_panel.get_config(), 'visualization': self.viz_panel.get_config(), 'report_generation': self.report_panel.get_config(), @@ -2416,14 +2387,10 @@ class WaterQualityGUI(QMainWindow): 'step3': self.step3_panel, 'step4': self.step4_panel, 'step5': self.step5_panel, - 'step8': self.step8_panel, + 'step6': self.step6_panel, 'step7': self.step7_panel, - 'step8_non_empirical_modeling': self.step8_non_empirical_panel, - 'step9': self.step9_panel, 'step10': self.step10_panel, 'step11_ml': self.step11_ml_panel, - 'step11': self.step11_panel, - 'step12': self.step12_panel, 'step14': self.step14_panel, } return panel_map.get(step_id) @@ -2518,7 +2485,7 @@ class WaterQualityGUI(QMainWindow): '3_deglint': 'step3', '4_processed_data': 'step4', '5_training_spectra': 'step5', - '6_water_quality_indices': 'step8', + '6_water_quality_indices': 'step6', '7_Supervised_Model_Training': 'step7', '8_Regression_Modeling': 'step8_non_empirical_modeling', '9_Custom_Regression_Modeling': 'step9', @@ -2572,7 +2539,7 @@ class WaterQualityGUI(QMainWindow): discovered_outputs[step_id]['processed_data'] = str(file_path) elif 'training_spectra' in file_name and step_id == 'step5': discovered_outputs[step_id]['training_spectra'] = str(file_path) - elif 'water_quality_indices' in file_name and step_id == 'step8': + elif 'water_quality_indices' in file_name and step_id == 'step6': discovered_outputs[step_id]['water_indices'] = str(file_path) elif 'sampling_spectra' in file_name and step_id == 'step10': discovered_outputs[step_id]['sampling_points'] = str(file_path) @@ -2599,7 +2566,7 @@ class WaterQualityGUI(QMainWindow): # 首先扫描工作目录发现已有的输出文件 self.scan_work_directory_for_files(work_path) - step_order = ['step2', 'step3', 'step4', 'step5', 'step8', 'step7', 'step8_non_empirical_modeling', 'step9', + step_order = ['step2', 'step3', 'step4', 'step5', 'step6', 'step7', 'step8_non_empirical_modeling', 'step9', 'step10', 'step11_ml', 'step11', 'step12', 'step14'] filled_count = 0 @@ -2622,14 +2589,10 @@ class WaterQualityGUI(QMainWindow): ('step2', self.step2_panel), ('step3', self.step3_panel), ('step5', self.step5_panel), - ('step8', self.step8_panel), + ('step6', self.step6_panel), ('step7', self.step7_panel), - ('step8_non_empirical_modeling', self.step8_non_empirical_panel), - ('step9', self.step9_panel), ('step10', self.step10_panel), ('step11_ml', self.step11_ml_panel), - ('step11', self.step11_panel), - ('step12', self.step12_panel), ('step14', self.step14_panel) ] @@ -2926,7 +2889,7 @@ class WaterQualityGUI(QMainWindow): "step4", # CSV 实测数据清洗 "step5", # 实测点光谱提取(→ training_csv_path) "step7", # ML 监督建模 - "step8", # 水质指数计算(辅助训练) + "step6", # 水质指数计算(辅助训练) "step8_non_empirical_modeling", # 非经验回归建模 "step9", # 自定义回归建模 ] @@ -3022,11 +2985,11 @@ class WaterQualityGUI(QMainWindow): # 准备实际运行配置(排除未启用的步骤) worker_config = copy.deepcopy(config) - step8_cfg = worker_config.get('step8') - if step8_cfg: - enabled = step8_cfg.pop('enabled', True) + step6_cfg = worker_config.get('step6') + if step6_cfg: + enabled = step6_cfg.pop('enabled', True) if not enabled: - worker_config.pop('step8', None) + worker_config.pop('step6', None) # 工作线程内创建 Pipeline,避免主线程阻塞及 Qt5Agg 子线程绘图卡死 self.worker = WorkerThread(work_dir, worker_config, mode='full', skip_list=skip_list) @@ -3256,12 +3219,12 @@ class WaterQualityGUI(QMainWindow): def update_ui_for_training_mode(self): """根据训练数据模式更新UI状态""" # 需要禁用的步骤ID(对应无训练数据模式下需要禁用的步骤) - disabled_step_ids = ['step4', 'step5', 'step8', 'step7', 'step8_non_empirical_modeling', 'step9'] + disabled_step_ids = ['step4', 'step5', 'step6', 'step7', 'step8_non_empirical_modeling', 'step9'] # 更新标签页的启用/禁用状态 step_id_to_tab = { 'step1': 0, 'step2': 1, 'step3': 2, 'step4': 3, - 'step5': 4, 'step8': 5, 'step7': 6, 'step8_non_empirical_modeling': 7, + 'step5': 4, 'step6': 5, 'step7': 6, 'step8_non_empirical_modeling': 7, 'step9': 8, 'step10': 9, 'step11_ml': 10, 'step11': 11, 'step12': 12, 'step14': 13, 'step9_viz': 14 }