AI Rare Bird Identification Pipeline

Train YOLO11 to identify endangered bird species from citizen photos.

Introduction

30 Second Summary

Every year, thousands of everyday people snap photos of birds in parks, backyards, and hiking trails. Some of those photos capture species on the brink of extinction, but the photographer rarely knows what they are looking at.

In this project, you will build an AI-powered pipeline that takes any bird photograph, identifies the species, and automatically generates a conservation alert when a rare or endangered bird is detected.

What You'll Build

You upload a bird photo, and within seconds the system draws a box around the bird, prints its species name with a confidence score, shows a full species profile, and fires off an alert if the bird is endangered.

By the end of this project, you'll have:

  • A working bird detection and classification pipeline that you can feed any photo and see the species name with a confidence percentage (e.g., "INDIAN VULTURE - 94.2% confidence").
  • A species information display showing the bird's scientific name, family, habitat, diet, and an interesting fact every time a species is identified.
  • An automatic conservation alert system that flags endangered species and generates a notification message for forest departments the moment a rare bird is found.
  • Secret Mission: Connect to the live IUCN Red List API for real-time conservation status verification instead of a static dictionary.

Are there any prerequisites?

You need a Google account to use Google Colab (the free cloud notebook where all the code runs). You will also create a free Kaggle account during Step 1 to download the bird image dataset. No local GPU or special hardware is required.

Before We Start

You are about to build the AI brain behind a citizen science conservation tool. Before writing any code, take a moment to commit to the problem you're solving and why it matters.

Set Up Google Colab with YOLO11 and Kaggle Access

Your rare bird identification pipeline relies on two things: a powerful GPU to train a deep learning model, and a large dataset of bird photos to learn from.

In this step, you will enable GPU acceleration in Google Colab, install the Ultralytics YOLO11 framework, and connect to Kaggle so you can download a bird species dataset in the next step.

In this step, get ready to:
  • Enable the T4 GPU runtime and mount Google Drive for saving your trained model.
  • Install the ultralytics package and verify YOLO11 loads correctly.
  • Set up Kaggle API credentials and verify access to datasets.
Enable GPU runtime and mount Google Drive

Training a deep learning model on CPU would take hours. A T4 GPU cuts that to minutes.

  • In your Google Colab notebook, click Runtime in the top menu bar.
  • Click Change runtime type.
  • In the Hardware accelerator dropdown, select T4 GPU.
  • Click Save.

Why T4 GPU?

The T4 is a free GPU option in Google Colab that provides enough power to train image classification models. Without it, training on 25 bird species would take significantly longer on CPU alone.

  • In the first code cell of your notebook, mount Google Drive by running this code:
# Mount Google Drive to save our trained model later
from google.colab import drive
drive.mount('/content/drive')

What does this code do?

Google Drive acts as persistent storage for your trained model. When the Colab session disconnects (after idle time), your Drive files survive. Without mounting Drive, you would lose the trained model every time the session ends.

Colab will ask you to grant permission to access your Google Drive. Click Allow when the prompt appears.

You should see the message Mounted at /content/drive printed below the cell.

Not seeing the mount confirmation?

Make sure you clicked Allow on the Google permissions popup. If the popup did not appear, try running the cell again.

If you see a timeout error, check that you are signed into the correct Google account in your browser.

help me mount Google Drive in Colab

Install ultralytics and verify GPU access

With Drive mounted, you now need the YOLO11 framework and confirmation that your GPU is active.

  • In the same cell, add the following code below the drive mount and run the full cell:
# Install YOLO11 framework and Kaggle CLI
import subprocess
subprocess.run(["pip", "install", "-q", "ultralytics", "kaggle"], check=True)

# Verify GPU is available
import torch
print(f"GPU Available: {torch.cuda.is_available()}")
print(f"GPU Name: {torch.cuda.get_device_name(0)}")

What does this code do?

  • subprocess.run(["pip", "install", "-q", "ultralytics", "kaggle"]...) installs the Ultralytics YOLO11 library and the Kaggle CLI tool quietly (the -q flag suppresses verbose output).
  • torch.cuda.is_available() checks whether a GPU is accessible to PyTorch, which YOLO11 uses under the hood.
  • torch.cuda.get_device_name(0) prints the name of the assigned GPU so you can confirm it is a Tesla T4.

You should see output like:

GPU Available: True
GPU Name: Tesla T4

Seeing GPU Available: False?

This means the T4 GPU runtime was not saved correctly. Go back to Runtime > Change runtime type and confirm T4 GPU is selected. After saving, you may need to click Restart runtime and re-run the cell.

help me enable GPU in Google Colab

Now verify that YOLO11 itself loads correctly. Before you run this, what do you expect to see if the installation worked?

  • In a new cell, verify YOLO11 loads by running:
# Verify YOLO11 loads without errors
from ultralytics import YOLO
print(YOLO("yolo11n.pt"))

You should see the YOLO11n model architecture printed with layer details and parameter counts. This confirms the framework is installed and can download pretrained weights.

Seeing a ModuleNotFoundError?

If you see ModuleNotFoundError: No module named 'ultralytics', the install did not complete. Re-run your Cell 1 and check for any error messages in the pip output.

help me install ultralytics in Google Colab

Set up Kaggle API credentials

The bird species dataset lives on Kaggle. To download it programmatically, you need an API token from your Kaggle account.

  • Go to kaggle.com and sign in (or create a free account if you do not have one).
  • Click your profile icon in the top right corner.
  • Click Settings.
  • Scroll down to the API section.
  • Click Create New Token. This downloads a kaggle.json file to your computer.

What is kaggle.json?

This file contains your Kaggle username and API key. The Kaggle CLI uses it to authenticate dataset downloads. Keep this file private as it grants access to your Kaggle account.

  • Back in your Colab notebook, upload and configure your Kaggle credentials by running this code in a new cell:
# Upload and configure Kaggle API credentials
from google.colab import files
import os
import shutil

