【高光谱数据处理实战】基于Python的ENVI图像交互式裁剪与光谱数据预处理

在处理高光谱数据(Hyperspectral Imaging, HSI)时,我们常常需要面对两个核心问题:一是如何从庞大的三维数据立方体(Data Cube)中高效、准确地提取感兴趣区域(ROI);二是如何对提取出的一维光谱数据进行科学的预处理,以消除噪声和基线漂移。

本文将分享两个非常实用的 Python 脚本,分别解决上述两个痛点,涵盖了从交互式图像裁剪到一维光谱数据预处理的完整工作流。

在开始之前,请确保你的 Python 环境中安装了以下库:

python 复制代码
pip install opencv-python spectral numpy pandas scipy openpyxl

第一部分:基于 OpenCV 的 ENVI 图像交互式中心点裁剪

高光谱图像通常体积庞大(RAW + HDR 格式)。第一段代码实现了一个带有基础 GUI 的工具,它可以弹出文件夹选择框批量读取 RAW 图像,通过 OpenCV 提取单波段进行灰度显示,允许用户鼠标点击图像上的目标中心点,随后自动裁剪出一个 像素、包含全部波段的区域,并保存为轻量化的 格式。

代码亮点:

  • 交互式选点 :利用cv2.setMouseCallback实现鼠标点击获取坐标,所见即所得。

  • 逐波段读取(防内存溢出) :没有直接将几 GB 的 RAW 文件全部读入内存,而是通过img.read_band()配合循环逐波段切片,极大地降低了内存占用。

  • 进度预览 :裁剪完成后自动计算各波段均值,生成 .jpg预览图。

python 复制代码
import cv2 as cv
import spectral.io.envi as envi
import numpy as np
import os
from tkinter import filedialog, Tk
import spectral

# 设置 ENVI 配置支持非小写的参数
spectral.settings.envi_support_nonlowercase_params = True

# 创建Tkinter窗口并隐藏,用于优雅地调用文件选择对话框
root = Tk()
root.withdraw()

# 弹出对话框选择包含数据文件的文件夹路径
folder_path = filedialog.askdirectory(title='选择输入文件夹(包含RAW和HDR文件)')
out_path = filedialog.askdirectory(title='选择输出文件夹')

os.makedirs(out_path, exist_ok=True)

print("=" * 50)
print(f"输入文件夹: {folder_path}")
print(f"输出文件夹: {out_path}")
print("=" * 50)

# 鼠标回调函数:手动标记中心点
def click_event(event, x, y, flags, param):
    global marked_point, image_raw_display, point_marked
    if event == cv.EVENT_LBUTTONDOWN and not point_marked:
        # 记录坐标(注意 OpenCV 和 numpy 的 x,y 对应关系)
        marked_point = (y, x) 
        point_marked = True
        print(f"标记点:({y}, {x})") 

        # 在原图像上绘制红色标记点
        cv.circle(image_raw_display, (x, y), 5, (0, 0, 255), -1) 
        cv.imshow('Original Image with Marked Points', image_raw_display)
        cv.waitKey(300)
        cv.destroyWindow('Original Image with Marked Points')

# 统计总文件数
raw_files = [f for f in os.listdir(folder_path) if f.endswith('.raw')]
print(f"找到 {len(raw_files)} 个RAW文件")

