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",
|
"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",
|
||||||
|
|
|
@ -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\")"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue