#!/usr/bin/env python3 """ pi-timolo - Raspberry Pi Long Duration Timelapse, Motion Tracking, with Low Light Capability written by Claude Pageau Jul-2017 (release 7.x) This release uses OpenCV to do Motion Tracking. It requires updated config.py Oct 2020 Added panoramic pantilt option plus other improvements. """ from __future__ import print_function PROG_VER = "ver 12.65" # Requires Latest 12.5 release of config.py __version__ = PROG_VER # May test for version number at a future time import os WARN_ON = False # Add short delay to review warning messages MY_PATH = os.path.abspath(__file__) # Find the full path of this python script # get the path location only (excluding script name) BASE_DIR = os.path.dirname(MY_PATH) BASE_FILENAME = os.path.splitext(os.path.basename(MY_PATH))[0] PROG_NAME = os.path.basename(__file__) LOG_FILE_PATH = os.path.join(BASE_DIR, BASE_FILENAME + ".log") HORIZ_LINE = "-------------------------------------------------------" print(HORIZ_LINE) print("%s %s written by Claude Pageau" % (PROG_NAME, PROG_VER)) print(HORIZ_LINE) print("Loading Wait ....") # import python library modules import datetime import logging import sys import subprocess import shutil import glob import time import math from threading import Thread from fractions import Fraction import numpy as np from PIL import Image from PIL import ImageFont from PIL import ImageDraw # Attempt to import dateutil try: from dateutil.parser import parse except ImportError: print("WARN : Could Not Import dateutil.parser") print(" Disabling TIMELAPSE_START_AT, MOTION_START_AT and VideoStartAt") print( " See https://github.com/pageauc/pi-timolo/wiki/Basic-Trouble-Shooting#problems-with-python-pip-install-on-wheezy" ) WARN_ON = True # Disable get_sched_start if import fails for Raspbian wheezy or Jessie TIMELAPSE_START_AT = "" MOTION_START_AT = "" VIDEO_START_AT = "" # Attempt to import pyexiv2. Note python3 can be a problem try: # pyexiv2 Transfers image exif data to writeTextToImage # For python3 install of pyexiv2 lib # See https://github.com/pageauc/pi-timolo/issues/79 # Bypass pyexiv2 if library Not Found import pyexiv2 except ImportError: print("WARN : Could Not Import pyexiv2. Required for Saving Image EXIF meta data") print( " If Running under python3 then Install pyexiv2 library for python3 per" ) print(" cd ~/pi-timolo") print(" ./install-py3exiv2.sh") WARN_ON = True except OSError as err: print("WARN : Could Not import python3 pyexiv2 due to an Operating System Error") print(" %s" % err) print(" Camera images will be missing exif meta data") WARN_ON = True """ This is a dictionary of the default settings for pi-timolo.py If you don't want to use a config.py file these will create the required variables with default values. Change dictionary values if you want different variable default values. A message will be displayed if a variable is Not imported from config.py. Note: plugins can override default and config.py values if plugins are enabled. This happens after config.py variables are initialized """ default_settings = { "CONFIG_FILENAME": "default_settings", "CONFIG_TITLE": "No config.py so using internal dictionary settings", "PLUGIN_ON": False, "PLUGIN_NAME": "shopcam", "VERBOSE_ON": True, "LOG_TO_FILE_ON": False, "DEBUG_ON": False, "IMAGE_NAME_PREFIX": "cam1-", "IMAGE_WIDTH": 1920, "IMAGE_HEIGHT": 1080, "IMAGE_FORMAT": ".jpg", "IMAGE_JPG_QUAL": 95, "IMAGE_ROTATION": 0, "IMAGE_VFLIP": True, "IMAGE_HFLIP": True, "IMAGE_GRAYSCALE": False, "IMAGE_PREVIEW": False, "IMAGE_PIX_AVE_TIMER_SEC": 15, "IMAGE_NO_NIGHT_SHOTS": False, "IMAGE_NO_DAY_SHOTS": False, "IMAGE_SHOW_STREAM": False, "STREAM_WIDTH": 320, "STREAM_HEIGHT": 240, "STREAM_FPS": 20, "STREAM_STOP_SEC": 0.7, "SHOW_DATE_ON_IMAGE": True, "SHOW_TEXT_FONT_SIZE": 18, "SHOW_TEXT_BOTTOM": True, "SHOW_TEXT_WHITE": True, "SHOW_TEXT_WHITE_NIGHT": True, "NIGHT_TWILIGHT_MODE_ON": True, "NIGHT_TWILIGHT_THRESHOLD": 90, "NIGHT_DARK_THRESHOLD": 50, "NIGHT_BLACK_THRESHOLD": 4, "NIGHT_SLEEP_SEC": 30, "NIGHT_MAX_SHUT_SEC": 5.9, "NIGHT_MAX_ISO": 800, "NIGHT_DARK_ADJUST": 4.7, "TIMELAPSE_ON": True, "TIMELAPSE_DIR": "media/timelapse", "TIMELAPSE_PREFIX": "tl-", "TIMELAPSE_START_AT": "", "TIMELAPSE_TIMER_SEC": 300, "TIMELAPSE_CAM_SLEEP_SEC": 4.0, "TIMELAPSE_NUM_ON": True, "TIMELAPSE_NUM_RECYCLE_ON": True, "TIMELAPSE_NUM_START": 1000, "TIMELAPSE_NUM_MAX": 2000, "TIMELAPSE_EXIT_SEC": 0, "TIMELAPSE_MAX_FILES": 0, "TIMELAPSE_SUBDIR_MAX_FILES": 0, "TIMELAPSE_SUBDIR_MAX_HOURS": 0, "TIMELAPSE_RECENT_MAX": 40, "TIMELAPSE_RECENT_DIR": "media/recent/timelapse", "MOTION_TRACK_ON": True, "MOTION_TRACK_QUICK_PIC_ON": False, "MOTION_TRACK_INFO_ON": True, "MOTION_TRACK_TIMEOUT_SEC": 0.3, "MOTION_TRACK_TRIG_LEN": 75, "MOTION_TRACK_MIN_AREA": 100, "MOTION_TRACK_QUICK_PIC_BIGGER": 3.0, "MOTION_DIR": "media/motion", "MOTION_PREFIX": "mo-", "MOTION_START_AT": "", "MOTION_VIDEO_ON": False, "MOTION_VIDEO_FPS": 15, "MOTION_VIDEO_WIDTH": 640, "MOTION_VIDEO_HEIGHT": 480, "MOTION_VIDEO_TIMER_SEC": 10, "MOTION_TRACK_MINI_TL_ON": False, "MOTION_TRACK_MINI_TL_SEQ_SEC": 20, "MOTION_TRACK_MINI_TL_TIMER_SEC": 4, "MOTION_TRACK_PANTILT_SEQ_ON": False, "MOTION_FORCE_SEC": 3600, "MOTION_NUM_ON": True, "MOTION_NUM_RECYCLE_ON": True, "MOTION_NUM_START": 1000, "MOTION_NUM_MAX": 500, "MOTION_SUBDIR_MAX_FILES": 0, "MOTION_SUBDIR_MAX_HOURS": 0, "MOTION_RECENT_MAX": 40, "MOTION_RECENT_DIR": "media/recent/motion", "MOTION_DOTS_ON": False, "MOTION_DOTS_MAX": 100, "MOTION_CAM_SLEEP": 0.7, "CREATE_LOCKFILE": False, "VIDEO_REPEAT_ON": False, "VIDEO_REPEAT_WIDTH": 1280, "VIDEO_REPEAT_HEIGHT": 720, "VIDEO_DIR": "media/videos", "VIDEO_PREFIX": "vid-", "VIDEO_START_AT": "", "VIDEO_FILE_SEC": 120, "VIDEO_SESSION_MIN": 60, "VIDEO_FPS": 30, "VIDEO_NUM_ON": False, "VIDEO_NUM_RECYCLE_ON": False, "VIDEO_NUM_START": 100, "VIDEO_NUM_MAX": 20, "PANTILT_ON": False, "PANTILT_IS_PIMORONI": False, "PANTILT_HOME": (0, -10), "PANTILT_SPEED": 0.5, "PANTILT_SEQ_ON": False, "PANTILT_SEQ_TIMER_SEC": 600, "PANTILT_SEQ_IMAGES_DIR": "media/pantilt_seq", "PANTILT_SEQ_IMAGE_PREFIX": "seq-", "PANTILT_SEQ_DAYONLY_ON": True, "PANTILT_SEQ_RECENT_DIR": "media/recent/pt-seq", "PANTILT_SEQ_NUM_MAX": 200, "PANTILT_SEQ_NUM_ON": True, "PANTILT_SEQ_NUM_START": 1000, "PANTILT_SEQ_NUM_RECYCLE_ON": True, "PANTILT_SEQ_NUM_MAX": 200, "PANTILT_SEQ_STOPS": [ (90, 10), (45, 10), (0, 10), (-45, 10), (-90, 10), ], "PANO_ON": False, "PANO_DAYONLY_ON": True, "PANO_TIMER_SEC": 160, "PANO_IMAGE_PREFIX": "pano-", "PANO_NUM_START": 1000, "PANO_NUM_MAX": 10, "PANO_NUM_RECYCLE": True, "PANO_PROG_PATH": "./image-stitching", "PANO_IMAGES_DIR": "./media/pano/images", "PANO_DIR": "./media/pano/panos", "PANO_CAM_STOPS": [ (36, 10), (0, 10), (-36, 10), ], "SPACE_TIMER_HOURS": 0, "SPACE_TARGET_MB": 500, "SPACE_MEDIA_DIR": "/home/pi/pi-timolo/media", "SPACE_TARGET_EXT": "jpg", "web_server_port": 8080, "web_server_root": "media", "web_page_title": "PI-TIMOLO Media", "web_page_refresh_on": True, "web_page_refresh_sec": "900", "web_page_blank": False, "web_image_height": "768", "web_iframe_width_usage": "70%", "web_iframe_width": "100%", "web_iframe_height": "100%", "web_max_list_entries": 0, "web_list_height": "768", "web_list_by_datetime": True, "web_list_sort_descending": True, } # Check for config.py variable file to import and error out if not found. CONFIG_FILE_PATH = os.path.join(BASE_DIR, "config.py") if os.path.isfile(CONFIG_FILE_PATH): try: from config import CONFIG_TITLE except ImportError: print("\n --- WARNING ---\n") print("pi-timolo.py ver 12.0 or greater requires an updated config.py") print("copy new config.py per commands below.\n") print(" cp config.py config.py.bak") print(" cp config.py.new config.py\n") print("config.py.bak will contain your previous settings") print("The NEW config.py has renamed variable names. If required") print("you will need to review previous settings and change") print("the appropriate NEW variable names using nano.\n") print( "Note: ver 12.0 has added a pantilthat panoramic image stitching feature\n" ) print(" Press Ctrl-c to Exit and update config.py") print(" or") text = raw_input(" Press Enter and Default Settings will be used.") try: # Read Configuration variables from config.py file from config import * except ImportError: print("WARN : Problem Importing Variables from %s" % CONFIG_FILE_PATH) WARN_ON = True else: print( "WARN : %s File Not Found. Cannot Import Configuration Variables." % CONFIG_FILE_PATH ) print(" Run Console Command Below to Download File from GitHub Repo") print( " wget -O config.py https://raw.github.com/pageauc/pi-timolo/master/source/config.py" ) print(" or cp config.py.new config.py") print(" Will now use default_settings dictionary variable values.") WARN_ON = True """ Check if variables were imported from config.py. If not create variable using the values in the default_settings dictionary above. """ for key, val in default_settings.items(): try: exec(key) except NameError: print("WARN : config.py Variable Not Found. Setting " + key + " = " + str(val)) exec(key + "=val") WARN_ON = True if PANTILT_ON: pan_x, tilt_y = PANTILT_HOME if PANTILT_IS_PIMORONI: try: import pantilthat except ImportError: print("ERROR : Import Pimoroni PanTiltHat Python Library per") print(" sudo apt install pantilthat") print(" Enable I2C support using sudo raspi-config") sys.exit() try: pantilthat.pan(pan_x) except IOError: print("ERROR: pimoroni pantilthat hardware problem") print(" if pimoroni pantilt installed check that I2C enabled in raspi-config.") print("if waveshare or conpatible pantilt installed perform the following") print("nano edit config.py per below") print(" nano config.py") print("Change value of variable per below. ctrl-x y to save and exit") print(" PANTILT_IS_PIMORONI = False") sys.exit() pantilt_is = "Pimoroni" else: try: # import pantilthat from waveshare.pantilthat import PanTilt except ImportError: print("ERROR : Install Waveshare PanTiltHat Python Library per") print( " curl -L https://raw.githubusercontent.com/pageauc/waveshare.pantilthat/main/install.sh | bash" ) sys.exit() try: pantilthat = PanTilt() pantilthat.pan(pan_x) except IOError: print("ERROR: pantilthat hardware problem") print("nano edit config.py per below") print(" nano config.py") print("Change value of variable per below. ctrl-x y to save and exit") print(" PANTILT_IS_PIMORONI = True") sys.exit() pantilt_is = "Waveshare" # Setup Logging now that variables are imported from config.py/plugin if LOG_TO_FILE_ON: logging.basicConfig( level=logging.DEBUG, format="%(asctime)s %(levelname)-8s %(funcName)-10s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", filename=LOG_FILE_PATH, filemode="w", ) elif VERBOSE_ON: logging.basicConfig( level=logging.DEBUG, format="%(asctime)s %(levelname)-8s %(funcName)-10s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) else: logging.basicConfig( level=logging.CRITICAL, format="%(asctime)s %(levelname)-8s %(funcName)-10s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) # Check for user_motion_code.py file to import and error out if not found. userMotionFilePath = os.path.join(BASE_DIR, "user_motion_code.py") if not os.path.isfile(userMotionFilePath): print( "WARN : %s File Not Found. Cannot Import user_motion_code functions." % userMotionFilePath ) WARN_ON = True else: # Read Configuration variables from config.py file try: motionCode = True import user_motion_code except ImportError: print("WARN : Failed Import of File user_motion_code.py Investigate Problem") motionCode = False WARN_ON = True # Give some time to read any warnings if WARN_ON and VERBOSE_ON: print("") print("Please Review Warnings Wait 10 sec ...") time.sleep(10) print("Loading Wait ....") try: import cv2 except ImportError: if sys.version_info > (2, 9): logging.error("Failed to import cv2 opencv for python3") logging.error("Try installing opencv for python3") logging.error("See https://github.com/pageauc/opencv3-setup") else: logging.error("Failed to import cv2 for python2") logging.error("Try reinstalling per command") logging.error("sudo apt-get install python-opencv") logging.error("Exiting %s Due to Error", PROG_NAME) sys.exit(1) try: from picamera import PiCamera except ImportError: logging.error("Problem importing picamera module") logging.error("Try command below to import module") if sys.version_info > (2, 9): logging.error("sudo apt-get install python3-picamera") else: logging.error("sudo apt-get install python-picamera") logging.error("Exiting %s Due to Error", PROG_NAME) sys.exit(1) from picamera.array import PiRGBArray import picamera.array # Check that pi camera module is installed and enabled logging.info("Checking Pi Camera Module using command - vcgencmd get_camera") camResult = subprocess.check_output("vcgencmd get_camera", shell=True) camResult = camResult.decode("utf-8") camResult = camResult.replace("\n", "") params = camResult.split() for x in range(0,2): if params[x].find("0") >= 0: logging.error("Detected picamera issue per %s", params[x]) logging.error(" if supported=0 Enable Camera per command sudo raspi-config") logging.error(" Bullseye and later enable Legacy picamera support.") logging.error(" if detected=0 Check Pi Camera Module and cable is Installed Correctly.") logging.error("%s %s Exiting Due to Error", PROG_NAME, PROG_VER) sys.exit(1) else: logging.info("Success Pi Camera %s", camResult) # use raspistill to check maximum image resolution of attached camera module logging.info("Checking Pi Camera Module Version Wait ...") import picamera with picamera.PiCamera() as camera: CAM_MAX_RESOLUTION = camera.MAX_RESOLUTION logging.info("PiCamera Max resolution is %s", CAM_MAX_RESOLUTION) CAM_MAX_WIDTH, CAM_MAX_HEIGHT = CAM_MAX_RESOLUTION.width, CAM_MAX_RESOLUTION.height if CAM_MAX_WIDTH == "3280": picameraVer = "2" else: picameraVer = "1" logging.info("PiCamera Module Hardware is Ver %s", picameraVer) if PLUGIN_ON: # Check and verify plugin and load variable overlay pluginDir = os.path.join(BASE_DIR, "plugins") # Check if there is a .py at the end of PLUGIN_NAME variable if PLUGIN_NAME.endswith(".py"): PLUGIN_NAME = PLUGIN_NAME[:-3] # Remove .py extensiion pluginPath = os.path.join(pluginDir, PLUGIN_NAME + ".py") logging.info("pluginEnabled - loading PLUGIN_NAME %s", pluginPath) if not os.path.isdir(pluginDir): logging.error("plugin Directory Not Found at %s", pluginDir) logging.error("Rerun github curl install script to install plugins") logging.error( "https://github.com/pageauc/pi-timolo/wiki/" "How-to-Install-or-Upgrade#quick-install" ) logging.error("Exiting %s Due to Error", PROG_NAME) sys.exit(1) elif not os.path.isfile(pluginPath): logging.error("File Not Found PLUGIN_NAME %s", pluginPath) logging.error("Check Spelling of PLUGIN_NAME Value in %s", CONFIG_FILE_PATH) logging.error("------- Valid Names -------") validPlugin = glob.glob(pluginDir + "/*py") validPlugin.sort() for entry in validPlugin: pluginFile = os.path.basename(entry) plugin = pluginFile.rsplit(".", 1)[0] if not ((plugin == "__init__") or (plugin == "current")): logging.error(" %s", plugin) logging.error("------- End of List -------") logging.error("Note: PLUGIN_NAME Should Not have .py Ending.") logging.error("or Rerun github curl install command. See github wiki") logging.error( "https://github.com/pageauc/pi-timolo/wiki/" "How-to-Install-or-Upgrade#quick-install" ) logging.error("Exiting %s Due to Error", PROG_NAME) sys.exit(1) else: pluginCurrent = os.path.join(pluginDir, "current.py") try: # Copy image file to recent folder logging.info("Copy %s to %s", pluginPath, pluginCurrent) shutil.copy(pluginPath, pluginCurrent) except OSError as err: logging.error( "Copy Failed from %s to %s - %s", pluginPath, pluginCurrent, err ) logging.error("Check permissions, disk space, Etc.") logging.error("Exiting %s Due to Error", PROG_NAME) sys.exit(1) logging.info("Import Plugin %s", pluginPath) sys.path.insert(0, pluginDir) # add plugin directory to program PATH from plugins.current import * try: if os.path.isfile(pluginCurrent): os.remove(pluginCurrent) pluginCurrentpyc = os.path.join(pluginDir, "current.pyc") if os.path.isfile(pluginCurrentpyc): os.remove(pluginCurrentpyc) except OSError as err: logging.warning("Failed Removal of %s - %s", pluginCurrentpyc, err) time.sleep(5) else: logging.info("No Plugin Enabled per PLUGIN_ON=%s", PLUGIN_ON) # Turn on VERBOSE_ON when DEBUG_ON mode is enabled if DEBUG_ON: VERBOSE_ON = True # Make sure image format extention starts with a dot if not IMAGE_FORMAT.startswith(".", 0, 1): IMAGE_FORMAT = "." + IMAGE_FORMAT # ================================== # System Variables # Should Not need to be customized # ================================== SECONDS2MICRO = 1000000 # Used to convert from seconds to microseconds NIGHT_MAX_SHUTTER = int(NIGHT_MAX_SHUT_SEC * SECONDS2MICRO) # default=5 seconds IMPORTANT- 6 seconds works sometimes # but occasionally locks RPI and HARD reboot required to clear darkAdjust = int((SECONDS2MICRO / 5.0) * NIGHT_DARK_ADJUST) daymode = False # default should always be False. MOTION_PATH = os.path.join(BASE_DIR, MOTION_DIR) # Store Motion images # motion dat file to save currentCount # Setup filepath's for storing image numbering data DATA_DIR = "./data" NUM_PATH_MOTION = os.path.join(DATA_DIR, MOTION_PREFIX + BASE_FILENAME + ".dat") NUM_PATH_TIMELAPSE = os.path.join(DATA_DIR, TIMELAPSE_PREFIX + BASE_FILENAME + ".dat") NUM_PATH_PANO = os.path.join(DATA_DIR, PANO_IMAGE_PREFIX + BASE_FILENAME + ".dat") NUM_PATH_PANTILT_SEQ = os.path.join( DATA_DIR, PANTILT_SEQ_IMAGE_PREFIX + BASE_FILENAME + ".dat" ) TIMELAPSE_PATH = os.path.join(BASE_DIR, TIMELAPSE_DIR) # Store Time Lapse images # timelapse dat file to save currentCount LOCK_FILEPATH = os.path.join(BASE_DIR, BASE_FILENAME + ".sync") # Colors for drawing lines cvWhite = (255, 255, 255) cvBlack = (0, 0, 0) cvBlue = (255, 0, 0) cvGreen = (0, 255, 0) cvRed = (0, 0, 255) LINE_THICKNESS = 1 # Thickness of opencv drawing lines LINE_COLOR = cvWhite # color of lines to highlight motion stream area # Round image resolution to avoid picamera errors if picameraVer == "2": imageWidthMax = 3280 imageHeightMax = 2464 else: imageWidthMax = 2592 imageHeightMax = 1944 logging.info( "picamera ver %s Max Resolution is %i x %i", picameraVer, imageWidthMax, imageHeightMax, ) # Round image resolution to avoid picamera errors image_width = (IMAGE_WIDTH + 31) // 32 * 32 if image_width > imageWidthMax: image_width = imageWidthMax image_height = (IMAGE_HEIGHT + 15) // 16 * 16 if image_height > imageHeightMax: image_height = imageHeightMax stream_width = (STREAM_WIDTH + 31) // 32 * 32 if stream_width > imageWidthMax: stream_width = imageWidthMax stream_height = (STREAM_HEIGHT + 15) // 16 * 16 if stream_height > imageHeightMax: stream_height = imageHeightMax stream_framerate = STREAM_FPS # camera framerate # If camera being used inside where there is no twilight # Reduce night threshold settings to reduce overexposures. if not NIGHT_TWILIGHT_MODE_ON: NIGHT_TWILIGHT_THRESHOLD = 20 NIGHT_DARK_THRESHOLD = 10 NIGHT_BLACK_THRESHOLD = 4 # increase size of MOTION_TRACK_QUICK_PIC_ON image bigImage = MOTION_TRACK_QUICK_PIC_BIGGER bigImageWidth = int(stream_width * bigImage) bigImageHeight = int(stream_height * bigImage) TRACK_TRIG_LEN = MOTION_TRACK_TRIG_LEN # Pixels moved to trigger motion photo # Don't track progress until this Len reached. TRACK_TRIG_LEN_MIN = int(MOTION_TRACK_TRIG_LEN / 6) # Set max overshoot triglen allowed half cam height TRACK_TRIG_LEN_MAX = int(stream_height / 2) # Timeout seconds Stops motion tracking when no activity TRACK_TIMEOUT = MOTION_TRACK_TIMEOUT_SEC # OpenCV Contour sq px area must be greater than this. MIN_AREA = MOTION_TRACK_MIN_AREA BLUR_SIZE = 10 # OpenCV setting for Gaussian difference image blur THRESHOLD_SENSITIVITY = 20 # OpenCV setting for difference image threshold # Fix range Errors Use zero to set default quality to 85 if IMAGE_JPG_QUAL < 1: IMAGE_JPG_QUAL = 85 elif IMAGE_JPG_QUAL > 100: IMAGE_JPG_QUAL = 100 # ------------------------------------------------------------------------------ class PiVideoStream: """ Create a picamera in memory video stream and return a frame when update called """ def __init__( self, resolution=(stream_width, stream_height), framerate=stream_framerate, rotation=0, hflip=False, vflip=False, ): # initialize the camera and stream try: self.camera = PiCamera() except: logging.error("PiCamera Already in Use by Another Process") logging.error("Exiting %s Due to Error", PROG_NAME) exit(1) self.camera.resolution = resolution self.camera.framerate = framerate self.camera.hflip = hflip self.camera.vflip = vflip self.camera.rotation = rotation self.rawCapture = PiRGBArray(self.camera, size=resolution) self.stream = self.camera.capture_continuous( self.rawCapture, format="bgr", use_video_port=True ) # initialize the frame and the variable used to indicate # if the thread should be stopped self.thread = None # Initialize thread self.frame = None self.stopped = False def start(self): """start the thread to read frames from the video stream""" self.thread = Thread(target=self.update, args=()) self.thread.daemon = True self.thread.start() return self def update(self): """keep looping infinitely until the thread is stopped""" for f in self.stream: # grab the frame from the stream and clear the stream in # preparation for the next frame self.frame = f.array self.rawCapture.truncate(0) # if the thread indicator variable is set, stop the thread # and release camera resources if self.stopped: self.stream.close() self.rawCapture.close() self.camera.close() return def read(self): """return the frame most recently read""" return self.frame def stop(self): """indicate that the thread should be stopped""" self.stopped = True if self.thread is not None: self.thread.join() # ------------------------------------------------------------------------------ def shut2sec(shutspeed): """Convert camera shutter speed setting to string""" shutspeedSec = shutspeed / float(SECONDS2MICRO) shutstring = str("%.4f") % (shutspeedSec) return shutstring # ------------------------------------------------------------------------------ def showTime(): """Show current date time in text format""" rightNow = datetime.datetime.now() currentTime = "%04d-%02d-%02d %02d:%02d:%02d" % ( rightNow.year, rightNow.month, rightNow.day, rightNow.hour, rightNow.minute, rightNow.second, ) return currentTime # ------------------------------------------------------------------------------ def showDots(dotcnt): """ If motionShowDots=True then display a progress dot for each cycle. If MOTION_TRACK_ON then this would normally be too fast and should be turned off """ if MOTION_DOTS_ON: if MOTION_TRACK_ON and VERBOSE_ON: dotcnt += 1 if dotcnt > MOTION_DOTS_MAX + 2: print("") dotcnt = 0 elif dotcnt > MOTION_DOTS_MAX: print("") stime = showTime() + " ." sys.stdout.write(stime) sys.stdout.flush() dotcnt = 0 else: sys.stdout.write(".") sys.stdout.flush() return dotcnt # ------------------------------------------------------------------------------ def checkConfig(): """ Check if both User disabled everything in config.py. At least one option needs to be enabled """ if not MOTION_TRACK_ON and not TIMELAPSE_ON and not PANTILT_SEQ_ON and not PANO_ON and not VIDEO_REPEAT_ON: errorText = ( "You need to have Motion, Timelapse, PanTilt Seq, Pano or Video Repeat turned ON\n" "MOTION_TRACK_ON=%s TIMELAPSE_ON=%s PANTILT_SEQ_ON=%s PANO_ON=%s VIDEO_REPEAT_ON=%s" % (MOTION_TRACK_ON, TIMELAPSE_ON, PANTILT_SEQ_ON, PANO_ON, VIDEO_REPEAT_ON) ) if VERBOSE_ON: logging.error(errorText) else: sys.stdout.write(errorText) sys.exit(1) # ------------------------------------------------------------------------------ def displayInfo(motioncount, timelapsecount): """Display variable settings with plugin overlays if required""" if VERBOSE_ON: print( "----------------------------------- Settings " "-----------------------------------" ) print( "Config File .. CONFIG_FILENAME=%s CONFIG_TITLE=%s" % (CONFIG_FILENAME, CONFIG_TITLE) ) if PLUGIN_ON: print( " Plugin .. PLUGIN_ON=%s PLUGIN_NAME=%s" " (Overlays %s Variable Settings)" % (PLUGIN_ON, PLUGIN_NAME, CONFIG_FILENAME) ) else: print(" Plugin .. PLUGIN_ON=%s" % PLUGIN_ON) print("") print( "Image Info ... Size=%ix%i ext=%s Prefix=%s" " VFlip=%s HFlip=%s Rotation=%i" % ( image_width, image_height, IMAGE_FORMAT, IMAGE_NAME_PREFIX, IMAGE_VFLIP, IMAGE_HFLIP, IMAGE_ROTATION, ) ) print( " IMAGE_GRAYSCALE=%s Preview=%s" % (IMAGE_GRAYSCALE, IMAGE_PREVIEW) ) if IMAGE_FORMAT == ".jpg" or IMAGE_FORMAT == ".jpeg": print( " JpegQuality=%i where 1=Low 100=High" % (IMAGE_JPG_QUAL) ) print( " Low Light.. NIGHT_TWILIGHT_MODE_ON=%s NIGHT_TWILIGHT_THRESHOLD=%i" " NIGHT_DARK_THRESHOLD=%i NIGHT_BLACK_THRESHOLD=%i" % ( NIGHT_TWILIGHT_MODE_ON, NIGHT_TWILIGHT_THRESHOLD, NIGHT_DARK_THRESHOLD, NIGHT_BLACK_THRESHOLD, ) ) print( " NIGHT_MAX_SHUT_SEC=%.2f NIGHT_MAX_ISO=%i" " NIGHT_DARK_ADJUST=%.2f NIGHT_SLEEP_SEC=%i" % (NIGHT_MAX_SHUT_SEC, NIGHT_MAX_ISO, NIGHT_DARK_ADJUST, NIGHT_SLEEP_SEC) ) print( " No Shots .. IMAGE_NO_NIGHT_SHOTS=%s IMAGE_NO_DAY_SHOTS=%s" % (IMAGE_NO_NIGHT_SHOTS, IMAGE_NO_DAY_SHOTS) ) if SHOW_DATE_ON_IMAGE: print( " Img Text .. On=%s Bottom=%s (False=Top) WhiteText=%s (False=Black)" % (SHOW_DATE_ON_IMAGE, SHOW_TEXT_BOTTOM, SHOW_TEXT_WHITE) ) print( " SHOW_TEXT_WHITE_NIGHT=%s SHOW_TEXT_FONT_SIZE=%i px height" % (SHOW_TEXT_WHITE_NIGHT, SHOW_TEXT_FONT_SIZE) ) else: print( " No Text .. SHOW_DATE_ON_IMAGE=%s Text on Image is Disabled" % (SHOW_DATE_ON_IMAGE) ) print("") if MOTION_TRACK_ON: print( "Motion Track.. On=%s Prefix=%s MinArea=%i sqpx" " TrigLen=%i-%i px TimeOut=%i sec" % ( MOTION_TRACK_ON, MOTION_PREFIX, MOTION_TRACK_MIN_AREA, MOTION_TRACK_TRIG_LEN, TRACK_TRIG_LEN_MAX, MOTION_TRACK_TIMEOUT_SEC, ) ) print( " MOTION_TRACK_INFO_ON=%s MOTION_DOTS_ON=%s IMAGE_SHOW_STREAM=%s" % (MOTION_TRACK_INFO_ON, MOTION_DOTS_ON, IMAGE_SHOW_STREAM) ) print( " Stream .... size=%ix%i framerate=%i fps" " STREAM_STOP_SEC=%.2f QuickPic=%s" % ( stream_width, stream_height, STREAM_FPS, STREAM_STOP_SEC, MOTION_TRACK_QUICK_PIC_ON, ) ) print( " Img Path .. MOTION_PATH=%s MOTION_CAM_SLEEP=%.2f sec" % (MOTION_PATH, MOTION_CAM_SLEEP) ) print( " Sched ..... MOTION_START_AT %s blank=Off or" " Set Valid Date and/or Time to Start Sequence" % MOTION_START_AT ) print( " Force ..... MOTION_FORCE_SEC=%i min (If No Motion)" % (MOTION_FORCE_SEC / 60) ) print( " Lockfile .. On=%s Path=%s NOTE: For Motion Images Only." % (CREATE_LOCKFILE, LOCK_FILEPATH) ) if MOTION_NUM_ON: print( " Num Seq ... MOTION_NUM_ON=%s numRecycle=%s" " numStart=%i numMax=%i current=%s" % ( MOTION_NUM_ON, MOTION_NUM_RECYCLE_ON, MOTION_NUM_START, MOTION_NUM_MAX, motioncount, ) ) print(" Num Path .. NUM_PATH_MOTION=%s " % (NUM_PATH_MOTION)) else: print( " Date-Time.. MOTION_NUM_ON=%s Image Numbering is Disabled" % (MOTION_NUM_ON) ) if MOTION_TRACK_MINI_TL_ON: print( " Quick TL .. MOTION_TRACK_MINI_TL_ON=%s MOTION_TRACK_MINI_TL_SEQ_SEC=%i" " sec MOTION_TRACK_MINI_TL_TIMER_SEC=%i sec (0=fastest)" % ( MOTION_TRACK_MINI_TL_ON, MOTION_TRACK_MINI_TL_SEQ_SEC, MOTION_TRACK_MINI_TL_TIMER_SEC, ) ) else: print( " Quick TL .. MOTION_TRACK_MINI_TL_ON=%s Quick Time Lapse Disabled" % MOTION_TRACK_MINI_TL_ON ) if MOTION_VIDEO_ON: print( " Video ..... MOTION_VIDEO_ON=%s MOTION_VIDEO_TIMER_SEC=%i" " sec MOTION_VIDEO_FPS=%i (superseded by QuickTL)" % (MOTION_VIDEO_ON, MOTION_VIDEO_TIMER_SEC, MOTION_VIDEO_FPS) ) else: print( " Video ..... MOTION_VIDEO_ON=%s Motion Video is Disabled" % MOTION_VIDEO_ON ) print( " Sub-Dir ... MOTION_SUBDIR_MAX_HOURS=%i (0-off)" " MOTION_SUBDIR_MAX_FILES=%i (0=off)" % (MOTION_SUBDIR_MAX_HOURS, MOTION_SUBDIR_MAX_FILES) ) print( " Recent .... MOTION_RECENT_MAX=%i (0=off) MOTION_RECENT_DIR=%s" % (MOTION_RECENT_MAX, MOTION_RECENT_DIR) ) else: print( "Motion ....... MOTION_TRACK_ON=%s Motion Tracking is Disabled)" % MOTION_TRACK_ON ) print("") if TIMELAPSE_ON: print( "Time Lapse ... On=%s Prefix=%s Timer=%i sec" " TIMELAPSE_EXIT_SEC=%i (0=Continuous)" % ( TIMELAPSE_ON, TIMELAPSE_PREFIX, TIMELAPSE_TIMER_SEC, TIMELAPSE_EXIT_SEC, ) ) print(" TIMELAPSE_MAX_FILES=%i" % (TIMELAPSE_MAX_FILES)) print( " Img Path .. TIMELAPSE_PATH=%s TIMELAPSE_CAM_SLEEP_SEC=%.2f sec" % (TIMELAPSE_PATH, TIMELAPSE_CAM_SLEEP_SEC) ) print( " Sched ..... TIMELAPSE_START_AT %s blank=Off or" " Set Valid Date and/or Time to Start Sequence" % TIMELAPSE_START_AT ) if TIMELAPSE_NUM_ON: print( " Num Seq ... On=%s numRecycle=%s numStart=%i numMax=%i current=%s" % ( TIMELAPSE_NUM_ON, TIMELAPSE_NUM_RECYCLE_ON, TIMELAPSE_NUM_START, TIMELAPSE_NUM_MAX, timelapsecount, ) ) print(" Num Path .. numPath=%s" % (NUM_PATH_TIMELAPSE)) else: print( " Date-Time.. MOTION_NUM_ON=%s Numbering Disabled" % TIMELAPSE_NUM_ON ) print( " Sub-Dir ... TIMELAPSE_SUBDIR_MAX_HOURS=%i (0=off)" " TIMELAPSE_SUBDIR_MAX_FILES=%i (0=off)" % (TIMELAPSE_SUBDIR_MAX_HOURS, TIMELAPSE_SUBDIR_MAX_FILES) ) print( " Recent .... TIMELAPSE_RECENT_MAX=%i (0=off) TIMELAPSE_RECENT_DIR=%s" % (TIMELAPSE_RECENT_MAX, TIMELAPSE_RECENT_DIR) ) else: print( "Time Lapse ... TIMELAPSE_ON=%s Timelapse is Disabled" % TIMELAPSE_ON ) print("") if SPACE_TIMER_HOURS > 0: # Check if disk mgmnt is enabled print( "Disk Space .. Enabled - Manage Target Free Disk Space." " Delete Oldest %s Files if Required" % (SPACE_TARGET_EXT) ) print( " Check Every SPACE_TIMER_HOURS=%i (0=off)" " Target SPACE_TARGET_MB=%i (min=100 MB) SPACE_TARGET_EXT=%s" % (SPACE_TIMER_HOURS, SPACE_TARGET_MB, SPACE_TARGET_EXT) ) print( " Delete Oldest SPACE_TARGET_EXT=%s SPACE_MEDIA_DIR=%s" % (SPACE_TARGET_EXT, SPACE_MEDIA_DIR) ) else: print( "Disk Space .. SPACE_TIMER_HOURS=%i " "(Disabled) - Manage Target Free Disk Space. Delete Oldest %s Files" % (SPACE_TIMER_HOURS, SPACE_TARGET_EXT) ) print( " .. Check Every SPACE_TIMER_HOURS=%i (0=Off)" " Target SPACE_TARGET_MB=%i (min=100 MB)" % (SPACE_TIMER_HOURS, SPACE_TARGET_MB) ) print("") print("Logging ...... VERBOSE_ON=%s (True=Enabled False=Disabled)" % VERBOSE_ON) print( " Log Path .. LOG_TO_FILE_ON=%s LOG_FILE_PATH=%s" % (LOG_TO_FILE_ON, LOG_FILE_PATH) ) print( "--------------------------------- Log Activity " "---------------------------------" ) checkConfig() # ------------------------------------------------------------------------------ def getLastSubdir(directory): """Scan for directories and return most recent""" dirList = [ name for name in os.listdir(directory) if os.path.isdir(os.path.join(directory, name)) ] if len(dirList) > 0: lastSubDir = sorted(dirList)[-1] lastSubDir = os.path.join(directory, lastSubDir) else: lastSubDir = directory return lastSubDir # ------------------------------------------------------------------------------ def createSubdir(directory, prefix): """ Create a subdirectory in directory with unique name based on prefix and date time """ now = datetime.datetime.now() # Specify folder naming subDirName = "%s%d-%02d%02d-%02d%02d" % ( prefix, now.year, now.month, now.day, now.hour, now.minute, ) subDirPath = os.path.join(directory, subDirName) if not os.path.isdir(subDirPath): try: os.makedirs(subDirPath) except OSError as err: logging.error( "Cannot Create Directory %s - %s, using default location.", subDirPath, err, ) subDirPath = directory else: logging.info("Created %s", subDirPath) else: subDirPath = directory return subDirPath # ------------------------------------------------------------------------------ def subDirCheckMaxFiles(directory, filesMax): """Count number of files in a folder path""" fileList = glob.glob(directory + "/*jpg") count = len(fileList) if count > filesMax: makeNewDir = True logging.info("Total Files in %s Exceeds %i", directory, filesMax) else: makeNewDir = False return makeNewDir # ------------------------------------------------------------------------------ def subDirCheckMaxHrs(directory, hrsMax, prefix): """ Note to self need to add error checking extract the date-time from the directory name """ dirName = os.path.split(directory)[1] # split dir path and keep dirName # remove prefix from dirName so just date-time left dirStr = dirName.replace(prefix, "") # convert string to datetime dirDate = datetime.datetime.strptime(dirStr, "%Y-%m%d-%H%M") rightNow = datetime.datetime.now() # get datetime now diff = rightNow - dirDate # get time difference between dates days, seconds = diff.days, diff.seconds dirAgeHours = float(days * 24 + (seconds / 3600.0)) # convert to hours if dirAgeHours > hrsMax: # See if hours are exceeded makeNewDir = True logging.info("MaxHrs %i Exceeds %i for %s", dirAgeHours, hrsMax, directory) else: makeNewDir = False return makeNewDir # ------------------------------------------------------------------------------ def subDirChecks(maxHours, maxFiles, directory, prefix): """Check if motion SubDir needs to be created""" if maxHours < 1 and maxFiles < 1: # No Checks required # logging.info('No sub-folders Required in %s', directory) subDirPath = directory else: subDirPath = getLastSubdir(directory) if subDirPath == directory: # No subDir Found logging.info("No sub folders Found in %s", directory) subDirPath = createSubdir(directory, prefix) # Check MaxHours Folder Age Only elif maxHours > 0 and maxFiles < 1: if subDirCheckMaxHrs(subDirPath, maxHours, prefix): subDirPath = createSubdir(directory, prefix) elif maxHours < 1 and maxFiles > 0: # Check Max Files Only if subDirCheckMaxFiles(subDirPath, maxFiles): subDirPath = createSubdir(directory, prefix) elif maxHours > 0 and maxFiles > 0: # Check both Max Files and Age if subDirCheckMaxHrs(subDirPath, maxHours, prefix): if subDirCheckMaxFiles(subDirPath, maxFiles): subDirPath = createSubdir(directory, prefix) else: logging.info("MaxFiles Not Exceeded in %s", subDirPath) os.path.abspath(subDirPath) return subDirPath # ------------------------------------------------------------------------------ def makeMediaDir(dir_path): """Create a folder sequence""" make_dir = False if not os.path.isdir(dir_path): make_dir = True logging.info("Create Folder %s", dir_path) try: os.makedirs(dir_path) except OSError as err: logging.error("Could Not Create %s - %s", dir_path, err) sys.exit(1) return make_dir # ------------------------------------------------------------------------------ def checkMediaPaths(): """ Checks for image folders and create them if they do not already exist. """ makeMediaDir(DATA_DIR) if MOTION_TRACK_ON: if makeMediaDir(MOTION_PATH): if os.path.isfile(NUM_PATH_MOTION): logging.info("Delete Motion dat File %s", NUM_PATH_MOTION) os.remove(NUM_PATH_MOTION) if TIMELAPSE_ON: if makeMediaDir(TIMELAPSE_PATH): if os.path.isfile(NUM_PATH_TIMELAPSE): logging.info("Delete TimeLapse dat file %s", NUM_PATH_TIMELAPSE) os.remove(NUM_PATH_TIMELAPSE) # Check for Recent Image Folders and create if they do not already exist. if MOTION_RECENT_MAX > 0: makeMediaDir(MOTION_RECENT_DIR) if TIMELAPSE_RECENT_MAX > 0: makeMediaDir(TIMELAPSE_RECENT_DIR) if PANTILT_SEQ_ON: makeMediaDir(PANTILT_SEQ_IMAGES_DIR) if PANTILT_SEQ_RECENT_MAX > 0: makeMediaDir(PANTILT_SEQ_RECENT_DIR) if PANO_ON: makeMediaDir(PANO_DIR) makeMediaDir(PANO_IMAGES_DIR) # ------------------------------------------------------------------------------ def deleteOldFiles(maxFiles, dirPath, prefix): """ Delete Oldest files gt or eq to maxfiles that match filename prefix """ try: fileList = sorted( glob.glob(os.path.join(dirPath, prefix + "*")), key=os.path.getmtime ) except OSError as err: logging.error("Problem Reading Directory %s - %s", dirPath, err) else: while len(fileList) >= maxFiles: oldest = fileList[0] oldestFile = oldest try: # Remove oldest file in recent folder fileList.remove(oldest) logging.info("%s", oldestFile) os.remove(oldestFile) except OSError as err: logging.error("Failed %s err: %s", oldestFile, err) # ------------------------------------------------------------------------------ def makeRelSymlink(sourceFilenamePath, symDestDir): ''' Creates a relative symlink in the specified symDestDir that points to the Target file via a relative rather than absolute path. If a symlink already exists it will be replaced. Warning message will be displayed if symlink path is a file rather than an existing symlink. ''' # Initialize target and symlink file paths targetDirPath = os.path.dirname(sourceFilenamePath) srcfilename = os.path.basename(sourceFilenamePath) symDestFilePath = os.path.join(symDestDir, srcfilename) # Check if symlink already exists and unlink if required. if os.path.islink(symDestFilePath): logging.info("Remove Existing Symlink at %s ", symDestFilePath) os.unlink(symDestFilePath) # Check if symlink path is a file rather than a symlink. Error out if required if os.path.isfile(symDestFilePath): logging.warning("Failed. File Exists at %s." % symDestFilePath) return # Initialize required entries for creating a relative symlink to target file absTargetDirPath = os.path.abspath(targetDirPath) absSymDirPath = os.path.abspath(symDestDir) relativeDirPath = os.path.relpath(absTargetDirPath, absSymDirPath) # Initialize relative symlink entries to target file. symFilePath = os.path.join(relativeDirPath, srcfilename) # logging.info("ln -s %s %s ", symFilePath, symDestFilePath) os.symlink(symFilePath, symDestFilePath) # Create the symlink # Check if symlink was created successfully if os.path.islink(symDestFilePath): logging.info("Saved at %s", symDestFilePath) else: logging.warning("Failed to Create Symlink at %s", symDestFilePath) # ------------------------------------------------------------------------------ def saveRecent(recentMax, recentDir, filepath, prefix): """ Create a symlink file in recent folder (timelapse or motion subfolder) Delete Oldest symlink file if recentMax exceeded. """ show_log = False if recentMax > 0: deleteOldFiles(recentMax, os.path.abspath(recentDir), prefix) makeRelSymlink(filepath, recentDir) # ------------------------------------------------------------------------------ def filesToDelete(mediaDirPath, extension=IMAGE_FORMAT): """ Deletes files of specified format extension by walking folder structure from specified mediaDirPath """ return sorted( ( os.path.join(dirname, filename) for dirname, dirnames, filenames in os.walk(mediaDirPath) for filename in filenames if filename.endswith(extension) ), key=lambda fn: os.stat(fn).st_mtime, reverse=True, ) # ------------------------------------------------------------------------------ def freeSpaceUpTo(freeMB, mediaDir, extension=IMAGE_FORMAT): """ Walks mediaDir and deletes oldest files until SPACE_TARGET_MB is achieved. You should Use with Caution this feature. """ mediaDirPath = os.path.abspath(mediaDir) if os.path.isdir(mediaDirPath): MB2Bytes = 1048576 # Conversion from MB to Bytes targetFreeBytes = freeMB * MB2Bytes fileList = filesToDelete(mediaDir, extension) totFiles = len(fileList) delcnt = 0 logging.info("Session Started") while fileList: statv = os.statvfs(mediaDirPath) availFreeBytes = statv.f_bfree * statv.f_bsize if availFreeBytes >= targetFreeBytes: break filePath = fileList.pop() try: os.remove(filePath) except OSError as err: logging.error("Del Failed %s", filePath) logging.error("Error is %s", err) else: delcnt += 1 logging.info("Del %s", filePath) logging.info( "Target=%i MB Avail=%i MB Deleted %i of %i Files ", targetFreeBytes / MB2Bytes, availFreeBytes / MB2Bytes, delcnt, totFiles, ) # Avoid deleting more than 1/4 of files at one time if delcnt > totFiles / 4: logging.warning("Max Deletions Reached %i of %i", delcnt, totFiles) logging.warning( "Deletions Restricted to 1/4 of " "total files per session." ) break logging.info("Session Ended") else: logging.error("Directory Not Found - %s", mediaDirPath) # ------------------------------------------------------------------------------ def freeDiskSpaceCheck(lastSpaceCheck): """ Perform Disk space checking and Clean up if enabled and return datetime done to reset ready for next sched date/time """ if SPACE_TIMER_HOURS > 0: # Check if disk free space timer hours is enabled # See if it is time to do disk clean-up check if ( datetime.datetime.now() - lastSpaceCheck ).total_seconds() > SPACE_TIMER_HOURS * 3600: lastSpaceCheck = datetime.datetime.now() if SPACE_TARGET_MB < 100: # set freeSpaceMB to reasonable value if too low diskFreeMB = 100 else: diskFreeMB = SPACE_TARGET_MB logging.info( "SPACE_TIMER_HOURS=%i diskFreeMB=%i SPACE_MEDIA_DIR=%s SPACE_TARGET_EXT=%s", SPACE_TIMER_HOURS, diskFreeMB, SPACE_MEDIA_DIR, SPACE_TARGET_EXT, ) freeSpaceUpTo(diskFreeMB, SPACE_MEDIA_DIR, SPACE_TARGET_EXT) return lastSpaceCheck # ------------------------------------------------------------------------------ def getCurrentCount(numberpath, numberstart): """ Create a .dat file to store currentCount or read file if it already Exists """ if not os.path.isfile(numberpath): # Create numberPath file if it does not exist logging.info("Creating New File %s numberstart= %s", numberpath, numberstart) open(numberpath, "w").close() f = open(numberpath, "w+") f.write(str(numberstart)) f.close() # Read the numberPath file to get the last sequence number with open(numberpath, "r") as f: writeCount = f.read() f.closed try: numbercounter = int(writeCount) # Found Corrupt dat file since cannot convert to integer except ValueError: # Try to determine if this is motion or timelapse if numberpath.find(MOTION_PREFIX) > 0: filePath = MOTION_PATH + "/*" + IMAGE_FORMAT fprefix = MOTION_PATH + MOTION_PREFIX + IMAGE_NAME_PREFIX else: filePath = TIMELAPSE_PATH + "/*" + IMAGE_FORMAT fprefix = TIMELAPSE_PATH + TIMELAPSE_PREFIX + IMAGE_NAME_PREFIX try: # Scan image folder for most recent file # and try to extract most recent number counter newest = max(glob.iglob(filePath), key=os.path.getctime) writeCount = newest[len(fprefix) + 1 : newest.find(IMAGE_FORMAT)] except: writeCount = numberstart try: numbercounter = int(writeCount) + 1 except ValueError: numbercounter = numberstart logging.warning( "Found Invalid Data in %s Resetting Counter to %s", numberpath, numbercounter, ) f = open(numberpath, "w+") f.write(str(numbercounter)) f.close() f = open(numberpath, "r") writeCount = f.read() f.close() numbercounter = int(writeCount) return numbercounter # ------------------------------------------------------------------------------ def writeTextToImage(imagename, datetoprint, currentDayMode): """ Function to write date/time stamp directly on top or bottom of images. """ if SHOW_TEXT_WHITE: FOREGROUND = (255, 255, 255) # rgb settings for white text foreground textColour = "White" else: FOREGROUND = (0, 0, 0) # rgb settings for black text foreground textColour = "Black" if SHOW_TEXT_WHITE_NIGHT and (not currentDayMode): # rgb settings for black text foreground FOREGROUND = (255, 255, 255) textColour = "White" img = cv2.imread(imagename) # This is grayscale image so channels is not avail or used height, width, channels = img.shape # centre text and compensate for graphics text being wider x = int((width / 2) - (len(imagename) * 2)) if SHOW_TEXT_BOTTOM: y = height - 50 # show text at bottom of image else: y = 10 # show text at top of image TEXT = IMAGE_NAME_PREFIX + datetoprint font_path = "/usr/share/fonts/truetype/freefont/FreeSansBold.ttf" font = ImageFont.truetype(font_path, SHOW_TEXT_FONT_SIZE, encoding="unic") try: text = TEXT.decode("utf-8") # required for python2 except: text = TEXT # Just set for python3 img = Image.open(imagename) # For python3 install of pyexiv2 lib # See https://github.com/pageauc/pi-timolo/issues/79 try: # Read exif data since ImageDraw does not save this metadata metadata = pyexiv2.ImageMetadata(imagename) metadata.read() except: pass draw = ImageDraw.Draw(img) # draw.text((x, y),"Sample Text",(r,g,b)) draw.text((x, y), text, FOREGROUND, font=font) if IMAGE_FORMAT.lower == ".jpg" or IMAGE_FORMAT.lower == ".jpeg": img.save(imagename, quality="keep") else: img.save(imagename) logging.info("Added %s Text [ %s ]", textColour, datetoprint) try: metadata.write() # Write previously saved exif data to image file except: logging.warning("Image EXIF Data Not Transferred.") logging.info("Saved %s", imagename) # ------------------------------------------------------------------------------ def writeCounter(counter, counter_path): """ Write next counter number to specified counter_path dat file to remember where counter is to start next in case app shuts down. """ str_count = str(counter) if not os.path.isfile(counter_path): logging.info("Create New Counter File Counter=%s %s", str_count, counter_path) open(counter_path, "w").close() f = open(counter_path, "w+") f.write(str_count) f.close() logging.info("Next Counter=%s %s", str_count, counter_path) # ------------------------------------------------------------------------------ def postImageProcessing( numberon, counterstart, countermax, counter, recycle, counterpath, filename, currentDaymode, ): """ If required process text to display directly on image """ rightNow = datetime.datetime.now() if SHOW_DATE_ON_IMAGE: dateTimeText = "%04d%02d%02d_%02d:%02d:%02d" % ( rightNow.year, rightNow.month, rightNow.day, rightNow.hour, rightNow.minute, rightNow.second, ) if numberon: if not recycle and countermax > 0: counterStr = "%i/%i " % (counter, counterstart + countermax) imageText = counterStr + dateTimeText else: counterStr = "%i " % (counter) imageText = counterStr + dateTimeText else: imageText = dateTimeText # Now put the imageText on the current image try: # This will fail for a video file writeTextToImage(filename, imageText, currentDaymode) except: pass if CREATE_LOCKFILE and MOTION_TRACK_ON: createSyncLockFile(filename) # Process currentCount for next image if number sequence is enabled if numberon: counter += 1 if countermax > 0: if counter >= counterstart + countermax: if recycle: counter = counterstart else: counter = counterstart + countermax + 1 logging.warning( "Exceeded Image Count numberMax=%i for %s \n", countermax, filename, ) # write next image counter number to dat file writeCounter(counter, counterpath) return counter # ------------------------------------------------------------------------------ def getVideoName(path, prefix, numberon, counter): """build image file names by number sequence or date/time""" if numberon: if MOTION_VIDEO_ON or VIDEO_REPEAT_ON: filename = os.path.join(path, prefix + str(counter) + ".h264") else: if MOTION_VIDEO_ON or VIDEO_REPEAT_ON: rightNow = datetime.datetime.now() filename = "%s/%s%04d%02d%02d-%02d%02d%02d.h264" % ( path, prefix, rightNow.year, rightNow.month, rightNow.day, rightNow.hour, rightNow.minute, rightNow.second, ) return filename # ------------------------------------------------------------------------------ def getImageFilename(path, prefix, numberon, counter): """build image file names by number sequence or date/time""" if numberon: filename = os.path.join(path, prefix + str(counter) + IMAGE_FORMAT) else: rightNow = datetime.datetime.now() filename = "%s/%s%04d%02d%02d-%02d%02d%02d%s" % ( path, prefix, rightNow.year, rightNow.month, rightNow.day, rightNow.hour, rightNow.minute, rightNow.second, IMAGE_FORMAT, ) return filename # ------------------------------------------------------------------------------ def showBox(filename): """ Show stream image detection area on image to align camera This is a quick fix for restricting motion detection to a portion of the final image. Change the stream image size on line 206 and 207 above Adjust track config.py file MOTION_TRACK_TRIG_LEN as required. """ working_image = cv2.imread(filename) x1y1 = ( int((IMAGE_WIDTH - stream_width) / 2), int((image_height - stream_height) / 2), ) x2y2 = (x1y1[0] + stream_width, x1y1[1] + stream_height) cv2.rectangle(working_image, x1y1, x2y2, LINE_COLOR, LINE_THICKNESS) cv2.imwrite(filename, working_image) # ------------------------------------------------------------------------------ def takeMotionQuickImage(image, filename): """Enlarge and Save stream image if MOTION_TRACK_QUICK_PIC_ON=True""" big_image = ( cv2.resize(image, (bigImageWidth, bigImageHeight)) if bigImage != 1 else image ) cv2.imwrite(filename, big_image) logging.info("Saved %ix%i Image to %s", bigImageWidth, bigImageHeight, filename) # ------------------------------------------------------------------------------ def takeDayImage(filename, cam_sleep_time): """Take a Day image using exp=auto and awb=auto""" with picamera.PiCamera() as camera: camera.resolution = (image_width, image_height) camera.vflip = IMAGE_VFLIP camera.hflip = IMAGE_HFLIP camera.rotation = IMAGE_ROTATION # Valid values are 0, 90, 180, 270 # Day Automatic Mode camera.exposure_mode = "auto" camera.awb_mode = "auto" if IMAGE_GRAYSCALE: camera.color_effects = (128, 128) time.sleep(cam_sleep_time) # use motion or TL camera sleep to get AWB if IMAGE_PREVIEW: camera.start_preview() if IMAGE_FORMAT == ".jpg": # Set quality if image is jpg camera.capture(filename, quality=IMAGE_JPG_QUAL) else: camera.capture(filename) camera.close() if IMAGE_SHOW_STREAM: # Show motion area on full image to align camera showBox(filename) logging.info( "camSleepSec=%.2f exp=auto awb=auto Size=%ix%i ", cam_sleep_time, image_width, image_height, ) # SHOW_DATE_ON_IMAGE displays FilePath so avoid showing twice if not SHOW_DATE_ON_IMAGE: logging.info("Saved %s", filename) # ------------------------------------------------------------------------------ def getShutterSetting(pxAve): """ Calculate a shutter speed based on image pixel average """ px = pxAve + 1 # avoid division by zero offset = NIGHT_MAX_SHUTTER - ( (NIGHT_MAX_SHUTTER / float(NIGHT_DARK_THRESHOLD) * px) ) brightness = offset * (1 / float(NIGHT_DARK_ADJUST)) # hyperbolic curve + brightness adjust shut = (NIGHT_MAX_SHUTTER * (1 / float(px))) + brightness return int(shut) # ------------------------------------------------------------------------------ def takeNightImage(filename, pixelAve): """Take low light Twilight or Night image""" with picamera.PiCamera() as camera: camera.resolution = (image_width, image_height) camera.vflip = IMAGE_VFLIP camera.hflip = IMAGE_HFLIP camera.rotation = IMAGE_ROTATION # valid values are 0, 90, 180, 270 if IMAGE_GRAYSCALE: camera.color_effects = (128, 128) # Use Twilight Threshold variable framerate_range if pixelAve >= NIGHT_DARK_THRESHOLD: camera.framerate_range = (Fraction(1, 6), Fraction(30, 1)) time.sleep(1) camera.iso = NIGHT_MAX_ISO logging.info( "%ix%i TwilightThresh=%i/%i MaxISO=%i uses framerate_range", image_width, image_height, pixelAve, NIGHT_TWILIGHT_THRESHOLD, NIGHT_MAX_ISO, ) time.sleep(4) else: # Set the framerate to a fixed value camera.framerate = Fraction(1, 6) time.sleep(1) camera.iso = NIGHT_MAX_ISO if pixelAve <= NIGHT_BLACK_THRESHOLD: # Black Threshold (very dark) camera.shutter_speed = NIGHT_MAX_SHUTTER logging.info( "%ix%i BlackThresh=%i/%i shutSec=%s MaxISO=%i NIGHT_SLEEP_SEC=%i", image_width, image_height, pixelAve, NIGHT_BLACK_THRESHOLD, shut2sec(NIGHT_MAX_SHUTTER), NIGHT_MAX_ISO, NIGHT_SLEEP_SEC, ) else: # Dark Threshold (Between Twilight and Black) camShut = getShutterSetting(pixelAve) if camShut > NIGHT_MAX_SHUTTER: camShut = NIGHT_MAX_SHUTTER # Set the shutter for long exposure camera.shutter_speed = camShut logging.info( "%ix%i DarkThresh=%i/%i shutSec=%s MaxISO=%i NIGHT_SLEEP_SEC=%i", image_width, image_height, pixelAve, NIGHT_DARK_THRESHOLD, shut2sec(camShut), NIGHT_MAX_ISO, NIGHT_SLEEP_SEC, ) time.sleep(NIGHT_SLEEP_SEC) camera.exposure_mode = "off" if IMAGE_FORMAT == ".jpg": camera.capture(filename, format="jpeg", quality=IMAGE_JPG_QUAL) else: camera.capture(filename) camera.framerate = 10 # Adhoc Fix for Stretch camera freeze issue # Perform sudo rpi-update camera.close() if IMAGE_SHOW_STREAM: # Show motion area on full image to align camera showBox(filename) # SHOW_DATE_ON_IMAGE displays FilePath to avoid showing twice if not SHOW_DATE_ON_IMAGE: logging.info("Saved %s", filename) # ------------------------------------------------------------------------------ def createSyncLockFile(imagefilename): """ If required create a lock file to indicate file(s) to process """ if CREATE_LOCKFILE: if not os.path.isfile(LOCK_FILEPATH): open(LOCK_FILEPATH, "w").close() logging.info("Create Lock File %s", LOCK_FILEPATH) rightNow = datetime.datetime.now() now = "%04d%02d%02d-%02d%02d%02d" % ( rightNow.year, rightNow.month, rightNow.day, rightNow.hour, rightNow.minute, rightNow.second, ) filecontents = ( now + " createSyncLockFile - " + imagefilename + " Ready to sync using sudo ./sync.sh command." ) f = open(LOCK_FILEPATH, "w+") f.write(filecontents) f.close() # ------------------------------------------------------------------------------ def getMotionTrackPoint(grayimage1, grayimage2): """ Process two cropped grayscale images. check for motion and return center point of motion for largest contour. """ movementCenterPoint = [] # initialize list of movementCenterPoints biggestArea = MIN_AREA # Get differences between the two greyed images differenceimage = cv2.absdiff(grayimage1, grayimage2) # Blur difference image to enhance motion vectors differenceimage = cv2.blur(differenceimage, (BLUR_SIZE, BLUR_SIZE)) # Get threshold of blurred difference image # based on THRESHOLD_SENSITIVITY variable retval, thresholdimage = cv2.threshold( differenceimage, THRESHOLD_SENSITIVITY, 255, cv2.THRESH_BINARY ) try: # opencv2 syntax default contours, hierarchy = cv2.findContours( thresholdimage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) except ValueError: # opencv 3 syntax thresholdimage, contours, hierarchy = cv2.findContours( thresholdimage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) if contours: for c in contours: cArea = cv2.contourArea(c) if cArea > biggestArea: biggestArea = cArea (x, y, w, h) = cv2.boundingRect(c) cx = int(x + w / 2) # x center point of contour cy = int(y + h / 2) # y center point of contour movementCenterPoint = [cx, cy] return movementCenterPoint # ------------------------------------------------------------------------------ def trackMotionDistance(mPoint1, mPoint2): """ Return the triangulated distance between two tracking locations """ x1, y1 = mPoint1 x2, y2 = mPoint2 trackLen = abs(math.hypot(x2 - x1, y2 - y1)) return trackLen # ------------------------------------------------------------------------------ def getStreamPixAve(streamData): """ Calculate the average pixel values for the specified stream used for determining day/night or twilight conditions """ pixAverage = int(np.average(streamData[..., 1])) # Use 0=red 1=green 2=blue return pixAverage # ------------------------------------------------------------------------------ def checkIfDayStream(currentDayMode, image): """Try to determine if it is day, night or twilight.""" dayPixAverage = 0 currentDayMode = False dayPixAverage = getStreamPixAve(image) if dayPixAverage > NIGHT_TWILIGHT_THRESHOLD: currentDayMode = True return currentDayMode # ------------------------------------------------------------------------------ def timeToSleep(currentDayMode): """ Based on weather it is day or night (exclude twilight) return sleepMode boolean based on variable settings for IMAGE_NO_NIGHT_SHOTS or IMAGE_NO_DAY_SHOTS config.py variables Note if both are enabled then no shots will be taken. """ if IMAGE_NO_NIGHT_SHOTS: if currentDayMode: sleepMode = False else: sleepMode = True elif IMAGE_NO_DAY_SHOTS: sleepMode = False if currentDayMode: sleepMode = True else: sleepMode = False return sleepMode # ------------------------------------------------------------------------------ def getSchedStart(dateToCheck): """ This function will try to extract a valid date/time from a date time formatted string variable If date/time is past then try to extract time and schedule for current date at extracted time """ goodDateTime = datetime.datetime.now() if len(dateToCheck) > 1: # Check if TIMELAPSE_START_AT is set try: # parse and convert string to date/time or return error goodDateTime = parse(dateToCheck) except: # Is there a colon indicating possible time format exists if ":" in dateToCheck: timeTry = dateToCheck[dateToCheck.find(":") - 2 :] # Try to extract time only from string try: # See if a valid time is found returns with current day goodDateTime = parse(timeTry) except: logging.error("Bad Date and/or Time Format %s", dateToCheck) logging.error( "Use a Valid Date and/or Time " 'Format Eg "DD-MMM-YYYY HH:MM:SS"' ) goodDateTime = datetime.datetime.now() logging.warning("Resetting date/time to Now: %s", goodDateTime) # Check if date/time is past if goodDateTime < datetime.datetime.now(): if ":" in dateToCheck: # Check if there is a time component # Extract possible time component timeTry = dateToCheck[dateToCheck.find(":") - 2 :] try: # parse for valid time # returns current day with parsed time goodDateTime = parse(timeTry) except: pass # Do Nothing return goodDateTime # ------------------------------------------------------------------------------ def checkSchedStart(schedDate): """ Based on schedule date setting see if current datetime is past and return boolean to indicate processing can start for timelapse or motiontracking """ startStatus = False if schedDate < datetime.datetime.now(): startStatus = True # sched date/time has passed so start sequence return startStatus # ------------------------------------------------------------------------------ def checkTimer(timer_start, timer_sec): """ Check if timelapse timer has expired Return updated start time status of expired timer True or False """ timer_expired = False rightNow = datetime.datetime.now() timeDiff = (rightNow - timer_start).total_seconds() if timeDiff >= timer_sec: timer_expired = True timer_start = rightNow return timer_start, timer_expired # ------------------------------------------------------------------------------ def takeMiniTimelapse(moPath, prefix, NumOn, motionNumCount, currentDayMode, NumPath): """ Take a motion tracking activated mini timelapse sequence using yield if motion triggered """ logging.info( "START - Run for %i secs with image every %i secs", MOTION_TRACK_MINI_TL_SEQ_SEC, MOTION_TRACK_MINI_TL_TIMER_SEC, ) checkTimeLapseTimer = datetime.datetime.now() keepTakingImages = True imgCnt = 0 filename = getImageFilename(moPath, prefix, NumOn, motionNumCount) while keepTakingImages: yield filename rightNow = datetime.datetime.now() timelapseDiff = (rightNow - checkTimeLapseTimer).total_seconds() motionNumCount = postImageProcessing( NumOn, MOTION_NUM_START, MOTION_NUM_MAX, motionNumCount, MOTION_NUM_RECYCLE_ON, NUM_PATH_MOTION, filename, currentDayMode, ) filename = getImageFilename(moPath, prefix, NumOn, motionNumCount) if timelapseDiff > MOTION_TRACK_MINI_TL_SEQ_SEC: keepTakingImages = False else: imgCnt += 1 saveRecent(MOTION_RECENT_MAX, MOTION_RECENT_DIR, filename, prefix) time.sleep(MOTION_TRACK_MINI_TL_TIMER_SEC) logging.info( "END - Total %i Images in %i sec every %i sec", imgCnt, timelapseDiff, MOTION_TRACK_MINI_TL_TIMER_SEC, ) print("") # ------------------------------------------------------------------------------ def takeVideo(filename, duration, vidW=1280, vidH=720, fps=25): """Take a short motion video if required""" # Working folder for h264 videos h264_work = os.path.join(BASE_DIR, "h264_work") if not os.path.isdir(h264_work): try: os.makedirs(h264_work) except OSError as err: logging.error("%s err: %s", h264_work, err) else: logging.info("Created Dir %s", h264_work) filePath264 = os.path.join(h264_work, os.path.basename(filename)) # Final destination for mp4 videos filePathMP4 = os.path.join( os.path.dirname(filename), os.path.splitext(os.path.basename(filename))[0] + ".mp4", ) # command to convert h264 video to mp4 h264_mp4_cmd = "/usr/bin/MP4Box -add %s:fps=%i -new %s" % ( filePath264, fps, filePathMP4, ) logging.info("File : %s", filePath264) logging.info("Start: Size %ix%i for %i sec at %i fps", vidW, vidH, duration, fps) if MOTION_VIDEO_ON or VIDEO_REPEAT_ON: with picamera.PiCamera() as camera: camera.resolution = (vidW, vidH) camera.vflip = IMAGE_VFLIP camera.hflip = IMAGE_HFLIP # rotation can be used if camera is on side camera.rotation = IMAGE_ROTATION camera.framerate = fps if SHOW_DATE_ON_IMAGE: rightNow = datetime.datetime.now() dateTimeText = " Started at %04d-%02d-%02d %02d:%02d:%02d " % ( rightNow.year, rightNow.month, rightNow.day, rightNow.hour, rightNow.minute, rightNow.second, ) camera.annotate_text_size = SHOW_TEXT_FONT_SIZE camera.annotate_foreground = picamera.Color("black") camera.annotate_background = picamera.Color("white") camera.annotate_text = dateTimeText camera.start_recording(filePath264) camera.wait_recording(duration) camera.stop_recording() camera.close() # This creates a subprocess that runs MP4Box to convert h264 file # to MP4 with the filename as a parameter. Note this will take # some time so MP4Box logging info will be delayed. try: logging.info("MP4Box %s", filePathMP4) proc = subprocess.Popen( h264_mp4_cmd, shell=True, stdin=None, stdout=None, stderr=None, close_fds=True, ) except IOError: logging.error("subprocess %s", h264_mp4_cmd) saveRecent(MOTION_RECENT_MAX, MOTION_RECENT_DIR, filePathMP4, MOTION_PREFIX) createSyncLockFile(filename) # ------------------------------------------------------------------------------ def pantiltGoHome(): """ Move pantilt to home position. If pantilt installed then this can position pantilt to a home position for consistent motion tracking and timelapse camera pointing. """ if PANTILT_ON: pantilthat.pan(PANTILT_HOME[0]) time.sleep(PANTILT_SLEEP_SEC) pantilthat.tilt(PANTILT_HOME[1]) time.sleep(PANTILT_SLEEP_SEC) # ------------------------------------------------------------------------------ def addFilepathSeq(filepath, seq_num): """ Add a sequence number to the filename just prior to the image format extension. """ index = filepath.find(IMAGE_FORMAT) seq_filepath = filepath[:index] + "-" + str(seq_num) + filepath[index:] return seq_filepath # ------------------------------------------------------------------------------ def takePantiltSequence(filename, daymode, pix_ave, num_count, num_path): """ Take a sequence of images based on a list of pantilt positions and save with a sequence number appended to the filename """ if (not daymode) and PANTILT_SEQ_DAYONLY_ON: logging.info('Skip since PANTILT_SEQ_DAYONLY_ON = %s and daymode = %s', PANTILT_SEQ_DAYONLY_ON, daymode) return elif not PANTILT_ON: logging.error('PANTILT_ON not Enabled in Config.py') return if MOTION_TRACK_ON and MOTION_TRACK_PANTILT_SEQ_ON: seq_prefix = MOTION_PREFIX + IMAGE_NAME_PREFIX if PANTILT_SEQ_ON: logging.warning('MOTION_TRACK_PANTILT_SEQ_ON takes precedence over PANTILT_SEQ_ON') logging.warning('Disable config.py MOTION_TRACK_PANTILT_SEQ_ON setting') logging.warning('to Enable Timelapse PANTILT_SEQ_ON option.') logging.info("... Start Motion Tracking PanTilt Sequence.") elif PANTILT_SEQ_ON: seq_prefix = PANTILT_SEQ_IMAGE_PREFIX + IMAGE_NAME_PREFIX logging.info("... Start Timelapse PanTilt Sequence.") # initialize counter to ensure each image filename is unique pantilt_seq_image_num = 0 for cam_pos in PANTILT_SEQ_STOPS: # take images at each specified stop pantilt_seq_image_num += 1 # Set image numbering for this image seq_filepath = addFilepathSeq(filename, pantilt_seq_image_num) pan_x, tilt_y = cam_pos # set pan tilt values for this image pantilthat.pan(pan_x) pantilthat.tilt(tilt_y) logging.info("pan_x=%i tilt_y=%i", pan_x, tilt_y) time.sleep(PANTILT_SLEEP_SEC) if daymode: takeDayImage(seq_filepath, TIMELAPSE_CAM_SLEEP_SEC) else: takeNightImage(seq_filepath, pix_ave) if MOTION_TRACK_PANTILT_SEQ_ON: postImageProcessing( MOTION_NUM_ON, MOTION_NUM_START, MOTION_NUM_MAX, num_count, MOTION_NUM_RECYCLE_ON, NUM_PATH_MOTION, seq_filepath, daymode, ) saveRecent( MOTION_NUM_MAX, MOTION_RECENT_DIR, seq_filepath, seq_prefix ) elif PANTILT_SEQ_ON: postImageProcessing( PANTILT_SEQ_NUM_ON, PANTILT_SEQ_NUM_START, PANTILT_SEQ_NUM_MAX, num_count, PANTILT_SEQ_NUM_RECYCLE_ON, NUM_PATH_PANTILT_SEQ, seq_filepath, daymode, ) saveRecent( PANTILT_SEQ_NUM_MAX, PANTILT_SEQ_RECENT_DIR, seq_filepath, PANTILT_SEQ_IMAGE_PREFIX, ) if PANTILT_SEQ_NUM_ON: num_count += 1 writeCounter(num_count, NUM_PATH_PANTILT_SEQ) deleteOldFiles(PANTILT_SEQ_RECENT_MAX, os.path.abspath(PANTILT_SEQ_RECENT_DIR), PANTILT_SEQ_IMAGE_PREFIX ) pantiltGoHome() # Center pantilt logging.info("... End") return num_count # ------------------------------------------------------------------------------ def takePano(pano_seq_num, daymode, pix_ave): """ Take a series of overlapping images using pantilt at specified PANO_CAM_STOPS then attempt to stitch the images into one panoramic image. Note this will take time so depending on number of cpu cores and speed. The PANO_TIMER should be set to avoid multiple stitching operations at once. use htop or top to check stitching PID activity. Successfuly Stitching needs good lighting so it should be restricted to day light hours or sufficient indoor lighting. Review pano source image overlap using webserver. Adjust pano stops accordingly. """ if (not daymode) and PANO_DAYONLY_ON: logging.info('Skip since PANO_DAYONLY_ON = %s and daymode = %s', PANO_DAYONLY_ON, daymode) return print("") logging.info("Start timer=%i sec pano_seq_num=%s", PANO_TIMER_SEC, pano_seq_num) pano_image_num = 0 # initialize counter to ensure each image filename is unique pano_image_files = ( "" # string of contatenated image input pano filenames for stitch command line ) pano_file_path = os.path.join( PANO_DIR, PANO_IMAGE_PREFIX + IMAGE_NAME_PREFIX + str(pano_seq_num) + IMAGE_FORMAT, ) for cam_pos in PANO_CAM_STOPS: # take images at each specified stop pano_image_num += 1 # Set image numbering for this image pan_x, tilt_y = cam_pos # set pan tilt values for this image pano_filename = os.path.join( PANO_IMAGES_DIR, PANO_IMAGE_PREFIX + IMAGE_NAME_PREFIX + str(pano_seq_num) + "-" + str(pano_image_num) + IMAGE_FORMAT, ) pano_image_files += " " + pano_filename pantilthat.pan(pan_x) pantilthat.tilt(tilt_y) if pano_seq_num == 1: time.sleep(0.3) time.sleep(PANTILT_SLEEP_SEC) if daymode: takeDayImage(pano_filename, TIMELAPSE_CAM_SLEEP_SEC) else: takeNightImage(pano_filename, pix_ave) logging.info( "Size %ix%i Saved %s at cam_pos(%i, %i)", image_width, image_height, pano_filename, pan_x, tilt_y, ) # Center pantilt pantiltGoHome() logging.info("End") if not os.path.isfile(PANO_PROG_PATH): logging.error("Cannot Find Pano Executable File at %s", PANO_PROG_PATH) logging.info("Please run menubox.sh UPGRADE to correct problem") logging.warning("Exiting - Cannot Run Image Stitching of Images.") return if not os.path.isfile("./config.cfg"): logging.error("Cannot Find ./config.cfg required for %s", PANO_PROG_PATH) logging.info("Please run menubox.sh UPGRADE to correct problem") logging.warning("Exiting - Cannot Run Image Stitching of Images.") return # Create the stitch command line string stitch_cmd = PANO_PROG_PATH + " " + pano_file_path + pano_image_files try: logging.info("Run Image Stitching Command per Below") print("%s" % stitch_cmd) # spawn stitch command with parameters as seperate task proc = subprocess.Popen( stitch_cmd, shell=True, stdin=None, stdout=None, stderr=None, close_fds=True ) except IOError: logging.error("Failed subprocess %s", stitch_cmd) pano_seq_num += 1 if PANO_NUM_RECYCLE and PANO_NUM_MAX > 0: if pano_seq_num >= PANO_NUM_START + PANO_NUM_MAX: logging.info( "PANO_NUM_RECYCLE Activated. Reset pano_seq_num to %i", PANO_NUM_START ) pano_seq_num = PANO_NUM_START writeCounter(pano_seq_num, NUM_PATH_PANO) return pano_seq_num # ------------------------------------------------------------------------------ def videoRepeat(): """ This is a special dash cam video mode that overrides both timelapse and motion tracking settings It has it's own set of settings to manage start, video duration, number recycle mode, Etc. """ # Check if folder exist and create if required if not os.path.isdir(VIDEO_DIR): logging.info("Create videoRepeat Folder %s", VIDEO_DIR) os.makedirs(VIDEO_DIR) print("--------------------------------------------------------------------") print("VideoRepeat . VIDEO_REPEAT_ON=%s" % VIDEO_REPEAT_ON) print( " Info ..... Size=%ix%i VIDEO_PREFIX=%s VIDEO_FILE_SEC=%i seconds VIDEO_FPS=%i" % ( VIDEO_REPEAT_WIDTH, VIDEO_REPEAT_HEIGHT, VIDEO_PREFIX, VIDEO_FILE_SEC, VIDEO_FPS, ) ) print(" Vid Path . VIDEO_DIR= %s" % VIDEO_DIR) print( " Sched .... VIDEO_START_AT=%s blank=Off or Set Valid Date and/or Time to Start Sequence" % VIDEO_START_AT ) print( " Timer .... VIDEO_SESSION_MIN=%i minutes 0=Continuous" % VIDEO_SESSION_MIN ) print( " Num Seq .. VIDEO_NUM_ON=%s VIDEO_NUM_RECYCLE_ON=%s VIDEO_NUM_START=%i" " VIDEO_NUM_MAX=%i 0=Continuous" % (VIDEO_NUM_ON, VIDEO_NUM_RECYCLE_ON, VIDEO_NUM_START, VIDEO_NUM_MAX) ) print("--------------------------------------------------------------------") print( "WARNING: VIDEO_REPEAT_ON=%s Suppresses TimeLapse and Motion Settings." % VIDEO_REPEAT_ON ) startVideoRepeat = getSchedStart(VIDEO_START_AT) if not checkSchedStart(startVideoRepeat): logging.info('VIDEO_START_AT = "%s" ', VIDEO_START_AT) logging.info( "Video Repeat: Sched Start Set For %s Please Wait ...", startVideoRepeat ) while not checkSchedStart(startVideoRepeat): pass videoStartTime = datetime.datetime.now() lastSpaceCheck = datetime.datetime.now() videoCount = 0 videoNumCounter = VIDEO_NUM_START keepRecording = True while keepRecording: # if required check free disk space and delete older files # Set variables SPACE_TARGET_EXT='mp4' and # SPACE_MEDIA_DIR= to appropriate folder path if SPACE_TIMER_HOURS > 0: lastSpaceCheck = freeDiskSpaceCheck(lastSpaceCheck) filename = getVideoName(VIDEO_DIR, VIDEO_PREFIX, VIDEO_NUM_ON, videoNumCounter) takeVideo( filename, VIDEO_FILE_SEC, VIDEO_REPEAT_WIDTH, VIDEO_REPEAT_HEIGHT, VIDEO_FPS ) timeUsed = (datetime.datetime.now() - videoStartTime).total_seconds() timeRemaining = (VIDEO_SESSION_MIN * 60 - timeUsed) / 60.0 videoCount += 1 if VIDEO_NUM_ON: videoNumCounter += 1 if VIDEO_NUM_MAX > 0: if videoNumCounter - VIDEO_NUM_START > VIDEO_NUM_MAX: if VIDEO_NUM_RECYCLE_ON: videoNumCounter = VIDEO_NUM_START logging.info( "Restart Numbering: VIDEO_NUM_RECYCLE_ON=%s " "and VIDEO_NUM_MAX=%i Exceeded", VIDEO_NUM_RECYCLE_ON, VIDEO_NUM_MAX, ) else: keepRecording = False logging.info( "Exit since VIDEO_NUM_RECYCLE_ON=%s " "and VIDEO_NUM_MAX=%i Exceeded %i Videos Recorded", VIDEO_NUM_RECYCLE_ON, VIDEO_NUM_MAX, videoCount, ) logging.info("Recorded %i of %i Videos", videoCount, VIDEO_NUM_MAX) else: logging.info( "Recorded %i Videos VIDEO_NUM_MAX=%i 0=Continuous", videoCount, VIDEO_NUM_MAX, ) else: logging.info( "Progress: %i Videos Recorded in Folder %s", videoCount, VIDEO_DIR ) if VIDEO_SESSION_MIN > 0: if timeUsed > VIDEO_SESSION_MIN * 60: keepRecording = False errorText = ( "Stop Recording Since VIDEO_SESSION_MIN=%i minutes Exceeded \n", VIDEO_SESSION_MIN, ) logging.warning(errorText) sys.stdout.write(errorText) else: logging.info( "Remaining Time %.1f of %i minutes", timeRemaining, VIDEO_SESSION_MIN, ) else: videoStartTime = datetime.datetime.now() logging.info("Exit: %i Videos Recorded in Folder %s", videoCount, VIDEO_DIR) # ------------------------------------------------------------------------------ def timolo(): """ Main motion and or motion tracking initialization and logic loop """ # Counter for showDots() display if not motion found # shows system is working cam_tl_pos = 0 # PANTILT_SEQ_STOPS List Start position of pantilt pan_x, tilt_y = PANTILT_SEQ_STOPS[cam_tl_pos] dotCount = 0 checkMediaPaths() timelapseNumCount = 0 motionNumCount = 0 tlstr = "" # Used to display if timelapse is selected mostr = "" # Used to display if motion is selected moCnt = "non" tlCnt = "non" daymode = False # Keep track of night and day based on dayPixAve motionFound = False take_timelapse = True stop_timelapse = False takeMotion = True stopMotion = False # Initialize some Timers pix_ave_timer = datetime.datetime.now() pantilt_seq_timer = datetime.datetime.now() motion_force_timer = datetime.datetime.now() timelapseExitStart = datetime.datetime.now() startTL = getSchedStart(TIMELAPSE_START_AT) startMO = getSchedStart(MOTION_START_AT) trackLen = 0.0 if SPACE_TIMER_HOURS > 0: lastSpaceCheck = datetime.datetime.now() if TIMELAPSE_ON: tlstr = "TimeLapse" # Check if timelapse subDirs reqd and create one if non exists tlPath = subDirChecks( TIMELAPSE_SUBDIR_MAX_HOURS, TIMELAPSE_SUBDIR_MAX_FILES, TIMELAPSE_DIR, TIMELAPSE_PREFIX, ) if TIMELAPSE_NUM_ON: timelapseNumCount = getCurrentCount(NUM_PATH_TIMELAPSE, TIMELAPSE_NUM_START) tlCnt = str(timelapseNumCount) else: logging.warning("Timelapse is Suppressed per TIMELAPSE_ON=%s", TIMELAPSE_ON) stop_timelapse = True if MOTION_TRACK_ON: logging.info("Start PiVideoStream ....") vs = PiVideoStream().start() vs.camera.rotation = IMAGE_ROTATION vs.camera.hflip = IMAGE_HFLIP vs.camera.vflip = IMAGE_VFLIP time.sleep(2) mostr = "Motion Tracking" # Check if motion subDirs required and # create one if required and non exists moPath = subDirChecks( MOTION_SUBDIR_MAX_HOURS, MOTION_SUBDIR_MAX_FILES, MOTION_DIR, MOTION_PREFIX ) if MOTION_NUM_ON: motionNumCount = getCurrentCount(NUM_PATH_MOTION, MOTION_NUM_START) moCnt = str(motionNumCount) trackTimeout = time.time() trackTimer = TRACK_TIMEOUT startPos = [] startTrack = False image1 = vs.read() image2 = vs.read() pixAve = getStreamPixAve(image2) grayimage1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY) daymode = checkIfDayStream(daymode, image2) else: vs = PiVideoStream().start() time.sleep(0.5) image2 = vs.read() # use video stream to check for pixAve & daymode pixAve = getStreamPixAve(image2) daymode = checkIfDayStream(daymode, image2) vs.stop() logging.info( "Motion Tracking is Suppressed per variable MOTION_TRACK_ON=%s", MOTION_TRACK_ON, ) stopMotion = True if TIMELAPSE_ON and MOTION_TRACK_ON: tlstr = " and " + tlstr displayInfo(moCnt, tlCnt) # Display config.py settings if LOG_TO_FILE_ON: logging.info("LOG_TO_FILE_ON=%s Logging to Console Disabled.", LOG_TO_FILE_ON) logging.info("Sending Console Messages to %s", LOG_FILE_PATH) logging.info("Entering Loop for %s%s", mostr, tlstr) else: if PLUGIN_ON: logging.info("plugin %s - Start %s%s Loop ...", PLUGIN_NAME, mostr, tlstr) else: logging.info("Start %s%s Loop ... ctrl-c Exits", mostr, tlstr) if MOTION_TRACK_ON and not checkSchedStart(startMO): logging.info('Motion Track: MOTION_START_AT = "%s"', MOTION_START_AT) logging.info("Motion Track: Sched Start Set For %s Please Wait ...", startMO) if TIMELAPSE_ON and not checkSchedStart(startTL): logging.info('Timelapse : TIMELAPSE_START_AT = "%s"', TIMELAPSE_START_AT) logging.info("Timelapee : Sched Start Set For %s Please Wait ...", startTL) logging.info("daymode=%s MOTION_DOTS_ON=%s ", daymode, MOTION_DOTS_ON) dotCount = showDots(MOTION_DOTS_MAX) # reset motion dots # Check to make sure PANTILT_ON is enabled if required. if PANTILT_SEQ_ON and not PANTILT_ON: logging.warning( "PANTILT_SEQ_ON=True but PANTILT_ON=False (Suggest you Enable PANTILT_ON=True)" ) if PANO_ON and not PANTILT_ON: logging.warning( "PANO_ON=True but PANTILT_ON=False (Suggest you Enable PANTILT_ON=True)" ) if (MOTION_TRACK_PANTILT_SEQ_ON and MOTION_TRACK_ON) and not PANTILT_ON: logging.warning( "MOTION_TRACK_PANTILT_SEQ_ON=True but PANTILT_ON=False (Suggest you Enable PANTILT_ON=True)" ) first_pano = True # Force a pano sequence on startup firstTimeLapse = True # Force a timelapse on startup while True: # Start main program Loop. motionFound = False if ( MOTION_TRACK_ON and (not MOTION_NUM_RECYCLE_ON) and (motionNumCount > MOTION_NUM_START + MOTION_NUM_MAX) and (not stopMotion) ): logging.warning( "MOTION_NUM_RECYCLE_ON=%s and motionNumCount %i Exceeds %i", MOTION_NUM_RECYCLE_ON, motionNumCount, MOTION_NUM_START + MOTION_NUM_MAX, ) logging.warning("Suppressing Further Motion Tracking") logging.warning( "To Reset: Change %s Settings or Archive Images", CONFIG_FILENAME ) logging.warning( "Then Delete %s and Restart %s \n", NUM_PATH_MOTION, PROG_NAME ) takeMotion = False stopMotion = True if stop_timelapse and stopMotion and not PANTILT_SEQ_ON and not PANO_ON and not VIDEO_REPEAT_ON: logging.warning( "NOTICE: Motion, Timelapse, pantilt_seq, Pano and Video Repeat are Disabled" ) logging.warning( "per Num Recycle=False and " "Max Counter Reached or TIMELAPSE_EXIT_SEC Settings" ) logging.warning( "Change %s Settings or Archive/Save Media Then", CONFIG_FILENAME ) logging.warning("Delete appropriate .dat File(s) to Reset Counter(s)") logging.warning("Exiting %s %s \n", PROG_NAME, PROG_VER) sys.exit(1) # if required check free disk space and delete older files (jpg) if SPACE_TIMER_HOURS > 0: lastSpaceCheck = freeDiskSpaceCheck(lastSpaceCheck) # check the timer for measuring pixel average of stream image frame pix_ave_timer, take_pix_ave = checkTimer(pix_ave_timer, IMAGE_PIX_AVE_TIMER_SEC) # use image2 to check daymode as image1 may be average # that changes slowly, and image1 may not be updated if take_pix_ave: pixAve = getStreamPixAve(image2) daymode = checkIfDayStream(daymode, image2) if daymode != checkIfDayStream(daymode, image2): daymode = not daymode if MOTION_TRACK_ON: if daymode != checkIfDayStream(daymode, image2): daymode = not daymode image2 = vs.read() image1 = image2 else: image2 = vs.read() elif TIMELAPSE_ON: vs = PiVideoStream().start() time.sleep(0.5) image2 = vs.read() # use video stream to check for daymode vs.stop() if not daymode and TIMELAPSE_ON: time.sleep(0.02) # short delay to aviod high cpu usage at night # Don't take images if IMAGE_NO_NIGHT_SHOTS # or IMAGE_NO_DAY_SHOTS settings are True if not timeToSleep(daymode): # Check if it is time for pantilt sequence if PANTILT_ON and PANTILT_SEQ_ON: pantilt_seq_timer, take_pantilt_sequence = checkTimer( pantilt_seq_timer, PANTILT_SEQ_TIMER_SEC ) if take_pantilt_sequence: if MOTION_TRACK_ON: vs.stop() time.sleep(STREAM_STOP_SEC) seq_prefix = PANTILT_SEQ_IMAGE_PREFIX + IMAGE_NAME_PREFIX seq_num_count = getCurrentCount( NUM_PATH_PANTILT_SEQ, PANTILT_SEQ_NUM_START ) filename = getImageFilename( PANTILT_SEQ_IMAGES_DIR, seq_prefix, PANTILT_SEQ_NUM_ON, seq_num_count, ) seq_num_count = takePantiltSequence( filename, daymode, pixAve, seq_num_count, NUM_PATH_PANTILT_SEQ ) if MOTION_TRACK_ON: vs = PiVideoStream().start() vs.camera.rotation = IMAGE_ROTATION vs.camera.hflip = IMAGE_HFLIP vs.camera.vflip = IMAGE_VFLIP time.sleep(1) # Allow camera to warm up and stream to start next_seq_time = pantilt_seq_timer + datetime.timedelta( seconds=PANTILT_SEQ_TIMER_SEC ) next_seq_at = "%02d:%02d:%02d" % ( next_seq_time.hour, next_seq_time.minute, next_seq_time.second, ) logging.info( "Next Pantilt Sequence in %i seconds at %s Waiting ...", PANTILT_SEQ_TIMER_SEC, next_seq_at ) # Process Timelapse events per timers if TIMELAPSE_ON and checkSchedStart(startTL): # Check for a scheduled date/time to start timelapse if firstTimeLapse: timelapse_timer = datetime.datetime.now() firstTimeLapse = False take_timelapse = True else: timelapse_timer, take_timelapse = checkTimer( timelapse_timer, TIMELAPSE_TIMER_SEC ) if (not stop_timelapse) and take_timelapse and TIMELAPSE_EXIT_SEC > 0: if ( datetime.datetime.now() - timelapseExitStart ).total_seconds() > TIMELAPSE_EXIT_SEC: logging.info( "TIMELAPSE_EXIT_SEC=%i Exceeded.", TIMELAPSE_EXIT_SEC ) logging.info("Suppressing Further Timelapse Images") logging.info( "To RESET: Restart %s to Restart " "TIMELAPSE_EXIT_SEC Timer. \n", PROG_NAME, ) # Suppress further timelapse images take_timelapse = False stop_timelapse = True if ( (not stop_timelapse) and TIMELAPSE_NUM_ON and (not TIMELAPSE_NUM_RECYCLE_ON) ): if TIMELAPSE_NUM_MAX > 0 and timelapseNumCount > ( TIMELAPSE_NUM_START + TIMELAPSE_NUM_MAX ): logging.warning( "TIMELAPSE_NUM_RECYCLE_ON=%s and Counter=%i Exceeds %i", TIMELAPSE_NUM_RECYCLE_ON, timelapseNumCount, TIMELAPSE_NUM_START + TIMELAPSE_NUM_MAX, ) logging.warning("Suppressing Further Timelapse Images") logging.warning( "To RESET: Change %s Settings or Archive Images", CONFIG_FILENAME, ) logging.warning( "Then Delete %s and Restart %s \n", NUM_PATH_TIMELAPSE, PROG_NAME, ) # Suppress further timelapse images take_timelapse = False stop_timelapse = True if take_timelapse and (not stop_timelapse): # Reset the timelapse timer if MOTION_DOTS_ON and MOTION_TRACK_ON: # reset motion dots dotCount = showDots(MOTION_DOTS_MAX + 2) else: print("") if PLUGIN_ON: if TIMELAPSE_EXIT_SEC > 0: exitSecProgress = ( datetime.datetime.now() - timelapseExitStart ).total_seconds() logging.info( "%s Sched TimeLapse daymode=%s Timer=%i sec" " ExitSec=%i/%i Status", PLUGIN_NAME, daymode, TIMELAPSE_TIMER_SEC, exitSecProgress, TIMELAPSE_EXIT_SEC, ) else: logging.info( "%s Sched TimeLapse daymode=%s" " Timer=%i sec ExitSec=%i 0=Continuous", PLUGIN_NAME, daymode, TIMELAPSE_TIMER_SEC, TIMELAPSE_EXIT_SEC, ) else: if TIMELAPSE_EXIT_SEC > 0: exitSecProgress = ( datetime.datetime.now() - timelapseExitStart ).total_seconds() logging.info( "Sched TimeLapse daymode=%s Timer=%i sec" " ExitSec=%i/%i Status", daymode, TIMELAPSE_TIMER_SEC, exitSecProgress, TIMELAPSE_EXIT_SEC, ) else: logging.info( "Sched TimeLapse daymode=%s Timer=%i sec" " ExitSec=%i 0=Continuous", daymode, TIMELAPSE_TIMER_SEC, TIMELAPSE_EXIT_SEC, ) tl_prefix = TIMELAPSE_PREFIX + IMAGE_NAME_PREFIX filename = getImageFilename( tlPath, tl_prefix, TIMELAPSE_NUM_ON, timelapseNumCount ) if MOTION_TRACK_ON: logging.info("Stop Motion Tracking PiVideoStream ...") vs.stop() time.sleep(STREAM_STOP_SEC) # Time to take a Day or Night Time Lapse Image if daymode: takeDayImage(filename, TIMELAPSE_CAM_SLEEP_SEC) else: takeNightImage(filename, pixAve) timelapseNumCount = postImageProcessing( TIMELAPSE_NUM_ON, TIMELAPSE_NUM_START, TIMELAPSE_NUM_MAX, timelapseNumCount, TIMELAPSE_NUM_RECYCLE_ON, NUM_PATH_TIMELAPSE, filename, daymode, ) saveRecent( TIMELAPSE_RECENT_MAX, TIMELAPSE_RECENT_DIR, filename, tl_prefix ) if MOTION_TRACK_ON: logging.info("Restart Motion Tracking PiVideoStream ....") vs = PiVideoStream().start() vs.camera.rotation = IMAGE_ROTATION vs.camera.hflip = IMAGE_HFLIP vs.camera.vflip = IMAGE_VFLIP time.sleep(1) # Allow camera to warm up and stream to start if TIMELAPSE_MAX_FILES > 0: deleteOldFiles(TIMELAPSE_MAX_FILES, TIMELAPSE_DIR, tl_prefix) dotCount = showDots(MOTION_DOTS_MAX) tlPath = subDirChecks( TIMELAPSE_SUBDIR_MAX_HOURS, TIMELAPSE_SUBDIR_MAX_FILES, TIMELAPSE_DIR, TIMELAPSE_PREFIX, ) next_timelapse_time = timelapse_timer + datetime.timedelta( seconds=TIMELAPSE_TIMER_SEC ) next_timelapse_at = "%02d:%02d:%02d" % ( next_timelapse_time.hour, next_timelapse_time.minute, next_timelapse_time.second, ) logging.info("Next Timelapse at %s Waiting ...", next_timelapse_at) pantiltGoHome() # Monitor for motion tracking events # and trigger selected action eg image, quick pic, video, mini TL, pantilt if ( MOTION_TRACK_ON and checkSchedStart(startMO) and takeMotion and (not stopMotion) ): # IMPORTANT - Night motion tracking may not work very well # due to long exposure times and low light image2 = vs.read() grayimage2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY) movePoint1 = getMotionTrackPoint(grayimage1, grayimage2) grayimage1 = grayimage2 if movePoint1 and not startTrack: startTrack = True trackTimeout = time.time() startPos = movePoint1 image2 = vs.read() grayimage2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY) movePoint2 = getMotionTrackPoint(grayimage1, grayimage2) if movePoint2 and startTrack: # Two sets of movement required trackLen = trackMotionDistance(startPos, movePoint2) # wait until track well started if trackLen > TRACK_TRIG_LEN_MIN: # Reset tracking timer object moved trackTimeout = time.time() if MOTION_TRACK_INFO_ON: logging.info( "Track Progress From(%i,%i) To(%i,%i) trackLen=%i/%i px", startPos[0], startPos[1], movePoint2[0], movePoint2[1], trackLen, TRACK_TRIG_LEN, ) # Track length triggered if trackLen >= TRACK_TRIG_LEN: # reduce chance of two objects at different positions if trackLen >= TRACK_TRIG_LEN_MAX: motionFound = False if MOTION_TRACK_INFO_ON: logging.info( "TrackLen %i px Exceeded %i px Max Trig Len Allowed.", trackLen, TRACK_TRIG_LEN_MAX, ) else: motionFound = True if PLUGIN_ON: logging.info( "%s Motion Triggered Start(%i,%i)" " End(%i,%i) trackLen=%.i/%i px", PLUGIN_NAME, startPos[0], startPos[1], movePoint2[0], movePoint2[1], trackLen, TRACK_TRIG_LEN, ) else: logging.info( "Motion Triggered Start(%i,%i)" " End(%i,%i) trackLen=%i/%i px", startPos[0], startPos[1], movePoint2[0], movePoint2[1], trackLen, TRACK_TRIG_LEN, ) print("") image1 = vs.read() image2 = image1 grayimage1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY) grayimage2 = grayimage1 startTrack = False startPos = [] trackLen = 0.0 # Track timed out if (time.time() - trackTimeout > trackTimer) and startTrack: image1 = vs.read() image2 = image1 grayimage1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY) grayimage2 = grayimage1 if MOTION_TRACK_ON and MOTION_TRACK_INFO_ON: logging.info( "Track Timer %.2f sec Exceeded. Reset Track", trackTimer ) startTrack = False startPos = [] trackLen = 0.0 if MOTION_FORCE_SEC > 0: motion_force_timer, motion_force_start = checkTimer( motion_force_timer, MOTION_FORCE_SEC ) else: motion_force_start = False if motion_force_start: image1 = vs.read() image2 = image1 grayimage1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY) grayimage2 = grayimage1 dotCount = showDots(MOTION_DOTS_MAX + 2) # New Line logging.info( "No Motion Detected for %s minutes. " "Taking Forced Motion Image.", (MOTION_FORCE_SEC / 60), ) if motionFound or motion_force_start: motion_prefix = MOTION_PREFIX + IMAGE_NAME_PREFIX filename = getImageFilename( moPath, motion_prefix, MOTION_NUM_ON, motionNumCount ) vs.stop() time.sleep(STREAM_STOP_SEC) # Save stream image frame to capture movement quickly if MOTION_TRACK_QUICK_PIC_ON: takeMotionQuickImage(image2, filename) motionNumCount = postImageProcessing( MOTION_NUM_ON, MOTION_NUM_START, MOTION_NUM_MAX, motionNumCount, MOTION_NUM_RECYCLE_ON, NUM_PATH_MOTION, filename, daymode, ) saveRecent( MOTION_RECENT_MAX, MOTION_RECENT_DIR, filename, motion_prefix, ) # Save a series of images per settings (no pantilt) elif MOTION_TRACK_MINI_TL_ON and daymode: with picamera.PiCamera() as camera: camera.resolution = (image_width, image_height) camera.vflip = IMAGE_VFLIP camera.hflip = IMAGE_HFLIP # valid rotation values 0, 90, 180, 270 camera.rotation = IMAGE_ROTATION time.sleep(MOTION_CAM_SLEEP) # This uses yield to loop through time lapse # sequence but does not seem to be faster # due to writing images camera.capture_sequence( takeMiniTimelapse( moPath, motion_prefix, MOTION_NUM_ON, motionNumCount, daymode, NUM_PATH_MOTION, ) ) camera.close() motionNumCount = getCurrentCount( NUM_PATH_MOTION, MOTION_NUM_START ) # Move camera pantilt through specified positions and take images elif MOTION_TRACK_ON and PANTILT_ON and MOTION_TRACK_PANTILT_SEQ_ON: motionNumCount = takePantiltSequence( filename, daymode, pixAve, motionNumCount, NUM_PATH_MOTION ) pantiltGoHome() elif MOTION_VIDEO_ON: filename = getVideoName( MOTION_PATH, motion_prefix, MOTION_NUM_ON, motionNumCount ) takeVideo( filename, MOTION_VIDEO_TIMER_SEC, MOTION_VIDEO_WIDTH, MOTION_VIDEO_HEIGHT, MOTION_VIDEO_FPS, ) if MOTION_NUM_ON: motionNumCount += 1 writeCounter(motionNumCount, NUM_PATH_MOTION) else: if daymode: takeDayImage(filename, MOTION_CAM_SLEEP) else: takeNightImage(filename, pixAve) motionNumCount = postImageProcessing( MOTION_NUM_ON, MOTION_NUM_START, MOTION_NUM_MAX, motionNumCount, MOTION_NUM_RECYCLE_ON, NUM_PATH_MOTION, filename, daymode, ) saveRecent( MOTION_RECENT_MAX, MOTION_RECENT_DIR, filename, motion_prefix, ) vs = PiVideoStream().start() vs.camera.rotation = IMAGE_ROTATION vs.camera.hflip = IMAGE_HFLIP vs.camera.vflip = IMAGE_VFLIP time.sleep(1) image1 = vs.read() image2 = image1 grayimage1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY) grayimage2 = grayimage1 trackLen = 0.0 trackTimeout = time.time() startPos = [] startTrack = False moPath = subDirChecks( MOTION_SUBDIR_MAX_HOURS, MOTION_SUBDIR_MAX_FILES, MOTION_DIR, MOTION_PREFIX, ) logging.info("Waiting for Next Motion Tracking Event ...") # Take panoramic images and stitch together if possible per settings if PANTILT_ON and PANO_ON: # force a pano on first startup then go by timer. if first_pano: first_pano = False start_pano = True pano_seq_num = getCurrentCount(NUM_PATH_PANO, PANO_NUM_START) pano_timer = datetime.datetime.now() else: # Check if pano timer expired and if so start a pano sequence pano_timer, start_pano = checkTimer(pano_timer, PANO_TIMER_SEC) if start_pano: if MOTION_TRACK_ON: logging.info("Stop Motion Tracking PiVideoStream ...") vs.stop() time.sleep(STREAM_STOP_SEC) pano_seq_num = takePano(pano_seq_num, daymode, pixAve) if MOTION_TRACK_ON: logging.info("Restart Motion Tracking PiVideoStream ....") vs = PiVideoStream().start() vs.camera.rotation = IMAGE_ROTATION vs.camera.hflip = IMAGE_HFLIP vs.camera.vflip = IMAGE_VFLIP time.sleep(1) next_pano_time = pano_timer + datetime.timedelta( seconds=PANO_TIMER_SEC ) next_pano_at = "%02d:%02d:%02d" % ( next_pano_time.hour, next_pano_time.minute, next_pano_time.second, ) logging.info("Next Pano at %s Waiting ...", next_pano_at) if motionFound and motionCode: # =========================================== # Put your user code in userMotionCode() function # In the File user_motion_code.py # =========================================== try: user_motion_code.userMotionCode(filename) dotCount = showDots(MOTION_DOTS_MAX) except ValueError: logging.error( "Problem running userMotionCode function from File %s", userMotionFilePath, ) else: # show progress dots when no motion found dotCount = showDots(dotCount) # ------------------------------------------------------------------------------ if __name__ == "__main__": """ Initialization prior to launching appropriate pi-timolo options """ logging.info("Testing if Pi Camera is in Use") # Test if the pi camera is already in use ts = PiVideoStream().start() time.sleep(1) ts.stop() time.sleep(STREAM_STOP_SEC) logging.info("Pi Camera is Available.") if PANTILT_ON: logging.info("Camera Pantilt Hardware is %s", pantilt_is) if PLUGIN_ON: logging.info( "Start pi-timolo per %s and plugins/%s.py Settings", CONFIG_FILE_PATH, PLUGIN_NAME, ) else: logging.info("Start pi-timolo per %s Settings", CONFIG_FILE_PATH) if not VERBOSE_ON: print("NOTICE: Logging Disabled per variable VERBOSE_ON=False ctrl-c Exits") try: pantiltGoHome() if VIDEO_REPEAT_ON: videoRepeat() else: timolo() except KeyboardInterrupt: print("") pantiltGoHome() # Ensure mouse is returned to home position if VERBOSE_ON: logging.info("User Pressed Keyboard ctrl-c") logging.info("Exiting %s %s", PROG_NAME, PROG_VER) else: sys.stdout.write("User Pressed Keyboard ctrl-c \n") sys.stdout.write("Exiting %s %s \n" % (PROG_NAME, PROG_VER)) try: if PLUGIN_ON: if os.path.isfile(pluginCurrent): os.remove(pluginCurrent) pluginCurrentpyc = os.path.join(pluginDir, "current.pyc") if os.path.isfile(pluginCurrentpyc): os.remove(pluginCurrentpyc) except OSError as err: logging.warning("Failed To Remove File %s - %s", pluginCurrentpyc, err) sys.exit(1)