# This opens a file picker - select your kaggle.json
uploaded = files.upload()

# Move the token to the location Kaggle CLI expects
os.makedirs("/root/.kaggle", exist_ok=True)
shutil.copy("kaggle.json", "/root/.kaggle/kaggle.json")
os.chmod("/root/.kaggle/kaggle.json", 0o600)
print("Kaggle credentials configured!")

What does this code do?

  • files.upload() opens a file picker in your browser so you can select the kaggle.json you just downloaded.
  • The file is copied to /root/.kaggle/kaggle.json which is where the Kaggle CLI looks for credentials.
  • os.chmod("/root/.kaggle/kaggle.json", 0o600) restricts file permissions so only the owner can read it. Kaggle CLI warns you if this permission is not set.

When you run this cell, a file upload button appears. Click it and select the kaggle.json file from your Downloads folder.

You should see the upload progress bar complete, then Kaggle credentials configured! printed.

  • Verify your Kaggle access works by searching for the bird dataset in a new cell:
# Verify Kaggle API access
!kaggle datasets list -s birds

You should see a table of bird-related datasets printed. This confirms your API token is working.

Seeing a 403 Forbidden or authentication error?

Double-check that you uploaded the correct kaggle.json file (not a renamed or empty file). Try downloading a fresh token from your Kaggle settings and re-running the upload cell.

If you see a permissions warning, make sure the os.chmod line ran successfully.

help me authenticate the Kaggle CLI in Google Colab

Verify the complete setup

Before you run the final check, predict what you expect to see. Will the GPU be a T4? Will the model load?

  • Confirm everything is ready by running this in a new cell:
# Final setup verification
!nvidia-smi

You should see a table showing Tesla T4 with approximately 15 GB of memory. This is your GPU ready for training.

✔️ Awesome, I've got everything!

Great. Double check you have Cell 1 saved with the drive mount, package install, and GPU verification.

ⓧ I'd like to double check the full code

Here is what your Cell 1 should look like at this point:

from google.colab import drive
drive.mount('/content/drive')

import subprocess
subprocess.run(["pip", "install", "-q", "ultralytics", "kaggle"], check=True)

import torch
print(f"GPU Available: {torch.cuda.is_available()}")
print(f"GPU Name: {torch.cuda.get_device_name(0)}")

Your Kaggle credentials cell should have uploaded kaggle.json and copied it to /root/.kaggle/kaggle.json with permissions set to 0o600.

Your environment is locked and loaded. Next up, you will download a bird species dataset from Kaggle and train YOLO11 to recognise 25 different species.

Download the Bird Dataset and Train YOLO11-cls Classifier

Your Colab environment is ready with GPU power and Kaggle access. Now it is time to teach the model what different bird species look like.

You will download a large bird image dataset, select a focused subset of 25 species, and fine-tune a YOLO11 classification model using transfer learning. Transfer learning lets you start from a model that already understands general image features, so you only need to teach it the specific differences between bird species.

In this step, get ready to:
  • Download the BIRDS 525 dataset from Kaggle and extract a 25-species subset.
  • Train YOLO11n-cls on the bird species subset for 30 epochs.
  • Validate the trained model and confirm it correctly classifies a sample image.
Download and extract the bird dataset

The BIRDS 525 dataset on Kaggle contains over 500 species of birds, neatly organized into train/valid/test folders. You will download the full dataset and then extract only the 25 species you need.

  • Create a new code cell in your Colab notebook and paste this code to download and extract the dataset:
import os
import shutil
import json

# Ensure Kaggle credentials directory exists
os.makedirs("/root/.kaggle", exist_ok=True)

# Download the BIRDS 525 dataset
subprocess.run([
    "kaggle", "datasets", "download",
    "-d", "gpiosenka/100-bird-species",
    "-p", "/content/bird_data"
], check=True)

# Extract the dataset
subprocess.run([
    "unzip", "-q",
    "/content/bird_data/100-bird-species.zip",
    "-d", "/content/bird_data"
], check=True)

What does this code do?

The kaggle datasets download command downloads the dataset as a zip file into /content/bird_data. The unzip -q command silently extracts it into the same directory, creating train/, valid/, and test/ folders with one subfolder per species.

The subprocess module was already imported in Cell 1. Because Colab runs cells sequentially, it remains available here.

  • Run the cell. You will see a progress bar as Kaggle downloads the dataset (this takes 1-2 minutes depending on connection speed).
  • Once complete, the cell finishes without errors and you see the progress bar reach 100%.

Seeing a 403 or authentication error?

  • Make sure your kaggle.json file was uploaded and copied to /root/.kaggle/kaggle.json in Step 1.
  • Verify the permissions are set to 0o600 by re-running the chmod command from the previous step.
  • Confirm you accepted the dataset's terms on the Kaggle website.

Why is my Kaggle dataset download failing in Colab?

Select a 25-species subset for training

Training on all 525 species would take too long on a free Colab GPU. Selecting 25 species (a mix of common and endangered birds) keeps training under 15 minutes while still producing a useful classifier.

  • Create a new code cell and paste this code to build the subset:
import random

# Mix of common and endangered bird species
SELECTED_SPECIES = [
    "ABBOTTS BABBLER", "AFRICAN CROWNED CRANE", "BALD EAGLE", "BLACK SWAN",
    "CALIFORNIA CONDOR", "CASSOWARY", "COCKATOO", "CRANE HAWK", "CROW",
    "DARK EYED JUNCO", "FLAMINGO", "GOLDEN EAGLE", "GREY PLOVER", "HARPY EAGLE",
    "INDIAN PEAFOWL", "KING VULTURE", "MARABOU STORK", "OSPREY", "PELICAN",
    "PURPLE SWAMPHEN", "RED TAILED HAWK", "SNOWY OWL", "SPOONBILL",
    "TRUMPETER SWAN", "VULTURE",
]

DATASET_ROOT = "/content/bird_data"
SUBSET_ROOT = "/content/bird_subset"

