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.QtCore import Qt, QThread, pyqtSignal, QSettings
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout,
|
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout,
|
||||||
QLabel, QCheckBox, QPushButton, QLineEdit,
|
QLabel, QCheckBox, QPushButton, QLineEdit, QComboBox,
|
||||||
QMessageBox, QFileDialog, QProgressBar,
|
QMessageBox, QFileDialog, QProgressBar,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,12 +33,14 @@ class ReportWorkerThread(QThread):
|
|||||||
finished = pyqtSignal(str)
|
finished = pyqtSignal(str)
|
||||||
error = 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__()
|
super().__init__()
|
||||||
self.work_dir = work_dir
|
self.work_dir = work_dir
|
||||||
self.output_dir = output_dir
|
self.output_dir = output_dir
|
||||||
self.report_title = report_title
|
self.report_title = report_title
|
||||||
self.enable_ai = enable_ai
|
self.enable_ai = enable_ai
|
||||||
|
self.report_mode = report_mode
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
@ -76,6 +78,7 @@ class ReportWorkerThread(QThread):
|
|||||||
out_path = gen.generate_report(
|
out_path = gen.generate_report(
|
||||||
work_dir=self.work_dir,
|
work_dir=self.work_dir,
|
||||||
report_title=self.report_title or "水质参数反演分析报告",
|
report_title=self.report_title or "水质参数反演分析报告",
|
||||||
|
report_mode=self.report_mode,
|
||||||
on_progress=lambda pct, text: self.progress.emit(int(pct), str(text)),
|
on_progress=lambda pct, text: self.progress.emit(int(pct), str(text)),
|
||||||
)
|
)
|
||||||
self.finished.emit(str(out_path))
|
self.finished.emit(str(out_path))
|
||||||
@ -257,6 +260,21 @@ class Step13ReportPanel(QWidget):
|
|||||||
""")
|
""")
|
||||||
execute_layout.addWidget(self.progress_bar)
|
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 = QHBoxLayout()
|
||||||
action_layout.addStretch()
|
action_layout.addStretch()
|
||||||
|
|
||||||
@ -394,7 +412,8 @@ class Step13ReportPanel(QWidget):
|
|||||||
self.progress_bar.setValue(0)
|
self.progress_bar.setValue(0)
|
||||||
self.progress_label.setText("正在准备生成…")
|
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.progress.connect(self._on_progress, Qt.QueuedConnection)
|
||||||
self._report_thread.finished.connect(self._on_finished, Qt.QueuedConnection)
|
self._report_thread.finished.connect(self._on_finished, Qt.QueuedConnection)
|
||||||
self._report_thread.error.connect(self._on_error, Qt.QueuedConnection)
|
self._report_thread.error.connect(self._on_error, Qt.QueuedConnection)
|
||||||
|
|||||||
@ -739,10 +739,16 @@ class WaterQualityReportGenerator:
|
|||||||
if not self.enable_ai_analysis:
|
if not self.enable_ai_analysis:
|
||||||
return "(AI分析已关闭)"
|
return "(AI分析已关闭)"
|
||||||
|
|
||||||
# 构造统计数据文本
|
# 构造统计数据文本(防御式 .get() 防 KeyError)
|
||||||
stats_text = "水质参数统计摘要:\n"
|
stats_text = "水质参数统计摘要:\n"
|
||||||
for stat in stats_data:
|
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 = """你是一位水质遥感与统计分析专家。
|
system = """你是一位水质遥感与统计分析专家。
|
||||||
请基于提供的统计数据,给出专业分析:
|
请基于提供的统计数据,给出专业分析:
|
||||||
@ -764,16 +770,16 @@ class WaterQualityReportGenerator:
|
|||||||
parameters: List[str] = None,
|
parameters: List[str] = None,
|
||||||
report_title: str = "水质参数反演分析报告",
|
report_title: str = "水质参数反演分析报告",
|
||||||
output_path: Optional[str] = None,
|
output_path: Optional[str] = None,
|
||||||
|
report_mode: str = 'ml',
|
||||||
on_progress=None) -> str:
|
on_progress=None) -> str:
|
||||||
"""
|
"""
|
||||||
生成 Word 报告 - 所有数据均来自工作目录(work_dir)
|
生成 Word 报告(双轨制入口)
|
||||||
可视化图片、统计数据等均从 work_dir/12_visualization 和 work_dir/4_processed_data 中读取
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
on_progress: 可选回调,签名 on_progress(percent: int, text: str)。
|
report_mode: 'ml' → 机器学习报告, 'formula' → 水色指数/物理公式报告
|
||||||
会在进度更新时被调用,用于驱动 Qt QProgressBar/QThread 信号。
|
on_progress: 可选回调 on_progress(percent: int, text: str)
|
||||||
"""
|
"""
|
||||||
# 设置工作目录(整个流程的核心)
|
# 设置工作目录
|
||||||
if work_dir is not None:
|
if work_dir is not None:
|
||||||
self.work_dir = Path(work_dir)
|
self.work_dir = Path(work_dir)
|
||||||
self.visualization_dir = self.work_dir / "12_visualization"
|
self.visualization_dir = self.work_dir / "12_visualization"
|
||||||
@ -781,29 +787,26 @@ class WaterQualityReportGenerator:
|
|||||||
self.output_dir = self.visualization_dir
|
self.output_dir = self.visualization_dir
|
||||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.ai_cache_path = self.output_dir / "ollama_image_analyses_cache.json"
|
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}")
|
|
||||||
|
|
||||||
# ── 管线模式自动检测 ──
|
if not self.visualization_dir.exists():
|
||||||
self._pipeline_mode = self._detect_pipeline_mode(vis_dir, parameters)
|
raise FileNotFoundError(f"可视化目录不存在: {self.visualization_dir}")
|
||||||
print(f"[报告] 管线模式: {self._pipeline_mode}")
|
|
||||||
if self._pipeline_mode == "water_index":
|
if parameters is None:
|
||||||
# 水色指数模式:只保留实际存在的图片类型
|
parameters = ["Chlorophyll", "COD", "DO", "PH", "Temperature",
|
||||||
_available_types = self._get_available_image_types(vis_dir, parameters)
|
"spCond", "Turbidity", "TDS", "Cl-", "NO3-N",
|
||||||
self.parameter_images = {
|
"NH3-N", "BGA", "TT"]
|
||||||
p: [f"{p}_{t}.png" for t in _available_types]
|
|
||||||
for p in parameters
|
self._pipeline_mode = 'water_index' if report_mode == 'formula' else 'ml'
|
||||||
}
|
print(f"[报告] 报告模式: {report_mode}")
|
||||||
print(f"[报告] 水色指数可用图片类型: {_available_types}")
|
|
||||||
|
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:
|
if output_path is None:
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
output_path = self.output_dir / f"水质参数反演分析报告_{timestamp}.docx"
|
output_path = self.output_dir / f"水质参数反演分析报告_{timestamp}.docx"
|
||||||
@ -924,7 +927,165 @@ class WaterQualityReportGenerator:
|
|||||||
print(f"✅ Word报告生成完成: {output_path}")
|
print(f"✅ Word报告生成完成: {output_path}")
|
||||||
|
|
||||||
return str(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(
|
def _add_parameter_section(
|
||||||
self, doc, param: str, vis_dir: Path, param_index: int = 1,
|
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,
|
start_figure_num: int = 1, all_image_analyses: Optional[List[Dict[str, Any]]] = None, progress=None,
|
||||||
|
|||||||
Reference in New Issue
Block a user