From 2cf945feecb5a53dc2dd4080c18fee959d9280be Mon Sep 17 00:00:00 2001 From: slashtechno <77907286+slashtechno@users.noreply.github.com> Date: Sun, 22 Oct 2023 12:02:07 -0500 Subject: [PATCH] Check if face directory exists; switch to ArcFace Other fixes: * Improve error handling during face recognition * Change VS Code run configuration name * Update `deepface-test.ipynb` * Reset version to `0.1.0` (not pushed to PyPi yet) --- .vscode/launch.json | 3 ++- deepface-test.ipynb | 8 +++---- pyproject.toml | 2 +- wyzely_detect/__main__.py | 42 ++++++++++++++++++++++-------------- wyzely_detect/utils/utils.py | 35 +++++++++++++++++++++--------- 5 files changed, 58 insertions(+), 32 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index dafa255..409d752 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,8 @@ "version": "0.2.0", "configurations": [ { - "name": "Python: Module", + // "name": "Python: Module", + "name": "Debug Wyzely Detect", "type": "python", "request": "launch", "module": "wyzely_detect", diff --git a/deepface-test.ipynb b/deepface-test.ipynb index aba1306..da7eeb5 100644 --- a/deepface-test.ipynb +++ b/deepface-test.ipynb @@ -36,12 +36,12 @@ "# cv2.imwrite(str(uuid_path), frame)\n", "# dfs = DeepFace.find(img_path=str(uuid_path), db_path = \"faces\")\n", "# Don't throw an error if no face is detected (enforce_detection=False)\n", - "dfs = DeepFace.find(frame, db_path = \"faces\", enforce_detection=False)\n", + "dfs = DeepFace.find(frame, db_path = \"faces\", enforce_detection=False, silent=False, model_name=\"ArcFace\", detector_backend=\"opencv\")\n", "# Get the identity of the person\n", "for i, pd_dataframe in enumerate(dfs):\n", " # Sort the dataframe by confidence\n", " # inplace=True means that the dataframe is modified so we don't need to assign it to a new variable\n", - " pd_dataframe.sort_values(by=['VGG-Face_cosine'], inplace=True, ascending=False)\n", + " # pd_dataframe.sort_values(by=['model_name=\"ArcFace\", detector_backend=\"opencv\")'], inplace=True, ascending=False)\n", " print(f'On dataframe {i}')\n", " print(pd_dataframe)\n", " # Get the most likely identity\n", @@ -49,7 +49,7 @@ " # We could use Path to get the parent directory of the image to use as the identity\n", " print(f'Most likely identity: {Path(pd_dataframe.iloc[0][\"identity\"]).parent.name}')\n", " # Get the most likely identity's confidence\n", - " print(f'Confidence: {pd_dataframe.iloc[0][\"VGG-Face_cosine\"]}')\n", + " print(f'Confidence: {pd_dataframe.iloc[0][\"model_name=\"ArcFace\", detector_backend=\"opencv\")\"]}')\n", "\n", "# uuid_path.unlink()" ] @@ -67,7 +67,7 @@ "metadata": {}, "outputs": [], "source": [ - "DeepFace.stream(db_path=\"faces\")" + "DeepFace.stream(db_path=\"faces\", model_name=\"ArcFace\", detector_backend=\"opencv\")" ] } ], diff --git a/pyproject.toml b/pyproject.toml index a5dc457..9a95876 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wyzely-detect" -version = "0.1.8" +version = "0.1.0" description = "Recognize faces/objects in a video stream (from a webcam or a security camera) and send notifications to your devices" authors = ["slashtechno <77907286+slashtechno@users.noreply.github.com>"] license = "MIT" diff --git a/wyzely_detect/__main__.py b/wyzely_detect/__main__.py index 2166a24..6b126e3 100644 --- a/wyzely_detect/__main__.py +++ b/wyzely_detect/__main__.py @@ -33,6 +33,8 @@ def main(): else: print("No .env file found") + # TODO: If possible, move the argparse stuff to a separate file + # It's taking up too many lines in this file argparser = argparse.ArgumentParser( prog="Wyzely Detect", description="Recognize faces/objects in a video stream (from a webcam or a security camera) and send notifications to your devices", # noqa: E501 @@ -87,7 +89,7 @@ def main(): if "FACES_DIRECTORY" in os.environ and os.environ["FACES_DIRECTORY"] != "" else "faces", type=str, - help="The directory to store the faces. Should contain 1 subdirectory of images per person", + help="The directory to store the faces. Can either contain images or subdirectories with images, the latter being the preferred method", # noqa: E501 ) argparser.add_argument( "--detect-object", @@ -118,7 +120,7 @@ def main(): # Defaults for the stuff here and down are already set in notify.py. # Setting them here just means that argparse will display the default values as defualt # TODO: Perhaps just remove the default parameter and just add to the help message that the default is set is x - # TODO: Make ntfy optional in ntfy.py + # TODO: Make ntfy optional in ntfy.py. Currently, unless there is a local or LAN instance of ntfy, this can't run offline notifcation_services = argparser.add_argument_group("Notification Services") notifcation_services.add_argument( "--ntfy-url", @@ -198,6 +200,10 @@ def main(): # view_frame = cv2.resize(frame, (0, 0), fx=args.view_scale, fy=args.view_scale) results = model(run_frame, verbose=False) + + path_to_faces = Path(args.faces_directory) + path_to_faces_exists = path_to_faces.is_dir() + for i, r in enumerate(results): # list of dicts with each dict containing a label, x1, y1, x2, y2 plot_boxes = [] @@ -205,20 +211,24 @@ def main(): # The following is stuff for people # This is still in the for loop as each result, no matter if anything is detected, will be present. # Thus, there will always be one result (r) - # TODO: Make it so this only runs if the faces directory is not empty - if face_details := utils.recognize_face( - path_to_directory=Path(args.faces_directory), run_frame=run_frame - ): - plot_boxes.append(face_details) - objects_and_peoples = notify.thing_detected( - thing_name=face_details["label"], - objects_and_peoples=objects_and_peoples, - detection_type="peoples", - detection_window=args.detection_window, - detection_duration=args.detection_duration, - notification_window=args.notification_window, - ntfy_url=args.ntfy_url, - ) + + # Only run if path_to_faces exists + # May be better to check every iteration, but this also works + if path_to_faces_exists: + if face_details := utils.recognize_face( + path_to_directory=path_to_faces, + run_frame=run_frame + ): + plot_boxes.append(face_details) + objects_and_peoples = notify.thing_detected( + thing_name=face_details["label"], + objects_and_peoples=objects_and_peoples, + detection_type="peoples", + detection_window=args.detection_window, + detection_duration=args.detection_duration, + notification_window=args.notification_window, + ntfy_url=args.ntfy_url, + ) # The following is stuff for objects # Setup dictionary of object names diff --git a/wyzely_detect/utils/utils.py b/wyzely_detect/utils/utils.py index ebbca11..22d369e 100644 --- a/wyzely_detect/utils/utils.py +++ b/wyzely_detect/utils/utils.py @@ -92,38 +92,53 @@ def recognize_face( """ global first_face_try - # If it's the first time the function is being run, remove representations_vgg_face.pkl, if it exists + # If it's the first time the function is being run, remove representations_arcface.pkl, if it exists if first_face_try: try: - Path("representations_vgg_face.pkl").unlink() - print("Removing representations_vgg_face.pkl") + path_to_directory.joinpath("representations_arcface.pkl").unlink() + print("Removing representations_arcface.pkl") except FileNotFoundError: - pass + print("representations_arcface.pkl does not exist") first_face_try = False + # For debugging + # if path_to_directory.joinpath("representations_arcface.pkl").exists(): + # print("representations_arcface.pkl exists") + # else: + # print("representations_arcface.pkl does not exist") + # face_dataframes is a vanilla list of dataframes - face_dataframes = [] + # It seems face_dataframes is empty if the face database (directory) doesn't exist. Seems to work if it's empty though + # This line is here to prevent a crash if that happens. However, there is a check in __main__ so it shouldn't happen + face_dataframes = [] try: face_dataframes = DeepFace.find( run_frame, db_path=str(path_to_directory), enforce_detection=True, silent=True, - ) + model_name="ArcFace", detector_backend="opencv" + ) except ValueError as e: if ( str(e) - == "Face could not be detected. Please confirm that the picture is a face photo or consider to set enforce_detection param to False." + == "Face could not be detected. Please confirm that the picture is a face photo or consider to set enforce_detection param to False." # noqa: E501 ): + # print("No faces recognized") # For debugging return None + else: + raise e # Iteate over the dataframes for df in face_dataframes: # The last row is the highest confidence # So we can just grab the path from there # iloc = Integer LOCation path_to_image = Path(df.iloc[-1]["identity"]) - # Get the name of the parent directory - label = path_to_image.parent.name + # If the parent name is the same as the path to the database, then set label to the image name instead of the parent directory name + if path_to_image.parent == Path(path_to_directory): + label = path_to_image.name + else: + label = path_to_image.parent.name # Return the coordinates of the box in xyxy format, rather than xywh # This is because YOLO uses xyxy, and that's how plot_label expects # Also, xyxy is just the top left and bottom right corners of the box @@ -135,7 +150,7 @@ def recognize_face( } # After some brief testing, it seems positve matches are > 0.3 # I have not seen any false positives, so there is no threashold yet - distance = df.iloc[-1]["VGG-Face_cosine"] + distance = df.iloc[-1]["ArcFace_cosine"] # if 0.5 < distance < 0.7: # label = "Unknown" to_return = dict(label=label, **coordinates)