Files
BRDF/GUI/brdf_gui.py
2026-04-22 09:27:59 +08:00

2883 lines
107 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
BRDF校正工具 - 统一GUI应用
支持陆地植被BRDF校正FlexBRDF和水体BRDF校正ocbrdf两个模块
"""
import sys
import os
import json
import subprocess
import threading
from pathlib import Path
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGroupBox,
QFormLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QComboBox,
QCheckBox, QRadioButton, QSpinBox, QDoubleSpinBox, QTabWidget, QMessageBox, QScrollArea,
QStackedWidget, QGridLayout, QDialog, QDialogButtonBox,
QListWidget, QListWidgetItem, QTextEdit, QProgressBar, QSplitter
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QItemSelectionModel
from PyQt5.QtGui import QFont, QIcon, QPalette, QWheelEvent
# ==================== 禁用滚轮的自定义控件 ====================
class NoWheelSpinBox(QSpinBox):
"""禁用鼠标滚轮的SpinBox"""
def wheelEvent(self, event: QWheelEvent):
event.ignore()
class NoWheelDoubleSpinBox(QDoubleSpinBox):
"""禁用鼠标滚轮的DoubleSpinBox"""
def wheelEvent(self, event: QWheelEvent):
event.ignore()
class NoWheelComboBox(QComboBox):
"""禁用鼠标滚轮的ComboBox"""
def wheelEvent(self, event: QWheelEvent):
event.ignore()
# ==================== 掩膜条件编辑器 ====================
class MaskConditionEditor(QWidget):
"""单个掩膜条件的编辑组件,支持类型选择和参数配置"""
# 掩膜类型及对应的参数定义
MASK_TYPES = {
"ndi": {
"params": [
("band_1", int, 550),
("band_2", int, 2150),
("min", float, -1.0),
("max", float, 0.0)
]
},
"ancillary": {
"params": [
("name", str, "slope"),
("min", float, 0.0),
("max", float, 1.0)
],
"name_choices": ["slope", "aspect", "cosine_i", "sensor_zn", "sensor_az",
"solar_zn", "solar_az", "phase", "path_length", "utc_time"]
},
"neon_edge": {
"params": [
("radius", int, 1)
]
},
"kernel_finite": {
"params": []
},
"water": {
"params": [
("band_1", int, 550),
("band_2", int, 850),
("threshold", float, 0.0)
]
},
"external": {
"params": [
("file_path", str, ""),
("class", int, 1)
]
},
"band": {
"params": [
("band", int, 650),
("min", float, 0.0),
("max", float, 1.0)
]
}
}
def __init__(self, condition=None, parent=None):
super().__init__(parent)
self.param_widgets = {} # 存储参数名对应的控件
self.init_ui()
if condition:
self.set_condition(condition)
def init_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 5)
# 类型选择行
type_layout = QHBoxLayout()
self.type_combo = NoWheelComboBox()
self.type_combo.addItems(list(self.MASK_TYPES.keys()))
self.type_combo.currentTextChanged.connect(self.on_type_changed)
type_layout.addWidget(QLabel("类型:"))
type_layout.addWidget(self.type_combo)
self.delete_btn = QPushButton("删除")
self.delete_btn.setFixedWidth(60)
type_layout.addStretch()
type_layout.addWidget(self.delete_btn)
layout.addLayout(type_layout)
# 参数区域(动态切换)
self.param_stack = QStackedWidget()
layout.addWidget(self.param_stack)
# 为每种类型创建一个参数面板
self.param_panels = {}
for mask_type, info in self.MASK_TYPES.items():
panel = self.create_param_panel(mask_type, info["params"])
self.param_panels[mask_type] = panel
self.param_stack.addWidget(panel)
self.on_type_changed(self.type_combo.currentText())
def create_param_panel(self, mask_type, params):
"""创建参数输入面板,返回 QWidget"""
widget = QWidget()
layout = QFormLayout(widget)
layout.setContentsMargins(10, 5, 10, 5)
layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
layout.setLabelAlignment(Qt.AlignLeft)
layout.setFormAlignment(Qt.AlignLeft)
for param_name, param_type, default_value in params:
if param_type == int:
if "band" in param_name or "radius" in param_name:
spin = NoWheelSpinBox()
spin.setRange(0, 3000)
spin.setValue(default_value)
param_widget = spin
else:
spin = NoWheelSpinBox()
spin.setRange(-10000, 10000)
spin.setValue(default_value)
param_widget = spin
elif param_type == float:
dspin = NoWheelDoubleSpinBox()
dspin.setRange(-1e6, 1e6)
dspin.setDecimals(6)
dspin.setValue(default_value)
param_widget = dspin
else: # str
if param_name == "name" and "name_choices" in self.MASK_TYPES[mask_type]:
combo = NoWheelComboBox()
combo.addItems(self.MASK_TYPES[mask_type]["name_choices"])
combo.setCurrentText(default_value)
param_widget = combo
elif param_name == "method" and "method_choices" in self.MASK_TYPES[mask_type]:
combo = NoWheelComboBox()
combo.addItems(self.MASK_TYPES[mask_type]["method_choices"])
combo.setCurrentText(default_value)
param_widget = combo
elif param_name == "file_path":
container = QWidget()
hbox = QHBoxLayout(container)
hbox.setContentsMargins(0, 0, 0, 0)
line_edit = QLineEdit(default_value)
btn = QPushButton("浏览")
btn.clicked.connect(lambda: self.browse_file(line_edit))
hbox.addWidget(line_edit)
hbox.addWidget(btn)
param_widget = container
# 存储 line_edit 以便获取值
setattr(param_widget, "line_edit", line_edit)
else:
line = QLineEdit(str(default_value))
param_widget = line
self.param_widgets[(mask_type, param_name)] = param_widget
label = param_name.replace("_", " ").title()
layout.addRow(f"{label}:", param_widget)
return widget
def browse_file(self, line_edit):
file_path, _ = QFileDialog.getOpenFileName(self, "选择外部掩膜文件")
if file_path:
line_edit.setText(file_path)
def on_type_changed(self, mask_type):
"""切换掩膜类型时显示对应的参数面板"""
idx = self.param_stack.indexOf(self.param_panels[mask_type])
if idx >= 0:
self.param_stack.setCurrentIndex(idx)
def get_condition(self):
"""返回当前条件的列表格式,如 ['ndi', {'band_1':550, ...}]"""
mask_type = self.type_combo.currentText()
params = {}
for (mt, param_name), widget in self.param_widgets.items():
if mt != mask_type:
continue
# 获取值
if isinstance(widget, QSpinBox):
value = widget.value()
elif isinstance(widget, QDoubleSpinBox):
value = widget.value()
elif isinstance(widget, QComboBox):
value = widget.currentText()
elif hasattr(widget, "line_edit"): # 文件路径容器
value = widget.line_edit.text()
elif isinstance(widget, QLineEdit):
value = widget.text()
else:
continue
# 特殊处理 external 的 class 参数(关键字 class 与 Python 冲突)
if param_name == "class":
param_name = "class"
params[param_name] = value
# 对于 kernel_finite 没有参数
if mask_type == "kernel_finite":
return [mask_type, {}]
return [mask_type, params]
def set_condition(self, condition):
"""从已有的条件(列表格式)填充界面"""
if not isinstance(condition, list) or len(condition) < 2:
return
mask_type = condition[0]
params = condition[1]
if mask_type not in self.MASK_TYPES:
return
self.type_combo.setCurrentText(mask_type)
# 设置参数值
for param_name, value in params.items():
# 处理关键字 class
if param_name == "class":
param_name = "class"
widget = self.param_widgets.get((mask_type, param_name))
if widget is None:
continue
if isinstance(widget, QSpinBox):
widget.setValue(int(value))
elif isinstance(widget, QDoubleSpinBox):
widget.setValue(float(value))
elif isinstance(widget, QComboBox):
idx = widget.findText(str(value))
if idx >= 0:
widget.setCurrentIndex(idx)
elif hasattr(widget, "line_edit"):
widget.line_edit.setText(str(value))
elif isinstance(widget, QLineEdit):
widget.setText(str(value))
class MaskListWidget(QWidget):
"""管理一组掩膜条件的列表,支持添加、删除、清空"""
def __init__(self, parent=None):
super().__init__(parent)
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(5)
self.conditions = [] # 存储 MaskConditionEditor 控件
# 添加按钮
add_btn = QPushButton("+ 添加掩膜条件")
add_btn.clicked.connect(self.add_condition)
self.layout.addWidget(add_btn)
# 容器用于存放条件编辑器
self.conditions_container = QWidget()
self.conditions_layout = QVBoxLayout(self.conditions_container)
self.conditions_layout.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.conditions_container)
def add_condition(self, condition=None):
editor = MaskConditionEditor(condition)
editor.delete_btn.clicked.connect(lambda: self.remove_condition(editor))
self.conditions.append(editor)
self.conditions_layout.addWidget(editor)
return editor
def remove_condition(self, editor):
if editor in self.conditions:
self.conditions.remove(editor)
editor.deleteLater()
self.conditions_layout.removeWidget(editor)
def clear(self):
for editor in self.conditions[:]:
self.remove_condition(editor)
def get_conditions(self):
return [editor.get_condition() for editor in self.conditions]
def set_conditions(self, conditions_list):
self.clear()
for cond in conditions_list:
self.add_condition(cond)
# ==================== 命令执行线程 ====================
class CommandThread(QThread):
"""后台执行命令行程序的线程"""
output_updated = pyqtSignal(str) # 输出更新信号
progress_updated = pyqtSignal(int) # 进度更新信号
finished_signal = pyqtSignal(bool, str) # 完成信号(成功/失败, 消息)
def __init__(self, command, working_dir=None, env=None):
super().__init__()
self.command = command
self.working_dir = working_dir
self.env = env # 环境变量字典
self.process = None
def run(self):
try:
# 合并环境变量
run_env = os.environ.copy()
if self.env:
run_env.update(self.env)
# 设置Python环境变量以确保UTF-8编码
run_env['PYTHONIOENCODING'] = 'utf-8'
self.process = subprocess.Popen(
self.command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
cwd=self.working_dir,
shell=True,
env=run_env,
encoding='utf-8',
errors='replace' # 无法解码的字符用替换
)
while True:
output = self.process.stdout.readline()
if output == '' and self.process.poll() is not None:
break
if output:
self.output_updated.emit(output.strip())
return_code = self.process.poll()
if return_code == 0:
self.finished_signal.emit(True, "处理完成!")
else:
self.finished_signal.emit(False, f"处理失败,退出代码:{return_code}")
except Exception as e:
self.finished_signal.emit(False, f"执行错误:{str(e)}")
def stop(self):
if self.process:
self.process.terminate()
# ==================== 模块选择主窗口 ====================
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("BRDF 校正工具 - 高光谱图像处理套件")
self.setMinimumSize(1000, 700)
# 设置应用图标和样式
self.setup_styles()
# 创建主界面
self.setup_main_interface()
def setup_styles(self):
"""设置NVIDIA风格的样式"""
self.setStyleSheet("""
QMainWindow {
background-color: #000000;
}
QWidget {
background-color: #000000;
color: #ffffff;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 14px;
}
QLabel {
color: #ffffff;
font-size: 14px;
}
QPushButton {
background-color: transparent;
color: #ffffff;
border: 2px solid #76b900;
border-radius: 8px;
padding: 12px 24px;
font-weight: bold;
font-size: 16px;
min-height: 40px;
}
QPushButton:hover {
background-color: #76b900;
color: #000000;
}
QPushButton:pressed {
background-color: #5e9400;
border-color: #5e9400;
}
QGroupBox {
background-color: #1a1a1a;
border: 2px solid #76b900;
border-radius: 8px;
margin-top: 20px;
padding-top: 20px;
font-weight: bold;
font-size: 16px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 15px;
padding: 5px 15px;
color: #000000;
background-color: #76b900;
border-radius: 4px;
font-weight: bold;
}
""")
def setup_main_interface(self):
"""创建模块选择主界面"""
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(40, 40, 40, 40)
main_layout.setSpacing(30)
# 标题区域
title_label = QLabel("BRDF 校正工具")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("""
font-size: 36px;
font-weight: bold;
color: #76b900;
margin-bottom: 20px;
""")
main_layout.addWidget(title_label)
subtitle_label = QLabel("高光谱图像双向反射分布函数校正 - 支持陆地植被和水体两种场景")
subtitle_label.setAlignment(Qt.AlignCenter)
subtitle_label.setStyleSheet("""
font-size: 16px;
color: #cccccc;
margin-bottom: 40px;
""")
main_layout.addWidget(subtitle_label)
# 模块选择区域
modules_layout = QHBoxLayout()
modules_layout.setSpacing(40)
# 陆地植被BRDF模块
land_group = self.create_module_card(
title="陆地植被 BRDF 校正",
subtitle="FlexBRDF算法",
description="适用于陆地植被和地表的BRDF、地形和镜面反射校正\n支持AVIRIS、AVIRIS-NG等机载高光谱数据",
features=[
"✓ FlexBRDF 算法",
"✓ 地形校正 (SCS, SCS+C, Cosine等)",
"✓ 镜面反射校正",
"✓ 多种掩膜条件",
"✓ 并行处理支持"
],
color="#76b900",
button_text="进入陆地模块",
callback=self.open_land_module
)
modules_layout.addWidget(land_group)
# 水体BRDF模块
water_group = self.create_module_card(
title="水体 BRDF 校正",
subtitle="Ocean BRDF Correction",
description="专用于水体和海洋的BRDF校正\n支持多种BRDF模型和不确定性估算",
features=[
"✓ L11, M02, O25 BRDF模型",
"✓ 不确定性估算",
"✓ 水体掩膜自动识别",
"✓ 分块处理支持",
"✓ 多种输出格式"
],
color="#0080ff",
button_text="进入水体模块",
callback=self.open_water_module
)
modules_layout.addWidget(water_group)
main_layout.addLayout(modules_layout)
main_layout.addStretch()
def create_module_card(self, title, subtitle, description, features, color, button_text, callback):
"""创建模块卡片"""
group = QGroupBox()
group.setMinimumHeight(400)
group.setStyleSheet(f"""
QGroupBox {{
background-color: #1a1a1a;
border: 2px solid {color};
border-radius: 12px;
margin: 10px;
padding: 20px;
}}
""")
layout = QVBoxLayout(group)
layout.setSpacing(20)
# 标题
title_label = QLabel(title)
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet(f"""
font-size: 24px;
font-weight: bold;
color: {color};
margin-bottom: 5px;
""")
layout.addWidget(title_label)
# 副标题
subtitle_label = QLabel(subtitle)
subtitle_label.setAlignment(Qt.AlignCenter)
subtitle_label.setStyleSheet("""
font-size: 14px;
color: #aaaaaa;
margin-bottom: 15px;
""")
layout.addWidget(subtitle_label)
# 描述
desc_label = QLabel(description)
desc_label.setAlignment(Qt.AlignCenter)
desc_label.setWordWrap(True)
desc_label.setStyleSheet("""
font-size: 13px;
color: #dddddd;
margin-bottom: 20px;
line-height: 1.4;
""")
layout.addWidget(desc_label)
# 功能特性列表
features_label = QLabel("\n".join(features))
features_label.setAlignment(Qt.AlignLeft)
features_label.setStyleSheet("""
font-size: 12px;
color: #cccccc;
margin-bottom: 20px;
background-color: #2a2a2a;
padding: 15px;
border-radius: 6px;
""")
layout.addWidget(features_label)
layout.addStretch()
# 操作按钮
button = QPushButton(button_text)
button.setStyleSheet(f"""
QPushButton {{
background-color: transparent;
color: {color};
border: 2px solid {color};
border-radius: 8px;
padding: 12px 24px;
font-weight: bold;
font-size: 16px;
min-height: 40px;
}}
QPushButton:hover {{
background-color: {color};
color: #000000;
}}
QPushButton:pressed {{
background-color: {color};
opacity: 0.8;
}}
""")
button.clicked.connect(callback)
layout.addWidget(button)
return group
def open_land_module(self):
"""打开陆地植被BRDF模块"""
self.land_window = LandBRDFWindow()
self.land_window.show()
# 可选:隐藏主窗口
# self.hide()
def open_water_module(self):
"""打开水体BRDF模块"""
self.water_window = WaterBRDFWindow()
self.water_window.show()
# 可选:隐藏主窗口
# self.hide()
# ==================== 陆地植被BRDF模块窗口 ====================
class LandBRDFWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("陆地植被 BRDF 校正 - FlexBRDF")
self.setMinimumSize(1200, 800)
self.resize(1400, 900) # 设置默认大小
# 存储匹配后的文件对列表
self.file_pairs = [] # [(rfl_path, obs_path), ...]
# 存储每个输入文件对应的辅助文件波段映射
self.band_mappings = {} # {rfl_path: {param_name: band_index}}
# 命令执行相关
self.command_thread = None
# 设置样式和界面
self.setup_land_styles()
self.setup_land_interface()
def setup_land_styles(self):
"""设置陆地植被BRDF模块的样式"""
self.setStyleSheet("""
QMainWindow {
background-color: #000000;
}
QWidget {
background-color: #000000;
color: #ffffff;
font-family: Arial, Helvetica, sans-serif;
font-size: 14px;
}
QTabWidget::pane {
border: 2px solid #76b900;
border-radius: 2px;
background-color: #000000;
padding: 10px;
}
QTabBar::tab {
background-color: #1a1a1a;
color: #ffffff;
border: 2px solid #5e5e5e;
border-radius: 2px;
padding: 10px 20px;
margin-right: 5px;
font-weight: 700;
font-size: 14px;
}
QTabBar::tab:selected {
background-color: #76b900;
color: #000000;
border: 2px solid #76b900;
}
QTabBar::tab:hover:!selected {
background-color: #3860be;
border: 2px solid #3860be;
}
QPushButton {
background-color: transparent;
color: #ffffff;
border: 2px solid #76b900;
border-radius: 2px;
padding: 8px 16px;
font-weight: 700;
font-size: 14px;
}
QPushButton:hover {
background-color: #1eaedb;
color: #ffffff;
}
QPushButton:pressed {
background-color: #007fff;
color: #ffffff;
border: 1px solid #003eff;
}
QLineEdit {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #5e5e5e;
border-radius: 2px;
padding: 5px;
}
QLineEdit:focus {
border: 1px solid #76b900;
}
QListWidget {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #5e5e5e;
border-radius: 2px;
padding: 5px;
}
QListWidget::item {
padding: 5px;
}
QListWidget::item:selected {
background-color: #76b900;
color: #000000;
}
QListWidget::item:hover {
background-color: #3860be;
}
QGroupBox {
background-color: #1a1a1a;
border: 1px solid #5e5e5e;
border-radius: 2px;
margin-top: 10px;
padding-top: 10px;
font-weight: 700;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px;
color: #ffffff;
}
QLabel {
color: #ffffff;
font-size: 14px;
}
QScrollArea {
border: none;
background-color: #000000;
}
QTextEdit {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #5e5e5e;
border-radius: 2px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
}
QProgressBar {
background-color: #1a1a1a;
border: 1px solid #5e5e5e;
border-radius: 2px;
text-align: center;
color: #ffffff;
}
QProgressBar::chunk {
background-color: #76b900;
border-radius: 2px;
}
""")
def setup_land_interface(self):
"""创建陆地植被BRDF模块界面"""
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(15)
main_layout.setContentsMargins(20, 20, 20, 20)
# 创建分割器:左侧配置,右侧日志
splitter = QSplitter(Qt.Horizontal)
# 左侧:配置选项卡
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
tabs = QTabWidget()
tabs.setDocumentMode(True)
# tabs.setMaximumHeight(500) # 限制标签页高度,确保按钮可见
left_layout.addWidget(tabs, 1) # stretch factor = 1
# 1. 文件与波段映射页
self.file_tab = QWidget()
tabs.addTab(self.file_tab, "文件与波段映射")
self.setup_file_tab()
# 2. 校正参数页
self.corr_tab = QWidget()
tabs.addTab(self.corr_tab, "校正参数")
self.setup_corr_tab()
# 3. 输出设置页
self.output_tab = QWidget()
tabs.addTab(self.output_tab, "输出设置")
self.setup_output_tab()
# 底部按钮区域 - 使用独立的分组框使其更显眼
btn_group = QGroupBox("操作")
btn_group.setMinimumHeight(90) # 确保按钮组有足够高度
btn_group.setStyleSheet("""
QGroupBox {
background-color: #1a1a1a;
border: 2px solid #76b900;
border-radius: 4px;
margin-top: 5px;
padding: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 2px 10px;
color: #76b900;
font-weight: bold;
font-size: 13px;
}
""")
btn_layout = QHBoxLayout(btn_group)
btn_layout.setSpacing(15)
load_btn = QPushButton("📂 加载 JSON 配置")
load_btn.setMinimumHeight(40)
load_btn.clicked.connect(self.load_config)
save_btn = QPushButton("💾 保存 JSON 配置")
save_btn.setMinimumHeight(40)
save_btn.clicked.connect(self.save_config)
generate_run_btn = QPushButton("▶ 生成配置并运行")
generate_run_btn.setMinimumHeight(55)
generate_run_btn.setMinimumWidth(220)
generate_run_btn.setStyleSheet("""
QPushButton {
background-color: #76b900;
color: #000000;
border: 3px solid #76b900;
border-radius: 8px;
font-weight: bold;
font-size: 16px;
padding: 15px 30px;
}
QPushButton:hover {
background-color: #8fce00;
border-color: #8fce00;
}
QPushButton:pressed {
background-color: #5e9400;
border-color: #5e9400;
}
""")
generate_run_btn.clicked.connect(self.generate_and_run)
btn_layout.addWidget(load_btn)
btn_layout.addWidget(save_btn)
btn_layout.addStretch()
btn_layout.addWidget(generate_run_btn)
left_layout.addWidget(btn_group, 0, Qt.AlignBottom) # 固定高度,底部对齐
# 右侧:日志和进度显示
right_widget = QWidget()
right_layout = QVBoxLayout(right_widget)
log_label = QLabel("执行日志")
log_label.setStyleSheet("font-weight: bold; color: #76b900; font-size: 16px;")
right_layout.addWidget(log_label)
self.log_text = QTextEdit()
self.log_text.setMaximumWidth(400)
self.log_text.setPlainText("等待执行命令...\n")
right_layout.addWidget(self.log_text)
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
right_layout.addWidget(self.progress_bar)
# 添加到分割器
splitter.addWidget(left_widget)
splitter.addWidget(right_widget)
splitter.setStretchFactor(0, 2) # 左侧占2/3
splitter.setStretchFactor(1, 1) # 右侧占1/3
main_layout.addWidget(splitter)
def generate_and_run(self):
"""生成JSON配置并运行FlexBRDF"""
if not self.file_pairs:
QMessageBox.warning(self, "警告", "没有有效的文件对,请先匹配文件。")
return
# 生成JSON配置
config = self.build_config()
# 保存临时JSON文件
temp_json_path = os.path.join(os.path.dirname(__file__), "temp_flexbrdf_config.json")
try:
with open(temp_json_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
except Exception as e:
QMessageBox.critical(self, "错误", f"保存配置文件失败: {e}")
return
# 构建命令
flexbrdf_path = os.path.join(os.path.dirname(__file__), "..", "Flexbrdf", "Flex_brdf.py")
flexbrdf_path = os.path.abspath(flexbrdf_path)
flexbrdf_dir = os.path.dirname(flexbrdf_path)
if not os.path.exists(flexbrdf_path):
QMessageBox.critical(self, "错误", f"找不到 Flex_brdf.py 文件:\n{flexbrdf_path}")
return
# 使用当前Python解释器
python_exe = sys.executable
command = f'"{python_exe}" "{flexbrdf_path}" "{temp_json_path}"'
# 显示进度条和日志
self.progress_bar.setVisible(True)
self.progress_bar.setRange(0, 0) # 不确定进度
self.log_text.clear()
self.log_text.append("开始执行 FlexBRDF 校正...")
self.log_text.append(f"Python: {python_exe}")
self.log_text.append(f"命令: {command}\n")
# 启动命令执行线程
self.command_thread = CommandThread(command, flexbrdf_dir)
self.command_thread.output_updated.connect(self.append_log)
self.command_thread.finished_signal.connect(self.command_finished)
self.command_thread.start()
def append_log(self, text):
"""添加日志文本"""
self.log_text.append(text)
# 自动滚动到底部
cursor = self.log_text.textCursor()
cursor.movePosition(cursor.End)
self.log_text.setTextCursor(cursor)
def command_finished(self, success, message):
"""命令执行完成"""
self.progress_bar.setVisible(False)
self.log_text.append(f"\n{message}")
if success:
QMessageBox.information(self, "成功", "FlexBRDF 校正完成!")
else:
QMessageBox.warning(self, "警告", f"FlexBRDF 校正失败:\n{message}")
def build_config(self):
"""构建FlexBRDF配置字典"""
config = {}
config["input_files"] = [rfl for rfl, _ in self.file_pairs]
config["file_type"] = "envi"
config["bad_bands"] = []
config["num_cpus"] = self.num_cpus_spin.value()
anc_files_dict = {}
for rfl_path, obs_path in self.file_pairs:
mapping = self.band_mappings.get(rfl_path, {})
anc_entry = {}
for param, spin in self.param_spinboxes.items():
band_idx = mapping.get(param, spin.value())
anc_entry[param] = [obs_path, band_idx]
anc_files_dict[rfl_path] = anc_entry
config["anc_files"] = anc_files_dict
corrections = []
if self.corr_topo_cb.isChecked():
corrections.append("topo")
if self.corr_brdf_cb.isChecked():
corrections.append("brdf")
if self.corr_glint_cb.isChecked():
corrections.append("glint")
config["corrections"] = corrections
config["topo"] = {
"type": self.topo_type.currentText(),
"calc_mask": self.topo_calc_mask_list.get_conditions(),
"apply_mask": self.topo_apply_mask_list.get_conditions(),
"c_fit_type": self.c_fit_type.currentText()
}
config["brdf"] = {
"type": self.brdf_type.currentText(),
"grouped": self.brdf_grouped.isChecked(),
"geometric": self.geometric_kernel.currentText(),
"volume": self.volume_kernel.currentText(),
"b/r": self.br_b_ratio.value(),
"h/b": self.hb_ratio.value(),
"sample_perc": self.sample_perc.value(),
"interp_kind": self.interp_kind.currentText(),
"calc_mask": self.brdf_calc_mask_list.get_conditions(),
"apply_mask": self.brdf_apply_mask_list.get_conditions(),
"bin_type": self.bin_type.currentText(),
"num_bins": self.num_bins.value(),
"ndvi_bin_min": self.ndvi_bin_min.value(),
"ndvi_bin_max": self.ndvi_bin_max.value(),
"ndvi_perc_min": self.ndvi_perc_min.value(),
"ndvi_perc_max": self.ndvi_perc_max.value(),
"solar_zn_type": self.solar_zn_type.currentText()
}
config["glint"] = {
"type": self.glint_type.currentText(),
"correction_wave": self.correction_wave.value(),
"apply_mask": self.glint_apply_mask_list.get_conditions()
}
subset_waves = []
if self.subset_waves_edit.text().strip():
try:
subset_waves = [int(x.strip()) for x in self.subset_waves_edit.text().split(",") if x.strip()]
except:
pass
config["export"] = {
"coeffs": self.coeffs_cb.isChecked(),
"image": self.image_cb.isChecked(),
"masks": self.masks_cb.isChecked(),
"subset_waves": subset_waves,
"output_dir": self.output_dir_edit.text(),
"suffix": self.suffix_edit.text()
}
config["resample"] = self.resample_cb.isChecked()
return config
# ==================== 文件与波段映射页 ====================
def setup_file_tab(self):
# 创建滚动区域作为主布局
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setStyleSheet("""
QScrollArea {
border: none;
background-color: #000000;
}
QScrollBar:vertical {
background-color: #1a1a1a;
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background-color: #5e5e5e;
border-radius: 6px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background-color: #76b900;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0px;
}
QScrollBar:horizontal {
background-color: #1a1a1a;
height: 12px;
border-radius: 6px;
}
QScrollBar::handle:horizontal {
background-color: #5e5e5e;
border-radius: 6px;
min-width: 20px;
}
QScrollBar::handle:horizontal:hover {
background-color: #76b900;
}
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
width: 0px;
}
""")
# 创建内容容器
content_widget = QWidget()
layout = QVBoxLayout(content_widget)
layout.setSpacing(15)
layout.setContentsMargins(15, 15, 15, 15)
# ===== 模式选择 =====
mode_group = QGroupBox("文件选择模式")
mode_group.setStyleSheet("""
QGroupBox {
background-color: #1a1a1a;
border: 2px solid #76b900;
border-radius: 2px;
color: #ffffff;
font-weight: 700;
font-size: 14px;
padding: 10px;
margin-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 5px 10px;
color: #000000;
background-color: #76b900;
border-radius: 2px;
font-weight: 700;
}
QRadioButton {
color: #ffffff;
font-size: 14px;
spacing: 8px;
}
QRadioButton::indicator {
width: 18px;
height: 18px;
border: 2px solid #76b900;
border-radius: 9px;
background-color: transparent;
}
QRadioButton::indicator:checked {
background-color: #76b900;
}
""")
mode_layout = QHBoxLayout()
self.mode_auto_radio = QRadioButton("自动匹配文件夹")
self.mode_auto_radio.setChecked(True)
self.mode_auto_radio.toggled.connect(self.on_mode_changed)
self.mode_manual_radio = QRadioButton("手动指定文件对")
self.mode_manual_radio.setChecked(False)
self.mode_manual_radio.toggled.connect(self.on_mode_changed)
mode_layout.addWidget(self.mode_auto_radio)
mode_layout.addWidget(self.mode_manual_radio)
mode_layout.addStretch()
mode_group.setLayout(mode_layout)
layout.addWidget(mode_group)
# 提示标签
hint_label = QLabel("提示:自动匹配会从文件夹批量导入文件对,手动模式可逐个指定。文件对列表中会标记 [自动] 或 [手动]。")
hint_label.setStyleSheet("color: #aaaaaa; font-size: 11px; margin-top: 5px;")
hint_label.setWordWrap(True)
layout.addWidget(hint_label)
# ===== 自动匹配区域 =====
self.auto_match_widget = QWidget()
auto_layout = QVBoxLayout(self.auto_match_widget)
auto_layout.setContentsMargins(0, 0, 0, 0)
auto_layout.setSpacing(10)
# 文件夹设置组 - 使用 NVIDIA 绿色主题
folder_group = QGroupBox("文件夹设置 (Folder Settings)")
folder_group.setStyleSheet("""
QGroupBox {
background-color: #1a1a1a;
border: 2px solid #76b900;
border-radius: 2px;
color: #ffffff;
font-weight: 700;
font-size: 14px;
padding: 10px;
margin-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 5px 10px;
color: #000000;
background-color: #76b900;
border-radius: 2px;
font-weight: 700;
}
QLabel {
color: #ffffff;
font-size: 14px;
font-weight: 700;
}
""")
folder_layout = QFormLayout()
folder_layout.setVerticalSpacing(10)
folder_layout.setHorizontalSpacing(10)
folder_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
folder_layout.setLabelAlignment(Qt.AlignLeft)
folder_layout.setFormAlignment(Qt.AlignLeft)
self.input_folder_edit = QLineEdit()
self.input_folder_edit.setPlaceholderText("选择反射率数据文件夹...")
self.anc_folder_edit = QLineEdit()
self.anc_folder_edit.setPlaceholderText("选择观测几何数据文件夹...")
btn_input = QPushButton("浏览...")
btn_anc = QPushButton("浏览...")
btn_input.clicked.connect(lambda: self.select_folder(self.input_folder_edit))
btn_anc.clicked.connect(lambda: self.select_folder(self.anc_folder_edit))
folder_layout.addRow("输入文件夹 (反射率):", self._horizontal_widget(self.input_folder_edit, btn_input))
folder_layout.addRow("辅助文件夹 (观测几何):", self._horizontal_widget(self.anc_folder_edit, btn_anc))
folder_group.setLayout(folder_layout)
auto_layout.addWidget(folder_group)
# 匹配按钮
match_btn = QPushButton("自动匹配文件对 (Auto Match File Pairs)")
match_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
color: #ffffff;
border: 2px solid #76b900;
border-radius: 2px;
padding: 10px 20px;
font-weight: 700;
font-size: 14px;
margin-top: 10px;
}
QPushButton:hover {
background-color: #1eaedb;
}
""")
match_btn.clicked.connect(self.match_files)
auto_layout.addWidget(match_btn)
layout.addWidget(self.auto_match_widget)
# ===== 手动指定区域 =====
self.manual_widget = QWidget()
manual_layout = QVBoxLayout(self.manual_widget)
manual_layout.setContentsMargins(0, 0, 0, 0)
manual_layout.setSpacing(10)
manual_group = QGroupBox("手动添加文件对")
manual_group.setStyleSheet("""
QGroupBox {
background-color: #1a1a1a;
border: 2px solid #0080ff;
border-radius: 2px;
color: #ffffff;
font-weight: 700;
font-size: 14px;
padding: 10px;
margin-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 5px 10px;
color: #ffffff;
background-color: #0080ff;
border-radius: 2px;
font-weight: 700;
}
QLabel {
color: #ffffff;
font-size: 14px;
font-weight: 700;
}
""")
manual_form = QFormLayout()
manual_form.setVerticalSpacing(10)
manual_form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
manual_form.setLabelAlignment(Qt.AlignLeft)
manual_form.setFormAlignment(Qt.AlignLeft)
self.manual_rfl_edit = QLineEdit()
self.manual_rfl_edit.setPlaceholderText("选择反射率文件...")
btn_rfl = QPushButton("浏览...")
btn_rfl.clicked.connect(self.select_rfl_file)
manual_form.addRow("反射率文件:", self._horizontal_widget(self.manual_rfl_edit, btn_rfl))
self.manual_obs_edit = QLineEdit()
self.manual_obs_edit.setPlaceholderText("选择观测几何文件...")
btn_obs = QPushButton("浏览...")
btn_obs.clicked.connect(self.select_obs_file)
manual_form.addRow("观测几何文件:", self._horizontal_widget(self.manual_obs_edit, btn_obs))
manual_group.setLayout(manual_form)
manual_layout.addWidget(manual_group)
# 添加文件对按钮
add_pair_btn = QPushButton("添加文件对 (Add File Pair)")
add_pair_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
color: #ffffff;
border: 2px solid #0080ff;
border-radius: 2px;
padding: 10px 20px;
font-weight: 700;
font-size: 14px;
margin-top: 5px;
}
QPushButton:hover {
background-color: #1eaedb;
}
""")
add_pair_btn.clicked.connect(self.add_manual_file_pair)
manual_layout.addWidget(add_pair_btn)
self.manual_widget.setVisible(False) # 默认隐藏
layout.addWidget(self.manual_widget)
# ===== 文件对列表 =====
list_group = QGroupBox("文件对列表 (点击编辑波段映射)")
list_group.setStyleSheet("""
QGroupBox {
background-color: #1a1a1a;
border: 1px solid #5e5e5e;
border-radius: 2px;
color: #ffffff;
font-weight: 700;
font-size: 14px;
padding: 10px;
margin-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px;
color: #ffffff;
}
""")
list_layout = QVBoxLayout(list_group)
# 文件列表(带滚动区域)
list_scroll = QScrollArea()
list_scroll.setWidgetResizable(True)
list_scroll.setMaximumHeight(200) # 限制最大高度,避免挤压
list_scroll.setStyleSheet("""
QScrollArea {
border: none;
background-color: #1a1a1a;
}
QScrollBar:vertical {
background-color: #1a1a1a;
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background-color: #5e5e5e;
border-radius: 6px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background-color: #76b900;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0px;
}
""")
list_container = QWidget()
list_container_layout = QVBoxLayout(list_container)
list_container_layout.setContentsMargins(0, 0, 0, 0)
list_container_layout.setSpacing(0)
self.file_list_widget = QListWidget()
self.file_list_widget.setSelectionMode(QListWidget.SingleSelection)
self.file_list_widget.itemClicked.connect(self.on_file_selected)
self.file_list_widget.setMaximumHeight(400) # 内部最大高度
self.file_list_widget.setStyleSheet("""
QListWidget {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #5e5e5e;
border-radius: 2px;
padding: 5px;
}
QListWidget::item {
padding: 8px;
border-bottom: 1px solid #5e5e5e;
}
QListWidget::item:selected {
background-color: #76b900;
color: #000000;
}
QListWidget::item:hover {
background-color: #3860be;
}
""")
list_container_layout.addWidget(self.file_list_widget)
list_scroll.setWidget(list_container)
list_layout.addWidget(list_scroll)
# 删除按钮
delete_btn = QPushButton("删除选中文件对 (Delete Selected)")
delete_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
color: #ff4444;
border: 2px solid #ff4444;
border-radius: 2px;
padding: 8px 16px;
font-weight: 700;
font-size: 13px;
}
QPushButton:hover {
background-color: #ff4444;
color: #000000;
}
""")
delete_btn.clicked.connect(self.delete_selected_file_pair)
list_layout.addWidget(delete_btn)
layout.addWidget(list_group)
# 波段映射组 - 使用蓝色主题
mapping_group = QGroupBox("当前选中文件的辅助文件波段索引 (0-based)")
mapping_group.setStyleSheet("""
QGroupBox {
background-color: #1a1a1a;
border: 2px solid #0046a4;
border-radius: 2px;
color: #ffffff;
font-weight: 700;
font-size: 14px;
padding: 10px;
margin-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 5px 10px;
color: #ffffff;
background-color: #0046a4;
border-radius: 2px;
font-weight: 700;
}
QLabel {
color: #ffffff;
font-size: 13px;
font-weight: 700;
}
QSpinBox {
background-color: #000000;
color: #ffffff;
border: 1px solid #0046a4;
border-radius: 2px;
padding: 3px;
min-width: 80px;
}
""")
mapping_layout = QGridLayout()
mapping_layout.setVerticalSpacing(10)
mapping_layout.setHorizontalSpacing(15)
self.param_spinboxes = {}
# 参数名称映射:内部名称 -> 显示名称
param_names_display = [
("path_length", "路径长度 (path_length)"),
("sensor_az", "传感器方位角 (sensor_az)"),
("sensor_zn", "传感器天顶角 (sensor_zn)"),
("solar_az", "太阳方位角 (solar_az)"),
("solar_zn", "太阳天顶角 (solar_zn)"),
("phase", "相位角 (phase)"),
("slope", "坡度 (slope)"),
("aspect", "坡向 (aspect)"),
("cosine_i", "入射角余弦 (cosine_i)"),
("utc_time", "UTC时间 (utc_time)")
]
# 使用两列网格布局
for i, (name, display_name) in enumerate(param_names_display):
row = i // 2
col = (i % 2) * 2 # 0, 2, 4... for labels
label = QLabel(f"{display_name}:")
label.setStyleSheet("color: #ffffff; font-size: 12px; min-width: 180px;")
mapping_layout.addWidget(label, row, col)
spin = NoWheelSpinBox()
spin.setRange(0, 1000)
spin.setValue(0)
spin.setFixedWidth(80)
spin.setStyleSheet("""
QSpinBox {
background-color: #000000;
color: #ffffff;
border: 1px solid #0046a4;
border-radius: 2px;
padding: 3px;
}
""")
self.param_spinboxes[name] = spin
mapping_layout.addWidget(spin, row, col + 1)
mapping_group.setLayout(mapping_layout)
layout.addWidget(mapping_group)
self.current_rfl_path = None
# 设置滚动区域的内容
scroll.setWidget(content_widget)
# 将滚动区域添加到文件标签页
tab_layout = QVBoxLayout(self.file_tab)
tab_layout.setContentsMargins(0, 0, 0, 0)
tab_layout.addWidget(scroll)
def _horizontal_widget(self, line_edit, button):
w = QWidget()
hbox = QHBoxLayout(w)
hbox.setContentsMargins(0, 0, 0, 0)
hbox.setSpacing(10)
hbox.addWidget(line_edit, 1)
hbox.addWidget(button)
return w
def select_folder(self, line_edit):
folder = QFileDialog.getExistingDirectory(self, "选择文件夹")
if folder:
line_edit.setText(folder)
def match_files(self):
input_dir = self.input_folder_edit.text()
anc_dir = self.anc_folder_edit.text()
if not input_dir or not anc_dir:
QMessageBox.warning(self, "警告", "请先选择输入文件夹和辅助文件夹")
return
input_files = []
for f in os.listdir(input_dir):
if f.endswith("_rfl") or (os.path.isdir(os.path.join(input_dir, f)) and f.endswith("_rfl")):
input_files.append(os.path.join(input_dir, f))
anc_files = {}
for f in os.listdir(anc_dir):
if f.endswith("_obs"):
anc_files[f[:-4]] = os.path.join(anc_dir, f)
self.file_pairs = []
self.file_list_widget.clear()
for rfl_path in input_files:
basename = os.path.basename(rfl_path)
key = basename[:-4] if basename.endswith("_rfl") else basename
if key in anc_files:
self.file_pairs.append((rfl_path, anc_files[key]))
self.file_list_widget.addItem(f"[自动] {basename} -> {os.path.basename(anc_files[key])}")
else:
self.file_list_widget.addItem(f"[自动] {basename} -> (未找到匹配的 obs 文件)")
if self.file_pairs:
self.file_list_widget.setCurrentRow(0)
self.on_file_selected(self.file_list_widget.item(0))
def on_mode_changed(self):
"""切换文件选择模式 - 互斥单选"""
auto_mode = self.mode_auto_radio.isChecked()
# 显示/隐藏对应区域
self.auto_match_widget.setVisible(auto_mode)
self.manual_widget.setVisible(not auto_mode)
def select_rfl_file(self):
"""手动选择反射率文件"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"选择反射率文件",
"",
"ENVI Files (*.dat *.img *.hdr);;All Files (*)"
)
if file_path:
self.manual_rfl_edit.setText(file_path)
def select_obs_file(self):
"""手动选择观测几何文件"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"选择观测几何文件",
"",
"ENVI Files (*.dat *.img *.hdr *.bip *.bsq *.bil);;All Files (*)"
)
if file_path:
self.manual_obs_edit.setText(file_path)
def add_manual_file_pair(self):
"""添加手动指定的文件对"""
rfl_path = self.manual_rfl_edit.text().strip()
obs_path = self.manual_obs_edit.text().strip()
# 验证输入
if not rfl_path:
QMessageBox.warning(self, "警告", "请选择反射率文件")
return
if not obs_path:
QMessageBox.warning(self, "警告", "请选择观测几何文件")
return
# 检查文件是否存在
if not os.path.exists(rfl_path):
QMessageBox.warning(self, "警告", f"反射率文件不存在:\n{rfl_path}")
return
if not os.path.exists(obs_path):
QMessageBox.warning(self, "警告", f"观测几何文件不存在:\n{obs_path}")
return
# 检查是否已存在相同的文件对
for existing_rfl, existing_obs in self.file_pairs:
if existing_rfl == rfl_path and existing_obs == obs_path:
QMessageBox.information(self, "提示", "该文件对已存在")
return
# 添加文件对
self.file_pairs.append((rfl_path, obs_path))
rfl_basename = os.path.basename(rfl_path)
obs_basename = os.path.basename(obs_path)
self.file_list_widget.addItem(f"[手动] {rfl_basename} -> {obs_basename}")
# 为新文件初始化默认波段映射
if rfl_path not in self.band_mappings:
self.band_mappings[rfl_path] = {
"path_length": 0, "sensor_az": 9, "sensor_zn": 8,
"solar_az": 7, "solar_zn": 6, "phase": 0,
"slope": 0, "aspect": 0, "cosine_i": 0, "utc_time": 0
}
# 选中新添加的文件对
last_idx = len(self.file_pairs) - 1
self.file_list_widget.setCurrentRow(last_idx)
self.on_file_selected(self.file_list_widget.item(last_idx))
# 清空输入框
self.manual_rfl_edit.clear()
self.manual_obs_edit.clear()
QMessageBox.information(self, "成功", "文件对已添加")
def delete_selected_file_pair(self):
"""删除选中的文件对"""
idx = self.file_list_widget.currentRow()
if idx < 0 or idx >= len(self.file_pairs):
QMessageBox.warning(self, "警告", "请先选择一个文件对")
return
rfl_path, obs_path = self.file_pairs[idx]
rfl_name = os.path.basename(rfl_path)
# 确认删除
reply = QMessageBox.question(
self, "确认删除",
f"确定要删除文件对吗?\n\n{rfl_name}",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
# 从列表和数据结构中删除
self.file_pairs.pop(idx)
self.file_list_widget.takeItem(idx)
# 清除对应的波段映射
if rfl_path in self.band_mappings:
del self.band_mappings[rfl_path]
# 清除当前选中
self.current_rfl_path = None
for spin in self.param_spinboxes.values():
spin.setValue(0)
# 如果有剩余文件对,选中第一个
if self.file_pairs:
self.file_list_widget.setCurrentRow(0)
self.on_file_selected(self.file_list_widget.item(0))
QMessageBox.information(self, "成功", "文件对已删除")
def on_file_selected(self, item):
idx = self.file_list_widget.row(item)
if idx >= len(self.file_pairs):
return
rfl_path, obs_path = self.file_pairs[idx]
self.current_rfl_path = rfl_path
# 更新波段映射spinbox值
if rfl_path in self.band_mappings:
mapping = self.band_mappings[rfl_path]
for param, spin in self.param_spinboxes.items():
value = mapping.get(param, 0)
spin.setValue(value)
else:
for spin in self.param_spinboxes.values():
spin.setValue(0)
# 强制刷新UI
QApplication.processEvents()
# 重新连接信号
for param, spin in self.param_spinboxes.items():
try:
spin.valueChanged.disconnect()
except:
pass
spin.valueChanged.connect(lambda val, p=param: self.update_band_mapping(p, val))
def update_band_mapping(self, param, value):
if self.current_rfl_path is None:
return
if self.current_rfl_path not in self.band_mappings:
self.band_mappings[self.current_rfl_path] = {}
self.band_mappings[self.current_rfl_path][param] = value
# ==================== 校正参数页 ====================
def setup_corr_tab(self):
scroll = QScrollArea()
scroll.setWidgetResizable(True)
container = QWidget()
scroll.setWidget(container)
layout = QVBoxLayout(container)
layout.setSpacing(20)
# NVIDIA 设计风格样式表
# Topo 组 - 蓝色系 (#0046a4)
# BRDF 组 - NVIDIA 绿色 (#76b900)
# Glint 组 - 橙色系 (#df6500)
# 1. Corrections 选择
corr_group = QGroupBox("需要执行的校正")
corr_group.setStyleSheet("""
QGroupBox {
background-color: #1a1a1a;
border: 2px solid #76b900;
border-radius: 2px;
color: #ffffff;
font-weight: 700;
font-size: 14px;
padding: 10px;
margin-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px;
color: #76b900;
}
QCheckBox {
color: #ffffff;
font-size: 14px;
font-weight: 700;
spacing: 8px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border: 2px solid #76b900;
border-radius: 2px;
background-color: transparent;
}
QCheckBox::indicator:checked {
background-color: #76b900;
}
""")
corr_layout = QHBoxLayout()
self.corr_topo_cb = QCheckBox("Topo (地形校正)")
self.corr_topo_cb.stateChanged.connect(self.on_correction_changed)
self.corr_brdf_cb = QCheckBox("BRDF (双向反射分布函数)")
self.corr_brdf_cb.stateChanged.connect(self.on_correction_changed)
self.corr_glint_cb = QCheckBox("Glint (耀斑校正)")
self.corr_glint_cb.stateChanged.connect(self.on_correction_changed)
corr_layout.addWidget(self.corr_topo_cb)
corr_layout.addWidget(self.corr_brdf_cb)
corr_layout.addWidget(self.corr_glint_cb)
corr_group.setLayout(corr_layout)
layout.addWidget(corr_group)
# 2. Topo 参数 - 蓝色主题 (#0046a4)
self.topo_group = QGroupBox("地形校正 (Topo Correction)")
self.topo_group.setVisible(False) # 默认隐藏
self.topo_group.setStyleSheet("""
QGroupBox {
background-color: #1a1a1a;
border: 2px solid #0046a4;
border-radius: 2px;
color: #ffffff;
font-weight: 700;
font-size: 14px;
padding: 10px;
margin-top: 15px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 5px 10px;
color: #ffffff;
background-color: #0046a4;
border-radius: 2px;
font-weight: 700;
font-size: 14px;
}
QLabel {
color: #ffffff;
font-size: 14px;
font-weight: 700;
}
QComboBox {
background-color: #000000;
color: #ffffff;
border: 1px solid #0046a4;
border-radius: 2px;
padding: 5px;
min-width: 150px;
}
QComboBox::drop-down {
border: none;
width: 25px;
}
QComboBox::down-arrow {
image: none;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #0046a4;
margin-right: 8px;
}
QComboBox QAbstractItemView {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #0046a4;
selection-background-color: #0046a4;
}
""")
topo_layout = QVBoxLayout()
topo_layout.setSpacing(10)
# type
type_layout = QHBoxLayout()
type_layout.setSpacing(10)
type_label = QLabel("类型 (type):")
type_layout.addWidget(type_label)
self.topo_type = NoWheelComboBox()
self.topo_type.addItems(["mod_minneart", "scs+c", "cosine", "c", "scs"])
self.topo_type.setMinimumWidth(250)
type_layout.addWidget(self.topo_type)
type_layout.addStretch()
topo_layout.addLayout(type_layout)
# c_fit_type
fit_layout = QHBoxLayout()
fit_layout.setSpacing(10)
fit_label = QLabel("拟合类型 (c_fit_type):")
fit_layout.addWidget(fit_label)
self.c_fit_type = NoWheelComboBox()
self.c_fit_type.addItems(["nnls", "ols", "wls"])
self.c_fit_type.setMinimumWidth(250)
fit_layout.addWidget(self.c_fit_type)
fit_layout.addStretch()
topo_layout.addLayout(fit_layout)
# 掩膜列表
calc_mask_label = QLabel("计算掩膜条件 (calc_mask - 取交集):")
calc_mask_label.setStyleSheet("color: #a7a7a7; font-size: 13px;")
topo_layout.addWidget(calc_mask_label)
self.topo_calc_mask_list = MaskListWidget()
topo_layout.addWidget(self.topo_calc_mask_list)
apply_mask_label = QLabel("应用掩膜条件 (apply_mask - 取交集):")
apply_mask_label.setStyleSheet("color: #a7a7a7; font-size: 13px;")
topo_layout.addWidget(apply_mask_label)
self.topo_apply_mask_list = MaskListWidget()
topo_layout.addWidget(self.topo_apply_mask_list)
self.topo_group.setLayout(topo_layout)
layout.addWidget(self.topo_group)
# 3. BRDF 参数 - NVIDIA 绿色主题 (#76b900)
self.brdf_group = QGroupBox("BRDF 校正 (BRDF Correction)")
self.brdf_group.setVisible(False) # 默认隐藏
self.brdf_group.setStyleSheet("""
QGroupBox {
background-color: #1a1a1a;
border: 2px solid #76b900;
border-radius: 2px;
color: #ffffff;
font-weight: 700;
font-size: 14px;
padding: 10px;
margin-top: 15px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 5px 10px;
color: #000000;
background-color: #76b900;
border-radius: 2px;
font-weight: 700;
font-size: 14px;
}
QLabel {
color: #ffffff;
font-size: 14px;
font-weight: 700;
}
QComboBox {
background-color: #000000;
color: #ffffff;
border: 1px solid #76b900;
border-radius: 2px;
padding: 5px;
min-width: 150px;
}
QComboBox::drop-down {
border: none;
width: 25px;
}
QComboBox::down-arrow {
image: none;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #76b900;
margin-right: 8px;
}
QComboBox QAbstractItemView {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #76b900;
selection-background-color: #76b900;
selection-color: #000000;
}
QCheckBox {
color: #ffffff;
font-size: 14px;
font-weight: 700;
spacing: 8px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border: 2px solid #76b900;
border-radius: 2px;
background-color: transparent;
}
QCheckBox::indicator:checked {
background-color: #76b900;
}
QDoubleSpinBox, QSpinBox {
background-color: #000000;
color: #ffffff;
border: 1px solid #76b900;
border-radius: 2px;
padding: 5px;
}
""")
brdf_layout = QFormLayout()
brdf_layout.setVerticalSpacing(10)
brdf_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
brdf_layout.setLabelAlignment(Qt.AlignLeft)
brdf_layout.setFormAlignment(Qt.AlignLeft)
self.brdf_type = NoWheelComboBox()
self.brdf_type.addItems(["flex", "universal"])
self.brdf_type.setMinimumWidth(300)
brdf_layout.addRow("算法类型 (type):", self.brdf_type)
self.brdf_grouped = QCheckBox("启用分组 (grouped)")
self.brdf_grouped.setChecked(True)
brdf_layout.addRow("", self.brdf_grouped)
self.geometric_kernel = NoWheelComboBox()
self.geometric_kernel.addItems(["li_sparse", "li_dense", "li_sparse_r", "li_dense_r", "roujean"])
self.geometric_kernel.setMinimumWidth(300)
brdf_layout.addRow("几何核 (geometric):", self.geometric_kernel)
self.volume_kernel = NoWheelComboBox()
self.volume_kernel.addItems(["ross_thin", "ross_thick", "hotspot", "roujean"])
self.volume_kernel.setMinimumWidth(300)
brdf_layout.addRow("体积核 (volume):", self.volume_kernel)
self.br_b_ratio = NoWheelDoubleSpinBox()
self.br_b_ratio.setRange(0.1, 10.0)
self.br_b_ratio.setValue(2.5)
self.br_b_ratio.setDecimals(2)
self.br_b_ratio.setMinimumWidth(300)
brdf_layout.addRow("b/r 比率:", self.br_b_ratio)
self.hb_ratio = NoWheelDoubleSpinBox()
self.hb_ratio.setRange(0.1, 5.0)
self.hb_ratio.setValue(2.0)
self.hb_ratio.setDecimals(2)
self.hb_ratio.setMinimumWidth(300)
brdf_layout.addRow("h/b 比率:", self.hb_ratio)
self.sample_perc = NoWheelDoubleSpinBox()
self.sample_perc.setRange(0.01, 1.0)
self.sample_perc.setValue(0.1)
self.sample_perc.setDecimals(2)
self.sample_perc.setMinimumWidth(300)
brdf_layout.addRow("采样比例 (sample_perc):", self.sample_perc)
self.interp_kind = NoWheelComboBox()
self.interp_kind.addItems(["linear", "nearest", "cubic"])
self.interp_kind.setMinimumWidth(300)
brdf_layout.addRow("插值方法 (interp_kind):", self.interp_kind)
# BRDF 掩膜
calc_mask_label_brdf = QLabel("计算掩膜条件 (calc_mask):")
calc_mask_label_brdf.setStyleSheet("color: #a7a7a7; font-size: 13px;")
brdf_layout.addRow(calc_mask_label_brdf)
self.brdf_calc_mask_list = MaskListWidget()
brdf_layout.addRow(self.brdf_calc_mask_list)
apply_mask_label_brdf = QLabel("应用掩膜条件 (apply_mask):")
apply_mask_label_brdf.setStyleSheet("color: #a7a7a7; font-size: 13px;")
brdf_layout.addRow(apply_mask_label_brdf)
self.brdf_apply_mask_list = MaskListWidget()
brdf_layout.addRow(self.brdf_apply_mask_list)
# bin_type 等
self.bin_type = NoWheelComboBox()
self.bin_type.addItems(["dynamic", "fixed"])
self.bin_type.setMinimumWidth(300)
brdf_layout.addRow("分箱类型 (bin_type):", self.bin_type)
self.num_bins = NoWheelSpinBox()
self.num_bins.setRange(1, 100)
self.num_bins.setValue(18)
self.num_bins.setMinimumWidth(300)
brdf_layout.addRow("分箱数量 (num_bins):", self.num_bins)
self.ndvi_bin_min = NoWheelDoubleSpinBox()
self.ndvi_bin_min.setRange(0.0, 1.0)
self.ndvi_bin_min.setValue(0.05)
self.ndvi_bin_min.setDecimals(2)
self.ndvi_bin_min.setMinimumWidth(300)
brdf_layout.addRow("NDVI 最小值 (ndvi_bin_min):", self.ndvi_bin_min)
self.ndvi_bin_max = NoWheelDoubleSpinBox()
self.ndvi_bin_max.setRange(0.0, 1.0)
self.ndvi_bin_max.setValue(1.0)
self.ndvi_bin_max.setDecimals(2)
self.ndvi_bin_max.setMinimumWidth(300)
brdf_layout.addRow("NDVI 最大值 (ndvi_bin_max):", self.ndvi_bin_max)
self.ndvi_perc_min = NoWheelSpinBox()
self.ndvi_perc_min.setRange(0, 100)
self.ndvi_perc_min.setValue(10)
self.ndvi_perc_min.setMinimumWidth(300)
brdf_layout.addRow("NDVI 百分比最小值 (ndvi_perc_min):", self.ndvi_perc_min)
self.ndvi_perc_max = NoWheelSpinBox()
self.ndvi_perc_max.setRange(0, 100)
self.ndvi_perc_max.setValue(95)
self.ndvi_perc_max.setMinimumWidth(300)
brdf_layout.addRow("NDVI 百分比最大值 (ndvi_perc_max):", self.ndvi_perc_max)
self.solar_zn_type = NoWheelComboBox()
self.solar_zn_type.addItems(["scene", "pixel"])
self.solar_zn_type.setMinimumWidth(300)
brdf_layout.addRow("太阳天顶角类型 (solar_zn_type):", self.solar_zn_type)
self.brdf_group.setLayout(brdf_layout)
layout.addWidget(self.brdf_group)
# 4. Glint 参数 - 橙色主题 (#df6500)
self.glint_group = QGroupBox("耀斑校正 (Glint Correction)")
self.glint_group.setVisible(False) # 默认隐藏
self.glint_group.setStyleSheet("""
QGroupBox {
background-color: #1a1a1a;
border: 2px solid #df6500;
border-radius: 2px;
color: #ffffff;
font-weight: 700;
font-size: 14px;
padding: 10px;
margin-top: 15px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 5px 10px;
color: #ffffff;
background-color: #df6500;
border-radius: 2px;
font-weight: 700;
font-size: 14px;
}
QLabel {
color: #ffffff;
font-size: 14px;
font-weight: 700;
}
QComboBox {
background-color: #000000;
color: #ffffff;
border: 1px solid #df6500;
border-radius: 2px;
padding: 5px;
min-width: 150px;
}
QComboBox::drop-down {
border: none;
width: 25px;
}
QComboBox::down-arrow {
image: none;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #df6500;
margin-right: 8px;
}
QComboBox QAbstractItemView {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #df6500;
selection-background-color: #df6500;
}
QSpinBox {
background-color: #000000;
color: #ffffff;
border: 1px solid #df6500;
border-radius: 2px;
padding: 5px;
}
""")
glint_layout = QFormLayout()
glint_layout.setVerticalSpacing(10)
glint_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
glint_layout.setLabelAlignment(Qt.AlignLeft)
glint_layout.setFormAlignment(Qt.AlignLeft)
self.glint_type = NoWheelComboBox()
self.glint_type.addItems(["hochberg", "gao", "hedley"])
self.glint_type.setMinimumWidth(300)
glint_layout.addRow("校正算法 (type):", self.glint_type)
self.correction_wave = NoWheelSpinBox()
self.correction_wave.setRange(1000, 2500)
self.correction_wave.setValue(2150)
self.correction_wave.setMinimumWidth(300)
glint_layout.addRow("校正波长 (nm):", self.correction_wave)
apply_mask_label_glint = QLabel("应用掩膜条件 (apply_mask):")
apply_mask_label_glint.setStyleSheet("color: #a7a7a7; font-size: 13px;")
glint_layout.addRow(apply_mask_label_glint)
self.glint_apply_mask_list = MaskListWidget()
glint_layout.addRow(self.glint_apply_mask_list)
self.glint_group.setLayout(glint_layout)
layout.addWidget(self.glint_group)
layout.addStretch()
self.corr_tab.setLayout(QVBoxLayout())
self.corr_tab.layout().addWidget(scroll)
# 设置容器背景色
container.setStyleSheet("background-color: #000000;")
# 初始化校正参数组显示状态
self.on_correction_changed()
def on_correction_changed(self):
"""根据选择的校正类型显示/隐藏对应的参数组"""
# 显示/隐藏对应的参数组
if hasattr(self, 'topo_group'):
self.topo_group.setVisible(self.corr_topo_cb.isChecked())
if hasattr(self, 'brdf_group'):
self.brdf_group.setVisible(self.corr_brdf_cb.isChecked())
if hasattr(self, 'glint_group'):
self.glint_group.setVisible(self.corr_glint_cb.isChecked())
# ==================== 输出设置页 ====================
def setup_output_tab(self):
layout = QFormLayout(self.output_tab)
layout.setVerticalSpacing(12)
layout.setHorizontalSpacing(10)
layout.setContentsMargins(15, 15, 15, 15)
layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
layout.setLabelAlignment(Qt.AlignLeft)
layout.setFormAlignment(Qt.AlignLeft)
# 设置输出设置页的样式
self.output_tab.setStyleSheet("""
QLabel {
color: #ffffff;
font-size: 14px;
font-weight: 700;
}
QLineEdit {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #76b900;
border-radius: 2px;
padding: 5px;
min-width: 300px;
}
QLineEdit:focus {
border: 2px solid #76b900;
}
QSpinBox {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #76b900;
border-radius: 2px;
padding: 5px;
min-width: 300px;
}
QSpinBox:focus {
border: 2px solid #76b900;
}
QCheckBox {
color: #ffffff;
font-size: 14px;
font-weight: 700;
spacing: 8px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border: 2px solid #76b900;
border-radius: 2px;
background-color: transparent;
}
QCheckBox::indicator:checked {
background-color: #76b900;
}
""")
# 输出目录
self.output_dir_edit = QLineEdit()
self.output_dir_edit.setPlaceholderText("选择输出目录...")
btn_out = QPushButton("浏览...")
btn_out.clicked.connect(lambda: self.select_folder(self.output_dir_edit))
layout.addRow("输出目录 (Output Directory):", self._horizontal_widget(self.output_dir_edit, btn_out))
# 后缀
self.suffix_edit = QLineEdit("topo_brdf_glint")
self.suffix_edit.setPlaceholderText("输出文件名后缀")
layout.addRow("后缀 (Suffix):", self.suffix_edit)
# 导出选项
export_label = QLabel("导出选项 (Export Options):")
export_label.setStyleSheet("color: #76b900; font-size: 14px; font-weight: 700;")
layout.addRow(export_label)
self.coeffs_cb = QCheckBox("输出系数 (coeffs)")
self.image_cb = QCheckBox("输出图像 (image)")
self.masks_cb = QCheckBox("输出掩膜 (masks)")
self.coeffs_cb.setChecked(False)
self.image_cb.setChecked(True)
self.masks_cb.setChecked(True)
layout.addRow("", self.coeffs_cb)
layout.addRow("", self.image_cb)
layout.addRow("", self.masks_cb)
# 子集波段
self.subset_waves_edit = QLineEdit("")
self.subset_waves_edit.setPlaceholderText("例如: 450, 550, 650 (逗号分隔,留空导出全部)")
layout.addRow("子集波段 (Subset Waves):", self.subset_waves_edit)
# CPU核心数
self.num_cpus_spin = NoWheelSpinBox()
self.num_cpus_spin.setRange(1, 64)
self.num_cpus_spin.setValue(2)
self.num_cpus_spin.setMinimumWidth(300)
layout.addRow("CPU核心数 (num_cpus):", self.num_cpus_spin)
# 重采样
self.resample_cb = QCheckBox("启用重采样 (Resample)")
layout.addRow("", self.resample_cb)
# ==================== 配置加载与保存 ====================
def load_config(self):
file_path, _ = QFileDialog.getOpenFileName(self, "加载 JSON 配置", "", "JSON Files (*.json)")
if not file_path:
return
try:
with open(file_path, 'r') as f:
config = json.load(f)
except Exception as e:
QMessageBox.critical(self, "错误", f"无法加载 JSON: {e}")
return
# 文件与波段映射 - 从 anc_files 恢复文件对和波段映射
if "anc_files" in config:
self.band_mappings = {}
self.file_pairs = []
self.file_list_widget.clear()
for rfl_path, anc_info in config["anc_files"].items():
# 恢复波段映射
mapping = {}
obs_path = None
for param, val in anc_info.items():
if isinstance(val, list) and len(val) >= 2:
mapping[param] = val[1]
# 提取第一个参数的obs路径作为该文件对的obs路径
if obs_path is None:
obs_path = val[0]
self.band_mappings[rfl_path] = mapping
# 恢复文件对(不检查文件是否存在,支持加载其他机器创建的配置)
if obs_path:
self.file_pairs.append((rfl_path, obs_path))
rfl_basename = os.path.basename(rfl_path)
obs_basename = os.path.basename(obs_path)
# 标记文件是否存在
exists_marker = "" if os.path.exists(rfl_path) else " [文件不存在]"
self.file_list_widget.addItem(f"{rfl_basename} -> {obs_basename}{exists_marker}")
# 自动选择第一个文件并刷新显示
if self.file_pairs:
self.file_list_widget.setCurrentRow(0)
# 先设置文件夹路径在on_file_selected之前
first_rfl_path = self.file_pairs[0][0]
first_obs_path = self.file_pairs[0][1]
input_dir = os.path.dirname(first_rfl_path)
anc_dir = os.path.dirname(first_obs_path)
self.input_folder_edit.setText(input_dir)
self.anc_folder_edit.setText(anc_dir)
# 触发UI更新
QApplication.processEvents()
# 然后选择文件并刷新波段映射
self.on_file_selected(self.file_list_widget.item(0))
# corrections
corrections = config.get("corrections", [])
self.corr_topo_cb.setChecked("topo" in corrections)
self.corr_brdf_cb.setChecked("brdf" in corrections)
self.corr_glint_cb.setChecked("glint" in corrections)
# 更新参数组显示状态
self.on_correction_changed()
# topo
topo = config.get("topo", {})
self.topo_type.setCurrentText(topo.get("type", "scs+c"))
self.c_fit_type.setCurrentText(topo.get("c_fit_type", "nnls"))
self.topo_calc_mask_list.set_conditions(topo.get("calc_mask", []))
self.topo_apply_mask_list.set_conditions(topo.get("apply_mask", []))
# brdf
brdf = config.get("brdf", {})
self.brdf_type.setCurrentText(brdf.get("type", "flex"))
self.brdf_grouped.setChecked(brdf.get("grouped", True))
self.geometric_kernel.setCurrentText(brdf.get("geometric", "li_dense_r"))
self.volume_kernel.setCurrentText(brdf.get("volume", "ross_thick"))
self.br_b_ratio.setValue(brdf.get("b/r", 2.5))
self.hb_ratio.setValue(brdf.get("h/b", 2.0))
self.sample_perc.setValue(brdf.get("sample_perc", 0.1))
self.interp_kind.setCurrentText(brdf.get("interp_kind", "linear"))
self.brdf_calc_mask_list.set_conditions(brdf.get("calc_mask", []))
self.brdf_apply_mask_list.set_conditions(brdf.get("apply_mask", []))
self.bin_type.setCurrentText(brdf.get("bin_type", "dynamic"))
self.num_bins.setValue(brdf.get("num_bins", 18))
self.ndvi_bin_min.setValue(brdf.get("ndvi_bin_min", 0.05))
self.ndvi_bin_max.setValue(brdf.get("ndvi_bin_max", 1.0))
self.ndvi_perc_min.setValue(brdf.get("ndvi_perc_min", 10))
self.ndvi_perc_max.setValue(brdf.get("ndvi_perc_max", 95))
self.solar_zn_type.setCurrentText(brdf.get("solar_zn_type", "scene"))
# glint
glint = config.get("glint", {})
self.glint_type.setCurrentText(glint.get("type", "hochberg"))
self.correction_wave.setValue(glint.get("correction_wave", 2150))
self.glint_apply_mask_list.set_conditions(glint.get("apply_mask", []))
# export
export = config.get("export", {})
self.output_dir_edit.setText(export.get("output_dir", ""))
self.suffix_edit.setText(export.get("suffix", "topo_brdf_glint"))
self.coeffs_cb.setChecked(export.get("coeffs", False))
self.image_cb.setChecked(export.get("image", True))
self.masks_cb.setChecked(export.get("masks", True))
subset = export.get("subset_waves", [])
self.subset_waves_edit.setText(",".join(str(w) for w in subset))
self.num_cpus_spin.setValue(config.get("num_cpus", 2))
self.resample_cb.setChecked(config.get("resample", False))
QMessageBox.information(self, "成功", f"已加载配置: {file_path}")
def save_config(self):
if not self.file_pairs:
QMessageBox.warning(self, "警告", "没有有效的文件对,请先匹配文件。")
return
config = {}
config["input_files"] = [rfl for rfl, _ in self.file_pairs]
config["file_type"] = "envi"
config["bad_bands"] = []
config["num_cpus"] = self.num_cpus_spin.value()
anc_files_dict = {}
for rfl_path, obs_path in self.file_pairs:
mapping = self.band_mappings.get(rfl_path, {})
anc_entry = {}
for param, spin in self.param_spinboxes.items():
band_idx = mapping.get(param, spin.value())
anc_entry[param] = [obs_path, band_idx]
anc_files_dict[rfl_path] = anc_entry
config["anc_files"] = anc_files_dict
corrections = []
if self.corr_topo_cb.isChecked():
corrections.append("topo")
if self.corr_brdf_cb.isChecked():
corrections.append("brdf")
if self.corr_glint_cb.isChecked():
corrections.append("glint")
config["corrections"] = corrections
config["topo"] = {
"type": self.topo_type.currentText(),
"calc_mask": self.topo_calc_mask_list.get_conditions(),
"apply_mask": self.topo_apply_mask_list.get_conditions(),
"c_fit_type": self.c_fit_type.currentText()
}
config["brdf"] = {
"type": self.brdf_type.currentText(),
"grouped": self.brdf_grouped.isChecked(),
"geometric": self.geometric_kernel.currentText(),
"volume": self.volume_kernel.currentText(),
"b/r": self.br_b_ratio.value(),
"h/b": self.hb_ratio.value(),
"sample_perc": self.sample_perc.value(),
"interp_kind": self.interp_kind.currentText(),
"calc_mask": self.brdf_calc_mask_list.get_conditions(),
"apply_mask": self.brdf_apply_mask_list.get_conditions(),
"bin_type": self.bin_type.currentText(),
"num_bins": self.num_bins.value(),
"ndvi_bin_min": self.ndvi_bin_min.value(),
"ndvi_bin_max": self.ndvi_bin_max.value(),
"ndvi_perc_min": self.ndvi_perc_min.value(),
"ndvi_perc_max": self.ndvi_perc_max.value(),
"solar_zn_type": self.solar_zn_type.currentText()
}
config["glint"] = {
"type": self.glint_type.currentText(),
"correction_wave": self.correction_wave.value(),
"apply_mask": self.glint_apply_mask_list.get_conditions()
}
subset_waves = []
if self.subset_waves_edit.text().strip():
try:
subset_waves = [int(x.strip()) for x in self.subset_waves_edit.text().split(",") if x.strip()]
except:
pass
config["export"] = {
"coeffs": self.coeffs_cb.isChecked(),
"image": self.image_cb.isChecked(),
"masks": self.masks_cb.isChecked(),
"subset_waves": subset_waves,
"output_dir": self.output_dir_edit.text(),
"suffix": self.suffix_edit.text()
}
config["resample"] = self.resample_cb.isChecked()
save_path, _ = QFileDialog.getSaveFileName(self, "保存 JSON 配置", "", "JSON Files (*.json)")
if not save_path:
return
try:
with open(save_path, 'w') as f:
json.dump(config, f, indent=3)
QMessageBox.information(self, "成功", f"配置已保存至: {save_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存失败: {e}")
# ==================== 水体BRDF模块窗口 ====================
class WaterBRDFWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("水体 BRDF 校正 - Ocean BRDF")
self.setMinimumSize(1000, 700)
# 命令执行相关
self.command_thread = None
# 设置样式和界面
self.setup_water_styles()
self.setup_water_interface()
def setup_water_styles(self):
"""设置水体BRDF模块的样式"""
self.setStyleSheet("""
QMainWindow {
background-color: #000000;
}
QWidget {
background-color: #000000;
color: #ffffff;
font-family: Arial, Helvetica, sans-serif;
font-size: 14px;
}
QGroupBox {
background-color: #1a1a1a;
border: 2px solid #0080ff;
border-radius: 8px;
margin-top: 15px;
padding-top: 15px;
font-weight: bold;
font-size: 16px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 15px;
padding: 5px 10px;
color: #ffffff;
background-color: #0080ff;
border-radius: 4px;
font-weight: bold;
}
QPushButton {
background-color: transparent;
color: #ffffff;
border: 2px solid #0080ff;
border-radius: 6px;
padding: 10px 20px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover {
background-color: #0080ff;
color: #000000;
}
QPushButton:pressed {
background-color: #0066cc;
border-color: #0066cc;
}
QLineEdit {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #0080ff;
border-radius: 4px;
padding: 8px;
font-size: 14px;
}
QLineEdit:focus {
border: 2px solid #0080ff;
}
QComboBox {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #0080ff;
border-radius: 4px;
padding: 8px;
font-size: 14px;
}
QComboBox::drop-down {
border: none;
width: 25px;
}
QComboBox::down-arrow {
image: none;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #0080ff;
margin-right: 8px;
}
QComboBox QAbstractItemView {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #0080ff;
selection-background-color: #0080ff;
}
QCheckBox {
color: #ffffff;
font-size: 14px;
spacing: 8px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border: 2px solid #0080ff;
border-radius: 2px;
background-color: transparent;
}
QCheckBox::indicator:checked {
background-color: #0080ff;
}
QSpinBox, QDoubleSpinBox {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #0080ff;
border-radius: 4px;
padding: 5px;
}
QTextEdit {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #0080ff;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
}
QProgressBar {
background-color: #1a1a1a;
border: 1px solid #0080ff;
border-radius: 4px;
text-align: center;
color: #ffffff;
}
QProgressBar::chunk {
background-color: #0080ff;
border-radius: 4px;
}
""")
def setup_water_interface(self):
"""创建水体BRDF模块界面"""
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(30, 30, 30, 30)
main_layout.setSpacing(20)
# 标题
title_label = QLabel("水体 BRDF 校正")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("""
font-size: 28px;
font-weight: bold;
color: #0080ff;
margin-bottom: 10px;
""")
main_layout.addWidget(title_label)
subtitle_label = QLabel("Ocean BRDF Correction - 支持 L11, M02, M02SeaDAS, O25 模型")
subtitle_label.setAlignment(Qt.AlignCenter)
subtitle_label.setStyleSheet("""
font-size: 14px;
color: #cccccc;
margin-bottom: 20px;
""")
main_layout.addWidget(subtitle_label)
# 创建分割器:左侧配置,右侧日志
splitter = QSplitter(Qt.Horizontal)
# 左侧:参数配置
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
# 输入文件组
input_group = QGroupBox("输入文件")
input_layout = QFormLayout(input_group)
input_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
input_layout.setLabelAlignment(Qt.AlignLeft)
input_layout.setFormAlignment(Qt.AlignLeft)
self.hyperspectral_edit = QLineEdit()
self.hyperspectral_edit.setPlaceholderText("选择高光谱数据文件 (.img, .dat, .hdr)")
hyperspectral_btn = QPushButton("浏览...")
hyperspectral_btn.clicked.connect(lambda: self.browse_file(self.hyperspectral_edit, "高光谱数据文件", "ENVI Files (*.img *.dat *.hdr);;All Files (*)"))
input_layout.addRow("高光谱文件:", self.create_file_row(self.hyperspectral_edit, hyperspectral_btn))
self.angle_edit = QLineEdit()
self.angle_edit.setPlaceholderText("选择角度文件 (.dat, .bsq, .bip, .bil, .hdr)")
angle_btn = QPushButton("浏览...")
angle_btn.clicked.connect(lambda: self.browse_file(self.angle_edit, "角度文件", "ENVI Files (*.dat *.bsq *.bip *.bil *.hdr);;All Files (*)"))
input_layout.addRow("角度文件:", self.create_file_row(self.angle_edit, angle_btn))
self.mask_edit = QLineEdit()
self.mask_edit.setPlaceholderText("选择水体掩膜文件 (.tif, .tiff)")
mask_btn = QPushButton("浏览...")
mask_btn.clicked.connect(lambda: self.browse_file(self.mask_edit, "水体掩膜文件", "GeoTIFF Files (*.tif *.tiff);;All Files (*)"))
input_layout.addRow("水体掩膜:", self.create_file_row(self.mask_edit, mask_btn))
left_layout.addWidget(input_group)
# BRDF模型参数组
model_group = QGroupBox("BRDF 模型参数")
model_layout = QFormLayout(model_group)
model_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
model_layout.setLabelAlignment(Qt.AlignLeft)
model_layout.setFormAlignment(Qt.AlignLeft)
self.brdf_model_combo = NoWheelComboBox()
self.brdf_model_combo.addItems(["L11", "M02", "O25"])
self.brdf_model_combo.setCurrentText("L11")
model_layout.addRow("BRDF 模型:", self.brdf_model_combo)
# 输出变量多选列表
output_vars_label = QLabel("输出变量:")
output_vars_label.setStyleSheet("color: #ffffff; font-weight: bold;")
model_layout.addRow(output_vars_label)
self.output_vars_list = QListWidget()
self.output_vars_list.setSelectionMode(QListWidget.MultiSelection)
self.output_vars_list.addItems([
"Rw_brdf - BRDF校正后的水体反射率",
"rho_ex_w - 水体反射率 (nrrs * π)",
"nrrs - 归一化遥感反射率",
"C_brdf - BRDF校正因子",
"brdf_unc - BRDF不确定度",
"nrrs_unc - nrrs不确定度"
])
# 默认选中 Rw_brdf, nrrs, C_brdf
self.output_vars_list.item(0).setSelected(True)
self.output_vars_list.item(2).setSelected(True)
self.output_vars_list.item(3).setSelected(True)
self.output_vars_list.setMaximumHeight(120)
self.output_vars_list.setStyleSheet("""
QListWidget {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #0080ff;
border-radius: 4px;
padding: 5px;
}
QListWidget::item {
padding: 5px;
border-radius: 2px;
}
QListWidget::item:selected {
background-color: #0080ff;
color: #ffffff;
}
QListWidget::item:hover {
background-color: #0046a4;
}
""")
model_layout.addRow(self.output_vars_list)
# 添加提示标签
hint_label = QLabel("提示:按住 Ctrl 键可多选brdf_unc 和 nrrs_unc 需要勾选计算不确定性")
hint_label.setStyleSheet("color: #aaaaaa; font-size: 11px; margin-top: 5px;")
hint_label.setWordWrap(True)
model_layout.addRow(hint_label)
self.chunk_size_spin = NoWheelSpinBox()
self.chunk_size_spin.setRange(1024, 16384)
self.chunk_size_spin.setValue(4096)
model_layout.addRow("处理块大小:", self.chunk_size_spin)
self.block_size_spin = NoWheelSpinBox()
self.block_size_spin.setRange(256, 2048)
self.block_size_spin.setValue(512)
model_layout.addRow("空间块大小:", self.block_size_spin)
self.compute_uncertainty_cb = QCheckBox("计算不确定性")
model_layout.addRow("", self.compute_uncertainty_cb)
left_layout.addWidget(model_group)
# 输出设置组
output_group = QGroupBox("输出设置")
output_layout = QFormLayout(output_group)
output_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
output_layout.setLabelAlignment(Qt.AlignLeft)
output_layout.setFormAlignment(Qt.AlignLeft)
self.output_file_edit = QLineEdit()
self.output_file_edit.setPlaceholderText("输出文件前缀路径")
output_btn = QPushButton("选择...")
output_btn.clicked.connect(self.select_output_file)
output_layout.addRow("输出路径:", self.create_file_row(self.output_file_edit, output_btn))
self.output_format_combo = NoWheelComboBox()
self.output_format_combo.addItems(["ENVI"])
output_layout.addRow("输出格式:", self.output_format_combo)
left_layout.addWidget(output_group)
# 运行按钮
run_btn = QPushButton("运行水体 BRDF 校正")
run_btn.setStyleSheet("""
QPushButton {
background-color: #0080ff;
color: #ffffff;
border: 2px solid #0080ff;
font-weight: bold;
padding: 12px 24px;
font-size: 16px;
}
QPushButton:hover {
background-color: #0066cc;
border-color: #0066cc;
}
""")
run_btn.clicked.connect(self.run_water_brdf)
left_layout.addWidget(run_btn)
# 右侧:日志和进度显示
right_widget = QWidget()
right_layout = QVBoxLayout(right_widget)
log_label = QLabel("执行日志")
log_label.setStyleSheet("font-weight: bold; color: #0080ff; font-size: 16px;")
right_layout.addWidget(log_label)
self.log_text = QTextEdit()
self.log_text.setMaximumWidth(400)
self.log_text.setPlainText("等待执行命令...\n")
right_layout.addWidget(self.log_text)
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
right_layout.addWidget(self.progress_bar)
# 添加到分割器
splitter.addWidget(left_widget)
splitter.addWidget(right_widget)
splitter.setStretchFactor(0, 2)
splitter.setStretchFactor(1, 1)
main_layout.addWidget(splitter)
def create_file_row(self, line_edit, button):
"""创建文件选择行"""
widget = QWidget()
layout = QHBoxLayout(widget)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(10)
layout.addWidget(line_edit, 1)
layout.addWidget(button)
return widget
def browse_file(self, line_edit, title, file_filter):
"""浏览选择文件"""
file_path, _ = QFileDialog.getOpenFileName(self, f"选择{title}", "", file_filter)
if file_path:
line_edit.setText(file_path)
def select_output_file(self):
"""选择输出文件路径"""
file_path, _ = QFileDialog.getSaveFileName(self, "选择输出路径", "", "All Files (*)")
if file_path:
self.output_file_edit.setText(file_path)
def run_water_brdf(self):
"""运行水体BRDF校正"""
# 检查必需参数
hyperspectral_file = self.hyperspectral_edit.text().strip()
angle_file = self.angle_edit.text().strip()
mask_file = self.mask_edit.text().strip()
output_file = self.output_file_edit.text().strip()
if not all([hyperspectral_file, angle_file, mask_file, output_file]):
QMessageBox.warning(self, "警告", "请填写所有必需的文件路径!")
return
# 检查文件是否存在
for file_path, name in [(hyperspectral_file, "高光谱文件"),
(angle_file, "角度文件"),
(mask_file, "掩膜文件")]:
if not os.path.exists(file_path):
QMessageBox.warning(self, "警告", f"{name} 不存在:\n{file_path}")
return
# 构建命令
ocbrdf_path = os.path.join(os.path.dirname(__file__), "..", "ocbrdf", "ocbrdf_main3.py")
ocbrdf_path = os.path.abspath(ocbrdf_path)
ocbrdf_dir = os.path.dirname(ocbrdf_path)
if not os.path.exists(ocbrdf_path):
QMessageBox.critical(self, "错误", f"找不到 ocbrdf_main3.py 文件:\n{ocbrdf_path}")
return
# 使用当前Python解释器而不是简单的'python'命令)
python_exe = sys.executable
# 获取选中的输出变量(解析列表项,取空格前的变量名)
selected_items = self.output_vars_list.selectedItems()
output_vars = []
for item in selected_items:
var_name = item.text().split(' - ')[0].strip()
output_vars.append(var_name)
if not output_vars:
QMessageBox.warning(self, "警告", "请至少选择一个输出变量!")
return
# 构建命令参数 - 注意ocbrdf_main3.py 需要从 ocbrdf 目录运行
# 以确保能正确导入同级目录的模块
cmd_parts = [
f'"{python_exe}"',
f'"{ocbrdf_path}"',
f'"{hyperspectral_file}"',
f'"{angle_file}"',
f'"{mask_file}"',
f'"{output_file}"',
f'--brdf-model {self.brdf_model_combo.currentText()}',
f'--output-var {" ".join(output_vars)}',
f'--output-format {self.output_format_combo.currentText()}',
f'--chunk-size {self.chunk_size_spin.value()}',
f'--block-size {self.block_size_spin.value()}'
]
if self.compute_uncertainty_cb.isChecked():
cmd_parts.append('--compute-uncertainty')
command = ' '.join(cmd_parts)
# 显示进度条和日志
self.progress_bar.setVisible(True)
self.progress_bar.setRange(0, 0) # 不确定进度
self.log_text.clear()
self.log_text.append("开始执行水体 BRDF 校正...")
self.log_text.append(f"BRDF 模型: {self.brdf_model_combo.currentText()}")
self.log_text.append(f"输出变量: {', '.join(output_vars)}")
self.log_text.append(f"Python: {python_exe}")
self.log_text.append(f"命令: {command}\n")
# 启动命令执行线程 - 在ocbrdf目录下执行确保能找到本地模块
env = {"PYTHONPATH": ocbrdf_dir} # 添加ocbrdf目录到Python路径
self.command_thread = CommandThread(command, ocbrdf_dir, env=env)
self.command_thread.output_updated.connect(self.append_log)
self.command_thread.finished_signal.connect(self.command_finished)
self.command_thread.start()
def append_log(self, text):
"""添加日志文本"""
self.log_text.append(text)
cursor = self.log_text.textCursor()
cursor.movePosition(cursor.End)
self.log_text.setTextCursor(cursor)
def command_finished(self, success, message):
"""命令执行完成"""
self.progress_bar.setVisible(False)
self.log_text.append(f"\n{message}")
if success:
QMessageBox.information(self, "成功", "水体 BRDF 校正完成!")
else:
QMessageBox.warning(self, "警告", f"水体 BRDF 校正失败:\n{message}")
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()