资讯动态

Qt C++ 读取 SEGY/SEGD 地震数据与波形变密度显示实战

发布时间:2026/10/16 0:56:19 来源:尧图企业网站定制
简介这份源码资源面向地震数据处理与地球物理可视化方向的开发者以及希望用 Qt C 实现专业图形界面的学习者。它基于老歪编写的读取 SEGY 与 SEGD 两种地震数据格式的完整工程在 Qt5.12 环境下编译通过可解决二进制地震道解析、头部信息提取、样本数据转换等核心问题并进一步完成波形显示与变密度显示两类可视化效果。压缩包共 436 个文件约 6.08MB以 52 个 cpp 源文件、51 个 h 头文件、9 个 ui 界面文件和 8 个 pro 工程文件为主体另含大量 png、jpg、bmp 等界面素材与图标资源便于直接编译运行和二次开发。目前已有 442 人学习下载。读者可从中获得文件读取与解析模块、地震道数据结构、基于 QGraphicsView 与 QPainter 的波形绘制、结合 QImage 颜色映射的变密度成像以及滚动缩放等交互实现思路适合作为地震数据可视化项目的参考模板。1. 老歪的 Qt C 地震数据读取方案为什么 SEGY 和 SEGD 值得单独啃地震数据格式里SEGY 和 SEGD 是两个绕不开的老家伙。SEGY 是带道头的标准交换格式SEGD 则是很多现场采集系统直接吐出来的原始格式两者结构相似但细节差异能把人逼疯。老歪用 Qt C 写了一套读取源码在 Qt5.12 上编译通过支持波形显示和变密度显示这件事的价值在于它把「格式解析」和「可视化」这两件本来要分开做的事塞进了一个可复用的工程骨架里。如果你手头有一堆野外采集的 SEGY/SEGD 文件想快速看一眼剖面长什么样或者想在自己的 Qt 项目里嵌一个地震数据查看器这套东西就是起点。它不适合做生产级处理系统但适合做验证、调试和二次开发的基础。下面我按「格式怎么读 → 显示怎么做 → 坑在哪」的顺序把这条路径拆开讲。2. SEGY 与 SEGD 的二进制结构从 3200 字节头到道采样2.1 两种格式的头部差异与读取顺序SEGY 的文件头是 3200 字节的文本头加 400 字节的二进制头合计 3600 字节。文本头里通常是 EBCDIC 编码的描述信息二进制头里藏着关键参数采样间隔、采样点数、道数、数据格式码。SEGD 没有那 3200 字节文本头直接以 400 字节二进制头开头但字段偏移和 SEGY 不完全一致尤其是道头里的坐标和采样点位置。读取顺序必须是先读文件头解析出采样点数、采样间隔、格式码再按道循环读道头最后读道采样数据。很多人翻车是因为先读道头再回头找采样点数结果道头里的采样点数在某些 SEGD 变体里是 0必须从文件头拿。// 读取 SEGY 文件头3600 字节 QFile file(path); if (!file.open(QIODevice::ReadOnly)) return false; QByteArray fileHeader file.read(3600); // 二进制头从第 3200 字节开始 QByteArray binHeader fileHeader.mid(3200, 400); // 采样间隔偏移 162 字节单位微秒 quint16 sampleInterval *reinterpret_castconst quint16*(binHeader.mid(16, 2).constData()); // 采样点数偏移 202 字节 quint16 sampleCount *reinterpret_castconst quint16*(binHeader.mid(20, 2).constData()); // 数据格式码偏移 242 字节1IBM浮点5IEEE浮点 quint16 formatCode *reinterpret_castconst quint16*(binHeader.mid(24, 2).constData());这段代码的关键是偏移量。SEGY 的二进制头字段偏移是固定的但字节序可能是大端或小端需要根据实际文件判断。我一般先读一个已知文件用十六进制查看器确认采样间隔的数值再决定要不要做字节交换。formatCode决定了后面怎么把原始字节转成浮点数IBM 浮点和 IEEE 浮点的转换逻辑完全不同这是第一个大坑。2.2 道头解析与数据体定位每道开头有 240 字节的道头。道头里最重要的是道号、CDP 号、坐标和采样点数。SEGY 的道头字段偏移是标准化的比如道号在偏移 0CDP 在偏移 20坐标在偏移 72 和 76。但 SEGD 的道头布局经常被采集系统改过不能硬编码。我的做法是先按标准 SEGY 偏移读一遍如果发现 CDP 号全是 0 或者坐标明显不对就说明这个 SEGD 文件用了非标准道头需要根据采集系统的文档调整偏移。没有文档怎么办看数据规律。比如道号通常是递增的找到递增的那个 2 字节或 4 字节字段就能反推道号偏移。// 按道循环读取 qint64 dataStart 3600; // SEGY 数据体起始位置 int traceCount 0; while (!file.atEnd()) { file.seek(dataStart traceCount * (240 sampleCount * 4)); QByteArray traceHeader file.read(240); // 道号偏移 04 字节 qint32 traceNumber *reinterpret_castconst qint32*(traceHeader.mid(0, 4).constData()); // CDP偏移 204 字节 qint32 cdp *reinterpret_castconst qint32*(traceHeader.mid(20, 4).constData()); // 读取采样数据 QByteArray traceData file.read(sampleCount * 4); // 转浮点 QVectorfloat samples convertToFloat(traceData, formatCode); traceCount; }这里假设每道采样点数和格式码一致。实际文件里偶尔会出现变长道那就不能靠固定步长 seek必须顺序读。convertToFloat里要处理 IBM 浮点转 IEEE 浮点IBM 浮点的指数是 16 的幂尾数是 24 位转换公式是(-1)^sign * 0.mantissa * 16^(exponent-64)。这个转换如果写错波形会变成一条直线或者乱跳。2.3 字节序与格式码的联合判断字节序和格式码必须一起判断。大端 IBM 浮点、小端 IBM 浮点、大端 IEEE 浮点、小端 IEEE 浮点四种组合的读取方式都不同。我的经验是先假设小端 IEEE 浮点读几道看波形是否合理如果波形幅度在 1e38 量级说明格式码判断错了如果波形是噪声但幅度正常可能是字节序反了。QVectorfloat convertToFloat(const QByteArray raw, quint16 formatCode) { QVectorfloat result; int sampleSize 4; for (int i 0; i raw.size(); i sampleSize) { if (formatCode 1) { // IBM 浮点转 IEEE quint32 ibm *reinterpret_castconst quint32*(raw.mid(i, 4).constData()); // 字节交换如果需要 // ibm qbswap(ibm); quint32 sign (ibm 31) 0x01; quint32 exponent (ibm 24) 0x7F; quint32 mantissa ibm 0x00FFFFFF; float value (sign ? -1 : 1) * (mantissa / 16777216.0f) * pow(16, exponent - 64); result.append(value); } else if (formatCode 5) { // IEEE 浮点直接读 float value *reinterpret_castconst float*(raw.mid(i, 4).constData()); result.append(value); } } return result; }formatCode 1是 IBM 浮点formatCode 5是 IEEE 浮点。有些文件格式码写的是 3那是 2 字节整数需要单独处理。qbswap是 Qt 的字节交换函数如果发现波形不对先试这个。注意pow(16, exponent - 64)里的 64 是 IBM 浮点的偏置值不是 127。3. 波形显示与变密度显示的 Qt 实现路径3.1 用 QPainter 画波形坐标映射与抽道策略波形显示的本质是把采样点映射到屏幕坐标。假设屏幕宽度 800 像素一道有 1000 个采样点那就需要抽道或者压缩。我的做法是如果采样点数大于屏幕宽度按比例取最大值和最小值画竖线如果小于屏幕宽度直接连线。void drawWaveform(QPainter painter, const QVectorfloat samples, int xOffset, int traceHeight) { int w painter.device()-width(); int h traceHeight; float maxAmp *std::max_element(samples.begin(), samples.end()); float minAmp *std::min_element(samples.begin(), samples.end()); float scale (h / 2.0f) / std::max(qAbs(maxAmp), qAbs(minAmp)); painter.drawLine(xOffset, h / 2, xOffset w, h / 2); // 基线 for (int i 0; i samples.size() - 1; i) { int x1 xOffset i * w / samples.size(); int x2 xOffset (i 1) * w / samples.size(); int y1 h / 2 - samples[i] * scale; int y2 h / 2 - samples[i 1] * scale; painter.drawLine(x1, y1, x2, y2); } }scale的计算决定了波形是否溢出。如果一道里有一个异常大值maxAmp会很大其他道就被压扁了。解决办法是用百分位数代替最大值比如取 95% 分位数。xOffset是道在屏幕上的水平偏移多道显示时每道占一个固定宽度。3.2 变密度显示的颜色映射与 QImage 填充变密度显示是把振幅映射成颜色通常用灰度或红蓝配色。核心是构建一个 QImage每个像素对应一个采样点或一个采样区间。QImage createDensityImage(const QVectorQVectorfloat traces, int width, int height) { QImage image(width, height, QImage::Format_RGB32); float globalMax 0; for (const auto trace : traces) for (float v : trace) globalMax std::max(globalMax, qAbs(v)); for (int x 0; x width; x) { int traceIndex x * traces.size() / width; const auto trace traces[traceIndex]; for (int y 0; y height; y) { int sampleIndex y * trace.size() / height; float value trace[sampleIndex] / globalMax; // 归一化到 -1~1 int gray static_castint((value 1.0f) * 127.5f); image.setPixel(x, y, qRgb(gray, gray, gray)); } } return image; }globalMax用全局最大值做归一化保证所有道颜色一致。如果只想看局部细节可以改成每道单独归一化但那样道与道之间的振幅关系就丢了。setPixel在大量像素时很慢更好的做法是直接操作QImage::bits()或者用QPainter画QImage。变密度显示对颜色映射敏感灰度线性映射适合看结构红蓝配色适合看极性。3.3 双显示模式的切换与性能取舍波形和变密度不能同时画在一张图上通常是切换或者分屏。切换时要注意波形显示需要抽道变密度显示需要重采样两者的数据准备逻辑不同。我的做法是保留原始浮点数据显示时按需生成绘制缓存。性能上波形显示在道数少时更快变密度显示在道数多时更有优势。如果道数超过 500波形会糊成一片这时候变密度更合适。Qt5.12 的 QPainter 在软件渲染下画 1000 条线大约需要 20 毫秒变密度生成 800x600 的 QImage 大约 50 毫秒都在可接受范围。如果卡顿优先优化setPixel改用scanLine直接写内存。4. 避坑与排查SEGY/SEGD 读取中最容易翻车的五个点4.1 波形显示成一条直线现象读出来的数据画波形全是零或者一条水平线。原因通常是格式码判断错误把 IBM 浮点当 IEEE 浮点读了或者字节序反了。解决先用十六进制查看器看几个采样点的原始字节如果是0x41 0x20 0x00 0x00这种大概率是 IBM 浮点如果是0x3F 0x80 0x00 0x00那是 IEEE 浮点的 1.0。确认后再改convertToFloat里的分支。4.2 道头偏移对不上导致 CDP 全为零现象读出来的 CDP 号全是 0或者道号不连续。原因是 SEGD 文件用了非标准道头布局。解决不要硬编码偏移先按标准 SEGY 读一遍如果关键字段不对就手动找。找的方法是把前 240 字节按 2 字节一组打印出来看哪一组的值是递增的那大概率是道号再看哪一组的值在 1 到总道数之间那可能是 CDP。4.3 大数据文件读取时内存爆掉现象读一个 2GB 的 SEGY 文件程序直接崩溃。原因是把整个文件读进了 QByteArray。解决用QFile::seek按道读取每次只读一道的数据。如果要做全局变密度显示可以只读每道的包络或者降采样后的数据不要保留全部原始采样点。4.4 变密度显示颜色全灰没有对比度现象变密度图看起来一片灰看不出同相轴。原因是归一化用了全局最大值而某一道有一个异常大值把其他道都压扁了。解决改用 99% 分位数或者每道单独归一化。如果每道单独归一化后道间能量关系丢失可以改用对数归一化。4.5 Qt5.12 编译时 qmake 找不到 opengl 模块现象编译报错Unknown module(s) in QT: opengl。原因是 Qt5.12 默认不包含 opengl 模块需要单独安装或者改用QPainter软件渲染。解决如果只是画波形和变密度不需要 OpenGL把.pro文件里的QT opengl删掉直接用QWidget的paintEvent。如果确实需要硬件加速安装libqt5opengl5-dev或者在 Qt 安装器里勾选对应模块。5. 从能读到好用三个让这套源码真正落地的技巧第一个技巧是给道头解析加一层「探测」逻辑。不要假设文件一定符合标准而是先读前几道用统计方法判断道头布局。比如道号字段的值应该是从 1 开始递增的CDP 字段的值应该在合理范围内。如果标准偏移读出来的值不满足这些条件就自动切换到备选偏移表。这个探测逻辑写一次后面遇到非标文件就不用每次都改代码。第二个技巧是把浮点转换做成可配置的。IBM 浮点转 IEEE 浮点有几种实现有的用查表有的用位运算。我一般会写一个FloatConverter类内部缓存转换表对于大批量数据能快 30% 左右。另外如果文件里全是 IEEE 浮点直接memcpy比逐字节转换快一个数量级。第三个技巧是显示层做降采样缓存。波形显示和变密度显示都需要把原始采样点映射到屏幕像素这个映射关系在窗口大小不变时是固定的。可以在第一次绘制时生成一个QVectorQPointF缓存窗口 resize 时再重新生成。这样滚动和缩放时不用重复计算交互会跟手很多。// 降采样缓存示例每道生成固定宽度的包络 QVectorQPairfloat, float buildEnvelope(const QVectorfloat trace, int targetWidth) { QVectorQPairfloat, float envelope; int step trace.size() / targetWidth; for (int i 0; i targetWidth; i) { float minVal std::numeric_limitsfloat::max(); float maxVal std::numeric_limitsfloat::lowest(); for (int j i * step; j (i 1) * step j trace.size(); j) { minVal std::min(minVal, trace[j]); maxVal std::max(maxVal, trace[j]); } envelope.append({minVal, maxVal}); } return envelope; }buildEnvelope把一道压缩成targetWidth个区间每个区间保留最小值和最大值。画波形时用这两个值画竖线比逐点连线快得多而且不会丢失尖峰。step是降采样步长如果trace.size()小于targetWidthstep会是 0需要加保护。这套源码在 Qt5.12 上编译通过只是起点真正让它好用需要根据实际数据调整。我自己的习惯是每拿到一批新数据先用十六进制查看器看头部再用一个最小读取程序验证采样点和格式码最后才接入显示。这个流程能省掉大量调试时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