authenticate_user.py 9.19 KB
Newer Older
Kyle Anderson's avatar
Kyle Anderson committed
1
"""
2
Methods for authenticating a user.
Kyle Anderson's avatar
Kyle Anderson committed
3
"""
Kyle Anderson's avatar
Kyle Anderson committed
4

5
import time
6

7
import cv2
8 9
import face_recognition
import imutils
10

11
import common
Kyle Anderson's avatar
Kyle Anderson committed
12
import data_handler
13

14 15 16 17
# How long to wait before timing out and saying failed authentication.
TIMEOUT: float = 30.0
# Minimum number of frames in which a user must be recognized in order to be authenticated.
MIN_USER_RECOGNITION_COUNT = 10
18
DEFAULT_CAMERA: int = 0
Kyle Anderson's avatar
Kyle Anderson committed
19
DEFAULT_WIDTH: int = 275
20 21 22


def load_encodings(file_location: str):
Kyle Anderson's avatar
Kyle Anderson committed
23
    """Loads the encodings for faces from the given file location."""
Kyle Anderson's avatar
Kyle Anderson committed
24
    return data_handler.load_database(file_location)
Kyle Anderson's avatar
Kyle Anderson committed
25 26


27
def determine_identity(face_encoding, known_faces) -> str:
28 29 30 31 32 33 34 35 36
    """
    "Determines the most likely identity of a single face. Returns the user id.
    :param face_encoding: The encoding which needs identification.
    :param known_faces: The database of known faces to use for searching.
    :return: The string user_id of the recognized user.
    """
    recognized_users = {}
    for (user_id, user_encodings) in known_faces.items():
        matches = face_recognition.compare_faces(user_encodings, face_encoding)
37 38 39 40
        count = matches.count(True)
        if count > 0:
            # Count the number of occurrences of true.
            recognized_users[user_id] = count
41

42 43 44 45
    matched_user = ""
    if len(recognized_users) > 0:
        matched_user: str = max(recognized_users,
                                key=recognized_users.get)
46 47
    return matched_user

Kyle Anderson's avatar
Kyle Anderson committed
48

49
def check_recognized_users(recognized_user_counts):
Kyle Anderson's avatar
Kyle Anderson committed
50 51
    """Determines if there are recognized users in the dictionary,
    and if so returns the list of their IDs"""
52
    recognized_users = []
Kyle Anderson's avatar
Kyle Anderson committed
53
    for user_id, count in recognized_user_counts.items():
54 55 56
        if count >= MIN_USER_RECOGNITION_COUNT:
            recognized_users.append(user_id)
    return recognized_users
Kyle Anderson's avatar
Kyle Anderson committed
57

Kyle Anderson's avatar
Kyle Anderson committed
58

Kyle Anderson's avatar
Kyle Anderson committed
59
def draw_rectangles_and_user_ids(image_frame, conversion: float, box_user_id_map: dict):
Kyle Anderson's avatar
Kyle Anderson committed
60
    """Draws the rectangles and user_ids onto the video stream so anyone viewing the stream could see them."""
61 62
    if box_user_id_map and len(box_user_id_map) > 0:
        for ((top, right, bottom, left), user_id) in box_user_id_map.items():
63 64 65 66 67 68 69 70 71 72
            top = round(top * conversion)
            right = round(right * conversion)
            bottom = round(bottom * conversion)
            left = round(left * conversion)

            # Draw the rectangle onto the face we've identified
            cv2.rectangle(image_frame, (left, top), (right, bottom), (0, 255, 0), 2)
            # Find the top so we can put the text there.
            y = top - 15 if top - 15 > 15 else top + 15
            cv2.putText(image_frame, user_id, (left, y), cv2.FONT_HERSHEY_PLAIN, 0.75, (0, 255, 0), 2)
73
    common.display_frame(image_frame)
Kyle Anderson's avatar
Kyle Anderson committed
74 75


76 77 78 79 80 81 82 83 84 85 86 87
def run_face_recognition(frame, known_faces: dict, encoding_model: str = "hog", draw_rectangles: bool = False,
                         image_width: int = DEFAULT_WIDTH) -> list:
    """

    :param frame: The frame to use in recognition.
    :param known_faces: The known faces data.
    :param encoding_model: The type of encoding model to use. CNN is more reliable but much slower, HOG is faster.
    :param draw_rectangles: True to draw the rectangles and user ids around identified faces, false otherwise.
    :param image_width: The width to which images should be resized in order to speed up processing.
    Lower quality means faster but less reliable recognition.
    :return: The list of recognized user ids.
    """
88
    recognized_user_ids: list = []
89
    original_frame = frame
90 91

    # Convert input from BGR to RGB
92
    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
93
    # Resize image to speed up processing.
94
    rgb_image = imutils.resize(frame, width=image_width)
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
    r = frame.shape[1] / float(rgb_image.shape[1])

    # Detect the location of each face and determine the boxes in which they lie
    boxes = face_recognition.face_locations(
        rgb_image, model=encoding_model)
    # Compute the facial embeddings (the encoding) at
    # each of the locations found in the previous line.
    encodings = face_recognition.face_encodings(rgb_image, boxes)

    box_user_id_mapping = {}
    for (i, encoding) in enumerate(encodings):
        user_id: str = determine_identity(encoding, known_faces)
        if user_id:
            box_user_id_mapping[boxes[i]] = user_id
            recognized_user_ids.append(user_id)

    if draw_rectangles:
112
        draw_rectangles_and_user_ids(original_frame, r, box_user_id_mapping)
113 114 115 116

    return recognized_user_ids


