前言

继上一篇LRPR基础教程后,本文将深入探讨地形校正与高级信号分析技术。随着嫦娥五号、六号任务数据的不断积累,对这些数据进行高级处理和分析变得愈发重要。地形校正能有效消除地表起伏对雷达数据的影响,而希尔伯特变换、相位分析等高级技术则能从原始数据中提取更多有价值的地质信息。本教程将系统讲解这些技术的原理和实现方法,并提供实际应用案例。

1. 地形校正原理与实现

1.1 地形校正的必要性

月球地表并非完全平坦,存在各种地形起伏。这些起伏会导致雷达反射界面在时间剖面上呈现"起伏状",干扰对地下结构的判断。地形校正的目的是消除地表地形起伏的影响,使地下反射界面在处理后的剖面上能更加接近其真实形态。

地形校正的主要优势有:

  1. 消除地表起伏引起的"假"地层结构
  2. 提高深层反射事件的连续性
  3. 便于准确估计地下界面的深度
  4. 改善后续处理(如迁移成像)的效果

1.2 地形校正算法

地形校正的核心思想是将各道数据沿时间轴(或深度轴)进行移动,使地表反射事件对齐到同一水平线上。主要步骤包括:

  1. 检测每道数据中的地表反射位置(底部位置)
  2. 确定参考水平面(通常为平均地表高度)
  3. 计算每道需要移动的时间/样本量
  4. 执行数据移动和填充操作

1.3 底部位置检测

在实施地形校正前,首先需要准确检测每道数据中的地表反射(底部)位置。常用方法有:

  1. 基于阈值的检测:设定振幅阈值,寻找超过该阈值的最深位置
  2. 基于能量的检测:通过计算信号能量分布,确定能量突变点
  3. 基于相关性的检测:利用道间相关性,确保检测结果的连续性

以下是基于阈值的底部位置检测算法:

def calculate_bottom_positions_optimized(data, threshold=0.00001):
    """
    优化后的计算每道底部位置的函数。
    
    参数:
    - data (ndarray): 地形校正前的数据,形状为 (num_traces, num_samples)
    - threshold (float): 判断底部的阈值
    
    返回:
    - bottom_positions (ndarray): 每道的底部位置索引,形状为 (num_traces,)
    """
    # 创建掩码,标记数据中大于阈值的值
    data = np.abs(data)
    mask = data > threshold  # 形状: (num_traces, num_samples)
    
    # 生成每个样本点的索引
    sample_indices = np.arange(data.shape[1])
    
    # 使用广播将样本索引与掩码结合
    # 对于每个Trace,未满足条件的位置设为-1
    masked_indices = np.where(mask, sample_indices, -1)
    
    # 找到每个Trace中最后一个满足条件的位置
    last_true_indices = masked_indices.max(axis=1)  # 形状: (num_traces,)
    
    # 计算底部位置
    bottom_positions = last_true_indices  # 因为索引从0开始
    
    # 如果没有任何值大于阈值,则底部位置设为0
    bottom_positions[last_true_indices == -1] = 0
    
    return bottom_positions

1.4 校正实现代码

以下是地形校正的核心实现代码:

def terrain_correct(data, bottom_positions):
    """
    进行地形校正,使每道数据的底部位置对齐。
    
    参数:
    - data (ndarray): 原始数据,形状为 (num_traces, num_samples)
    - bottom_positions (ndarray): 每道的底部位置索引,形状为 (num_traces,)
    
    返回:
    - corrected_data (ndarray): 校正后的数据,形状与输入相同
    """
    num_traces, num_samples = data.shape
    corrected_data = np.full_like(data, fill_value=0)
    
    # 确定参考位置(平均底部位置)
    reference_position = np.mean(bottom_positions)
    reference_position = int(reference_position)
    
    # 计算每道需要移动的采样点数
    shifts = reference_position - bottom_positions
    
    # 应用移动
    for i in range(num_traces):
        shift = shifts[i]
        if shift > 0:  # 向下移动
            # 使用roll函数移动数据
            rolled = np.roll(data[i], shift)
            # 将移入的部分填充为0
            rolled[:shift] = 0
            corrected_data[i] = rolled
        elif shift < 0:  # 向上移动
            # 使用roll函数移动数据
            rolled = np.roll(data[i], shift)
            # 将移入的部分填充为0
            rolled[shift:] = 0
            corrected_data[i] = rolled
        else:  # 不需要移动
            corrected_data[i] = data[i]
    
    return corrected_data

