From fa9c9400748315bc4965bee7f78208f1379acf01 Mon Sep 17 00:00:00 2001 From: DXC Date: Wed, 10 Jun 2026 09:41:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(visualization+report):=20=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=20Step9=20=E6=B5=93=E5=BA=A6=E5=8F=8D=E6=BC=94?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=87=B3=E5=8F=AF=E8=A7=86=E5=8C=96=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E4=B8=8E=E6=8A=A5=E5=91=8A=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/panels/visualization_panel.py | 60 ++++++++++++++++++ src/postprocessing/report_word.py | 91 ++++++++++++++++++++++++++- 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/src/gui/panels/visualization_panel.py b/src/gui/panels/visualization_panel.py index 7064ed8..bf24411 100644 --- a/src/gui/panels/visualization_panel.py +++ b/src/gui/panels/visualization_panel.py @@ -359,6 +359,64 @@ class VisualizationWorkerThread(QThread): parts.append("采样点图: 跳过(无CSV)") else: parts.append("采样点图: 跳过(无影像)") + + if self.extra.get("gen_concentration"): + conc_dir = wp / "9_Concentration" + conc_csv = conc_dir / "final_concentrations.csv" + if conc_csv.is_file(): + charts_dir = conc_dir / "charts" + charts_dir.mkdir(parents=True, exist_ok=True) + try: + import pandas as pd + df = pd.read_csv(conc_csv) + exclude_kw = ( + "wavelength", "lon", "lat", "utm_x", "utm_y", + "x", "y", "coord", "longitude", "latitude", + "sample_id", "id", "index", "name", "pixel", + ) + conc_cols = [ + c for c in df.select_dtypes(include=[np.number]).columns + if not any(k in str(c).lower() for k in exclude_kw) + ] + if conc_cols: + orig_out = viz.output_dir + viz.output_dir = str(charts_dir) + output_dict = viz.plot_statistical_charts( + csv_path=str(conc_csv), + parameter_columns=conc_cols, + ) + viz.output_dir = orig_out + count = len([v for v in output_dict.values() if v]) + parts.append(f"浓度统计图: {count} 个") + stats_rows = [] + for col in conc_cols: + s = df[col].dropna() + if len(s) == 0: + continue + stats_rows.append({ + "参数": col, + "点位数": len(s), + "最小值": round(float(s.min()), 4), + "最大值": round(float(s.max()), 4), + "均值": round(float(s.mean()), 4), + "中位数": round(float(s.median()), 4), + "标准差": round(float(s.std()), 4) if len(s) > 1 else 0.0, + }) + if stats_rows: + pd.DataFrame(stats_rows).to_csv( + conc_dir / "statistics_summary.csv", + index=False, + encoding="utf-8-sig", + ) + parts.append("浓度统计表: 已生成") + else: + parts.append("浓度统计表: 跳过(无可用列)") + else: + parts.append("浓度统计图: 跳过(无可用浓度列)") + except Exception as e: + parts.append(f"浓度统计图: 失败({e})") + else: + parts.append("浓度统计图: 跳过(无浓度CSV)") self.finished_ok.emit({"task": "generate_all_selected", "parts": parts}) else: self.failed.emit(f"未知可视化任务: {self.task}") @@ -568,6 +626,7 @@ class ImageCategoryTree(QTreeWidget): "output_dir": "输出目录", "8_spatial_inversion": "空间反演", "4_processed_data": "处理数据", + "9_Concentration": "物理反演浓度分布", } def __init__(self, parent=None): @@ -767,6 +826,7 @@ class ImageCategoryTree(QTreeWidget): self._work_path / "3_deglint", self._work_path / "1_water_mask", self._work_path / "9_water_quality_prediction", + self._work_path / "9_Concentration", ] # 只保留存在的目录,并补充工作根目录作为兜底 diff --git a/src/postprocessing/report_word.py b/src/postprocessing/report_word.py index a572b9f..09d0266 100644 --- a/src/postprocessing/report_word.py +++ b/src/postprocessing/report_word.py @@ -834,7 +834,12 @@ class WaterQualityReportGenerator: figure_counter = self._add_result_analysis_section( doc, vis_dir, figure_counter, all_image_analyses, progress=progress ) - + + # 物理模型反演浓度统计与分析(第4.1节) + figure_counter = self._add_physical_inversion_section( + doc, self.work_dir, figure_counter, all_image_analyses, progress=progress + ) + # 设置页眉和页码(从正文开始) self._setup_header_and_footer(section) @@ -1747,6 +1752,90 @@ class WaterQualityReportGenerator: doc.add_page_break() return start_figure_num + (1 if heatmap_path.exists() else 0) + def _add_physical_inversion_section( + self, + doc: Document, + work_dir: Path, + start_figure_num: int = 1, + all_image_analyses: Optional[List[Dict[str, Any]]] = None, + progress=None, + ) -> int: + """新增章节:物理模型反演浓度统计与分析(第4节之后)""" + conc_dir = work_dir / "9_Concentration" + if not conc_dir.is_dir(): + doc.add_paragraph("[物理反演浓度章节:9_Concentration 目录不存在,已跳过]") + return start_figure_num + + stats_csv = conc_dir / "statistics_summary.csv" + charts_dir = conc_dir / "charts" + + h = doc.add_heading("4.1 物理模型反演浓度统计与分析", level=2) + self._style_heading(h, level=2) + + fig_num = start_figure_num + + if stats_csv.is_file(): + try: + stats_df = pd.read_csv(stats_csv) + table = doc.add_table(rows=1, cols=len(stats_df.columns)) + table.style = "Table Grid" + hdr_cells = table.rows[0].cells + for i, col_name in enumerate(stats_df.columns): + hdr_cells[i].text = str(col_name) + for run in hdr_cells[i].paragraphs[0].runs: + run.font.name = self.chinese_font + run.font.size = Pt(10) + run.font.bold = True + run._element.rPr.rFonts.set(qn('w:eastAsia'), self.chinese_font) + for _, row_data in stats_df.iterrows(): + row_cells = table.add_row().cells + for i, val in enumerate(row_data): + row_cells[i].text = str(val) + for run in row_cells[i].paragraphs[0].runs: + run.font.name = self.chinese_font + run.font.size = Pt(10) + run._element.rPr.rFonts.set(qn('w:eastAsia'), self.chinese_font) + doc.add_paragraph() + except Exception as e: + doc.add_paragraph(f"[浓度统计表插入失败: {e}]") + else: + doc.add_paragraph("[浓度统计表不存在: statistics_summary.csv]") + + if charts_dir.is_dir(): + image_extensions = ['*.png', '*.jpg', '*.jpeg', '*.tif', '*.tiff'] + chart_files: List[Path] = [] + for ext in image_extensions: + chart_files.extend(sorted(charts_dir.glob(ext))) + for chart_file in chart_files: + caption_text = f"图{fig_num} {chart_file.stem} 分布图" + if self._add_image_with_caption(doc, str(chart_file), caption_text, width=Inches(5.5)): + if all_image_analyses is not None: + image_type = "boxplot" if "boxplot" in chart_file.stem.lower() else "distribution" + analysis_text = self._analyze_and_cache_image( + image_path=chart_file, + image_type=image_type, + param=chart_file.stem, + figure_num=fig_num, + ) + self._add_ai_analysis_paragraph(doc, analysis_text) + all_image_analyses.append({ + "figure_num": fig_num, + "param": chart_file.stem, + "image_type": image_type, + "image_name": chart_file.name, + "analysis": analysis_text, + }) + fig_num += 1 + try: + if progress is not None: + progress.update(1) + except Exception: + pass + else: + doc.add_paragraph("[浓度图表目录不存在: 9_Concentration/charts/]") + + return fig_num + # ==================== 使用示例 ==================== def generate_full_water_quality_report(