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)
This commit is contained in:
slashtechno 2023-10-22 12:02:07 -05:00
parent d83315518a
commit 2cf945feec
Signed by: slashtechno
GPG Key ID: 8EC1D9D9286C2B17
5 changed files with 58 additions and 32 deletions

3
.vscode/launch.json vendored
View File

@ -5,7 +5,8 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Python: Module", // "name": "Python: Module",
"name": "Debug Wyzely Detect",
"type": "python", "type": "python",
"request": "launch", "request": "launch",
"module": "wyzely_detect", "module": "wyzely_detect",

View File

@ -36,12 +36,12 @@
"# cv2.imwrite(str(uuid_path), frame)\n", "# cv2.imwrite(str(uuid_path), frame)\n",
"# dfs = DeepFace.find(img_path=str(uuid_path), db_path = \"faces\")\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", "# 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", "# Get the identity of the person\n",
"for i, pd_dataframe in enumerate(dfs):\n", "for i, pd_dataframe in enumerate(dfs):\n",
" # Sort the dataframe by confidence\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", " # 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(f'On dataframe {i}')\n",
" print(pd_dataframe)\n", " print(pd_dataframe)\n",
" # Get the most likely identity\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", " # 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", " print(f'Most likely identity: {Path(pd_dataframe.iloc[0][\"identity\"]).parent.name}')\n",
" # Get the most likely identity's confidence\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", "\n",
"# uuid_path.unlink()" "# uuid_path.unlink()"
] ]
@ -67,7 +67,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"DeepFace.stream(db_path=\"faces\")" "DeepFace.stream(db_path=\"faces\", model_name=\"ArcFace\", detector_backend=\"opencv\")"
] ]
} }
], ],

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "wyzely-detect" 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" 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>"] authors = ["slashtechno <77907286+slashtechno@users.noreply.github.com>"]
license = "MIT" license = "MIT"

View File

@ -33,6 +33,8 @@ def main():
else: else:
print("No .env file found") 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( argparser = argparse.ArgumentParser(
prog="Wyzely Detect", 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 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"] != "" if "FACES_DIRECTORY" in os.environ and os.environ["FACES_DIRECTORY"] != ""
else "faces", else "faces",
type=str, 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( argparser.add_argument(
"--detect-object", "--detect-object",
@ -118,7 +120,7 @@ def main():
# Defaults for the stuff here and down are already set in notify.py. # 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 # 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: 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 = argparser.add_argument_group("Notification Services")
notifcation_services.add_argument( notifcation_services.add_argument(
"--ntfy-url", "--ntfy-url",
@ -198,6 +200,10 @@ def main():
# view_frame = cv2.resize(frame, (0, 0), fx=args.view_scale, fy=args.view_scale) # view_frame = cv2.resize(frame, (0, 0), fx=args.view_scale, fy=args.view_scale)
results = model(run_frame, verbose=False) 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): for i, r in enumerate(results):
# list of dicts with each dict containing a label, x1, y1, x2, y2 # list of dicts with each dict containing a label, x1, y1, x2, y2
plot_boxes = [] plot_boxes = []
@ -205,20 +211,24 @@ def main():
# The following is stuff for people # 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. # 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) # 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( # Only run if path_to_faces exists
path_to_directory=Path(args.faces_directory), run_frame=run_frame # May be better to check every iteration, but this also works
): if path_to_faces_exists:
plot_boxes.append(face_details) if face_details := utils.recognize_face(
objects_and_peoples = notify.thing_detected( path_to_directory=path_to_faces,
thing_name=face_details["label"], run_frame=run_frame
objects_and_peoples=objects_and_peoples, ):
detection_type="peoples", plot_boxes.append(face_details)
detection_window=args.detection_window, objects_and_peoples = notify.thing_detected(
detection_duration=args.detection_duration, thing_name=face_details["label"],
notification_window=args.notification_window, objects_and_peoples=objects_and_peoples,
ntfy_url=args.ntfy_url, 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 # The following is stuff for objects
# Setup dictionary of object names # Setup dictionary of object names

View File

@ -92,16 +92,24 @@ def recognize_face(
""" """
global first_face_try 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: if first_face_try:
try: try:
Path("representations_vgg_face.pkl").unlink() path_to_directory.joinpath("representations_arcface.pkl").unlink()
print("Removing representations_vgg_face.pkl") print("Removing representations_arcface.pkl")
except FileNotFoundError: except FileNotFoundError:
pass print("representations_arcface.pkl does not exist")
first_face_try = False 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 is a vanilla list of 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 = [] face_dataframes = []
try: try:
face_dataframes = DeepFace.find( face_dataframes = DeepFace.find(
@ -109,21 +117,28 @@ def recognize_face(
db_path=str(path_to_directory), db_path=str(path_to_directory),
enforce_detection=True, enforce_detection=True,
silent=True, silent=True,
) model_name="ArcFace", detector_backend="opencv"
)
except ValueError as e: except ValueError as e:
if ( if (
str(e) 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 return None
else:
raise e
# Iteate over the dataframes # Iteate over the dataframes
for df in face_dataframes: for df in face_dataframes:
# The last row is the highest confidence # The last row is the highest confidence
# So we can just grab the path from there # So we can just grab the path from there
# iloc = Integer LOCation # iloc = Integer LOCation
path_to_image = Path(df.iloc[-1]["identity"]) path_to_image = Path(df.iloc[-1]["identity"])
# Get the name of the parent directory # 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
label = path_to_image.parent.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 # 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 # 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 # 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 # After some brief testing, it seems positve matches are > 0.3
# I have not seen any false positives, so there is no threashold yet # 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: # if 0.5 < distance < 0.7:
# label = "Unknown" # label = "Unknown"
to_return = dict(label=label, **coordinates) to_return = dict(label=label, **coordinates)