feat: 报告生成模块双轨制重构 — 公式模式 + ML 模式独立分流
**UI (step13_report_panel.py):**
- 新增 QComboBox 报告模式选择:
机器学习(ml) / 水色指数公式(formula)
- WorkerThread 传递 report_mode 到 generate_report
**后端 (report_word.py):**
- generate_report(report_mode='ml'/'formula') 参数路由
- _generate_formula_report(): 水色指数专属结构
1.项目背景 → 2.影像预处理 → 3.公式列表 →
4.指数统计(10_WaterIndex_CSV) → 5.分布专题图(11_Thematic_Map)
绝不包含模型训练/R²/RMSE 等 ML 内容
- _generate_ml_report(): 原有 ML 逻辑完整保留
- _analyze_statistics(): 全部改用 .get() 防御式读取
兼容 参数/Parameter/name, 点位数/数量/count,
最小值/min, 最大值/max, 平均值/mean, 标准差/std
This commit is contained in:
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user