简介:这份资源是一篇面向专科、本科毕业生的原创毕业论文,主题为基于Python实现地震数据可视化的设计与实现,适合计算机科学与技术、数据挖掘方向的学生作为毕业设计选题参考,也适合想练习数据处理与可视化技能的自学者。压缩包内共1个docx文件,约33KB,即完整的论文正文,包含摘要、关键词、引言、地震数据可视化技术综述、Python语言及相关库介绍、算法设计与实现、系统测试与分析等章节,并附有目录和参考文献框架。论文围绕USGS等公开地震数据的爬取、清洗、Pandas特征提取,以及Matplotlib、Seaborn、Bokeh等库的时间序列图、空间分布图、震级频率分布图与三维震源结构可视化展开,还整合了图形用户界面设计。已有689人学习下载,读者可借此获得完整论文结构、研究方法与可视化实现思路,用于选题借鉴或写作参考。
1. 从一份 2GB 的 SEG-Y 说起:Python 地震数据可视化到底在解决什么
现场拿到的地震数据,大概率不是一份整洁的 CSV,而是采集系统直接吐出的 SEG-Y:道头里塞着炮号、CDP、检波点坐标和标量因子,数据体本身还可能是 IBM 32 位浮点。业务方要的东西却很具体——一张能贴进汇报的叠后剖面、一条能拖动的时间切片、一个挂在浏览器里随时能看的看板。Python 在这两头之间当翻译:segyio 和 ObsPy 把格式翻成 NumPy 数组,matplotlib、Plotly、PyVista 把数组画成图,而中间那层参数(采样间隔、色标截断分位、AGC 窗口长度)才真正决定这张图能不能用。这篇适合两类人:写过基础 Python、但没碰过地震数据格式的;以及手里压着一堆 MATLAB 脚本、想迁到 Python 数据分析与可视化工具链上的。往下按读取、剖面、三维交互、结果验证四步推,每一步都给能直接跑的代码和出问题时的排查方向。
2. 读进 NumPy 之前:SEG-Y、MiniSEED 与 SAC 的读取与道头解析
2.1 先分清格式:三类地震数据文件与对应的 Python 库
同一个项目里往往混着好几种格式,读错库比写错代码更浪费时间。常见的对应关系如下。
| 格式 | 典型后缀 | 推荐库 | 数据特征 | 典型场景 |
|---|---|---|---|---|
| SEG-Y | .sgy / .segy | segyio | 道集 + 道头,IBM 或 IEEE 浮点 | 反射地震叠后/叠前剖面、数据体 |
| MiniSEED | .mseed / .seed | ObsPy | 连续时间序列,Steim 压缩 | 天然地震台站、微震监测 |
| SAC | .sac | ObsPy | 单道或等长多道 | 科研波形交换、震相分析 |
| HDF5 派生 | .h5 | h5py | 任意数组 + 元数据 | 中间结果、切片缓存 |
选择依据其实就一条:数据里有没有「道头」。SEG-Y 的价值一半在数据、一半在道头,道头里的 CDP、inline、xline、标量因子决定了后面能不能把数组还原成有坐标的剖面;MiniSEED 是纯时间序列,时间信息在块头里,靠 ObsPy 的UTCDateTime对齐。不要用 NumPy 的fromfile硬读 SEG-Y,2400 字节的卷头和 4000 字节的扩展卷头处理起来很容易出错。
2.2 用 segyio 把 SEG-Y 读成数组:道头、采样间隔与 IBM 浮点
先看一段能直接跑的读取代码,注意道头字段是批量取的。
import segyio import numpy as np path = "line_1201_poststack.sgy" with segyio.open(path, "r", ignore_geometry=True) as f: n_trace = f.tracecount # 总道数 dt_us = segyio.tools.dt(f) # 采样间隔,单位微秒 ns = f.samples.size # 每道样点数 # 一次读完整块,得到 (道数, 样点数) 的二维数组 data = segyio.tools.collect(f.trace[:]) # 道头字段按字节偏移批量取,比逐道读 f.header 快一个量级 cdp = f.attributes(segyio.TraceField.CDP)[:] il = f.attributes(segyio.TraceField.INLINE_3D)[:] xl = f.attributes(segyio.TraceField.CROSSLINE_3D)[:] t_ms = np.arange(ns) * dt_us / 1000.0 # 第 i 个样点对应的双程旅行时(毫秒) print(data.shape, dt_us, ns, cdp.size)参数含义逐个说清楚:ignore_geometry=True表示不按 inline/crossline 重建规则网格,先拿到文件里的原始道序,做 QC 时用这个模式最省事;真要按线号切片,再调f.segyio.set_geometry或者segyio.tools.cube重排成三维体。segyio.tools.dt会自动处理 2400 与 2401 字节处采样率单位不一致的坑,比自己读f.bin[segyio.BinField.Interval]稳妥。f.attributes一次拿一个字段的全部道,几十万道也是秒级。
IBM 浮点是另一个高频雷区。SEG-Y 的 format code 1 表示 4 字节 IBM 浮点,segyio 会自动转成 IEEE,但如果有人直接np.fromfile(..., dtype=">u4")读原始卷,就必须自己转:
def ibm2ieee(raw: np.ndarray) -> np.ndarray: """把 IBM 32 位浮点字节流转成 IEEE float32,用于绕过库直读原始卷""" b = raw.astype(np.uint32) sign = np.where(b >> 31, -1.0, 1.0) # 最高位是符号 exp = ((b >> 24) & 0x7F).astype(np.int32) - 64 # 7 位指数,偏移 64 frac = (b & 0x00FFFFFF).astype(np.float64) / float(0x1000000) # 24 位尾数 return (sign * frac * np.power(16.0, exp)).astype(np.float32)这里两个细节容易写错:指数以 16 为底而不是 2,尾数是 24 位而不是 23 位。转错的表现是剖面整体幅值差好几个数量级,或者出现inf。
2.3 用 ObsPy 读 MiniSEED/SAC 并对齐时间轴
台站数据走另一条路,读进来是 Stream,合并、去趋势、滤波一条链。
from obspy import read st = read("20240517_*.mseed") # 支持通配符批量读 st.merge(method=1, fill_value="interpolate") # 处理分段、重叠与空洞 tr = st.select(component="Z")[0] # 取垂直分量 tr.detrend("linear") # 去线性趋势 tr.filter("bandpass", freqmin=1.0, freqmax=10.0, corners=4, zerophase=True) # 零相位带通 data = tr.data.astype(np.float32) t = tr.times("relative") # 相对起始时刻的秒数 print(tr.stats.starttime, tr.stats.sampling_rate, tr.stats.npts)freqmax必须小于采样率的一半(Nyquist 频率),100 Hz 采样时上限是 50 Hz,写 60 会直接抛异常,这是新手最常见的报错来源。zerophase=True做双向滤波,好处是不引入相位延迟,不会把震相到时整体挪位;代价是边界效应更明显,出图前通常要掐掉头尾各一两秒。merge的fill_value="interpolate"只在空洞很小时用,大段缺道应该先补数据而不是插值,否则频谱图上会出现明显的人工台阶。
2.4 读出来不对劲时,先查这几项
道数对不上,先确认是否误用了 geometry 模式;数据体整体上下颠倒,检查道序与时间轴方向;幅值量级离谱,回头看 format code 和 IBM 转换;坐标差一个数量级,去查道头里的scalco标量因子——它可能是 100,也可能是 -100,符号表示乘或除。把这几项写成一个validate(path)函数在读取阶段跑一遍,比在图上去猜快得多。
3. 用 matplotlib 画地震剖面:变密度、波形与增益的取值逻辑
3.1 变密度剖面:imshow 的 extent 与时深标定
变密度图是地震数据可视化的主力图型,核心是让颜色代表振幅、让坐标轴带物理量纲。
import matplotlib matplotlib.use("Agg") # 批量出图时不要弹窗,服务器上必须加 import matplotlib.pyplot as plt import numpy as np fig, ax = plt.subplots(figsize=(10, 6), dpi=150) clip = np.percentile(np.abs(data), 98) # 用分位数定色标上下限 im = ax.imshow(data.T, cmap="seismic", vmin=-clip, vmax=clip, # 双向对称色标,零值居中 aspect="auto", interpolation="bilinear", extent=[cdp.min(), cdp.max(), t_ms[-1], t_ms[0]]) ax.set_xlabel("CDP") ax.set_ylabel("双程旅行时 / ms") fig.colorbar(im, ax=ax, label="振幅") fig.savefig("section.png", bbox_inches="tight")extent的 y 轴写成[t_ms[-1], t_ms[0]]是刻意的,imshow 原点默认在左上,这样时间向下增大,符合地震剖面的阅读习惯,省掉一次invert_yaxis。clip取 98 分位而不是np.abs(data).max(),是因为一条异常道就能把色标撑爆、整张图压成灰色,取 95 到 99 分位通常都行,需要突出弱反射就降到 90。cmap="seismic"是红白蓝双向色标,正负振幅一眼可辨;换成"gray"就是传统的黑白变密度显示,汇报里两种都常见。interpolation在大体量数据上建议设成"nearest",速度更快,也不会糊掉高频细节。
3.2 波形图与变面积填充:道抽稀和横向放大倍数
波形图(wiggle)适合看同相轴的连续性和极性。
scale = 3.0 # 每道横向放大倍数,一般取道间距的 1~3 倍 step = 2 # 隔道抽稀,2000 道以上建议至少隔 3 道 ax.cla() for i in range(0, n_trace, step): tr = data[i] / (np.abs(data[i]).max() + 1e-9) * scale + i ax.plot(tr, t_ms, "k-", linewidth=0.4) ax.fill_betweenx(t_ms, i, tr, where=tr > i, color="k", linewidth=0) ax.invert_yaxis() ax.set_xlim(0, n_trace)scale是这类图上唯一需要反复试的参数:太小波形挤在一起分不清,太大相邻道互相穿插。经验值是按道间距的 1 到 3 倍给,12.5 米面元的小区通常取 2 到 3。step控制密度,图宽 10 英寸、dpi 150 的情况下,1000 道已经接近肉眼分辨极限,再密就是一团黑。fill_betweenx的where=tr > i只填正半周,形成经典的黑白相间效果;要正负都填就分两次调用,颜色换成红蓝。
3.3 增益与 AGC:同一份数据两张图差别为什么那么大
增益方式直接决定剖面能不能看出深部弱反射,几种常见做法可以对照着选。
| 增益方式 | 实现 | 适用 | 副作用 |
|---|---|---|---|
| 全局归一化 | data / np.abs(data).max() | 只做相对振幅对比 | 深部弱反射被压没 |
| 道均衡 | data / np.abs(data).max(axis=1) | 道间能量差异大 | 破坏道间真实振幅关系 |
| AGC 滑动 RMS | 滑动窗口 RMS 归一 | 看构造、看同相轴 | 破坏振幅保真,禁用于属性分析 |
| 分位数截断 | ±np.percentile(abs, 98) | 显示用 | 强振幅被削顶 |
AGC 的滑动 RMS 用累积平方和实现,复杂度是线性的,几十万道也不慢:
def agc(data, win=31): """按道做滑动窗口 RMS 归一化;win 为窗口样点数,建议取主频周期的 1~2 倍""" pad = win // 2 p = np.pad(data, ((0, 0), (pad, pad)), mode="reflect") # 边界镜像,避免边缘变暗 csum = np.cumsum(p ** 2, axis=1) rms = np.sqrt((csum[:, win:] - csum[:, :-win]) / win)[:, :data.shape[1]] return data / (rms + 1e-9)win怎么定:采样间隔 2 ms、主频 30 Hz 时,一个周期约 33 ms,换算成样点约 17 个,窗口取 31 到 51 比较稳。窗口太小会在每个同相轴上留下周期性亮暗条纹,太大就退化成道均衡,看不出浅层弱信号。做振幅类属性(如均方根振幅、瞬时相位)之前不要上 AGC,那些属性靠的就是真实的相对振幅。
3.4 大体量数据体的抽样与渲染性能
数据超过 2 GB 以后,出图慢通常不在绘图库,而在没做抽稀。几条实用的做法:先把float64转float32,内存直接减半;横向按 stride 抽道,纵向样点保持不动,因为时间方向的分辨率对形态判断更关键;imshow的数据量控制在两千万点以内,超了就抽稀或分块出图;保存用bbox_inches="tight"会重新计算边界,批量出上千张图时反而是负担,可以关掉。做完这些,一张 4000 道 × 1500 样点的剖面出图通常在 1 到 3 秒。
4. 从二维剖面到三维交互:PyVista、Plotly 与可视化大屏落地
4.1 三维体渲染:用 PyVista 做正交切片
拿到的若是叠后数据体(inline × xline × time),三维切片比二维剖面更能说明构造形态。
import pyvista as pv import numpy as np grid = pv.ImageData() grid.dimensions = (n_il, n_xl, n_t) # 注意这里填的是点数 grid.spacing = (12.5, 12.5, 2.0) # 面元 12.5 m,时间采样 2 ms grid.point_data["amp"] = volume.ravel(order="F") # 顺序必须与 dimensions 一致 slices = grid.slice_orthogonal(x=..., y=..., z=...) # 三个方向的正交切片 p = pv.Plotter(off_screen=True) # 服务器上出图;去掉即交互窗口 p.add_mesh(slices, cmap="seismic", clim=[-clip, clip]) p.camera_position = "xz" p.screenshot("volume_slices.png")最容易出错的点是ravel(order="F")必须与dimensions的顺序对应。NumPy 里三维数组通常是 (iline, xline, time) 的 C 序,而 PyVista 的 ImageData 按 x 变化最快的顺序吃数据,直接ravel()会让切片整体错位,表现是切片上出现规则的斜条纹。索引换算写完先用一个小立方体(比如 5×5×5)验证一遍,比在大数据体上调试省时间。clim同样建议用分位数而不是极值。
4.2 交互式剖面:Plotly 的 Heatmap 与降采样边界
要在浏览器里拖指针看振幅,Plotly 的 Heatmap 是最省事的路线。
import plotly.graph_objects as go fig = go.Figure(go.Heatmap( z=data[:600, :800], # 先降到 60 万点以内 x=cdp[:800], y=t_ms[:600], colorscale="RdBu", zmid=0, # zmid 让零值落在色标中点 hovertemplate="CDP %{x}<br>t=%{y:.1f} ms<br>幅值 %{z:.3g}<extra></extra>")) fig.update_yaxes(autorange="reversed") # 时间向下 fig.write_html("section.html", include_plotlyjs="cdn")zmid=0是双向色标的关键,不设的话色标会按数据极值居中,零值偏色。include_plotlyjs="cdn"让 HTML 只有几十 KB,离线交付时改成True内联。点数超过六十万以后浏览器渲染会明显卡顿,这是硬边界,前端只能看降采样后的摘要,原始体数据留在后端切片接口里。
4.3 挂到大屏:Dash 与 pyecharts 的分工
内部监控页常要求把地震剖面当成一路图层接进已有的数据可视化大屏里。Plotly Dash 适合做有交互回调的分析页面,pyecharts 适合把图层塞进已有的 ECharts 大屏,两者都只需要一段最小骨架。
from dash import Dash, dcc, html, Input, Output app = Dash(__name__) app.layout = html.Div([ dcc.Dropdown(id="line", options=[{"label": f"L{i}", "value": i} for i in (1201, 1202, 1203)], value=1201), dcc.Graph(id="sec"), ]) @app.callback(Output("sec", "figure"), Input("line", "value")) def render(line): d, cdp, t = load_line(line) # 后端读 + 抽稀 + 分位数截断 return build_heatmap(d, cdp, t) # 复用 4.2 的绘图函数回调里做两件事就够:缓存已读的数据体,避免每次切线下发都重新解 SEG-Y;接口只返回抽稀后的数组,别把原始体数据序列化成 JSON。做企业级数据可视化时,这一层通常还要加权限和审计,但绘图函数本身不用改,保持「读 → 抽稀 → 绘图」三段解耦,前端换框架时只动最后一段。
4.4 工程目录与依赖
把读取、二维、三维、Web 四层分开放,后面加格式或换图型都不用动别的文件。
seismic_viz/ ├── data/ # 原始 sgy/mseed,只读 ├── src/readers.py # segyio / ObsPy 读取与校验 ├── src/gain.py # AGC、道均衡、分位数截断 ├── src/plot2d.py # 变密度、波形 ├── src/plot3d.py # PyVista 切片 ├── app/dash_app.py # Web 层 └── out/ # 出图产物依赖清单固定成numpy / scipy / segyio / obspy / matplotlib / pyvista / plotly / dash / pyecharts,Python 版本建议 3.10 以上,用 venv 建独立环境,在 VS Code 里选中这个解释器作为工作区环境,避免和系统 Python 混装。obspy 在 Windows 上装依赖较慢,Linux 下用系统包管理器先装科学计算基础库会快很多。版本号不要锁死在很旧的区间,segyio 与 numpy 的 ABI 一旦不匹配,报错信息会指向与实际问题无关的地方。
5. 进阶技巧与验证:频域图、自检表与一键出图
5.1 时频与频谱:判断滤波效果最直接的两张图
剖面看着干净不代表处理没问题,频域图是更硬的证据。
from scipy import signal import matplotlib.pyplot as plt f, t, Sxx = signal.spectrogram(tr, fs=1.0 / (dt_us / 1e6), nperseg=128, noverlap=96, scaling="density", mode="psd") plt.pcolormesh(t, f, 10 * np.log10(Sxx + 1e-20), shading="auto", cmap="viridis") plt.ylim(0, 60); plt.xlabel("时间 / s"); plt.ylabel("频率 / Hz") # F-K 谱:横轴波数,纵轴频率,用于识别线性干扰 f2, k, P = signal.spectrogram(data, fs=1.0 / (dt_us / 1e6), nperseg=64, axis=0, mode="psd")nperseg决定时间与频率分辨率的取舍:取 128 时窗长约 256 ms,看得到频带随时间的变化,但定位突变会糊;取 256 定位更准,低频分辨率反而下降。F-K 谱上线性干扰表现为从原点出发的斜线,滤波后斜线能量应明显衰减——这是验证带通滤波有没有起作用的量化依据,比肉眼看剖面可靠。scaling="density"输出功率谱密度,做多道对比时比默认的"spectrum"更稳。
5.2 出图前的自检清单
把下面几项固定成函数,在批量出图前跑一遍,能拦掉大部分返工。
| 检查项 | 判定方式 | 常见异常原因 |
|---|---|---|
| 道数与道头长度一致 | data.shape[0] == cdp.size | 读取时跳道或重复读 |
| 时间轴与采样间隔匹配 | ns * dt_us / 1000 == t_ms[-1] | 采样率单位误当毫秒 |
| 幅值分布合理 | np.percentile(abs(data), [1, 50, 99]) | IBM 浮点转换错误 |
| 极性一致 | 相邻道互相关峰值应为正 | 道头极性标志未读 |
| 坐标不畸形 | CDP 与 xline 差值单调 | 标量因子符号反了 |
| 图与数一致 | 抽稀前后同一道波形叠加对比 | extent 或转置写错 |
最后一项最容易被跳过,也最值得做:写一个assert_same_trace(raw, plotted),把抽稀过的数组和原始数据里对应的那一道画在同一张图上,肉眼一比就知道有没有转置错误或时间轴偏移。
5.3 一键批量出图的最小脚本
# 批量出图,避免逐条手工执行 python -m src.batch_plot \ --input ./data/*.sgy \ --out ./out \ --clip-percentile 98 \ --agc-win 31 \ --dpi 150 \ --workers 4把clip_percentile、agc_win、dpi做成命令行参数而不是写死在函数里,是让这套脚本能被别人复用的关键——同一个项目里,做 QC 的人想要 90 分位的高对比图,做汇报的人想要 98 分位的柔和图,参数化之后就不用改代码。多进程出图时注意matplotlib.use("Agg")必须在导入pyplot之前调用,否则在无显示环境里会直接抛后端错误。真正决定这类脚本能不能长期用的,是读取层与绘图层之间的那个数组契约:只传(data, cdp, t_ms)三个对象,格式细节全部封在readers.py里。
本文还有配套的精品资源,点击获取