# -*- 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()