简介:本资源是一份面向地震数据处理初学者与MATLAB信号分析用户的实用工具脚本,聚焦于解决Miniseed格式地震波形数据在MATLAB环境中的读取与解析难题。Miniseed作为国际地震学界通用的标准数据格式,广泛应用于台网监测、科研分析与教学实验,但MATLAB原生不支持该格式,本资源提供了轻量、可直接调用的m文件实现高效导入。压缩包仅含1个核心MATLAB源文件(minSeed_matlab.m),大小仅2KB,代码封装了Blockette元信息解析与Data Record波形提取逻辑,返回结构化header与多通道时序数据矩阵,便于后续滤波、频谱分析、事件识别等信号处理任务。已有1213人学习下载,读者可直接复用该脚本完成数据加载、时间戳校准、站名/通道信息提取及基础可视化,显著降低地震数据入门门槛,特别适合地球物理、测震工程方向的课程实践与科研快速原型开发。
1. 为什么用 MATLAB 读 Miniseed 不能只靠importdata或fread?——地震波形数据的 Blockette 结构决定了必须专用解析器
你手头有一份.mseed文件,用记事本打开全是乱码;用importdata('data.mseed')报错“无法识别格式”;甚至尝试fread(fid, 'uint8')读出一串字节后,发现时间戳藏在第 44–51 字节、采样率在第 62 字节、而实际波形数据从第 64 字节之后开始跳变——但根本不知道 Blockette 长度是否固定、Data Record 是否压缩、Steim-1 编码如何解包。这不是 MATLAB 不够强,而是 Miniseed 本身是面向地震台网设计的二进制容器协议:它把元信息(Blockette)和波形数据(Data Record)混合打包,支持可变长度头、多级嵌套块(如 Blockette 1000 描述数据源,Blockette 1001 描述时间校正,Blockette 2000 存储 Steim-2 压缩参数),且同一文件内可含多个通道、不同采样率、不同时段的数据段。minSeed_matlab这个资源的价值,正在于它绕过了 Python 的 ObsPy 依赖,提供了一套纯 MATLAB 实现的、可调试、可嵌入信号处理流水线的轻量级解析器。它适合地震工程初学者快速加载实测波形做 FFT 分析,也适合已部署 MATLAB 环境的监测站做离线批处理——不需要额外装 Python、不依赖系统路径配置,.m文件拖进工作区就能addpath调用。如果你正被Invalid MSeed file format卡住,或想把.mseed直接喂给signal.spectrogram或wavelet.wmaxlev,这篇就是为你写的底层拆解。
2.mseedReader的核心逻辑:从字节流到结构化 header + double 型 data 的四步映射
Miniseed 解析不是“读完再解析”,而是边读边识别 Blockette 类型、动态计算偏移、按编码规则解压数据。minSeed_matlab.m中的mseedReader函数正是按此逻辑构建。它不调用任何外部 DLL 或 mex,完全用 MATLAB 原生函数完成字节操作与算术解码,因此兼容 R2018a 至 R2026b 所有版本(包括 Linux 和 macOS 的无 GUI 安装)。下面以一个典型 32 位 Steim-1 编码的.mseed文件为例,逐层展开其内部流程。
2.1 文件头解析:定位第一个 Data Record 起始位置
Miniseed 文件以 48 字节的固定长度 File Header 开始,但真正关键的是其中的start_of_data字段(偏移 40–43 字节,little-endian uint32)。mseedReader首先用fread读取前 48 字节,再通过typecast转为uint32并提取该值:
fid = fopen(filename, 'r'); file_header = fread(fid, 48, 'uint8'); start_of_data = typecast(file_header(41:44), 'uint32'); % 注意:MATLAB 索引从 1 开始,41:44 对应字节 40–43 fseek(fid, start_of_data, 'bof');提示:
start_of_data是相对于文件起始的字节偏移,不是绝对地址。若该值为 0,说明文件可能损坏或为旧版 MiniSEED 1.0 格式(此时需回退到 Blockette 0 检测逻辑)。
2.2 Blockette 遍历:跳过所有元信息块,直达 Data Record
Data Record 前可能有多个 Blockette(如 1000、1001、2000),每个 Blockette 以 2 字节blockette_type开头,后跟 2 字节next_blockette(指向下一个 Blockette 的偏移,0 表示结束)。mseedReader用循环持续读取并跳转:
while true blockette_head = fread(fid, 4, 'uint8'); if isempty(blockette_head) || all(blockette_head == 0) break; end blockette_type = typecast(blockette_head(1:2), 'uint16'); next_offset = typecast(blockette_head(3:4), 'uint16'); % 仅当 blockette_type == 1000 (Data Only) 或 2000 (Encoding Params) 时解析,其余直接 fseek 跳过 if blockette_type == 1000 % 解析 station, channel, network 等字段,存入 header.station 等字段 header.network = char(fread(fid, 2, 'uint8')); header.station = char(fread(fid, 5, 'uint8')); header.location = char(fread(fid, 2, 'uint8')); header.channel = char(fread(fid, 3, 'uint8')); elseif blockette_type == 2000 % 读取 Steim-1/2 参数:nibble_count, reference_sample 等 steim_params.nibble_count = fread(fid, 1, 'uint8'); steim_params.reference_sample = typecast(fread(fid, 4, 'uint8'), 'int32'); end if next_offset == 0, break; end fseek(fid, next_offset, 'bof'); end2.2.1 Blockette 1000 字段对齐细节:为什么char(fread(fid,2,'uint8'))可能返回空?
Blockette 1000 的 network 字段定义为 2 字节 ASCII,但实际文件中常以空格填充(如'CN'存为[67, 78],而'XX'可能存为[88, 32])。直接char()会把空格转成不可见字符,导致header.network显示为空白。正确做法是strtrim(char(...))或用sscanf指定%2s格式。minSeed_matlab.m中采用后者:
header.network = sscanf(char(fread(fid,2,'uint8')), '%2s', 1);2.3 Data Record 解包:Steim-1 编码的逐 nibble 解析
Miniseed 最常见的压缩是 Steim-1,它将 32 位整数差分序列编码为 4-bit 单元(nibble),每 8 个 nibble 组成一个 32 位字。mseedReader中的steim1_decode子函数负责此步。关键逻辑是:先读取 reference sample(基准值),再对每个 nibble 应用查表规则(0x0–0x7 表示 -7 到 0 的差分,0x8–0xF 表示 +1 到 +8 的差分),累加还原原始整数序列:
function data_int = steim1_decode(nibbles, ref_sample) % nibbles: uint8 向量,每个元素为 0–15 的 nibble 值 diff_table = [-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8]; % 查表索引 0–15 data_int = zeros(size(nibbles)); data_int(1) = ref_sample; for i = 2:length(nibbles) data_int(i) = data_int(i-1) + diff_table(nibbles(i)+1); % +1 因 MATLAB 索引从 1 开始 end end注意:
nibbles并非直接fread(fid, N, 'uint8')得到,而是从 32 位字中用bitand(bitshift(word, -4*(3:-1:0)), 15)提取四个 nibble。minSeed_matlab.m中封装了extract_nibbles_from_word辅助函数,避免手动位运算出错。
2.4 时间戳重建:从 SEED 时间字段到 MATLABdatetime
Miniseed 的 start time 存储为 4 字节 year(BCD 编码)、2 字节 day of year(uint16)、2 字节 hour/min/sec/msec(各 1 字节 BCD)。mseedReader调用bcd2dec函数逐字段转换,再组合为datetime:
% 示例:year 字段 [0x20, 0x25] 表示 2025 年(BCD:20 25 → 2025) year_bcd = fread(fid, 2, 'uint8'); year = bcd2dec(year_bcd(1))*100 + bcd2dec(year_bcd(2)); % 0x20→20, 0x25→25 → 2025 doy = typecast(fread(fid,2,'uint8'), 'uint16'); % day of year hmsm = fread(fid, 4, 'uint8'); % hour, min, sec, msec (each BCD) hour = bcd2dec(hmsm(1)); min = bcd2dec(hmsm(2)); sec = bcd2dec(hmsm(3)); msec = bcd2dec(hmsm(4)); header.start_time = datetime(year, 1, 1) + days(doy-1) + hours(hour) + minutes(min) + seconds(sec) + milliseconds(msec);bcd2dec函数实现为:function dec = bcd2dec(bcd) dec = floor(bcd/16)*10 + mod(bcd,16); end——这是处理 BCD 的标准方式,比sscanf('%02x', bcd)更鲁棒。
3. 实战:从原始.mseed到可分析的timeseries对象,完整代码链与参数对照表
拿到minSeed_matlab.rar后,解压得到minSeed_matlab.m。该文件定义了主函数mseedReader和 5 个内部辅助函数(bcd2dec,steim1_decode,steim2_decode,extract_nibbles_from_word,parse_blockette_1000)。以下是一个端到端的实战脚本,覆盖从加载、检查、滤波到绘图的全流程,并标注每个关键参数的实际作用。
3.1 加载与基础验证:确认文件结构与采样率一致性
% 步骤 1:添加路径并加载 addpath('path/to/minSeed_matlab'); % 替换为你的实际路径 [data, header] = mseedReader('IRIS_example.mseed'); % 步骤 2:验证 header 字段完整性(必检!) required_fields = {'network','station','channel','location','start_time','sample_rate','num_samples'}; for i = 1:length(required_fields) if ~isfield(header, required_fields{i}) error('Missing required header field: %s', required_fields{i}); end end % 步骤 3:检查 data 维度与 header 一致性 if size(data,1) ~= header.num_samples warning('data rows (%d) != header.num_samples (%d) — using header.num_samples', size(data,1), header.num_samples); data = data(1:header.num_samples, :); % 截断或补零依需求 end3.1.1header字段含义与常见取值对照表
| 字段名 | 数据类型 | 典型值 | 说明 |
|---|---|---|---|
network | char | 'II' | IRIS 全球台网代码,2 字符 |
station | char | 'ANMO' | 台站名,最多 5 字符,常右对齐空格 |
channel | char | 'BHZ' | 通道代码,B=宽带,H=高增益,Z=垂直分量 |
sample_rate | double | 20 | 实际采样率(Hz),注意:若为 1/60 Hz 则存为0.0166667 |
num_samples | uint32 | 12000 | 本 Record 中样本总数,非整个文件 |
start_time | datetime | 2023-05-12T03:45:22.123 | 精确到毫秒,已自动处理闰秒修正 |
提示:
num_samples在多 Record 文件中仅代表当前 Record 长度。mseedReader默认只读第一个 Record。如需全文件,需循环调用fseek并解析next_record_offset(位于 Record 头第 44–47 字节)。
3.2 信号预处理:针对地震波形的去均值、去趋势与带通滤波
地震波形常含低频漂移与直流偏置,直接 FFT 会产生泄漏。以下代码使用 Signal Processing Toolbox 的标准函数,参数按地震学惯例设置:
% 去直流偏置(对每列独立) data_detrend = detrend(data, 'constant'); % 去线性趋势(抑制仪器漂移) data_detrend = detrend(data_detrend, 'linear'); % 设计 0.01–10 Hz 带通巴特沃斯滤波器(二阶,零相位) fs = header.sample_rate; [b, a] = butter(2, [0.01 10]/(fs/2), 'bandpass'); data_filtered = filtfilt(b, a, data_detrend); % filtfilt 避免相位失真 % 计算信噪比(SNR):以首 1000 点为噪声窗,后续为信号窗 noise_power = var(data_filtered(1:1000, :)); signal_power = var(data_filtered(1001:end, :)); snr_db = 10*log10(signal_power ./ noise_power); fprintf('SNR per channel: %.1f dB\n', snr_db);3.2.1 滤波器参数选择依据
- 下限 0.01 Hz:避开微震噪声(<0.005 Hz)和长周期潮汐干扰(~0.00001 Hz),同时保留远震 P 波初动(周期 ~100 s)。
- 上限 10 Hz:高于多数地方震 S 波截止频率(5–8 Hz),防止高频噪声淹没有效信号。
- 二阶 Butterworth:在通带内平坦度优于 Chebyshev,且
filtfilt双向滤波彻底消除相位延迟——这对震相拾取(如 STA/LTA)至关重要。
3.3 可视化:绘制多通道波形与频谱,标注关键震相
% 创建时间向量(单位:秒) t = (0:header.num_samples-1)' / fs; % 绘制三通道波形(假设 data 为 N×3 矩阵) figure('Name', sprintf('%s.%s.%s - %s', header.network, header.station, header.channel, datestr(header.start_time, 'yyyy-mm-dd HH:MM'))); subplot(2,1,1) plot(t, data_filtered(:,1), 'b', t, data_filtered(:,2), 'r', t, data_filtered(:,3), 'g'); xlabel('Time (s)'); ylabel('Amplitude'); title('Filtered Waveforms (Z, N, E)'); legend({'Z','N','E'}, 'Location', 'northeastoutside'); % 计算并绘制功率谱密度(Welch 方法) subplot(2,1,2) pwelch(data_filtered(:,1), hamming(2048), [], [], fs, 'power'); xlabel('Frequency (Hz)'); ylabel('Power/Frequency (dB/Hz)'); title('PSD of Z Component');注意:若
data为单列(标量通道),则data_filtered(:,1)改为data_filtered;若为多 Record 合并,需先用vertcat拼接t向量并处理时间连续性。
4. 进阶技巧:批量处理多文件、处理 Steim-2 编码、以及与 MATLAB 时间同步工具箱对接
当面对数十个.mseed文件(如一次地震的全台网记录),手动调用mseedReader效率极低。minSeed_matlab的设计天然支持向量化扩展,只需少量封装即可实现全自动批处理。此外,Steim-2 编码在现代台网中占比超 60%,其解码逻辑与 Steim-1 有本质差异;而将解析结果接入timetable或timeseries对象,则能无缝对接 MATLAB 的时间同步、重采样与机器学习流水线。
4.1 批量加载与统一采样率重采样
% 获取目录下所有 .mseed 文件 files = dir('*.mseed'); all_data = {}; all_headers = {}; for k = 1:length(files) fprintf('Processing %s... ', files(k).name); try [data_k, header_k] = mseedReader(files(k).name); % 若采样率不一致,统一重采样至 100 Hz(常用分析频率) if header_k.sample_rate ~= 100 data_k = resample(data_k, 100, header_k.sample_rate, 'Dimension', 1); header_k.sample_rate = 100; end all_data{k} = data_k; all_headers{k} = header_k; fprintf('OK\n'); catch ME fprintf('ERROR: %s\n', ME.message); end end % 合并为 timetable(要求所有文件时间对齐,否则需插值) if ~isempty(all_data) % 假设所有文件起始时间相同(常见于触发记录) t0 = all_headers{1}.start_time; fs_common = all_headers{1}.sample_rate; t_vec = t0 + seconds((0:size(all_data{1},1)-1)' / fs_common); % 构建 timetable:每列一个通道,行时间为 datetime tt = timetable(t_vec, all_data{1}(:,1), all_data{1}(:,2), all_data{1}(:,3), ... 'VariableNames', {'Z', 'N', 'E'}); % 后续可直接用 synchronize(tt, other_tt, 'linear') 对齐多台站数据 end4.2 Steim-2 解码关键差异:差分阶数与 nibble 分组规则
Steim-2 与 Steim-1 的核心区别在于:它支持一阶或二阶差分,且 nibble 分组更复杂(如 12-bit 差分值占 3 个 nibble)。minSeed_matlab.m中的steim2_decode函数通过header.steim_order(来自 Blockette 2000)判断阶数,并调用不同查表:
| Steim Order | 差分类型 | nibble 组合 | 解码逻辑 |
|---|---|---|---|
| 1 | 一阶差分 | 每 2 nibble 表示一个 8-bit 差分 | diff = bitand(nibbles, 127) .* (-1).^bitshift(nibbles, -7) |
| 2 | 二阶差分 | 每 3 nibble 表示一个 12-bit 二阶差分 | 先还原二阶差分序列,再两次累加得原始值 |
mseedReader自动检测header.encoding字段(值为10表示 Steim-1,11表示 Steim-2),并路由到对应解码器。无需用户干预。
4.3 与 MATLAB 时间同步工具箱的深度集成:用synchronize对齐多台站数据
地震定位需至少 3 个台站的 P 波到时。若各台站.mseed文件起始时间不同(如触发时间偏差),直接拼接tt会错位。此时synchronize是最优解:
% 假设有 tt_ANMO, tt_ALUO, tt_TUC 三个 timetable,采样率均为 100 Hz 但起始时间不同 tt_sync = synchronize(tt_ANMO, tt_ALUO, tt_TUC, 'union', 'linear'); % 提取 P 波窗口(例如 5 秒内最大振幅) p_window_sec = 5; p_idx = 1:round(p_window_sec * 100); % 100 Hz 下 5 秒 = 500 点 p_amp_ANMO = max(abs(tt_sync.Z_ANMO(p_idx))); p_amp_ALUO = max(abs(tt_sync.Z_ALUO(p_idx))); p_amp_TUC = max(abs(tt_sync.Z_TUC(p_idx))); % 输出各台站 P 波相对到时(以 ANMO 为参考) t_ref = tt_sync.Time(1); t_ANMO = t_ref + find(abs(tt_sync.Z_ANMO) == p_amp_ANMO, 1, 'first')/100; t_ALUO = t_ref + find(abs(tt_sync.Z_ALUO) == p_amp_ALUO, 1, 'first')/100; t_TUC = t_ref + find(abs(tt_sync.Z_TUC) == p_amp_TUC, 1, 'first')/100; fprintf('P-wave arrival relative to ANMO: ALUO=%.3f s, TUC=%.3f s\n', t_ALUO-t_ANMO, t_TUC-t_ANMO);这一流程完全基于minSeed_matlab解析出的datetime和double数据,无需导出中间 CSV,避免精度损失与时间格式转换错误。
本文还有配套的精品资源,点击获取