To download the code to your computer, you can click on the Download Project Bundle link below, and uncompress the .zip file. Inside the zip file are a couple of CircuitPython folders. You can use either one. Since the necessary files are already on your Pi, you can ignore the lib folder.

import time
from enum import Enum, auto
import board
from digitalio import DigitalInOut, Direction, Pull
import picamera
import io
from PIL import Image
from lobe import ImageModel
import os
import adafruit_dotstar
from datetime import datetime
import pwmio
from adafruit_motor import servo

LABEL_CAT = "Cat"
LABEL_MULTI_CAT = "Multiple Cats"
LABEL_NOTHING = "No Cats"
SERVO_PIN = board.D12
WARNING_COUNT = 3

pwm = pwmio.PWMOut(SERVO_PIN, duty_cycle=0, frequency=50)
servo = servo.Servo(pwm, min_pulse=400, max_pulse=2400)

# Boiler Plate code for buttons and joystick on the braincraft
BUTTON_PIN = board.D17
JOYDOWN_PIN = board.D27
JOYLEFT_PIN = board.D22
JOYUP_PIN = board.D23
JOYRIGHT_PIN = board.D24
JOYSELECT_PIN = board.D16

buttons = [BUTTON_PIN, JOYUP_PIN, JOYDOWN_PIN,
           JOYLEFT_PIN, JOYRIGHT_PIN, JOYSELECT_PIN]

for i, pin in enumerate(buttons):
    buttons[i] = DigitalInOut(pin)
    buttons[i].direction = Direction.INPUT
    buttons[i].pull = Pull.UP
button, joyup, joydown, joyleft, joyright, joyselect = buttons


class Input(Enum):
    BUTTON = auto()
    UP = auto()
    DOWN = auto()
    LEFT = auto()
    RIGHT = auto()
    SELECT = auto()


def get_inputs():
    inputs = []
    if not button.value:
        inputs.append(Input.BUTTON)
    if not joyup.value:
        inputs.append(Input.UP)
    if not joydown.value:
        inputs.append(Input.DOWN)
    if not joyleft.value:
        inputs.append(Input.LEFT)
    if not joyright.value:
        inputs.append(Input.RIGHT)
    if not joyselect.value:
        inputs.append(Input.SELECT)
    return inputs

DOTSTAR_DATA = board.D5
DOTSTAR_CLOCK = board.D6

RED = (0, 0, 255)
GREEN = (255, 0, 0)
OFF = (0, 0, 0)

dots = adafruit_dotstar.DotStar(DOTSTAR_CLOCK, DOTSTAR_DATA, 3, brightness=0.1)

jingle_count = 0

def color_fill(color, wait):
    dots.fill(color)
    dots.show()
    time.sleep(wait)

def jingle_keys(jingle_hard=False):
    global jingle_count
    jingle_count += 1
    if jingle_count > WARNING_COUNT:
        jingle_hard = True
    delay = 0.5 if jingle_hard else 2
    loop = 5 if jingle_hard else 1
    travel = 180 if jingle_hard else 135
    for _ in range(0, loop):
        for angle in (0, travel):
            servo.angle = angle
            time.sleep(delay)
    servo.angle = None

def main():
    global jingle_count
    model = ImageModel.load('~/model')

    # Check if there is a folder to keep the retraining data, if it there isn't make it
    if (not os.path.exists('./retraining_data')):
        os.mkdir('./retraining_data')

    with picamera.PiCamera(resolution=(224, 224), framerate=30) as camera:
        stream = io.BytesIO()
        camera.start_preview()
        # Camera warm-up time
        time.sleep(2)
        label = ''
        while True:
            stream.seek(0)
            camera.annotate_text = None
            camera.capture(stream, format='jpeg')
            camera.annotate_text = label
            img = Image.open(stream)
            result = model.predict(img)
            label = result.prediction
            confidence = result.labels[0][1]
            camera.annotate_text = label
            print(f'\rLabel: {label} | Confidence: {confidence*100: .2f}%', end='', flush=True)

            # Check if the current label is package and that the label has changed since last tine the code ran
            if label == LABEL_CAT:
                # Make Servo Jingle Keys
                jingle_keys()
            elif label == LABEL_MULTI_CAT:
                jingle_keys(True)
            elif label == LABEL_NOTHING:
                jingle_count = 0

            time.sleep(0.5)

            inputs = get_inputs()
            # Check if the joystick is pushed up
            if (Input.UP in inputs):
                color_fill(GREEN, 0)
                # Check if there is a folder to keep the retraining data, if it there isn't make it
                if (not os.path.exists(f'./retraining_data/{label}')):
                    os.mkdir(f'./retraining_data/{label}')
                # Remove the text annotation
                camera.annotate_text = None

                # File name
                name = datetime.now()
                # Save the current frame
                camera.capture(
                    os.path.join(
                        f'./retraining_data/{label}', 
                        f'{datetime.now().strftime("%Y-%m-%d_%H:%M:%S")}.jpg'
                    )
                )
                
                color_fill(OFF, 0)

            # Check if the joystick is pushed down
            elif (Input.DOWN in inputs or Input.BUTTON in inputs):
                color_fill(RED, 0)
                # Remove the text annotation
                camera.annotate_text = None
                # Save the current frame to the top level retraining directory
                camera.capture(
                    os.path.join(
                        f'./retraining_data',
                        f'{datetime.now().strftime("%Y-%m-%d_%H:%M:%S")}.jpg'
                    )
                )
                color_fill(OFF, 0)


