修复 PyQt 0xC0000409 崩溃:修复 window 属性命名冲突、全局异常钩子、可视化面板健壮重构
This commit is contained in:
@ -102,11 +102,11 @@ class Step6_5Panel(QWidget):
|
|||||||
self.spectral_start_col.setValue(1)
|
self.spectral_start_col.setValue(1)
|
||||||
params_layout.addRow("光谱起始列索引:", self.spectral_start_col)
|
params_layout.addRow("光谱起始列索引:", self.spectral_start_col)
|
||||||
|
|
||||||
# 窗口大小
|
# 窗口大小 (变量名已修正,避免覆盖 QWidget.window)
|
||||||
self.window = QSpinBox()
|
self.window_size_spinbox = QSpinBox()
|
||||||
self.window.setRange(1, 20)
|
self.window_size_spinbox.setRange(1, 20)
|
||||||
self.window.setValue(5)
|
self.window_size_spinbox.setValue(5)
|
||||||
params_layout.addRow("窗口大小:", self.window)
|
params_layout.addRow("窗口大小:", self.window_size_spinbox)
|
||||||
|
|
||||||
params_group.setLayout(params_layout)
|
params_group.setLayout(params_layout)
|
||||||
layout.addWidget(params_group)
|
layout.addWidget(params_group)
|
||||||
@ -160,7 +160,7 @@ class Step6_5Panel(QWidget):
|
|||||||
'algorithms': selected_algorithms,
|
'algorithms': selected_algorithms,
|
||||||
'value_cols': value_cols,
|
'value_cols': value_cols,
|
||||||
'spectral_start_col': self.spectral_start_col.value(),
|
'spectral_start_col': self.spectral_start_col.value(),
|
||||||
'window': self.window.value(),
|
'window': self.window_size_spinbox.value(),
|
||||||
'enabled': self.enable_checkbox.isChecked()
|
'enabled': self.enable_checkbox.isChecked()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,7 +205,7 @@ class Step6_5Panel(QWidget):
|
|||||||
self.spectral_start_col.setValue(config['spectral_start_col'])
|
self.spectral_start_col.setValue(config['spectral_start_col'])
|
||||||
|
|
||||||
if 'window' in config:
|
if 'window' in config:
|
||||||
self.window.setValue(config['window'])
|
self.window_size_spinbox.setValue(config['window'])
|
||||||
if 'output_dir' in config:
|
if 'output_dir' in config:
|
||||||
self.output_dir.set_path(config['output_dir'])
|
self.output_dir.set_path(config['output_dir'])
|
||||||
if 'csv_path' in config:
|
if 'csv_path' in config:
|
||||||
@ -218,6 +218,9 @@ class Step6_5Panel(QWidget):
|
|||||||
work_dir: 工作目录路径
|
work_dir: 工作目录路径
|
||||||
pipeline: Pipeline 实例(未使用,保留接口兼容性)
|
pipeline: Pipeline 实例(未使用,保留接口兼容性)
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
import traceback
|
||||||
|
|
||||||
if work_dir:
|
if work_dir:
|
||||||
self.work_dir = work_dir
|
self.work_dir = work_dir
|
||||||
elif hasattr(self, 'work_dir') and self.work_dir:
|
elif hasattr(self, 'work_dir') and self.work_dir:
|
||||||
@ -225,10 +228,17 @@ class Step6_5Panel(QWidget):
|
|||||||
else:
|
else:
|
||||||
self.work_dir = None
|
self.work_dir = None
|
||||||
|
|
||||||
# 1. 尝试从 Step5 界面读取训练光谱 CSV 路径
|
# 借用父组件的 window() 方法,安全绕过当前类的命名冲突
|
||||||
main_window = self.window()
|
parent_widget = self.parentWidget()
|
||||||
|
main_window = parent_widget.window() if parent_widget else None
|
||||||
if main_window and hasattr(main_window, 'step5_panel'):
|
if main_window and hasattr(main_window, 'step5_panel'):
|
||||||
step5_output_path = main_window.step5_panel.output_file.get_path()
|
step5_widget = getattr(main_window.step5_panel, 'output_file', None)
|
||||||
|
step5_output_path = ""
|
||||||
|
if hasattr(step5_widget, 'get_path'):
|
||||||
|
step5_output_path = step5_widget.get_path() or ""
|
||||||
|
elif hasattr(step5_widget, 'text'):
|
||||||
|
step5_output_path = step5_widget.text() or ""
|
||||||
|
|
||||||
if step5_output_path:
|
if step5_output_path:
|
||||||
# 若为相对路径,使用 work_dir 合成为绝对路径
|
# 若为相对路径,使用 work_dir 合成为绝对路径
|
||||||
if not os.path.isabs(step5_output_path):
|
if not os.path.isabs(step5_output_path):
|
||||||
@ -244,12 +254,18 @@ class Step6_5Panel(QWidget):
|
|||||||
existing_out = self.output_dir.get_path()
|
existing_out = self.output_dir.get_path()
|
||||||
if not existing_out or not existing_out.strip():
|
if not existing_out or not existing_out.strip():
|
||||||
self.output_dir.set_path(output_dir)
|
self.output_dir.set_path(output_dir)
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print(f"【{self.__class__.__name__}】自动填充失败,跳过: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def _get_default_work_dir(self):
|
def _get_default_work_dir(self):
|
||||||
"""获取 work_dir,优先用 panel 自身缓存的,否则尝试从主窗口取"""
|
"""获取 work_dir,优先用 panel 自身缓存的,否则尝试从主窗口取"""
|
||||||
if hasattr(self, 'work_dir') and self.work_dir:
|
if hasattr(self, 'work_dir') and self.work_dir:
|
||||||
return str(self.work_dir)
|
return str(self.work_dir)
|
||||||
mw = self.window()
|
# 借用父组件的 window() 方法,安全绕过当前类的命名冲突
|
||||||
|
parent_widget = self.parentWidget()
|
||||||
|
mw = parent_widget.window() if parent_widget else None
|
||||||
if mw and hasattr(mw, 'work_dir') and mw.work_dir:
|
if mw and hasattr(mw, 'work_dir') and mw.work_dir:
|
||||||
return str(mw.work_dir)
|
return str(mw.work_dir)
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@ -493,17 +493,9 @@ class ChartViewerDialog(QDialog):
|
|||||||
|
|
||||||
|
|
||||||
class ImageCategoryTree(QTreeWidget):
|
class ImageCategoryTree(QTreeWidget):
|
||||||
"""图像分类目录树 - 按类别组织图像文件"""
|
"""图像分类目录树 - 按真实物理文件夹结构组织图像文件"""
|
||||||
|
|
||||||
CATEGORIES = [
|
# 文件名中文翻译映射(key: 文件名前缀 → 中文显示名)
|
||||||
("模型评估", ["scatter", "regression", "validation", "r2", "rmse"], "📊"),
|
|
||||||
("光谱分析", ["spectrum", "spectral", "band", "wavelength"], "📈"),
|
|
||||||
("统计图表", ["boxplot", "histogram", "heatmap", "statistics", "stats"], "📉"),
|
|
||||||
("处理结果", ["mask", "glint", "deglint", "preview", "overlay", "water_mask"], "🖼️"),
|
|
||||||
("含量分布图", [], "📁"),
|
|
||||||
]
|
|
||||||
|
|
||||||
# 文件名中文翻译映射
|
|
||||||
NAME_MAPPING = {
|
NAME_MAPPING = {
|
||||||
"hsi_preview": "高光谱影像预览",
|
"hsi_preview": "高光谱影像预览",
|
||||||
"hsi_original": "原始高光谱影像",
|
"hsi_original": "原始高光谱影像",
|
||||||
@ -537,14 +529,52 @@ class ImageCategoryTree(QTreeWidget):
|
|||||||
"error_analysis": "误差分析图",
|
"error_analysis": "误差分析图",
|
||||||
"rmse": "RMSE评估图",
|
"rmse": "RMSE评估图",
|
||||||
"r2_score": "R²得分图",
|
"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": "可视化产物",
|
||||||
|
"glint_deglint_previews": "耀斑与去耀斑预览",
|
||||||
|
"sampling_maps": "采样点地图",
|
||||||
|
"scatter_plots": "模型评估散点图",
|
||||||
|
"flight_maps": "飞行轨迹图",
|
||||||
|
"11_12_13_predictions": "预测结果",
|
||||||
|
"Machine_Learning_Prediction": "机器学习预测",
|
||||||
|
"Non_Empirical_Prediction": "非经验模型预测",
|
||||||
|
"Custom_Regression_Prediction": "自定义回归预测",
|
||||||
|
"8_Regression_Modeling": "回归建模",
|
||||||
|
"10_feature_construction": "特征构建",
|
||||||
|
"5_training_spectra": "训练光谱",
|
||||||
|
"2_glint": "耀斑分析",
|
||||||
|
"3_deglint": "去耀斑处理",
|
||||||
|
"1_water_mask": "水掩膜",
|
||||||
|
"9_water_quality_prediction": "水质预测",
|
||||||
|
"8_spatial_inversion": "空间反演",
|
||||||
|
"4_processed_data": "处理数据",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._dir_node_map: dict = {} # 目录路径字符串 → QTreeWidgetItem
|
||||||
|
self._work_path: Optional[Path] = None
|
||||||
self.setHeaderLabel("图像目录")
|
self.setHeaderLabel("图像目录")
|
||||||
self.setMaximumWidth(300)
|
self.setMaximumWidth(300)
|
||||||
self.setMinimumWidth(250)
|
self.setMinimumWidth(250)
|
||||||
self.setup_categories()
|
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
QTreeWidget {
|
QTreeWidget {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
@ -564,77 +594,184 @@ class ImageCategoryTree(QTreeWidget):
|
|||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
def setup_categories(self):
|
|
||||||
"""初始化类别节点"""
|
|
||||||
self.category_items = {}
|
|
||||||
for category_name, keywords, icon in self.CATEGORIES:
|
|
||||||
item = QTreeWidgetItem(self)
|
|
||||||
item.setText(0, f"{icon} {category_name}")
|
|
||||||
item.setData(0, Qt.UserRole, {"type": "category", "keywords": keywords, "name": category_name})
|
|
||||||
item.setExpanded(True)
|
|
||||||
self.category_items[category_name] = item
|
|
||||||
|
|
||||||
def clear_all_images(self):
|
def clear_all_images(self):
|
||||||
"""清除所有图像项"""
|
"""清除所有图像项"""
|
||||||
for category_item in self.category_items.values():
|
try:
|
||||||
while category_item.childCount() > 0:
|
self.invisibleRootItem().takeChildren()
|
||||||
category_item.removeChild(category_item.child(0))
|
if hasattr(self, '_dir_node_map'):
|
||||||
|
self._dir_node_map.clear()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"清空树状图出错: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def add_image(self, file_path: Path, display_name: str = None):
|
def _translate_dir_name(self, dir_name: str) -> str:
|
||||||
"""添加图像到对应的类别(带中文名称翻译)"""
|
"""翻译目录名为中文"""
|
||||||
if display_name is None:
|
return self.DIR_MAPPING.get(dir_name, dir_name)
|
||||||
# 使用翻译映射,查询不到则用原文件名
|
|
||||||
file_base = file_path.stem
|
|
||||||
display_name = self.NAME_MAPPING.get(file_base, file_base)
|
|
||||||
|
|
||||||
category = self._determine_category(file_path.name)
|
def _translate_filename(self, filename: str) -> str:
|
||||||
category_item = self.category_items.get(category, self.category_items["含量分布图"])
|
"""翻译文件名为中文(动态替换后缀片段)"""
|
||||||
|
# 依次替换常见后缀模式
|
||||||
|
replacements = [
|
||||||
|
("_spectrum_comparison", " 光谱曲线对比图"),
|
||||||
|
("_scatter_true_vs_pred", " 真值-预测散点图"),
|
||||||
|
("_true_vs_pred", " 真值-预测散点图"),
|
||||||
|
("_histogram", " 分布直方图"),
|
||||||
|
("_boxplot", " 箱线图"),
|
||||||
|
("_distribution_map", " 分布图"),
|
||||||
|
("_distribution_enhanced", " 增强分布图"),
|
||||||
|
("_thematic_map", " 专题图"),
|
||||||
|
("_water_quality_map", " 水质分布图"),
|
||||||
|
("_prediction_map", " 预测结果图"),
|
||||||
|
("_inversion_map", " 反演结果图"),
|
||||||
|
("_glint_deglint", " 耀斑去耀斑对比"),
|
||||||
|
("_glint_mask", " 耀斑掩膜"),
|
||||||
|
("_deglint", " 去耀斑"),
|
||||||
|
("_mask_overlay", " 掩膜叠加"),
|
||||||
|
("_content", " 含量"),
|
||||||
|
("_distribution", " 分布"),
|
||||||
|
("_prediction", " 预测"),
|
||||||
|
("_inversion", " 反演"),
|
||||||
|
("_enhanced", " 增强"),
|
||||||
|
("_scatter", " 散点图"),
|
||||||
|
("_boxplot", " 箱线图"),
|
||||||
|
("_correlation_heatmap", " 相关性热力图"),
|
||||||
|
("_parameter_boxplot", " 箱线图"),
|
||||||
|
("_sampling_point", " 采样点"),
|
||||||
|
("_sampling_points", " 采样点"),
|
||||||
|
("_flight_path", " 飞行轨迹"),
|
||||||
|
("_trajectory", " 轨迹"),
|
||||||
|
]
|
||||||
|
result = filename
|
||||||
|
for pattern, replacement in replacements:
|
||||||
|
result = result.replace(pattern, replacement)
|
||||||
|
|
||||||
image_item = QTreeWidgetItem(category_item)
|
# 处理参数名(常见水质参数翻译)
|
||||||
image_item.setText(0, f" └─ {display_name}")
|
param_map = {
|
||||||
|
"Chla": "叶绿素", "COD": "化学需氧量", "TN": "总氮", "TP": "总磷",
|
||||||
|
"Turbidity": "浊度", "DO": "溶解氧", "pH": "pH值",
|
||||||
|
"Conductivity": "电导率", "BOD": "生化需氧量", "NH3_N": "氨氮",
|
||||||
|
}
|
||||||
|
for eng, chn in param_map.items():
|
||||||
|
# 在文件名中找到参数名并翻译
|
||||||
|
if eng.lower() in result.lower():
|
||||||
|
result = result.replace(eng, chn)
|
||||||
|
if eng.lower() in result.lower():
|
||||||
|
result = result.replace(eng.lower(), chn)
|
||||||
|
|
||||||
|
# 如果没有任何替换,返回原文件名(去掉扩展名)
|
||||||
|
if result == filename:
|
||||||
|
return filename.rsplit(".", 1)[0] if "." in filename else filename
|
||||||
|
return result
|
||||||
|
|
||||||
|
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
|
||||||
|
image_item = QTreeWidgetItem(parent_item)
|
||||||
|
image_item.setText(0, f" 🖼 {display_name}")
|
||||||
image_item.setData(0, Qt.UserRole, {"type": "image", "path": str(file_path), "display_name": 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))
|
image_item.setToolTip(0, str(file_path))
|
||||||
|
|
||||||
return image_item
|
return image_item
|
||||||
|
|
||||||
def _determine_category(self, filename: str) -> str:
|
|
||||||
"""根据文件名确定类别"""
|
|
||||||
filename_lower = filename.lower()
|
|
||||||
|
|
||||||
for category_name, keywords, _ in self.CATEGORIES:
|
|
||||||
if any(keyword in filename_lower for keyword in keywords):
|
|
||||||
return category_name
|
|
||||||
|
|
||||||
return "含量分布图"
|
|
||||||
|
|
||||||
def scan_directory(self, work_dir: str):
|
def scan_directory(self, work_dir: str):
|
||||||
"""扫描目录中的所有图像文件(深度递归扫描)"""
|
"""扫描目录中的所有图像文件(深度递归扫描)—— 按真实物理目录结构挂载"""
|
||||||
self.clear_all_images()
|
try:
|
||||||
|
if not work_dir:
|
||||||
work_path = Path(work_dir)
|
print("可视化面板:工作目录为空,跳过扫描")
|
||||||
if not work_path.exists():
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._work_path = Path(work_dir)
|
||||||
|
|
||||||
|
# 阻塞信号,防止在清空树状图时触发 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']
|
image_extensions = ['*.png', '*.jpg', '*.jpeg', '*.tif', '*.tiff', '*.bmp']
|
||||||
|
|
||||||
# 扫描根目录列表(按优先级)
|
# 拓宽扫描根目录列表(新增多个遗漏目录)
|
||||||
scan_roots: List[Path] = [
|
scan_roots: List[Path] = [
|
||||||
work_path / "14_visualization",
|
self._work_path / "14_visualization",
|
||||||
work_path / "1_water_mask",
|
self._work_path / "11_12_13_predictions",
|
||||||
work_path / "9_water_quality_prediction",
|
self._work_path / "8_Regression_Modeling",
|
||||||
work_path / "10_feature_construction",
|
self._work_path / "10_feature_construction",
|
||||||
|
self._work_path / "5_training_spectra",
|
||||||
|
self._work_path / "2_glint",
|
||||||
|
self._work_path / "3_deglint",
|
||||||
|
self._work_path / "1_water_mask",
|
||||||
|
self._work_path / "9_water_quality_prediction",
|
||||||
]
|
]
|
||||||
|
|
||||||
# 只保留存在的目录,并补充工作根目录作为兜底
|
# 只保留存在的目录,并补充工作根目录作为兜底
|
||||||
scan_roots = [p for p in scan_roots if p.is_dir()]
|
scan_roots = [p for p in scan_roots if p.is_dir()]
|
||||||
if not scan_roots:
|
if not scan_roots:
|
||||||
scan_roots.append(work_path)
|
scan_roots.append(self._work_path)
|
||||||
|
|
||||||
seen_norm: set = set()
|
seen_norm: set = set()
|
||||||
image_files: List[Path] = []
|
image_files: List[Path] = []
|
||||||
for root in scan_roots:
|
for root in scan_roots:
|
||||||
for ext in image_extensions:
|
for ext in image_extensions:
|
||||||
# 使用 rglob 进行深度递归扫描
|
|
||||||
for p in root.rglob(ext):
|
for p in root.rglob(ext):
|
||||||
key = os.path.normcase(os.path.normpath(str(p.resolve())))
|
key = os.path.normcase(os.path.normpath(str(p.resolve())))
|
||||||
if key in seen_norm:
|
if key in seen_norm:
|
||||||
@ -645,15 +782,23 @@ class ImageCategoryTree(QTreeWidget):
|
|||||||
for img_file in sorted(image_files):
|
for img_file in sorted(image_files):
|
||||||
if img_file.name.startswith('.') or 'thumb' in img_file.name.lower():
|
if img_file.name.startswith('.') or 'thumb' in img_file.name.lower():
|
||||||
continue
|
continue
|
||||||
self.add_image(img_file)
|
self.add_image_by_dir(img_file, self._work_path)
|
||||||
|
|
||||||
for category_name, item in self.category_items.items():
|
# 更新目录节点计数
|
||||||
|
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()
|
count = item.childCount()
|
||||||
if count > 0:
|
name = item.text(0)
|
||||||
for cat_name, _, icon in self.CATEGORIES:
|
if count > 0 and f"({count})" not in name:
|
||||||
if cat_name == category_name:
|
# 从目录名中提取显示名并附加计数
|
||||||
item.setText(0, f"{icon} {category_name} ({count})")
|
display = name.strip()
|
||||||
break
|
item.setText(0, f" 📂 {display} ({count})")
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print(f"可视化面板图片挂载出错: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def get_selected_image_path(self) -> Optional[str]:
|
def get_selected_image_path(self) -> Optional[str]:
|
||||||
"""获取当前选中的图像路径"""
|
"""获取当前选中的图像路径"""
|
||||||
@ -1295,6 +1440,7 @@ class VisualizationPanel(QWidget):
|
|||||||
4. {work_dir}/14_visualization(可视化目录)
|
4. {work_dir}/14_visualization(可视化目录)
|
||||||
5. {work_dir}(工作目录根)
|
5. {work_dir}(工作目录根)
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
if work_dir:
|
if work_dir:
|
||||||
self.work_dir = work_dir
|
self.work_dir = work_dir
|
||||||
self.work_dir_edit.setText(str(work_dir))
|
self.work_dir_edit.setText(str(work_dir))
|
||||||
@ -1328,21 +1474,44 @@ class VisualizationPanel(QWidget):
|
|||||||
|
|
||||||
# 自动触发加载第一张图像
|
# 自动触发加载第一张图像
|
||||||
self._load_first_image_from_tree()
|
self._load_first_image_from_tree()
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print(f"可视化面板 update_from_config 出错: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def _load_first_image_from_tree(self):
|
def _load_first_image_from_tree(self):
|
||||||
"""从目录树中加载第一张图像到右侧查看器"""
|
"""自动加载树状图中的第一张有效图片(兼容物理目录层级结构)"""
|
||||||
tree = self.image_tree
|
try:
|
||||||
if tree is None:
|
tree = getattr(self, 'image_tree', None)
|
||||||
|
if not tree:
|
||||||
return
|
return
|
||||||
for category_item in tree.category_items.values():
|
|
||||||
for i in range(category_item.childCount()):
|
from PyQt5.QtCore import Qt
|
||||||
child = category_item.child(i)
|
|
||||||
data = child.data(0, Qt.UserRole)
|
def find_first_image(item):
|
||||||
if data and data.get("type") == "image":
|
# 检查当前节点是否是图片节点
|
||||||
img_path = data.get("path")
|
data = item.data(0, Qt.UserRole)
|
||||||
if img_path and Path(img_path).exists():
|
if isinstance(data, dict) and data.get("type") == "image":
|
||||||
self.image_viewer.load_image(img_path)
|
return item
|
||||||
|
# 如果不是,递归检查所有子节点
|
||||||
|
for i in range(item.childCount()):
|
||||||
|
found = find_first_image(item.child(i))
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 遍历所有顶层节点
|
||||||
|
for i in range(tree.topLevelItemCount()):
|
||||||
|
first_img_item = find_first_image(tree.topLevelItem(i))
|
||||||
|
if first_img_item:
|
||||||
|
tree.setCurrentItem(first_img_item)
|
||||||
|
# 主动触发一次点击槽函数,以在右侧渲染图片
|
||||||
|
self.on_tree_item_clicked(first_img_item, 0)
|
||||||
return
|
return
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print(f"自动加载首张图片失败: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def scan_work_directory(self):
|
def scan_work_directory(self):
|
||||||
"""扫描工作目录中的图像文件"""
|
"""扫描工作目录中的图像文件"""
|
||||||
|
|||||||
@ -29,6 +29,18 @@ from PyQt5.QtWidgets import (
|
|||||||
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer, QAbstractTableModel, QSize
|
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer, QAbstractTableModel, QSize
|
||||||
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPalette, QColor, QPixmap
|
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPalette, QColor, QPixmap
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
def global_exception_handler(exc_type, exc_value, exc_traceback):
|
||||||
|
print("\n" + "="*50)
|
||||||
|
print("【严重错误拦截 - PyQt 崩溃死因】")
|
||||||
|
traceback.print_exception(exc_type, exc_value, exc_traceback)
|
||||||
|
print("="*50 + "\n")
|
||||||
|
|
||||||
|
# 挂载全局异常钩子,阻止 PyQt 静默闪退
|
||||||
|
sys.excepthook = global_exception_handler
|
||||||
|
|
||||||
# 导入样式模块 - 兼容开发环境和 PyInstaller 打包
|
# 导入样式模块 - 兼容开发环境和 PyInstaller 打包
|
||||||
try:
|
try:
|
||||||
from styles import ModernStylesheet
|
from styles import ModernStylesheet
|
||||||
|
|||||||
Reference in New Issue
Block a user