def streamHandler(start, sleepTime=None): """streamHandler() : Starts or stops the live stream to AWS, sleeping after starting briefly to allow it to get situated. It will also check the error codes of the respective start and stop shell scripts to verify the stream actually started/stopped. :param start: Boolean denoting whether we are starting or stopping the stream :param sleepTime: Int for how long to sleep for after starting the stream """ if start: # Boot up live stream startStreamRet = subprocess.call( f"{os.getenv('ROOT_DIR')}/src/scripts/startStream.sh", close_fds=True) if startStreamRet != 0: return commons.respond( messageType="ERROR", message= "Stream failed to start (see content for exit code), see log for details", content={"ERROR": str(startStreamRet)}, code=5) else: # We have to sleep for a bit here as the steam takes time to boot, then return control to caller time.sleep(sleepTime) else: # Terminate streaming and reset signal handler everytime stopStreamRet = subprocess.call( f"{os.getenv('ROOT_DIR')}/src/scripts/stopStream.sh") if stopStreamRet != 0: return commons.respond( messageType="ERROR", message= "Stream failed to die (see CONTENT field is the exit code), see log for details", content={"ERROR": str(stopStreamRet)}, code=6)
def add_face_to_collection(imagePath, s3Name=None): """add_face_to_collection() : Retrieves an image and indexes it to a rekognition collection, ready for examination. :param imagePath: Path to file to be uploaded :param objectName: S3 object name and or path. If not specified then file_name is used :return: Face object details that were created """ # If an objectName was not specified, use the file name if s3Name is None: objectName = commons.parseObjectName(imagePath) else: objectName = commons.parseImageObject(s3Name) # Check if we're using a local file if os.path.isfile(imagePath): try: Image.open(imagePath) except IOError: return commons.respond( messageType="ERROR", message=f"File {imagePath} exists but is not an image. Only jpg and png files are valid", code=7 ) with open(imagePath, "rb") as fileBytes: print(f"[INFO] Indexing local image {imagePath} with collection object name {objectName}") response = client.index_faces( CollectionId=os.getenv('FACE_RECOG_COLLECTION'), Image={'Bytes': fileBytes.read()}, ExternalImageId=objectName, MaxFaces=1, QualityFilter="AUTO", DetectionAttributes=['ALL'] ) else: # Use an S3 object if no file was found at the image path given print(f"[WARNING] {imagePath} does not exist as a local file. Attempting to retrieve the image using the same path from S3 with object name {objectName}") try: response = client.index_faces( CollectionId=os.getenv('FACE_RECOG_COLLECTION'), Image={'S3Object': { 'Bucket': os.getenv('FACE_RECOG_BUCKET'), 'Name': imagePath }}, ExternalImageId=objectName, MaxFaces=1, QualityFilter="AUTO", DetectionAttributes=['ALL'] ) except client.exceptions.InvalidS3ObjectException: return commons.respond( messageType="ERROR", message=f"No such file found locally or in S3: {imagePath}", code=9 ) # We're only looking to return one face print(f"[SUCCESS] {imagePath} was successfully added to the collection with image id {objectName}") return json.dumps(response['FaceRecords'][0])
def main(argv): """main() : Main method that parses the input opts and returns the result""" # Parse input parameters argumentParser = argparse.ArgumentParser( description="Adds a face from S3 or local drive to a rekognition collection", formatter_class=argparse.RawTextHelpFormatter ) argumentParser.add_argument( "-a", "--action", required=True, choices=["add", "delete"], help="""Action to be conducted on the --file. Only one action can be performed at one time:\n\nadd: Adds the --file to the collection. --name can optionally be added if the name of the --file is not what it should be in S3.\n\ndelete: Deletes the --file inside the collection.\n\nNote: There is no edit/rename action as collections don't support image renaming or deletion. If you wish to rename an image, delete the original and create a new one. """ ) argumentParser.add_argument( "-f", "--file", required=True, help="Full path to a jpg or png image file (s3 or local) to add to collection OR (if deleting) the file or username of the face to delete" ) argumentParser.add_argument( "-n", "--name", required=False, help="ID that the image will have inside the collection. If not specified then the filename is used" ) argDict = argumentParser.parse_args() if argDict.action == "delete": response = remove_face_from_collection(argDict.file) if response is not None: return commons.respond( messageType="SUCCESS", message=f"{argDict.file} was successfully removed from the Rekognition Collection", content=response, code=0 ) else: commons.respond( messageType="ERROR", message=f"No face found in collection with object name {argDict.file}", code=2 ) else: response = add_face_to_collection(argDict.file, argDict.name) return commons.respond( messageType="SUCCESS", message=f"{argDict.file} was added to the Rekognition Collection!", content=response, code=0 )
def awaitProject(start): """awaitProject() : Halts execution while waiting for a project to start up or shutdown :param start: Boolean denoting whether we are starting or stopping the project """ if start: delay = 30 maxAttempts = 50 timeoutSeconds = delay * maxAttempts print(f"[INFO] {os.getenv('GESTURE_RECOG_PROJECT_NAME')} has been started. Waiting {timeoutSeconds}s for confirmation from AWS...") waitHandler = rekogClient.get_waiter('project_version_running') try: waitHandler.wait( ProjectArn=os.getenv("PROJECT_ARN"), VersionNames=[ os.getenv('LATEST_MODEL_VERSION') ], WaiterConfig={ "Delay": delay, "MaxAttempts": maxAttempts } ) except WaiterError: return commons.respond( messageType="ERROR", message=f"{os.getenv('GESTURE_RECOG_PROJECT_NAME')} FAILED to start properly before {timeoutSeconds}s timeout expired. Model is likely still booting up", code=15 ) else: # Stopping a model takes less time than starting one stopTimeout = 200 print(f"[INFO] Request to stop {os.getenv('GESTURE_RECOG_PROJECT_NAME')} model was successfully sent! Waiting {stopTimeout}s for the model to stop...") time.sleep(stopTimeout)
def getUserCombinationFile(username): """getUserCombinationFile() : Retrieves a user's gesture configuration file contents :param username: The user to retrieve the config file for """ try: return json.loads(s3Client.get_object( Bucket=os.getenv('FACE_RECOG_BUCKET'), Key=f"users/{username}/gestures/GestureConfig.json" )["Body"].read()) except s3Client.exceptions.NoSuchKey: return commons.respond( messageType="ERROR", message="No such user gesture config file exists in S3. Does this user exist?", code=9 ) except Exception as e: return commons.respond( messageType="ERROR", message="Failed to retrieve user gesture config file. Please check your internet connection.", content={"ERROR": str(e)}, code=2 )
def delete_file(fileName): """delete_file() : Deletes a S3 object file :param fileName: S3 Path to file to be deleted :return: S3 file object path that was successfully deleted """ try: print("[INFO] Deleting...") s3Client.delete_object(Bucket=os.getenv("FACE_RECOG_BUCKET"), Key=fileName) except EndpointConnectionError: return commons.respond( messageType="ERROR", message= "FAILED to delete object from S3. Could not establish a connection to AWS", code=3) # Verify the object was deleted try: deletionRequest = s3Client.get_object_acl( Bucket=os.getenv("FACE_RECOG_BUCKET"), Key=fileName) except s3Client.exceptions.NoSuchKey: print(f"[SUCCESS] {fileName} has been successfully deleted from S3!") return fileName except EndpointConnectionError: return commons.respond( messageType="ERROR", message= "FAILED to verify if object was successfully deleted from S3. Could not establish a connection to AWS", code=2) return commons.respond( messageType="ERROR", message= f"FAILED to verify if {fileName} object was successfully deleted from S3", content=deletionRequest, code=4)
def getProjectVersions(): """getProjectVersions() : Retrieves all versions of the custom labels model. Often, we will only use the first/latest version as that is generally the most accurate and up-to-date :return: List of project version in chronological order (latest to oldest) """ try: return rekogClient.describe_project_versions( ProjectArn=os.getenv("PROJECT_ARN"), VersionNames=[ os.getenv("LATEST_MODEL_VERSION"), ] )["ProjectVersionDescriptions"] except Exception as e: return commons.respond( messageType="ERROR", message="Failed to retrieve project version descriptions. Please check your internet connection and .env file.", content={"ERROR": str(e)}, code=2 )
def remove_face_from_collection(imageId): """remove_face_from_collection() : Removes a face from the rekognition collection. :param imageId: External image id to be deleted (e.g. morgan.jpg) :return: Face details object that was deleted from the collection """ foundFace = faceInCollection(imageId) if foundFace == {}: # If no face was found, check to see if there is an alternative jpg or png file of the same name if "jpg" in imageId: altImageId = imageId.replace(".jpg", ".png") else: altImageId = imageId.replace(".png", ".jpg") print(f"[WARNING] No face found with {imageId} image id. Trying to find a {altImageId} id to delete...") foundFace = faceInCollection(altImageId) # Still no face was found. Item does not likely exist so we return and leave error handling to caller if foundFace == {}: return None # Delete Object deletedResponse = client.delete_faces( CollectionId=os.getenv('FACE_RECOG_COLLECTION'), FaceIds=[foundFace['FaceId']] ) # Verify face was deleted if deletedResponse["DeletedFaces"][0] != foundFace['FaceId']: return commons.respond( messageType="ERROR", message=f"Failed to delete face with id {foundFace['FaceId']}. Face ID {deletedResponse['DeletedFaces'][0]} was deleted instead.", code=4 ) print(f"[SUCCESS] {imageId} was successfully removed from the collection!") return foundFace
def checkForGestures(image): """checkForGestures() : Queries the latest AWS Custom Label model for the gesture metadata. I.e. Does this image contain a gesture and if so, which one is it most likely? :param image: Locally stored image OR image bytes OR stream frame to scan for authentication gestures :return: JSON object containing the gesture with the highest confidence OR None if no recognised gesture was found """ minConfidence = 50 arn = os.getenv("LATEST_MODEL_ARN") # The param given is a local image file if os.path.isfile(image): with open(image, "rb") as fileBytes: # This may throw a ImageTooLargeException as the max allowed by AWS in byte format is 4mb (we let the caller deal with that) try: detectedLabels = rekogClient.detect_custom_labels( Image={ 'Bytes': fileBytes.read(), }, MinConfidence=minConfidence, ProjectVersionArn=arn )['CustomLabels'] except ClientError as e: # On rare occassions, image is too big for AWS and will fail to process client side rather than server side return commons.respond( messageType="ERROR", message=f"An error occured while processing {image} prior to uploading. Image may be too large for AWS to handle, try cropping or compressing the problamatic image.", content={"ERROR": str(e)}, code=25 ) else: # The param given is a file path to an image in s3 print("[WARNING] Given parameter is not image bytes or a local image, likelihood is we are dealing with an s3 object path...") try: detectedLabels = rekogClient.detect_custom_labels( Image={ 'S3Object': { 'Bucket': os.getenv('FACE_RECOG_BUCKET'), 'Name': image, } }, MinConfidence=minConfidence, ProjectVersionArn=arn )['CustomLabels'] except Exception as e: # The param given is none of the above return commons.respond( messageType="ERROR", message=f"{image} is an invalid object to analyse for custom labels or another exception occurred", content={"ERROR": str(e)}, code=7 ) # Extract gesture with highest confidence (or None if no gesture found) try: foundGesture = max(detectedLabels, key=lambda ev: ev["Confidence"]) if foundGesture is not None: return foundGesture else: # Leave no gesture handling to caller (same applies to a value error which equates to the same thing) return None except ValueError: return None
def main(argv): """main() : Main method that parses the input opts and returns the result""" # Parse input parameters argumentParser = argparse.ArgumentParser( description="Runs gesture recognition of an image or video frame against an image and has the option of exapnding it to check if a specific user possesses said gesture", formatter_class=argparse.RawTextHelpFormatter ) argumentParser.add_argument( "-a", "--action", required=True, choices=["gesture", "start", "stop"], help="""Only one action can be performed at one time:\n\ngesture: Runs gesture recognition analysis against a set of images (paths seperated by spaces).\n\nstart: Starts the rekognition project\n\nstop: Stops the rekognition project. """ ) argumentParser.add_argument( "-f", "--files", required=False, action="extend", nargs="+", help="List of full paths (seperated by spaces) to a gesture combination (in order) that you would like to analyse." ) argumentParser.add_argument( "-m", "--maintain", action="store_true", required=False, help="If this parameter is set, the gesture recognition project will not be closed after rekognition is complete (only applicable with -a gesture" ) argDict = argumentParser.parse_args() if argDict.action == "gesture": # Start Rekog project projectHandler(True) # Iterate through given images foundGestures = [] try: for imagePath in argDict.files: # We will always be using a local file (or it's file bytes) so no need to check if in s3 or not here if os.path.isfile(imagePath): try: Image.open(imagePath) except IOError: return commons.respond( messageType="ERROR", message=f"File {imagePath} exists but is not an image. Only jpg and png files are valid", code=7 ) foundGesture = checkForGestures(imagePath) # We have found a gesture if foundGesture is not None: foundGestures.append({f"{imagePath}": foundGesture}) else: print(f"[WARNING] No gesture was found within {imagePath} (Available gestures = {' '.join(getGestureTypes())})") foundGestures.append({f"{imagePath}": None}) else: return commons.respond( messageType="ERROR", message=f"No such file {imagePath}", code=8 ) finally: if argDict.maintain is False: projectHandler(False) # Display results if foundGestures == []: return commons.respond( messageType="ERROR", message="No gestures were found within the images", code=17 ) else: return commons.respond( messageType="SUCCESS", message="Found gestures!", content={"GESTURES": foundGestures}, code=0 ) elif argDict.action == "start": projectHandler(True) return commons.respond( messageType="SUCCESS", message="Latest project model is now running!", code=0 ) elif argDict.action == "stop": projectHandler(False) return commons.respond( messageType="SUCCESS", message="Latest project model is now stopped!", code=0 ) else: return commons.respond( messageType="ERROR", message=f"Invalid action type - {argDict.action}", code=13 )
def projectHandler(start): """projectHandler() : Starts or stops the custom labels project in AWS. It will wait for the project to boot up after starting and will verify the project actually stopped after stopping. :param start: Boolean denoting whether we are starting or stopping the project :return: Error code and execution exit if request failed. True otherwwise. """ # Only bother retrieving the newest version versionDetails = getProjectVersions()[0] if start: # Verify that the latest rekognition model is running print(f"[INFO] Checking if {os.getenv('GESTURE_RECOG_PROJECT_NAME')} has already been started...") if versionDetails["Status"] == "STOPPED" or versionDetails["Status"] == "TRAINING_COMPLETED": print(f"[INFO] {os.getenv('GESTURE_RECOG_PROJECT_NAME')} is not running. Starting latest model for this project (created at {versionDetails['CreationTimestamp']}) now...") # Start it and wait to be in a usable state try: rekogClient.start_project_version( ProjectVersionArn=os.getenv("LATEST_MODEL_ARN"), MinInferenceUnits=1 # Stick to one unit to save money ) except rekogClient.exceptions.ResourceInUseException: return commons.respond( messageType="ERROR", message=f"Failed to start {os.getenv('GESTURE_RECOG_PROJECT_NAME')}. System is in use (e.g. starting or stopping).", code=14 ) except Exception as e: return commons.respond( messageType="ERROR", message=f"Failed to start {os.getenv('GESTURE_RECOG_PROJECT_NAME')}.", content={"ERROR": str(e)}, code=14 ) awaitProject(start) print(f"[SUCCESS] Model {versionDetails['CreationTimestamp']} is running!") return True elif versionDetails["Status"] == "STOPPING": return commons.respond( messageType="ERROR", message=f"{os.getenv('GESTURE_RECOG_PROJECT_NAME')} is stopping. Please check again later when the process is not busy...", code=23 ) elif versionDetails["Status"] == "STARTING": awaitProject(start) print(f"[SUCCESS] Model {versionDetails['CreationTimestamp']} is running!") return True else: # Model is already running print(f"[SUCCESS] The latest model (created at {versionDetails['CreationTimestamp']} is already running!") return True # Stop the model after recog is complete else: if (versionDetails["Status"] == "RUNNING"): print(f"[INFO] Stopping latest {os.getenv('GESTURE_RECOG_PROJECT_NAME')} model...") try: rekogClient.stop_project_version( ProjectVersionArn=os.getenv("LATEST_MODEL_ARN") ) except Exception as e: return commons.respond( messageType="ERROR", message=f"Failed to stop the latest model of {os.getenv('GESTURE_RECOG_PROJECT_NAME')}", content={"ERROR": str(e)}, code=15 ) # Verify model was actually stopped stoppingVersion = getProjectVersions()[0] if stoppingVersion["Status"] != "RUNNING": awaitProject(start) stoppedVersion = getProjectVersions()[0] if stoppedVersion["Status"] == "STOPPED": print(f"[SUCCESS] {os.getenv('GESTURE_RECOG_PROJECT_NAME')} model was successfully stopped!") return True else: return commons.respond( messageType="ERROR", message=f"{os.getenv('GESTURE_RECOG_PROJECT_NAME')} FAILED to stop properly before timeout expired", content={"STATUS": stoppedVersion["Status"]}, code=15 ) else: return commons.respond( messageType="ERROR", message=f"{os.getenv('GESTURE_RECOG_PROJECT_NAME')} stop request was successfull but the latest model is still running.", content={"MODEL": stoppingVersion['CreationTimestamp'], "STATUS": stoppingVersion['Status']}, code=1 ) elif versionDetails["Status"] == "STARTING": # Wait for the project to finish starting, then try and stop it again awaitProject(True) projectHandler(False) elif versionDetails["Status"] == "STOPPING": return commons.respond( messageType="ERROR", message=f"{os.getenv('GESTURE_RECOG_PROJECT_NAME')} is already stopping", code=23 ) else: print(f"[WARNING] {os.getenv('GESTURE_RECOG_PROJECT_NAME')} model has already stopped!") return True
def upload_file(fileName, username, locktype=None, s3Name=None): """upload_file() : Uploads a file to an S3 bucket based off the input params entered. :param fileName: Path to file to be uploaded :param username: User to upload the new face details to :param s3Name: S3 object name and or path. If not specified then the filename is used :return: S3 object path to the uploaded object """ # This is appended to an error messsage in case the user is creating an account and something goes wrong errorSuffix = "WARNING: If you are executing this via manager.py -a create your profile has been partially created on s3. To ensure you do not suffer hard to debug problems, please ensure you delete your profile with -a delete before trying -a create again" # Verify file exists if os.path.isfile(fileName): try: Image.open(fileName) except IOError: return commons.respond( messageType="ERROR", message= f"File {fileName} exists but is not an image. Only jpg and png files are valid. {errorSuffix}", code=7) else: # Throw an error here instead of a response as sometimes the file will be a label for gesture recognition raise FileNotFoundError # If S3 name was not specified, use fileName if s3Name is None: objectName = commons.parseObjectName(fileName) else: objectName = commons.parseImageObject(s3Name) # For gestures include the locktype folder path if locktype is not None: objectName = f"users/{username}/gestures/{locktype}/{objectName}" else: objectName = f"users/{username}/{objectName}" # Upload the file try: print(f"[INFO] Uploading {fileName}...") # Sometimes this will time out on a first file upload with open(fileName, "rb") as fileBytes: s3Client.upload_fileobj(Fileobj=fileBytes, Bucket=os.getenv("FACE_RECOG_BUCKET"), Key=objectName, Config=TransferConfig( multipart_threshold=16777216, max_concurrency=20, num_download_attempts=10)) except ClientError as e: return commons.respond( messageType="ERROR", message=f"{fileName} FAILED to upload to S3. {errorSuffix}", content={"ERROR": str(e)}, code=3) except EndpointConnectionError as e: return commons.respond( messageType="ERROR", message= f"{fileName} FAILED to upload to S3. Could not establish a connection to AWS. {errorSuffix}", content={"ERROR": str(e)}, code=3) return objectName
def main(parsedArgs=None): """main() : Main method that parses the input opts and returns the result""" global TIMEOUT_SECONDS # Delete old response file if it exists if os.path.isfile(os.getenv("RESPONSE_FILE_PATH")): try: os.remove(os.getenv("RESPONSE_FILE_PATH")) except Exception as e: print(f"[WARNING] Failed to delete old response json file\n{e}") # Parse input parameters if parsedArgs is None: # Parse with sys args if running by command line argDict = parseArgs(sys.argv[1:]) else: # Assume the args have already been parsed argDict = parsedArgs # Create a new user profile in the rekognition collection and s3 if argDict.action == "create": # Verify we have a face to create if argDict.face is None: return commons.respond( messageType="ERROR", message= "-f was not given. Please provide a face to be used in recognition for your account.", code=13) else: if os.path.isfile(argDict.face): try: Image.open(argDict.face) except IOError: return commons.respond( messageType="ERROR", message= f"File {argDict.face} exists but is not an image. Only jpg and png files are valid", code=7) else: return commons.respond( messageType="ERROR", message=f"Could not find file {argDict.face}", code=8) # Verify we have a unlock gesture combination if argDict.unlock is None: return commons.respond( messageType="ERROR", message= "-l or -u was not given. Please provide a unlocking (-u) gesture combination so your user account can be created.", code=13) # Verify we have a username to upload the object to if argDict.profile is None: return commons.respond( messageType="ERROR", message= "-p was not given. Please provide a profile username for your account.", code=13) # uploadedImage will the objectName so no need to check if there is a user in this function if (argDict.name is not None): index_photo.add_face_to_collection(argDict.face, argDict.name) else: index_photo.add_face_to_collection(argDict.face, argDict.profile) # First, start the rekog project so we can actually analyse the given images gesture_recog.projectHandler(True) try: # Now iterate over lock and unlock image files, processing one a time while constructing our gestures.json lockGestureConfig = {} if argDict.lock is not None: lockGestureConfig = constructGestureFramework( argDict.lock, argDict.profile, "lock") unlockGestureConfig = constructGestureFramework( argDict.unlock, argDict.profile, "unlock", lockGestureConfig) else: unlockGestureConfig = constructGestureFramework( argDict.unlock, argDict.profile, "unlock") gestureConfig = { "lock": lockGestureConfig, "unlock": unlockGestureConfig } # Finally, upload all the files featured in these processes (including the gesture config file) print( "[INFO] All tests passed and profiles constructed. Uploading all files to database..." ) # Upload face try: if argDict.name is not None: upload_file(argDict.face, argDict.profile, None, argDict.name) else: upload_file(argDict.face, argDict.profile, None, f"{argDict.profile}.jpg") except FileNotFoundError: return commons.respond(messageType="ERROR", message=f"No such file {argDict.face}", code=8) # Upload gestures, adjusting the path of the config file to be s3 relative for locktype in gestureConfig.keys(): if gestureConfig[locktype] != {}: for position, details in gestureConfig[locktype].items(): try: gestureObjectPath = upload_file( details["path"], argDict.profile, locktype, f"{locktype.capitalize()}Gesture{position}") except FileNotFoundError: return commons.respond( messageType="ERROR", message= f"Could no longer find file {details['path']}", code=8) gestureConfig[locktype][position][ "path"] = gestureObjectPath try: gestureConfigStr = json.dumps(gestureConfig, indent=2).encode("utf-8") s3Client.put_object( Body=gestureConfigStr, Bucket=os.getenv("FACE_RECOG_BUCKET"), Key=f"users/{argDict.profile}/gestures/GestureConfig.json") except Exception as e: return commons.respond( messageType="ERROR", message= "Failed to upload the gesture configuration file. Gesture and face images have already been uploaded. Recommend you delete your user account with -a delete and try remaking it.", content={"ERROR": str(e)}, code=3) print("[SUCCESS] Config file uploaded!") return commons.respond( messageType="SUCCESS", message= "Facial recognition and gesture recognition images and configs files have been successfully uploaded!", code=0) finally: # Finally, close down the rekog project if specified if argDict.maintain is False: gesture_recog.projectHandler(False) elif argDict.action == "edit": # Verify we have a user to edit if argDict.profile is None: return commons.respond( messageType="ERROR", message= "-p was not given. Please provide a profile username for your account.", code=13) else: try: s3Client.get_object_acl( Bucket=os.getenv("FACE_RECOG_BUCKET"), Key=f"users/{argDict.profile}/{argDict.profile}.jpg") except s3Client.exceptions.NoSuchKey: return commons.respond( messageType="ERROR", message= f"User {argDict.profile} does not exist or failed to find face file", code=9) if argDict.face is None and argDict.lock is None and argDict.unlock is None: # Verify at least one editable feature was given return commons.respond( messageType="ERROR", message= "Neither -f, -u or -l was given. Please provide a profile feature to edit.", code=13) if argDict.face is not None: # Check that face file exists now as it will try to delete from collection without verify otherwise if os.path.isfile(argDict.face): try: Image.open(argDict.face) except IOError: return commons.respond( messageType="ERROR", message= f"File {argDict.face} exists but is not an image. Only jpg and png files are valid", code=7) else: return commons.respond( messageType="ERROR", message=f"Could not find file {argDict.face}", code=8) # Delete old user image from collection print( f"[INFO] Removing old face from {os.getenv('FACE_RECOG_COLLECTION')} for user {argDict.profile}" ) deletedFace = index_photo.remove_face_from_collection( f"{argDict.profile}.jpg") if deletedFace is None: # This can sometimes happen if deletion was attempted before but was not completed print( f"[WARNING] No face found in {os.getenv('FACE_RECOG_COLLECTION')} for user {argDict.profile}. We will assume it has already been removed." ) index_photo.add_face_to_collection(argDict.face, argDict.name) # Replace user face in S3 try: upload_file(argDict.face, argDict.profile, None, f"{argDict.profile}.jpg") except FileNotFoundError: return commons.respond(messageType="ERROR", message=f"No such file {argDict.face}", code=8) print( f"[SUCCESS] {argDict.face} has successfully replaced user {argDict.profile} face!" ) # Encase within two conditionals to avoid pointless running of gesture project if argDict.lock is not None or argDict.unlock is not None: try: # Start gesture project to allow for gesture recognition gesture_recog.projectHandler(True) if argDict.lock is not None and argDict.unlock is not None: # We are editing both combinations so run rules and construction sequentially adjustedLock = adjustConfigFramework( argDict.lock, argDict.profile, "lock") print( f"[SUCCESS] Lock gesture combination has been successfully replaced for user {argDict.profile}" ) adjustConfigFramework(argDict.unlock, argDict.profile, "unlock", adjustedLock["lock"]) print( f"[SUCCESS] Unlock gesture combination has been successfully replaced for user {argDict.profile}" ) else: # Get user config to compare edited rules against currentConfig = gesture_recog.getUserCombinationFile( argDict.profile) if argDict.lock is not None: adjustConfigFramework(argDict.lock, argDict.profile, "lock", currentConfig["unlock"]) print( f"[SUCCESS] Lock gesture combination has been successfully replaced for user {argDict.profile}" ) else: adjustConfigFramework(argDict.unlock, argDict.profile, "unlock", currentConfig["lock"]) print( f"[SUCCESS] Unlock gesture combination has been successfully replaced for user {argDict.profile}" ) finally: if argDict.maintain is False: gesture_recog.projectHandler(False) return commons.respond(messageType="SUCCESS", message="All done!", code=0) # Ensure the user account to be edited or deleted exists. Then, delete the data from both the collection and S3 elif argDict.action == "delete": # Verify we have a username to delete if argDict.profile is None or "": return commons.respond( messageType="ERROR", message= "-p was not specified. Please pass in a user account name to delete.", code=13) # Remove relevant face from rekog collection print( f"[INFO] Removing face from {os.getenv('FACE_RECOG_COLLECTION')} for user {argDict.profile}" ) deletedFace = index_photo.remove_face_from_collection( f"{argDict.profile}.jpg") if deletedFace is None: # This can sometimes happen if deletion was attempted before but was not completed print( f"[WARNING] No face found in {os.getenv('FACE_RECOG_COLLECTION')} for user {argDict.profile}. We will assume it has already been removed." ) # Check the user's folder actually exists in s3 print(f"[INFO] Deleting user folder for {argDict.profile} from s3...") s3FilePath = f"users/{argDict.profile}/{argDict.profile}.jpg" try: s3Client.get_object_acl(Bucket=os.getenv('FACE_RECOG_BUCKET'), Key=s3FilePath) except s3Client.exceptions.NoSuchKey: return commons.respond(messageType="ERROR", message="No such user profile exists", code=9) # Delete user folder delete_file(s3FilePath) return commons.respond( messageType="SUCCESS", message= f"User profile for {argDict.profile} was successfully removed from S3 and respective face reference removed from the Rekognition Collection.", content=deletedFace, code=0) # Run face comparison on stream elif argDict.action == "compare": if argDict.face is not None: # Verify params if argDict.profile is None or "": return commons.respond( messageType="ERROR", message= "-p was not specified. Please pass in a user account name", code=13) if os.path.isfile(argDict.face): try: Image.open(argDict.face) except IOError: return commons.respond( messageType="ERROR", message= f"File {argDict.face} exists but is not an image. Only jpg and png files are valid", code=7) else: return commons.respond( messageType="ERROR", message=f"Could not find file {argDict.face}", code=8) # Run face comparison print( f"[INFO] Running facial comparison library to compare {argDict.face} against the stored face for {argDict.profile}" ) faceCompare = compare_faces.compareFaces(argDict.face, argDict.profile) if faceCompare["FaceMatches"] is not [] and len( faceCompare["FaceMatches"]) == 1: # Get source landmarks with open(argDict.face, "rb") as fileBytes: sourceFaceAttr = rekogClient.detect_faces( Image={"Bytes": fileBytes.read()}) # Check if face is a presentation attack by checking details are close enough sourceLandmarks = sourceFaceAttr["FaceDetails"][0]["Landmarks"] targetLandmarks = rekogClient.detect_faces( Image={ 'S3Object': { 'Bucket': os.getenv('FACE_RECOG_BUCKET'), 'Name': f"users/{argDict.profile}/{argDict.profile}.jpg" } })["FaceDetails"][0]["Landmarks"] if compare_faces.checkPresentationAttack( sourceLandmarks, targetLandmarks, argDict.profile) is False: return commons.respond( messageType="SUCCESS", message= f"Input face {argDict.face} matched successfully with stored user's {argDict.profile} face", code=0) else: return commons.respond( messageType="ERROR", message= f"Input face {argDict.face} does not match stored user's {argDict.profile} face, try adjusting your camera's viewpoint so it more closely matches your profile's stored face", code=10) else: return commons.respond( messageType="ERROR", message= f"Input face {argDict.face} does not match stored user's {argDict.profile} face", code=10) else: if argDict.timeout is not None: TIMEOUT_SECONDS = argDict.timeout if argDict.profile is None: print( f"[INFO] Running facial comparison library to check for any known faces in current stream (timing out after {TIMEOUT_SECONDS}s)..." ) else: print( f"[INFO] Running facial comparison library to check for {argDict.profile} stored face in current stream (timing out after {TIMEOUT_SECONDS}s)..." ) # Start/end stream streamHandler(True, 3) # Start comparing, timing out if no face is found within the limit try: signal.signal(signal.SIGALRM, timeoutHandler) signal.alarm(TIMEOUT_SECONDS) matchedFace = compare_faces.checkForFaces() # If a profile has been specified, check if the face belongs to that user if argDict.profile is not None: if argDict.profile not in matchedFace['Face'][ 'ExternalImageId']: return commons.respond( messageType="ERROR", message= f"Captured face in stream does not match the stored face for {argDict.profile}", content=matchedFace, code=27) # By this point, we have found a face so cancel the timeout and return the matched face signal.alarm(0) return commons.respond(messageType="SUCCESS", message="Found a matching face!", content=matchedFace, code=0) except TimeoutError: signal.signal(signal.SIGALRM, signal.SIG_DFL) return commons.respond( messageType="ERROR", message= f"TIMEOUT FIRED AFTER {TIMEOUT_SECONDS}s, NO FACES WERE FOUND IN THE STREAM!", code=10) finally: # Reset signal handler everytime signal.signal(signal.SIGALRM, signal.SIG_DFL) streamHandler(False) # Run gesture recognition against given images elif argDict.action == "gesture": if argDict.profile is None: return commons.respond( messageType="ERROR", message= "-p was not given. Please pass a user profile to conduct gesture recognition against", code=13) # We can't try to unlock AND lock the system at the same time if argDict.lock is not None and argDict.unlock is not None: return commons.respond( messageType="ERROR", message="Cannot lock (-l) and unlock (-u) at the same time.", code=13) # Likewise, we cannot try to find a gesture if we don't know which locktype to authenticate with elif argDict.lock is None and argDict.unlock is None: return commons.respond( messageType="ERROR", message="Neither Lock (-l) or Unlock (-u) indicator was given.", code=13) # Otherwise, figure out if we are locking or unlocking else: if argDict.lock is not None: locktype = "lock" imagePaths = argDict.lock else: locktype = "unlock" imagePaths = argDict.unlock try: # Start rekognition model gesture_recog.projectHandler(True) # Get user's combination length to identify when we have filled the combination userComboLength = int( max( gesture_recog.getUserCombinationFile( argDict.profile)[locktype])) # No lock file for this user if userComboLength == 0 and locktype == "lock": return commons.respond( messageType="SUCCESS", message= f"No lock combination for {argDict.profile}, skipping authentication", code=0) print( f"[INFO] Running gesture recognition library to check for the correct {locktype}ing gestures performed in the given images..." ) matchedGestures = 1 for path in imagePaths: # Verify file exists if os.path.isfile(path): try: Image.open(path) except IOError: return commons.respond( messageType="ERROR", message= f"File {path} exists but is not an image. Only jpg and png files are valid.", code=7) else: return commons.respond( messageType="ERROR", message=f"File does not exist at {path}", code=8) # Run gesture recog lib try: foundGesture = gesture_recog.checkForGestures(path) except rekogClient.exceptions.ImageTooLargeException: print( f"[WARNING] {path} is too large (>4MB) to check for gestures directly. Uploading to S3 first and then checking for gestures..." ) # The s3 filename is going to be temporarily added and then deleted s3FilePath = f"temp/image_{str(random.randrange(1,1000000))}.jpg" try: gestureObjectPath = upload_file( path, argDict.profile, None, s3FilePath) except FileNotFoundError: return commons.respond( messageType="ERROR", message=f"Could not find file at {path}", code=8) delete_file(s3FilePath) foundGesture = gesture_recog.checkForGestures( gestureObjectPath) if foundGesture is not None: print( f"[INFO] Checking if the {locktype} combination contains the same gesture at position {matchedGestures}..." ) try: hasGesture = gesture_recog.inUserCombination( foundGesture, argDict.profile, locktype, str(matchedGestures)) except RateLimitException: return commons.respond( messageType="ERROR", message= "Too many user requests in too short a time. Please try again later", code=26) # User has same gesture and in right position, don't dump log as malicious users could figure out which gestures are correct if hasGesture is True: matchedGestures += 1 continue else: return commons.respond( messageType="ERROR", message=f"No gesture was found in image {path}", code=17) if matchedGestures - 1 == userComboLength: return commons.respond( messageType="SUCCESS", message= f"Matched {locktype} gesture combination for user {argDict.profile}", code=0) else: # Include this check just in case something goes wrong with the timeout handler return commons.respond( messageType="ERROR", message="Incorrect gesture combination was given", code=18) finally: if argDict.maintain is False: gesture_recog.projectHandler(False) else: return commons.respond( messageType="ERROR", message=f"Invalid action type - {argDict.action}", code=13)
def constructGestureFramework(imagePaths, username, locktype, previousFramework=None): """constructGestureFramework() : Uploads gesture recognition images and config file to the user's S3 folder :param imagePaths: List of images paths or gesture types (in combination order) :param username: Username as per their s3 folder :param locktype: Either lock or unlock (usually), depicts which combination the images are a part of :param previousFramework: If a framework has already been created, we will run tests against both it and the soon to be created framework in tandem :returns: A completed gestures.json config """ position = 1 gestureConfig = {} for path in imagePaths: if os.path.isfile(path): # Verify local file is an actual image try: Image.open(path) except IOError: return commons.respond( messageType="ERROR", message= f"File {path} exists but is not an image. Only jpg and png files are valid", code=7) # Identify the gesture type print(f"[INFO] Identifying gesture type for {path}") try: gestureType = gesture_recog.checkForGestures(path) except rekogClient.exceptions.ImageTooLargeException: print( f"[WARNING] {path} is too large (>4MB) to check for gestures directly. Uploading to S3 first and then checking for gestures." ) try: gestureObjectPath = upload_file( path, username, locktype, f"{locktype.capitalize()}Gesture{position}") except FileNotFoundError: return commons.respond( messageType="ERROR", message=f"Could no longer find file {path}", code=8) gestureType = gesture_recog.checkForGestures(gestureObjectPath) if gestureType is not None: # Extract the actual gesture type here since error's will return None above gestureType = gestureType['Name'] print(f"[SUCCESS] Gesture type identified as {gestureType}") else: return commons.respond( messageType="ERROR", message="No recognised gesture was found within the image", code=17) else: gestureType = path print( f"[WARNING] No image to upload ({gestureType} is the assumed name of the gesture type). Verifying this is a supported gesture type..." ) # We don't need to do this for a file as it is scanned for valid gestures during analysis gestureTypes = gesture_recog.getGestureTypes() if gestureType not in gestureTypes: return commons.respond( messageType="ERROR", message= f"{gestureType} is not an existing file or valid gesture type. Valid gesture types = {gestureTypes}", code=17) else: print(f"[SUCCESS] Gesture type identified as {gestureType}") # We leave path empty for now as it's updated when we uploaded the files gestureConfig[str(position)] = {"gesture": gestureType, "path": path} position += 1 # Finally, verify we are not using bad "password" practices (e.g. all the same values) print("[INFO] Checking combination meets rule requirements...") userGestures = list( map(lambda position: gestureConfig[position]["gesture"], gestureConfig)) # Gesture combination length is too short if len(userGestures) < 4: return commons.respond( messageType="ERROR", message= f"{locktype.capitalize()}ing gesture combination is too short, the minimum length permitted for a gesture combination is 4", code=28) # All gestures are the same in one combination if len(set(userGestures)) == 1: return commons.respond( messageType="ERROR", message= f"All gestures for {locktype}ing combination are the same. Please specify at least one different gesture in your combination", code=20) # Conduct tests against the previous gesture combination that was created if previousFramework is not None: previousGestures = list( map(lambda position: previousFramework[position]["gesture"], previousFramework)) # Both combinations are the same if previousGestures == userGestures: return commons.respond( messageType="ERROR", message= "The two gesture combinations are identical. Please ensure the combinations are different and not reversed versions of each other", code=21) # One combination is the same as the other when reversed if list(previousGestures.copy())[::-1] == userGestures or list( userGestures.copy())[::-1] == previousGestures: return commons.respond( messageType="ERROR", message= "One gesture combination is the same as the other when reversed. Please ensure the combinations are different and not reversed versions of each other", code=22) print( f"[SUCCESS] {locktype.capitalize()}ing combination passed all rule restraints!" ) return gestureConfig
def adjustConfigFramework(imagePaths, username, locktype, previousFramework=None): """adjustConfigFramework() : Modifies a gesture configuration file according to the user's edit changes :param imagePaths: New gesture paths to add :param username: User's config file to update :param locktype: Lock or unlock combination to update :param previousFramework: If a lock gesture has been generated, use it to run the tests in the downstream construction :return: Updated config dictionary """ # Retrieve old configuration file try: oldFullConfig = json.loads( s3Client.get_object( Bucket=os.getenv("FACE_RECOG_BUCKET"), Key=f"users/{username}/gestures/GestureConfig.json") ["Body"].read()) except s3Client.exceptions.NoSuchKey: return commons.respond( messageType="ERROR", message= f"{username} is not an existing user or download of the user details failed", code=2) if locktype == "lock": # Adjust file to account for changes, ignoring if lock combination deletion requested if len(imagePaths) != 1 and imagePaths[0] != "DELETE": newGestureLockConfig = constructGestureFramework( imagePaths, username, locktype, previousFramework) newGestureConfig = { "lock": newGestureLockConfig, "unlock": oldFullConfig["unlock"] } else: newGestureConfig = {"lock": {}, "unlock": oldFullConfig["unlock"]} else: newGestureUnlockConfig = constructGestureFramework( imagePaths, username, locktype, previousFramework) newGestureConfig = { "lock": oldFullConfig["lock"], "unlock": newGestureUnlockConfig } # Upload new gestures, adjusting the path of the config file to be s3 relative for position, details in newGestureConfig[locktype].items(): try: gestureObjectPath = upload_file( details["path"], username, locktype, f"{locktype.capitalize()}Gesture{position}") except FileNotFoundError: return commons.respond( messageType="ERROR", message=f"Could no longer find file {details['path']}", code=8) newGestureConfig[locktype][position]["path"] = gestureObjectPath # Upload gesture configuration file try: s3Client.put_object( Body=json.dumps(newGestureConfig, indent=2).encode("utf-8"), Bucket=os.getenv("FACE_RECOG_BUCKET"), Key=f"users/{username}/gestures/GestureConfig.json") except Exception as e: return commons.respond( messageType="ERROR", message="Failed to upload updated gesture configuration file", content={"ERROR": str(e)}, code=3) return newGestureConfig