Eric Bergman-Terrell's Blog

Camera Trap and Animal Detection Program
August 19, 2026

Camera Traps

A "camera trap" is just a camera that automatically captures images of animals. While camera traps are a wonderful, non-invasive way to photograph wildlife, they tend to capture hundreds or thousands of images that don't contain any animals. Often the effects of wind will be misconstrued as animals moving in front of the lens.

Manually filtering through so many images can be a total waste of time!

MegaDetector for the Win!

Microsoft's MegaDetector is an AI model that identifies animals, people, and vehicles in camera trap images. It's perfect for reviewing camera trap images and finding the ones that are probably images of animals.


collared lizard found by MegaDetector

Eastern Collared Lizard (Crotaphytus collaris) a.k.a. "mountain boomer" captured by my camera trap, and found by MegaDetector

My camera trap is just a Raspberry Pi 3B+ with an inexpensive camera, powered by an Anker power pack. It's disguised as a mis-delivered package. The app just snaps a picture every minute. Because the camera is mounted upside-down, it automatically corrects the images with this transform:

transform=Transform(hflip=True, vflip=True)

I previously tried triggering the camera with motion detection hardware. I used an "HC-SR501 PIR Infrared Sensor". This sensor resulted in way too many false positives and false negatives. It had a problem with direct sunlight and electrical interference. Also, because it's triggered by body heat, it didn't work detect the beautiful reptiles that prowl around Southwestern Colorado.

Some Raspberry Pi-based camera traps do the image analysis and classification in the device itself. While that is frankly amazing, I prefer to conserve the camera trap's battery. After my camera trap has amassed a day's worth of photos, I download them to a flash drive, and use my PC to find the animal photos with MegaDetector.


Camera Trap Program for Raspberry Pi 3B+:

import os
import logging
import logging.config
import time
from datetime import datetime
from picamera2 import Picamera2
from libcamera import Transform

logger = None

photo_quality = 90

photo_width = 1920
photo_height = 1080

# Take a photo every photo_interval seconds
photo_interval = 60 * 1

def setup_logging():
    global logger

    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        handlers=[
            logging.FileHandler('log.log'),  # Write logs to a file
            logging.StreamHandler()  # Print logs to the console
        ]
    )

    logger = logging.getLogger(__name__)

def take_photo():
    def get_photo_filename():
        now = datetime.now()
        return f'{now.year:04}{now.month:02}{now.day:02}_{now.hour:02}{now.minute:02}{now.second:02}.jpg'

    folder = "photos"
    photo_file_path = f'{folder}{os.path.sep}{get_photo_filename()}'

    logger.info(f'taking photograph "{photo_file_path}"')

    with Picamera2() as cam:
        try:
            # The transform compensates for the camera being upside-down.
            config = cam.create_still_configuration({"size": (photo_width, photo_height)},
                                                    transform=Transform(hflip=True, vflip=True))

            cam.configure(config)
            cam.options['quality'] = photo_quality

            cam.start()
            time.sleep(2)  # Give the sensor time to adjust to light levels

            cam.capture_file(photo_file_path)

            cam.stop()
            logger.info(f'Photo captured. size: {os.stat(photo_file_path).st_size:,} bytes')
        except Exception as ex:
            logger.info(f'Exception: {ex}')

def main():
    global logger

    setup_logging()

    logger.info(f'photo_interval: {photo_interval}')

    while True:
        try:
            take_photo()
            time.sleep(photo_interval)
        except Exception as ex:
            logger.exception('Exception', ex)

if __name__ == '__main__':
    main()

Here's the script I use to go through a set of photos to find the interesting ones:

Note: MegaDetector does not currently run on the latest version of Python. I use Python 3.12.

Note: I only use MegaDetector to determine if there may be animals in a photo. I do not use it to further classify the animals. I have not found the classification to be very accurate, at least for the denizens of my back yard.


Photo Filtering Program for PC:

import os
import shutil
import time
from pathlib import Path
from PytorchWildlife.models import detection as pw_detection
import logging
import logging.config
from PIL import Image

logger = None

# https://github.com/microsoft/MegaDetector

# Requires python version <= 3.12.

# Models
#
# MDV6-yolov9-c, MDV6-yolov9-e, MDV6-yolov10-c, MDV6-yolov10-e, MDV6-rtdetr-c

model_version = 'MDV6-yolov10-e'

detection_threshold = 0.70

def setup_logging():
    global logger

    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        handlers=[
            logging.FileHandler('log.log'),  # Write logs to a file
            logging.StreamHandler()  # Print logs to the console
        ]
    )

    logger = logging.getLogger(__name__)

def classify_images(photos_path, output_path):
    def animal_detected(labels):
        def is_animal(label):
            values = label.split(' ')

            return values[0] == 'animal' and float(values[1]) >= detection_threshold

        return any(is_animal(label) for label in labels)

    start_time = time.perf_counter()

    model = pw_detection.MegaDetectorV6(None, 'cpu', True, model_version)

    photos_processed = 0

    for photo_path in [f for f in photos_path.rglob("*") if f.is_file()]:
        detection_result = model.single_image_detection(os.path.abspath(photo_path))

        if animal_detected(detection_result["labels"]):
            logger.info(f'ANIMAL: "{photo_path}": {detection_result}\n"{photo_path}"')

            dest_image_file_path = Path(os.path.join(Path(output_path), Path(os.path.basename(photo_path))))
            text_file_path = Path(os.path.join(Path(output_path), Path(os.path.basename(photo_path)))).with_suffix('.txt')

            with open(text_file_path, "w", encoding="utf-8") as text_file:
                text_file.write(f'{detection_result}')

            shutil.copy(photo_path, dest_image_file_path)

            photos_processed += 1

    logger.info(f'Processed {photos_processed} Elapsed Time: {(time.perf_counter() - start_time):,.2f} s')

def invert_images(folder_path):
    print('invert_images')

    for file_path in [f for f in folder_path.rglob("*") if f.is_file()]:
        print(f'{file_path}')

        img = Image.open(file_path)

        flipped = img.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
        flipped.save(file_path)

def main():
    setup_logging()

    logger.info(f'detection_threshold: {detection_threshold:.2f}\n')

    root_folder = Path('C:/Temp/animals/2026-08-08')

    dest_folder = Path(root_folder, 'output')

    logger.info(f'dest_folder: "{dest_folder}"')

    try:
        shutil.rmtree(dest_folder, True)
        Path(dest_folder).mkdir(False, True)

        classify_images(Path(root_folder, 'captured_photos'), dest_folder)
    except Exception as ex:
        logger.exception(f'Fatal Error', ex)

if __name__ == '__main__':
    main()


camera trap implemented on Raspberry Pi 3B+

Raspberry Pi Camera Trap

Keywords: Camera Trap, Raspberry Pi, MegaDetector, AI, Image Recognition, Image Classification, Python, HC-SR501 PIR Infrared Sensor

Reader Comments

Comment on this Blog Post

Recent Posts

TitleDate
Camera Trap and Animal Detection ProgramAugust 19, 2026
Python Tip: Fix Incorrect Orientation of Digital PhotosAugust 8, 2026
EBT Weather is now available for Windows and LinuxMay 30, 2026
Node.js + Express: How to Block Requests by User-Agent HeadersJanuary 7, 2026
Vault 3 is Now Available for Windows on ARM Machines!December 13, 2025
Vault 3: How to Include Outline Text in Exported PhotosOctober 26, 2025
.NET Public-Key (Asymmetric) Cryptography DemoJuly 20, 2025