Close

May 16th v0.2 code

A project log for whosthatbird

Pi-based platform to photograph and record birds, and classify them at both species and individual level

neil-k-sheridanNeil K. Sheridan 05/16/2025 at 18:410 Comments

Today I got the v0.2 code completed. So we take a photo on PIR=HIGH, run inference using TFLITE, and then annotate it with the top class, using Pillow! We also take a video, but we are not doing any video classification or object detection at the moment, and it seems a cheat to label it with the species name from the still image!

What's to-do next per software?

  1.  We can run a web server, and SQL database on the Pi, so we can store all the images with their associated species classifications. Then we can show a list with images of all the species identified. Add a thumbs up/down to see if user agrees with the CNN classification. Side bar for total species seen, how many in last hr/day. Also stick the videos on the web server too (although remain unclassified)
  2. Update so we can add sound to the video recordings from the USB mic
  3. Make some kind of Android app that pulls data from the SQL database, so images and videos can be viewed easily

import RPi.GPIO as GPIO
import time
from PIL import Image, ImageDraw, ImageFont
import numpy as np
import tflite_runtime.interpreter as tflite
import subprocess
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BOARD)
GPIO.setup(7, GPIO.IN)         #Read output from PIR motion sensor

# Load class labels
class_labels = ["Blackbird", "Bluetit", "Carrion Crow", "Chaffinch", "Coal Tit", "Collared Dove",
"Dunnock", "Feral Pigeon", "Goldfinch", "Great Tit", "Greenfinch", "House Sparrow", "Jackdaw",
"Long Tailed Tit", "Magpie", "Robin", "Song Thrush", "Starling", "Wood Pigeon", "Wren"]  # class labels here
# Load the TFLite model
interpreter = tflite.Interpreter(model_path="birds4.tflite")
interpreter.allocate_tensors()

# Get input & output tensor details
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

IMG_SIZE = (224, 224)  # make sure these are made to correct size for model!

# Load and preprocess image
def preprocess_image(image_path):
    image = Image.open(image_path)
    image = image.resize(IMG_SIZE)
    img_array = np.array(image, dtype=np.float32)
    img_array = img_array / 255.0  # Normalize if needed
    img_array = np.expand_dims(img_array, axis=0)  # Add batch dimension
    return img_array


# Run inference
def predict(image_path):
    image = preprocess_image(image_path)

    # Set input tensor
    interpreter.set_tensor(input_details[0]['index'], image)

    # Run inference
    interpreter.invoke()

    predictions = interpreter.get_tensor(output_details[0]['index'])[0]

    # Get predictions
    predicted_class_idx = np.argmax(predictions)
    predicted_class_label = class_labels[predicted_class_idx]
    confidence_score = float(predictions[predicted_class_idx])
        
    # Return predicted class & probabilities
    return{
    "predicted_class": predicted_class_label,
    "confidence": round(confidence_score * 100,2)
    }

def annotate_photo(image_path, species):
    image = Image.open(image_path)
    draw = ImageDraw.Draw(image)

    font_size = int(image.height *0.06)
    
    try:
        font = ImageFont.truetype("usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", font_size)
    except:
        font = ImageFont.load_default()
    
    #add text
    text = f"Detected: {species}"
    
    text_width, text_height = draw.textsize(text, font=font)
    padding = 10
    box_x = 10
    box_y = image.height - text_height - 100
    box_width = text_width +2 * padding
    box_height = text_height +2 * padding

    draw.rectangle(
        [box_x, box_y, box_x + box_width, box_y + box_height],
        fill="black"
    )

    draw.text(
        (box_x + padding, box_y + padding),
        text,
        font=font,
        fill="white"
    )

    annotated_path = image_path.replace(".jpg", "_labeled.jpg")
    image.save(annotated_path)
    
    print(f"Annotateed image saved as {annotated_path}")


def take_photo():
    timestamp = time.strftime("%Y%m%d-%H%M%S")
    image_path = f"/home/whosthatbird/photos/photo_{timestamp}.jpg"
    subprocess.run([
        "libcamera-still",
        "-o", image_path,
        "-t", "2000"
    ])
    return image_path

def record_video():
    timestamp = time.strftime("%Y%m%d-%H%M%S")
    filename = f"/home/whosthatbird/Videos/video_{timestamp}.h264"
    
    subprocess.run([
        "libcamera-vid",
        "-o", filename,
        "-t", "10000"
    ])
    


while True:
    i=GPIO.input(7)
    if i==0:                 #When output from motion sensor is LOW
        print("No birds")
        time.sleep(0.1)
    elif i==1:               #When output from motion sensor is HIGH
        print("bird detected")
        image_path = take_photo()
        print(image_path)
        species = predict(image_path)
        print(species)
        annotated_img = annotate_photo(image_path, species)
        
        #record_video()
        time.sleep(2)

time.sleep(0.1)

Discussions