Close

Daily time lapse movies from NixieBot

A project log for NixieBot

A neon faced, twitter connected, clock, social monitor and picture server

Robin BussellRobin Bussell 10/10/2016 at 20:180 Comments

Recent new feature: As well as tweeting user requested images NixieBot will send out a daily movie tweet about "how my day went". This movie is composed of one frame taken every 15 minutes throughout the day so you can see how lighting changes and weather affect the images the camera produces. The word to display is either picked from the last user requested word from the previous 7.5 minutes or else, if there was no request made during that time period, the most popular word (of four or more letters in length) used in the random tweet feed is displayed. Certain very common words:

boringWords=["this","that","with","from","have","what","your","like","when","just"]
are filtered out to make it more interesting. The movie attempts to summarize the twitter 'ZeitGeist' for the day.

How it works:

The time interval between frames is kept in the timeLapseInterval variable, every time round the loop in the main runClock() function this happens:

    if  int(t.minute) % timeLapseInterval == 0 :
                doTimeLapse()  #either choose a frame from recent first frames or, if none available, take one from random stats
                               #if it's the appointed hour, generate and tweet the time lapse movie. 
            else : 
                lapseDone = False 
The minutes value of the time variable t (set at the top of the loop) is checked to see if it's a multiple of the required interval, if so the doTimelapse() function is called. The lapseDone variable acts as a flag to make sure that doTimeLapse only gets called once per interval. Without this, if the timelapse process takes less than a minute to run, it would be called multiple times.

So what does doTimelapse do then? here it is:

def doTimeLapse() :
    global cam
    global lapseDone
    global makeMovie
    global effx
    global effxspeed
    if lapseDone :
        return
    print("doTimeLapse called")    
    #delete all lapse*.jpg older than (lapseTime / 2)
    #pick youngest lapse*.jpg file and copy to lapseFrames directory    youngestName = ""
    youngestTime = time.time()
    youngestFile= ""
    timeLimit = time.time() - ((timeLapseInterval/2) * 60)
    files = glob(basePath+"lapse*.jpg")
    for f in  files :
        fileTime = os.path.getatime(f)
        if fileTime < timeLimit :
            print("deleting ", f, " age =", (time.time() - fileTime)/60)
            os.remove(f)
        elif fileTime < youngestTime :
            youngtestTime = fileTime
            youngestFile = f
    if youngestFile != "" :
        print("moving file", youngestFile , "into frame store")
        move(youngestFile, basePath+"lapseFrames/")
    else :
        #take frame of most popular word in random tweet sample of four or more letters
        words=randstream.allWords()['wordList']
        bigEnough=[]
        for w in words :
            if len(w) >= 4 and w not in boringWords and "&" not in w:
                bigEnough.append(w)
        c = collections.Counter(bigEnough)
        topWords=c.most_common(20)
        theWord=topWords[0][0]
        print(topWords, theWord)        
        makeMovie = False
        stashfx = effx
        stashspeed = fxspeed
        setEffex(0,0)
        lockCamExposure(cam)
        displayString(theWord)
        cam.capture(basePath+"/lapseFrames/lapse-"+time.strftime("%Y%m%d-%H%M%S")+".jpg",resize=(320,200))
        unlockCamExposure(cam)
        setEffex(stashfx,stashspeed)
        
    lapseFrames = glob(basePath+"lapseFrames/*.jpg")
    #if there are now 96 files in the frames folder, make a movie and tweet it out #NixieLapse
    print(len(lapseFrames),"lapse frames found")
    if len(lapseFrames) >=96 :
        print("making daily time lapse")
        delay=20
        mresult = call(["gm","convert","-delay",str(delay),"-loop", "0", basePath+"/lapseFrames/*.jpg","Tlapse.gif"]) 
        print("Make movie command result code = ",mresult)
        if mresult == 0 :
            uploadRetries = 0
            while uploadRetries < 3 : 
                try:
                    pic =open("Tlapse.gif","rb")
                    print(">>>>>>>>>>>>> Uploading Timelapse Movie ", datetime.datetime.now().strftime('%H:%M:%S.%f'))
                    response = twitter.upload_media(media=pic )
                    print(">>>>>>>>>>>>> Updating status ", datetime.datetime.now().strftime('%H:%M:%S.%f'))  
                    twitter.update_status( status="This is how my day went: #NixieBotTimelapse", 
                       media_ids=[response['media_id']] )
                    print(">>>>>>>>>>>>> Done  ", datetime.datetime.now().strftime('%H:%M:%S.%f'))
                    uploadRetries = 200
                except BaseException as e:
                    print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Tweeting movie exception!" + str(e))
                    uploadRetries += 1 
            move(basePath+"Tlapse.gif", basePath+"lapseFrames/Tlapse"+time.strftime("%Y%m%d-%H%M%S")+ ".gif")        
        for f in lapseFrames :
            os.remove(f)
    lapseDone = True        
    return()
    
In between calls to doTimeLapse the word displaying and picture taking routines save the image as a file with name composed of the string "lapse" then a timestamp.

doTimeLapse() first iterates through all files named lapse*.jpg, it discards any that were created in the first half of the current lapse period and keeps track of the youngest file it finds that was created in the second half of the lapse period.

If this process finds a youngest file it will move it into a subdirectory where all frames for the day's movie are kept.

If no file is found then it retrieves a list of all words used in current buffer of random tweets by invoking the allWords() method of the randstream object (this TwythonStreamer object is in charge of receiving random tweets and keeps a circular buffer of the last 1000 tweets received, this buffer is a deque ).

This word list is first iterated through to remove words that are too small or in the boringWords list.

The resulting pruned word list is then fed into another of python's many handy collection types , the Counter.

A counter accepts values and compiles a dictionary of unique values against a count of how many times that value occurs.

Counters also have a handy most_common() method which is used in this case, to extract the most used word (actually for reasons lost in the mist of debugging time it extracts the top twenty words then picks the number one from those ... that should probably get neatened up one day).

Having found the most popular word it then displays it, takes a photo, then stores the image in the directory where the other timelapse frames are kept.

Next job is to see if there are a full day's worth of frames yet (and, in explaining all this to you I have found a potential bug, there's a hard coded value for number of frames per day when it should be calculated from the timeLapseInterval variable ... explaining your code to someone is a great technique for optimising and debugging! )

If a full day's worth of frames have been recorded then they are assembled into an animated gif with the "gm convert " command and that is posted to twitter (here there is substantial code duplication as there are other places where movies are posted to twitter... one day it might get refactored out into a separate function but this was a quick and dirty feature addition )

Finally the lapseDone flag is set so that the routine doesn't get called again next time round the main clock loop.

So there you go ... an insight into what happens when I start coding and keep adding things without a refactor ... lots of global variables serving mysterious purposes and code duplication, it still works though!

Discussions