Ejemplo n.º 1
0
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)
Ejemplo n.º 2
0
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])
Ejemplo n.º 3
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
        )
Ejemplo n.º 4
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)
Ejemplo n.º 5
0
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
        )
Ejemplo n.º 6
0
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)
Ejemplo n.º 7
0
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
        )
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
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
        )
Ejemplo n.º 11
0
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
Ejemplo n.º 12
0
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
Ejemplo n.º 13
0
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)
Ejemplo n.º 14
0
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
Ejemplo n.º 15
0
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