From 1d17bb629b3f8ada8d66d8255870b89cd72fa3a1 Mon Sep 17 00:00:00 2001 From: slashtechno <77907286+slashtechno@users.noreply.github.com> Date: Sat, 14 Oct 2023 19:25:27 -0500 Subject: [PATCH] Fixed scaling :tada: --- set_detect_notify/__main__.py | 34 +++++++++--------- set_detect_notify/utils/notify.py | 38 +++++++++----------- set_detect_notify/utils/utils.py | 58 ++++++++++++++++++++----------- 3 files changed, 71 insertions(+), 59 deletions(-) diff --git a/set_detect_notify/__main__.py b/set_detect_notify/__main__.py index f975af3..5a79b4c 100644 --- a/set_detect_notify/__main__.py +++ b/set_detect_notify/__main__.py @@ -46,8 +46,8 @@ def main(): # Set it to the env RUN_SCALE if it isn't blank, otherwise set it to 0.25 default=os.environ["RUN_SCALE"] if "RUN_SCALE" in os.environ and os.environ["RUN_SCALE"] != "" - # else 0.25, - else 1, + # else 0.25, + else 1, type=float, help="The scale to run the detection at, default is 0.25", ) @@ -56,8 +56,8 @@ def main(): # Set it to the env VIEW_SCALE if it isn't blank, otherwise set it to 0.75 default=os.environ["VIEW_SCALE"] if "VIEW_SCALE" in os.environ and os.environ["VIEW_SCALE"] != "" - # else 0.75, - else 1, + # else 0.75, + else 1, type=float, help="The scale to view the detection at, default is 0.75", ) @@ -88,7 +88,7 @@ def main(): type=str, help="The directory to store the faces. Should contain 1 subdirectory of images per person", ) - + stream_source = argparser.add_mutually_exclusive_group() stream_source.add_argument( "--url", @@ -107,7 +107,7 @@ def main(): help="The capture device to use. Can also be a url.", ) - # 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 # TODO: Perhaps just remove the default parameter and just add to the help message that the default is set is x @@ -197,9 +197,11 @@ 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) - 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( + 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", @@ -209,12 +211,12 @@ def main(): ntfy_url=args.ntfy_url, ) - - - # The following is stuff for objects # Setup dictionary of object names - if objects_and_peoples["objects"] == {} or objects_and_peoples["objects"] is None: + if ( + objects_and_peoples["objects"] == {} + or objects_and_peoples["objects"] is None + ): for name in r.names.values(): objects_and_peoples["objects"][name] = { "last_detection_time": None, @@ -264,7 +266,7 @@ def main(): } ) - objects_and_peoples=notify.thing_detected( + objects_and_peoples = notify.thing_detected( thing_name=class_id, objects_and_peoples=objects_and_peoples, detection_type="objects", @@ -274,8 +276,7 @@ def main(): ntfy_url=args.ntfy_url, ) - # TODO: On 10-14-2023, while testing, it seemed the bounding box was too low. Troubleshoot if it's a plotting problem. - # To do so, use r.plot() to cross reference the bounding box drawn by the plot_label function and r.plot() + # To debug plotting, use r.plot() to cross reference the bounding boxes drawn by the plot_label() and r.plot() frame_to_show = utils.plot_label( boxes=plot_boxes, full_frame=frame, @@ -297,5 +298,6 @@ def main(): video_capture.release() cv2.destroyAllWindows() + if __name__ == "__main__": main() diff --git a/set_detect_notify/utils/notify.py b/set_detect_notify/utils/notify.py index 2d8756a..89ae851 100644 --- a/set_detect_notify/utils/notify.py +++ b/set_detect_notify/utils/notify.py @@ -2,10 +2,9 @@ import httpx import time -''' +""" Structure of objects_and_peoples Really, the only reason peoples is a separate dictionary is to prevent duplicates, though it just makes the code more complicated. -TODO: Make a function to check if a person is in the objects dictionary and vice versa { "objects": { "object_name": { @@ -22,24 +21,24 @@ TODO: Make a function to check if a person is in the objects dictionary and vice }, }, } -''' +""" # objects_and_peoples = {} def thing_detected( - thing_name: str, - objects_and_peoples: dict, - detection_type: str = "objects", - detection_window: int = 15, - detection_duration: int = 2, - notification_window: int = 15, - ntfy_url: str = "https://ntfy.sh/set-detect-notify" - ) -> dict: - ''' + thing_name: str, + objects_and_peoples: dict, + detection_type: str = "objects", + detection_window: int = 15, + detection_duration: int = 2, + notification_window: int = 15, + ntfy_url: str = "https://ntfy.sh/set-detect-notify", +) -> dict: + """ A function to make sure 2 seconds of detection is detected in 15 seconds, 15 seconds apart. Takes a dict that will be retured with the updated detection times. MAKE SURE TO SAVE THE RETURNED DICTIONARY - ''' - + """ + # "Alias" the objects and peoples dictionaries so it's easier to work with respective_type = objects_and_peoples[detection_type] @@ -93,22 +92,18 @@ def thing_detected( # (re)send notification # Check if detection has been ongoing for 2 seconds or more in the past 15 seconds if ( - respective_type[thing_name]["detection_duration"] - >= detection_duration + respective_type[thing_name]["detection_duration"] >= detection_duration and time.time() - respective_type[thing_name]["last_detection_time"] <= detection_window ): # If the last notification was more than 15 seconds ago, then send a notification if ( respective_type[thing_name]["last_notification_time"] is None - or time.time() - - respective_type[thing_name]["last_notification_time"] + or time.time() - respective_type[thing_name]["last_notification_time"] > notification_window ): respective_type[thing_name]["last_notification_time"] = time.time() - print( - f"Detected {thing_name} for {detection_duration} seconds" - ) + print(f"Detected {thing_name} for {detection_duration} seconds") headers = construct_ntfy_headers( title=f"{thing_name} detected", tag="rotating_light", @@ -140,4 +135,3 @@ def send_notification(data: str, headers: dict, url: str): if url is None or data is None: raise ValueError("url and data cannot be None") httpx.post(url, data=data.encode("utf-8"), headers=headers) - diff --git a/set_detect_notify/utils/utils.py b/set_detect_notify/utils/utils.py index f08a223..f193459 100644 --- a/set_detect_notify/utils/utils.py +++ b/set_detect_notify/utils/utils.py @@ -5,6 +5,7 @@ from deepface import DeepFace first_face_try = True + def plot_label( # list of dicts with each dict containing a label, x1, y1, x2, y2 boxes: list = None, @@ -18,20 +19,23 @@ def plot_label( view_scale: float = None, font: int = cv2.FONT_HERSHEY_SIMPLEX, ): + # x1 and y1 are the top left corner of the box + # x2 and y2 are the bottom right corner of the box + # Example scaling: full_frame: 1 run_frame: 0.5 view_frame: 0.25 view_frame = cv2.resize(full_frame, (0, 0), fx=view_scale, fy=view_scale) for thing in boxes: cv2.rectangle( # Image view_frame, - # Start point + # Top left corner ( - int(thing["x1"] * (run_scale / view_scale)), - int(thing["y1"] * (run_scale / view_scale)), + int((thing["x1"] / run_scale) * view_scale), + int((thing["y1"] / run_scale) * view_scale), ), - # End point + # Bottom right corner ( - int(thing["x2"] * (run_scale / view_scale)), - int(thing["y2"] * (run_scale / view_scale)), + int((thing["x2"] / run_scale) * view_scale), + int((thing["y2"] / run_scale) * view_scale), ), # Color (0, 255, 0), @@ -45,8 +49,8 @@ def plot_label( thing["label"], # Origin ( - int(thing["x1"] * (run_scale / view_scale)), - int(thing["y1"] * (run_scale / view_scale)), + int((thing["x1"] / run_scale) * view_scale), + int((thing["y1"] / run_scale) * view_scale) - 10, ), # Font font, @@ -61,14 +65,14 @@ def plot_label( def recognize_face( - path_to_directory: Path = Path("faces"), - # opencv image - run_frame: np.ndarray = None, + path_to_directory: Path = Path("faces"), + # opencv image + run_frame: np.ndarray = None, ) -> np.ndarray: - ''' + """ Accepts a path to a directory of images of faces to be used as a refference In addition, accepts an opencv image to be used as the frame to be searched - + Returns a single dictonary as currently only 1 face can be detected in each frame dict contains the following keys: label, x1, y1, x2, y2 The directory should be structured as follows: @@ -85,7 +89,7 @@ def recognize_face( Point is, `name` is the name of the person in the images in the directory `name` That name will be used as the label for the face in the frame - ''' + """ global first_face_try # If it's the first time the function is being run, remove representations_vgg_face.pkl, if it exists @@ -99,9 +103,17 @@ def recognize_face( # face_dataframes is a vanilla list of dataframes try: - face_dataframes = DeepFace.find(run_frame, db_path=str(path_to_directory), enforce_detection=True, silent=True) + face_dataframes = DeepFace.find( + run_frame, + db_path=str(path_to_directory), + enforce_detection=True, + silent=True, + ) 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.": + 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." + ): return None # Iteate over the dataframes for df in face_dataframes: @@ -112,7 +124,7 @@ def recognize_face( # Get the name of the parent directory 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 + # 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 coordinates = { "x1": df.iloc[-1]["source_x"], @@ -120,14 +132,18 @@ def recognize_face( "x2": df.iloc[-1]["source_x"] + df.iloc[-1]["source_w"], "y2": df.iloc[-1]["source_y"] + df.iloc[-1]["source_h"], } + # 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"] # if 0.5 < distance < 0.7: - # label = "Unknown" + # label = "Unknown" to_return = dict(label=label, **coordinates) - print(f'Confindence: {distance}, filname: {path_to_image.name}, to_return: {to_return}') + print( + f"Confindence: {distance}, filname: {path_to_image.name}, to_return: {to_return}" + ) return to_return - ''' + """ Example dataframe, for reference identity (path to image) | source_x | source_y | source_w | source_h | VGG-Face_cosine (pretty much the confidence \_('_')_/) - ''' \ No newline at end of file + """