├── NV_Cover.png ├── shutter_sound.wav ├── tweet_options.txt ├── README.md ├── .gitignore └── photobooth.py /NV_Cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrleeman/PiBooth/HEAD/NV_Cover.png -------------------------------------------------------------------------------- /shutter_sound.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrleeman/PiBooth/HEAD/shutter_sound.wav -------------------------------------------------------------------------------- /tweet_options.txt: -------------------------------------------------------------------------------- 1 | These people really pushed my buttons 2 | ...and all I got was these photo booth pictures 3 | Awww. 4 | Hey, I just met you, this is crazy 5 | I wasn't lucky, I deserved it 6 | I had fun once, it was horrible 7 | Smile :) 8 | Hi there! 9 | OMG that's so cute 10 | Collect moments, not things 11 | These people... 12 | Best selfie ever 13 | Frankly my dear, I don't Instagram 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PiBooth 2 | 3 | Raspberry Pi photobooth project. This is a simple photobooth built for my 4 | wedding that allowed guests to take a series of photos with on-screen 5 | countdown timer, external lighting control, camera sounds, and optional 6 | automatic tweeting of photos with snarky comments. 7 | 8 | This project was published in the electronics magazine "Nuts and 9 | Volts" as well, so be sure to checkout that article if you are interested in 10 | the step-by-step hardware build. Hookup is straight forward and documented in 11 | the code as well. 12 | 13 | ![Nuts and Volts Cover](NV_Cover.png) 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /photobooth.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pygame 3 | import picamera 4 | import random 5 | import RPi.GPIO as GPIO 6 | from time import sleep, strftime, gmtime 7 | import os 8 | 9 | 10 | def drawText(font, textstr, clear_screen=True, color=(250, 10, 10)): 11 | """ 12 | Draws the given string onto the pygame screen. 13 | 14 | Parameters: 15 | ----------- 16 | font : object 17 | pygame font object 18 | textstr: string 19 | text to be written to the screen 20 | clean_screan : boolean 21 | determines if previously shown text should be cleared 22 | color : tuple 23 | RGB tuple of font color 24 | 25 | Returns: 26 | -------- 27 | None 28 | """ 29 | if clear_screen: 30 | screen.fill(black) # black screen 31 | 32 | # Render font 33 | pltText = font.render(textstr, 1, color) 34 | 35 | # Center text 36 | textpos = pltText.get_rect() 37 | textpos.centerx = screen.get_rect().centerx 38 | textpos.centery = screen.get_rect().centery 39 | 40 | # Blit onto screen 41 | screen.blit(pltText, textpos) 42 | 43 | # Update 44 | pygame.display.update() 45 | 46 | 47 | def clearScreen(): 48 | """ 49 | Clears the pygame screen of all drawn objects. 50 | 51 | Parameters: 52 | ----------- 53 | None 54 | 55 | Returns: 56 | -------- 57 | None 58 | """ 59 | screen.fill(black) 60 | pygame.display.update() 61 | 62 | 63 | def doCountdown(pretext="Ready", pretext_fontsize=600, countfrom=5): 64 | """ 65 | Performs on screen countdown 66 | 67 | Parameters: 68 | ----------- 69 | pretext : string 70 | Text shown before countdown starts 71 | pretext_fontsize : int 72 | Size of pretext font 73 | countfrom : int 74 | Number to count down from 75 | """ 76 | pretext_font = pygame.font.Font(None, pretext_fontsize) 77 | drawText(pretext_font, pretext) 78 | sleep(1) 79 | clearScreen() 80 | 81 | # Count down on the display 82 | for i in range(countfrom, 0, -1): 83 | # Draw text on the screen 84 | drawText(bigfont, str(i)) 85 | 86 | # Flash the LED during the second of dead time 87 | for j in range(4): 88 | outputToggle(ledPin, False, time=0.125) 89 | outputToggle(ledPin, True, time=0.125) 90 | 91 | # Clear the screen one final time so no numbers are left 92 | clearScreen() 93 | 94 | 95 | def takePhoto(): 96 | """ 97 | Captures and stores a photo from the pi camera board 98 | 99 | Paramters: 100 | ---------- 101 | None 102 | 103 | Returns: 104 | -------- 105 | path : str 106 | path, including filename, of captured photo 107 | 108 | Notes: 109 | ------ 110 | Can add use_video_port=True to the capture call, which does prevent 111 | the preview from not matching the captured size. This seemed to 112 | signifcantly degrade the capture quality though, so I let it be. 113 | Photos can be trimmed after the fact, or just left as is. 114 | """ 115 | # Adjust to image capture brightness 116 | camera.brightness = photoBrightness 117 | 118 | # Grab the capture 119 | time_stamp = strftime("%Y_%m_%dT%H_%M_%S", gmtime()) 120 | path = "/home/pi/photobooth_photos/%s.jpg" % time_stamp 121 | if play_shutter_sound: 122 | shutter_sound.play() 123 | 124 | # Unflip the photo so it is correct when taken 125 | camera.hflip = False 126 | 127 | # Take the photo 128 | camera.capture(path) 129 | 130 | # Go back to flipped preview 131 | camera.hflip = True 132 | 133 | # Go back to preview brightness 134 | camera.brightness = previewBrightness 135 | 136 | return path 137 | 138 | 139 | def outputToggle(pin, status, time=False): 140 | """ 141 | Changes the state of an ouput GPIO pin with optional time delay. 142 | 143 | Parameters: 144 | ----------- 145 | pin : int 146 | Pin number to manipulate 147 | status : boolean 148 | Status to be assigned to the pin 149 | time : int, float 150 | Time to wait before returning (optional) 151 | """ 152 | GPIO.output(pin, status) 153 | if time: 154 | sleep(time) 155 | return status 156 | 157 | 158 | def photoButtonPress(event): 159 | """ 160 | Event handler for the big red photo button. 161 | 162 | Parameters: 163 | ----------- 164 | event : object 165 | Button press event from GPIO 166 | 167 | Returns: 168 | -------- 169 | None 170 | """ 171 | # Wait for 0.1 sec to be sure it's a person pressing the 172 | # button, not noise. 173 | sleep(0.1) 174 | if GPIO.input(photobuttonPin) != GPIO.LOW: 175 | return 176 | 177 | # Turn on the lights and let people adjust 178 | sleep(1) 179 | outputToggle(auxlightPin, True) 180 | sleep(2) 181 | 182 | # Take photos 183 | photo_names = [] 184 | for i in range(number_photos): 185 | doCountdown() 186 | fname = takePhoto() 187 | photo_names.append(fname) 188 | sleep(1) 189 | 190 | # Turn off the lights 191 | outputToggle(auxlightPin, False) 192 | 193 | # Tweet the photos 194 | if tweet_photos: 195 | print tweet_text 196 | print ".txt" in tweet_text 197 | if ".txt" in tweet_text: 198 | try: 199 | text = getRandomTweet(tweet_text) 200 | except: 201 | print "Error getting random tweet!" 202 | text = "Photo booth photos!" 203 | else: 204 | text = tweet_text 205 | tweetPhotos(photo_names, tweet_text=text) 206 | 207 | 208 | def getRandomTweet(fname): 209 | """ 210 | Gets a random line from a file of possible tweets to go with 211 | photo posts to Twitter. 212 | 213 | Parameters: 214 | ----------- 215 | fname : str 216 | filename 217 | 218 | Returns: 219 | -------- 220 | tweet : str 221 | text of random tweet from file 222 | """ 223 | lines = [] 224 | with open(fname, "r") as f: 225 | lines = f.readlines() 226 | random_line_num = random.randrange(0, len(lines)) 227 | return lines[random_line_num].strip('\n\r') 228 | 229 | 230 | def tweetPhotos(photo_files, tweet_text="Photobooth photos!"): 231 | """ 232 | Posts photos to twitter with the given tweet text 233 | 234 | Paramters: 235 | ---------- 236 | photo_files : list 237 | list of full paths to the files to tweet 238 | tweet_text : str 239 | text to tweet with photos 240 | 241 | Returns: 242 | -------- 243 | None 244 | 245 | Notes: 246 | ------ 247 | If you allow many photos per session (>3 photos/button press) 248 | it is probably a good idea to only tweet a few to save time 249 | and not make the Twitter API angry. Untested with high numbers. 250 | """ 251 | responses = [] 252 | media_ids = [] 253 | for photo_file in photo_files: 254 | photo = open(photo_file) 255 | response = twitter.upload_media(media=photo) 256 | responses.append(response) 257 | media_ids.append(response['media_id']) 258 | twitter.update_status(status=tweet_text, media_ids=media_ids) 259 | 260 | 261 | def shutdownPi(): 262 | """ 263 | Shutdown the system totally. Full halt. 264 | 265 | Paramters: 266 | ---------- 267 | None 268 | 269 | Returns: 270 | -------- 271 | None 272 | """ 273 | os.system("sudo shutdown -h now") 274 | 275 | 276 | def shutdownButtonPress(event, hold_time=3): 277 | """ 278 | Event handler for the shutdown button. Makes sure that 279 | the button is held before shutting down completely. 280 | 281 | Parameters: 282 | ----------- 283 | event : object 284 | Event from GPIO 285 | hold_time : int, float 286 | Time (seconds) the button must be held for shutdown to 287 | be activated. Helps prevent accidental shutdowns. 288 | """ 289 | sleep(hold_time) 290 | if GPIO.input(shutdownbuttonPin) != GPIO.LOW: 291 | return 292 | 293 | safeClose() 294 | shutdownPi() 295 | 296 | 297 | def safeClose(): 298 | """ 299 | Cleanly exits the program by turning off the lights, stopping 300 | the camera, and cleaning up the resources. 301 | 302 | Parameters: 303 | ----------- 304 | None 305 | 306 | Returns: 307 | -------- 308 | None 309 | """ 310 | outputToggle(ledPin, False) 311 | outputToggle(auxlightPin, False) 312 | camera.stop_preview() 313 | camera.close() 314 | GPIO.cleanup() 315 | 316 | # Setup Parameters 317 | # Only change things here unless you want to dig into the program 318 | tweet_photos = True # Turn on/off photo tweeting 319 | number_photos = 3 # Number of pictures taken after each activation 320 | tweet_text = "tweet_options.txt" # Default text or file of tweets 321 | play_shutter_sound = True # Turn on/off shutter sound effects 322 | photo_path = '/home/pi/photobooth_photos' # Where photos will be stored 323 | CONSUMER_KEY = "YOUR_KEY_HERE" # Keys from twitter 324 | CONSUMER_SECRET = "YOUR_KEY_HERE" # Keys from twitter 325 | ACCESS_TOKEN = "YOUR_KEY_HERE" # Keys from twitter 326 | ACCESS_TOKEN_SECRET = "YOUR_KEY_HERE" # Keys from twitter 327 | 328 | # Initial Setup 329 | if not os.path.exists(photo_path): 330 | os.makedirs(photo_path) 331 | 332 | if tweet_photos: 333 | import twython 334 | 335 | twitter = twython.Twython( 336 | CONSUMER_KEY, 337 | CONSUMER_SECRET, 338 | ACCESS_TOKEN, 339 | ACCESS_TOKEN_SECRET 340 | ) 341 | 342 | # Setup sound. Note that if the full path to the audio file is not 343 | # specified, it will not play when auto-startup happens, but will 344 | # when manually started from the correct directory. Use the full path 345 | # for it to work as expected. Could always use os.getcwd() to construct 346 | # the path, but here it's really very simple. 347 | 348 | pygame.init() 349 | pygame.mixer.init() 350 | shutter_sound = pygame.mixer.Sound("/home/pi/PiBooth/shutter_sound.wav") 351 | 352 | # Pin configuration 353 | ledPin = 19 # GPIO of the indicator LED 354 | auxlightPin = 20 # GPIO of the AUX lighting output 355 | photobuttonPin = 17 # GPIO of the photo push button 356 | shutdownbuttonPin = 18 # GPIO of the shutdown push button 357 | 358 | # Camera Settings 359 | previewBrightness = 60 # Lighter than normal to offset the alpha distortion 360 | photoBrightness = 57 # Darker than preview since there is no alpha 361 | photoContrast = 0 # Default 362 | 363 | # pygame Settings 364 | size = width, height = 1280, 720 365 | black = 0, 0, 0 366 | screen = pygame.display.set_mode(size, pygame.FULLSCREEN) 367 | bigfont = pygame.font.Font(None, 800) 368 | smfont = pygame.font.Font(None, 600) 369 | tinyfont = pygame.font.Font(None, 300) 370 | 371 | # Setup camera 372 | camera = picamera.PiCamera() 373 | camera.resolution = (2592, 1944) # 1280,720 also works for some setups 374 | camera.framerate = 10 # slower is necessary for high-resolution 375 | camera.brightness = previewBrightness # Turned up so the black isn't too dark 376 | camera.preview_alpha = 210 # Set transparency so we can see the countdown 377 | camera.hflip = True 378 | camera.vflip = False 379 | camera.start_preview() 380 | 381 | # Fill screen 382 | screen.fill(black) 383 | 384 | # Turn off mouse 385 | pygame.mouse.set_visible(False) 386 | 387 | # Setup and tie GPIO pins 388 | GPIO.setmode(GPIO.BCM) 389 | GPIO.setup(photobuttonPin, GPIO.IN, GPIO.PUD_UP) # Take photo button 390 | GPIO.setup(shutdownbuttonPin, GPIO.IN, GPIO.PUD_UP) # Shutdown button 391 | GPIO.setup(ledPin, GPIO.OUT) # Front LED 392 | GPIO.setup(auxlightPin, GPIO.OUT) # Aux Lights 393 | GPIO.add_event_detect(photobuttonPin, GPIO.FALLING, 394 | callback=photoButtonPress, bouncetime=1000) 395 | GPIO.add_event_detect(shutdownbuttonPin, GPIO.FALLING, 396 | callback=shutdownButtonPress, bouncetime=1000) 397 | 398 | outputToggle(ledPin, True) # Turn on the camera "power" LED 399 | 400 | # Main loop. Waits for keypress events. Everything else is 401 | # an interrupt. Most of the time this just loops doing nothing. 402 | while 1: 403 | for event in pygame.event.get(): 404 | if event.type == pygame.QUIT: 405 | print "QUIT event detected" 406 | safeClose() 407 | sys.exit() 408 | 409 | elif event.type == pygame.KEYDOWN: 410 | # Quit the program on escape 411 | if event.key == pygame.K_ESCAPE: 412 | safeClose() 413 | sys.exit() 414 | 415 | # Adjust brightness with the up and down arrows 416 | if event.key == pygame.K_UP: 417 | photoBrightness += 1 418 | previewBrightness += 1 419 | camera.brightness = previewBrightness 420 | print "New brightness (preview/photo): %d/%d" % ( 421 | photoBrightness, previewBrightness) 422 | 423 | if event.key == pygame.K_DOWN: 424 | photoBrightness -= 1 425 | previewBrightness -= 1 426 | camera.brightness = previewBrightness 427 | print "New brightness (preview/photo): %d/%d" % ( 428 | photoBrightness, previewBrightness) 429 | 430 | # Adjust contrast with the right and left arrows 431 | if event.key == pygame.K_RIGHT: 432 | photoContrast += 1 433 | camera.contrast = photoContrast 434 | print "New contrast: %d" % (photoContrast) 435 | 436 | if event.key == pygame.K_LEFT: 437 | photoContrast -= 1 438 | camera.contrast = photoContrast 439 | print "New contrast: %d" % (photoContrast) 440 | 441 | else: 442 | pass 443 | --------------------------------------------------------------------------------