From 33a502f60943a16eb57e0061b4d6791186013ada Mon Sep 17 00:00:00 2001 From: Mehrab Shahbazi Date: Wed, 1 Jan 2025 12:29:20 +0330 Subject: [PATCH 01/55] video path is enabled in stream --- deepface/modules/streaming.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/deepface/modules/streaming.py b/deepface/modules/streaming.py index cc44783..3f16f82 100644 --- a/deepface/modules/streaming.py +++ b/deepface/modules/streaming.py @@ -22,6 +22,7 @@ os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" IDENTIFIED_IMG_SIZE = 112 TEXT_COLOR = (255, 255, 255) + # pylint: disable=unused-variable def analysis( db_path: str, @@ -82,7 +83,11 @@ def analysis( num_frames_with_faces = 0 tic = time.time() - cap = cv2.VideoCapture(source) # webcam + # If source is an integer, use it as a webcam index. Otherwise, treat it as a video file path. + if isinstance(source, int): + cap = cv2.VideoCapture(source) # webcam + else: + cap = cv2.VideoCapture(str(source)) # video file while True: has_frame, img = cap.read() if not has_frame: From 2f9ef19e097ead76d61545a6f9c5e25fa110fa96 Mon Sep 17 00:00:00 2001 From: Mehrab Shahbazi Date: Wed, 1 Jan 2025 13:15:39 +0330 Subject: [PATCH 02/55] saving video output in stream is enabled --- deepface/DeepFace.py | 137 ++++++++++++++++++---------------- deepface/modules/streaming.py | 99 +++++++++++------------- 2 files changed, 115 insertions(+), 121 deletions(-) diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index 6eb31ac..a948f37 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -68,18 +68,18 @@ def build_model(model_name: str, task: str = "facial_recognition") -> Any: def verify( - img1_path: Union[str, np.ndarray, List[float]], - img2_path: Union[str, np.ndarray, List[float]], - model_name: str = "VGG-Face", - detector_backend: str = "opencv", - distance_metric: str = "cosine", - enforce_detection: bool = True, - align: bool = True, - expand_percentage: int = 0, - normalization: str = "base", - silent: bool = False, - threshold: Optional[float] = None, - anti_spoofing: bool = False, + img1_path: Union[str, np.ndarray, List[float]], + img2_path: Union[str, np.ndarray, List[float]], + model_name: str = "VGG-Face", + detector_backend: str = "opencv", + distance_metric: str = "cosine", + enforce_detection: bool = True, + align: bool = True, + expand_percentage: int = 0, + normalization: str = "base", + silent: bool = False, + threshold: Optional[float] = None, + anti_spoofing: bool = False, ) -> Dict[str, Any]: """ Verify if an image pair represents the same person or different persons. @@ -164,14 +164,14 @@ def verify( def analyze( - img_path: Union[str, np.ndarray], - actions: Union[tuple, list] = ("emotion", "age", "gender", "race"), - enforce_detection: bool = True, - detector_backend: str = "opencv", - align: bool = True, - expand_percentage: int = 0, - silent: bool = False, - anti_spoofing: bool = False, + img_path: Union[str, np.ndarray], + actions: Union[tuple, list] = ("emotion", "age", "gender", "race"), + enforce_detection: bool = True, + detector_backend: str = "opencv", + align: bool = True, + expand_percentage: int = 0, + silent: bool = False, + anti_spoofing: bool = False, ) -> List[Dict[str, Any]]: """ Analyze facial attributes such as age, gender, emotion, and race in the provided image. @@ -263,20 +263,20 @@ def analyze( def find( - img_path: Union[str, np.ndarray], - db_path: str, - model_name: str = "VGG-Face", - distance_metric: str = "cosine", - enforce_detection: bool = True, - detector_backend: str = "opencv", - align: bool = True, - expand_percentage: int = 0, - threshold: Optional[float] = None, - normalization: str = "base", - silent: bool = False, - refresh_database: bool = True, - anti_spoofing: bool = False, - batched: bool = False, + img_path: Union[str, np.ndarray], + db_path: str, + model_name: str = "VGG-Face", + distance_metric: str = "cosine", + enforce_detection: bool = True, + detector_backend: str = "opencv", + align: bool = True, + expand_percentage: int = 0, + threshold: Optional[float] = None, + normalization: str = "base", + silent: bool = False, + refresh_database: bool = True, + anti_spoofing: bool = False, + batched: bool = False, ) -> Union[List[pd.DataFrame], List[List[Dict[str, Any]]]]: """ Identify individuals in a database @@ -369,15 +369,15 @@ def find( def represent( - img_path: Union[str, np.ndarray], - model_name: str = "VGG-Face", - enforce_detection: bool = True, - detector_backend: str = "opencv", - align: bool = True, - expand_percentage: int = 0, - normalization: str = "base", - anti_spoofing: bool = False, - max_faces: Optional[int] = None, + img_path: Union[str, np.ndarray], + model_name: str = "VGG-Face", + enforce_detection: bool = True, + detector_backend: str = "opencv", + align: bool = True, + expand_percentage: int = 0, + normalization: str = "base", + anti_spoofing: bool = False, + max_faces: Optional[int] = None, ) -> List[Dict[str, Any]]: """ Represent facial images as multi-dimensional vector embeddings. @@ -441,15 +441,16 @@ def represent( def stream( - db_path: str = "", - model_name: str = "VGG-Face", - detector_backend: str = "opencv", - distance_metric: str = "cosine", - enable_face_analysis: bool = True, - source: Any = 0, - time_threshold: int = 5, - frame_threshold: int = 5, - anti_spoofing: bool = False, + db_path: str = "", + model_name: str = "VGG-Face", + detector_backend: str = "opencv", + distance_metric: str = "cosine", + enable_face_analysis: bool = True, + source: Any = 0, + time_threshold: int = 5, + frame_threshold: int = 5, + anti_spoofing: bool = False, + output_path: Optional[str] = None, # New parameter ) -> None: """ Run real time face recognition and facial attribute analysis @@ -478,6 +479,9 @@ def stream( frame_threshold (int): The frame threshold for face recognition (default is 5). anti_spoofing (boolean): Flag to enable anti spoofing (default is False). + + output_path (str): Path to save the output video. If None, no video is saved. + Returns: None """ @@ -495,19 +499,20 @@ def stream( time_threshold=time_threshold, frame_threshold=frame_threshold, anti_spoofing=anti_spoofing, + output_path=output_path, # Pass the output_path to analysis ) def extract_faces( - img_path: Union[str, np.ndarray], - detector_backend: str = "opencv", - enforce_detection: bool = True, - align: bool = True, - expand_percentage: int = 0, - grayscale: bool = False, - color_face: str = "rgb", - normalize_face: bool = True, - anti_spoofing: bool = False, + img_path: Union[str, np.ndarray], + detector_backend: str = "opencv", + enforce_detection: bool = True, + align: bool = True, + expand_percentage: int = 0, + grayscale: bool = False, + color_face: str = "rgb", + normalize_face: bool = True, + anti_spoofing: bool = False, ) -> List[Dict[str, Any]]: """ Extract faces from a given image @@ -584,11 +589,11 @@ def cli() -> None: def detectFace( - img_path: Union[str, np.ndarray], - target_size: tuple = (224, 224), - detector_backend: str = "opencv", - enforce_detection: bool = True, - align: bool = True, + img_path: Union[str, np.ndarray], + target_size: tuple = (224, 224), + detector_backend: str = "opencv", + enforce_detection: bool = True, + align: bool = True, ) -> Union[np.ndarray, None]: """ Deprecated face detection function. Use extract_faces for same functionality. diff --git a/deepface/modules/streaming.py b/deepface/modules/streaming.py index 3f16f82..b79e4f4 100644 --- a/deepface/modules/streaming.py +++ b/deepface/modules/streaming.py @@ -34,42 +34,29 @@ def analysis( time_threshold=5, frame_threshold=5, anti_spoofing: bool = False, + output_path: Optional[str] = None, # New parameter ): """ - Run real time face recognition and facial attribute analysis + Run real-time face recognition and facial attribute analysis, with optional video output. Args: - db_path (string): Path to the folder containing image files. All detected faces - in the database will be considered in the decision-making process. - - model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, - OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face) - - detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', - 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'yolov11n', 'yolov11s', 'yolov11m', - 'centerface' or 'skip' (default is opencv). - - distance_metric (string): Metric for measuring similarity. Options: 'cosine', - 'euclidean', 'euclidean_l2' (default is cosine). - - enable_face_analysis (bool): Flag to enable face analysis (default is True). - - source (Any): The source for the video stream (default is 0, which represents the - default camera). - - time_threshold (int): The time threshold (in seconds) for face recognition (default is 5). - - frame_threshold (int): The frame threshold for face recognition (default is 5). - - anti_spoofing (boolean): Flag to enable anti spoofing (default is False). + db_path (str): Path to the folder containing image files. + model_name (str): Model for face recognition. + detector_backend (str): Face detector backend. + distance_metric (str): Metric for measuring similarity. + enable_face_analysis (bool): Flag to enable face analysis. + source (Any): The source for the video stream (camera index or video file path). + time_threshold (int): Time threshold (in seconds) for face recognition. + frame_threshold (int): Frame threshold for face recognition. + anti_spoofing (bool): Flag to enable anti-spoofing. + output_path (str): Path to save the output video. If None, no video is saved. Returns: None """ - # initialize models + # Initialize models build_demography_models(enable_face_analysis=enable_face_analysis) build_facial_recognition_model(model_name=model_name) - # call a dummy find function for db_path once to create embeddings before starting webcam _ = search_identity( detected_face=np.zeros([224, 224, 3]), db_path=db_path, @@ -78,35 +65,40 @@ def analysis( model_name=model_name, ) + cap = cv2.VideoCapture(source if isinstance(source, str) else int(source)) + if not cap.isOpened(): + logger.error(f"Cannot open video source: {source}") + return + + # Get video properties + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + fps = cap.get(cv2.CAP_PROP_FPS) + fourcc = cv2.VideoWriter_fourcc(*"mp4v") # Codec for output file + + # Initialize video writer if output_path is provided + video_writer = None + if output_path: + video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) + freezed_img = None freeze = False num_frames_with_faces = 0 tic = time.time() - # If source is an integer, use it as a webcam index. Otherwise, treat it as a video file path. - if isinstance(source, int): - cap = cv2.VideoCapture(source) # webcam - else: - cap = cv2.VideoCapture(str(source)) # video file while True: has_frame, img = cap.read() if not has_frame: break - # we are adding some figures into img such as identified facial image, age, gender - # that is why, we need raw image itself to make analysis raw_img = img.copy() - faces_coordinates = [] - if freeze is False: + + if not freeze: faces_coordinates = grab_facial_areas( img=img, detector_backend=detector_backend, anti_spoofing=anti_spoofing ) - - # we will pass img to analyze modules (identity, demography) and add some illustrations - # that is why, we will not be able to extract detected face from img clearly detected_faces = extract_facial_areas(img=img, faces_coordinates=faces_coordinates) - img = highlight_facial_areas(img=img, faces_coordinates=faces_coordinates) img = countdown_to_freeze( img=img, @@ -116,22 +108,18 @@ def analysis( ) num_frames_with_faces = num_frames_with_faces + 1 if len(faces_coordinates) else 0 - freeze = num_frames_with_faces > 0 and num_frames_with_faces % frame_threshold == 0 + if freeze: - # add analyze results into img - derive from raw_img img = highlight_facial_areas( img=raw_img, faces_coordinates=faces_coordinates, anti_spoofing=anti_spoofing ) - - # age, gender and emotion analysis img = perform_demography_analysis( enable_face_analysis=enable_face_analysis, img=raw_img, faces_coordinates=faces_coordinates, detected_faces=detected_faces, ) - # facial recogntion analysis img = perform_facial_recognition( img=img, faces_coordinates=faces_coordinates, @@ -141,30 +129,31 @@ def analysis( distance_metric=distance_metric, model_name=model_name, ) - - # freeze the img after analysis freezed_img = img.copy() - - # start counter for freezing tic = time.time() - logger.info("freezed") + logger.info("Image frozen for analysis") - elif freeze is True and time.time() - tic > time_threshold: + elif freeze and time.time() - tic > time_threshold: freeze = False freezed_img = None - # reset counter for freezing tic = time.time() - logger.info("freeze released") + logger.info("Freeze released") freezed_img = countdown_to_release(img=freezed_img, tic=tic, time_threshold=time_threshold) + display_img = img if freezed_img is None else freezed_img - cv2.imshow("img", img if freezed_img is None else freezed_img) + # Save the frame to output video if writer is initialized + if video_writer: + video_writer.write(display_img) - if cv2.waitKey(1) & 0xFF == ord("q"): # press q to quit + cv2.imshow("img", display_img) + if cv2.waitKey(1) & 0xFF == ord("q"): break - # kill open cv things + # Release resources cap.release() + if video_writer: + video_writer.release() cv2.destroyAllWindows() From c513b26d6d8ecaf3cff0f9d4c2e41c4abf90a0a3 Mon Sep 17 00:00:00 2001 From: Mehrab Shahbazi Date: Thu, 2 Jan 2025 21:51:20 +0330 Subject: [PATCH 03/55] solved pr comments: restore comments and used one line if --- deepface/DeepFace.py | 6 +-- deepface/modules/streaming.py | 70 +++++++++++++++++++++++++---------- 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index a948f37..b2a8ca1 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -450,7 +450,7 @@ def stream( time_threshold: int = 5, frame_threshold: int = 5, anti_spoofing: bool = False, - output_path: Optional[str] = None, # New parameter + output_path: Optional[str] = None, ) -> None: """ Run real time face recognition and facial attribute analysis @@ -480,7 +480,7 @@ def stream( anti_spoofing (boolean): Flag to enable anti spoofing (default is False). - output_path (str): Path to save the output video. If None, no video is saved. + output_path (str): Path to save the output video. If None, no video is saved (default is None). Returns: None @@ -499,7 +499,7 @@ def stream( time_threshold=time_threshold, frame_threshold=frame_threshold, anti_spoofing=anti_spoofing, - output_path=output_path, # Pass the output_path to analysis + output_path=output_path, ) diff --git a/deepface/modules/streaming.py b/deepface/modules/streaming.py index b79e4f4..335f6ee 100644 --- a/deepface/modules/streaming.py +++ b/deepface/modules/streaming.py @@ -34,29 +34,45 @@ def analysis( time_threshold=5, frame_threshold=5, anti_spoofing: bool = False, - output_path: Optional[str] = None, # New parameter + output_path: Optional[str] = None, ): """ - Run real-time face recognition and facial attribute analysis, with optional video output. + Run real time face recognition and facial attribute analysis Args: - db_path (str): Path to the folder containing image files. - model_name (str): Model for face recognition. - detector_backend (str): Face detector backend. - distance_metric (str): Metric for measuring similarity. - enable_face_analysis (bool): Flag to enable face analysis. - source (Any): The source for the video stream (camera index or video file path). - time_threshold (int): Time threshold (in seconds) for face recognition. - frame_threshold (int): Frame threshold for face recognition. - anti_spoofing (bool): Flag to enable anti-spoofing. - output_path (str): Path to save the output video. If None, no video is saved. + db_path (string): Path to the folder containing image files. All detected faces + in the database will be considered in the decision-making process. + + model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, + OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet (default is VGG-Face) + + detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', + 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'yolov11n', 'yolov11s', 'yolov11m', + 'centerface' or 'skip' (default is opencv). + + distance_metric (string): Metric for measuring similarity. Options: 'cosine', + 'euclidean', 'euclidean_l2' (default is cosine). + + enable_face_analysis (bool): Flag to enable face analysis (default is True). + + source (Any): The source for the video stream (default is 0, which represents the + default camera). + + time_threshold (int): The time threshold (in seconds) for face recognition (default is 5). + + frame_threshold (int): The frame threshold for face recognition (default is 5). + + anti_spoofing (boolean): Flag to enable anti spoofing (default is False). + + output_path (str): Path to save the output video. If None, no video is saved (default is None). Returns: None """ - # Initialize models + # initialize models build_demography_models(enable_face_analysis=enable_face_analysis) build_facial_recognition_model(model_name=model_name) + # call a dummy find function for db_path once to create embeddings before starting webcam _ = search_identity( detected_face=np.zeros([224, 224, 3]), db_path=db_path, @@ -77,9 +93,11 @@ def analysis( fourcc = cv2.VideoWriter_fourcc(*"mp4v") # Codec for output file # Initialize video writer if output_path is provided - video_writer = None - if output_path: - video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) + video_writer = ( + cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height)) + if output_path + else None + ) freezed_img = None freeze = False @@ -91,6 +109,8 @@ def analysis( if not has_frame: break + # we are adding some figures into img such as identified facial image, age, gender + # that is why, we need raw image itself to make analysis raw_img = img.copy() faces_coordinates = [] @@ -98,6 +118,9 @@ def analysis( faces_coordinates = grab_facial_areas( img=img, detector_backend=detector_backend, anti_spoofing=anti_spoofing ) + + # we will pass img to analyze modules (identity, demography) and add some illustrations + # that is why, we will not be able to extract detected face from img clearly detected_faces = extract_facial_areas(img=img, faces_coordinates=faces_coordinates) img = highlight_facial_areas(img=img, faces_coordinates=faces_coordinates) img = countdown_to_freeze( @@ -111,15 +134,19 @@ def analysis( freeze = num_frames_with_faces > 0 and num_frames_with_faces % frame_threshold == 0 if freeze: + # add analyze results into img - derive from raw_img img = highlight_facial_areas( img=raw_img, faces_coordinates=faces_coordinates, anti_spoofing=anti_spoofing ) + + # age, gender and emotion analysis img = perform_demography_analysis( enable_face_analysis=enable_face_analysis, img=raw_img, faces_coordinates=faces_coordinates, detected_faces=detected_faces, ) + # facial recogntion analysis img = perform_facial_recognition( img=img, faces_coordinates=faces_coordinates, @@ -129,13 +156,18 @@ def analysis( distance_metric=distance_metric, model_name=model_name, ) + + # freeze the img after analysis freezed_img = img.copy() + + # start counter for freezing tic = time.time() - logger.info("Image frozen for analysis") + logger.info("freezed") elif freeze and time.time() - tic > time_threshold: freeze = False freezed_img = None + # reset counter for freezing tic = time.time() logger.info("Freeze released") @@ -222,10 +254,10 @@ def search_identity( # detected face is coming from parent, safe to access 1st index df = dfs[0] - if df.shape[0] == 0: + if df.shape[0] == 0: # type: ignore return None, None - candidate = df.iloc[0] + candidate = df.iloc[0] # type: ignore target_path = candidate["identity"] logger.info(f"Hello, {target_path}") From aa47e33cf47ff2a875ef6133cadfbc93bf523d4b Mon Sep 17 00:00:00 2001 From: Mehrab Shahbazi Date: Thu, 2 Jan 2025 21:54:59 +0330 Subject: [PATCH 04/55] remove #type ignore comments --- deepface/modules/streaming.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deepface/modules/streaming.py b/deepface/modules/streaming.py index 335f6ee..c16816d 100644 --- a/deepface/modules/streaming.py +++ b/deepface/modules/streaming.py @@ -254,10 +254,10 @@ def search_identity( # detected face is coming from parent, safe to access 1st index df = dfs[0] - if df.shape[0] == 0: # type: ignore + if df.shape[0] == 0: return None, None - candidate = df.iloc[0] # type: ignore + candidate = df.iloc[0] target_path = candidate["identity"] logger.info(f"Hello, {target_path}") From 5af32fa841a246e131d3faff59c8f0802205bf5e Mon Sep 17 00:00:00 2001 From: Mehrab Shahbazi Date: Fri, 3 Jan 2025 14:55:14 +0330 Subject: [PATCH 05/55] ensure output path --- deepface/modules/streaming.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/deepface/modules/streaming.py b/deepface/modules/streaming.py index c16816d..78eaa1e 100644 --- a/deepface/modules/streaming.py +++ b/deepface/modules/streaming.py @@ -91,7 +91,9 @@ def analysis( height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = cap.get(cv2.CAP_PROP_FPS) fourcc = cv2.VideoWriter_fourcc(*"mp4v") # Codec for output file - + # Ensure the output directory exists if output_path is provided + if output_path: + os.makedirs(os.path.dirname(output_path), exist_ok=True) # Initialize video writer if output_path is provided video_writer = ( cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height)) @@ -254,10 +256,10 @@ def search_identity( # detected face is coming from parent, safe to access 1st index df = dfs[0] - if df.shape[0] == 0: + if df.shape[0] == 0: return None, None - candidate = df.iloc[0] + candidate = df.iloc[0] target_path = candidate["identity"] logger.info(f"Hello, {target_path}") From 3be40a5f3c69398f9ded0e807c6c0ba65465276a Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Fri, 3 Jan 2025 11:57:58 +0000 Subject: [PATCH 06/55] hackernews badge added --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 822f298..dc2e988 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Downloads](https://static.pepy.tech/personalized-badge/deepface?period=total&units=international_system&left_color=grey&right_color=blue&left_text=downloads)](https://pepy.tech/project/deepface) [![Stars](https://img.shields.io/github/stars/serengil/deepface?color=yellow&style=flat&label=%E2%AD%90%20stars)](https://github.com/serengil/deepface/stargazers) +[![Hacker News](https://img.shields.io/badge/dynamic/json?color=orange&label=Hacker%20News&query=score&url=https%3A%2F%2Fhacker-news.firebaseio.com%2Fv0%2Fitem%2F26340789.json&logo=hackernews)](https://news.ycombinator.com/item?id=26340789) [![License](http://img.shields.io/:license-MIT-green.svg?style=flat)](https://github.com/serengil/deepface/blob/master/LICENSE) [![Tests](https://github.com/serengil/deepface/actions/workflows/tests.yml/badge.svg)](https://github.com/serengil/deepface/actions/workflows/tests.yml) [![DOI](http://img.shields.io/:DOI-10.17671/gazibtd.1399077-blue.svg?style=flat)](https://doi.org/10.17671/gazibtd.1399077) @@ -404,6 +405,10 @@ If you do like this work, then you can support it financially on [Patreon](https Also, your company's logo will be shown on README on GitHub and PyPI if you become a sponsor in gold, silver or bronze tiers. +Additionally, you can help us reach more people by upvoting our Hacker News post. + +[![Featured on Hacker News](https://hackerbadge.vercel.app/api?id=26340789&type=orange)](https://news.ycombinator.com/item?id=26340789) + ## Citation Please cite deepface in your publications if it helps your research - see [`CITATIONS`](https://github.com/serengil/deepface/blob/master/CITATION.md) for more details. Here are its BibTex entries: From 79f1eddda6cf51b61c7616719f66feb868f51516 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Fri, 3 Jan 2025 11:59:21 +0000 Subject: [PATCH 07/55] Update README.md support section updated --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index dc2e988..210e1db 100644 --- a/README.md +++ b/README.md @@ -393,7 +393,7 @@ Before creating a PR, you should run the unit tests and linting locally by runni There are many ways to support a project - starring⭐️ the GitHub repo is just one 🙏 -If you do like this work, then you can support it financially on [Patreon](https://www.patreon.com/serengil?repo=deepface), [GitHub Sponsors](https://github.com/sponsors/serengil) or [Buy Me a Coffee](https://buymeacoffee.com/serengil). +If you do like this work, then you can support it financially on [Patreon](https://www.patreon.com/serengil?repo=deepface), [GitHub Sponsors](https://github.com/sponsors/serengil) or [Buy Me a Coffee](https://buymeacoffee.com/serengil). Also, your company's logo will be shown on README on GitHub and PyPI if you become a sponsor in gold, silver or bronze tiers. @@ -403,8 +403,6 @@ If you do like this work, then you can support it financially on [Patreon](https -Also, your company's logo will be shown on README on GitHub and PyPI if you become a sponsor in gold, silver or bronze tiers. - Additionally, you can help us reach more people by upvoting our Hacker News post. [![Featured on Hacker News](https://hackerbadge.vercel.app/api?id=26340789&type=orange)](https://news.ycombinator.com/item?id=26340789) From cd6cdb4ab43d6194a14bbbb6fe0ad75ebc5a6cdc Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Fri, 3 Jan 2025 12:05:59 +0000 Subject: [PATCH 08/55] Update README.md new hacker rank product id --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 210e1db..aa0ab51 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Downloads](https://static.pepy.tech/personalized-badge/deepface?period=total&units=international_system&left_color=grey&right_color=blue&left_text=downloads)](https://pepy.tech/project/deepface) [![Stars](https://img.shields.io/github/stars/serengil/deepface?color=yellow&style=flat&label=%E2%AD%90%20stars)](https://github.com/serengil/deepface/stargazers) -[![Hacker News](https://img.shields.io/badge/dynamic/json?color=orange&label=Hacker%20News&query=score&url=https%3A%2F%2Fhacker-news.firebaseio.com%2Fv0%2Fitem%2F26340789.json&logo=hackernews)](https://news.ycombinator.com/item?id=26340789) +[![Hacker News](https://img.shields.io/badge/dynamic/json?color=orange&label=Hacker%20News&query=score&url=https%3A%2F%2Fhacker-news.firebaseio.com%2Fv0%2Fitem%2F42584896.json&logo=hackernews)](https://news.ycombinator.com/item?id=42584896) [![License](http://img.shields.io/:license-MIT-green.svg?style=flat)](https://github.com/serengil/deepface/blob/master/LICENSE) [![Tests](https://github.com/serengil/deepface/actions/workflows/tests.yml/badge.svg)](https://github.com/serengil/deepface/actions/workflows/tests.yml) [![DOI](http://img.shields.io/:DOI-10.17671/gazibtd.1399077-blue.svg?style=flat)](https://doi.org/10.17671/gazibtd.1399077) @@ -405,7 +405,7 @@ If you do like this work, then you can support it financially on [Patreon](https Additionally, you can help us reach more people by upvoting our Hacker News post. -[![Featured on Hacker News](https://hackerbadge.vercel.app/api?id=26340789&type=orange)](https://news.ycombinator.com/item?id=26340789) +[![Featured on Hacker News](https://hackerbadge.vercel.app/api?id=42584896&type=orange)](https://news.ycombinator.com/item?id=42584896) ## Citation From 0d1fb9606cadd97f8c7e259660d5bda4a1358113 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Fri, 3 Jan 2025 13:04:26 +0000 Subject: [PATCH 09/55] Update README.md product hunt --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aa0ab51..30bb63b 100644 --- a/README.md +++ b/README.md @@ -403,10 +403,12 @@ If you do like this work, then you can support it financially on [Patreon](https -Additionally, you can help us reach more people by upvoting our Hacker News post. +Additionally, you can help us reach a wider audience by upvoting our posts on Hacker News and Product Hunt. [![Featured on Hacker News](https://hackerbadge.vercel.app/api?id=42584896&type=orange)](https://news.ycombinator.com/item?id=42584896) +DeepFace - A Lightweight Deep Face Recognition Library for Python | Product Hunt + ## Citation Please cite deepface in your publications if it helps your research - see [`CITATIONS`](https://github.com/serengil/deepface/blob/master/CITATION.md) for more details. Here are its BibTex entries: From ed31afb43e07d71f80248b326f1da5d7f3a16760 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Fri, 3 Jan 2025 13:05:28 +0000 Subject: [PATCH 10/55] Update README.md support --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 30bb63b..fa360da 100644 --- a/README.md +++ b/README.md @@ -405,9 +405,17 @@ If you do like this work, then you can support it financially on [Patreon](https Additionally, you can help us reach a wider audience by upvoting our posts on Hacker News and Product Hunt. -[![Featured on Hacker News](https://hackerbadge.vercel.app/api?id=42584896&type=orange)](https://news.ycombinator.com/item?id=42584896) - -DeepFace - A Lightweight Deep Face Recognition Library for Python | Product Hunt +
+ + + Featured on Hacker News + + + + + DeepFace - A Lightweight Deep Face Recognition Library for Python | Product Hunt + +
## Citation From bf12703a12faf98cc0dbb03e0a91bed1293691d9 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Fri, 3 Jan 2025 13:08:32 +0000 Subject: [PATCH 11/55] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa360da..600de36 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ [![Downloads](https://static.pepy.tech/personalized-badge/deepface?period=total&units=international_system&left_color=grey&right_color=blue&left_text=downloads)](https://pepy.tech/project/deepface) [![Stars](https://img.shields.io/github/stars/serengil/deepface?color=yellow&style=flat&label=%E2%AD%90%20stars)](https://github.com/serengil/deepface/stargazers) -[![Hacker News](https://img.shields.io/badge/dynamic/json?color=orange&label=Hacker%20News&query=score&url=https%3A%2F%2Fhacker-news.firebaseio.com%2Fv0%2Fitem%2F42584896.json&logo=hackernews)](https://news.ycombinator.com/item?id=42584896) [![License](http://img.shields.io/:license-MIT-green.svg?style=flat)](https://github.com/serengil/deepface/blob/master/LICENSE) [![Tests](https://github.com/serengil/deepface/actions/workflows/tests.yml/badge.svg)](https://github.com/serengil/deepface/actions/workflows/tests.yml) [![DOI](http://img.shields.io/:DOI-10.17671/gazibtd.1399077-blue.svg?style=flat)](https://doi.org/10.17671/gazibtd.1399077) @@ -19,6 +18,7 @@ + From 0f3ffbe6a7bfc366294395f2f5534f56d55128b0 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Fri, 3 Jan 2025 13:14:01 +0000 Subject: [PATCH 12/55] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 600de36..e68853a 100644 --- a/README.md +++ b/README.md @@ -408,7 +408,7 @@ Additionally, you can help us reach a wider audience by upvoting our posts on Ha
- Featured on Hacker News + Featured on Hacker News From aef0a8e89bb6615f834eb132d0293f611a184477 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Fri, 3 Jan 2025 13:18:21 +0000 Subject: [PATCH 13/55] Update README.md summary badges for hacker news and product hunt --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e68853a..c0c4fb5 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,11 @@ [![GitHub Sponsors](https://img.shields.io/github/sponsors/serengil?logo=GitHub&color=lightgray)](https://github.com/sponsors/serengil) [![Buy Me a Coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](https://buymeacoffee.com/serengil) +[![Hacker News](https://img.shields.io/badge/dynamic/json?color=orange&label=Hacker%20News&query=score&url=https%3A%2F%2Fhacker-news.firebaseio.com%2Fv0%2Fitem%2F42584896.json&logo=hackernews)](https://news.ycombinator.com/item?id=42584896) +[![Product Hunt](https://img.shields.io/badge/Product%20Hunt-%E2%96%B2-orange)](https://www.producthunt.com/posts/deepface?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-deepface) + -
From ef72640ee4d8f79e7019df9d560a9a84ef23dfd4 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Fri, 3 Jan 2025 13:27:49 +0000 Subject: [PATCH 14/55] Update README.md product hunt logo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c0c4fb5..d170fcc 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ [![Buy Me a Coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](https://buymeacoffee.com/serengil) [![Hacker News](https://img.shields.io/badge/dynamic/json?color=orange&label=Hacker%20News&query=score&url=https%3A%2F%2Fhacker-news.firebaseio.com%2Fv0%2Fitem%2F42584896.json&logo=hackernews)](https://news.ycombinator.com/item?id=42584896) -[![Product Hunt](https://img.shields.io/badge/Product%20Hunt-%E2%96%B2-orange)](https://www.producthunt.com/posts/deepface?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-deepface) +[![Product Hunt](https://img.shields.io/badge/Product%20Hunt-%E2%96%B2-orange?logo=producthunt)](https://www.producthunt.com/posts/deepface?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-deepface) From d5ba8fda09f6bbf710c509d1fd533f00a1021f63 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Fri, 3 Jan 2025 13:30:32 +0000 Subject: [PATCH 15/55] Update README.md y-combinator logo added --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d170fcc..f351c20 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ [![GitHub Sponsors](https://img.shields.io/github/sponsors/serengil?logo=GitHub&color=lightgray)](https://github.com/sponsors/serengil) [![Buy Me a Coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](https://buymeacoffee.com/serengil) -[![Hacker News](https://img.shields.io/badge/dynamic/json?color=orange&label=Hacker%20News&query=score&url=https%3A%2F%2Fhacker-news.firebaseio.com%2Fv0%2Fitem%2F42584896.json&logo=hackernews)](https://news.ycombinator.com/item?id=42584896) +[![Hacker News](https://img.shields.io/badge/dynamic/json?color=orange&label=Hacker%20News&query=score&url=https%3A%2F%2Fhacker-news.firebaseio.com%2Fv0%2Fitem%2F42584896.json&logo=y-combinator)](https://news.ycombinator.com/item?id=42584896) [![Product Hunt](https://img.shields.io/badge/Product%20Hunt-%E2%96%B2-orange?logo=producthunt)](https://www.producthunt.com/posts/deepface?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-deepface) From 15a4f46608f6f754bbede332df9653f806ea1651 Mon Sep 17 00:00:00 2001 From: Mehrab Shahbazi Date: Sat, 4 Jan 2025 11:23:51 +0330 Subject: [PATCH 16/55] write small dockstring for lint --- deepface/DeepFace.py | 3 ++- deepface/modules/streaming.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index b2a8ca1..f8930e5 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -480,7 +480,8 @@ def stream( anti_spoofing (boolean): Flag to enable anti spoofing (default is False). - output_path (str): Path to save the output video. If None, no video is saved (default is None). + output_path (str): Path to save the output video. (default is None + If None, no video is saved). Returns: None diff --git a/deepface/modules/streaming.py b/deepface/modules/streaming.py index 78eaa1e..e461e56 100644 --- a/deepface/modules/streaming.py +++ b/deepface/modules/streaming.py @@ -64,8 +64,8 @@ def analysis( anti_spoofing (boolean): Flag to enable anti spoofing (default is False). - output_path (str): Path to save the output video. If None, no video is saved (default is None). - + output_path (str): Path to save the output video. (default is None + If None, no video is saved). Returns: None """ From 4255b4167ff02eefe0a0ce705f51d5e24078cf42 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Sat, 4 Jan 2025 12:05:58 -0500 Subject: [PATCH 17/55] pickle objects are now dumped with pickle.HIGHEST_PROTOCOL --- deepface/modules/recognition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deepface/modules/recognition.py b/deepface/modules/recognition.py index 1edb430..f153132 100644 --- a/deepface/modules/recognition.py +++ b/deepface/modules/recognition.py @@ -149,7 +149,7 @@ def find( # Ensure the proper pickle file exists if not os.path.exists(datastore_path): with open(datastore_path, "wb") as f: - pickle.dump([], f) + pickle.dump([], f, pickle.HIGHEST_PROTOCOL) # Load the representations from the pickle file with open(datastore_path, "rb") as f: @@ -232,7 +232,7 @@ def find( if must_save_pickle: with open(datastore_path, "wb") as f: - pickle.dump(representations, f) + pickle.dump(representations, f, pickle.HIGHEST_PROTOCOL) if not silent: logger.info(f"There are now {len(representations)} representations in {file_name}") From 5747d9648b731e23586db344e2dc863c012438df Mon Sep 17 00:00:00 2001 From: h-alice Date: Mon, 6 Jan 2025 11:37:19 +0800 Subject: [PATCH 18/55] Predictor. --- deepface/models/Demography.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index d240f1e..bf4ea6c 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -21,14 +21,37 @@ class Demography(ABC): def predict(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[np.ndarray, np.float64]: pass + def _predict_internal(self, img_batch: np.ndarray) -> np.ndarray: + """ + Predict for single image or batched images. + This method uses legacy method while receiving single image as input. + And switch to batch prediction if receives batched images. + + Args: + img_batch: Batch of images as np.ndarray (n, 224, 224, 3), with n >= 1. + """ + if not self.model_name: # Check if called from derived class + raise NotImplementedError("virtual method must not be called directly") + + assert img_batch.ndim == 4, "expected 4-dimensional tensor input" + + if img_batch.shape[0] == 1: # Single image + img_batch = img_batch.squeeze(0) # Remove batch dimension + predict_result = self.model(img_batch, training=False).numpy()[0, :] + predict_result = np.expand_dims(predict_result, axis=0) # Add batch dimension + return predict_result + else: # Batch of images + return self.model.predict_on_batch(img_batch) + def _preprocess_batch_or_single_input(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: """ Preprocess single or batch of images, return as 4-D numpy array. Args: img: Single image as np.ndarray (224, 224, 3) or - List of images as List[np.ndarray] or - Batch of images as np.ndarray (n, 224, 224, 3) + List of images as List[np.ndarray] or + Batch of images as np.ndarray (n, 224, 224, 3) + NOTE: If the imput is grayscale, then there's no channel dimension. Returns: Four-dimensional numpy array (n, 224, 224, 3) """ From 5a1881492f17deb1f4b11136bd4651ed1ebcf194 Mon Sep 17 00:00:00 2001 From: h-alice Date: Mon, 6 Jan 2025 11:51:22 +0800 Subject: [PATCH 19/55] Add comment. --- deepface/models/Demography.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index bf4ea6c..64e56e2 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -37,8 +37,7 @@ class Demography(ABC): if img_batch.shape[0] == 1: # Single image img_batch = img_batch.squeeze(0) # Remove batch dimension - predict_result = self.model(img_batch, training=False).numpy()[0, :] - predict_result = np.expand_dims(predict_result, axis=0) # Add batch dimension + predict_result = self.model(img_batch, training=False).numpy()[0, :] # Predict with legacy method. return predict_result else: # Batch of images return self.model.predict_on_batch(img_batch) From 72b94d11de1d40f23dc23d63690c4641ae5c33a8 Mon Sep 17 00:00:00 2001 From: h-alice Date: Mon, 6 Jan 2025 11:51:49 +0800 Subject: [PATCH 20/55] Add new predictor. --- deepface/models/demography/Age.py | 4 ++-- deepface/models/demography/Emotion.py | 4 ++-- deepface/models/demography/Gender.py | 4 ++-- deepface/models/demography/Race.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index 5bc409f..7284aad 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -54,8 +54,8 @@ class ApparentAgeClient(Demography): # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) - # Batch prediction - age_predictions = self.model.predict_on_batch(imgs) + # Prediction + age_predictions = self._predict_internal(imgs) # Calculate apparent ages apparent_ages = np.array( diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py index 10e5115..e6ebf5f 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -78,8 +78,8 @@ class EmotionClient(Demography): # Add channel dimension for grayscale images processed_imgs = np.expand_dims(processed_imgs, axis=-1) - # Batch prediction - predictions = self.model.predict_on_batch(processed_imgs) + # Prediction + predictions = self._predict_internal(processed_imgs) return predictions diff --git a/deepface/models/demography/Gender.py b/deepface/models/demography/Gender.py index f7e705e..b6a3ef1 100644 --- a/deepface/models/demography/Gender.py +++ b/deepface/models/demography/Gender.py @@ -54,8 +54,8 @@ class GenderClient(Demography): # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) - # Batch prediction - predictions = self.model.predict_on_batch(imgs) + # Prediction + predictions = self._predict_internal(imgs) return predictions diff --git a/deepface/models/demography/Race.py b/deepface/models/demography/Race.py index 0c6a2f0..eae5154 100644 --- a/deepface/models/demography/Race.py +++ b/deepface/models/demography/Race.py @@ -54,8 +54,8 @@ class RaceClient(Demography): # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) - # Batch prediction - predictions = self.model.predict_on_batch(imgs) + # Prediction + predictions = self._predict_internal(imgs) return predictions From ca9ecbb3cab99ecccbc5286595034987214d0c09 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Mon, 6 Jan 2025 09:06:37 -0500 Subject: [PATCH 21/55] list_images now stores valid image and PIL exts in sets built ahead of time rather than on each iteration. exact_path is not created unless the file's ext is a valid image ext. --- deepface/commons/image_utils.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/deepface/commons/image_utils.py b/deepface/commons/image_utils.py index b72ce0b..f0facd6 100644 --- a/deepface/commons/image_utils.py +++ b/deepface/commons/image_utils.py @@ -23,18 +23,15 @@ def list_images(path: str) -> List[str]: images (list): list of exact image paths """ images = [] + image_exts = {".jpg", ".jpeg", ".png"} + pil_exts = {"jpeg", "png"} for r, _, f in os.walk(path): for file in f: - exact_path = os.path.join(r, file) - - ext_lower = os.path.splitext(exact_path)[-1].lower() - - if ext_lower not in {".jpg", ".jpeg", ".png"}: - continue - - with Image.open(exact_path) as img: # lazy - if img.format.lower() in {"jpeg", "png"}: - images.append(exact_path) + if os.path.splitext(file)[1].lower() in image_exts: + exact_path = os.path.join(r, file) + with Image.open(exact_path) as img: # lazy + if img.format.lower() in pil_exts: + images.append(exact_path) return images From b11eec0eab1f20c5e7d0b746e73a37e6cba97279 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Mon, 6 Jan 2025 09:08:24 -0500 Subject: [PATCH 22/55] created a new yield_images generator function to yield the images in a given path. The functionality is equivalent to list_images, but, instead of building then return a list, it yields the image path at each iteration. --- deepface/commons/image_utils.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/deepface/commons/image_utils.py b/deepface/commons/image_utils.py index f0facd6..40bf925 100644 --- a/deepface/commons/image_utils.py +++ b/deepface/commons/image_utils.py @@ -1,7 +1,7 @@ # built-in dependencies import os import io -from typing import List, Union, Tuple +from typing import Generator, List, Union, Tuple import hashlib import base64 from pathlib import Path @@ -35,6 +35,26 @@ def list_images(path: str) -> List[str]: return images +def yield_images(path: str) -> Generator[str]: + """ + List images in a given path + Args: + path (str): path's location + Yields: + image (str): image path + """ + images = [] + image_exts = {".jpg", ".jpeg", ".png"} + pil_exts = {"jpeg", "png"} + for r, _, f in os.walk(path): + for file in f: + if os.path.splitext(file)[1].lower() in image_exts: + exact_path = os.path.join(r, file) + with Image.open(exact_path) as img: # lazy + if img.format.lower() in pil_exts: + yield exact_path + + def find_image_hash(file_path: str) -> str: """ Find the hash of given image file with its properties From 9dc261b080f2e4f4e097da68e3752c7c9e3ddc65 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Mon, 6 Jan 2025 09:08:48 -0500 Subject: [PATCH 23/55] clarify docstring --- deepface/commons/image_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepface/commons/image_utils.py b/deepface/commons/image_utils.py index 40bf925..941513f 100644 --- a/deepface/commons/image_utils.py +++ b/deepface/commons/image_utils.py @@ -37,7 +37,7 @@ def list_images(path: str) -> List[str]: def yield_images(path: str) -> Generator[str]: """ - List images in a given path + Yield images in a given path Args: path (str): path's location Yields: From 2ee02e0003cff0d33c8bffe228692aadef4cea2b Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Mon, 6 Jan 2025 09:11:23 -0500 Subject: [PATCH 24/55] storage_images is now built as a set with the new deepface.commons.image_utils.yield_images generator function. Previously, storage_images was created with deepface.commons.image_utils.list_images as a list, then converted to a set while never being used as purely a list. --- deepface/modules/recognition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deepface/modules/recognition.py b/deepface/modules/recognition.py index f153132..4dc440f 100644 --- a/deepface/modules/recognition.py +++ b/deepface/modules/recognition.py @@ -168,7 +168,7 @@ def find( pickled_images = [representation["identity"] for representation in representations] # Get the list of images on storage - storage_images = image_utils.list_images(path=db_path) + storage_images = set(image_utils.yield_images(path=db_path)) if len(storage_images) == 0 and refresh_database is True: raise ValueError(f"No item found in {db_path}") @@ -186,8 +186,8 @@ def find( # Enforce data consistency amongst on disk images and pickle file if refresh_database: - new_images = set(storage_images) - set(pickled_images) # images added to storage - old_images = set(pickled_images) - set(storage_images) # images removed from storage + new_images = storage_images - set(pickled_images) # images added to storage + old_images = set(pickled_images) - storage_images # images removed from storage # detect replaced images for current_representation in representations: From 799cb0f6cfb56595535df2b7f6f32de7bfdd2697 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Mon, 6 Jan 2025 09:13:01 -0500 Subject: [PATCH 25/55] pickled_images is now created using a set comprehension, instead of a list comprehension as before. Like storage_images, all subsequent actions where set and not list actions, so it saves time re-creating the list as a set later on. --- deepface/modules/recognition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deepface/modules/recognition.py b/deepface/modules/recognition.py index 4dc440f..d964693 100644 --- a/deepface/modules/recognition.py +++ b/deepface/modules/recognition.py @@ -165,7 +165,7 @@ def find( ) # embedded images - pickled_images = [representation["identity"] for representation in representations] + pickled_images = {representation["identity"] for representation in representations} # Get the list of images on storage storage_images = set(image_utils.yield_images(path=db_path)) @@ -186,8 +186,8 @@ def find( # Enforce data consistency amongst on disk images and pickle file if refresh_database: - new_images = storage_images - set(pickled_images) # images added to storage - old_images = set(pickled_images) - storage_images # images removed from storage + new_images = storage_images - pickled_images # images added to storage + old_images = pickled_images - storage_images # images removed from storage # detect replaced images for current_representation in representations: From 56d3b66a5cf08484c5975d48f3ab6ccf63d32310 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Mon, 6 Jan 2025 09:15:38 -0500 Subject: [PATCH 26/55] df_cols is now created as a set. All operations were already set operations on the object. If the order of the columns will need to be maintained in future versions, this should be restored to the original list. --- deepface/modules/recognition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deepface/modules/recognition.py b/deepface/modules/recognition.py index d964693..fc6f35b 100644 --- a/deepface/modules/recognition.py +++ b/deepface/modules/recognition.py @@ -136,7 +136,7 @@ def find( representations = [] # required columns for representations - df_cols = [ + df_cols = { "identity", "hash", "embedding", @@ -144,7 +144,7 @@ def find( "target_y", "target_w", "target_h", - ] + } # Ensure the proper pickle file exists if not os.path.exists(datastore_path): @@ -157,7 +157,7 @@ def find( # check each item of representations list has required keys for i, current_representation in enumerate(representations): - missing_keys = set(df_cols) - set(current_representation.keys()) + missing_keys = df_cols - set(current_representation.keys()) if len(missing_keys) > 0: raise ValueError( f"{i}-th item does not have some required keys - {missing_keys}." From 661f13f3b3ee47322903ca0043d1fcb11ea134a3 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Mon, 6 Jan 2025 09:22:18 -0500 Subject: [PATCH 27/55] Remove no longer needed images list --- deepface/commons/image_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/deepface/commons/image_utils.py b/deepface/commons/image_utils.py index 941513f..1393af1 100644 --- a/deepface/commons/image_utils.py +++ b/deepface/commons/image_utils.py @@ -43,7 +43,6 @@ def yield_images(path: str) -> Generator[str]: Yields: image (str): image path """ - images = [] image_exts = {".jpg", ".jpeg", ".png"} pil_exts = {"jpeg", "png"} for r, _, f in os.walk(path): From 9995343e248adbb1d7ed30b77bd0f63e25964d90 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Mon, 6 Jan 2025 09:34:23 -0500 Subject: [PATCH 28/55] delay creating pickled_images until necessary --- deepface/modules/recognition.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/deepface/modules/recognition.py b/deepface/modules/recognition.py index fc6f35b..90e8c29 100644 --- a/deepface/modules/recognition.py +++ b/deepface/modules/recognition.py @@ -164,9 +164,6 @@ def find( f"Consider to delete {datastore_path}" ) - # embedded images - pickled_images = {representation["identity"] for representation in representations} - # Get the list of images on storage storage_images = set(image_utils.yield_images(path=db_path)) @@ -186,6 +183,11 @@ def find( # Enforce data consistency amongst on disk images and pickle file if refresh_database: + # embedded images + pickled_images = { + representation["identity"] for representation in representations + } + new_images = storage_images - pickled_images # images added to storage old_images = pickled_images - storage_images # images removed from storage From 2aa8ebfec89f908030613c183cd7904701d53335 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Mon, 6 Jan 2025 09:43:29 -0500 Subject: [PATCH 29/55] explicitly specified the SendType and ReturnType paramaters for yield_images --- deepface/commons/image_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepface/commons/image_utils.py b/deepface/commons/image_utils.py index 1393af1..b984f91 100644 --- a/deepface/commons/image_utils.py +++ b/deepface/commons/image_utils.py @@ -35,7 +35,7 @@ def list_images(path: str) -> List[str]: return images -def yield_images(path: str) -> Generator[str]: +def yield_images(path: str) -> Generator[str, None, None]: """ Yield images in a given path Args: From 6a7505269cff4663908598e4bd44e62f9cab4291 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Mon, 6 Jan 2025 10:11:10 -0500 Subject: [PATCH 30/55] Test image_utils.yield_images returns the same files as image_utils.list_images --- tests/test_find.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/test_find.py b/tests/test_find.py index ffea91b..de8956d 100644 --- a/tests/test_find.py +++ b/tests/test_find.py @@ -95,12 +95,23 @@ def test_filetype_for_find(): def test_filetype_for_find_bulk_embeddings(): - imgs = image_utils.list_images("dataset") + # List + list_imgs = image_utils.list_images("dataset") - assert len(imgs) > 0 + assert len(list_imgs) > 0 # img47 is webp even though its extension is jpg - assert "dataset/img47.jpg" not in imgs + assert "dataset/img47.jpg" not in list_imgs + + # Generator + gen_imgs = list(image_utils.yield_images("dataset")) + + assert len(gen_imgs) > 0 + + # img47 is webp even though its extension is jpg + assert "dataset/img47.jpg" not in gen_imgs + + assert gen_imgs == list_imgs def test_find_without_refresh_database(): From 85e2d8d863abf11d2d3cb3bb98c1702857a7aeb2 Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 04:05:11 +0800 Subject: [PATCH 31/55] [update] modify comment for multi models --- deepface/models/Demography.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index 64e56e2..d0a00f1 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -28,7 +28,7 @@ class Demography(ABC): And switch to batch prediction if receives batched images. Args: - img_batch: Batch of images as np.ndarray (n, 224, 224, 3), with n >= 1. + img_batch: Batch of images as np.ndarray (n, x, y, c), with n >= 1, x = image width, y = image height, c = channel """ if not self.model_name: # Check if called from derived class raise NotImplementedError("virtual method must not be called directly") From ba0d0c5bb66587fd67b7407e7bfed5e5a879b98e Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 04:54:17 +0800 Subject: [PATCH 32/55] [update] make process to one-line --- deepface/models/demography/Emotion.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py index e6ebf5f..caf862b 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -72,13 +72,11 @@ class EmotionClient(Demography): # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) - # Preprocess each image - processed_imgs = np.array([self._preprocess_image(img) for img in imgs]) - - # Add channel dimension for grayscale images - processed_imgs = np.expand_dims(processed_imgs, axis=-1) + # Preprocess each image and add channel dimension for grayscale images + processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=0) # Prediction + # Emotion model input shape is (48, 48, 1, n), where n is the batch size predictions = self._predict_internal(processed_imgs) return predictions From 29141b3cd5293c88fca852b1e0a7a64a09f75304 Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 04:54:35 +0800 Subject: [PATCH 33/55] [update] add hint for the shape of input img --- deepface/models/demography/Age.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deepface/models/demography/Age.py b/deepface/models/demography/Age.py index 7284aad..ae53487 100644 --- a/deepface/models/demography/Age.py +++ b/deepface/models/demography/Age.py @@ -54,9 +54,9 @@ class ApparentAgeClient(Demography): # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) - # Prediction + # Prediction from 3 channels image age_predictions = self._predict_internal(imgs) - + # Calculate apparent ages apparent_ages = np.array( [find_apparent_age(age_prediction) for age_prediction in age_predictions] From 36fb512bec44284f0b02bed98f8c121f7bc54884 Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 04:55:14 +0800 Subject: [PATCH 34/55] [fix] handle between grayscale and RGB image for models --- deepface/models/Demography.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index d0a00f1..7bba87f 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -36,7 +36,8 @@ class Demography(ABC): assert img_batch.ndim == 4, "expected 4-dimensional tensor input" if img_batch.shape[0] == 1: # Single image - img_batch = img_batch.squeeze(0) # Remove batch dimension + if img_batch.shape[-1] != 3: # Check if grayscale + img_batch = img_batch.squeeze(0) # Remove batch dimension predict_result = self.model(img_batch, training=False).numpy()[0, :] # Predict with legacy method. return predict_result else: # Batch of images From 431544ac523e6d1ebae573b0cc2efc99604a1784 Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 04:56:05 +0800 Subject: [PATCH 35/55] [update] add process for single and multiple image --- deepface/modules/demography.py | 40 +++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 6b6382f..36b8305 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -182,30 +182,49 @@ def analyze( # Build the emotion model model = modeling.build_model(task="facial_attribute", model_name="Emotion") emotion_predictions = model.predict(faces_array) - + + # Handle single vs multiple emotion predictions + if len(emotion_predictions.shape) == 1: + # Single face case - reshape predictions to 2D array for consistent handling + emotion_predictions = emotion_predictions.reshape(1, -1) + + # Process predictions for each face for idx, predictions in enumerate(emotion_predictions): sum_of_predictions = predictions.sum() resp_objects[idx]["emotion"] = {} - + + # Calculate emotion probabilities and store in response for i, emotion_label in enumerate(Emotion.labels): - emotion_prediction = 100 * predictions[i] / sum_of_predictions - resp_objects[idx]["emotion"][emotion_label] = emotion_prediction - - resp_objects[idx]["dominant_emotion"] = Emotion.labels[np.argmax(predictions)] + emotion_probability = 100 * predictions[i] / sum_of_predictions + resp_objects[idx]["emotion"][emotion_label] = emotion_probability + + # Store dominant emotion + resp_objects[idx]["dominant_emotion"] = Emotion.labels[np.argmax(predictions)] elif action == "age": # Build the age model model = modeling.build_model(task="facial_attribute", model_name="Age") age_predictions = model.predict(faces_array) + # Handle single vs multiple age predictions + if len(age_predictions.shape) == 1: + # Single face case - reshape predictions to 2D array for consistent handling + age_predictions = age_predictions.reshape(1, -1) + for idx, age in enumerate(age_predictions): - resp_objects[idx]["age"] = int(age) + resp_objects[idx]["age"] = np.argmax(age) elif action == "gender": # Build the gender model model = modeling.build_model(task="facial_attribute", model_name="Gender") gender_predictions = model.predict(faces_array) + + # Handle single vs multiple gender predictions + if len(gender_predictions.shape) == 1: + # Single face case - reshape predictions to 2D array for consistent handling + gender_predictions = gender_predictions.reshape(1, -1) + # Process predictions for each face for idx, predictions in enumerate(gender_predictions): resp_objects[idx]["gender"] = {} @@ -219,7 +238,12 @@ def analyze( # Build the race model model = modeling.build_model(task="facial_attribute", model_name="Race") race_predictions = model.predict(faces_array) - + + # Handle single vs multiple race predictions + if len(race_predictions.shape) == 1: + # Single face case - reshape predictions to 2D array for consistent handling + race_predictions = race_predictions.reshape(1, -1) + for idx, predictions in enumerate(race_predictions): sum_of_predictions = predictions.sum() resp_objects[idx]["race"] = {} From 041773232fc59c150995ad80ed4fbea56d456c58 Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 05:26:07 +0800 Subject: [PATCH 36/55] [fix] model input size -> (n, w, h, c) --- deepface/models/demography/Emotion.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py index caf862b..66d09cc 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -73,7 +73,10 @@ class EmotionClient(Demography): imgs = self._preprocess_batch_or_single_input(img) # Preprocess each image and add channel dimension for grayscale images - processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=0) + processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=-1) + + # Reshape input for model (expected shape=(n, 48, 48, 1)), where n is the batch size + processed_imgs = processed_imgs.reshape(processed_imgs.shape[0], 48, 48, 1) # Prediction # Emotion model input shape is (48, 48, 1, n), where n is the batch size From c44af00269678748b4b68656454a817d39cc0d4f Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 05:26:27 +0800 Subject: [PATCH 37/55] [fix] check for input number of faces --- deepface/modules/demography.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 36b8305..447d146 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -192,7 +192,7 @@ def analyze( for idx, predictions in enumerate(emotion_predictions): sum_of_predictions = predictions.sum() resp_objects[idx]["emotion"] = {} - + # Calculate emotion probabilities and store in response for i, emotion_label in enumerate(Emotion.labels): emotion_probability = 100 * predictions[i] / sum_of_predictions @@ -205,14 +205,16 @@ def analyze( # Build the age model model = modeling.build_model(task="facial_attribute", model_name="Age") age_predictions = model.predict(faces_array) - - # Handle single vs multiple age predictions - if len(age_predictions.shape) == 1: - # Single face case - reshape predictions to 2D array for consistent handling - age_predictions = age_predictions.reshape(1, -1) - for idx, age in enumerate(age_predictions): - resp_objects[idx]["age"] = np.argmax(age) + # Handle single vs multiple age predictions + if faces_array.shape[0] == 1: + # Single face case - reshape predictions to 2D array for consistent handling + resp_objects[idx]["age"] = int(np.argmax(age_predictions)) + else: + # Multiple face case - iterate over each prediction + for idx, age in enumerate(age_predictions): + resp_objects[idx]["age"] = int(age) + elif action == "gender": # Build the gender model From ad577b42063b236268d9e5f8670e8117342b8a72 Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 05:33:47 +0800 Subject: [PATCH 38/55] [update] refactor response object creation in analyze function --- deepface/modules/demography.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 447d146..0c52eb0 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -162,10 +162,8 @@ def analyze( # Convert the list of valid faces to a numpy array faces_array = np.array(valid_faces) - # Create placeholder response objects for each face - for _ in range(len(valid_faces)): - resp_objects.append({}) - + # Preprocess the result to a list of dictionaries + resp_objects = [] # For each action, predict the corresponding attribute pbar = tqdm( @@ -177,6 +175,7 @@ def analyze( for index in pbar: action = actions[index] pbar.set_description(f"Action: {action}") + resp_object = {} if action == "emotion": # Build the emotion model @@ -191,15 +190,15 @@ def analyze( # Process predictions for each face for idx, predictions in enumerate(emotion_predictions): sum_of_predictions = predictions.sum() - resp_objects[idx]["emotion"] = {} + resp_object["emotion"] = {} # Calculate emotion probabilities and store in response for i, emotion_label in enumerate(Emotion.labels): emotion_probability = 100 * predictions[i] / sum_of_predictions - resp_objects[idx]["emotion"][emotion_label] = emotion_probability + resp_object["emotion"][emotion_label] = emotion_probability # Store dominant emotion - resp_objects[idx]["dominant_emotion"] = Emotion.labels[np.argmax(predictions)] + resp_object["dominant_emotion"] = Emotion.labels[np.argmax(predictions)] elif action == "age": # Build the age model @@ -209,11 +208,11 @@ def analyze( # Handle single vs multiple age predictions if faces_array.shape[0] == 1: # Single face case - reshape predictions to 2D array for consistent handling - resp_objects[idx]["age"] = int(np.argmax(age_predictions)) + resp_object["age"] = int(np.argmax(age_predictions)) else: # Multiple face case - iterate over each prediction for idx, age in enumerate(age_predictions): - resp_objects[idx]["age"] = int(age) + resp_object["age"] = int(age) elif action == "gender": @@ -228,13 +227,13 @@ def analyze( # Process predictions for each face for idx, predictions in enumerate(gender_predictions): - resp_objects[idx]["gender"] = {} + resp_object["gender"] = {} for i, gender_label in enumerate(Gender.labels): gender_prediction = 100 * predictions[i] - resp_objects[idx]["gender"][gender_label] = gender_prediction + resp_object["gender"][gender_label] = gender_prediction - resp_objects[idx]["dominant_gender"] = Gender.labels[np.argmax(predictions)] + resp_object["dominant_gender"] = Gender.labels[np.argmax(predictions)] elif action == "race": # Build the race model @@ -248,13 +247,16 @@ def analyze( for idx, predictions in enumerate(race_predictions): sum_of_predictions = predictions.sum() - resp_objects[idx]["race"] = {} + resp_object["race"] = {} for i, race_label in enumerate(Race.labels): race_prediction = 100 * predictions[i] / sum_of_predictions - resp_objects[idx]["race"][race_label] = race_prediction + resp_object["race"][race_label] = race_prediction - resp_objects[idx]["dominant_race"] = Race.labels[np.argmax(predictions)] + resp_object["dominant_race"] = Race.labels[np.argmax(predictions)] + + # Add the response object to the list of response objects + resp_objects.append(resp_object) # Add the face region and confidence to the response objects for idx, resp_obj in enumerate(resp_objects): From 52a38ba21a9b7edb331e2a6f25e68115ca1a663c Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 05:54:44 +0800 Subject: [PATCH 39/55] [fix] use prediction shape to avoid confuse situation of predictions --- deepface/modules/demography.py | 196 ++++++++++++++------------------- 1 file changed, 81 insertions(+), 115 deletions(-) diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 0c52eb0..9789007 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -1,5 +1,5 @@ # built-in dependencies -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Union, Optional # 3rd party dependencies import numpy as np @@ -117,8 +117,6 @@ def analyze( f"Invalid action passed ({repr(action)})). " "Valid actions are `emotion`, `age`, `gender`, `race`." ) - # --------------------------------- - resp_objects = [] img_objs = detection.extract_faces( img_path=img_path, @@ -130,137 +128,105 @@ def analyze( anti_spoofing=anti_spoofing, ) - # Anti-spoofing check if anti_spoofing and any(img_obj.get("is_real", True) is False for img_obj in img_objs): raise ValueError("Spoof detected in the given image.") - # Prepare the input for the model - valid_faces = [] - face_regions = [] - face_confidences = [] - - for img_obj in img_objs: - # Extract the face content + def preprocess_face(img_obj: Dict[str, Any]) -> Optional[np.ndarray]: + """ + Preprocess the face image for analysis. + """ img_content = img_obj["face"] - # Check if the face content is empty if img_content.shape[0] == 0 or img_content.shape[1] == 0: - continue + return None + img_content = img_content[:, :, ::-1] # BGR to RGB + return preprocessing.resize_image(img=img_content, target_size=(224, 224)) - # Convert the image to RGB format from BGR - img_content = img_content[:, :, ::-1] - # Resize the image to the target size for the model - img_content = preprocessing.resize_image(img=img_content, target_size=(224, 224)) - - valid_faces.append(img_content) - face_regions.append(img_obj["facial_area"]) - face_confidences.append(img_obj["confidence"]) - - # If no valid faces are found, return an empty list - if not valid_faces: + # Filter out empty faces + face_data = [(preprocess_face(img_obj), img_obj["facial_area"], img_obj["confidence"]) + for img_obj in img_objs if img_obj["face"].size > 0] + + if not face_data: return [] - # Convert the list of valid faces to a numpy array + # Unpack the face data + valid_faces, face_regions, face_confidences = zip(*face_data) faces_array = np.array(valid_faces) - # Preprocess the result to a list of dictionaries - resp_objects = [] + # Initialize the results list with face regions and confidence scores + results = [{"region": region, "face_confidence": conf} + for region, conf in zip(face_regions, face_confidences)] - # For each action, predict the corresponding attribute + # Iterate over the actions and perform analysis pbar = tqdm( - range(0, len(actions)), + actions, desc="Finding actions", disable=silent if len(actions) > 1 else True, ) - - for index in pbar: - action = actions[index] + + for action in pbar: pbar.set_description(f"Action: {action}") - resp_object = {} + model = modeling.build_model(task="facial_attribute", model_name=action.capitalize()) + predictions = model.predict(faces_array) + # If the model returns a single prediction, reshape it to match the number of faces + # Use number of faces and number of predictions shape to determine the correct shape of predictions + # For example, if there are 1 face to predict with Emotion model, reshape predictions to (1, 7) + if faces_array.shape[0] == 1 and len(predictions.shape) == 1: + # For models like `Emotion`, which return a single prediction for a single face + predictions = predictions.reshape(1, -1) + + # Update the results with the predictions + # ---------------------------------------- + # For emotion, calculate the percentage of each emotion and find the dominant emotion if action == "emotion": - # Build the emotion model - model = modeling.build_model(task="facial_attribute", model_name="Emotion") - emotion_predictions = model.predict(faces_array) - - # Handle single vs multiple emotion predictions - if len(emotion_predictions.shape) == 1: - # Single face case - reshape predictions to 2D array for consistent handling - emotion_predictions = emotion_predictions.reshape(1, -1) - - # Process predictions for each face - for idx, predictions in enumerate(emotion_predictions): - sum_of_predictions = predictions.sum() - resp_object["emotion"] = {} - - # Calculate emotion probabilities and store in response - for i, emotion_label in enumerate(Emotion.labels): - emotion_probability = 100 * predictions[i] / sum_of_predictions - resp_object["emotion"][emotion_label] = emotion_probability - - # Store dominant emotion - resp_object["dominant_emotion"] = Emotion.labels[np.argmax(predictions)] - + emotion_results = [ + { + "emotion": { + label: 100 * pred[i] / pred.sum() + for i, label in enumerate(Emotion.labels) + }, + "dominant_emotion": Emotion.labels[np.argmax(pred)] + } + for pred in predictions + ] + for result, emotion_result in zip(results, emotion_results): + result.update(emotion_result) + # ---------------------------------------- + # For age, find the dominant age category (0-100) elif action == "age": - # Build the age model - model = modeling.build_model(task="facial_attribute", model_name="Age") - age_predictions = model.predict(faces_array) - - # Handle single vs multiple age predictions - if faces_array.shape[0] == 1: - # Single face case - reshape predictions to 2D array for consistent handling - resp_object["age"] = int(np.argmax(age_predictions)) - else: - # Multiple face case - iterate over each prediction - for idx, age in enumerate(age_predictions): - resp_object["age"] = int(age) - - + age_results = [{"age": int(np.argmax(pred) if len(pred.shape) > 0 else pred)} + for pred in predictions] + for result, age_result in zip(results, age_results): + result.update(age_result) + # ---------------------------------------- + # For gender, calculate the percentage of each gender and find the dominant gender elif action == "gender": - # Build the gender model - model = modeling.build_model(task="facial_attribute", model_name="Gender") - gender_predictions = model.predict(faces_array) - - # Handle single vs multiple gender predictions - if len(gender_predictions.shape) == 1: - # Single face case - reshape predictions to 2D array for consistent handling - gender_predictions = gender_predictions.reshape(1, -1) - - # Process predictions for each face - for idx, predictions in enumerate(gender_predictions): - resp_object["gender"] = {} - - for i, gender_label in enumerate(Gender.labels): - gender_prediction = 100 * predictions[i] - resp_object["gender"][gender_label] = gender_prediction - - resp_object["dominant_gender"] = Gender.labels[np.argmax(predictions)] - + gender_results = [ + { + "gender": { + label: 100 * pred[i] + for i, label in enumerate(Gender.labels) + }, + "dominant_gender": Gender.labels[np.argmax(pred)] + } + for pred in predictions + ] + for result, gender_result in zip(results, gender_results): + result.update(gender_result) + # ---------------------------------------- + # For race, calculate the percentage of each race and find the dominant race elif action == "race": - # Build the race model - model = modeling.build_model(task="facial_attribute", model_name="Race") - race_predictions = model.predict(faces_array) + race_results = [ + { + "race": { + label: 100 * pred[i] / pred.sum() + for i, label in enumerate(Race.labels) + }, + "dominant_race": Race.labels[np.argmax(pred)] + } + for pred in predictions + ] + for result, race_result in zip(results, race_results): + result.update(race_result) - # Handle single vs multiple race predictions - if len(race_predictions.shape) == 1: - # Single face case - reshape predictions to 2D array for consistent handling - race_predictions = race_predictions.reshape(1, -1) - - for idx, predictions in enumerate(race_predictions): - sum_of_predictions = predictions.sum() - resp_object["race"] = {} - - for i, race_label in enumerate(Race.labels): - race_prediction = 100 * predictions[i] / sum_of_predictions - resp_object["race"][race_label] = race_prediction - - resp_object["dominant_race"] = Race.labels[np.argmax(predictions)] - - # Add the response object to the list of response objects - resp_objects.append(resp_object) - - # Add the face region and confidence to the response objects - for idx, resp_obj in enumerate(resp_objects): - resp_obj["region"] = face_regions[idx] - resp_obj["face_confidence"] = face_confidences[idx] - - return resp_objects + return results \ No newline at end of file From ba8c651c7a16f98db983d84e10959811b17e29bf Mon Sep 17 00:00:00 2001 From: NatLee Date: Tue, 7 Jan 2025 05:56:09 +0800 Subject: [PATCH 40/55] [fix] 1 img input for the `Emotion` model --- deepface/models/demography/Emotion.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/deepface/models/demography/Emotion.py b/deepface/models/demography/Emotion.py index 66d09cc..ac2af29 100644 --- a/deepface/models/demography/Emotion.py +++ b/deepface/models/demography/Emotion.py @@ -72,14 +72,16 @@ class EmotionClient(Demography): # Preprocessing input image or image list. imgs = self._preprocess_batch_or_single_input(img) - # Preprocess each image and add channel dimension for grayscale images - processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=-1) - - # Reshape input for model (expected shape=(n, 48, 48, 1)), where n is the batch size - processed_imgs = processed_imgs.reshape(processed_imgs.shape[0], 48, 48, 1) + if imgs.shape[0] == 1: + # Preprocess single image and add channel dimension for grayscale images + processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=0) + else: + # Preprocess batch of images and add channel dimension for grayscale images + processed_imgs = np.expand_dims(np.array([self._preprocess_image(img) for img in imgs]), axis=-1) + # Reshape input for model (expected shape=(n, 48, 48, 1)), where n is the batch size + processed_imgs = processed_imgs.reshape(processed_imgs.shape[0], 48, 48, 1) # Prediction - # Emotion model input shape is (48, 48, 1, n), where n is the batch size predictions = self._predict_internal(processed_imgs) return predictions From 4284252a265823f05ab0ae5dfcf84379d838497a Mon Sep 17 00:00:00 2001 From: h-alice Date: Tue, 7 Jan 2025 11:19:35 +0800 Subject: [PATCH 41/55] Remove obsolete comment. --- deepface/models/Demography.py | 1 - 1 file changed, 1 deletion(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index 7bba87f..911032d 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -51,7 +51,6 @@ class Demography(ABC): img: Single image as np.ndarray (224, 224, 3) or List of images as List[np.ndarray] or Batch of images as np.ndarray (n, 224, 224, 3) - NOTE: If the imput is grayscale, then there's no channel dimension. Returns: Four-dimensional numpy array (n, 224, 224, 3) """ From cde5236fc9605b3ec765da4ceb580a89ae2f0619 Mon Sep 17 00:00:00 2001 From: Gustav Date: Tue, 7 Jan 2025 13:51:16 +0100 Subject: [PATCH 42/55] fix small spelling error --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f351c20..d4aae8b 100644 --- a/README.md +++ b/README.md @@ -423,7 +423,7 @@ Additionally, you can help us reach a wider audience by upvoting our posts on Ha Please cite deepface in your publications if it helps your research - see [`CITATIONS`](https://github.com/serengil/deepface/blob/master/CITATION.md) for more details. Here are its BibTex entries: -If you use deepface in your research for facial recogntion or face detection purposes, please cite these publications: +If you use deepface in your research for facial recognition or face detection purposes, please cite these publications: ```BibTeX @article{serengil2024lightface, From 83031a427d29322d9476e939c45bca7f6d303724 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Tue, 7 Jan 2025 10:25:40 -0500 Subject: [PATCH 43/55] image_exts and pil_exts are now global variables and are now named as IMAGE_EXTS and PIL_EXTS to match Python naming conventions. --- deepface/commons/image_utils.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/deepface/commons/image_utils.py b/deepface/commons/image_utils.py index b984f91..868eaf2 100644 --- a/deepface/commons/image_utils.py +++ b/deepface/commons/image_utils.py @@ -14,6 +14,10 @@ from PIL import Image from werkzeug.datastructures import FileStorage +IMAGE_EXTS = {".jpg", ".jpeg", ".png"} +PIL_EXTS = {"jpeg", "png"} + + def list_images(path: str) -> List[str]: """ List images in a given path @@ -23,14 +27,12 @@ def list_images(path: str) -> List[str]: images (list): list of exact image paths """ images = [] - image_exts = {".jpg", ".jpeg", ".png"} - pil_exts = {"jpeg", "png"} for r, _, f in os.walk(path): for file in f: - if os.path.splitext(file)[1].lower() in image_exts: + if os.path.splitext(file)[1].lower() in IMAGE_EXTS: exact_path = os.path.join(r, file) with Image.open(exact_path) as img: # lazy - if img.format.lower() in pil_exts: + if img.format.lower() in PIL_EXTS: images.append(exact_path) return images @@ -43,14 +45,12 @@ def yield_images(path: str) -> Generator[str, None, None]: Yields: image (str): image path """ - image_exts = {".jpg", ".jpeg", ".png"} - pil_exts = {"jpeg", "png"} for r, _, f in os.walk(path): for file in f: - if os.path.splitext(file)[1].lower() in image_exts: + if os.path.splitext(file)[1].lower() in IMAGE_EXTS: exact_path = os.path.join(r, file) with Image.open(exact_path) as img: # lazy - if img.format.lower() in pil_exts: + if img.format.lower() in PIL_EXTS: yield exact_path From 94f336b95f586cfb18eb6658688dde31dbc2fcf8 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Wed, 8 Jan 2025 16:49:28 +0000 Subject: [PATCH 44/55] Create CODE_OF_CONDUCT.md --- .github/CODE_OF_CONDUCT.md | 127 +++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 .github/CODE_OF_CONDUCT.md diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a98cd15 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,127 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at serengil@gmail.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. From 4f0fa6ee22170ced65d78fd1e7ee35b756408694 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Thu, 9 Jan 2025 10:35:47 -0500 Subject: [PATCH 45/55] load_image now accepts file objects that support being read --- deepface/commons/image_utils.py | 40 +++++++++++++++++++++++++++++---- tests/test_represent.py | 16 +++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/deepface/commons/image_utils.py b/deepface/commons/image_utils.py index 868eaf2..10d177f 100644 --- a/deepface/commons/image_utils.py +++ b/deepface/commons/image_utils.py @@ -1,7 +1,7 @@ # built-in dependencies import os import io -from typing import Generator, List, Union, Tuple +from typing import IO, Generator, List, Union, Tuple import hashlib import base64 from pathlib import Path @@ -77,11 +77,11 @@ def find_image_hash(file_path: str) -> str: return hasher.hexdigest() -def load_image(img: Union[str, np.ndarray]) -> Tuple[np.ndarray, str]: +def load_image(img: Union[str, np.ndarray, IO[bytes]]) -> Tuple[np.ndarray, str]: """ - Load image from path, url, base64 or numpy array. + Load image from path, url, file object, base64 or numpy array. Args: - img: a path, url, base64 or numpy array. + img: a path, url, file object, base64 or numpy array. Returns: image (numpy array): the loaded image in BGR format image name (str): image name itself @@ -91,6 +91,14 @@ def load_image(img: Union[str, np.ndarray]) -> Tuple[np.ndarray, str]: if isinstance(img, np.ndarray): return img, "numpy array" + # The image is an object that supports `.read` + if hasattr(img, 'read') and callable(img.read): + if isinstance(img, io.StringIO): + raise ValueError( + 'img requires bytes and cannot be an io.StringIO object.' + ) + return load_image_from_io_object(img), 'io object' + if isinstance(img, Path): img = str(img) @@ -120,6 +128,30 @@ def load_image(img: Union[str, np.ndarray]) -> Tuple[np.ndarray, str]: return img_obj_bgr, img +def load_image_from_io_object(obj: IO[bytes]) -> np.ndarray: + """ + Load image from an object that supports being read + Args: + obj: a file like object. + Returns: + img (np.ndarray): The decoded image as a numpy array (OpenCV format). + """ + try: + _ = obj.seek(0) + except (AttributeError, TypeError, io.UnsupportedOperation): + seekable = False + obj = io.BytesIO(obj.read()) + else: + seekable = True + try: + nparr = np.frombuffer(obj.read(), np.uint8) + img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + return img + finally: + if not seekable: + obj.close() + + def load_image_from_base64(uri: str) -> np.ndarray: """ Load image from base64 string. diff --git a/tests/test_represent.py b/tests/test_represent.py index 085dff2..91a24b6 100644 --- a/tests/test_represent.py +++ b/tests/test_represent.py @@ -1,4 +1,5 @@ # built-in dependencies +import io import cv2 # project dependencies @@ -18,6 +19,21 @@ def test_standard_represent(): logger.info("✅ test standard represent function done") +def test_standard_represent_with_io_object(): + img_path = "dataset/img1.jpg" + defualt_embedding_objs = DeepFace.represent(img_path) + io_embedding_objs = DeepFace.represent(open(img_path, 'rb')) + assert defualt_embedding_objs == io_embedding_objs + + # Confirm non-seekable io objects are handled properly + io_obj = io.BytesIO(open(img_path, 'rb').read()) + io_obj.seek = None + no_seek_io_embedding_objs = DeepFace.represent(io_obj) + assert defualt_embedding_objs == no_seek_io_embedding_objs + + logger.info("✅ test standard represent with io object function done") + + def test_represent_for_skipped_detector_backend_with_image_path(): face_img = "dataset/img5.jpg" img_objs = DeepFace.represent(img_path=face_img, detector_backend="skip") From f9af73c1c740f6ef206a8dd7042844e9ac6ba4e9 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Thu, 9 Jan 2025 11:51:31 -0500 Subject: [PATCH 46/55] defualt --> default --- tests/test_represent.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_represent.py b/tests/test_represent.py index 91a24b6..d0a215d 100644 --- a/tests/test_represent.py +++ b/tests/test_represent.py @@ -21,15 +21,15 @@ def test_standard_represent(): def test_standard_represent_with_io_object(): img_path = "dataset/img1.jpg" - defualt_embedding_objs = DeepFace.represent(img_path) + default_embedding_objs = DeepFace.represent(img_path) io_embedding_objs = DeepFace.represent(open(img_path, 'rb')) - assert defualt_embedding_objs == io_embedding_objs + assert default_embedding_objs == io_embedding_objs # Confirm non-seekable io objects are handled properly io_obj = io.BytesIO(open(img_path, 'rb').read()) io_obj.seek = None no_seek_io_embedding_objs = DeepFace.represent(io_obj) - assert defualt_embedding_objs == no_seek_io_embedding_objs + assert default_embedding_objs == no_seek_io_embedding_objs logger.info("✅ test standard represent with io object function done") From 242bd3eb84bb3838b7d2a65d022cb9ca505b6552 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Fri, 10 Jan 2025 09:14:15 -0500 Subject: [PATCH 47/55] failure to decode an io object as an image raises an exception --- deepface/commons/image_utils.py | 2 ++ tests/test_represent.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/deepface/commons/image_utils.py b/deepface/commons/image_utils.py index 10d177f..9c7a21f 100644 --- a/deepface/commons/image_utils.py +++ b/deepface/commons/image_utils.py @@ -146,6 +146,8 @@ def load_image_from_io_object(obj: IO[bytes]) -> np.ndarray: try: nparr = np.frombuffer(obj.read(), np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + if img is None: + raise ValueError("Failed to decode image") return img finally: if not seekable: diff --git a/tests/test_represent.py b/tests/test_represent.py index d0a215d..1b76dfa 100644 --- a/tests/test_represent.py +++ b/tests/test_represent.py @@ -1,6 +1,7 @@ # built-in dependencies import io import cv2 +import pytest # project dependencies from deepface import DeepFace @@ -31,6 +32,10 @@ def test_standard_represent_with_io_object(): no_seek_io_embedding_objs = DeepFace.represent(io_obj) assert default_embedding_objs == no_seek_io_embedding_objs + # Confirm non-image io objects raise exceptions + with pytest.raises(ValueError, match='Failed to decode image'): + DeepFace.represent(io.BytesIO(open(__file__, 'rb').read())) + logger.info("✅ test standard represent with io object function done") From 8c5a23536d1a93addab81dc187a3738736d1e0e4 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Fri, 10 Jan 2025 10:40:02 -0500 Subject: [PATCH 48/55] use requirements.txt for testing non-image io objects --- tests/test_represent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_represent.py b/tests/test_represent.py index 1b76dfa..b33def7 100644 --- a/tests/test_represent.py +++ b/tests/test_represent.py @@ -34,7 +34,7 @@ def test_standard_represent_with_io_object(): # Confirm non-image io objects raise exceptions with pytest.raises(ValueError, match='Failed to decode image'): - DeepFace.represent(io.BytesIO(open(__file__, 'rb').read())) + DeepFace.represent(io.BytesIO(open(r'../requirements.txt', 'rb').read())) logger.info("✅ test standard represent with io object function done") From 39173b748bdd9cf335ee02cf77018cfcaccab7f4 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Fri, 10 Jan 2025 11:05:43 -0500 Subject: [PATCH 49/55] adding IO[bytes] types to functions that now accept io objects --- deepface/DeepFace.py | 14 +++++++------- deepface/modules/detection.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index f8930e5..38e9ca7 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -2,7 +2,7 @@ import os import warnings import logging -from typing import Any, Dict, List, Union, Optional +from typing import Any, Dict, IO, List, Union, Optional # this has to be set before importing tensorflow os.environ["TF_USE_LEGACY_KERAS"] = "1" @@ -68,8 +68,8 @@ def build_model(model_name: str, task: str = "facial_recognition") -> Any: def verify( - img1_path: Union[str, np.ndarray, List[float]], - img2_path: Union[str, np.ndarray, List[float]], + img1_path: Union[str, np.ndarray, IO[bytes], List[float]], + img2_path: Union[str, np.ndarray, IO[bytes], List[float]], model_name: str = "VGG-Face", detector_backend: str = "opencv", distance_metric: str = "cosine", @@ -164,7 +164,7 @@ def verify( def analyze( - img_path: Union[str, np.ndarray], + img_path: Union[str, np.ndarray, IO[bytes]], actions: Union[tuple, list] = ("emotion", "age", "gender", "race"), enforce_detection: bool = True, detector_backend: str = "opencv", @@ -263,7 +263,7 @@ def analyze( def find( - img_path: Union[str, np.ndarray], + img_path: Union[str, np.ndarray, IO[bytes]], db_path: str, model_name: str = "VGG-Face", distance_metric: str = "cosine", @@ -369,7 +369,7 @@ def find( def represent( - img_path: Union[str, np.ndarray], + img_path: Union[str, np.ndarray, IO[bytes]], model_name: str = "VGG-Face", enforce_detection: bool = True, detector_backend: str = "opencv", @@ -505,7 +505,7 @@ def stream( def extract_faces( - img_path: Union[str, np.ndarray], + img_path: Union[str, np.ndarray, IO[bytes]], detector_backend: str = "opencv", enforce_detection: bool = True, align: bool = True, diff --git a/deepface/modules/detection.py b/deepface/modules/detection.py index 221e1d2..ae3fa61 100644 --- a/deepface/modules/detection.py +++ b/deepface/modules/detection.py @@ -1,5 +1,5 @@ # built-in dependencies -from typing import Any, Dict, List, Tuple, Union, Optional +from typing import Any, Dict, IO, List, Tuple, Union, Optional # 3rd part dependencies from heapq import nlargest @@ -19,7 +19,7 @@ logger = Logger() def extract_faces( - img_path: Union[str, np.ndarray], + img_path: Union[str, np.ndarray, IO[bytes]], detector_backend: str = "opencv", enforce_detection: bool = True, align: bool = True, From f3da544812230971db5ff26697783b5657cf2046 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Fri, 10 Jan 2025 11:21:01 -0500 Subject: [PATCH 50/55] updated docstrings for fucntions that now accept IO[bytes] --- deepface/DeepFace.py | 12 ++++++------ deepface/modules/detection.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index 38e9ca7..b42c40d 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -84,11 +84,11 @@ def verify( """ Verify if an image pair represents the same person or different persons. Args: - img1_path (str or np.ndarray or List[float]): Path to the first image. + img1_path (str or np.ndarray or IO[bytes] or List[float]): Path to the first image. Accepts exact image path as a string, numpy array (BGR), base64 encoded images or pre-calculated embeddings. - img2_path (str or np.ndarray or List[float]): Path to the second image. + img2_path (str or np.ndarray or IO[bytes] or List[float]): Path to the second image. Accepts exact image path as a string, numpy array (BGR), base64 encoded images or pre-calculated embeddings. @@ -176,7 +176,7 @@ def analyze( """ Analyze facial attributes such as age, gender, emotion, and race in the provided image. Args: - img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format, + img_path (str or np.ndarray or IO[bytes]): The exact path to the image, a numpy array in BGR format, or a base64 encoded image. If the source image contains multiple faces, the result will include information for each detected face. @@ -281,7 +281,7 @@ def find( """ Identify individuals in a database Args: - img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format, + img_path (str or np.ndarray or IO[bytes]): The exact path to the image, a numpy array in BGR format, or a base64 encoded image. If the source image contains multiple faces, the result will include information for each detected face. @@ -383,7 +383,7 @@ def represent( Represent facial images as multi-dimensional vector embeddings. Args: - img_path (str or np.ndarray): The exact path to the image, a numpy array in BGR format, + img_path (str or np.ndarray or IO[bytes]): The exact path to the image, a numpy array in BGR format, or a base64 encoded image. If the source image contains multiple faces, the result will include information for each detected face. @@ -519,7 +519,7 @@ def extract_faces( Extract faces from a given image Args: - img_path (str or np.ndarray): Path to the first image. Accepts exact image path + img_path (str or np.ndarray or IO[bytes]): Path to the first image. Accepts exact image path as a string, numpy array (BGR), or base64 encoded images. detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', diff --git a/deepface/modules/detection.py b/deepface/modules/detection.py index ae3fa61..907785a 100644 --- a/deepface/modules/detection.py +++ b/deepface/modules/detection.py @@ -34,7 +34,7 @@ def extract_faces( Extract faces from a given image Args: - img_path (str or np.ndarray): Path to the first image. Accepts exact image path + img_path (str or np.ndarray or IO[bytes]): Path to the first image. Accepts exact image path as a string, numpy array (BGR), or base64 encoded images. detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', From 7112766966132163cdb8905ac73fcdf5c3127096 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Fri, 10 Jan 2025 11:26:16 -0500 Subject: [PATCH 51/55] correct import ordering to be alphabetized --- deepface/commons/image_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepface/commons/image_utils.py b/deepface/commons/image_utils.py index 9c7a21f..50f2196 100644 --- a/deepface/commons/image_utils.py +++ b/deepface/commons/image_utils.py @@ -1,7 +1,7 @@ # built-in dependencies import os import io -from typing import IO, Generator, List, Union, Tuple +from typing import Generator, IO, List, Union, Tuple import hashlib import base64 from pathlib import Path From 86fa2dfa83b25f0dcec943ffa70b3959f4818751 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Fri, 10 Jan 2025 11:46:28 -0500 Subject: [PATCH 52/55] updating docstrings to appease linter --- deepface/DeepFace.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index b42c40d..58547fa 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -176,9 +176,9 @@ def analyze( """ Analyze facial attributes such as age, gender, emotion, and race in the provided image. Args: - img_path (str or np.ndarray or IO[bytes]): The exact path to the image, a numpy array in BGR format, - or a base64 encoded image. If the source image contains multiple faces, the result will - include information for each detected face. + img_path (str or np.ndarray or IO[bytes]): The exact path to the image, a numpy array + in BGR format, or a base64 encoded image. If the source image contains multiple faces, + the result will include information for each detected face. actions (tuple): Attributes to analyze. The default is ('age', 'gender', 'emotion', 'race'). You can exclude some of these attributes from the analysis if needed. @@ -281,9 +281,9 @@ def find( """ Identify individuals in a database Args: - img_path (str or np.ndarray or IO[bytes]): The exact path to the image, a numpy array in BGR format, - or a base64 encoded image. If the source image contains multiple faces, the result will - include information for each detected face. + img_path (str or np.ndarray or IO[bytes]): The exact path to the image, a numpy array + in BGR format, or a base64 encoded image. If the source image contains multiple + faces, the result will include information for each detected face. db_path (string): Path to the folder containing image files. All detected faces in the database will be considered in the decision-making process. @@ -383,9 +383,9 @@ def represent( Represent facial images as multi-dimensional vector embeddings. Args: - img_path (str or np.ndarray or IO[bytes]): The exact path to the image, a numpy array in BGR format, - or a base64 encoded image. If the source image contains multiple faces, the result will - include information for each detected face. + img_path (str or np.ndarray or IO[bytes]): The exact path to the image, a numpy array + in BGR format, or a base64 encoded image. If the source image contains multiple faces, + the result will include information for each detected face. model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, OpenFace, DeepFace, DeepID, Dlib, ArcFace, SFace and GhostFaceNet From e4cba05a10624b57cac9439769fa33a1a268c20f Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Fri, 10 Jan 2025 12:04:01 -0500 Subject: [PATCH 53/55] updated doctstring descriptions for functions that accept IO[bytes] file objects --- deepface/DeepFace.py | 18 ++++++++++++------ deepface/modules/detection.py | 3 ++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index 58547fa..3abe6db 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -85,11 +85,13 @@ def verify( Verify if an image pair represents the same person or different persons. Args: img1_path (str or np.ndarray or IO[bytes] or List[float]): Path to the first image. - Accepts exact image path as a string, numpy array (BGR), base64 encoded images + Accepts exact image path as a string, numpy array (BGR), a file object that supports + at least `.read` and is opened in binary mode, base64 encoded images or pre-calculated embeddings. img2_path (str or np.ndarray or IO[bytes] or List[float]): Path to the second image. - Accepts exact image path as a string, numpy array (BGR), base64 encoded images + Accepts exact image path as a string, numpy array (BGR), a file object that supports + at least `.read` and is opened in binary mode, base64 encoded images or pre-calculated embeddings. model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, @@ -177,7 +179,8 @@ def analyze( Analyze facial attributes such as age, gender, emotion, and race in the provided image. Args: img_path (str or np.ndarray or IO[bytes]): The exact path to the image, a numpy array - in BGR format, or a base64 encoded image. If the source image contains multiple faces, + in BGR format, a file object that supports at least `.read` and is opened in binary + mode, or a base64 encoded image. If the source image contains multiple faces, the result will include information for each detected face. actions (tuple): Attributes to analyze. The default is ('age', 'gender', 'emotion', 'race'). @@ -282,7 +285,8 @@ def find( Identify individuals in a database Args: img_path (str or np.ndarray or IO[bytes]): The exact path to the image, a numpy array - in BGR format, or a base64 encoded image. If the source image contains multiple + in BGR format, a file object that supports at least `.read` and is opened in binary + mode, or a base64 encoded image. If the source image contains multiple faces, the result will include information for each detected face. db_path (string): Path to the folder containing image files. All detected faces @@ -384,7 +388,8 @@ def represent( Args: img_path (str or np.ndarray or IO[bytes]): The exact path to the image, a numpy array - in BGR format, or a base64 encoded image. If the source image contains multiple faces, + in BGR format, a file object that supports at least `.read` and is opened in binary + mode, or a base64 encoded image. If the source image contains multiple faces, the result will include information for each detected face. model_name (str): Model for face recognition. Options: VGG-Face, Facenet, Facenet512, @@ -520,7 +525,8 @@ def extract_faces( Args: img_path (str or np.ndarray or IO[bytes]): Path to the first image. Accepts exact image path - as a string, numpy array (BGR), or base64 encoded images. + as a string, numpy array (BGR), a file object that supports at least `.read` and is + opened in binary mode, or base64 encoded images. detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'yolov11n', 'yolov11s', 'yolov11m', diff --git a/deepface/modules/detection.py b/deepface/modules/detection.py index 907785a..c31a026 100644 --- a/deepface/modules/detection.py +++ b/deepface/modules/detection.py @@ -35,7 +35,8 @@ def extract_faces( Args: img_path (str or np.ndarray or IO[bytes]): Path to the first image. Accepts exact image path - as a string, numpy array (BGR), or base64 encoded images. + as a string, numpy array (BGR), a file object that supports at least `.read` and is + opened in binary mode, or base64 encoded images. detector_backend (string): face detector backend. Options: 'opencv', 'retinaface', 'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'yolov11n', 'yolov11s', 'yolov11m', From eb7b8411e88e275f6b625ac3f1405f46840fd514 Mon Sep 17 00:00:00 2001 From: h-alice Date: Mon, 13 Jan 2025 17:23:44 +0800 Subject: [PATCH 54/55] Documentation --- deepface/models/Demography.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index 911032d..fb9d106 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -29,19 +29,21 @@ class Demography(ABC): Args: img_batch: Batch of images as np.ndarray (n, x, y, c), with n >= 1, x = image width, y = image height, c = channel + Or Single image as np.ndarray (1, x, y, c), with x = image width, y = image height and c = channel + The channel dimension may be omitted if the image is grayscale. (For emotion model) """ if not self.model_name: # Check if called from derived class - raise NotImplementedError("virtual method must not be called directly") + raise NotImplementedError("no model selected") assert img_batch.ndim == 4, "expected 4-dimensional tensor input" if img_batch.shape[0] == 1: # Single image - if img_batch.shape[-1] != 3: # Check if grayscale + if img_batch.shape[-1] != 3: # Check if grayscale by checking last dimension, if not 3, it is grayscale. img_batch = img_batch.squeeze(0) # Remove batch dimension predict_result = self.model(img_batch, training=False).numpy()[0, :] # Predict with legacy method. return predict_result else: # Batch of images - return self.model.predict_on_batch(img_batch) + return self.model.predict_on_batch(img_batch) # Predict with batch prediction def _preprocess_batch_or_single_input(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: @@ -54,10 +56,8 @@ class Demography(ABC): Returns: Four-dimensional numpy array (n, 224, 224, 3) """ - if isinstance(img, list): # Convert from list to image batch. - image_batch = np.array(img) - else: - image_batch = img + + image_batch = np.array(img) # Remove batch dimension in advance if exists image_batch = image_batch.squeeze() From 688fbe6b902f5a368142284fad03cc6054bb0d9b Mon Sep 17 00:00:00 2001 From: NatLee Date: Mon, 13 Jan 2025 22:27:11 +0800 Subject: [PATCH 55/55] [fix] lint --- deepface/models/Demography.py | 38 +++++++++++++++++++--------------- deepface/modules/demography.py | 8 +++---- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/deepface/models/Demography.py b/deepface/models/Demography.py index fb9d106..329a156 100644 --- a/deepface/models/Demography.py +++ b/deepface/models/Demography.py @@ -28,24 +28,32 @@ class Demography(ABC): And switch to batch prediction if receives batched images. Args: - img_batch: Batch of images as np.ndarray (n, x, y, c), with n >= 1, x = image width, y = image height, c = channel - Or Single image as np.ndarray (1, x, y, c), with x = image width, y = image height and c = channel - The channel dimension may be omitted if the image is grayscale. (For emotion model) + img_batch: + Batch of images as np.ndarray (n, x, y, c) + with n >= 1, x = image width, y = image height, c = channel + Or Single image as np.ndarray (1, x, y, c) + with x = image width, y = image height and c = channel + The channel dimension may be omitted if the image is grayscale. (For emotion model) """ if not self.model_name: # Check if called from derived class raise NotImplementedError("no model selected") - assert img_batch.ndim == 4, "expected 4-dimensional tensor input" + # Single image + if img_batch.shape[0] == 1: + # Check if grayscale by checking last dimension, if not 3, it is grayscale. + if img_batch.shape[-1] != 3: + # Remove batch dimension + img_batch = img_batch.squeeze(0) + # Predict with legacy method. + return self.model(img_batch, training=False).numpy()[0, :] + # Batch of images + # Predict with batch prediction + return self.model.predict_on_batch(img_batch) - if img_batch.shape[0] == 1: # Single image - if img_batch.shape[-1] != 3: # Check if grayscale by checking last dimension, if not 3, it is grayscale. - img_batch = img_batch.squeeze(0) # Remove batch dimension - predict_result = self.model(img_batch, training=False).numpy()[0, :] # Predict with legacy method. - return predict_result - else: # Batch of images - return self.model.predict_on_batch(img_batch) # Predict with batch prediction - - def _preprocess_batch_or_single_input(self, img: Union[np.ndarray, List[np.ndarray]]) -> np.ndarray: + def _preprocess_batch_or_single_input( + self, + img: Union[np.ndarray, List[np.ndarray]] + ) -> np.ndarray: """ Preprocess single or batch of images, return as 4-D numpy array. @@ -56,15 +64,11 @@ class Demography(ABC): Returns: Four-dimensional numpy array (n, 224, 224, 3) """ - image_batch = np.array(img) - # Remove batch dimension in advance if exists image_batch = image_batch.squeeze() - # Check input dimension if len(image_batch.shape) == 3: # Single image - add batch dimension image_batch = np.expand_dims(image_batch, axis=0) - return image_batch diff --git a/deepface/modules/demography.py b/deepface/modules/demography.py index 9789007..c78f73b 100644 --- a/deepface/modules/demography.py +++ b/deepface/modules/demography.py @@ -168,9 +168,9 @@ def analyze( model = modeling.build_model(task="facial_attribute", model_name=action.capitalize()) predictions = model.predict(faces_array) - # If the model returns a single prediction, reshape it to match the number of faces - # Use number of faces and number of predictions shape to determine the correct shape of predictions - # For example, if there are 1 face to predict with Emotion model, reshape predictions to (1, 7) + # If the model returns a single prediction, reshape it to match the number of faces. + # Determine the correct shape of predictions by using number of faces and predictions shape. + # Example: For 1 face with Emotion model, predictions will be reshaped to (1, 7). if faces_array.shape[0] == 1 and len(predictions.shape) == 1: # For models like `Emotion`, which return a single prediction for a single face predictions = predictions.reshape(1, -1) @@ -229,4 +229,4 @@ def analyze( for result, race_result in zip(results, race_results): result.update(race_result) - return results \ No newline at end of file + return results