Skip to content

Instantly share code, notes, and snippets.

@jdbcode
Last active February 17, 2024 08:55
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jdbcode/b852155bc5074bf0b76d033a19f69bd7 to your computer and use it in GitHub Desktop.
Save jdbcode/b852155bc5074bf0b76d033a19f69bd7 to your computer and use it in GitHub Desktop.
Earth Engine LandTrendr FTV image time series deck visualization
Display the source blob
Display the rendered blob
Raw
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"name": "ee_landtrendr_ftv_deck_viz.ipynb",
"provenance": [],
"collapsed_sections": [],
"include_colab_link": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
}
},
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#@title Copyright 2021 The Earth Engine Community Authors { display-mode: \"form\" }\n",
"#\n",
"# Licensed under the Apache License, Version 2.0 (the \"License\");\n",
"# you may not use this file except in compliance with the License.\n",
"# You may obtain a copy of the License at\n",
"#\n",
"# https://www.apache.org/licenses/LICENSE-2.0\n",
"#\n",
"# Unless required by applicable law or agreed to in writing, software\n",
"# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
"# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
"# See the License for the specific language governing permissions and\n",
"# limitations under the License."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/gist/jdbcode/b852155bc5074bf0b76d033a19f69bd7/ee_landtrendr_ftv_deck_viz.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "eV-Fl0ct9_M5"
},
"source": [
"Setup Earth Engine"
]
},
{
"cell_type": "code",
"metadata": {
"id": "OpQ2NEFT6aAp"
},
"source": [
"import ee\n",
"ee.Authenticate()\n",
"ee.Initialize()"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "NuVJPhN0-Bx5"
},
"source": [
"Import other packages"
]
},
{
"cell_type": "code",
"metadata": {
"id": "Cgo_PDWhDAhl"
},
"source": [
"import requests \n",
"import os\n",
"import glob\n",
"import numpy as np\n",
"from PIL import Image, ImageDraw, ImageFont\n",
"import urllib.request\n",
"from IPython.display import Image as ImageGIF\n",
"import time"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "Kyxwp-636rs5"
},
"source": [
"Provide inputs"
]
},
{
"cell_type": "code",
"metadata": {
"id": "GbS74KSq6qQI"
},
"source": [
"# Set area of interest (should be relatively small and square, use Code Editor to draw and get coordinates).\n",
"aoi = ee.Geometry.Polygon(\n",
" [[[-122.95498955149333, 43.86519813585072],\n",
" [-122.95498955149333, 43.81988364136319],\n",
" [-122.88941490549723, 43.81988364136319],\n",
" [-122.88941490549723, 43.86519813585072]]], None, False);\n",
"\n",
"# Set LandTrendr asset output info.\n",
"eeUserName = 'YOUR_EE_USER_NAME' # your Earth Engine user name\n",
"eeLtAssetName = 'lt_thumbnail_series_test' # asset name\n",
"\n",
"# Set LandTrendr composites parameters.\n",
"startYear = 1984\n",
"endYear = 2020\n",
"startMonth = 6\n",
"startDay = 15\n",
"nDays = 90\n",
"\n",
"# Set LandTrendr parameters.\n",
"maxSegments = 10\n",
"spikeThreshold = 0.7\n",
"vertexCountOvershoot = 3\n",
"preventOneYearRecovery = True\n",
"recoveryThreshold = 0.5\n",
"pvalThreshold = 0.05\n",
"bestModelProportion = 0.75\n",
"minObservationsNeeded = 6"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "MfeC_VqqAIIf"
},
"source": [
"Build Landsat collection and run LandTrendr. No inputs here, just run the cell."
]
},
{
"cell_type": "code",
"metadata": {
"id": "9MlJsKqN6YnA"
},
"source": [
"# Define a collection filter by date, bounds, and quality.\n",
"def colFilter(col, aoi):#, startDate, endDate):\n",
" return(col.filterBounds(aoi))\n",
"\n",
"# Landsat collection preprocessingEnabled\n",
"# Get Landsat surface reflectance collections for OLI, ETM+ and TM sensors.\n",
"LC08col = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR')\n",
"LE07col = ee.ImageCollection('LANDSAT/LE07/C01/T1_SR')\n",
"LT05col = ee.ImageCollection('LANDSAT/LT05/C01/T1_SR')\n",
"LT04col = ee.ImageCollection('LANDSAT/LT04/C01/T1_SR')\n",
"\n",
"# Define a collection filter by date, bounds, and quality.\n",
"def colFilter(col, aoi, startDate, endDate):\n",
" return(col\n",
" .filterBounds(aoi)\n",
" .filterDate(startDate, endDate))\n",
" #.filter('CLOUD_COVER < 5')\n",
" #.filter('GEOMETRIC_RMSE_MODEL < 15')\n",
" #.filter('IMAGE_QUALITY == 9 || IMAGE_QUALITY_OLI == 9'))\n",
"\n",
"# Function to get and rename bands of interest from OLI.\n",
"def renameOli(img):\n",
" return(img.select(\n",
" ['B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'pixel_qa'],\n",
" ['Blue', 'Green', 'Red', 'NIR', 'SWIR1', 'SWIR2', 'pixel_qa']))\n",
"\n",
"# Function to get and rename bands of interest from ETM+.\n",
"def renameEtm(img):\n",
" return(img.select(\n",
" ['B1', 'B2', 'B3', 'B4', 'B5', 'B7', 'pixel_qa'],\n",
" ['Blue', 'Green', 'Red', 'NIR', 'SWIR1', 'SWIR2', 'pixel_qa']))\n",
"\n",
"# Add NBR for LandTrendr segmentation.\n",
"def calcNbr(img):\n",
" return(img.addBands(img.normalizedDifference(['NIR', 'SWIR2'])\n",
" .multiply(-10000).rename('NBR')).int16())\n",
"\n",
"# Define function to mask out clouds and cloud shadows in images.\n",
"# Use CFmask band included in USGS Landsat SR image product.\n",
"def fmask(img):\n",
" cloudShadowBitMask = 1 << 3\n",
" cloudsBitMask = 1 << 5\n",
" qa = img.select('pixel_qa')\n",
" mask = qa.bitwiseAnd(cloudShadowBitMask).eq(0) \\\n",
" .And(qa.bitwiseAnd(cloudsBitMask).eq(0))\n",
" return(img.updateMask(mask))\n",
"\n",
"# Define function to prepare OLI images.\n",
"def prepOli(img):\n",
" orig = img\n",
" img = renameOli(img)\n",
" img = fmask(img)\n",
" return (ee.Image(img.copyProperties(orig, orig.propertyNames()))\n",
" .resample('bicubic'))\n",
"\n",
"# Define function to prepare ETM+ images.\n",
"def prepEtm(img):\n",
" orig = img\n",
" img = renameEtm(img)\n",
" img = fmask(img)\n",
" return(ee.Image(img.copyProperties(orig, orig.propertyNames()))\n",
" .resample('bicubic'))\n",
"\n",
"# Get annual median collection. \n",
"def getAnnualComp(y):\n",
" startDate = ee.Date.fromYMD(\n",
" ee.Number(y), ee.Number(startMonth), ee.Number(startDay))\n",
" endDate = startDate.advance(ee.Number(nDays), 'day')\n",
" \n",
" # Filter collections and prepare them for merging.\n",
" LC08coly = colFilter(LC08col, aoi, startDate, endDate).map(prepOli)\n",
" LE07coly = colFilter(LE07col, aoi, startDate, endDate).map(prepEtm)\n",
" LT05coly = colFilter(LT05col, aoi, startDate, endDate).map(prepEtm)\n",
" LT04coly = colFilter(LT04col, aoi, startDate, endDate).map(prepEtm)\n",
" \n",
" # Merge the collections.\n",
" col = LC08coly.merge(LE07coly).merge(LT05coly).merge(LT04coly)\n",
" \n",
" yearImg = col.median()\n",
" nBands = yearImg.bandNames().size()\n",
" yearImg = ee.Image(ee.Algorithms.If(\n",
" nBands,\n",
" yearImg,\n",
" dummyImg))\n",
" return(calcNbr(yearImg)\n",
" .set({'year': y, 'system:time_start': startDate.millis(), 'nBands': nBands}))\n",
"\n",
"\n",
"################################################################################\n",
"\n",
"# Make a dummy image for missing years.\n",
"bandNames = ee.List(['Blue', 'Green', 'Red', 'NIR', 'SWIR1', 'SWIR2', 'pixel_qa'])\n",
"fillerValues = ee.List.repeat(0, bandNames.size())\n",
"dummyImg = ee.Image.constant(fillerValues).rename(bandNames) \\\n",
" .selfMask().int16()\n",
"\n",
"################################################################################\n",
"# Get a list of years\n",
"years = ee.List.sequence(startYear, endYear)\n",
"\n",
"################################################################################\n",
"# Make list of annual image composites.\n",
"imgList = years.map(getAnnualComp)\n",
"\n",
"# Convert image composite list to collection\n",
"imgCol = ee.ImageCollection.fromImages(imgList)\n",
"\n",
"################################################################################\n",
"# Run LandTrendr.\n",
"lt = ee.Algorithms.TemporalSegmentation.LandTrendr(\n",
" timeSeries=imgCol.select(['NBR', 'SWIR1', 'NIR', 'Green']),\n",
" maxSegments=maxSegments,\n",
" spikeThreshold=spikeThreshold,\n",
" vertexCountOvershoot=vertexCountOvershoot,\n",
" preventOneYearRecovery=preventOneYearRecovery,\n",
" recoveryThreshold=recoveryThreshold,\n",
" pvalThreshold=pvalThreshold,\n",
" bestModelProportion=bestModelProportion,\n",
" minObservationsNeeded=minObservationsNeeded)\n",
"\n",
"################################################################################\n",
"# Get fitted imagery. This starts export tasks.\n",
"def getYearStr(year):\n",
" return(ee.String('yr_').cat(ee.Algorithms.String(year).slice(0,4)))\n",
"\n",
"yearsStr = years.map(getYearStr)\n",
"\n",
"r = lt.select(['SWIR1_fit']).arrayFlatten([yearsStr]).toShort()\n",
"g = lt.select(['NIR_fit']).arrayFlatten([yearsStr]).toShort()\n",
"b = lt.select(['Green_fit']).arrayFlatten([yearsStr]).toShort()"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "qMqWXudJA9HR"
},
"source": [
"Export the LandTrendr results for RGB bands (SWIR1, NIR, Green)."
]
},
{
"cell_type": "code",
"metadata": {
"id": "4dsy_RAyDQIs"
},
"source": [
"print('Exporting:')\n",
"tasks = []\n",
"for i, c in zip([r, g, b], ['r', 'g', 'b']):\n",
" descr = eeLtAssetName + '_' + c\n",
" name = 'users/'+ eeUserName + '/' + descr\n",
" print(name)\n",
" ee.data.deleteAsset(name) # delete if it already exists\n",
" task = ee.batch.Export.image.toAsset(\n",
" image=i,\n",
" region=aoi.getInfo()['coordinates'],\n",
" assetId=name,\n",
" description=descr,\n",
" scale=30,\n",
" crs='EPSG:3857',\n",
" maxPixels=1e13)\n",
" task.start()\n",
" tasks.append(task)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "cbTaL0lM6sj3"
},
"source": [
"Check on task status, the cell will print \"Tasks complete, run next cell\" when ready. If the Colab session expires while running, check your tasks in the Code Editor."
]
},
{
"cell_type": "code",
"metadata": {
"id": "K0PUQYXj040O"
},
"source": [
"def check_status(tasks):\n",
" status = []\n",
" for task in tasks:\n",
" status.append(task.status()['state'] == 'COMPLETED')\n",
" return all(status)\n",
"\n",
"print('Processing, please wait.')\n",
"while check_status(tasks) == False:\n",
" time.sleep(15)\n",
"\n",
"print('\\nTasks complete, run next cell.')"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "OCM91UZ_Dj95"
},
"source": [
"Once export tasks are finished run this section to download visualization thumbnails to the Colab VM. If the Colab sesssion expired while you were waiting for the tasks to complete, you'll need to run all previous cells except for the export cell."
]
},
{
"cell_type": "code",
"metadata": {
"id": "PpGmGQl_6gMB"
},
"source": [
"# Set maximum thumbnail dimension size (pixels).\n",
"maxThumbDim = 300\n",
"\n",
"\n",
"#######################################################################\n",
"\n",
"r = ee.Image('users/'+ eeUserName + '/' + eeLtAssetName + '_r')\n",
"g = ee.Image('users/'+ eeUserName + '/' + eeLtAssetName + '_g')\n",
"b = ee.Image('users/'+ eeUserName + '/' + eeLtAssetName + '_b')\n",
"def getLtRgb(year):\n",
" return(ee.Image.cat(\n",
" r.select([year]),\n",
" g.select([year]),\n",
" b.select([year]))\n",
" .visualize(min=100, max=4500, gamma=0.8))\n",
"yearsStrPy = yearsStr.getInfo()\n",
"for yr in yearsStrPy:\n",
" print(yr)\n",
" img_url = getLtRgb(yr) \\\n",
" .getThumbUrl({\n",
" 'dimensions': maxThumbDim,\n",
" 'region': aoi.getInfo()['coordinates']})\n",
" img_data = requests.get(img_url).content\n",
" with open(yr + '.png', 'wb') as handler:\n",
" handler.write(img_data)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "9v-Xl35vHSrw"
},
"source": [
"How does a sample look?"
]
},
{
"cell_type": "code",
"metadata": {
"id": "FeIJbNQwHI8H"
},
"source": [
"images = sorted(glob.glob('*.png'))\n",
"print(images[0])\n",
"test_img = Image.open(images[0])\n",
"display(test_img)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "W4vQGcLlDhRV"
},
"source": [
"# https://stackoverflow.com/questions/11142851/adding-borders-to-an-image-using-python\n",
"def add_border(img, x, y, color):\n",
" in_size = img.size\n",
" out_size = (in_size[0]+x*2, in_size[1]+y*2)\n",
" new_img = Image.new('RGB', out_size, color)\n",
" new_img.paste(img, ((out_size[0]-in_size[0])//2,\n",
" (out_size[1]-in_size[1])//2))\n",
" return new_img\n",
"\n",
"# https://stackoverflow.com/questions/53032270/perspective-transform-with-python-pil-using-src-target-coordinates\n",
"def find_coeffs(source_coords, target_coords):\n",
" matrix = []\n",
" for s, t in zip(source_coords, target_coords):\n",
" matrix.append([t[0], t[1], 1, 0, 0, 0, -s[0]*t[0], -s[0]*t[1]])\n",
" matrix.append([0, 0, 0, t[0], t[1], 1, -s[1]*t[0], -s[1]*t[1]])\n",
" A = np.matrix(matrix, dtype=np.float)\n",
" B = np.array(source_coords).reshape(8)\n",
" res = np.dot(np.linalg.inv(A.T * A) * A.T, B)\n",
" return np.array(res).reshape(8)\n",
"\n",
"def transform_img(img, ul, ll, ur, lr):\n",
" source_coords = [ul[0], ur[0], lr[0], ll[0]]\n",
" target_coords = [ul[1], ur[1], lr[1], ll[1]]\n",
" coeffs = find_coeffs(source_coords, target_coords)\n",
"\n",
" img_rgba = img.convert('RGBA')\n",
" return img_rgba.transform((ur[1][0], height), Image.PERSPECTIVE, coeffs,\n",
" Image.BICUBIC, fillcolor=(255, 255, 255, 0))\n",
" \n",
"def text_overlay(img, text, FONT_SIZE, POSITION, font_name):\n",
" font = ImageFont.truetype(font_name, FONT_SIZE)\n",
" txt = Image.new('RGBA', font.getsize(text), (0, 0, 0, 0))\n",
" d = ImageDraw.Draw(txt)\n",
" d.text((0, 0), text, font=font, fill=(255, 255, 255))\n",
" w = txt.rotate(90, expand=1)\n",
" img.paste(w, POSITION, w)\n",
" return img"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "pfeZxLj85lqO"
},
"source": [
"# Inputs\n",
"BORDER_X = 7\n",
"BORDER_Y = 2\n",
"BORDER_COLOR = (0, 0, 0)\n",
"\n",
"\n",
"#######################################################################\n",
"\n",
"test_img_alt = add_border(test_img, BORDER_X, BORDER_Y, BORDER_COLOR)\n",
"display(test_img_alt)\n",
"\n",
"width, height = test_img_alt.size\n",
"print('width: ', width)\n",
"print('height: ', height)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "_T4YESNxMx6s"
},
"source": [
"Define the transformation. Need to play around with from -> to coordinates."
]
},
{
"cell_type": "code",
"metadata": {
"id": "gmoz0SG0EapM"
},
"source": [
"# Inputs\n",
"WIDTH_REDUCTION = 0.78\n",
"HEIGHT_REDUCTION = 0.86\n",
"\n",
"\n",
"#######################################################################\n",
"\n",
"# [(from_x, from_y), (to_x, to_y)]\n",
"ul = [(0, 0), (0, 0)]\n",
"ll = [(0, height), (0, height)]\n",
"ur = [(width, 0),\n",
" (round((width-(width*WIDTH_REDUCTION))), round(height-(height*HEIGHT_REDUCTION)))]\n",
"lr = [(width, height),\n",
" (round(width-(width*WIDTH_REDUCTION)), round((height*HEIGHT_REDUCTION)))]\n",
"print(ul)\n",
"print(ll)\n",
"print(ur)\n",
"print(lr)\n",
"test_img_alt_t = transform_img(test_img_alt, ul, ll, ur, lr)\n",
"display(test_img_alt_t)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "TWab7obrM_6u"
},
"source": [
"Want to add year overaly? Use this section to test location.\n",
"\n",
"\n"
]
},
{
"cell_type": "code",
"metadata": {
"id": "E1QeLAV1Ti-J"
},
"source": [
"!fc-list"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"metadata": {
"id": "uhON5-BfNYbL"
},
"source": [
"# Inputs\n",
"FONT_SIZE = 24\n",
"POSITION = (5, 215)\n",
"FONT_NAME = '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf'\n",
"\n",
"\n",
"#######################################################################\n",
"print(test_img_alt_t.size)\n",
"test_result = text_overlay(test_img_alt_t.copy(), '1984', FONT_SIZE, POSITION, FONT_NAME)\n",
"display(test_result)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "S0MdK8S1VqXd"
},
"source": [
"Generate the time series deck. When rendered, left click the images to select a download option."
]
},
{
"cell_type": "code",
"metadata": {
"id": "zTdVb55E8s1k"
},
"source": [
"# Inputs\n",
"GAP = 40 # gap between images\n",
"INCLUDE_YEAR = True\n",
"\n",
"\n",
"#######################################################################\n",
"\n",
"alt_t_width, alt_t_height = test_result.size\n",
"final_width = len(images) * alt_t_width + (GAP * (len(images) - 1))\n",
"final_width = GAP * (len(images) - 1) + alt_t_width\n",
"composite = Image.new('RGBA', (final_width, alt_t_height), (255, 255, 255, 255))\n",
"img_list = [];\n",
"for i, img in enumerate(images):\n",
" year = images[i][-8:].replace('.png', '')\n",
" img_alt_b = add_border(Image.open(img), BORDER_X, BORDER_Y, BORDER_COLOR)\n",
" img_alt_t = transform_img(img_alt_b, ul, ll, ur, lr)\n",
" if INCLUDE_YEAR:\n",
" img_alt_t = text_overlay(img_alt_t, year, FONT_SIZE, POSITION, FONT_NAME)\n",
" \n",
" composite.paste(img_alt_t, (GAP*i, 0))\n",
" img_list.append(composite.copy())\n",
"\n",
"display(composite)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "poFNZun9_mwb"
},
"source": [
"Make an animated GIF."
]
},
{
"cell_type": "code",
"metadata": {
"id": "UAxJzzFl_pas"
},
"source": [
"GIF_NAME = 'landtrendr_ftv_deck_animation.gif'\n",
"img_list[0].save(GIF_NAME, save_all=True, append_images=img_list[1:], duration=200, loop=0)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "0ivZbiej_yAM"
},
"source": [
"Preview the GIF."
]
},
{
"cell_type": "code",
"metadata": {
"id": "cuvKZMdEbyEm"
},
"source": [
"with open(GIF_NAME,'rb') as f:\n",
" display(ImageGIF(data=f.read(), format='png'))"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "hC6eWeAvBi34"
},
"source": [
"Download the GIF."
]
},
{
"cell_type": "code",
"metadata": {
"id": "zhlHZ-QSBgOG"
},
"source": [
"from google.colab import files\n",
"files.download(GIF_NAME)"
],
"execution_count": null,
"outputs": []
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment