测试修改

This commit is contained in:
DXC
2026-06-25 15:50:02 +08:00
parent 67aaaaa6b2
commit 43f50ec07b
16 changed files with 1098 additions and 1029 deletions

View File

@ -290,7 +290,10 @@ class Step11MapPanel(QWidget):
params_layout.addRow("输入坐标系:", self.input_crs)
self.output_crs = QLineEdit()
self.output_crs.setText("EPSG:4326")
# ★★★ 强制默认输出坐标系与输入一致,禁止从 GUI 误改为 EPSG:4326 ★★★
# 历史默认值 'EPSG:4326' 会让 ContentMapper 把栅格重投影到经纬度,
# 与基于 EPSG:32651 的水域掩膜叠加时发生仿射变换撕裂(栅格错位、坐标轴扭曲)。
self.output_crs.setText("EPSG:32651")
params_layout.addRow("输出坐标系:", self.output_crs)
self.show_points = QCheckBox("显示采样点")
@ -416,7 +419,9 @@ class Step11MapPanel(QWidget):
'boundary_shp_path': self.boundary_file.get_path(),
'resolution': self.resolution.value(),
'input_crs': self.input_crs.text(),
'output_crs': self.output_crs.text(),
# ★★★ 强制 output_crs = input_crs,不再信任 GUI 的 output_crs 输入框 ★★★
# 否则 ContentMapper 会把栅格重投影到 EPSG:4326,与水域掩膜叠加时撕裂。
'output_crs': self.input_crs.text(),
'show_sample_points': self.show_points.isChecked(),
'use_distance_diffusion': self.use_diffusion.isChecked(),
}
@ -437,7 +442,8 @@ class Step11MapPanel(QWidget):
'boundary_shp_path': self.boundary_file.get_path(),
'resolution': self.resolution.value(),
'input_crs': self.input_crs.text(),
'output_crs': self.output_crs.text(),
# ★★★ 强制 output_crs = input_crs,不再信任 GUI 的 output_crs 输入框 ★★★
'output_crs': self.input_crs.text(),
'show_sample_points': self.show_points.isChecked(),
'use_distance_diffusion': self.use_diffusion.isChecked(),
}
@ -475,8 +481,9 @@ class Step11MapPanel(QWidget):
self.resolution.setValue(config['resolution'])
if 'input_crs' in config:
self.input_crs.setText(config['input_crs'])
if 'output_crs' in config:
self.output_crs.setText(config['output_crs'])
# ★★★ 反灌入时强制 output_crs = input_crs,避免旧 config 中的 EPSG:4326 回填 ★★★
if 'output_crs' in config or 'input_crs' in config:
self.output_crs.setText(config.get('input_crs') or config.get('output_crs') or 'EPSG:32651')
if 'show_sample_points' in config:
self.show_points.setChecked(config['show_sample_points'])
if 'use_distance_diffusion' in config:
@ -573,10 +580,8 @@ class Step11MapPanel(QWidget):
return
boundary_shp_path = self.boundary_file.get_path()
if not boundary_shp_path:
QMessageBox.warning(self, "输入验证失败", "请选择边界文件")
return
if not os.path.exists(boundary_shp_path):
# ── Plan C: 允许 boundary_shp_path 为空(跳过水域掩膜,纯采样点插值)────
if boundary_shp_path and not os.path.exists(boundary_shp_path):
QMessageBox.warning(self, "输入验证失败", "边界文件不存在")
return
@ -631,7 +636,8 @@ class Step11MapPanel(QWidget):
output_dir=out_dir,
boundary_shp_path=boundary_shp_path,
input_crs=self.input_crs.text(),
output_crs=self.output_crs.text(),
# ★★★ 强制 output_crs = input_crs,不再信任 GUI 的 output_crs 输入框 ★★★
output_crs=self.input_crs.text(),
)
main_win = parent
@ -671,7 +677,8 @@ class Step11MapPanel(QWidget):
boundary_shp_path = self.boundary_file.get_path()
input_crs = self.input_crs.text()
output_crs = self.output_crs.text()
# ★★★ 强制 output_crs = input_crs,不再信任 GUI 的 output_crs 输入框 ★★★
output_crs = self.input_crs.text()
# 构造输出路径
out_dir = (self.output_dir.get_path() or "").strip()

View File