在实际应用中,地形校正可能需要针对特定地形特点进行参数调整。此外,为了改善校正效果,有时会将预设的移动量应用于数据,而不是自动检测底部位置。这在以下代码中有所体现:

def terrain_correct_with_manual_shifts(data):
    num_traces, num_samples = data.shape
    corrected_data = np.full_like(data, fill_value=0)
    
    # 预设的移动量(通常基于先验知识或前期分析结果)
    shifts = [...]
     
    for i in range(num_traces):
        shift = shifts[i] if i < len(shifts) else 0
        if shift > 0:
            rolled = np.roll(data[i], -shift)
            rolled[-shift:] = 0
            corrected_data[i] = rolled
        else:
            corrected_data[i] = data[i]
            
    return corrected_data

在处理完成后,可以使用可视化函数验证校正效果:

def visualize_data(data, bottom_positions, cmap='seismic', figsize=(15, 8)):
    """
    可视化地形校正后的数据,并在图像上绘制底部位置。
    
    参数:
    - data (ndarray): 地形校正后的数据,形状为 (num_traces, num_samples)
    - bottom_positions (ndarray): 每道的底部位置索引,形状为 (num_traces,)
    - cmap (str): 颜色映射
    - figsize (tuple): 图像尺寸
    """
    data_transposed = data.T  # 转置为 (num_samples, num_traces)

    plt.figure(figsize=figsize)

    # 使用imshow绘制数据
    img = plt.imshow(data_transposed, aspect='auto', cmap=cmap)
    plt.colorbar(img, label='振幅')  # 根据数据调整标签

    # 绘制底部位置
    traces = np.arange(data.shape[0])
    plt.plot(traces, bottom_positions, color='yellow', linewidth=1, label='底部')

    # 添加标签和标题
    plt.xlabel('道号 (Traces)')
    plt.ylabel('样本索引 (Samples)')
    plt.title('地形校正后的数据及底部位置')
    plt.legend()

    plt.show()

2. 零时校正技术

2.1 零时点定义与意义

零时点(zero time)是指雷达波开始传播的时刻。在理想情况下,雷达数据的零时点应该对应于电磁波从发射天线发出的那一刻。然而,由于设备延迟、系统抖动、触发时间等因素,实际数据中的零时点可能存在偏移,导致深度估计误差。

零时校正的目标是确定真实的零时点,并将数据进行适当移动,使波形起点对应于电磁波真实的发射时刻。这对于准确估计地下界面深度至关重要。

2.2 基于能量的零时点检测

最常用的零时点检测方法是基于信号能量或振幅的突变点。通常,在零时点附近,信号会出现明显的能量变化,表示电磁波开始传播。以下是一种基于能量的零时点检测方法:

def detect_zero_time_by_energy(data, window_size=20, threshold_factor=0.3):
    """
    基于能量变化检测零时点
    
    参数:
    - data: 输入数据,形状为(traces, samples)
    - window_size: 计算能量的窗口大小
    - threshold_factor: 能量阈值因子
    
    返回:
    - zero_times: 每道的零时点位置
    """
    num_traces, num_samples = data.shape
    zero_times = np.zeros(num_traces, dtype=int)
    
    for i in range(num_traces):
        trace = data[i, :]
        
        # 计算移动窗口能量
        energy = np.zeros(num_samples - window_size)
        for j in range(len(energy)):
            energy[j] = np.sum(trace[j:j+window_size]**2)
        
        # 找到能量超过阈值的第一个点
        max_energy = np.max(energy)
        threshold = max_energy * threshold_factor
        for j in range(len(energy)):
            if energy[j] > threshold:
                zero_times[i] = j
                break
    
    return zero_times

2.3 希尔伯特变换辅助零时校正

希尔伯特变换为零时点检测提供了更强大的工具,因为它能够转换为信号的包络,更容易检测信号的起始点。以下是基于希尔伯特变换的零时校正方法:

