Day and Night Time Lapse with RaspberryPI

Some days ago my wife brought planting soil with seeds of some kind and said: “wouldn’t that be nice if you and the boys plant these?”

I thought to myself “ohhh yeah that could be nice… but it would be much nicer if I’ll document the plant growth and make a time lapse clip… in fact that will be awesome.”

I have a couple of RaspBerry PIs lying around in my man cave… Prefect, I thought to myself.

I spent a couple of hours googling and searching for a python script that will shoot a time lapse series, a one that will auto adjust the exposure and ISO values for day and night shooting, and guess what, on the third or fourth google page i found something that could fit exactly for my needs, some dude with a PhD in computer science wrote something that seems perfect.

Happy and cheered up I downloaded the script from the GitHub repository that it is stored on and tested it on my RaspBerry PI…
the first functionality was OK but when I simulated a low light conditions, I was heartbroken… the script fails to switch to night mode and exits with an error.

I’ve tried to contact the author by Email, opened an issue on GitHub, tried to look for him on FaceBook (he’s name is common like “Mike O’niel” and I got about ten thousand results so I gave up on that really quickly), and even tried to contact him on LinkedIn profile that was linked to the GitHub repo…
nada de nada… no reply from MR PhD on any of the channels.

Then I thought to myself “OK, I’m familiar with python, I’m using it regularly, why won’t I just try to deep dive in those scripts and try to trace the reason for the failure…”
I wore my diving suit and dived in.

After an hour or two in which I practiced frustration I decided that MR PhDs scripts are not for me, it will be easier to write my own scripts.

