Skip to content

Commit f0c68f1

Browse files
authored
Merge pull request #3149 from adafruit/pi_halloween
adding pi halloween costume detector code
2 parents 7c4f689 + c1b98be commit f0c68f1

File tree

1 file changed

+236
-0
lines changed
  • Raspberry_Pi_Halloween_Costume_Detector

1 file changed

+236
-0
lines changed
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
'''Raspberry Pi Halloween Costume Detector with OpenCV and Claude Vision API'''
5+
6+
#!/usr/bin/python3
7+
import os
8+
import subprocess
9+
import time
10+
import base64
11+
import wave
12+
import cv2
13+
from picamera2 import Picamera2
14+
import anthropic
15+
from piper import PiperVoice, SynthesisConfig
16+
import board
17+
import digitalio
18+
19+
ANTHROPIC_API_KEY = "your-api-key-here"
20+
21+
try:
22+
username = os.getlogin()
23+
print(f"The current user is: {username}")
24+
except OSError:
25+
print("Could not determine the login name.")
26+
print("Consider checking environment variables like USER or LOGNAME.")
27+
# Fallback to environment variables if os.getlogin() fails
28+
username = os.environ.get('USER') or os.environ.get('LOGNAME')
29+
if username:
30+
print(f"The user from environment variable is: {username}")
31+
else:
32+
print("User information not found in environment variables either.")
33+
34+
# Initialize LED
35+
led = digitalio.DigitalInOut(board.D5)
36+
led.direction = digitalio.Direction.OUTPUT
37+
led.value = False # Start with LED off
38+
39+
# Initialize detectors
40+
upperbody_detector = cv2.CascadeClassifier(
41+
"/usr/share/opencv4/haarcascades/haarcascade_upperbody.xml")
42+
fgbg = cv2.createBackgroundSubtractorMOG2(detectShadows=False)
43+
44+
# Initialize camera
45+
cv2.startWindowThread()
46+
picam2 = Picamera2()
47+
picam2.configure(picam2.create_preview_configuration(
48+
main={"format": 'XRGB8888', "size": (640, 480)}))
49+
picam2.start()
50+
51+
# Initialize Piper voice
52+
voice = PiperVoice.load("/home/pi/en_US-joe-medium.onnx")
53+
syn_config = SynthesisConfig(
54+
volume=1.0,
55+
length_scale=1.5,
56+
noise_scale=1.2,
57+
noise_w_scale=1.5,
58+
normalize_audio=False,
59+
)
60+
61+
# Check for and create audio files if they don't exist
62+
def create_audio_file_if_needed(filename, text):
63+
"""Create audio file if it doesn't exist"""
64+
if not os.path.exists(filename):
65+
print(f"Creating {filename}...")
66+
with wave.open(filename, "wb") as wav:
67+
voice.synthesize_wav(text, wav, syn_config=syn_config)
68+
print(f"{filename} created!")
69+
else:
70+
print(f"{filename} already exists.")
71+
72+
# Create the hello and halloween audio files
73+
create_audio_file_if_needed("hello.wav", "Hello? Who goes there?")
74+
create_audio_file_if_needed("halloween.wav", "Happy Halloween!")
75+
76+
# Initialize Anthropic client
77+
client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
78+
# set API key via the terminal as an environmental variable:
79+
# export ANTHROPIC_API_KEY="your-api-key-here"
80+
81+
# Detection parameters
82+
MOTION_THRESHOLD = 100000
83+
COOLDOWN_SECONDS = 15
84+
last_capture_time = 0
85+
capture_delay = 5
86+
motion_frame_count = 0
87+
MOTION_FRAMES_REQUIRED = 5
88+
89+
def encode_image(image_path):
90+
"""Encode image to base64 for Claude API"""
91+
with open(image_path, "rb") as image_file:
92+
return base64.standard_b64encode(image_file.read()).decode("utf-8")
93+
94+
def get_costume_joke(image_path):
95+
"""Send image to Claude and get dad joke about costume"""
96+
print("Analyzing costume with Claude...")
97+
98+
# Encode the image
99+
image_data = encode_image(image_path)
100+
101+
# Send to Claude API
102+
message = client.messages.create(
103+
model="claude-sonnet-4-5-20250929",
104+
max_tokens=250,
105+
messages=[
106+
{
107+
"role": "user",
108+
"content": [
109+
{
110+
"type": "image",
111+
"source": {
112+
"type": "base64",
113+
"media_type": "image/jpeg",
114+
"data": image_data,
115+
},
116+
},
117+
{
118+
"type": "text",
119+
"text": """Look at this image.
120+
Your response must follow these rules exactly:
121+
122+
1. If you see a person in a Halloween costume, respond with ONLY a single-sentence cute,
123+
family-friendly, encouraging, dad joke about the costume. Nothing else.
124+
125+
2. If you do NOT see a clear Halloween costume (empty room, unclear image, person in regular clothes, etc.),
126+
respond with ONLY the character: 0
127+
128+
Examples of good responses:
129+
- "Looks like that ghost costume is really boo-tiful!"
130+
- "0"
131+
- "I guess you could say that vampire costume really sucks... in a good way!"
132+
133+
Do not add any explanations, commentary, or descriptions. Just the joke or just 0.
134+
"""
135+
}
136+
],
137+
}
138+
],
139+
)
140+
141+
# Extract the joke from the response
142+
joke = message.content[0].text
143+
print(f"Claude's joke: {joke}")
144+
return joke
145+
# pylint: disable=subprocess-run-check, broad-except
146+
def play_audio_file(filename):
147+
"""Play a pre-existing audio file"""
148+
print(f"Playing {filename}...")
149+
subprocess.run(["su", username, "-c", f"aplay {filename}"])
150+
151+
def speak_joke(joke_text):
152+
"""Convert joke to speech and play it"""
153+
print("Generating speech...")
154+
155+
wav_file = "joke.wav"
156+
157+
# Generate audio with Piper
158+
with wave.open(wav_file, "wb") as wav:
159+
voice.synthesize_wav(joke_text, wav, syn_config=syn_config)
160+
161+
print("Playing audio...")
162+
# Play the audio file (using aplay for Raspberry Pi)
163+
subprocess.run(["su", username, "-c", f"aplay {wav_file}"])
164+
165+
# Optional: clean up the audio file after playing
166+
# os.remove(wav_file)
167+
168+
# Main loop
169+
while True:
170+
im = picam2.capture_array()
171+
grey = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
172+
173+
# Check for motion
174+
fgmask = fgbg.apply(im)
175+
motion_amount = cv2.countNonZero(fgmask)
176+
motion_detected = motion_amount > MOTION_THRESHOLD
177+
178+
if motion_detected:
179+
motion_frame_count += 1
180+
else:
181+
motion_frame_count = 0
182+
183+
if motion_frame_count >= MOTION_FRAMES_REQUIRED:
184+
# Detect upperbody
185+
bodies = upperbody_detector.detectMultiScale(grey, 1.1, 3)
186+
person_detected = len(bodies) > 0
187+
188+
# Draw rectangles
189+
for (x, y, w, h) in bodies:
190+
cv2.rectangle(im, (x, y), (x + w, y + h), (0, 255, 0), 2)
191+
192+
# Process if person detected and cooldown passed
193+
current_time = time.time()
194+
if person_detected and (current_time - last_capture_time) > COOLDOWN_SECONDS:
195+
print("Person detected!")
196+
197+
# Turn on LED and play hello message
198+
led.value = True
199+
play_audio_file("hello.wav")
200+
201+
im = picam2.capture_array()
202+
# Capture image
203+
timestamp = time.strftime("%Y%m%d-%H%M%S")
204+
file = f"costume_{timestamp}.jpg"
205+
cv2.imwrite(file, im)
206+
print(f"\nPicture saved: {file}")
207+
208+
try:
209+
# Get joke from Claude
210+
the_joke = get_costume_joke(file)
211+
212+
# If Claude returns 0, use the halloween fallback
213+
if the_joke.strip() == "0":
214+
print("No costume detected, playing halloween.wav")
215+
play_audio_file("halloween.wav")
216+
else:
217+
# Speak the joke
218+
speak_joke(the_joke)
219+
220+
except Exception as e:
221+
print(f"Error: {e}")
222+
# Fallback to halloween.wav if something goes wrong
223+
play_audio_file("halloween.wav")
224+
225+
# Turn off LED after processing
226+
led.value = False
227+
228+
last_capture_time = current_time
229+
motion_frame_count = 0
230+
231+
# Show motion amount
232+
cv2.putText(im, f"Motion: {motion_amount}", (10, 30),
233+
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
234+
235+
cv2.imshow("Camera", im)
236+
cv2.waitKey(1)

0 commit comments

Comments
 (0)