Skip to main content

PI 카메라 <핵심> piwebcamera.py

piwebcamera.py

 

import os
import sys
import time
import math
import getopt
import numpy as np
import cv2
import threading
import subprocess
from collections import deque
from slackSender import send_slack_mp4
from slackSender import send_slack_img
from gpiofiring import BoobyTrapFiring

from lock_manager import Lock_Manager
from util import Util


# Set target area 
X1_RATE = 0.1 # withd = 10%
X2_RATE = 0.9 # width = 90%
Y1_RATE = 0.1 # height = 20% 
Y2_RATE = 0.9 # height = 90%

gpiofiring = BoobyTrapFiring()

class PiWebCamera(threading.Thread):
	def __init__(self, video_source=0, source=None, do_record=True, do_display=True, do_add_contours=True, do_add_target=False):
		
		threading.Thread.__init__(self)

		self.name = self.__class__.__name__
		self.archive = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'archive')

		self.writer = None
		self.current_frame = None

		self.codec = cv2.VideoWriter_fourcc('M','J', 'P', 'G')
		self.OBSERVER_LENGTH = 5 # Time in seconds to be observed for motion
		self.threshold = 15

		self.CAMERA_SOURCE = video_source
		self.REMAIN_RECORDING_FILES = 10 # 10이상 부터 삭제 후 저장
		self.do_display = do_display
		self.do_record = do_record
		self.do_add_contours = do_add_contours
		self.do_add_target = do_add_target
		self.current_file = None

		self.source = cv2.VideoCapture(source) if source is not None else self.init_camera()

		self.fps = self.find_fps(self.source)
		self.height, self.width = self.get_dimensions(self.source)
		Util.log(self.name, "Initializing pi camera class with video_source=" + str(self.CAMERA_SOURCE))
		Util.log(self.name,"width: {"+str(self.width)+"}, height : {"+str(self.height)+"}")

		self.lock_manager = Lock_Manager("motion")

	def __del__(self):
		# Release camera
		self.source.release()

		# Close all windows
		cv2.destroyAllWindows()

		# Remove lock if exists
		self.lock_manager.remove()

	def get_captureFrame(self):
		return self.current_frame if self.current_frame is not None else None

	def get_frame(self):
		"""
		Return the current frame

		@return bytes
		"""
		return self.frame_to_jpg(self.current_frame) if self.current_frame is not None else None

	def frame_to_jpg(self, frame):
		"""
		Convert video frame to jpg

		@param array frame
		@return bytes
		"""
		ret, jpeg = cv2.imencode('.jpg', self.current_frame)
		return jpeg.tobytes()

	def get_dimensions(self, source):
		"""
		Determine height and width of the video source

		@return tuple(int, int)
		"""
		frame = cv2.cvtColor(source.read()[1],cv2.COLOR_RGB2GRAY)
		return frame.shape[0: 2]

	def find_fps(self, source):
		"""
		Determine frames per second of the video source

		@param video source
		@return int
		"""
		Util.log(self.name, "Determining FPS...")

		# How many frames to capture
		num_frames = 120

		# Start time
		start = time.time()

		# Grab a few frames
		for i in range(0, num_frames):
			ret, frame = source.read()

		# End time
		end = time.time()

		# Calculate frames per second
		fps = int(math.floor(num_frames / (end - start)))
		Util.log(self.name, "Setting FPS to " + str(fps))

		return fps

	def init_camera(self):
		"""
		Start the camera

		@return cv2.VideoCapture
		"""
		# Init camera
		camera = cv2.VideoCapture(self.CAMERA_SOURCE)
		#camera.set(3, 320)
		#camera.set(4, 240)

		# Wait half a second for light adjustment
		time.sleep(0.5)

		return camera

	def start_recording(self):
		"""
		Setup the recorder
		"""

		self.current_file = self.archive + "/" + self.detected_at + "-pic.avi"

		Util.log(self.name, "Motion detected! Recording...")

		# Set path and FPS
		self.writer = cv2.VideoWriter(self.current_file, self.codec, self.fps, (self.width, self.height))

	def stop_recording(self):
		"""
		Reset values to default
		"""
		self.writer = None
		self.current_file = None
		self.detected_at = None

	def convert_to_mp4(self, path):
		"""
		Convert video file to mp4 using ffmpeg

		@param string path
		"""
		try:
			Util.log(self.name, "Converting video...")
			destination = os.path.splitext(path)[0] + '.mp4'
			cmd = 'ffmpeg -i "{}" "{}" 2> /dev/null && rm "{}"'.format(path, destination, path)
			#cmd = 'for i in ' + self.archive + '/*.avi; do ffmpeg -i "$i" "${i%.*}.mp4" 2> /dev/null && rm "$i"; done'
			p = subprocess.Popen(cmd, shell=True)
			(output, err) = p.communicate()

		except subprocess.CalledProcessError:
			Util.log(self.name, "Error converting video")
		
		return destination

	def run(self):
		"""
		Main worker
		"""
		observer = deque(maxlen=self.fps * self.OBSERVER_LENGTH)
		previous_frame = None

		while True:
            
			# Grab a frame
			(grabbed, self.current_frame) = self.source.read()

			# End of feed
			if not grabbed:
				break
			
			# Gray frame
			frame_gray = cv2.cvtColor(self.current_frame, cv2.COLOR_BGR2GRAY)

			# Blur frame
			frame_blur = cv2.GaussianBlur(frame_gray, (21, 21), 0)

			# If there's no previous frame, us the current one
			if previous_frame is None:
				previous_frame = frame_blur
				continue

			# Delta frame
			delta_frame = cv2.absdiff(previous_frame, frame_blur)

			# Threshold frame
			threshold_frame = cv2.threshold(delta_frame, 15, 255, cv2.THRESH_BINARY)[1]

			# Dilate the thresholded image to fill in holes
			kernel = np.ones((5, 5), np.uint8)
			dilated_frame = cv2.dilate(threshold_frame, kernel, iterations=4)

			# Find difference in percent
			res = dilated_frame.astype(np.uint8)
			movement = (np.count_nonzero(res) * 100) / res.size

			# Add movement percentage to observer
			observer.append(movement)

			# Add contours, add_target
			if self.do_add_contours or self.do_add_target:
				self.current_frame, targets = self.add_contours(self.current_frame, dilated_frame)

				if self.do_add_target:
					self.current_frame = self.add_target(self.current_frame, targets)
				if targets:
					tx = 0
					ty = 0
					for x, y, a in targets:
						tx += x
						ty += y
					tx = int(round(tx / len(targets), 0))
					ty = int(round(ty / len(targets), 0))
					#print(">>>>> " + str(mx) + " , " + str(my))
					# if 영역 안으로 들어 온 경우 
					x1 = int(self.width*X1_RATE)
					x2 = int(self.width*X2_RATE)
					y1 = int(self.height*Y1_RATE)
					y2 = int(self.height*Y2_RATE)
					if ( x1 < tx < x2 ) and ( y1 < ty < y2 ):
						self.do_add_target = True
						gpiofiring.booby_trap_firing()
						#cv2.imwrite(self.archive+'/ontarget_' + str(tx) + '_' + str(ty) + '_object.jpg', self.current_frame)
						#send_slack_img(tx, ty)
						time.sleep(1)
						gpiofiring.booby_trap_stoping()
					else:
						self.do_add_target = False


			if self.do_record and self.detected(sum([x > self.threshold for x in observer]) > 0):
				if not self.recording():
					self.start_recording()

				self.writer.write(self.current_frame)
			elif self.recording():
				# Delete Old files
				self.delete()
				
				# Convert
				destination = self.convert_to_mp4(self.current_file)
				
				# Reset all
				self.stop_recording()
				gpiofiring.booby_trap_stoping()
				
				# Send Slack message 
				send_slack_mp4(destination)

				Util.log(self.name, "Observing...")

			# Set blurred frame as new previous frame
			previous_frame = frame_blur
			
			# Booby trap area setting 
			TL_outside = (int(self.width*X1_RATE),int(self.height*Y1_RATE))
			BR_outside = (int(self.width*X2_RATE),int(self.height*Y2_RATE))			
			self.current_frame = cv2.rectangle(self.current_frame,TL_outside,BR_outside,(0,0,255),1)
					
			# Booby trap stop
			gpiofiring.booby_trap_stoping()
			
			# Display
			if self.do_display:
				cv2.imshow("Current frame:", self.current_frame)

			# Exit on 'q'
			key = cv2.waitKey(1) & 0xFF

			if key == ord('q'):
				break

	def delete(self):
		"""
		delete mic data to a mp4 file.
		@param list data
		"""
		count = 0
		Util.log(self.name, "Delete PI Cam video...")
		
		#file_list = sorted(os.listdir(self.archive), reverse=True)
		for filename in sorted(os.listdir(self.archive), reverse=True):
		#for filename in [file for file in file_list if file.endswith("pic.mp4")]:
			if not filename.startswith('.'):
				type = self.get_type(filename)
				if type == "video":
					count = count + 1
					if self.REMAIN_RECORDING_FILES < count:
						Util.log(self.name, "Delete PIC video filename=" + filename + ", type=" + type + ", count=" + str(count))
						os.remove(self.archive + "/" + filename)

	def get_type(self, filename):
		name, extension = os.path.splitext(filename)
		return 'video' if extension == '.mp4' else 'video' if extension == '.avi' else 'audio' if extension == '.wav' else 'audio' if extension == '.mp3' else 'photo'

	def add_contours(self, raw_frame, dilated_frame):
		"""
		Add contours to frame

		@param array raw_frame
		@param array dilated_frame
		@return tuple(array, list)
		"""
		# Find contours on thresholded image
		contours, nada = cv2.findContours(dilated_frame.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

		# Make coutour frame
		contour_frame = raw_frame.copy()

		# Target contours
		targets = []

		# Loop over the contour
		for c in contours:
			# If the contour is too small, ignore it
			if cv2.contourArea(c) < 500:
				# Make sure this has a less than sign, not an html escape
				continue

			# Contour data
			M = cv2.moments(c)
			cx = int(M['m10']/M['m00'])
			cy = int(M['m01']/M['m00'])
			x, y, w, h = cv2.boundingRect(c)
			rx = x + int(w / 2)
			ry = y + int(h / 2)
			ca = cv2.contourArea(c)

			# plot contours
			# 윤곽 그리기 cv2.drawContours(contour_frame,[c],0,(0,0,255),2)
			# 네모 때리기 cv2.rectangle(contour_frame,(x,y),(x+w,y+h),(0,255,0),2)
			cv2.circle(contour_frame,(cx,cy),2,(0,0,255),2)
			cv2.circle(contour_frame,(rx,ry),2,(0,255,0),2)

			# save target contours
			targets.append((rx,ry,ca))

		return contour_frame, targets

	def add_target(self, raw_frame, targets):
		"""
		Add crosshairs to frame

		@param array raw_frame
		@param list targets
		@return array
		"""
		# Make target
		area = sum([x[2] for x in targets])
		mx = 0
		my = 0

		if targets:
			for x, y, a in targets:
				mx += x
				my += y
			mx = int(round(mx / len(targets), 0))
			my = int(round(my / len(targets), 0))

		# Plot target
		tr = 50
		target_frame = raw_frame.copy()

		if targets:
			cv2.circle(target_frame, (mx, my), tr, (0, 0, 255, 0), 2)
			cv2.line(target_frame, (mx - tr, my), (mx + tr, my), (0, 0, 255, 0), 2)
			cv2.line(target_frame, (mx, my - tr), (mx, my + tr), (0, 0, 255, 0), 2)

		return target_frame

	def detected(self, has_motion):
		"""
		Check if this or another detector detected something

		@param boolean has_motion
		@return boolean
		"""
		if has_motion:
			self.lock_manager.set()
		else:
			self.lock_manager.remove()

		self.detected_at = self.lock_manager.get_lock_time()

		return self.detected_at is not None

	def recording(self):
		"""
		Check if currently recording

		@return boolean
		"""
		return self.writer is not None