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.
# SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
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
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!
Page last edited January 22, 2025
Text editor powered by tinymce.