117
def recognize_user(known_faces: dict, encoding_model: str = "hog", image_flip: int = None,
118
                   draw_rectangles: bool = False, camera: int = DEFAULT_CAMERA, image_width: int = DEFAULT_WIDTH):
Kyle Anderson's avatar
Kyle Anderson committed
119
    """Attempts to recognize a user.
Kyle Anderson's avatar
Kyle Anderson committed
120
    Returns the ID of the user if identified, or None if no users are identified."""
121 122
    recognized_users_count = {}
    recognized_user = None
123
    video_stream = common.start_video_stream(camera)
124 125 126

    # Determine the time at which we will time out. Equal to current time + timeout.
    timeout_time: float = time.time() + TIMEOUT
127
    while time.time() < timeout_time:
Kyle Anderson's avatar
Kyle Anderson committed
128 129 130 131 132
        # Read a image_frame from the video stream.
        ret, image_frame = video_stream.read()
        if image_flip is not None:
            image_frame = cv2.flip(image_frame, image_flip)

133
        recognized_user_ids = run_face_recognition(image_frame, known_faces, encoding_model=encoding_model,
134
                                                   draw_rectangles=draw_rectangles, image_width=image_width)
135

136 137 138 139
        for user_id in recognized_user_ids:
            if user_id not in recognized_users_count:
                recognized_users_count[user_id] = 0
            recognized_users_count[user_id] += 1
Kyle Anderson's avatar
Kyle Anderson committed
140

141 142 143 144
        # Now check if we have already positively identified a user enough times
        recognized_users = check_recognized_users(recognized_users_count)
        if len(recognized_users) > 0:
            break
145
        cv2.waitKey(20)  # Required or else video stream doesn't really render.
Kyle Anderson's avatar
Kyle Anderson committed
146

147 148 149 150 151
    if recognized_users_count:
        recognized_user = max(recognized_users_count,
                              key=recognized_users_count.get)
        if recognized_users_count[recognized_user] < MIN_USER_RECOGNITION_COUNT:
            recognized_user = None
152 153 154
    return recognized_user


Kyle Anderson's avatar
Kyle Anderson committed
155
def recognize_user_from_database(database_loc: str = common.DATABASE_LOC, encoding_model: str = "hog",
156 157
                                 image_flip: int = None, draw_rectangles: bool = False, camera: int = DEFAULT_CAMERA,
                                 image_width: int = DEFAULT_WIDTH):
Kyle Anderson's avatar
Kyle Anderson committed
158 159 160 161 162 163
    """
    Recognizes a user
    :param database_loc: The database containing the face encodings for users.
    :param encoding_model: The encoding model to be used for recognition.
    :param image_flip: The type of image flip to be applied to the image, if it will be upside-down or horizontally inverted.
    :param draw_rectangles: True to draw the rectangles to the screen, false otherwise.
164 165 166
    :param camera: Which camera to use
    :param image_width: The width to which images should be resized in order to speed up processing.
    Lower quality means faster but less reliable recognition.
Kyle Anderson's avatar
Kyle Anderson committed
167 168
    :return: The recognized user's id, or None if no user was recognized.
    """
Kyle Anderson's avatar
Kyle Anderson committed
169 170
    return recognize_user(data_handler.load_database(database_loc), encoding_model=encoding_model,
                          image_flip=image_flip,
171
                          draw_rectangles=draw_rectangles, camera=camera, image_width=image_width)
Kyle Anderson's avatar
Kyle Anderson committed
172 173


174 175
# If this program is the main program, authenticate the user.
if __name__ == "__main__":
Kyle Anderson's avatar
Kyle Anderson committed
176
    import argparse
177 178 179

    parser = argparse.ArgumentParser(description="Facial Identification Options")
    parser.add_argument("--encodings", "-e", type=str, help="File location of facial encodings.", required=False,
Kyle Anderson's avatar
Kyle Anderson committed
180
                        default=None)
181 182
    parser.add_argument("--model", "-m", type=str, help="Type of encoding method, either \"hog\" or \"cnn\". HOG is "
                                                        "faster, CNN is more accurate.", required=False,
Kyle Anderson's avatar
Kyle Anderson committed
183
                        default=None, choices=["cnn", "hog"])
184
    parser.add_argument("--flip", "-f", type=int,
Kyle Anderson's avatar
Kyle Anderson committed
185 186
                        help="Whether or not to flip the image vertically or horizontally. 0 to flip horizontally, "
                             "1 to flip vertically.",
187 188 189
                        required=False, default=None, choices=[0, 1])
    parser.add_argument("--show", "-s", action="store_true",
                        help="Include this argument to have the image shown to you.", default=False)
190 191 192 193
    parser.add_argument("--camera", "-c", type=int, required=False,
                        help="Which camera to be used during authentication.", default=DEFAULT_CAMERA)
    parser.add_argument("--width", "-w", type=int, required=False,
                        help=f"The image width to use, in pixels. Default is {DEFAULT_WIDTH} pixels.")
194
    args = parser.parse_args()
Kyle Anderson's avatar
Kyle Anderson committed
195 196 197

    args_dict = {}
    if args.encodings is not None:
198
        args_dict["database_loc"] = args.encodings
Kyle Anderson's avatar
Kyle Anderson committed
199 200 201 202
    if args.model is not None:
        args_dict["encoding_model"] = args.model

    user = recognize_user_from_database(**args_dict, image_flip=args.flip,
203
                                        draw_rectangles=args.show, camera=args.camera, image_width=args.width)
204 205
    if user:
        print(f"Recognized user {user}.")