-
Notifications
You must be signed in to change notification settings - Fork 33
/
PictureFrame.py
375 lines (352 loc) · 15.4 KB
/
PictureFrame.py
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
#!/usr/bin/python
from __future__ import absolute_import, division, print_function, unicode_literals
''' Simplified slideshow system using ImageSprite and without threading for background
loading of images (so may show delay for v large images).
Also has a minimal use of PointText and TextBlock system with reduced codepoints
and reduced grid_size to give better resolution for large characters.
Also shows a simple use of MQTT to control the slideshow parameters remotely
see http://pi3d.github.io/html/FAQ.html and https://www.thedigitalpictureframe.com/control-your-digital-picture-frame-with-home-assistents-wifi-presence-detection-and-mqtt/
and https://www.cloudmqtt.com/plans.html
USING exif info to rotate images
by default the global KEYBOARD is set False so the only way to stop the
probram is Alt-F4 or reboot. If you intend to test from command line set
KEYBOARD True. After that:
ESC to quit, 's' to reverse, any other key to move on one.
'''
import os
import time
import random
import demo
import pi3d
from PIL import Image, ExifTags, ImageFilter # these are needed for getting exif data from images
#####################################################
# these variables are constants
#####################################################
PIC_DIR = '/home/pi/pi3d_demos/textures' #'textures'
#PIC_DIR = '/home/patrick/python/pi3d_demos/textures' #'textures'
FPS = 20
FIT = True
EDGE_ALPHA = 0.5 # see background colour at edge. 1.0 would show reflection of image
BACKGROUND = (0.2, 0.2, 0.2, 1.0)
RESHUFFLE_NUM = 5 # times through before reshuffling
FONT_FILE = '/home/pi/pi3d_demos/fonts/NotoSans-Regular.ttf'
#FONT_FILE = '/home/patrick/python/pi3d_demos/fonts/NotoSans-Regular.ttf'
CODEPOINTS = '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ., _-/' # limit to 49 ie 7x7 grid_size
USE_MQTT = False
RECENT_N = 4 # shuffle the most recent ones to play before the rest
SHOW_NAMES = False
CHECK_DIR_TM = 60.0 # seconds to wait between checking if directory has changed
#####################################################
BLUR_EDGES = False # use blurred version of image to fill edges - will override FIT = False
BLUR_AMOUNT = 12 # larger values than 12 will increase processing load quite a bit
BLUR_ZOOM = 1.0 # must be >= 1.0 which expands the backgorund to just fill the space around the image
KENBURNS = False # will set FIT->False and BLUR_EDGES->False
KEYBOARD = False # set to False when running headless to avoid curses error. True for debugging
#####################################################
# these variables can be altered using MQTT messaging
#####################################################
time_delay = 10.0 # between slides
fade_time = 3.0
shuffle = True # shuffle on reloading
date_from = None
date_to = None
quit = False
paused = False # NB must be set to True after the first iteration of the show!
#####################################################
# only alter below here if you're keen to experiment!
#####################################################
if KENBURNS:
kb_up = True
FIT = False
BLUR_EDGES = False
if BLUR_ZOOM < 1.0:
BLUR_ZOOM = 1.0
delta_alpha = 1.0 / (FPS * fade_time) # delta alpha
last_file_change = 0.0 # holds last change time in directory structure
next_check_tm = time.time() + CHECK_DIR_TM # check if new file or directory every hour
#####################################################
# some functions to tidy subsequent code
#####################################################
def tex_load(fname, orientation, size=None):
try:
im = Image.open(fname)
im.putalpha(255) # this will convert to RGBA and set alpha to opaque
if orientation == 2:
im = im.transpose(Image.FLIP_LEFT_RIGHT)
if orientation == 3:
im = im.transpose(Image.ROTATE_180) # rotations are clockwise
if orientation == 4:
im = im.transpose(Image.FLIP_TOP_BOTTOM)
if orientation == 5:
im = im.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.ROTATE_270)
if orientation == 6:
im = im.transpose(Image.ROTATE_270)
if orientation == 7:
im = im.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.ROTATE_90)
if orientation == 8:
im = im.transpose(Image.ROTATE_90)
if BLUR_EDGES and size is not None:
wh_rat = (size[0] * im.size[1]) / (size[1] * im.size[0])
if abs(wh_rat - 1.0) > 0.01: # make a blurred background
(sc_b, sc_f) = (size[1] / im.size[1], size[0] / im.size[0])
if wh_rat > 1.0:
(sc_b, sc_f) = (sc_f, sc_b) # swap round
(w, h) = (round(size[0] / sc_b / BLUR_ZOOM), round(size[1] / sc_b / BLUR_ZOOM))
(x, y) = (round(0.5 * (im.size[0] - w)), round(0.5 * (im.size[1] - h)))
box = (x, y, x + w, y + h)
blr_sz = (int(x * 512 / size[0]) for x in size)
im_b = im.resize(size, resample=0, box=box).resize(blr_sz)
im_b = im_b.filter(ImageFilter.GaussianBlur(BLUR_AMOUNT))
im_b = im_b.resize(size, resample=Image.BICUBIC)
im_b.putalpha(round(255 * EDGE_ALPHA)) # to apply the same EDGE_ALPHA as the no blur method.
im = im.resize((int(x * sc_f) for x in im.size), resample=Image.BICUBIC)
im_b.paste(im, box=(round(0.5 * (im_b.size[0] - im.size[0])),
round(0.5 * (im_b.size[1] - im.size[1]))))
im = im_b # have to do this as paste applies in place
tex = pi3d.Texture(im, blend=True, m_repeat=True, automatic_resize=True, free_after_load=True)
except Exception as e:
print('''Couldn't load file {} giving error: {}'''.format(fname, e))
tex = None
return tex
def tidy_name(path_name):
name = os.path.basename(path_name).upper()
name = ''.join([c for c in name if c in CODEPOINTS])
return name
def check_changes():
global last_file_change
update = False
for root, _, _ in os.walk(PIC_DIR):
mod_tm = os.stat(root).st_mtime
if mod_tm > last_file_change:
last_file_change = mod_tm
update = True
return update
def get_files(dt_from=None, dt_to=None):
# dt_from and dt_to are either None or tuples (2016,12,25)
if dt_from is not None:
dt_from = time.mktime(dt_from + (0, 0, 0, 0, 0, 0))
if dt_to is not None:
dt_to = time.mktime(dt_to + (0, 0, 0, 0, 0, 0))
global shuffle, PIC_DIR, EXIF_DATID, last_file_change
file_list = []
extensions = ['.png','.jpg','.jpeg'] # can add to these
for root, _dirnames, filenames in os.walk(PIC_DIR):
mod_tm = os.stat(root).st_mtime # time of alteration in a directory
if mod_tm > last_file_change:
last_file_change = mod_tm
for filename in filenames:
ext = os.path.splitext(filename)[1].lower()
if ext in extensions and not '.AppleDouble' in root and not filename.startswith('.'):
file_path_name = os.path.join(root, filename)
include_flag = True
orientation = 1 # this is default - unrotated
if EXIF_DATID is not None and EXIF_ORIENTATION is not None:
try:
im = Image.open(file_path_name) # lazy operation so shouldn't load (better test though)
#print(filename, end="")
exif_data = im._getexif()
#print('orientation is {}'.format(exif_data[EXIF_ORIENTATION]))
dt = time.mktime(
time.strptime(exif_data[EXIF_DATID], '%Y:%m:%d %H:%M:%S'))
orientation = int(exif_data[EXIF_ORIENTATION])
except Exception as e: # NB should really check error here but it's almost certainly due to lack of exif data
print('trying to read exif', e)
dt = os.path.getmtime(file_path_name) # so use file last modified date
if (dt_from is not None and dt < dt_from) or (dt_to is not None and dt > dt_to):
include_flag = False
if include_flag:
file_list.append((file_path_name, orientation, os.path.getmtime(file_path_name))) # iFiles now list of tuples (file_name, orientation)
if shuffle:
file_list.sort(key=lambda x: x[2]) # will be later files last
temp_list_first = file_list[-RECENT_N:]
temp_list_last = file_list[:-RECENT_N]
random.shuffle(temp_list_first)
random.shuffle(temp_list_last)
file_list = temp_list_first + temp_list_last
else:
file_list.sort() # if not suffled; sort by name
return file_list, len(file_list) # tuple of file list, number of pictures
EXIF_DATID = None # this needs to be set before get_files() above can extract exif date info
EXIF_ORIENTATION = None
for k in ExifTags.TAGS:
if ExifTags.TAGS[k] == 'DateTimeOriginal':
EXIF_DATID = k
if ExifTags.TAGS[k] == 'Orientation':
EXIF_ORIENTATION = k
##############################################
# MQTT functionality - see https://www.thedigitalpictureframe.com/
##############################################
if USE_MQTT:
try:
import paho.mqtt.client as mqtt
def on_connect(client, userdata, flags, rc):
print("Connected to MQTT broker")
def on_message(client, userdata, message):
# TODO not ideal to have global but probably only reasonable way to do it
global next_pic_num, iFiles, nFi, date_from, date_to, time_delay
global delta_alpha, fade_time, shuffle, quit, paused, nexttm
msg = message.payload.decode("utf-8")
reselect = False
if message.topic == "frame/date_from": # NB entered as mqtt string "2016:12:25"
df = msg.split(":")
date_from = tuple(int(i) for i in df)
reselect = True
elif message.topic == "frame/date_to":
df = msg.split(":")
date_to = tuple(int(i) for i in df)
reselect = True
elif message.topic == "frame/time_delay":
time_delay = float(msg)
elif message.topic == "frame/fade_time":
fade_time = float(msg)
delta_alpha = 1.0 / (FPS * fade_time)
elif message.topic == "frame/shuffle":
shuffle = True if msg == "True" else False
reselect = True
elif message.topic == "frame/quit":
quit = True
elif message.topic == "frame/paused":
paused = not paused # toggle from previous value
elif message.topic == "frame/back":
next_pic_num -= 2
if next_pic_num < -1:
next_pic_num = -1
nexttm = time.time() - 86400.0
if reselect:
iFiles, nFi = get_files(date_from, date_to)
next_pic_num = 0
# set up MQTT listening
client = mqtt.Client()
client.username_pw_set("orhellow", "z6kfIctiONxP") # replace with your own id
client.connect("postman.cloudmqtt.com", 16845, 60) # replace with your own server
client.loop_start()
client.subscribe("frame/date_from", qos=0)
client.subscribe("frame/date_to", qos=0)
client.subscribe("frame/time_delay", qos=0)
client.subscribe("frame/fade_time", qos=0)
client.subscribe("frame/shuffle", qos=0)
client.subscribe("frame/quit", qos=0)
client.subscribe("frame/paused", qos=0)
client.subscribe("frame/back", qos=0)
client.on_connect = on_connect
client.on_message = on_message
except Exception as e:
print("MQTT not set up because of: {}".format(e))
##############################################
DISPLAY = pi3d.Display.create(x=0, y=0, frames_per_second=FPS,
display_config=pi3d.DISPLAY_CONFIG_HIDE_CURSOR, background=BACKGROUND)
CAMERA = pi3d.Camera(is_3d=False)
print(DISPLAY.opengl.gl_id)
shader = pi3d.Shader("/home/pi/pi3d_demos/shaders/blend_new")
#shader = pi3d.Shader("/home/patrick/python/pi3d_demos/shaders/blend_new")
slide = pi3d.Sprite(camera=CAMERA, w=DISPLAY.width, h=DISPLAY.height, z=5.0)
slide.set_shader(shader)
slide.unif[47] = EDGE_ALPHA
if KEYBOARD:
kbd = pi3d.Keyboard()
# images in iFiles list
nexttm = 0.0
iFiles, nFi = get_files(date_from, date_to)
next_pic_num = 0
sfg = None # slide for background
sbg = None # slide for foreground
if nFi == 0:
print('No files selected!')
exit()
# PointText and TextBlock. If SHOW_NAMES is False then this is just used for no images message
font = pi3d.Font(FONT_FILE, codepoints=CODEPOINTS, grid_size=7, shadow_radius=4.0,
shadow=(0,0,0,128))
text = pi3d.PointText(font, CAMERA, max_chars=200, point_size=50)
textblock = pi3d.TextBlock(x=-DISPLAY.width * 0.5 + 50, y=-DISPLAY.height * 0.4,
z=0.1, rot=0.0, char_count=199,
text_format="{}".format(" "), size=0.99,
spacing="F", space=0.02, colour=(1.0, 1.0, 1.0, 1.0))
text.add_text_block(textblock)
num_run_through = 0
while DISPLAY.loop_running():
tm = time.time()
if nFi > 0:
if (tm > nexttm and not paused) or (tm - nexttm) >= 86400.0: # this must run first iteration of loop
nexttm = tm + time_delay
a = 0.0 # alpha - proportion front image to back
sbg = sfg
sfg = None
while sfg is None: # keep going through until a usable picture is found TODO break out how?
pic_num = next_pic_num
sfg = tex_load(iFiles[pic_num][0], iFiles[pic_num][1], (DISPLAY.width, DISPLAY.height))
next_pic_num += 1
if next_pic_num >= nFi:
num_run_through += 1
if shuffle and num_run_through >= RESHUFFLE_NUM:
num_run_through = 0
random.shuffle(iFiles)
next_pic_num = 0
if sbg is None: # first time through
sbg = sfg
slide.set_textures([sfg, sbg])
slide.unif[45:47] = slide.unif[42:44] # transfer front width and height factors to back
slide.unif[51:53] = slide.unif[48:50] # transfer front width and height offsets
wh_rat = (DISPLAY.width * sfg.iy) / (DISPLAY.height * sfg.ix)
if (wh_rat > 1.0 and FIT) or (wh_rat <= 1.0 and not FIT):
sz1, sz2, os1, os2 = 42, 43, 48, 49
else:
sz1, sz2, os1, os2 = 43, 42, 49, 48
wh_rat = 1.0 / wh_rat
slide.unif[sz1] = wh_rat
slide.unif[sz2] = 1.0
slide.unif[os1] = (wh_rat - 1.0) * 0.5
slide.unif[os2] = 0.0
if KENBURNS:
xstep, ystep = (slide.unif[i] * 2.0 / time_delay for i in (48, 49))
slide.unif[48] = 0.0
slide.unif[49] = 0.0
kb_up = not kb_up
# set the file name as the description
if SHOW_NAMES:
textblock.set_text(text_format="{}".format(tidy_name(iFiles[pic_num][0])))
text.regen()
if KENBURNS:
t_factor = nexttm - tm
if kb_up:
t_factor = time_delay - t_factor
slide.unif[48] = xstep * t_factor
slide.unif[49] = ystep * t_factor
if a < 1.0: # transition is happening
a += delta_alpha
slide.unif[44] = a
if SHOW_NAMES:
# this sets alpha for the TextBlock from 0 to 1 then back to 0
textblock.colouring.set_colour(alpha=(1.0 - abs(1.0 - 2.0 * a)))
text.regen()
else: # no transition effect safe to resuffle etc
if tm > next_check_tm:
if check_changes():
iFiles, nFi = get_files(date_from, date_to)
num_run_through = 0
next_pic_num = 0
next_check_tm = tm + CHECK_DIR_TM # once per hour
slide.draw()
else:
textblock.set_text("NO IMAGES SELECTED")
textblock.colouring.set_colour(alpha=1.0)
text.regen()
text.draw()
if KEYBOARD:
k = kbd.read()
if k != -1:
nexttm = time.time() - 86400.0
if k==27 or quit: #ESC
break
if k==ord(' '):
paused = not paused
if k==ord('s'): # go back a picture
next_pic_num -= 2
if next_pic_num < -1:
next_pic_num = -1
try:
client.loop_stop()
except Exception as e:
print("this was going to fail if previous try failed!")
if KEYBOARD:
kbd.close()
DISPLAY.destroy()