Files
BRDF/GUI/brdf_gui.py
2026-04-10 16:46:45 +08:00

1303 lines
49 KiB
Python

# -*- coding: utf-8 -*-
import sys
import os
import json
from pathlib import Path
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGroupBox,
QFormLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QComboBox,
QCheckBox, QSpinBox, QDoubleSpinBox, QTabWidget, QMessageBox, QScrollArea,
QStackedWidget, QGridLayout, QDialog, QDialogButtonBox,
QListWidget, QListWidgetItem # 添加这两个
)
from PyQt5.QtCore import Qt
# ==================== 掩膜条件编辑器 ====================
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 = QComboBox()
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)
for param_name, param_type, default_value in params:
if param_type == int:
if "band" in param_name or "radius" in param_name:
spin = QSpinBox()
spin.setRange(0, 3000)
spin.setValue(default_value)
param_widget = spin
else:
spin = QSpinBox()
spin.setRange(-10000, 10000)
spin.setValue(default_value)
param_widget = spin
elif param_type == float:
dspin = QDoubleSpinBox()
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 = QComboBox()
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 = QComboBox()
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 MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("BRDF / Topo / Glint 校正配置工具")
self.setMinimumSize(1100, 800)
# 存储匹配后的文件对列表
self.file_pairs = [] # [(rfl_path, obs_path), ...]
# 存储每个输入文件对应的辅助文件波段映射
self.band_mappings = {} # {rfl_path: {param_name: band_index}}
# 设置全局 NVIDIA 风格样式表
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;
}
""")
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(15)
main_layout.setContentsMargins(20, 20, 20, 20)
tabs = QTabWidget()
tabs.setDocumentMode(True)
main_layout.addWidget(tabs)
# 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_layout = QHBoxLayout()
load_btn = QPushButton("加载 JSON 配置")
load_btn.clicked.connect(self.load_config)
save_btn = QPushButton("保存 JSON 配置")
save_btn.clicked.connect(self.save_config)
btn_layout.addWidget(load_btn)
btn_layout.addWidget(save_btn)
main_layout.addLayout(btn_layout)
# ==================== 文件与波段映射页 ====================
def setup_file_tab(self):
layout = QVBoxLayout(self.file_tab)
layout.setSpacing(15)
layout.setContentsMargins(15, 15, 15, 15)
# 文件夹设置组 - 使用 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)
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)
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)
layout.addWidget(match_btn)
# 文件列表
list_label = QLabel("匹配的文件对 (点击编辑波段映射):")
list_label.setStyleSheet("color: #a7a7a7; font-size: 13px; margin-top: 10px;")
layout.addWidget(list_label)
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.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;
}
""")
layout.addWidget(self.file_list_widget)
# 波段映射组 - 使用蓝色主题
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 = QFormLayout()
mapping_layout.setVerticalSpacing(8)
self.param_spinboxes = {}
param_names = [
"path_length", "sensor_az", "sensor_zn", "solar_az", "solar_zn",
"phase", "slope", "aspect", "cosine_i", "utc_time"
]
for name in param_names:
spin = QSpinBox()
spin.setRange(0, 1000)
spin.setValue(0)
self.param_spinboxes[name] = spin
mapping_layout.addRow(f"{name}:", spin)
mapping_group.setLayout(mapping_layout)
layout.addWidget(mapping_group)
self.current_rfl_path = None
def _horizontal_widget(self, line_edit, button):
w = QWidget()
hbox = QHBoxLayout(w)
hbox.setContentsMargins(0, 0, 0, 0)
hbox.addWidget(line_edit)
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_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
if rfl_path in self.band_mappings:
mapping = self.band_mappings[rfl_path]
for param, spin in self.param_spinboxes.items():
spin.setValue(mapping.get(param, 0))
else:
for spin in self.param_spinboxes.values():
spin.setValue(0)
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_brdf_cb = QCheckBox("BRDF (双向反射分布函数)")
self.corr_glint_cb = QCheckBox("Glint (耀斑校正)")
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)
topo_group = QGroupBox("地形校正 (Topo Correction)")
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_label = QLabel("类型 (type):")
type_layout.addWidget(type_label)
self.topo_type = QComboBox()
self.topo_type.addItems(["mod_minneart", "scs+c", "cosine", "c", "scs"])
type_layout.addWidget(self.topo_type)
type_layout.addStretch()
topo_layout.addLayout(type_layout)
# c_fit_type
fit_layout = QHBoxLayout()
fit_label = QLabel("拟合类型 (c_fit_type):")
fit_layout.addWidget(fit_label)
self.c_fit_type = QComboBox()
self.c_fit_type.addItems(["nnls", "ols", "wls"])
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)
topo_group.setLayout(topo_layout)
layout.addWidget(topo_group)
# 3. BRDF 参数 - NVIDIA 绿色主题 (#76b900)
brdf_group = QGroupBox("BRDF 校正 (BRDF Correction)")
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)
self.brdf_type = QComboBox()
self.brdf_type.addItems(["flex", "universal"])
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 = QComboBox()
self.geometric_kernel.addItems(["li_sparse", "li_dense", "li_sparse_r", "li_dense_r", "roujean"])
brdf_layout.addRow("几何核 (geometric):", self.geometric_kernel)
self.volume_kernel = QComboBox()
self.volume_kernel.addItems(["ross_thin", "ross_thick", "hotspot", "roujean"])
brdf_layout.addRow("体积核 (volume):", self.volume_kernel)
self.br_b_ratio = QDoubleSpinBox()
self.br_b_ratio.setRange(0.1, 10.0)
self.br_b_ratio.setValue(2.5)
self.br_b_ratio.setDecimals(2)
brdf_layout.addRow("b/r 比率:", self.br_b_ratio)
self.hb_ratio = QDoubleSpinBox()
self.hb_ratio.setRange(0.1, 5.0)
self.hb_ratio.setValue(2.0)
self.hb_ratio.setDecimals(2)
brdf_layout.addRow("h/b 比率:", self.hb_ratio)
self.sample_perc = QDoubleSpinBox()
self.sample_perc.setRange(0.01, 1.0)
self.sample_perc.setValue(0.1)
self.sample_perc.setDecimals(2)
brdf_layout.addRow("采样比例 (sample_perc):", self.sample_perc)
self.interp_kind = QComboBox()
self.interp_kind.addItems(["linear", "nearest", "cubic"])
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 = QComboBox()
self.bin_type.addItems(["dynamic", "fixed"])
brdf_layout.addRow("分箱类型 (bin_type):", self.bin_type)
self.num_bins = QSpinBox()
self.num_bins.setRange(1, 100)
self.num_bins.setValue(18)
brdf_layout.addRow("分箱数量 (num_bins):", self.num_bins)
self.ndvi_bin_min = QDoubleSpinBox()
self.ndvi_bin_min.setRange(0.0, 1.0)
self.ndvi_bin_min.setValue(0.05)
self.ndvi_bin_min.setDecimals(2)
brdf_layout.addRow("NDVI 最小值 (ndvi_bin_min):", self.ndvi_bin_min)
self.ndvi_bin_max = QDoubleSpinBox()
self.ndvi_bin_max.setRange(0.0, 1.0)
self.ndvi_bin_max.setValue(1.0)
self.ndvi_bin_max.setDecimals(2)
brdf_layout.addRow("NDVI 最大值 (ndvi_bin_max):", self.ndvi_bin_max)
self.ndvi_perc_min = QSpinBox()
self.ndvi_perc_min.setRange(0, 100)
self.ndvi_perc_min.setValue(10)
brdf_layout.addRow("NDVI 百分比最小值 (ndvi_perc_min):", self.ndvi_perc_min)
self.ndvi_perc_max = QSpinBox()
self.ndvi_perc_max.setRange(0, 100)
self.ndvi_perc_max.setValue(95)
brdf_layout.addRow("NDVI 百分比最大值 (ndvi_perc_max):", self.ndvi_perc_max)
self.solar_zn_type = QComboBox()
self.solar_zn_type.addItems(["scene", "pixel"])
brdf_layout.addRow("太阳天顶角类型 (solar_zn_type):", self.solar_zn_type)
brdf_group.setLayout(brdf_layout)
layout.addWidget(brdf_group)
# 4. Glint 参数 - 橙色主题 (#df6500)
glint_group = QGroupBox("耀斑校正 (Glint Correction)")
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)
self.glint_type = QComboBox()
self.glint_type.addItems(["hochberg", "gao", "hedley"])
glint_layout.addRow("校正算法 (type):", self.glint_type)
self.correction_wave = QSpinBox()
self.correction_wave.setRange(1000, 2500)
self.correction_wave.setValue(2150)
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)
glint_group.setLayout(glint_layout)
layout.addWidget(glint_group)
layout.addStretch()
self.corr_tab.setLayout(QVBoxLayout())
self.corr_tab.layout().addWidget(scroll)
# 设置容器背景色
container.setStyleSheet("background-color: #000000;")
# ==================== 输出设置页 ====================
def setup_output_tab(self):
layout = QFormLayout(self.output_tab)
layout.setVerticalSpacing(12)
layout.setHorizontalSpacing(10)
layout.setContentsMargins(15, 15, 15, 15)
# 设置输出设置页的样式
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;
}
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)
# 重采样
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
# 文件与波段映射(只恢复映射关系,不自动恢复文件对)
if "anc_files" in config:
self.band_mappings = {}
for rfl_path, anc_info in config["anc_files"].items():
mapping = {}
for param, val in anc_info.items():
if isinstance(val, list) and len(val) >= 2:
mapping[param] = val[1]
self.band_mappings[rfl_path] = mapping
# 刷新当前选中的文件对映射显示
if self.file_pairs and self.file_list_widget.currentItem():
self.on_file_selected(self.file_list_widget.currentItem())
# 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)
# 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.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"] = 2
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}")
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()