Compare commits

...

19 Commits

Author SHA1 Message Date
600400a4d9 fix: step13 auto-detect report mode on panel load (in _auto_pull_work_dir), not just on generate click 2026-07-09 15:38:47 +08:00
208e067334 feat: step13 auto-detect mode + user confirmation combo (with fallback to manual override) 2026-07-09 15:33:45 +08:00
66a181cbe2 feat: step13 auto-detect ML vs formula mode like step11, remove manual selector 2026-07-09 15:21:45 +08:00
3f4cb94929 fix: clean up step13 mode selector labels - remove misleading step numbers 2026-07-09 15:18:55 +08:00
a13b4be6d4 feat: formula report AI analysis per distribution map with configurable limit in UI panel 2026-07-09 15:03:39 +08:00
683cf77da5 fix: restore generic sections and AI analysis in formula report 2026-07-09 15:00:25 +08:00
a3e83e846a fix: remove leftover glint_analysis reference causing NameError 2026-07-09 14:52:50 +08:00
a572b52239 fix: formula report reads distribution maps from 12_visualization/distribution_maps/ instead of 11_Thematic_Map 2026-07-09 14:49:36 +08:00
5fe71917a6 fix: hyperspectral images section - skip missing instead of showing placeholder text, fix flight path rglob scope 2026-07-09 14:47:44 +08:00
f99fa63f0e fix: remove duplicate hyperspectral images and conflicting generic sections from formula report 2026-07-09 14:45:53 +08:00
8a7273dd57 fix: 公式报告进度步数用实际脉冲数而非原始数据量 (87→30)
旧: total_steps = 4 + 63 + 20 = 87 (但大部分步骤不发脉冲)
新: total_steps = 4 + 63//10 + 20 = 30 (与 _next 调用次数一致)
2026-07-09 13:27:47 +08:00
dd04ee323c fix: ML 报告进度增加 on_progress 回调 + 两条报告逻辑审查确认
ML 报告: 参数循环中加入 on_progress 回调,
  终端显示 ML报告 参数 3/13: Chlorophyll

审查结论:
  公式报告 (_generate_formula_report):
    数据源: 10_WaterIndex_CSV + 11_Thematic_Map + 1_water_mask
    不含 ML 内容 (无 scatter/boxplot/heatmap/R²/RMSE)
    进度: 数据驱动 (N_csv + N_maps + 4 固定)

  ML 报告 (_generate_ml_report):
    数据源: 12_visualization + 5_Data_Cleaning + 9_Concentration
    完整 ML 流程 (统计表→热力图→浓度→逐参数图表→总结)
    进度: 图片数 + 参数数 + 2
2026-07-09 13:26:09 +08:00
836c322b10 fix: 公式报告进度数据驱动 — 按实际 CSV/专题图数量计算总步数
旧: total_steps=7 硬编码, 看不到实际数据处理进度
新: 启动时扫描 10_WaterIndex_CSV 和 11_Thematic_Map,
  总步数 = 4(固定章节) + N_csv + N_maps,
  每处理 10% CSV 和每张专题图均更新进度,
  终端显示 统计 7/63 / 专题图 3/20 等实时信息
2026-07-09 13:21:32 +08:00
b88bab57ee fix: 公式报告 4 项修复 — 通用章节+AI接口+图片路径+标题数字
修复一: _generate_formula_report 封面后恢复
  _add_company_description_page/doc
  _add_data_acquisition_section/doc
  _add_data_processing_section/doc

修复二: _call_minimax_text / _call_minimax_vision
  URL 自动补齐 /chat/completions 后缀

修复三: _call_minimax_vision
  MIME 类型动态检测 (.png→image/png, 其他→image/jpeg)

修复四: _add_hyperspectral_images_section
  航线图搜索: 多路径 + rglob 递归查找
  去掉所有硬编码编号 (3.1/图3-2/3-3/3-4/3-5/3-6)
