☰
MATLAB读取ENVI高光谱数据:HDR解析、interleave与分块读取
2026/10/28 16:15:22 网站建设 项目流程

简介:一份面向MATLAB使用者的HDR高光谱图像数据集读取与处理完整实践资源,覆盖从数据导入、辐射定标、光谱平滑、维度压缩到光谱分类与异常检测的典型流程,适用于遥感、精准农业、医学影像及地质勘探方向的研究者与中高级学习者对照练习。压缩包共11个文件,大小22.2MB,包含hdr与dat格式的原始高光谱数据、tif图像、enp辅助文件、可直接运行的hsi_read.m读取脚本以及txt说明文档,zbak为备份文件,结构清晰便于按需调用。目前已有47人学习下载。借助配套的HDR数据集与示例代码,用户可快速掌握三维高光谱数组的空间-波段组织方式,并实践基于hypercube对象的交互式波段浏览、单波段显示与伪彩色合成;同时结合K均值聚类、支持向量机等算法完成像元分类,通过主成分分析实现降维,并利用交叉验证与混淆矩阵评估模型效果,为深入挖掘光谱信息提供完整可复用的参考流程。

1. 你拿到的高光谱数据集不是.mat:HDR头文件加二进制才是真身

做MATLAB图像处理久了,第一次接触高光谱数据集的人都会有个固有印象:下载一个Indian Pines或者AVIRIS的压缩包,里面躺着.hdr和.raw两个文件,顺手load('xxx.hdr')就完事了。结果要么提示文件格式不支持,要么读出一堆意义不明的数字。原因很简单:遥感/高光谱领域的绝大多数公开数据集都采用ENVI标准存储,.hdr是一个纯文本头文件,里面写的是samples、lines、bands这些元信息,真正的像素灰度和反射率全在配套的二进制数据文件里。这套“头文件+二进制”的玩法,跟你平时处理的.jpg、.mat完全是两条路。

好在整个读取链条并不复杂:解析头文件、按字节读取二进制、根据interleave重排维度、按data type做数值缩放。这篇文章就把这条链路的每一步拆开讲,从手写hdr解析器开始,到一个能直接复用的read_envi_cube函数,再到坏波段剔除、反射率换算,最后给出超大文件的分块读取方案。适合刚拿到高光谱数据集、想在MATLAB里做分类或异常检测的从业者,也适合想把手头.mat专用脚本改成通用ENVI读取器的熟手。

2. ENVI hdr头文件解读:六个字段决定你能不能读对

2.1 interleave到底是什么:BSQ、BIP、BIL三种排列方式

打开任何一个.hdr文件,里面都是一长串key=value文本:先是ENVI description,然后依次是samples、lines、bands、data type、interleave、byte order。samples是每一行的像元数,lines是行数,bands是波段数,这三个数直接决定最终立方体的三维尺寸是lines×samples×bands还是bands×lines×samples。真正卡住大多数人的不是这三个数,而是interleave字段。它描述波段在一个一维二进制流里是怎么排列的,分成BSQ(Band Sequential)、BIP(Band Interleaved by Pixel)、BIL(Band Interleaved by Line)三种。

为什么遥感领域绕了这么多年还绕不开ENVI这套老格式?因为高光谱数据的特征是波段数大、单波段动态范围高,ENVI这种开放文本头加裸二进制的设计,让任何语言用最基础的fopen、fread就能打开,完全不需要依赖大型SDK。MATLAB自带工具箱里找不到现成的读取器,恰恰是因为这个格式太过“裸”,每个数据集甚至可能用不同的data type和缩放因子。

BSQ最直观,整个文件先写波段0的所有像元,再写波段1的所有像元,波段之间连续。想单独抽某几个波段,直接按bandlinessamples算偏移量跳到对应位置即可。BIP则把波段打散到像元内部,文件里第一个单位是像元(0,0)的全部波段,接着是像元(0,1)的全部波段,这种排列在像素级运算时缓存友好,很多机载传感器实时处理用BIP。BIL介于两者之间,每一行内先写波段0的一整行,再写波段1的同一行,然后换下一行。

三种排列在磁盘上占用空间完全一样,差别只在顺序。MATLAB的fread读出来是一条按列优先排列的一维向量,想让这条向量变回正确的三维高光谱立方体,必须知道原始文件是这三种里的哪一种。否则即使reshape的维度数字是对的,数据也是乱的,热力图会看到明显的条纹状错位。说句实在话,很多群里所谓的“读取失败”根本不是二进制损坏,就是interleave选错。

2.2 手写解析器:把hdr文本变成MATLAB的struct

解析hdr文件不需要第三方工具箱,MATLAB的regexp足够处理这类纯文本。常见做法是把整个文件读成字符串,对samples、lines、bands、data type、interleave、byte order这六项逐一匹配等号后的内容。我一般会写成下面这个函数,返回一个struct,后续所有读取逻辑都从它取参数:

function meta = parse_envi_hdr(hdr_file) % PARSE_ENVI_HDR 解析ENVI标准头文件 % 输入: hdr_file - .hdr文件路径 % 输出: meta - 结构体,包含samples/lines/bands/interleave等字段 fid = fopen(hdr_file, 'r'); raw = fread(fid, '*char')'; fclose(fid); result = struct(); key_map = {'samples', 'lines', 'bands', ... 'data type', 'interleave', 'byte order'}; for i = 1:length(key_map) key = key_map{i}; pattern = [key, '\s*=\s*([^\n]+)']; tokens = regexp(raw, pattern, 'tokens'); if isempty(tokens) continue; end val = strtrim(tokens{1}{1}); if ~isempty(regexp(val, '^[0-9\s,]+$', 'once')) result.(matlab.lang.makeValidName(key)) = str2num(val); %#ok<ST2NM> else result.(matlab.lang.makeValidName(key)) = val; end end meta = result; end

这段代码先检查等号后面的值是否全由数字、空格、逗号组成,是则转成数值数组,否则保留字符串。data type、samples是纯数字,interleave、byte order则需要区分对待:byte order是0或1这种整数,interleave是BSQ/BIP/BIL字符串,分别处理能避免后续读取时类型混乱。需要特别说明的是matlab.lang.makeValidName的作用:data type中间有空格,直接result.data type = 这种写法会报错,所以统一转成dataType这样的合法字段名。

拿到meta以后,实际数据集里最常见的参数组合长这样:

字段典型值说明
samples100~1000每行像元数,对应图像宽度
lines100~1000图像行数
bands100~200波段数,决定光谱维度
data type4 或 124为float32,12为uint16
interleaveBSQ / BIP / BIL波段交错方式
byte order0 或 10为小端,1为大端

字节序这栏很多人直接忽略,但它在后面是“图像数值异常”的高频元凶之一。x86平台和Windows上的MATLAB默认小端,如果hdr里写着byte order=1,二进制文件就是大端,必须在fread里显式指定机器格式,否则读出来的uint16高低字节互换,数值直接差一个量级。除了这六个核心字段,实际hdr里还可能见到wavelength、wavelength units、default bands。wavelength对波段标定很重要,做植被指数或矿物识别时需要读出来,default bands只是ENVI默认合成波段的显示设置,对数据分析没有实质影响,解析器里用同样的regexp模式一并提取就行。

3. 写一个通用的read_envi_cube:适配三种interleave和常见数据类型

3.1 主函数实现:从fread到reshape与permute

有了meta,接下来就是读数据主力。第一步用fopen打开二进制文件,fread一次性读出所有字节;第二步把一维数组reshape成临时矩阵;第三步用permute把它调整成统一的bands×lines×samples顺序。三种interleave的reshape策略完全不同,核心逻辑都放在下面这个函数里:

function cube = read_envi_cube(hdr_file, data_file) % READ_ENVI_CUBE 读取ENVI格式高光谱数据 % 返回三维数组 cube: bands × lines × samples % 内置uint16/float32等常见类型映射 meta = parse_envi_hdr(hdr_file); type_map = containers.Map( ... {1,2,3,4,5,12,13,14}, ... {'*uint8','*int16','*int32','*float32','*double', ... '*uint16','*uint32','*int64'}); type_str = type_map(meta.dataType); fid = fopen(data_file, 'rb'); [raw, count] = fread(fid, inf, type_str, 0, 'l'); fclose(fid); samples = meta.samples; lines = meta.lines; bands = meta.bands; expected = samples * lines * bands; if count ~= expected error('二进制字节数(%d)与头文件声明(%d)不符', count, expected); end interleave = upper(meta.interleave); switch interleave case 'BSQ' tmp = reshape(raw, [samples, lines, bands]); cube = permute(tmp, [3, 2, 1]); case 'BIP' tmp = reshape(raw, [bands, samples, lines]); cube = permute(tmp, [1, 3, 2]); case 'BIL' tmp = reshape(raw, [samples, bands, lines]); cube = permute(tmp, [2, 3, 1]); otherwise error('未知interleave: %s', interleave); end end

