#!/usr/bin/python # A surveillance camera script for the raspberry pi camera. # When the camera detects motion, a video is recorded. # Optionally, a live video stream is accessible over http. # On a default Raspian installation, the package 'python-opencv' is required: # apt-get install python-opencv # Known Issues # # 2019-09-24 (stretch) # The automatic white balance (awb) algorithm was updated in firmware # with kernel 4.19. This caused spontaneous purple distortions # when recording dark scenes. The old algorithm is still available and # can be activated with the following command: # vcdbg set awb_mode 0 # Alternatively, the latest 4.14 kernel can be used: # BRANCH='stable' rpi-update 45a2e771e7272781e62ae92322734c2b90e0268a # # 2019-05-24 (stretch) # self.video_writer.write(frame) has a memory leak. # This script will exit after config['output']['max_frames_written'] # has been reached. # # 2017-05-24 (jessie) # There is an issue with cv2.VideoWriter (or the MJPG codec). # At some point, all newly created files have size zero after a frame # should have been written. # # As a workaround for the issues above, # the following shell script can be used to restart this python script: # # vcdbg set awb_mode 0 # RESTART=1 # while [ "$RESTART" -ne 0 ]; do # ./cam_surveillance.py # RESTART=$? # done # __author__ = "Gernot Walzl" __date__ = "2019-11-04" import os import sys import logging import signal from time import time, sleep from datetime import datetime from logging.handlers import TimedRotatingFileHandler from picamera import PiCamera from picamera.array import PiRGBArray import cv2 from threading import Thread from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer import base64 try: from configparser import ConfigParser except ImportError: from ConfigParser import ConfigParser def load_config(): config = ConfigParser() config.add_section('camera') config.set('camera', 'width', '960') config.set('camera', 'height', '720') config.set('camera', 'fps', '5') config.set('camera', 'mode', '0') config.set('camera', 'rotation', '0') config.add_section('output') config.set('output', 'path', os.path.expanduser('~/camera/')) config.set('output', 'max_used_space', str(32 * 1024**3)) config.set('output', 'min_length_video', '5.0') config.set('output', 'max_frames_written', '9000') config.add_section('server') config.set('server', 'port', '8000') config.set('server', 'user', 'user') config.set('server', 'pass', 'pass') try: configfilepath = os.path.expanduser('~/.config/cam_surveillance.ini') with open(configfilepath) as configfile: config.readfp(configfile) except IOError: pass return config def init_logging(): logger = logging.getLogger() logger.setLevel(logging.INFO) loghandler = TimedRotatingFileHandler( config.get('output', 'path') + 'cam_surveillance.log', 'midnight') formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s') loghandler.setFormatter(formatter) logger.addHandler(loghandler) class SurveillanceCamera: def __init__(self): self.logger = logging.getLogger(self.__class__.__name__) self.path_output = config.get('output', 'path') self.min_length_video = config.getfloat('output', 'min_length_video') self.max_frames_written = config.getint('output', 'max_frames_written') self.camera = None self.capturing = False self.capturing_retval = 0 self.frame_curr_bgr = None self.frame_curr_annotated = None self.frame_curr_gray = None self.frame_prev_bgr = None self.frame_prev_annotated = None self.frame_prev_gray = None self.time_last_change = 0.0 self.video_writer = None self.recording = False self.filenumber = 0 self.num_frames_written = 0 def has_frame_changed(self, frame_gray_1, frame_gray_2, min_changed_pixels): frame_diff_gray = cv2.absdiff(frame_gray_1, frame_gray_2) frame_thresh = cv2.threshold(frame_diff_gray, 32, 255, cv2.THRESH_BINARY)[1] num_diff_pixels = frame_thresh.sum() / 255 return num_diff_pixels >= min_changed_pixels def annotate_frame(self, frame_bgr): timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') frame = frame_bgr.copy() position = (8, 16) font = cv2.FONT_HERSHEY_PLAIN color = (255, 255, 255) cv2.putText(frame, timestamp, position, font, 1.0, color) return frame def write_frame(self, frame_bgr): self.video_writer.write(frame_bgr) self.num_frames_written += 1 def fourcc(self, c1, c2, c3, c4): if hasattr(cv2, 'cv'): return cv2.cv.FOURCC(c1, c2, c3, c4) else: return cv2.VideoWriter_fourcc(c1, c2, c3, c4) def start_recording(self): self.filenumber += 1 dt_now = datetime.now() dirname = dt_now.strftime('%Y-%m-%d') basename = dt_now.strftime('%Y-%m-%d_%H%M%S') + '_' + str(self.filenumber).zfill(4) filename = self.path_output + dirname + '/' + basename + '.avi' fourcc = self.fourcc('M', 'J', 'P', 'G') if not os.path.exists(self.path_output + dirname): os.makedirs(self.path_output + dirname) self.logger.info("Start recording to \"" + filename + "\"") self.recording = True self.logger.debug("fourcc=" + str(fourcc) + ", fps=" + str(self.camera.framerate) + ", res=" + str(self.camera.resolution)) self.video_writer = cv2.VideoWriter(filename, fourcc, self.camera.framerate, self.camera.resolution) self.logger.debug("video_writer=" + str(self.video_writer)+ " isOpened=" + str(self.video_writer.isOpened())) self.logger.debug("frame_shape=" + str(self.frame_prev_bgr.shape)) self.write_frame(self.frame_prev_annotated) #self.camera.start_recording(filename) #if os.path.getsize(filename) == 0: # self.logger.error("Unable to encode frames into file " + # "(filesize=0)") # self.stop_capturing(1) def stop_recording(self): if self.video_writer: self.video_writer.release() self.video_writer = None #self.camera.stop_recording() self.recording = False self.logger.info("Recording stopped") def handle_frame(self, frame): self.frame_curr_bgr = frame self.frame_curr_annotated = self.annotate_frame(frame) self.frame_curr_gray = cv2.cvtColor(self.frame_curr_bgr, cv2.COLOR_BGR2GRAY) # resizing is faster than cv2.GaussianBlur(frame_curr_gray, (33, 33), 0) self.frame_curr_gray = cv2.resize(self.frame_curr_gray, (160, 120)) if self.frame_prev_gray is not None: if self.recording: if self.has_frame_changed(self.frame_prev_gray, self.frame_curr_gray, 8): self.time_last_change = time() else: if self.has_frame_changed(self.frame_prev_gray, self.frame_curr_gray, 32): self.time_last_change = time() self.start_recording() if self.recording: self.write_frame(self.frame_curr_annotated) if self.time_last_change + self.min_length_video < time(): self.stop_recording() self.frame_prev_bgr = self.frame_curr_bgr self.frame_prev_annotated = self.frame_curr_annotated self.frame_prev_gray = self.frame_curr_gray def start_capturing(self): try: with PiCamera(resolution = ( config.getint('camera', 'width'), config.getint('camera', 'height')), framerate = config.getint('camera', 'fps'), sensor_mode = config.getint('camera', 'mode')) as self.camera: self.camera.rotation = config.getint('camera', 'rotation') with PiRGBArray(self.camera) as output: self.logger.info("Start capturing") self.capturing = True for _ in self.camera.capture_continuous(output, 'bgr', use_video_port=True): self.handle_frame(output.array) output.seek(0) output.truncate() if not self.capturing: break if not self.recording: if self.num_frames_written >= self.max_frames_written: self.logger.info( "Maximum number of frames written " + "(num_frames_written="+str(self.num_frames_written)+")") self.stop_capturing(1) finally: if self.recording: self.stop_recording() self.logger.info("Capturing stopped " + "(retval="+str(self.capturing_retval)+")") return self.capturing_retval def stop_capturing(self, retval=0): self.capturing_retval = retval self.capturing = False def get_frame_annotated(self): if self.capturing: return self.frame_curr_annotated else: return None class CameraHTTPRequestHandler(BaseHTTPRequestHandler): def logger(self): return logging.getLogger('HTTPRequestHandler') def check_auth(self): if (self.server.server_auth is None or self.server.server_auth == self.headers.getheader('Authorization')): return True else: self.send_response(401) self.send_header('WWW-Authenticate', 'Basic') self.end_headers() return False def send_jpg(self): frame = survcam.get_frame_annotated() if frame is None: self.logger().warning("Did not get a frame to send") return False else: ret, jpg = cv2.imencode('.jpg', frame) self.send_header('Content-type', 'image/jpeg') self.send_header('Content-length', str(jpg.size)) self.end_headers() self.wfile.write(jpg.tostring()) return True def do_GET(self): if self.path == "/cam.jpg": if self.check_auth(): self.send_response(200) self.send_jpg() elif self.path == "/cam.mjpg": if self.check_auth(): self.send_response(200) self.send_header('Content-type', 'multipart/x-mixed-replace; boundary=jpgboundary') self.end_headers() while not self.wfile.closed: self.wfile.write("--jpgboundary\n") if not self.send_jpg(): break self.wfile.write("\n") self.wfile.flush() sleep(0.2) else: self.send_error(404) def log_error(self, format, *args): self.logger().error("%s - %s" % (self.address_string(), format%args)) def log_message(self, format, *args): self.logger().info("%s - %s" % (self.address_string(), format%args)) class CameraHTTPServer(HTTPServer): def handle_error(self, request, client_address): logging.getLogger('HTTPServer').warning( "Exception happened during processing of request from "+ str(client_address)) class HTTPServerThread(Thread): def __init__(self): Thread.__init__(self) self.logger = logging.getLogger(self.__class__.__name__) self.server = CameraHTTPServer(('', config.getint('server', 'port')), CameraHTTPRequestHandler) self.server.server_auth = None if config.get('server', 'user') and config.get('server', 'pass'): self.server.server_auth = 'Basic ' + base64.b64encode( config.get('server', 'user')+':'+config.get('server', 'pass')) def run(self): self.logger.info("Starting HTTP server on port " + str(self.server.server_port)) self.server.serve_forever() def stop_serving(self): self.logger.info("Stopping HTTP server") self.server.shutdown() class StorageCleaner: def __init__(self, path, max_used_space): self.logger = logging.getLogger(self.__class__.__name__) self.path = path self.max_used_space = max_used_space def get_filelist(self): filelist = list() for dirpath, _, filenames in os.walk(self.path): for filename in filenames: filepath = os.path.join(dirpath, filename) filemtime = os.path.getmtime(filepath) filesize = os.path.getsize(filepath) fileinfo = (filemtime, filesize, filepath) filelist.append(fileinfo) filelist.sort() return filelist def remove_oldest_files(self): filelist = self.get_filelist() sumfilesizes = sum(filesize for _, filesize, _ in filelist) for fileinfo in filelist: if sumfilesizes > self.max_used_space: filepath = fileinfo[2] self.logger.info("Removing "+filepath) os.remove(filepath) sumfilesizes -= fileinfo[1] else: break def remove_empty_dirs(self): for dirpath, dirnames, filenames in os.walk(self.path): if (not dirnames) and (not filenames): self.logger.info("Removing empty dir "+dirpath) os.rmdir(dirpath) class StorageCleanerThread(Thread): def __init__(self): Thread.__init__(self) self.cleaning = False self.cleaner = StorageCleaner(config.get('output', 'path'), config.getint('output', 'max_used_space')) def run(self): self.cleaning = True while self.cleaning: self.cleaner.remove_oldest_files() self.cleaner.remove_empty_dirs() for _ in xrange(60): sleep(1) if not self.cleaning: break def stop_cleaning(self): self.cleaning = False class SignalHandler: def __init__(self): self.logger = logging.getLogger(self.__class__.__name__) signal.signal(signal.SIGINT, self.handle_signal) # Ctrl-C signal.signal(signal.SIGTERM, self.handle_signal) # kill def handle_signal(self, signum, frame): self.logger.info("Handling signal " + str(signum)) survcam.stop_capturing() if __name__ == '__main__': config = load_config(); if not os.path.exists(config.get('output', 'path')): os.makedirs(config.get('output', 'path')) init_logging() logging.info("Logging initialized") survcam = SurveillanceCamera() httpthread = None if config.getint('server', 'port') > 0: httpthread = HTTPServerThread() httpthread.start() storagecleanerthread = None if config.getint('output', 'max_used_space') > 0: storagecleanerthread = StorageCleanerThread() storagecleanerthread.start() sighandler = SignalHandler() exit_status = survcam.start_capturing() if storagecleanerthread: storagecleanerthread.stop_cleaning() if httpthread: httpthread.stop_serving() logging.info("Exit with status "+str(exit_status)) sys.exit(exit_status)