简介:高光谱图像作为一种特殊的遥感数据,包含数十至数百个连续波段,能反映地物精细的光谱差异,在环境监测、精准农业、地质勘探等领域具有重要价值。面向MATLAB学习者、遥感初学者及科研人员,这份压缩包提供了经典的高光谱图像数据和配套读取程序,帮助解决多波段数据读取与分析入门问题。包内共6个文件,涵盖TIFF格式的多波段图像、MATLAB的m脚本、ENVI头文件与数据文件以及说明文档,总大小约11MB,结构紧凑便于对照学习。资源已有3143人学习,足见其对于入门高光谱遥感与MATLAB编程的参考价值。通过实际运行脚本,读者可以掌握multibandread()函数的编写思路,理解从TIFF格式解码、多波段数据组织到元数据提取、假彩色可视化、光谱分析及统计计算的全流程,同时结合readme文档快速理解高光谱图像的格式特性与应用方法,适用于课程实验、毕业设计或自学进阶。 做高光谱数据处理也有些年头了,从最开始拿着ENVI一顿点鼠标,到后来不得不面对海量数据、批量处理,最后彻底转向MATLAB自己写读取程序,这条路走过不少弯路。今天这篇不聊虚的,就围绕“高光谱图像和matlab读取程序”这个主题,把我在实际项目中怎么读数据、怎么处理格式、怎么避开那些坑,一次性讲清楚。
这篇文章适合谁看?刚接触高光谱、被数据格式搞晕的研究生,或者已经在用ENVI但想转MATLAB做批量处理的工程师,甚至只是课程作业需要读取一组高光谱影像的同学,都能在这里找到可以直接抄走的代码和思路。
1. 内容整体设计与思路拆解
1.1 高光谱图像到底特殊在哪
高光谱图像和普通RGB图像最大的区别,就是波段数量。普通图像一般3个波段,高光谱可以轻松上百甚至上千个波段,每个波段对应一个狭窄的光谱区间。这意味着每个像素点都拥有一条连续的光谱曲线,可以用来识别地物、反演参数,这是多光谱做不到的。
但高光谱数据也有个显著问题——数据量巨大。一个常见的机载高光谱数据集,比如AVIRIS,224个波段,单景影像动辄上百MB甚至上GB。如果用ENVI这类软件手动操作,不仅慢,而且做批量处理时非常痛苦。更麻烦的是,高光谱数据通常不是普通的图像格式,而是带有特定头文件描述的二进制文件,初学者拿到手根本不知道从哪下手。
我用MATLAB做高光谱读取,核心考量就是三点:一是MATLAB处理矩阵运算天然高效,适合对三维数据块做逐波段、逐像素的数学操作;二是MATLAB的文件IO和内存映射机制,可以处理那些单个文件几个GB的大数据;三是MATLAB的可视化能力足够强,读进来之后能快速出图、做光谱曲线分析,配合自带的图像处理工具箱,很多算法不用自己从头写。
1.2 为什么选择用MATLAB而不是其他工具
市面上读取高光谱数据的工具不少,ENVI是老牌软件,Python有spectral库,GDAL也能读。但实际做科研和工程项目时,我还是最常切回MATLAB。
我个人的使用场景主要是做光谱分析和算法验证。MATLAB的脚本式交互环境,让我可以快速加载一个数据立方体,然后立即写几行代码提取某个像素的光谱曲线、计算NDVI、做主成分分析。这种“读取-分析-可视化”闭环的流畅度,Python当然也能做到,但MATLAB的矩阵语法更适合这种三维数组操作,代码更短、更直观。
另外,很多课题组和实验室已有的代码资产都是MATLAB写的,如果我只用Python,就得把别人的老代码重写一遍,这显然不现实。所以掌握MATLAB读取高光谱数据的程序,并不只是会一个函数调用,而是打通了后续一系列算法实现的基础。
1.3 高光谱数据格式的核心认识
高光谱数据格式五花八门,但最常遇到的还是ENVI标准格式,也就是一个.hdr文本头文件加上一个二进制数据文件。头文件里记录了行数、列数、波段数、数据类型、字节序、文件类型、波段存储方式等关键信息。
这里有个关键概念——波段存储方式(Interleave),常见三种:BSQ(Band Sequential)按波段顺序存储,每个波段一个完整的二维图像;BIL(Band Interleaved by Line)按行存储,同一行内所有波段的数据排在一起;BIP(Band Interleaved by Pixel)按像素存储,同一个像素的所有波段值排在一起。这个参数直接决定了你怎么从二进制文件里把数据切出来,读错就是一片乱码。
我在实际处理中遇到过很多次,头文件显示BSQ,但代码里按BIL读,出来的图像完全没法看。所以你在写读取程序之前,第一步必须先打开.hdr文件,看清楚interleave那个字段写的是什么。
2. 核心细节解析与实操要点
2.1 读取程序需要处理哪些关键参数
写一个通用的MATLAB读取程序,本质上就是解析头文件的参数,然后根据这些参数从二进制文件中正确地读出三维数组。核心参数有这么几个:
- samples:每行的像素数,也就是宽度
- lines:影像的行数,也就是高度
- bands:波段数
- data type:数据类型,1代表uint8,2代表int16,4代表float32等
- byte order:0代表小端序,1代表大端序
- interleave:bsq、bil还是bip
数据类型的理解很关键。地面物体反射率数据一般用uint16存储,但经过大气校正后的反射率数据往往是float32。如果类型读错了,不仅数值完全不对,内存占用也会差很多。比如同样一个1000×1000×100的数据立方体,uint16大约200MB,float32就要400MB,差距很可观。
字节序也是个容易出问题的坑。Windows系统通常是小端序(little-endian),而一些Unix工作站生成的数据可能是大端序(big-endian)。MATLAB默认按本机字节序读取,如果数据文件是大端序而你按小端读,读出来的数值会完全错乱,而且不会报错。我的建议是:读入后立刻检查数据的数值范围是否合理,如果uint16数据动不动出现6万多这种值,那大概率就是字节序反了。
2.2 读取流程中需要避开的典型误区
很多初学者拿到高光谱数据,第一反应就是用imread或者multibandread直接读。imread只能读标准图像格式,对高光谱这种二进制数据没有用。multibandread是MATLAB提供的函数,可以读取ENVI格式,但它的参数顺序容易搞混,而且对超大文件支持一般。
我建议自己写一个通用的读取函数,基于fopen+fread来做。这样能完全控制读取过程,特别是当你只需要读取部分波段、部分区域的时候,可以跳过大量无关数据,读取效率高很多。
另一个容易踩的坑是内存不够。高光谱图像动不动就是几个GB,如果直接一条命令把所有数据一次性读入内存,32GB内存的机器都可能会卡死。实际项目中我一般先读取部分波段或者对图像做降采样,先看看数据大概情况,再决定是否全量读入。
2.3 MATLAB读取程序的基本设计思路
我先说一个最简单的读取流程,然后再给完整代码。基本思路是:
- 打开并解析.hdr文件,把关键参数记下来
- 根据interleave方式,确定数据在二进制文件中的排列规则
- 用fread读取全部数据,然后reshape成正确尺寸的三维数组
- 对读取结果进行验证,比如输出数据尺寸、数值范围、显示某个波段的图像
这个流程看起来简单,但每一步都有细节。比如第3步,reshape的顺序和interleave直接相关——BSQ数据读出来是一段按波段顺序排好的长向量,reshape时先按每波段图像尺寸(lines, samples)还原,再按波段数排列成三维数组;而BIP数据则是按像素排列的,需要先reshape成(samples×lines, bands)再转置,顺序错了数据就是乱的。
我在写程序时还加了一个判断:如果数据文件特别大,就按波段分块读取,用循环的方式一块一块读进来,避免内存爆掉。
3. 实操过程与核心环节实现
3.1 环境准备与输入数据约定
我用的MATLAB版本是R2021a以上,实际测试R2018b也能跑通。需要确保机器有足够的可用内存,建议16GB以上。数据方面,我这里用的是一个机场高光谱数据集作为例子,格式是ENVI标准格式,包含一个.hdr文件和一个.bin数据文件(也可能是.dat、.raw等扩展名,本质上都一样)。
你需要提前准备好下面的文件结构:
D:\hyperspectral_data\ ├── airport.hdr ├── airport.bin └── read_hsi.m如果你的文件名不一样,只需要修改read_hsi.m开头的文件路径变量即可。
3.2 完整的MATLAB读取程序(可直接抄作业)
下面这段代码是我在实际项目中一直在用的读取函数,我给它做了注释,方便你理解每一步在干什么:
function [img, meta] = read_hsi(hdr_path, data_path, varargin) % READ_HSI 读取ENVI标准格式的高光谱图像 % 输入: % hdr_path - .hdr头文件的完整路径(字符串) % data_path - 二进制数据文件的完整路径(字符串) % 可选参数: % 'bands', [10 20 30] - 只读取指定波段(从1开始计数) % 'region', [r1 c1; r2 c2] - 只读取指定区域 % 'downsample', 2 - 空间降采样因子,减少内存占用 % 输出: % img - 三维数组,维度为 [lines, samples, bands] % meta - 结构体,包含头文件中的关键参数 p = inputParser; addParameter(p, 'bands', []); addParameter(p, 'region', []); addParameter(p, 'downsample', 1); parse(p, varargin{:}); opts = p.Results; % 1. 解析头文件 meta = parse_envi_hdr(hdr_path); % 2. 打开数据文件 fid = fopen(data_path, 'rb'); if fid == -1 error('无法打开数据文件:%s', data_path); end % 3. 生锈? 计算基本参数 nSamples = meta.samples; nLines = meta.lines; nBands = meta.bands; dataType = meta.data_type; % 数据类型映射表 typeMap = { ... 'uint8', 1, 'uint8'; ... 'int16', 2, 'int16'; ... 'int32', 3, 'int32'; ... 'float32', 4, 'single'; ... 'float64', 5, 'double'; ... 'uint16', 12, 'uint16'; ... 'uint32', 13, 'uint32'}; matlabType = 'uint8'; for k = 1:size(typeMap, 1) if strcmpi(typeMap{k,2}, num2str(dataType)) matlabType = typeMap{k,3}; break; end end bytesPerPixel = get_bytes_per_pixel(dataType); % 4. 根据采样设置修正尺寸 ds = opts.downsample; outLines = ceil(nLines / ds); outSamples = ceil(nSamples / ds); % 5. 判断是否只读取部分波段 if isempty(opts.bands) useBands = 1:nBands; else useBands = opts.bands; end nUseBands = length(useBands); % 6. 根据interleave方式读取数据 switch lower(meta.interleave) case 'bsq' img = zeros(outLines, outSamples, nUseBands, matlabType); for i = 1:nUseBands bidx = useBands(i); % 跳到该波段起始位置 offset = (bidx - 1) * nLines * nSamples * bytesPerPixel; fseek(fid, offset, 'bof'); % 读取该波段数据 if ds == 1 bandData = fread(fid, [nSamples, nLines], matlabType)'; else rawData = fread(fid, [nSamples, nLines], matlabType)'; bandData = rawData(1:ds:end, 1:ds:end); end img(:, :, i) = bandData; end case 'bil' % BIL需要先读入所有需要的行,再从中提取波段 img = zeros(outLines, outSamples, nUseBands, matlabType); lineBytes = nSamples * nBands * bytesPerPixel; for i = 1:outLines srcLine = (i - 1) * ds + 1; offset = (srcLine - 1) * lineBytes; fseek(fid, offset, 'bof'); oneLine = fread(fid, [nSamples * nBands], matlabType)'; oneLine = reshape(oneLine, [nSamples, nBands]); % 提取需要的波段 for j = 1:nUseBands img(i, :, j) = oneLine(:, useBands(j)); end end case 'bip' img = zeros(outLines, outSamples, nUseBands, matlabType); for i = 1:outLines srcLine = (i - 1) * ds + 1; offset = (srcLine - 1) * nSamples * nBands * bytesPerPixel; fseek(fid, offset, 'bof'); oneLine = fread(fid, [nBands, nSamples], matlabType)'; for j = 1:nUseBands img(i, :, j) = oneLine(:, useBands(j)); end end otherwise error('未知的interleave类型:%s', meta.interleave); end fclose(fid); end function meta = parse_envi_hdr(hdr_path) % 解析ENVI头文件,生成结构体 meta = struct(); fid = fopen(hdr_path, 'r'); if fid == -1 error('无法打开头文件:%s', hdr_path); end % ENVI头文件是简单的key = value格式,但也可能有大括号块 content = fread(fid, '*char')'; fclose(fid); % 按行处理 lines = strsplit(content, '\n'); for i = 1:length(lines) line = strtrim(lines{i}); if isempty(line) continue; end % 跳过大括号块 if startsWith(line, '{') continue; end eqPos = strfind(line, '='); if isempty(eqPos) continue; end key = strtrim(line(1:eqPos(1)-1)); val = strtrim(line(eqPos(1)+1:end)); % 处理可能的注释 commentPos = strfind(val, ';'); if ~isempty(commentPos) val = strtrim(val(1:commentPos(1)-1)); end % 去掉可能存在的引号 val = regexprep(val, '^"|"$', ''); switch lower(key) case 'samples' meta.samples = str2double(val); case 'lines' meta.lines = str2double(val); case 'bands' meta.bands = str2double(val); case 'data type' meta.data_type = str2double(val); case 'byte order' meta.byte_order = str2double(val); case 'interleave' meta.interleave = lower(val); case 'header offset' meta.header_offset = str2double(val); end end % 设置默认值 if ~isfield(meta, 'byte_order') meta.byte_order = 0; end if ~isfield(meta, 'header_offset') meta.header_offset = 0; end end function bytesPerPixel = get_bytes_per_pixel(dataType) % 根据ENVI数据类型编号计算每个像素占用的字节数 switch dataType case 1 bytesPerPixel = 1; % uint8 case {2, 12} bytesPerPixel = 2; % int16 / uint16 case {3, 13} bytesPerPixel = 4; % int32 / uint32 case 4 bytesPerPixel = 4; % float32 case 5 bytesPerPixel = 8; % float64 otherwise error('不支持的数据类型编号:%d', dataType); end end3.3 读取程序的使用方法与效果展示
保存好read_hsi.m之后,在MATLAB的命令窗口或者脚本中这样使用:
% 设置文件路径 hdrPath = 'D:\hyperspectral_data\airport.hdr'; dataPath = 'D:\hyperspectral_data\airport.bin'; % 全量读取 [img, meta] = read_hsi(hdrPath, dataPath); % 查看数据和元信息 size(img) meta.interleave meta.data_type % 显示某个波段的灰度图 figure; imagesc(img(:, :, 50)); colormap gray; axis image; colorbar; title('第50波段灰度图'); % 查看某个像素的光谱曲线 figure; plot(squeeze(img(200, 300, :))); xlabel('波段'); ylabel('数值'); title('像素(200,300)的光谱曲线');在实际项目中,我经常只读取部分波段来快速预览数据,比如先读第1、50、100、150、200这5个波段,看看影像的范围和地物大致分布,再决定后续分析策略。这样比一次性把全部224个波段读进来快得多,内存占用也很小。
下面的代码演示了如何只读取部分波段和做假彩色合成:
% 只读部分波段 [img_part, meta] = read_hsi(hdrPath, dataPath, 'bands', [30 20 10]); % 假彩色合成显示(R=波段30, G=波段20, B=波段10) img_display = img_part; % 做简单的线性拉伸方便显示 for b = 1:3 band = img_display(:, :, b); band = double(band); band = (band - min(band(:))) / (max(band(:)) - min(band(:))); img_display(:, :, b) = band; end figure; imshow(img_display); title('假彩色合成(R=30,G=20,B=10)');3.4 大文件分块读取的内存优化方案
当你面对的是几百个波段、单文件几十GB的高光谱数据时,上面的一次性全读方案就会非常吃力。我一般会把数据分成多个小块,逐个读取并处理。这里给出一个思路:
% 分块读取处理示例,以BSQ格式为例 blockLines = 100; % 每次读取100行 nBlocks = ceil(meta.lines / blockLines); for blockIdx = 1:nBlocks lineStart = (blockIdx - 1) * blockLines + 1; lineEnd = min(blockIdx * blockLines, meta.lines); % 读取这个块的所有波段数据 % 这里为了代码简洁,直接调用read_hsi的部分功能区 % 实际可以写一个read_hsi_block函数,按行列范围读取 end我在做真实项目时,还会结合parfor并行循环,把不同块分给多个worker同时处理。不过要注意,parfor对内存的消耗会成倍增加,所以内存不足时建议串行处理,保证稳定可靠最关键。
4. 常见问题与排查技巧实录
4.1 图像显示出来完全不对或者全黑色
这是我遇到最多的一个问题。数据明明读进来了,但imagesc显示出来要么一片黑,要么全是噪点。这种情况我几乎一猜就是类型问题或者字节序问题。
排查步骤很简单:先检查meta.data_type判断是不是读成了错误的类型,然后检查数值范围。如果是uint16的数据,正常范围应该在0到10000左右,如果出现65535或负数,先检查字节序。另外,显示图像时一定要用imagesc而不是imshow,因为imshow默认对uint8友好,对uint16的显示容易出问题;即使用了imagesc,也要注意数据是双精度还是单精度。
我之前处理过一批数据,头文件写的是float32,实际文件里是uint16,结果读出来图像完全是花的。后来用十六进制工具直接查看文件头部数据,才发现类型标注和实际内容对不上。所以拿到陌生数据时,先花几分钟用fread读一小段,验证数值范围是否合理,再去全量读,这是最稳妥的流程。
4.2 读取大文件时内存不足
一个224波段、1000×1000像素、uint16类型的高光谱数据,大小大约是:
1000 * 1000 * 224 * 2 bytes ≈ 448 MB这个大小在绝大多数电脑上都能读。但如果你处理的是航拍大区域数据,比如10000×10000像素,那就是44.8GB,显然直接读肯定不行。
我的做法是:先用降采样参数“瞄一眼”数据整体情况,比如downsample设为4或8。然后再针对感兴趣区域用region参数读取局部数据。这样做既能了解全貌,又不会占用太多内存。
另外有个小技巧:读取的时候尽量用单精度(single)类型存储,而不是默认的双精度(double)。单精度只占4字节,是双精度的一半,而高光谱数据本身的存储精度一般也就是16位,转成单精度完全不影响后续分析。
4.3 头文件解析出错
ENVI头文件虽然格式比较简单,但不同版本会有一些差异。有些头文件里会多出“description”这种大括号块,有的会出现UTF-8 BOM头。我开始写解析函数时没考虑到这些,结果读某些文件就报错。
后来我在parse_envi_hdr里加了处理大括号的跳过的逻辑,并且用regexprep去掉可能存在的引号,这样大部分文件都能正常解析了。如果你自己写的解析函数还是出错,可以先用记事本打开.hdr看一看,手动检查一下格式。万一遇到乱码,很可能是编码问题,用MATLAB的fopen(fid, 'r', 'n', 'UTF-8')重新读一下即可。
4.4 BIL和BIP格式读取容易出错
BSQ格式最容易理解,就是按波段存完一个再存下一个。BIL格式是每行内所有波段都排在一起,BIP是每个像素内所有波段排在一起。我在前面代码里已经实现了这三种格式的读取,但这里要特别提醒一点:很多人拿到BIP数据时,如果reshape的顺序不对,图像会变成“条纹状”——每个像素列是一条暗一条亮,然后整幅图像看起来像被拉花了。
解决这个问题的方法是理解BIP的本质:原始数据按扫描顺序排列,即第一个像素的全部波段值,接着第二个像素的全部波段值。所以要先把数据resize成[nBands, totalPixels],再转置成[totalPixels, nBands],最后再和图像行列数关联起来。如果你的数据是BIP但用BSQ方式读,完全看不出图像内容,对排查来说干扰性很强。
4.5 MATLAB版本兼容性造成的问题
不同版本MATLAB对某些函数的处理有差异,比如contains、startsWith这些字符串函数在老版本里可能不存在。我在r2016a上跑过一个老代码,里面用了startsWith就报错了。解决办法是把版本升级到R2016b以上,或者在代码里用strfind替代。
另外,有些用户会遇到MATLAB安装成功后一闪就没了打不开的情况,这通常和许可证、路径或者显卡驱动有关系。这在高光谱处理任务里很麻烦,因为这类任务对图像显示要求高,显卡驱动有问题时MATLAB可能在启动阶段就崩溃。建议更新显卡驱动,或者在MATLAB启动时加-softwareopengl参数强制使用软件渲染,这样能避免很多显示层面的启动崩溃。
4.6 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 读取结果全黑 | 数据类型错误 / 字节序不对 | 检查data type和byte order,验证数值范围 |
| 图像呈条纹状 | BIP/BIL格式读取顺序错误 | 检查interleave,调整reshape方式 |
| 显示时有噪点 | 用imshow显示uint16数据 | 改用imagesc并做线性拉伸 |
| 内存不足 | 全量读取超大文件 | 使用downsample参数或region参数 |
| 头文件解析失败 | 头文件有BOM或特殊格式 | 手动清理头文件,或用UTF-8读取 |
| 数据范围不对 | 字节序反了 | 使用swapbytes或重新以正确字节序读取 |
| MATLAB打不开 | 显卡驱动冲突 | 用-softwareopengl启动 |
5. 拓展:从读取到分析的进阶玩法
5.1 光谱曲线提取与地物识别
高光谱图像最有价值的地方在于你可以提取任意像素的光谱曲线,然后和标准光谱库进行匹配。读取程序搞定之后,我一般会做这几步:
% 提取指定区域的均值光谱 regionMask = zeros(size(img(:, :, 1))); regionMask(150:180, 200:230) = 1; regionPixels = img(repmat(regionMask, [1 1 size(img, 3)])); regionPixels = reshape(regionPixels, [], size(img, 3)); meanSpectrum = mean(regionPixels, 1); % 绘制光谱曲线 figure; plot(meanSpectrum, 'LineWidth', 1.5); xlabel('波段号'); ylabel('反射率值'); title('感兴趣区域平均光谱曲线'); grid on;这里有个经验:提取光谱曲线之前,先确认数据是反射率还是DN值。如果是DN值,不同波段之间的可比性较差;如果是反射率,直接比较不同像素的光谱形状才有意义。如果数据只是DN值,可以先用暗目标减法做简单的大气校正,再提取光谱。
5.2 NDVI等植被指数计算
高光谱数据有几十上百个波段,你可以从中挑选出红波段(约650nm)和近红外波段(约800nm)来计算NDVI,这比多光谱数据更灵活,因为可以用波段平均来降低噪声:
% 假设你的数据中波段20对应红光(650nm),波段35对应近红外(800nm) redBand = double(img(:, :, 20)); nirBand = double(img(:, :, 35)); ndvi = (nirBand - redBand) ./ (nirBand + redBand + 1e-6); figure; imagesc(ndvi); colormap jet; axis image; colorbar; title('NDVI计算结果');这里加了个1e-6的小量,防止分母为零。计算NDVI的另一个细节是:如果数据中有极端值或NaN,要先做掩膜处理,否则计算结果会污染整体统计。
5.3 主成分分析(PCA)快速降维
高光谱波段多、相关性高,直接分类或回归之前,先做PCA降维几乎成了标准流程。我在实际项目中,150个波段的数据,一般降维后保留前10~20个主成分就能解释95%以上的方差,计算速度快很多。
% 把三维数组整理成二维矩阵:像素×波段 origSize = size(img); pixels = reshape(img, [], origSize(3)); pixels = double(pixels); % 去均值 mu = mean(pixels, 1); pixels_centered = pixels - mu; % 做PCA(可以用pca函数) [coeff, score, latent] = pca(pixels_centered, 'NumComponents', 10); % 把结果还原成图像格式 pc_images = reshape(score, [origSize(1), origSize(2), size(score, 2)]); % 显示前三个主成分 figure; for i = 1:3 subplot(1, 3, i); imagesc(pc_images(:, :, i)); colormap jet; axis image; title(sprintf('PC%d', i)); end这里提醒一下:如果图像尺寸大,pixels矩阵会非常大。10000×10000像素、150个波段,那就是150亿个元素,直接double化会直接内存爆炸。这种情况下要么用分块PCA,要么用随机PCA算法,或者干脆降采样后再做PCA。
5.4 分类:以K-means为例
读取数据之后做分类也是常见需求。一个简单的无监督分类可以这样写:
% 用PCA降维后的前5个主成分做K-means聚类 features = score(:, 1:5); rng(42); k = 6; clusterIdx = kmeans(features, k, 'Replicates', 3); clusterMap = reshape(clusterIdx, origSize(1), origSize(2)); figure; imagesc(clusterMap); axis image; colormap(lines(k)); colorbar; title('K-means分类结果');K-means聚类的k值选择是个经验问题。我在实际项目里,会先跑一遍k从3到10的聚类,计算轮廓系数(silhouette)来辅助选k。但最终到底选几个类别,还是要回归到应用场景——比如这块区域到底有几种地物,需要结合地面调查来定,单纯看统计指标很容易选错。
6. 一些工具搭配与性能优化经验
6.1 MATLAB中读取速度优化的三个方向
高光谱数据大,读取速度自然慢。我总结下来,提速点主要在三个方面。
第一是减少fseek和fread的调用次数。每调用一次fread就有一次IO开销,读几百个波段如果每个都单独调用fread,速度会慢不少。理想做法是能一次把整个文件读完,再在内存里做数组重构。比如BSQ格式,可以一次性用fread把整个数据块读入,然后reshape成[lines, samples, bands]。虽然内存占用大,但速度提升非常明显。
第二是利用内存映射。MATLAB的memmapfile函数可以把文件映射到内存地址空间,后续访问数据就像访问普通数组一样,不需要手动fseek和fread,读取随机位置的波段和区域特别快。我做大区域高光谱数据时基本都用这个。
第三是并行计算。如果只是读取,多线程帮助有限,但如果读取后要做算法处理,可以用parfor把波段分给不同worker并行处理。我处理过一个200波段的数据,每20个波段分一组,10组并行处理,整体耗时减少了6倍左右(因为内存带宽限制,不是完全线性加速)。
6.2 MATLAB内置图像处理工具箱的使用衔接
MATLAB的图像处理工具箱(Image Processing Toolbox)里有很多函数可以直接用来处理高光谱图像的单个波段,比如imadjust做对比度增强,medfilt2做中值滤波去噪,imresize做尺度变换。但需要注意的是,这些函数大多针对二维图像,对三维的高光谱立方体需要逐波段调用。
我自己写了一个小工具函数,把常用的二维处理批量应用到每个波段上:
function out = apply_bandwise(img, funcHandle) % 对每个波段应用指定的函数句柄 out = zeros(size(img), 'like', img); for i = 1:size(img, 3) out(:, :, i) = funcHandle(img(:, :, i)); end end用法示例:
% 对每个波段做3x3中值滤波 img_denoised = apply_bandwise(img, @(x) medfilt2(x, [3 3]));这个函数写起来很简洁,实际用起来却非常顺手,建议你也保留一个。
6.3 与Python/GDAL工具链的协作
虽然这里聊的是MATLAB读取,但实际工作中不可能只用一种工具。我经常遇到的情况是:别人给的预处理数据是Python脚本处理的,而我自己的算法是在MATLAB里实现的。这时候最好的衔接方式就是文件格式。
我建议在MATLAB里读入数据后,如果你需要传给Python处理,先保存成.mat文件:
save('airport_img.mat', 'img', 'meta', '-v7.3');Python读取用scipy:
import scipy.io as sio data = sio.loadmat('airport_img.mat') img = data['img'] meta = data['meta']这里的-v7.3参数很重要,如果不加,超过2GB的变量保存不了。Python读取v7.3的mat文件需要额外安装h5py,因为这种格式本质上是HDF5格式。
如果你要传给ENVI处理,也可以用multibandwrite或者自己写一个ENVI格式输出函数,把处理结果保存成ENVI标准格式,方便其他人用ENVI打开。我在项目交付时经常这么做,因为对方用ENVI已经很熟练。
7. 经验心得与进一步扩展思路
高光谱图像读取这件事,看起来只是“打开文件”的简单操作,但真正做深入之后,你会发现它几乎决定了后续所有处理流程的走向。格式解析错误、数据类型读错、内存管理不当,任何一个环节出问题,后面做分类也好、做反演也好,结果全部不可信。
有一次我在做土地覆盖分类,前面读取的数据暗暗觉得不太对,但没细查,直接跑了分类模型,结果每一类的光谱曲线都异常,后来才发现是最开始读数据时字节序弄错了,导致所有数值都乱了。那一次让我养成了习惯:读完数据之后,先花30秒打印一下数据的基本统计信息,确认数值范围合理、空间分布连续,再继续往下做。这个习惯帮我避免了很多次无效工作。
你可以继续扩展的方向包括:接入MATLAB的深度学习工具箱,把高光谱数据读取之后送进3D-CNN做分类;或者把读取函数与实时无人机数据流打通,做在线处理;还可以基于读取函数进一步封装成App,方便实验室里不会写代码的同学直接可视化数据。我自己后续就打算写一个基于App Designer的高光谱数据浏览器,把读取、显示、光谱提取、简单分类这些都集成进去,让处理流程更顺手。
最后再分享一个很实用的小技巧:写读取函数时,从一开始就把“支持部分读取”和“支持降采样”这两个功能放进去。虽然最开始写的时候多花了一点时间,但后面做大规模数据处理时,这个决定帮我节省了无数个小时的等待时间,也避免了好几次内存爆掉导致的程序崩溃。数据量大、波段多、文件格式杂,这些都是高光谱领域躲不开的事情,但其实只要读取这一关地基打好了,后面的路就顺畅得多。
本文还有配套的精品资源,点击获取