Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ You can learn more about the products on the [product page](https://mapsplatform

[Docs](https://developers.google.com/maps/documentation/population-dynamics-insights)

### Custom Satellite Embeddings

[Docs](https://developers.google.com/maps/documentation/custom-satellite-embeddings)

## Available Recipe Collections

* [`places_insights/`](places_insights/): Samples for analyzing the Places Insights BigQuery dataset.
Expand Down Expand Up @@ -48,4 +52,4 @@ See [`SECURITY.md`](SECURITY.md) for details on how to report security vulnerabi

## Contributing

See [`CONTRIBUTING.md`](CONTRIBUTING.md) for details on how to contribute to this project.
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for details on how to contribute to this project.
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"provenance": [],
"toc_visible": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
}
},
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "96k7JUiHDEv2"
},
"outputs": [],
"source": [
"# Copyright 2026 Google LLC\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",
"source": [
"<table align=\"left\">\n",
" <td style=\"text-align: center\">\n",
" <a href=\"https://console.cloud.google.com/vertex-ai/colab/import/https:%2F%2Fraw.githubusercontent.com%2Fgooglemaps-samples%2Finsights-samples%2Fmain%2Fcustom_satellite_embeddings%2Fnotebooks%2Fee_classification%2FCustom_Satellite_Embeddings_Classification.ipynb?utm_source=custom_satellite_embeddings_notebooks\">\n",
" <img width=\"32px\" src=\"https://lh3.googleusercontent.com/JmcxdQi-qOpctIvWKgPtrzZdJJK-J3sWE1RsfjZNwshCFgE_9fULcNpuXYTilIR2hjwN\" alt=\"Google Cloud Colab Enterprise logo\"><br> Open in Colab Enterprise\n",
" </a>\n",
" </td>\n",
"</table>"
],
"metadata": {
"id": "Kmf1K3lUDnPr"
}
},
{
"cell_type": "markdown",
"source": [
"# Custom Satellite Embeddings Supervised and Unsupervised Classification\n",
"\n",
"Custom satellite embeddings imagery is delivered as [Cloud Optimized Geotiffs](https://cogeo.org/) (COGs) in [Google Cloud Storage](https://cloud.google.com/storage). This guide assumes that [Google Earth Engine](https://earthengine.google.com) `ImageCollection`s of COG-backed images are already created. See the Custom Satellite Embeddings Data Exploration guide for more information about the embeddings and COGs in Earth Engine.\n",
"\n",
"In this guide, supervised and unsupervised classifications are fitted to a time series of six monthly satellite embeddings images in an agricultural area of the California, USA central valley. Use these methods to indentify areas of agricultural change or temporal trajectories of particular crop types. An animated visualizion is produced to illustrate change over time."
],
"metadata": {
"id": "NKzgZyG8K-Se"
}
},
{
"cell_type": "markdown",
"source": [
"## Earth Engine Authentication and Initialization\n",
"\n",
"To use Earth Engine, you must have a Cloud Project with the Earth Engine API activated."
],
"metadata": {
"id": "HGoBIB4GN9F-"
}
},
{
"cell_type": "code",
"source": [
"import ee\n",
"import geemap\n",
"\n",
"from IPython.display import Image, display\n",
"from pprint import pprint"
],
"metadata": {
"id": "LSoNkZrkWoCC"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Your project with the Earth Engine API activated.\n",
"PROJECT = \"YOUR-PROJECT\""
],
"metadata": {
"id": "JpJ-w6dDHYCL"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Initialize Earth Engine (Assumes you have already authenticated via gcloud)\n",
"try:\n",
" ee.Initialize(project=PROJECT)\n",
"except Exception:\n",
" ee.Authenticate()\n",
" ee.Initialize(project=PROJECT)"
],
"metadata": {
"id": "0XT8DsPROapQ"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"## Load the satellite embeddings `ImageCollection`\n",
"\n",
"This sample is a time series of monthly images, centered on the 15th of the month, and spanning six months between 2025-11-15 and 2026-05-15. The data covers an area of ~18,000 hectares of mostly cropland in California."
],
"metadata": {
"id": "6e1dQHpoOnoI"
}
},
{
"cell_type": "code",
"source": [
"meloland_california_2026_crop_cycles = ee.ImageCollection()"
],
"metadata": {
"id": "-6tnd_TzOmt2"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"## Spatial mosaicking\n",
"\n",
"Since Satellite embeddings are tiled, first aggregate the collection spatially, such that each image in the collection represents one time. Here `mosaic()` is used to spatially aggregate the imagery."
],
"metadata": {
"id": "CBEBYfRrS9Yu"
}
},
{
"cell_type": "code",
"source": [
"# Spatial mosaicking.\n",
"time_starts = meloland_california_2026_crop_cycles.aggregate_histogram('system:time_start')\n",
"\n",
"def mosaic_function(time):\n",
" time_start = ee.Number.parse(time)\n",
" date = ee.Date(time_start)\n",
" filtered = meloland_california_2026_crop_cycles.filterDate(date)\n",
" # Transfer time and geographic properties.\n",
" return filtered.mosaic().set('system:time_start', time_start).clip(filtered.geometry(100))\n",
"\n",
"c = ee.ImageCollection.fromImages(time_starts.keys().map(mosaic_function))"
],
"metadata": {
"id": "15F9O5vZT3R1"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"## Unsupervised Classification\n",
"\n",
"### Stack the entire time series in a single image\n",
"\n",
"Stack each of the six 64-band images into a single image. Use clusters in this space to represent temporal trajectories of different crops through time. Earth Engine has native methods for sampling and clustering. Here Weka's k-means clusterer with seven clusters is used."
],
"metadata": {
"id": "1E_5ChrkQvHQ"
}
},
{
"cell_type": "code",
"source": [
"# K-means on the 6x64D stack.\n",
"k = 7\n",
"n = 1000\n",
"\n",
"# Temporal trajectory clusters.\n",
"big_stack = c.toBands()\n",
"training = big_stack.sample(\n",
" region=c.first().geometry(),\n",
" numPixels=n,\n",
" scale=10,\n",
")\n",
"training = training.filter(ee.Filter.notNull(big_stack.bandNames()))\n",
"\n",
"clusterer = ee.Clusterer.wekaKMeans(k).train(training, big_stack.bandNames())\n",
"clustered = big_stack.cluster(clusterer)"
],
"metadata": {
"id": "Cj5VTspSXArV"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Use `geemap` to visualize the results interactively."
],
"metadata": {
"id": "oqNYdrp1rDLZ"
}
},
{
"cell_type": "code",
"source": [
"Map = geemap.Map(center=(32.8287, -115.45131), zoom=13)\n",
"roygbiv = [\"FFFFFF\", \"#F60000\", \"#FF8C00\", \"#FFEE00\", \"#4DE94C\", \"#3783FF\", \"#4815AA\"]\n",
"cluster_vis = {\"min\": 0, \"max\": k-1, \"palette\": roygbiv}\n",
"Map.addLayer(clustered, cluster_vis, 'clustered')\n",
"Map"
],
"metadata": {
"id": "-FGSbmU3opE9"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Or request a thumbnail URL, which you can display using an IPython widget, download with `curl`, copy/paste, etc. These URLs are temporary and should not be used for long term access."
],
"metadata": {
"id": "cIQ4daz1iWwu"
}
},
{
"cell_type": "code",
"source": [
"thumb_clusters_vis = cluster_vis.copy()\n",
"thumb_clusters_vis[\"dimensions\"] = \"512x512\"\n",
"thumb_url = clustered.clip(c.first().geometry()).getThumbUrl(thumb_clusters_vis)\n",
"display(Image(url=thumb_url))"
],
"metadata": {
"id": "YwLDKVLvgtBY"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"## Supervised Classification\n",
"\n",
"### Use the annual USDA Cropland Data Layer (CDL) as reference labels\n",
"\n",
"For reference labels, overlay the annual CDL crop type map with annual satellite embeddings to get a training dataset. Train a Random Forest classifier to predict the top five crops in the specified region. Classify every image in the custom satellite embeddings time series, then make an animated thumbnail of the time series of classifications."
],
"metadata": {
"id": "JSdbl0cOZizs"
}
},
{
"cell_type": "code",
"source": [
"# CDL classification for 2025.\n",
"cdl = ee.ImageCollection(\"USDA/NASS/CDL\")\n",
"cdl_2025 = cdl.filter(ee.Filter.calendarRange(2025, 2025, 'year')).first()\n",
"\n",
"# Sample the CDL and annual embeddings for the same year.\n",
"aef = ee.ImageCollection(\"GOOGLE/SATELLITE_EMBEDDING/V1/ANNUAL\")\n",
"aef_2025 = aef.filter(ee.Filter.calendarRange(2025, 2025, 'year')).mosaic()\n",
"sample = aef_2025.addBands(cdl_2025).sample(\n",
" region=c.first().geometry(),\n",
" numPixels=n,\n",
" scale=10,\n",
")\n",
"\n",
"# Dominant classes only, the top five.\n",
"cropland = 'cropland'\n",
"histo = ee.Dictionary(sample.aggregate_histogram(cropland))\n",
"keys = histo.keys().sort(histo.values())\n",
"top_classes = keys.slice(keys.length().subtract(5))\n",
"\n",
"def remap_class(f: ee.Feature) -> ee.Feature:\n",
" \"\"\"Remap the top five classes to consecutive integers, all else to 0.\"\"\"\n",
" old_class = f.get(cropland)\n",
" new_class = top_classes.indexOf(ee.String(old_class)).add(1)\n",
" return f.set(cropland, new_class)\n",
"\n",
"training = sample.map(remap_class)\n",
"rf = ee.Classifier.smileRandomForest(10).train(training, cropland, aef_2025.bandNames())"
],
"metadata": {
"id": "xW8MWoMovF2A"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Display an animation of the classificatons."
],
"metadata": {
"id": "RxSK8VMU3Otq"
}
},
{
"cell_type": "code",
"source": [
"rf_vis = {\"min\": 0, \"max\": 5, \"palette\": roygbiv[0:6]}\n",
"# Classify and visualize each image in the time series.\n",
"classified = c.map(lambda image: image.classify(rf).visualize(**rf_vis))\n",
"# Get a URL for the animation and use an IPython widget to display it.\n",
"anim_url = classified.getVideoThumbURL({ \"framesPerSecond\": 3, \"dimensions\": \"512x512\" })\n",
"display(Image(url=anim_url))"
],
"metadata": {
"id": "3pP7IXU-0KOl"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"## What's next?\n",
"\n",
"Explore [other Custom Satellite Embeddings samples](https://developers.devsite.corp.google.com/maps/documentation/custom-satellite-embeddings/samples)."
],
"metadata": {
"id": "tt2vtzYSjvFN"
}
}
]
}
12 changes: 12 additions & 0 deletions custom_satellite_embeddings/notebooks/ee_classification/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Custom Satellite Embeddings Supervised and Unsupervised Classification in Earth Engine

In this guide, supervised and unsupervised classifications are fitted to a time series of
six monthly satellite embeddings images in an agricultural area of the California, USA central valley.
Use these methods to indentify areas of agricultural change or temporal trajectories of
particular crop types. An animated visualizion is produced to illustrate change over time.

This guide demonstrates:

- Unsupervised classification in Earth Engine
- Supervised Classification (Random Forest) in Earth Engine
- Making an animation of a time series of classifications
Loading