if __name__ == '__main__':
    try:
        print(f"Predictions starting, to stop press \"CTRL+C\"")
        main()
    except KeyboardInterrupt:
        print("")
        print(f"Caught interrupt, exiting...")

You can now make changes to the code such as adjusting your label names or just upload directly to your Pi in the same way you did the model file, except place it in your home directory.

Alternatively, you can use wget to directly download the code to your Pi:

wget https://github.com/adafruit/Adafruit_Learning_System_Guides/raw/main/Lobe_Cat_Detector/lobe-cat-detector.py

Run the Code

To run the code, just run it using Python:

python3 lobe-cat-detector.py

It should display what the camera sees along with a label of what is being detected. If it detects a cat, the servo should start turning. If it still detects a cat after a few seconds, the servo will start moving a bit more. Also, if multiple cats are detected, the servo will move more as soon as it detects them.

When you point it at your cat, it should display "Cat" at the top and start jingling the keys.

Code Walkthrough

Now let's go through the code by each section. Since there's so many imports, we'll skip past that for now and get into what the code is actually doing. First we have the settings you can change. Since changing labels in the model isn't as easy as in the code, you can adjust them here.

SERVO_PIN corresponds to the side of the board that you plugged the servo into. If you plugged it in the other side, you can change this to board.D13.

WARNING_COUNT is the number of attempts the script will make before jingling the keys even harder.

LABEL_CAT = "Cat"
LABEL_MULTI_CAT = "Multiple Cats"
LABEL_NOTHING = "No Cats"
SERVO_PIN = board.D12
WARNING_COUNT = 3

This next section initializes the servo. You can adjust the min_pulse and max_pulse if you used a different servo and want to get more range.

pwm = pwmio.PWMOut(SERVO_PIN, duty_cycle=0, frequency=50)
servo = servo.Servo(pwm, min_pulse=400, max_pulse=2400)

This next section initialize the buttons on the BrainCraft HAT.

# Boiler Plate code for buttons and joystick on the braincraft
BUTTON_PIN = board.D17
JOYDOWN_PIN = board.D27
JOYLEFT_PIN = board.D22
JOYUP_PIN = board.D23
JOYRIGHT_PIN = board.D24
JOYSELECT_PIN = board.D16

buttons = [BUTTON_PIN, JOYUP_PIN, JOYDOWN_PIN,
           JOYLEFT_PIN, JOYRIGHT_PIN, JOYSELECT_PIN]

for i, pin in enumerate(buttons):
    buttons[i] = DigitalInOut(pin)
    buttons[i].direction = Direction.INPUT
    buttons[i].pull = Pull.UP
button, joyup, joydown, joyleft, joyright, joyselect = buttons

These functions are used to get the current state of button pushes. They support pushing multiple buttons at the same time.

class Input(Enum):
    BUTTON = auto()
    UP = auto()
    DOWN = auto()
    LEFT = auto()
    RIGHT = auto()
    SELECT = auto()


def get_inputs():
    inputs = []
    if not button.value:
        inputs.append(Input.BUTTON)
    if not joyup.value:
        inputs.append(Input.UP)
    if not joydown.value:
        inputs.append(Input.DOWN)
    if not joyleft.value:
        inputs.append(Input.LEFT)
    if not joyright.value:
        inputs.append(Input.RIGHT)
    if not joyselect.value:
        inputs.append(Input.SELECT)
    return inputs

