简介:一份基于MATLAB的卫星数据读取与可视化示例包,面向海洋、气象、遥感等领域需要处理卫星资料的科研人员、工程师和高校学生。资源以真实海表面温度数据为例,完整演示了从数据文件读取、变量解析、坐标处理到绘制分布图的流程,可帮助初学者快速掌握卫星图像读取与出图的核心思路,避开常见的数据格式陷阱和绘图参数误区。压缩包共三个文件:一个wmv格式的操作录屏,便于直观跟随实际运行环境;一个read.m脚本,提供可直接修改和复用的出图代码;一个doc说明文档,补充介绍数据来源、程序逻辑和操作要点。整个压缩包仅1.38MB,轻量高效,适合碎片化学习。目前已有634人学习下载,尤其适合刚接触MATLAB卫星数据处理、希望快速跑通完整流程并理解程序结构的用户。借助该示例包,可以大幅缩短从零调试的时间,快速搭建自己的卫星数据读取与可视化脚本框架,也便于扩展到其他卫星数据的分析场景。
1. 从“例子-卫星数据.rar”开始:MATLAB 卫星数据读取的真实入口
很多人拿到的第一个卫星数据集不是一个目录结构清晰的文件夹,而是一个 .rar 压缩包。解压开以后,里面有 .tif、.nc、.hdf、.mat,既有卫星图,也有信噪比序列,命名还不太规律。这时候你会发现 MATLAB 的 imread、load 在这里不够用:imread 读不了带地理坐标的 GeoTIFF,load 只能碰 .mat,ncread 和 h5read 又要先弄清楚文件内部结构。下面按一条完整链路往下走:先拆包,再盘点格式,然后给出每种格式的最小读取代码、批量组装时间序列的思路,最后是画图和数值验证。适合做遥感、气象和通信信号分析的工程师,目标是把“能打开文件”变成“能拿数据做计算,做统计,画对图画准数”。
2. 先拆包再盘点:把“例子-卫星数据.rar”解放成 MATLAB 认识的文件集
2.1 .rar 解压:MATLAB 没有原生 rar 接口时的处理方式
先说一个现实问题:MATLAB 自带的 unzip 只支持 zip,不支持 rar 格式。所以第一步通常不是写 MATLAB 解析代码,而是调用外部解压工具。我一般优先用 7-Zip,因为它开源且命令行接口稳定,Window 和 Linux 都有对应版本。命令行调用方式如下:
# Windows 下优先找 7-Zip 安装目录里的命令行程序 "C:\Program Files\7-Zip\7z.exe" x -o"D:\satellite_workspace" -y "D:\examples\例子-卫星数据.rar" # Linux 下 p7zip 提供 7z 命令 7z x -o/home/user/satellite_workspace -y "/home/user/example_satdata.rar"参数说明:x表示解压并保留压缩包内目录结构;-o指定输出目录,注意-o后面不能有空格;-y让解压过程覆盖同名文件时不再询问。路径里有空格或中文时,务必用双引号包住,否则命令会被错误拆分成多个参数,这是最常见的失败原因。
在 MATLAB 里自动化这一步,推荐先判断外部程序是否存在,再拼接命令:
% 在 MATLAB 中调用 7-Zip 解压 .rar 压缩包 zip7Path = 'C:\Program Files\7-Zip\7z.exe'; archive = 'D:\examples\例子-卫星数据.rar'; outDir = 'D:\satellite_workspace'; if isfile(zip7Path) if ~exist(outDir, 'dir') mkdir(outDir); end cmd = sprintf('"%s" x -o"%s" -y "%s"', zip7Path, outDir, archive); [status, result] = system(cmd); if status ~= 0 error('解压失败,请查看 system 返回信息:%s', result); end fprintf('解压完成: %s\n', outDir); else error('未找到 7z.exe,请先安装 7-Zip 并确认安装路径'); end这段代码里用isfile检查 7z.exe 是否存在,比exist(x,'file')更精确,不会把同名文件夹误判成可执行文件。system返回的status为 0 表示命令成功,非 0 时要把result打出来排查,错误信息往往直接指向压缩包损坏或权限不足。如果服务端没有图形界面,安装 7-Zip 的字符版本后,命令用法完全相同。
2.2 用 dir 递归通配符盘点卫星数据格式与文件数量
解压完先别急着读数据,跑一个递归遍历脚本,看看这个数据包里到底有哪些格式,各有多少个文件:
rootDir = 'D:\satellite_workspace'; files = dir(fullfile(rootDir, '**', '*')); % 递归列出全部内容 files = files(~[files.isdir]); % 只保留文件 exts = regexpi({files.name}, '\.([a-z0-9]+)$', 'tokens', 'once'); exts = string(exts); [G, extList] = findgroups(exts); counts = splitapply(@numel, exts', G); tally = table(extList(:), counts(:), 'VariableNames', {'Extension', 'Count'}); tally = sortrows(tally, 'Count', 'descend'); disp(tally);这段代码用到 MATLAB 在dir函数中支持的**递归模式,可以一次扫出所有子目录里的文件。regexpi提取文件名的最后一段扩展名,结果转成 string 数组后,用findgroups和splitapply做分组计数。运行后会看到 .tif、.nc、.h5、.mat 各自的数量。这个环节的目的不是欣赏统计数字,而是确认该为哪几种格式写读取器。很多数据包里还会出现 .tfw、.hdr、.xml 这类辅助文件,它们不是数据本体,误当成 GeoTIFF 处理会报错,盘点阶段就能提早发现。
2.3 按扩展名分工:MATLAB 卫星数据读取接口对照
数据格式占比明确后,下一步是为每种格式选择合适的函数。整理成一张接口对照表,开发时照着查即可:
| 扩展名 | 典型数据内容 | 首选函数 | 注意事项 |
|---|---|---|---|
| .tif/.tiff | Landsat 等光学影像、地表反射率 | readgeoraster | 返回像素矩阵和地理栅格引用对象 R,需要 Mapping Toolbox |
| .nc | 大气、海洋再分析产品 | ncinfo / ncread | 内置支持,无额外工具箱要求 |
| .h5/.he5 | MODIS 切块数据、部分大气探测产品 | h5info / h5read | 读取路径带 group,结构比 NetCDF 复杂一层 |
| .hdf | 早期 HDF4 产品 | hdfread / hdfinfo | HDF4 接口维护较保守,优先确认能否转换格式 |
| .mat | 信噪比序列、轨道参数、中间处理结果 | load / matfile | 注意嵌套结构体和变量名冲突 |
这张表是读取架构的依据。实际项目里,我绝大多数情况只处理三类:GeoTIFF、NetCDF/HDF5、MAT。.tfw这类辅助文件通常不需要单独读,GeoTIFF 的参考信息已经内嵌在文件里。
3. 三种高频卫星数据格式的最小读取代码
3.1 GeoTIFF:readgeoraster 从像素值换算到经纬度
GeoTIFF 是把遥感影像和地理参考信息封装在同一个文件里的格式。MATLAB 读取它的标准做法是:
[A, R] = readgeoraster('LC08_20241105_B10.TIF'); % A 是二维灰度矩阵 % R 是地理栅格引用对象,保存了坐标转换所需的全部参数 size(A) R.RasterSizeR对象里最常用到的属性包括:LatitudeLimits和LongitudeLimits,对应整个栅格覆盖的纬度经度范围;CellExtentInLatitude和CellExtentInLongitude,对应每个像素代表的实际尺寸,单位是度;ProjectedCRS记录投影坐标系。凡是涉及“把像元位置换算成经纬度”的计算,都应该直接依赖 R,而不是自己写死角点坐标。同一类产品在不同版本中栅格边界经常微调,硬编码会让你的数据产品维护成本直线上升。
如果当前环境没有 Mapping Toolbox,readgeoraster会直接报错。这时可以用 MATLAB 内置的Tiff类退回读像素:
t = Tiff('LC08_20241105_B10.TIF', 'r'); A = read(t); close(t);这样拿到的仍是没有地理参考的纯矩阵,需要自己解析投影模型才能把像素位置对应到经纬度,非常容易出错。这里的建议很直接:要做卫星数据处理,先把 Mapping Toolbox 装上,后续省出的排错时间远远大于安装成本。
3.2 NetCDF 与 HDF5:ncinfo 定位变量,ncread 取数
NetCDF 和 HDF5 文件的难点在于“不知道读哪个变量”。先用ncinfo看顶层结构:
f = 'MODIS_Surface_Reflectance_A2004001.nc'; info = ncinfo(f); for k = 1:numel(info.Variables) fprintf('%12s | dims: %s\n', info.Variables(k).Name, ... strjoin(info.Variables(k).Dimensions.Name, ', ')); end注意ncinfo返回的Variables是结构体数组,每个元素包含Name、Dimensions、Attributes等字段。以 MODIS 地表反射率产品为例,数据变量通常叫sur_refl_b01、sur_refl_b02,经纬度要么漂在变量里,要么独立叫Latitude、Longitude。取数时还要顺带读变量属性,因为很多产品的缺省值不是纳姆(NaN),而是-9999,并且物理量本身带有缩放:
data = ncread(f, 'sur_refl_b01'); scale = ncreadatt(f, 'sur_refl_b01', 'scale_factor'); offset = ncreadatt(f, 'sur_refl_b01', 'add_offset'); % 先还原物理值,再屏蔽填充坏值 data = double(data) * scale + offset; data(data <= -9999) = NaN;HDF5 的读取逻辑类似,区别是变量路径要带 group 层级:
plist = h5info('AIRS_L2_20250101.h5'); % 先打印 plist. Groups 结构,确认变量所在路径 data = h5read('AIRS_L2_20250101.h5', '/standard/air_temp'); lon = h5read('AIRS_L2_20250101.h5', '/geolocation/longitude');先通过h5info查看 group 名称,再拼完整路径。直接用h5read猜路径失败率极高,因为不同产品对变量组织方式的约定差别很大。
3.3 .mat 卫星数据:load 之后先看层级再取字段
.mat 文件通常是卫星信号处理链路里的中间产物,比如一段时间的信噪比(SNR)序列。多数情况下load后直接暴露在工作区的并不是一个只有两层结构的变量,而是嵌套的结构体:
S = load('example_snr_series.mat'); disp(fieldnames(S));假设输出发现顶层变量名是track,而track内部又有Lat、Lon、Snr_dB、TimeUTC几个字段,顺序索引的意义就体现出来了。直接load有污染当前工作区的风险,把整个文件读进结构体S后,不仅能避免变量名覆盖,还能用isfield做防御性检查。如果 .mat 文件体积接近 1 GB 以上,不建议用load一次性载入,优先用matfile对象做切片读取,这个后文专门讲。
3.4 统一读取函数:按扩展名分发到不同解析器
为了避免每个脚本里都堆一堆if ... elseif ...,通常做法是把上面的分支集中到一个readSatFile函数里:
function out = readSatFile(f) %READSATFILE 按扩展名分发的卫星数据读取器 % 输出 struct,包含 raw、lon、lat、units [~, ~, ext] = fileparts(f); fprintf('读取 %s\n', f); switch lower(ext) case {'.tif', '.tiff'} [out.raw, R] = readgeoraster(f); out.lon = linspace(R.LongitudeLimits(1), R.LongitudeLimits(2), R.RasterSize(2)); out.lat = linspace(R.LatitudeLimits(1), R.LatitudeLimits(2), R.RasterSize(1)); out.units = ''; case '.nc' info = ncinfo(f); vname = info.Variables(1).Name; % 实际使用时应按产品显式指定 out.raw = ncread(f, vname); out.lat = ncread(f, 'lat'); out.lon = ncread(f, 'lon'); out.units = 'unknown'; case {'.h5', '.he5'} out.raw = h5read(f, '/standard/reflectance'); out.lat = h5read(f, '/geolocation/latitude'); out.lon = h5read(f, '/geolocation/longitude'); out.units = 'unknown'; case '.mat' S = load(f); flds = fieldnames(S); if isfield(S, 'snr') out.raw = S.snr; % 常见字段名,按实际项目调整 else out.raw = S.(flds{1}); % 保底:取第一个字段 end out.units = 'dB'; otherwise error('不支持的格式: %s', ext); end end这个函数的核心价值是把“每种格式怎么读”的知识收敛到一处。调用时:
d = readSatFile('D:\satellite_workspace\data_01.nc'); imagesc(d.raw);后续的批量循环、画图、统计都复用同一套输出结构。换数据集时只需调整这里面的变量名映射,而不需要动上百行的分析代码。实际项目里,这个函数的第二个迭代版本会加入_FillValue、scale_factor的自动处理,以及把维序统一成 lat × lon 的逻辑。
4. 从单景到时间序列:批量读取“一段时间的卫星数据”
真实业务里很快会面对这样一个场景:某个目录下连续存放了几十天甚至几个月的文件,每帧是一个样本,你需要把它们组装成一个时间序列,比如提取某段时间内的信噪比变化或逐像元反射率趋势。这一步的核心不是读取本身,而是“文件名解析 + 维度对齐 + 数据掩码”。
4.1 从文件名中解析观测时间
假设文件名是20250101_snr.nc这类带日期戳的命名,提取观测时间的做法:
dataDir = 'D:\satellite_workspace\snr_by_day'; files = dir(fullfile(dataDir, '*.nc')); obsTime = datetime(NaT(size(files))); for k = 1:numel(files) [~, baseName, ~] = fileparts(files(k).name); tok = regexp(baseName, '(20\d{2})(\d{2})(\d{2})', 'tokens', 'once'); if ~isempty(tok) obsTime(k) = datetime(str2double(tok{1}), str2double(tok{2}), str2double(tok{3})); end end % 剔除解析失败的文件 validTime = ~isnat(obsTime); files = files(validTime); obsTime = obsTime(validTime);正则(20\d{2})(\d{2})(\d{2})一次捕获年、月、日三段数字,datetime(Y,M,D)接收数值三元组生成datetime对象。解析失败的先保留为 NaT,最后统一过滤,避免循环中途判断影响代码可读性。如果文件名里确实没有日期,退而求其次用文件系统的修改时间,但要注意有些批处理任务会把同一天处理的文件写成“最近修改”,逻辑上不够严谨。
4.2 把多帧数据堆成三维数组或 timetable
拿到的是栅格数据时,用三维矩阵堆叠最直接。以 MODIS 地表反射率为例,每帧是nLat × nLon,几十天的数据就堆成nLat × nLon × nT:
% 先用第一帧确定尺寸 first = readSatFile(fullfile(dataDir, files(1).name)); nLat = size(first.raw, 1); nLon = size(first.raw, 2); nT = numel(files); cube = NaN(nLat, nLon, nT); for k = 1:nT d = readSatFile(fullfile(dataDir, files(k).name)); if isequal(size(d.raw), [nLat, nLon]) cube(:, :, k) = d.raw; else warning('第 %d 帧尺寸异常,已跳过: %s', k, files(k).name); end end % 逐像素计算多年均值 meanMap = mean(cube, 3, 'omitnan'); % 查看每个时刻的有效覆盖率 validCount = squeeze(sum(~isnan(cube), [1 2])); plot(obsTime, validCount, 'o-');cube预先用 NaN 填充,好处是后续所有统计天然带有掩膜。尺寸不一致是最容易翻车的点:卫星轨道宽度变化、裁切范围不同都会导致某些帧出现偏移。直接跳过用 warning 挑出来,比强行塞进去让拼接结果错位要好。如果后期需要更精细的对齐,可以用mapresize或imresize把栅格统一到参考网格,但做这一步之前先搞清楚到底是谁的坐标系发生变化。
对于信噪比这类的点位序列,不需要三维矩阵,用timetable更顺手:
T = timetable(obsTime, snrData, 'VariableNames', {'SNR'}); T = sortrows(T, 'Time'); % 把非均匀采样规整到小时 Th = retime(T, 'hourly', 'mean');retime支持'hourly'、'daily'、'minutely'等多种时间粒度,第二参数的聚合方式可以是'mean'、'min'、'max',也可以传函数句柄。
4.3 填充值、缩放因子与地理维序的三大坑
把读取流程做对,本质上是在和三类数据本身的问题做对抗:
填充值没处理。很多产品缺测区域写的是
-9999,直接mean会把时间序列拉低到离谱的程度。正确做法是先读_FillValue或missing_value属性,再把等值像素替换成 NaN,推荐把这步收进统一读取函数,而不是散落在各分析脚本里。scale_factor / add_offset 漏乘。MODIS 这类数据为了压缩体积,会以整数存储真实物理值,读取后必须执行
data * scale + offset。忘了这步,画出来的图明暗层次都在,但颜色条上的单位全错。这个错误最具迷惑性,因为图像看起来“正常”。lat 和 lon 的维度顺序。有些数据文件变量维度是 lon × lat,有些是 lat × lon,读取后到底要不要转置,取决于你后续怎么索引。建议在统一读取函数的输出中固定为 lat × lon,把这个决定做在源头:
% 假设 info 显示变量维序是 [lon, lat] % 则读取后转置一次,后续所有分析代码都按 lat × lon 处理 raw = ncread(f, 'reflectance')'; % 注意转置前要确保 lon 是第一维,避免误用这段转置逻辑同样应该收进读取函数,而不是靠每个脚本各自祈祷。
5. 卫星图读取之后的地理可视化与交叉验证
读对了数据还只是第一步。直接imagesc(A)得到的坐标轴是像素索引,图和地图之间没有任何映射关系。要“画出来像一张遥感图”,至少要把经纬度坐标编织进去。
5.1 worldmap + geoshow 画带经纬度的卫星图
Mapping Toolbox 里最常用的组合是worldmap设定制图范围和投影方式,geoshow绘制网格化数据:
lat = d.lat; lon = d.lon; A = d.raw; figure; worldmap([min(lat) max(lat)], [min(lon) max(lon)]); geoshow(lat, lon, A, 'DisplayType', 'texturemap'); % 叠加海岸线作为参照 coast = load('coastlines'); plotm(coast.lat, coast.lon, 'k', 'LineWidth', 0.5); colormap(parula); colorbar;worldmap(latlim, lonlim)创建一个带投影信息的地图坐标轴,默认投影适合中低纬度展示。geoshow的DisplayType设为'texturemap'时,二维矩阵会被当作纹理贴到经纬度网格上,图像自动处于经纬度坐标体系中。plotm是地图坐标系下的专用绘图函数,这里不能用普通plot叠加海岸线,否则坐标基准不统一点位会错位。输出图的横纵轴自动显示经纬度,颜色条对应物理值单位,这才是能拿去做报告的卫星图。
5.2 没有 Mapping Toolbox 时的替代画法
没有安装 Mapping Toolbox 时也有临时方案,只是没有投影变换能力,只能按经纬度线性展开:
figure; imagesc(lon, lat, A); set(gca, 'YDir', 'normal'); % 把纬度方向反转回北在上 xlabel('Longitude (°)'); ylabel('Latitude (°)'); axis tight; colorbar;imagesc(lon, lat, A)会自动把横轴映射到经度范围、纵轴映射到纬度范围,但 MATLAB 默认的 Y 轴是递增方向朝上,因此必须用set(gca,'YDir','normal')把南在上翻转为北在上。注意:这个方案只在数据本身没有做投影处理时才成立。如果数据产品是 UTM 投影,横纵轴代表的是东向和北向坐标,必须先把投影坐标转换为经纬度再作图,不能直接用像素或米值替代经纬度。
5.3 用锚点经纬度做读取结果的数值验证
画图只能看出数据大概形态,真正证明自己读对了的是数值验证。常见做法是选一个已知地理位置锚点,比如沿海站点或城市中心,查出经纬度,再读取卫星数据对应像素值,与外部参考值对比。
有 Mapping Toolbox 时可以直接用latlon2pix:
[row, col] = latlon2pix(R, anchorLat, anchorLon); row = round(row); col = round(col); pixelValue = A(row, col);latlon2pix的输入R是readgeoraster返回的地理栅格引用对象,纬度在前、经度在后。没有工具箱时,手工换算:
col = round((anchorLon - lon(1)) / (lon(2) - lon(1))) + 1; row = round((anchorLat - lat(1)) / (lat(2) - lat(1))) + 1;注意lat数组的方向问题。GeoTIFF 的纬度数组通常从北向南排列,lat(1)是北端最大值,此时手动计算row要把分子倒过来,或者直接用nLat - round(...)变换。这套换算的精度上限是半个像元,适合验证数据读取正确性,不适合做精确定量定位。比对时相对误差保持在千分位量级,基本可以确认整条读取链路没有系统性错误。这一步必须放在批量处理之前,而不是之后——数百万像素算到一半才发现坐标系反了,返工成本不可接受。
6. 进阶:给卫星数据读取结果加一个可回查的持久化收尾
前五章解决的是“从压缩包到数值”的链路,这章解决的是“如何让读出来的东西能反复用”。实际项目里经常遇到的情况是:原始数据在服务器上,脚本跑完退出工作区,第二天想换个参数重新分析,又要从头解压、读取、校验。这里有一个更高效的收尾方式:把第一次读取并完成基础处理后得到的结果连同元数据一起落盘,后续所有分析脚本直接基于这份中间产物运行。
6.1 用 matfile 做大文件的增量落盘
当数据总量达到几个 GB 时,save整个工作区会触发内存复制,非常容易把桌面端 MATLAB 顶到内存上限。换成matfile对象,按切片写入 v7.3 格式的 .mat 文件:
matObj = matfile('processed_satdata.mat', 'Writable', true); % 预先分配好维度,后续按帧写入 matObj.cube = NaN(nLat, nLon, nT, 'single'); for k = 1:nT matObj.cube(:, :, k) = squeeze(cube(:, :, k)); endmatfile不会把整个文件载入内存,写入时只操作对应磁盘区间。首帧写入前必须先通过赋值确定变量的维度,之后再逐帧覆盖指定切片,这样即使是 30 GB 的三维数组也能在普通工作站上跑完。读取时用matObj.cube(:, :, 5)拉取单个时间帧,RAM 占用始终可控。
6.2 用 timetable 统一时间戳并做重采样
第 4 章里提到过timetable,在持久化收尾阶段它是更合适的统一出口。把时间列、经纬度列、物理量列组装进同一个表对象,后续筛选、重采样和绘图都基于它完成:
T = timetable(obsTime(:), snr(:), lat(:), lon(:), ... 'VariableNames', {'SNR', 'Lat', 'Lon'}); T = sortrows(T, 'Time'); Th = retime(T, 'daily', 'mean');retime在带有 NaN 的时间戳上会返回 NaN,不会自动插值,这对卫星数据这种带云的观测来说反而是优点,能天然保留缺失区间。如果业务上必须填补,再加fillmissing显式指定插值方法,避免隐式填出一个“假趋势”。
6.3 一个收尾技巧:用函数句柄做读取分派表
最后一招是给前面写的readSatFile做一个更可扩展的升级:用containers.Map存储扩展名到函数句柄的映射,避免以后的格式分支都堆进同一个 switch:
readers = containers.Map(... {'.tif', '.nc', '.h5', '.mat'}, ... {@readGeoTIFF, @readNetCDF, @readHDF5, @readMAT}, ... 'UniformValues', true); function out = readGeoTIFF(f) [out.raw, R] = readgeoraster(f); out.lon = linspace(R.LongitudeLimits(1), R.LongitudeLimits(2), R.RasterSize(2)); out.lat = linspace(R.LatitudeLimits(1), R.LatitudeLimits(2), R.RasterSize(1)); end调用侧简洁为一个动作:
[~, ~, ext] = fileparts(fileName); d = readers(lower(ext))(fileName);以后新增格式,只要写一个新函数并在 Map 里注册,主流程代码完全不动。收尾阶段最后再执行一次whos('-file', 'processed_satdata.mat')核对落盘数据的变量名和维度,确认无误后再把中间文件接入下游分析。这套做法可以让整个卫星数据读取流程在几周后再回看时,仍然能够快速定位到“数据从哪来,格式怎么读,结果在哪里”。
本文还有配套的精品资源,点击获取