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.