Esempio n. 1
0
class AppEngine(QObject):
	"""
	AppEngine class - derived from QObject

	INPUTS:  None
	OUTPUTS: None

	Notes: Mediator object for the application, facilitates communication b/t other objects
	"""

	# signal to update the VideoWidget's current frame
	changeFrame = pyqtSignal(ndarray)

	def __init__(self):
		"""Initialize the AppEngine

		Input:  None
		Output: Creates an instance of the AppEngine.
		"""

		# init QObject
		super().__init__()

		# create an instance of the database
		self.database = Database('../data/face_data.db')

		# ensure appropriate table is created
		create_sql = "CREATE TABLE IF NOT EXISTS faces (FaceName TEXT PRIMARY KEY," \
					 "FaceEncodings BLOB NOT NULL)"
		self.database.createDatabase(create_sql)

		# create an instance of the VideoStreamer
		self.videoStreamer = VideoStreamer()
		# connect the VideoStreamer's newPiFrame signal to the processNewFrame method
		self.videoStreamer.newPiFrame.connect(self.processNewFrame)

		# create an instance of the FaceRecognizer
		self.faceRecognizer = FaceRecognizer()

		# bool which determines if facial recognition is performed on each incoming frame from the Pi
		self.performFacialRecognition = False

		# current frame sent from the Pi, used for adding a new face
		self.currentFrame = None

		# current face name and encodings, used for adding a new face
		self.currentAddFaceName = None
		self.currentAddFaceEncodings = list()

		# create an instance of the AlertObserver
		self.alertObserver = AlertObserver()

		# bool determines if an alert has already been sent (only one is sent per application run)
		self.alertSent = False

	def setCurrentFrame(self, frame):
		"""
		AppEngine - setCurrentFrame
		:param frame: numpy.ndarray - represents a cv2 image
		:return:

		NOTES: set the AppEngine's current frame member
		"""

		self.currentFrame = frame

	def setCurrentAddFaceName(self, name):
		"""
		AppEngine - setCurrentAddFaceName
		:param name: str - the name to set
		:return:

		NOTES: set the AppEngine's current add face name member
		"""

		self.currentAddFaceName = name

	def resetCurrentAddFaceData(self):
		"""
		AppEngine - resetCurrentAddFaceData
		:return:

		NOTES: resets the members holding info for a new face to be added.
		"""

		self.currentAddFaceName = None
		self.currentAddFaceEncodings = list()

	def setFacialRecognitionState(self, state_bool):
		"""
		AppEngine - setFacialRecognitionState
		:param state_bool: bool
		:return:

		NOTES: sets the AppEngine's facial recognition state (determines if facial recognition will be performed on
			   each frame.
		"""

		self.performFacialRecognition = state_bool

	def getAllFaceInfoFromDb(self):
		"""
		AppEngine - getAllFaceInfoFromDb
		:return: face_names - list<str>, face_encodings - list<array>

		NOTES: gets all face names and their corresponding encodings from the database and returns them.
		"""

		# empty lists to hold names and encodings
		face_names = list()
		face_encodings = list()

		# sql to get everything from the faces table
		sql = "SELECT * FROM faces"

		results = self.database.executeQuery(sql, variables=[])

		# iterate over results, collect data into the lists
		for row in results:

			# load pickled data - returns list of arrays
			# https://stackoverflow.com/questions/52890916/python3-unpickle-a-string-representation-of-bytes-object
			current_face_encodings = pickle.loads(eval(row['FaceEncodings']))

			# make sure length of names is the same as encodings - necessary for recognizing named faces
			face_names = face_names + ([row['FaceName']] * len(current_face_encodings))

			face_encodings = face_encodings + current_face_encodings

		# face encodings list needs to be flattened (b/c each sublist is only length 1)
		return face_names, [encoding[0] for encoding in face_encodings]

	def connectToPi(self, pi_address):
		"""
		AppEngine - connectToPi
		:param pi_address: str - the ip address (or hostname) of the RaspberryPi
		:return:

		NOTES: sets up the VideoStreamer to connect to the Pi, update FaceRecognizer data, and then starts the
			   VideoStreamer's thread.
		"""

		# set the address of the VideoStreamer
		self.videoStreamer.setPiAddress(pi_address)

		# update known face data
		self.updateFaceRecognizerData()

		# start VideoStreamer thread
		self.videoStreamer.start()

	def updateFaceRecognizerData(self):
		"""
		AppEngine - updateFaceRecognizerData
		:return:

		NOTES: gets all face info from the database then updates that data in the FaceRecognizer.
		"""

		# get face data from the database
		known_names, known_encodings = self.getAllFaceInfoFromDb()

		# set FaceRecognizer data using the new info
		self.faceRecognizer.updateKnownFaces(known_names, known_encodings)

	@pyqtSlot(ndarray)
	def processNewFrame(self, cv_img):
		"""
		AppEngine - processNewFrame
		:param cv_img: numpy.ndarray
		:return:

		NOTES: updates AppEngine's current frame, performs facial recognition if needed, alerts observers if unknown
			   face is found, sends signal to update the VideoWidget's frame.
		"""

		# update the current frame
		self.setCurrentFrame(cv_img)

		# check if facial recognition needs to be performed
		if self.performFacialRecognition:

			# get box-overlayed image and the names of detected faces in the image
			cv_img, names = self.faceRecognizer.findImageFaces(cv_img)

			# check if an alert has already been sent, if not, check if an unknown face was found in the image
			if not self.alertSent:
				if "Unknown" in names:

					# try-except catches errors resulting from unregistered phone numbers
					try:
						# send SMS messages to each of the observers
						self.alertObserver.alertObservers()
					except Exception:
						pass

					self.alertSent = True

		# emit signal to change the VideoWidget's displayed frame (may be overlaid with face boxes or not)
		self.changeFrame.emit(cv_img)

	def recognizeAndRecordCurrentFrame(self):
		"""
		AppEngine - recognizeAndRecordCurrentFrame
		:return:

		NOTES: only called when user clicks 'Capture' in the MainWindow's CaptureFaceImagesDialog. This method
			   will get the face encoding from the immage and update the AppEngine's list of encodings.
		"""

		# get face encoding data from the current frame
		face_encoding = self.faceRecognizer.getImageFaceEncoding(self.currentFrame)

		# update list of encodings for the new face to be added
		self.currentAddFaceEncodings.append(face_encoding)

	def checkNameExistenceInDb(self, name):
		"""
		AppEngine - checkNameExistenceInDb
		:param name: str
		:return: bool - true if name already exists in the database

		NOTES: checks if the specified name is already present in the database.
		"""

		# sql to check if the name is in the database
		sql = "SELECT FaceName FROM faces WHERE FaceName = ?"

		# execute the query
		db_response = self.database.executeQuery(sql, variables=[name])

		# response will be an empty list if name does not exist in the database
		if db_response:
			return True
		else:
			return False

	def addObserver(self, observer_number):
		"""
		AppEngine - addObserver
		:param observer_number: str - phone number of the observer to add
		:return:

		NOTES: adds the new phone number to the AlertObserver's list of observers
		"""
		self.alertObserver.addObserver(observer_number)

	def addFaceToDb(self):
		"""
		AppEngine - addFaceToDb
		:return:

		NOTES: only called when the user clicks 'Done' in the MainWindow's CaptureFaceImagesDialog. This will add the
			   current face name and corresponding encodings to the database.
		"""

		# pickle the encodings (list of arrays) so that they can be saved into the database and reconstructed later
		encodings_bytes = pickle.dumps(self.currentAddFaceEncodings)

		# sql to insert face data
		sql = "INSERT INTO faces (FaceName, FaceEncodings) VALUES (?, ?)"

		self.database.executeNonQuery(sql, variables=[self.currentAddFaceName, encodings_bytes])

		# reset the AppEngine's data so that another named face can be added
		self.resetCurrentAddFaceData()

		# update the FaceRecognizer's data so that it can recognize the new face
		self.updateFaceRecognizerData()

	def deleteFaceFromDb(self, name):
		"""
		AppEngine - deleteFaceFromDb
		:param name: str
		:return:

		NOTES: deletes data from the database corresponding to the given name
		"""

		# sql to delete the specific face
		sql = "DELETE FROM faces WHERE FaceName = ?"

		self.database.executeNonQuery(sql, variables=[name])

		# update the FaceRecognizer's data since it just changed
		self.updateFaceRecognizerData()

	def deleteAllFacesFromDb(self):
		"""
		AppEngine - deleteAllFacesFromDb
		:return:

		NOTES: wipes the database and then recreates the faces table.
		"""

		# wipe the database (drops all tables)
		self.database.wipeDatabase()

		# sql to recreate the faces table since it was dropped
		create_sql = "CREATE TABLE IF NOT EXISTS faces (FaceName TEXT PRIMARY KEY," \
					 "FaceEncodings BLOB NOT NULL)"

		# recreate the faces table
		self.database.createDatabase(create_sql)

		# update the FaceRecognizer's data
		self.updateFaceRecognizerData()