Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Python

A DIY Digital Picture Frame in Python

5.00/5 (2 votes)
21 Dec 2023CPOL7 min read 7.9K  
A DIY digital picture frame application in Python
A Python application reads pictures and displays them in a slide-show. Programmable wake and sleep times allow the display to be blanked during sleeping hours. The show can be modified from time to time to change features or pictures to be displayed.

Introduction

A computer display shows photos from a library of photos. This software application written in Python can run on almost any computer that runs python3 and the cv2 image library.

Background

There are numerous applications available for digital photo display applications. However, I had an old retired Raspberry Pi that was looking for a job, so naturally, a tinkering engineer should spend weeks writing some software so that the grey bearded old Raspberry Pi could have a useful life. What can I say, some people get sentimental about their pets, I guess the Pi felt like some kind of a pet for me...

I decided to use the Python environment as this allowed me to develop on Windows, yet run the software on Linux (Raspberian).

Using the Code

The code is stored in a single Python source file which is run from a command line using the python3 interpreter.

Python
python3 digitalFotoFrame.py  photoframe.ini

There is a single command line parameter - the name of some configuration file that allows the operation of the photo frame software to be configured.

Configuration File

The configuration file is a simple text file with the following four lines in key-value format. The lines can be in any order. Comment lines beginning with a # are ignored.

DELAY=4.5
WAKE=6
SLEEP=22
PATH=/media/HDD 

There are four keywords (they are case sensitive, so use all upper case!)

DELAY=4.5 This means to show each picture for 4.5 seconds.
WAKE=6 This configures the wake-up time. At this time, the show starts in this case 0600 hours.
SLEEP=22 This is the sleep time - at this time, the show stops and screen blanks - in this case, 2200 hours or 10pm.
PATH=/media/HDD This is the folder where the pictures are stored - there can be subfolders here with pictures.

Between the WAKE and SLEEP hours, pictures will be continuously displayed in a random order.

Between the SLEEP and WAKE hours (night time), the screen will be blanked and no pictures are shown.

The selected PATH may contain many files, organized into folders and subfolders. Only files with .jpg or .png will be considered for display. Other files may be in this folder, but they will be ignored.

Software Overview

The software is a single Python source file written for Python 3. It consists of just three Python functions and a main program.

At a top level view, the application will initialize by reading the configuration file (passed in on the command line). From this file, four parameters (as shown above) are extracted.

At an operational level, the PATH is scanned for pictures to display. These pictures are displayed in a random order with each being held on the screen for the DELAY time from the configuration file. This happens between the WAKE and SLEEP hours of the day. Once per hour - if a configuration file is found - the parameters are re-read (possibly changed) and there is a new scan for photos.

Between the SLEEP and WAKE hours, the screen is blanked and no photos are displayed.

Software Functions

The software uses the cv2 library (opencv) for image functions such as reading, resizing, displaying the pictures. See the link below for full details on this Python library. It is available for Windows, MacOS, Linux, and Raspberian. This library must be installed on the machine you are running the application on.

pip install opencv-python

or:

pip3 install opencv-python 

link to OPENCV-PYTHON

The other dependencies as far as Python libraries are all standard ones, so no special installs are required.

Top Level Program Code

The following shows the top-level Python code for the application. It basically tries to find and read the configuration file. This defines the parameters for photoFolder, delay, wakehour, and bedtimehour. If the configuration file cannot be read successfully, the application cannot run and it will terminate, displaying an error message.

Once these parameters are read successfully, the main function of the application is called (runFotoFrame) to display the files and handle wake and sleep times.

Python
#------------------------------------------------------------
# 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)  

Scanning for Photos to Display

The function scanForFiles is used to recursively scan the PATH folder and any sub-folders for pictures to display. It returns a list of file names of pictures to display. I've used it for a structure of over 10000 files successfully.

Python
#-------------------------------------------------------------
#--- 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 

Nothing special here - the Python standard os.scandir() function is used to scan for all files. Files with .jpg or .png extensions are added to the list. Any folders are recursively scanned for .jpg or .png files and these are added to the list as well.

Reading the Configuration File

The configuration file contains settings needed for operation. The function checkForControlFile() is used to try to read these settings from an external text file and return a list of the values.

The values are returned as a list of five elements.

  • result[0] - the delay in seconds
  • result[1] - the wake time in hours (0..23)
  • result[2] - the sleep time in hours (0..23)
  • result[3] - the path where the pictures are stored
  • result[4] - true if the parameters are read successfully
Python
#-------------------------------------------------------------
#--- 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 

Main Picture Display Function

The main picture display function is the more complex function of the application. It handles the two modes (awake and displaying photos, asleep and a blank screen). It also handles the transitions between these two modes.

Periodically (about once an hour), it will look in the specified path for a configuration file. If one is found, the new configuration file is read, and the folder (perhaps a new folder even) are scanned for photos. This means that if you want to change the photos, you can change the configuration file in the photos folder (which must be named photoframe.ini). This could be done directly (via some text editor) or you could potentially have the photos folder on some FTP accessible or SMB accessible shared drive and just copy new pictures and a new photoframe.ini file there).

Once per hour, the application will look for a photoframe.ini file and rescan - so you can update photos, change the photos folder, change the delay time, change the wake and sleep hours, and these changes will be responded to at the top of the next hour.

Preparing Pictures for Display

The OpenCV library allows us to create a 'full-screen' window to display the pictures in. Of course this is very nice, but the dimensions of the display (pixels horizontal and vertical) may not match the dimensions of the picture being displayed. The horizontal dimension of the picture might be wider than the display for example,   

To make sure all the pictures display as well as possible on the display, we may need to resize them so that they fit within the bounds of the display - this may involve scaling up or down depending on the picture.  This is figured out by the code towards the bottom of the function.   

After resizing the picture, at least one of its dimensions will be the same as the dimension of the display - so it will fill the display either vertically (with some extra space on the sides) or horizontally (with some extra space at the top / bottom).

Now to make sure this extra space is displayed as black pixels, we add some border pixels to either the sides or the top/bottom. This is called bordering the image. After this bordering step, we have a picture that is exactly the same dimensions as the display so we know it is displayed in the best way possible given the dimensions of the picture and the screen.

Python
#-------------------------------------------------------------
# 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) 

The complete source code is available on GitHub digitalFotoFrame.

History

  • Version 1.0, November 27, 2023
  • Version 1.1, November 28, 2023: Added discussion on preparing pictures for displaying on the physical screen
  • Version 1.2, November 29, 2023: Changed language to Python

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)