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] = { STEP_MAP_OLD_TO_NEW: Dict[str, str] = {
"step5_5": "step8", "step5_5": "step7",
"step6_5": "step8_non_empirical_modeling", "step6_5": "step8_non_empirical_modeling",
"step6_75": "step9", "step6_75": "step9",
"step8_5": "step11", "step8_5": "step11",
"step8_75": "step12", "step7": "step8",
"step7": "step10", "step8": "step7",
"step9": "step14", "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()} 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="耀斑去除", description="耀斑去除",
), ),
StepSpec( StepSpec(
step_id="step4", method_name="step4_process_csv", step_id="step4", method_name="step5_process_csv",
requires=["csv_path"], produces=["processed_csv_path"], requires=["csv_path"], produces=["processed_csv_path"],
required_input_files=["csv_path"], required_input_files=["csv_path"],
output_file="{work_dir}/4_processed_data/processed_data.csv", output_file="{work_dir}/4_processed_data/processed_data.csv",
description="CSV 异常值清洗", description="CSV 异常值清洗",
), ),
StepSpec( 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"], requires=["deglint_img_path", "processed_csv_path", "csv_path", "boundary_path", "glint_mask_path"],
produces=["training_csv_path"], produces=["training_csv_path"],
parameter_map={ parameter_map={
@ -115,14 +115,14 @@ PIPELINE_STEPS: List[StepSpec] = [
description="实测样本点光谱提取", description="实测样本点光谱提取",
), ),
StepSpec( 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"], requires=["training_csv_path"], produces=["indices_path", "trad_indices_dir"],
required_input_files=["training_csv_path"], required_input_files=["training_csv_path"],
output_file="{work_dir}/6_water_quality_indices/training_spectra_indices.csv", output_file="{work_dir}/6_water_quality_indices/training_spectra_indices.csv",
description="水质光谱指数计算双轨输出A轨宽表 + B轨单文件", description="水质参数指数计算双轨输出A轨宽表 + B轨单文件",
), ),
StepSpec( StepSpec(
step_id="step7", method_name="step7_ml_modeling", step_id="step8", method_name="step8_train_ml",
requires=["training_csv_path"], produces=["models_dir"], requires=["training_csv_path"], produces=["models_dir"],
required_input_files=["training_csv_path"], required_input_files=["training_csv_path"],
output_file="{work_dir}/7_Supervised_Model_Training/best_models.pkl", output_file="{work_dir}/7_Supervised_Model_Training/best_models.pkl",
@ -138,22 +138,21 @@ PIPELINE_STEPS: List[StepSpec] = [
description="非经验统计回归", description="非经验统计回归",
), ),
StepSpec( StepSpec(
step_id="step9", method_name="step9_custom_regression", step_id="step9", method_name="step9_watercolor_inversion",
requires=["indices_path"], produces=["models_dir"], requires=["deglint_img_path", "water_mask_path"], produces=["watercolor_index_dir"],
parameter_map={"indices_path": "csv_path"}, required_input_files=["deglint_img_path"],
required_input_files=["indices_path"], output_file="{work_dir}/9_WaterColor_Index_Images",
output_file="{work_dir}/9_Custom_Regression_Modeling/custom_regression_models.pkl", description="水色指数反演BSQ 影像直接处理)",
description="自定义回归分析",
), ),
StepSpec( 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"], requires=["deglint_img_path", "water_mask_path"], produces=["sampling_csv_path"],
required_input_files=["deglint_img_path", "water_mask_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="整景密集采样点生成 + 光谱提取", description="整景密集采样点生成 + 光谱提取",
), ),
StepSpec( 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"], requires=["sampling_csv_path", "models_dir"], produces=["prediction_csv_path"],
required_input_files=["sampling_csv_path", "models_dir"], required_input_files=["sampling_csv_path", "models_dir"],
output_file="{work_dir}/11_12_13_predictions/prediction_results.csv", output_file="{work_dir}/11_12_13_predictions/prediction_results.csv",
@ -168,16 +167,7 @@ PIPELINE_STEPS: List[StepSpec] = [
description="非经验模型预测", description="非经验模型预测",
), ),
StepSpec( StepSpec(
step_id="step12", method_name="step12_custom_regression_prediction", step_id="step14", method_name="step10_map",
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",
requires=["prediction_csv_path", "boundary_shp_path"], requires=["prediction_csv_path", "boundary_shp_path"],
produces=["distribution_map_path"], produces=["distribution_map_path"],
required_input_files=["prediction_csv_path", "boundary_shp_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 import time

View File

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

View File

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

View File

@ -272,10 +272,10 @@ class WorkerThread(QThread):
ctx = PipelineContext( ctx = PipelineContext(
img_path=self.config.get('step1', {}).get('img_path'), img_path=self.config.get('step1', {}).get('img_path'),
water_mask_path=self.config.get('step1', {}).get('mask_path'), water_mask_path=self.config.get('step1', {}).get('mask_path'),
csv_path=self.config.get('step4', {}).get('csv_path'), csv_path=self.config.get('step4_sampling', {}).get('csv_path'),
boundary_path=self.config.get('step5', {}).get('boundary_path'), boundary_path=self.config.get('step5_clean', {}).get('boundary_path'),
boundary_shp_path=self.config.get('step14', {}).get('boundary_shp_path'), boundary_shp_path=self.config.get('step11_map', {}).get('boundary_shp_path'),
formula_csv_path=self.config.get('step12', {}).get('formula_csv_path'), formula_csv_path=self.config.get('step8_non_empirical_modeling', {}).get('formula_csv_path'),
work_dir=self.work_dir, work_dir=self.work_dir,
user_config=self.config user_config=self.config
) )
@ -323,19 +323,16 @@ class WorkerThread(QThread):
'step1': 'step1_generate_water_mask', 'step1': 'step1_generate_water_mask',
'step2': 'step2_find_glint_area', 'step2': 'step2_find_glint_area',
'step3': 'step3_remove_glint', 'step3': 'step3_remove_glint',
'step4': 'step4_process_csv', 'step4_sampling': 'step4_sampling',
'step5': 'step5_extract_training_spectra', 'step5_clean': 'step5_process_csv',
'step6': 'step6_water_quality_indices', 'step6_feature': 'step6_extract_spectra',
'step7': 'step7_ml_modeling', 'step7_index': 'step7_calc_indices',
'step8_ml_train': 'step8_train_ml',
'step8_non_empirical_modeling': 'step8_non_empirical_modeling', 'step8_non_empirical_modeling': 'step8_non_empirical_modeling',
'step8_qaa': 'step8_qaa_inversion', 'step8_qaa': 'step8_qaa_inversion',
'step9_concentration': 'step9_concentration_inversion', 'step9_ml_predict': 'step9_predict_ml',
'step9': 'step9_custom_regression', 'step10_watercolor': 'step9_watercolor_inversion',
'step10': 'step10_sampling', 'step11_map': 'step10_map',
'step11_ml': 'step11_ml_prediction',
'step11': 'step11_non_empirical_prediction',
'step12': 'step12_custom_regression_prediction',
'step14': 'step14_distribution_map'
} }
if step_name not in step_method_map: if step_name not in step_method_map:
@ -351,12 +348,6 @@ class WorkerThread(QThread):
result = method(**config) result = method(**config)
return result 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'] 传入) # 透传面板顶层传入的外部预训练模型GUI step11_prediction_panel 通过 config['_external_model'] 传入)
# 非空才覆盖(遵循 feedback_never_overwrite_with_empty 原则) # 非空才覆盖(遵循 feedback_never_overwrite_with_empty 原则)
for key in ('_external_model', '_external_model_path', for key in ('_external_model', '_external_model_path',
@ -371,17 +362,9 @@ class WorkerThread(QThread):
step_config['skip_dependency_check'] = True step_config['skip_dependency_check'] = True
if step_name == 'step14': if step_name in ['step2', 'step3', 'step4_sampling', 'step5_clean', 'step7_index', 'step9_ml_predict']:
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']:
step_config.pop('output_path', None) 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) method = getattr(self.pipeline, method_name)
result = method(**step_config) result = method(**step_config)
@ -440,100 +423,76 @@ class WorkerThread(QThread):
" → 请确认「耀斑去除」已成功运行,或重新配置路径。" " → 请确认「耀斑去除」已成功运行,或重新配置路径。"
) )
# ── 步骤4实测水质数据 CSV ── # ── 步骤4_sampling:实测水质数据 CSV ──
step4_cfg = config.get('step4', {}) step4_cfg = config.get('step4_sampling', {})
csv_path = step4_cfg.get('csv_path') csv_path = step4_cfg.get('csv_path')
if csv_path and not os.path.isfile(csv_path): if csv_path and not os.path.isfile(csv_path):
errors.append( errors.append(
f"步骤 4实测水质数据文件不存在\n {csv_path}\n" f"步骤 4_sampling:实测水质数据文件不存在:\n {csv_path}\n"
" → 请检查 CSV 路径是否正确,或重新上传数据文件。" " → 请检查 CSV 路径是否正确,或重新上传数据文件。"
) )
# ── 步骤5采样点平均光谱提取 ── # ── 步骤5_clean:采样点平均光谱提取 ──
step5_cfg = config.get('step5', {}) step5_cfg = config.get('step5_clean', {})
step5_csv = step5_cfg.get('csv_path') step5_csv = step5_cfg.get('csv_path')
boundary_path = step5_cfg.get('boundary_path') boundary_path = step5_cfg.get('boundary_path')
if step5_csv and not os.path.isfile(step5_csv): if step5_csv and not os.path.isfile(step5_csv):
errors.append( errors.append(
f"步骤 5实测水质数据文件不存在\n {step5_csv}\n" f"步骤 5_clean:实测水质数据文件不存在:\n {step5_csv}\n"
" → 请检查「流程步骤-阶段五」中的 CSV 路径。" " → 请检查「流程步骤-阶段五」中的 CSV 路径。"
) )
if boundary_path and not os.path.isfile(boundary_path): if boundary_path and not os.path.isfile(boundary_path):
errors.append( errors.append(
f"步骤 5边界矢量文件不存在\n {boundary_path}\n" f"步骤 5_clean:边界矢量文件不存在:\n {boundary_path}\n"
" → 请确认「流程步骤-阶段五」中已填写有效的边界 shp 路径。" " → 请确认「流程步骤-阶段五」中已填写有效的边界 shp 路径。"
) )
# ── 步骤6水质光谱指数训练光谱 CSV ── # ── 步骤6_feature(水质光谱指数):训练光谱 CSV ──
step6_cfg = config.get('step6', {}) step6_cfg = config.get('step6_feature', {})
training_csv = step6_cfg.get('training_csv_path') training_csv = step6_cfg.get('training_csv_path')
if training_csv and not os.path.isfile(training_csv): if training_csv and not os.path.isfile(training_csv):
errors.append( errors.append(
f"步骤 6水质光谱指数训练光谱文件不存在\n {training_csv}\n" f"步骤 6_feature(水质光谱指数):训练光谱文件不存在:\n {training_csv}\n"
" → 请确认步骤 5 已成功运行并生成了训练光谱。" " → 请确认步骤 5_clean 已成功运行并生成了训练光谱。"
) )
# ── 步骤7ML 建模) ── # ── 步骤8_ml_trainML 建模) ──
step7_cfg = config.get('step7', {}) step7_cfg = config.get('step8_ml_train', {})
step7_csv = step7_cfg.get('training_csv_path') step7_csv = step7_cfg.get('training_csv_path')
if step7_csv and not os.path.isfile(step7_csv): if step7_csv and not os.path.isfile(step7_csv):
errors.append( errors.append(
f"步骤 7ML 建模):训练光谱文件不存在:\n {step7_csv}\n" f"步骤 8_ml_trainML 建模):训练光谱文件不存在:\n {step7_csv}\n"
" → 请确认步骤 5 已成功运行并生成了训练光谱。" " → 请确认步骤 5_clean 已成功运行并生成了训练光谱。"
) )
# ── 步骤11 ML 预测:密集采样点 CSV + 模型目录 ── # ── 步骤9_ml_predict:密集采样点 CSV + 模型目录 ──
step11_ml_cfg = config.get('step11_ml', {}) step9_cfg = config.get('step9_ml_predict', {})
ml_csv = step11_ml_cfg.get('sampling_csv_path') ml_csv = step9_cfg.get('sampling_csv_path')
models_dir = step11_ml_cfg.get('models_dir') models_dir = step9_cfg.get('models_dir')
if ml_csv and not os.path.isfile(ml_csv): if ml_csv and not os.path.isfile(ml_csv):
errors.append( 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): if models_dir and not os.path.isdir(models_dir):
errors.append( errors.append(
f"步骤 11 ML 预测:模型目录不存在:\n {models_dir}\n" f"步骤 9_ml_predict:模型目录不存在:\n {models_dir}\n"
" → 请确认「流程步骤-阶段六(机器学习建模)」已成功运行。" " → 请确认「流程步骤-阶段六(机器学习建模)」已成功运行。"
) )
# ── 步骤11 回归预测:模型目录 ── # ── 步骤11_map 专题图:预测结果 CSV + 边界 shp ──
step11_cfg = config.get('step11', {}) step11_cfg = config.get('step11_map', {})
step11_csv = step11_cfg.get('sampling_csv_path') pred_csv = step11_cfg.get('prediction_csv_path')
step11_dir = step11_cfg.get('models_dir') boundary_shp = step11_cfg.get('boundary_shp_path')
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')
if pred_csv and not os.path.isfile(pred_csv): if pred_csv and not os.path.isfile(pred_csv):
errors.append( 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): if boundary_shp and not os.path.isfile(boundary_shp):
errors.append( errors.append(
f"步骤 14(专题图):边界 shp 文件不存在:\n {boundary_shp}\n" f"步骤 11_map(专题图):边界 shp 文件不存在:\n {boundary_shp}\n"
" → 请确认「流程步骤-阶段十」中已填写有效的边界矢量文件路径。" " → 请确认「流程步骤-阶段十」中已填写有效的边界矢量文件路径。"
) )
# ── 汇总报错:任一缺失立即抛出 PipelineHalt ── # ── 汇总报错:任一缺失立即抛出 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)}") QMessageBox.critical(self, "错误", f"保存失败:\n{str(e)}")
class VisualizationPanel(QWidget): class Step12VizPanel(QWidget):
"""可视化分析面板 - 重构版:左侧目录树 + 右侧图像查看器""" """步骤12可视化展示"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.work_dir = None self.work_dir = None

View File

@ -225,6 +225,6 @@ class Step12Panel(QWidget):
parent = parent.parent() parent = parent.parent()
if parent and hasattr(parent, 'run_single_step'): 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: else:
QMessageBox.critical(self, "错误", "无法找到父级GUI对象") QMessageBox.critical(self, "错误", "无法找到父级GUI对象")

View File

@ -79,8 +79,8 @@ class ReportGenerateThread(QThread):
self.failed.emit(f"{e}\n{traceback.format_exc()}") self.failed.emit(f"{e}\n{traceback.format_exc()}")
class ReportGenerationPanel(QWidget): class Step13ReportPanel(QWidget):
"""Word 报告生成面板。AI 配置统一由 AISettingsDialog 管理,本面板不持有配置状态。""" """步骤13分析报告生成。AI 配置统一由 AISettingsDialog 管理,本面板不持有配置状态。"""
def __init__(self, main_window=None, parent=None): def __init__(self, main_window=None, parent=None):
super().__init__(parent) 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") kw["output_image_path"] = str(Path(self.output_dir_optional) / f"{stem}_distribution.png")
else: else:
kw["output_image_path"] = None kw["output_image_path"] = None
pipeline.step14_distribution_map(**kw) pipeline.step10_map(**kw)
self.finished_ok.emit(n) self.finished_ok.emit(n)
except Exception as e: except Exception as e:
self.failed.emit(f"{e}\n{traceback.format_exc()}") self.failed.emit(f"{e}\n{traceback.format_exc()}")
@ -122,9 +122,10 @@ class Step14GeoTIFFBatchThread(QThread):
n = len(self.tif_paths) n = len(self.tif_paths)
for i, tif_path in enumerate(self.tif_paths): for i, tif_path in enumerate(self.tif_paths):
self.progress.emit(i + 1, n) self.progress.emit(i + 1, n)
tif_name = Path(tif_path).stem tif_stem = Path(tif_path).stem
output_png = str(Path(self.output_dir) / f"{tif_name}_map.png") chinese_name = mapper._get_chinese_title(tif_stem)
self.log_message.emit(f"GeoTIFF 渲染 [{i + 1}/{n}] {tif_name}", "info") output_png = str(Path(self.output_dir) / f"{chinese_name}_专题图.png")
self.log_message.emit(f"GeoTIFF 渲染 [{i + 1}/{n}] {tif_stem}", "info")
try: try:
mapper.visualize_raster( mapper.visualize_raster(
raster_tif_path=tif_path, raster_tif_path=tif_path,
@ -132,7 +133,6 @@ class Step14GeoTIFFBatchThread(QThread):
boundary_shp_path=self.boundary_shp_path, boundary_shp_path=self.boundary_shp_path,
nodata_value=-9999.0, nodata_value=-9999.0,
figsize=(14, 10), figsize=(14, 10),
title=f"水色指数专题图 - {tif_name}",
alpha=0.9, alpha=0.9,
) )
except Exception as vis_err: except Exception as vis_err:
@ -762,8 +762,9 @@ class Step14Panel(QWidget):
if not out_dir: if not out_dir:
out_dir = os.path.join(self._get_default_work_dir(), "14_visualization") out_dir = os.path.join(self._get_default_work_dir(), "14_visualization")
os.makedirs(out_dir, exist_ok=True) os.makedirs(out_dir, exist_ok=True)
tif_name = Path(geotiff_path).stem tif_stem = Path(geotiff_path).stem
output_png = os.path.join(out_dir, f"{tif_name}_rendered.png") chinese_name = mapper._get_chinese_title(tif_stem)
output_png = os.path.join(out_dir, f"{chinese_name}_专题图.png")
self.run_button.setEnabled(False) self.run_button.setEnabled(False)
try: try:
@ -775,7 +776,6 @@ class Step14Panel(QWidget):
boundary_shp_path=boundary_shp_path if boundary_shp_path else None, boundary_shp_path=boundary_shp_path if boundary_shp_path else None,
nodata_value=-9999.0, nodata_value=-9999.0,
figsize=(14, 10), figsize=(14, 10),
title=f"水色指数专题图 - {tif_name}",
alpha=0.9, alpha=0.9,
) )
self.run_button.setEnabled(True) self.run_button.setEnabled(True)

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Step10 面板 - 采样点生成 Step4 面板 - 采样点布设
""" """
import os import os
@ -16,8 +16,8 @@ from src.gui.dialogs import SamplingViewerDialog
from src.gui.styles import ModernStylesheet from src.gui.styles import ModernStylesheet
class Step10Panel(QWidget): class Step4SamplingPanel(QWidget):
"""步骤10:采样点生成""" """步骤4:采样点布设"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.init_ui() self.init_ui()
@ -71,7 +71,7 @@ class Step10Panel(QWidget):
"输出采样点:", "输出采样点:",
"CSV Files (*.csv);;All Files (*.*)" "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) layout.addWidget(self.output_file)
# 启用步骤 # 启用步骤
@ -207,7 +207,7 @@ class Step10Panel(QWidget):
# 3. 自动填充输出路径(绝对路径) # 3. 自动填充输出路径(绝对路径)
if self.work_dir: 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) os.makedirs(os.path.dirname(output_path), exist_ok=True)
self.output_file.set_path(output_path.replace('\\', '/')) self.output_file.set_path(output_path.replace('\\', '/'))
@ -215,7 +215,7 @@ class Step10Panel(QWidget):
self._check_csv_exists() self._check_csv_exists()
def run_step(self): def run_step(self):
"""独立运行步骤10""" """独立运行步骤4"""
deglint_img_path = self.deglint_img_file.get_path() deglint_img_path = self.deglint_img_file.get_path()
if not deglint_img_path: if not deglint_img_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!") QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
@ -223,8 +223,8 @@ class Step10Panel(QWidget):
main_window = self.window() main_window = self.window()
if hasattr(main_window, 'run_single_step'): if hasattr(main_window, 'run_single_step'):
config = {'step10': self.get_config()} config = {'step4_sampling': self.get_config()}
main_window.run_single_step('step10', config) main_window.run_single_step('step4_sampling', config)
def _check_csv_exists(self): def _check_csv_exists(self):
"""检查 output csv 是否存在,驱动预览按钮启停""" """检查 output csv 是否存在,驱动预览按钮启停"""
@ -243,7 +243,7 @@ class Step10Panel(QWidget):
if not csv_path or not os.path.exists(csv_path): if not csv_path or not os.path.exists(csv_path):
QMessageBox.warning( QMessageBox.warning(
self, "文件不存在", self, "文件不存在",
f"采样点 CSV 文件不存在:{csv_path}\n请先运行步骤10生成数据。" f"采样点 CSV 文件不存在:{csv_path}\n请先运行步骤4生成数据。"
) )
return return
dialog = SamplingViewerDialog(csv_path, self) 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 from src.gui.styles import ModernStylesheet
class Step4Panel(QWidget): class Step5CleanPanel(QWidget):
"""步骤4:数据预处理""" """步骤5:数据清洗"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.init_ui() self.init_ui()
@ -135,18 +135,16 @@ class Step4Panel(QWidget):
self.output_file.set_path("") self.output_file.set_path("")
def run_step(self): def run_step(self):
"""独立运行步骤4""" """独立运行步骤5"""
# 验证输入
csv_path = self.csv_file.get_path() csv_path = self.csv_file.get_path()
if not csv_path: if not csv_path:
QMessageBox.warning(self, "输入错误", "请选择水质参数文件!") QMessageBox.warning(self, "输入错误", "请选择水质参数文件!")
return return
# 获取主窗口并运行步骤
main_window = self.window() main_window = self.window()
if hasattr(main_window, 'run_single_step'): if hasattr(main_window, 'run_single_step'):
config = {'step4': self.get_config()} config = {'step5_clean': self.get_config()}
main_window.run_single_step('step4', config) main_window.run_single_step('step5_clean', config)
def reset_preview(self, message="请选择CSV文件并点击刷新预览"): 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 from src.gui.styles import ModernStylesheet
class Step5Panel(QWidget): class Step6FeaturePanel(QWidget):
"""步骤5:光谱提取""" """步骤6:光谱特征提取"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.init_ui() self.init_ui()
@ -124,7 +124,7 @@ class Step5Panel(QWidget):
glint_mask_path = self.glint_mask_file.get_path() glint_mask_path = self.glint_mask_file.get_path()
if glint_mask_path: if glint_mask_path:
config['glint_mask_path'] = 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 自动生成。 # 参数,输出路径由 pipeline 内部根据 training_spectra_dir 自动生成。
return config return config
@ -202,8 +202,8 @@ class Step5Panel(QWidget):
# 5. 尝试从 Step4 界面读取已处理的水质参数 CSV 路径,自动填入本面板 # 5. 尝试从 Step4 界面读取已处理的水质参数 CSV 路径,自动填入本面板
main_window = self.window() main_window = self.window()
if main_window and hasattr(main_window, 'step4_panel'): if main_window and hasattr(main_window, 'step5_panel'):
step4_output_path = main_window.step4_panel.output_file.get_path() step4_output_path = main_window.step5_panel.output_file.get_path()
if step4_output_path: if step4_output_path:
# 若为相对路径,使用 work_dir 合成为绝对路径 # 若为相对路径,使用 work_dir 合成为绝对路径
if not os.path.isabs(step4_output_path): if not os.path.isabs(step4_output_path):
@ -235,5 +235,5 @@ class Step5Panel(QWidget):
# 获取主窗口并运行步骤 # 获取主窗口并运行步骤
main_window = self.window() main_window = self.window()
if hasattr(main_window, 'run_single_step'): if hasattr(main_window, 'run_single_step'):
config = {'step5': self.get_config()} config = {'step6_feature': self.get_config()}
main_window.run_single_step('step5', 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 os
import sys import sys
import pandas as pd 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)) return str(base_dir / os.path.basename(relative_path))
class Step6Panel(QWidget): class Step7IndexPanel(QWidget):
COLOR_RATIO = QColor(255, 255, 255) COLOR_RATIO = QColor(255, 255, 255)
COLOR_CONCENTRATION = QColor(220, 240, 255) COLOR_CONCENTRATION = QColor(220, 240, 255)
COLOR_HEADER = QColor(245, 245, 245) COLOR_HEADER = QColor(245, 245, 245)
@ -291,8 +297,8 @@ class Step6Panel(QWidget):
if work_dir: if work_dir:
self.work_dir = work_dir self.work_dir = work_dir
main = self.window() main = self.window()
if hasattr(main, 'step5_panel'): if hasattr(main, 'step6_panel'):
p5 = main.step5_panel.output_file.get_path() p5 = main.step6_panel.output_file.get_path()
if p5: if p5:
if not os.path.isabs(p5): if not os.path.isabs(p5):
p5 = os.path.join(self.work_dir or '', p5) p5 = os.path.join(self.work_dir or '', p5)

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Step7 面板 - 机器学习建模 Step8 面板 - 机器学习建模
""" """
import os import os
@ -68,8 +68,8 @@ SPLIT_CHINESE = {
} }
class Step7Panel(QWidget): class Step8MlTrainPanel(QWidget):
"""步骤7:机器学习建模""" """步骤8:机器学习建模"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.init_ui() self.init_ui()
@ -392,7 +392,7 @@ class Step7Panel(QWidget):
self.output_path.set_path("") self.output_path.set_path("")
def run_step(self): def run_step(self):
"""独立运行步骤7""" """独立运行步骤8"""
training_csv_path = self.training_csv_file.get_path() training_csv_path = self.training_csv_file.get_path()
if not training_csv_path: if not training_csv_path:
QMessageBox.warning(self, "输入错误", "请选择训练数据CSV文件") QMessageBox.warning(self, "输入错误", "请选择训练数据CSV文件")
@ -400,8 +400,8 @@ class Step7Panel(QWidget):
main_window = self.window() main_window = self.window()
if hasattr(main_window, 'run_single_step'): if hasattr(main_window, 'run_single_step'):
config = {'step7': self.get_config()} config = {'step8_ml_train': self.get_config()}
main_window.run_single_step('step7', config) main_window.run_single_step('step8_ml_train', config)
def get_training_params(self): 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 #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Step8 面板 - 机器学习预测 Step11 面板 - 机器学习预测
""" """
import os import os
@ -19,8 +19,8 @@ from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet from src.gui.styles import ModernStylesheet
class Step11MlPanel(QWidget): class Step9MlPredictPanel(QWidget):
"""步骤11:机器学习预测""" """步骤9:机器学习预测"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.external_models_dict = {} # {subdir_name: model_obj, ...} self.external_models_dict = {} # {subdir_name: model_obj, ...}
@ -190,7 +190,7 @@ class Step11MlPanel(QWidget):
"""浏览模型母文件夹,自动扫描子目录中的 .joblib 文件""" """浏览模型母文件夹,自动扫描子目录中的 .joblib 文件"""
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: if default:
default = os.path.join(default, "7_Supervised_Model_Training") default = os.path.join(default, "9_supervised_modeling")
dir_path = QFileDialog.getExistingDirectory( dir_path = QFileDialog.getExistingDirectory(
self, self,
"选择模型母文件夹", "选择模型母文件夹",
@ -216,7 +216,6 @@ class Step11MlPanel(QWidget):
] ]
if not joblib_files: if not joblib_files:
continue continue
# 每个子目录只取第一个 .joblib 文件(与 batch 逻辑一致)
joblib_path = joblib_files[0].path joblib_path = joblib_files[0].path
try: try:
loaded = joblib.load(joblib_path) loaded = joblib.load(joblib_path)
@ -319,43 +318,41 @@ class Step11MlPanel(QWidget):
main_window = self.window() main_window = self.window()
# 1. 尝试从 Step7 界面读取全湖采样点 CSV 路径 # 1. 尝试从 Step4采样点布设读取全湖采样点 CSV 路径
if main_window and hasattr(main_window, 'step10_panel'): if main_window and hasattr(main_window, 'step4_sampling_panel'):
step7_widget = getattr(main_window.step10_panel, 'output_file', None) step4_widget = getattr(main_window.step4_sampling_panel, 'output_file', None)
step7_output_path = "" step4_output_path = ""
if hasattr(step7_widget, 'get_path'): if hasattr(step4_widget, 'get_path'):
step7_output_path = step7_widget.get_path() or "" step4_output_path = step4_widget.get_path() or ""
elif hasattr(step7_widget, 'text'): elif hasattr(step4_widget, 'text'):
step7_output_path = step7_widget.text() or "" step4_output_path = step4_widget.text() or ""
if step7_output_path: if step4_output_path:
# 若为相对路径,使用 work_dir 合成为绝对路径 if not os.path.isabs(step4_output_path):
if not os.path.isabs(step7_output_path): step4_output_path = os.path.join(self.work_dir or '', step4_output_path).replace('\\', '/')
step7_output_path = os.path.join(self.work_dir or '', step7_output_path).replace('\\', '/')
existing = self.sampling_csv_file.get_path() existing = self.sampling_csv_file.get_path()
if not existing or not existing.strip(): 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 界面读取监督模型目录 # 2. 尝试从 Step9监督建模读取模型目录
if main_window and hasattr(main_window, 'step7_panel'): if main_window and hasattr(main_window, 'step9_panel'):
step6_widget = getattr(main_window.step7_panel, 'output_dir', None) step9_widget = getattr(main_window.step9_panel, 'output_dir', None)
step6_models_dir = "" step9_models_dir = ""
if hasattr(step6_widget, 'get_path'): if hasattr(step9_widget, 'get_path'):
step6_models_dir = step6_widget.get_path() or "" step9_models_dir = step9_widget.get_path() or ""
elif hasattr(step6_widget, 'text'): elif hasattr(step9_widget, 'text'):
step6_models_dir = step6_widget.text() or "" step9_models_dir = step9_widget.text() or ""
if step6_models_dir: if step9_models_dir:
# 若为相对路径,使用 work_dir 合成为绝对路径 if not os.path.isabs(step9_models_dir):
if not os.path.isabs(step6_models_dir): step9_models_dir = os.path.join(self.work_dir or '', step9_models_dir).replace('\\', '/')
step6_models_dir = os.path.join(self.work_dir or '', step6_models_dir).replace('\\', '/')
existing_models = self.models_dir_file.get_path() existing_models = self.models_dir_file.get_path()
if not existing_models or not existing_models.strip(): 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. 自动填充输出路径(机器学习预测目录) # 3. 自动填充输出路径(机器学习预测目录)
if self.work_dir: 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) os.makedirs(output_dir, exist_ok=True)
existing_out = self.output_file.get_path() existing_out = self.output_file.get_path()
if not existing_out or not existing_out.strip(): if not existing_out or not existing_out.strip():
@ -378,7 +375,7 @@ class Step11MlPanel(QWidget):
"""浏览模型目录""" """浏览模型目录"""
default = self._get_default_work_dir() default = self._get_default_work_dir()
if default: 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) dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录", default)
if dir_path: if dir_path:
self.models_dir_file.set_path(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']) self.output_file.set_path(config['output_path'])
def run_step(self): def run_step(self):
"""独立运行步骤8""" """独立运行步骤11"""
sampling_csv_path = self.sampling_csv_file.get_path() sampling_csv_path = self.sampling_csv_file.get_path()
if not sampling_csv_path: if not sampling_csv_path:
QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件") QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件")
@ -431,7 +428,6 @@ class Step11MlPanel(QWidget):
"请先点击「浏览...」按钮选择模型母文件夹!", "请先点击「浏览...」按钮选择模型母文件夹!",
) )
return return
# 只传递用户勾选的模型
checked_dict = self._get_checked_models_dict() checked_dict = self._get_checked_models_dict()
if not checked_dict: if not checked_dict:
QMessageBox.warning( QMessageBox.warning(
@ -443,11 +439,11 @@ class Step11MlPanel(QWidget):
main_window = self.window() main_window = self.window()
if hasattr(main_window, 'run_single_step'): if hasattr(main_window, 'run_single_step'):
config = { config = {
'step11_ml': self.get_config(), 'step9_ml_predict': self.get_config(),
'_external_models_dict': checked_dict, '_external_models_dict': checked_dict,
'_external_model_dir': self.external_model_dir, '_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 return
# 默认流程:使用模型目录 # 默认流程:使用模型目录
@ -458,5 +454,5 @@ class Step11MlPanel(QWidget):
main_window = self.window() main_window = self.window()
if hasattr(main_window, 'run_single_step'): if hasattr(main_window, 'run_single_step'):
config = {'step11_ml': self.get_config()} config = {'step9_ml_predict': self.get_config()}
main_window.run_single_step('step11_ml', 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.step1_panel import Step1Panel
from src.gui.panels.step2_panel import Step2Panel from src.gui.panels.step2_panel import Step2Panel
from src.gui.panels.step3_panel import Step3Panel from src.gui.panels.step3_panel import Step3Panel
from src.gui.panels.step4_panel import Step4Panel from src.gui.panels.step4_sampling_panel import Step4SamplingPanel # 采样点布设
from src.gui.panels.step5_panel import Step5Panel from src.gui.panels.step5_clean_panel import Step5CleanPanel # 数据清洗
from src.gui.panels.step6_panel import Step6Panel # was step8_panel from src.gui.panels.step6_feature_panel import Step6FeaturePanel # 光谱特征
from src.gui.panels.step7_panel import Step7Panel # was step6_panel from src.gui.panels.step7_index_panel import Step7IndexPanel # 水质光谱指数
from src.gui.panels.step8_waterindex_panel import Step8WaterIndexPanel # 水色指数反演 from src.gui.panels.step10_watercolor_panel import Step10WatercolorPanel # 水色指数反演
from src.gui.panels.step9_concentration_panel import Step9ConcentrationPanel # 浓度反演 from src.gui.panels.step8_ml_train_panel import Step8MlTrainPanel # 机器学习建模
from src.gui.panels.step10_panel import Step10Panel # was step7_panel from src.gui.panels.step9_ml_predict_panel import Step9MlPredictPanel # 机器学习预测
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.dialogs import BandConfirmDialog, AISettingsDialog from src.gui.dialogs import BandConfirmDialog, AISettingsDialog
from src.gui.panels.visualization_panel import VisualizationPanel from src.gui.panels.step11_map_panel import Step11MapPanel # 专题图生成
from src.gui.panels.report_generation_panel import ReportGenerationPanel from src.gui.panels.step12_viz_panel import Step12VizPanel # 可视化
from src.gui.panels.step13_report_panel import Step13ReportPanel # 报告生成
# Pipeline 核心异常(用于预检弹窗) # Pipeline 核心异常(用于预检弹窗)
from src.core.pipeline.runner import PipelineHalt from src.core.pipeline.runner import PipelineHalt
@ -1380,93 +1379,64 @@ class WaterQualityGUI(QMainWindow):
'deglint_goodman': '3_deglint/deglint_goodman.bsq', 'deglint_goodman': '3_deglint/deglint_goodman.bsq',
'deglint_hedley': '3_deglint/deglint_hedley.bsq', 'deglint_hedley': '3_deglint/deglint_hedley.bsq',
'deglint_sugar': '3_deglint/deglint_sugar.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' 'processed_data': '4_processed_data/processed_data.csv'
}, },
'step5': { 'step6_feature': {
'training_spectra': '5_training_spectra/training_spectra.csv' 'training_spectra': '5_training_spectra/training_spectra.csv'
}, },
'step6': { 'step7_index': {
'water_indices': '6_water_quality_indices/water_quality_indices.csv' 'water_indices': '6_water_quality_indices/water_quality_indices.csv'
}, },
'step7': { 'step8_ml_train': {
'models': '7_Supervised_Model_Training/' # 目录,包含各参数子目录 'models': '7_Supervised_Model_Training/'
}, },
'step8_non_empirical_modeling': { 'step4_sampling': {
'regression_models': '8_Regression_Modeling/' # 目录,包含各参数子目录
},
'step9': {
'custom_regression_models': '9_Custom_Regression_Modeling/' # 目录
},
'step10': {
'sampling_points': '10_sampling/sampling_spectra.csv' 'sampling_points': '10_sampling/sampling_spectra.csv'
}, },
'step11_ml': { 'step9_ml_predict': {
'predictions': '11_12_13_predictions/Machine_Learning_Prediction/' # 目录,包含机器学习预测结果 'predictions': '11_12_13_predictions/Machine_Learning_Prediction/'
}, },
'step11': { 'step11_map': {
'regression_predictions': '11_12_13_predictions/Non_Empirical_Prediction/' # 目录,包含非经验模型预测结果 'distribution_maps': '14_visualization/'
},
'step12': {
'custom_predictions': '11_12_13_predictions/Custom_Regression_Prediction/' # 目录,包含自定义回归预测结果
},
'step14': {
'distribution_maps': '14_visualization/' # 目录,包含专题图
} }
} }
# 定义步骤间的依赖关系:{当前步骤: {输入字段: (依赖步骤, 输出类型, 面板属性名)}} # 定义步骤间的依赖关系:{当前步骤: {输入字段: (依赖步骤, 输出类型, 面板属性名)}}
self.step_dependencies = { self.step_dependencies = {
'step2': { 'step2': {
'img_path': ('step1', 'reference_img', 'img_file'), # 步骤2需要参考影像 'img_path': ('step1', 'reference_img', 'img_file'),
'water_mask_path': ('step1', 'water_mask', 'water_mask_file') # 步骤2可选水域掩膜 'water_mask_path': ('step1', 'water_mask', 'water_mask_file')
}, },
'step3': { 'step3': {
'img_path': ('step1', 'reference_img', 'img_file'), # 步骤3需要参考影像 'img_path': ('step1', 'reference_img', 'img_file'),
'water_mask': ('step1', 'water_mask', 'water_mask_file'), # 步骤3需要水域掩膜 'water_mask': ('step1', 'water_mask', 'water_mask_file'),
}, },
'step4': { 'step6_feature': {
# 步骤4主要处理CSV文件一般不依赖前面步骤的输出 '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': { 'step7_index': {
'deglint_img_path': ('step3', 'deglint_image', 'deglint_img_file'), # 步骤5需要去耀斑影像 'training_csv_path': ('step6_feature', 'training_spectra', 'output_file')
'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可选耀斑掩膜
}, },
'step6': { 'step8_ml_train': {
'training_csv_path': ('step5', 'training_spectra', 'output_file') # 步骤6需要步骤5输出的训练光谱 'training_csv_path': ('step7_index', 'water_indices', 'csv_file')
}, },
'step7': { 'step4_sampling': {
'csv_path': ('step5', 'training_spectra', 'csv_file') # 步骤7需要训练光谱数据 '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': { 'step9_ml_predict': {
'csv_path': ('step5', 'training_spectra', 'csv_file') # 步骤8非经验建模需要训练光谱数据 'sampling_csv_path': ('step4_sampling', 'sampling_points', 'sampling_csv_file'),
'models_dir': ('step8_ml_train', 'models', 'models_dir_file')
}, },
'step9': { 'step11_map': {
'csv_path': ('step5', 'training_spectra', 'csv_file') # 步骤9需要训练光谱数据 'prediction_csv_path': ('step9_ml_predict', 'predictions', 'prediction_csv_file')
},
'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
} }
} }
@ -1843,26 +1813,23 @@ class WaterQualityGUI(QMainWindow):
"阶段一:影像预处理": [ "阶段一:影像预处理": [
("step1", "1. 水域掩膜生成"), ("step1", "1. 水域掩膜生成"),
("step2", "2. 耀斑区域识别"), ("step2", "2. 耀斑区域识别"),
("step3", "3. 耀斑去除与修复"), ("step3", "3. 耀斑去除与修复")
], ],
"阶段二:样本数据准备 ": [ "阶段二:样本数据准备": [
("step4", "4. 数据标准化处理"), ("step4_sampling", "4. 采样点布设"),
("step5", "5. 光谱特征提取"), ("step5_clean", "5. 数据清洗"),
("step6", "6. 水质参数指数计算"), ("step6_feature", "6. 光谱特征提取"),
("step7_index", "7. 水质指数计算")
], ],
"阶段三:模型构建与训练": [ "阶段三:模型构建与训练": [
("step7", "7. 机器学习模型训练"), ("step8_ml_train", "8. 机器学习")
("step8_non_empirical_modeling", "8. 回归模型训练"),
("step9", "9. 自定义回归模型训练"),
], ],
"阶段四:预测与成果输出 ": [ "阶段四:预测与成果输出": [
("step10", "10. 采样点布设"), ("step9_ml_predict", "9. 机器学习预测"),
("step11_ml", "11. 机器学习预测"), ("step10_watercolor", "10. 水色指数反演"),
("step11", "12. 回归预测"), ("step11_map", "11. 专题图生成"),
("step12", "13. 自定义回归预测"), ("step12_viz", "12. 可视化展示"),
("step14", "14. 专题图生成"), ("step13_report", "13. 分析报告生成")
("step9_viz", "15. 可视化分析"),
("step_report", "16. 分析报告生成"),
] ]
} }
@ -1882,11 +1849,7 @@ class WaterQualityGUI(QMainWindow):
self.step_list.addItem(stage_item) self.step_list.addItem(stage_item)
# 添加该阶段的所有步骤 # 添加该阶段的所有步骤
HIDDEN_STEP_IDS = {"step8_non_empirical_modeling", "step9", "step11", "step12"}
for step_id, step_display in steps: for step_id, step_display in steps:
if step_id in HIDDEN_STEP_IDS:
continue
item = QListWidgetItem(f" └─ {step_display}") item = QListWidgetItem(f" └─ {step_display}")
item.setData(Qt.UserRole, step_id) item.setData(Qt.UserRole, step_id)
@ -1956,38 +1919,35 @@ class WaterQualityGUI(QMainWindow):
self.step3_panel = Step3Panel() self.step3_panel = Step3Panel()
self.step_stack.addTab(self.create_scroll_area(self.step3_panel), QIcon(self.get_icon_path("3.png")), "耀斑去除") self.step_stack.addTab(self.create_scroll_area(self.step3_panel), QIcon(self.get_icon_path("3.png")), "耀斑去除")
self.step4_panel = Step4Panel() self.step4_sampling_panel = Step4SamplingPanel()
self.step_stack.addTab(self.create_scroll_area(self.step4_panel), QIcon(self.get_icon_path("4.png")), "数据清洗") self.step_stack.addTab(self.create_scroll_area(self.step4_sampling_panel), QIcon(self.get_icon_path("4.png")), "采样点布设")
self.step5_panel = Step5Panel() self.step5_clean_panel = Step5CleanPanel()
self.step_stack.addTab(self.create_scroll_area(self.step5_panel), QIcon(self.get_icon_path("5.png")), "特征构建") self.step_stack.addTab(self.create_scroll_area(self.step5_clean_panel), QIcon(self.get_icon_path("5.png")), "数据清洗")
self.step6_panel = Step6Panel() self.step6_feature_panel = Step6FeaturePanel()
self.step_stack.addTab(self.create_scroll_area(self.step6_panel), QIcon(self.get_icon_path("6.png")), "水质光谱指数计算") self.step_stack.addTab(self.create_scroll_area(self.step6_feature_panel), QIcon(self.get_icon_path("6.png")), "光谱特征")
self.step7_panel = Step7Panel() self.step7_index_panel = Step7IndexPanel()
self.step_stack.addTab(self.create_scroll_area(self.step7_panel), QIcon(self.get_icon_path("7.png")), "监督建模") 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.step8_ml_train_panel = Step8MlTrainPanel()
self.step_stack.addTab(self.create_scroll_area(self.step8_waterindex_panel), QIcon(self.get_icon_path("6.png")), "水色指数反演") 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.step9_ml_predict_panel = Step9MlPredictPanel()
self.step_stack.addTab(self.create_scroll_area(self.step9_concentration_panel), QIcon(self.get_icon_path("6.png")), "浓度反演") 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.step10_watercolor_panel = Step10WatercolorPanel()
self.step_stack.addTab(self.create_scroll_area(self.step10_panel), QIcon(self.get_icon_path("7.png")), "采样点布设") 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.step11_map_panel = Step11MapPanel()
self.step_stack.addTab(self.create_scroll_area(self.step11_ml_panel), QIcon(self.get_icon_path("8.png")), "监督预测") self.step_stack.addTab(self.create_scroll_area(self.step11_map_panel), QIcon(self.get_icon_path("10.png")), "专题图生成")
self.step14_panel = Step14Panel() self.step12_viz_panel = Step12VizPanel()
self.step_stack.addTab(self.create_scroll_area(self.step14_panel), QIcon(self.get_icon_path("10.png")), "专题图生成") self.step_stack.addTab(self.create_scroll_area(self.step12_viz_panel), QIcon(self.get_icon_path("9.png")), "可视化")
self.viz_panel = VisualizationPanel() self.step13_report_panel = Step13ReportPanel(main_window=self)
self.step_stack.addTab(self.create_scroll_area(self.viz_panel), QIcon(self.get_icon_path("9.png")), "可视化") self.step_stack.addTab(self.create_scroll_area(self.step13_report_panel), QIcon(self.get_icon_path("10.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")), "报告生成")
# 连接Tab切换信号实现双向同步必须在step_stack创建后 # 连接Tab切换信号实现双向同步必须在step_stack创建后
self.step_stack.currentChanged.connect(self.on_tab_changed) self.step_stack.currentChanged.connect(self.on_tab_changed)
@ -2126,22 +2086,11 @@ class WaterQualityGUI(QMainWindow):
# 根据步骤ID查找对应的tab索引 # 根据步骤ID查找对应的tab索引
step_id_to_tab = { step_id_to_tab = {
'step1': 0, 'step1': 0, 'step2': 1, 'step3': 2, 'step4_sampling': 3,
'step2': 1, 'step5_clean': 4, 'step6_feature': 5, 'step7_index': 6,
'step3': 2, 'step8_ml_train': 7, 'step9_ml_predict': 8,
'step4': 3, 'step10_watercolor': 9, 'step11_map': 10,
'step5': 4, 'step12_viz': 11, 'step13_report': 12,
'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,
} }
if item_data in step_id_to_tab: if item_data in step_id_to_tab:
@ -2155,24 +2104,13 @@ class WaterQualityGUI(QMainWindow):
if index < 0: if index < 0:
return return
# Tab索引到步骤ID的反向映射 # Tab索引到步骤ID的反向映射13个Tabindex 0-12
tab_to_step_id = { tab_to_step_id = {
0: 'step1', 0: 'step1', 1: 'step2', 2: 'step3', 3: 'step4_sampling',
1: 'step2', 4: 'step5_clean', 5: 'step6_feature', 6: 'step7_index',
2: 'step3', 7: 'step8_ml_train', 8: 'step9_ml_predict',
3: 'step4', 9: 'step10_watercolor', 10: 'step11_map',
4: 'step5', 11: 'step12_viz', 12: 'step13_report',
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',
} }
if index not in tab_to_step_id: if index not in tab_to_step_id:
@ -2191,53 +2129,27 @@ class WaterQualityGUI(QMainWindow):
self.step_list.setCurrentRow(row) self.step_list.setCurrentRow(row)
break break
# Step2 切换时自动填充数据流转路径 # 面板自动填充:统一 mapping 覆盖 index 0-12
if index == 1: mapping = {
self.step2_panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline) 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 切换时自动填充数据流转路径 if index in mapping:
elif index == 2: panel, _ = mapping[index]
self.step3_panel.update_from_config(work_dir=self.work_dir) if hasattr(panel, 'update_from_config'):
panel.update_from_config(work_dir=self.work_dir, pipeline=self.pipeline)
# 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)
def apply_stylesheet(self): def apply_stylesheet(self):
"""应用样式表 - 应用现代化设计风格""" """应用样式表 - 应用现代化设计风格"""
@ -2276,24 +2188,22 @@ class WaterQualityGUI(QMainWindow):
self.step2_panel.set_config(config['step2']) self.step2_panel.set_config(config['step2'])
if 'step3' in config: if 'step3' in config:
self.step3_panel.set_config(config['step3']) self.step3_panel.set_config(config['step3'])
if 'step4' in config: if 'step4_sampling' in config:
self.step4_panel.set_config(config['step4']) self.step4_sampling_panel.set_config(config['step4_sampling'])
if 'step5' in config: if 'step5_clean' in config:
self.step5_panel.set_config(config['step5']) self.step5_clean_panel.set_config(config['step5_clean'])
if 'step6' in config: if 'step6_feature' in config:
self.step6_panel.set_config(config['step6']) self.step6_feature_panel.set_config(config['step6_feature'])
if 'step7' in config: if 'step7_index' in config:
self.step7_panel.set_config(config['step7']) self.step7_index_panel.set_config(config['step7_index'])
if 'step10' in config: if 'step9_ml_predict' in config:
self.step10_panel.set_config(config['step10']) self.step9_ml_predict_panel.set_config(config['step9_ml_predict'])
if 'step11_ml' in config: if 'step11_map' in config:
self.step11_ml_panel.set_config(config['step11_ml']) self.step11_map_panel.set_config(config['step11_map'])
if 'step14' in config: if 'step12_viz' in config:
self.step14_panel.set_config(config['step14']) self.step12_viz_panel.set_config(config['step12_viz'])
if 'visualization' in config: if 'step13_report' in config:
self.viz_panel.set_config(config['visualization']) self.step13_report_panel.set_config(config['step13_report'])
if 'report_generation' in config:
self.report_panel.set_config(config['report_generation'])
self.config_file = file_path self.config_file = file_path
self.log_message(f"已加载配置: {file_path}", "info") self.log_message(f"已加载配置: {file_path}", "info")
@ -2330,15 +2240,15 @@ class WaterQualityGUI(QMainWindow):
'step1': self.step1_panel.get_config(), 'step1': self.step1_panel.get_config(),
'step2': self.step2_panel.get_config(), 'step2': self.step2_panel.get_config(),
'step3': self.step3_panel.get_config(), 'step3': self.step3_panel.get_config(),
'step4': self.step4_panel.get_config(), 'step4_sampling': self.step4_sampling_panel.get_config(),
'step5': self.step5_panel.get_config(), 'step5_clean': self.step5_clean_panel.get_config(),
'step6': self.step6_panel.get_config(), 'step6_feature': self.step6_feature_panel.get_config(),
'step7': self.step7_panel.get_config(), 'step7_index': self.step7_index_panel.get_config(),
'step10': self.step10_panel.get_config(), 'step8_ml_train': self.step8_ml_train_panel.get_config(),
'step11_ml': self.step11_ml_panel.get_config(), 'step9_ml_predict': self.step9_ml_predict_panel.get_config(),
'step14': self.step14_panel.get_config(), 'step11_map': self.step11_map_panel.get_config(),
'visualization': self.viz_panel.get_config(), 'step12_viz': self.step12_viz_panel.get_config(),
'report_generation': self.report_panel.get_config(), 'step13_report': self.step13_report_panel.get_config(),
} }
return config return config
@ -2385,13 +2295,15 @@ class WaterQualityGUI(QMainWindow):
'step1': self.step1_panel, 'step1': self.step1_panel,
'step2': self.step2_panel, 'step2': self.step2_panel,
'step3': self.step3_panel, 'step3': self.step3_panel,
'step4': self.step4_panel, 'step4_sampling': self.step4_sampling_panel,
'step5': self.step5_panel, 'step5_clean': self.step5_clean_panel,
'step6': self.step6_panel, 'step6_feature': self.step6_feature_panel,
'step7': self.step7_panel, 'step7_index': self.step7_index_panel,
'step10': self.step10_panel, 'step8_ml_train': self.step8_ml_train_panel,
'step11_ml': self.step11_ml_panel, 'step9_ml_predict': self.step9_ml_predict_panel,
'step14': self.step14_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) return panel_map.get(step_id)
@ -2483,17 +2395,17 @@ class WaterQualityGUI(QMainWindow):
'1_water_mask': 'step1', '1_water_mask': 'step1',
'2_glint': 'step2', '2_glint': 'step2',
'3_deglint': 'step3', '3_deglint': 'step3',
'4_processed_data': 'step4', '4_processed_data': 'step4_sampling',
'5_training_spectra': 'step5', '5_training_spectra': 'step5_clean',
'6_water_quality_indices': 'step6', '6_water_quality_indices': 'step6_feature',
'7_Supervised_Model_Training': 'step7', '7_Supervised_Model_Training': 'step7_index',
'8_Regression_Modeling': 'step8_non_empirical_modeling', '8_Regression_Modeling': 'step8_ml_train',
'9_Custom_Regression_Modeling': 'step9', '9_Custom_Regression_Modeling': 'step9_ml_predict',
'10_sampling': 'step10', '11_12_13_predictions/Machine_Learning_Prediction': 'step9_ml_predict',
'11_12_13_predictions/Machine_Learning_Prediction': 'step11_ml', '11_12_13_predictions/Non_Empirical_Prediction': 'step11_map',
'11_12_13_predictions/Non_Empirical_Prediction': 'step11', '11_12_13_predictions/Custom_Regression_Prediction': 'step12_viz',
'11_12_13_predictions/Custom_Regression_Prediction': 'step12', '14_visualization': 'step13_report',
'14_visualization': 'step14' '10_geotiff_batch_rendering': 'step11_map'
} }
for subdir, step_ids in subdirs.items(): for subdir, step_ids in subdirs.items():
@ -2535,15 +2447,15 @@ class WaterQualityGUI(QMainWindow):
discovered_outputs[step_id]['glint_mask'] = str(file_path) discovered_outputs[step_id]['glint_mask'] = str(file_path)
elif 'deglint' in file_name and step_id == 'step3': elif 'deglint' in file_name and step_id == 'step3':
discovered_outputs[step_id]['deglint_image'] = str(file_path) 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) 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) 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) 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) 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) discovered_outputs[step_id]['predictions'] = str(file_path)
# 更新内部记录 # 更新内部记录
@ -2566,8 +2478,8 @@ class WaterQualityGUI(QMainWindow):
# 首先扫描工作目录发现已有的输出文件 # 首先扫描工作目录发现已有的输出文件
self.scan_work_directory_for_files(work_path) self.scan_work_directory_for_files(work_path)
step_order = ['step2', 'step3', 'step4', 'step5', 'step6', 'step7', 'step8_non_empirical_modeling', 'step9', step_order = ['step2', 'step3', 'step4_sampling', 'step5_clean', 'step6_feature', 'step7_index',
'step10', 'step11_ml', 'step11', 'step12', 'step14'] 'step8_ml_train', 'step9_ml_predict', 'step11_map', 'step12_viz', 'step13_report']
filled_count = 0 filled_count = 0
for step_id in step_order: for step_id in step_order:
@ -2588,12 +2500,15 @@ class WaterQualityGUI(QMainWindow):
panels_with_dependencies = [ panels_with_dependencies = [
('step2', self.step2_panel), ('step2', self.step2_panel),
('step3', self.step3_panel), ('step3', self.step3_panel),
('step5', self.step5_panel), ('step4_sampling', self.step4_sampling_panel),
('step6', self.step6_panel), ('step5_clean', self.step5_clean_panel),
('step7', self.step7_panel), ('step6_feature', self.step6_feature_panel),
('step10', self.step10_panel), ('step7_index', self.step7_index_panel),
('step11_ml', self.step11_ml_panel), ('step8_ml_train', self.step8_ml_train_panel),
('step14', self.step14_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: for step_id, panel in panels_with_dependencies:
@ -2663,10 +2578,10 @@ class WaterQualityGUI(QMainWindow):
self.statusBar().showMessage(f"工作目录: {dir_path}") self.statusBar().showMessage(f"工作目录: {dir_path}")
# 同步到可视化面板 # 同步到可视化面板
if hasattr(self, 'viz_panel'): if hasattr(self, 'step12_viz_panel'):
self.viz_panel.set_work_dir(dir_path) self.step12_viz_panel.set_work_dir(dir_path)
if hasattr(self, 'report_panel'): if hasattr(self, 'step13_report_panel'):
self.report_panel.set_work_dir(dir_path) self.step13_report_panel.set_work_dir(dir_path)
def open_work_directory(self): def open_work_directory(self):
"""打开工作目录""" """打开工作目录"""
@ -2985,11 +2900,11 @@ class WaterQualityGUI(QMainWindow):
# 准备实际运行配置(排除未启用的步骤) # 准备实际运行配置(排除未启用的步骤)
worker_config = copy.deepcopy(config) worker_config = copy.deepcopy(config)
step6_cfg = worker_config.get('step6') step6_cfg = worker_config.get('step6_feature')
if step6_cfg: if step6_cfg:
enabled = step6_cfg.pop('enabled', True) enabled = step6_cfg.pop('enabled', True)
if not enabled: if not enabled:
worker_config.pop('step6', None) worker_config.pop('step6_feature', None)
# 工作线程内创建 Pipeline避免主线程阻塞及 Qt5Agg 子线程绘图卡死 # 工作线程内创建 Pipeline避免主线程阻塞及 Qt5Agg 子线程绘图卡死
self.worker = WorkerThread(work_dir, worker_config, mode='full', skip_list=skip_list) 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): def update_ui_for_training_mode(self):
"""根据训练数据模式更新UI状态""" """根据训练数据模式更新UI状态"""
# 需要禁用的步骤ID对应无训练数据模式下需要禁用的步骤 # 需要禁用的步骤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 = { step_id_to_tab_training = {
'step1': 0, 'step2': 1, 'step3': 2, 'step4': 3, 'step1': 0, 'step2': 1, 'step3': 2, 'step4_sampling': 3,
'step5': 4, 'step6': 5, 'step7': 6, 'step8_non_empirical_modeling': 7, 'step5_clean': 4, 'step6_feature': 5, 'step7_index': 6, 'step9_ml_predict': 7,
'step9': 8, 'step10': 9, 'step11_ml': 10, 'step11': 11, 'step10_watercolor': 9, 'step11_map': 10, 'step12_viz': 11, 'step13_report': 12
'step12': 12, 'step14': 13, 'step9_viz': 14
} }
for step_id in disabled_step_ids: for step_id in disabled_step_ids:
if step_id in step_id_to_tab: if step_id in step_id_to_tab_training:
tab_index = step_id_to_tab[step_id] tab_index = step_id_to_tab_training[step_id]
if tab_index < self.step_stack.count(): if tab_index < self.step_stack.count():
self.step_stack.setTabEnabled(tab_index, self.has_training_data) 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 from pyproj import CRS, Transformer
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import matplotlib.patches as patches import matplotlib.patches as patches
from matplotlib.ticker import FuncFormatter from matplotlib.ticker import FuncFormatter, MaxNLocator
from matplotlib_scalebar.scalebar import ScaleBar from matplotlib_scalebar.scalebar import ScaleBar
from scipy.interpolate import griddata from scipy.interpolate import griddata
from scipy import ndimage 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: class ContentMapper:
def __init__(self, input_crs='EPSG:32651', output_crs='EPSG:4326'): def __init__(self, input_crs='EPSG:32651', output_crs='EPSG:4326'):
""" """
@ -97,6 +163,63 @@ class ContentMapper:
print(f"坐标转换设置: {input_crs} -> {output_crs}") 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): def _extract_param_name(self, csv_file):
""" """
从CSV文件名或内容中提取参数名称 从CSV文件名或内容中提取参数名称
@ -2080,12 +2203,15 @@ class ContentMapper:
str str
输出图片路径 输出图片路径
""" """
# ── 输出路径自动派生 ────────────────────────────────────────── # ── 始终从路径提取 stem供后续中文标题和文件派生使用──────────
stem = Path(raster_tif_path).stem
# ── 输出路径自动派生(中文文件名)──────────────────────────────
if output_file is None: if output_file is None:
stem = Path(raster_tif_path).stem chinese_title = self._get_chinese_title(stem)
out_dir = Path(raster_tif_path).parent / 'visualization' out_dir = Path(raster_tif_path).parent / 'visualization'
out_dir.mkdir(parents=True, exist_ok=True) 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────────────────── # ── 读取 GeoTIFF优先 rasterio备选 GDAL──────────────────
tif_path = Path(raster_tif_path) tif_path = Path(raster_tif_path)
@ -2214,6 +2340,10 @@ class ContentMapper:
param_name = self._extract_param_name(str(tif_path)) param_name = self._extract_param_name(str(tif_path))
cmap = self._get_colormap(param_name) cmap = self._get_colormap(param_name)
# ── 中文标题(文件名汉化 + 绘图标题)──────────────────────────
# 用户显式传入 title 时直接使用;否则用中文映射
chinese_title = self._get_chinese_title(stem) if not title else title
# ── 计算空间范围extent────────────────────────────────────── # ── 计算空间范围extent──────────────────────────────────────
# 优先使用 rasterio 原生 bounds保证坐标轴为真实 UTM 米 # 优先使用 rasterio 原生 bounds保证坐标轴为真实 UTM 米
# GDAL 回退使用 GeoTransform 计算 # GDAL 回退使用 GeoTransform 计算
@ -2251,23 +2381,24 @@ class ContentMapper:
safe_figsize = (safe_w, safe_h) safe_figsize = (safe_w, safe_h)
fig, ax = plt.subplots(figsize=safe_figsize) fig, ax = plt.subplots(figsize=safe_figsize)
# 计算有效值统计(使用 nanpercentile 精准锁定水体内部,排除陆地 NoData 干扰) # 计算有效值统计(2σ 标准差拉伸,排除长尾异常值干扰)
valid = array[~np.isnan(array)] valid = array[~np.isnan(array)]
if valid.size == 0: if valid.size == 0:
raise ValueError("GeoTIFF 中没有有效数据(全部为 NoData") raise ValueError("GeoTIFF 中没有有效数据(全部为 NoData")
vmin = float(np.nanpercentile(array, 2)) mean_val = float(np.nanmean(array))
vmax = float(np.nanpercentile(array, 98)) std_val = float(np.nanstd(array))
data_range = vmax - vmin 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: if (vmax - vmin) < 1e-9:
center = float(np.nanmean(array)) center = mean_val
exp = max(abs(center) * 0.01, 1e-9) exp = max(abs(center) * 0.01, 1e-9)
vmin = center - exp vmin = center - exp
vmax = center + exp vmax = center + exp
print(f"[visualize_raster] 分位数拉伸: P2={vmin:.4f}, P98={vmax:.4f}" print(f"[visualize_raster] 2σ 拉伸: vmin={vmin:.4f}, vmax={vmax:.4f}"
f"有效像元: {valid.size}/{array.size}") f"mean={mean_val:.4f}, std={std_val:.4f}有效像元: {valid.size}/{array.size}")
# ── 栅格绘图 ───────────────────────────────────────────────── # ── 栅格绘图 ─────────────────────────────────────────────────
# 使用 masked arrayNaN 区域自动不显示 # 使用 masked arrayNaN 区域自动不显示
@ -2321,21 +2452,16 @@ class ContentMapper:
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.4, color='gray') ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.4, color='gray')
ax.set_axisbelow(True) ax.set_axisbelow(True)
# ── 标题 ───────────────────────────────────────────────────── # ── 标题(中文)──────────────────────────────────────────────
if title: ax.set_title(chinese_title, fontsize=13, fontweight='bold', pad=10)
ax.set_title(title, fontsize=13, fontweight='bold', pad=10)
elif param_name:
ax.set_title(param_name, fontsize=13, fontweight='bold', pad=10)
# ── 颜色条 ─────────────────────────────────────────────────── # ── 颜色条工业级样式extend 三角 + MaxNLocator 刻度防重叠)─────────
if show_colorbar and im is not None: if show_colorbar and im is not None:
try: 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) cbar.set_label('Index Value', fontsize=10)
if data_range > 1e-9: cbar.locator = MaxNLocator(nbins=6)
ticks = np.linspace(vmin, vmax, 6) cbar.update_ticks()
cbar.set_ticks(ticks)
cbar.set_ticklabels([f'{t:.3f}' for t in ticks])
print("[visualize_raster] 颜色条添加成功") print("[visualize_raster] 颜色条添加成功")
except Exception as e: except Exception as e:
print(f"[visualize_raster] 颜色条添加失败: {e}") print(f"[visualize_raster] 颜色条添加失败: {e}")

View File

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

View File

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