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