Showcase — the Suez Canal blockage (Ever Given, March 2021)¶
On 23 March 2021 the 400 m container ship Ever Given ran aground and wedged across the Suez Canal, halting one of the world's busiest shipping lanes for six days until it was refloated on 29 March. This notebook pulls a Sentinel-2 time series around the blockage and renders it entirely through pyramids' own plotting — reading the scenes off disk with DatasetCollection.read_multiple_files() and drawing each one in true colour with Dataset.plot(rgb_options=...).
What this notebook does¶
- Fetch a Sentinel-2 true-colour scene (
B4,B3,B2) for six dates spanning the blockage, into one folder. - Read the folder as a collection with
DatasetCollection.read_multiple_files(). - Compare before / during / after with
Dataset.plot(rgb_options=...). - Show the full true-colour progression across all six dates.
- Animate the scenes as a real-colour GIF.
Needs
GEE_SERVICE_ACCOUNT/GEE_SERVICE_KEYandpyramids-gis[viz]; the download cells skip gracefully without credentials.
Setup¶
The imports and a bit of noise-suppression. pyramids provides Dataset / DatasetCollection (reading + plotting); earthlens provides the unified EarthLens entry point. cleopatra's ArrayGlyph is used later to prepare RGB animation frames, and tempfile / shutil / os handle the scratch folders the scenes are downloaded into.
import os
import shutil
import tempfile
import warnings
import matplotlib.pyplot as plt
import numpy as np
from dotenv import find_dotenv, load_dotenv
from IPython.display import Image, display
from loguru import logger
from pyramids.dataset import Dataset, DatasetCollection # pyramids' own IO + plotting
from earthlens import EarthLens
warnings.filterwarnings("ignore")
logger.remove()
plt.rcParams["figure.dpi"] = 80 # keep embedded outputs lean
Credentials¶
This notebook reads its credentials from a .env file at the repository root (loaded with python-dotenv; your PATH is left untouched). Create that .env with at least:
GEE_SERVICE_ACCOUNT="your-sa@your-project.iam.gserviceaccount.com"
GEE_SERVICE_KEY="/path/to/your/service-account-key.json"
load_dotenv(find_dotenv(usecwd=True))
SERVICE_ACCOUNT = os.environ[
"GEE_SERVICE_ACCOUNT"
] # Earth Engine service-account email (.env)
SERVICE_KEY = os.environ[
"GEE_SERVICE_KEY"
] # path to the service-account JSON key (.env)
1 · Fetch the Sentinel-2 scenes¶
Six dates straddle the blockage — two before, the grounding and refloat, and two after. We download a true-colour scene (B4,B3,B2) at 10 m over the canal AOI for each date and collect them into one folder so they can be read back as a single time series.
AOI = [32.570, 30.005, 32.592, 30.030]
COLL = "COPERNICUS/S2_SR_HARMONIZED"
DATES = [
"2021-03-04",
"2021-03-19",
"2021-03-24",
"2021-03-29",
"2021-04-03",
"2021-04-13",
]
scene_dir = tempfile.mkdtemp()
For each date we build the GEE request, authenticate it, then download() the scene — kept on separate lines (construct → authenticate → download, with the [0] index split onto its own line) so each step reads clearly. The downloaded GeoTIFF is copied into scene_dir under a date-stamped name.
dates = []
for d in DATES:
scene = EarthLens(
data_source="gee",
cadence="raw",
path=tempfile.mkdtemp(),
dataset=COLL,
variables=["B4", "B3", "B2"],
aoi=AOI,
start=d,
end=d,
scale=10,
reducer="median",
)
scene.authenticate(service_account=SERVICE_ACCOUNT, service_key=SERVICE_KEY)
outputs = scene.download(progress_bar=False)
p = str(outputs[0])
if p:
shutil.copy(p, os.path.join(scene_dir, f"suez_{d.replace('-', '.')}.tif"))
dates.append(d)
print(f"{len(dates)} scenes:", dates)
Read the folder as a collection¶
DatasetCollection.read_multiple_files() reads every GeoTIFF in scene_dir into one indexable collection (date=False keeps the on-disk order). This is the object every plotting cell below draws from.
dc = (
DatasetCollection.read_multiple_files(scene_dir, date=False) if dates else None
) # read the folder
print(f"{len(dates)} scenes loaded")
2 · Before / during / after — Dataset.plot()¶
Each scene is rendered with pyramids' Dataset.plot(rgb_options=...), which percentile-stretches the R/G/B bands and draws geographic axes. The 19 Mar canal flows normally; on 24 Mar the Ever Given is wedged across it; by 3 Apr traffic is moving again.
# True-colour stills straight from the collection (iloc -> Dataset.plot).
pick = {
"19 Mar (before)": "2021-03-19",
"24 Mar (grounded)": "2021-03-24",
"3 Apr (after)": "2021-04-03",
}
sel = [(t, dates.index(d)) for t, d in pick.items() if d in dates]
if sel:
fig, axes = plt.subplots(1, len(sel), figsize=(4 * len(sel), 4.8))
axes = np.atleast_1d(axes)
for ax, (t, i) in zip(axes, sel):
dc.iloc(i).plot(
rgb_options={"rgb": [0, 1, 2], "percentile": 2}, ax=ax, title=f"Suez - {t}"
)
ax.set_xticks([])
ax.set_yticks([])
plt.tight_layout()
plt.show()
else:
print("no scenes - set GEE_SERVICE_ACCOUNT / GEE_SERVICE_KEY and rerun")
3 · Full true-colour progression¶
All six dates rendered as real-colour composites with Dataset.plot(rgb_options=...): the canal flows on 4–19 Mar, the Ever Given is wedged across it on 24 Mar, and traffic is moving again by 3–13 Apr.
# Real-colour progression across all six dates - one Dataset.plot per scene.
if dc:
n = dc.time_length
fig, ax = plt.subplots(1, n, figsize=(2.4 * n, 2.7))
for i, d in enumerate(dates):
dc.iloc(i).plot(
rgb_options={"rgb": [0, 1, 2], "percentile": 2}, ax=ax[i], title=d
)
ax[i].set_xticks([])
ax[i].set_yticks([])
fig.suptitle("Suez Canal - Sentinel-2 true colour", y=1.03)
plt.tight_layout()
plt.show()
else:
print("no scenes to show")
4 · Animated true colour — DatasetCollection.plot(rgb_options=...).save_animation()¶
The six scenes as a real-colour animation — the Ever Given appears wedged across the canal, then clears. DatasetCollection.plot(rgb_options=...) composites the RGB bands per frame and animates them across the dates.
if dc:
gif = os.path.join(tempfile.mkdtemp(), "true_colour.gif")
dc.plot(
rgb_options={"rgb": [0, 1, 2], "percentile": 2},
figsize=(5, 5),
title="Suez Canal - Sentinel-2 true colour",
).save_animation(gif, fps=1.2)
plt.close("all") # drop the static builder frame; keep only the GIF
display(Image(filename=gif))
else:
print("no scenes to animate")
Recap¶
The series is read from one folder with DatasetCollection.read_multiple_files(), every scene is drawn in true colour with Dataset.plot(rgb_options=...), and the same scenes animate as a real-colour GIF (frames prepared by cleopatra's prepare_array).
Try it yourself¶
- Change
DATES/AOI, or add the NIR band for a false-colour view. - See the GEE backend reference.