简介:这份源码资源面向地震数据处理与地球物理可视化方向的开发者,以及希望用 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); // 采样间隔:偏移 16,2 字节,单位微秒 quint16 sampleInterval = *reinterpret_cast<const quint16*>(binHeader.mid(16, 2).constData()); // 采样点数:偏移 20,2 字节 quint16 sampleCount = *reinterpret_cast<const quint16*>(binHeader.mid(20, 2).constData()); // 数据格式码:偏移 24,2 字节,1=IBM浮点,5=IEEE浮点 quint16 formatCode = *reinterpret_cast<const quint16*>(binHeader.mid(24, 2).constData());这段代码的关键是偏移量。SEGY 的二进制头字段偏移是固定的,但字节序可能是大端或小端,需要根据实际文件判断。我一般先读一个已知文件,用十六进制查看器确认采样间隔的数值,再决定要不要做字节交换。formatCode决定了后面怎么把原始字节转成浮点数,IBM 浮点和 IEEE 浮点的转换逻辑完全不同,这是第一个大坑。
2.2 道头解析与数据体定位
每道开头有 240 字节的道头。道头里最重要的是道号、CDP 号、坐标和采样点数。SEGY 的道头字段偏移是标准化的,比如道号在偏移 0,CDP 在偏移 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); // 道号:偏移 0,4 字节 qint32 traceNumber = *reinterpret_cast<const qint32*>(traceHeader.mid(0, 4).constData()); // CDP:偏移 20,4 字节 qint32 cdp = *reinterpret_cast<const qint32*>(traceHeader.mid(20, 4).constData()); // 读取采样数据 QByteArray traceData = file.read(sampleCount * 4); // 转浮点 QVector<float> 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 量级,说明格式码判断错了;如果波形是噪声但幅度正常,可能是字节序反了。
QVector<float> convertToFloat(const QByteArray& raw, quint16 formatCode) { QVector<float> result; int sampleSize = 4; for (int i = 0; i < raw.size(); i += sampleSize) { if (formatCode == 1) { // IBM 浮点转 IEEE quint32 ibm = *reinterpret_cast<const 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_cast<const 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 QVector<float>& 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 QVector<QVector<float>>& 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_cast<int>((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比逐字节转换快一个数量级。
第三个技巧是显示层做降采样缓存。波形显示和变密度显示都需要把原始采样点映射到屏幕像素,这个映射关系在窗口大小不变时是固定的。可以在第一次绘制时生成一个QVector<QPointF>缓存,窗口 resize 时再重新生成。这样滚动和缩放时不用重复计算,交互会跟手很多。
// 降采样缓存示例:每道生成固定宽度的包络 QVector<QPair<float, float>> buildEnvelope(const QVector<float>& trace, int targetWidth) { QVector<QPair<float, float>> envelope; int step = trace.size() / targetWidth; for (int i = 0; i < targetWidth; i++) { float minVal = std::numeric_limits<float>::max(); float maxVal = std::numeric_limits<float>::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()小于targetWidth,step会是 0,需要加保护。
这套源码在 Qt5.12 上编译通过只是起点,真正让它好用需要根据实际数据调整。我自己的习惯是:每拿到一批新数据,先用十六进制查看器看头部,再用一个最小读取程序验证采样点和格式码,最后才接入显示。这个流程能省掉大量调试时间。希望帮到你。
本文还有配套的精品资源,点击获取