def zero_time_correction_with_hilbert(data, startPoint=None, endPoint=None, window=100):
    """
    使用希尔伯特变换进行零时校正
    
    参数:
    - data: 输入数据,形状为(traces, samples)
    - startPoint: 分析的起始点
    - endPoint: 分析的终止点
    - window: 寻找谷点的窗口大小
    
    返回:
    - corrected_data: 校正后的数据
    - valley_time: 检测到的谷点位置
    """
    # 设置分析范围
    if startPoint is None:
        startPoint = 0
    if endPoint is None:
        endPoint = data.shape[1]
    
    data_slice = data[:, startPoint:endPoint]
    
    # 计算希尔伯特变换和包络
    analytic_signal = hilbert(data_slice, axis=1)
    envelope = np.abs(analytic_signal)
    
    # 计算平均包络
    mean_envelope = np.mean(envelope, axis=0)
    
    # 找到最大能量点
    max_energy_time = np.argmax(mean_envelope)
    
    # 在最大能量点之前寻找谷点
    valley_time = find_valley_before_peak(mean_envelope, max_energy_time, window)
    
    # 应用校正
    corrected_data = np.zeros_like(data)
    for i in range(data.shape[0]):
        # 执行数据移位
        rolled_data = np.roll(data[i], -valley_time)
        # 对移位后末尾的无效数据置零
        rolled_data[-valley_time:] = 0
        corrected_data[i] = rolled_data
        
    return corrected_data, valley_time

def find_valley_before_peak(signal, peak_idx, window=100):
    """
    在峰值前寻找谷点
    
    参数:
    - signal: 信号数组
    - peak_idx: 峰值位置
    - window: 搜索窗口大小
    
    返回:
    - valley_idx: 谷点位置
    """
    start_idx = max(0, peak_idx - window)
    search_region = signal[start_idx:peak_idx]
    
    if len(search_region) == 0:
        return start_idx
    
    valley_idx = start_idx + np.argmin(search_region)
    return valley_idx

2.4 逐道零时校正算法

在某些应用中,每道数据的零时点可能不同,需要对每道单独进行校正。以下是逐道零时校正的算法:

def zero_time_calibration_per_trace(data):
    """
    对每条迹线单独进行零时校正。
    
    参数:
        data (numpy.ndarray): 输入数据,形状为 (traces, samples)
        
    返回:
        corrected_data (numpy.ndarray): 校正后的数据,形状与输入相同
        zero_time_indices (list): 每条迹线的零时刻位置索引
    """
    traces, samples = data.shape
    corrected_data = np.zeros_like(data)
    zero_time_indices = []

    center_index = samples // 2

    for trace_idx in range(traces):
        # 使用希尔伯特变换计算包络
        envelope = np.abs(hilbert(data[trace_idx, :]))
        
        # 找到包络的最大值位置作为零时点
        zero_time_index = np.argmax(envelope)
        zero_time_indices.append(zero_time_index)

        # 计算需要移动的点数,使零时点移动到中心位置
        shift = center_index - zero_time_index

        # 应用移动
        if shift > 0:
            corrected_data[trace_idx, shift:] = data[trace_idx, :samples - shift]
            corrected_data[trace_idx, :shift] = 0
        elif shift < 0:
            corrected_data[trace_idx, :shift] = 0
            corrected_data[trace_idx, shift:] = data[trace_idx, -shift:]
        else:
            corrected_data[trace_idx, :] = data[trace_idx, :]

    return corrected_data, zero_time_indices

3. 希尔伯特变换与瞬时属性

3.1 希尔伯特变换基本原理

希尔伯特变换是信号处理中的重要工具,它在时域中对输入信号进行90度相移。希尔伯特变换的数学定义为:

H [ x ( t ) ] = 1 π ∫ − ∞ ∞ x ( τ ) t − τ d τ H[x(t)] = \frac{1}{\pi} \int_{-\infty}^{\infty} \frac{x(\tau)}{t - \tau} d\tau H[x(t)]=π1tτx(τ)dτ

在离散信号处理中,通常通过频域计算实现希尔伯特变换:

  1. 对信号进行傅里叶变换
  2. 将负频率分量乘以-j,正频率分量乘以j
  3. 进行逆傅里叶变换得到变换结果

3.2 解析信号与瞬时属性

希尔伯特变换最常用的应用是构建解析信号。解析信号是一个复数信号,由原始信号作为实部,希尔伯特变换结果作为虚部:

z ( t ) = x ( t ) + j ⋅ H [ x ( t ) ] z(t) = x(t) + j \cdot H[x(t)] z(t)=x(t)+jH[x(t)]