这里几个关键点必须展开说。fread的第四个参数0表示跳过字节数,第五个参数'l'显式指定小端,byte order=1时改成'b'即可,比依赖系统默认值稳得多。count是实际读到的元素个数,拿它和sampleslinesbands对账:头文件字段写错或文件本身不完整时,这里第一个报错,比后面取数据时提示维度错误好排查得多。type_map是containers.Map用法,key是ENVI定义的data type数值,value是MATLAB fread的精度字符串,range里没有覆盖6、9这两个复数类型,因为高光谱数据集基本用不到。

reshape和permute的组合值得记一下。BSQ时先把raw按[samples, lines, bands]展开,这里samples放最前面是因为MATLAB列优先,一维向量前samples个元素正好是波段0第一行;然后permute(cube,[3,2,1])把第一维和第三维对调,得到bands在前。BIP时波段维变化最快,所以reshape的第一维写成bands,对应临时矩阵[bands, samples, lines],permute成[1,3,2]。BIL时sample变化最快,然后才是波段,reshape成[samples, bands, lines],permute成[2,3,1]。如果记不住,回去看第2章的排列解释,对照着推导一遍就通了。

3.2 数据类型映射:为什么uint16不能直接当double用

ENVI的data type字段有十几种取值,实际高光谱数据集里遇到最多的就是4(float32)和12(uint16)。float32对应浮点反射率,比如0.1254;uint16存的是缩放后的整数,比如1254。两者差了一个10000倍的量纲。读取时如果忽略差异,直接把uint16当float32解释,画面会变成雪花噪点或者大片死黑。

上面type_map里我把5映射成了'*double',但注意fread返回的始终是一维数组,uint16场景下拿到的就是uint16类型。uint16在MATLAB里做减法很容易溢出回绕,比如uint16(10)-uint16(20)不会等于-10,而是接近65526。所以读完数据后,第一步永远是转double。我自己习惯在read_envi_cube函数外单独做这一步,因为归一化策略依赖数据源,放进主函数反而不灵活:

if ~isa(cube, 'double') cube = double(cube); end

转到double后立刻做一次归一化,把数值压到0~1区间,方便后续imshow和算法输入。对float32数据通常是除以最大值或除以10000,对uint16是除以10000或65535,具体看第4章的反射率换算部分。加载完成后,验证读取是否正确的第一件事,是取第一个波段和中间波段各画一张图:

figure; subplot(1,2,1); imagesc(cube(1,:,:)); axis image; colorbar; title('波段1'); subplot(1,2,2); imagesc(cube(round(size(cube,1)/2),:,:)); axis image; colorbar; title('中间波段');

如果显示出来有清晰的地物轮廓而不是横条纹,说明reshape和permute的顺序对了;如果全是噪点,优先怀疑interleave,其次是byte order。这一步不花时间,但能在第一时间排除掉后面所有分析的变量。我每次拿到新数据集都先跑这个过程,避免在一个错误的数据上浪费后面整天的精力。

4. 预处理三板斧:坏波段剔除、归一化与反射率换算

4.1 坏波段筛查:用方差和热力图排除全零与水汽波段

高光谱数据不是每个波段都可信。传感器坏像元、暗电流响应不稳定、大气水汽吸收带,都会让某些波段变成全零或者全是噪点。这些波段如果混进分类或异常检测,会把均值、方差全部带偏。常规做法是先对整个立方体做逐波段的方差统计:

band_var = squeeze(var(cube, 0, [2 3])); band_mean = squeeze(mean(cube, [2 3])); bad_idx = (band_var < 1e-6) | (band_mean < 1e-6); fprintf('疑似坏波段数量: %d\n', sum(bad_idx));

var和mean的第二个维度参数[2 3]表示在lines和samples两个维度上聚合,MATLAB从R2018b开始支持这种向量维度的写法。低于阈值的波段大概率是全零或者死值。我一般把bad_idx再叠加上一个“极端值占比”条件,统计每个波段有几个像元超过均值加6倍标准差,占比超过5%就视为噪点层。这个阈值属于经验值,不同传感器会有差异,但在多个公开数据集上表现稳定。

