diff --git a/src/gui/panels/step13_report_panel.py b/src/gui/panels/step13_report_panel.py index 4108f7b..f16a4df 100644 --- a/src/gui/panels/step13_report_panel.py +++ b/src/gui/panels/step13_report_panel.py @@ -14,7 +14,7 @@ from src.gui.panels._step_path_resolver import resolve_subdir from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSettings from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, - QLabel, QCheckBox, QPushButton, QLineEdit, + QLabel, QCheckBox, QPushButton, QLineEdit, QComboBox, QMessageBox, QFileDialog, QProgressBar, ) @@ -33,12 +33,14 @@ class ReportWorkerThread(QThread): finished = pyqtSignal(str) error = pyqtSignal(str) - def __init__(self, work_dir: str, output_dir: Optional[str], report_title: str, enable_ai: bool): + def __init__(self, work_dir: str, output_dir: Optional[str], report_title: str, + enable_ai: bool, report_mode: str = 'ml'): super().__init__() self.work_dir = work_dir self.output_dir = output_dir self.report_title = report_title self.enable_ai = enable_ai + self.report_mode = report_mode def run(self): try: @@ -76,6 +78,7 @@ class ReportWorkerThread(QThread): out_path = gen.generate_report( work_dir=self.work_dir, report_title=self.report_title or "水质参数反演分析报告", + report_mode=self.report_mode, on_progress=lambda pct, text: self.progress.emit(int(pct), str(text)), ) self.finished.emit(str(out_path)) @@ -257,6 +260,21 @@ class Step13ReportPanel(QWidget): """) execute_layout.addWidget(self.progress_bar) + # 报告模式选择 + mode_row = QHBoxLayout() + mode_row.setContentsMargins(0, 4, 0, 0) + mode_row.setSpacing(10) + mode_label = QLabel("报告模式:") + mode_label.setMinimumWidth(120) + mode_label.setMaximumWidth(120) + mode_row.addWidget(mode_label) + self.report_mode_cb = QComboBox() + self.report_mode_cb.addItem("机器学习水质参数反演 (Step 9/12/13)", "ml") + self.report_mode_cb.addItem("水色指数与物理经验公式反演 (Step 10/11)", "formula") + self.report_mode_cb.setStyleSheet(common_lineedit_css) + mode_row.addWidget(self.report_mode_cb, 1) + execute_layout.addLayout(mode_row) + action_layout = QHBoxLayout() action_layout.addStretch() @@ -394,7 +412,8 @@ class Step13ReportPanel(QWidget): self.progress_bar.setValue(0) self.progress_label.setText("正在准备生成…") - self._report_thread = ReportWorkerThread(wd, out, title, enable_ai) + report_mode = self.report_mode_cb.currentData() + self._report_thread = ReportWorkerThread(wd, out, title, enable_ai, report_mode) self._report_thread.progress.connect(self._on_progress, Qt.QueuedConnection) self._report_thread.finished.connect(self._on_finished, Qt.QueuedConnection) self._report_thread.error.connect(self._on_error, Qt.QueuedConnection) diff --git a/src/postprocessing/report_word.py b/src/postprocessing/report_word.py index 0f677cf..a500790 100644 --- a/src/postprocessing/report_word.py +++ b/src/postprocessing/report_word.py @@ -739,10 +739,16 @@ class WaterQualityReportGenerator: if not self.enable_ai_analysis: return "(AI分析已关闭)" - # 构造统计数据文本 + # 构造统计数据文本(防御式 .get() 防 KeyError) stats_text = "水质参数统计摘要:\n" for stat in stats_data: - stats_text += f"- {stat['参数']}: 点位数={stat['点位数']}, 范围=[{stat['最小值']}, {stat['最大值']}], 均值={stat['平均值']}, 标准差={stat['标准差']}\n" + param = stat.get('参数', stat.get('Parameter', stat.get('name', '未知'))) + count = stat.get('点位数', stat.get('数量', stat.get('count', '?'))) + min_v = stat.get('最小值', stat.get('min', stat.get('Min', '?'))) + max_v = stat.get('最大值', stat.get('max', stat.get('Max', '?'))) + mean_v = stat.get('平均值', stat.get('mean', stat.get('Mean', '?'))) + std_v = stat.get('标准差', stat.get('std', stat.get('Std', '?'))) + stats_text += f"- {param}: 点位数={count}, 范围=[{min_v}, {max_v}], 均值={mean_v}, 标准差={std_v}\n" system = """你是一位水质遥感与统计分析专家。 请基于提供的统计数据,给出专业分析: @@ -764,16 +770,16 @@ class WaterQualityReportGenerator: parameters: List[str] = None, report_title: str = "水质参数反演分析报告", output_path: Optional[str] = None, + report_mode: str = 'ml', on_progress=None) -> str: """ - 生成 Word 报告 - 所有数据均来自工作目录(work_dir) - 可视化图片、统计数据等均从 work_dir/12_visualization 和 work_dir/4_processed_data 中读取 + 生成 Word 报告(双轨制入口) Args: - on_progress: 可选回调,签名 on_progress(percent: int, text: str)。 - 会在进度更新时被调用,用于驱动 Qt QProgressBar/QThread 信号。 + report_mode: 'ml' → 机器学习报告, 'formula' → 水色指数/物理公式报告 + on_progress: 可选回调 on_progress(percent: int, text: str) """ - # 设置工作目录(整个流程的核心) + # 设置工作目录 if work_dir is not None: self.work_dir = Path(work_dir) self.visualization_dir = self.work_dir / "12_visualization" @@ -781,29 +787,26 @@ class WaterQualityReportGenerator: self.output_dir = self.visualization_dir self.output_dir.mkdir(parents=True, exist_ok=True) self.ai_cache_path = self.output_dir / "ollama_image_analyses_cache.json" - - if parameters is None: - parameters = ["Chlorophyll", "COD", "DO", "PH", "Temperature", - "spCond", "Turbidity", "TDS", "Cl-", "NO3-N", - "NH3-N", "BGA", "TT"] - - vis_dir = self.visualization_dir - - if not vis_dir.exists(): - raise FileNotFoundError(f"可视化目录不存在: {vis_dir}") - # ── 管线模式自动检测 ── - self._pipeline_mode = self._detect_pipeline_mode(vis_dir, parameters) - print(f"[报告] 管线模式: {self._pipeline_mode}") - if self._pipeline_mode == "water_index": - # 水色指数模式:只保留实际存在的图片类型 - _available_types = self._get_available_image_types(vis_dir, parameters) - self.parameter_images = { - p: [f"{p}_{t}.png" for t in _available_types] - for p in parameters - } - print(f"[报告] 水色指数可用图片类型: {_available_types}") - + if not self.visualization_dir.exists(): + raise FileNotFoundError(f"可视化目录不存在: {self.visualization_dir}") + + if parameters is None: + parameters = ["Chlorophyll", "COD", "DO", "PH", "Temperature", + "spCond", "Turbidity", "TDS", "Cl-", "NO3-N", + "NH3-N", "BGA", "TT"] + + self._pipeline_mode = 'water_index' if report_mode == 'formula' else 'ml' + print(f"[报告] 报告模式: {report_mode}") + + if report_mode == 'formula': + return self._generate_formula_report(parameters, report_title, output_path, on_progress) + else: + return self._generate_ml_report(parameters, report_title, output_path, on_progress) + + def _generate_ml_report(self, parameters, report_title, output_path, on_progress): + """机器学习水质参数反演报告""" + vis_dir = self.visualization_dir if output_path is None: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = self.output_dir / f"水质参数反演分析报告_{timestamp}.docx" @@ -924,7 +927,165 @@ class WaterQualityReportGenerator: print(f"✅ Word报告生成完成: {output_path}") return str(output_path) - + + def _generate_formula_report(self, parameters, report_title, output_path, on_progress): + """水色指数与物理经验公式反演报告 (Step 10/11)""" + from docx.shared import Inches, Pt, Cm, RGBColor + from docx.enum.text import WD_ALIGN_PARAGRAPH + + vis_dir = self.visualization_dir + thematic_dir = self.work_dir / "11_Thematic_Map" + csv_dir = self.work_dir / "10_WaterIndex_CSV" + + if output_path is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + output_path = self.output_dir / f"水色指数反演分析报告_{timestamp}.docx" + else: + output_path = Path(output_path) + + # 进度: 封面+背景+预处理+公式表+统计+分布图+总结 = 7 步 + total_steps = 7 + progress = self._create_progress(total=total_steps, desc="生成公式报告", on_step=on_progress) + + doc = Document() + section = doc.sections[0] + section.page_width = Cm(21) + section.page_height = Cm(29.7) + section.left_margin = Cm(2.5) + section.right_margin = Cm(2.5) + section.top_margin = Cm(2.5) + section.bottom_margin = Cm(2.5) + + # ── 封面 ── + self._add_cover_page(doc, report_title, "水色指数 / 物理经验公式反演分析") + try: progress.update(1) + except Exception: pass + + # ── 1. 项目背景 ── + h1 = doc.add_heading("1 项目背景", level=1) + self._style_heading(h1, 1) + doc.add_paragraph("本报告基于高光谱遥感影像,通过物理经验公式(水色指数)反演水体关键参数的空间分布。" + "涵盖叶绿素、蓝绿藻、浊度、CDOM 等多种水色指标的定量化空间制图。") + doc.add_paragraph("数据来源:机载 / 星载高光谱成像仪,经过辐射定标、大气校正、耀斑去除等预处理。") + doc.add_page_break() + + # ── 2. 影像预处理 ── + h1 = doc.add_heading("2 影像预处理", level=1) + self._style_heading(h1, 1) + self._add_hyperspectral_images_section(doc, vis_dir, start_figure_num=1) + doc.add_page_break() + + # ── 3. 水色指数公式列表 ── + h1 = doc.add_heading("3 水色指数计算方法", level=1) + self._style_heading(h1, 1) + if csv_dir.is_dir(): + csv_files = sorted(csv_dir.glob("*.csv")) + doc.add_paragraph(f"本次反演共应用 {len(csv_files)} 个水色指数公式," + f"各公式基于特征波段比值或差分原理计算:") + for i, cf in enumerate(csv_files[:30], 1): # 最多列出30个 + doc.add_paragraph(f" {i}. {cf.stem}", style='List Number') + if len(csv_files) > 30: + doc.add_paragraph(f" ... 共 {len(csv_files)} 个公式,详情见统计章节。") + else: + doc.add_paragraph(f"(未找到水色指数目录: {csv_dir})") + doc.add_page_break() + + # ── 4. 指数统计结果 ── + h1 = doc.add_heading("4 水色指数统计结果", level=1) + self._style_heading(h1, 1) + figure_num = 10 + if csv_dir.is_dir(): + csv_files = sorted(csv_dir.glob("*.csv")) + stats_rows = [] + for cp in csv_files: + try: + df_one = pd.read_csv(cp, sep=',') + val_col = df_one.columns[-1] + vals = pd.to_numeric(df_one[val_col], errors='coerce').dropna() + if len(vals) > 0: + stats_rows.append({ + '参数': Path(cp).stem, '数量': len(vals), + '最小值': round(float(vals.min()), 4), + '最大值': round(float(vals.max()), 4), + '平均值': round(float(vals.mean()), 4), + '标准差': round(float(vals.std(ddof=0)), 4) if len(vals) > 1 else 0, + }) + except Exception: + pass + + if stats_rows: + # 统计表 + table = doc.add_table(rows=1, cols=6, style='Table Grid') + for j, h in enumerate(['公式名称', '数据量', '最小值', '最大值', '平均值', '标准差']): + table.rows[0].cells[j].text = h + for r in stats_rows: + cells = table.add_row().cells + cells[0].text = r['参数'] + cells[1].text = str(r['数量']) + cells[2].text = str(r['最小值']) + cells[3].text = str(r['最大值']) + cells[4].text = str(r['平均值']) + cells[5].text = str(r['标准差']) + doc.add_paragraph() + figure_num += 1 + + # AI 分析(防御式读取) + stats_data = [{ + '参数': r['参数'], '点位数': r['数量'], + '最小值': str(r['最小值']), '最大值': str(r['最大值']), + '平均值': str(r['平均值']), '标准差': str(r['标准差']), + } for r in stats_rows] + param_names = [r['参数'] for r in stats_rows] + analysis = self._analyze_statistics(stats_data, param_names) + self._add_ai_analysis_paragraph(doc, analysis) + else: + doc.add_paragraph("未能解析水色指数 CSV 数据。") + else: + doc.add_paragraph(f"(未找到水色指数目录: {csv_dir})") + doc.add_page_break() + try: progress.update(1) + except Exception: pass + + # ── 5. 指数空间分布专题图 ── + h1 = doc.add_heading("5 水色指数空间分布专题图", level=1) + self._style_heading(h1, 1) + figure_num += 1 + maps_found = 0 + if thematic_dir.is_dir(): + tifs = sorted(thematic_dir.glob("*.tif")) + sorted(thematic_dir.glob("*.png")) + for tf in tifs[:20]: # 最多展示 20 张 + try: + param_name = tf.stem.split('_')[0] if '_' in tf.stem else tf.stem + caption = f"图{figure_num} {param_name} 空间分布图" + self._add_image_with_caption(doc, str(tf), caption, width=Inches(5.5)) + figure_num += 1 + maps_found += 1 + except Exception as e: + doc.add_paragraph(f"[专题图插入失败: {tf.name} — {e}]") + if len(tifs) > 20: + doc.add_paragraph(f"... 共 {len(tifs)} 张专题图,此处仅展示前 20 张。") + else: + doc.add_paragraph(f"(未找到专题图目录: {thematic_dir})") + if maps_found == 0: + doc.add_paragraph("(未找到专题图文件,请确认 Step 11 已完成。)") + doc.add_page_break() + try: progress.update(1) + except Exception: pass + + # ── 6. 综合总结 ── + h1 = doc.add_heading("6 综合总结", level=1) + self._style_heading(h1, 1) + doc.add_paragraph("本报告基于高光谱遥感影像的水色指数反演方法,对研究区域内的关键水质参数" + "进行了定量化空间制图。各指数的统计结果和空间分布专题图如上所示。") + doc.add_paragraph("注意事项:水色指数反演结果为半定量指标,其绝对值可能受大气校正精度、" + "水体光学特性复杂性等因素影响。建议结合实测水质数据进行校验。") + try: progress.update(1) + except Exception: pass + + doc.save(str(output_path)) + print(f"[公式报告] 生成完成: {output_path}") + return str(output_path) + def _add_parameter_section( self, doc, param: str, vis_dir: Path, param_index: int = 1, start_figure_num: int = 1, all_image_analyses: Optional[List[Dict[str, Any]]] = None, progress=None,