# Copy selected species into subset for each split
for split in ["train", "valid", "test"]:
    source_dir = os.path.join(DATASET_ROOT, split)
    if not os.path.exists(source_dir):
        continue
    for species in SELECTED_SPECIES:
        src = os.path.join(source_dir, species)
        dst = os.path.join(SUBSET_ROOT, split, species)
        if os.path.exists(src):
            shutil.copytree(src, dst, dirs_exist_ok=True)

# Verify the subset
train_dir = os.path.join(SUBSET_ROOT, "train")
species_found = os.listdir(train_dir)
print(f"Species in subset: {len(species_found)}")

What does this code do?

  • SELECTED_SPECIES defines the 25 bird species you will train on. This includes endangered species like California Condor and Vulture (Indian Vulture) so the pipeline can learn to identify rare birds.
  • The loop copies only these species from the full dataset into a new /content/bird_subset directory, preserving the train/valid/test split structure that YOLO11-cls expects.
  • The final print statement confirms the subset was created correctly.
  • Run the cell. You should see the output:
Species in subset: 25

Seeing fewer than 25 species?

  • Make sure Cell 2 finished without errors. If the unzip failed, the species folders will not exist.
  • Check that the dataset path /content/bird_data contains train, valid, and test subdirectories by running !ls /content/bird_data in a new cell.

Why is my bird species subset missing some species?

Train YOLO11n-cls on the bird species subset

With the dataset ready, you can now fine-tune the pretrained YOLO11n-cls model. The pretrained weights already understand general image features like edges, textures, and shapes. Fine-tuning teaches it the specific visual differences between your 25 bird species.

  • Create a new code cell and paste this training code:
from ultralytics import YOLO

# Load pretrained YOLO11 classification model
model = YOLO("yolo11n-cls.pt")

# Train on bird species subset
results = model.train(
    data=SUBSET_ROOT,
    epochs=30,
    imgsz=224,
    batch=64,
    project="/content/drive/MyDrive/bird_model",
    name="yolo11_bird_cls",
    exist_ok=True,
)

print(f"Training complete!")
print(f"Best model saved to: /content/drive/MyDrive/bird_model/yolo11_bird_cls/weights/best.pt")

What do these training parameters mean?

  • yolo11n-cls.pt is the nano classification variant of YOLO11. It is small enough to train quickly on a free T4 GPU while still being accurate.
  • epochs=30 means the model sees every training image 30 times. This is enough for transfer learning since it already has pretrained knowledge.
  • imgsz=224 resizes all images to 224x224 pixels. This is the standard input size for classification models.
  • project and name save the trained model to Google Drive so it persists after the Colab session ends.
  • Run the cell. Training takes approximately 10-15 minutes on a T4 GPU.
  • You will see epoch-by-epoch progress with loss values and a progress bar for each epoch.
  • When training finishes, you should see the output:
Training complete!
Best model saved to: /content/drive/MyDrive/bird_model/yolo11_bird_cls/weights/best.pt

Training takes a while

While training runs, watch the loss values decrease epoch by epoch. Lower loss means the model is learning to distinguish between species more accurately. You do not need to interact with anything during training.

Training stopped or runtime disconnected?

  • If Colab disconnects mid-training, go to **Runtime** then **Reconnect**. Because exist_ok=True is set, you can re-run the training cell and it will overwrite the previous attempt.
  • If you see an out-of-memory error, reduce batch=64 to batch=32 and re-run.

Why did my YOLO11 training fail or stop in Google Colab?

Validate the trained model and test a prediction

Training is done. Now you need to confirm the model actually learned to identify birds correctly. You will run it against the test set to measure accuracy, then predict the species of a single image.

  • Before you run this next cell, predict: do you think the model will achieve above or below 85% accuracy on clean dataset images?
  • Create a new code cell and paste this validation code:
import glob

# Load the best trained model
best_model = YOLO("/content/drive/MyDrive/bird_model/yolo11_bird_cls/weights/best.pt")

# Validate on test set
metrics = best_model.val(data=SUBSET_ROOT, split="test")
print(f"Top-1 Accuracy: {metrics.top1:.2%}")
print(f"Top-5 Accuracy: {metrics.top5:.2%}")

# Test on a sample image from validation set
sample_species = species_found[0]
sample_images = glob.glob(os.path.join(SUBSET_ROOT, "valid", sample_species, "*.jpg"))
if sample_images:
    result = best_model(sample_images[0])
    pred_class = result[0].probs.top1
    pred_conf = result[0].probs.top1conf.item()
    pred_name = result[0].names[pred_class]
    print(f"\nSample prediction:")
    print(f"  Image: {os.path.basename(sample_images[0])}")
    print(f"  Predicted: {pred_name} ({pred_conf:.1%} confidence)")
    print(f"  Actual: {sample_species}")

What does this code do?

  • best_model.val() runs the trained model against every image in the test split and calculates accuracy. Top-1 means the model's best guess is correct. Top-5 means the correct answer is in the model's top 5 guesses.
  • The sample prediction picks one image from the validation set and shows the predicted species name with a confidence percentage. This gives you a concrete example of the model in action.
  • Run the cell. You should see output similar to:
Top-1 Accuracy: 87.20%
Top-5 Accuracy: 98.40%

Sample prediction:
  Image: 001.jpg
  Predicted: ABBOTTS BABBLER (96.3% confidence)
  Actual: ABBOTTS BABBLER

Your model achieves 85%+ top-1 accuracy on clean dataset images. The sample prediction shows a high confidence score with the correct species name. This confirms the model has learned to distinguish between the 25 bird species.

Accuracy below 85%?

  • Ensure you trained for 30 full epochs. If training was interrupted, re-run Cell 4.
  • Verify the subset has 25 species (re-run Cell 3 and check the output says 25).
  • Try increasing epochs to 50 by editing epochs=30 to epochs=50 and re-running.

Why is my YOLO11 classification accuracy low after training?

