feat: 波长偏移修正 + 项目架构文档

- BandMathCalculator 支持 wavelength_offset 参数,公式波长统一加减偏移后匹配传感器波段
- WaterQualityIndexCalculator 全链传递偏移量 (band_math → calculate_one → calculate_many)
- WaterIndexCsvProcessor / Step7Handler / DataPreparationStep 传播偏移参数
- Step7/Step10 面板新增 QDoubleSpinBox 波长偏移控件 (±200nm, 默认0)
- 偏移控件去除单位后缀,避免编辑时需手动移动光标
- 新增 ARCHITECTURE.md 完整项目架构文档
This commit is contained in:
duxin
2026-06-30 13:13:38 +08:00
parent 8ff5b08190
commit 9e433395f4
8 changed files with 537 additions and 15 deletions

View File

@ -114,6 +114,7 @@ class WaterIndexCsvProcessor:
output_dir: str,
selected_formulas: Optional[List[str]] = None,
progress_callback: Optional[Callable[[str, float], None]] = None,
wavelength_offset: float = 0.0,
) -> Dict[str, str]:
"""
散点 CSV → 按指数拆分的多个 CSV。
@ -128,6 +129,8 @@ class WaterIndexCsvProcessor:
要计算的公式名列表;None 或空列表 = 全部公式
progress_callback : callable, optional
进度回调 ``(msg: str, pct: float)``
wavelength_offset : float
波长偏移修正量(nm),公式波长统一加上此值后再匹配波段
Returns
-------
@ -192,7 +195,7 @@ class WaterIndexCsvProcessor:
notify(f"开始逐行计算 {len(targets)} 个公式…", 25)
spectra_df = df[wl_cols]
try:
results_df = calc.calculate_many(targets, spectra_df)
results_df = calc.calculate_many(targets, spectra_df, wavelength_offset=wavelength_offset)
except Exception as e:
raise RuntimeError(f"公式计算失败: {e}")

View File

@ -38,6 +38,7 @@ class Step7CalcIndicesHandler(BaseStepHandler):
output_file=config.get('output_file'),
enabled=config.get('enabled', True),
output_dir=str(context.indices_dir),
wavelength_offset=float(config.get('wavelength_offset', 0)),
)
context.indices_path = result

View File

@ -133,6 +133,7 @@ class DataPreparationStep:
enabled: bool = True,
output_dir: Union[str, Path] = "./7_Water_Quality_Indices",
callback: Optional[Callable] = None,
wavelength_offset: float = 0.0,
) -> Optional[str]:
"""根据训练光谱计算水质光谱指数(使用 band_math 方法)"""
output_dir = Path(output_dir)
@ -170,7 +171,7 @@ class DataPreparationStep:
from src.utils.band_math import BandMathCalculator
calculator = BandMathCalculator(training_csv_path)
calculator = BandMathCalculator(training_csv_path, wavelength_offset=wavelength_offset)
result_df = calculator.process_formulas_from_csv(
formula_csv_file=formula_csv_file,
formula_names=formula_names,

View File

@ -26,7 +26,7 @@ from PyQt5.QtWidgets import (
QGroupBox, QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton,
QFileDialog, QMessageBox, QListWidget, QListWidgetItem,
QAbstractItemView, QProgressBar, QTextEdit, QFrame,
QScrollArea, QSizePolicy,
QScrollArea, QSizePolicy, QDoubleSpinBox,
)
from PyQt5.QtGui import QFont
from PyQt5.QtCore import Qt, QThread, pyqtSignal
@ -72,6 +72,7 @@ class WaterIndexWorker(QThread):
selected_formulas: List[str],
waterindex_csv: str,
work_dir: Optional[str] = None,
wavelength_offset: float = 0.0,
):
super().__init__()
self.sampling_csv_path = sampling_csv_path
@ -79,6 +80,7 @@ class WaterIndexWorker(QThread):
self.selected_formulas = selected_formulas
self.waterindex_csv = waterindex_csv
self.work_dir = work_dir
self.wavelength_offset = float(wavelength_offset)
def run(self):
try:
@ -96,6 +98,7 @@ class WaterIndexWorker(QThread):
output_dir=self.output_dir,
selected_formulas=self.selected_formulas or None,
progress_callback=lambda m, p: self.progress.emit(m, p),
wavelength_offset=self.wavelength_offset,
)
self.progress.emit(
@ -166,7 +169,23 @@ class Step10WatercolorPanel(QWidget):
self.sampling_csv_file.label.setMinimumWidth(100)
input_layout.addWidget(self.sampling_csv_file)
# 注意:彻底去掉了 self.meta_label 及其相关的布局代码
# ── 波长偏移修正 ──
offset_layout = QHBoxLayout()
offset_label = QLabel("波长偏移 (nm):")
offset_label.setMinimumWidth(120)
self.wavelength_offset_spin = QDoubleSpinBox()
self.wavelength_offset_spin.setRange(-200.0, 200.0)
self.wavelength_offset_spin.setValue(0.0)
self.wavelength_offset_spin.setDecimals(1)
self.wavelength_offset_spin.setSuffix("")
self.wavelength_offset_spin.setToolTip(
"传感器波长系统偏移修正量。正数=公式波长加偏移(如+100则w450→找≈w550的波段);"
"负数=公式波长减偏移。默认0表示不做修正。"
)
offset_layout.addWidget(offset_label)
offset_layout.addWidget(self.wavelength_offset_spin)
offset_layout.addStretch()
input_layout.addLayout(offset_layout)
input_group.setLayout(input_layout)
layout.addWidget(input_group)
@ -653,6 +672,7 @@ class Step10WatercolorPanel(QWidget):
selected_formulas=selected,
waterindex_csv=self._waterindex_csv,
work_dir=work_dir,
wavelength_offset=self.wavelength_offset_spin.value(),
)
self._worker.progress.connect(self._on_progress)
self._worker.finished_ok.connect(self._on_finished)

View File

@ -14,6 +14,7 @@ from PyQt5.QtWidgets import (
QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout,
QLabel, QPushButton, QMessageBox, QListWidget,
QListWidgetItem, QSizePolicy, QWidget, QComboBox,
QDoubleSpinBox,
)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
@ -238,6 +239,30 @@ class Step7InversionPanel(QWidget):
params_group.setLayout(params_layout)
main_layout.addWidget(params_group)
# ==========================================
# 波长偏移修正
# ==========================================
offset_group = QGroupBox("🔧 波长偏移修正")
offset_layout = QHBoxLayout()
offset_layout.setContentsMargins(20, 16, 20, 16)
offset_label = QLabel("统一偏移量 (nm):")
offset_label.setMinimumWidth(120)
self.wavelength_offset_spin = QDoubleSpinBox()
self.wavelength_offset_spin.setRange(-200.0, 200.0)
self.wavelength_offset_spin.setValue(0.0)
self.wavelength_offset_spin.setDecimals(1)
self.wavelength_offset_spin.setSuffix("")
self.wavelength_offset_spin.setToolTip(
"传感器波长系统偏移修正。公式中的目标波长统一加上此值后再匹配传感器波段。\n"
"例如:偏移 +100 意味着公式中的 w450 实际去找传感器 ~550nm 的波段。\n"
"默认 0 不做修正。适用于传感器标定漂移或不同传感器间的波段对齐。"
)
offset_layout.addWidget(offset_label)
offset_layout.addWidget(self.wavelength_offset_spin)
offset_layout.addStretch()
offset_group.setLayout(offset_layout)
main_layout.addWidget(offset_group)
# ==========================================
# 卡片 3:输出与执行
# ==========================================
@ -309,7 +334,8 @@ class Step7InversionPanel(QWidget):
'training_csv_path': self.training_data_widget.get_path(),
'formula_csv_file': self.formula_file.get_path(),
'formula_names': selected_names,
'enabled': True
'enabled': True,
'wavelength_offset': self.wavelength_offset_spin.value(),
}
output_path = self.output_file.get_path()
if output_path:
@ -337,6 +363,12 @@ class Step7InversionPanel(QWidget):
if 'output_path' in config:
self.output_file.set_path(config['output_path'])
if 'wavelength_offset' in config:
try:
self.wavelength_offset_spin.setValue(float(config['wavelength_offset']))
except (ValueError, TypeError):
pass
def _load_formulas_from_csv(self):
"""解析公式 CSV 文件并填充列表框"""
csv_path = self.formula_file.get_path()

View File

@ -4,13 +4,16 @@ import re
class BandMathCalculator:
def __init__(self, csv_file):
def __init__(self, csv_file, wavelength_offset=0.0):
"""
初始化计算器
csv_file: 包含光谱反射率的CSV文件路径
wavelength_offset: 波长偏移修正量(nm),公式中的目标波长会统一加上此偏移后再匹配最近的传感器波段。
例如 offset=100 意味着公式中的 w450 实际会去找传感器波段 ~550nm。
"""
self.df = pd.read_csv(csv_file)
self.wavelengths = self._extract_wavelengths()
self.wavelength_offset = float(wavelength_offset)
def _extract_wavelengths(self):
"""从列名中提取波长信息"""
@ -25,19 +28,25 @@ class BandMathCalculator:
return wavelengths
def _find_closest_wavelength(self, target_wavelength):
"""找到最接近目标波长的列索引"""
"""找到最接近目标波长的列索引(自动应用波长偏移修正)"""
# 应用波长偏移修正
adjusted_target = target_wavelength + self.wavelength_offset
valid_indices = [i for i, wl in enumerate(self.wavelengths) if wl is not None]
if not valid_indices:
raise ValueError("未找到有效的波长列")
# 计算与目标波长的差值
differences = [abs(self.wavelengths[i] - target_wavelength) for i in valid_indices]
differences = [abs(self.wavelengths[i] - adjusted_target) for i in valid_indices]
min_diff_index = np.argmin(differences)
closest_index = valid_indices[min_diff_index]
closest_wavelength = self.wavelengths[closest_index]
print(
f"目标波长 {target_wavelength}nm -> 最接近波长 {closest_wavelength}nm (列: {self.df.columns[closest_index]})")
if abs(self.wavelength_offset) > 0.01:
print(
f"公式波长 {target_wavelength}nm + 偏移 {self.wavelength_offset}nm → 目标 {adjusted_target}nm → 最接近波段 {closest_wavelength}nm (列: {self.df.columns[closest_index]})")
else:
print(
f"目标波长 {target_wavelength}nm -> 最接近波长 {closest_wavelength}nm (列: {self.df.columns[closest_index]})")
return closest_index
def _parse_expression(self, expression):

View File

@ -90,13 +90,14 @@ class WaterQualityIndexCalculator:
parts = [float(x.strip()) for x in s.split(",")]
return np.array(parts)
def _band_math_all_rows(self, df: pd.DataFrame, expression: str) -> pd.Series:
def _band_math_all_rows(self, df: pd.DataFrame, expression: str, wavelength_offset: float = 0.0) -> pd.Series:
"""
使用 BandMathCalculator 的公式计算引擎,在整个 DataFrame 上批量求值。
Args:
df: 输入光谱数据(列名为 wNNN 格式)
expression: 波段计算表达式,如 "(w715 - w686) / (w715 + w686)"
wavelength_offset: 波长偏移修正量(nm)
Returns:
pd.Series,与 df 等长的计算结果
@ -104,6 +105,7 @@ class WaterQualityIndexCalculator:
calc = BandMathCalculator.__new__(BandMathCalculator)
calc.df = df.copy()
calc.wavelengths = calc._extract_wavelengths()
calc.wavelength_offset = float(wavelength_offset)
variables = calc._parse_expression(expression)
results = []
@ -126,13 +128,14 @@ class WaterQualityIndexCalculator:
return pd.Series(results, index=df.index, name=expression)
def calculate_one(self, name: str, df: pd.DataFrame) -> pd.Series:
def calculate_one(self, name: str, df: pd.DataFrame, wavelength_offset: float = 0.0) -> pd.Series:
"""
计算单个水质指数。
Args:
name: 公式名称(对应 Formula_Name)
df: 光谱反射率 DataFrame
wavelength_offset: 波长偏移修正量(nm)
Returns:
pd.Series,计算结果
@ -145,7 +148,7 @@ class WaterQualityIndexCalculator:
ftype = cfg["type"]
coeff_str = cfg["coeff"]
raw = self._band_math_all_rows(df, expr)
raw = self._band_math_all_rows(df, expr, wavelength_offset=wavelength_offset)
if ftype == "concentration":
coeff = self._parse_coeff(coeff_str)
@ -155,13 +158,14 @@ class WaterQualityIndexCalculator:
raw.name = name
return raw
def calculate_many(self, names: List[str], df: pd.DataFrame) -> pd.DataFrame:
def calculate_many(self, names: List[str], df: pd.DataFrame, wavelength_offset: float = 0.0) -> pd.DataFrame:
"""
批量计算多个水质指数。
Args:
names: 公式名称列表
df: 光谱反射率 DataFrame
wavelength_offset: 波长偏移修正量(nm)
Returns:
pd.DataFrame,每列对应一个公式的计算结果
@ -169,7 +173,7 @@ class WaterQualityIndexCalculator:
results = {}
for name in names:
try:
results[name] = self.calculate_one(name, df)
results[name] = self.calculate_one(name, df, wavelength_offset=wavelength_offset)
except Exception as e:
print(f"⚠️ 计算 {name} 失败: {e}")
results[name] = pd.Series(np.nan, index=df.index, name=name)