3. 侧扫声呐数据格式与读取

侧扫声呐的原始数据并不是一张“图片”,而是按时间顺序记录的回波强度序列,外加大量描述采集条件的元数据。能否正确读取这些数据,直接决定了后续目标识别是否可靠。本节围绕三种最常见的原始格式——XTF、JSF 以及厂商私有格式——讲清它们的文件结构、元数据含义,并给出可实际运行的读取思路。

一、为什么不能把原始文件当图片直接打开

侧扫声呐每次发射后,换能器按采样率对回波进行离散采样,形成一条“ping”。每个 ping 包含左右舷两段强度序列,采样点对应斜距方向的位置。要把它还原成地理图像,必须知道:采样率、声速、拖鱼离底高度、航向、航速、时间戳、通道增益等参数。这些信息分散在文件头、通道头和逐 ping 头中,缺一不可。因此读取工具的核心任务不是“显示”,而是“解析结构 + 提取元数据 + 对齐通道”。

二、XTF 格式:开放、自描述、最常用

XTF(eXtended Triton Format)是 Triton 公司提出、被业界广泛采用的开放格式,SonarWiz、Hypack、DeepView 等软件均可读写。它采用“文件头 + 若干数据包”的链式结构,每个数据包以 256 字节的 XTFPINGHEADER 开头,通过 NextPing 偏移量串联。关键字段包括:

  • NumChansToFollow:本 ping 的通道数,侧扫通常为 2(左、右舷)。
  • SonarName、CHANINFO:声呐型号与每通道的采样率、增益、频率等。
  • SensorPrimaryAltitude、SensorPrimaryDepth:离底高度与拖鱼深度。
  • SensorHeading、SensorSpeed、SensorYaw/Pitch/Roll:姿态与运动信息。
  • PingTime:以 Windows FILETIME 记录的绝对时间。

XTF 的优点是自描述性强,元数据与回波数据在同一文件中,适合做批处理。缺点是不同采集软件对可选字段的填充不一致,读取时必须做容错判断,例如通道数缺失时按 2 处理、姿态字段全零时视为无效。

三、JSF 格式:结构紧凑、需注意字节序

JSF(Jeffrey’s Sonar File)常见于 Klein 等系统的导出数据,结构比 XTF 更紧凑,通常由文件头、逐 ping 头和通道数据块组成。它的一个典型陷阱是字节序:部分 JSF 文件采用大端序存储,若按小端解析,采样率、ping 数等字段会变成明显异常的巨大数值。读取时应先读取文件头中的魔数或版本字段做校验,再决定解析字节序。JSF 的元数据字段命名与 XTF 不同,但语义相近,映射关系需要在读取层统一抽象。

四、文件结构与元数据的解析流程

下图展示了一个典型读取工具内部的数据流:从文件头校验开始,逐 ping 解析头信息,分离左右舷通道,提取元数据,最终输出为可处理的数组与元数据表。

侧扫原始数据读取流程与元数据提取 原始文件 XTF / JSF / 私有格式 文件头校验 魔数 / 版本 / 字节序 通道数初判 逐 ping 解析 PingHeader + 通道数据 按 NextPing 偏移推进 元数据表 时间/姿态/高度 采样率/增益 通道分离 左舷 / 右舷强度序列 按采样点数切分 数据归一化 8bit/16bit 转浮点 增益补偿标记 输出:ping 数组 + 元数据记录 每条记录含:时间戳、左右舷数组、高度、航向、采样率 供后续斜距校正与目标检测使用 异常处理分支 通道数缺失 → 按 2 处理 姿态全零 → 标记无效 关键校验点:① 文件头魔数与版本;② 每 ping 的 NextPing 偏移是否单调递增;③ 通道采样点数是否与 CHANINFO 一致; ④ 时间戳是否连续;⑤ 高度/姿态字段是否在合理物理范围内。任一异常都应记录到日志而非静默跳过。

五、读取工具实践:用 Python 解析 XTF 的关键片段

下面给出一个最小可用的 XTF 读取骨架,重点展示“按偏移推进 + 提取元数据”的写法。示例参数为教学假设,实际字段偏移需以所用 XTF 版本规范为准。

import struct

def read_xtf(path):
    with open(path, 'rb') as f:
        header = f.read(1024)          # 文件头
        magic = header[:4]
        assert magic == b'XTF\x00', '非 XTF 文件'
        f.seek(1024)
        pings = []
        while True:
            ph = f.read(256)           # XTFPINGHEADER
            if len(ph) < 256:
                break
            num_chans = struct.unpack_from('<H', ph, 20)[0]
            ping_time = struct.unpack_from('<Q', ph, 32)[0]
            alt = struct.unpack_from('<f', ph, 60)[0]
            next_ping = struct.unpack_from('<I', ph, 4)[0]
            chans = []
            for _ in range(num_chans):
                ch = f.read(64)        # XTFCHANINFO
                n_samples = struct.unpack_from('<I', ch, 8)[0]
                data = f.read(n_samples)   # 8bit 强度
                chans.append(list(data))
            pings.append({'t': ping_time, 'alt': alt, 'ch': chans})
            if next_ping == 0:
                break
            f.seek(next_ping)
    return pings

运行后应检查:len(pings) 是否与文件头声明的 ping 数接近;每条记录的 ch 长度是否等于采样点数;alt 是否在 2~50 m 的合理区间。若出现大量 alt=0,说明该字段未被采集软件填充,后续斜距校正需改用其他高度来源或直接按固定量程处理。

六、元数据解析的常见坑与判断依据

第一是时间基准。XTF 的 PingTime 是 FILETIME(自 1601 年起 100ns 计数),而 JSF 常用 Unix 时间或 GPS 周秒,混用会导致轨迹错位。第二是通道顺序。部分文件先存右舷后存左舷,若不做校验,目标会左右镜像。第三是采样率与量程的对应关系:量程 = 采样点数 × 声速 / (2 × 采样率),读取后应反算量程并与元数据中的声明值比对,偏差超过 5% 就要怀疑字段解析错误。

建议在读取层统一抽象出“Ping 记录”结构:时间戳、左右舷强度数组、离底高度、航向、采样率、量程。无论底层是 XTF 还是 JSF,上层目标检测代码只依赖这一结构,格式差异被隔离在读取适配器中。

完成本节读取后,你应当能够:识别 XTF 与 JSF 的基本结构差异;用偏移量正确遍历 ping;提取并校验关键元数据;对异常字段做出合理降级处理。这些是后续斜距校正、图像镶嵌和目标检测的数据基础。