✔️ Awesome, I've got everything!

Great. Make sure all five cells are saved in your notebook.

ⓧ I'd like to double check the full code

Scroll through your notebook and confirm these cells are present in order:

  • Cell 1: Drive mount, pip install, GPU verification (from Step 1).
  • Cell 2: Kaggle dataset download and unzip.
  • Cell 3: Species subset selection with SELECTED_SPECIES list and shutil.copytree loop.
  • Cell 4: model.train() call with epochs=30, imgsz=224, batch=64.
  • Cell 5: best_model.val() and sample prediction with species_found[0].

Each cell should run successfully in sequence from top to bottom.

Your classifier works beautifully on clean, well-framed dataset images. But citizens do not take studio-quality bird photos. Next up, you will test this model on messy real-world photos and discover why raw citizen images need special handling.

Test on Real-World Citizen Photos

Your model scored 85%+ accuracy on the clean test set in Step 2. Those dataset images have plain backgrounds, centered subjects, and consistent lighting.

Real citizen photos are a different story. Birds perch on branches with dappled forest behind them, share the frame with leaves and rocks, and appear at unpredictable angles. This step puts your classifier through a real-world stress test to see how it handles the mess.

In this step, get ready to:
  • Download a citizen-style bird photo from the internet.
  • Run the trained classifier directly on the raw photo.
  • Document the accuracy gap between clean dataset images and cluttered real-world photos.
Download and classify a citizen-style photo

A typical citizen photo features a bird in its natural habitat. The image you are about to download shows an Eastern Yellow Robin perched in a campground, surrounded by branches and natural clutter.

  • Test your model on a real-world citizen photo by pasting this code into a new Colab cell:
import urllib.request
import cv2
import numpy as np
from google.colab.patches import cv2_imshow

# URLs of citizen-style bird photos (birds in natural settings)
test_urls = [
    "https://upload.wikimedia.org/wikipedia/commons/thumb/4/45/Eopsaltria_australis_-_Mogo_Campground.jpg/1280px-Eopsaltria_australis_-_Mogo_Campground.jpg",
]

os.makedirs("/content/test_photos", exist_ok=True)

for i, url in enumerate(test_urls):
    filepath = f"/content/test_photos/citizen_photo_{i}.jpg"
    try:
        urllib.request.urlretrieve(url, filepath)
        print(f"Downloaded: citizen_photo_{i}.jpg")

        # Direct classification on raw photo
        result = best_model(filepath)
        pred_class = result[0].probs.top1
        pred_conf = result[0].probs.top1conf.item()
        pred_name = result[0].names[pred_class]
        print(f"  Direct classification: {pred_name} ({pred_conf:.1%} confidence)")
    except Exception as e:
        print(f"  Could not download image {i}: {e}")

What does this code do?

  • It imports urllib.request for downloading images from URLs, and cv2 (OpenCV) for image processing you will use in later steps.
  • It creates a /content/test_photos directory to store downloaded citizen photos.
  • For each URL, it downloads the photo, then immediately runs best_model on the raw image and prints the predicted species and confidence score.

Before you run this cell, think about what will happen. Your model scored 85%+ on clean dataset images with plain backgrounds. Do you expect the same confidence on a photo where a bird is surrounded by branches, leaves, and a campground background?

  • Run the cell.

You will see the model output a species prediction with a confidence score significantly lower than what you saw in Step 2. The prediction itself may even be wrong.

Image failed to download?

  • Check that your Colab runtime is still connected (look for the green checkmark in the top right).
  • Wikimedia URLs occasionally time out. If you see a timeout error, re-run the cell.
  • help me troubleshoot why my citizen photo failed to download in Colab
Compare against your clean dataset results

Look at the confidence score printed by the cell you just ran. Now recall the sample prediction from Step 2, where the model identified a clean validation image with 90%+ confidence and got the species correct.

  • Note the confidence score from the citizen photo. It is likely 30-60% rather than the 85%+ you saw on clean images.
  • Note whether the predicted species name is correct or completely wrong.

That gap is the problem this project solves. The classifier is seeing the entire image: sky, branches, ground, leaves, and a small bird somewhere in the middle.

Why does confidence drop on real-world photos?

Your YOLO11 classifier was trained on images where the bird fills most of the frame. When it receives a photo where the bird occupies only 20% of the pixels, the remaining 80% is noise (branches, sky, grass).

The model has no way to focus on just the bird. It classifies the entire image as a whole, and all that background noise confuses its prediction.

✔️ Awesome, I've got everything!

Great. Make sure you have saved your notebook.

ⓧ I'd like to double check the full code

Here is the complete Cell 6 you should have in your notebook:

import urllib.request
import cv2
import numpy as np
from google.colab.patches import cv2_imshow

test_urls = [
    "https://upload.wikimedia.org/wikipedia/commons/thumb/4/45/Eopsaltria_australis_-_Mogo_Campground.jpg/1280px-Eopsaltria_australis_-_Mogo_Campground.jpg",
]

os.makedirs("/content/test_photos", exist_ok=True)

for i, url in enumerate(test_urls):
    filepath = f"/content/test_photos/citizen_photo_{i}.jpg"
    try:
        urllib.request.urlretrieve(url, filepath)
        print(f"Downloaded: citizen_photo_{i}.jpg")

        result = best_model(filepath)
        pred_class = result[0].probs.top1
        pred_conf = result[0].probs.top1conf.item()
        pred_name = result[0].names[pred_class]
        print(f"  Direct classification: {pred_name} ({pred_conf:.1%} confidence)")
    except Exception as e:
        print(f"  Could not download image {i}: {e}")

You have hit the core limitation of running a classifier on raw real-world photos. In the next step, you will fix this by adding bird detection to isolate the subject before classification.

Add Bird Detection to Fix Real-World Accuracy

In Step 3, you proved that the classifier struggles with real-world citizen photos. The confidence dropped and predictions went wrong.