# 遍历处理
for idx, filename in enumerate(raw_files, 1):
    print(f"\n[{idx}/{len(raw_files)}] 处理文件: {filename}")

    raw_file_path = os.path.join(folder_path, filename)
    name = filename[:-4]
    hdr_file_path = os.path.join(folder_path, filename[:-4] + '.hdr')

    if os.path.isfile(hdr_file_path):
        img = envi.open(hdr_file_path, raw_file_path)
        x, y = img.shape[:2]
        z = img.nbands
        print(f"  原始尺寸: {x} x {y} x {z}")

        # 生成预览图以供选点
        preview_band = min(50, z - 1) 
        image_data = img.read_band(preview_band)
        image_raw_display = np.uint8(image_data / np.max(image_data) * 255)

        marked_point = None
        point_marked = False

        cv.imshow('Original Image with Marked Points', image_raw_display)
        cv.setMouseCallback('Original Image with Marked Points', click_event)
        print("  请在弹出的图像上点击目标中心点...")

        start_time = cv.getTickCount()
        while not point_marked:
            elapsed_time = (cv.getTickCount() - start_time) / cv.getTickFrequency()
            if elapsed_time > 30: # 30秒超时机制
                print("  30秒内未标记点,跳过该图像。")
                cv.destroyWindow('Original Image with Marked Points')
                break
            if cv.waitKey(10) == 27:  # ESC键退出
                print("  用户取消标记,跳过该图像。")
                cv.destroyWindow('Original Image with Marked Points')
                break

        if not marked_point:
            print("  未标记点,跳过")
            continue

        center_x, center_y = marked_point
        crop_size = 500 # 设定裁剪大小

        # 计算裁剪边界,防止越界
        start_x = max(center_x - crop_size // 2, 0)
        start_y = max(center_y - crop_size // 2, 0)
        end_x = min(center_x + crop_size // 2, x)
        end_y = min(center_y + crop_size // 2, y)

        band_start = 0
        band_end = min(616, z)

        # 初始化空数组存储裁剪数据
        cropped_image = np.zeros((end_x - start_x, end_y - start_y, band_end - band_start), dtype=np.float32)

        # 重点:逐波段读取,避免内存溢出
        for band in range(band_start, band_end):
            band_data = img.read_band(band)[start_x:end_x, start_y:end_y]
            cropped_image[:, :, band - band_start] = band_data

        print(f"  裁剪后尺寸: {cropped_image.shape}")

        # 保存为 .npy 格式
        output_npy_path = os.path.join(out_path, name + '.npy')
        np.save(output_npy_path, cropped_image)

        # 生成均值灰度预览图
        mean_image = np.mean(cropped_image, axis=2)
        mean_image = (mean_image / mean_image.max() * 255).astype(np.uint8)
        cv.imwrite(os.path.join(out_path, name + '.jpg'), mean_image)
        print(f"  ✓ 完成")
    else:
        print(f"  ⚠ 缺少HDR文件,跳过")

print("\n" + "=" * 50)
print("所有文件处理完成!")
print("=" * 50)

第二部分:一维光谱数据的预处理流水线

  • 把高光谱图像整理成一维的光谱特征(例如提取平均光谱保存到 Excel 中)后,往往需要进行数学预处理以用于后续的机器学习/深度学习建模。第二段代码构建了一个高度可定制的预处理流水线。 可以通过简单地修改一个列表 ,让数据依次经过 SG平滑 -> SNV -> 一阶导数choices = [3, 1, 4]等操作,并自动根据处理流程重命名输出文件。

支持的预处理算法:

SNV (标准正态变换):消除表面散射影响。

**MSC(多元散射校正):**校正由于样品颗粒大小不同造成的散射。

Savitzky-Golay (SG) 平滑:去除高频噪声,保留光谱特征。

一阶/二阶导数:突出光谱吸收峰特征,消除基线平移。

Detrend (去趋势):消除由于仪器发热等引起的线性基线漂移。

python 复制代码
import pandas as pd
import numpy as np
from scipy.signal import savgol_filter, detrend
import os

# ==================== 预处理方法定义 ====================

def snv(data):
    """标准正态变换 (SNV)"""
    mean = np.mean(data, axis=1, keepdims=True)
    std = np.std(data, axis=1, keepdims=True)
    std[std == 0] = 1e-8
    return (data - mean) / std

def msc(data):
    """多元散射校正 (MSC)"""
    n_samples, n_features = data.shape
    ref_spectrum = np.mean(data, axis=0)
    data_msc = np.zeros_like(data)
    for i in range(n_samples):
        fit = np.polyfit(ref_spectrum, data[i, :], 1, full=True)
        k, b = fit[0][0], fit[0][1]
        data_msc[i, :] = (data[i, :] - b) / k
    return data_msc

def sg_smooth(data, window_length=15, polyorder=2):
    """Savitzky-Golay 平滑"""
    return savgol_filter(data, window_length=window_length, polyorder=polyorder, deriv=0, axis=1)

def diff_1st(data):
    """一阶导数 (直接差分计算)"""
    return np.gradient(data, axis=1)

def diff_2nd(data):
    """二阶导数 (直接差分计算)"""
    return np.gradient(np.gradient(data, axis=1), axis=1)

def detrend_data(data):
    """去趋势 (消除基线漂移)"""
    return detrend(data, axis=1, type='linear')


# ==================== 主程序逻辑 ====================

def main(input_file, choices):
    output_dir = os.path.dirname(input_file)
    base_name = os.path.basename(input_file)

    print(f"正在读取数据: {input_file} ...")
    try:
        df = pd.read_excel(input_file)
    except Exception as e:
        print(f"读取文件失败,请检查路径。错误信息: {e}")
        return

    # 分离元数据(前两列)和光谱数据(后续列)
    meta_data = df.iloc[:, :2]
    spectra_data = df.iloc[:, 2:].values
    band_names = df.columns[2:]

    print("正在进行光谱预处理...")

    # 构建操作映射字典 (函数, 缩写前缀, 描述)
    operations = {
        1: (snv, "SNV", "SNV (标准正态变换)"),
        2: (msc, "MSC", "MSC (多元散射校正)"),
        3: (lambda d: sg_smooth(d, window_length=15, polyorder=2), "SG", "SG 平滑"),
        4: (diff_1st, "1stDeriv", "一阶导数"),
        5: (diff_2nd, "2ndDeriv", "二阶导数"),
        8: (detrend_data, "Detrend", "去趋势 (消除基线漂移)")
    }

    if not choices or 7 in choices:
        processed_spectra = spectra_data
        final_prefix = "RAW"
        print("-> 正在应用: Raw (保持原始数据不作处理)")
    else:
        processed_spectra = spectra_data.copy()
        prefixes = []

        # 兼容旧版本的组合逻辑
        if 6 in choices:
            choices.remove(6)
            choices.extend([3, 1])  # SG -> SNV

        # 流水线执行
        for c in choices:
            if c in operations:
                func, prefix_name, desc = operations[c]
                print(f"-> 正在应用: {desc}")
                processed_spectra = func(processed_spectra)
                prefixes.append(prefix_name)
            else:
                print(f"! 警告: 选项 {c} 无效,已跳过。")

        final_prefix = "_".join(prefixes) if prefixes else "RAW"

    # 动态生成输出文件名
    if "RAW" in base_name:
        output_name = base_name.replace("RAW", final_prefix)
    else:
        output_name = f"{final_prefix}_{base_name}"

    # 重组数据并保存
    df_processed_spectra = pd.DataFrame(processed_spectra, columns=band_names)
    df_final = pd.concat([meta_data, df_processed_spectra], axis=1)

    output_path = os.path.join(output_dir, output_name)
    print(f"正在保存处理后的数据至: {output_path} ...")
    df_final.to_excel(output_path, index=False)
    print("处理完成!")

if __name__ == "__main__":
    # ==================== 配置区 ====================
    input_file = r"E:\GouQi\data\RAW_GLS.xlsx"

    # 选择预处理方法 (支持列表组合,按顺序执行)
    # [3]       表示只做 SG平滑
    # [3, 4]    表示先做 SG平滑,再做 一阶导数
    # [3, 1, 4] 表示先SG平滑,再SNV,再一阶导数
    choices = [3, 1, 8]  # <--- 在这里传入包含步骤序号的列表
    # ================================================

    main(input_file, choices)

为了直观检验预处理的效果,我们往往需要绘制光谱曲线。以下代码可以随机抽取几个样本,对比原始光谱(RAW)和经过 SG+SNV 处理后的光谱
*

python 复制代码
import matplotlib.pyplot as plt

def plot_spectra_comparison(raw_data, processed_data, band_names):
    plt.figure(figsize=(12, 5))

    # 绘制原始光谱
    plt.subplot(1, 2, 1)
    plt.plot(band_names, raw_data.T, alpha=0.3)
    plt.title("原始光谱 (RAW)")
    plt.xlabel("Wavelength (nm)")
    plt.ylabel("Intensity")

    # 绘制处理后光谱
    plt.subplot(1, 2, 2)
    plt.plot(band_names, processed_data.T, alpha=0.3)
    plt.title("预处理后 (例如 SG + SNV)")
    plt.xlabel("Wavelength (nm)")
    plt.ylabel("Normalized Intensity")

    plt.tight_layout()
    plt.show()

总结

这两段代码组合起来,几乎涵盖了实验室采集高光谱数据后到进行模型训练前的核心数据清洗步骤。 特别是内存友好的逐波段切片基于字典的预处理流水线设计,不仅提高了代码的执行效率,也极大增强了可读性和可扩展性。

如果这篇内容对你的课题或项目有帮助,欢迎 点赞 + 收藏!有任何问题或优化建议,也欢迎在评论区留言讨论哦!

相关推荐
2301_787312432 小时前
SQL视图与存储过程有何区别_架构设计中的选择策略
jvm·数据库·python
Dxy12393102162 小时前
Python如何处理树状分类数据
大数据·python·分类
a7963lin2 小时前
C# 文件系统Filter Hook C#能否在用户模式下拦截文件系统调用
jvm·数据库·python
a7963lin2 小时前
如何在 Tkinter 网格中动态增删行
jvm·数据库·python
运气好好的2 小时前
CSS如何实现响应式表单项对齐_利用Flexbox按比例分配宽度
jvm·数据库·python
用户8356290780512 小时前
Python 操作 PowerPoint 表格的创建与格式化
后端·python
IT猿手2 小时前
光伏模型参数估计:基于山羊优化算法(GOA )的光伏模型参数辨识问题求解研究,免费提供完整MATLAB代码链接
开发语言·算法·matlab·群智能优化算法·智能优化算法·光伏模型参数估计·光伏模型参数辨识
forestqq2 小时前
基于openeuler2403sp3的容器,打包django运行环境镜像
后端·python·django
2501_901006472 小时前
Python大屏展示怎么做_Dash与Streamlit框架快速构建Web版数据看板
jvm·数据库·python