#!/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-05-24 (stretch) # self.video_writer.write(frame) has a memory leak. # This script will exit after cfg['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: # # RESTART=1 # while [ "$RESTART" -ne 0 ]; do # ./cam_surveillance.py # RESTART=$? # done # __author__ = "Gernot Walzl" __date__ = "2019-05-28" import os import sys import logging import signal from time import time, sleep from datetime import datetime from picamera import PiCamera from picamera.array import PiRGBArray import cv2 from threading import Thread from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer import base64 cfg = dict() cfg['camera_resolution'] = (640, 480) cfg['camera_fps'] = 5 cfg['path_output'] = "./camera/" cfg['min_length_video'] = 5.0 cfg['max_frames_written'] = 9000 cfg['server_port'] = 8000 cfg['server_auth'] = "user:pass" class SurveillanceCamera: def __init__(self): self.path_output = cfg['path_output'] self.min_length_video = cfg['min_length_video'] self.max_frames_written = cfg['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 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 + '.mjpg' #fourcc = cv2.cv.FOURCC('X', 'V', 'I', 'D') fourcc = cv2.cv.FOURCC('M', 'J', 'P', 'G') if not os.path.exists(self.path_output + dirname): os.makedirs(self.path_output + dirname) logging.info("Start recording to \"" + filename + "\"") self.recording = True logging.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) logging.debug("video_writer=" + str(self.video_writer)+ " isOpened=" + str(self.video_writer.isOpened())) logging.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: logging.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 logging.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() as self.camera: self.camera.resolution = cfg['camera_resolution'] self.camera.framerate = cfg['camera_fps'] #self.camera.rotation = 180 with PiRGBArray(self.camera) as output: logging.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: logging.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() logging.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 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: logging.warning("HTTPRequestHandler 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): logging.error("%s - %s" % (self.address_string(), format%args)) def log_message(self, format, *args): logging.info("%s - %s" % (self.address_string(), format%args)) class CameraHTTPServer(HTTPServer): def handle_error(self, request, client_address): logging.warning("Exception happened during processing of request from "+ str(client_address)) class HTTPServerThread(Thread): def __init__(self): Thread.__init__(self) self.server = CameraHTTPServer(('', cfg['server_port']), CameraHTTPRequestHandler) self.server.server_auth = None if cfg['server_auth']: self.server.server_auth = 'Basic '+base64.b64encode(cfg['server_auth']) def run(self): logging.info("Starting HTTP server on port " + str(self.server.server_port)) self.server.serve_forever() def stop_serving(self): logging.info("Stopping HTTP server") self.server.shutdown() class SignalHandler: def __init__(self): signal.signal(signal.SIGINT, self.handle_signal) # Ctrl-C signal.signal(signal.SIGTERM, self.handle_signal) # kill def handle_signal(self, signum, frame): logging.info("Handling signal " + str(signum)) survcam.stop_capturing() if __name__ == '__main__': if not os.path.exists(cfg['path_output']): os.makedirs(cfg['path_output']) logging.basicConfig(filename=cfg['path_output']+'cam_surveillance.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') survcam = SurveillanceCamera() httpthread = None if cfg['server_port'] > 0: httpthread = HTTPServerThread() httpthread.start() sighandler = SignalHandler() exit_status = survcam.start_capturing() if httpthread: httpthread.stop_serving() logging.info("Exit with status "+str(exit_status)) sys.exit(exit_status)