The root cause is that the classifier sees the entire image. It processes the sky, branches, grass, and other background elements alongside the actual bird. By using a second YOLO11 model to detect and crop just the bird from the photo first, you isolate the subject and feed the classifier only what matters.

In this step, get ready to:
  • Load a pretrained YOLO11 detection model that already recognises birds.
  • Write a function that detects birds and crops them from raw photos.
  • Run the detection-then-classification pipeline and compare results against Step 3.
Load the detection model and build the bird cropping function

The pretrained yolo11n.pt model was trained on the COCO dataset, which includes "bird" as class 14. You do not need to train a detector. You can use this model directly to find birds in any photo.

  • Create a new code cell below your Step 3 cell, then paste the following code:
# Load pretrained YOLO11 detection model (knows "bird" as class 14)
detector = YOLO("yolo11n.pt")

def detect_and_crop_bird(image_path, detector_model):
    """Detect birds in image and return cropped bird regions."""
    img = cv2.imread(image_path)
    if img is None:
        return []

    results = detector_model(image_path, conf=0.3)
    crops = []

    for result in results:
        for box in result.boxes:
            cls_id = int(box.cls[0])
            # Class 14 in COCO is "bird"
            if cls_id == 14:
                x1, y1, x2, y2 = map(int, box.xyxy[0])
                crop = img[y1:y2, x1:x2]
                conf = box.conf[0].item()
                crops.append({
                    "crop": crop,
                    "bbox": (x1, y1, x2, y2),
                    "detection_conf": conf
                })

    return crops

What does this code do?

This code has two parts: loading the detector and defining the cropping function.

  • detector = YOLO("yolo11n.pt") loads a general-purpose object detection model. Unlike your classification model that was trained on birds, this one detects 80 different object types from the COCO dataset.
  • detect_and_crop_bird() reads the image with OpenCV, runs detection, then filters results to only keep class 14 (bird). For each bird found, it extracts the bounding box coordinates and crops that region from the original image.
  • conf=0.3 sets the minimum detection confidence. The model only reports birds it is at least 30% sure about, filtering out noise.
  • Run this cell by pressing the play button.

You should see YOLO11n download and load without errors. No bird images are processed yet.

Seeing a download error?

  • Make sure your Colab runtime is still connected. Check the green checkmark next to "RAM" and "Disk" in the top right.
  • If the session disconnected, reconnect and re-run all previous cells from the top before running this one.

Why isn't my detection model loading?

Run the detection-then-classification pipeline on citizen photos

Now you will chain both models together. The detector finds the bird, crops it, and the species classifier identifies the cropped region. This is the same citizen photo that failed in Step 3.

  • Add the following code below the function definition in the same cell:
# Test detection + classification pipeline on citizen photos
for i in range(len(test_urls)):
    filepath = f"/content/test_photos/citizen_photo_{i}.jpg"
    if not os.path.exists(filepath):
        continue

    crops = detect_and_crop_bird(filepath, detector)
    if crops:
        # Save crop temporarily for classification
        crop_path = f"/content/test_photos/crop_{i}.jpg"
        cv2.imwrite(crop_path, crops[0]["crop"])

        # Classify the cropped bird
        result = best_model(crop_path)
        pred_class = result[0].probs.top1
        pred_conf = result[0].probs.top1conf.item()
        pred_name = result[0].names[pred_class]
        print(f"  Detection+Classification: {pred_name} ({pred_conf:.1%} confidence)")
        print(f"  Detection confidence: {crops[0]['detection_conf']:.1%}")
    else:
        print(f"  No bird detected in citizen_photo_{i}.jpg")

What does this code do?

  • The loop iterates over the same citizen photos you tested in Step 3.
  • detect_and_crop_bird() finds the bird bounding box and returns the cropped region.
  • cv2.imwrite() saves the crop to disk so the classifier can load it as a standard image file.
  • best_model() classifies only the cropped bird region instead of the entire cluttered photo. The background noise is gone.

Before you run this, think back to Step 3. The classifier gave low confidence on the full photo. What do you think will happen now that it only sees the cropped bird?

  • Run the full cell again (it will re-load the detector and then execute the test loop).

You should see output like:

  Detection+Classification: DARK EYED JUNCO (87.3% confidence)
  Detection confidence: 82.4%

The confidence score jumps back to 80%+ with a correct species prediction. Compare this to the low-confidence or incorrect result from Step 3 when the classifier processed the full image.

Why does cropping fix the accuracy?

Your species classifier was trained on clean dataset images where the bird fills most of the frame. When a citizen photo includes trees, sky, and ground, the classifier gets confused by pixels that have nothing to do with the bird.

The detection model isolates the bird first. Now the classifier receives an image that looks much closer to its training data. Same model, same weights, dramatically better results.

Still seeing low confidence or no bird detected?

  • If you see "No bird detected", the citizen photo may not contain a recognisable bird at the 0.3 confidence threshold. Try a different bird photo URL in your Cell 6 test_urls list.
  • If confidence is still low after cropping, the species may not be one of the 25 in your training subset. The classifier can only identify species it was trained on.
  • Make sure you re-ran the full cell so the detector loaded before the test loop executed.

Why is my detection-then-classification pipeline still giving low confidence?

✔️ Awesome, I've got everything!

Great. Make sure you have saved your notebook.

ⓧ I'd like to double check the full code

Your Cell 7 should contain both chunks in order. Scroll up and confirm your cell has:

  • The detector load line: detector = YOLO("yolo11n.pt")
  • The detect_and_crop_bird() function definition.
  • The test loop starting with for i in range(len(test_urls)): that runs detection, saves the crop, classifies it, and prints results.

Your two-stage pipeline now handles messy real-world photos with high accuracy. Next up, you will build a complete identification system that shows species information and generates conservation alerts when rare birds are found.

Build the Complete Rare Bird Identification System

Your detection-then-classification pipeline works. It finds birds in cluttered photos and correctly identifies their species.

