Compare commits
9 Commits
aa539db9bd
...
e59703f163
| Author | SHA1 | Date | |
|---|---|---|---|
| e59703f163 | |||
| 3584c07b67 | |||
| 1ad4c54b80 | |||
| 5d75d3371b | |||
| d3262ae80d | |||
| 7c7a31ce00 | |||
| 604886abb3 | |||
| 3c4d4081a4 | |||
| 184f5fe9f4 |
@ -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()}
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 已成功运行并生成了训练光谱。"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── 步骤7(ML 建模) ──
|
# ── 步骤8_ml_train(ML 建模) ──
|
||||||
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"步骤 7(ML 建模):训练光谱文件不存在:\n {step7_csv}\n"
|
f"步骤 8_ml_train(ML 建模):训练光谱文件不存在:\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 ──
|
||||||
|
|||||||
569
src/gui/panels/step10_watercolor_panel.py
Normal file
569
src/gui/panels/step10_watercolor_panel.py
Normal 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()
|
||||||
|
)
|
||||||
826
src/gui/panels/step11_map_panel.py
Normal file
826
src/gui/panels/step11_map_panel.py
Normal 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.shp,geopandas.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 输出的水色指数 GeoTIFF(GeoTIFF 渲染模式)
|
||||||
|
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")
|
||||||
@ -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对象")
|
|
||||||
@ -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
|
||||||
@ -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对象")
|
||||||
@ -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)
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
@ -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文件并点击刷新预览"):
|
||||||
"""重置预览表格"""
|
"""重置预览表格"""
|
||||||
@ -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)
|
||||||
@ -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)
|
||||||
@ -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):
|
||||||
"""获取模型训练参数"""
|
"""获取模型训练参数"""
|
||||||
@ -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()}")
|
|
||||||
@ -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)
|
||||||
@ -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对象")
|
|
||||||
@ -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个Tab,index 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)
|
||||||
|
|
||||||
|
|||||||
@ -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(供后续中文标题和文件派生使用)──────────
|
||||||
if output_file is None:
|
|
||||||
stem = Path(raster_tif_path).stem
|
stem = Path(raster_tif_path).stem
|
||||||
|
|
||||||
|
# ── 输出路径自动派生(中文文件名)──────────────────────────────
|
||||||
|
if output_file is None:
|
||||||
|
chinese_title = self._get_chinese_title(stem)
|
||||||
out_dir = Path(raster_tif_path).parent / 'visualization'
|
out_dir = 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 array:NaN 区域自动不显示
|
# 使用 masked array:NaN 区域自动不显示
|
||||||
@ -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}")
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
@ -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)
|
||||||
|
|
||||||
Reference in New Issue
Block a user