解析信号有一个重要特性:它的频谱在负频率区间为零,这使得我们可以方便地计算信号的瞬时属性。

3.3 瞬时振幅计算

瞬时振幅(也称为信号包络)是解析信号模的计算结果:

A ( t ) = ∣ z ( t ) ∣ = x ( t ) 2 + H [ x ( t ) ] 2 A(t) = |z(t)| = \sqrt{x(t)^2 + H[x(t)]^2} A(t)=z(t)=x(t)2+H[x(t)]2

以下是使用SciPy库计算瞬时振幅的代码:

def calculate_instantaneous_amplitude(data):
    """
    计算信号的瞬时振幅(包络)
    
    参数:
    - data: 输入数据,可以是一维数组或二维数组
    
    返回:
    - instantaneous_amplitude: 瞬时振幅,形状与输入相同
    """
    if data.ndim == 1:
        # 一维数据
        analytic_signal = hilbert(data)
        instantaneous_amplitude = np.abs(analytic_signal)
    else:
        # 二维数据,对每一道分别处理
        instantaneous_amplitude = np.zeros_like(data)
        for i in range(data.shape[0]):
            analytic_signal = hilbert(data[i])
            instantaneous_amplitude[i] = np.abs(analytic_signal)
    
    return instantaneous_amplitude

3.4 瞬时相位计算

瞬时相位是解析信号相角的计算结果:

ϕ ( t ) = arctan ⁡ ( H [ x ( t ) ] x ( t ) ) \phi(t) = \arctan\left(\frac{H[x(t)]}{x(t)}\right) ϕ(t)=arctan(x(t)H[x(t)])

以下是计算瞬时相位的代码:

def calculate_instantaneous_phase(data):
    """
    计算信号的瞬时相位
    
    参数:
    - data: 输入数据,可以是一维数组或二维数组
    
    返回:
    - instantaneous_phase: 瞬时相位(弧度),形状与输入相同
    """
    if data.ndim == 1:
        # 一维数据
        analytic_signal = hilbert(data)
        instantaneous_phase = np.angle(analytic_signal)
    else:
        # 二维数据,对每一道分别处理
        instantaneous_phase = np.zeros_like(data)
        for i in range(data.shape[0]):
            analytic_signal = hilbert(data[i])
            instantaneous_phase[i] = np.angle(analytic_signal)
    
    return instantaneous_phase

3.5 瞬时频率计算

瞬时频率是瞬时相位对时间的导数:

f ( t ) = 1 2 π d ϕ ( t ) d t f(t) = \frac{1}{2\pi} \frac{d\phi(t)}{dt} f(t)=2π1dtdϕ(t)

在离散信号中,通常使用相位差分来近似导数:

def calculate_instantaneous_frequency(data, dt):
    """
    计算信号的瞬时频率
    
    参数:
    - data: 输入数据,可以是一维数组或二维数组
    - dt: 采样时间间隔(秒)
    
    返回:
    - instantaneous_frequency: 瞬时频率(Hz),形状与输入相同
    """
    if data.ndim == 1:
        # 一维数据
        analytic_signal = hilbert(data)
        instantaneous_phase = np.angle(analytic_signal)
        instantaneous_frequency = np.zeros_like(instantaneous_phase)
        
        # 计算相位差分
        phase_diff = np.diff(instantaneous_phase)
        
        # 处理相位跳变(超过π的部分通过加减2π调整)
        phase_diff = np.where(phase_diff > np.pi, phase_diff - 2*np.pi, phase_diff)
        phase_diff = np.where(phase_diff < -np.pi, phase_diff + 2*np.pi, phase_diff)
        
        # 转换为频率
        instantaneous_frequency[1:] = phase_diff / (2*np.pi*dt)
        instantaneous_frequency[0] = instantaneous_frequency[1]  # 边界处理
    else:
        # 二维数据,对每一道分别处理
        instantaneous_frequency = np.zeros_like(data)
        for i in range(data.shape[0]):
            analytic_signal = hilbert(data[i])
            instantaneous_phase = np.angle(analytic_signal)
            
            # 计算相位差分
            phase_diff = np.diff(instantaneous_phase)
            
            # 处理相位跳变
            phase_diff = np.where(phase_diff > np.pi, phase_diff - 2*np.pi, phase_diff)
            phase_diff = np.where(phase_diff < -np.pi, phase_diff + 2*np.pi, phase_diff)
            
            # 转换为频率
            instantaneous_frequency[i, 1:] = phase_diff / (2*np.pi*dt)
            instantaneous_frequency[i, 0] = instantaneous_frequency[i, 1]  # 边界处理
    
    return instantaneous_frequency