2026-07-09 13:12:14 +08:00
fc6036efb7 fix: _add_hyperspectral_images_section 去掉多余的 start_figure_num 参数 2026-07-09 11:53:23 +08:00
43d56e74d9 fix: _add_cover_page 参数对齐 + 确认 _generate_formula_report 无 ML 混入 2026-07-09 11:42:32 +08:00
650e505ac4 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
2026-07-09 11:33:50 +08:00
692a88c7bb fix: heatmap_path 提前声明避免水色指数模式下 UnboundLocalError 2026-07-09 11:22:50 +08:00
1740425152 fix: 报告水色指数模式 stats_data 补全缺失的统计字段
水色指数模式 stats_data 只复制了 参数/点位数,
  缺少 最小值/最大值/平均值/标准差 →
  _analyze_statistics 访问时 KeyError: '最小值'
2026-07-09 11:17:54 +08:00
2 changed files with 395 additions and 143 deletions

View File

@ -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, QSpinBox,
QMessageBox, QFileDialog, QProgressBar,
)
@ -33,12 +33,15 @@ 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 = 'auto', ai_maps_limit: int = 999):
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
self.ai_maps_limit = ai_maps_limit
def run(self):
try:
@ -76,6 +79,8 @@ class ReportWorkerThread(QThread):
out_path = gen.generate_report(
work_dir=self.work_dir,
report_title=self.report_title or "水质参数反演分析报告",
report_mode=self.report_mode,
ai_maps_limit=self.ai_maps_limit,
on_progress=lambda pct, text: self.progress.emit(int(pct), str(text)),
)
self.finished.emit(str(out_path))
@ -257,6 +262,37 @@ 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("机器学习水质参数反演", "ml")
self.report_mode_cb.addItem("水色指数与物理经验公式反演", "formula")
self.report_mode_cb.setStyleSheet(common_lineedit_css)
mode_row.addWidget(self.report_mode_cb, 1)
execute_layout.addLayout(mode_row)
# AI 专题图分析数量(仅水色指数公式模式生效,ML 模式忽略)
ai_maps_row = QHBoxLayout()
ai_maps_row.setContentsMargins(0, 0, 0, 0)
ai_maps_row.setSpacing(10)
ai_maps_label = QLabel("AI分析专题图数:")
ai_maps_label.setMinimumWidth(120)
ai_maps_label.setMaximumWidth(120)
ai_maps_row.addWidget(ai_maps_label)
self.ai_maps_spin = QSpinBox()
self.ai_maps_spin.setRange(0, 999)
self.ai_maps_spin.setValue(999)
self.ai_maps_spin.setToolTip("公式报告中启用AI分析的分布图数量上限(0=跳过,999=全部分析)")
self.ai_maps_spin.setStyleSheet(common_lineedit_css)
ai_maps_row.addWidget(self.ai_maps_spin, 1)
execute_layout.addLayout(ai_maps_row)
action_layout = QHBoxLayout()
action_layout.addStretch()
@ -326,13 +362,25 @@ class Step13ReportPanel(QWidget):
self.work_dir_edit.setText(str(work_dir))
def _auto_pull_work_dir(self):
"""从主窗口自动同步工作目录到 work_dir_edit(无需用户操作)。"""
"""从主窗口自动同步工作目录到 work_dir_edit,并自动检测报告模式。"""
mw = self.main_window
if mw is not None and getattr(mw, "work_dir", None):
wd = str(mw.work_dir)
cur = self.work_dir_edit.text().strip()
if wd and wd != cur:
self.work_dir_edit.setText(wd)
# 自动检测报告模式(参考 Step11 逻辑)
self._auto_detect_report_mode(wd)
def _auto_detect_report_mode(self, wd: str):
"""根据工作目录内容自动选择报告模式下拉框"""
wd_path = Path(wd)
ml_dir = wd_path / "9_ML_Prediction"
formula_dir = wd_path / "10_WaterIndex_CSV"
if ml_dir.is_dir() and list(ml_dir.glob("*.csv")):
self.report_mode_cb.setCurrentIndex(0) # ML
elif formula_dir.is_dir() and list(formula_dir.glob("*.csv")):
self.report_mode_cb.setCurrentIndex(1) # 公式
def update_from_config(self, work_dir=None, pipeline=None):
"""切入面板时由主窗口统一调用,把当前 work_dir 同步到本面板。
@ -381,6 +429,7 @@ class Step13ReportPanel(QWidget):
f"未找到可视化目录:\n{viz}\n请先完成流程或生成可视化。",
)
return
if self._report_thread and self._report_thread.isRunning():
QMessageBox.information(self, "提示", "报告正在生成中,请稍候。")
return
@ -394,7 +443,9 @@ 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()
ai_maps_limit = self.ai_maps_spin.value()
self._report_thread = ReportWorkerThread(wd, out, title, enable_ai, report_mode, ai_maps_limit)
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)

View File

@ -368,6 +368,8 @@ class WaterQualityReportGenerator:
return "(Minimax API Key 未配置,请设置 MINIMAX_API_KEY 环境变量)"
url = self.minimax_base_url
if not url.endswith("/chat/completions"):
url = f"{url.rstrip('/')}/chat/completions"
payload: Dict[str, Any] = {
"model": self.minimax_text_model,
@ -433,6 +435,12 @@ class WaterQualityReportGenerator:
return f"(读取图片失败:{e})"
url = self.minimax_base_url
if not url.endswith("/chat/completions"):
url = f"{url.rstrip('/')}/chat/completions"
# 动态 MIME 类型:PNG 和 JPEG 分别处理,避免 API 400
ext = image_path.suffix.lower()
mime_type = "image/png" if ext == ".png" else "image/jpeg"
payload: Dict[str, Any] = {
"model": self.minimax_vision_model,
@ -444,7 +452,7 @@ class WaterQualityReportGenerator:
{"type": "text", "text": user_prompt},
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{img_b64}"},
"image_url": {"url": f"data:{mime_type};base64,{img_b64}"},
},
],
}
@ -739,10 +747,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 +778,16 @@ class WaterQualityReportGenerator:
parameters: List[str] = None,
report_title: str = "水质参数反演分析报告",
output_path: Optional[str] = None,
report_mode: str = 'auto',
ai_maps_limit: int = 999,
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: 'auto'→自动检测(参考Step11), 'ml'/'formula'→强制模式
"""
# 设置工作目录(整个流程的核心)
# 设置工作目录
if work_dir is not None:
self.work_dir = Path(work_dir)
self.visualization_dir = self.work_dir / "12_visualization"
@ -781,29 +795,38 @@ 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"]
# ── 确定管线模式(优先用户选择,否则自动检测)──
if report_mode == 'auto':
ml_dir = self.work_dir / "9_ML_Prediction"
formula_dir = self.work_dir / "10_WaterIndex_CSV"
if ml_dir.is_dir() and list(ml_dir.glob("*.csv")):
report_mode = 'ml'
elif formula_dir.is_dir() and list(formula_dir.glob("*.csv")):
report_mode = 'formula'
else:
report_mode = 'ml'
print(f"[报告] 自动检测 → {report_mode}")
else:
print(f"[报告] 用户选择 → {report_mode}")
if report_mode == 'formula':
return self._generate_formula_report(parameters, report_title, output_path,
ai_maps_limit=ai_maps_limit, on_progress=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"
@ -853,9 +876,14 @@ class WaterQualityReportGenerator:
# 按参数生成内容(带编号):参数章节从 5 开始编号
base_section_num = 5
last_param_section_num = base_section_num + len(parameters) - 1
for section_num, param in enumerate(parameters, base_section_num):
progress.set_description(f"正在分析 {param} 数据 ({section_num - base_section_num + 1}/{len(parameters)})")
n_params = len(parameters)
last_param_section_num = base_section_num + n_params - 1
for s_i, param in enumerate(parameters):
section_num = base_section_num + s_i
pct = int((s_i + 1) / n_params * 100)
if on_progress:
try: on_progress(pct, f"ML报告 参数 {s_i+1}/{n_params}: {param}")
except Exception: pass
figure_counter = self._add_parameter_section(
doc,
param,
@ -924,7 +952,198 @@ class WaterQualityReportGenerator:
print(f"✅ Word报告生成完成: {output_path}")
return str(output_path)
def _generate_formula_report(self, parameters, report_title, output_path,
ai_maps_limit=999, on_progress=None):
"""水色指数与物理经验公式反演报告 (Step 10/11)"""
from docx.shared import Inches, Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
vis_dir = self.visualization_dir
# 分布图优先从 12_visualization/distribution_maps 找,再回退 11_Thematic_Map
dist_map_dir = vis_dir / "distribution_maps"
thematic_dir = dist_map_dir if dist_map_dir.is_dir() else 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)
# 扫描实际数据量:专题图优先匹配 distribution/rendered 命名
csv_files = sorted(csv_dir.glob("*.csv")) if csv_dir.is_dir() else []
tif_files = []
if thematic_dir.is_dir():
tif_files = (sorted(thematic_dir.glob("*distribution*.png"))
+ sorted(thematic_dir.glob("*rendered*.png"))
+ sorted(thematic_dir.glob("*.tif"))
+ sorted(thematic_dir.glob("*.png")))
# 去重(同名文件可能多次匹配)
seen = set()
tif_files = [f for f in tif_files if f.name not in seen and not seen.add(f.name)]
n_csv = len(csv_files)
n_maps = min(len(tif_files), 20)
n_csv_pulses = max(1, n_csv // 10) # 每 10 个 CSV 一次脉冲
total_steps = 4 + n_csv_pulses + n_maps # 固定章节 + 统计脉冲 + 每张专题图
print(f"[公式报告] 数据: {n_csv} CSV, {len(tif_files)} 专题图 → 总步数 {total_steps}")
progress = self._create_progress(total=total_steps, desc="生成公式报告", on_step=on_progress)
_step = 0
def _next(text=""):
nonlocal _step
_step += 1
try: progress.update(1)
except Exception: pass
if on_progress:
try: on_progress(int(_step / total_steps * 100), text or f"步骤 {_step}/{total_steps}")
except Exception: pass
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)
self._add_company_description_page(doc)
self._add_data_acquisition_section(doc)
self._add_data_processing_section(doc)
_next("封面与通用章节")
# ── 1. 项目背景 ──
h1 = doc.add_heading("1 项目背景", level=1)
self._style_heading(h1, 1)
doc.add_paragraph("本报告基于高光谱遥感影像,通过物理经验公式(水色指数)反演水体关键参数的空间分布。"
"涵盖叶绿素、蓝绿藻、浊度、CDOM 等多种水色指标的定量化空间制图。")
doc.add_paragraph("数据来源:机载 / 星载高光谱成像仪,经过辐射定标、大气校正、耀斑去除等预处理。")
doc.add_page_break()
_next("项目背景")
# ── 2. 影像预处理 ──
h1 = doc.add_heading("2 影像预处理", level=1)
self._style_heading(h1, 1)
self._add_hyperspectral_images_section(doc)
doc.add_page_break()
_next("影像预处理")
# ── 3. 水色指数公式列表 ──
h1 = doc.add_heading("3 水色指数计算方法", level=1)
self._style_heading(h1, 1)
if csv_files:
doc.add_paragraph(f"本次反演共应用 {n_csv} 个水色指数公式,"
f"各公式基于特征波段比值或差分原理计算:")
for i, cf in enumerate(csv_files[:30], 1):
doc.add_paragraph(f" {i}. {cf.stem}", style='List Number')
if n_csv > 30:
doc.add_paragraph(f" ... 共 {n_csv} 个公式,详情见统计章节。")
else:
doc.add_paragraph(f"(未找到水色指数目录: {csv_dir})")
doc.add_page_break()
# ── 4. 指数统计结果(每个 CSV 更新一次进度)──
h1 = doc.add_heading("4 水色指数统计结果", level=1)
self._style_heading(h1, 1)
figure_num = 10
if csv_files:
stats_rows = []
for i_c, cp in enumerate(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
# 每处理 10 个 CSV 更新进度
if (i_c + 1) % max(1, n_csv // 10) == 0 or i_c == n_csv - 1:
_next(f"统计 {i_c+1}/{n_csv}")
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()
# ── 5. 指数空间分布专题图(每张更新进度)──
h1 = doc.add_heading("5 水色指数空间分布专题图", level=1)
self._style_heading(h1, 1)
figure_num += 1
maps_found = 0
if tif_files:
n_show = min(len(tif_files), 20)
for i_t, tf in enumerate(tif_files[:n_show]):
try:
param_name = tf.stem.split('_')[0] if '_' in tf.stem else tf.stem
caption = f"图{figure_num} {param_name} 空间分布图"
if self._add_image_with_caption(doc, str(tf), caption, width=Inches(5.5)):
if self.enable_ai_analysis and i_t < ai_maps_limit:
ai_text = self._analyze_and_cache_image(
image_path=tf, image_type="distribution",
param=param_name, figure_num=figure_num)
self._add_ai_analysis_paragraph(doc, ai_text)
figure_num += 1
maps_found += 1
except Exception as e:
doc.add_paragraph(f"[专题图插入失败: {tf.name} — {e}]")
_next(f"专题图 {i_t+1}/{n_show}")
if len(tif_files) > 20:
doc.add_paragraph(f"... 共 {len(tif_files)} 张专题图,此处仅展示前 20 张。")
else:
doc.add_paragraph(f"(未找到专题图目录: {thematic_dir})")
if maps_found == 0:
doc.add_paragraph("(未找到专题图文件,请确认 Step 11 已完成。)")
doc.add_page_break()
# ── 6. 综合总结 ──
h1 = doc.add_heading("6 综合总结", level=1)
self._style_heading(h1, 1)
doc.add_paragraph("本报告基于高光谱遥感影像的水色指数反演方法,对研究区域内的关键水质参数"
"进行了定量化空间制图。各指数的统计结果和空间分布专题图如上所示。")
doc.add_paragraph("注意事项:水色指数反演结果为半定量指标,其绝对值可能受大气校正精度、"
"水体光学特性复杂性等因素影响。建议结合实测水质数据进行校验。")
_next("综合总结")
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,
@ -1254,130 +1473,106 @@ class WaterQualityReportGenerator:
run.font.size = Pt(12)
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'SimSun')
# 添加高光谱图像、耀斑区域和去耀斑图像展示
self._add_hyperspectral_images_section(doc)
doc.add_page_break()
def _add_hyperspectral_images_section(self, doc):
"""添加高光谱图像、耀斑区域和去耀斑图像展示"""
h = doc.add_heading("3.1 高光谱图像处理过程", level=2)
self._style_heading(h, level=2)
"""添加高光谱图像、耀斑区域和去耀斑图像展示
找不到文件的章节直接跳过,不显示占位文字。
"""
work_dir_path = self.work_dir
vis_dir = self.visualization_dir
# 0. 航线规划图
flight_path_img_path = work_dir_path / "12_visualization" / "flight_paths"
h3 = doc.add_heading("航线规划:", level=3)
self._style_heading(h3, level=3)
# 查找航线图文件
# 0. 航线规划图(仅搜索专用目录,找不到则整节跳过)
flight_map_files = []
if flight_path_img_path.exists():
flight_map_files = list(flight_path_img_path.glob("*.png")) + list(flight_path_img_path.glob("*.jpg"))
flight_dirs = [
work_dir_path / "12_visualization" / "flight_paths",
vis_dir / "flight_paths",
]
for d in flight_dirs:
if d.exists():
flight_map_files = sorted(d.glob("*.png")) + sorted(d.glob("*.jpg"))
if flight_map_files:
break
if flight_map_files:
# 使用最新的航线图文件
latest_flight_map = max(flight_map_files, key=lambda p: p.stat().st_mtime)
success = self._add_image_with_caption(doc, str(latest_flight_map), "图3-1 航线规划", width=Inches(5.5))
if success:
# AI 分析航线规划图
flight_analysis = self._analyze_flight_path_image(str(latest_flight_map))
self._add_ai_analysis_paragraph(doc, flight_analysis)
else:
doc.add_paragraph("[航线规划图 - 文件未找到]")
h3 = doc.add_heading("航线规划:", level=3)
self._style_heading(h3, level=3)
latest = max(flight_map_files, key=lambda p: p.stat().st_mtime)
if self._add_image_with_caption(doc, str(latest), "航线规划", width=Inches(5.5)):
if self.enable_ai_analysis:
self._add_ai_analysis_paragraph(doc,
self._analyze_flight_path_image(str(latest)))
# 1. 高光谱原始图像
hyperspectral_img_path = work_dir_path / "1_water_mask" / "hsi_preview.png"
h3 = doc.add_heading("高光谱原始影像:", level=3)
self._style_heading(h3, level=3)
if hyperspectral_img_path.exists():
self._add_image_with_caption(doc, str(hyperspectral_img_path), "图3-2 高光谱原始影像", width=Inches(5.5))
else:
doc.add_paragraph("[高光谱原始影像 - 文件未找到]")
hsi_path = work_dir_path / "1_water_mask" / "hsi_preview.png"
if hsi_path.exists():
h3 = doc.add_heading("高光谱原始影像:", level=3)
self._style_heading(h3, level=3)
self._add_image_with_caption(doc, str(hsi_path), "高光谱原始影像", width=Inches(5.5))
# 2. 水体掩膜叠加图
water_mask_overlay_path = work_dir_path / "1_water_mask" / "water_mask_overlay.png"
h3 = doc.add_heading("水体区域识别:", level=3)
self._style_heading(h3, level=3)
if water_mask_overlay_path.exists():
success = self._add_image_with_caption(doc, str(water_mask_overlay_path),
"图3-3 水体区域识别(蓝色半透明区域为水域)",
width=Inches(5.5))
if success:
water_analysis = self._analyze_water_mask_overlay(str(water_mask_overlay_path))
self._add_ai_analysis_paragraph(doc, water_analysis)
else:
doc.add_paragraph("[水体区域识别图 - 文件未找到]")
wm_path = work_dir_path / "1_water_mask" / "water_mask_overlay.png"
if wm_path.exists():
h3 = doc.add_heading("水体区域识别:", level=3)
self._style_heading(h3, level=3)
if self._add_image_with_caption(doc, str(wm_path),
"水体区域识别(蓝色半透明区域为水域)",
width=Inches(5.5)):
if self.enable_ai_analysis:
self._add_ai_analysis_paragraph(doc,
self._analyze_water_mask_overlay(str(wm_path)))
doc.add_paragraph()
# 3. 耀斑区域
glint_dirs = [vis_dir / "glint_deglint_previews",
work_dir_path / "2_Glint_Detection"]
glint_img = None
for d in glint_dirs:
if d.exists():
cands = sorted(d.glob("*glint*.png")) + sorted(d.glob("*severe*.png"))
if cands:
glint_img = cands[0]
break
if glint_img:
h3 = doc.add_heading("耀斑区域识别结果:", level=3)
self._style_heading(h3, level=3)
if self._add_image_with_caption(doc, str(glint_img), "耀斑区域识别结果", width=Inches(5.5)):
if self.enable_ai_analysis:
self._add_ai_analysis_paragraph(doc,
self._analyze_glint_distribution_with_ai(str(glint_img)))
# 2. 耀斑区域
glint_img_path = vis_dir / "glint_deglint_previews" / "glint_severe_glint_area_preview.png"
h3 = doc.add_heading("耀斑区域识别结果:", level=3)
self._style_heading(h3, level=3)
if glint_img_path.exists():
self._add_image_with_caption(doc, str(glint_img_path), "图3-4 耀斑区域识别结果", width=Inches(5.5))
else:
# 尝试查找其他可能的耀斑预览图
glint_files = list(vis_dir.glob("glint_deglint_previews/*glint*.png"))
if glint_files:
glint_img_path = glint_files[0]
self._add_image_with_caption(doc, str(glint_img_path), "图3-4 耀斑区域识别结果", width=Inches(5.5))
else:
doc.add_paragraph("[耀斑区域识别结果 - 文件未找到]")
# 4. 去除耀斑后的图像
deglint_img = None
for d in glint_dirs:
if d.exists():
cands = sorted(d.glob("*deglint*.png"))
if cands:
deglint_img = cands[0]
break
deglint_dir2 = work_dir_path / "3_deglint"
if deglint_img is None and deglint_dir2.exists():
cands = sorted(deglint_dir2.glob("*.png"))
if cands:
deglint_img = cands[0]
if deglint_img:
h3 = doc.add_heading("去除耀斑后的影像:", level=3)
self._style_heading(h3, level=3)
self._add_image_with_caption(doc, str(deglint_img), "去除耀斑后的影像", width=Inches(5.5))
doc.add_paragraph()
# 3. 去除耀斑后的图像
deglint_img_path = vis_dir / "glint_deglint_previews" / "deglint_deglint_image_preview.png"
h3 = doc.add_heading("去除耀斑后的影像:", level=3)
self._style_heading(h3, level=3)
if deglint_img_path.exists():
self._add_image_with_caption(doc, str(deglint_img_path), "图3-5 去除耀斑后的高光谱影像", width=Inches(5.5))
else:
# 尝试查找其他去耀斑预览图
deglint_files = list(vis_dir.glob("glint_deglint_previews/*deglint*.png"))
if deglint_files:
deglint_img_path = deglint_files[0]
self._add_image_with_caption(doc, str(deglint_img_path), "图3-5 去除耀斑后的影像", width=Inches(5.5))
else:
doc.add_paragraph("[去除耀斑后的影像 - 文件未找到]")
doc.add_paragraph()
# 4. AI分析耀斑位置分布
self._style_heading(h3, level=3)
glint_analysis = self._analyze_glint_distribution_with_ai(
str(glint_img_path) if 'glint_img_path' in locals() and Path(str(glint_img_path)).exists() else None,
str(hyperspectral_img_path) if hyperspectral_img_path.exists() else None
)
self._add_ai_analysis_paragraph(doc, glint_analysis)
# 5. 采样点分布图
# 5. 采样点分布图(找不到则整节跳过)
sampling_map_dir = vis_dir / "sampling_maps"
h3 = doc.add_heading("采样点分布:", level=3)
self._style_heading(h3, level=3)
# 查找采样点分布图文件
sampling_map_files = []
if sampling_map_dir.exists():
sampling_map_files = list(sampling_map_dir.glob("*.png")) + list(sampling_map_dir.glob("*.jpg"))
sampling_map_files = (list(sampling_map_dir.glob("*.png"))
+ list(sampling_map_dir.glob("*.jpg")))
if sampling_map_files:
# 使用最新的采样点分布图文件
h3 = doc.add_heading("采样点分布:", level=3)
self._style_heading(h3, level=3)
latest_sampling_map = max(sampling_map_files, key=lambda p: p.stat().st_mtime)
success = self._add_image_with_caption(doc, str(latest_sampling_map), "图3-6 采样点分布图", width=Inches(5.5))
if success:
# AI 分析采样点分布图
sampling_analysis = self._analyze_sampling_distribution(str(latest_sampling_map))
self._add_ai_analysis_paragraph(doc, sampling_analysis)
else:
doc.add_paragraph("[采样点分布图 - 文件未找到]")
if self._add_image_with_caption(doc, str(latest_sampling_map), "采样点分布图", width=Inches(5.5)):
if self.enable_ai_analysis:
self._add_ai_analysis_paragraph(doc,
self._analyze_sampling_distribution(str(latest_sampling_map)))
def _analyze_glint_distribution_with_ai(self, glint_img_path: str = None, original_img_path: str = None) -> str:
"""使用AI分析耀斑的位置分布"""
@ -1724,7 +1919,11 @@ class WaterQualityReportGenerator:
row_cells = table.add_row().cells
for j, (col_name, hdr_name) in enumerate(header_map.items()):
row_cells[j].text = str(row_dict.get(col_name, ''))
stats_data = [{'参数': r['参数'], '点位数': r['数量']} for r in stats_rows]
stats_data = [{
'参数': r['参数'], '点位数': r['数量'],
'最小值': str(r['最小值']), '最大值': str(r['最大值']),
'平均值': str(r['平均值']), '标准差': str(r['标准差']),
} for r in stats_rows]
else:
# ML 模式:逐列统计
stats_data = []
@ -1780,10 +1979,10 @@ class WaterQualityReportGenerator:
doc.add_paragraph() # 表格和热力图之间的空行
# 2. 添加相关性热力图(放在表格下方)—— 仅 ML 模式
heatmap_path = vis_dir / "correlation_heatmap.png"
if getattr(self, '_pipeline_mode', 'ml') == 'ml':
h3 = doc.add_heading("4.2 水质参数相关性分析", level=2)
self._style_heading(h3, level=2)
heatmap_path = vis_dir / "correlation_heatmap.png"
figure_num = start_figure_num
if heatmap_path.exists():
try:
@ -1823,7 +2022,9 @@ class WaterQualityReportGenerator:
pass
doc.add_page_break()
return start_figure_num + (1 if heatmap_path.exists() else 0)
heatmap_added = 1 if (getattr(self, '_pipeline_mode', 'ml') == 'ml'
and heatmap_path.exists()) else 0
return start_figure_num + heatmap_added
def _add_physical_inversion_section(
self,