But right now it only prints a species name and a confidence number. A real conservation tool needs to present detailed species information, assess IUCN conservation status, and generate actionable alerts when a rare bird is found. In this step, you will turn the technical proof into a complete identification system.

In this step, get ready to:
  • Create a species information database with scientific names, habitats, diets, and conservation statuses.
  • Build a single pipeline function that chains detection, classification, species lookup, and alert generation together.
  • Run the full system on a citizen photo and see a complete formatted report with conservation alerts.
Create the species information database

The pipeline needs to look up detailed profiles for each species it identifies. You will store this data in a Python dictionary keyed by the species name that the classifier outputs.

  • Create a new cell in your Colab notebook (below Cell 7) and paste the following species database:
# Species information database with conservation details
SPECIES_INFO = {
    "CALIFORNIA CONDOR": {"scientific_name": "Gymnogyps californianus", "family": "Cathartidae", "habitat": "Rocky shrubland, coniferous forests, oak savannas", "diet": "Carrion (dead animals), primarily large mammals", "distribution": "Western United States, Baja California", "fun_fact": "Largest flying land bird in North America with a wingspan of up to 3 meters", "conservation_status": "CR", "status_label": "Critically Endangered"},
    "BALD EAGLE": {"scientific_name": "Haliaeetus leucocephalus", "family": "Accipitridae", "habitat": "Near large bodies of open water, forests", "diet": "Fish, waterfowl, small mammals", "distribution": "North America, from Alaska to Mexico", "fun_fact": "National bird of the United States, recovered from near extinction", "conservation_status": "LC", "status_label": "Least Concern"},
    "HARPY EAGLE": {"scientific_name": "Harpia harpyja", "family": "Accipitridae", "habitat": "Tropical lowland rainforests", "diet": "Sloths, monkeys, large birds", "distribution": "Central and South America", "fun_fact": "One of the most powerful eagles with talons as large as a grizzly bear claw", "conservation_status": "VU", "status_label": "Vulnerable"},
    "KING VULTURE": {"scientific_name": "Sarcoramphus papa", "family": "Cathartidae", "habitat": "Tropical lowland forests, savannas", "diet": "Carrion, primarily large animal carcasses", "distribution": "Southern Mexico to northern Argentina", "fun_fact": "Most colorful New World vulture with bright orange, yellow and red head", "conservation_status": "VU", "status_label": "Vulnerable"},
    "CASSOWARY": {"scientific_name": "Casuarius casuarius", "family": "Casuariidae", "habitat": "Tropical rainforests", "diet": "Fruits, fungi, insects, small vertebrates", "distribution": "New Guinea, northeastern Australia", "fun_fact": "Third tallest and second heaviest living bird, considered most dangerous bird", "conservation_status": "LC", "status_label": "Least Concern"},
    "VULTURE": {"scientific_name": "Gyps indicus", "family": "Accipitridae", "habitat": "Open country, near human habitation", "diet": "Carrion, primarily cattle carcasses", "distribution": "Indian subcontinent", "fun_fact": "Indian Vulture population declined 97% due to diclofenac poisoning", "conservation_status": "CR", "status_label": "Critically Endangered"},
    "INDIAN PEAFOWL": {"scientific_name": "Pavo cristatus", "family": "Phasianidae", "habitat": "Forests, farmland, near water sources", "diet": "Seeds, insects, fruits, small reptiles", "distribution": "Indian subcontinent, Sri Lanka", "fun_fact": "National bird of India, male tail feathers can be over 1.5 meters long", "conservation_status": "LC", "status_label": "Least Concern"},
    "AFRICAN CROWNED CRANE": {"scientific_name": "Balearica regulorum", "family": "Gruidae", "habitat": "Wetlands, grasslands, cultivated fields", "diet": "Insects, plants, seeds, small vertebrates", "distribution": "Eastern and southern Africa", "fun_fact": "Only crane species that can roost in trees due to a prehensile hind toe", "conservation_status": "EN", "status_label": "Endangered"},
    "MARABOU STORK": {"scientific_name": "Leptoptilos crumenifer", "family": "Ciconiidae", "habitat": "Savannas, wetlands, near human settlements", "diet": "Carrion, fish, insects, scraps", "distribution": "Sub-Saharan Africa", "fun_fact": "Has the largest wingspan of any land bird at up to 3.7 meters", "conservation_status": "LC", "status_label": "Least Concern"},
    "GOLDEN EAGLE": {"scientific_name": "Aquila chrysaetos", "family": "Accipitridae", "habitat": "Mountains, cliffs, open moorland", "diet": "Rabbits, hares, marmots, ground squirrels", "distribution": "Northern hemisphere (North America, Europe, Asia)", "fun_fact": "Can dive at speeds over 240 km/h when hunting prey", "conservation_status": "LC", "status_label": "Least Concern"},
}

What does this dictionary hold?

Each key is a species name that matches exactly what the classifier outputs. Each value is a profile dictionary with eight fields: scientific name, taxonomic family, habitat, diet, distribution range, an interesting fact, a two-letter IUCN conservation code, and a human-readable status label.

The conservation codes follow the IUCN Red List system: CR (Critically Endangered), EN (Endangered), VU (Vulnerable), NT (Near Threatened), and LC (Least Concern). These codes drive the alert system you will build next.

  • In the same cell, add the alert level definitions below the SPECIES_INFO dictionary:
# Conservation status alert levels
ALERT_LEVELS = {
    "CR": {"level": "CRITICAL", "action": "IMMEDIATE ALERT to Forest Department and all authorized bird watchers"},
    "EN": {"level": "HIGH", "action": "ALERT to Forest Department and regional bird watchers"},
    "VU": {"level": "MODERATE", "action": "NOTIFY authorized bird watchers in the area"},
    "NT": {"level": "LOW", "action": "LOG observation for monitoring records"},
    "LC": {"level": "NONE", "action": "Record observation in citizen science database"},
}

How do alert levels work?

