2883 lines
107 KiB
Python
2883 lines
107 KiB
Python
# -*- 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() |