资讯动态

气象雷达基数据解析:从二进制流到PPI可视化

发布时间:2026/10/13 21:27:25 来源:尧图企业网站定制
简介面向雷达基数据解析与显示应用的C工程资源服务需要掌握雷达原始数据读取、解码与可视化链路的中高级开发者适用于气象雷达、交通监控、教学演示等场景的学习与二次开发。压缩包共80个文件大小7.83MB以13个cpp源文件和17个h头文件为核心涵盖信号处理、图像格式转换、显示控制等模块同时附带Visual Studio工程配置、调试中间文件、图标位图、exe和lib文件便于在开发环境中直接打开编译或快速验证。代码从二进制雷达基数据入手覆盖数据结构定义、解码算法、JPEG/BMP转换及平面位置指示器PPI绘制有助于理解从原始数据到屏幕回波的完整链路对雷达数据组织结构与解析流程的掌握尤为直接。目前已有344人学习适合具备C基础、希望深入雷达数据解析细节的开发者借鉴也可用作课程设计或雷达显示项目的参考框架。1. 基数据是个黑匣子RadarYHM 到底要解出什么拿到一个叫 RadarYHM 的压缩包里面多半是雷达基数据文件和一两个解析工具真正要解决的问题是雷达数据解析把雷达原始二进制基数据还原成反射率、径向速度、谱宽这些物理量再叠加角度和距离信息最后画成能看的雷达显示产品。雷达基数据不像普通图像里面没有一个像素全是一条条极坐标采样的径向数据流。本文把这套流程完整拆开从文件结构、逐字节解析到 PPI 显示和结果校验让新手能照着跑通也让熟手能少踩几个我已经踩过的坑。适合手里攒了一堆.bin、.DAT、.RAD文件却不知道从哪下手的从业者。2. 雷达基数据格式按仰角层和径向一条条读2.1 基数据不是图像而是一串按径向编织的二进制流气象雷达基数据的一个核心特征是它里面根本没有“像素”这个概念。雷达天线在某个固定仰角上旋转按固定方位间隔发射脉冲每一个脉冲的回波沿距离方向按距离门range gate采样得到一条从近到远的回波序列这就是一根径向radial。天线转一圈会积累几十到几百根径向这些径向组成一个仰角层再抬升仰角扫下一层最后得到的多层数据叫体扫volume scan。基数据文件落盘时通常就是按“文件描述块 → 仰角层块 → 径向头 → 数据段”的顺序连续写入的。理解了这一点你就知道为什么不能把基数据当成普通图像去读。它更像一个稀疏的极坐标测量结果每根径向有自己的方位角、仰角、起始距离和距离库数数据段的长度也不一定相等。不同雷达型号的基数据格式细节差异很大但大框架是一致的。RadarYHM 这类解析工具要做的第一件事就是把文件头里那些关于“这个文件是谁扫的、什么时候扫的、扫了多少层”的信息拆出来。我这里整理了一张常见基数据结构的字段表你可以对照着去勘察手头的文件数据块常见字段字节数 / 类型说明文件描述块雷达标识、站点编号、扫描时间8~128 字节不等有的格式有版本号字符串仰角层块仰角值、库长、径向数4~16 字节/层告诉程序这一层有多少根径向径向头方位角、仰角、距离库数4~12 字节/径向每根径向一条决定后续数据长度数据段反射率、径向速度、谱宽2 字节 × 3 × 库数三个物理量常交叉存放2.2 为什么按“径向 × 距离库”读是唯一正确姿势很多人第一次打开雷达基数据习惯性想按图像的行列去读结果发现数据完全对不上——因为基数据压根不是按行扫描生成的而是按“方位角-距离”组织的。一根径向上第一个距离库是雷达站最近处的回波最后一个距离库是最大探测距离附近的回波。你把整个文件当成一个大数组按行铺开相当于把几百根径向首尾相接完全丢失了方位角信息。正确做法是逐径向解析先读文件描述块然后按仰角层循环每一层内再按径向循环。每根径向先读它的方位角、距离库数再读数据段。这样得到的是一组带极坐标标签的物理量数组方位角数组、距离数组、反射率矩阵径向 × 距离库。后续做雷达显示时再把这些极坐标数据插值到直角坐标上。动手之前可以先用一个 Python 小脚本看文件头到底是什么样的with open(RADAR.DAT, rb) as f: head f.read(16) print(head)如果前 8 个字节是可见字符比如RDA1、CFRAD这类标识说明文件头有版本号如果全是乱码也别慌直接用十六进制窗口看下一个整数。这一步只需要花十分钟就能确定文件是不是按大端存贮、文件头从哪个字节开始有有效信息。很多新手跳过了这个勘察步骤直接套网上找的解析代码最后读出来的数据全乱回头还以为是代码有问题。2.3 动手前先查三样东西字节序、偏移量、缩放系数雷达基数据解析这个方向前期花一小时勘察格式比后期改代码省十小时。我最常提醒别人的三件事一是字节序二是偏移量三是缩放系数。字节序错是最隐蔽的你按大端读一个无符号短整数如果读出来的数值动辄五六万换小端H很可能就正常了。偏移量错更麻烦文件头长度不同数据段起点就差几十字节读出来的反射率全是 -999 或 0。缩放系数是另一个容易翻车的点。原始基数据里存的通常不是物理量本身而是一个整数计数LSB。常见约定是反射率按 0.5 dBZ/LSB 缩放径向速度按 0.0625 m/s/LSB 缩放谱宽类似。但也有的格式把反射率按 0.1 dBZ 存或者速度直接存浮点数。我一般先拿文件里一段已知强回波的原始值反推缩放再决定代码里用多少。参数常见值说明字节序或读出来数值异常先换这个方位角存储uint16 × 0.01° 或 uint8 映射方向约定看格式说明反射率缩放0.5 dBZ/LSB乘以缩放才得到 dBZ速度缩放0.0625 m/s/LSB注意速度还有退模糊问题缺测值-999 或 0xFF画图前统一转 NaN3. 用 Python 逐字节解析基数据最小可用脚本与关键参数3.1 先写一个能读出文件头和径向头的骨架确定了字节序和偏移量之后解析代码的核心就是一个循环读层头、读径向头、读数据段。下面这个骨架用struct模块从二进制流中解出文件描述块和径向列表。代码里的偏移量是按常见气象雷达基数据格式示意写的你遇到不同型号时改偏移量即可读取逻辑是通用的。import struct import numpy as np def parse_radar(buf): # 文件描述块解析雷达标识、扫描时间和仰角层数 radar_id buf[0:8].decode(ascii, errorsignore).strip(\x00) year, month, day struct.unpack_from(HHH, buf, 8) hour, minute, second struct.unpack_from(HHH, buf, 14) layer_count struct.unpack_from(H, buf, 22)[0] print(f{radar_id} {year:04d}-{month:02d}-{day:02d} f{hour:02d}:{minute:02d}:{second:02d} 层数{layer_count}) # 数据段起点按文件头长度 128 字节示意 offset 128 radials [] for _ in range(layer_count): radial_count struct.unpack_from(H, buf, offset)[0] offset 2 elevation struct.unpack_from(f, buf, offset)[0] offset 4 for _ in range(radial_count): az struct.unpack_from(H, buf, offset)[0] / 100.0 offset 2 gate_count struct.unpack_from(H, buf, offset)[0] offset 2 radials.append((elevation, az, gate_count, offset)) # 跳过三个物理量的数据段反射率、速度、谱宽各 2 字节 offset gate_count * 2 * 3 return radials with open(RADAR.DAT, rb) as f: buf f.read() radials parse_radar(buf) print(radials[:3])代码里struct.unpack_from(HHH, buf, 8)是连续解出三个大端无符号 16 位整数分别对应年、月、日f表示单精度浮点用于仰角值。radials列表里每一项保存了仰角、方位角、距离库数和数据段在文件中的起始偏移量。注意代码里offset gate_count * 2 * 3是为了跳到下一根径向的位置真正解析时应该在这一步把数据也读出来而不是只跳过。这段代码跑通后建议把前几根径向的方位角、库数打印出来。正常情况下库数应该在一个稳定区间比如 460、920 这种和雷达最大探测距离匹配的值如果出现忽大忽小的异常值说明偏移量对得不准回到第 2.3 节把字节序和文件头长度再核一遍。3.2 一个径向的数据段三个物理量怎么拆径向头读完紧接着就是数据段。常见组织方式是三个物理量按距离库交叉存放第一个距离库的反射率、第一个库的速度、第一个库的谱宽然后第二个库、第三个库……所以只要把整个数据段按 2 字节为一个单元读出来再隔三取一就能拆出三个独立数组。def read_radial_data(buf, offset, gate_count, scale(0.5, 0.0625, 0.0625)): dtype np.dtype(i2) raw np.frombuffer(buf, dtypedtype, countgate_count * 3, offsetoffset) ref raw[0::3] * scale[0] vel raw[1::3] * scale[1] sw raw[2::3] * scale[2] # 原始值低于 -990 视为缺测统一转 NaN画图时自动透明 ref[ref -990] np.nan vel[vel -990] np.nan sw[sw -990] np.nan return ref, vel, sw # 拿第一根径向测试 elev, az, gate_count, offset radials[0] ref, vel, sw read_radial_data(buf, offset, gate_count) print(f方位角 {az:.1f}° 库数 {gate_count} 最大反射率 {np.nanmax(ref):.1f} dBZ)这段代码用np.frombuffer直接从文件缓冲中按i2大端有符号 16 位整数读取gate_count * 3个数然后用切片raw[0::3]取反射率、raw[1::3]取速度、raw[2::3]取谱宽。scale参数把整数计数换算成物理量如果换到不同型号的雷达只需要改这个元组。raw[0::3]这种写法的意思是从 0 开始每隔 3 个取一个。因为数据是三个物理量交叉存放的所以切片间隔正好是 3。如果读出来的反射率全是 128、130 这种偏大的值先检查是不是该用无符号类型u2如果反射率几乎全是 -999那大概率是offset没对准真实数据段差了几个字节。顺着这个思路排查比漫无目的地改代码快得多。3.3 参数速查把常见值列成表解析代码里最容易调错的就是缩放系数和缺测值。这里把常见值汇总成一张表方便你直接对照物理量典型存储缩放系数单位缺测处理反射率因子int160.5 dBZ / LSBdBZ乘缩放后 -990 转 NaN径向速度int160.0625 m/s / LSBm/s同上谱宽int160.0625 m/s / LSBm/s同上方位角uint160.01° / LSB(°)—仰角float32 或 uint16直接读出或 0.01°(°)—需要特别提醒的是这张表只覆盖最常见的约定。有的商业雷达把反射率按 0.1 dBZ 存有的直接存浮点有的缺测值不是 -999 而是 0xFF。遇到这类情况时最笨但最有效的办法是造一个已知答案的测试找一张同一时刻的雷达产品图对比最大回波中心的位置和数值反推缩放系数是否合理。这比翻格式文档更快也更接近实际工程里的做事方式。4. 把解析结果画成雷达图PPI 显示里最容易抄错的三处4.1 极坐标网格转直角坐标先补全方位角再插值解析出物理量后“雷达显示”这一步大多数人会直接在pcolormesh上把方位角当 x 轴、距离当 y 轴结果画出来要么是扇形空隙要么是放射状条纹。原因是每条径向量到的方位角不是均匀分布的尤其在实际扫描中天线旋转速度波动会导致方位角间隔有大有小。正确做法是先建立一个规则的方位角网格把原始径向数据插值上去再转直角坐标。常见做法是先把所有径向重采样到 1° 一个方位再转直角坐标。下面代码演示了如何把某个距离库上的反射率沿方位角插值成规则网格az_grid np.arange(360) ref_grid np.full((gate_count, 360), np.nan) for i in range(gate_count): valid ~np.isnan(ref[:, i]) if valid.sum() 2: ref_grid[i, :] np.interp(az_grid, azimuths[valid], ref[valid, i])这段代码对每一个距离库单独做一维插值。azimuths是所有径向的方位角数组ref是前面解析出来的反射率矩阵形状为径向数 × 库数。插值完成后ref_grid就是一个规则的 360° 极坐标网格。如果雷达方位角分辨率高于 1°你可以把az_grid改成np.arange(0, 360, 0.5)但注意插值密度不要超过原始采样间隔太多否则属于过度平滑会丢失真实结构。转直角坐标时雷达方位角 0° 指正北、顺时针增大而数学坐标系默认 0° 指正东、逆时针增大所以转换公式要写成az_rad np.deg2rad(az_grid) R np.arange(gate_count) * gate_len_km X R[:, None] * np.sin(az_rad[None, :]) Y R[:, None] * np.cos(az_rad[None, :])X、Y的单位是公里。R[:, None]把距离向量变成列向量az_rad[None, :]把方位角变成行向量广播相乘后得到一个网格。如果你把sin和cos写反了图上回波就会整体旋转 90°这是雷达显示里最常见的方向性错误。4.2 dBZ 色标与坐标轴别让颜色吞掉回波结构反射率图能不能一眼看清回波强度分布色标映射比绘图函数本身更重要。直接用 matplotlib 默认色标20 dBZ 以下的弱回波在图上会糊成一片。我一般用BoundaryNorm按 dBZ 业务等级切分色阶让弱回波、强回波在视觉上拉开差距。import matplotlib.pyplot as plt import matplotlib.colors as mcolors def plot_ppi(X, Y, ref_grid, titlePPI): levels [-20, -10, 0, 10, 20, 30, 40, 50, 60, 65] cmap plt.get_cmap(jet) norm mcolors.BoundaryNorm(levels, cmap.N) plt.figure(figsize(6, 6)) plt.pcolormesh(X, Y, ref_grid, cmapcmap, normnorm, shadingnearest) plt.colorbar(labeldBZ) plt.title(title) plt.gca().set_aspect(equal) plt.xlabel(东向距离 (km)) plt.ylabel(北向距离 (km)) plt.show()BoundaryNorm(levels, cmap.N)把连续的反射率值映射到离散色阶每个区间对应一种颜色这样做的好处是图中超过 60 dBZ 的强回波会非常显眼。pcolormesh里的shadingnearest参数也很关键它让每个网格点的颜色和它代表的空间区域对齐避免高亮回波边缘出现细线状的白洞。最后set_aspect(equal)保证 x、y 方向尺度一致否则圆形回波会被拉伸成椭圆。4.3 速度场显示与模糊速度速度场显示和反射率不同它本身是带符号的一般用蓝红渐变表示朝向和远离雷达的运动。常规画法是把色标范围定在正负最大不模糊速度之间比如 ±30 m/s超出这个范围的速度值会在图上直接跳成相反的符号这就是速度模糊velocity folding。如果你的速度图出现了大面积的蓝红对跳先别高兴那不是真的强风切变而是没有做退模糊。判断是否存在速度模糊可以检查相邻距离库的速度差。正常情况下径向速度沿距离是连续变化的如果相邻库的差值突然超过某个阈值这一带基本就是模糊区delta np.abs(np.diff(vel, axis1)) alias_mask delta 20 # 阈值按雷达最大不模糊速度调整这段代码不负责解模糊但能帮你快速定位问题区域。真正解模糊需要做退模糊算法比如沿径向做连续性展开或者利用多普勒双频信息这些都是单独的专题。雷达显示阶段至少要把模糊区域标注出来避免把速度场假象当真实风场汇报出去。5. 雷达基数据解析避坑5 条血泪经验5.1 方位角读出来只到 180°一字节存储的坑现象打印方位角数组发现最大值只有 180°可雷达明明扫描了完整一圈。原因部分老式雷达基数据把方位角存放在一个无符号字节里0~255 映射整个 360° 范围。你用H按 16 位去读就会把相邻字段的字节拼进来导致方位角数值被截断或整体错位。解决改为按 8 位读取再把原始值映射到 0~360°az raw_uint8.astype(float) * 360.0 / 256.0注意360 / 256而不是360 / 255因为有些格式的满量程就是 256。改完后检查方位角的最小值是否接近 0、最大值是否接近 360且分布是否覆盖了完整的扫描扇区。5.2 整场反射率全是 -999偏移量错位的典型现象现象解析完成画图后整幅 PPI 几乎是空的反射率值全部落在 -999 附近个别像素偶尔出现一个离谱的大数。原因数据段偏移量计算错误程序读到了径向头和数据段之间的填充字节或者读到了上一个仰角的末尾。这类问题的根源一般是文件头长度没核对128 字节的文件头实际可能是 256 字节。解决先打印第一根径向的offset附近 32 个字节与 hexdump 手动比对。具体操作是用xxd -s 128 -l 64 RADAR.DAT查看文件头之后的原始字节再对照代码里读到的gate_count和方位角找到它们对应的原始位置偏差几个字节一目了然。xxd -s 128 -l 64 RADAR.DAT5.3 速度场红蓝对跳没做退模糊现象速度图上相邻径向之间出现大片符号相反的“马赛克”颜色从深蓝直接跳到深红没有过渡。原因雷达测速存在最大不模糊速度超过这个区间的真实速度被折叠到相反方向。这不是解析错误而是物理限制。解决先用 4.3 节的差值检测画出模糊区再做退模糊。如果只是临时看一下风场结构可以把模糊区直接置 NaN 并在图上标注“存在速度模糊”避免误判为强风切变。如果要正式使用就得实现退模糊算法常见的有沿径向连续性展开和区域增长法。5.4 画出来的 PPI 像被拧了 90°方位角的方向约定现象回波形状和雷达官方产品完全对不上整个图顺时针或逆时针转了 90°。原因matplotlib 的极坐标默认 0° 在右侧且逆时针为正而雷达方位角 0° 指正北、顺时针为正。如果直接拿方位角当极坐标角度用方向就反了。解决转到直角坐标时用x r * sin(az)、y r * cos(az)保证 0° 对应 x 为 0、y 为正即正北方向。如果你发现回波转了 180°那多半是 sin 和 cos 的位置反了或负号漏了。5.5 基数据文件版本不匹配一个文件能解下一个全崩现象同一个脚本解析上一个文件正常换到下一个文件就报数组越界或者读出来的库数变成负值。原因同一个站点的雷达换了信号处理器或软件版本文件头长度从 128 字节变成 256 字节字段位置也有调整。解析代码里的硬编码偏移在新版本上全部失效。解决在解析函数开头加版本检测根据魔法字节选择对应的偏移表和缩放系数def detect_version(buf): if buf[0:4] bRDA1: return 1, 128, if buf[0:4] bRDA2: return 2, 256, raise ValueError(未知格式)拿不到格式说明时可以直接把不同版本的偏移量、文件头长度写成字典参数化到解析函数里。这是雷达基数据解析工程化必做的一步能省掉未来大量排查时间。6. 用统计校验解析结果验证、导出与下一步6.1 先跑统计再画图最大回波中心不会骗你解析完一批数据我习惯先算统计量再画图而不是直接出 PPI。最大回波位置、回波面积、速度场覆盖率这些数字能快速暴露方向错误和偏移错误。比如最大回波中心应该在雷达站东南 80 公里你画出来却在西北那基本可以断定方位角符号反了。valid ref_grid 0 area_km2 valid.sum() * (gate_len_km ** 2) max_dbz np.nanmax(ref_grid) max_pos np.unravel_index(np.nanargmax(ref_grid), ref_grid.shape) print(f回波面积 {area_km2:.0f} km²最大回波 {max_dbz:.1f} dBZ)valid是大于 0 dBZ 的回波掩膜gate_len_km是距离库长度。回波面积和最大回波位置是和业务雷达产品做交叉验证最直接的两个指标。如果最大回波位置和实况差了几十公里先回 5.4 检查方位角方向再回 5.2 检查偏移量。6.2 导出成 CSV 或 NetCDF交给下游算法解析结果验证正确后我一般会把它从内存里落盘方便下游做风场反演、定量降水估计或者机器学习训练集。CSV 适合快速分发NetCDF 适合保留完整的维度信息。import pandas as pd df pd.DataFrame({ azimuth: np.repeat(azimuths, gate_count), range_km: np.tile(np.arange(gate_count) * gate_len_km, len(azimuths)), reflectivity: ref_flat, velocity: vel_flat, }) df.to_csv(radar_polar.csv, indexFalse)注意导出的是极坐标格式而不是直角坐标网格。极坐标保留了原始采样信息下游做径向插值或退模糊时都需要原来的径向结构一旦转成直角网格相当于做了一次重采样信息的可回溯性就差了。6.3 下一步组合反射率与体扫拼图基数据解析和显示跑通后我建议你下一个目标做组合反射率把所有仰角的反射率层插值到同一个水平网格再逐格取最大值得到最能反映天气系统全貌的“大图”。实现思路是在 4.1 节网格化代码外面套一层仰角循环最后对多层网格np.nanmax。要注意先统一各层的网格范围和分辨率否则低仰角细网格和高仰角粗网格叠加时会出空洞。我第一次解析气象雷达基数据时就是在统计校验这一步发现最大回波中心偏了 90°回头排查出方位角方向反了那个下午让我记住了雷达显示的方向约定。之后我每次拿到新格式都会先跑一遍统计再画图这个习惯帮我少翻了很多车。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