只看数字还不够直观,建议把band_mean画成一条随波段变化的曲线。水汽吸收波段会在曲线里形成几个明显的尖峰凹谷,连续几条全零会在曲线上变成一个平台。曲线配合单波段热力图确认,比单看一组阈值更不容易误判。我一般会在这一步把坏波段索引存成一个.mat文件,后续所有波段子集操作都带着这个掩码走,避免每次重新计算。水汽波段集中在1400nm和1900nm附近这个规律,在多数VNIR-SWIR传感器数据里都能看到,筛查时心里有数。

4.2 归一化与反射率换算:0-1区间还是0-10000整数

坏波段筛掉后,处理的就是量纲问题。ENVI对反射率数据的存储习惯有两种:float32直接存0~1的小数,比如0.3152;uint16存放大10000倍的整数,比如3152。也有存放大65535倍的,但主流是10000。判断标准很简单:读出来如果是uint16,先查统计值,max超过1就基本确定是缩放过;再查hdr里有没有reflectance scale factor字段,有就直接用这个因子。如果没有,默认先除以10000,再看光谱曲线是否符合典型地物特征。

if isa(cube, 'uint16') || max(cube(:)) > 1 cube = double(cube) / 10000; end

除以10000之后还有一个细节:辐射定标的边界效应会产生负值或大于1的异常点。稳妥做法是先裁到[0,1]区间,再做一次min-max归一化把动态范围拉开:

mn = min(cube(:)); mx = max(cube(:)); cube_norm = (cube - mn) / (mx - mn);

min-max归一化适合作为特征输入,但它改变了光谱的相对关系:原来0.3和0.4的差异,归一化后不一定是原来的比例。如果后续要做光谱角匹配或波段比值,应该直接用反射率原值除以10000的结果,保留波段之间的相对大小关系。这里需要单独提醒一种数据:有些数据集存的是辐射亮度DN值,不是反射率。DN值没有统一量纲,不同传感器之间不可比,这时只做[0,1]归一化,不做除以10000。判断依据是hdr里有没有reflectance字样,或者数据集文档是否写明做过辐射定标。三种存储形态的差异我整理在下表:

存储形态典型data type典型数值范围处理方式
反射率浮点4 (float32)0~1直接使用
反射率缩放整数12 (uint16)0~10000除以10000
辐射亮度DN值12 (uint16)0~65535min-max归一化

对应的实际代码路径是:先判断类型和数值范围,再决定除以10000还是直接min-max。这个判断写成一个函数返回数据源类型,比每次新数据集都要手动试一次省心得多。

5. 避坑指南:读取高光谱数据集的五个翻车现场

5.1 维度错乱、图像颠倒、内存溢出:三个最典型的故障排查

第一条是interleave选错。现象:imagesc(cube(1,:,:))显示的不是地物图像,而是均匀的横向条纹或者像被打乱的马赛克。原因:reshape和permute的参数按错误interleave展开。解决:不要瞎猜,回hdr文件里看interleave字段,修switch里的映射。如果头文件里没有interleave字段,默认先按BSQ试一次,不行再试BIL/BIP,三个里必然有一个是对的。唯一要小心的是少数数据集hdr里写的是bip,实际二进制却是bil,传感器厂商改过输出顺序但没同步文档。这时用目视交叉验证最可靠:找一个有清晰边界特征的波段,分别按三种布局显示一遍,轮廓最自然那张就是正确排列。

第二条是图像上下颠倒或者左右翻转。现象:图像轮廓清晰,但河流、道路的走向看起来像镜像。原因:byte order=1被当成0读取,或者传感器按从下到上扫描导致行序反过来。需要区分的是,byte order错误会让数值直接变乱码而不是简单地上下倒置,如果轮廓正常但数值异常,通常不是字节序问题。解决:先在fread里用'b'重新读一次对比;行序问题用flipud(cube)翻转即可,但翻转前先确认是倒置而不是镜像。翻转操作会把像素地理坐标反转,后续如果和GPS数据、矢量边界叠加,要注意地理配准,不然分析结论全是错的。

第三条是内存溢出。现象:读取大尺寸数据集时MATLAB直接报Out of memory,或者系统变得极度卡顿。原因:200波段×1000行×1000列的uint16数据本身约400MB,转成double后立刻膨胀到1.6GB,如果中间再不小心创建了多个临时变量,内存就爆了。解决:fread时不要一次读全,按波段或按行分块,具体方案在第6章展开。临时补救措施有两个:一是读进来后立刻转single而不是double,高光谱分类算法对float32精度完全足够;二是读完一块就clear一块中间变量,用whos查看占用。但这两个都只是治标,数据量一上来还是要走分块路线。

