Skip to content

Instantly share code, notes, and snippets.

@jdbcode
Last active June 1, 2021 23:20
Show Gist options
  • Save jdbcode/7c93a40ac38349b2c9249af8355e1af5 to your computer and use it in GitHub Desktop.
Save jdbcode/7c93a40ac38349b2c9249af8355e1af5 to your computer and use it in GitHub Desktop.
goes_storm_ana_gif.ipynb
Display the source blob
Display the rendered blob
Raw
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"name": "goes_storm_ana_gif.ipynb",
"provenance": [],
"collapsed_sections": [],
"include_colab_link": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
}
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/gist/jdbcode/7c93a40ac38349b2c9249af8355e1af5/goes_storm_ana_gif.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "CElMC5OPzNpV"
},
"source": [
"This notebook makes an animated GIF image from an Earth Engine GOES-16 image collection (tropical storm Ana, 2021). A filmstrip is created that is chopped into frames. The frames are annotated with the image date according to text position and style you define. The resulting frames are combined into an animated GIF file using the image magick utility. The final step optionally compresses the GIF."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "i8uDwPpGzLdX"
},
"source": [
"Set up Earth Engine"
]
},
{
"cell_type": "code",
"metadata": {
"id": "-s7YMddEmGI4"
},
"source": [
"import ee\n",
"ee.Authenticate()\n",
"ee.Initialize()"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "-UsqetURqLeR"
},
"source": [
"Inputs"
]
},
{
"cell_type": "code",
"metadata": {
"id": "VrLtXBYBqKjA"
},
"source": [
"COLLECTION = 'NOAA/GOES/16/MCMIPF'\n",
"\n",
"# Observation start and end time of interest.\n",
"START_TIME = '2021-05-21T00:00:00'\n",
"END_TIME = '2021-05-24T00:00:00'\n",
"TIME_ZONE = 'UTC'\n",
"\n",
"# Lambert Conformal Conic projection.\n",
"proj_wkt = \"\"\"PROJCS[\"North_America_Lambert_Conformal_Conic\",\n",
" GEOGCS[\"GCS_North_American_1983\",\n",
" DATUM[\"North_American_Datum_1983\",\n",
" SPHEROID[\"GRS_1980\",6378137,298.257222101]],\n",
" PRIMEM[\"Greenwich\",0],\n",
" UNIT[\"Degree\",0.017453292519943295]],\n",
" PROJECTION[\"Lambert_Conformal_Conic_2SP\"],\n",
" PARAMETER[\"False_Easting\",0],\n",
" PARAMETER[\"False_Northing\",0],\n",
" PARAMETER[\"Central_Meridian\",-55],\n",
" PARAMETER[\"Standard_Parallel_1\",10],\n",
" PARAMETER[\"Standard_Parallel_2\",50],\n",
" PARAMETER[\"Latitude_Of_Origin\",30],\n",
" UNIT[\"Meter\",1],\n",
" AUTHORITY[\"EPSG\",\"102009\"]]\"\"\"\n",
"proj = ee.Projection(proj_wkt)\n",
"\n",
"# Region of interest.\n",
"center = ee.Geometry.Point(-59.848, 35.778)\n",
"region = center.buffer(**{\n",
" 'distance': 400e3,\n",
" 'maxError': ee.ErrorMargin(1, 'units'),\n",
" 'proj': proj\n",
"}).bounds(ee.ErrorMargin(1, 'units'), proj)\n",
"\n",
"# Video parameters.\n",
"THUMB_PARAMS = {\n",
" 'dimensions': 512, # Max dim.\n",
" 'region': region,\n",
" 'crs': proj\n",
"}\n",
"\n",
"# Visualization params.\n",
"GOES_RGB_VIS = {\n",
" 'min': 0.03,\n",
" 'max': 0.9,\n",
" 'gamma': 1.5,\n",
"}"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "5N7CO-rAzFbV"
},
"source": [
"Define functions"
]
},
{
"cell_type": "code",
"metadata": {
"id": "nAdYisRzmK9G"
},
"source": [
"# Band names.\n",
"BLUE = 'CMI_C01'\n",
"RED = 'CMI_C02'\n",
"VEGGIE = 'CMI_C03'\n",
"GREEN = 'GREEN'\n",
"\n",
"GOES_RGB_VIS['bands'] = [RED, GREEN, BLUE]\n",
"\n",
"def apply_scale_and_offset(image):\n",
" \"\"\"Properly scales an MCMIPM image.\"\"\"\n",
" names = image.select('CMI_C..').bandNames()\n",
"\n",
" # Scale the radiance bands using the image's metadata.\n",
" scales = names.map(lambda name: image.getNumber(ee.String(name).cat('_scale')))\n",
" offsets = names.map(lambda name: image.getNumber(ee.String(name).cat('_offset')))\n",
" scaled = (image.select('CMI_C..')\n",
" .multiply(ee.Image.constant(scales))\n",
" .add(ee.Image.constant(offsets)))\n",
"\n",
" return image.addBands(**{'srcImg': scaled, 'overwrite': True})\n",
"\n",
"def add_green_band(image):\n",
" \"\"\"Computes and adds a green radiance band to a MCMIPM image.\"\"\"\n",
" def toBandExpression(bandName):\n",
" return 'b(\\'' + bandName + '\\')'\n",
"\n",
" B_BLUE = toBandExpression(BLUE)\n",
" B_RED = toBandExpression(RED)\n",
" B_VEGGIE = toBandExpression(VEGGIE)\n",
"\n",
" # Green = 0.45 * Red + 0.10 * NIR + 0.45 * Blue\n",
" GREEN_EXPR = GREEN + ' = 0.45 * ' + B_RED + ' + 0.10 * ' + B_VEGGIE + ' + 0.45 * ' + B_BLUE\n",
"\n",
" green = image.expression(GREEN_EXPR).select(GREEN)\n",
" return image.addBands(green)\n",
"\n",
"\n",
"def visualize_goes(image):\n",
" \"\"\"Creates GOES visualization layer.\"\"\"\n",
" return (image.visualize(**GOES_RGB_VIS)\n",
" .resample('bicubic')\n",
" .set({\n",
" 'system:time_start': image.get('system:time_start'),\n",
" 'date': image.date().format('YYYY-MM-dd HH:mm')\n",
" }))"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "CedZQe40pzqO"
},
"source": [
" Build the collection, generate a thumbnail filmstrip URL from the image collection"
]
},
{
"cell_type": "code",
"metadata": {
"id": "YY0adunXp132"
},
"source": [
"start = ee.Date(START_TIME, TIME_ZONE)\n",
"end = ee.Date(END_TIME, TIME_ZONE)\n",
"col = (ee.ImageCollection(COLLECTION)\n",
" .filterDate(start, end)\n",
" .filter(ee.Filter.calendarRange(9, 22, 'hour'))\n",
" .map(apply_scale_and_offset)\n",
" .map(add_green_band)\n",
" .map(visualize_goes))\n",
"\n",
"col = ee.ImageCollection.fromImages(col.toList(col.size()).slice(0, -1, 3))\n",
"dates = col.aggregate_array('date').getInfo()\n",
"filmstrip_url = col.getFilmstripThumbURL(THUMB_PARAMS)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "uhjjzRhDzAKN"
},
"source": [
"Get some packages and modules for dealing with images"
]
},
{
"cell_type": "code",
"metadata": {
"id": "5RH1bJvTuvW9"
},
"source": [
"import urllib.request\n",
"from PIL import Image, ImageDraw, ImageFont\n",
"import glob\n",
"from IPython.display import Image as ImageGIF"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "VEYN3VfCy-DW"
},
"source": [
"Fetch the image collection thumbnail filmstrip"
]
},
{
"cell_type": "code",
"metadata": {
"id": "jPRbMT2buxZj"
},
"source": [
"filmstrip_name = 'filmstrip.png'\n",
"urllib.request.urlretrieve(filmstrip_url, filmstrip_name)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "y_goUF2fy6zM"
},
"source": [
"Get some info about the filmstrip"
]
},
{
"cell_type": "code",
"metadata": {
"id": "f8ua-qvtuxx9"
},
"source": [
"filmstrip_im = Image.open(filmstrip_name)\n",
"width, height = filmstrip_im.size\n",
"crop_interval = height / len(dates)\n",
"print(width, height, crop_interval)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "aIhgjSCqy48V"
},
"source": [
"Preview the first frame"
]
},
{
"cell_type": "code",
"metadata": {
"id": "9sqkB_NVu0NV"
},
"source": [
"frame_0 = filmstrip_im.crop((0, 0, width, crop_interval))\n",
"print(frame_0)\n",
"display(frame_0)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "SBi5585ky3U1"
},
"source": [
"Get a font (Consolas)"
]
},
{
"cell_type": "code",
"metadata": {
"id": "ZoaZnUvnu2B0"
},
"source": [
"font_url = 'https://github.com/tsenart/sight/raw/master/fonts/Consolas.ttf'\n",
"font_name = 'Consolas.ttf'\n",
"urllib.request.urlretrieve(font_url, font_name)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "fTZRatqay13M"
},
"source": [
"Figure out annotation position and size"
]
},
{
"cell_type": "code",
"metadata": {
"id": "3O76hdApu4jE"
},
"source": [
"right = 10\n",
"down = 10\n",
"font_size = 26\n",
"fill = '#eb4d4b'\n",
"\n",
"font = ImageFont.truetype(font_name, font_size)\n",
"anno_test = filmstrip_im.crop((0, 0, width, crop_interval))\n",
"ImageDraw.Draw(anno_test).text((right, down), dates[0], fill=fill, font=font)\n",
"display(anno_test)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "-DeXeYQny0Ek"
},
"source": [
"Loop through the frames to crop and annotate"
]
},
{
"cell_type": "code",
"metadata": {
"id": "64re7T-zu6VE"
},
"source": [
"!rm *_frame.png\n",
"for frame, date in enumerate(dates):\n",
" frame_im = filmstrip_im.crop(\n",
" (0, frame*crop_interval, width, (frame+1)*crop_interval))\n",
" ImageDraw.Draw(frame_im).text((right, down), date, fill=fill, font=font)\n",
" frame_im.save(str(frame).zfill(3)+'_frame.png')"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "MFYMU6-byyOj"
},
"source": [
"Did it work?"
]
},
{
"cell_type": "code",
"metadata": {
"id": "9uvH0ywou6SE"
},
"source": [
"print('Found these frames:\\n')\n",
"!ls *_frame.png\n",
"\n",
"print('\\nFinal frame:\\n')\n",
"Image.open(str(frame).zfill(3)+'_frame.png')"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "4Jf73SRZyviU"
},
"source": [
"Make the GIF using ImageMagick"
]
},
{
"cell_type": "code",
"metadata": {
"id": "chVXs4G0u6Ol"
},
"source": [
"!apt install imagemagick"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "p_uWgtT8u6K9"
},
"source": [
"import os"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "7O8eYbVgwZmc"
},
"source": [
"gif_name = 'filmstrip_magick.gif'\n",
"delay = 10\n",
"\n",
"cmd = f'convert -loop 0 -delay {delay} *_frame.png {gif_name}'\n",
"print(cmd)\n",
"os.system(cmd)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "wPX0rSRdwZks"
},
"source": [
"with open('filmstrip_magick.gif','rb') as f:\n",
" display(ImageGIF(data=f.read(), format='png'))"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "9R1fA8Ilylab"
},
"source": [
"Compress the GIF"
]
},
{
"cell_type": "code",
"metadata": {
"id": "vOK2sD7MwZiU"
},
"source": [
"in_gif = 'filmstrip_magick.gif'\n",
"out_gif = 'filmstrip_magick_compress.gif'\n",
"fuzz = 5 # percent\n",
"\n",
"cmd = f'convert -fuzz {fuzz}% -layers Optimize {in_gif} {out_gif}'\n",
"print(cmd)\n",
"os.system(cmd)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "ql-hYNFkyXrz"
},
"source": [
"Preview the compressed version"
]
},
{
"cell_type": "code",
"metadata": {
"id": "Noer7KMGwZgH"
},
"source": [
"with open('filmstrip_magick.gif','rb') as f:\n",
" display(ImageGIF(data=f.read(), format='png'))"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "zQIMMQ8vwhCc"
},
"source": [
"Download the animation"
]
},
{
"cell_type": "code",
"metadata": {
"id": "7uEBBTVtu6Dt"
},
"source": [
"from google.colab import files\n",
"files.download(out_gif) "
],
"execution_count": null,
"outputs": []
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment