Automated wildlife identification from ALA camera-trap images using SpeciesNet
Author details: Dr Renuka Sharma
Editor details: Xiang Zhao
Contact details: support@ecocommons.org.au
Copyright statement: This script is the product of the EcoCommons platform. Please refer to the EcoCommons website for more details: https://www.ecocommons.org.au/
This notebook, developed by the EcoCommons and WildObs team, demonstrates how to automatically identify wildlife in camera-trap images by combining two open tools: the Atlas of Living Australia (ALA) and Google’s SpeciesNet model. We query ALA for camera-trap image records using the galah-python package, download a sample of images, run SpeciesNet to detect and classify the animals in each image, and visualise the predictions.
Other file formats of this notebook
Notebook
Files
Open in Colab / ARDC Jupyter
DOI
SpeciesNet
Introduction
Wildlife monitoring at scale is one of ecology’s most data-intensive challenges. Camera traps deployed across remote landscapes can accumulate thousands of images in a single survey season — far more than any team can manually review in a reasonable time. Automatically identifying the species in each photograph would free researchers to focus on analysis rather than image sorting, but doing this accurately requires a model trained on a large and diverse set of wildlife images.
This notebook shows how to combine two open tools to tackle this problem: the Atlas of Living Australia (ALA) — Australia’s national biodiversity data platform — and SpeciesNet, a deep learning model developed by Google specifically for wildlife image classification. We query ALA for camera-trap images of target species, download a sample, run SpeciesNet to automatically identify animals in each image, and then visualise the results.
Atlas of Living Australia (ALA)
The Atlas of Living Australia (ALA) is Australia’s national biodiversity data infrastructure, funded by the Australian Government through the National Collaborative Research Infrastructure Strategy (NCRIS). It provides free, online access to hundreds of millions of occurrence records aggregated from museums, herbaria, citizen-science platforms (e.g. iNaturalist), government surveys, and camera-trap deployments. Many records include field photographs — including camera-trap imagery — which makes ALA a rich source of labelled images for testing wildlife-classification models such as SpeciesNet.
galah is the official data-access package built by the ALA, available for both R and Python. The Python version, galah-python, lets you query and download ALA’s occurrence records, media, and taxonomic information programmatically. In this notebook we use galah to count records, retrieve camera-trap image metadata, and filter to the datasets and species of interest.
Downloading data through galah requires a registered ALA email address (free to obtain at ala.org.au). For more information, see the galah-python documentation.
Wildlife Observatory of Australia (WildObs)
The Wildlife Observatory of Australia (WildObs) is Australia’s national platform for processing and sharing wildlife camera-trap data. WildObs streamlines biodiversity monitoring by breaking down data silos and providing researchers and policymakers with consistent, high-quality data. Raw camera-trap images are processed using artificial intelligence — including MegaDetector, SpeciesNet, and bespoke regional computer-vision models — and published using the CamtrapDP data standard. The WildObs network is supported by Australia’s National Research Infrastructure: TERN supports the field observatory and standard protocols, ALA hosts the tagged image repository, and the ARDC Planet Research Data Commons (through QCIF) is building the data management and access platform. The camera-trap images used in this notebook are drawn from datasets published to ALA through this WildObs pipeline.
By the end of this notebook you will be able to: - retrieve camera-trap image metadata from ALA using galah-python - run automated species identification with SpeciesNet - interpret the model’s predictions and understand when to trust them
Workflow Overview
Notebook section
What happens
1 Setup
Install and import the required Python packages
2 Download WildObs data from ALA
Query ALA with galah-python, filter to WildObs camera-trap datasets, and download a sample of images
3 Run SpeciesNet
Load SpeciesNet and run detection, classification, and geofenced predictions on the images
4 Summary
Review results, limitations, and next steps
Note: Run cells from top to bottom in order. All required packages are installed in the Setup cell below.
1 Setup
Install the required Python packages. Run this cell once at the start of every session — packages are not persisted between sessions.
Package
Purpose
pandas
Organising, cleaning, and analysing tabular data
galah-python
Querying and downloading biodiversity records from ALA
requests
Retrieving image data from URLs
speciesnet
Running the wildlife detection and classification model
Pillow
Opening, resizing, and annotating images
Tip: If you see an error after installing (e.g. a version conflict), please re-run from this cell.
# Install required packages. Harmless pip DEPRECATION / [notice] build messages# are filtered out for a cleaner log; genuine errors still show.! pip install pandas galah-python requests speciesnet Pillow --quiet 2>&1| grep -viE "DEPRECATION|^\[notice\]|new release of pip|To update, run"|| true
Import standard libraries and define a utility function used later to manage the image download folder. SpeciesNet-specific imports are deferred to the Run SpeciesNet section so that this cell runs quickly even before the model weights are downloaded.
import osimport timeimport mathimport pickleimport shutilfrom pathlib import Pathimport pandas as pdimport requestsimport matplotlib.pyplot as pltfrom PIL import Imagedef empty_directory(directory: Path):"""Delete all files and sub-directories inside *directory* without removing the directory itself. Creates it if it does not yet exist."""ifnot directory.exists(): directory.mkdir(parents=True)returnfor item in directory.iterdir():if item.is_file() or item.is_symlink(): item.unlink()elif item.is_dir(): shutil.rmtree(item)
2 Download WildObs data from ALA
The Atlas of Living Australia (ALA) is Australia’s national biodiversity data infrastructure, aggregating over 130 million occurrence records from museums, herbaria, citizen science platforms (e.g. iNaturalist), and government surveys. Many of these records include photographs taken in the field — including images from camera-trap deployments — which is exactly what we need to test SpeciesNet.
We use the galah-python package to query ALA’s API and retrieve image metadata for our target species.
2.1 Configure galah
galah requires a registered ALA email address to submit download requests. Registration is free at ala.org.au. Replace the placeholder email below with your own. You can also change the taxa list to any species you are interested in — use the scientific (Latin) name.
import osimport galah# Replace with your own ALA-registered email, or set the ALA_EMAIL environment# variable before running. Registration is free at https://www.ala.org.au/galah.galah_config( atlas="Australia", email=os.environ.get("ALA_EMAIL", "your-email@example.com"),)print("galah config applied.")# Species to query — use scientific (Latin) names# You can add or remove species from this list, keep it under 3 for ease of processing at this stage# taxa = ['Felis catus', 'Sus scrofa', 'Canis dingo']taxa = ['Felis catus', 'Sus scrofa']print(f"\nQuerying {len(taxa)} species:")for i, sp inenumerate(taxa, 1):print(f" {i}. {sp}")
Before fetching full metadata, it is worth checking how many image records exist for your taxa and geographic filter. This is a fast call that does not trigger a download — it gives you a sense of dataset size so you can decide whether to narrow your filters further. The cell below returns a count broken down by media type.
Now we retrieve the full image metadata using galah.atlas_media(). This returns a table of records that includes the image URL, species name, data-resource name, and observation coordinates for every matching record. Crucially, setting collect=False means we are only downloading the metadata — not the image files themselves, which we will do in a later step.
This call submits a download job to ALA’s servers and polls until it is ready, which can take anywhere from under a minute to well over ten minutes depending on how many records match your filters. Adding year and month filters (as below) significantly reduces the wait time — as a reference point, filtering to a single Australian state, year, and month typically completes in two to three minutes.
Note for Google Colab users: ALA’s download service occasionally blocks requests from cloud provider IP addresses, returning a JSONDecodeError or 403 Forbidden. If you encounter this, try re-running the cell. If it persists, run this cell locally, save the result with media_df.to_csv("media_df.csv"), then upload that file to Colab and load it with media_df = pd.read_csv("media_df.csv").
media_df = galah.atlas_media( taxa=taxa, filters=["stateProvince=Queensland", "year=2023", "month=1"], # narrow filters = faster multimedia="images", collect=False, # metadata only — image files are NOT downloaded here)print(f"Retrieved {len(media_df)} image records.")media_df.head()
Retrieved 984 image records.
decimalLatitude
decimalLongitude
eventDate
scientificName
recordID
dataResourceName
occurrenceStatus
multimedia
images
videos
sounds
creator
license
mimetype
width
height
imageUrl
0
-25.646330
152.648088
2023-01-20T11:03:00Z
Sus scrofa
c1927840-53ff-4669-b205-ba3236411881
iNaturalist Australia
PRESENT
Image
26ccaec7-1f97-457d-aaf0-9a0b04c66629
NaN
NaN
Scott W. Gavins
http://creativecommons.org/licenses/by-nc/4.0/
image/jpeg
2048
1536
https://images.ala.org.au/store/9/2/6/6/26ccae...
1
-17.289829
145.795254
2023-01-27T00:00:00Z
Sus scrofa
e0dbce7e-ccf0-45ea-8293-0ab9ac4155c0
Camera trap surveys in Queensland's Wet Tropic...
PRESENT
Image
b6d29931-1065-4e18-839c-bbbcee13d632
NaN
NaN
None
None
image/jpeg
5376
3024
https://images.ala.org.au/store/2/3/6/d/b6d299...
2
-17.289829
145.795254
2023-01-01T00:00:00Z
Sus scrofa
d21a13ec-a189-44fd-aa5d-2267ce9ec962
Camera trap surveys in Queensland's Wet Tropic...
PRESENT
Image
4496b905-1199-40d0-8817-8170487d2693
NaN
NaN
None
None
image/jpeg
5376
3024
https://images.ala.org.au/store/3/9/6/2/4496b9...
3
-17.289829
145.795254
2023-01-06T00:00:00Z
Sus scrofa
4d222fde-90ff-4387-b38d-ce696dc6cf46
Camera trap surveys in Queensland's Wet Tropic...
PRESENT
Image
2e14e786-8fbe-43e4-a4e3-088dc16af8f1
NaN
NaN
None
None
image/jpeg
5376
3024
https://images.ala.org.au/store/1/f/8/f/2e14e7...
4
-17.289829
145.795254
2023-01-27T00:00:00Z
Sus scrofa
a58133e1-e5c3-47a6-8eac-5542821ef28f
Camera trap surveys in Queensland's Wet Tropic...
PRESENT
Image
7d94d39b-0f7c-4c62-8f65-33cdc3d08831
NaN
NaN
None
None
image/jpeg
5376
3024
https://images.ala.org.au/store/1/3/8/8/7d94d3...
2.4 Inspect the data
Before filtering, take a look at what data resources and species came back. ALA aggregates records from many different providers, and the quality and type of images varies considerably between them. Knowing which datasets are present lets you make an informed decision about which ones to keep.
print("Columns:", media_df.columns.tolist())print("\nUnique data resources:")for dr in media_df['dataResourceName'].unique():print(" •", dr)print("\nUnique scientific names:")for sp in media_df['scientificName'].unique():print(" •", sp)
Columns: ['decimalLatitude', 'decimalLongitude', 'eventDate', 'scientificName', 'recordID', 'dataResourceName', 'occurrenceStatus', 'multimedia', 'images', 'videos', 'sounds', 'creator', 'license', 'mimetype', 'width', 'height', 'imageUrl']
Unique data resources:
• iNaturalist Australia
• Camera trap surveys in Queensland's Wet Tropics 2022-2023
Unique scientific names:
• Sus scrofa
• Felis catus
2.5 Filter to camera-trap datasets
Not all ALA image records are equally suitable for SpeciesNet. The model was trained on camera-trap images and performs best on that type of photograph. Field photos, museum specimens, or heavily cropped images can produce unreliable predictions. Here we restrict the data to records from the Wildlife Observatories of Australia (WildObs) dataset, which contains high-quality, labelled camera-trap images, and keep only the target species of interest.
Update values_to_keep with the dataset names you saw in the previous cell, and adjust target_species to match your taxa of interest.
values_to_keep = ["Camera trap surveys in Queensland's Wet Tropics 2022-2023","Monitoring many landscapes in VIC-NSW border to assess impacts of 2019-20 gigafires on wildlife diel activity","Wombat burrows are hotspots for small vertebrates in a landscape subject to gigafire",]target_species = ['Felis catus', 'Sus scrofa', 'Canis dingo']filtered_df = media_df[ media_df["dataResourceName"].isin(values_to_keep) & media_df["scientificName"].isin(target_species)]print(f"Records after filtering: {len(filtered_df)}")filtered_df.head()
Records after filtering: 981
decimalLatitude
decimalLongitude
eventDate
scientificName
recordID
dataResourceName
occurrenceStatus
multimedia
images
videos
sounds
creator
license
mimetype
width
height
imageUrl
1
-17.289829
145.795254
2023-01-27T00:00:00Z
Sus scrofa
e0dbce7e-ccf0-45ea-8293-0ab9ac4155c0
Camera trap surveys in Queensland's Wet Tropic...
PRESENT
Image
b6d29931-1065-4e18-839c-bbbcee13d632
NaN
NaN
None
None
image/jpeg
5376
3024
https://images.ala.org.au/store/2/3/6/d/b6d299...
2
-17.289829
145.795254
2023-01-01T00:00:00Z
Sus scrofa
d21a13ec-a189-44fd-aa5d-2267ce9ec962
Camera trap surveys in Queensland's Wet Tropic...
PRESENT
Image
4496b905-1199-40d0-8817-8170487d2693
NaN
NaN
None
None
image/jpeg
5376
3024
https://images.ala.org.au/store/3/9/6/2/4496b9...
3
-17.289829
145.795254
2023-01-06T00:00:00Z
Sus scrofa
4d222fde-90ff-4387-b38d-ce696dc6cf46
Camera trap surveys in Queensland's Wet Tropic...
PRESENT
Image
2e14e786-8fbe-43e4-a4e3-088dc16af8f1
NaN
NaN
None
None
image/jpeg
5376
3024
https://images.ala.org.au/store/1/f/8/f/2e14e7...
4
-17.289829
145.795254
2023-01-27T00:00:00Z
Sus scrofa
a58133e1-e5c3-47a6-8eac-5542821ef28f
Camera trap surveys in Queensland's Wet Tropic...
PRESENT
Image
7d94d39b-0f7c-4c62-8f65-33cdc3d08831
NaN
NaN
None
None
image/jpeg
5376
3024
https://images.ala.org.au/store/1/3/8/8/7d94d3...
5
-17.289829
145.795254
2023-01-07T00:00:00Z
Sus scrofa
8ac2d718-dbf8-49d9-b067-08584289ea2a
Camera trap surveys in Queensland's Wet Tropic...
PRESENT
Image
97918c43-22e0-458d-bebd-48d3292515a0
NaN
NaN
None
None
image/jpeg
5376
3024
https://images.ala.org.au/store/0/a/5/1/97918c...
2.6 Select a sample
Running SpeciesNet on hundreds of images can take a long time, especially without a GPU. For now, we randomly select a small subset to keep things manageable. Once you are comfortable with the workflow, you can increase N_IMAGES or remove the sampling step entirely to process the full filtered dataset. Setting random_state=42 ensures the same images are chosen every time you run the cell, which is useful for reproducibility.
N_IMAGES =6# number of images to downloadsubset_df = filtered_df.sample(min(N_IMAGES, len(filtered_df)), random_state=42)print(f"Subset size: {len(subset_df)}")subset_df[["scientificName", "dataResourceName", "imageUrl"]].reset_index(drop=True)
Subset size: 6
scientificName
dataResourceName
imageUrl
0
Felis catus
Camera trap surveys in Queensland's Wet Tropic...
https://images.ala.org.au/store/b/a/e/c/696946...
1
Sus scrofa
Camera trap surveys in Queensland's Wet Tropic...
https://images.ala.org.au/store/5/e/2/5/8b95d6...
2
Sus scrofa
Camera trap surveys in Queensland's Wet Tropic...
https://images.ala.org.au/store/6/7/0/0/7ac65b...
3
Sus scrofa
Camera trap surveys in Queensland's Wet Tropic...
https://images.ala.org.au/store/a/3/3/b/ab0f5c...
4
Sus scrofa
Camera trap surveys in Queensland's Wet Tropic...
https://images.ala.org.au/store/2/a/2/0/3514bf...
5
Felis catus
Camera trap surveys in Queensland's Wet Tropic...
https://images.ala.org.au/store/5/2/2/5/9313e1...
2.7 Download images
Now we download the actual image files from their ALA-hosted URLs and save them to the EC_images folder. The folder is cleared at the start of each run so that images from a previous run do not accumulate. Each file is named after its DataFrame row index (e.g. 144.jpg), making it straightforward to trace a prediction back to the original metadata record.
image_folder = Path("EC_images")empty_directory(image_folder) # clears the folder (or creates it if new)for i, row in subset_df.iterrows(): url = row["imageUrl"] filename = image_folder /f"{i}.jpg"try: r = requests.get(url, timeout=30) r.raise_for_status() filename.write_bytes(r.content)print(f"Downloaded: {filename.name}")exceptExceptionas e:print(f"Failed to download {url}: {e}")print(f"\nDone. {len(list(image_folder.glob('*.jpg')))} images saved to '{image_folder}'.")
Before running the model, display the downloaded images as a grid to confirm they look as expected. This is a good moment to spot any blank frames, setup photos, or corrupted files that might produce unreliable predictions and should be excluded before analysis.
SpeciesNet is a deep learning model developed by Google for automated wildlife identification in camera-trap images. It was trained on over 65 million camera-trap images from the Wildlife Insights platform, making it one of the most extensively trained wildlife classifiers available.
The model works in two stages. First, a detector scans the image and draws a bounding box around any animal it finds, along with a confidence score (0–1) indicating how certain it is that an animal is present. Second, a classifier examines the content inside the bounding box — or the full image if no box was found — and assigns the most likely species label from a vocabulary of over 2,000 labels. These labels span individual species (e.g. Sus scrofa, wild boar), broader taxonomic groups (e.g. "felidae", cat family), and non-animal classes ("blank", "vehicle", "human").
3.1 Geofencing
SpeciesNet includes a geofencing step that filters out predictions for species known not to occur in a given country. For example, if you supply country="AUS", the model will not predict “lion” or “elephant” for an Australian image, even if the raw classifier score for those labels is high. Geofencing is enabled by default and is strongly recommended whenever you are working within a defined geographic region — it meaningfully reduces false positives.
3.2 Load libraries
Import the SpeciesNet library and define a small helper function that prints predictions in a readable format. We also print the available models — DEFAULT_MODEL is the recommended choice for most use cases.
import warningswarnings.filterwarnings('ignore')from IPython.display import display, JSONfrom speciesnet import DEFAULT_MODEL, SUPPORTED_MODELSfrom speciesnet import draw_bboxes, load_rgb_image, SpeciesNetdef print_predictions(predictions_dict: dict) ->None:"""Print a human-readable summary of SpeciesNet predictions."""print("Predictions:")for pred in predictions_dict["predictions"]:print(f" {Path(pred['filepath']).name} => {pred['prediction']}")print("Default model :", DEFAULT_MODEL)print("Supported models:", SUPPORTED_MODELS)
Default model : kaggle:google/speciesnet/pyTorch/v4.0.3a/1
Supported models: ['kaggle:google/speciesnet/pyTorch/v4.0.3a/1', 'kaggle:google/speciesnet/pyTorch/v4.0.3b/1']
3.3 Load the model
The cell below loads SpeciesNet. The first time it runs, it will download the model weights (approximately 1–2 GB), which can take a few minutes. Subsequent runs in the same session use the cached weights instantly.
Tip for Colab users: Colab’s local disk is wiped when the runtime disconnects, so the weights will need to be re-downloaded each session. To avoid this, mount your Google Drive and point the cache there:
from google.colab import drivedrive.mount('/content/drive')model = SpeciesNet(DEFAULT_MODEL, cache_dir="/content/drive/MyDrive/speciesnet_cache")
model = SpeciesNet(DEFAULT_MODEL) # geofencing ON (default)# model = SpeciesNet(DEFAULT_MODEL, geofence=False) # uncomment to disable geofencingprint("Model loaded.")
Model loaded.
3.4 Run predictions
Pass the image folder to model.predict(). SpeciesNet will find every .jpg, .jpeg, and .png file in the folder, run the detector and classifier on each one, and return a dictionary of results — one entry per image.
predictions_dict = model.predict(folders=[image_folder])print_predictions(predictions_dict)# display(JSON(predictions_dict)) # uncomment for the full verbose output
UUID — unique label identifier in the SpeciesNet taxonomy
Class → Species — full taxonomic hierarchy from class down to species epithet
Common name — plain-language species name (last field, easiest to read)
If the model is not confident about a species, it may predict at a higher taxonomic level (e.g. "felidae;;; cat family" with blank genus/species fields).
3.5 Run on specific files
Instead of a whole folder, you can pass a list of specific file paths. This is useful when you want to quickly test the model on a single image or a hand-picked selection without re-processing the entire folder.
image_paths =sorted( p for p in image_folder.iterdir()if p.suffix.lower() in [".jpg", ".jpeg", ".png"])predictions_dict = model.predict(filepaths=[image_paths[0], image_paths[1]])print_predictions(predictions_dict)
Predictions:
140.jpg => d372cda5-a8ca-4b7b-97ed-4e4fab9c9b4b;mammalia;artiodactyla;suidae;sus;scrofa;wild boar
498.jpg => 988b8b2d-6d3b-4c5f-899c-c76b6108b90b;mammalia;artiodactyla;tayassuidae;;;peccary family
3.6 Visualise results
The cell below displays each image with its predictions overlaid. Where the detector found an animal, a red bounding box is drawn around it. The detector’s confidence score appears in the top-left corner of the box (e.g. animal: 0.88), and the classifier’s species label with its confidence score is shown in the bottom-right corner (e.g. domestic cat: 0.93). The exact labels and scores you see will depend on your images and taxa.
Where the detector found nothing — which can happen with blurry, fast-moving, or partially visible animals — the classifier’s prediction is shown as a banner at the bottom of the image. The classifier still runs on the full image in these cases, so a species label is always produced.
from PIL import ImageDraw, ImageFont# Run SpeciesNet on all images in the folderpredictions_dict = model.predict(folders=[image_folder])# Load a font font = ImageFont.load_default(size=14) n_show =min(3, len(predictions_dict["predictions"]))for pred_item in predictions_dict["predictions"][:n_show]: fname = Path(pred_item["filepath"]).name pred_text = pred_item.get("prediction", "") detections = pred_item.get("detections", [])# The prediction string is semicolon-separated:# UUID ; class ; order ; family ; genus ; species ; common-name species_name = pred_text.split(";")[-1] if pred_text else"unknown"# Classifier confidence: classifications = {"classes": [...], "scores": [...]}# scores[0] is the top prediction's confidence (0–1) scores = pred_item.get("classifications", {}).get("scores", []) conf = scores[0] if scores elseNone species_label =f"{species_name}: {conf:.2f}"if conf isnotNoneelse species_nameprint(f"File : {fname}")print(f"Prediction: {pred_text}") img = load_rgb_image(pred_item["filepath"]) img.thumbnail(size=(800, 800))if detections:# draw_bboxes draws a red box + "animal: conf" label for each detection.# It returns a NEW annotated image — the return value must be captured. img = draw_bboxes(img, detections)# Overlay the classifier's species label at the bottom-right of each box draw = ImageDraw.Draw(img)for det in detections: xmin, ymin, bw, bh = det["bbox"] x_right =int((xmin + bw) * img.width) y_bottom =int((ymin + bh) * img.height) x1, y1, x2, y2 = draw.textbbox((0, 0), species_label, font=font) x_px = x_right - (x2 - x1) # right-align to box edge y_px = y_bottom - (y2 - y1) # bottom-align to box edge draw.rectangle([x_px -3, y_px -3, x_right +3, y_bottom +3], fill=(0, 0, 0)) draw.text((x_px, y_px), species_label, fill=(255, 255, 0), font=font)print(f" {len(detections)} bounding box(es) drawn.")else:# No detection: the animal was not located above the detector's confidence# threshold (common with motion blur or partially visible subjects).# Draw the classifier result as a banner at the bottom of the image. draw = ImageDraw.Draw(img) w, h = img.size banner =f"Classifier: {species_label} (no detection bbox)" draw.rectangle([0, h -30, w, h], fill=(0, 0, 0)) draw.text((6, h -24), banner, fill=(255, 255, 255), font=font)print(" No bounding boxes — classifier label shown as banner.")print() display(img)
Confidence scores represent how certain the model is about a prediction, on a scale of 0 to 1. A score of 0.93 means the model assigns 93% of its probability mass to that label. As a rough guide: - > 0.9 — high confidence; likely a reliable prediction - 0.6–0.9 — moderate confidence; worth a visual check - < 0.6 — low confidence; treat with caution
When the model is uncertain at the species level, it often predicts at a higher taxonomic rank instead — for example, predicting the order "cetartiodactyla" rather than Sus scrofa. This is a deliberate and sensible fallback: a broad but correct label is more useful than a confident but wrong species name.
3.7.1 Understanding the range of predictions you may see
Even when all images in a batch share the same ALA species label, SpeciesNet may return a spread of different predictions. For example, a set of Sus scrofa (wild boar) images might yield species-level labels such as wild boar, domestic cattle, or bearded pig, alongside higher-rank predictions like Cetartiodactyla (order) or mammal (class). Your results will vary depending on which species you query and the quality of the images retrieved.
Species-level mismatches like these often reflect genuine visual ambiguity rather than random error. Animals within the same family or order — for instance, wild boar, bearded pig (Sus barbatus), and domestic cattle (Bos taurus) — all belong to Cetartiodactyla (even-toed ungulates) and can look strikingly similar in a camera-trap photograph, especially when the image is blurry, partially obstructed, or taken in low light at night.
Higher-rank predictions — such as an order or class label instead of a species name — indicate that the model could not confidently distinguish between species in that group and chose to abstain from a species-level call. This is a deliberate design choice: returning a broad but correct label (e.g. "cetartiodactyla") is more useful than a confident but wrong species name.
Taken together, this range of outputs illustrates an important point: SpeciesNet’s predictions should be treated as a probabilistic first-pass label, not a definitive identification. Predictions below ~0.9 confidence, or those returned at a higher taxonomic level than species, are best treated as candidates for expert validation or a second-pass review.
3.7.2 Known limitations
Image quality matters. SpeciesNet was trained on camera-trap images and performs best on those. Images taken at night, in heavy rain, or with severe motion blur will produce lower-confidence and less reliable predictions.
The detector and classifier are independent. The detector can miss an animal and still have the classifier produce a correct species label — and vice versa. Always look at both the bounding box and the prediction string together.
Closely related species are harder to distinguish. Species within the same family or order (e.g. Sus scrofa vs Sus barbatus) are more likely to be confused with each other than with distantly related animals, because they share similar body shapes and colouring.
“Blank” predictions are useful. If SpeciesNet predicts blank, neither the detector nor the classifier found strong evidence of an animal — valuable for automatically filtering empty frames from large datasets.
Geofencing can suppress correct predictions. If a species is present but outside its expected geographic range (e.g. an escaped or introduced animal), geofencing may filter it out. Disable it with geofence=False in those cases.
3.8 Run the classifier only
SpeciesNet’s detector and classifier can be called independently. model.classify() runs only the classifier on the full image, skipping detection entirely. This is faster and still produces a species label, but gives no information about where the animal is located in the frame. It is useful for large-scale screening where location is not needed.
# Classifier only — no bounding boxes, just species labels for the full imagepredictions_dict = model.classify(filepaths=[image_paths[0], image_paths[1]])print("Classifier-only results (top prediction per image):")for pred in predictions_dict["predictions"]: fname = Path(pred["filepath"]).name label = pred.get("prediction", "").split(";")[-1] scores = pred.get("classifications", {}).get("scores", []) conf =f"{scores[0]:.2f}"if scores else"n/a"print(f" {fname} => {label} (confidence: {conf})")# Uncomment to see the full raw output including all candidate classifications:# display(JSON(predictions_dict))
model.detect() runs only the detector. It returns bounding boxes and confidence scores for any animals found, but does not identify the species — each detection is simply labelled "animal". This is useful for quickly checking occupancy (is an animal present?) or filtering blank frames, without the computational overhead of species classification.
# Detector only — returns bounding boxes with confidence scores, no species labelspredictions_dict = model.detect(filepaths=[image_paths[0], image_paths[1]])print("Detector-only results (bounding boxes per image):")for pred in predictions_dict["predictions"]: fname = Path(pred["filepath"]).name detections = pred.get("detections", [])if detections:for d in detections:print(f" {fname} => {d['label']} conf: {d['conf']:.2f} bbox: {d['bbox']}")else:print(f" {fname} => no detections above threshold")# Uncomment to see the full raw output:# display(JSON(predictions_dict))
To explicitly activate geofencing with a country code, pass an instances_dict to model.predict(). Each entry in the "instances" list is a dict with a "filepath" and a "country" key. SpeciesNet uses ISO 3166-1 alpha-3 three-letter codes (case-insensitive):
Country
Code
Country
Code
Australia
AUS
South Africa
ZAF
New Zealand
NZL
Kenya
KEN
United States
USA
India
IND
Canada
CAN
Brazil
BRA
United Kingdom
GBR
Indonesia
IDN
Supplying a country code is strongly recommended when working with a known study region — it meaningfully reduces false positives for species that could not plausibly occur there.
When to disable geofencing: Useful if you are working with images from multiple countries in a single batch, or if you want to inspect the model’s raw confidence scores before geographic filtering.
4 Summary
This notebook has shown how to combine two open tools — ALA’s biodiversity data platform and Google’s SpeciesNet model — to automatically identify animal species in camera-trap images. Starting from a simple species query, we retrieved image records from ALA, downloaded a sample, and ran SpeciesNet to produce species labels and bounding boxes for each photograph.
Automated image classification won’t replace expert review for all use cases, but it can dramatically reduce the manual effort involved in processing large camera-trap datasets — particularly for filtering blank frames, flagging images likely to contain a species of interest, or generating a first-pass label for subsequent expert validation.
To take this further, try: - replacing the taxa or geographic filter to work with a different study system - increasing N_IMAGES to run the model on your full filtered dataset - exporting predictions to a CSV with pd.DataFrame(predictions_dict["predictions"]).to_csv("results.csv") - joining predictions back to subset_df on the image filename to add location, date, and data-resource context
Atlas of Living Australia (2026). Occurrence and multimedia records. Atlas of Living Australia. Available at: https://www.ala.org.au/ (Accessed: 16 July 2026).
Westgate, M., Kellie, D., Stevenson, M., & Newman, P. galah: Biodiversity Data from the Atlas of Living Australia and the GBIF Node Network. Python package. Documentation: https://galah.ala.org.au/Python/
Google LLC (2024). SpeciesNet: Deep learning models for identifying animals in camera-trap imagery (cameratrapai). GitHub repository (Apache License 2.0). https://github.com/google/cameratrapai
How to cite this Notebook
Sharma, R., & Zhao, X. Y. (2026). Automated wildlife identification from ALA camera-trap images using SpeciesNet (Version 1.0) [Computational notebook]. Zenodo. https://doi.org/10.5281/zenodo.21522834
Copyright information from SpeciesNet
Copyright 2024 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
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.
footer
EcoCommons received investment (https://doi.org/10.3565/chbq-mr75) from the Australian Research Data Commons (ARDC). The ARDC is enabled by the National Collaborative Research Infrastructure Strategy (NCRIS).
Our Partner
How to Cite EcoCommons
If you use EcoCommons in your research, please cite the platform as follows: