Animating RGB / true-colour stacks¶
ArrayGlyph.animate can animate two kinds of stack:
- a 3-D
(time, rows, cols)single-band stack — rendered as a colormapped field with a colorbar; and - a 4-D
(time, rows, cols, 3|4)RGB / RGBA stack — each frame drawn as true colour throughimshow, with no colormap or colorbar.
This notebook demonstrates the true-colour path (added in issue #168), so you no longer have to hand-roll matplotlib.animation.FuncAnimation for an RGB time-lapse (e.g. a Sentinel-2 true-colour GIF).
import numpy as np
from IPython.display import HTML
from cleopatra.array_glyph import ArrayGlyph
Build a synthetic RGB stack¶
We make a small (time, rows, cols, 3) stack: a coloured Gaussian blob that drifts across the frame and shifts hue over time. The frames must be display-ready RGB — floats in [0, 1] or uint8 in [0, 255] (the same form prepare_array produces). Out-of-range values are clipped by matplotlib, so scale your data first if needed.
n_frames, h, w = 12, 48, 48
yy, xx = np.mgrid[0:h, 0:w]
rgb_stack = np.zeros((n_frames, h, w, 3), dtype=float)
for t in range(n_frames):
# blob centre drifts left-to-right, top-to-bottom
cx = w * (t + 1) / (n_frames + 1)
cy = h * (t + 1) / (n_frames + 1)
blob = np.exp(-((xx - cx) ** 2 + (yy - cy) ** 2) / (2 * (h / 6) ** 2))
# rotate the blob's colour through the channels over time
phase = 2 * np.pi * t / n_frames
rgb_stack[t, ..., 0] = blob * (0.5 + 0.5 * np.cos(phase))
rgb_stack[t, ..., 1] = blob * (0.5 + 0.5 * np.cos(phase + 2 * np.pi / 3))
rgb_stack[t, ..., 2] = blob * (0.5 + 0.5 * np.cos(phase + 4 * np.pi / 3))
rgb_stack.shape
(12, 48, 48, 3)
Animate it as true colour¶
Pass the 4-D stack straight to ArrayGlyph. Because the frames are true colour, animate skips the norm / colormap / colorbar machinery — glyph.cbar is left None.
labels = [f"t{t}" for t in range(n_frames)]
glyph = ArrayGlyph(rgb_stack, figsize=(4, 4), title="RGB time-lapse")
anim = glyph.animate(labels, interval=150)
print("colorbar drawn?", glyph.cbar) # None for true colour
HTML(anim.to_jshtml())
colorbar drawn? None
RGBA stacks and saving to a file¶
A trailing axis of length 4 (RGBA, with an alpha channel) is handled the same way. The animation can be written to a GIF with save_animation exactly as for single-band animations.
import tempfile
from pathlib import Path
# add a fully-opaque alpha channel -> (time, rows, cols, 4)
alpha = np.ones((n_frames, h, w, 1))
rgba_stack = np.concatenate([rgb_stack, alpha], axis=-1)
glyph_rgba = ArrayGlyph(rgba_stack, figsize=(4, 4), title="RGBA time-lapse")
glyph_rgba.animate(labels, interval=150)
out = Path(tempfile.gettempdir()) / "cleopatra_rgba.gif"
glyph_rgba.save_animation(str(out), fps=6)
print("saved:", out.exists(), "| bytes:", out.stat().st_size)
saved: True | bytes: 250820
Lazy frames via data_getter¶
For large or remote sources (e.g. a NetCDF time slab) you can stream frames lazily. The callback may return an RGB (rows, cols, 3) array; only the spatial dims need to match the glyph's shape template (its last two axes).
template = np.zeros((h, w)) # 2-D shape template only
lazy_glyph = ArrayGlyph(template, figsize=(4, 4), title="Lazy RGB")
def get_frame(i):
"""Return RGB frame i on demand (here, straight from rgb_stack)."""
return rgb_stack[i]
lazy_anim = lazy_glyph.animate(labels, data_getter=get_frame, interval=150)
print("colorbar drawn?", lazy_glyph.cbar)
HTML(lazy_anim.to_jshtml())
colorbar drawn? None
Single-band animations are unchanged¶
For contrast: a 3-D (time, rows, cols) stack still animates as a colormapped field with a colorbar — the true-colour path does not affect existing behaviour.
single_band = np.linalg.norm(rgb_stack, axis=-1) # (time, rows, cols)
sb_glyph = ArrayGlyph(single_band, figsize=(4, 4), title="Single band")
sb_anim = sb_glyph.animate(labels, interval=150)
print("colorbar drawn?", sb_glyph.cbar is not None)
HTML(sb_anim.to_jshtml())
colorbar drawn? True