4. 相位分析与瞬时频率计算

4.1 相位解缠绕技术

在计算瞬时相位时,得到的结果通常被限制在[-π, π]范围内,导致相位跳变。相位解缠绕是指移除这些2π跳变,使相位曲线连续的过程。以下是相位解缠绕的实现:

def unwrap_phase(phase):
    """
    对相位进行解缠绕处理
    
    参数:
    - phase: 输入相位数据(弧度),可以是一维或二维数组
    
    返回:
    - unwrapped_phase: 解缠绕后的相位
    """
    if phase.ndim == 1:
        # 一维数据
        unwrapped_phase = np.unwrap(phase)
    else:
        # 二维数据,对每一道分别处理
        unwrapped_phase = np.zeros_like(phase)
        for i in range(phase.shape[0]):
            unwrapped_phase[i] = np.unwrap(phase[i])
    
    return unwrapped_phase

4.2 相位属性的地质意义

相位信息在雷达数据解释中具有重要意义:

  1. 瞬时相位:与地层边界和连续性有关,有助于识别细微的地层变化
  2. 相位连续性:反映地层的连续性和断裂情况
  3. 相位反转:通常表示介电常数反转,有助于识别地下介质变化

在月球环境中,相位反转可能指示月壤和岩石界面,或不同类型月壤之间的过渡。

4.3 频率属性分析与可视化

频率属性分析是通过研究信号的频率变化来提取地质信息的方法。以下代码展示了如何分析并可视化瞬时频率属性:

def analyze_frequency_attributes(data, dt):
    """
    分析数据的频率属性并可视化
    
    参数:
    - data: 输入数据,形状为(traces, samples)
    - dt: 采样时间间隔(秒)
    """
    # 计算瞬时频率
    instantaneous_frequency = calculate_instantaneous_frequency(data, dt)
    
    # 计算频率统计特征
    mean_freq = np.mean(instantaneous_frequency, axis=1)
    std_freq = np.std(instantaneous_frequency, axis=1)
    
    # 可视化
    plt.figure(figsize=(15, 10))
    
    # 瞬时频率图
    plt.subplot(2, 1, 1)
    plt.imshow(instantaneous_frequency.T, aspect='auto', cmap='viridis')
    plt.colorbar(label='Frequency (Hz)')
    plt.title('Instantaneous Frequency')
    plt.xlabel('Trace Number')
    plt.ylabel('Sample Number')
    
    # 频率统计图
    plt.subplot(2, 1, 2)
    plt.plot(mean_freq, label='Mean Frequency')
    plt.fill_between(np.arange(len(mean_freq)), 
                     mean_freq - std_freq, 
                     mean_freq + std_freq, 
                     alpha=0.3, label='±1 Std Dev')
    plt.title('Frequency Statistics per Trace')
    plt.xlabel('Trace Number')
    plt.ylabel('Frequency (Hz)')
    plt.grid(True)
    plt.legend()
    
    plt.tight_layout()
    plt.show()

4.4 基于窗口的频谱分析

窗口频谱分析可以揭示信号随时间变化的频率特性。短时傅里叶变换(STFT)是一种常用的窗口频谱分析方法:

def calculate_stft(data, dt, nperseg=64, noverlap=48):
    """
    计算短时傅里叶变换
    
    参数:
    - data: 输入数据,一维数组
    - dt: 采样时间间隔(秒)
    - nperseg: 每个窗口的长度
    - noverlap: 窗口重叠的点数
    
    返回:
    - f: 频率数组
    - t: 时间数组
    - Zxx_db: STFT结果(dB)
    """
    fs = 1 / dt  # 采样频率
    f, t, Zxx = signal.stft(data, fs=fs,
                           nperseg=nperseg,
                           noverlap=noverlap,
                           window='hann',
                           boundary=None)

    # 转换为dB
    Zxx_db = 20 * np.log10(np.abs(Zxx) + 1e-16)

    return f, t, Zxx_db

5. 信号包络提取与振幅归一化

5.1 包络提取方法比较

