Compare commits

..

9 Commits

25 changed files with 1905 additions and 1570 deletions

View File

@ -20,13 +20,16 @@ from typing import Any, Dict, List, Optional, Set
# ============================================================
STEP_MAP_OLD_TO_NEW: Dict[str, str] = {
"step5_5": "step8",
"step5_5": "step7",
"step6_5": "step8_non_empirical_modeling",
"step6_75": "step9",
"step8_5": "step11",
"step8_75": "step12",
"step7": "step10",
"step7": "step8",
"step8": "step7",
"step9": "step14",
"step10": "step4",
"step11_ml": "step10",
"step11": "step11",
}
STEP_MAP_NEW_TO_OLD: Dict[str, str] = {v: k for k, v in STEP_MAP_OLD_TO_NEW.items()}

View File

@ -95,14 +95,14 @@ PIPELINE_STEPS: List[StepSpec] = [
description="耀斑去除",
),
StepSpec(
step_id="step4", method_name="step4_process_csv",
step_id="step4", method_name="step5_process_csv",
requires=["csv_path"], produces=["processed_csv_path"],
required_input_files=["csv_path"],
output_file="{work_dir}/4_processed_data/processed_data.csv",
description="CSV 异常值清洗",
),
StepSpec(
step_id="step5", method_name="step5_extract_training_spectra",
step_id="step5", method_name="step6_extract_spectra",
requires=["deglint_img_path", "processed_csv_path", "csv_path", "boundary_path", "glint_mask_path"],
produces=["training_csv_path"],
parameter_map={
@ -115,14 +115,14 @@ PIPELINE_STEPS: List[StepSpec] = [
description="实测样本点光谱提取",
),
StepSpec(
step_id="step8", method_name="step8_water_quality_indices",
step_id="step7", method_name="step7_calc_indices",
requires=["training_csv_path"], produces=["indices_path", "trad_indices_dir"],
required_input_files=["training_csv_path"],
output_file="{work_dir}/6_water_quality_indices/training_spectra_indices.csv",
description="水质光谱指数计算双轨输出A轨宽表 + B轨单文件",
description="水质参数指数计算双轨输出A轨宽表 + B轨单文件",
),
StepSpec(
step_id="step7", method_name="step7_ml_modeling",
step_id="step8", method_name="step8_train_ml",
requires=["training_csv_path"], produces=["models_dir"],
required_input_files=["training_csv_path"],
output_file="{work_dir}/7_Supervised_Model_Training/best_models.pkl",
@ -138,22 +138,21 @@ PIPELINE_STEPS: List[StepSpec] = [
description="非经验统计回归",
),
StepSpec(
step_id="step9", method_name="step9_custom_regression",
requires=["indices_path"], produces=["models_dir"],
parameter_map={"indices_path": "csv_path"},
required_input_files=["indices_path"],
output_file="{work_dir}/9_Custom_Regression_Modeling/custom_regression_models.pkl",
description="自定义回归分析",
step_id="step9", method_name="step9_watercolor_inversion",
requires=["deglint_img_path", "water_mask_path"], produces=["watercolor_index_dir"],
required_input_files=["deglint_img_path"],
output_file="{work_dir}/9_WaterColor_Index_Images",
description="水色指数反演BSQ 影像直接处理)",
),
StepSpec(
step_id="step10", method_name="step10_sampling",
step_id="step10", method_name="step4_sampling",
requires=["deglint_img_path", "water_mask_path"], produces=["sampling_csv_path"],
required_input_files=["deglint_img_path", "water_mask_path"],
output_file="{work_dir}/10_sampling/sampling_spectra.csv",
output_file="{work_dir}/4_sampling/sampling_spectra.csv",
description="整景密集采样点生成 + 光谱提取",
),
StepSpec(
step_id="step11_ml", method_name="step11_ml_prediction",
step_id="step11_ml", method_name="step9_predict_ml",
requires=["sampling_csv_path", "models_dir"], produces=["prediction_csv_path"],
required_input_files=["sampling_csv_path", "models_dir"],
output_file="{work_dir}/11_12_13_predictions/prediction_results.csv",
@ -168,16 +167,7 @@ PIPELINE_STEPS: List[StepSpec] = [
description="非经验模型预测",
),
StepSpec(
step_id="step12", method_name="step12_custom_regression_prediction",
requires=["sampling_csv_path", "models_dir", "formula_csv_path"],
produces=["prediction_dir"],
parameter_map={"models_dir": "custom_regression_dir"},
required_input_files=["sampling_csv_path", "models_dir", "formula_csv_path"],
output_file="{work_dir}/11_12_13_predictions/custom_regression_predictions",
description="自定义回归预测",
),
StepSpec(
step_id="step14", method_name="step14_distribution_map",
step_id="step14", method_name="step10_map",
requires=["prediction_csv_path", "boundary_shp_path"],
produces=["distribution_map_path"],
required_input_files=["prediction_csv_path", "boundary_shp_path"],

View File

@ -2,7 +2,7 @@
"""
数据准备步骤
包含 step4_process_csv, step5_extract_training_spectra, step5_5_calculate_water_quality_indices
包含 step5_process_csv, step6_extract_spectra, step5_5_calculate_water_quality_indices
"""
import time

View File

@ -585,7 +585,7 @@ class WaterQualityInversionPipeline:
status="failed", error=str(e))
raise
def step4_process_csv(self, csv_path: str, skip_dependency_check: bool = False, **kwargs) -> str:
def step5_process_csv(self, csv_path: str, skip_dependency_check: bool = False, **kwargs) -> str:
"""
步骤4: 对csv文件进行处理筛选剔除异常值
@ -606,7 +606,7 @@ class WaterQualityInversionPipeline:
self._notify("completed", f"处理后的CSV文件已保存: {result}")
return result
def step5_extract_training_spectra(self, deglint_img_path: Optional[str] = None,
def step6_extract_spectra(self, deglint_img_path: Optional[str] = None,
radius: int = 5,
source_epsg: int = 4326,
csv_path: Optional[str] = None,
@ -657,7 +657,7 @@ class WaterQualityInversionPipeline:
self._notify("completed", f"训练光谱数据已保存: {result}")
return result
def step6_water_quality_indices(self,
def step7_calc_indices(self,
training_csv_path: Optional[str] = None,
formula_csv_file: Optional[str] = None,
formula_names: Optional[List[str]] = None,
@ -701,7 +701,7 @@ class WaterQualityInversionPipeline:
self._notify("completed", f"水质指数已保存: {result}")
return result
def step7_ml_modeling(self, feature_start_column: str = "374.285004",
def step8_train_ml(self, feature_start_column: str = "374.285004",
preprocessing_methods: List[str] = None,
model_names: List[str] = None,
split_methods: List[str] = None,
@ -859,7 +859,7 @@ class WaterQualityInversionPipeline:
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,
def step4_sampling(self, deglint_img_path: Optional[str] = None,
interval: int = 50,
sample_radius: int = 5,
chunk_size: int = 1000,
@ -906,7 +906,7 @@ class WaterQualityInversionPipeline:
self._notify("completed", f"采样点光谱数据已保存: {result}")
return result
def step11_ml_prediction(self, sampling_csv_path: str,
def step9_predict_ml(self, sampling_csv_path: str,
models_dir: Optional[str] = None,
metric: str = 'test_r2',
prediction_column: str = 'prediction',
@ -947,7 +947,7 @@ class WaterQualityInversionPipeline:
self._notify("completed", f"预测完成,结果保存在: {self.prediction_dir}")
return result
def step14_distribution_map(self, prediction_csv_path: str,
def step10_map(self, prediction_csv_path: str,
boundary_shp_path: str,
output_image_path: Optional[str] = None,
resolution: float = 30,
@ -1623,7 +1623,7 @@ class WaterQualityInversionPipeline:
# 步骤4: 处理CSV文件
if 'step4' in config:
self._notify("步骤4: 数据预处理", "start")
self.step4_process_csv(**config['step4'])
self.step5_process_csv(**config['step4'])
self._notify("步骤4: 数据预处理", "completed", f"(输出: {self.processed_csv_path})")
else:
self._notify("步骤4: 数据预处理", "skipped", "未配置")
@ -1631,7 +1631,7 @@ class WaterQualityInversionPipeline:
# 步骤5: 提取训练样本点光谱
if 'step5' in config:
self._notify("步骤5: 光谱提取", "start")
self.step5_extract_training_spectra(**config['step5'])
self.step6_extract_spectra(**config['step5'])
self._notify("步骤5: 光谱提取", "completed", f"(输出: {self.training_csv_path})")
else:
self._notify("步骤5: 光谱提取", "skipped", "未配置")
@ -1639,7 +1639,7 @@ class WaterQualityInversionPipeline:
# 步骤6: 计算水质指数
if 'step6' in config:
self._notify("步骤6: 水质光谱指数计算", "start")
self.step6_water_quality_indices(**config['step6'])
self.step7_calc_indices(**config['step6'])
self._notify("步骤6: 水质光谱指数计算", "completed", f"(输出: {self.indices_path})")
else:
self._notify("步骤6: 水质光谱指数计算", "skipped", "未配置")
@ -1647,7 +1647,7 @@ class WaterQualityInversionPipeline:
# 步骤7: 训练模型
if 'step7' in config:
self._notify("步骤7: 模型训练", "start")
self.step7_ml_modeling(**config['step7'])
self.step8_train_ml(**config['step7'])
self._notify("步骤7: 模型训练", "completed", f"(输出: {self.models_dir})")
else:
self._notify("步骤7: 模型训练", "skipped", "未配置")
@ -1671,7 +1671,7 @@ class WaterQualityInversionPipeline:
# 步骤10: 生成预测采样点
if 'step10' in config:
self._notify("步骤10: 采样点生成", "start")
sampling_csv_path = self.step10_sampling(**config['step10'])
sampling_csv_path = self.step4_sampling(**config['step10'])
self._notify("步骤10: 采样点生成", "completed", f"(输出: {sampling_csv_path})")
else:
sampling_csv_path = None
@ -1682,7 +1682,7 @@ class WaterQualityInversionPipeline:
self._notify("步骤11: 参数预测", "start")
step11_ml_config = config['step11_ml'].copy()
step11_ml_config['sampling_csv_path'] = sampling_csv_path
prediction_files = self.step11_ml_prediction(**step11_ml_config)
prediction_files = self.step9_predict_ml(**step11_ml_config)
self._notify("步骤11: 参数预测", "completed", f"(生成{len(prediction_files)}个预测文件)")
else:
prediction_files = {}
@ -1724,7 +1724,7 @@ class WaterQualityInversionPipeline:
step14_config['prediction_csv_path'] = pred_file
if 'output_image_path' not in step14_config:
step14_config['output_image_path'] = None
dist_map_path = self.step14_distribution_map(**step14_config)
dist_map_path = self.step10_map(**step14_config)
distribution_maps[target_name] = dist_map_path
self._notify("步骤14: 分布图生成", "completed", f"(生成{len(distribution_maps)}个分布图)")
else:
@ -2426,7 +2426,7 @@ def example_independent_steps():
# 示例3: 独立运行步骤4 - 数据预处理
print("\n示例3: 独立运行步骤4 - 数据预处理")
try:
processed_csv = pipeline.step4_process_csv(
processed_csv = pipeline.step5_process_csv(
csv_path="path/to/water_quality_data.csv"
)
print(f"处理后的CSV文件: {processed_csv}")
@ -2436,7 +2436,7 @@ def example_independent_steps():
# 示例4: 独立运行步骤5 - 光谱提取
print("\n示例4: 独立运行步骤5 - 光谱提取")
try:
training_spectra = pipeline.step5_extract_training_spectra(
training_spectra = pipeline.step6_extract_spectra(
deglint_img_path="path/to/deglint_image.bsq",
csv_path="path/to/processed_data.csv",
glint_mask_path="path/to/severe_glint_area.dat",
@ -2460,7 +2460,7 @@ def example_independent_steps():
# 示例6: 独立运行步骤10 - 采样点生成
print("\n示例6: 独立运行步骤10 - 采样点生成")
try:
sampling_csv = pipeline.step10_sampling(
sampling_csv = pipeline.step4_sampling(
deglint_img_path="path/to/deglint_image.bsq",
water_mask_path="path/to/water_mask.dat",
skip_dependency_check=True
@ -2472,7 +2472,7 @@ def example_independent_steps():
# 示例7: 独立运行步骤11 - 水质预测
print("\n示例7: 独立运行步骤11 - 水质预测")
try:
predictions = pipeline.step11_ml_prediction(
predictions = pipeline.step9_predict_ml(
sampling_csv_path="path/to/sampling_spectra.csv",
models_dir="path/to/models_directory",
skip_dependency_check=True
@ -2484,7 +2484,7 @@ def example_independent_steps():
# 示例8: 独立运行步骤14 - 分布图生成
print("\n示例8: 独立运行步骤14 - 分布图生成")
try:
distribution_map = pipeline.step14_distribution_map(
distribution_map = pipeline.step10_map(
prediction_csv_path="path/to/prediction_results.csv",
boundary_shp_path="path/to/boundary.shp",
skip_dependency_check=True

View File

@ -59,14 +59,12 @@ class PreflightDialog(QDialog):
"step3": ("耀斑去除", 2),
"step4": ("数据清洗", 3),
"step5": ("特征构建", 4),
"step8": ("水质指数", 5),
"step7": ("监督建模", 6),
"step7": ("水质指数", 5),
"step8_non_empirical_modeling": ("回归建模", 7),
"step9": ("自定义回归建模", 8),
"step10": ("采样点布设", 9),
"step11_ml": ("监督预测", 10),
"step11": ("回归预测", 11),
"step12": ("自定义回归预测", 12),
"step9": ("水色指数反演", 8),
"step10": ("采样点布设", 10),
"step11_ml": ("监督预测", 11),
"step11": ("回归预测", 12),
"step14": ("专题图生成", 13),
}

View File

@ -272,10 +272,10 @@ class WorkerThread(QThread):
ctx = PipelineContext(
img_path=self.config.get('step1', {}).get('img_path'),
water_mask_path=self.config.get('step1', {}).get('mask_path'),
csv_path=self.config.get('step4', {}).get('csv_path'),
boundary_path=self.config.get('step5', {}).get('boundary_path'),
boundary_shp_path=self.config.get('step14', {}).get('boundary_shp_path'),
formula_csv_path=self.config.get('step12', {}).get('formula_csv_path'),
csv_path=self.config.get('step4_sampling', {}).get('csv_path'),
boundary_path=self.config.get('step5_clean', {}).get('boundary_path'),
boundary_shp_path=self.config.get('step11_map', {}).get('boundary_shp_path'),
formula_csv_path=self.config.get('step8_non_empirical_modeling', {}).get('formula_csv_path'),
work_dir=self.work_dir,
user_config=self.config
)
@ -323,19 +323,16 @@ class WorkerThread(QThread):
'step1': 'step1_generate_water_mask',
'step2': 'step2_find_glint_area',
'step3': 'step3_remove_glint',
'step4': 'step4_process_csv',
'step5': 'step5_extract_training_spectra',
'step6': 'step6_water_quality_indices',
'step7': 'step7_ml_modeling',
'step4_sampling': 'step4_sampling',
'step5_clean': 'step5_process_csv',
'step6_feature': 'step6_extract_spectra',
'step7_index': 'step7_calc_indices',
'step8_ml_train': 'step8_train_ml',
'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',
'step11': 'step11_non_empirical_prediction',
'step12': 'step12_custom_regression_prediction',
'step14': 'step14_distribution_map'
'step9_ml_predict': 'step9_predict_ml',
'step10_watercolor': 'step9_watercolor_inversion',
'step11_map': 'step10_map',
}
if step_name not in step_method_map:
@ -351,12 +348,6 @@ class WorkerThread(QThread):
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',
@ -371,17 +362,9 @@ class WorkerThread(QThread):
step_config['skip_dependency_check'] = True
if step_name == 'step14':
step_config.pop('step9_batch_mode', None)
step_config.pop('prediction_csv_dir', None)
step_config.pop('recursive_csv_scan', None)
if step_name in ['step2', 'step3', 'step4', 'step5', 'step7', 'step10', 'step11_ml', 'step11', 'step12']:
if step_name in ['step2', 'step3', 'step4_sampling', 'step5_clean', 'step7_index', 'step9_ml_predict']:
step_config.pop('output_path', None)
if step_name == 'step11' and 'models_dir' in step_config:
step_config['non_empirical_models_dir'] = step_config.pop('models_dir')
method = getattr(self.pipeline, method_name)
result = method(**step_config)
@ -440,100 +423,76 @@ class WorkerThread(QThread):
" → 请确认「耀斑去除」已成功运行,或重新配置路径。"
)
# ── 步骤4实测水质数据 CSV ──
step4_cfg = config.get('step4', {})
# ── 步骤4_sampling:实测水质数据 CSV ──
step4_cfg = config.get('step4_sampling', {})
csv_path = step4_cfg.get('csv_path')
if csv_path and not os.path.isfile(csv_path):
errors.append(
f"步骤 4实测水质数据文件不存在\n {csv_path}\n"
f"步骤 4_sampling:实测水质数据文件不存在:\n {csv_path}\n"
" → 请检查 CSV 路径是否正确,或重新上传数据文件。"
)
# ── 步骤5采样点平均光谱提取 ──
step5_cfg = config.get('step5', {})
# ── 步骤5_clean:采样点平均光谱提取 ──
step5_cfg = config.get('step5_clean', {})
step5_csv = step5_cfg.get('csv_path')
boundary_path = step5_cfg.get('boundary_path')
if step5_csv and not os.path.isfile(step5_csv):
errors.append(
f"步骤 5实测水质数据文件不存在\n {step5_csv}\n"
f"步骤 5_clean:实测水质数据文件不存在:\n {step5_csv}\n"
" → 请检查「流程步骤-阶段五」中的 CSV 路径。"
)
if boundary_path and not os.path.isfile(boundary_path):
errors.append(
f"步骤 5边界矢量文件不存在\n {boundary_path}\n"
f"步骤 5_clean:边界矢量文件不存在:\n {boundary_path}\n"
" → 请确认「流程步骤-阶段五」中已填写有效的边界 shp 路径。"
)
# ── 步骤6水质光谱指数训练光谱 CSV ──
step6_cfg = config.get('step6', {})
# ── 步骤6_feature(水质光谱指数):训练光谱 CSV ──
step6_cfg = config.get('step6_feature', {})
training_csv = step6_cfg.get('training_csv_path')
if training_csv and not os.path.isfile(training_csv):
errors.append(
f"步骤 6水质光谱指数训练光谱文件不存在\n {training_csv}\n"
" → 请确认步骤 5 已成功运行并生成了训练光谱。"
f"步骤 6_feature(水质光谱指数):训练光谱文件不存在:\n {training_csv}\n"
" → 请确认步骤 5_clean 已成功运行并生成了训练光谱。"
)
# ── 步骤7ML 建模) ──
step7_cfg = config.get('step7', {})
# ── 步骤8_ml_trainML 建模) ──
step7_cfg = config.get('step8_ml_train', {})
step7_csv = step7_cfg.get('training_csv_path')
if step7_csv and not os.path.isfile(step7_csv):
errors.append(
f"步骤 7ML 建模):训练光谱文件不存在:\n {step7_csv}\n"
" → 请确认步骤 5 已成功运行并生成了训练光谱。"
f"步骤 8_ml_trainML 建模):训练光谱文件不存在:\n {step7_csv}\n"
" → 请确认步骤 5_clean 已成功运行并生成了训练光谱。"
)
# ── 步骤11 ML 预测:密集采样点 CSV + 模型目录 ──
step11_ml_cfg = config.get('step11_ml', {})
ml_csv = step11_ml_cfg.get('sampling_csv_path')
models_dir = step11_ml_cfg.get('models_dir')
# ── 步骤9_ml_predict:密集采样点 CSV + 模型目录 ──
step9_cfg = config.get('step9_ml_predict', {})
ml_csv = step9_cfg.get('sampling_csv_path')
models_dir = step9_cfg.get('models_dir')
if ml_csv and not os.path.isfile(ml_csv):
errors.append(
f"步骤 11 ML 预测:采样点 CSV 不存在:\n {ml_csv}\n"
f"步骤 9_ml_predict:采样点 CSV 不存在:\n {ml_csv}\n"
" → 请确认「流程步骤-阶段七(采样点布设)」已成功运行。"
)
if models_dir and not os.path.isdir(models_dir):
errors.append(
f"步骤 11 ML 预测:模型目录不存在:\n {models_dir}\n"
f"步骤 9_ml_predict:模型目录不存在:\n {models_dir}\n"
" → 请确认「流程步骤-阶段六(机器学习建模)」已成功运行。"
)
# ── 步骤11 回归预测:模型目录 ──
step11_cfg = config.get('step11', {})
step11_csv = step11_cfg.get('sampling_csv_path')
step11_dir = step11_cfg.get('models_dir')
if step11_csv and not os.path.isfile(step11_csv):
errors.append(
f"步骤 11 回归预测:采样点 CSV 不存在:\n {step11_csv}\n"
" → 请确认「流程步骤-阶段七(采样点布设)」已成功运行。"
)
if step11_dir and not os.path.isdir(step11_dir):
errors.append(
f"步骤 11 回归预测:模型目录不存在:\n {step11_dir}\n"
" → 请确认「流程步骤-阶段八(非经验建模)」已成功运行。"
)
# ── 步骤12 自定义回归预测:公式 CSV ──
step12_cfg = config.get('step12', {})
formula_csv = step12_cfg.get('formula_csv_path')
if formula_csv and not os.path.isfile(formula_csv):
errors.append(
f"步骤 12自定义回归预测公式 CSV 文件不存在:\n {formula_csv}\n"
" → 请确认「流程步骤-阶段十二」中已填写有效的公式文件路径。"
)
# ── 步骤14 专题图:预测结果 CSV + 边界 shp ──
step14_cfg = config.get('step14', {})
pred_csv = step14_cfg.get('prediction_csv_path')
boundary_shp = step14_cfg.get('boundary_shp_path')
# ── 步骤11_map 专题图:预测结果 CSV + 边界 shp ──
step11_cfg = config.get('step11_map', {})
pred_csv = step11_cfg.get('prediction_csv_path')
boundary_shp = step11_cfg.get('boundary_shp_path')
if pred_csv and not os.path.isfile(pred_csv):
errors.append(
f"步骤 14(专题图):预测结果 CSV 不存在:\n {pred_csv}\n"
f"步骤 11_map(专题图):预测结果 CSV 不存在:\n {pred_csv}\n"
" → 请确认机器学习或回归预测步骤已成功运行。"
)
if boundary_shp and not os.path.isfile(boundary_shp):
errors.append(
f"步骤 14(专题图):边界 shp 文件不存在:\n {boundary_shp}\n"
" → 请确认「流程步骤-阶段十」中已填写有效的边界矢量文件路径。"
f"步骤 11_map(专题图):边界 shp 文件不存在:\n {boundary_shp}\n"
" → 请确认「流程步骤-阶段十」中已填写有效的边界矢量文件路径。"
)
# ── 汇总报错:任一缺失立即抛出 PipelineHalt ──

View File

@ -0,0 +1,569 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step10 面板 - 水色指数反演(直接处理去耀斑 BSQ 影像)
将 waterindex.csv 中的公式直接应用于去耀斑高光谱影像,
输出各水质参数指数的 GeoTIFF 栅格图像。
"""
import os
import traceback
from pathlib import Path
from typing import Dict, List, Optional
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QFormLayout,
QGroupBox, QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton,
QFileDialog, QMessageBox, QListWidget, QListWidgetItem,
QAbstractItemView, QProgressBar, QTextEdit, QFrame,
QScrollArea, QSizePolicy,
)
from PyQt5.QtGui import QFont
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
class WaterIndexWorker(QThread):
"""后台线程:执行水色指数反演"""
finished_ok = pyqtSignal(dict)
failed = pyqtSignal(str)
progress = pyqtSignal(str, float) # message, percent
log = pyqtSignal(str)
def __init__(
self,
bsq_path: str,
hdr_path: str,
output_dir: str,
selected_formulas: List[str],
waterindex_csv: str,
water_mask_path: Optional[str] = None,
work_dir: Optional[str] = None,
):
super().__init__()
self.bsq_path = bsq_path
self.hdr_path = hdr_path
self.output_dir = output_dir
self.selected_formulas = selected_formulas
self.waterindex_csv = waterindex_csv
self.water_mask_path = water_mask_path
self.work_dir = work_dir
def run(self):
try:
from src.core.algorithms.waterindex_inversion import WaterIndexProcessor
self.progress.emit("正在初始化水色指数处理器…", 2)
processor = WaterIndexProcessor(self.waterindex_csv)
self.progress.emit("正在读取影像元数据…", 5)
# 获取影像元数据
meta = processor.get_image_metadata(self.bsq_path, self.hdr_path)
if not meta:
self.failed.emit("无法读取影像元数据,请检查 BSQ 和 HDR 文件是否匹配")
return
n_bands = meta.get('bands', 0)
wv_range = meta.get('wavelength_range', '未知')
self.log.emit(
f"影像信息: {meta['width']}×{meta['height']} 像素, "
f"{n_bands} 波段, {wv_range}"
)
if self.water_mask_path:
self.log.emit(f"使用水域掩膜: {self.water_mask_path}")
# 使用 run_inversion 入口(含掩膜拦截链路)
results = processor.run_inversion(
deglint_img_path=self.bsq_path,
work_dir=self.work_dir or self.output_dir,
formula_csv_path=self.waterindex_csv,
selected_formulas=self.selected_formulas,
water_mask_path=self.water_mask_path,
callback=self._on_progress,
)
self.progress.emit(f"完成!共生成 {len(results)} 个指数图", 100)
self.finished_ok.emit(results)
except Exception as e:
self.failed.emit(f"{e}\n{traceback.format_exc()}")
def _on_progress(self, msg: str, pct: float):
self.progress.emit(msg, pct)
class Step10WatercolorPanel(QWidget):
"""步骤10水色指数反演直接处理 BSQ 影像)"""
def __init__(self, parent=None):
super().__init__(parent)
self._worker: Optional[WaterIndexWorker] = None
self._waterindex_csv = self._find_waterindex_csv()
self._categories: List[str] = []
self._all_formulas: List[Dict] = []
self._formula_list_widgets: Dict[str, QListWidgetItem] = {}
self.init_ui()
self._load_formulas()
def init_ui(self):
layout = QVBoxLayout()
# ---- 标题 ----
title = QLabel("步骤10水色指数反演高光谱影像直接处理")
title.setFont(QFont("Arial", 12, QFont.Bold))
layout.addWidget(title)
# ---- 说明 ----
hint = QLabel(
"将 waterindex.csv 中的公式直接应用于去耀斑高光谱影像BSQ"
"输出各水质参数指数的 GeoTIFF 栅格图像。"
"指数图可直接用于水质专题图生成。"
)
hint.setWordWrap(True)
hint.setStyleSheet(f"color: {ModernStylesheet.COLORS.get('text_secondary', '#666')};")
layout.addWidget(hint)
# ---- 输入影像选择 ----
input_group = QGroupBox("输入影像")
input_layout = QFormLayout()
self.bsq_file = FileSelectWidget(
"去耀斑 BSQ 影像:",
"BSQ Files (*.bsq);;DAT Files (*.dat);;All Files (*.*)"
)
self.bsq_file.line_edit.setPlaceholderText("选择去耀斑处理后的 BSQ 影像")
self.bsq_file.browse_btn.clicked.disconnect()
self.bsq_file.browse_btn.clicked.connect(self._browse_bsq)
input_layout.addRow("BSQ 影像:", self.bsq_file)
self.hdr_file = FileSelectWidget(
"ENVI 头文件:",
"HDR Files (*.hdr);;All Files (*.*)"
)
self.hdr_file.line_edit.setPlaceholderText("自动关联同路径 .hdr 文件")
self.hdr_file.browse_btn.clicked.disconnect()
self.hdr_file.browse_btn.clicked.connect(self._browse_hdr)
input_layout.addRow("HDR 文件:", self.hdr_file)
# 影像信息显示
self.meta_label = QLabel("未加载影像")
self.meta_label.setStyleSheet(
"background: #f0f0f0; padding: 4px 8px; border-radius: 4px; "
"font-size: 12px; color: #333;"
)
input_layout.addRow("影像信息:", self.meta_label)
input_group.setLayout(input_layout)
layout.addWidget(input_group)
# ---- 公式选择 ----
formula_group = QGroupBox("公式选择")
formula_layout = QGridLayout()
# 类别过滤
formula_layout.addWidget(QLabel("按类别筛选:"), 0, 0)
self.category_combo = QComboBox()
self.category_combo.currentTextChanged.connect(self._on_category_changed)
formula_layout.addWidget(self.category_combo, 0, 1, 1, 2)
# 全选/取消全选
select_btn_layout = QHBoxLayout()
self.select_all_btn = QPushButton("全选")
self.select_all_btn.setMaximumWidth(80)
self.select_all_btn.clicked.connect(self._select_all)
select_btn_layout.addWidget(self.select_all_btn)
self.deselect_all_btn = QPushButton("取消全选")
self.deselect_all_btn.setMaximumWidth(80)
self.deselect_all_btn.clicked.connect(self._deselect_all)
select_btn_layout.addWidget(self.deselect_all_btn)
select_btn_layout.addStretch()
formula_layout.addLayout(select_btn_layout, 0, 3)
# 公式列表
self.formula_list = QListWidget()
self.formula_list.setSelectionMode(QAbstractItemView.MultiSelection)
self.formula_list.setMinimumHeight(200)
self.formula_list.itemChanged.connect(self._on_item_changed)
formula_layout.addWidget(self.formula_list, 1, 0, 1, 4)
formula_group.setLayout(formula_layout)
layout.addWidget(formula_group)
# ---- 输出设置 ----
output_group = QGroupBox("输出设置")
output_layout = QFormLayout()
self.output_dir = FileSelectWidget(
"输出目录:",
"Directories"
)
self.output_dir.line_edit.setPlaceholderText("留空 → 工作目录/8_WaterIndex_Images")
self.output_dir.browse_btn.clicked.disconnect()
self.output_dir.browse_btn.clicked.connect(self._browse_output_dir)
output_layout.addRow("输出目录:", self.output_dir)
self.format_combo = QComboBox()
self.format_combo.addItems(["GTiff (GeoTIFF)", "ENVI", "PCI"])
self.format_combo.setCurrentIndex(0)
output_layout.addRow("输出格式:", self.format_combo)
output_group.setLayout(output_layout)
layout.addWidget(output_group)
# ---- 进度显示 ----
self.progress_bar = QProgressBar()
self.progress_bar.setMinimum(0)
self.progress_bar.setMaximum(100)
self.progress_bar.setValue(0)
self.progress_bar.setTextVisible(True)
layout.addWidget(self.progress_bar)
self.progress_label = QLabel("")
self.progress_label.setStyleSheet("font-size: 11px; color: #666;")
layout.addWidget(self.progress_label)
# ---- 启用 & 运行 ----
self.enable_checkbox = QCheckBox("启用此步骤")
self.enable_checkbox.setChecked(True)
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 _find_waterindex_csv(self) -> str:
"""查找 waterindex.csv 路径"""
candidates = [
Path(__file__).parent.parent.parent / "model" / "waterindex.csv",
Path(__file__).parent.parent.parent.parent / "src" / "gui" / "model" / "waterindex.csv",
]
for c in candidates:
if c.exists():
return str(c)
return ""
def _load_formulas(self):
"""加载 waterindex.csv 中的公式"""
if not self._waterindex_csv or not Path(self._waterindex_csv).exists():
self.meta_label.setText("⚠️ waterindex.csv 未找到")
return
import csv
self._all_formulas = []
try:
with open(self._waterindex_csv, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f)
self._all_formulas = list(reader)
except Exception as e:
self.meta_label.setText(f"⚠️ 加载公式失败: {e}")
return
# 提取所有类别
cats = set()
for f in self._all_formulas:
c = f.get('Category', '').strip()
if c:
cats.add(c)
self._categories = sorted(cats)
self.category_combo.clear()
self.category_combo.addItem("全部")
self.category_combo.addItems(self._categories)
self._populate_list("全部")
def _populate_list(self, category: str):
"""根据类别填充公式列表"""
self.formula_list.clear()
self._formula_list_widgets.clear()
formulas_to_show = (
[f for f in self._all_formulas if f.get('Category', '') == category]
if category != "全部"
else self._all_formulas
)
for f in formulas_to_show:
name = f.get('Formula_Name', '')
formula_str = f.get('Formula', '')
cat = f.get('Category', '')
ftype = f.get('Formula_Type', '')
item = QListWidgetItem()
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked)
item.setData(Qt.UserRole, name)
item.setText(
f"{name} [{cat}] ({ftype})\n {formula_str}"
)
item.setToolTip(f"{name}\n{category}\n{formula_str}")
self.formula_list.addItem(item)
self._formula_list_widgets[name] = item
def _on_category_changed(self, category: str):
self._populate_list(category)
def _select_all(self):
for item in self.formula_list.selectedItems():
item.setCheckState(Qt.Checked)
# 也全选当前显示的
for i in range(self.formula_list.count()):
it = self.formula_list.item(i)
it.setCheckState(Qt.Checked)
def _deselect_all(self):
for i in range(self.formula_list.count()):
it = self.formula_list.item(i)
it.setCheckState(Qt.Unchecked)
def _on_item_changed(self, item: QListWidgetItem):
pass # 可扩展:实时统计选中数量
def _browse_bsq(self):
path, _ = QFileDialog.getOpenFileName(
self, "选择去耀斑 BSQ 影像",
"",
"BSQ Files (*.bsq);;DAT Files (*.dat);;All Files (*.*)"
)
if path:
self.bsq_file.set_path(path)
# 自动关联同路径 hdr
hdr = Path(path).with_suffix('.hdr')
if hdr.exists():
self.hdr_file.set_path(str(hdr))
self._load_metadata(path, str(hdr) if hdr.exists() else "")
def _browse_hdr(self):
path, _ = QFileDialog.getOpenFileName(
self, "选择 ENVI 头文件",
"",
"HDR Files (*.hdr);;All Files (*.*)"
)
if path:
self.hdr_file.set_path(path)
bsq_path = self.bsq_file.get_path()
if bsq_path:
self._load_metadata(bsq_path, path)
def _browse_output_dir(self):
d = QFileDialog.getExistingDirectory(self, "选择输出目录", "")
if d:
self.output_dir.set_path(d)
def _load_metadata(self, bsq_path: str, hdr_path: str):
"""加载并显示影像元数据"""
if not bsq_path or not Path(bsq_path).exists():
self.meta_label.setText("⚠️ 影像文件不存在")
return
if not hdr_path or not Path(hdr_path).exists():
self.meta_label.setText("⚠️ 头文件不存在")
return
try:
from src.core.algorithms.waterindex_inversion import WaterIndexProcessor
processor = WaterIndexProcessor(self._waterindex_csv)
meta = processor.get_image_metadata(bsq_path, hdr_path)
if meta:
self.meta_label.setText(
f"{meta['width']}×{meta['height']} | "
f"{meta['bands']} 波段 | {meta.get('wavelength_range', '未知')} | "
f"驱动: {meta['driver']}"
)
else:
self.meta_label.setText("⚠️ 无法读取元数据")
except Exception as e:
self.meta_label.setText(f"⚠️ 元数据读取失败: {e}")
def _get_selected_formula_names(self) -> List[str]:
names = []
for i in range(self.formula_list.count()):
item = self.formula_list.item(i)
if item.checkState() == Qt.Checked:
name = item.data(Qt.UserRole)
if name:
names.append(name)
return names
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 get_config(self) -> dict:
bsq = self.bsq_file.get_path()
return {
'bsq_path': bsq,
'hdr_path': self.hdr_file.get_path(),
'deglint_img_path': bsq,
'output_dir': self.output_dir.get_path(),
'output_format': self.format_combo.currentText().split()[0],
'selected_formulas': self._get_selected_formula_names(),
}
def set_config(self, config: dict):
if config.get('bsq_path'):
self.bsq_file.set_path(config['bsq_path'])
if config.get('hdr_path'):
self.hdr_file.set_path(config['hdr_path'])
if config.get('output_dir'):
self.output_dir.set_path(config['output_dir'])
if 'selected_formulas' in config:
names = set(config['selected_formulas'])
for i in range(self.formula_list.count()):
item = self.formula_list.item(i)
name = item.data(Qt.UserRole)
item.setCheckState(Qt.Checked if name in names else Qt.Unchecked)
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
main_window = self.window()
# 自动填入去耀斑影像
if main_window and hasattr(main_window, 'step3_panel'):
deglint_path = main_window.step3_panel.output_file.get_path()
if deglint_path and not self.bsq_file.get_path():
if not os.path.isabs(deglint_path):
deglint_path = os.path.join(self.work_dir or '', deglint_path).replace('\\', '/')
self.bsq_file.set_path(deglint_path)
hdr = Path(deglint_path).with_suffix('.hdr')
if hdr.exists():
self.hdr_file.set_path(str(hdr))
self._load_metadata(deglint_path, str(hdr))
# 自动填入输出目录
if self.work_dir:
out_dir = os.path.join(self.work_dir, "8_WaterIndex_Images").replace('\\', '/')
os.makedirs(out_dir, exist_ok=True)
if not self.output_dir.get_path():
self.output_dir.set_path(out_dir)
def run_step(self):
bsq_path = self.bsq_file.get_path().strip()
hdr_path = self.hdr_file.get_path().strip()
output_dir = self.output_dir.get_path().strip()
# 验证输入
if not bsq_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑 BSQ 影像!")
return
if not Path(bsq_path).exists():
QMessageBox.warning(self, "输入错误", f"BSQ 影像不存在:\n{bsq_path}")
return
if not hdr_path:
# 尝试自动查找
auto_hdr = Path(bsq_path).with_suffix('.hdr')
if auto_hdr.exists():
hdr_path = str(auto_hdr)
self.hdr_file.set_path(hdr_path)
else:
QMessageBox.warning(self, "输入错误", "请选择 ENVI 头文件!")
return
if not Path(hdr_path).exists():
QMessageBox.warning(self, "输入错误", f"HDR 文件不存在:\n{hdr_path}")
return
if not output_dir:
work_dir = self._get_default_work_dir()
output_dir = os.path.join(work_dir, "8_WaterIndex_Images").replace('\\', '/')
os.makedirs(output_dir, exist_ok=True)
self.output_dir.set_path(output_dir)
selected = self._get_selected_formula_names()
if not selected:
QMessageBox.warning(self, "输入错误", "请至少选择一个公式!")
return
if self._waterindex_csv and not Path(self._waterindex_csv).exists():
QMessageBox.warning(self, "配置错误", f"waterindex.csv 不存在:\n{self._waterindex_csv}")
return
# ── 自动扫描工作目录下的水域掩膜文件 ────────────────────────────
work_dir = self.work_dir or str(Path(bsq_path).parent)
mask_dir = os.path.join(work_dir, "1_water_mask")
water_mask_path: Optional[str] = None
if os.path.isdir(mask_dir):
# ★★★ glob 智能扫描:取任意 .dat 或 .tif 文件 ★★★
for pattern in ("*.dat", "*.tif", "*.TIF", "*.DT"):
candidates = sorted(Path(mask_dir).glob(pattern))
if candidates:
water_mask_path = str(candidates[0])
break
if water_mask_path:
print(f"[Step8] 自动找到水域掩膜: {water_mask_path}")
else:
print(f"[Step8] 未找到水域掩膜,跳过陆地剔除(陆地将保留在指数图中)")
# 开始后台处理
self.run_btn.setEnabled(False)
self.progress_bar.setValue(0)
self.progress_label.setText("")
self._worker = WaterIndexWorker(
bsq_path=bsq_path,
hdr_path=hdr_path,
output_dir=output_dir,
selected_formulas=selected,
waterindex_csv=self._waterindex_csv,
water_mask_path=water_mask_path,
work_dir=work_dir,
)
self._worker.progress.connect(self._on_progress)
self._worker.finished_ok.connect(self._on_finished)
self._worker.failed.connect(self._on_failed)
self._worker.log.connect(lambda m: self.progress_label.setText(m))
self._worker.start()
def _on_progress(self, msg: str, pct: float):
self.progress_bar.setValue(int(pct))
self.progress_label.setText(msg)
def _on_finished(self, results: Dict[str, str]):
self.run_btn.setEnabled(True)
n = len(results)
QMessageBox.information(
self, "执行成功",
f"水色指数反演完成!\n"
f"共生成 {n} 个指数图GeoTIFF\n\n"
f"输出目录: {self.output_dir.get_path()}"
)
main_window = self.window()
if main_window and hasattr(main_window, 'log_message'):
main_window.log_message(f"步骤8水色指数反演完成生成 {n} 个指数图", "info")
def _on_failed(self, err: str):
self.run_btn.setEnabled(True)
self.progress_bar.setValue(0)
QMessageBox.critical(self, "执行错误", f"水色指数反演失败:\n\n{err[:500]}")
def get_output_dir(self) -> str:
return self.output_dir.get_path().strip() or ""
def get_output_tif_paths(self) -> List[str]:
"""获取输出目录下的所有 GeoTIFF 文件路径"""
out_dir = self.get_output_dir()
if not out_dir or not os.path.isdir(out_dir):
return []
return sorted(
str(p) for p in Path(out_dir).glob("*.tif")
if p.is_file()
)

View File

@ -0,0 +1,826 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step10 面板 - 专题图生成
"""
import os
import traceback
from pathlib import Path
from typing import List, Optional
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QHBoxLayout,
QLabel, QCheckBox, QPushButton, QLineEdit, QDoubleSpinBox,
QRadioButton, QButtonGroup, QMessageBox, QFileDialog, QComboBox,
QProgressBar,
)
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
# Pipeline 可用性(与 core/worker_thread.py 保持一致)
try:
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
PIPELINE_AVAILABLE = True
except ImportError:
PIPELINE_AVAILABLE = False
class Step11MapBatchThread(QThread):
"""专题图:按文件夹内多个预测 CSV 批量生成分布图。"""
finished_ok = pyqtSignal(int)
failed = pyqtSignal(str)
log_message = pyqtSignal(str, str)
progress = pyqtSignal(int, int) # (current, total)
def __init__(self, work_dir: str, csv_paths: List[str], step10_kwargs: dict, output_dir_optional: Optional[str]):
super().__init__()
self.work_dir = work_dir
self.csv_paths = csv_paths
self.step10_kwargs = step10_kwargs
self.output_dir_optional = (output_dir_optional or "").strip() or None
def run(self):
mpl_prev = None
try:
import matplotlib
mpl_prev = matplotlib.get_backend()
except Exception:
pass
try:
import matplotlib.pyplot as plt
plt.switch_backend("Agg")
except Exception:
mpl_prev = None
try:
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
pipeline = WaterQualityInversionPipeline(work_dir=self.work_dir)
n = len(self.csv_paths)
for i, csv_p in enumerate(self.csv_paths):
self.progress.emit(i + 1, n)
self.log_message.emit(f"专题图 [{i + 1}/{n}] {csv_p}", "info")
kw = {**self.step10_kwargs, "prediction_csv_path": csv_p, "skip_dependency_check": True}
if self.output_dir_optional:
stem = Path(csv_p).stem
kw["output_image_path"] = str(Path(self.output_dir_optional) / f"{stem}_distribution.png")
else:
kw["output_image_path"] = None
pipeline.step10_map(**kw)
self.finished_ok.emit(n)
except Exception as e:
self.failed.emit(f"{e}\n{traceback.format_exc()}")
finally:
if mpl_prev:
try:
import matplotlib.pyplot as plt
plt.switch_backend(mpl_prev)
except Exception:
pass
class Step11GeoTIFFBatchThread(QThread):
"""GeoTIFF 批量渲染:遍历文件夹下所有 .tif/.bsq 逐一渲染成分布图 PNG。"""
finished_ok = pyqtSignal(int)
failed = pyqtSignal(str)
log_message = pyqtSignal(str, str)
progress = pyqtSignal(int, int) # (current, total)
def __init__(
self,
tif_paths: List[str],
output_dir: str,
boundary_shp_path: Optional[str],
input_crs: str,
output_crs: str,
):
super().__init__()
self.tif_paths = tif_paths
self.output_dir = output_dir
self.boundary_shp_path = boundary_shp_path
self.input_crs = input_crs
self.output_crs = output_crs
def run(self):
mpl_prev = None
try:
import matplotlib
mpl_prev = matplotlib.get_backend()
except Exception:
pass
try:
import matplotlib.pyplot as plt
plt.switch_backend("Agg")
except Exception:
mpl_prev = None
try:
from src.postprocessing.map import ContentMapper
mapper = ContentMapper()
n = len(self.tif_paths)
for i, tif_path in enumerate(self.tif_paths):
self.progress.emit(i + 1, n)
tif_stem = Path(tif_path).stem
chinese_name = mapper._get_chinese_title(tif_stem)
output_png = str(Path(self.output_dir) / f"{chinese_name}_专题图.png")
self.log_message.emit(f"GeoTIFF 渲染 [{i + 1}/{n}] {tif_stem}", "info")
try:
mapper.visualize_raster(
raster_tif_path=tif_path,
output_file=output_png,
boundary_shp_path=self.boundary_shp_path,
nodata_value=-9999.0,
figsize=(14, 10),
alpha=0.9,
)
except Exception as vis_err:
self.log_message.emit(f" ⚠️ 渲染失败,跳过: {vis_err}", "warning")
continue
self.finished_ok.emit(n)
except Exception as e:
self.failed.emit(f"{e}\n{traceback.format_exc()}")
finally:
if mpl_prev:
try:
import matplotlib.pyplot as plt
plt.switch_backend(mpl_prev)
except Exception:
pass
class Step11MapPanel(QWidget):
"""步骤11专题图生成"""
def __init__(self, parent=None):
super().__init__(parent)
self._batch_thread = None
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
hint = QLabel(
"独立运行:可选「单个 CSV」或「文件夹批量」扫描目录下所有 .csv"
"GeoTIFF 栅格模式下亦支持批量渲染步骤8输出的所有水色指数 GeoTIFF 文件。"
)
hint.setWordWrap(True)
hint.setStyleSheet(
f"color: {ModernStylesheet.COLORS.get('text_secondary', '#666')};"
)
layout.addWidget(hint)
mode_row = QHBoxLayout()
self.mode_single_rb = QRadioButton("单个 CSV 文件")
self.mode_folder_rb = QRadioButton("文件夹批量")
self._mode_group = QButtonGroup(self)
self._mode_group.addButton(self.mode_single_rb, 0)
self._mode_group.addButton(self.mode_folder_rb, 1)
mode_row.addWidget(self.mode_single_rb)
mode_row.addWidget(self.mode_folder_rb)
mode_row.addStretch()
layout.addLayout(mode_row)
# ---------- 渲染模式选择器CSV vs GeoTIFF ----------
render_row = QHBoxLayout()
render_row.addWidget(QLabel("渲染模式:"))
self.render_mode_combo = QComboBox()
self.render_mode_combo.addItems(["CSV 插值模式", "GeoTIFF 栅格模式"])
self.render_mode_combo.setMinimumWidth(180)
self.render_mode_combo.currentTextChanged.connect(self._toggle_input_mode)
render_row.addWidget(self.render_mode_combo)
render_row.addStretch()
layout.addLayout(render_row)
# ---------- RadioButton 美化样式(选中状态为方形实心块,贴合主界面风格) ----------
radio_style = """
QRadioButton {
font-size: 14px;
spacing: 8px;
color: #333333;
}
QRadioButton::indicator {
width: 16px;
height: 16px;
border: 2px solid #999999;
border-radius: 3px;
background-color: white;
}
QRadioButton::indicator:checked {
border: 2px solid #0078d4;
background-color: #0078d4;
image: none;
}
QRadioButton::indicator:hover {
border: 2px solid #005a9e;
}
"""
self.mode_single_rb.setStyleSheet(radio_style)
self.mode_folder_rb.setStyleSheet(radio_style)
self.prediction_csv_file = FileSelectWidget(
"预测结果CSV:",
"CSV Files (*.csv);;All Files (*.*)"
)
layout.addWidget(self.prediction_csv_file)
folder_row = QHBoxLayout()
self.prediction_csv_dir_label = QLabel("预测CSV目录:")
self.prediction_csv_dir_label.setMinimumWidth(120)
self.prediction_csv_dir_edit = QLineEdit()
self.prediction_csv_dir_edit.setPlaceholderText("选择含多个预测结果 CSV 的文件夹…")
pred_dir_btn = QPushButton("浏览…")
pred_dir_btn.setMaximumWidth(80)
pred_dir_btn.clicked.connect(self.browse_prediction_csv_dir)
folder_row.addWidget(self.prediction_csv_dir_label)
folder_row.addWidget(self.prediction_csv_dir_edit, 1)
folder_row.addWidget(pred_dir_btn)
self._folder_row_widget = QWidget()
self._folder_row_widget.setLayout(folder_row)
layout.addWidget(self._folder_row_widget)
# ---------- GeoTIFF 栅格文件选择器 ----------
self.geotiff_file = FileSelectWidget(
"水色指数 GeoTIFF:",
"GeoTIFF Files (*.tif);;All Files (*.*)"
)
self.geotiff_file.line_edit.setPlaceholderText("选择步骤8输出的水色指数 GeoTIFF 文件…")
self.geotiff_file.setVisible(False)
layout.addWidget(self.geotiff_file)
# ---------- GeoTIFF 文件夹批量选择器GeoTIFF + 文件夹模式时显示) ----------
geotiff_dir_row = QHBoxLayout()
self.geotiff_dir_label = QLabel("水色指数目录:")
self.geotiff_dir_label.setMinimumWidth(120)
self.geotiff_dir_edit = QLineEdit()
self.geotiff_dir_edit.setPlaceholderText("选择 8_WaterIndex_Images 文件夹(批量渲染)…")
geotiff_dir_btn = QPushButton("浏览…")
geotiff_dir_btn.setMaximumWidth(80)
geotiff_dir_btn.clicked.connect(self.browse_geotiff_dir)
geotiff_dir_row.addWidget(self.geotiff_dir_label)
geotiff_dir_row.addWidget(self.geotiff_dir_edit, 1)
geotiff_dir_row.addWidget(geotiff_dir_btn)
self._geotiff_dir_widget = QWidget()
self._geotiff_dir_widget.setLayout(geotiff_dir_row)
self._geotiff_dir_widget.setVisible(False)
layout.addWidget(self._geotiff_dir_widget)
self.recursive_csv_cb = QCheckBox("包含子文件夹(递归扫描 *.csv")
layout.addWidget(self.recursive_csv_cb)
self.boundary_file = FileSelectWidget(
"边界文件:",
"Shapefiles (*.shp);;All Files (*.*)"
)
layout.addWidget(self.boundary_file)
# 参数设置
params_group = QGroupBox("生成参数")
params_layout = QFormLayout()
self.resolution = QDoubleSpinBox()
self.resolution.setRange(1, 1000)
self.resolution.setValue(30)
params_layout.addRow("分辨率(米):", self.resolution)
self.input_crs = QLineEdit()
self.input_crs.setText("EPSG:32651")
params_layout.addRow("输入坐标系:", self.input_crs)
self.output_crs = QLineEdit()
self.output_crs.setText("EPSG:4326")
params_layout.addRow("输出坐标系:", self.output_crs)
self.show_points = QCheckBox("显示采样点")
params_layout.addRow("", self.show_points)
self.use_diffusion = QCheckBox("启用距离扩散")
self.use_diffusion.setChecked(True)
params_layout.addRow("", self.use_diffusion)
params_group.setLayout(params_layout)
layout.addWidget(params_group)
# 输出目录
self.output_dir = FileSelectWidget(
"输出分布图目录:",
"Directories;;All Files (*.*)"
)
self.output_dir.line_edit.setPlaceholderText("留空→工作目录/14_visualization")
self.output_dir.browse_btn.clicked.disconnect()
self.output_dir.browse_btn.clicked.connect(self.browse_output_dir)
layout.addWidget(self.output_dir)
# 启用步骤
self.enable_checkbox = QCheckBox("启用此步骤")
self.enable_checkbox.setChecked(True)
layout.addWidget(self.enable_checkbox)
# 独立运行按钮
self.run_button = QPushButton("独立运行此步骤")
self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_button.clicked.connect(self.run_step)
layout.addWidget(self.run_button)
# 批量渲染进度条
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
self.progress_bar.setMinimum(0)
self.progress_bar.setMaximum(100)
self.progress_bar.setValue(0)
layout.addWidget(self.progress_bar)
layout.addStretch()
self.setLayout(layout)
# 信号绑定与初始状态
self.mode_single_rb.toggled.connect(self._toggle_input_mode)
self.mode_folder_rb.toggled.connect(self._toggle_input_mode)
self.mode_single_rb.setChecked(True) # 默认选中"单个 CSV"
self._toggle_input_mode() # 根据默认值设置初始显示状态
def _toggle_input_mode(self):
"""槽函数:根据渲染模式和输入模式动态显示/隐藏对应的输入组件。"""
geotiff_mode = self.render_mode_combo.currentText() == "GeoTIFF 栅格模式"
folder_mode = self.mode_folder_rb.isChecked()
# CSV 插值模式
if not geotiff_mode:
self.prediction_csv_file.setVisible(not folder_mode)
self._folder_row_widget.setVisible(folder_mode)
self.recursive_csv_cb.setVisible(folder_mode)
self.geotiff_file.setVisible(False)
self._geotiff_dir_widget.setVisible(False)
# GeoTIFF 栅格模式
else:
self.prediction_csv_file.setVisible(False)
self._folder_row_widget.setVisible(False)
self.recursive_csv_cb.setVisible(False)
# GeoTIFF + 文件夹批量 → 显示文件夹选择器;否则 → 显示单文件选择器
self.geotiff_file.setVisible(not folder_mode)
self._geotiff_dir_widget.setVisible(folder_mode)
def _get_default_work_dir(self):
"""获取 work_dir优先用 panel 自身缓存的,否则尝试从主窗口取"""
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_prediction_csv_dir(self):
default = self._get_default_work_dir()
if default:
default = os.path.join(default, "11_12_13_predictions")
d = QFileDialog.getExistingDirectory(self, "选择预测结果 CSV 所在文件夹", default)
if d:
self.prediction_csv_dir_edit.setText(d)
def _collect_csv_paths_from_folder(self) -> List[str]:
folder = (self.prediction_csv_dir_edit.text() or "").strip()
if not folder or not os.path.isdir(folder):
return []
root = Path(folder)
if self.recursive_csv_cb.isChecked():
files = sorted(root.rglob("*.csv"))
else:
files = sorted(root.glob("*.csv"))
return [str(p) for p in files if p.is_file()]
def browse_geotiff_dir(self):
"""浏览 GeoTIFF 文件夹(批量模式)"""
default = self._get_default_work_dir()
if default:
default = os.path.join(default, "8_WaterIndex_Images")
d = QFileDialog.getExistingDirectory(
self, "选择水色指数 GeoTIFF 文件夹", default
)
if d:
self.geotiff_dir_edit.setText(d)
def _collect_tif_paths_from_folder(self) -> List[str]:
"""扫描所选文件夹,收集所有 .tif 和 .bsq 文件路径"""
folder = (self.geotiff_dir_edit.text() or "").strip()
if not folder or not os.path.isdir(folder):
return []
root = Path(folder)
tif_files = sorted(root.glob("*.tif"))
bsq_files = sorted(root.glob("*.bsq"))
return [str(p) for p in tif_files + bsq_files if p.is_file()]
def _step10_base_pipeline_kwargs(self) -> dict:
return {
'boundary_shp_path': self.boundary_file.get_path(),
'resolution': self.resolution.value(),
'input_crs': self.input_crs.text(),
'output_crs': self.output_crs.text(),
'show_sample_points': self.show_points.isChecked(),
'use_distance_diffusion': self.use_diffusion.isChecked(),
}
def get_config(self):
pred_csv = (self.prediction_csv_file.get_path() or "").strip()
folder_mode = self.mode_folder_rb.isChecked()
pred_dir = (self.prediction_csv_dir_edit.text() or "").strip()
geotiff_path = (self.geotiff_file.get_path() or "").strip()
config = {
'step10_batch_mode': 'folder' if folder_mode else 'single',
'render_mode': self.render_mode_combo.currentText(),
'prediction_csv_dir': pred_dir if pred_dir else None,
'recursive_csv_scan': self.recursive_csv_cb.isChecked(),
'prediction_csv_path': None if folder_mode else (pred_csv if pred_csv else None),
'geotiff_path': geotiff_path if geotiff_path else None,
'geotiff_dir': (self.geotiff_dir_edit.text() or "").strip() or None,
'boundary_shp_path': self.boundary_file.get_path(),
'resolution': self.resolution.value(),
'input_crs': self.input_crs.text(),
'output_crs': self.output_crs.text(),
'show_sample_points': self.show_points.isChecked(),
'use_distance_diffusion': self.use_diffusion.isChecked(),
}
out_dir = (self.output_dir.get_path() or "").strip()
if not folder_mode and pred_csv and out_dir:
stem = Path(pred_csv).stem
config['output_image_path'] = str(Path(out_dir) / f"{stem}_distribution.png")
else:
config['output_image_path'] = None
return config
def set_config(self, config):
mode = config.get('step10_batch_mode', 'single')
if mode == 'folder':
self.mode_folder_rb.setChecked(True)
else:
self.mode_single_rb.setChecked(True)
render_mode = config.get('render_mode', 'CSV 插值模式')
idx = self.render_mode_combo.findText(render_mode)
if idx >= 0:
self.render_mode_combo.setCurrentIndex(idx)
if config.get('prediction_csv_dir'):
self.prediction_csv_dir_edit.setText(str(config['prediction_csv_dir']))
if 'recursive_csv_scan' in config:
self.recursive_csv_cb.setChecked(bool(config['recursive_csv_scan']))
if 'prediction_csv_path' in config and config['prediction_csv_path']:
self.prediction_csv_file.set_path(str(config['prediction_csv_path']))
if 'geotiff_path' in config and config['geotiff_path']:
self.geotiff_file.set_path(str(config['geotiff_path']))
if 'geotiff_dir' in config and config['geotiff_dir']:
self.geotiff_dir_edit.setText(str(config['geotiff_dir']))
if 'boundary_shp_path' in config:
self.boundary_file.set_path(config['boundary_shp_path'])
if 'resolution' in config:
self.resolution.setValue(config['resolution'])
if 'input_crs' in config:
self.input_crs.setText(config['input_crs'])
if 'output_crs' in config:
self.output_crs.setText(config['output_crs'])
if 'show_sample_points' in config:
self.show_points.setChecked(config['show_sample_points'])
if 'use_distance_diffusion' in config:
self.use_diffusion.setChecked(config['use_distance_diffusion'])
if 'output_dir' in config and config['output_dir']:
self.output_dir.set_path(str(config['output_dir']))
elif config.get('output_image_path'):
p = Path(str(config['output_image_path']))
if p.parent and str(p.parent) != '.':
self.output_dir.set_path(str(p.parent))
def update_from_config(self, work_dir=None, pipeline=None):
"""从全局配置自动填充预测结果目录
优先使用 Step8机器学习预测的输出目录作为待预测 CSV 目录;
其次回退到 Step8.5(回归预测)或 Step8.75(自定义回归预测)的输出目录。
Args:
work_dir: 工作目录路径
pipeline: Pipeline 实例(未使用,保留接口兼容性)
"""
try:
import traceback
if work_dir:
self.work_dir = work_dir
elif hasattr(self, 'work_dir') and self.work_dir:
pass
else:
self.work_dir = None
main_window = self.window()
if not main_window:
return
# 1. 尝试从 Step8 界面读取机器学习预测输出目录(最优先)
pred_dir = None
if hasattr(main_window, 'step11_prediction_panel'):
step8_widget = getattr(main_window.step11_prediction_panel, 'output_file', None)
step8_output = ""
if hasattr(step8_widget, 'get_path'):
step8_output = step8_widget.get_path() or ""
elif hasattr(step8_widget, 'text'):
step8_output = step8_widget.text() or ""
if step8_output:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step8_output):
step8_output = os.path.join(self.work_dir or '', step8_output).replace('\\', '/')
# 提取父目录后追加 Machine_Learning_Prediction最底层真实子目录
base_pred_dir = str(Path(step8_output).parent)
ml_pred_dir = Path(base_pred_dir) / "Machine_Learning_Prediction"
pred_dir = str(ml_pred_dir) if ml_pred_dir.exists() else base_pred_dir
# 2. 备选:从 Step11 界面读取非经验预测输出目录
if not pred_dir and hasattr(main_window, 'step11_panel'):
step8_5_widget = getattr(main_window.step11_panel, 'output_file', None)
step8_5_output = ""
if hasattr(step8_5_widget, 'get_path'):
step8_5_output = step8_5_widget.get_path() or ""
elif hasattr(step8_5_widget, 'text'):
step8_5_output = step8_5_widget.text() or ""
if step8_5_output:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step8_5_output):
step8_5_output = os.path.join(self.work_dir or '', step8_5_output).replace('\\', '/')
pred_dir = str(Path(step8_5_output).parent)
# 3. 备选:从 Step12 界面读取自定义回归预测输出目录
if not pred_dir and hasattr(main_window, 'step12_panel'):
step8_75_widget = getattr(main_window.step12_panel, 'output_dir_widget', None)
step8_75_output = ""
if hasattr(step8_75_widget, 'get_path'):
step8_75_output = step8_75_widget.get_path() or ""
elif hasattr(step8_75_widget, 'text'):
step8_75_output = step8_75_widget.text() or ""
if step8_75_output:
pred_dir = step8_75_output
# 自动填入"预测CSV目录"(文件夹批量模式)
if pred_dir:
existing_dir = (self.prediction_csv_dir_edit.text() or "").strip()
if not existing_dir:
self.prediction_csv_dir_edit.setText(pred_dir)
# 切换到文件夹批量模式
self.mode_folder_rb.setChecked(True)
# 4. 自动填充输出目录14_visualization
if self.work_dir:
output_dir = os.path.join(self.work_dir, "14_visualization")
os.makedirs(output_dir, exist_ok=True)
existing_out = self.output_dir.get_path()
if not existing_out or not existing_out.strip():
self.output_dir.set_path(output_dir)
# 5. 自动探测原始矢量边界文件(.shp作为专题图底图
# 优先回溯 input-test/roi.shpgeopandas.read_file 仅支持矢量格式
if self.work_dir:
possible_shp = None
candidates = [
Path(self.work_dir).parent / "input-test" / "roi.shp",
Path(self.work_dir) / "roi.shp",
Path(self.work_dir).parent / "roi.shp",
]
for candidate in candidates:
if candidate.exists() and candidate.suffix.lower() == ".shp":
possible_shp = candidate
break
existing_boundary = (self.boundary_file.get_path() or "").strip()
if not existing_boundary and possible_shp:
self.boundary_file.set_path(str(possible_shp))
elif not existing_boundary:
self.boundary_file.set_path("")
print("⚠️ 提示:专题图生成模块需传入标准矢量边界文件 (.shp),请手动选择。")
# 6. 自动探测 Step 8 输出的水色指数 GeoTIFFGeoTIFF 渲染模式)
step8_out_dir = Path(self.work_dir) / "8_WaterIndex_Images" if self.work_dir else None
if step8_out_dir and step8_out_dir.is_dir():
# GeoTIFF 批量模式:填充目录供批量渲染
if not (self.geotiff_dir_edit.text() or "").strip():
self.geotiff_dir_edit.setText(str(step8_out_dir))
# GeoTIFF 单文件模式:默认选中第一个
tif_files = sorted(step8_out_dir.glob("*.tif"))
if tif_files and not (self.geotiff_file.get_path() or "").strip():
self.geotiff_file.set_path(str(tif_files[0]))
except Exception as e:
import traceback
print(f"{self.__class__.__name__}】自动填充失败,跳过: {e}")
traceback.print_exc()
def browse_output_dir(self):
"""浏览输出目录"""
default = self._get_default_work_dir()
if default:
default = os.path.join(default, "14_visualization")
dir_path = QFileDialog.getExistingDirectory(self, "选择输出分布图目录", default)
if dir_path:
self.output_dir.set_path(dir_path)
def _start_batch_run(self, csv_list, work_dir, base_kw, out_dir_opt, parent):
"""封装 CSV 批量启动逻辑,统一处理信号连接和进度条"""
self.run_button.setEnabled(False)
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
self._batch_thread = Step11MapBatchThread(work_dir, csv_list, base_kw, out_dir_opt)
main_win = parent
def _batch_log(msg, lvl):
if hasattr(main_win, "log_message"):
main_win.log_message(msg, lvl)
def _on_progress(cur, total):
if total > 0:
self.progress_bar.setMaximum(total)
self.progress_bar.setValue(cur)
self.progress_bar.setFormat(f"{cur}/{total} 张 (%p%)")
self._batch_thread.log_message.connect(_batch_log, Qt.QueuedConnection)
self._batch_thread.progress.connect(_on_progress, Qt.QueuedConnection)
self._batch_thread.finished_ok.connect(self._on_step10_batch_ok, Qt.QueuedConnection)
self._batch_thread.failed.connect(self._on_step10_batch_fail, Qt.QueuedConnection)
self._batch_thread.finished.connect(
lambda: (self.run_button.setEnabled(True), self.progress_bar.setVisible(False)),
Qt.QueuedConnection,
)
self._batch_thread.start()
if hasattr(parent, "log_message"):
parent.log_message(f"专题图批量:共 {len(csv_list)} 个 CSV工作目录 {work_dir}", "info")
def run_step(self):
"""独立运行步骤11"""
if self._batch_thread and self._batch_thread.isRunning():
QMessageBox.information(self, "提示", "批量任务正在运行,请稍候。")
return
boundary_shp_path = self.boundary_file.get_path()
if not boundary_shp_path:
QMessageBox.warning(self, "输入验证失败", "请选择边界文件")
return
if not os.path.exists(boundary_shp_path):
QMessageBox.warning(self, "输入验证失败", "边界文件不存在")
return
parent = self.parent()
while parent and not hasattr(parent, 'run_single_step'):
parent = parent.parent()
if not parent or not hasattr(parent, 'run_single_step'):
QMessageBox.critical(self, "错误", "无法找到父级GUI对象")
return
if self.mode_folder_rb.isChecked():
# -------- CSV 插值批量 --------
if self.render_mode_combo.currentText() != "GeoTIFF 栅格模式":
csv_list = self._collect_csv_paths_from_folder()
if not csv_list:
QMessageBox.warning(
self,
"输入验证失败",
"所选文件夹中未找到 .csv 文件,或目录无效。\n"
"可勾选「包含子文件夹」以递归扫描。",
)
return
if not PIPELINE_AVAILABLE:
QMessageBox.critical(self, "错误", "Pipeline 模块不可用,无法批量生成专题图。")
return
work_dir = getattr(parent, "work_dir", None) or "./work_dir"
work_dir = str(work_dir)
base_kw = self._step10_base_pipeline_kwargs()
out_dir_opt = (self.output_dir.get_path() or "").strip() or None
self._start_batch_run(csv_list, work_dir, base_kw, out_dir_opt, parent)
return
# -------- GeoTIFF 栅格批量 --------
tif_list = self._collect_tif_paths_from_folder()
if not tif_list:
QMessageBox.warning(
self,
"输入验证失败",
"所选文件夹中未找到 .tif / .bsq 文件,\n"
"请确认目录包含步骤8输出的水色指数 GeoTIFF 文件。",
)
return
out_dir = (self.output_dir.get_path() or "").strip()
if not out_dir:
out_dir = os.path.join(self._get_default_work_dir(), "14_visualization")
os.makedirs(out_dir, exist_ok=True)
self.run_button.setEnabled(False)
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
self._batch_thread = Step11GeoTIFFBatchThread(
tif_paths=tif_list,
output_dir=out_dir,
boundary_shp_path=boundary_shp_path,
input_crs=self.input_crs.text(),
output_crs=self.output_crs.text(),
)
main_win = parent
def _batch_log(msg, lvl):
if hasattr(main_win, "log_message"):
main_win.log_message(msg, lvl)
def _on_progress(cur, total):
if total > 0:
pct = int(cur / total * 100)
self.progress_bar.setMaximum(total)
self.progress_bar.setValue(cur)
self.progress_bar.setFormat(f"{cur}/{total} 张 (%p%)")
self._batch_thread.log_message.connect(_batch_log, Qt.QueuedConnection)
self._batch_thread.progress.connect(_on_progress, Qt.QueuedConnection)
self._batch_thread.finished_ok.connect(self._on_step10_batch_ok, Qt.QueuedConnection)
self._batch_thread.failed.connect(self._on_step10_batch_fail, Qt.QueuedConnection)
self._batch_thread.finished.connect(
lambda: (self.run_button.setEnabled(True), self.progress_bar.setVisible(False)),
Qt.QueuedConnection,
)
self._batch_thread.start()
if hasattr(parent, "log_message"):
parent.log_message(f"GeoTIFF 批量渲染:共 {len(tif_list)} 个文件 → {out_dir}", "info")
return
# -------- GeoTIFF 栅格单文件模式 --------
if self.render_mode_combo.currentText() == "GeoTIFF 栅格模式":
geotiff_path = (self.geotiff_file.get_path() or "").strip()
if not geotiff_path:
QMessageBox.warning(self, "输入验证失败", "请选择水色指数 GeoTIFF 文件")
return
if not os.path.isfile(geotiff_path):
QMessageBox.warning(self, "输入验证失败", f"GeoTIFF 文件不存在:\n{geotiff_path}")
return
boundary_shp_path = self.boundary_file.get_path()
input_crs = self.input_crs.text()
output_crs = self.output_crs.text()
# 构造输出路径
out_dir = (self.output_dir.get_path() or "").strip()
if not out_dir:
out_dir = os.path.join(self._get_default_work_dir(), "14_visualization")
os.makedirs(out_dir, exist_ok=True)
tif_stem = Path(geotiff_path).stem
chinese_name = mapper._get_chinese_title(tif_stem)
output_png = os.path.join(out_dir, f"{chinese_name}_专题图.png")
self.run_button.setEnabled(False)
try:
from src.postprocessing.map import ContentMapper
mapper = ContentMapper()
result_path = mapper.visualize_raster(
raster_tif_path=geotiff_path,
output_file=output_png,
boundary_shp_path=boundary_shp_path if boundary_shp_path else None,
nodata_value=-9999.0,
figsize=(14, 10),
alpha=0.9,
)
self.run_button.setEnabled(True)
QMessageBox.information(
self, "完成",
f"GeoTIFF 栅格渲染完成!\n{result_path}"
)
if hasattr(parent, "log_message"):
parent.log_message(f"Step10 GeoTIFF 渲染完成 → {result_path}", "info")
except Exception as e:
self.run_button.setEnabled(True)
QMessageBox.critical(self, "渲染失败", f"{e}\n{traceback.format_exc()[:500]}")
if hasattr(parent, "log_message"):
parent.log_message(str(e), "error")
return
prediction_csv_path = (self.prediction_csv_file.get_path() or "").strip()
if not prediction_csv_path:
QMessageBox.warning(
self,
"输入验证失败",
"请选择「预测结果 CSV」文件或切换到「文件夹批量」。",
)
return
if not os.path.isfile(prediction_csv_path):
QMessageBox.warning(self, "输入验证失败", "预测结果 CSV 不存在或不是文件")
return
config = self.get_config()
parent.run_single_step('step11_map', {'step11_map': config})
def _on_step10_batch_ok(self, n: int):
self.progress_bar.setVisible(False)
QMessageBox.information(self, "完成", f"已批量生成 {n} 个分布图。")
parent = self.parent()
while parent and not hasattr(parent, "log_message"):
parent = parent.parent()
if parent and hasattr(parent, "log_message"):
parent.log_message(f"专题图批量完成,共 {n} 个文件。", "info")
def _on_step10_batch_fail(self, err: str):
self.progress_bar.setVisible(False)
QMessageBox.critical(self, "失败", f"批量生成中断:\n{err[:900]}")
parent = self.parent()
while parent and not hasattr(parent, "log_message"):
parent = parent.parent()
if parent and hasattr(parent, "log_message"):
parent.log_message(err, "error")

View File

@ -1,226 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step11 面板 - 非经验模型预测
"""
import os
from pathlib import Path
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout,
QPushButton, QCheckBox, QComboBox, QLineEdit, QMessageBox,
QFileDialog,
)
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
class Step11Panel(QWidget):
"""步骤11非经验模型预测"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
# 采样光谱CSV文件选择
self.sampling_csv_file = FileSelectWidget(
"采样光谱CSV:",
"CSV Files (*.csv);;All Files (*.*)"
)
layout.addWidget(self.sampling_csv_file)
# 模型目录选择
self.models_dir_file = FileSelectWidget(
"模型目录:",
"Directories;;All Files (*.*)"
)
self.models_dir_file.label.setText("模型目录:")
self.models_dir_file.browse_btn.clicked.disconnect()
self.models_dir_file.browse_btn.clicked.connect(self.browse_models_dir)
layout.addWidget(self.models_dir_file)
# 参数设置
params_group = QGroupBox("预测参数")
params_layout = QFormLayout()
self.metric = QComboBox()
self.metric.addItems(['Average Accuracy(%)', 'Min Accuracy(%)', 'Max Accuracy(%)'])
params_layout.addRow("模型选择指标:", self.metric)
self.prediction_column = QLineEdit()
self.prediction_column.setText("prediction")
params_layout.addRow("预测列名:", self.prediction_column)
params_group.setLayout(params_layout)
layout.addWidget(params_group)
# 输出路径
self.output_file = FileSelectWidget(
"输出文件夹:",
"Directories;;All Files (*.*)"
)
self.output_file.label.setText("输出文件夹:")
self.output_file.browse_btn.clicked.disconnect()
self.output_file.browse_btn.clicked.connect(self.browse_output_dir)
layout.addWidget(self.output_file)
# 启用步骤
self.enable_checkbox = QCheckBox("启用此步骤")
self.enable_checkbox.setChecked(True)
layout.addWidget(self.enable_checkbox)
# 独立运行按钮
self.run_button = QPushButton("独立运行此步骤")
self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_button.clicked.connect(self.run_step)
layout.addWidget(self.run_button)
layout.addStretch()
self.setLayout(layout)
def update_from_config(self, work_dir=None, pipeline=None):
"""从全局配置自动填充采样光谱和回归模型目录
Args:
work_dir: 工作目录路径
pipeline: Pipeline 实例(未使用,保留接口兼容性)
"""
try:
import traceback
if work_dir:
self.work_dir = work_dir
elif hasattr(self, 'work_dir') and self.work_dir:
pass
else:
self.work_dir = None
main_window = self.window()
# 1. 尝试从 Step7 界面读取全湖采样点 CSV 路径
if main_window and hasattr(main_window, 'step7_panel'):
step7_widget = getattr(main_window.step7_panel, 'output_file', None)
step7_output_path = ""
if hasattr(step7_widget, 'get_path'):
step7_output_path = step7_widget.get_path() or ""
elif hasattr(step7_widget, 'text'):
step7_output_path = step7_widget.text() or ""
if step7_output_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step7_output_path):
step7_output_path = os.path.join(self.work_dir or '', step7_output_path).replace('\\', '/')
existing = self.sampling_csv_file.get_path()
if not existing or not existing.strip():
self.sampling_csv_file.set_path(step7_output_path)
# 2. 尝试从 Step8_Non_Empirical 界面读取回归模型目录
if main_window and hasattr(main_window, 'step8_non_empirical_panel'):
step8_non_empirical_widget = getattr(main_window.step8_non_empirical_panel, 'output_dir', None)
step8_non_empirical_models_dir = ""
if hasattr(step8_non_empirical_widget, 'get_path'):
step8_non_empirical_models_dir = step8_non_empirical_widget.get_path() or ""
elif hasattr(step8_non_empirical_widget, 'text'):
step8_non_empirical_models_dir = step8_non_empirical_widget.text() or ""
if step8_non_empirical_models_dir:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step8_non_empirical_models_dir):
step8_non_empirical_models_dir = os.path.join(self.work_dir or '', step8_non_empirical_models_dir).replace('\\', '/')
existing_models = self.models_dir_file.get_path()
if not existing_models or not existing_models.strip():
self.models_dir_file.set_path(step8_non_empirical_models_dir)
# 3. 自动填充输出路径(非经验模型预测目录)
if self.work_dir:
output_dir = os.path.join(self.work_dir, "11_12_13_predictions/Non_Empirical_Prediction")
os.makedirs(output_dir, exist_ok=True)
existing_out = self.output_file.get_path()
if not existing_out or not existing_out.strip():
self.output_file.set_path(output_dir)
except Exception as e:
import traceback
print(f"{self.__class__.__name__}】自动填充失败,跳过: {e}")
traceback.print_exc()
def _get_default_work_dir(self):
"""获取 work_dir优先用 panel 自身缓存的,否则尝试从主窗口取"""
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_models_dir(self):
"""浏览模型目录"""
default = self._get_default_work_dir()
if default:
default = os.path.join(default, "8_Regression_Modeling")
dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录", default)
if dir_path:
self.models_dir_file.set_path(dir_path)
def browse_output_dir(self):
"""浏览输出目录"""
default = self._get_default_work_dir()
if default:
default = os.path.join(default, "11_12_13_predictions/Non_Empirical_Prediction")
dir_path = QFileDialog.getExistingDirectory(self, "选择输出文件夹", default)
if dir_path:
self.output_file.set_path(dir_path)
def get_config(self):
"""获取配置"""
config = {
'metric': self.metric.currentText(),
'prediction_column': self.prediction_column.text(),
'enabled': self.enable_checkbox.isChecked()
}
sampling_csv_path = self.sampling_csv_file.get_path()
if sampling_csv_path:
config['sampling_csv_path'] = sampling_csv_path
models_dir = self.models_dir_file.get_path()
if models_dir:
config['models_dir'] = models_dir
output_path = self.output_file.get_path()
if output_path:
config['output_path'] = output_path
return config
def set_config(self, config):
"""设置配置"""
if 'metric' in config:
idx = self.metric.findText(config['metric'])
if idx >= 0:
self.metric.setCurrentIndex(idx)
if 'prediction_column' in config:
self.prediction_column.setText(config['prediction_column'])
if 'sampling_csv_path' in config:
self.sampling_csv_file.set_path(config['sampling_csv_path'])
if 'models_dir' in config:
self.models_dir_file.set_path(config['models_dir'])
if 'enabled' in config:
self.enable_checkbox.setChecked(config['enabled'])
def run_step(self):
"""独立运行步骤11"""
sampling_csv_path = self.sampling_csv_file.get_path()
if not sampling_csv_path:
QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件")
return
config = self.get_config()
parent = self.parent()
while parent and not hasattr(parent, 'run_single_step'):
parent = parent.parent()
if parent and hasattr(parent, 'run_single_step'):
parent.run_single_step('step11', {'step11': config})
else:
QMessageBox.critical(self, "错误", "无法找到父级GUI对象")

View File

@ -1211,8 +1211,8 @@ class ChartBrowserDialog(QDialog):
QMessageBox.critical(self, "错误", f"保存失败:\n{str(e)}")
class VisualizationPanel(QWidget):
"""可视化分析面板 - 重构版:左侧目录树 + 右侧图像查看器"""
class Step12VizPanel(QWidget):
"""步骤12可视化展示"""
def __init__(self, parent=None):
super().__init__(parent)
self.work_dir = None

View File

@ -225,6 +225,6 @@ class Step12Panel(QWidget):
parent = parent.parent()
if parent and hasattr(parent, 'run_single_step'):
parent.run_single_step('step12', {'step12': config})
parent.run_single_step('step13_report', {'step13_report': config})
else:
QMessageBox.critical(self, "错误", "无法找到父级GUI对象")

View File

@ -79,8 +79,8 @@ class ReportGenerateThread(QThread):
self.failed.emit(f"{e}\n{traceback.format_exc()}")
class ReportGenerationPanel(QWidget):
"""Word 报告生成面板。AI 配置统一由 AISettingsDialog 管理,本面板不持有配置状态。"""
class Step13ReportPanel(QWidget):
"""步骤13分析报告生成。AI 配置统一由 AISettingsDialog 管理,本面板不持有配置状态。"""
def __init__(self, main_window=None, parent=None):
super().__init__(parent)

View File

@ -68,7 +68,7 @@ class Step14BatchThread(QThread):
kw["output_image_path"] = str(Path(self.output_dir_optional) / f"{stem}_distribution.png")
else:
kw["output_image_path"] = None
pipeline.step14_distribution_map(**kw)
pipeline.step10_map(**kw)
self.finished_ok.emit(n)
except Exception as e:
self.failed.emit(f"{e}\n{traceback.format_exc()}")
@ -122,9 +122,10 @@ class Step14GeoTIFFBatchThread(QThread):
n = len(self.tif_paths)
for i, tif_path in enumerate(self.tif_paths):
self.progress.emit(i + 1, n)
tif_name = Path(tif_path).stem
output_png = str(Path(self.output_dir) / f"{tif_name}_map.png")
self.log_message.emit(f"GeoTIFF 渲染 [{i + 1}/{n}] {tif_name}", "info")
tif_stem = Path(tif_path).stem
chinese_name = mapper._get_chinese_title(tif_stem)
output_png = str(Path(self.output_dir) / f"{chinese_name}_专题图.png")
self.log_message.emit(f"GeoTIFF 渲染 [{i + 1}/{n}] {tif_stem}", "info")
try:
mapper.visualize_raster(
raster_tif_path=tif_path,
@ -132,7 +133,6 @@ class Step14GeoTIFFBatchThread(QThread):
boundary_shp_path=self.boundary_shp_path,
nodata_value=-9999.0,
figsize=(14, 10),
title=f"水色指数专题图 - {tif_name}",
alpha=0.9,
)
except Exception as vis_err:
@ -762,8 +762,9 @@ class Step14Panel(QWidget):
if not out_dir:
out_dir = os.path.join(self._get_default_work_dir(), "14_visualization")
os.makedirs(out_dir, exist_ok=True)
tif_name = Path(geotiff_path).stem
output_png = os.path.join(out_dir, f"{tif_name}_rendered.png")
tif_stem = Path(geotiff_path).stem
chinese_name = mapper._get_chinese_title(tif_stem)
output_png = os.path.join(out_dir, f"{chinese_name}_专题图.png")
self.run_button.setEnabled(False)
try:
@ -775,7 +776,6 @@ class Step14Panel(QWidget):
boundary_shp_path=boundary_shp_path if boundary_shp_path else None,
nodata_value=-9999.0,
figsize=(14, 10),
title=f"水色指数专题图 - {tif_name}",
alpha=0.9,
)
self.run_button.setEnabled(True)

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step10 面板 - 采样点生成
Step4 面板 - 采样点布设
"""
import os
@ -16,8 +16,8 @@ from src.gui.dialogs import SamplingViewerDialog
from src.gui.styles import ModernStylesheet
class Step10Panel(QWidget):
"""步骤10:采样点生成"""
class Step4SamplingPanel(QWidget):
"""步骤4:采样点布设"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
@ -71,7 +71,7 @@ class Step10Panel(QWidget):
"输出采样点:",
"CSV Files (*.csv);;All Files (*.*)"
)
self.output_file.line_edit.setPlaceholderText("sampling_points.csv")
self.output_file.line_edit.setPlaceholderText("sampling_spectra.csv")
layout.addWidget(self.output_file)
# 启用步骤
@ -207,7 +207,7 @@ class Step10Panel(QWidget):
# 3. 自动填充输出路径(绝对路径)
if self.work_dir:
output_path = os.path.join(self.work_dir, "10_sampling", "sampling_spectra.csv")
output_path = os.path.join(self.work_dir, "4_sampling", "sampling_spectra.csv")
os.makedirs(os.path.dirname(output_path), exist_ok=True)
self.output_file.set_path(output_path.replace('\\', '/'))
@ -215,7 +215,7 @@ class Step10Panel(QWidget):
self._check_csv_exists()
def run_step(self):
"""独立运行步骤10"""
"""独立运行步骤4"""
deglint_img_path = self.deglint_img_file.get_path()
if not deglint_img_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
@ -223,8 +223,8 @@ class Step10Panel(QWidget):
main_window = self.window()
if hasattr(main_window, 'run_single_step'):
config = {'step10': self.get_config()}
main_window.run_single_step('step10', config)
config = {'step4_sampling': self.get_config()}
main_window.run_single_step('step4_sampling', config)
def _check_csv_exists(self):
"""检查 output csv 是否存在,驱动预览按钮启停"""
@ -243,7 +243,7 @@ class Step10Panel(QWidget):
if not csv_path or not os.path.exists(csv_path):
QMessageBox.warning(
self, "文件不存在",
f"采样点 CSV 文件不存在:{csv_path}\n请先运行步骤10生成数据。"
f"采样点 CSV 文件不存在:{csv_path}\n请先运行步骤4生成数据。"
)
return
dialog = SamplingViewerDialog(csv_path, self)

View File

@ -18,8 +18,8 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
class Step4Panel(QWidget):
"""步骤4:数据预处理"""
class Step5CleanPanel(QWidget):
"""步骤5:数据清洗"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
@ -135,18 +135,16 @@ class Step4Panel(QWidget):
self.output_file.set_path("")
def run_step(self):
"""独立运行步骤4"""
# 验证输入
"""独立运行步骤5"""
csv_path = self.csv_file.get_path()
if not csv_path:
QMessageBox.warning(self, "输入错误", "请选择水质参数文件!")
return
# 获取主窗口并运行步骤
main_window = self.window()
if hasattr(main_window, 'run_single_step'):
config = {'step4': self.get_config()}
main_window.run_single_step('step4', config)
config = {'step5_clean': self.get_config()}
main_window.run_single_step('step5_clean', config)
def reset_preview(self, message="请选择CSV文件并点击刷新预览"):
"""重置预览表格"""

View File

@ -17,8 +17,8 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
class Step5Panel(QWidget):
"""步骤5:光谱提取"""
class Step6FeaturePanel(QWidget):
"""步骤6:光谱特征提取"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
@ -124,7 +124,7 @@ class Step5Panel(QWidget):
glint_mask_path = self.glint_mask_file.get_path()
if glint_mask_path:
config['glint_mask_path'] = glint_mask_path
# 注意step5_extract_training_spectra 不接受 output_path / training_csv_path
# 注意step6_extract_spectra 不接受 output_path / training_csv_path
# 参数,输出路径由 pipeline 内部根据 training_spectra_dir 自动生成。
return config
@ -202,8 +202,8 @@ class Step5Panel(QWidget):
# 5. 尝试从 Step4 界面读取已处理的水质参数 CSV 路径,自动填入本面板
main_window = self.window()
if main_window and hasattr(main_window, 'step4_panel'):
step4_output_path = main_window.step4_panel.output_file.get_path()
if main_window and hasattr(main_window, 'step5_panel'):
step4_output_path = main_window.step5_panel.output_file.get_path()
if step4_output_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step4_output_path):
@ -235,5 +235,5 @@ class Step5Panel(QWidget):
# 获取主窗口并运行步骤
main_window = self.window()
if hasattr(main_window, 'run_single_step'):
config = {'step5': self.get_config()}
main_window.run_single_step('step5', config)
config = {'step6_feature': self.get_config()}
main_window.run_single_step('step6_feature', config)

View File

@ -1,3 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step7 面板 - 水质指数计算
"""
import os
import sys
import pandas as pd
@ -35,7 +41,7 @@ def get_resource_path(relative_path: str) -> str:
return str(base_dir / os.path.basename(relative_path))
class Step6Panel(QWidget):
class Step7IndexPanel(QWidget):
COLOR_RATIO = QColor(255, 255, 255)
COLOR_CONCENTRATION = QColor(220, 240, 255)
COLOR_HEADER = QColor(245, 245, 245)
@ -291,8 +297,8 @@ class Step6Panel(QWidget):
if work_dir:
self.work_dir = work_dir
main = self.window()
if hasattr(main, 'step5_panel'):
p5 = main.step5_panel.output_file.get_path()
if hasattr(main, 'step6_panel'):
p5 = main.step6_panel.output_file.get_path()
if p5:
if not os.path.isabs(p5):
p5 = os.path.join(self.work_dir or '', p5)

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step7 面板 - 机器学习建模
Step8 面板 - 机器学习建模
"""
import os
@ -68,8 +68,8 @@ SPLIT_CHINESE = {
}
class Step7Panel(QWidget):
"""步骤7:机器学习建模"""
class Step8MlTrainPanel(QWidget):
"""步骤8:机器学习建模"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
@ -392,7 +392,7 @@ class Step7Panel(QWidget):
self.output_path.set_path("")
def run_step(self):
"""独立运行步骤7"""
"""独立运行步骤8"""
training_csv_path = self.training_csv_file.get_path()
if not training_csv_path:
QMessageBox.warning(self, "输入错误", "请选择训练数据CSV文件")
@ -400,8 +400,8 @@ class Step7Panel(QWidget):
main_window = self.window()
if hasattr(main_window, 'run_single_step'):
config = {'step7': self.get_config()}
main_window.run_single_step('step7', config)
config = {'step8_ml_train': self.get_config()}
main_window.run_single_step('step8_ml_train', config)
def get_training_params(self):
"""获取模型训练参数"""

View File

@ -1,424 +0,0 @@
import os
import sys
import pandas as pd
import numpy as np
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QGridLayout,
QHBoxLayout, QLabel, QCheckBox, QPushButton, QMessageBox,
QScrollArea, QListWidget, QListWidgetItem, QAbstractItemView,
QRadioButton, QButtonGroup
)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QBrush, QFont
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
def get_resource_path(relative_path: str) -> str:
"""适配开发与 PyInstaller 环境的路径获取逻辑。"""
if hasattr(sys, '_MEIPASS'):
internal = os.path.join(sys._MEIPASS, '_internal', relative_path)
if os.path.exists(internal):
return internal
return os.path.join(sys._MEIPASS, relative_path)
exe_dir = os.path.dirname(sys.executable)
internal = os.path.join(exe_dir, '_internal', relative_path)
if os.path.exists(internal):
return internal
base_dir = Path(__file__).resolve().parent.parent / "model"
return str(base_dir / os.path.basename(relative_path))
class Step8Panel(QWidget):
COLOR_RATIO = QColor(255, 255, 255)
COLOR_CONCENTRATION = QColor(220, 240, 255)
COLOR_HEADER = QColor(245, 245, 245)
def __init__(self, parent=None):
super().__init__(parent)
self.index_checkboxes: Dict[str, QListWidgetItem] = {}
self.work_dir: Optional[str] = None
self.builtin_formula_path = get_resource_path("waterindex.csv")
self._formula_type_map: Dict[str, str] = {}
self._formula_color_map: Dict[str, QColor] = {}
self._formula_coef_map: Dict[str, List[float]] = {}
self.init_ui()
self._auto_load_formulas()
def init_ui(self):
main_layout = QVBoxLayout()
main_layout.setContentsMargins(20, 20, 20, 20)
main_layout.setSpacing(10)
# 1. 公式配置源 (只读)
path_group = QGroupBox("公式配置源 (内置)")
path_layout = QVBoxLayout()
self.formula_csv_widget = FileSelectWidget("内置CSV路径:", "CSV Files (*.csv)")
self.formula_csv_widget.set_path(self.builtin_formula_path)
self.formula_csv_widget.set_read_only(True)
self.formula_csv_widget.line_edit.setStyleSheet("background-color: #f0f0f0; color: #666;")
path_layout.addWidget(self.formula_csv_widget)
path_group.setLayout(path_layout)
main_layout.addWidget(path_group)
# 2. 训练数据输入
input_group = QGroupBox("输入样本数据")
input_layout = QVBoxLayout()
self.training_data_widget = FileSelectWidget("特征提取CSV:", "CSV Files (*.csv)")
input_layout.addWidget(self.training_data_widget)
input_group.setLayout(input_layout)
main_layout.addWidget(input_group)
# 3. 公式选择区 (分组 ListWidget)
self.formula_group = QGroupBox("待计算水质指数勾选")
formula_outer_layout = QVBoxLayout()
btn_layout = QHBoxLayout()
self.select_all_btn = QPushButton("全选")
self.deselect_all_btn = QPushButton("清空")
self.select_ratio_btn = QPushButton("仅选比值型")
self.select_conc_btn = QPushButton("仅选浓度型")
self.select_all_btn.clicked.connect(self.select_all_formulas)
self.deselect_all_btn.clicked.connect(self.deselect_all_formulas)
self.select_ratio_btn.clicked.connect(self._select_ratio_only)
self.select_conc_btn.clicked.connect(self._select_conc_only)
btn_layout.addWidget(self.select_all_btn)
btn_layout.addWidget(self.deselect_all_btn)
btn_layout.addWidget(self.select_ratio_btn)
btn_layout.addWidget(self.select_conc_btn)
btn_layout.addStretch()
self.refresh_button = QPushButton("重新加载")
self.refresh_button.clicked.connect(lambda: self.refresh_formulas(silent=False))
btn_layout.addWidget(self.refresh_button)
formula_outer_layout.addLayout(btn_layout)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setMinimumHeight(280)
self.scroll_content = QWidget()
self.formula_layout = QVBoxLayout(self.scroll_content)
self.formula_layout.setContentsMargins(4, 4, 4, 4)
self.formula_layout.setSpacing(2)
self.formula_layout.setAlignment(Qt.AlignTop)
self.formula_list = QListWidget()
self.formula_list.setSelectionMode(QAbstractItemView.MultiSelection)
self.formula_list.itemChanged.connect(self._on_item_changed)
self.formula_layout.addWidget(self.formula_list)
scroll.setWidget(self.scroll_content)
formula_outer_layout.addWidget(scroll)
self.formula_group.setLayout(formula_outer_layout)
main_layout.addWidget(self.formula_group)
# 4. 输出选项
output_group = QGroupBox("输出模式")
output_layout = QVBoxLayout()
mode_layout = QHBoxLayout()
self.mode_group = QButtonGroup()
self.radio_both = QRadioButton("两者皆出")
self.radio_wide = QRadioButton("仅宽表")
self.radio_single = QRadioButton("仅单文件")
self.mode_group.addButton(self.radio_both, 0)
self.mode_group.addButton(self.radio_wide, 1)
self.mode_group.addButton(self.radio_single, 2)
self.radio_both.setChecked(True)
mode_layout.addWidget(self.radio_both)
mode_layout.addWidget(self.radio_wide)
mode_layout.addWidget(self.radio_single)
mode_layout.addStretch()
output_layout.addLayout(mode_layout)
self.enable_checkbox = QCheckBox("启用计算流程")
self.enable_checkbox.setChecked(True)
output_layout.addWidget(self.enable_checkbox)
output_group.setLayout(output_layout)
main_layout.addWidget(output_group)
# 5. 运行按钮
self.run_button = QPushButton("立即执行计算")
self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_button.setMinimumHeight(40)
self.run_button.clicked.connect(self.run_step)
main_layout.addWidget(self.run_button)
self.setLayout(main_layout)
def _on_item_changed(self, item: QListWidgetItem):
if item.checkState() == Qt.Checked:
bg_color = self.COLOR_RATIO
for name, ref_item in self.index_checkboxes.items():
if ref_item is item:
bg_color = self._formula_color_map.get(name, self.COLOR_RATIO)
break
item.setBackground(QBrush(bg_color))
else:
item.setBackground(QBrush(self.COLOR_RATIO))
def _auto_load_formulas(self):
if os.path.exists(self.builtin_formula_path):
self.refresh_formulas(silent=True)
else:
print(f"DEBUG: 自动加载失败,路径不存在: {self.builtin_formula_path}")
def refresh_formulas(self, silent=False):
path = self.builtin_formula_path
if not os.path.exists(path):
if not silent:
QMessageBox.warning(self, "错误", f"找不到内置公式文件:\n{path}")
return
try:
df = None
for enc in ('utf-8', 'gbk', 'utf-8-sig'):
try:
df = pd.read_csv(path, encoding=enc)
if 'Formula_Name' in df.columns:
break
except Exception:
continue
if df is None or 'Formula_Name' not in df.columns:
if not silent:
QMessageBox.critical(self, "错误", "CSV缺少 'Formula_Name'")
return
self._formula_type_map.clear()
self._formula_coef_map.clear()
for _, row in df.iterrows():
name = str(row['Formula_Name']).strip()
if not name:
continue
ftype = str(row.get('Formula_Type', 'ratio')).strip().lower()
self._formula_type_map[name] = ftype
# Parse Coefficient for concentration formulas
coef_str = str(row.get('Coefficient', '')).strip()
if coef_str:
try:
coeffs = [float(c.strip()) for c in coef_str.split(',') if c.strip()]
self._formula_coef_map[name] = coeffs
except Exception:
self._formula_coef_map[name] = []
else:
self._formula_coef_map[name] = []
self.formula_list.clear()
self.index_checkboxes.clear()
self._formula_color_map.clear()
for name, ftype in self._formula_type_map.items():
item = QListWidgetItem(name, self.formula_list)
item.setCheckState(Qt.Checked)
if ftype == 'concentration':
bg_color = QColor(220, 240, 255)
else:
bg_color = self.COLOR_RATIO
self._formula_color_map[name] = bg_color
item.setBackground(QBrush(bg_color))
self.index_checkboxes[name] = item
self.formula_list.adjustSize()
print(f"✅ 加载 {len(self.index_checkboxes)} 个公式")
except Exception as e:
if not silent:
QMessageBox.critical(self, "加载失败", f"原因: {str(e)}")
def _select_ratio_only(self):
for name, item in self.index_checkboxes.items():
ftype = self._formula_type_map.get(name, 'ratio')
item.setCheckState(Qt.Checked if ftype == 'ratio' else Qt.Unchecked)
def _select_conc_only(self):
for name, item in self.index_checkboxes.items():
ftype = self._formula_type_map.get(name, 'ratio')
item.setCheckState(Qt.Checked if ftype == 'concentration' else Qt.Unchecked)
def select_all_formulas(self):
for item in self.index_checkboxes.values():
item.setCheckState(Qt.Checked)
def deselect_all_formulas(self):
for item in self.index_checkboxes.values():
item.setCheckState(Qt.Unchecked)
def get_config(self) -> Dict:
selected = [
name for name, item in self.index_checkboxes.items()
if item.checkState() == Qt.Checked
]
# Build coefficient dict for selected formulas
formula_coefficients = {
name: self._formula_coef_map.get(name, [])
for name in selected
}
return {
'training_csv_path': self.training_data_widget.get_path(),
'formula_csv_file': self.builtin_formula_path,
'formula_names': selected,
'formula_coefficients': formula_coefficients,
'enabled': self.enable_checkbox.isChecked(),
'output_mode': self.mode_group.checkedId(),
}
def set_config(self, config: Dict):
if 'training_csv_path' in config:
self.training_data_widget.set_path(config['training_csv_path'])
if 'formula_names' in config:
sel = set(config['formula_names'])
for name, item in self.index_checkboxes.items():
item.setCheckState(Qt.Checked if name in sel else Qt.Unchecked)
self.enable_checkbox.setChecked(config.get('enabled', True))
if 'output_mode' in config:
btn = self.mode_group.button(config['output_mode'])
if btn:
btn.setChecked(True)
def update_from_config(self, work_dir=None, pipeline=None):
if work_dir:
self.work_dir = work_dir
main = self.window()
if hasattr(main, 'step5_panel'):
p5 = main.step5_panel.output_file.get_path()
if p5:
if not os.path.isabs(p5):
p5 = os.path.join(self.work_dir or '', p5)
p5 = p5.replace('\\', '/')
self.training_data_widget.set_path(p5)
def _get_work_dir(self) -> Optional[str]:
if self.work_dir:
return self.work_dir
main = self.window()
if hasattr(main, 'work_dir') and main.work_dir:
return main.work_dir
return None
def _get_coord_cols(self, df: pd.DataFrame) -> Tuple[str, str]:
coord_candidates = ['lon', 'lng', 'longitude', '经度', 'x', 'lon_utm', 'utm_x', 'pixel_x']
lat_candidates = ['lat', 'latitude', '纬度', 'y', 'lat_utm', 'utm_y', 'pixel_y']
x_col, y_col = None, None
for col in df.columns:
cl = col.lower()
if x_col is None and any(c in cl for c in coord_candidates):
x_col = col
if y_col is None and any(c in cl for c in lat_candidates):
y_col = col
if x_col is None and len(df.columns) >= 2:
x_col = df.columns[0]
if y_col is None and len(df.columns) >= 2:
y_col = df.columns[1]
return x_col or 'x_coord', y_col or 'y_coord'
def run_step(self):
config = self.get_config()
if not config['enabled']:
QMessageBox.information(self, "提示", "已禁用计算流程(启用计算流程未勾选)")
return
training_path = config['training_csv_path']
if not training_path or not os.path.exists(training_path):
QMessageBox.warning(self, "提示", "请先选择输入特征提取CSV文件")
return
formula_names = config['formula_names']
if not formula_names:
QMessageBox.warning(self, "提示", "请至少勾选一个公式")
return
output_mode = config['output_mode']
try:
from src.core.steps.data_preparation_step import DataPreparationStep
spec_df = pd.read_csv(training_path)
x_col, y_col = self._get_coord_cols(spec_df)
# 构建 formula_csv_path使用内置 waterindex.csv
import os
formula_csv_path = self.builtin_formula_path
if not formula_csv_path or not os.path.exists(formula_csv_path):
# 尝试从 src/gui/model/ 目录找
possible_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'gui', 'model', 'waterindex.csv')
if os.path.exists(possible_path):
formula_csv_path = possible_path
work_dir = self._get_work_dir()
# 调用 DataPreparationStep 的静态方法计算水质指数(宽表输出)
indices_csv_path = DataPreparationStep.calculate_water_quality_indices(
training_csv_path=training_path,
formula_csv_file=formula_csv_path,
formula_names=formula_names,
output_file=None, # 不在此处指定输出,由下面的双轨输出逻辑接管
enabled=True,
output_dir=work_dir if work_dir else os.getcwd(),
)
# 读取计算结果(宽表)
if indices_csv_path and os.path.exists(indices_csv_path):
output_df = pd.read_csv(indices_csv_path)
else:
output_df = spec_df # fallback
track_a_path = None
track_b_dir = None
if output_mode in (0, 1):
track_a_dir = os.path.join(work_dir, "6_water_quality_indices") if work_dir else "6_water_quality_indices"
os.makedirs(track_a_dir, exist_ok=True)
track_a_path = os.path.join(track_a_dir, "training_spectra_indices.csv")
if output_mode in (0, 2):
track_b_dir = os.path.join(work_dir, "11_12_13_predictions", "Traditional_Indices") if work_dir else "11_12_13_predictions/Traditional_Indices"
os.makedirs(track_b_dir, exist_ok=True)
saved = []
if output_mode in (0, 1):
output_df.to_csv(track_a_path, index=False, float_format='%.6f')
saved.append(f"宽表: {track_a_path}")
if output_mode in (0, 2):
coord_x = spec_df[x_col].values if x_col in spec_df.columns else np.arange(len(spec_df))
coord_y = spec_df[y_col].values if y_col in spec_df.columns else np.zeros(len(spec_df))
for formula_name in formula_names:
if formula_name not in output_df.columns:
continue
single_df = pd.DataFrame({
'x_coord': coord_x,
'y_coord': coord_y,
'value': output_df[formula_name].values,
})
safe_name = formula_name.replace('/', '_').replace(' ', '_')
out_path = os.path.join(track_b_dir, f"{safe_name}_prediction.csv")
single_df.to_csv(out_path, index=False, float_format='%.6f')
saved.append(f"单文件目录: {track_b_dir}")
QMessageBox.information(
self, "计算完成",
f"已保存 {len(saved)} 个输出目标:\n" + "\n".join(saved)
)
except ImportError as e:
QMessageBox.critical(self, "依赖错误", f"无法导入模块:\n{e}")
except Exception as e:
import traceback
QMessageBox.critical(self, "计算失败", f"原因: {str(e)}\n{traceback.format_exc()}")

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step8 面板 - 机器学习预测
Step11 面板 - 机器学习预测
"""
import os
@ -19,8 +19,8 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
class Step11MlPanel(QWidget):
"""步骤11:机器学习预测"""
class Step9MlPredictPanel(QWidget):
"""步骤9:机器学习预测"""
def __init__(self, parent=None):
super().__init__(parent)
self.external_models_dict = {} # {subdir_name: model_obj, ...}
@ -190,7 +190,7 @@ class Step11MlPanel(QWidget):
"""浏览模型母文件夹,自动扫描子目录中的 .joblib 文件"""
default = self._get_default_work_dir()
if default:
default = os.path.join(default, "7_Supervised_Model_Training")
default = os.path.join(default, "9_supervised_modeling")
dir_path = QFileDialog.getExistingDirectory(
self,
"选择模型母文件夹",
@ -216,7 +216,6 @@ class Step11MlPanel(QWidget):
]
if not joblib_files:
continue
# 每个子目录只取第一个 .joblib 文件(与 batch 逻辑一致)
joblib_path = joblib_files[0].path
try:
loaded = joblib.load(joblib_path)
@ -319,43 +318,41 @@ class Step11MlPanel(QWidget):
main_window = self.window()
# 1. 尝试从 Step7 界面读取全湖采样点 CSV 路径
if main_window and hasattr(main_window, 'step10_panel'):
step7_widget = getattr(main_window.step10_panel, 'output_file', None)
step7_output_path = ""
if hasattr(step7_widget, 'get_path'):
step7_output_path = step7_widget.get_path() or ""
elif hasattr(step7_widget, 'text'):
step7_output_path = step7_widget.text() or ""
# 1. 尝试从 Step4采样点布设读取全湖采样点 CSV 路径
if main_window and hasattr(main_window, 'step4_sampling_panel'):
step4_widget = getattr(main_window.step4_sampling_panel, 'output_file', None)
step4_output_path = ""
if hasattr(step4_widget, 'get_path'):
step4_output_path = step4_widget.get_path() or ""
elif hasattr(step4_widget, 'text'):
step4_output_path = step4_widget.text() or ""
if step7_output_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step7_output_path):
step7_output_path = os.path.join(self.work_dir or '', step7_output_path).replace('\\', '/')
if step4_output_path:
if not os.path.isabs(step4_output_path):
step4_output_path = os.path.join(self.work_dir or '', step4_output_path).replace('\\', '/')
existing = self.sampling_csv_file.get_path()
if not existing or not existing.strip():
self.sampling_csv_file.set_path(step7_output_path)
self.sampling_csv_file.set_path(step4_output_path)
# 2. 尝试从 Step6 界面读取监督模型目录
if main_window and hasattr(main_window, 'step7_panel'):
step6_widget = getattr(main_window.step7_panel, 'output_dir', None)
step6_models_dir = ""
if hasattr(step6_widget, 'get_path'):
step6_models_dir = step6_widget.get_path() or ""
elif hasattr(step6_widget, 'text'):
step6_models_dir = step6_widget.text() or ""
# 2. 尝试从 Step9监督建模读取模型目录
if main_window and hasattr(main_window, 'step9_panel'):
step9_widget = getattr(main_window.step9_panel, 'output_dir', None)
step9_models_dir = ""
if hasattr(step9_widget, 'get_path'):
step9_models_dir = step9_widget.get_path() or ""
elif hasattr(step9_widget, 'text'):
step9_models_dir = step9_widget.text() or ""
if step6_models_dir:
# 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step6_models_dir):
step6_models_dir = os.path.join(self.work_dir or '', step6_models_dir).replace('\\', '/')
if step9_models_dir:
if not os.path.isabs(step9_models_dir):
step9_models_dir = os.path.join(self.work_dir or '', step9_models_dir).replace('\\', '/')
existing_models = self.models_dir_file.get_path()
if not existing_models or not existing_models.strip():
self.models_dir_file.set_path(step6_models_dir)
self.models_dir_file.set_path(step9_models_dir)
# 3. 自动填充输出路径(机器学习预测目录)
if self.work_dir:
output_dir = os.path.join(self.work_dir, "11_12_13_predictions/Machine_Learning_Prediction")
output_dir = os.path.join(self.work_dir, "11_ml_prediction")
os.makedirs(output_dir, exist_ok=True)
existing_out = self.output_file.get_path()
if not existing_out or not existing_out.strip():
@ -378,7 +375,7 @@ class Step11MlPanel(QWidget):
"""浏览模型目录"""
default = self._get_default_work_dir()
if default:
default = os.path.join(default, "7_Supervised_Model_Training")
default = os.path.join(default, "9_supervised_modeling")
dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录", default)
if dir_path:
self.models_dir_file.set_path(dir_path)
@ -416,7 +413,7 @@ class Step11MlPanel(QWidget):
self.output_file.set_path(config['output_path'])
def run_step(self):
"""独立运行步骤8"""
"""独立运行步骤11"""
sampling_csv_path = self.sampling_csv_file.get_path()
if not sampling_csv_path:
QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件")
@ -431,7 +428,6 @@ class Step11MlPanel(QWidget):
"请先点击「浏览...」按钮选择模型母文件夹!",
)
return
# 只传递用户勾选的模型
checked_dict = self._get_checked_models_dict()
if not checked_dict:
QMessageBox.warning(
@ -443,11 +439,11 @@ class Step11MlPanel(QWidget):
main_window = self.window()
if hasattr(main_window, 'run_single_step'):
config = {
'step11_ml': self.get_config(),
'step9_ml_predict': self.get_config(),
'_external_models_dict': checked_dict,
'_external_model_dir': self.external_model_dir,
}
main_window.run_single_step('step11_ml', config)
main_window.run_single_step('step9_ml_predict', config)
return
# 默认流程:使用模型目录
@ -458,5 +454,5 @@ class Step11MlPanel(QWidget):
main_window = self.window()
if hasattr(main_window, 'run_single_step'):
config = {'step11_ml': self.get_config()}
main_window.run_single_step('step11_ml', config)
config = {'step9_ml_predict': self.get_config()}
main_window.run_single_step('step9_ml_predict', config)

View File

@ -1,400 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step9 面板 - 自定义回归分析
"""
import os
from typing import Dict
import pandas as pd
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout,
QHBoxLayout, QLabel, QLineEdit, QCheckBox, QPushButton,
QScrollArea, QMessageBox,
)
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
class Step9Panel(QWidget):
"""步骤9自定义回归分析"""
def __init__(self, parent=None):
super().__init__(parent)
self.x_column_checkboxes: Dict[str, QCheckBox] = {}
self.y_column_checkboxes: Dict[str, QCheckBox] = {}
self.method_checkboxes: Dict[str, QCheckBox] = {}
self.csv_columns = []
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
hint = QLabel("指定自变量与因变量列,批量尝试不同回归方法")
hint.setStyleSheet("color: #666; font-size: 11px;")
layout.addWidget(hint)
# CSV文件选择
csv_group = QGroupBox("数据文件")
csv_layout = QVBoxLayout()
self.csv_file = FileSelectWidget(
"输入CSV文件:",
"CSV Files (*.csv);;All Files (*.*)"
)
self.csv_file.line_edit.textChanged.connect(self.on_csv_file_changed)
csv_layout.addWidget(self.csv_file)
self.refresh_btn = QPushButton("刷新列信息")
self.refresh_btn.clicked.connect(self.refresh_csv_columns)
csv_layout.addWidget(self.refresh_btn)
csv_group.setLayout(csv_layout)
layout.addWidget(csv_group)
# 自变量选择
x_group = QGroupBox("自变量列选择 (可多选)")
x_layout = QVBoxLayout()
x_scroll = QScrollArea()
x_scroll.setWidgetResizable(True)
x_scroll.setMinimumHeight(250)
x_scroll.setMaximumHeight(350)
x_widget = QWidget()
self.x_columns_layout = QGridLayout()
x_widget.setLayout(self.x_columns_layout)
x_scroll.setWidget(x_widget)
x_layout.addWidget(x_scroll)
x_btn_layout = QHBoxLayout()
self.x_select_all = QPushButton("全选")
self.x_deselect_all = QPushButton("全不选")
self.x_select_all.clicked.connect(lambda: self.toggle_checkboxes(self.x_column_checkboxes, True))
self.x_deselect_all.clicked.connect(lambda: self.toggle_checkboxes(self.x_column_checkboxes, False))
x_btn_layout.addWidget(self.x_select_all)
x_btn_layout.addWidget(self.x_deselect_all)
x_btn_layout.addStretch()
x_layout.addLayout(x_btn_layout)
x_group.setLayout(x_layout)
layout.addWidget(x_group)
# 因变量选择
y_group = QGroupBox("因变量列选择 (可多选)")
y_layout = QVBoxLayout()
y_scroll = QScrollArea()
y_scroll.setWidgetResizable(True)
y_scroll.setMinimumHeight(200)
y_scroll.setMaximumHeight(300)
y_widget = QWidget()
self.y_columns_layout = QGridLayout()
y_widget.setLayout(self.y_columns_layout)
y_scroll.setWidget(y_widget)
y_layout.addWidget(y_scroll)
y_btn_layout = QHBoxLayout()
self.y_select_all = QPushButton("全选")
self.y_deselect_all = QPushButton("全不选")
self.y_select_all.clicked.connect(lambda: self.toggle_checkboxes(self.y_column_checkboxes, True))
self.y_deselect_all.clicked.connect(lambda: self.toggle_checkboxes(self.y_column_checkboxes, False))
y_btn_layout.addWidget(self.y_select_all)
y_btn_layout.addWidget(self.y_deselect_all)
y_btn_layout.addStretch()
y_layout.addLayout(y_btn_layout)
y_group.setLayout(y_layout)
layout.addWidget(y_group)
# 回归方法选择
method_group = QGroupBox("回归方法选择 (可多选)")
method_layout = QVBoxLayout()
method_grid = QGridLayout()
regression_methods = [
'linear', 'exponential', 'power', 'logarithmic',
'polynomial', 'hyperbolic', 'sigmoidal'
]
for i, method in enumerate(regression_methods):
checkbox = QCheckBox(method)
if method in ['linear', 'exponential', 'power', 'logarithmic']:
checkbox.setChecked(True)
self.method_checkboxes[method] = checkbox
method_grid.addWidget(checkbox, i // 3, i % 3)
method_layout.addLayout(method_grid)
method_btn_layout = QHBoxLayout()
self.method_select_all = QPushButton("全选")
self.method_deselect_all = QPushButton("全不选")
self.method_select_all.clicked.connect(lambda: self.toggle_checkboxes(self.method_checkboxes, True))
self.method_deselect_all.clicked.connect(lambda: self.toggle_checkboxes(self.method_checkboxes, False))
method_btn_layout.addWidget(self.method_select_all)
method_btn_layout.addWidget(self.method_deselect_all)
method_btn_layout.addStretch()
method_layout.addLayout(method_btn_layout)
method_group.setLayout(method_layout)
layout.addWidget(method_group)
# 输出目录
output_group = QGroupBox("输出设置")
output_layout = QFormLayout()
self.output_dir = QLineEdit()
self.output_dir.setText("") # 路径由 update_from_config 根据 work_dir 自动填充
output_layout.addRow("输出目录名:", self.output_dir)
output_group.setLayout(output_layout)
layout.addWidget(output_group)
# 启用步骤
self.enable_checkbox = QCheckBox("启用此步骤")
self.enable_checkbox.setChecked(True)
layout.addWidget(self.enable_checkbox)
# 独立运行按钮
self.run_button = QPushButton("独立运行此步骤")
self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
self.run_button.clicked.connect(self.run_step)
layout.addWidget(self.run_button)
layout.addStretch()
self.setLayout(layout)
def toggle_checkboxes(self, checkboxes_dict, checked):
"""统一设置checkbox状态"""
for checkbox in checkboxes_dict.values():
checkbox.setChecked(checked)
def on_csv_file_changed(self):
"""CSV文件改变时自动刷新列信息"""
self.refresh_csv_columns()
def refresh_csv_columns(self):
"""刷新CSV文件的列信息"""
csv_path = self.csv_file.get_path()
if not csv_path or not os.path.exists(csv_path):
self.csv_columns = []
self.update_column_widgets()
return
try:
df = pd.read_csv(csv_path, nrows=0)
self.csv_columns = list(df.columns)
self.update_column_widgets()
except Exception as e:
self.csv_columns = []
self.update_column_widgets()
print(f"读取CSV列信息失败: {e}")
def update_column_widgets(self):
"""更新列选择组件"""
for checkbox in self.x_column_checkboxes.values():
checkbox.setParent(None)
self.x_column_checkboxes.clear()
for checkbox in self.y_column_checkboxes.values():
checkbox.setParent(None)
self.y_column_checkboxes.clear()
if not self.csv_columns:
return
for i, col in enumerate(self.csv_columns):
checkbox = QCheckBox(col)
if any(keyword in col.lower() for keyword in ['index', 'ratio', 'normalized', 'nd', 'b']):
checkbox.setChecked(True)
self.x_column_checkboxes[col] = checkbox
self.x_columns_layout.addWidget(checkbox, i // 3, i % 3)
for i, col in enumerate(self.csv_columns):
checkbox = QCheckBox(col)
if any(keyword in col.lower() for keyword in ['chl', 'tn', 'tp', 'turbidity', 'do', 'ph', 'conductivity']):
checkbox.setChecked(True)
self.y_column_checkboxes[col] = checkbox
self.y_columns_layout.addWidget(checkbox, i // 2, i % 2)
self.x_columns_layout.update()
self.y_columns_layout.update()
def get_config(self):
selected_x_columns = [
col for col, checkbox in self.x_column_checkboxes.items()
if checkbox.isChecked()
]
selected_y_columns = [
col for col, checkbox in self.y_column_checkboxes.items()
if checkbox.isChecked()
]
selected_methods = [
method for method, checkbox in self.method_checkboxes.items()
if checkbox.isChecked()
]
if not selected_methods:
selected_methods = 'all'
return {
'csv_path': self.csv_file.get_path() or None,
'x_columns': selected_x_columns,
'y_columns': selected_y_columns,
'methods': selected_methods,
'output_dir': self.output_dir.text().strip() or None,
'enabled': self.enable_checkbox.isChecked()
}
def set_config(self, config):
if 'csv_path' in config:
self.csv_file.set_path(config['csv_path'])
self.refresh_csv_columns()
if 'x_columns' in config:
selected_x = set(config['x_columns']) if isinstance(config['x_columns'], list) else set()
for col, checkbox in self.x_column_checkboxes.items():
checkbox.setChecked(col in selected_x)
if 'y_columns' in config:
selected_y = set(config['y_columns']) if isinstance(config['y_columns'], list) else set()
for col, checkbox in self.y_column_checkboxes.items():
checkbox.setChecked(col in selected_y)
if 'methods' in config:
methods = config['methods']
if isinstance(methods, list):
selected_methods = set(methods)
elif methods == 'all':
selected_methods = set(self.method_checkboxes.keys())
else:
selected_methods = set()
for method, checkbox in self.method_checkboxes.items():
checkbox.setChecked(method in selected_methods)
if 'output_dir' in config:
self.output_dir.setText(config['output_dir'] or "9_Custom_Regression_Modeling")
if 'enabled' in config:
self.enable_checkbox.setChecked(config['enabled'])
def update_from_config(self, work_dir=None, pipeline=None):
"""从全局配置自动填充训练数据和输出路径
Args:
work_dir: 工作目录路径
pipeline: Pipeline 实例(未使用,保留接口兼容性)
"""
try:
import traceback
if work_dir:
self.work_dir = work_dir
elif hasattr(self, 'work_dir') and self.work_dir:
pass
else:
self.work_dir = None
# 1. 尝试从 Step8 界面读取训练光谱 CSV 路径
main_window = self.window()
if main_window and hasattr(main_window, 'step8_panel'):
step8_widget = main_window.step8_panel.training_data_widget
step8_output_path = ""
if hasattr(step8_widget, 'get_path'):
step8_output_path = step8_widget.get_path() or ""
if step8_output_path:
if not os.path.isabs(step8_output_path):
step8_output_path = os.path.join(self.work_dir or '', step8_output_path).replace('\\', '/')
existing = self.csv_file.get_path()
if not existing or not existing.strip():
self.csv_file.set_path(step8_output_path)
# 1.2 尝试从 pipeline 读取 Step 8 宽表 indices_path优先级最高
if pipeline and hasattr(pipeline, 'indices_path') and pipeline.indices_path:
step8_indices_path = pipeline.indices_path
if not os.path.isabs(step8_indices_path):
step8_indices_path = os.path.join(self.work_dir or '', step8_indices_path).replace('\\', '/')
current_path = self.csv_file.get_path()
if not current_path or not current_path.strip():
self.csv_file.set_path(step8_indices_path)
print(f"✅ 从pipeline.indices_path回填Step8产出: {step8_indices_path}")
# 1.5 自动探测并回填 Step 8 双轨输出的 Traditional_Indices 目录
if self.work_dir:
trad_indices_dir = os.path.join(
self.work_dir, "11_12_13_predictions", "Traditional_Indices"
)
if os.path.isdir(trad_indices_dir):
csv_files = [
f for f in os.listdir(trad_indices_dir)
if f.lower().endswith('.csv')
]
if csv_files:
csv_files.sort()
first_csv = os.path.join(trad_indices_dir, csv_files[0])
existing = self.csv_file.get_path()
if not existing or not existing.strip():
self.csv_file.set_path(first_csv)
self.refresh_csv_columns()
print(f"✅ 自动探测到 Traditional_Indices 目录加载首个CSV: {csv_files[0]}")
# 2. 自动填充输出目录9_Custom_Regression_Modeling
if self.work_dir:
output_dir = os.path.join(self.work_dir, "9_Custom_Regression_Modeling")
os.makedirs(output_dir, exist_ok=True)
existing_out = self.output_dir.text().strip()
if not existing_out:
self.output_dir.setText(output_dir)
except Exception as e:
import traceback
print(f"{self.__class__.__name__}】自动填充失败,跳过: {e}")
traceback.print_exc()
def run_step(self):
"""独立运行步骤9"""
csv_path = self.csv_file.get_path()
if not csv_path:
QMessageBox.warning(self, "输入验证失败", "请选择输入CSV文件")
return
if not os.path.exists(csv_path):
QMessageBox.warning(self, "输入验证失败", "输入CSV文件不存在")
return
selected_x_columns = [
col for col, checkbox in self.x_column_checkboxes.items()
if checkbox.isChecked()
]
if not selected_x_columns:
QMessageBox.warning(self, "输入验证失败", "请至少选择一个自变量列")
return
selected_y_columns = [
col for col, checkbox in self.y_column_checkboxes.items()
if checkbox.isChecked()
]
if not selected_y_columns:
QMessageBox.warning(self, "输入验证失败", "请至少选择一个因变量列")
return
selected_methods = [
method for method, checkbox in self.method_checkboxes.items()
if checkbox.isChecked()
]
if not selected_methods:
QMessageBox.warning(self, "输入验证失败", "请至少选择一种回归方法")
return
config = self.get_config()
parent = self.parent()
while parent and not hasattr(parent, 'run_single_step'):
parent = parent.parent()
if parent and hasattr(parent, 'run_single_step'):
parent.run_single_step('step9', {'step9': config})
else:
QMessageBox.critical(self, "错误", "无法找到父级GUI对象")

View File

@ -117,18 +117,17 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.panels.step1_panel import Step1Panel
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.step6_panel import Step6Panel # was step8_panel
from src.gui.panels.step7_panel import Step7Panel # was step6_panel
from src.gui.panels.step8_waterindex_panel import Step8WaterIndexPanel # 水色指数反演
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.step14_panel import Step14Panel # was step9_panel
from src.gui.panels.step4_sampling_panel import Step4SamplingPanel # 采样点布设
from src.gui.panels.step5_clean_panel import Step5CleanPanel # 数据清洗
from src.gui.panels.step6_feature_panel import Step6FeaturePanel # 光谱特征
from src.gui.panels.step7_index_panel import Step7IndexPanel # 水质光谱指数
from src.gui.panels.step10_watercolor_panel import Step10WatercolorPanel # 水色指数反演
from src.gui.panels.step8_ml_train_panel import Step8MlTrainPanel # 机器学习建模
from src.gui.panels.step9_ml_predict_panel import Step9MlPredictPanel # 机器学习预测
from src.gui.dialogs import BandConfirmDialog, AISettingsDialog
from src.gui.panels.visualization_panel import VisualizationPanel
from src.gui.panels.report_generation_panel import ReportGenerationPanel
from src.gui.panels.step11_map_panel import Step11MapPanel # 专题图生成
from src.gui.panels.step12_viz_panel import Step12VizPanel # 可视化
from src.gui.panels.step13_report_panel import Step13ReportPanel # 报告生成
# Pipeline 核心异常(用于预检弹窗)
from src.core.pipeline.runner import PipelineHalt
@ -1380,93 +1379,64 @@ class WaterQualityGUI(QMainWindow):
'deglint_goodman': '3_deglint/deglint_goodman.bsq',
'deglint_hedley': '3_deglint/deglint_hedley.bsq',
'deglint_sugar': '3_deglint/deglint_sugar.bsq',
'deglint_interpolated': '3_deglint/interpolated_*.bsq' # * = interpolation_method
'deglint_interpolated': '3_deglint/interpolated_*.bsq'
},
'step4': {
'step5_clean': {
'processed_data': '4_processed_data/processed_data.csv'
},
'step5': {
'step6_feature': {
'training_spectra': '5_training_spectra/training_spectra.csv'
},
'step6': {
'step7_index': {
'water_indices': '6_water_quality_indices/water_quality_indices.csv'
},
'step7': {
'models': '7_Supervised_Model_Training/' # 目录,包含各参数子目录
'step8_ml_train': {
'models': '7_Supervised_Model_Training/'
},
'step8_non_empirical_modeling': {
'regression_models': '8_Regression_Modeling/' # 目录,包含各参数子目录
},
'step9': {
'custom_regression_models': '9_Custom_Regression_Modeling/' # 目录
},
'step10': {
'step4_sampling': {
'sampling_points': '10_sampling/sampling_spectra.csv'
},
'step11_ml': {
'predictions': '11_12_13_predictions/Machine_Learning_Prediction/' # 目录,包含机器学习预测结果
'step9_ml_predict': {
'predictions': '11_12_13_predictions/Machine_Learning_Prediction/'
},
'step11': {
'regression_predictions': '11_12_13_predictions/Non_Empirical_Prediction/' # 目录,包含非经验模型预测结果
},
'step12': {
'custom_predictions': '11_12_13_predictions/Custom_Regression_Prediction/' # 目录,包含自定义回归预测结果
},
'step14': {
'distribution_maps': '14_visualization/' # 目录,包含专题图
'step11_map': {
'distribution_maps': '14_visualization/'
}
}
# 定义步骤间的依赖关系:{当前步骤: {输入字段: (依赖步骤, 输出类型, 面板属性名)}}
self.step_dependencies = {
'step2': {
'img_path': ('step1', 'reference_img', 'img_file'), # 步骤2需要参考影像
'water_mask_path': ('step1', 'water_mask', 'water_mask_file') # 步骤2可选水域掩膜
'img_path': ('step1', 'reference_img', 'img_file'),
'water_mask_path': ('step1', 'water_mask', 'water_mask_file')
},
'step3': {
'img_path': ('step1', 'reference_img', 'img_file'), # 步骤3需要参考影像
'water_mask': ('step1', 'water_mask', 'water_mask_file'), # 步骤3需要水域掩膜
'img_path': ('step1', 'reference_img', 'img_file'),
'water_mask': ('step1', 'water_mask', 'water_mask_file'),
},
'step4': {
# 步骤4主要处理CSV文件一般不依赖前面步骤的输出
'step6_feature': {
'deglint_img_path': ('step3', 'deglint_image', 'deglint_img_file'),
'csv_path': ('step5_clean', 'processed_data', 'csv_file'),
'boundary_mask_path': ('step1', 'water_mask', 'boundary_mask_file'),
'glint_mask_path': ('step2', 'glint_mask', 'glint_mask_file')
},
'step5': {
'deglint_img_path': ('step3', 'deglint_image', 'deglint_img_file'), # 步骤5需要去耀斑影像
'csv_path': ('step4', 'processed_data', 'csv_file'), # 步骤5需要处理后的CSV
'boundary_mask_path': ('step1', 'water_mask', 'boundary_mask_file'), # 步骤5可选水体掩膜
'glint_mask_path': ('step2', 'glint_mask', 'glint_mask_file') # 步骤5可选耀斑掩膜
'step7_index': {
'training_csv_path': ('step6_feature', 'training_spectra', 'output_file')
},
'step6': {
'training_csv_path': ('step5', 'training_spectra', 'output_file') # 步骤6需要步骤5输出的训练光谱
'step8_ml_train': {
'training_csv_path': ('step7_index', 'water_indices', 'csv_file')
},
'step7': {
'csv_path': ('step5', 'training_spectra', 'csv_file') # 步骤7需要训练光谱数据
'step4_sampling': {
'deglint_img_path': ('step3', 'deglint_image', 'deglint_img_file'),
'water_mask_path': ('step1', 'water_mask', 'water_mask_file'),
'glint_mask_path': ('step2', 'glint_mask', 'glint_mask_file')
},
'step8_non_empirical_modeling': {
'csv_path': ('step5', 'training_spectra', 'csv_file') # 步骤8非经验建模需要训练光谱数据
'step9_ml_predict': {
'sampling_csv_path': ('step4_sampling', 'sampling_points', 'sampling_csv_file'),
'models_dir': ('step8_ml_train', 'models', 'models_dir_file')
},
'step9': {
'csv_path': ('step5', 'training_spectra', 'csv_file') # 步骤9需要训练光谱数据
},
'step10': {
'deglint_img_path': ('step3', 'deglint_image', 'deglint_img_file'), # 步骤10需要去耀斑影像
'water_mask_path': ('step1', 'water_mask', 'water_mask_file'), # 步骤10需要水域掩膜
'glint_mask_path': ('step2', 'glint_mask', 'glint_mask_file') # 步骤10可选耀斑掩膜
},
'step11_ml': {
'sampling_csv_path': ('step10', 'sampling_points', 'sampling_csv_file'), # 步骤11ML需要采样点
'models_dir': ('step7', 'models', 'models_dir_file') # 步骤11ML需要训练好的模型
},
'step11': {
'sampling_csv_path': ('step10', 'sampling_points', 'sampling_csv_file'), # 步骤11需要采样点
'models_dir': ('step8_non_empirical_modeling', 'regression_models', 'models_dir') # 步骤11需要回归模型
},
'step12': {
'sampling_csv_path': ('step10', 'sampling_points', 'sampling_csv_file'), # 步骤12需要采样点
'models_dir': ('step9', 'custom_regression_models', 'models_dir') # 步骤12需要自定义回归模型
},
'step14': {
'prediction_csv_path': ('step11_ml', 'predictions', 'prediction_csv_file') # 步骤14需要预测结果CSV
'step11_map': {
'prediction_csv_path': ('step9_ml_predict', 'predictions', 'prediction_csv_file')
}
}
@ -1843,26 +1813,23 @@ class WaterQualityGUI(QMainWindow):
"阶段一:影像预处理": [
("step1", "1. 水域掩膜生成"),
("step2", "2. 耀斑区域识别"),
("step3", "3. 耀斑去除与修复"),
("step3", "3. 耀斑去除与修复")
],
"阶段二:样本数据准备": [
("step4", "4. 数据标准化处理"),
("step5", "5. 光谱特征提取"),
("step6", "6. 水质参数指数计算"),
("step4_sampling", "4. 采样点布设"),
("step5_clean", "5. 数据清洗"),
("step6_feature", "6. 光谱特征提取"),
("step7_index", "7. 水质指数计算")
],
"阶段三:模型构建与训练": [
("step7", "7. 机器学习模型训练"),
("step8_non_empirical_modeling", "8. 回归模型训练"),
("step9", "9. 自定义回归模型训练"),
("step8_ml_train", "8. 机器学习")
],
"阶段四:预测与成果输出": [
("step10", "10. 采样点布设"),
("step11_ml", "11. 机器学习预测"),
("step11", "12. 回归预测"),
("step12", "13. 自定义回归预测"),
("step14", "14. 专题图生成"),
("step9_viz", "15. 可视化分析"),
("step_report", "16. 分析报告生成"),
("step9_ml_predict", "9. 机器学习预测"),
("step10_watercolor", "10. 水色指数反演"),
("step11_map", "11. 专题图生成"),
("step12_viz", "12. 可视化展示"),
("step13_report", "13. 分析报告生成")
]
}
@ -1882,11 +1849,7 @@ class WaterQualityGUI(QMainWindow):
self.step_list.addItem(stage_item)
# 添加该阶段的所有步骤
HIDDEN_STEP_IDS = {"step8_non_empirical_modeling", "step9", "step11", "step12"}
for step_id, step_display in steps:
if step_id in HIDDEN_STEP_IDS:
continue
item = QListWidgetItem(f" └─ {step_display}")
item.setData(Qt.UserRole, step_id)
@ -1956,38 +1919,35 @@ class WaterQualityGUI(QMainWindow):
self.step3_panel = Step3Panel()
self.step_stack.addTab(self.create_scroll_area(self.step3_panel), QIcon(self.get_icon_path("3.png")), "耀斑去除")
self.step4_panel = Step4Panel()
self.step_stack.addTab(self.create_scroll_area(self.step4_panel), QIcon(self.get_icon_path("4.png")), "数据清洗")
self.step4_sampling_panel = Step4SamplingPanel()
self.step_stack.addTab(self.create_scroll_area(self.step4_sampling_panel), QIcon(self.get_icon_path("4.png")), "采样点布设")
self.step5_panel = Step5Panel()
self.step_stack.addTab(self.create_scroll_area(self.step5_panel), QIcon(self.get_icon_path("5.png")), "特征构建")
self.step5_clean_panel = Step5CleanPanel()
self.step_stack.addTab(self.create_scroll_area(self.step5_clean_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.step6_feature_panel = Step6FeaturePanel()
self.step_stack.addTab(self.create_scroll_area(self.step6_feature_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("7.png")), "监督建模")
self.step7_index_panel = Step7IndexPanel()
self.step_stack.addTab(self.create_scroll_area(self.step7_index_panel), QIcon(self.get_icon_path("7.png")), "水质光谱指数计算")
self.step8_waterindex_panel = Step8WaterIndexPanel()
self.step_stack.addTab(self.create_scroll_area(self.step8_waterindex_panel), QIcon(self.get_icon_path("6.png")), "水色指数反演")
self.step8_ml_train_panel = Step8MlTrainPanel()
self.step_stack.addTab(self.create_scroll_area(self.step8_ml_train_panel), QIcon(self.get_icon_path("8.png")), "机器学习建模")
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.step9_ml_predict_panel = Step9MlPredictPanel()
self.step_stack.addTab(self.create_scroll_area(self.step9_ml_predict_panel), QIcon(self.get_icon_path("10.png")), "机器学习预测")
self.step10_panel = Step10Panel()
self.step_stack.addTab(self.create_scroll_area(self.step10_panel), QIcon(self.get_icon_path("7.png")), "采样点布设")
self.step10_watercolor_panel = Step10WatercolorPanel()
self.step_stack.addTab(self.create_scroll_area(self.step10_watercolor_panel), QIcon(self.get_icon_path("10.png")), "水色指数反演")
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_map_panel = Step11MapPanel()
self.step_stack.addTab(self.create_scroll_area(self.step11_map_panel), QIcon(self.get_icon_path("10.png")), "专题图生成")
self.step14_panel = Step14Panel()
self.step_stack.addTab(self.create_scroll_area(self.step14_panel), QIcon(self.get_icon_path("10.png")), "专题图生成")
self.step12_viz_panel = Step12VizPanel()
self.step_stack.addTab(self.create_scroll_area(self.step12_viz_panel), QIcon(self.get_icon_path("9.png")), "可视化")
self.viz_panel = VisualizationPanel()
self.step_stack.addTab(self.create_scroll_area(self.viz_panel), QIcon(self.get_icon_path("9.png")), "可视化")
self.report_panel = ReportGenerationPanel(main_window=self)
self.step_stack.addTab(self.create_scroll_area(self.report_panel), QIcon(self.get_icon_path("10.png")), "报告生成")
self.step13_report_panel = Step13ReportPanel(main_window=self)
self.step_stack.addTab(self.create_scroll_area(self.step13_report_panel), QIcon(self.get_icon_path("10.png")), "报告生成")
# 连接Tab切换信号实现双向同步必须在step_stack创建后
self.step_stack.currentChanged.connect(self.on_tab_changed)
@ -2126,22 +2086,11 @@ class WaterQualityGUI(QMainWindow):
# 根据步骤ID查找对应的tab索引
step_id_to_tab = {
'step1': 0,
'step2': 1,
'step3': 2,
'step4': 3,
'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,
'step_report': 15,
'step1': 0, 'step2': 1, 'step3': 2, 'step4_sampling': 3,
'step5_clean': 4, 'step6_feature': 5, 'step7_index': 6,
'step8_ml_train': 7, 'step9_ml_predict': 8,
'step10_watercolor': 9, 'step11_map': 10,
'step12_viz': 11, 'step13_report': 12,
}
if item_data in step_id_to_tab:
@ -2155,24 +2104,13 @@ class WaterQualityGUI(QMainWindow):
if index < 0:
return
# Tab索引到步骤ID的反向映射
# Tab索引到步骤ID的反向映射13个Tabindex 0-12
tab_to_step_id = {
0: 'step1',
1: 'step2',
2: 'step3',
3: 'step4',
4: 'step5',
5: 'step6',
6: 'step7',
7: 'step8_non_empirical_modeling',
8: 'step9',
9: 'step10',
10: 'step11_ml',
11: 'step11',
12: 'step12',
13: 'step14',
14: 'step9_viz',
15: 'step_report',
0: 'step1', 1: 'step2', 2: 'step3', 3: 'step4_sampling',
4: 'step5_clean', 5: 'step6_feature', 6: 'step7_index',
7: 'step8_ml_train', 8: 'step9_ml_predict',
9: 'step10_watercolor', 10: 'step11_map',
11: 'step12_viz', 12: 'step13_report',
}
if index not in tab_to_step_id:
@ -2191,53 +2129,27 @@ class WaterQualityGUI(QMainWindow):
self.step_list.setCurrentRow(row)
break
# Step2 切换时自动填充数据流转路径
if index == 1:
self.step2_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# 面板自动填充:统一 mapping 覆盖 index 0-12
mapping = {
0: (self.step1_panel, "Step1"),
1: (self.step2_panel, "Step2"),
2: (self.step3_panel, "Step3"),
3: (self.step4_sampling_panel, "Step4"),
4: (self.step5_clean_panel, "Step5"),
5: (self.step6_feature_panel, "Step6"),
6: (self.step7_index_panel, "Step7"),
7: (self.step8_ml_train_panel, "Step8"),
8: (self.step9_ml_predict_panel, "Step9"),
9: (self.step10_watercolor_panel, "Step10"), # 水色指数反演
10: (self.step11_map_panel, "Step11"), # 专题图生成
11: (self.step12_viz_panel, "Step12"),
12: (self.step13_report_panel, "Step13")
}
# Step3 切换时自动填充数据流转路径
elif index == 2:
self.step3_panel.update_from_config(work_dir=self.work_dir)
# Step4 切换时自动填充输出路径
elif index == 3:
self.step4_panel.update_from_config(work_dir=self.work_dir)
# Step5 切换时自动填充数据流转路径
elif index == 4:
self.step5_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step6水质光谱指数切换时自动填充输出路径
elif index == 5:
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 水色指数反演切换时自动填充光谱数据和输出路径
elif index == 7:
self.step8_waterindex_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step9 浓度反演切换时自动填充 QAA 结果和输出路径
elif index == 8:
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)
# Step11机器学习预测切换时自动填充采样光谱和模型目录
elif index == 10:
self.step11_ml_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# Step14专题图生成切换时自动填充预测结果目录
elif index == 11:
self.step14_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# 可视化分析面板切换时自动推断图像目录并加载目录树
elif index == 12:
self.viz_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
if index in mapping:
panel, _ = mapping[index]
if hasattr(panel, 'update_from_config'):
panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
def apply_stylesheet(self):
"""应用样式表 - 应用现代化设计风格"""
@ -2276,24 +2188,22 @@ class WaterQualityGUI(QMainWindow):
self.step2_panel.set_config(config['step2'])
if 'step3' in config:
self.step3_panel.set_config(config['step3'])
if 'step4' in config:
self.step4_panel.set_config(config['step4'])
if 'step5' in config:
self.step5_panel.set_config(config['step5'])
if 'step6' in config:
self.step6_panel.set_config(config['step6'])
if 'step7' in config:
self.step7_panel.set_config(config['step7'])
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 'step14' in config:
self.step14_panel.set_config(config['step14'])
if 'visualization' in config:
self.viz_panel.set_config(config['visualization'])
if 'report_generation' in config:
self.report_panel.set_config(config['report_generation'])
if 'step4_sampling' in config:
self.step4_sampling_panel.set_config(config['step4_sampling'])
if 'step5_clean' in config:
self.step5_clean_panel.set_config(config['step5_clean'])
if 'step6_feature' in config:
self.step6_feature_panel.set_config(config['step6_feature'])
if 'step7_index' in config:
self.step7_index_panel.set_config(config['step7_index'])
if 'step9_ml_predict' in config:
self.step9_ml_predict_panel.set_config(config['step9_ml_predict'])
if 'step11_map' in config:
self.step11_map_panel.set_config(config['step11_map'])
if 'step12_viz' in config:
self.step12_viz_panel.set_config(config['step12_viz'])
if 'step13_report' in config:
self.step13_report_panel.set_config(config['step13_report'])
self.config_file = file_path
self.log_message(f"已加载配置: {file_path}", "info")
@ -2330,15 +2240,15 @@ class WaterQualityGUI(QMainWindow):
'step1': self.step1_panel.get_config(),
'step2': self.step2_panel.get_config(),
'step3': self.step3_panel.get_config(),
'step4': self.step4_panel.get_config(),
'step5': self.step5_panel.get_config(),
'step6': self.step6_panel.get_config(),
'step7': self.step7_panel.get_config(),
'step10': self.step10_panel.get_config(),
'step11_ml': self.step11_ml_panel.get_config(),
'step14': self.step14_panel.get_config(),
'visualization': self.viz_panel.get_config(),
'report_generation': self.report_panel.get_config(),
'step4_sampling': self.step4_sampling_panel.get_config(),
'step5_clean': self.step5_clean_panel.get_config(),
'step6_feature': self.step6_feature_panel.get_config(),
'step7_index': self.step7_index_panel.get_config(),
'step8_ml_train': self.step8_ml_train_panel.get_config(),
'step9_ml_predict': self.step9_ml_predict_panel.get_config(),
'step11_map': self.step11_map_panel.get_config(),
'step12_viz': self.step12_viz_panel.get_config(),
'step13_report': self.step13_report_panel.get_config(),
}
return config
@ -2385,13 +2295,15 @@ class WaterQualityGUI(QMainWindow):
'step1': self.step1_panel,
'step2': self.step2_panel,
'step3': self.step3_panel,
'step4': self.step4_panel,
'step5': self.step5_panel,
'step6': self.step6_panel,
'step7': self.step7_panel,
'step10': self.step10_panel,
'step11_ml': self.step11_ml_panel,
'step14': self.step14_panel,
'step4_sampling': self.step4_sampling_panel,
'step5_clean': self.step5_clean_panel,
'step6_feature': self.step6_feature_panel,
'step7_index': self.step7_index_panel,
'step8_ml_train': self.step8_ml_train_panel,
'step9_ml_predict': self.step9_ml_predict_panel,
'step11_map': self.step11_map_panel,
'step12_viz': self.step12_viz_panel,
'step13_report': self.step13_report_panel,
}
return panel_map.get(step_id)
@ -2483,17 +2395,17 @@ class WaterQualityGUI(QMainWindow):
'1_water_mask': 'step1',
'2_glint': 'step2',
'3_deglint': 'step3',
'4_processed_data': 'step4',
'5_training_spectra': 'step5',
'6_water_quality_indices': 'step6',
'7_Supervised_Model_Training': 'step7',
'8_Regression_Modeling': 'step8_non_empirical_modeling',
'9_Custom_Regression_Modeling': 'step9',
'10_sampling': 'step10',
'11_12_13_predictions/Machine_Learning_Prediction': 'step11_ml',
'11_12_13_predictions/Non_Empirical_Prediction': 'step11',
'11_12_13_predictions/Custom_Regression_Prediction': 'step12',
'14_visualization': 'step14'
'4_processed_data': 'step4_sampling',
'5_training_spectra': 'step5_clean',
'6_water_quality_indices': 'step6_feature',
'7_Supervised_Model_Training': 'step7_index',
'8_Regression_Modeling': 'step8_ml_train',
'9_Custom_Regression_Modeling': 'step9_ml_predict',
'11_12_13_predictions/Machine_Learning_Prediction': 'step9_ml_predict',
'11_12_13_predictions/Non_Empirical_Prediction': 'step11_map',
'11_12_13_predictions/Custom_Regression_Prediction': 'step12_viz',
'14_visualization': 'step13_report',
'10_geotiff_batch_rendering': 'step11_map'
}
for subdir, step_ids in subdirs.items():
@ -2535,15 +2447,15 @@ class WaterQualityGUI(QMainWindow):
discovered_outputs[step_id]['glint_mask'] = str(file_path)
elif 'deglint' in file_name and step_id == 'step3':
discovered_outputs[step_id]['deglint_image'] = str(file_path)
elif 'processed_data' in file_name and step_id == 'step4':
elif 'processed_data' in file_name and step_id == 'step4_sampling':
discovered_outputs[step_id]['processed_data'] = str(file_path)
elif 'training_spectra' in file_name and step_id == 'step5':
elif 'training_spectra' in file_name and step_id == 'step5_clean':
discovered_outputs[step_id]['training_spectra'] = str(file_path)
elif 'water_quality_indices' in file_name and step_id == 'step6':
elif 'water_quality_indices' in file_name and step_id == 'step6_feature':
discovered_outputs[step_id]['water_indices'] = str(file_path)
elif 'sampling_spectra' in file_name and step_id == 'step10':
elif 'sampling_spectra' in file_name and step_id == 'step4_sampling':
discovered_outputs[step_id]['sampling_points'] = str(file_path)
elif file_name.endswith('.csv') and step_id in ['step11_ml', 'step11', 'step12']:
elif file_name.endswith('.csv') and step_id in ['step9_ml_predict', 'step11_map', 'step12_viz']:
discovered_outputs[step_id]['predictions'] = str(file_path)
# 更新内部记录
@ -2566,8 +2478,8 @@ class WaterQualityGUI(QMainWindow):
# 首先扫描工作目录发现已有的输出文件
self.scan_work_directory_for_files(work_path)
step_order = ['step2', 'step3', 'step4', 'step5', 'step6', 'step7', 'step8_non_empirical_modeling', 'step9',
'step10', 'step11_ml', 'step11', 'step12', 'step14']
step_order = ['step2', 'step3', 'step4_sampling', 'step5_clean', 'step6_feature', 'step7_index',
'step8_ml_train', 'step9_ml_predict', 'step11_map', 'step12_viz', 'step13_report']
filled_count = 0
for step_id in step_order:
@ -2588,12 +2500,15 @@ class WaterQualityGUI(QMainWindow):
panels_with_dependencies = [
('step2', self.step2_panel),
('step3', self.step3_panel),
('step5', self.step5_panel),
('step6', self.step6_panel),
('step7', self.step7_panel),
('step10', self.step10_panel),
('step11_ml', self.step11_ml_panel),
('step14', self.step14_panel)
('step4_sampling', self.step4_sampling_panel),
('step5_clean', self.step5_clean_panel),
('step6_feature', self.step6_feature_panel),
('step7_index', self.step7_index_panel),
('step8_ml_train', self.step8_ml_train_panel),
('step9_ml_predict', self.step9_ml_predict_panel),
('step11_map', self.step11_map_panel),
('step12_viz', self.step12_viz_panel),
('step13_report', self.step13_report_panel),
]
for step_id, panel in panels_with_dependencies:
@ -2663,10 +2578,10 @@ class WaterQualityGUI(QMainWindow):
self.statusBar().showMessage(f"工作目录: {dir_path}")
# 同步到可视化面板
if hasattr(self, 'viz_panel'):
self.viz_panel.set_work_dir(dir_path)
if hasattr(self, 'report_panel'):
self.report_panel.set_work_dir(dir_path)
if hasattr(self, 'step12_viz_panel'):
self.step12_viz_panel.set_work_dir(dir_path)
if hasattr(self, 'step13_report_panel'):
self.step13_report_panel.set_work_dir(dir_path)
def open_work_directory(self):
"""打开工作目录"""
@ -2985,11 +2900,11 @@ class WaterQualityGUI(QMainWindow):
# 准备实际运行配置(排除未启用的步骤)
worker_config = copy.deepcopy(config)
step6_cfg = worker_config.get('step6')
step6_cfg = worker_config.get('step6_feature')
if step6_cfg:
enabled = step6_cfg.pop('enabled', True)
if not enabled:
worker_config.pop('step6', None)
worker_config.pop('step6_feature', None)
# 工作线程内创建 Pipeline避免主线程阻塞及 Qt5Agg 子线程绘图卡死
self.worker = WorkerThread(work_dir, worker_config, mode='full', skip_list=skip_list)
@ -3219,19 +3134,18 @@ class WaterQualityGUI(QMainWindow):
def update_ui_for_training_mode(self):
"""根据训练数据模式更新UI状态"""
# 需要禁用的步骤ID对应无训练数据模式下需要禁用的步骤
disabled_step_ids = ['step4', 'step5', 'step6', 'step7', 'step8_non_empirical_modeling', 'step9']
disabled_step_ids = ['step4_sampling', 'step5_clean', 'step6_feature', 'step7_index', 'step9_ml_predict']
# 更新标签页的启用/禁用状态
step_id_to_tab = {
'step1': 0, 'step2': 1, 'step3': 2, 'step4': 3,
'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
step_id_to_tab_training = {
'step1': 0, 'step2': 1, 'step3': 2, 'step4_sampling': 3,
'step5_clean': 4, 'step6_feature': 5, 'step7_index': 6, 'step9_ml_predict': 7,
'step10_watercolor': 9, 'step11_map': 10, 'step12_viz': 11, 'step13_report': 12
}
for step_id in disabled_step_ids:
if step_id in step_id_to_tab:
tab_index = step_id_to_tab[step_id]
if step_id in step_id_to_tab_training:
tab_index = step_id_to_tab_training[step_id]
if tab_index < self.step_stack.count():
self.step_stack.setTabEnabled(tab_index, self.has_training_data)

View File

@ -7,7 +7,7 @@ from typing import Optional, Tuple
from pyproj import CRS, Transformer
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.ticker import FuncFormatter
from matplotlib.ticker import FuncFormatter, MaxNLocator
from matplotlib_scalebar.scalebar import ScaleBar
from scipy.interpolate import griddata
from scipy import ndimage
@ -63,6 +63,72 @@ PARAMS_CMAP = {
}
# ── 水色指数英文名 → 中文标题映射(精确整词匹配)────────────────────
# 优先于动态拼接;当文件名恰好命中完整 key 时使用
INDEX_TITLE_MAP = {
# Chl_a 叶绿素a
"Chl_Conc_NDCI": "叶绿素a浓度估算_NDCI模型",
"Chl_MM12NDCI": "叶绿素a相对指数_Matthews12模型",
"Chl_Conc_Gao": "叶绿素a浓度估算_Gao模型",
"Chl_Conc_QAA": "叶绿素a浓度估算_QAA模型",
# BGA 蓝藻/藻蓝蛋白
"BGA_Go04MCI": "蓝藻相对指数_Gower04模型",
"BGA_PC_Conc_Mishra": "藻蓝蛋白浓度估算_Mishra模型",
"BGA_Conc": "蓝藻浓度估算",
"BGA_Am09KBBI": "蓝藻相对指数_Am09模型",
# Turb 浊度
"Turb_Dox02NIRoverRed": "水体浊度指数_Doxaran02模型",
"Turbidity": "水体浊度",
"Turb_Conc": "浊度估算",
# TSM 悬浮物
"TSM_Conc_Bowling": "总悬浮物浓度估算_Bowling模型",
"TSM_Conc": "总悬浮物浓度估算",
# CDOM 有色溶解有机物
"CDOM_Conc": "有色溶解有机物浓度估算",
# WI 水色指数综合
"WaterIndex": "水色指数",
"NDCI": "归一化叶绿素差值指数_NDCI",
"MCI": "最大Chlorophyll指数_MCI",
}
# ── 关键词 → 中文词根映射(用于动态拼接)────────────────────────────
# 顺序即优先级:长词优先,短词兜底
PART_NAME_MAP = [
# 1. 指数/浓度类(最高优先级,描述输出类型)
("Conc", "浓度估算"),
("_Conc", "浓度估算"),
("Concentration", "浓度"),
("Index", "指数"),
("_Index", "指数"),
# 2. 模型/方法标识(次高优先级)
("NDCI", "NDCI模型"),
("MCI", "MCI模型"),
("FLH", "FLH荧光基线"),
("QAA", "QAA模型"),
("Go04", "Gower04"),
("MM12", "Matthews12"),
("Gao", "Gao模型"),
("Dox02", "Doxaran02"),
("Bowling", "Bowling模型"),
("Mishra", "Mishra模型"),
("Am09", "Am09"),
("KBBI", "KBBI"),
("PC", "藻蓝蛋白"),
# 3. 参数大类(核心水质指标)
("BGA", "蓝藻相对指数"),
("Chl", "叶绿素a"),
("Chl_a", "叶绿素a"),
("Turb", "浊度"),
("TSM", "总悬浮物"),
("CDOM", "有色溶解有机物"),
("DO", "溶解氧"),
("pH", "pH值"),
("NH3", "氨氮"),
("NO3", "硝态氮"),
("TDS", "溶解性总固体"),
]
class ContentMapper:
def __init__(self, input_crs='EPSG:32651', output_crs='EPSG:4326'):
"""
@ -97,6 +163,63 @@ class ContentMapper:
print(f"坐标转换设置: {input_crs} -> {output_crs}")
# ── 内部工具 ─────────────────────────────────────────────────────
@staticmethod
def _get_chinese_title(stem: str) -> str:
"""
根据 GeoTIFF 文件名 stem 返回中文图表标题(绝对唯一)。
匹配策略:
1. 精确整词命中 INDEX_TITLE_MAP
2. 中文分类前缀 + 原始模型后缀(绝对唯一保证)
3. 未匹配任何关键词 → 返回原英文 stem
Parameters
----------
stem : str
GeoTIFF 文件名(不含路径和扩展名)
Returns
-------
str
中文标题;若未匹配则返回英文 stem
"""
# 策略1精确整词匹配优先级最高
if stem in INDEX_TITLE_MAP:
return INDEX_TITLE_MAP[stem]
# 策略2中文分类前缀 + 原始模型后缀(确保绝对唯一)
category = ""
suffix = ""
if stem.startswith("BGA_PC_Conc"):
category = "藻蓝蛋白浓度估算"
suffix = stem.replace("BGA_PC_Conc_", "")
elif stem.startswith("BGA"):
category = "蓝藻相对指数"
suffix = stem.replace("BGA_", "")
elif stem.startswith("Chl_Conc"):
category = "叶绿素a浓度估算"
suffix = stem.replace("Chl_Conc_", "")
elif stem.startswith("Chl"):
category = "叶绿素a相对指数"
suffix = stem.replace("Chl_", "")
elif stem.startswith("Turb_Conc"):
category = "浊度浓度估算"
suffix = stem.replace("Turb_Conc_", "")
elif stem.startswith("Turb"):
category = "浊度相对指数"
suffix = stem.replace("Turb_", "")
elif stem.startswith("TSM_Conc"):
category = "总悬浮物浓度估算"
suffix = stem.replace("TSM_Conc_", "")
else:
# 兜底机制:未知分类直接使用原名
category = "水质参数"
suffix = stem
return f"{category}_{suffix}"
def _extract_param_name(self, csv_file):
"""
从CSV文件名或内容中提取参数名称
@ -2080,12 +2203,15 @@ class ContentMapper:
str
输出图片路径
"""
# ── 输出路径自动派生 ──────────────────────────────────────────
if output_file is None:
# ── 始终从路径提取 stem供后续中文标题和文件派生使用──────────
stem = Path(raster_tif_path).stem
# ── 输出路径自动派生(中文文件名)──────────────────────────────
if output_file is None:
chinese_title = self._get_chinese_title(stem)
out_dir = Path(raster_tif_path).parent / 'visualization'
out_dir.mkdir(parents=True, exist_ok=True)
output_file = str(out_dir / f"{stem}_map.png")
output_file = str(out_dir / f"{chinese_title}_专题图.png")
# ── 读取 GeoTIFF优先 rasterio备选 GDAL──────────────────
tif_path = Path(raster_tif_path)
@ -2214,6 +2340,10 @@ class ContentMapper:
param_name = self._extract_param_name(str(tif_path))
cmap = self._get_colormap(param_name)
# ── 中文标题(文件名汉化 + 绘图标题)──────────────────────────
# 用户显式传入 title 时直接使用;否则用中文映射
chinese_title = self._get_chinese_title(stem) if not title else title
# ── 计算空间范围extent──────────────────────────────────────
# 优先使用 rasterio 原生 bounds保证坐标轴为真实 UTM 米
# GDAL 回退使用 GeoTransform 计算
@ -2251,23 +2381,24 @@ class ContentMapper:
safe_figsize = (safe_w, safe_h)
fig, ax = plt.subplots(figsize=safe_figsize)
# 计算有效值统计(使用 nanpercentile 精准锁定水体内部,排除陆地 NoData 干扰)
# 计算有效值统计(2σ 标准差拉伸,排除长尾异常值干扰)
valid = array[~np.isnan(array)]
if valid.size == 0:
raise ValueError("GeoTIFF 中没有有效数据(全部为 NoData")
vmin = float(np.nanpercentile(array, 2))
vmax = float(np.nanpercentile(array, 98))
data_range = vmax - vmin
mean_val = float(np.nanmean(array))
std_val = float(np.nanstd(array))
vmin = max(float(np.nanmin(array)), mean_val - 2 * std_val)
vmax = min(float(np.nanmax(array)), mean_val + 2 * std_val)
if data_range < 1e-9:
center = float(np.nanmean(array))
if (vmax - vmin) < 1e-9:
center = mean_val
exp = max(abs(center) * 0.01, 1e-9)
vmin = center - exp
vmax = center + exp
print(f"[visualize_raster] 分位数拉伸: P2={vmin:.4f}, P98={vmax:.4f}"
f"有效像元: {valid.size}/{array.size}")
print(f"[visualize_raster] 2σ 拉伸: vmin={vmin:.4f}, vmax={vmax:.4f}"
f"mean={mean_val:.4f}, std={std_val:.4f}有效像元: {valid.size}/{array.size}")
# ── 栅格绘图 ─────────────────────────────────────────────────
# 使用 masked arrayNaN 区域自动不显示
@ -2321,21 +2452,16 @@ class ContentMapper:
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.4, color='gray')
ax.set_axisbelow(True)
# ── 标题 ─────────────────────────────────────────────────────
if title:
ax.set_title(title, fontsize=13, fontweight='bold', pad=10)
elif param_name:
ax.set_title(param_name, fontsize=13, fontweight='bold', pad=10)
# ── 标题(中文)──────────────────────────────────────────────
ax.set_title(chinese_title, fontsize=13, fontweight='bold', pad=10)
# ── 颜色条 ───────────────────────────────────────────────────
# ── 颜色条工业级样式extend 三角 + MaxNLocator 刻度防重叠)─────────
if show_colorbar and im is not None:
try:
cbar = plt.colorbar(im, ax=ax, shrink=0.55, aspect=35, pad=0.02)
cbar = fig.colorbar(im, ax=ax, shrink=0.55, aspect=35, pad=0.02, extend='both')
cbar.set_label('Index Value', fontsize=10)
if data_range > 1e-9:
ticks = np.linspace(vmin, vmax, 6)
cbar.set_ticks(ticks)
cbar.set_ticklabels([f'{t:.3f}' for t in ticks])
cbar.locator = MaxNLocator(nbins=6)
cbar.update_ticks()
print("[visualize_raster] 颜色条添加成功")
except Exception as e:
print(f"[visualize_raster] 颜色条添加失败: {e}")

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step9 面板 - 浓度反演基于 QAA 物理反演的二次反演
Step10 面板 - 浓度反演基于 QAA 物理反演的二次反演
"""
import os
@ -18,8 +18,8 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
class Step9ConcentrationPanel(QWidget):
"""步骤9:浓度反演(物理模型二次反演)"""
class Step10ConcentrationPanel(QWidget):
"""步骤10:浓度反演(物理模型二次反演)"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
@ -27,7 +27,7 @@ class Step9ConcentrationPanel(QWidget):
def init_ui(self):
layout = QVBoxLayout()
title = QLabel("步骤9:浓度反演(物理模型二次反演)")
title = QLabel("步骤10:浓度反演(物理模型二次反演)")
title.setFont(QFont("Arial", 12, QFont.Bold))
layout.addWidget(title)

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step8 面板 - 水色指数反演直接处理去耀斑 BSQ 影像
Step9 面板 - 水色指数反演直接处理去耀斑 BSQ 影像
waterindex.csv 中的公式直接应用于去耀斑高光谱影像
输出各水质参数指数的 GeoTIFF 栅格图像
@ -98,8 +98,8 @@ class WaterIndexWorker(QThread):
self.progress.emit(msg, pct)
class Step8WaterIndexPanel(QWidget):
"""步骤8:水色指数反演(直接处理 BSQ 影像)"""
class Step11WaterColorPanel(QWidget):
"""步骤11:水色指数反演(直接处理 BSQ 影像)"""
def __init__(self, parent=None):
super().__init__(parent)
@ -115,7 +115,7 @@ class Step8WaterIndexPanel(QWidget):
layout = QVBoxLayout()
# ---- 标题 ----
title = QLabel("步骤8:水色指数反演(高光谱影像直接处理)")
title = QLabel("步骤11:水色指数反演(高光谱影像直接处理)")
title.setFont(QFont("Arial", 12, QFont.Bold))
layout.addWidget(title)