From ca9ecbb3cab99ecccbc5286595034987214d0c09 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Mon, 6 Jan 2025 09:06:37 -0500 Subject: [PATCH 01/22] 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 02/22] 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 03/22] 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 04/22] 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 05/22] 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 06/22] 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 07/22] 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 08/22] 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 09/22] 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 10/22] 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 cde5236fc9605b3ec765da4ceb580a89ae2f0619 Mon Sep 17 00:00:00 2001 From: Gustav Date: Tue, 7 Jan 2025 13:51:16 +0100 Subject: [PATCH 11/22] 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 12/22] 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 13/22] 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 14/22] 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 15/22] 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 16/22] 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 17/22] 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 18/22] 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 19/22] 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 20/22] 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 21/22] 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 22/22] 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',