信号包络提取是一种重要的处理方法,它可以突出显示信号的能量变化。常用的包络提取方法包括:

  1. 希尔伯特变换法:基于希尔伯特变换构建解析信号,然后计算其振幅
  2. 峰值插值法:识别信号的局部极大值,然后通过插值连接这些极大值
  3. 均方根包络:使用滑动窗口计算信号的均方根值

以下代码比较了不同的包络提取方法:

def compare_envelope_methods(signal, method='all'):
    """
    比较不同的包络提取方法
    
    参数:
    - signal: 输入信号,一维数组
    - method: 要计算的包络方法,'all'表示所有方法
    
    返回:
    - envelopes: 包含不同方法的包络结果的字典
    """
    envelopes = {}
    
    if method in ['hilbert', 'all']:
        # 希尔伯特变换法
        analytic_signal = hilbert(signal)
        envelopes['hilbert'] = np.abs(analytic_signal)
    
    if method in ['peak', 'all']:
        # 峰值插值法
        envelopes['peak'] = envelope_peak_interp(signal, kind='cubic')
    
    if method in ['rms', 'all']:
        # 均方根包络
        window_size = min(51, len(signal)//10)  # 自适应窗口大小
        window_size = window_size + 1 if window_size % 2 == 0 else window_size  # 确保窗口大小为奇数
        envelopes['rms'] = envelope_rms(signal, window_size)
    
    return envelopes

def plot_envelope_comparison(signal, envelopes):
    """
    绘制不同包络提取方法的比较图
    
    参数:
    - signal: 原始信号
    - envelopes: 包含不同方法的包络结果的字典
    """
    plt.figure(figsize=(12, 8))
    
    # 绘制原始信号
    plt.plot(signal, 'b-', label='Original Signal', alpha=0.6)
    
    # 绘制各种包络
    colors = ['r-', 'g-', 'm-']
    for i, (method, envelope) in enumerate(envelopes.items()):
        plt.plot(envelope, colors[i], label=f'{method.capitalize()} Envelope', linewidth=2)
    
    plt.title('Envelope Extraction Method Comparison')
    plt.xlabel('Sample')
    plt.ylabel('Amplitude')
    plt.legend()
    plt.grid(True)
    plt.show()

5.2 峰值插值法

峰值插值法是一种简单而有效的包络提取方法,特别适用于振荡信号。以下是其实现:

def envelope_peak_interp(data, kind='linear'):
    """
    使用峰值插值法计算包络线
    
    参数:
    data (np.ndarray): 输入数据
    kind (str): 插值方法 ('linear', 'cubic', 'quadratic')
    
    返回:
    np.ndarray: 计算得到的包络线
    """
    from scipy.interpolate import interp1d
    
    # 找到所有局部最大值
    peaks = []
    peak_indices = []
    
    for i in range(1, len(data) - 1):
        if data[i] > data[i - 1] and data[i] > data[i + 1]:
            peaks.append(abs(data[i]))
            peak_indices.append(i)
    
    # 确保包含起点和终点
    if len(peak_indices) < 2:
        return np.abs(data)  # 如果找不到足够的峰值,返回绝对值
    
    # 添加起点和终点
    peaks.insert(0, abs(data[0]))
    peak_indices.insert(0, 0)
    peaks.append(abs(data[-1]))
    peak_indices.append(len(data) - 1)
    
    # 使用插值生成包络线
    f = interp1d(peak_indices, peaks, kind=kind, fill_value="extrapolate")
    envelope = f(np.arange(len(data)))
    
    return envelope

5.3 振幅归一化技术

振幅归一化是将数据缩放到特定范围(如[-1, 1])的过程,有助于不同数据集的比较和处理。以下是振幅归一化的代码实现:

def amplitude_normalization(data):
    """
    对输入的二维数据进行振幅归一化,并使用 imshow 进行可视化展示。
    
    参数:
    ----------
    data : np.ndarray
        输入数据,形状应为 (num_traces, num_samples),且 num_traces < num_samples。
        
    返回:
    ----------
    normalized_data : np.ndarray
        归一化后的数据,形状与输入相同。
    """
    # 输入验证
    if not isinstance(data, np.ndarray):
        raise TypeError("输入数据必须是一个 numpy.ndarray 对象。")
    
    if data.ndim != 2:
        raise ValueError(f"输入数据必须是二维数组,但当前维度为 {data.ndim}。")
    
    num_traces, num_samples = data.shape
    if num_traces >= num_samples:
        print(f"警告:输入数据的形状可能需调整,当前 num_traces={num_traces} >= num_samples={num_samples}。")
    
    # 振幅归一化
    max_vals = np.max(np.abs(data), axis=1).reshape(-1, 1)
    max_vals[max_vals == 0] = 1  # 避免除以零
    normalized_data = data / max_vals
    
    return normalized_data

5.4 道间均衡处理

道间均衡是一种使不同道的振幅水平相似的处理方法,有助于减少采集条件变化导致的振幅差异:

def trace_equalization(data, method='rms', window=None):
    """
    对数据进行道间均衡处理
    
    参数:
    - data: 输入数据,形状为(traces, samples)
    - method: 均衡方法,'rms'或'max'
    - window: 用于计算均衡因子的时间窗口 (start, end),默认使用全部数据
    
    返回:
    - equalized_data: 均衡后的数据
    """
    ntraces, nsamples = data.shape
    
    if window is None:
        window = (0, nsamples)
    
    start, end = window
    window_data = data[:, start:end]
    
    if method == 'rms':
        # 使用均方根均衡
        factors = np.sqrt(np.mean(window_data ** 2, axis=1))
    elif method == 'max':
        # 使用最大值均衡
        factors = np.max(np.abs(window_data), axis=1)
    else:
        raise ValueError("方法必须是 'rms' 或 'max'")
    
    # 处理零因子
    factors = np.where(factors > 0, factors, 1)
    
    # 应用均衡因子
    equalized_data = data / factors[:, np.newaxis]
    
    return equalized_data

6. GPR信号综合分析方法

6.1 时域-频域联合分析

时域-频域联合分析是同时研究信号的时域特性和频域特性的方法,能够提供更全面的信号理解。以下是一个实现例子:

def time_frequency_analysis(trace, dt):
    """
    对单道数据进行时域-频域联合分析
    
    参数:
    - trace: 输入的单道数据
    - dt: 采样时间间隔(秒)
    """
    # 计算希尔伯特变换及相关属性
    analytic_signal = hilbert(trace)
    envelope = np.abs(analytic_signal)
    instantaneous_phase = np.angle(analytic_signal)
    unwrapped_phase = np.unwrap(instantaneous_phase)
    
    # 计算瞬时频率
    inst_freq = np.diff(unwrapped_phase) / (2*np.pi*dt)
    inst_freq = np.append(inst_freq, inst_freq[-1])  # 补充边界点
    
    # 计算FFT
    n = len(trace)
    freqs = np.fft.rfftfreq(n, dt)
    fft_mag = np.abs(np.fft.rfft(trace))
    
    # 计算STFT
    f_stft, t_stft, Zxx_db = calculate_stft(trace, dt)
    
    # 可视化
    fig = plt.figure(figsize=(15, 12))
    
    # 时域信号
    ax1 = fig.add_subplot(3, 2, 1)
    ax1.plot(trace)
    ax1.plot(envelope, 'r-', linewidth=1.5)
    ax1.set_title('Time Domain Signal & Envelope')
    ax1.set_xlabel('Sample')
    ax1.set_ylabel('Amplitude')
    ax1.grid(True)
    
    # 相位
    ax2 = fig.add_subplot(3, 2, 2)
    ax2.plot(unwrapped_phase)
    ax2.set_title('Unwrapped Phase')
    ax2.set_xlabel('Sample')
    ax2.set_ylabel('Phase (rad)')
    ax2.grid(True)
    
    # 瞬时频率
    ax3 = fig.add_subplot(3, 2, 3)
    ax3.plot(inst_freq / 1e6)  # 转换为MHz
    ax3.set_title('Instantaneous Frequency')
    ax3.set_xlabel('Sample')
    ax3.set_ylabel('Frequency (MHz)')
    ax3.grid(True)
    
    # FFT频谱
    ax4 = fig.add_subplot(3, 2, 4)
    ax4.plot(freqs / 1e6, fft_mag)  # 转换为MHz
    ax4.set_title('Frequency Spectrum')
    ax4.set_xlabel('Frequency (MHz)')
    ax4.set_ylabel('Magnitude')
    ax4.grid(True)
    
    # STFT时频图
    ax5 = fig.add_subplot(3, 1, 3)
    im = ax5.pcolormesh(t_stft, f_stft / 1e6, Zxx_db, shading='gouraud', cmap='viridis')
    ax5.set_title('Short-Time Fourier Transform')
    ax5.set_xlabel('Time (s)')
    ax5.set_ylabel('Frequency (MHz)')
    fig.colorbar(im, ax=ax5, label='Magnitude (dB)')
    
    plt.tight_layout()
    plt.show()

6.2 短时傅里叶变换(STFT)

短时傅里叶变换通过对信号应用一系列窗口,然后对每个窗口内的数据进行傅里叶变换,从而生成时频表示。以下是更详细的STFT实现:

def stft_analysis(data, dt, window_type='hann', nperseg=64, noverlap=48, nfft=None):
    """
    对数据进行短时傅里叶变换分析
    
    参数:
    - data: 输入数据,可以是一维或二维数组
    - dt: 采样时间间隔(秒)
    - window_type: 窗口类型,如'hann', 'hamming', 'blackman'等
    - nperseg: 每个段的长度
    - noverlap: 段之间的重叠点数
    - nfft: FFT的长度,默认为None(自动选择)
    
    返回:
    - 如果输入是一维数据,返回 (f, t, Zxx)
    - 如果输入是二维数据,返回包含每个道STFT结果的列表
    """
    fs = 1 / dt  # 采样频率
    
    if data.ndim == 1:
        # 一维数据
        f, t, Zxx = signal.stft(data, fs=fs,
                               window=window_type,
                               nperseg=nperseg,
                               noverlap=noverlap,
                               nfft=nfft,
                               boundary=None)
        return f, t, Zxx
    else:
        # 二维数据,对每一道分别处理
        results = []
        for i in range(data.shape[0]):
            f, t, Zxx = signal.stft(data[i], fs=fs,
                                  window=window_type,
                                  nperseg=nperseg,
                                  noverlap=noverlap,
                                  nfft=nfft,
                                  boundary=None)
            results.append((f, t, Zxx))
        return results

6.3 质心频率分析

质心频率是频谱能量分布的加权平均频率,可以用来研究信号的频率变化趋势。对于月球雷达数据,质心频率的下降通常与介质损耗有关:

def cal_center_freq_stft(data_input, dt, overlop_time=0.76, nfft=8192):
    """
    计算STFT的质心频率
    
    参数:
    - data_input: 输入数据,一维数组
    - dt: 采样时间间隔(秒)
    - overlop_time: 重叠时间(纳秒)
    - nfft: FFT的长度
    
    返回:
    - t: 时间数组
    - center_freq: 质心频率数组
    """
    fs = 1 / dt  # 采样频率
    nperseg = int(overlop_time / (dt * 1e9)) + 1  # 将纳秒转换为样本数
    noverlap = nperseg - 3  # 重叠样本数
    
    # 计算STFT
    f, t, Zxx = signal.stft(data_input,
                           fs=fs,
                           window='hann',
                           nperseg=nperseg,
                           noverlap=noverlap,
                           nfft=nfft,
                           boundary=None,
                           padded=False)
    
    # 计算幅度
    magnitude = np.abs(Zxx)
    
    # 对每个时间点计算质心频率
    center_freq = np.zeros(len(t))
    for i in range(len(t)):
        if np.sum(magnitude[:, i]) != 0:
            # 质心频率 = Σ(f * magnitude) / Σ(magnitude)
            center_freq[i] = np.sum(f * magnitude[:, i]) / np.sum(magnitude[:, i])
    
    return t, center_freq

8. 总结与展望

本教程详细介绍了LRPR数据处理中的地形校正与信号分析。通过应用这些技术,可以显著提高雷达数据的信息提取能力,为月球次表面结构的研究提供更准确的依据。

主要内容包括:

  1. 地形校正技术,用于消除地表起伏对雷达数据的影响
  2. 零时校正方法,确保电磁波传播时间的准确计算
  3. 希尔伯特变换与瞬时属性分析,包括瞬时振幅、相位和频率
  4. 质心频率分析
  5. 信号包络提取和振幅归一化技术

通过深入研究这些技术,将能更好地理解月球地下结构,为未来的月球资源勘探和人类月球活动提供科学支持。

Logo

永洪科技,致力于打造全球领先的数据技术厂商,具备从数据应用方案咨询、BI、AIGC智能分析、数字孪生、数据资产、数据治理、数据实施的端到端大数据价值服务能力。

更多推荐