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",
"configurations": [
{
"name": "Python: Module",
// "name": "Python: Module",
"name": "Debug Wyzely Detect",
"type": "python",
"request": "launch",
"module": "wyzely_detect",

View File

@ -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\")"
]
}
],

View File

@ -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"

View File

@ -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

View File

@ -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)