Each IUCN status code maps to an alert level and a recommended action string. When the pipeline identifies a species with a CR, EN, or VU status, it will generate an alert message using the corresponding action text. Species with NT or LC status are simply logged without triggering an alert.

  • Run Cell 8 by pressing Shift+Enter.
  • Verify the data loaded by running this quick check in a temporary cell:
print(f"Species loaded: {len(SPECIES_INFO)}")
print(f"Alert levels: {list(ALERT_LEVELS.keys())}")
print(f"Example: {SPECIES_INFO['CALIFORNIA CONDOR']['status_label']}")

You should see Species loaded: 10, the five alert level codes, and Critically Endangered printed for the California Condor.

Seeing a KeyError?

Make sure you ran Cell 8 before the check cell. Colab cells execute independently, so the SPECIES_INFO variable only exists after its cell runs.

help me debug a KeyError when accessing SPECIES_INFO in Google Colab

Build the complete pipeline function

Now you will combine detection, classification, species lookup, and alert generation into a single function. The function walks through five stages and prints a formatted report at each one.

  • Create a new cell (Cell 9). Start the pipeline function with stages 1 and 2 (detection and classification):
def identify_rare_bird(image_path, detector_model, classifier_model, species_db, alert_levels):
    """Complete rare bird identification pipeline."""
    print("=" * 60)
    print("  RARE BIRD IDENTIFICATION SYSTEM")
    print("=" * 60)
    print(f"\nProcessing: {os.path.basename(image_path)}")
    print("-" * 60)

    # Stage 1: Detect bird in the image
    print("\n[Stage 1] Bird Detection...")
    crops = detect_and_crop_bird(image_path, detector_model)
    if not crops:
        print("  No bird detected in this image.")
        return None
    print(f"  Birds detected: {len(crops)}")
    bird = crops[0]
    print(f"  Detection confidence: {bird['detection_conf']:.1%}")

    # Stage 2: Classify the detected bird's species
    print("\n[Stage 2] Species Classification...")
    crop_path = "/content/temp_crop.jpg"
    cv2.imwrite(crop_path, bird["crop"])
    result = classifier_model(crop_path, verbose=False)
    pred_class = result[0].probs.top1
    pred_conf = result[0].probs.top1conf.item()
    species_name = result[0].names[pred_class]
    print(f"  Species: {species_name}")
    print(f"  Confidence: {pred_conf:.1%}")

What do stages 1 and 2 do?

Stage 1 reuses your detect_and_crop_bird() function from Step 4 to find and crop the bird. Stage 2 runs the trained classifier on the cropped image to get a species name and confidence score. These are the same operations you ran manually before, now wrapped in one flow.

  • Still in the same cell, continue the function with stages 3 and 4 (species lookup and conservation alert):
    # Stage 3: Look up species profile in the database
    print("\n[Stage 3] Species Information...")
    info = species_db.get(species_name, None)
    if info:
        print(f"  Scientific Name: {info['scientific_name']}")
        print(f"  Family: {info['family']}")
        print(f"  Habitat: {info['habitat']}")
        print(f"  Diet: {info['diet']}")
        print(f"  Distribution: {info['distribution']}")
        print(f"  Fun Fact: {info['fun_fact']}")
    else:
        print(f"  Detailed information not available for {species_name}")
        info = {"conservation_status": "DD", "status_label": "Data Deficient"}

    # Stage 4: Check conservation status and generate alert
    print("\n[Stage 4] Conservation Status Check...")
    status_code = info.get("conservation_status", "DD")
    status_label = info.get("status_label", "Data Deficient")
    print(f"  IUCN Status: {status_code} - {status_label}")
    alert = alert_levels.get(status_code, {"level": "UNKNOWN", "action": "Manual review required"})
    if status_code in ["CR", "EN", "VU"]:
        print(f"\n  *** RARE/ENDANGERED SPECIES DETECTED ***")
        print(f"  Alert Level: {alert['level']}")
        print(f"  Action: {alert['action']}")
        print(f"\n  SMS Alert Message:")
        print(f"  ALERT: {status_label} species '{species_name}' detected!")
        print(f"  Confidence: {pred_conf:.1%}")
        print(f"  Action Required: {alert['action']}")
    else:
        print(f"  Species is not endangered. Observation logged.")

What do stages 3 and 4 do?

Stage 3 looks up the classified species name in your SPECIES_INFO dictionary. If the species is found, it prints the full profile. If not, it assigns a "Data Deficient" placeholder so the pipeline still completes.

