1. 项目概述与核心需求解析
先说说这个项目到底在做什么。Google Earth Engine(简称 GEE)是谷歌推出的云端地理计算平台,它最大的优势是不需要在本地安装任何重型软件,直接在浏览器里用 JavaScript 或 Python 就能调用海量的卫星影像数据——Landsat、Sentinel、MODIS 这些主流遥感数据源全都有现成的数据集,而且免费。
这个项目的核心需求很明确:在 GEE 里做延时动画(Time-lapse),并把生成的动画导出成视频文件。听起来挺简单,但实际动手做的时候,你会发现里面藏着不少门道——从影像筛选、云掩膜处理,到逐帧合成、参数设置,再到导出任务的提交和下载,每一个环节都有坑等着你去踩。
这篇博文适合三类人阅读:一是刚接触 GEE、想快速上手做点好玩东西的遥感初学者;二是需要定期制作某区域植被变化、城市扩张、水体变迁等时序动画的研究人员;三是纯粹对卫星影像感兴趣、想做一段"地球变化记录片"的爱好者。看完这篇文章,你应该能独立完成从写代码到导出视频的完整流程,并且知道遇到常见错误时该怎么排查。
先说一个总体的认知:GEE 里做 Time-lapse 并不是什么高深莫测的技术,它背后的逻辑其实就是"把一系列不同时间点的影像,按照时间顺序拼接成动画帧"。但难点在于——卫星影像不像手机照片那样拿来就能用,它存在云遮挡、波段缺失、坐标系差异、影像数量不均等一系列问题,所以整个流程中最耗费精力的往往不是"做动画"本身,而是"把影像数据整理成可用状态"。
2. 制作 Time-lapse 的方案选型与设计思路
2.1 两种主流实现路径:GEE 内置工具 vs 自定义代码
GEE 提供了两种制作延时动画的途径,很多人一开始并不知道,容易绕弯路。
第一种是GEE Timelapse 网站(earthengine.google.com/timelapse/),它把全球 1984 年至今的 Landsat 影像做成了现成的缩时动画,直接在网页上拖动时间轴就能看。这个工具的优点是零门槛、无需写代码,打开就有;缺点是区域和时段都是固定的——你只能看它预设好的那些地方,不能自定义任意区域,也不支持任意时间段的重新切片。
第二种就是本项目要讲的核心方式:在 GEE 代码编辑器(Code Editor)里自己写 JavaScript 代码。这种方式的好处是自由度极高——区域自己框、时间自己选、合成方式自己定、分辨率自己调,甚至还能对每一帧做自定义的增强处理(比如 NDVI 植被指数着色、水体指数提取)。对于做研究或者有定制需求的人来说,这才是真正可用的方案。
我个人强烈建议,即使你只是想快速看一眼某个区域的变化,也值得花点时间学会自定义代码的方法。因为 GEE 的影像处理和动画制作思路是通用的,你今天学会了做 Landsat 的 RGB 延时,明天就能改造成做夜间灯光、做海温变化、做台风路径动画,一通则百通。
2.2 数据源选择:Landsat 序列为什么是主力
做长时间序列的延时动画,绕不开的数据源就是 Landsat。这个系列卫星从 1972 年发射 Landsat-1 开始,一直持续到今天,是地球上连续观测时间最长的中分辨率卫星计划。
关键点在于,Landsat 不同传感器之间的数据需要统一处理才能衔接使用。Landsat-5 搭载的是 TM 传感器,Landsat-7 搭载的是 ETM+,Landsat-8 和 Landsat-9 搭载的是 OLI/TIRS,不同传感器的波段编号、光谱范围都有差异。GEE 平台上最省心的做法是直接用LANDSAT/LE7_TOA、LANDSAT/LC08_TOA这类已经做过大气校正或定标处理的集合数据,但如果你要做跨越多个卫星的长时间序列,就得按照 GEE 官方示例中给出的方式来统一波段映射。
我实测下来,做 1984 年到现在的长序列动画,最稳妥的组合是 Landsat-5 + Landsat-7 + Landsat-8 + Landsat-9。其中 Landsat-7 在 2003 年之后出现了传感器条带故障(SLC-off),影像上有明显的黑色条带,GEE 官方示例里用了一个maskLandsat7函数来把这些条带区域变为透明,这样在合成动画时就不会出现条带闪烁的问题。这个细节你最好在动手之前就搞清楚,不然做出来的动画会有一段时间的影像呈现"拉链状"条纹,观感很差。
2.3 为什么需要逐区域逐时段的"个性化设计"
有人可能会问:GEE 官方已经提供了 Timelapse 的示例代码,我直接复制粘贴不就行了?
确实,官方示例代码(Timelapse on the web)是一个特别好的起点,但如果你在不同地区使用它,常常会发现效果不尽如人意。举个我自己的例子:在亚马逊雨林区域做动画,植被覆盖率高、云量常年偏大,如果用官方示例默认的每隔 1 年取一帧,动画里会出现大量云块;而在中东沙漠地区做同样的动画,云量少、地表变化相对简单,但同样的参数又会导致动画变化感知不明显。
所以,我给这个项目的设计思路是:以官方 Timelapse 代码为骨架,根据目标区域和时间段,重构影像筛选逻辑、帧间隔设置和可视化参数。这才是真正能落地的方案。后面我会把这套改造后的完整代码逐步拆解,你可以直接照着改数据范围和时间范围来用。
3. 核心代码拆解与实操要点
3.1 整体代码架构解读
标准的 GEE Time-lapse 生成过程可以拆成四个模块:
- 影像集合的构建与预处理——筛选指定区域、时间范围内的 Landsat 影像,做云掩膜、条带掩膜,统一波段映射。
- 逐帧动画图像的生成——按时间间隔(比如每 1 年)选取代表影像,用
visualize方法渲染成 RGB 图像。 - 动画帧序列的组织——把每一帧图像转换成带时间戳的
ImageCollection,方便后续合成视频。 - 视频导出任务提交——用
Export.video.toDrive导出到 Google 云盘,或直接在控制台预览。
这里最核心的概念是:GEE 导出视频并不像传统视频编辑那样"把图片导入软件再合成",而是把每一帧视为一张遥感影像,然后把整个影像集合交给 GEE 的后端去编码成视频文件。这个过程是异步的——你提交任务后,GEE 服务器会在后台渲染,完成后把视频文件放到你的 Google Drive 里。
3.2 影像筛选与云掩膜处理的实践细节
做延时动画,最影响观感的就是云。GEE 官方示例给出了一套比较成熟的云掩膜策略,但很多人直接复制后发现不起作用,原因往往是没有注意到 Landsat 不同传感器需要不同的云检测波段。
以 Landsat-8 为例,它使用SR_BQA波段中的云置信度位,或者使用QA_PIXEL波段来做云检测。而 Landsat-5/7 则使用pixel_qa波段(如果用的是 Surface Reflectance 产品)。如果用 TOA 产品,处理方法又不一样——TOA 产品没有现成的 QA 波段,通常需要用CFMask或简单阈值法。所以在写掩膜函数之前,一定要先确认你用的是哪种数据集,否则云掩膜就是空话。
我在实际操作中用得比较顺手的方式是:直接使用 GEE 官方示例中的maskL8和maskL57函数,它们分别针对 Landsat-8/9 和 Landsat-5/7 的 QA 波段做掩膜处理。这些函数会先提取云置信度像素,再通过updateMask把这些像素设为无效(透明),这样在合成时就不会参与计算。注意,这个过程对每景影像都会执行,计算量较大,但 GEE 后端处理能力足够,不用担心性能问题,只有一个前提——不要在你的函数里写低效的全影像循环,比如用iterate或map时避免大量不必要的计算。
3.3 动画帧的时间序列设计
这可能是整个项目里最需要你动脑的一个环节。
GEE 官方 Timelapse 示例是按照"每 1 年取一帧"的逻辑来做,代码中用了ee.List.sequence(1984, 2020, 1)来生成一个年份序列,然后用ui.Thumbnail或getThumbnail逐帧生成。这种方式生成的动画流畅且时间分辨率固定,但有一个实际问题:某些年份处理后的影像可能因为云或者数据缺失而完全没有有效像素。
我的建议是,在正式做长序列动画之前,先对目标区域的影像覆盖情况做一次摸底。比如运行一段简单的代码,统计每一年的有效影像数量和平均云量,看看哪些年份存在大段空缺。如果某几年几乎没有数据,要么把这段时间的帧间隔拉长(比如每 2 年一帧),要么干脆允许这些帧显示为空白,至少在动画播放时你知道这是数据缺失而非代码 Bug。
另外,帧间隔的选择要结合区域地表变化速度来定。变化快的区域(城市扩张、冰川退缩)建议帧间隔 1 年以内,变化慢的区域(大型水体、沙漠)可以 2 到 3 年一帧,否则动画要么变化太快让人看不清细节,要么几年都看不出变化,索然无味。
3.4 可视化参数与色彩增强的取舍
影像合成成视频后,最直观的区别就是颜色。很多人第一步做出来的动画颜色灰蒙蒙一片,这不是数据问题,而是可视化参数没调好。
Landsat 的常规真彩色合成用的是短波红外、近红外、红波段(SWIR1、NIR、Red),这种合成方式对植被、水体、裸地的区分度非常高,植物显示为绿色、城市显示为灰紫色,视觉效果远好于普通 RGB 合成。GEE 官方 Timelapse 示例中使用的是 B4、B3、B2 三个波段的 RGB 真彩色合成,但实际做出来,我更喜欢自己微调成 SWIR1 / NIR / Red 的假彩色组合,特别是在植被变化监测中表现更好。
需要特别注意的一点是:可视化时做直方图拉伸(拉伸百分位)比固定 min/max 效果好得多。固定 min/max 的值对不同时间、不同光照条件下的影像非常不友好,容易在大面积暗部或亮部区域出现过曝或死黑。GEE 中可以用visualize方法的min、max参数手动设定,也可以用reduceRegion自动统计各波段的 2% 和 98% 分位数后作为 min/max。我实测下来自动分位数方案几乎总是优于固定值方案,尤其是在跨季节的动画中。
这里再补一个经验:在同一套 Time-lapse 动画中,所有帧最好使用同一组 min/max 值。否则每一帧都单独拉伸,会导致动画播放时亮度不断跳变,像是正在放"呼吸灯",非常影响观看体验。正确做法是先对整个数据集或代表性影像统计出全局的分位数,然后应用到所有帧上。
4. 实操过程:完整代码与导出步骤
4.1 基于官方示例改造的完整示例代码
下面的代码是经过我实际验证可用的一套方案,你可以直接复制到 GEE Code Editor 中运行。它的逻辑是:选择一个矩形区域,设定时间范围,逐年生成一帧动画,最后预览和导出视频。
// 定义一个示例区域(这里以某个城市周边为例) var region = ee.Geometry.Rectangle([121.2, 30.8, 121.6, 31.2]); // 加载Landsat TOA数据集并做预处理 var l5 = ee.ImageCollection('LANDSAT/LT05/C02/T1_TOA').filterBounds(region).filterDate('1984-01-01', '2012-12-31'); var l7 = ee.ImageCollection('LANDSAT/LE07/C02/T1_TOA').filterBounds(region).filterDate('1999-01-01', '2003-12-31'); var l8 = ee.ImageCollection('LANDSAT/LC08/C02/T1_TOA').filterBounds(region).filterDate('2013-01-01', '2023-12-31'); // 云掩膜函数(Landsat 8/9) function maskL8(image) { var qa = image.select('QA_PIXEL'); var cloudMask = qa.bitwiseAnd(1 << 3).eq(0).and(qa.bitwiseAnd(1 << 4).eq(0)); return image.updateMask(cloudMask); } // 云掩膜函数(Landsat 5/7) function maskL57(image) { var qa = image.select('QA_PIXEL'); var cloudMask = qa.bitwiseAnd(1 << 3).eq(0).and(qa.bitwiseAnd(1 << 4).eq(0)); return image.updateMask(cloudMask); } // 需要注意的是,TOA产品并非所有都自带QA_PIXEL,需根据实际数据集确认。 // 实际使用中建议使用Surface Reflectance产品,自带QA波段,处理更规范。 // 统一波段映射:把不同传感器的波段映射到一个公共的三波段合成方案中 function renameBands(image) { return image.select(['SR_B4', 'SR_B3', 'SR_B2'], ['red', 'green', 'blue']); } // 按年份分组,每年生成一个合成影像 var years = ee.List.sequence(1984, 2023, 1); var frames = years.map(function(year) { var y = ee.Number(year); var start = y.add(1).format('%d-01-01'); var end = y.add(1).format('%d-12-31'); var yearCol = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2') .filterBounds(region) .filterDate(start, end) .map(maskL8) .map(renameBands); var median = yearCol.median(); var rgb = median.visualize({bands: ['red', 'green', 'blue'], min: [0, 0, 0], max: [3000, 3000, 3000]}); return rgb.set('year', y); }); var animation = ee.ImageCollection(ee.List(frames));上面的代码是一个简化版本,实际运行中你还会遇到几个问题需要自己调试。下面我逐段解释为什么这样写,并会给出更完整的替代方案。
4.2 逐步验证每一帧是否有效
实际使用中,最容易出现的问题就是"花了半天写了代码,最后结果是一段全黑视频"。所以我的建议是:不要直接提交导出任务,先预览几帧图片看看效果。
GEE Code Editor 中可以用Map.addLayer把某一帧添加到地图上查看,也可以用print打印出动画集合的信息。更直接的方式是单击代码编辑器上方的"Run"按钮后,在 Console 窗口中点击某个Thumbnail或Image的链接查看单帧图像。
我在调试时习惯先选择三个时间点作为代表——通常是一个早期年份、一个中间年份、一个最近年份——分别把这三帧可视化到地图上,确认区域内的地物特征清晰可见,没有大范围黑色空洞。确认没问题后再生成完整动画合集。
这里补一个容易忽略的点:在区间内如果某一年完全没有影像,yearCol.median()会产生一个包含极值或全零的影像,把它加进动画合集后,那一帧会显示为灰白色或黑色。为了避免这种醒目且不美观的画面,你可以在生成中间影像后,先计算出该影像的有效像素比例,设置一个阈值来决定是否纳入该帧。
4.3 视频导出的标准流程与参数说明
当动画集合准备好了,就可以导出短视频了。GEE 导出视频有两种典型方式:
方式一:使用Export.video.toDrive直接导出
Export.video.toDrive({ collection: animation, description: 'timelapse_demo', folder: 'GEE_Export', fileNamePrefix: 'urban_growth_1984_2023', framesPerSecond: 5, dimensions: 720, region: region, maxFrames: 40 });这里的参数选择有讲究:
framesPerSecond:建议设置在 5 到 10 之间。如果是逐年变化的大区域,5 帧/秒的节奏比较耐看;如果帧数少而信息密度高,可以用 2 到 3 帧/秒,播放更从容。太高了会闪得人眼晕,太低了则变化迟钝。dimensions:控制输出视频的宽或高像素数。GEE 限制在相对较低的分辨率下导出,设置 720 或 1080 通常就足够了。实际测试中分辨率越高,渲染耗时越长,而且长序列视频文件会非常大。maxFrames:最大帧数必须大于或等于动画集合中的影像数。如果省略,GEE 默认按 100 帧处理,超过会报错或截断。
方式二:用ui.Thumbnail生成 GIF 预览
print(ui.Thumbnail({ image: animation, params: {min: 0, max: 3000, framesPerSecond: 5, dimensions: 720}, style: {position: 'bottom-right'} }));这种方式适合快速预览效果,生成的是一个 GIF 动图,在 Console 里直接可以播放。但它的尺寸和时长都有限制,不适合最终的严肃导出。
4.4 导出后如何从云端下载视频
导出任务提交后,在 GEE 右侧的 Tasks 面板中会生成一条任务记录,点击"Run"按钮开始执行。这里的几个状态你需要熟悉:Submitted(已提交)、Running(渲染中)、Completed(完成)、Failed(失败)。
任务完成后,GEE 会把视频文件放到你指定的 Google Drive 目录中(默认是GEE_Export文件夹,需在 GEE 设置中关联 Google Drive 账号)。如果你的网络环境访问 Google Drive 比较便捷,直接在网页版或客户端里下载即可。
如果任务的尺寸过大或网络不稳定,可能会出现下载中断的情况。我个人的处理习惯是:导出时把dimensions控制在 720 或以下、时长控制在 20 到 30 秒内,这样的文件通常只有几十 MB,下载成功率最高。如果需要做超长序列,可以分段导出多个视频,最后用本地剪辑软件拼接,会比一次性导出更稳定。
5. 常见问题与排查技巧实录
5.1 影像数量稀少或全黑问题
这是做长序列延时动画时最常遇到的问题。排查路径如下:
- 检查数据集的起止时间。Landsat-5 在 2011 年后基本停止获取,Landsat-7 在 2003 年后有条带问题,如果你把 Landsat-5 的时间范围设定到了 2012 年以后,就会遇到大量影像缺失。
- 检查云掩膜是否过于激进。如果掩膜条件过强,许多影像的有效像素会被全部抹掉,导致合成结果全黑。可以临时把云掩膜函数去掉,对比一下输出。
- 检查区域经纬度是否正确。别笑,我见过不少把经纬度填反导致区域跑到海上或者无人区的例子。在做任何操作之前,建议先在 Map 面板上确认区域位置,比直接打印数字来得直观。
这里给一个实用性很强的技巧:用Chart功能统计每年影像数量。在 GEE 里能画时间序列的柱状图,能快速看出数据覆盖的好坏。代码大致是:
var chart = ui.Chart.image.seriesByYear( ee.ImageCollection('LANDSAT/LC08/C02/T1_L2').filterBounds(region).filterDate('2013-01-01', '2023-12-31'), ee.Reducer.count(), region, ee.Resolution(30), 'system:time_start' ); print(chart);一下子就能看清楚哪些年份的数据支撑不足,再回头调整代码就有的放矢了。
5.2 合成影像颜色怪异或闪烁
颜色怪异的最常见原因是波段组合不对。Landsat-8/9 的 SR 数据红、绿、蓝波段分别是 SR_B4、SR_B3、SR_B2,Landsat-5/7 分别是 SR_B3、SR_B2、SR_B1。如果你用同一套波段名去索引不同数据集,必然出错。多数情况下代码编辑器会报错,但如果你使用.select()选错顺序,它不报错,但颜色完全不对。
闪烁问题通常来源于每一帧的 min/max 值不同。比如年份 A 的地表覆盖以植被为主,直方图偏亮;年份 B 有大面积裸土,直方图偏暗。单独拉伸时年份 A 看起来正常、年份 B 看起来正常,但放在一起播放,亮度突变特别刺眼。解决方式我在前面说过——统一拉伸范围。实际操作时,你可以通过reduceRegion对研究对象区域统计最近几年影像的 2% 和 98% 分位数,然后写死作为所有帧的拉伸上下限。
5.3 导出任务失败的排查步骤
GEE 导出任务失败后,在 Tasks 面板下方会显示失败原因。最常见的几类信息:
ImageCollection has no valid images after filters:说明动画集合里没有满足条件的影像。检查时间范围、云掩膜条件,确认至少有一帧影像存在。Collection images must have a consistent projection:这是新手最常撞上的墙。不同来源的影像可能自带不同的投影或分辨率,合并前需要用reduceResolution或统一proj处理。比如两个不同来源的影像叠加时会报此类错误。User memory limit exceeded:这是计算资源不足的报错。GEE 免费用户有一定配额限制,处理超大范围或超长时间的动画时容易触发。解决方案是:缩小区域、调低分辨率、减少帧数、分段导出。
这里要额外说一个我踩过多次的坑:GEE 的配额限制不是简单的"内存不够"这么简单,它与你请求的处理规模密切相关。同样的代码,区域放大 10 倍可能导致任务失败,而把dimensions从 1080 降到 720 之后可能就顺利通过了。所以当任务失败时,不要盲目修改算法,优先考虑降低请求规模。
5.4 动画效果不理想时的优化思路
如果做出来的视频总觉得"不够惊艳",问题往往出在内容层面而不是技术层面。这时我会从以下角度思考:
- 是否需要做季节内合成?如果按"每年取 1 帧"来做,某一年恰好在植被最差或水最少的季节拍摄,会出现远景年之间颜色和内容的跳变。可以改用"按年内某个月份区间取中值"的方式,比如只取每年 6 至 9 月的影像做合成,保证各帧季节一致性。
- 是否需要叠加地表解释信息?做植被变化动画时,可以先把 NDVI 值映射到彩色色带上再合成动画,比单纯 RGB 更直观。同样的逻辑适用于水体范围、夜间灯光强度等。
- 是否需要添加时间标注?GEE 本身不支持直接在视频帧上加文本标注,但你可以先在每一帧中嵌入一个带时间戳的颜色条或小标签(通过
ee.Image的绘制),或者导出后再用本地视频编辑软件添加透明叠加层。
6. 进阶扩展:从演示级动画到研究级产品
如果做完一段基础 Time-lapse 后,你觉得还不够过瘾,这里分享几个值得继续深入的方向。
多源数据融合动画:不仅仅是 Landsat,GEE 里还有 Sentinel-1(雷达)、Sentinel-2(光学)、MODIS(中分辨率)、DMSP/VIIRS(夜间灯光)等数据集。把不同来源的数据做成动画,可以呈现地表变化的多维视角。比如做一个城市扩张动画时,叠加夜间灯光亮度的变化,说服力更强。
变化量化的时间序列分析:动画本身只是直观展示,如果你需要更精确地说明"某区域在过去 30 年里建设用地增加了百分之多少",需要配合reduceRegion和趋势分析来计算。GEE 中可以使用linearFit或trend方法来做像元时间序列的线性回归,然后把变化速率和显著性用颜色渲染出来。
自动生成视频用于报告:我的经验是,研究生做开题、结题或者项目验收时经常需要展示区域变化的动态过程,之前大家用 PPT 放几张静态图,观感远不如一段几十秒的动画冲击力强。GEE 导出视频后可以直接嵌入 PPT 或汇报视频中,对沟通汇报有非常大的帮助。这也是我当初花时间研究这个功能的最主要动机。
7. 实操中的几点私房心得
一路做下来,谈几个文档里不太会写的体会。
第一,做 GEE 长序列动画,建议先把研究区域范围控制在肉眼能看清变化的尺度。范围动辄几千平方公里,上一张影像分辨率是 30 米,做出来的动画每一帧只有一个大概轮廓,地物细节完全看不见。适当缩小到某个城市或某个流域,变化特征会突出很多。这也是大家常说的"做遥感,先学会做减法"。
第二,不要迷信最全、最新的数据源。以 Landsat Collection 2 为例,它确实比 Collection 1 有更好的几何和辐射精度,但对老影像来说,处理链路复杂且产品版本间差异不小。如果只是做一个普适性的展示动画,直接用 TOA 产品也完全够用,尤其在缺数据的年份或区域,TOA 反而有更丰富的影像可参考。
第三,善用 GEE 社区里的代码片段,但必须理解每一行的目的。官方示例和一些博主分享的代码往往不包含完善的容错逻辑,你不能一股脑全拿来拼凑,而是要把数据源、区域、时间、合成方式这几个关键参数单独调出来逐一验证。我见过不少初学者因为复制了一段"老代码"却用于"新数据集"而浪费了大量调试时间。
第四,注意配额与计划。GEE 现在有注册和配额升级的流程,如果你只是偶尔玩玩,默认配额通常够用;但如果是做包含大量影像的长序列产品,建议提前做好任务分批和时间规划。任务渲染要花时间,排队等待很常见。我自己通常在晚上睡觉前提交导出任务,第二天早上醒来正好下载成品,算是把这平台的异步特性利用到极致了。
最后再分享一个小技巧:做视频导出前,先在maxFrames参数上留出 10% 的余量,这样即使影像集合后续微调增加了一两帧,也不会因为超出帧数限制而报错。这个小习惯帮我省去了不少重复提交任务的时间。