First I searched for a method so the picamera could detect if the conditions are day conditions or night conditions, somehow I stumbled upon this GitHub repo (https://github.com/NestBoxTech/Ambient-Light-Monitoring/blob/master/ambient_lightMonitor.py) from which I adopted the first three functions, added another function to retrieve the needed file for brightness analyzation (by my design it was the last file created in a destination folder) and added all those functions to light.py file, perfect, now I know to calculate the brightness of a latest photo stored in a given directory.

Next I need to start a time lapse shooting loop and implement the light detection functions.

For that matter I navigated to picamera recipes and used the standard time laps loop that would be used for Day Mode (https://picamera.readthedocs.io/en/release-1.10/recipes1.html#capturing-timelapse-sequences) I enriched it with timestamped output and a custom location to store the images.

From the same source I took the “Capturing in low light” piece of code and manipulated it to run as a loop, just replaced the “camera.capture to camera.capture_continuouse… blab la bla…” and that would be my Night Mode, which if very similar to the Day Mode just some other parameters.

I’ve placed both Day and Night modes in the same timelapse.py file and defined them as functions, initially I thought to use the “GOTO” method (like in bash scripts) but after reading the following Stack Overflow (https://stackoverflow.com/questions/18863309/the-equivalent-of-a-goto-in-python/18863545) I changed my mind, although GOTO could be very helpful for my usecase.

Next I’ve added conditions to functions loops, so that after every photo that is taken the light detection function will process the photo and determine if the light conditions are night or day conditions.

After I tested that this works correctly, I’ve added another functionality to time lapse functions, based on the calculation of the light detection function a day mode loop was stopped and night mode loop was triggered, and vice versa.

Then I performed a tiny little smoke test and VIOLLA, I got myself a Day and Night time lapse mechanism.

But wait a second… now I got myself a bunch of photos, now I need to stich them into a video.

For that matter I was hoping to use OpenCV, I followed a tutorial I found and as soon as I executed the “stitching” script my RPI almost immediately got stuck… hmmm

I dunno if it’s because of the Raspberry PI that is not powerful enough to perform the stitching or whatever was the reason for the PI to get stuck, anyway I tried it one more time with less photos (about200), and it crashed again, so I’ve moved to another method and continued my search on google which led me to this wonderful script (https://tsaith.github.io/combine-images-into-a-video-with-python-3-and-opencv-3.html) it has to be changed a little cuz by default it brings an array that is ordered in arbitrary order which makes the time lapse video unusable, I replaced “listdir” with “glob” which from my experience is better for that matter and now I’m all good.

* (FYI, if you interested in more fancy script you can go over the comments and look for “ButAlsoHam”s comment, he created two more versions of the script)

I am ready for production!
where were those seeds that she brought…

Conclusions:
this mechanism works very well, some improvements will be made though when I’ll create the second version.
there are some tweaks that could make it more elegant.
if you have an idea please post a comment below.

For more detailed information and instructions please visit the project page: https://eli-bukin.com/home/projects/day-and-night-timelapse-with-raspberrypi/

Leave a Comment

Day and Night Time Lapse with RaspberryPI

In this tutorial I will demonstrate how to build an automatic mechanism for shooting timelapse sequences both for day and night conditions.

For that we will use python3.
Raspberry PI (choose your flavour, i’ve used 3B+).
PIcamera (i am using v1.3 but i guess you can go with NOIR as well).
if you want your camera to be portable you could use a powerbank as a power source, i’ll cover that in another post.

The script will start running in Day Mode, after each photo taken “check_light” function will be triggered to analyze the light condotion of the photo, if\when it will change, then mode will change from day to night and vice versa.
The last photo taken before the switch will be ineligible because it exceeds the light conditions therefore will be moved to “ineligible_files” folder.

  • you can drastically minimize the amount of ineligible photos by adjusting better the light value (check_light.light) that is the condition of breaking the loop.

The structure of the mechanism as i am using it (you of course can change it):
    day-and-night-timalapse (folder)
        timelapse (folder)

        ineligible_files (folder)
        check_light_func.py (file that holds the function that will calculate light conditions)
        timelapse.py (the main file that should be executed (timelapse loops))
        cam_adjust.py (a tyni little script that will start a preview of the camera so you could adjust the angle of the camera (not required but nice to have))

  • FYI, if you play with conditions and variables remember do delete “__pycache__” folder that will be created inside “day-and-night-timelapse” folder.

I will start from creating the function that will analyze light conditions:

the following piece of code are functions to analyze an image for it’s brightness, consist of main function and another three functions:
check_light – is the main function, will be called by timelapse loops to analyze the photos taken.
brightness_GreyScaleMean + brightness_GreyScaleRMS + brightness_Perceived – summed together will be used to determine of the conditions are for night or day.

#!/usr/bin/python

import glob
import os
import time
from PIL import Image, ImageStat
import math
#
import sys

def check_light(image):
   # gets the latest file from the list of files
   list_of_files = glob.glob(image) # * means all, if need specific format then *.csv
   latest_file = max(list_of_files, key=os.path.getctime)
   
   imgFile = latest_file
   
   #Covert image to greyscale, return average pixel brightness
   def brightness_GreyScaleMean():
      im = Image.open(imgFile).convert('L')
      stat = ImageStat.Stat(im)
      return stat.mean[0]
   
   #Covert image to greyscale, return RMS pixel brightness.
   def brightness_GreyScaleRMS():
      im = Image.open(imgFile).convert('L')
      stat = ImageStat.Stat(im)
      return stat.rms[0]   
   
   #Average pixels, then transform to "perceived brightness".
   def brightness_Perceived():
      im = Image.open(imgFile)
      stat = ImageStat.Stat(im)
      r,g,b = stat.mean
      return math.sqrt(0.241*(r**2) + 0.691*(g**2) + 0.068*(b**2))
   
   summ_of_all_three = brightness_GreyScaleMean()+brightness_GreyScaleRMS()+brightness_Perceived()
   summ_of_all_three_int = int(summ_of_all_three)
   check_light.light = summ_of_all_three_int
   check_light.img = imgFile
   

Next we will create the loops that will actually take the photos, set them as functions, and add some conditions so the loop could be broken when light conditions change:

the first loop will be the Day Mode loop:

### Day Mode
def day_mode():
   with picamera.PiCamera() as camera:
      camera.start_preview()
      time.sleep(2)
      for filename in camera.capture_continuous(dest_photo):
         print ('[INFO]: DM Captured %s' % filename)
         time.sleep(25) # interval in seconds
         check_light(dest_location)
         print ('[INFO]: light is ' + str(check_light.light))
         if check_light.light < 240:
            print ('[INFO]: '+(check_light.img +' is a night photo'))
            ineligible = os.path.basename(check_light.img)
            shutil.move(check_light.img, ineligible_files+ineligible)
            hellonight()
            camera.close()
            night_mode()
            break 
         else: 
            print ('[INFO]: '+(check_light.img +' is a day photo'))

day_mode: function name,
camera parameters:
    i’ve used default parameters which work best for me, but you could define yours.
variables:
    dest_photo: the path to photo’s location and file name,
    time.sleep: is the interval between the photos,
    dest_location: the path to photos folder to be used by check_light function.
    ineligible: a file that exceeds the mode conditions therefore has to be moved out from the sequence.
conditions:
    check_light.light: is a condition under which the loop breaks, i’ve reached the conclusion that 240 works best for me, but you could play with it.

After finishing with Day Mode let’s proceed to Night Mode, the concept is very similar, a for loop that configured as a function with some parameters and conditions:

### Night Mode
def night_mode():
   camera = PiCamera(resolution=(1920, 1080), framerate=Fraction(1, 6))
   camera.shutter_speed = 6000000
   camera.iso = 800
   # Give the camera a good long time to set gains and
   # measure AWB (you may wish to use fixed AWB instead)
   sleep(5)
   camera.exposure_mode = 'off'
   # Finally, capture an image with a 6s exposure. Due
   # to mode switching on the still port, this will take
   # longer than 6 seconds
   ###
   for filename in camera.capture_continuous(dest_photo):
      print('[INFO]: NM Captured %s' % filename)
      time.sleep(15) # interval in seconds
      check_light(dest_location)
      print ('[INFO]: light is ' + str(check_light.light))
      if check_light.light < 650:
         print ('[INFO]: '+(check_light.img +' is a night photo'))
      else: 
         print ('[INFO]: '+(check_light.img +' is a day photo'))
         ineligible = os.path.basename(check_light.img)
         shutil.move(check_light.img, ineligible_files+ineligible)
         helloday()
         camera.close()
         day_mode()
         break 

night_mode: function name,
camera parameters:
    resolution: self explanatory,
    framerate: self explanatory,
    shutter_speed: the maximum for V1.3 camera module is 6000000, for module V2 it could be set to 10000000,
    iso: for night conditions could be set to 800 (max value).
variables:
    dest_photo: path to photo’s location and filename,
    time.sleep: is the interval
    dest_location: path to photos folder to be used by check_light function.
    ineligible: a file that exceeds the mode conditions therefore has to be moved out from the sequence.
conditions:
    <650 is the condition under which the loop will break and Day Mode will be triggered, 650 is my value that works for me but you could play with it.

Additional two tiny little functions that are used as informers:

def helloday():
   print ('[INFO]: Switching to Day Mode')
   
def hellonight():
   print ('[INFO]: Switching to Night Mode')

Last but not least we get to imports and variables declaration:

import time
import picamera
from picamera import PiCamera
from fractions import Fraction
import sys
from check_light_func import check_light
from time import sleep
from datetime import datetime
import os
import shutil

dest_photo = '<PATH/TO/WHERE/YOU/WANT/TO/STORE/YOUR/PHOTOS/>{timestamp:%a-%d.%m.%Y-%H-%M-%S}'+'_{counter:05d}.jpg'
dest_location = '<PATH/TO/WHERE/THE/PHOTOS/ARE/STORED>*'
ineligible_files = '<PATH-TO-INELIGIBLE-FILES-FOLDER>'

Full timelapse.py file should look like this:
as you can see it starts in Day Mode and switches to Night Mode when the right conditions met.

#!/usr/bin/python

import time
import picamera
from picamera import PiCamera
from fractions import Fraction
import sys
from check_light_func import check_light
from time import sleep
from datetime import datetime
import os
import shutil

dest_photo = '/home/pi/Desktop/sysconfig_rpi-sputnik-portable-1/day-and-night-timelapse/timelapse/{timestamp:%a-%d.%m.%Y-%H-%M-%S}'+'_{counter:05d}.jpg'
dest_location = '/home/pi/Desktop/sysconfig_rpi-sputnik-portable-1/day-and-night-timelapse/timelapse/*'
ineligible_files = '/home/pi/Desktop/sysconfig_rpi-sputnik-portable-1/day-and-night-timelapse/ineligible_files/'

def helloday():
   print ('[INFO]: Switching to Day Mode')
   
def hellonight():
   print ('[INFO]: Switching to Night Mode')

### Night Mode
def night_mode():
   camera = PiCamera(resolution=(1920, 1080), framerate=Fraction(1, 6))
   camera.shutter_speed = 6000000
   camera.iso = 800
   # Give the camera a good long time to set gains and
   # measure AWB (you may wish to use fixed AWB instead)
   sleep(5)
   camera.exposure_mode = 'off'
   # Finally, capture an image with a 6s exposure. Due
   # to mode switching on the still port, this will take
   # longer than 6 seconds
   #camera.capture('dark9.jpg')
   ###
   for filename in camera.capture_continuous(dest_photo):
      print('[INFO]: NM Captured %s' % filename)
      time.sleep(15) # interval in seconds
      check_light(dest_location)
      print ('[INFO]: light is ' + str(check_light.light))
      if check_light.light < 650:
         print ('[INFO]: '+(check_light.img +' is a night photo'))
      else: 
         print ('[INFO]: '+(check_light.img +' is a day photo'))
         ineligible = os.path.basename(check_light.img)
         shutil.move(check_light.img, ineligible_files+ineligible)
         helloday()
         camera.close()
         day_mode()
         break 

### Day Mode
def day_mode():
   with picamera.PiCamera() as camera:
      camera.start_preview()
      time.sleep(2)
      for filename in camera.capture_continuous(dest_photo):
         print ('[INFO]: DM Captured %s' % filename)
         time.sleep(25) # interval in seconds
         check_light(dest_location)
         print ('[INFO]: light is ' + str(check_light.light))
         if check_light.light < 240:
            print ('[INFO]: '+(check_light.img +' is a night photo'))
            ineligible = os.path.basename(check_light.img)
            shutil.move(check_light.img, ineligible_files+ineligible)
            hellonight()
            camera.close()
            night_mode()
            break 
         else: 
            print ('[INFO]: '+(check_light.img +' is a day photo'))
            
day_mode()


So now we have a big pile of photos that should be stitched into one video, that we will accomplish with OpenCV.

Create another file “stitch_photos_to_video.py” with the following code:

#!/usr/local/bin/python3

import cv2
import argparse
import os
import functools
from functools import cmp_to_key

def help ():
 with open(__file__, "r", encoding="UTF-8") as open_file:
  for line in open_file:
   if ":" in line and "def" in line and "if " not in line:
    print(line.split(" ")[1])

#Function to check if string can be cast to int
def isnum (num):
 try:
  int(num)
  return True
 except:
  return False

#Numerically sorts filenames
def image_sort_name (x,y):
 
 x = int(x.split(".")[0])
 y = int(y.split(".")[0])
 return x-y

#Sort filenames by their last edited datetime stamp
#This way, even if you didn't name and number your frames, you can still use their order of creation for sorting
#10/10! :)
def image_sort_datetime (x,y):
 x = os.path.getmtime(x)
 y = os.path.getmtime(y)
 return x - y

def render ():
 # Construct the argument parser and parse the arguments
 arg_parser = argparse.ArgumentParser()
 arg_parser.add_argument("-e", "--extension", required=False, default='png', help="Extension name. default is 'png'.")
 arg_parser.add_argument("-o", "--output", required=False, default='output.mp4', help="Output video file.")
 arg_parser.add_argument("-d", "--directory", required=False, default='.', help="Specify image directory.")
 arg_parser.add_argument("-fps", "--framerate", required=False, default='10', help="Set the video framerate.")
 arg_parser.add_argument("-s", "--sort", required=False, default='numeric', help="Determines the type of file-order sort that will be used. Current values: none, numeric, datetime")
 arg_parser.add_argument("-t", "--time", required=False, default='none', help="Sets the framerate so that the video length matches the time in seconds.")
 arg_parser.add_argument("-v", "--visual", required=False, default='false', help="If 'true' then will display preview window.")
 arg_parser.add_argument("-safe", "--safe", required=False, default='true', help="If 'false' then will try to render all images, not just consistenly-sized ones.")
 args = vars(arg_parser.parse_args())

 # Arguments
 dir_path = args['directory']
 ext = args['extension']
 output = args['output']
 framerate = args['framerate']
 sort_type = args['sort']
 time = args['time']
 visual = args['visual']

 #Flips bools to a bool-type
 visual = visual == "true"
 safe = args['safe'] == "true"

 #Sets the framerate to argument, or defaults to 10
 if not isnum(framerate):
  framerate = 10
 else:
  framerate = int(framerate)

 #Get the files from directory
 images = []
 for f in os.listdir(dir_path):
  if f.endswith(ext):
   images.append(f)

 #Sort the files found in the directory
 if sort_type == "numeric":
  int_name = images[0].split(".")[0]
  if isnum(int_name):
   images = sorted(images, key=cmp_to_key(image_sort_name))
  else:
   print("Failed to sort numerically, switching to alphabetic sort")
   images.sort()
 elif sort_type == "datetime":
  images = [dir_path + "/" + im for im in images]
  images = sorted(images, key=cmp_to_key(image_sort_datetime))
  images = ["".join(im.split(dir_path + "/")[1:]) for im in images]
 elif sort_type == "alphabetic":
  images.sort()

 #Change framerate to fit the time in seconds if a time has been specified.
 #Overrides the -fps arg
 if isnum(time):
  framerate = int(len(images) / int(time))
  print("Adjusting framerate to " + str(framerate))

 # Determine the width and height from the first image
 image_path = os.path.join(dir_path, images[0])
 frame = cv2.imread(image_path)

 if visual:
  cv2.imshow('video',frame)
 regular_size = os.path.getsize(image_path)
 height, width, channels = frame.shape

 # Define the codec and create VideoWriter object
 fourcc = cv2.VideoWriter_fourcc(*'mp4v') # Be sure to use lower case
 out = cv2.VideoWriter(output, fourcc, framerate, (width, height))

 for n, image in enumerate(images):
  image_path = os.path.join(dir_path, image)
  image_size = os.path.getsize(image_path)
  if image_size < regular_size / 1.5 and safe:
   print("Cancelled: " + image)
   print("Abnormal image size. Use the '-safe false' to disable this check")
   continue

  frame = cv2.imread(image_path)
  out.write(frame) # Write out frame to video
  if visual:
   cv2.imshow('video', frame)

  if (cv2.waitKey(1) & 0xFF) == ord('q'): # Hit `q` to exit
   break
  if n%100 == 0:
   print("Frame " + str(n))

 # Release everything if job is finished
 out.release()
 cv2.destroyAllWindows()

 print("The output video is {}".format(output))
render()

Usage: stitch_photos_to_video.py -e jpg -o /OUTPUT-FOLDER/FILE-NAME.EXT -d /INPUT-FOLDER -fps 20 -s datetime -v false -safe false

arguments:
    “-e”, “–extension”, required=False, default=’png’, help=”Extension name. default is ‘png’.”
    “-o”, “–output”, required=False, default=’output.mp4′, help=”Output video file.”
    “-d”, “–directory”, required=False, default=’.’, help=”Specify image directory.”
    “-fps”, “–framerate”, required=False, default=’10’, help=”Set the video framerate.”
    “-s”, “–sort”, required=False, default=’numeric’, help=”Determines the type of file-order sort that will be used. Current values: none, numeric, datetime”
    “-t”, “–time”, required=False, default=’none’, help=”Sets the framerate so that the video length matches the time in seconds.”
    “-v”, “–visual”, required=False, default=’false’, help=”If ‘true’ then will display preview window.”
    “-safe”, “–safe”, required=False, default=’true’, help=”If ‘false’ then will try to render all images, not just consistenly-sized ones.”

To adjust the camera angle when preparing for footage you can use the following piece of code, just paste it in “cam_adjust.py” and you can run it from terminal.

  • insert tha same resolution as in the timelapse script.
  • if you are not using a monitor connected to the PI but connected via VNC then go to VNC options > troubleshooting > and enable “direct capture mode”, then you’ll be able to view the camera preview image over the vnc connection.
    if you are using SSH then no sup for you, you’ll have to adjust the angle by viewing the photos.
import time
import picamera

with picamera.PiCamera() as camera:
    camera.resolution = (1920, 1080)
    camera.start_preview()
    time.sleep(90)
    camera.stop_preview()

And that’s about it… now you can start creating your timelapse videos.

i want to give credits to KB sources i was inspired by:
light detection was inspired by https://github.com/NestBoxTech/Ambient-Light-Monitoring/blob/master/ambient_lightMonitor.py
timelapse was inspired by https://picamera.readthedocs.io/en/release-1.10/recipes1.html#capturing-timelapse-sequences
stitching process was adopted from https://tsaith.github.io/combine-images-into-a-video-with-python-3-and-opencv-3.html
* in the comments of the stitching process there is a dude who modified the script to a much better version (which is shown here) so i think you should use it instead of the original.