Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
485571d
feat(people-and-planet-ai): modernize wildlife image classification w…
iennae Oct 28, 2026
fdfa92d
docs(people-and-planet-ai): restore ASF notebook header, add in-noteb…
iennae Oct 28, 2026
3a7ece3
docs(people-and-planet-ai): align README.ipynb with vertex-ai-samples…
iennae Oct 28, 2026
6cb06b5
docs(people-and-planet-ai): restore original ASF license notice in Ce…
iennae Oct 28, 2026
951f3ee
docs(people-and-planet-ai): auto-derive bucket and dataset from PROJE…
iennae Oct 28, 2026
7cafdf2
chore(ppai): point notebook clone and launch links to branch for testing
iennae Oct 28, 2026
37f4de8
feat(ppai): use %pip install and remove kernel restart requirement
iennae Oct 28, 2026
15c8dad
fix(ppai): use git fetch and checkout FETCH_HEAD for idempotent branc…
iennae Oct 29, 2026
5130974
feat(ppai): use git sparse-checkout in isolated sample directory
iennae Oct 29, 2026
921dba3
fix(ppai): disable soft-delete on Dataflow temp bucket and silence de…
iennae Oct 29, 2026
1598cf6
docs(ppai): add expected runtime duration comments to notebook cells
iennae Oct 29, 2026
c2265b6
fix(ppai): enable INFO logging for Dataflow progress and merge param …
iennae Oct 29, 2026
06d1588
fix(ppai): enable INFO logging in run_wildlife_inference.py
iennae Oct 29, 2026
36a806b
feat(ppai): make BigQuery VECTOR_SEARCH section runnable in notebook
iennae Oct 29, 2026
f002da9
Merge remote-tracking branch 'origin/main' into feat/modernize-ppai-i…
iennae Oct 29, 2026
a3bfc86
chore(ppai): align README.ipynb with cloud_sample_notebook rubric checks
iennae Oct 29, 2026
dc69138
fix(ppai): add HTTP retry and specific pipeline exception handling pe…
iennae Oct 29, 2026
9517341
fix(ppai): use googlegenaisdk_ region tag prefix for Gen AI SDK snippets
iennae Oct 29, 2026
10b3227
chore(ppai): remove outdated Google Form feedback link from notebook
iennae Oct 29, 2026
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
22 changes: 12 additions & 10 deletions people-and-planet-ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@

## 🦏 [Wildlife Insights -- _image-classification_](image-classification)

