-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
377 lines (304 loc) · 15 KB
/
main.py
File metadata and controls
377 lines (304 loc) · 15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
import cv2
import kivy
from kivy.app import App
from kivy.graphics.texture import Texture
from kivy.uix.image import Image
from kivy.clock import Clock
import queue
from PIL import Image as PILImage
from kivy.uix.popup import Popup
from kivy.uix.label import Label
import os
# Import our modules
from camera import CameraThread
from ui import create_main_layout, create_file_chooser_popup, create_photo_saved_popup, create_qr_popup, create_video_saved_popup, create_email_popup
from utils import ensure_directory_exists, generate_photo_filename, generate_video_filename, verify_path_security, load_image_with_alpha
from cloud import CloudUploader
from email_manager import EmailManager
kivy.require('2.0.0')
class PhotoBoothApp(App):
def build(self):
# Initialize email manager
self.email_manager = EmailManager()
# Create thread-safe queues for communication
self.frame_queue = queue.Queue(maxsize=5) # Limit to 5 frames to prevent memory issues
self.command_queue = queue.Queue()
self.result_queue = queue.Queue() # For getting results back from the camera thread
# Initialize camera thread
self.camera_thread = CameraThread(self.frame_queue, self.command_queue, self.result_queue)
self.camera_thread.daemon = True # Thread will close when main app closes
self.camera_thread.start()
self.img_widget = Image()
# Countdown variables
self.countdown_active = False
self.countdown_seconds = 0
self.countdown_event = None
# Video recording variables
self.is_recording = False
# Ensure directories exist - create them early to verify they work
print("Setting up directories...")
self.overlay_dir = ensure_directory_exists("overlays")
self.qrcode_dir = ensure_directory_exists("qrcodes")
self.video_dir = ensure_directory_exists("videos")
self.photo_dir = ensure_directory_exists("photos")
# Verify these directories are writable
print(f"Overlay directory writable: {os.access(self.overlay_dir, os.W_OK)}")
print(f"QR code directory writable: {os.access(self.qrcode_dir, os.W_OK)}")
print(f"Video directory writable: {os.access(self.video_dir, os.W_OK)}")
print(f"Photo directory writable: {os.access(self.photo_dir, os.W_OK)}")
# Initialize the cloud uploader
self.cloud_uploader = CloudUploader(
callback=self.on_upload_complete,
error_callback=self.on_upload_error,
qr_save_dir=self.qrcode_dir
)
# Create the main layout
self.main_layout = create_main_layout(
self.img_widget,
self.start_countdown,
self.open_file_chooser,
self.clear_overlay,
self.start_recording,
self.stop_recording
)
# Start the frame update and result monitoring schedules
Clock.schedule_interval(self.update, 1.0 / 30)
Clock.schedule_interval(self.check_results, 0.1) # Check for results 10 times per second
return self.main_layout
def on_start(self):
"""Called when the app starts"""
print("App started")
# show email popup after build is complete
self.show_email_popup()
def show_email_popup(self):
"""Show the email collection popup"""
print("Showing email collection popup")
def on_email_submitted(email):
if self.email_manager.save_email(email):
print(f"Email saved: {email}")
else:
print("Failed to save email")
popup = create_email_popup(on_email_submitted)
popup.open()
def check_results(self, dt):
"""Check for results from the camera thread"""
try:
while not self.result_queue.empty():
event_type, *args = self.result_queue.get_nowait()
if event_type == "photo_saved":
self.on_photo_saved(*args)
elif event_type == "recording_started":
self.on_recording_started(*args)
elif event_type == "recording_stopped":
self.on_recording_stopped(*args)
except queue.Empty:
pass
def on_photo_saved(self, filepath, success):
"""Handle the event when a photo has been saved"""
if success:
print(f"Photo saved successfully: {filepath}")
# Update the photo saved popup message
if hasattr(self, 'current_photo_popup') and hasattr(self.current_photo_popup.content, 'text'):
self.current_photo_popup.content.text = f"Photo saved as:\n{filepath}\n\nUploading to Dropbox..."
# Now it's safe to upload to Dropbox
self.cloud_uploader.upload_file(filepath)
else:
print(f"Failed to save photo: {filepath}")
# Show an error message
self.show_error_popup("Failed to save photo. Please try again.")
def on_recording_started(self, filepath, success):
"""Handle the event when video recording has started"""
if success:
print(f"Recording started: {filepath}")
self.is_recording = True
else:
print(f"Failed to start recording: {filepath}")
self.is_recording = False
self.show_error_popup("Failed to start recording. Please try again.")
def on_recording_stopped(self, filepath, duration):
"""Handle the event when video recording has stopped"""
self.is_recording = False
print(f"Recording stopped: {filepath}, duration: {duration:.2f} seconds")
# Show the video saved popup
self.current_video_popup = create_video_saved_popup(filepath, duration)
self.current_video_popup.open()
# Upload the video to Dropbox
self.cloud_uploader.upload_file(filepath) # Reusing the photo upload method as it works the same way
def on_upload_error(self, error_message, dt):
"""Handle Dropbox upload errors"""
# Dismiss the photo saved popup if it's open
if hasattr(self, 'current_photo_popup'):
self.current_photo_popup.dismiss()
# Dismiss the video saved popup if it's open
if hasattr(self, 'current_video_popup'):
self.current_video_popup.dismiss()
# Show an error message
self.show_error_popup(f"Cloud upload failed: {error_message}")
def show_error_popup(self, message):
"""Show an error popup with the given message"""
error_popup = Popup(
title='Error',
content=Label(text=message),
size_hint=(0.6, 0.4)
)
error_popup.open()
Clock.schedule_once(lambda dt: error_popup.dismiss(), 5) # Auto-close after 5 seconds
def open_file_chooser(self, instance):
"""Open a file chooser to select a PNG overlay"""
# Don't open file chooser during countdown or recording
if self.countdown_active or self.is_recording:
return
# Create and open the file chooser popup
popup = create_file_chooser_popup(self.overlay_dir, self.load_overlay)
popup.open()
def load_overlay(self, filepath):
"""Load the overlay image file"""
try:
# Verify the file is within the overlays directory
if not verify_path_security(filepath, self.overlay_dir):
raise Exception("Security error: Cannot load files outside of overlays directory")
# Load the PNG with alpha channel
overlay = load_image_with_alpha(filepath)
if overlay:
overlay = overlay.transpose(PILImage.FLIP_TOP_BOTTOM)
# Send overlay to camera thread
self.command_queue.put(("set_overlay", overlay))
print(f"Loaded overlay: {filepath}")
except Exception as e:
print(f"Error loading overlay: {e}")
self.show_error_popup(f"Error loading overlay: {e}")
def clear_overlay(self, instance):
"""Remove the current overlay"""
# Don't clear overlay during countdown or recording
if self.countdown_active or self.is_recording:
return
self.command_queue.put(("clear_overlay", None))
print("Overlay cleared")
def start_countdown(self, instance):
"""Start the countdown to take a photo"""
# Don't start countdown if one is already active or if recording
if self.countdown_active or self.is_recording:
return
# Check if we have an email in the database
if not os.path.exists(self.email_manager.csv_path) or os.path.getsize(self.email_manager.csv_path) <= 0:
self.show_email_popup()
return
self.countdown_active = True
self.countdown_seconds = 3 # Start from 3
# Schedule the countdown
self.countdown_event = Clock.schedule_interval(self.update_countdown, 1)
def update_countdown(self, dt):
"""Update countdown timer and take photo when it reaches 0"""
if self.countdown_seconds > 0:
print(f"Countdown: {self.countdown_seconds}")
self.countdown_seconds -= 1
else:
# Cancel the countdown event
if self.countdown_event:
self.countdown_event.cancel()
self.countdown_event = None
# Take the photo
self.take_photo()
# Reset countdown state
self.countdown_active = False
def update(self, dt):
try:
processed_frame, raw_frame = self.frame_queue.get_nowait()
# Add countdown display to the frame if active
if self.countdown_active and self.countdown_seconds > 0:
# Add a large countdown number to the center of the frame
height, width = processed_frame.shape[:2]
font = cv2.FONT_HERSHEY_DUPLEX
text = str(self.countdown_seconds)
text_size = cv2.getTextSize(text, font, 7, 10)[0]
# Calculate position (center)
text_x = (width - text_size[0]) // 2
text_y = (height + text_size[1]) // 2
# Draw with a background for better visibility
cv2.putText(processed_frame, text, (text_x, text_y), font, 7, (0, 0, 0), 15, cv2.LINE_AA) # Black shadow
cv2.putText(processed_frame, text, (text_x, text_y), font, 7, (255, 255, 255), 10, cv2.LINE_AA) # White text
processed_frame = cv2.rotate(processed_frame, cv2.ROTATE_180)
processed_frame = cv2.flip(processed_frame, 1)
# Convert to texture for display
buf = cv2.cvtColor(processed_frame, cv2.COLOR_BGR2RGB)
buf = buf.tobytes()
image_texture = Texture.create(size=(processed_frame.shape[1], processed_frame.shape[0]), colorfmt='rgb')
image_texture.blit_buffer(buf, colorfmt='rgb', bufferfmt='ubyte')
self.img_widget.texture = image_texture
except queue.Empty:
pass # Skip if no new frame is available
def take_photo(self, instance=None):
"""Take a photo and save it"""
try:
# Generate a filename
filename = generate_photo_filename()
# Show confirmation popup with initial "saving" message
self.current_photo_popup = create_photo_saved_popup(filename)
self.current_photo_popup.open()
# Send command to camera thread to take photo
self.command_queue.put(("take_photo", filename))
# The upload to Dropbox will be initiated from on_photo_saved
# after the photo is successfully saved
except Exception as e:
print(f"Error taking photo: {e}")
self.show_error_popup(f"Error taking photo: {e}")
def start_recording(self, instance):
"""Start recording video"""
# Don't start recording if already recording or during countdown
if self.is_recording or self.countdown_active:
return
# Check if we have an email in the database
if not os.path.exists(self.email_manager.csv_path) or os.path.getsize(self.email_manager.csv_path) <= 0:
self.show_email_popup()
return
try:
# Generate a filename for the video
filename = generate_video_filename()
# Log detailed path information
print(f"Generated video filename: {filename}")
print(f"Absolute path: {os.path.abspath(filename)}")
# Make sure videos directory exists
os.makedirs(os.path.dirname(filename), exist_ok=True)
# Send command to camera thread to start recording
self.command_queue.put(("start_recording", filename))
except Exception as e:
print(f"Error starting recording: {e}")
self.show_error_popup(f"Error starting recording: {e}")
def stop_recording(self, instance):
"""Stop recording video"""
# Only stop if we're currently recording
if not self.is_recording:
return
try:
# Send command to camera thread to stop recording
self.command_queue.put(("stop_recording", None))
except Exception as e:
print(f"Error stopping recording: {e}")
self.show_error_popup(f"Error stopping recording: {e}")
def on_upload_complete(self, download_url, qr_code_path, dt):
"""Called when the Dropbox upload is complete"""
try:
# Close the "Saving photo" popup if it's still open
if hasattr(self, 'current_photo_popup'):
self.current_photo_popup.dismiss()
# Close the "Video saved" popup if it's still open
if hasattr(self, 'current_video_popup'):
self.current_video_popup.dismiss()
# Create and show the QR code popup
qr_popup = create_qr_popup(download_url, qr_code_path)
qr_popup.open()
except Exception as e:
print(f"Error showing QR code: {e}")
self.show_error_popup(f"Error showing QR code: {e}")
def on_stop(self):
# Cancel any scheduled events
if self.countdown_event:
self.countdown_event.cancel()
# Stop recording if active
if self.is_recording:
self.stop_recording(None)
# Stop camera thread
if hasattr(self, 'camera_thread'):
self.camera_thread.stop()
if __name__ == '__main__':
PhotoBoothApp().run()