The Aral Sea drying up from orbit, 1986 → 2023¶
Once the fourth-largest lake on Earth, the Aral Sea (between Kazakhstan and Uzbekistan) has all but
vanished — drained after Soviet-era irrigation projects diverted the two rivers that fed it, the Amu
Darya and Syr Darya. What is left today is mostly the Aralkum, a new salt desert. This notebook
builds a true-colour time-lapse — one satellite frame per year — from the earthlens gee backend,
and embeds it as an animation you can just watch.
The collapse spans four decades, so we reach across the Landsat archive: Landsat 5 carries us from 1986 through 1998, Landsat 7 covers 1999–2012, and Landsat 8 takes over from 2013 — the one catch being that their true-colour bands have different names.
Setup¶
pyramids reads and animates the yearly rasters (DatasetCollection), and IPython.display.HTML
embeds the GIF. Earth Engine needs a service account, read from the GEE_SERVICE_ACCOUNT /
GEE_SERVICE_KEY environment variables.
import os
from pathlib import Path
import matplotlib.pyplot as plt
from cleopatra.glyphs.gridded.array_glyph import FrameLabel
from IPython.display import Image, display
from pyramids.dataset.collection import DatasetCollection
from earthlens.core import EarthLens
SERVICE_ACCOUNT = os.environ["GEE_SERVICE_ACCOUNT"]
SERVICE_KEY = os.environ["GEE_SERVICE_KEY"]
OUT = Path("out") / "aral"
OUT.mkdir(parents=True, exist_ok=True)
ARAL = dict(lat_lim=[43.4, 46.8], lon_lim=[58.2, 61.6]) # the whole Aral basin
One true-colour frame per year¶
For each year we pull a summer median composite (June–September) over the whole basin — median throws
out the odd cloud, and summer is when the shrinking water and the growing salt flats show most clearly.
The only branch is the sensor era: Landsat 5 (≤1998) and Landsat 7 (1999–2012) share the same
true-colour bands SR_B3 / SR_B2 / SR_B1, while Landsat 8 (≥2013) uses SR_B4 / SR_B3 /
SR_B2.
frames = {}
for year in range(1986, 2024):
if year <= 1998:
asset, bands = "LANDSAT/LT05/C02/T1_L2", ["SR_B3", "SR_B2", "SR_B1"]
elif year <= 2012:
asset, bands = "LANDSAT/LE07/C02/T1_L2", ["SR_B3", "SR_B2", "SR_B1"]
else:
asset, bands = "LANDSAT/LC08/C02/T1_L2", ["SR_B4", "SR_B3", "SR_B2"]
# The per-year folder is the cache: reuse an already-downloaded year so a re-run
# only fetches the missing years, not the whole 38-year archive.
cached = sorted((OUT / str(year)).glob("*.tif"))
if cached:
frames[year] = cached[0]
continue
job = EarthLens(
data_source="gee",
dataset=asset,
variables=bands,
start=f"{year}-06-01",
end=f"{year}-09-30",
temporal_resolution="raw",
reducer="median",
scale=400.0,
path=OUT / str(year),
export_via="url",
**ARAL,
)
job.authenticate(service_account=SERVICE_ACCOUNT, service_key=SERVICE_KEY)
paths = job.download(progress_bar=False)
if paths:
frames[year] = paths[0]
len(frames)
Build the time-lapse¶
pyramids animates a stack of rasters natively — no hand-rolled frame loop. We load the yearly frames
into a DatasetCollection, and collection.plot(rgb_options={"rgb": [0, 1, 2], "surface_reflectance": …}) composites
the red/green/blue Landsat bands per year into a true-colour time-lapse. glyph.animate(...) stamps
each frame with its year — which we nudge up into the top margin so it reads like a title instead of
sitting on the water — and glyph.save_animation(...) writes a larger, social-post-sized animated GIF
(which plays in any viewer — Jupyter, VS Code, GitHub, nbviewer — with no JavaScript).
timeline = sorted(frames)
cube = DatasetCollection.from_files([frames[year] for year in timeline])
# full_bleed=True fills the whole square figure edge-to-edge -- no title bar and no
# white margins top/bottom (the Aral basin AOI is ~square, so nothing is letterboxed).
glyph = cube.plot(
rgb_options={"rgb": [0, 1, 2], "surface_reflectance": 26000},
figsize=(8, 8),
full_bleed=True,
)
# Place the year label INSIDE the frame (top-left): FrameLabel(location=None) anchors
# it just inside that corner; white so it reads over both the dark water and pale bed.
glyph.animate(
timeline,
interval=300,
frame_label=FrameLabel(location=[20, 72], color="#3a2a17", size=30),
full_bleed=True,
)
gif_path = OUT / "aral_timelapse.gif"
mp4_path = OUT / "aral_timelapse.mp4"
glyph.fig.set_dpi(150) # GIF renders at figsize*dpi -> 1200 px
# PyCharm animates GIFs but not HTML5 <video>: show the GIF inline, and also write a
# 1200x1200 MP4 to out/ for a LinkedIn post.
glyph.save_animation(str(gif_path), fps=4)
glyph.save_animation(
str(mp4_path), fps=4, dpi=150, extra_args=["-vf", "scale=1200:1200"]
)
plt.close("all")
Embed the GIF as a base64 data URI so the animation travels inside the notebook.
display(Image(filename=str(gif_path)))
What you are watching¶
- 1986–1990 — a single dark body of water still fills most of the basin, though the shoreline is already pulling back from its 1960 extent.
- ~2000 — the sea splits: the small North Aral (fed by the Syr Darya) separates from the large South Aral, and the water turns visibly paler and shallower.
- 2000s — the eastern South Aral empties fastest, its bed turning into the pale, salt-crusted Aralkum desert; by the mid-2010s it disappears almost entirely in dry years.
- 2020s — only the North Aral (partly rescued by the Kok-Aral dam, 2005) and a thin western sliver of the South Aral survive; the rest is sand and salt.
- The slight shift in tone around 1998→1999 and 2012→2013 is the handover between Landsat 5, 7 and 8 — different instruments, very slightly different colour, not a change on the ground.
Make it your own: move ARAL to any coordinates (try Lake Urmia in Iran, the Salton Sea, or Lake
Poopó in Bolivia), widen the year range, or drop scale for a sharper, heavier movie.