5.2 头文件缺失与反射率量纲:两个容易被忽略的隐性坑

第四条是数据文件里根本没有hdr头文件。现象:下载的压缩包里只有一个.raw或者.bin文件,没有任何.hdr。原因:部分数据发布者只传二进制,头文件要么漏发要么需要从说明文档里手动构造。解决:从文档里找samples、lines、bands、data type、interleave这五个值,自己写一个最简hdr文本文件,五行就够。如果文档也缺失,用文件大小反推:samples×lines×bands×每像元字节数必须等于文件大小,这是唯一可靠的验证方式。每像元字节数由data type决定,uint16是2字节,float32是4字节。三个未知数一个等式通常解不出来,但配合场景知识比如“这是AVIRIS Classic数据”就能把bands猜个八九不离十。

第五条是反射率量纲没换算。现象:求出的光谱曲线整体都是几百甚至几千的整数,画出来严重失真。原因:数据是uint16存的缩放反射率,直接当成浮点反射率用了。解决:max(cube(:))大于1时先除以10000,再检查曲线是否落在0~1之间。这条在第4章已经详细展开,这里再次单列是因为它在真实项目里的被问频率仅次于interleave,很多做深度学习的人训练网络前把数据直接喂给模型,完全没意识到输入数值范围都不对。五个翻车现场讲完,总结成一句话:拿到新数据集的第一步永远是跑第3章的对账逻辑和单波段可视化,而不是急着去load或者训练。

6. 进阶:超大高光谱图像的分块读取与内存控制

6.1 按需读取:fseek定位跳过无关波段

第5章的内存溢出问题,这里给一个真正可用的解决方案。核心思路是用fseek跳过不需要的字节,只把目标波段或子区域读进内存。以Indian Pines这种200波段以内的小数据为例整幅读完压力不大,但机载推扫传感器的数据动辄几千行、文件几个GB,一次性读完非常危险。高光谱分类器通常按行块处理,那就把文件指针停在目标行块开头,读一块处理一块,处理完清掉再读下一块。

function block = read_envi_block(hdr_file, data_file, band_list, row_range) % 读取指定波段和行范围的数据块 % band_list - 需要读取的波段索引 % row_range - 行范围,如 [101, 200] meta = parse_envi_hdr(hdr_file); samples = meta.samples; bands = meta.bands; % 每个像元字节数由data type决定,这里以uint16为例 bytes_per_pixel = 2; fid = fopen(data_file, 'rb'); block = zeros(length(band_list), length(row_range(1):row_range(2)), samples); for k = 1:length(band_list) band = band_list(k); for r = row_range(1):row_range(2) % 文件偏移 = 行偏移 + 波段偏移 offset = (r-1) * samples * bands * bytes_per_pixel + ... (band-1) * samples * bytes_per_pixel; status = fseek(fid, offset, 'bof'); if status ~= 0 error('fseek失败,偏移越界'); end block(k, r - row_range(1) + 1, :) = fread(fid, samples, '*uint16')'; end end fclose(fid); end

这个函数的偏移计算基于BSQ布局,BIP与BIL需要按各自公式重写,但思路完全一样。fseek的'bof'表示相对文件开头定位。这里每读一行都做一次fseek,IO开销虽然比整块读大,但对超大文件来说内存收益远大于这点损耗。如果机器内存吃紧,我会把block在循环里直接喂给分类器,而不是等所有块拼完再处理,这样几千行的大文件在内存里永远只有一个块的大小。

另一个进展是验证读出来的块和整幅读的结果是否一致。取一个中等数据集,用read_envi_cube读完整个立方体,再用read_envi_block读同一个区域,用isequal对比两个数组:

whole = read_envi_cube('data.hdr', 'data.raw'); part = read_envi_block('data.hdr', 'data.raw', 1:5, 51:60); isequal(whole(1:5, 51:60, :), part) % 返回1则一致

那以后我每次拿到新的HDR格式高光谱数据集,第一件事就是跑一遍parse_envi_hdr核对interleave和data type,再决定整幅读还是分块读,然后立刻画单波段图验证。这套流程救了我好几次项目,也让我彻底不再担心新数据集是ENVI格式。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询