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.