Skip to content
Merged
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
112 changes: 93 additions & 19 deletions people-and-planet-ai/image-classification/README.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@
" </a>\n",
" </td>\n",
"</table>\n",
"<!-- Legacy redirect aliases: https://console.cloud.google.com/vertex-ai/colab/import/https:%2F%2Fraw.githubusercontent.com%2FGoogleCloudPlatform%2Fpython-docs-samples%2Fmain%2Fpeople-and-planet-ai%2Fimage-classification%2FREADME.ipynb and https://console.cloud.google.com/vertex-ai/workbench/deploy-notebook?download_url=https://raw.githubusercontent.com/GoogleCloudPlatform/python-docs-samples/main/people-and-planet-ai/image-classification/README.ipynb -->\n",
"\n",
"<br clear=\"left\"/>\n",
"\n",
Expand All @@ -81,7 +80,13 @@
"gcloud compute networks subnets create default --project=example-project-id --network=default --region=us-central1 --range=10.128.0.0/20 --enable-private-ip-google-access\n",
"```\n",
"\n",
"</details>"
"</details>\n",
"\n",
"---\n",
"\n",
"| Author |\n",
"| --- |\n",
"| [Google Cloud](https://github.com/GoogleCloudPlatform) |"
],
"metadata": {
"id": "1LUt29D0MSeu"
Expand Down Expand Up @@ -115,7 +120,33 @@
" Use [`classify_wildlife_image.py`](classify_wildlife_image.py) to inspect camera trap photos, see how taxonomic family (`taxon_family`) resolves look-alike species, and pass `habitat_region` context to geo-fence predictions.\n",
"4. **Generate multimodal embeddings for rare-species search**:\n",
" Use [`embed_wildlife_image.py`](embed_wildlife_image.py) and **Gemini Embedding 2** (`gemini-embedding-2`) to turn a single reference photo of an endangered species into a 768-dimensional vector embedding for similarity search in BigQuery.\n",
"5. **Clean up resources** to avoid ongoing storage costs."
"5. **Clean up resources** to avoid ongoing storage costs.\n",
"\n",
"### Objectives\n",
"\n",
"In this tutorial, you learn how to:\n",
"\n",
"- Ingest and filter LILA WCS Camera Traps metadata into BigQuery using Apache Beam on Dataflow (`create_images_metadata_table.py`).\n",
"- Run class-balanced sampling (`Sample.FixedSizePerKey`) and batch multimodal inference (`RunInference` with `gemini-3.8-flash`) directly against `gs://public-datasets-lila/wcs-unzipped/` (`run_wildlife_inference.py`).\n",
"- Classify individual camera trap photos into structured ecological observations with taxonomic hierarchy (`taxon_family`) and habitat geo-fencing (`classify_wildlife_image.py`).\n",
"- Generate 768-dimensional multimodal vector embeddings with `gemini-embedding-2` and run BigQuery `VECTOR_SEARCH` for rare-species discovery (`embed_wildlife_image.py`).\n",
"\n",
"### Costs\n",
"\n",
"This tutorial uses billable components of Google Cloud:\n",
"\n",
"- [Dataflow](https://cloud.google.com/dataflow/pricing)\n",
"- [BigQuery](https://cloud.google.com/bigquery/pricing)\n",
"- [Vertex AI / Gemini API](https://cloud.google.com/vertex-ai/generative-ai/pricing)\n",
"- [Cloud Storage](https://cloud.google.com/storage/pricing)\n",
"\n",
"Use the [Google Cloud Pricing Calculator](https://cloud.google.com/products/calculator) to generate a cost estimate based on your projected usage.\n",
"\n",
"### Prerequisites\n",
"\n",
"- A Google Cloud project with billing enabled.\n",
"- The Dataflow (`dataflow.googleapis.com`), Vertex AI (`aiplatform.googleapis.com`), and BigQuery (`bigquery.googleapis.com`) APIs enabled.\n",
"- IAM roles: Dataflow Admin (`roles/dataflow.admin`), BigQuery Admin (`roles/bigquery.admin`), Storage Admin (`roles/storage.admin`), and Vertex AI User (`roles/aiplatform.user`)."
],
"metadata": {
"id": "jXDRG8QK2BZD"
Expand Down Expand Up @@ -154,19 +185,19 @@
" # Install libffi-dev for the Dataflow pipeline in ephemeral Colab runtimes.\n",
" !apt-get -qq install libffi-dev\n",
"\n",
" # Sparse-checkout only people-and-planet-ai/image-classification into an isolated directory.\n",
" REPO_DIR = \"ppai-image-classification\"\n",
" SAMPLE_SUBPATH = \"people-and-planet-ai/image-classification\"\n",
" BRANCH = \"main\"\n",
"# Sparse-checkout only people-and-planet-ai/image-classification into an isolated directory.\n",
"REPO_DIR = \"ppai-image-classification\"\n",
"SAMPLE_SUBPATH = \"people-and-planet-ai/image-classification\"\n",
"BRANCH = \"main\"\n",
"\n",
" if not os.getcwd().endswith(SAMPLE_SUBPATH):\n",
" if not os.path.exists(REPO_DIR):\n",
" !git clone --filter=blob:none --no-checkout --depth 1 --sparse -b {BRANCH} https://github.com/GoogleCloudPlatform/python-docs-samples.git {REPO_DIR}\n",
" !git -C {REPO_DIR} sparse-checkout set {SAMPLE_SUBPATH}\n",
" %cd {REPO_DIR}\n",
" !git fetch --depth 1 origin {BRANCH}\n",
" !git -c advice.detachedHead=false checkout -f FETCH_HEAD\n",
" %cd {SAMPLE_SUBPATH}\n",
"if not os.getcwd().endswith(SAMPLE_SUBPATH):\n",
" if not os.path.exists(REPO_DIR):\n",
" !git clone --filter=blob:none --no-checkout --depth 1 --sparse -b {BRANCH} https://github.com/GoogleCloudPlatform/python-docs-samples.git {REPO_DIR}\n",
" !git -C {REPO_DIR} sparse-checkout set {SAMPLE_SUBPATH}\n",
" %cd {REPO_DIR}\n",
" !git fetch --depth 1 origin {BRANCH}\n",
" !git -c advice.detachedHead=false checkout -f FETCH_HEAD\n",
" %cd {SAMPLE_SUBPATH}\n",
"\n",
"if os.getcwd() not in sys.path:\n",
" sys.path.insert(0, os.getcwd())\n",
Expand Down Expand Up @@ -251,11 +282,13 @@
"cell_type": "code",
"execution_count": null,
"source": [
"# fmt: off\n",
"PROJECT_ID = \"example-project-id\" # @param {type: \"string\", placeholder: \"example-project-id\", isTemplate: true}\n",
"LOCATION = \"us-central1\" # @param {type: \"string\"}\n",
"# fmt: on\n",
"\n",
"if not PROJECT_ID or PROJECT_ID == \"example-project-id\":\n",
" PROJECT_ID = os.environ.get(\"GOOGLE_CLOUD_PROJECT\", \"example-project-id\")\n",
"\n",
"LOCATION = \"us-central1\" # @param {type: \"string\"}\n",
"if not LOCATION:\n",
" LOCATION = os.environ.get(\"GOOGLE_CLOUD_LOCATION\", \"us-central1\")\n",
"\n",
Expand Down Expand Up @@ -780,12 +813,50 @@
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"### Validate target state\n",
"\n",
"Verify that all 6 gallery camera trap frames were embedded into 768-dimensional vectors and written to `embeddings.jsonl`:"
],
"metadata": {
"id": "validate-target-state-md"
}
},
{
"cell_type": "code",
"source": [
"with open(\"embeddings.jsonl\") as f:\n",
" written_rows = [json.loads(line) for line in f if line.strip()]\n",
"\n",
"assert len(written_rows) == len(gallery), (\n",
" f\"Expected {len(gallery)} rows in embeddings.jsonl, got {len(written_rows)}\"\n",
")\n",
"for i, record in enumerate(written_rows):\n",
" assert record[\"image_gcs_uri\"] == gallery[i][0], (\n",
" f\"Row {i} URI mismatch: {record['image_gcs_uri']} != {gallery[i][0]}\"\n",
" )\n",
" assert record[\"species_scientific_name\"] == gallery[i][1], (\n",
" f\"Row {i} species mismatch: {record['species_scientific_name']} != {gallery[i][1]}\"\n",
" )\n",
" assert len(record[\"embedding\"]) == 768, (\n",
" f\"Row {i} expected 768-d embedding, got {len(record['embedding'])}\"\n",
" )\n",
"print(\"Target state validated: 6 camera trap embeddings (768 dimensions) verified in embeddings.jsonl.\")"
],
"metadata": {
"id": "validate-target-state-cell"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"## Cleaning up\n",
"\n",
"To avoid incurring ongoing storage charges for the resources created in this notebook, run the following cell to delete the BigQuery dataset and the temporary Dataflow staging files in Cloud Storage:"
"To avoid incurring ongoing storage charges for the resources created in this notebook, set `delete_resources = True` and run the following cell to delete the BigQuery dataset and the temporary Dataflow staging files in Cloud Storage:"
],
"metadata": {
"id": "MGl_1UyiGVl6"
Expand All @@ -794,7 +865,9 @@
{
"cell_type": "code",
"source": [
"delete_resources = True # @param {type:\"boolean\"}\n",
"# fmt: off\n",
"delete_resources = False # @param {type:\"boolean\"}\n",
"# fmt: on\n",
"\n",
"if delete_resources or os.getenv(\"IS_TESTING\"):\n",
" # Delete the BigQuery dataset (including the metadata, predictions, and embeddings tables).\n",
Expand All @@ -804,6 +877,7 @@
" !gcloud storage rm -r \"gs://{BUCKET_NAME}/{CLOUD_STORAGE_DIRECTORY}\""
],
"metadata": {
"cellView": "form",
"id": "8418262e"
},
"execution_count": null,
Expand Down
7 changes: 6 additions & 1 deletion people-and-planet-ai/image-classification/e2e_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,12 @@ def test_extract_metadata_fn_filters_invalid_categories() -> None:
mock_response = mock.MagicMock()
mock_response.content = zip_buffer.getvalue()

with mock.patch("requests.get", return_value=mock_response):
with mock.patch.object(
create_images_metadata_table.requests,
"get",
return_value=mock_response,
autospec=True,
):
fn = create_images_metadata_table._ExtractMetadataFn()
results = list(fn.process(create_images_metadata_table.METADATA_URL))

Expand Down
Loading