用 Python 制作 DIY 数字相框
用 Python 制作的 DIY 数字相框应用
引言
一台电脑显示器上显示着照片库中的照片。这个用 Python 编写的软件应用程序几乎可以在任何运行 python3
和 cv2
图像库的计算机上运行。
背景
市面上有许多数字照片显示应用程序。然而,我有一个旧的、退役的树莓派,它正待业,所以,理所当然,一个喜欢捣鼓的工程师应该花几周时间编写一些软件,让这个头发灰白的老树莓派能发挥作用。我能说什么呢,有些人对他们的宠物很感性,我想树莓派对我来说就像某种宠物……
我决定使用 Python 环境,这使我可以在 Windows 上开发,然后在 Linux (Raspberian) 上运行软件。
Using the Code
代码存储在一个 Python 源文件中,该文件使用 python3
解释器从命令行运行。
python3 digitalFotoFrame.py photoframe.ini
有一个命令行参数——一个配置文件名,允许配置照片帧软件的操作。
配置文件
配置文件是一个简单的文本文件,包含以下四行键值对。行可以按任何顺序排列。以 #
开头的注释行将被忽略。
DELAY=4.5
WAKE=6
SLEEP=22
PATH=/media/HDD
有四个关键字(它们区分大小写,所以请全部使用大写!)
DELAY=4.5 | 这意味着每张图片显示 4.5 秒。 |
WAKE=6 | 这配置了唤醒时间。在这种情况下,显示将在 0600 点开始。 |
SLEEP=22 | 这是睡眠时间——此时,显示停止,屏幕变黑——在这种情况下,是 2200 点,即晚上 10 点。 |
PATH=/media/HDD | 这是存储图片的文件夹——里面可以包含图片的子文件夹。 |
在 WAKE
和 SLEEP
小时之间,图片将以随机顺序连续显示。
在 SLEEP
和 WAKE
小时之间(夜间),屏幕将变黑,不显示任何图片。
选定的 PATH
可能包含许多文件,这些文件被组织在文件夹和子文件夹中。只有扩展名为 **.jpg 或 .png** 的文件才会被考虑显示。该文件夹中可能存在其他文件,但它们将被忽略。
软件概述
该软件是一个为 Python 3 编写的单一 Python 源文件。它只包含三个 Python 函数和一个主程序。
从顶层来看,应用程序将通过读取配置文件(在命令行上传递)来初始化。从该文件中提取四个参数(如上所示)。
在操作层面,将扫描 PATH
查找要显示的图片。这些图片将以随机顺序显示,每张图片在屏幕上停留 DELAY
时间(来自配置文件)。这发生在一天中的 WAKE
和 SLEEP
小时之间。每小时一次——如果找到配置文件——参数将被重新读取(可能已更改),并重新扫描照片。
在 SLEEP
和 WAKE
小时之间,屏幕将变黑,不显示任何照片。
软件功能
该软件使用 cv2 库(opencv
)进行图像处理,如读取、调整大小、显示图片。有关此 Python 库的完整详细信息,请参见下面的链接。它可用于 Windows、MacOS、Linux 和 Raspberian。此库必须安装在您运行应用程序的机器上。
pip install opencv-python
或
pip3 install opencv-python
链接到 OPENCV-PYTHON
至于 Python 库,其他依赖项都是标准的,因此无需特殊安装。
顶层程序代码
以下是应用程序的顶层 Python 代码。它基本上尝试查找并读取配置文件。这定义了 photoFolder
、delay
、wakehour
和 bedtimehour
的参数。如果配置文件无法成功读取,应用程序将无法运行并终止,显示错误消息。
一旦成功读取这些参数,应用程序的主函数(runFotoFrame
)将被调用,以显示文件并处理唤醒和睡眠时间。
#------------------------------------------------------------
# top-level python script
#
# set up some global variables to default values.
# The config file sets these values on startup
#
# later on, these may be read and changed by a control
# file that is looked for every hour or two in the scanned
# folder. If you want to change these while the pix frame
# is running, you can change this file at any time.
#
# command line:
# python3 digitalFotoFrame.py configfile.txt
#-------------------------------------------------------------
if __name__ == "__main__":
#----------------------------------
#---- default parameter values ----
#----------------------------------
photoFolder='/media/HDD' # top level folder for pictures
delay=4 # seconds for each displayed picture
wakehour=7 # 07am = 0700 = 7, start showing pictures
bedtimehour=21 # 10pm = 2100hrs = 21, stop showing pictures
print("---- Digital Foto Frame - Starting ----")
configFileRead=False
configfn="photoframe.ini" # default name to look for hourly in top level folder
# search arg list for
if len(sys.argv)>1:
configfn=sys.argv[1]
print("reading config file: ",configfn)
result=checkForControlFile(configfn,delay,wakehour,bedtimehour,photoFolder)
if result[4]: # set to true if successful read of config file
delay=result[0]
wakehour=result[1]
bedtimehour=result[2]
photoFolder=result[3]
configFileRead=True
print("Config file read: ",delay,wakehour,bedtimehour,photoFolder)
time.sleep(3) # wait just a bit to read messages if any
if not configFileRead:
print("\n--- Unable to read config file ---\n")
else:
# and then, let's get this show on the road
params=[delay,wakehour,bedtimehour,photoFolder,configfn]
runFotoFrame(params)
扫描要显示的图片
scanForFiles
函数用于递归扫描 PATH 文件夹及其所有子文件夹以查找要显示的图片。它返回一个要显示的图片文件名列表。我已经成功地用它处理了超过 10000 个文件的结构。
#-------------------------------------------------------------
#--- scan a folder tree (recursively) for jpg or png files
#-------------------------------------------------------------
def scanForFiles(folder):
pictureFiles=[]
itr=os.scandir(folder)
for entry in itr:
if entry.is_file():
fn=entry.name.lower()
if fn.endswith('.jpg') or fn.endswith('.png'):
pictureFiles.append(entry.path)
if entry.is_dir(): # recurse for sub-folders
x=scanForFiles(entry.path)
pictureFiles.extend(x)
#itr.close()
return pictureFiles
这里没有什么特别的——使用了 Python 标准的 os.scandir()
函数来扫描所有文件。扩展名为 .jpg 或 .png 的文件被添加到列表中。任何文件夹都会被递归扫描以查找 .jpg 或 .png 文件,并将它们添加到列表中。
读取配置文件
配置文件包含操作所需的设置。checkForControlFile()
函数用于尝试从外部文本文件中读取这些设置并返回一个值列表。
值作为包含五个元素的列表返回。
result[0]
- 延迟(秒)result[1]
- 唤醒时间(小时,0..23)result[2]
- 睡眠时间(小时,0..23)result[3]
- 存储图片的路径result[4]
- 如果参数读取成功,则为true
#-------------------------------------------------------------
#--- Try to open a configuration file and read settings from it
#-------------------------------------------------------------
#-------------------------------------------------------------
def checkForControlFile(controlFn,delay,wakeHour,bedtimeHour,photoFolder):
#
# Sample control file has four lines in KEY=VALUE format
# - delay in seconds
# - wake hour (0..23)
# - sleep hour (0..23)
# - path to find pictures and control file
#
# File is deposited into the top folder where the pictures are stored (PATH)
# File is named instructions.ini
# File has 4 lines
#
# Control file will be read hourly
#
#DELAY=3.5
#WAKE=6
#SLEEP=21
#PATH=/media/pi/photoframe
#
result=[delay,wakeHour,bedtimeHour,photoFolder,False]
readparams=0 # bitwise log of keywords found to verify we had a full
# configuration file with every line in it that we expect
#print(controlFn)
try:
with open(controlFn,'r') as finp:
print(datetime.datetime.now(),'Reading configuration file')
for line in finp:
print(line)
if line.startswith('DELAY='):
x=float(line[6:])
x=max(1.,x)
x=min(60.,x) # limit 1..60
result[0]=x
readparams=readparams | 1
if line.startswith('WAKE='):
x=float(line[5:])
x=max(0.,x)
x=min(10.,x) # limit 0..60
result[1]=int(x)
readparams=readparams | 2
if line.startswith('SLEEP='):
x=float(line[6:])
x=max(0.,x)
x=min(23.,x) # limit 0..60
result[2]=int(x)
readparams=readparams | 4
if line.startswith('PATH='):
result[3]=line[5:-1] # strip off new line at end
readparams=readparams | 8
except:
pass
print('Read configuration file results ',result)
if (readparams == 15):
result[4] = True # read file properly, all 4 bits set = 1111 = 0xf = 15
return result
主图片显示函数
主图片显示函数是应用程序中更复杂的功能。它处理两种模式(清醒和显示照片,睡眠和黑屏)。它还处理这两种模式之间的过渡。
它会定期(大约每小时一次)在指定的路径中查找配置文件。如果找到,将读取新的配置文件,并扫描文件夹(甚至可能是新的文件夹)中的照片。这意味着,如果您想更改照片,可以更改照片文件夹中的配置文件(该文件必须命名为 photoframe.ini)。这可以通过某些文本编辑器直接完成,或者您可以将照片文件夹放在某个可访问 FTP 或 SMB 的共享驱动器上,然后将新图片和新的 photoframe.ini 文件复制过去。
每小时一次,应用程序将查找 photoframe.ini 文件并重新扫描——因此您可以更新照片、更改照片文件夹、更改延迟时间、更改唤醒和睡眠小时,这些更改将在下一个小时的开头得到响应。
准备显示用的图片
OpenCV 库允许我们创建一个“全屏”窗口来显示图片。当然,这非常好,但显示器的尺寸(像素水平和垂直)可能与要显示的图片的尺寸不匹配。例如,图片的水平尺寸可能比显示器宽,
为了确保所有图片在显示器上尽可能好地显示,我们可能需要调整它们的大小,使它们适合显示器的边界——这可能涉及放大或缩小,具体取决于图片。这由函数底部附近的代码计算得出。
调整图片大小后,其至少一个尺寸将与显示器尺寸相同——因此,它将垂直填充显示器(两侧有一些额外空间)或水平填充显示器(顶部/底部有一些额外空间)。
现在为了确保这些额外空间显示为黑色像素,我们在侧面或顶部/底部添加了一些边框像素。这被称为图像加边框。在此加边框步骤之后,我们得到一张尺寸与显示器完全相同的图片,因此我们知道它已根据图片和屏幕的尺寸以最佳方式显示。
#-------------------------------------------------------------
# main photo frame loop
#
# - Run the photo frame code continuously displaying photos
# from the specified folder.
# - Go dark at bedtimehour and light up again at wakehour
# - Check every hour for a new instructions.ini file with
# parameter updates.
# - Rescan for pictures every hour in case user has deleted
# or added pictures
# - One idea is that an FTP server or SAMBA remote disk mount
# could be used to update the photos to be shown and to
# update the instructions.ini file to change parameters
#-------------------------------------------------------------
def runFotoFrame(params):
# grab our parameters that control operation of the
# digital foto frame
delay=params[0] # delay in seconds to show each picture
wakehour=params[1] # hour (int 24 hr format 0..23) to start showing
bedtimehour=params[2] # hour (in 24 hr format 0..23) to stop showing
photoFolder=params[3] # be real careful when changing this in config file
configfn=params[4] # name of config file to look for in top level folder
# determine if this is a windows OS based system
isWindows=sys.platform.startswith('win') # 'win32' or 'linux2' or 'linux'
# initialize a CV2 frame to cover the entire screen
cv2frame='frame'
cv2.namedWindow(cv2frame, cv2.WINDOW_NORMAL)
cv2.setWindowProperty(cv2frame, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
# let's find out what size of display we're working with
tmp=cv2.getWindowImageRect(cv2frame)
wid=float(tmp[2])
hgt=float(tmp[3])
# sometimes the getWindowImageRect returns nonsense like
# width or height of -1 or some other non useful value
if hgt<480. or wid<640.:
hgt=1080. # assume a 9h x 16w form factor
wid=1920.
#print(hgt,wid)
# scan the photoFolder for a list of picture files
pictureFiles=scanForFiles(photoFolder)
random.shuffle(pictureFiles) # randomly shuffle the picture files
print(datetime.datetime.now(),'Scan found',len(pictureFiles),'files')
# initialize for hourly and sleep processing
lastHour=datetime.datetime.now().hour
sleeping=False
done=False
if not isWindows:
os.system("xset s off") # screen blanking off
# and loop forever (until some key is hit)
while not done:
# during waking hours, display pictures
# during sleeping hours, keep display blanked
for fn in pictureFiles:
# let's see if it's time to do hourly tasks
now=datetime.datetime.now()
hour=now.hour
if not isWindows:
os.system("xset dpms force on");
#-- hourly tasks, only done when not sleeping
if hour!=lastHour and not sleeping:
lastHour=hour
if not isWindows:
os.system("xset s off") # screen blanking off
# try to read configuration file instructions.ini
controlFn=os.path.join(photoFolder,configfn)
result=checkForControlFile(controlFn,delay,wakehour,bedtimehour,photoFolder)
if result[4]: # set to true for successful config file read
delay=result[0]
wakehour=result[1]
bedtimehour=result[2]
photoFolder=result[3]
# rescan folder
pictureFiles=scanForFiles(photoFolder)
random.shuffle(pictureFiles)
print(datetime.datetime.now(),'Scan found',len(pictureFiles),'files')
#--- run always, do wake up tasks or sleep tasks
#
# for example wakehour might be 9am and bedtimehour might be 9pm
# wakehour=9 and bedtimehour=21 (12+9)
if hour>=wakehour and hour<bedtimehour:
# we are in wake up time of day
#--- if we were sleeping, then it is time to wake up
if sleeping:
print(datetime.datetime.now(),'Wake up')
if not isWindows:
os.system("xset s off") # screen blanking off
os.system("xset dpms force on");
sleeping=False
#--- display a photo
# handle fault in loading a picture
gotImg=False
try:
print('loading',fn)
img = cv2.imread(fn, 1)
if len(img)>0:
gotImg=True
except:
gotImg=False
if not gotImg:
continue
#-- now, maybe resize image so it shows up well without changing the aspect ratio
# add a border if the aspect ratio is different than the screen
# so we upscale or downscale so it maxes out either the
# horizontal or vertical portion of the screen
# then add a border around it to make sure any left-over
# parts of the screen are blacked out
widratio=wid/img.shape[1]
hgtratio=hgt/img.shape[0]
ratio=min(widratio,hgtratio)
dims=(int(ratio*img.shape[1]),int(ratio*img.shape[0]))
#print(fn,img.shape,ratio,dims[1],dims[0])
imgresized=cv2.resize(img,dims,interpolation = cv2.INTER_AREA)
#print(imgresized.shape)
# now, one dimension (width or height) will be same as screen dim
# and the other may be smaller than the screen dim.
# we're going to use cv.copyMakeBorder to add a border so we
# end up with an image that is exactly screen sized
widborder=max(1,int((wid-imgresized.shape[1])/2))
hgtborder=max(1,int((hgt-imgresized.shape[0])/2))
#print(hgtborder,widborder)
imgbordered=cv2.copyMakeBorder(imgresized,hgtborder,hgtborder,widborder,widborder,cv2.BORDER_CONSTANT)
#print('resized,bordered',imgbordered.shape)
# and now show the image that has been resized and bordered
cv2.imshow(cv2frame, imgbordered)
#--- now we pause while the photo is displayed, we do this
# by waiting for a key stroke.
k = cv2.waitKey(int(delay*1000)) & 0xff
# 255 if no key pressed (-1) or ascii-key-code (13=CR, 27=esc, 65=A, 32=spacebar)
if k!=0xff:
# if a key was pressed, exit the photo frame program
done=True
break
else:
#-- during sleep time we go here
# during sleep time, blank the screen
if not sleeping:
print(datetime.datetime.now(),'Going to sleep')
if not isWindows:
os.system("xset dpms force standby");
sleeping=True
k = cv2.waitKey(300*1000) & 0xff # wait 300 seconds
# 255 if no key pressed (-1) or ascii-key-code (13=CR, 27=esc, 65=A, 32=spacebar)
if k!=0xff:
done=True
# when the photo display session ends,
# we need to clean up the cv2 full-frame window
cv2.destroyWindow(cv2frame)
完整的源代码可在 GitHub digitalFotoFrame 上找到。
历史
- 版本 1.0,2023 年 11 月 27 日
- 版本 1.1,2023 年 11 月 28 日:添加了关于准备图片在物理屏幕上显示的讨论
- 版本 1.2,2023 年 11 月 29 日:将语言更改为 Python