From 4f0fa6ee22170ced65d78fd1e7ee35b756408694 Mon Sep 17 00:00:00 2001 From: "Samuel J. Woodward" Date: Thu, 9 Jan 2025 10:35:47 -0500 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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',