GEE批量下载Sentinel-1数据:任务队列与断点续传全指南
简介这套代码包专为需要批量获取Sentinel-1雷达影像的研究者设计依托Google Earth Engine云平台免去本地下载与预处理的重型计算。代码涵盖四个核心环节其一是定义矢量或坐标范围作为研究区其二是按极化方式、轨道方向、时间区间等条件过滤ImageCollection其三是将Sentinel-1原始的后向散射系数从dB单位转换为线性单位确保后续定量分析准确其四是通过逐景映射完成裁剪后批量Export至Google Drive。压缩包共3个文件、总体积5KB包含一个inscode脚本GEE代码编辑器的可执行工程、一个index.html用于结果展示或参数界面和一个.gitignore版本控制忽略规则结构小巧而清晰。目前已有340人下载学习适合具备基本GEE编程概念、希望直接复用现成流程缩短开发周期的遥感与GIS从业者。1. GEE批量下载Sentinel-1数据命令好写任务队列才是真正的门槛GEE批量下载Sentinel-1数据听起来就是把影像集合循环导出成GeoTIFF真正跑过的人都知道卡点从来不在导出那几行命令而在成百个任务怎么排队、怎么少失败、怎么断点续传。这个方向适合要做时序分析的从业者洪水淹没提取、水稻制图、船舰检测、地表形变监测都需要某个区域一年几十上百景哨兵一号SAR数据。用GEE的GRD产品配合云端硬盘批量导出是目前最省人力的路径。但它有几个硬门槛任务配额、投影设置、极化过滤每一项踩错都可能整批重跑。这篇我按实际调试顺序从集合组织讲到参数设置再讲四个高频翻车现场和一套断点续传的办法。2. 先搞清Sentinel-1在GEE里的组织方式集合、条带与极化很多人一上来就写Export结果导出的不是自己想要的。批量下载的第一步是确认目标数据存在哪个集合、是什么单位、有哪些可过滤的属性。这一步错了后面所有任务都是白跑。2.1 GRD与GRD_FLOATdB与线性后向散射选错单位会带偏分析GEE目录里哨兵一号最常用的两个集合是COPERNICUS/S1_GRD和COPERNICUS/S1_GRD_FLOAT。两个集合的数据源头和处理流程一致都做了热噪声去除、辐射定标、基于SRTM的地形校正区别只在存储单位GRD直接存后向散射系数的dB值GRD_FLOAT存线性功率值。也就是说GRD里水体像元通常在-18到-22 dB而GRD_FLOAT里对应的数值是0.01到0.02这种极小的小数。我一般按目标选如果只是看图、做阈值分割、训练分类器GRD的dB值更直观拉伸范围设成[-25, 0]就能看清地物如果要做后向散射时间序列的定量分析或者要把多个传感器数据放一起比较GRD_FLOAT更合适因为dB值在时间平均时会引入系统性偏差。需要显示FLOAT数据时用10*log10转回dB即可// 把线性后向散射转成dB显示dB 10 * log10(linear) var s1_float ee.ImageCollection(COPERNICUS/S1_GRD_FLOAT) .filterBounds(roi) .first(); var db ee.Image(10).multiply(s1_float.select(VV).log10());参数说明ee.Image(10)是常数图像log10()对每个像元取对数两者相乘得到dB值。注意这里不要直接对GRD_FLOAT集合里的影像做算术平均后再转dB正确顺序是先转dB再平均否则小数值被平均后放大的误差会污染时序曲线。另外两个集合的波段名都统一成了VV、VH或HH、HV不再有原始产品里那种带极化顺序的长波段名这让后面的select省了很多事。2.2 集合过滤的三个必写条件范围、时间与轨道对哨兵一号GRD集合做过滤我习惯固定写四行少了哪一行都会给后面留坑。filterBounds负责把覆盖研究区的影像筛出来filterDate负责时间窗剩下两个不是可选项而是准必须项instrumentMode等于IW以及orbitProperties_pass等于ASCENDING或DESCENDING。IW是干涉宽幅模式是覆盖大陆区域的主力模式条带模式SM和海洋模式EW的几何、分辨率、极化组合都不一样混进集合会让时间序列参差不齐。升轨和降轨的成像几何完全相反侧视方向不同同一地物的雷达亮度在两轨上有系统差异混用等于给数据凭空加噪声。// 研究区先用点缓冲跑通以后再换成完整矢量边界 var roi ee.Geometry.Point([120.12, 30.31]).buffer(5000); var s1 ee.ImageCollection(COPERNICUS/S1_GRD) .filterBounds(roi) .filterDate(2023-06-01, 2023-07-01) .filter(ee.Filter.eq(instrumentMode, IW)) .filter(ee.Filter.eq(orbitProperties_pass, ASCENDING)) .filter(ee.Filter.listContains(transmitterReceiverPolarisation, VV)); print(影像数量, s1.size()); print(第一景时间, s1.first().get(system:time_start)); print(波段, s1.first().bandNames()); print(轨道号, s1.first().get(relativeOrbitNumberStart));参数说明listContains是这里的关键——transmitterReceiverPolarisation在GEE里是一个列表属性比如[VV,VH]不能拿eq直接跟字符串比必须用listContains判断数组里是否包含目标极化。buffer(5000)把点扩展成半径5公里的圆形测试区足够验证集合和导出链路。先跑这段确认打印出的波段里有VV、轨道号正常再往下走。如果是做形变类时序分析我会再加一个relativeOrbitNumberStart过滤让所有影像落在同一个轨道框架下。轨道号不同意味着入射角不同直接按时间画后向散射曲线会抖得很厉害这种抖动不是地物变化是几何视角差异。2.3 为什么在GEE里批量下载而不是去原始分发站点一景一景抓GRD在GEE里是“成品”数据。去原始分发站点拿L1级产品要自己处理热噪声、辐射定标、地形校正逐景下载还要维护轨道文件列表几十景的批量任务很容易变成半个星期的体力活。GEE把预处理做掉了还把下载复杂度收敛成“集合过滤加逐景导出”这是它最大的价值。边界条件也要说清楚如果你做的是需要严格基线控制的干涉处理、需要单视复数SLC数据或者对轨道精度有特殊要求GRD产品不合适GEE也不是该走的路。这个批量下载方案针对的是“后向散射影像”这个层次的需求拿到手就能做分类、阈值分割和时序统计。3. 最小可跑的批量导出脚本先验证单景再放出一整年的任务队列集合过滤确认无误后按“先单景后批量”的顺序跑。直接放一百个任务上去再检查失败率一定高。我先导出测试景确认几何、波段、投影都对再循环放任务。3.1 先导出一景测试景验证集合是否筛对测试景的目的有两个一是确认select的波段在目标影像里真实存在二是确认导出参数组合不会报错。用前面过滤好的集合先导出第一景// 导出测试景到云端硬盘crs固定为UTM 50N避免WGS84下10米无法表达 Export.image.toDrive({ image: s1.first().select(VV).clip(roi), description: S1_test_202306, folder: S1_download, region: roi, scale: 10, crs: EPSG:32650, maxPixels: 1e9, fileFormat: GeoTIFF });参数说明clip(roi)把影像裁到研究区避免导出整条条带造成浪费crs固定为EPSG:32650是因为roi落在UTM 50N分区10米分辨率在这个投影下是真实的地面距离。如果不指定crsGEE默认按WGS84输出那10米会被解释成10度出来的就是一张分辨率惨不忍睹的巨型图几乎必然触发maxPixels报错。提交后等一两分钟去Tasks面板看状态下载到本地用GIS软件打开确认数值范围、坐标原点都正常再进行批量。3.2 按时间戳循环生成任务队列单景验证通过后批量部分的核心是“遍历时间戳每个时间戳下的每条条带生成一个导出任务”。这里有个容易忽略的点同一个时刻研究区可能被两到三条相邻条带同时覆盖按日期去重会把其中几条漏掉。我用时间窗口把同一时刻的多条条带都取出来逐个导出// 获取该区域全部影像的时间戳毫秒一次性取回客户端 var timestamps s1.aggregate_array(system:time_start).getInfo(); timestamps.forEach(function(ms) { var d new Date(ms); var pad function(x) { return (x 10 ? 0 : ) x; }; var dayStr d.getFullYear() pad(d.getMonth() 1) pad(d.getDate()); var hhmm pad(d.getHours()) pad(d.getMinutes()); // 同一时刻可能跨多条条带用正负一分钟窗口取回全部景 var sameTime s1.filterDate( ee.Date(ms).advance(-1, minute), ee.Date(ms).advance(1, minute) ).toList(10); var subCount sameTime.size().getInfo(); for (var i 0; i subCount; i) { var img ee.Image(sameTime.get(i)); var desc S1_ dayStr _ hhmm _ (i 1); Export.image.toDrive({ image: img.select([VV, VH]).clip(roi), description: desc, folder: S1_batch, region: roi, scale: 10, crs: EPSG:32650, maxPixels: 1e10, fileFormat: GeoTIFF }); } });逻辑说明getInfo()是同步调用把服务器端的时间戳数组取回客户端变成普通JS数组后面才能用forEach循环。每个时间戳对应一次过境同一秒内过境的影像用filterDate配正负一分钟窗口全部捞出来避免丢失同轨相邻条带。desc里带上日期、时刻和序号保证任务描述唯一——任务描述重复会直接提交失败这是批量场景里最常见的低级错误。循环里每次toList(10)最多处理10条条带对绝大多数陆地场景够用。3.3 任务提交后去哪看Tasks面板与任务状态轮询批量任务提交后右侧Tasks面板会依次出现READY、RUNNING、COMPLETED、FAILED四种状态。READY表示在排队等配额不是出错RUNNING超过一两个小时就要警惕。几百个任务时逐个盯面板不现实我用代码把最近任务的状态拉出来核对// 列出最近100个任务的id、描述与状态复制到表格里做清单 ee.data.getTaskList().slice(0, 100).forEach(function(t) { print(t.id, t.state, t.description); });参数说明t.id是任务的唯一编号t.description就是提交时写的描述名后面断点续传时要拿它跟本地文件名对账。复制控制台输出的这些描述名存成文本就是你的导出清单。4. 影响批量导出成败的四个参数分辨率、投影、maxPixels与分块批量导出失败绝大多数不是代码逻辑问题而是参数组合不对。四个参数最值得花时间调crs、crsTransform、maxPixels、fileDimensions。4.1 crs与crsTransform为什么“10米”在WGS84下会导出失败GEE里的scale不是绝对分辨率它表示输出CRS下的像元尺寸。默认CRS是EPSG:4326单位是度这时scale: 10代表10度差不多1110公里GEE会尝试把影像重采样成这种巨像元然后区域里的像素数反而少得反常任务要么报像素超限要么报重采样失败。正确做法是给输出指定投影坐标系。// 根据研究区中心点经度估算UTM分区号拼出crs var lon roi.centroid().coordinates().get(0).getInfo(); var zone Math.floor((lon 180) / 6) 1; var crsStr EPSG:326 (zone 10 ? 0 zone : zone); print(建议输出投影, crsStr);参数说明UTM分区号等于经度加180除以6取整再加1。北半球用326开头南半球用327开头例如南纬区域是EPSG:32750。这段代码在批量循环外面跑一次把crs填进每个导出任务。如果你的结果要跟其他栅格严格对齐可以用crsTransform锁定网格原点格式是[x分辨率, x剪切, y剪切, y分辨率, 原点x, 原点y]其中y分辨率为负数表示影像自上而下排列比如[10, 0, 0, -10, 466960, 3500520]。日常批量下载不强制用但如果你是做时序栈叠加建议固定它省得每期影像网格原点漂移。4.2 maxPixels与fileDimensions大区域导出的两条出路maxPixels默认是1e8单景IW条带范围裁剪后通常够用但如果你把多景mosaic成一张大图再导出像素数轻松破亿。我的建议是能不加maxPixels就不加优先改导出策略。参数默认行为建议值 / 使用场景scale取影像原始分辨率与crs配套使用别单独改crsEPSG:4326按UTM分区指定如EPSG:32650crsTransform由crs和scale推导多期影像网格对齐时用maxPixels1e8大区域提到1e9到1e10再高容易撑爆worker内存fileDimensions整幅输出设成[10240, 10240]切块适合超大区域大面积导出我一般不做mosaic而是每景单独导出让每个任务的像素数可控。SAR影像不同条带之间有辐射差异mosaic还会在条带重叠区引入拼接痕后续分析反而要再处理。fileDimensions适合你确实需要一张完整大图的场景输出会自动加_0_0这种分块后缀下载后本地用栅格工具合并。4.3 极化选择集合过滤只负责“有没有”select负责“下哪些”集合里过滤了VV不代表每景都有VH。IW模式下绝大多数GRD产品是VVVH双极化但单极化场景和覆盖异常的场景确实存在。如果select([VV,VH])里有一个波段在某景中不存在整个导出任务直接失败。保险做法是把两个极化都写进集合过滤// 保险做法VV和VH都要求存在select才不会中途报错 var s1_dp s1 .filter(ee.Filter.listContains(transmitterReceiverPolarisation, VV)) .filter(ee.Filter.listContains(transmitterReceiverPolarisation, VH));参数说明两条listContains是“并且”关系最终集合里的每一景都同时含VV和VH波段select可以放心用。如果你只要VV做单极化时序也要保持只过滤VV、只select VV别顺手把VH带上。另外注意海洋模式EW下的GRD产品极化组合是HHHV不是VVVH。要下海洋区域时把instrumentMode换成EW极化过滤同步换成HH或HV这一条经常有人踩。5. 批量下载Sentinel-1的常见坑与排查从任务失败到数据断档批量任务跑起来以后问题不会只出现在代码阶段。这里整理四个高频场景全部按“现象、原因、解决”来写能直接对着排查。5.1 任务在队列里报错先看这张错误特征对照表Tasks面板里FAILED任务的错误信息要主动点开看不要只看状态。常见的报错特征和处理路径如下错误特征Tasks面板摘要常见原因第一时间做什么Band VH does not existselect里写了某些景不存在的波段打印bandNames核对极化条件补进集合过滤Computed image has too many pixels像素数超过maxPixels提高maxPixels或按fileDimensions切块Export region must be a Geometryregion传了FeatureCollection调用.geometry()或在Map手动画几何Invalid region / Geometry has too many edges研究区边界过细或自相交用simplify(maxError)简化后再导出Project has too many concurrent exports同时提交的任务超过项目配额分批提交每批控制在50个以内我踩得最多的是第二条和第三条。第三条的隐蔽之处在于从GIS软件导出的矢量在GEE里默认是FeatureCollection直接塞进region参数必报错解决就是在导入时first().geometry()取几何体。第一条的办法则在4.3已经写好把VH也过滤进去从源头掐断。5.2 大面积区域导出“超限”或者任务永远Running现象任务状态长时间停在RUNNING或者提交后立刻FAILED错误里带pixels或memory字样。原因区域面积大又用了mosaic单任务像素数爆炸或者一次提交了几百个任务把项目的并发预算和小伙伴共用资源一起挤爆。解决先按条带导出不做mosaic把每个任务的区域收敛到单景覆盖范围再不行就fileDimensions: [10240, 10240]切块单块像素量立减。maxPixels最多加到1e10就收手再往上加容易把执行节点内存打爆任务反而挂掉。注意同一个项目里多个人同时放任务共享并发配额。放批量任务前跟同项目的人打声招呼避免互相踢掉对方的RUNNING任务这种事发生过不止一次。5.3 云端硬盘里下载一半断掉是重下不是重导出现象浏览器下载大GeoTIFF到一半断掉第一反应是回GEE重新跑任务。原因任务其实早就COMPLETED了GeoTIFF完整躺在云端硬盘里断的是浏览器下载通道不是导出链路。重新导出既烧配额又浪费时间。解决不要重跑任务。下载这一步改用支持断点续传的方式挂同步客户端或者写脚本调Drive API分块拉取都比浏览器稳。批量几百个文件时尤其明显浏览器逐个另存为的操作量不现实。下载完成后跟任务清单对账缺哪个补哪个对账脚本见第6章。5.4 时序断档与边缘覆盖不足升轨降轨分开存现象小区域一年只导出十几景相邻日期间隔超过两周或者部分导出图有一半区域是NoData。原因单轨道重访周期本身就长加上研究区靠近条带边缘时同一景影像只覆盖研究区一部分硬导出就会得到半幅黑图。混用升轨降轨虽然能加密时间序列但两套几何的亮度不可直接比。解决升轨和降轨都导但按轨道方向分目录存分析时再做配准和归一化。对边缘覆盖不足的景加一个覆盖率过滤覆盖少于八成的直接跳过别把NoData大片带进时序// 覆盖率过滤相交面积占比低于0.8就跳过这景 var covRatio img.geometry().intersection(roi, 1).area().divide(roi.area()); if (covRatio.getInfo() 0.8) { Export.image.toDrive({ image: img.select(VV), description: desc, folder: S1_pass, region: roi, scale: 10, crs: crsStr, maxPixels: 1e10 }); }逻辑说明intersection(roi, 1)的第二个参数是误差容差单位米1米足够面积比等于相交面积除以研究区面积。getInfo()拿到数值后跟阈值比较是客户端分支脚本里很好用只是循环里每景多一次网络往返几十景规模完全能接受。6. 进阶用文件名指纹做断点续传重跑不再从头再来最后这套东西是我被整批重跑教训过之后总结出来的。批量下载的痛点是文件缺几个你往往要翻半天才知道缺谁最后干脆全部重跑。我的习惯是把任务描述直接定为“文件指纹”让对账变成一条命令的事。指纹用影像的system:index这是GEE里每景的唯一标识结构类似S1A_IW_GRDH_1SDV_20230615T101230_20230615T101255_043791_0530EC_2F19包含卫星、模式、时间、轨道和产品编号全程只有字母数字和下划线天然适合当文件名。提交任务前清洗一遍去掉意外出现的非法字符// 拿系统唯一索引当任务描述清洗后做文件名指纹 var rawIndex img.get(system:index).getInfo(); var desc rawIndex.replace(/[^a-zA-Z0-9_\-]/g, _);然后按四步走第一步批量任务提交后从Tasks面板把任务列表导出成CSV或用3.3的代码打印描述名存成文本第二步云端硬盘的全部文件同步到本地一个目录对应一个folder第三步跑下面的对账脚本列出本地缺失的描述名第四步只对缺失清单重新提交导出已经完成的景完全不动。import os, re, csv, sys local_dir sys.argv[1] # 本地存放GeoTIFF的目录 manifest sys.argv[2] # 从GEE导出的任务CSV have set() for f in os.listdir(local_dir): m re.search(r(S1[AB]_[A-Z0-9_]), f) if m: have.add(m.group(1)) need [] with open(manifest, encodingutf-8) as fp: for row in csv.reader(fp): for token in ,.join(row).split(): if re.match(r^(S1[AB]_[A-Z0-9_])$, token) and token not in have: need.append(token) print(本地已有:, len(have), 缺失:, len(need)) for t in sorted(set(need))[:100]: print(t)说明re.search从文件名里提取指纹re.match严格校验CSV里的描述名。因为指纹就是system:index清洗后的结果本地文件名、任务CSV、GEE影像三方能直接对上不需要任何人工翻译。重跑前如果要清理还在排队或运行的旧任务可以用ee.data.cancelTask(t.id)逐个取消但这段代码会停掉所有任务只在你确认要重来时用。这整套流程我现在每次放批量任务前都会走一遍建目录、存CSV、同步、对账缺哪景补哪景再没为一次断网重跑整批。指纹命名的好处是长期积累后目录里全是规律文件名别人接手也看得懂。希望帮到你。本文还有配套的精品资源点击获取