This next section initializes the on-board DotStar LEDs and provides and easy function for filling the DotStars for a set amount of time.

DOTSTAR_DATA = board.D5
DOTSTAR_CLOCK = board.D6

RED = (0, 0, 255)
GREEN = (255, 0, 0)
OFF = (0, 0, 0)

dots = adafruit_dotstar.DotStar(DOTSTAR_CLOCK, DOTSTAR_DATA, 3, brightness=0.1)

def color_fill(color, wait):
    dots.fill(color)
    dots.show()
    time.sleep(wait)

This next section contains the jingle_keys function. The jingle_count is used to keep track of the number of warnings that have been given to the cat. If jingle_hard is True, then it will jingle the keys harder.

jingle_count = 0

def jingle_keys(jingle_hard=False):
    global jingle_count
    jingle_count += 1
    if jingle_count > WARNING_COUNT:
        jingle_hard = True
    delay = 0.5 if jingle_hard else 2
    loop = 5 if jingle_hard else 1
    travel = 180 if jingle_hard else 135
    for _ in range(0, loop):
        for angle in (0, travel):
            servo.angle = angle
            time.sleep(delay)
    servo.angle = None

Finally, we get to the main section of the code. The first bit here will load the model you created earlier.  It will then capture the data from the camera and feed it into the model, which will return a label and a confidence value.

global jingle_count
model = ImageModel.load('~/model')

# Check if there is a folder to keep the retraining data, if it there isn't make it
if (not os.path.exists('./retraining_data')):
    os.mkdir('./retraining_data')

with picamera.PiCamera(resolution=(224, 224), framerate=30) as camera:
    stream = io.BytesIO()
    camera.start_preview()
    # Camera warm-up time
    time.sleep(2)
    label = ''
    while True:
        stream.seek(0)
        camera.annotate_text = None
        camera.capture(stream, format='jpeg')
        camera.annotate_text = label
        img = Image.open(stream)
        result = model.predict(img)
        label = result.prediction
        confidence = result.labels[0][1]
        camera.annotate_text = label
        print(f'\rLabel: {label} | Confidence: {confidence*100: .2f}%', end='', flush=True)

This section will compare the detected label and either jingle the keys or reset the jingle count.

# Check if the current label is package and that the label has changed since last tine the code ran
if label == LABEL_CAT:
    # Make Servo Jingle Keys
    jingle_keys()
elif label == LABEL_MULTI_CAT:
    jingle_keys(True)
elif label == LABEL_NOTHING:
    jingle_count = 0

time.sleep(0.5)

This last section will check the inputs and stare the currently detected image in a retraining_data folder for improving the accuracy of the detection.

Press up to save the image in a subfolder with the name of the label that it was detected with.

Press down to save the image inside the root of the retraining_data folder.

inputs = get_inputs()
# Check if the joystick is pushed up
if (Input.UP in inputs):
    color_fill(GREEN, 0)
    # Check if there is a folder to keep the retraining data, if it there isn't make it
    if (not os.path.exists(f'./retraining_data/{label}')):
        os.mkdir(f'./retraining_data/{label}')
    # Remove the text annotation
    camera.annotate_text = None

    # File name
    name = datetime.now()
    # Save the current frame
    camera.capture(
        os.path.join(
            f'./retraining_data/{label}', 
            f'{datetime.now().strftime("%Y-%m-%d_%H:%M:%S")}.jpg'
        )
    )
    
    color_fill(OFF, 0)

# Check if the joystick is pushed down
elif (Input.DOWN in inputs or Input.BUTTON in inputs):
    color_fill(RED, 0)
    # Remove the text annotation
    camera.annotate_text = None
    # Save the current frame to the top level retraining directory
    camera.capture(
        os.path.join(
            f'./retraining_data',
            f'{datetime.now().strftime("%Y-%m-%d_%H:%M:%S")}.jpg'
        )
    )
    color_fill(OFF, 0)

This final bit is just the first thing that is run when the Python script is started. It displays messages and calls the main() function.

if __name__ == '__main__':
    try:
        print(f"Predictions starting, to stop press \"CTRL+C\"")
        main()
    except KeyboardInterrupt:
        print("")
        print(f"Caught interrupt, exiting...")

Have fun using the project!

This guide was first published on Jul 13, 2021. It was last updated on 2021-07-13 22:54:39 -0400.

This page (Python Code) was last updated on Sep 15, 2021.

Text editor powered by tinymce.