增加模块;增加主调用命令
This commit is contained in:
440
prosail_method/prosail_gui.py
Normal file
440
prosail_method/prosail_gui.py
Normal file
@ -0,0 +1,440 @@
|
||||
import sys
|
||||
import os
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
||||
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
|
||||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QGroupBox, QLabel, QDoubleSpinBox, QRadioButton, QPushButton,
|
||||
QButtonGroup, QSplitter, QFrame, QFileDialog, QMessageBox,
|
||||
QCheckBox, QProgressBar, QStatusBar)
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt5.QtGui import QFont, QIcon
|
||||
|
||||
# 添加当前目录到Python路径,以便导入本地模块
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if current_dir not in sys.path:
|
||||
sys.path.insert(0, current_dir)
|
||||
|
||||
from modules.load_coefficients import load_coefficients
|
||||
from modules.prospect_d import prospect_d
|
||||
from modules.sail import sail
|
||||
|
||||
class SimulationWorker(QThread):
|
||||
"""Worker thread for running PROSAIL simulation"""
|
||||
finished = pyqtSignal(object) # Signal emitted when simulation is complete
|
||||
error = pyqtSignal(str) # Signal emitted when error occurs
|
||||
|
||||
def __init__(self, params):
|
||||
super().__init__()
|
||||
self.params = params
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
# Extract parameters
|
||||
N, Cab, Car, Ant, Cbrown, Cw, Cm = self.params['leaf_params']
|
||||
LIDFa, LIDFb, TypeLidf, LAI, q = self.params['canopy_params']
|
||||
tts, tto, psi = self.params['angle_params']
|
||||
rsoil = self.params['soil_param']
|
||||
|
||||
# Load coefficients
|
||||
data_path = os.path.join(current_dir, "data", "dataSpec_PDB.txt")
|
||||
wl, nr, Kab, Kcar, Kant, Kbrown, Kw, Km, Rsoil1, Rsoil2 = load_coefficients(data_path)
|
||||
|
||||
# Run PROSPECT-D
|
||||
LRT = prospect_d(N, Cab, Car, Ant, Cbrown, Cw, Cm, wl, nr, Kab, Kcar, Kant, Kbrown, Kw, Km)
|
||||
rho = LRT[:, 1]
|
||||
tau = LRT[:, 2]
|
||||
|
||||
# Run SAIL
|
||||
refl = sail(rho, tau, LIDFa, LIDFb, TypeLidf, LAI, q, tts, tto, psi, rsoil, wl)
|
||||
rdot = refl[:, 1]
|
||||
rsot = refl[:, 2]
|
||||
|
||||
# Calculate RESV
|
||||
data_all = np.loadtxt(data_path, comments='%', delimiter=None)
|
||||
Es = data_all[:, 8]
|
||||
Ed = data_all[:, 9]
|
||||
rd = np.pi / 180
|
||||
skyl = 0.847 - 1.61 * np.sin((90 - tts) * rd) + 1.04 * np.sin((90 - tts) * rd) ** 2
|
||||
PARdiro = (1 - skyl) * Es
|
||||
PARdifo = skyl * Ed
|
||||
denominator = PARdiro + PARdifo
|
||||
denominator[denominator == 0] = 1e-6
|
||||
resv = (rdot * PARdifo + rsot * PARdiro) / denominator
|
||||
|
||||
# Create DataFrame
|
||||
df = pd.DataFrame({"wavelength": wl, "resv": resv})
|
||||
self.finished.emit(df)
|
||||
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
class PROSAILSimulator(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.results = [] # Store simulation results as (params, df) tuples
|
||||
self.worker = None
|
||||
self.initUI()
|
||||
|
||||
def initUI(self):
|
||||
self.setWindowTitle("PROSAIL Simulator")
|
||||
self.setGeometry(100, 100, 1200, 800)
|
||||
|
||||
# Create central widget and main layout
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
main_layout = QHBoxLayout(central_widget)
|
||||
|
||||
# Create splitter for resizable panels
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
main_layout.addWidget(splitter)
|
||||
|
||||
# Left panel - Parameters
|
||||
self.create_parameter_panel()
|
||||
splitter.addWidget(self.parameter_panel)
|
||||
|
||||
# Right panel - Plot
|
||||
self.create_plot_panel()
|
||||
splitter.addWidget(self.plot_panel)
|
||||
|
||||
# Set splitter proportions
|
||||
splitter.setSizes([400, 800])
|
||||
|
||||
# Status bar
|
||||
self.status_bar = self.statusBar()
|
||||
self.status_bar.showMessage("就绪")
|
||||
|
||||
# Progress bar for simulation
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
self.status_bar.addPermanentWidget(self.progress_bar)
|
||||
|
||||
def create_parameter_panel(self):
|
||||
self.parameter_panel = QWidget()
|
||||
layout = QVBoxLayout(self.parameter_panel)
|
||||
|
||||
# Control buttons
|
||||
control_group = QGroupBox("控制选项")
|
||||
control_layout = QHBoxLayout(control_group)
|
||||
|
||||
self.keep_checkbox = QCheckBox(" 保留")
|
||||
self.simulate_btn = QPushButton(" 模拟")
|
||||
self.simulate_btn.clicked.connect(self.run_simulation)
|
||||
self.clear_btn = QPushButton(" 清空")
|
||||
self.clear_btn.clicked.connect(self.clear_results)
|
||||
|
||||
control_layout.addWidget(self.keep_checkbox)
|
||||
control_layout.addWidget(self.simulate_btn)
|
||||
control_layout.addWidget(self.clear_btn)
|
||||
layout.addWidget(control_group)
|
||||
|
||||
# Parameters section
|
||||
params_scroll = QWidget()
|
||||
params_layout = QVBoxLayout(params_scroll)
|
||||
|
||||
# Canopy parameters
|
||||
self.create_canopy_params(params_layout)
|
||||
|
||||
# Angle parameters
|
||||
self.create_angle_params(params_layout)
|
||||
|
||||
# Leaf parameters
|
||||
self.create_leaf_params(params_layout)
|
||||
|
||||
# Soil parameters
|
||||
self.create_soil_params(params_layout)
|
||||
|
||||
layout.addWidget(params_scroll)
|
||||
|
||||
# Save button
|
||||
self.save_btn = QPushButton(" 保存为 CSV")
|
||||
self.save_btn.clicked.connect(self.save_results)
|
||||
self.save_btn.setEnabled(False)
|
||||
layout.addWidget(self.save_btn)
|
||||
|
||||
|
||||
|
||||
def create_canopy_params(self, parent_layout):
|
||||
canopy_group = QGroupBox("1. 冠层参数")
|
||||
layout = QVBoxLayout(canopy_group)
|
||||
|
||||
# LIDF Type
|
||||
lidf_layout = QHBoxLayout()
|
||||
lidf_layout.addWidget(QLabel("LIDF 类型:"))
|
||||
self.lidf_group = QButtonGroup()
|
||||
self.lidf_1 = QRadioButton("双参数分布")
|
||||
self.lidf_2 = QRadioButton("Campbell 分布")
|
||||
self.lidf_1.setChecked(True)
|
||||
self.lidf_group.addButton(self.lidf_1, 1)
|
||||
self.lidf_group.addButton(self.lidf_2, 2)
|
||||
lidf_layout.addWidget(self.lidf_1)
|
||||
lidf_layout.addWidget(self.lidf_2)
|
||||
layout.addLayout(lidf_layout)
|
||||
|
||||
# Parameters
|
||||
params_layout = QHBoxLayout()
|
||||
|
||||
left_col = QVBoxLayout()
|
||||
self.lai_input = self.create_spinbox("LAI (叶面积指数)", 3.0, 0.0, 8.0, 0.1)
|
||||
left_col.addWidget(self.lai_input)
|
||||
self.lidfb_input = self.create_spinbox("LIDFb", 0.0, -1.0, 1.0, 0.05)
|
||||
left_col.addWidget(self.lidfb_input)
|
||||
|
||||
right_col = QVBoxLayout()
|
||||
self.lidfa_input = self.create_spinbox("LIDFa", 0.0, -1.0, 1.0, 0.05)
|
||||
right_col.addWidget(self.lidfa_input)
|
||||
self.q_input = self.create_spinbox("hotspot q", 0.05, 0.0, 1.0, 0.01)
|
||||
right_col.addWidget(self.q_input)
|
||||
|
||||
params_layout.addLayout(left_col)
|
||||
params_layout.addLayout(right_col)
|
||||
layout.addLayout(params_layout)
|
||||
|
||||
# Connect LIDF type change
|
||||
self.lidf_group.buttonClicked.connect(self.on_lidf_changed)
|
||||
|
||||
parent_layout.addWidget(canopy_group)
|
||||
|
||||
def create_angle_params(self, parent_layout):
|
||||
angle_group = QGroupBox("2. 观测与光照角度")
|
||||
layout = QHBoxLayout(angle_group)
|
||||
|
||||
left_col = QVBoxLayout()
|
||||
self.tts_input = self.create_spinbox("tts (太阳天顶角)", 30.0, 0.0, 90.0, 1.0)
|
||||
left_col.addWidget(self.tts_input)
|
||||
self.psi_input = self.create_spinbox("psi (方位角差)", 0.0, 0.0, 180.0, 5.0)
|
||||
left_col.addWidget(self.psi_input)
|
||||
|
||||
right_col = QVBoxLayout()
|
||||
self.tto_input = self.create_spinbox("tto (观测天顶角)", 20.0, 0.0, 90.0, 1.0)
|
||||
right_col.addWidget(self.tto_input)
|
||||
|
||||
layout.addLayout(left_col)
|
||||
layout.addLayout(right_col)
|
||||
|
||||
parent_layout.addWidget(angle_group)
|
||||
|
||||
def create_leaf_params(self, parent_layout):
|
||||
leaf_group = QGroupBox("3. 叶片参数")
|
||||
layout = QHBoxLayout(leaf_group)
|
||||
|
||||
left_col = QVBoxLayout()
|
||||
self.n_input = self.create_spinbox("N (叶结构参数)", 1.5, 1.0, 3.0, 0.1)
|
||||
left_col.addWidget(self.n_input)
|
||||
self.car_input = self.create_spinbox("Car (类胡萝卜素)", 8.0, 0.0, 30.0, 1.0)
|
||||
left_col.addWidget(self.car_input)
|
||||
self.cm_input = self.create_spinbox("Cm (干物质含量)", 0.009, 0.0, 0.05, 0.001)
|
||||
left_col.addWidget(self.cm_input)
|
||||
self.ant_input = self.create_spinbox("Ant (花青素)", 0.0, 0.0, 5.0, 0.1)
|
||||
left_col.addWidget(self.ant_input)
|
||||
|
||||
right_col = QVBoxLayout()
|
||||
self.cab_input = self.create_spinbox("Cab (叶绿素含量)", 40.0, 0.0, 100.0, 1.0)
|
||||
right_col.addWidget(self.cab_input)
|
||||
self.cw_input = self.create_spinbox("Cw (叶片含水量)", 0.015, 0.0, 0.05, 0.001)
|
||||
right_col.addWidget(self.cw_input)
|
||||
self.cbrown_input = self.create_spinbox("Cbrown (棕色素)", 0.0, 0.0, 1.0, 0.05)
|
||||
right_col.addWidget(self.cbrown_input)
|
||||
|
||||
layout.addLayout(left_col)
|
||||
layout.addLayout(right_col)
|
||||
|
||||
parent_layout.addWidget(leaf_group)
|
||||
|
||||
def create_soil_params(self, parent_layout):
|
||||
soil_group = QGroupBox("4. 土壤湿度")
|
||||
layout = QVBoxLayout(soil_group)
|
||||
|
||||
self.rsoil_input = self.create_spinbox("rsoil (土壤反射率)", 0.2, 0.0, 1.0, 0.01)
|
||||
layout.addWidget(self.rsoil_input)
|
||||
|
||||
parent_layout.addWidget(soil_group)
|
||||
|
||||
def create_spinbox(self, label, default, min_val, max_val, step):
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.addWidget(QLabel(label))
|
||||
spinbox = QDoubleSpinBox()
|
||||
spinbox.setRange(min_val, max_val)
|
||||
spinbox.setValue(default)
|
||||
spinbox.setSingleStep(step)
|
||||
spinbox.setDecimals(3)
|
||||
layout.addWidget(spinbox)
|
||||
return widget
|
||||
|
||||
def create_plot_panel(self):
|
||||
self.plot_panel = QWidget()
|
||||
layout = QVBoxLayout(self.plot_panel)
|
||||
|
||||
# Create matplotlib figure
|
||||
self.figure = plt.Figure(figsize=(10, 6))
|
||||
self.canvas = FigureCanvas(self.figure)
|
||||
self.toolbar = NavigationToolbar(self.canvas, self)
|
||||
|
||||
layout.addWidget(self.toolbar)
|
||||
layout.addWidget(self.canvas)
|
||||
|
||||
# Initial empty plot
|
||||
self.update_plot()
|
||||
|
||||
def on_lidf_changed(self):
|
||||
lidf_type = self.lidf_group.checkedId()
|
||||
if lidf_type == 1: # 双参数分布
|
||||
self.lidfa_input.findChild(QDoubleSpinBox).setRange(-1.0, 1.0)
|
||||
self.lidfa_input.findChild(QDoubleSpinBox).setValue(0.0)
|
||||
else: # Campbell 分布
|
||||
self.lidfa_input.findChild(QDoubleSpinBox).setRange(0.0, 90.0)
|
||||
self.lidfa_input.findChild(QDoubleSpinBox).setValue(30.0)
|
||||
|
||||
def get_params(self):
|
||||
leaf_params = (
|
||||
self.n_input.findChild(QDoubleSpinBox).value(),
|
||||
self.cab_input.findChild(QDoubleSpinBox).value(),
|
||||
self.car_input.findChild(QDoubleSpinBox).value(),
|
||||
self.ant_input.findChild(QDoubleSpinBox).value(),
|
||||
self.cbrown_input.findChild(QDoubleSpinBox).value(),
|
||||
self.cw_input.findChild(QDoubleSpinBox).value(),
|
||||
self.cm_input.findChild(QDoubleSpinBox).value()
|
||||
)
|
||||
|
||||
canopy_params = (
|
||||
self.lidfa_input.findChild(QDoubleSpinBox).value(),
|
||||
self.lidfb_input.findChild(QDoubleSpinBox).value(),
|
||||
self.lidf_group.checkedId(),
|
||||
self.lai_input.findChild(QDoubleSpinBox).value(),
|
||||
self.q_input.findChild(QDoubleSpinBox).value()
|
||||
)
|
||||
|
||||
angle_params = (
|
||||
self.tts_input.findChild(QDoubleSpinBox).value(),
|
||||
self.tto_input.findChild(QDoubleSpinBox).value(),
|
||||
self.psi_input.findChild(QDoubleSpinBox).value()
|
||||
)
|
||||
|
||||
soil_param = self.rsoil_input.findChild(QDoubleSpinBox).value()
|
||||
|
||||
return {
|
||||
'leaf_params': leaf_params,
|
||||
'canopy_params': canopy_params,
|
||||
'angle_params': angle_params,
|
||||
'soil_param': soil_param
|
||||
}
|
||||
|
||||
def run_simulation(self):
|
||||
if self.worker and self.worker.isRunning():
|
||||
return
|
||||
|
||||
self.simulate_btn.setEnabled(False)
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setRange(0, 0) # Indeterminate progress
|
||||
self.status_bar.showMessage("正在运行 PROSAIL 模拟...")
|
||||
|
||||
params = self.get_params()
|
||||
self.worker = SimulationWorker(params)
|
||||
self.worker.finished.connect(self.on_simulation_finished)
|
||||
self.worker.error.connect(self.on_simulation_error)
|
||||
self.worker.start()
|
||||
|
||||
def on_simulation_finished(self, df):
|
||||
self.progress_bar.setVisible(False)
|
||||
self.simulate_btn.setEnabled(True)
|
||||
self.status_bar.showMessage("模拟完成")
|
||||
|
||||
# Get current parameters at simulation time
|
||||
current_params = self.get_params()
|
||||
|
||||
if self.keep_checkbox.isChecked():
|
||||
self.results.append((current_params, df))
|
||||
else:
|
||||
self.results = [(current_params, df)]
|
||||
|
||||
self.save_btn.setEnabled(True)
|
||||
self.update_plot()
|
||||
|
||||
def on_simulation_error(self, error_msg):
|
||||
self.progress_bar.setVisible(False)
|
||||
self.simulate_btn.setEnabled(True)
|
||||
self.status_bar.showMessage("模拟失败")
|
||||
QMessageBox.critical(self, "错误", f"模拟过程中出现错误:\n{error_msg}")
|
||||
|
||||
def clear_results(self):
|
||||
self.results = []
|
||||
self.save_btn.setEnabled(False)
|
||||
self.update_plot()
|
||||
self.status_bar.showMessage("结果已清空")
|
||||
|
||||
def update_plot(self):
|
||||
self.figure.clear()
|
||||
ax = self.figure.add_subplot(111)
|
||||
|
||||
if self.results:
|
||||
for i, (params, df_plot) in enumerate(self.results):
|
||||
ax.plot(df_plot["wavelength"], df_plot["resv"], label=f"refl #{i+1}")
|
||||
ax.set_ylabel("Reflectance")
|
||||
ax.legend()
|
||||
else:
|
||||
ax.text(0.5, 0.5, "无模拟结果", fontsize=16, ha='center', va='center',
|
||||
alpha=0.5, transform=ax.transAxes)
|
||||
|
||||
ax.set_xlabel("Wavelength (nm)")
|
||||
ax.grid(True)
|
||||
self.canvas.draw()
|
||||
|
||||
def save_results(self):
|
||||
if not self.results:
|
||||
return
|
||||
|
||||
filename, _ = QFileDialog.getSaveFileName(
|
||||
self, "保存结果", "prosail_resv.csv",
|
||||
"CSV 文件 (*.csv);;所有文件 (*)"
|
||||
)
|
||||
|
||||
if filename:
|
||||
try:
|
||||
# Prepare data for CSV - each row represents one sample
|
||||
all_data = []
|
||||
|
||||
for i, (params, df) in enumerate(self.results):
|
||||
# Create one row per sample with parameters + reflectance spectrum
|
||||
data_row = {
|
||||
'sample_id': i + 1,
|
||||
'N': params['leaf_params'][0],
|
||||
'Cab': params['leaf_params'][1],
|
||||
'Car': params['leaf_params'][2],
|
||||
'Ant': params['leaf_params'][3],
|
||||
'Cbrown': params['leaf_params'][4],
|
||||
'Cw': params['leaf_params'][5],
|
||||
'Cm': params['leaf_params'][6],
|
||||
'LIDFa': params['canopy_params'][0],
|
||||
'LIDFb': params['canopy_params'][1],
|
||||
'TypeLidf': params['canopy_params'][2],
|
||||
'LAI': params['canopy_params'][3],
|
||||
'q': params['canopy_params'][4],
|
||||
'tts': params['angle_params'][0],
|
||||
'tto': params['angle_params'][1],
|
||||
'psi': params['angle_params'][2],
|
||||
'rsoil': params['soil_param']
|
||||
}
|
||||
|
||||
# Add reflectance values for each wavelength
|
||||
for _, row in df.iterrows():
|
||||
wavelength = int(row['wavelength'])
|
||||
data_row[f'refl_{wavelength}'] = row['resv']
|
||||
|
||||
all_data.append(data_row)
|
||||
|
||||
# Create DataFrame and save
|
||||
result_df = pd.DataFrame(all_data)
|
||||
result_df.to_csv(filename, index=False)
|
||||
self.status_bar.showMessage(f"结果已保存到 {filename} (共 {len(all_data)} 个样本)")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"保存文件时出现错误:\n{str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
app.setStyle('Fusion') # Modern style
|
||||
window = PROSAILSimulator()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
Reference in New Issue
Block a user