> [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/GoogleCloudPlatform/python-docs-samples/blob/main/people-and-planet-ai/image-classification/README.ipynb)
> [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/GoogleCloudPlatform/python-docs-samples/blob/feat/modernize-ppai-image-classification/people-and-planet-ai/image-classification/README.ipynb)
>
> [Watch the video in YouTube<br> ![thumbnail](http://img.youtube.com/vi/hUzODH3uGg0/0.jpg)](https://youtu.be/hUzODH3uGg0)
> [Watch the video on YouTube ![thumbnail](https://img.youtube.com/vi/hUzODH3uGg0/0.jpg)](https://youtu.be/hUzODH3uGg0)

This model is trained to recognize animal species from
Classify wildlife species and extract ecological recovery metrics from
[camera trap](https://en.wikipedia.org/wiki/Camera_trap)
pictures.
pictures in the public [LILA Science](https://lila.science/datasets/wcscameratraps) Cloud Storage bucket (`gs://public-datasets-lila/`).

* **Model**: [AutoML] Vision
* **Creating datasets**: [Apache Beam] in [Dataflow]
* **Training the model**: [AutoML] in [Vertex AI]
* **Getting predictions**: [Vertex AI]
* **Multimodal classification and ecological extraction**: [Gemini] (`gemini-3.8-flash`) via the [Google Gen AI SDK]
* **Rare-species visual similarity and vector search**: [Gemini Embedding 2] (`gemini-embedding-2`)
* **Class-balanced batch inference pipeline**: [Apache Beam] `RunInference` in [Dataflow]
* **Metadata and structured observation storage**: [BigQuery]

[Apache Beam]: https://beam.apache.org
[BigQuery]: https://cloud.google.com/bigquery
[Dataflow]: https://cloud.google.com/dataflow
[AutoML]: https://cloud.google.com/vertex-ai/docs/beginner/beginners-guide
[Vertex AI]: https://cloud.google.com/vertex-ai
[Gemini]: https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/3-8-flash
[Gemini Embedding 2]: https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/embedding-2
[Google Gen AI SDK]: https://googleapis.github.io/python-genai/

---

Expand Down
848 changes: 440 additions & 408 deletions people-and-planet-ai/image-classification/README.ipynb

Large diffs are not rendered by default.

31 changes: 20 additions & 11 deletions people-and-planet-ai/image-classification/README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
# 🦏 Wildlife Insights -- _image-classification_

[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/GoogleCloudPlatform/python-docs-samples/blob/main/people-and-planet-ai/image-classification/README.ipynb)
[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/GoogleCloudPlatform/python-docs-samples/blob/feat/modernize-ppai-image-classification/people-and-planet-ai/image-classification/README.ipynb)

> [Watch the video in YouTube<br> ![thumbnail](http://img.youtube.com/vi/hUzODH3uGg0/0.jpg)](https://youtu.be/hUzODH3uGg0)
> [Watch the video on YouTube ![thumbnail](https://img.youtube.com/vi/hUzODH3uGg0/0.jpg)](https://youtu.be/hUzODH3uGg0)

This model is trained to recognize animal species from
Classify wildlife species and extract ecological recovery metrics from
[camera trap](https://en.wikipedia.org/wiki/Camera_trap)
pictures.
pictures in the public [LILA Science](https://lila.science/datasets/wcscameratraps) Cloud Storage bucket (`gs://public-datasets-lila/`).

* **Model**: [AutoML] Vision
* **Creating datasets**: [Apache Beam] in [Dataflow]
* **Training the model**: [AutoML] in [Vertex AI]
* **Getting predictions**: [Vertex AI]
* **Multimodal classification and ecological extraction**: [Gemini] (`gemini-3.8-flash`) via the [Google Gen AI SDK]
* **Rare-species visual similarity and vector search**: [Gemini Embedding 2] (`gemini-embedding-2`)
* **Class-balanced batch inference pipeline**: [Apache Beam] `RunInference` in [Dataflow]
* **Metadata and structured observation storage**: [BigQuery]

## Snippet files

* [`create_images_metadata_table.py`](create_images_metadata_table.py) (`dataflow_wildlife_ingest_metadata`): Ingests and cleans LILA WCS camera trap annotations into BigQuery.
* [`classify_wildlife_image.py`](classify_wildlife_image.py) (`googlegenaisdk_wildlife_classify_image`): Classifies a camera trap image into structured ecological metadata (`species_scientific_name`, `taxon_family`, `common_name`, `animal_count`, `juvenile_present`, `is_empty_or_misfire`, `confidence`) with optional habitat geo-fencing (`habitat_region`).
* [`embed_wildlife_image.py`](embed_wildlife_image.py) (`googlegenaisdk_wildlife_embed_image`): Generates a 768-dimensional multimodal vector embedding for a camera trap image using `gemini-embedding-2`.
* [`run_wildlife_inference.py`](run_wildlife_inference.py) (`dataflow_wildlife_run_inference`): Runs class-balanced sampling (`Sample.FixedSizePerKey`) and batch Gemini inference (`RunInference`) directly against `gs://public-datasets-lila/wcs-unzipped/` and writes results to BigQuery.

[Apache Beam]: https://beam.apache.org
[BigQuery]: https://cloud.google.com/bigquery
[Dataflow]: https://cloud.google.com/dataflow
[AutoML]: https://cloud.google.com/vertex-ai/docs/beginner/beginners-guide
[Vertex AI]: https://cloud.google.com/vertex-ai
[Gemini]: https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/3-8-flash
[Gemini Embedding 2]: https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/embedding-2
[Google Gen AI SDK]: https://googleapis.github.io/python-genai/

> 📜 Read more about this sample in the [blog post](https://cloud.google.com/blog/topics/developers-practitioners/recovering-global-wildlife-populations-using-ml).
> 📜 For more information about the background of this solution, see [Recovering global wildlife populations using ML](https://cloud.google.com/blog/topics/developers-practitioners/recovering-global-wildlife-populations-using-ml).
136 changes: 136 additions & 0 deletions people-and-planet-ai/image-classification/classify_wildlife_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env python

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# [START googlegenaisdk_wildlife_classify_image]
import sys

from google import genai
from google.genai import errors
from google.genai import types
from pydantic import BaseModel


class WildlifeObservation(BaseModel):
species_scientific_name: str
taxon_family: str
common_name: str
animal_count: int
juvenile_present: bool
is_empty_or_misfire: bool
confidence: float


def classify_wildlife_image(
project_id: str,
image_gcs_uri: str,
habitat_region: str = "Southeast Asia tropical rainforest",
) -> None:
"""Classifies a camera trap image into structured ecological metadata.

Uses Gemini 3.8 Flash on Gemini Enterprise Agent Platform using the global
endpoint with a Pydantic response schema and geographic habitat context
(geo-fencing) to identify the species and taxonomic family, count
individuals, detect juveniles, and flag empty or misfired frames.

Args:
project_id: Google Cloud project identifier (for example, 'example-project-id').
image_gcs_uri: Cloud Storage URI of the camera trap JPEG image.
habitat_region: Geographic region used to ground species predictions.
"""
# gemini-3.8-flash serves on the global endpoint.
location = "global"
client = genai.Client(vertexai=True, project=project_id, location=location)

image_part = types.Part.from_uri(
file_uri=image_gcs_uri,
mime_type="image/jpeg",
)
habitat_prefix = (
f"Camera trap habitat region: {habitat_region}. "
if habitat_region
else ""
)
prompt = (
f"{habitat_prefix}"
"Identify the wildlife species (scientific name, taxonomic family, and "
"common name) in this camera trap photo, count the individuals, "
"indicate whether juvenile offspring are present, and flag whether the "
"frame is empty or a camera misfire."
)
thinking_config = types.ThinkingConfig(
thinking_level=types.ThinkingLevel.LOW,
)
config = types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=WildlifeObservation,
thinking_config=thinking_config,
)

try:
response = client.models.generate_content(
model="gemini-3.8-flash",
contents=[image_part, prompt],
config=config,
)
except errors.APIError as err:
print(
f"error: failed to classify camera trap image '{image_gcs_uri}' in "
f"location '{location}' for project '{project_id}': {err.message}",
file=sys.stderr,
)
else:
observation = WildlifeObservation.model_validate_json(response.text)
print(f"Resource ID: {image_gcs_uri}")
print(f"Animal Count: {observation.animal_count}")
print(f"Common Name: {observation.common_name}")
print(f"Confidence: {observation.confidence:.2f}")
print(f"Empty or Misfire: {observation.is_empty_or_misfire}")
print(f"Habitat Region: {habitat_region}")
print(f"Juvenile Present: {observation.juvenile_present}")
print(f"Species: {observation.species_scientific_name}")
print(f"Taxon Family: {observation.taxon_family}")
# [END googlegenaisdk_wildlife_classify_image]


if __name__ == "__main__":
import argparse
import os

parser = argparse.ArgumentParser()
parser.add_argument(
"--project",
default=os.environ.get("GOOGLE_CLOUD_PROJECT", "example-project-id"),
help="Google Cloud project ID.",
)
parser.add_argument(
"--image-gcs-uri",
default=(
"gs://public-datasets-lila/wcs-unzipped/animals/0036/0072.jpg"
),
help="Cloud Storage URI of the camera trap JPEG image.",
)
parser.add_argument(
"--habitat-region",
default="Southeast Asia tropical rainforest",
help="Geographic region of the camera trap for geo-fenced classification.",
)
args = parser.parse_args()

classify_wildlife_image(
project_id=args.project,
image_gcs_uri=args.image_gcs_uri,
habitat_region=args.habitat_region,
)
Loading
Loading