简介:面向高光谱遥感初学者和MATLAB编程者,这份资源将真实高光谱图像与自编读取程序打包在一起,针对TIFF多波段数据不知如何打开、解析和可视化的问题,提供了一条完整的入门路径。压缩包共6个文件,总大小约11.11MB,包含典型的高光谱图像样例、可运行的MATLAB读取脚本、配套说明文档,以及hdr、dat、enp等头信息与辅助数据文件,各文件用途清晰,便于对照学习。目前已有3153人浏览学习。通过运行和研读该脚本,读者可以理解高光谱图像几十个波段在内存中的组织方式,掌握从原始DN值到多维数组的转换过程,并进一步完成假彩色合成、光谱曲线绘制和主成分分析等常用操作;同时,结合readme与头文件还能了解波段范围、空间分辨率等元数据含义,为后续地物识别和遥感定量分析打下扎实基础,适合课程实验、毕业设计或自学入门。
1. 高光谱图像和Matlab读取程序:为什么读取这一步劝退了大多数人
很多人拿到高光谱图像后,第一反应是直接用 imread 读,然后发现报错,或者读出来是一堆数字却怎么都拼不成图像。高光谱图像和普通图像最大的区别是它本质上是三维数据块:两个空间维加一个光谱维,每个像素点都对应一条连续的光谱曲线。Matlab 读取程序这件事,难点不在函数调用,而在于你得先搞清楚数据是 BIP、BIL 还是 BSQ 格式,字节序是 little-endian 还是 big-endian,有没有头文件,头文件里写不写波长信息。这些问题不弄清楚,代码写得再漂亮也读不出对的东西。
这篇文章适合需要处理机载高光谱、地面成像光谱仪或者实验室高光谱相机数据的从业者,无论你是做遥感反演、食品品质检测还是医学高光谱分析,读取这一步都是绕不开的。我会直接把常见的几种格式、对应的 Matlab 读取方案、参数怎么改、以及我这些年踩过的坑和排查思路全部梳理清楚,让新手能照着跑通第一个程序,让熟手能少走弯路。
2. 先看懂高光谱数据的三维结构:波段顺序和存储布局的底层差异
2.1 从二维图像到三维立方体:为什么不能直接用 imread
普通彩色图像在 Matlab 里是一个 m×n×3 的数组,三个通道分别是 R、G、B,含义明确。高光谱图像则是 m×n×L,L 通常从几十到几百不等,每个波段对应一个较窄的光谱区间。比如常见的高光谱相机可能有 128 个波段、270 个波段甚至更多,每个波段的中心波长都不一样。数据本身没有变复杂,变复杂的是你需要在读取时把波段维对应的波长信息一并恢复出来——否则你拿到的是一个没有刻度标尺的数字立方体,根本无法做光谱分析。
这里有个很反直觉的地方:市面上很多高光谱数据的原始存储不是按照「先空间后波段」的自然顺序存放在二进制文件里的,而是为了硬件读写效率,采用了交错存储。也就是说,同样一个 m×n×L 的数据,在磁盘上可能是先存一个像素的所有波段,再存下一个像素;也可能是先存一整行所有像素的所有波段;还可能是每个波段单独一整块存。这三种排列方式就是常说的 BIP、BIL 和 BSQ。读取之前不确认排列方式,轻则读出来的图像错位,重则直接把相邻波段的数据混淆在一起。
2.2 三类存储布局和头文件:决定读取代码怎么写
BIP(Band Interleaved by Pixel)方式是每个像素点连续存储所有波段数据,读一个像素就能拿到该位置的完整光谱曲线,适合光谱维操作,比如找某个像素的特征光谱。BIL(Band Interleaved by Line)方式先存第一行所有像素的第一个波段,再存第一行所有像素的第二个波段,适合按行处理的算法。BSQ(Band Sequential)方式每个波段单独存一整块区域,读取某个波段的全图效率最高。
还有一个经常被忽略的点:Matlab 读取程序要从二进制文件中把数据还原成 m×n×L 的三维数组,必须知道原始数据的位深和字节序。大多数高光谱相机输出的是 uint16 位深,少数是单精度浮点,位深写错会让灰度范围整整差 256 倍。字节序写错会导致每个像素值发生字节交换,图像看起来像噪声。这些信息通常在数据配套的头文件或者说明文档里,但并不是所有设备厂商都会把内容写得规规矩矩。
| 存储布局 | 顺序特征 | 适合读取的维度 | 常见来源 |
|---|---|---|---|
| BIP | 像素优先,逐像素存所有波段 | 光谱维(单点光谱) | 部分地面成像光谱仪 |
| BIL | 行优先,逐行存所有波段 | 行处理 | 部分机载传感器 |
| BSQ | 波段优先,逐波段存整幅图 | 单波段大图读取 | ENVI 格式常用 |
我一般拿到陌生数据的第一件事不是写读取函数,而是用一个十六进制查看命令先看二进制文件的前几个字节,初步判断位深和数据量是否吻合。文件名有时能给出线索——比如 .raw 后缀可能没有头文件,.dat 后缀可能对应某个 SPECIM 或 HySpex 的已知格式,但这些只能作为参考,不能当作依据。
3. 用 Matlab 读取高光谱图像:三种主流格式的完整读法
3.1 从 ENVI 标准格式开始:用 multibandread 读懂二进制
ENVI 是美国 ITT Visual Information Solutions 公司提出的标准格式,高光谱数据处理里最常见,几乎所有设备厂商都会提供转换成 ENVI 格式的选项。ENVI 格式由一个二进制数据文件和一个 .hdr 头文件组成,数据文件里存储的是原始像素值,头文件里包含波段数、行数、列数、存储布局、位深等关键信息。实际读取时可以用 Matlab 自带的 multibandread 函数,它像是一个专门为这种场景设计的工具。
% 参数按 .hdr 头文件内容填写 hdr_info = multibandread('img.dat', ... [rows, cols, bands], ... % 行数、列数、波段数,取自头文件 'uint16', ... % 位深,常见 uint16 0, ... % 跳过偏移量,有头文件时通常为 0 'bil', ... % 存储布局:bil / bip / bsq 'ieee-le'); % 字节序:little-endian 或 big-endian上面这段代码是核心流程:第一个参数是数据文件的路径,第二个参数是数据立方体的尺寸,第三个参数是原始数据的类型,第四个参数表示数据开始前的偏移字节数,第五个参数指定存储布局,第六个参数指定字节序。这些值都必须和头文件保持一致,任何一个参数不对,读出来的结果都会是乱的。行数和列数以头文件里写的内容为准,不要自己去猜图像的尺寸。
ENVI 的头文件可以用文本编辑器打开,里面有一行叫 samples、lines、bands,分别对应列数、行数、波段数。还有一行叫 interleave,写的是 bip、bil 或 bsq。data type 字段对应数值类型,1 是 uint8,12 是 uint16,13 是单精度浮点。byte order 字段如果为 0 表示 little-endian,为 1 表示 big-endian。要注意的是,头文件本身不保存波长信息,如果设备没有生成单独的 .wvl 文件,波长数据可能需要从其他来源获取。
3.2 HDF5 格式:用 h5read 直接读取时注意把属性带上
很多新型高光谱相机直接把数据保存为 HDF5 格式,好处是数据集、属性和元数据都放在同一个文件里,不会出现 ENVI 那种数据文件丢失头文件就变成裸二进制的情况。HDF5 文件有一个内置的层级结构,路径类似 /data、/data_spec 之类,里面的 Dataset 是实际数据,Attribute 则存放波长、曝光时间等元数据。
% 查看文件内部结构 info = h5info('hsi_image.h5'); disp(info.Datasets); % 查看有哪几个数据集 disp(info.Datasets.Attributes); % 查看数据集的属性 % 读取数据 hsi_data = h5read('hsi_image.h5', '/data'); wavelengths = h5readatt('hsi_image.h5', '/data', 'Wavelength');这段代码有两个关键点:第一个 h5info 是在读取之前先看一下文件内部结构,因为不同厂家的 HDF5 路径名差异很大,有的叫 /Image,有的叫 /data/HSI,如果路径猜错会直接报错。第二个 h5readatt 是把波长信息读出来,这一步很多人会漏掉,回去只读数据不读属性,后续做光谱分析时才发现没有波长坐标,被迫重新读文件。建议拿到文件后先把 h5info 的结果完整过一遍,把 Dataset 的主路径和 Attribute 的字段名都记录下来,再决定怎么读取。
有些设备的 HDF5 文件的维度顺序是 bands × rows × cols,和 Matlab 习惯的 rows × cols × bands 正好相反。读取之后需要做一次 permute 转换:hsi_data = permute(hsi_data, [2 3 1])。检查维度顺序的方法很简单:读出来之后看看数组各个维度的大小,然后结合已知的空间尺寸判断哪个是行、哪个是列。
3.3 厂商私有格式:先导出再读取,这是最省事的路径
很多设备的原始格式是厂商私有的,比如含有特定压缩算法或者附加了 GPS 定位信息。Matlab 没有内置函数能解析这些私有格式,最常见做法是先把数据在设备自带软件里导出成 ENVI 或 HDF5 格式,再交给 Matlab 处理。有人问过能不能直接读原始文件,我通常会反问:厂商的软件吃干饭的吗?能导出就导出,省得为个别格式写解析器。
还有一种情况是数据已经是 .mat 文件,里面可能是经过预处理的矩阵,变量名可能是 hsi、data、cube 之类。直接 load 之后先确认变量尺寸和类型,如果是三维数组且尺寸合理,那已经可以当作用例来用了。如果里面还带了一个波长向量,那就更省事。
4. 应付格式不标准的图像数据:写一个自动适配的 Matlab 读取函数
4.1 为什么格式不标准的情况越来越多,怎么收集元数据
高光谱图像在实验室里常见的应用是把多个样品拍成一批数据,输出到同一个文件夹里,里面有 .dat 文件、.hdr 文件、还有 .txt 配置文件。不同实验条件下,同一个文件夹里的数据可能是不同波段数、不同位深。特别是做了明场、暗场校正之后,有些设备会额外输出一版反射率数据,格式从整型变成浮点型,如果程序还是按 uint16 去读,就会读出一个空白画面或者全是噪声的画面,这时就需要自动适配。我的做法是先写一个读取头文件并提取关键字段的函数,再根据这些字段动态设置 multibandread 的参数。
function hsi = read_hsi(dat_path, hdr_path) % 读取 ENVI 头文件关键字段 txt = fileread(hdr_path); samples = regexp(txt, 'samples\s*=\s*(\d+)', 'tokens', 'once'); lines = regexp(txt, 'lines\s*=\s*(\d+)', 'tokens', 'once'); bands = regexp(txt, 'bands\s*=\s*(\d+)', 'tokens', 'once'); % 解析存储布局 if contains(txt, 'interleave = bip') interleave = 'bip'; elseif contains(txt, 'interleave = bil') interleave = 'bil'; else interleave = 'bsq'; end % 解析位深 if contains(txt, 'data type = 12') dtype = 'uint16'; else dtype = 'uint8'; end % multibandread 读取 hsi = multibandread(dat_path, ... [str2double(lines{1}), str2double(samples{1}), str2double(bands{1})], ... dtype, 0, interleave, 'ieee-le'); end这里有几个解析细节需要说明:samples 和 lines 的顺序容易搞反,头文件里 samples 对应的是列数,lines 对应的是行数。regexp 提取出来的是文本,要经过 str2double 转成数字才能用于数组维度。contains 判断是否存在某个字符串时,要注意头文件里可能有多行注释也包含 data type 字符串,所以判断优先级要放在靠后的位置,或者先按行拆分再匹配。
这种自动适配函数不适合所有场景,比如字节序如果是 big-endian 时,代码就需要再增加一个判断分支。但是把它作为程序的第一版没有坏处,因为大部分实验室设备数据都是 little-endian。
4.2 批量读取时注意内存上限,用数据分块而不是一把梭
高光谱图像的尺寸普遍很大,一幅 1000×1000×200 的 uint16 数据占用的内存接近 400MB,如果一次批量读取二十个场景,直接全部塞进内存只会让系统卡死。我一般会先计算好单幅图像的预期大小,批量读取时采用边读边处理的方式,把读取和计算放在同一个循环里,而不是先把所有数据收集到一起再做后续操作。如果算法必须用全量数据,那就分成多个批次,每次只保留一个场景的结果,而不是保留原始数据。
内存问题可以通过一个简单的公式判断:行数 × 列数 × 波段数 × 每个像素字节数 ÷ 1024 ÷ 1024 就是单幅图像的 MB 数。uint16 是 2 字节,float32 是 4 字节。如果计算出来的单幅超过 1GB,就要考虑用 tall 数组或者分段读取。multibandread 支持只读取部分波段,通过设置读取的波段范围就能来降低内存占用。
4.3 如果只有二进制没有头文件,怎么恢复正确的读取参数
有些旧数据的头文件丢失了,只留下一堆 .dat 文件,这时候读取就变成了一个侦探过程。第一步是查看文件大小,然后尝试确定尺寸。比如文件大小除以 2 是 2600000,可能的尺寸有 1000×1300×2、650×2000×2,还要结合波段数去验证。很多时候需要根据设备型号的已知信息来猜波段数,比如某设备的波段数固定是 320,那么空间维尺寸就是文件大小除以 320 再除以 2 字节。这一步没有万能公式,完全依赖你对设备的了解程度。
另一个技巧是通过统计二进制文件的数值范围来判断位深:uint16 的最大值是 65535,读取时如果发现所有值都不超过 255,有可能是 8 位数据;如果数值全是接近 3 开头的浮点数特征,那很可能是单精度浮点。也可以用脚本读取前 100 个字节,然后尝试不同的位深去展示图像灰度分布,肉眼判断哪一种是正常图像。这个过程不容易自动化,但实践经验越多,判断越准。
5. 读取高光谱图像避坑指南:5 个我踩过的常见问题
5.1 读出来的图像全是噪声条纹,原因可能是字节序填错
现象:读取后显示图像时,图中大量出现横条纹或斜纹噪声,看不出任何物体轮廓。此时如果用 imagesc 显示单个波段,画面像一幅抽象条纹画。这个现象最容易出现在跨平台传递的数据中,Windows 上生成的数据是 little-endian,在 Linux 或者某些老旧设备上可能是 big-endian。如果 multibandread 的字节序参数填反,每个 16 位值都会发生高低字节交换,原本的灰度值就变成了乱序的数字。
解决:先检查 .hdr 头文件里的 byte order 字段,然后对比读取结果的直方图。正确读取的数据直方图通常集中在某个区间,而且空间上有连续性;错误字节序的数据直方图几乎均匀分布。把字节序参数从 'ieee-le' 改成 'ieee-be' 再重新读,就能解决。
5.2 数据维度怎么看都不对,波段顺序恰好是反的
现象:读入后数据的尺寸是 128×512×640,但你知道空间维应该是 640×512。仔细一看会发现,把数据 permute 之后空间维是对的。这其实是 HDF5 格式的常见布局差异,很多设备的 SDK 按 bands × rows × cols 存储,导入 Matlab 后就需要转换维度顺序。
解决:读完后先打印 size,然后用 permute 调整维度顺序。permute(data, [3 2 1]) 可以把 bands 维放到最后,这样数据就成了 rows × cols × bands。如果你不确定调整正确与否,可以取一个波段绘制图像,看图像是横的还是竖的,再判断需要哪种 permute。
5.3 头文件编码有问题,contains 匹配总是失败
现象:读取像素尺寸时返回空值,程序报错。很多 ENVI 头文件是从设备软件里导出的,编码可能是 ANSI 或者 GBK 而不全是 UTF-8,尤其在国内设备的场景下,说明文字使用中文字符导致编码转换异常。fileread 在某些情况下对非 UTF-8 编码的支持不完美,读取结果里出现乱码字节,导致 regexp 匹配失败。
解决:先用 fopen 加上 'n' 参数或者用 webread 读成字节,再转换成目标编码。也可以直接用 fopen + fgetl 逐行读取,不依赖 fileread。还有一种更简单的思路:如果头文件里有中文字段,绕开它,只解析数值字段,匹配时用松弛的正则表达式比如 'samples\s*=\s*\d+',允许前后空格变化。
5.4 读出来的图像出现亮斑或坏线,不全是读取程序的锅
现象:图像大部分正常,但某几个像素或者某几列亮度异常。一开始以为是读取代码的问题,反复调整参数也没变化。后来才发现是相机本身的坏像素或未做暗场校正导致的。高光谱数据处理流程里,暗场校正刚开始没做,CCD 传感器的暗电流信号积累在图像里,表现就是局部亮斑。这不是读取函数的错误,是预处理链路缺失。
解决:先做一个简单判断,如果是坏像素,位置在不同波段上是固定的,而且通常在图像边缘或传感器死区;如果是电路噪声,会表现出周期性条纹。然后分别按两种路径处理:坏像素用中值滤波替代,暗电流用全黑帧做减法。注意这两种校正应该在读取完成后立即做,不要拖到光谱分析阶段再处理。
5.5 打不开 .hdr 文件或者报错 out of memory
现象:读取数据时出现内存不足。1000×1000×200 的 uint16 数据约占 400MB,这在现代电脑上不算大问题,但如果同时加载多个变量,或者配合机器学习工具箱使用,内存占用会直线上升。Matlab 默认数组是 double 类型,如果被强制转成 double,内存会翻倍,uint16 转 double 等于 400MB 变成 1.6GB。
解决:尽量保持数据类型为 uint16 或单精度浮点,不要轻易转 double。如果确实要转,也要等完成归一化或反射率转换之后再进行。归一化这一步如果用 double 计算,建议先计算一个波段做验证,避免整幅数据一次性转换超出内存上限。
6. 把读取程序封装成个人习惯,让后续分析少走弯路
经过选型、编写、调试、排错这一圈,读取高光谱图像和 Matlab 读取程序这个方向的基本框架已经理顺了。但最后我还想补一个回购率很高的技巧:把读取、预览和转存封装成一个固定的管线,每次都走同一套流程。我自己这三年所有的高光谱数据处理都是以一个标准脚本开始的,不管数据来自哪台设备,第一步永远是把原始数据转成内存里的三维数组,顺便把波长信息也提取好,然后保存成一份带波长说明的 .mat 文件。以后再分析,直接 load 这个 .mat 就够了,不需要重复处理原始数据。
% 读取后立即执行的最小流程 hsi_raw = read_hsi('data.dat', 'data.hdr'); % 自己封装的读取函数 hsi_norm = double(hsi_raw) / 65535; % 归一化到 0-1 wavelengths = load('wavelength_200.txt'); % 波长文件单独保存 save('scene01.mat', 'hsi_norm', 'wavelengths', '-v7.3');这段代码综合了这个章节的要点:先读取,再归一化,最后连同波长一起存档。关键在最后的 -v7.3 参数,当数据超过 2GB 时,只有这个版本的 MAT 文件格式能正常保存,其他版本会直接报错。波长文件单独保存是因为头文件不携带波长场景很常见,单独一个 txt 或 wvl 文件更可控。
这里还有一个小技巧:预览时不要直接 imagesc(hsi(:,:,1)),因为单波段图像灰度范围可能很窄,看起来全是黑的。先做一次拉伸显示或者选 3 个代表性波段合成假彩色图,能更快判断数据是否正常。我习惯用这段代码来快速预览:
% 假彩色预览:选三个波段分别作为 R、G、B r = hsi_norm(:,:,50); g = hsi_norm(:,:,60); b = hsi_norm(:,:,70); rgb = cat(3, r, g, b); imshow(rgb, []);波段选择不是固定的,可以根据目标物在不同波长处的差异来决定。比如水体场景选近红外波段,植被检测选红边波段附近。用这种预览方式,每次读取完毕扫一眼假彩色图,就能确认图形是否符合预期,不用静态去看单波段的灰度图。我通常会在调色板上分别确认每个波段的均值,若明显异常,就立刻回查头文件参数。
最后回来说一句心法:读取程序不是数据处理的配角,它决定了整个上游管线是否可靠。数据读入有误,后面无论做多少分析都是白费。把这个目录走顺了,后面的高光谱分类、目标识别、光谱匹配才有根基,希望这篇总结能帮到你。
本文还有配套的精品资源,点击获取