@ -440,6 +440,62 @@ class VisualizationWorkerThread(QThread):
parts.append(f"浓度统计图: 失败({e})")
else:
parts.append("浓度统计图: 跳过(无浓度CSV)")
if self.extra.get("gen_distribution_map"):
dist_dir = wp / "11_Thematic_Map"
out_sub = Path(viz.output_dir) / "distribution_maps"
out_sub.mkdir(parents=True, exist_ok=True)
n_rendered = 0
if dist_dir.exists():
import shutil
# 1. 拷贝现成的 PNG
for png in list(dist_dir.glob("*_distribution.png")) + list(dist_dir.glob("*_专题图.png")):
try:
shutil.copy2(png, out_sub / png.name)
n_rendered += 1
except Exception:
pass
# 2. 渲染生成的 TIF
tif_files = list(dist_dir.glob("*_distribution.tif")) + list(dist_dir.glob("*_kriging.tif"))
if tif_files:
from src.postprocessing.map import ContentMapper
mapper = ContentMapper()
# 优先级1:直接使用 Step 1 面板中缓存的外部原始 .shp 绝对路径!
boundary_path = self.extra.get("boundary_shp_path")
# 优先级2:如果没拿到,全局搜索整个工作目录下的 .shp 文件(放宽限制)
if not boundary_path:
shp_candidates = list(wp.rglob("**/*.shp"))
if shp_candidates:
boundary_path = str(shp_candidates[0])
# 优先级3:兜底使用 1_water_mask 下的栅格掩膜
if not boundary_path:
mask_files = list(wp.rglob("1_water_mask/*"))
other_candidates = [f for f in mask_files if f.suffix.lower() in ('.dat', '.bsq', '.tif', '.tiff')]
if other_candidates:
boundary_path = str(other_candidates[0])
if not boundary_path:
print(f"[distribution_maps] 未找到水域边界文件,跳过裁剪")
for tif in tif_files:
dst_png = out_sub / f"{tif.stem}_rendered.png"
try:
mapper.visualize_raster(
raster_tif_path=str(tif),
output_file=str(dst_png),
boundary_shp_path=boundary_path,
nodata_value=-9999.0,
figsize=(14, 10),
alpha=0.9
)
n_rendered += 1
except Exception as e:
print(f"渲染 TIF 失败 {tif.name}: {e}")
parts.append(f"空间分布图: {n_rendered} 个")
else:
parts.append("空间分布图: 跳过(无 11_Thematic_Map 目录)")
self.finished_ok.emit({"task": "generate_all_selected", "parts": parts})
else:
self.failed.emit(f"未知可视化任务: {self.task}")
@ -574,272 +630,104 @@ class ChartViewerDialog(QDialog):
class ImageCategoryTree(QTreeWidget):
"""图像分类目录树 - 按真实物理文件夹结构组织图像文件"""
# 文件名中文翻译映射(key: 文件名前缀 → 中文显示名)
NAME_MAPPING = {
"hsi_preview": "高光谱影像预览",
"hsi_original": "原始高光谱影像",
"hsi_deglint": "去耀斑高光谱影像",
"water_mask_overlay": "水域掩膜叠加图",
"water_mask": "水域掩膜图",
"glint_mask": "耀斑掩膜预览",
"glint_overlay": "耀斑叠加对比图",
"deglint_comparison": "去耀斑前后对比",
"training_spectra": "训练光谱特征",
"spectrum_by_param": "参数光谱图",
"model_evaluation": "模型评估散点图",
"model_scatter": "模型散点图",
"regression": "回归分析图",
"validation": "验证结果图",
"spatial_distribution": "参数空间分布图",
"distribution_map": "分布图",
"thematic_map": "水质专题图",
"water_quality_map": "水质分布图",
"prediction_map": "预测结果图",
"inversion_map": "反演结果图",
"correlation_matrix": "特征相关性矩阵",
"feature_correlation": "特征相关性",
"sampling_point_map": "采样点分布图",
"sampling_points": "采样点图",
"point_locations": "采样位置图",
"boxplot": "箱线图",
"histogram": "直方图",
"statistics": "统计图表",
"statistical_chart": "统计图",
"error_analysis": "误差分析图",
"rmse": "RMSE评估图",
"r2_score": "R²得分图",
"flight": "飞行轨迹图",
"path": "轨迹图",
"trajectory": "轨迹图",
"glint_deglint": "耀斑去耀斑影像",
"enhanced": "增强分布图",
"content": "含量分布图",
"distribution": "分布图",
"prediction": "预测图",
"inversion": "反演图",
"scatter_true_vs_pred": "真值-预测散点图",
"true_vs_pred": "真值-预测散点图",
"correlation_heatmap": "相关性热力图",
"parameter_boxplot": "水质参数箱线图",
"spectrum_comparison": "光谱曲线对比图",
"scatter": "散点图",
}
# 目录层级中文翻译
DIR_MAPPING = {
"14_visualization": "统计与分析报表",
"1_water_mask": "水域掩膜识别",
"2_Glint_Detection": "耀斑区域检测",
"3_deglint": "去耀斑影像结果",
"5_training_spectra": "训练光谱特征",
"8_Regression_Modeling": "回归建模分析",
"9_water_quality_prediction": "水质预测结果",
"10_feature_construction": "特征构建散点",
"11_12_13_predictions": "空间分布专题图",
"glint_deglint_previews": "耀斑处理预览",
"sampling_maps": "采样点空间分布",
"flight_maps": "无人机飞行轨迹",
"9_ML_Prediction": "机器学习预测",
"Non_Empirical_Prediction": "非经验模型预测",
"Custom_Regression_Prediction": "自定义回归预测",
"boxplot_dir": "水质参数箱线图",
"boxplot": "水质参数箱线图",
"output_dir": "输出目录",
"8_spatial_inversion": "空间反演",
"4_processed_data": "处理数据",
"9_Concentration": "物理反演浓度分布",
}
"""现代化的图像分类目录树 - 支持智能归类、筛选和高颜值样式"""
def __init__(self, parent=None):
super().__init__(parent)
self._dir_node_map: dict = {} # 目录路径字符串 → QTreeWidgetItem
self._work_path: Optional[Path] = None
self.setHeaderLabel("图像目录")
self.setMaximumWidth(300)
self.setMinimumWidth(250)
self._work_path = None
self._all_image_files = [] # 缓存所有扫描到的图片路径
self.setHeaderHidden(True) # 隐藏表头,显得更清爽
self.setAlternatingRowColors(True) # 斑马纹交替背景
self.setMaximumWidth(340)
self.setMinimumWidth(280)
# 核心:高颜值现代化 CSS 样式
self.setStyleSheet("""
QTreeWidget {
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f8f9fa;
border: 1px solid #E2E8F0;
border-radius: 8px;
background-color: #FFFFFF;
alternate-background-color: #F8FAFC;
font-family: "Microsoft YaHei", "Segoe UI";
font-size: 13px;
padding: 4px;
}
QTreeWidget::item {
padding: 5px;
border-radius: 3px;
}
QTreeWidget::item:selected {
background-color: #0078D4;
color: white;
height: 32px;
border-radius: 6px;
margin: 2px 4px;
}
QTreeWidget::item:hover {
background-color: #e3f2fd;
background-color: #F1F5F9;
}
QTreeWidget::item:selected {
background-color: #E0F2FE;
color: #0369A1;
font-weight: bold;
}
QTreeWidget::branch:has-children:!has-siblings:closed,
QTreeWidget::branch:closed:has-children:has-siblings {
border-image: none;
image: none;
}
QTreeWidget::branch:open:has-children:!has-siblings,
QTreeWidget::branch:open:has-children:has-siblings {
border-image: none;
image: none;
}
""")
def clear_all_images(self):
"""清除所有图像项"""
try:
self.invisibleRootItem().takeChildren()
if hasattr(self, '_dir_node_map'):
self._dir_node_map.clear()
except Exception as e:
print(f"清空树状图出错: {e}")
import traceback
traceback.print_exc()
def _parse_file_info(self, file_path: Path):
"""智能解析文件名,提取【水质参数】和【图表类型】"""
name_upper = file_path.name.upper()
def _translate_dir_name(self, dir_name: str) -> str:
"""翻译目录名为中文"""
return self.DIR_MAPPING.get(dir_name, dir_name)
# 1. 提取图表类型
chart_type = "其他图表"
if "DISTRIBUTION" in name_upper or "专题图" in name_upper or "RENDERED" in name_upper:
chart_type = "空间分布图"
elif "SCATTER" in name_upper or "散点" in name_upper:
chart_type = "模型散点图"
elif "SPECTRUM" in name_upper or "光谱" in name_upper:
chart_type = "光谱曲线图"
elif "HEATMAP" in name_upper or "热力图" in name_upper:
chart_type = "相关性热力图"
elif "BOXPLOT" in name_upper or "HISTOGRAM" in name_upper or "箱线" in name_upper or "直方" in name_upper:
chart_type = "统计箱线图"
elif "SAMPLING" in name_upper or "采样" in name_upper:
chart_type = "采样点地图"
elif "GLINT" in name_upper or "MASK" in name_upper or "PREVIEW" in name_upper:
chart_type = "掩膜与预览"
def _translate_filename(self, filename: str) -> str:
# 1. 后缀替换 (图表类型)
type_mapping = {
'_scatter_true_vs_pred': ' 真值预测散点图',
'_spectrum_comparison': ' 光谱曲线对比图',
'_spectrum': ' 光谱特征图',
'_histogram': ' 分布直方图',
'_boxplot_seaborn': ' Seaborn箱线图',
'_boxplot': ' 箱线图',
'_distribution_enhanced': ' 增强空间分布图',
'_distribution': ' 空间分布图',
'_sampling_map': ' 采样点地图',
'_flight_paths': ' 飞行轨迹图',
'_preview': ' 效果预览图',
'water_mask_overlay': '水域掩膜叠加图',
'hsi_preview': '原始影像预览',
'correlation_heatmap': '特征相关性热力图',
'parameter_boxplot': '水质参数汇总箱线图',
'all_parameters_boxplot': '全参数汇总箱线图',
'content_map': '含量分布专题图',
'_scatter_with_confidence': ' 置信区间散点图'
# 2. 提取参数名
param_name = "综合/未分类"
# 常见水质参数字典映射
params_map = {
'CHLOROPHYLL': 'Chlorophyll (叶绿素)', 'CHL_A': 'Chlorophyll (叶绿素)', 'CHLA': 'Chlorophyll (叶绿素)',
'COD': 'COD (化学需氧量)', 'DO': 'DO (溶解氧)', 'PH': 'pH',
'TEMPERATURE': 'Temperature (温度)', 'SPCOND': 'spCond (电导率)',
'TURBIDITY': 'Turbidity (浊度)', 'TDS': 'TDS (总溶解固体)',
'CL-': 'Cl- (氯离子)', 'NO3-N': 'NO3-N (硝态氮)', 'NH3-N': 'NH3-N (氨氮)',
'BGA': 'BGA (蓝绿藻)', 'TT': 'TT (透明度)'
}
for key, display_name in params_map.items():
if key in name_upper or key.replace('-', '') in name_upper:
param_name = display_name
break
name = filename
for eng, chn in type_mapping.items():
if eng in name:
name = name.replace(eng, chn)
# 2. 常见水质参数前缀替换
param_mapping = {
'Chlorophyll': '叶绿素', 'Chl_a': '叶绿素a', 'Chla': '叶绿素a',
'Turbidity': '浊度', 'Temperature': '温度', 'spCond': '电导率',
'COD': '化学需氧量', 'DO': '溶解氧', 'PH': 'pH值', 'TDS': '总溶解固体',
'BGA': '蓝绿藻', 'TT': '透明度', 'NH3-N': '氨氮', 'NO3-N': '硝酸盐氮',
'glint_severe_glint_area': '重度耀斑区域',
'severe_glint_area': '重度耀斑区域',
'deglint_goodman': 'Goodman算法去耀斑',
'deglint_Goodman': 'Goodman算法去耀斑',
'glint_': '耀斑检测_',
'deglint_': '耀斑去除_',
}
for eng, chn in param_mapping.items():
if name.startswith(eng + ' ') or name.startswith(eng + '_'):
name = name.replace(eng, chn, 1)
elif eng in name:
name = name.replace(eng, chn)
return name.strip('_')
def add_image_by_dir(self, file_path: Path, work_path: Path):
"""按真实物理目录层级挂载图片节点
Args:
file_path: 图片文件的完整路径
work_path: 工作目录根路径
"""
# 计算相对路径
try:
rel_path = file_path.relative_to(work_path)
except ValueError:
rel_path = Path(file_path.name)
# 分离父目录链和文件名
parts = rel_path.parts
if len(parts) <= 1:
parent_key = "__root__"
parent_display = "根目录"
else:
# 父目录路径(相对于work_path)
parent_key = str(Path(*parts[:-1]))
# 取最后一层目录名作为显示名
parent_display = self._translate_dir_name(parts[-2])
# 根目录节点特殊处理
root_display = self._translate_dir_name(parts[0]) if parts else "根目录"
# 获取或创建根目录节点
if root_display not in self._dir_node_map:
root_item = QTreeWidgetItem(self)
root_item.setText(0, f"📁 {root_display}")
root_item.setData(0, Qt.UserRole, {"type": "root_dir", "path": str(work_path / parts[0])})
root_item.setExpanded(True)
self._dir_node_map[root_display] = root_item
self._dir_node_map[f"__root__{root_display}"] = root_item
root_item = self._dir_node_map.get(f"__root__{root_display}")
if len(parts) > 1:
# 获取或创建子目录节点
if parent_key not in self._dir_node_map:
dir_item = QTreeWidgetItem(root_item)
dir_item.setText(0, f" 📂 {parent_display}")
dir_item.setData(0, Qt.UserRole, {"type": "sub_dir", "path": str(work_path / parent_key)})
dir_item.setExpanded(True)
self._dir_node_map[parent_key] = dir_item
parent_item = self._dir_node_map[parent_key]
else:
parent_item = root_item
# 创建图片节点(根据翻译后的名称分配图标)
display_name = self._translate_filename(file_path.stem) + file_path.suffix
icon = "🖼️" # 默认
if "散点" in display_name:
icon = "📊"
elif "光谱" in display_name or "曲线" in display_name:
icon = "📈"
elif "箱线" in display_name or "直方" in display_name:
icon = "📉"
elif "分布" in display_name or "地图" in display_name or "轨迹" in display_name:
icon = "🗺️"
image_item = QTreeWidgetItem(parent_item)
image_item.setText(0, f" {icon} {display_name}")
image_item.setData(0, Qt.UserRole, {"type": "image", "path": str(file_path), "display_name": display_name})
image_item.setToolTip(0, str(file_path))
return image_item
return param_name, chart_type
def scan_directory(self, work_dir: str):
"""扫描目录中的所有图像文件(深度递归扫描)—— 按真实物理目录结构挂载"""
"""全量扫描文件并缓存(不直接构建树,而是交给 rebuild_tree 渲染)"""
try:
if not work_dir:
print("可视化面板:工作目录为空,跳过扫描")
return
if not work_dir: return
self._work_path = Path(work_dir)
if not self._work_path.exists(): return
# 阻塞信号,防止在清空树状图时触发 selected 槽函数导致崩溃
# 因为当前类继承自 QTreeWidget,所以 self 本身就是树
self.blockSignals(True)
self.clear_all_images()
self.blockSignals(False)
if not self._work_path.exists():
return
except Exception as e:
import traceback
print(f"可视化面板初始化扫描出错: {e}")
traceback.print_exc()
# 确保信号锁被解开
self.blockSignals(False)
return
try:
image_extensions = ['*.png', '*.jpg', '*.jpeg', '*.tif', '*.tiff', '*.bmp']
# 拓宽扫描根目录列表(新增多个遗漏目录)
scan_roots: List[Path] = [
# 仅扫描用于视觉展示的常规图片格式,屏蔽科学栅格 TIF 以免无法渲染报错
image_extensions = ['*.png', '*.jpg', '*.jpeg', '*.bmp']
# 扩展扫描路径
scan_roots = [
Path(resolve_subdir(str(self._work_path), 'visualization')),
Path(resolve_subdir(str(self._work_path), 'prediction_dir')),
Path(resolve_subdir(str(self._work_path), 'regression_modeling')),
@ -850,51 +738,98 @@ class ImageCategoryTree(QTreeWidget):
Path(resolve_subdir(str(self._work_path), 'water_mask')),
self._work_path / "9_water_quality_prediction",
self._work_path / "9_Concentration",
self._work_path / "11_Thematic_Map"
]
# 只保留存在的目录,并补充工作根目录作为兜底
scan_roots = [p for p in scan_roots if p.is_dir()]
if not scan_roots:
scan_roots.append(self._work_path)
if not scan_roots: scan_roots.append(self._work_path)
seen_norm = set()
self._all_image_files = []
seen_norm: set = set()
image_files: List[Path] = []
for root in scan_roots:
for ext in image_extensions:
for p in root.rglob(ext):
key = os.path.normcase(os.path.normpath(str(p.resolve())))
if key in seen_norm:
continue
if key in seen_norm: continue
seen_norm.add(key)
image_files.append(p)
if p.name.startswith('.') or 'thumb' in p.name.lower(): continue
self._all_image_files.append(p)
for img_file in sorted(image_files):
if img_file.name.startswith('.') or 'thumb' in img_file.name.lower():
continue
self.add_image_by_dir(img_file, self._work_path)
# 更新目录节点计数
for key, item in self._dir_node_map.items():
if key.startswith("__root__"):
continue
if item.data(0, Qt.UserRole).get("type") == "sub_dir":
count = item.childCount()
name = item.text(0)
if count > 0 and f"({count})" not in name:
# 从目录名中提取显示名并附加计数
display = name.strip()
item.setText(0, f" 📂 {display} ({count})")
self._all_image_files.sort(key=lambda x: x.name)
# 默认构建模式
self.rebuild_tree(group_mode='parameter', filter_type='all')
except Exception as e:
import traceback
print(f"可视化面板图片挂载出错: {e}")
traceback.print_exc()
print(f"目录扫描出错: {e}")
def rebuild_tree(self, group_mode='parameter', filter_type='all'):
"""根据下拉框的【模式】和【筛选条件】实时重新构建 UI 树"""
self.blockSignals(True)
self.clear()
root_nodes = {}
for img_file in self._all_image_files:
param, chart_type = self._parse_file_info(img_file)
# 1. 应用筛选器逻辑
if filter_type != 'all' and filter_type != chart_type:
continue
# 2. 决定分组基准
if group_mode == 'parameter':
group_key = param
group_icon = "💧" if param != "综合/未分类" else "📁"
display_name = f"[{chart_type}] {img_file.name}"
elif group_mode == 'type':
group_key = chart_type
group_icon = "📊"
display_name = f"[{param.split(' ')[0]}] {img_file.name}"
else:
# 物理文件夹原样模式
try:
rel_path = img_file.relative_to(self._work_path)
group_key = str(rel_path.parent) if len(rel_path.parts) > 1 else "根目录"
except:
group_key = "其他"
group_icon = "📂"
display_name = img_file.name
# 3. 创建父节点
if group_key not in root_nodes:
root_item = QTreeWidgetItem(self)
root_item.setText(0, f"{group_icon} {group_key}")
root_item.setExpanded(True)
font = root_item.font(0)
font.setBold(True)
root_item.setFont(0, font)
root_item.setData(0, Qt.UserRole, {"type": "root"})
root_nodes[group_key] = root_item
parent_item = root_nodes[group_key]
# 4. 挂载子节点及专属图标
icon = "🖼️"
if "散点" in chart_type: icon = "📌"
elif "光谱" in chart_type: icon = "📈"
elif "分布" in chart_type: icon = "🗺️"
elif "箱线" in chart_type: icon = "📉"
image_item = QTreeWidgetItem(parent_item)
image_item.setText(0, f" {icon} {display_name}")
image_item.setData(0, Qt.UserRole, {"type": "image", "path": str(img_file)})
image_item.setToolTip(0, str(img_file))
# 统计数量
for i in range(self.topLevelItemCount()):
root_item = self.topLevelItem(i)
count = root_item.childCount()
old_text = root_item.text(0)
root_item.setText(0, f"{old_text} ({count})")
self.blockSignals(False)
def get_selected_image_path(self) -> Optional[str]:
"""获取当前选中的图像路径"""
selected_item = self.currentItem()
if not selected_item:
return None
if not selected_item: return None
data = selected_item.data(0, Qt.UserRole)
if data and data.get("type") == "image":
return data.get("path")
@ -1401,17 +1336,19 @@ class Step12VizPanel(QWidget):
QMessageBox.critical(self, "错误", f"可视化任务失败:\n{err[:1200]}")
def init_ui(self):
"""初始化UI - 使用左右分栏布局"""
"""初始化UI - 使用全新的三列布局(控制参数 | 独立满高目录树 | 图像查看器)"""
main_layout = QHBoxLayout()
main_layout.setSpacing(10)
main_layout.setSpacing(12)
main_layout.setContentsMargins(10, 10, 10, 10)
# ===== 左侧面板 =====
left_panel = QWidget()
left_layout = QVBoxLayout()
left_layout.setContentsMargins(0, 0, 0, 0)
# ==========================================
# 第一列:控制面板(目录选择 + 生成配置)
# ==========================================
control_panel = QWidget()
control_layout = QVBoxLayout()
control_layout.setContentsMargins(0, 0, 0, 0)
# 工作目录选择
# 1. 工作目录选择
dir_group = QGroupBox("工作目录")
dir_layout = QHBoxLayout()
self.work_dir_edit = QLineEdit()
@ -1422,9 +1359,9 @@ class Step12VizPanel(QWidget):
dir_layout.addWidget(self.work_dir_edit, 1)
dir_layout.addWidget(dir_browse_btn)
dir_group.setLayout(dir_layout)
left_layout.addWidget(dir_group)
control_layout.addWidget(dir_group)
# 图像目录选择(优先指向预测结果目录)
# 2. 图像目录选择
img_dir_group = QGroupBox("图像目录")
img_dir_layout = QHBoxLayout()
self.img_dir_edit = QLineEdit()
@ -1435,18 +1372,9 @@ class Step12VizPanel(QWidget):
img_dir_layout.addWidget(self.img_dir_edit, 1)
img_dir_layout.addWidget(img_dir_browse_btn)
img_dir_group.setLayout(img_dir_layout)
left_layout.addWidget(img_dir_group)
control_layout.addWidget(img_dir_group)
# 图像目录树
tree_group = QGroupBox("图像目录")
tree_layout = QVBoxLayout()
self.image_tree = ImageCategoryTree()
self.image_tree.itemClicked.connect(self.on_tree_item_clicked)
tree_layout.addWidget(self.image_tree)
tree_group.setLayout(tree_layout)
left_layout.addWidget(tree_group, 1)
# 可视化配置
# 3. 可视化配置
config_group = QGroupBox("可视化配置")
config_layout = QVBoxLayout()
@ -1470,6 +1398,11 @@ class Step12VizPanel(QWidget):
self.gen_sampling_map.setChecked(True)
config_layout.addWidget(self.gen_sampling_map)
self.gen_distribution_map = QCheckBox("空间分布图 (Step 11 产物)")
self.gen_distribution_map.setChecked(True)
self.gen_distribution_map.setToolTip("渲染并汇总 Step 11 生成的 TIF 分布图")
config_layout.addWidget(self.gen_distribution_map)
config_layout.addSpacing(10)
line = QFrame()
line.setFrameShape(QFrame.HLine)
@ -1479,23 +1412,73 @@ class Step12VizPanel(QWidget):
self.gen_all_btn = QPushButton("🚀 生成全部")
self.gen_all_btn.setToolTip("生成所有类型的可视化图表")
self.gen_all_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
self.gen_all_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 8px; border-radius: 4px;")
self.gen_all_btn.clicked.connect(self.generate_all_visualizations)
config_layout.addWidget(self.gen_all_btn)
self.scan_btn = QPushButton("📁 扫描目录")
self.scan_btn.setToolTip("扫描工作目录中的图像文件")
self.scan_btn.setStyleSheet("padding: 6px; border-radius: 4px;")
self.scan_btn.clicked.connect(self.scan_work_directory)
config_layout.addWidget(self.scan_btn)
config_group.setLayout(config_layout)
left_layout.addWidget(config_group)
control_layout.addWidget(config_group)
left_panel.setLayout(left_layout)
left_panel.setMaximumWidth(350)
main_layout.addWidget(left_panel, 0)
control_layout.addStretch() # 把控制面板的内容往上顶
control_panel.setLayout(control_layout)
control_panel.setMaximumWidth(280) # 稍微收窄第一列
control_panel.setMinimumWidth(230)
main_layout.addWidget(control_panel, 0) # stretch=0,不横向拉伸
# ===== 右侧面板 =====
# ==========================================
# 第二列:满高独立的目录树(选图与筛选面板)
# ==========================================
from PyQt5.QtWidgets import QComboBox
tree_panel = QWidget()
tree_layout = QVBoxLayout()
tree_layout.setContentsMargins(0, 0, 0, 0)
tree_group = QGroupBox("图像浏览与筛选")
group_layout = QVBoxLayout()
group_layout.setSpacing(8)
# 添加过滤控制栏
filter_layout = QFormLayout()
filter_layout.setContentsMargins(0, 0, 0, 0)
# 分组模式下拉框
self.view_mode_cb = QComboBox()
self.view_mode_cb.addItems(["按水质参数归类", "按图表类型归类", "按物理文件夹"])
self.view_mode_cb.currentIndexChanged.connect(self.update_image_tree_view)
self.view_mode_cb.setStyleSheet("QComboBox { padding: 4px; border-radius: 4px; border: 1px solid #ccc; }")
# 图表类型筛选下拉框
self.chart_filter_cb = QComboBox()
self.chart_filter_cb.addItems(["全部图表", "空间分布图", "模型散点图", "光谱曲线图", "统计箱线图", "相关性热力图", "掩膜与预览", "采样点地图"])
self.chart_filter_cb.currentIndexChanged.connect(self.update_image_tree_view)
self.chart_filter_cb.setStyleSheet("QComboBox { padding: 4px; border-radius: 4px; border: 1px solid #ccc; }")
filter_layout.addRow("视图模式:", self.view_mode_cb)
filter_layout.addRow("类型筛选:", self.chart_filter_cb)
group_layout.addLayout(filter_layout)
# 挂载新版大树
self.image_tree = ImageCategoryTree()
self.image_tree.itemClicked.connect(self.on_tree_item_clicked)
group_layout.addWidget(self.image_tree, 1) # stretch=1 让树垂直填满
tree_group.setLayout(group_layout)
tree_layout.addWidget(tree_group, 1) # stretch=1 让GroupBox垂直填满
tree_panel.setLayout(tree_layout)
tree_panel.setMaximumWidth(320)
tree_panel.setMinimumWidth(260)
main_layout.addWidget(tree_panel, 0) # stretch=0,不抢占右侧图片的宽度
# ==========================================
# 第三列:图像查看器
# ==========================================
right_panel = QWidget()
right_layout = QVBoxLayout()
right_layout.setContentsMargins(0, 0, 0, 0)
@ -1503,10 +1486,33 @@ class Step12VizPanel(QWidget):
self.image_viewer.refresh_btn.clicked.connect(self.scan_work_directory)
right_layout.addWidget(self.image_viewer, 1)
right_panel.setLayout(right_layout)
main_layout.addWidget(right_panel, 1)
main_layout.addWidget(right_panel, 1) # stretch=1,右侧画板填满所有剩余宽度
self.setLayout(main_layout)
def update_image_tree_view(self):
"""响应下拉框改变,重新渲染树状图"""
if not hasattr(self, 'image_tree') or not self.image_tree._all_image_files:
return
# 1. 提取当前选中的分组模式
mode_idx = self.view_mode_cb.currentIndex()
if mode_idx == 0:
group_mode = 'parameter'
elif mode_idx == 1:
group_mode = 'type'
else:
group_mode = 'folder'
# 2. 提取当前的图表筛选条件
filter_type = self.chart_filter_cb.currentText()
if filter_type == "全部图表":
filter_type = 'all'
# 3. 触发重绘
self.image_tree.rebuild_tree(group_mode, filter_type)
self._load_first_image_from_tree()
def set_work_dir(self, work_dir):
"""设置工作目录"""
self.work_dir = work_dir
@ -1678,7 +1684,7 @@ class Step12VizPanel(QWidget):
return
if not (self.gen_scatter.isChecked() or self.gen_spectrum.isChecked() or
self.gen_boxplots.isChecked() or self.gen_mask_glint.isChecked() or
self.gen_sampling_map.isChecked()):
self.gen_sampling_map.isChecked() or self.gen_distribution_map.isChecked()):
QMessageBox.information(self, "提示", "请至少勾选一项可视化配置选项以生成图表。")
return
reply = QMessageBox.question(
@ -1694,9 +1700,20 @@ class Step12VizPanel(QWidget):
"gen_boxplots": self.gen_boxplots.isChecked(),
"gen_mask_glint": self.gen_mask_glint.isChecked(),
"gen_sampling_map": self.gen_sampling_map.isChecked(),
"gen_distribution_map": self.gen_distribution_map.isChecked(),
}
main_window = self.window()
factory = getattr(main_window, '_panel_factory', None) if main_window else None
# [新增] 直接从 Step 1 面板读取原始 .shp 的绝对路径,突破工作目录限制
step1_panel = factory.get_panel('step1_mask') if factory else None
if step1_panel:
s1_conf = step1_panel.get_config()
s1_mask = s1_conf.get('mask_path')
# 确保文件存在且是shp格式,存入extra透传给后台线程
if s1_mask and Path(s1_mask).is_file() and str(s1_mask).lower().endswith('.shp'):
extra["boundary_shp_path"] = str(s1_mask)
step6_panel = factory.get_panel('step6_feature') if factory else None
if step6_panel and getattr(step6_panel, 'output_file', None):
_resolved_csv = step6_panel.output_file.get_path()
@ -1886,6 +1903,7 @@ class Step12VizPanel(QWidget):
'generate_spectrum': self.gen_spectrum.isChecked(),
'generate_glint_previews': self.gen_mask_glint.isChecked(),
'generate_sampling_maps': self.gen_sampling_map.isChecked(),
'generate_distribution_maps': self.gen_distribution_map.isChecked(),
'scatter_config': {
'metric': 'test_r2', 'feature_start_column': 13,
'test_size': 0.2, 'random_state': 42
@ -1913,3 +1931,5 @@ class Step12VizPanel(QWidget):
self.gen_mask_glint.setChecked(config['generate_glint_previews'])
if 'generate_sampling_maps' in config:
self.gen_sampling_map.setChecked(config.get('generate_sampling_maps', True))
if 'generate_distribution_maps' in config:
self.gen_distribution_map.setChecked(config.get('generate_distribution_maps', True))