
开头先说说我为什么碰这个方向。干农业遥感这一行最头疼的往往不是算法而是从“数据”到“结论”中间那条漫长的链路。你下载了Landsat影像打开了ENVI做完一堆操作最后发现离“作物产量预测”还差着十万八千里。我也是在这个反复折腾的过程中才逐渐梳理出一套用Python把Landsat遥感影像和产量预测串起来的完整流程。这篇文章就把这套实战路径原原本本写出来适合已经懂一点Python语法、又恰好需要碰遥感数据的农业、环境相关专业的学生或从业者也适合想从零开始接触遥感估产的科研小白。读完你至少能搞清楚一件事给你一颗卫星影像你该怎么一步步把它变成一张产量分布图。1. 为什么农业估产绕不开卫星遥感——从田间抽样到光谱反演的思路转变传统农业估产靠什么靠人下地靠选取样方靠经验判断。这个方法到今天依然有效但它有两个很难绕开的短板一个是覆盖范围小一个是不够及时。一个县如果有一百万亩玉米靠地面样方去估产哪怕抽样方案设计得再科学也免不了以点带面的误差。更现实的问题是作物生长关键期就那么几周等人工调查走完一圈最佳观测窗口可能已经过了。卫星遥感恰好补上了这两个短板。Landsat系列卫星的重访周期是16天虽然不算快但胜在数据免费、存档时间长、波段设置合理可见光、近红外、短波红外都有非常适合做长时序的作物长势监测。加之和Sentinel-2相比Landsat的历史跨度从1984年延续到现在做跨年份的产量对比分析时数据一致性更有保障。从技术路线上看用遥感做产量预测本质上是建立“光谱信号”和“作物产量”之间的映射关系。作物的叶面积指数、叶绿素含量、生物量这些农学参数会直接影响到叶片对红光和近红外光的吸收与反射特性。产量高不高首先反映在冠层光谱上。我们说得直白一点卫星影像里的每一个像元本质上就是一块田地的光谱记录而产量预测就是把这些光谱记录翻译成干物质积累量的过程。这个翻译过程在实现时通常分成三步走。第一步是数据准备把原始影像变成干净的反射率数据。第二步是特征提取从反射率中计算出和作物生长状态密切相关的指数比如NDVI归一化植被指数、EVI增强型植被指数、NDWI归一化水体指数等。第三步是建模反演用历史产量数据和遥感特征建立统计模型或机器学习模型再用模型去预测当年的产量分布。这三个环节听起来简单实际操作中每一步都有讲究。比如NDVI对高植被覆盖区容易饱和玉米封垄之后NDVI就不再敏感这时候可能需要换EVI。再比如Landsat影像中云和云影的干扰如果处理不干净模型输入里的噪声会比真实信号还大。后面我会逐个环节展开讲处理细节这里先建立一个整体框架。还有一个容易忽略的宏观问题时序数据比单期影像有用得多。单期影像只能反映作物某个节点的状态而产量是作物整个生长季累积的结果。拔节期的长势和灌浆期的长势对最终产量的贡献完全不同。所以更靠谱的做法是把整个生长季的影像按时间排列提取NDVI的峰值、累积值、增长速率等时序特征再拿这些特征去训练模型。这才是“遥感估产”能落地的大前提。2. 环境准备与Landsat数据获取版本选型和下载实操2.1 用Python做遥感你需要准备哪套环境先明确一点做遥感影像处理Python环境和做普通数据分析的环境稍有不同重点在于要装对库。下面是我在实际项目中验证过的标准组合各库的版本不需要追新稳定即可。核心库清单numpy、pandas基础的数据结构和计算没它们后面什么都做不了。rasterio读写GeoTIFF影像的利器比gdal直接操作更Pythonic能和numpy数组无缝配合。geopandas处理矢量数据比如你画的地块边界shapefile在做区域裁剪时必不可少。matplotlib绘图和出图用来快速可视化影像和结果。scikit-learn建模用的内置了线性回归、随机森林、梯度提升树等常用算法做产量预测足够。rasterstats用矢量边界统计栅格区域内的均值、最大值等这个在提取地块尺度的特征时非常好用。关于环境配置我自己的经验是用conda创建独立环境免得把系统Python搞坏。命令行执行以下命令即可conda create -n agri_remote python3.9 conda activate agri_remote conda install numpy pandas matplotlib scikit-learn conda install -c conda-forge rasterio geopandas rasterstats我用的是Python 3.9目前这些库都支持。如果你想用Python 3.11或更高版本也没问题conda-forge源的更新速度足够快。需要提醒一点最好不要用pip直接装gdal编译起来十分痛苦用conda-forge装最省心。rasterio在装gdal时也会自动拉一堆依赖同样建议用conda官网源。2.2 Landsat数据版本怎么选Landsat 8还是Landsat 9目前可用的Landsat数据主要是Landsat 8和Landsat 9两者的传感器基本一致OLI陆地成像仪加TIRS热红外传感器。Landsat 8于2013年发射Landsat 9于2021年发射两者轨道设计一致重访周期错开配合使用下可以实现每8天一次的中等分辨率观测。对于产量预测任务我优先推荐用Landsat 8/9的Collection 2 Level-2产品。Level-2产品已经做过大气校正和地表反射率反演直接下载下来就能用省掉了自己在大气校正上的大量摸索。如果你下载的是Level-1原始影像那还需要做辐射定标和大气校正处理流程会复杂很多。教程的意义在于减少路径摸索所以能用Level-2就尽量用Level-2。波段选择方面做作物监测主要关心这几个波段蓝色波段Band 2波长482nm对叶绿素和土壤背景有一定敏感性。绿色波段Band 3波长561nm反映作物绿度变化常参与植被指数计算。红色波段Band 4波长655nm叶绿素强吸收带是NDVI的重要组成部分。近红外波段Band 5波长865nm叶片内部结构散射强烈NDVI的另一个核心波段。短波红外波段Band 6波长1610nm对叶片含水量和冠层结构敏感和植被含水量监测有关。Landsat 8/9各波段的空间分辨率除热红外100米外均为30米。按一个像元30米×30米计算单点覆盖面积是900平方米约1.35亩。这个精度对县域尺度的作物估产足够了但对小块农田来说一个像元内可能混入道路和建筑物需要在后续处理中注意混合像元的影响。另外顺便提一下Sentinel-2的适用场景它的空间分辨率更高10米重访周期也更短5天做精细农业监测很有优势。但本篇以Landsat为主线因为Landsat数据的时间跨度更长更适合做长时序的产量趋势建模。2.3 数据下载实操在USGS EarthExplorer上快速找到目标影像Landsat数据下载渠道我常用的是USGS EarthExplorerearthexplorer.usgs.gov。注册账号、登录后按下面流程搜索在“Search Criteria”选项卡里输入你研究区域的范围。可以直接输入经纬度也可以上传一个GeoJSON或Shapefile或者在地图上手动拉一个框。在“Date Range”里设定时间范围。如果做当年产量预测通常选择作物生长季的影像比如预测玉米产量就选7月到9月的影像。在“Data Sets”选项卡中展开“Landsat Archive”选择“Landsat Collection 2 Level-2”再选择“Landsat 8-9 OLI/TIRS C2 L2”数据集。点击“Results”查看搜索结果筛选云量低小于10%的影像点击下载图标选择GeoTIFF格式即可。需要重点注意的是每景Landsat影像是由多个波段文件组成的一组GeoTIFF文件名会带有类似“LC08_L2SP_119039_20230815_20230817_02_T1”的标识。下载来的压缩包中以SR_B4.TIF结尾的是红光波段以SR_B5.TIF结尾的是近红外波段以QA_PIXEL.TIF结尾的是像元质量波段。后面做云掩膜时会用到QA_PIXEL文件。如果是要做长时序分析建议把多个时期的影像都下载下来组织成独立文件夹方便代码统一遍历读取。考虑到国内下载USGS数据速度较慢如果条件允许也可以用GEEGoogle Earth Engine在线提取数据但本篇聚焦本地Python处理所以还是以EarthExplorer的下载路径为主。下载时注意选择投影坐标系一致的影像如果跨了UTM分带后续处理时还要考虑重投影和拼接。3. 遥感影像预处理从DN值到反射率数据的完整处理链路3.1 用rasterio读取影像并构建波段组合拿到手的Landsat影像虽然文件名上标注了各种信息但读进程序里才能看到真正的数据结构。我习惯先把所需波段读取进一个多维数组方便后续计算。下面是一段读取红光、近红外波段并计算NDVI的基础代码import rasterio import numpy as np red_path LC08_L2SP_119039_20230815_20230817_02_T1_SR_B4.TIF nir_path LC08_L2SP_119039_20230815_20230817_02_T1_SR_B5.TIF with rasterio.open(red_path) as src: red src.read(1).astype(float32) profile src.profile with rasterio.open(nir_path) as src: nir src.read(1).astype(float32) print(Red band shape:, red.shape) print(NIR band shape:, nir.shape) print(Profile:, profile)这里有几个细节需要留意。Landsat C2 Level-2产品的表面反射率数据已经做了大气校正并转换为16位整型数值范围通常是0到10000。要得到0到1之间的真实反射率需要将这些整型值乘以0.00001的缩放系数。有些波段还设定了数值范围外的填充值比如-9999表示无效数据处理时需要先排除这些无效像元。如果直接拿整型值去算NDVI结果不会错但数值会差好几个数量级后续特征提取和建模时容易踩坑。因此建议在预处理阶段统一做缩放保留成浮点型数据red red * 0.00001 nir nir * 0.00001 # 排除异常值比如负值或大于1的值 red[red 0] np.nan red[red 1] np.nan nir[nir 0] np.nan nir[nir 1] np.nannumpy的广播特性和nan处理能力对遥感影像这种大数组运算来说非常友好。3.2 云掩膜不用QA_PIXEL波段你的指数值可能全是噪声我刚开始做遥感影像的时候踩过最深的坑就是忽略云掩膜。尤其是在多雨的农业区影像上总会有零星的云和云影。这些像元的光谱特征跟正常作物完全不同——云在可见光波段反射极高近红外也高得离谱云影则恰好相反所有波段都明显偏暗。如果你不把这些像元剔除掉提取的NDVI就会出现很多极端的伪信号这些伪信号进入统计模型后往往会把整个预测结果带偏。Landsat C2 Level-2产品中提供了像素质量波段QA_PIXEL用位标志的方式记录了每个像元的云、云影、冰雪、水体等类别。处理时我建议优先查阅官方文档根据对应的位标志来生成掩膜。这里给一个我自己常用的简化方案用内置常量过滤主要干扰。# 读取QA_PIXEL波段 qa_path LC08_L2SP_119039_20230815_20230817_02_T1_QA_PIXEL.TIF with rasterio.open(qa_path) as src: qa src.read(1) # Cloud bit 3, Cloud Shadow bit 4, Cirrus bit 1 # 推荐直接使用官方常量 # https://www.usgs.gov/landsat-missions/landsat-collection-2-level-2-science-products cloud_shadow_bit 1 4 # bit 4 cloud_bit 1 3 # bit 3 cirrus_bit 1 1 # bit 1 mask ( ((qa cloud_shadow_bit) ! 0) | ((qa cloud_bit) ! 0) | ((qa cirrus_bit) ! 0) ) # 将云和云影像元置为NaN red_clean red.copy() nir_clean nir.copy() red_clean[mask] np.nan nir_clean[mask] np.nan这样做完参与后续计算的像元就是基本干净的陆地观测值了。判断哪些像元应该剔除时可以参考QA波段文档中的bit定义也可以根据影像的具体情况适当放宽条件。比如在比较多云的生长季如果云掩膜条件太严格会导致大量有效像元被剔除这时候可以考虑对被云遮挡的区域做时间维度的插值补全。关于时间维度的处理我这里再多说一句。聚合多期影像时云掩膜后的空缺区域可以用前后时相的NDVI插值来填补。最简单的做法是按地块统计每个时期的平均NDVI遇到缺测时用相邻时期的线性插值补充。这个方法在长时序分析中很常用能有效减少云遮挡带来的时序断裂。3.3 区域裁剪让影像处理聚焦到你的地块边界上下载的Landsat影像范围是一整条轨道带宽度约185公里但我们的研究区域可能只是其中一个小地块。全图处理不仅效率低而且会把无关的地类城镇、水域、林地引入模型干扰产量预测的准确性。所以预处理中必须做一步区域裁剪。说到区域裁剪很多人第一反应是直接用rasterio的窗口读取功能。但更优雅、更贴合真实业务场景的做法是按矢量边界做掩膜裁剪。这里就要用到geopandas和rasterstats了。import geopandas as gpd from rasterio.mask import mask as rio_mask # 读取地块矢量边界 aoi gpd.read_file(farm_boundary.shp) # 确保矢量与影像坐标系统一 if aoi.crs ! src.crs: aoi aoi.to_crs(src.crs) # 按边界裁剪 with rasterio.open(red_path) as src: out_image, out_transform rio_mask( src, aoi.geometry, cropTrue, nodatanp.nan ) out_meta src.profile.copy() out_meta.update({ driver: GTiff, height: out_image.shape[1], width: out_image.shape[2], transform: out_transform }) # 保存裁剪结果 with rasterio.open(red_cropped.tif, w, **out_meta) as dst: dst.write(out_image)这里提醒一个我在实际项目中反复遇到的问题——矢量边界和影像的坐标系不一致。最常见的情况是矢量数据使用WGS 84经纬度坐标EPSG:4326而Landsat影像使用的是UTM投影坐标比如EPSG:32650。如果不做投影转换直接裁剪边界会完全对不上。上面的代码里用aoi.to_crs(src.crs)解决了这个问题务必保留这一步。4. 作物特征提取从单波段到NDVI、EVI与物候特征4.1 NDVI、EVI、NDWI的计算与物理意义做完预处理之后就要进入整个流程中最核心的环节特征提取。所谓特征就是把原始的光谱波段组合成对农学状态更敏感的指标。我再强调一遍产量预测模型看到的不应该是原始波段而应该是这些有明确物理含义的指数。因为指数一方面能消除一部分大气、光照、地形的影响另一方面能突出作物本身的生物物理特征。最常用的指数毫无疑问是NDVI归一化植被指数。公式如下NDVI (NIR - Red) / (NIR Red)它利用的是植被在近红外波段强反射、在红光波段强吸收的光谱特性。裸土的NDVI通常接近0甚至为负值健康植被的NDVI在0.6到0.9之间。NDVI和叶面积指数、光合有效辐射吸收比例有很强的相关性所以它算得上是产量预测模型中最基础的输入特征。可以用numpy一行代码计算ndvi (nir_clean - red_clean) / (nir_clean red_clean) # 避免除零 ndvi[(nir_clean red_clean) 0] np.nan但NDVI有一个很明显的问题作物长势茂盛时容易饱和。一片浓密的玉米田和一片稍微稀疏的玉米田NDVI可能都在0.85左右区分度很差。这时候需要引入EVI增强型植被指数EVI 2.5 * (NIR - Red) / (NIR 6 * Red - 7.5 * Blue 1)EVI里增加了蓝光波段并对红光做了气溶胶修正在高植被覆盖区比NDVI更敏感。计算时注意Landsat 8/9的蓝光波段是Band 2blue_path LC08_L2SP_119039_20230815_20230817_02_T1_SR_B2.TIF with rasterio.open(blue_path) as src: blue src.read(1).astype(float32) * 0.00001 L 1 C1 6 C2 7.5 G 2.5 evi G * (nir_clean - red_clean) / (nir_clean C1 * red_clean - C2 * blue L) evi[(nir_clean C1 * red_clean - C2 * blue L) 0] np.nan除了反映生长状态和绿度的指数水分状况也是决定产量的关键因素尤其对处在灌浆期的作物来说水分胁迫直接能造成几成减产。NDWI归一化水分指数就是监测冠层含水量变化的常用指标NDWI (Green - NIR) / (Green NIR)这里绿光波段是Band 3。注意这个NDWI的公式版本和McFeeters提出的水体指数并不相同用于作物水分监测时这里的绿光受叶片水分吸收的影响近红外反映植被结构和水分含量组合起来对叶片水分变化相当敏感。green_path LC08_L2SP_119039_20230815_20230817_02_T1_SR_B3.TIF with rasterio.open(green_path) as src: green src.read(1).astype(float32) * 0.00001 ndwi (green - nir_clean) / (green nir_clean) ndwi[(green nir_clean) 0] np.nan这三个指数各有侧重在实际建模中不建议只用NDVI最好把EVI、NDWI一起作为候选特征让模型自己去选择哪些特征更有效。4.2 时间序列特征峰值、累积值和生长速率单期影像的指数值只能代表作物的一个瞬时状态。如果要预测最终产量我更推荐构建整个生长季的时间序列特征。先说一个反直觉但很重要的规律最终产量和生长季某一时刻的NDVI相关性并不高但和NDVI在生长季内的时间积分值相关性非常高。原因很简单作物的干物质积累是光合作用在整个生长季中不断累积的结果想用一个瞬间的快照替代一整个季节的生物量积累过程逻辑上就站不住脚。所以做产量预测时时间序列上的统计特征往往比任何单期影像都重要。常见的时序特征包括NDVI峰值Peak NDVI反映作物生长的最大繁荣程度一般出现在抽穗期到灌浆期之间。NDVI累积值Time-integrated NDVI或称为SINDVI对整个生长季的NDVI曲线做积分可以理解为生长季内光合有效辐射总量的近似值。NDVI增长速率从出苗到峰值这一段斜率反映了作物的生长速度和长势趋势。NDVI衰退速率从峰值到收获期的下降速度和作物的成熟速度及早衰情况有关。生育期长度NDVI超过某一阈值如0.3的时间跨度。这些时间序列特征提取首先需要把各期影像按时间顺序排列然后对每个像元分别计算统计量。一个基础但完整的计算思路如下import os import glob # 假设你有多个时期的NDVI GeoTIFF文件 ndvi_files sorted(glob.glob(ndvi_*.tif)) dates [...] # 对应的日期列表可以用datetime格式 # 将所有NDVI读入一个三维数组时间×高度×宽度 ndvi_stack [] for f in ndvi_files: with rasterio.open(f) as src: ndvi_stack.append(src.read(1)) ndvi_stack np.stack(ndvi_stack, axis0) # 峰值NDVI peak_ndvi np.nanmax(ndvi_stack, axis0) # 累积NDVI简单求和也可做梯形积分 cum_ndvi np.nansum(ndvi_stack, axis0) # 生长速率可以用峰值出现前NDVI的平均斜率来近似这里提醒一个容易出问题的地方处理时间序列时NaN值的处理策略很关键。某个像元如果某一期是云直接用nansum或者nanmax计算时会跳过它。但如果跳过太多统计量会失真。比如一个像元在整个生长季只有三期有效观测值另一个像元有八期有效观测值两者算出来的累积NDVI就不具备可比性。所以在时序聚合之前最好先统计每个像元的有效观测次数并设定一个最低阈值。我自己的阈值为总期数的60%低于这个阈值的像元在建模时直接过滤掉。4.3 地块尺度特征聚合从像元到农田管理单元遥感指数和时序特征都是像元级别的。但在真实的农业生产和管理中我们关心的是地块尺度的产量。把像元级的特征聚合到地块矢量多边形尺度是一种更接近业务落地需求的处理方式。最常用的聚合统计量是均值、标准差、中位数和分位数。均值代表这个地块的整体长势标准差反映地块内部的均匀程度——如果一个大田块内标准差很大说明长势不一致可能存在养分分布不均或墒情不均的问题。用rasterstats库可以非常简洁地实现这个聚合过程from rasterstats import zonal_stats # 假设vector为地块矢量ndvi_tif为某期NDVI栅格文件 stats zonal_stats( farm_boundary.shp, ndvi_20230815.tif, stats[mean, std, median, p25, p75, max, min] ) # 结果是一个列表每个元素对应一个地块的统计字典 print(stats[0])这段代码的关键在于栅格和矢量文件必须位于同一坐标系下rasterstats内部会做像素采样和矢量叠加统计出每个多边形内部所有像元的聚合值。得到每个地块的NDVI均值、峰值、累积值后再接上历年产量记录就构成了建模数据集的一行一行样本。我在实际项目中通常会把多个时相的统计量都拼成宽表格式地块ID生长季NDVI_peak生长季NDVI_integralEVI_peakNDWI_peak历史产量(kg/亩)A0010.8716.420.710.26612.5A0020.7914.880.640.21548.3A0030.9117.160.750.30648.7有了这张表就可以进入建模环节了。5. 从特征到产量回归建模、精度检验与实战调参5.1 为什么先试线性回归再上随机森林和梯度提升树农业产量和环境因子之间的关系并不是纯线性过程但线性模型在遥感估产中依然有很重要的地位。原因也很实在一是线性模型简单、稳定、不容易过拟合在训练数据量有限的情况下比如一个县只有几十个地块的产量记录复杂模型反而容易学到噪声。二是线性模型的系数可以解释比如某个特征的回归系数为正值说明它和产量正相关这对面向农学专家的业务汇报很有价值。我自己的建模习惯是先用线性回归跑一遍基线看看特征和产量之间最基本的关联强度再上随机森林或梯度提升树来做非线性拟合。这样做的好处是能快速判断数据集中是否存在明显的bug——如果强特征在简单线性模型里都毫无表现那数据集大概率有问题先检查再优化。下面是最简线性回归建模代码import pandas as pd from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import r2_score, mean_absolute_error # 读取特征表 df pd.read_csv(field_features_yield.csv) # 特征列和目标列 features [ndvi_peak, ndvi_integral, evi_peak, ndwi_peak] X df[features] y df[yield_kg_mu] # 划分训练测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) # 训练线性回归 model LinearRegression() model.fit(X_train, y_train) # 预测与评估 y_pred model.predict(X_test) print(R2:, r2_score(y_test, y_pred)) print(MAE:, mean_absolute_error(y_test, y_pred)) print(Coefficients:, dict(zip(features, model.coef_)))如果线性回归的R²很低比如低于0.4接下来就尝试非线性模型。在农业估产场景中随机森林和梯度提升树比如XGBoost或LightGBM都是非常成熟的选择。以随机森林为例from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators300, max_depth6, min_samples_leaf3, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) y_pred_rf rf.predict(X_test) print(Random Forest R2:, r2_score(y_test, y_pred_rf)) print(Random Forest MAE:, mean_absolute_error(y_test, y_pred_rf)) # 查看特征重要性 importance pd.Series(rf.feature_importances_, indexfeatures).sort_values(ascendingFalse) print(importance)对于随机森林有两个参数需要重点调max_depth和min_samples_leaf。限制树的深度能有效防止过拟合尤其是在训练样本量不大的情况下。min_samples_leaf保证叶子节点有足够多的样本避免模型对个别地块的产量值产生强记忆。n_estimators设到300基本够用再增加对精度提升很小反而消耗计算时间。如果是梯度提升树XGBoost在中小数据集上表现稳定LightGBM在数据量较大时训练速度很快。具体选哪个可以都试一下用交叉验证对比表现。5.2 交叉验证与特征重要性分析防止评估分数骗人单一的训练测试集划分运气成分太大——如果测试集恰好分了几个长势特别好的地块R²会虚高如果分了几个受灾地块R²又会断崖式下跌。在小样本的估产任务上单次train_test_split完全不够看更可靠的做法是K折交叉验证。from sklearn.model_selection import cross_val_score scores cross_val_score(rf, X, y, cv5, scoringr2) print(CV R2:, scores) print(CV R2 mean:, scores.mean(), /-, scores.std())交叉验证的核心逻辑是把所有样本切成K份每次用K-1份训练、剩下1份测试轮流做K次。这样每个样本都被测试过一次评估结果对数据划分的依赖大大减弱。做完交叉验证之后如果发现R²均值和单次测试结果差异很大比如单次测试R²0.72但CV均值只有0.35那基本可以断定单次划分是有偏差的应该以交叉验证的结果为准。特征重要性分析同样值得重视。用随机森林的feature_importances_属性可以查看每个特征对模型的贡献度。在产量预测的项目里我发现NDVI累积值的特征重要性通常排在第一位其次是峰值NDVI。这个结果和农学常识是一致的——最终产量更依赖整个生长季的光合产物累积而不只是某一刻的长势好坏。如果模型表现出不一样的特征排序比如某个月份的单期EVI重要性最高那可能需要思考研究区域的降雨或温度分布情况判断模型是否捕捉到了特定的农学规律还是单纯对某个时期的噪声产生了过拟合。5.3 产量制图把模型预测结果写回每个像元模型训练好之后最终产出不应该只是对测试集样本的预测值列表而应该是一张空间连续的产量分布图。这样才能看出各个地块内部产量的空间差异为精准施肥、差异化收割等农事决策提供参考。制图的基本思路是把模型需要的特征栅格NDVI峰值、NDVI累积值、EVI峰值等逐像元地组织成表格送入模型预测再把预测结果写回栅格。# 假设已有各特征的m×n数组 ndvi_peak_arr ... # 二维数组 ndvi_integral_arr ... evi_peak_arr ... ndwi_peak_arr ... rows, cols ndvi_peak_arr.shape # 将特征展平成表 X_map np.stack([ ndvi_peak_arr.flatten(), ndvi_integral_arr.flatten(), evi_peak_arr.flatten(), ndwi_peak_arr.flatten() ], axis1) # 只预测有效像元非NaN valid_mask ~np.any(np.isnan(X_map), axis1) y_map np.full(rows * cols, np.nan) y_map[valid_mask] rf.predict(X_map[valid_mask]) # 重塑为二维并输出栅格 yield_map y_map.reshape(rows, cols) with rasterio.open( yield_prediction.tif, w, driverGTiff, heightrows, widthcols, count1, dtypefloat32, crssrc.crs, transformsrc.transform, ) as dst: dst.write(yield_map, 1)这步操作看上去简单但有一个隐性要求各特征栅格的行列数、坐标系、范围都必须完全一致。所以特征栅格最好在预处理阶段就统一裁剪到同一研究区域并保持原始影像的分辨率。如果特征栅格间存在像元偏移制图结果会出现边缘错位肉眼可能不容易发现但在叠加对比时会非常明显。产量图的单位取决于历史产量数据的单位比如kg/亩。输出tif后可以在ArcGIS Pro或QGIS中打开用色带渲染直观展示地块内部产量高低差异。6. 实际预测中的常见陷阱与处理心法6.1 云掩膜过度导致时序断裂的应对策略前面反复提到云掩膜的重要性和标准做法但凡事都有两面性。在南方多雨地区尤其是水稻生长的夏季能拿到无云影像的窗口期可能很短。如果严格要求每一期影像都必须云量低于10%一个生长季最后可能只凑出三四期有效影像时序分析的基本盘就崩了。面对这种情况我的处理策略有三条把Landsat 8和Landsat 9的影像结合起来使用两个传感器的重访周期错开相当于每8天就有一次观测机会好影像的获取概率会高不少。对做过云掩膜的NDVI时序做拟合插值。比如用scipy的插值函数对时间轴上的NDVI曲线做平滑把被云遮挡的时点补全。经典的Savitzky-Golay滤波器对NDVI时序平滑效果很好能较好地保留作物生长曲线的整体形态。在建模阶段引入观测期数作为辅助特征。这样模型能感知到哪些地块的时序信息更完整、哪些地块有较多的缺失信息避免把所有样本一视同仁地对待。从效果上来说方案二最符合遥感物候分析的习惯。Savitzky-Golay滤波器的原理是在滑动窗口内做多项式拟合用拟合值替代原始值在去除噪声的同时对峰值的保持能力优于普通移动平均。scipy.signal.savgol_filter可以直接调用窗口长度建议设置为奇数我的经验是设成7或9多项式的阶数设为2效果较为稳定。6.2 混合像元干扰怎么处理地块矢量裁剪与缓冲区的应用前面提到过Landsat影像的30米分辨率在地块尺度上一个像元内可能混入道路、沟渠、田埂、防护林等地物。正好压在田块边界上的像元光谱值混合了作物和非作物成分如果大量混入统计结果特征和产量之间的真实关系会被显著稀释。解决混合像元问题最简单有效的办法是对地块矢量边界做负缓冲区。我的实际操作中通常在地块边界内部收缩10到20米再去做zonal_stats统计。比如一个100米宽的地块两边各缩进10米后用的是中间80米宽的内部区域能明显减少边界混合像元的干扰。这个方法做起来成本极低效果却是立竿见影的。用shapely的buffer方法就能实现# 收缩地块边界10米 aoi_buffered aoi.geometry.buffer(-10)需要注意缓冲区参数是负值。正值是向外扩负值才是向内收缩。如果地块本身很小比如宽度小于30米收缩10米后可能只剩很窄一条甚至完全消失这种情况下建议把收缩距离改成5米或直接用原始边界再配合像元面积加权统计来改善。6.3 模型过拟合样本量不足时的保守选择一个县有几百个地块的产量记录已经算不错的数据条件了。但在实际项目中我经常遇到的情况是只有几十个地块的产量记录。样本量这么少的情况下随机森林或者XGBoost这类复杂模型会非常容易过拟合——训练集上R²高得惊人一拿到新数据就崩。这种情况下我有几条建议优先选择简单的线性模型或带强正则化的模型比如Ridge回归、Lasso回归。少量样本、少量特征条件下它们的预测稳定性往往优于复杂模型。如果一定要用随机森林粗调参数后马上做交叉验证并且留意单棵树的复杂度max_depth设置6以内、min_samples_leaf设置5左右是比较保守的选择。减少特征数量只保留农学意义最明确的特征比如NDVI峰值和NDVI累积值。特征太多但样本太少模型学到的大概率是噪声。如果条件允许尝试把多个年度的数据合并在一起来扩充样本量比如用2020到2023年四年的地块产量记录配对应年份的遥感特征统一建模。这个方法能显著提升模型的泛化能力代价是数据准备时间更长。6.4 精度验证里最容易被忽略的一步空间自相关这块内容放到最后想特别强调一下。遥感数据和产量数据都有很强的空间自相关性——相邻两个地块的长势往往相似产量也往往接近。如果训练样本中有两个相距不到100米的地块它们的遥感特征高度相似模型可以很轻松地“记住”其中一个的产量并用来预测另一个。这样一来交叉验证的R²会比真实泛化能力虚高不少。更严格的做法是采用空间分组交叉验证。比如按一定距离对地块做空间聚类确保同一组内的地块在空间上相互远离再按组划分训练集和测试集。这样模型在测试集上的表现才能反映真实业务中预测全新地块时的效果。python中有sklearn的KMeans可以按经纬度做聚类实现空间分组虽然不够精细但作为初步检验工具完全够用。这一步在很多遥感估产的论文里都会被弱化甚至忽略但实际操作中我发现当数据本身存在强空间自相关时普通交叉验证和空间交叉验证的R²差异可能达到0.2以上。做产量预测项目时建议把空间交叉验证作为最终的检验标准。7. 写在最后从“跑通流程”到“拿到可信结果”的三点体会第一点遥感估产项目的成败数据质量永远比模型算法更重要。我见过太多人把精力花在调模型参数上却忽略了云掩膜不干净、坐标系不一致、地块边界偏移这些基础问题。基础数据做好了哪怕只用一个线性回归都能得到相当不错的结果基础数据有硬伤再花哨的深度学习模型都救不回来。第二点要花时间梳理清楚农学逻辑。做产量预测不只是让模型去拟合数字更要理解作物在关键生育期的生理需求——拔节期缺水会严重影响茎秆和叶面积建成灌浆期光照不足会直接影响籽粒充实。每一类特征背后对应的农学过程想明白了选特征、解释模型、判断结果合理性这些环节就会顺畅很多。第三点这套流程里每一步的代码都不难难的是把整条链路串起来调试。遇到预测结果明显异常的时候建议倒着排查——先检查特征栅格有没有异常值再检查掩膜是否生效最后再回头检查模型输入输出维度是否对齐。按照这个顺序走绝大多数问题都能定位出来。最后分享一个小习惯我每次跑完一批数据都会把中间产物裁剪后的波段、云掩膜后的指数栅格、地块统计表按日期和版本编号保存下来。表面上看多占了一些磁盘空间但有一版出问题需要回溯的时候就能省下大量重新处理的时间。农业遥感和别的工作有一个共性就是对可重复性要求极高今年做的方法明年还要用数据组织和代码注释规范一些长期来看回报非常可观。