Stage 4 reads the conservation status code and checks if it is CR, EN, or VU. For any of these threatened categories, it generates a formatted SMS alert message that a forest department could act on.

  • Complete the function in the same cell with stage 5 (visual output) and the return statement:
    # Stage 5: Draw bounding box and label on the original image
    print("\n[Stage 5] Generating Visual Report...")
    img = cv2.imread(image_path)
    x1, y1, x2, y2 = bird["bbox"]
    # Red box for endangered, green for safe
    color = (0, 0, 255) if status_code in ["CR", "EN"] else (0, 255, 0)
    cv2.rectangle(img, (x1, y1), (x2, y2), color, 3)
    label = f"{species_name} ({pred_conf:.0%}) [{status_code}]"
    cv2.putText(img, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
    cv2_imshow(img)
    print("\n" + "=" * 60)
    print("  IDENTIFICATION COMPLETE")
    print("=" * 60)

    return {"species": species_name, "confidence": pred_conf, "conservation_status": status_code, "status_label": status_label, "alert_level": alert.get("level"), "info": info, "bbox": bird["bbox"]}

What does stage 5 do?

Stage 5 uses OpenCV to draw a bounding box on the original image. The box color indicates danger level: red for critically endangered or endangered species, green for everything else. It also overlays the species name, confidence score, and IUCN code as a text label above the box.

  • Run Cell 9 by pressing Shift+Enter. The cell defines the function but produces no output yet.

Seeing a SyntaxError?

Each stage block must be indented inside the function (4 spaces). If you pasted the chunks separately, make sure the indentation is consistent across all stages. Every line inside the function starts with at least 4 spaces.

help me fix a SyntaxError or IndentationError in my identify_rare_bird function

✔️ Awesome, I've got everything!

Great. Make sure Cell 9 ran without errors before continuing.

ⓧ I'd like to double check the full code

Your Cell 9 should contain one function definition with all five stages assembled together. Scroll through and confirm these sections appear in order:

  • Function signature: def identify_rare_bird(image_path, detector_model, classifier_model, species_db, alert_levels):
  • Stage 1: Bird Detection (calls detect_and_crop_bird).
  • Stage 2: Species Classification (writes crop to /content/temp_crop.jpg, runs classifier_model).
  • Stage 3: Species Information (looks up species_db).
  • Stage 4: Conservation Status Check (checks for CR, EN, VU and prints alert message).
  • Stage 5: Visual Report (draws bounding box with cv2.rectangle, calls cv2_imshow).
  • Return statement returning a dictionary with keys: species, confidence, conservation_status, status_label, alert_level, info, bbox.
Run the pipeline on a citizen photo

Everything is wired together. Time to see the full system in action on one of the citizen photos from Step 3.

  • Create a new cell (Cell 10) and paste the following code to run the pipeline:
# Run the complete identification pipeline
test_image = "/content/test_photos/citizen_photo_0.jpg"
if os.path.exists(test_image):
    result = identify_rare_bird(
        image_path=test_image,
        detector_model=detector,
        classifier_model=best_model,
        species_db=SPECIES_INFO,
        alert_levels=ALERT_LEVELS,
    )

What does this cell do?

It passes the citizen photo through the entire pipeline: detection, cropping, classification, species info lookup, conservation status check, and visual annotation. All five stages execute in sequence, printing their results as they go.

Before you run this cell, what do you expect to see? Will the system produce an alert, or just log the observation?

  • Run Cell 10 by pressing Shift+Enter.

You should see a complete formatted report printing through all five stages. The output includes: a bird detected with its bounding box coordinates, the species name with a confidence percentage, a full species profile (scientific name, family, habitat, diet, fun fact), the IUCN conservation status, and the annotated image displayed inline.

If the classified species has a CR, EN, or VU status, you will also see an alert message with the recommended action for the forest department.

Seeing 'No bird detected in this image'?

This means the detection model did not find a bird in the photo. Confirm that /content/test_photos/citizen_photo_0.jpg still exists by running os.path.exists("/content/test_photos/citizen_photo_0.jpg"). If it returns False, re-run Cell 6 to re-download the test photos.

help me debug why no bird is detected in my citizen photo

Your rare bird identification system is complete. It takes any citizen photo, detects the bird, identifies the species, presents a detailed profile, and flags endangered species with actionable alerts. Next up, the Secret Mission shows you how to connect this system to a live conservation API.

Secret mission

Connect to the IUCN Red List API for Live Conservation Status

Your pipeline currently uses a static dictionary for conservation status. What if a species gets reclassified next month? In this secret mission, connect to the live IUCN Red List API so your system always pulls the latest conservation data in real time.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely on Google Colab's free GPU tier and Google Drive, so there are no ongoing costs.

Resources you used:

  • Trained YOLO11-cls model saved to Google Drive at /content/drive/MyDrive/bird_model/ (no ongoing cost).
  • Google Colab notebook saved to Google Drive (free, auto-disconnects after idle).
  • Kaggle API token stored in Colab session (ephemeral, disappears on disconnect).

Keep everything running

No action needed. Choose this if you plan to continue building on this pipeline or extend it into a web application.

  • Your trained model file (best.pt) is saved to Google Drive permanently. It is roughly 5MB and costs nothing to store.
  • The Colab notebook auto-saves to your Google Drive. You can reopen it anytime.
  • The Kaggle API token only exists in the Colab session. You will need to re-upload kaggle.json if you reconnect after the session expires.

Pause - I'll come back to this later

Shut down the GPU runtime to free up Colab resources. Your files on Google Drive remain intact.

  • Close the Colab browser tab. The GPU runtime automatically disconnects after 90 minutes of inactivity.
  • Your trained model and notebook stay on Google Drive. Reconnect anytime by reopening the notebook.
  • The dataset files at /content/bird_data/ and /content/bird_subset/ live in the ephemeral Colab runtime. They will be gone when the session ends. Re-run the download cell to restore them.

Delete - I don't want to use this again

Remove all project resources from Google Drive to free up space.

  • Delete the trained model folder from Google Drive by running this in a Colab cell:
import shutil
shutil.rmtree('/content/drive/MyDrive/bird_model', ignore_errors=True)
print('bird_model folder deleted from Google Drive.')
  • Alternatively, open Google Drive in your browser, navigate to My Drive > bird_model, right-click the folder, and select Move to trash.
  • Delete the Colab notebook from Google Drive if you no longer need it. Open Google Drive, find the notebook file in My Drive > Colab Notebooks, right-click, and select Move to trash.
  • The Colab session storage (dataset files, test photos, temporary crops) is automatically wiped when the runtime disconnects. No action needed for those.

Nice Work!

Nice Work!

Nice work! You just built a complete AI-powered rare bird identification system from scratch using YOLO11 on Google Colab.

You went from a blank notebook to a working pipeline that detects birds in messy real-world photos, identifies their species, and generates conservation alerts for endangered birds.

You learned how to:

  • Train a YOLO11 classification model to identify 25 bird species using transfer learning on Google Colab's free GPU.
  • Build a two-stage detection-then-classification pipeline that solves the real-world accuracy problem by cropping birds out of cluttered backgrounds before classifying them.
  • Create a full rare bird identification system that displays species profiles, checks conservation status, and generates actionable alerts for forest departments when endangered species are detected.
  • Secret Mission: Connected to the live IUCN Red List API for dynamic conservation status verification with graceful fallback to a static dictionary.

Ready to quiz yourself?