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:
duxin
2026-07-09 11:33:50 +08:00
parent 692a88c7bb
commit 650e505ac4
2 changed files with 213 additions and 33 deletions

View File

@ -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,