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:
parent
d83315518a
commit
2cf945feec
|
@ -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",
|
||||
|
|
|
@ -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\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue