From 821cb6b8951365265cbb0933e2392af45d433eb3 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sat, 13 Apr 2024 07:36:43 +0100 Subject: [PATCH 1/5] creating image utils --- deepface/DeepFace.py | 4 +-- deepface/commons/file_utils.py | 55 +++++++++++++++++++++++++++++++ deepface/commons/package_utils.py | 24 -------------- deepface/modules/recognition.py | 34 +++---------------- 4 files changed, 61 insertions(+), 56 deletions(-) create mode 100644 deepface/commons/file_utils.py diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index e833377..f035423 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -10,7 +10,6 @@ os.environ["TF_USE_LEGACY_KERAS"] = "1" # pylint: disable=wrong-import-position # 3rd party dependencies -import cv2 import numpy as np import pandas as pd import tensorflow as tf @@ -26,6 +25,7 @@ from deepface.modules import ( demography, detection, streaming, + preprocessing, ) from deepface import __version__ @@ -548,5 +548,5 @@ def detectFace( extracted_face = None if len(face_objs) > 0: extracted_face = face_objs[0]["face"] - extracted_face = cv2.resize(extracted_face, target_size) + extracted_face = preprocessing.resize_image(img=extracted_face, target_size=target_size) return extracted_face diff --git a/deepface/commons/file_utils.py b/deepface/commons/file_utils.py new file mode 100644 index 0000000..ef53560 --- /dev/null +++ b/deepface/commons/file_utils.py @@ -0,0 +1,55 @@ +# built-in dependencies +import os +from typing import List +import hashlib + +# 3rd party dependencies +from PIL import Image + + +def list_images(path: str) -> List[str]: + """ + List images in a given path + Args: + path (str): path's location + Returns: + images (list): list of exact image paths + """ + images = [] + for r, _, f in os.walk(path): + for file in f: + exact_path = os.path.join(r, file) + + _, ext = os.path.splitext(exact_path) + ext_lower = ext.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) + return images + + +def find_hash_of_file(file_path: str) -> str: + """ + Find the hash of given image file with its properties + finding the hash of image content is costly operation + Args: + file_path (str): exact image path + Returns: + hash (str): digest with sha1 algorithm + """ + file_stats = os.stat(file_path) + + # some properties + file_size = file_stats.st_size + creation_time = file_stats.st_ctime + modification_time = file_stats.st_mtime + + properties = f"{file_size}-{creation_time}-{modification_time}" + + hasher = hashlib.sha1() + hasher.update(properties.encode("utf-8")) + return hasher.hexdigest() diff --git a/deepface/commons/package_utils.py b/deepface/commons/package_utils.py index 3d68235..8f49bac 100644 --- a/deepface/commons/package_utils.py +++ b/deepface/commons/package_utils.py @@ -1,6 +1,5 @@ # built-in dependencies import os -import hashlib # 3rd party dependencies import tensorflow as tf @@ -29,29 +28,6 @@ def get_tf_minor_version() -> int: return int(tf.__version__.split(".", maxsplit=-1)[1]) -def find_hash_of_file(file_path: str) -> str: - """ - Find the hash of given image file with its properties - finding the hash of image content is costly operation - Args: - file_path (str): exact image path - Returns: - hash (str): digest with sha1 algorithm - """ - file_stats = os.stat(file_path) - - # some properties - file_size = file_stats.st_size - creation_time = file_stats.st_ctime - modification_time = file_stats.st_mtime - - properties = f"{file_size}-{creation_time}-{modification_time}" - - hasher = hashlib.sha1() - hasher.update(properties.encode("utf-8")) - return hasher.hexdigest() - - def validate_for_keras3(): tf_major = get_tf_major_version() tf_minor = get_tf_minor_version() diff --git a/deepface/modules/recognition.py b/deepface/modules/recognition.py index 03541c2..ba65791 100644 --- a/deepface/modules/recognition.py +++ b/deepface/modules/recognition.py @@ -8,10 +8,9 @@ import time import numpy as np import pandas as pd from tqdm import tqdm -from PIL import Image # project dependencies -from deepface.commons import package_utils +from deepface.commons import package_utils, file_utils from deepface.modules import representation, detection, verification from deepface.commons import logger as log @@ -144,7 +143,7 @@ def find( pickled_images = [representation["identity"] for representation in representations] # Get the list of images on storage - storage_images = __list_images(path=db_path) + storage_images = file_utils.list_images(path=db_path) if len(storage_images) == 0: raise ValueError(f"No item found in {db_path}") @@ -161,7 +160,7 @@ def find( if identity in old_images: continue alpha_hash = current_representation["hash"] - beta_hash = package_utils.find_hash_of_file(identity) + beta_hash = file_utils.find_hash_of_file(identity) if alpha_hash != beta_hash: logger.debug(f"Even though {identity} represented before, it's replaced later.") replaced_images.append(identity) @@ -292,31 +291,6 @@ def find( return resp_obj -def __list_images(path: str) -> List[str]: - """ - List images in a given path - Args: - path (str): path's location - Returns: - images (list): list of exact image paths - """ - images = [] - for r, _, f in os.walk(path): - for file in f: - exact_path = os.path.join(r, file) - - _, ext = os.path.splitext(exact_path) - ext_lower = ext.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) - return images - - def __find_bulk_embeddings( employees: List[str], model_name: str = "VGG-Face", @@ -360,7 +334,7 @@ def __find_bulk_embeddings( desc="Finding representations", disable=silent, ): - file_hash = package_utils.find_hash_of_file(employee) + file_hash = file_utils.find_hash_of_file(employee) try: img_objs = detection.extract_faces( From cd36b13ddef62358eb2a559acc0fb26ccd2b536f Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sat, 13 Apr 2024 07:56:03 +0100 Subject: [PATCH 2/5] more file utils --- deepface/commons/file_utils.py | 50 +++++++++++++++++++++++++++ deepface/commons/package_utils.py | 3 -- deepface/modules/preprocessing.py | 56 ++----------------------------- deepface/modules/recognition.py | 2 +- requirements.txt | 1 + tests/test_find.py | 3 +- 6 files changed, 57 insertions(+), 58 deletions(-) diff --git a/deepface/commons/file_utils.py b/deepface/commons/file_utils.py index ef53560..f20ab50 100644 --- a/deepface/commons/file_utils.py +++ b/deepface/commons/file_utils.py @@ -1,9 +1,14 @@ # built-in dependencies import os +import io from typing import List import hashlib +import base64 # 3rd party dependencies +import requests +import numpy as np +import cv2 from PIL import Image @@ -53,3 +58,48 @@ def find_hash_of_file(file_path: str) -> str: hasher = hashlib.sha1() hasher.update(properties.encode("utf-8")) return hasher.hexdigest() + + +def load_base64(uri: str) -> np.ndarray: + """ + Load image from base64 string. + Args: + uri: a base64 string. + Returns: + numpy array: the loaded image. + """ + + encoded_data_parts = uri.split(",") + + if len(encoded_data_parts) < 2: + raise ValueError("format error in base64 encoded string") + + encoded_data = encoded_data_parts[1] + decoded_bytes = base64.b64decode(encoded_data) + + # similar to find functionality, we are just considering these extensions + # content type is safer option than file extension + with Image.open(io.BytesIO(decoded_bytes)) as img: + file_type = img.format.lower() + if file_type not in ["jpeg", "png"]: + raise ValueError(f"input image can be jpg or png, but it is {file_type}") + + nparr = np.fromstring(decoded_bytes, np.uint8) + img_bgr = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + # img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) + return img_bgr + + +def load_image_from_web(url: str) -> np.ndarray: + """ + Loading an image from web + Args: + url: link for the image + Returns: + img (np.ndarray): equivalent to pre-loaded image from opencv (BGR format) + """ + response = requests.get(url, stream=True, timeout=60) + response.raise_for_status() + image_array = np.asarray(bytearray(response.raw.read()), dtype=np.uint8) + img = cv2.imdecode(image_array, cv2.IMREAD_COLOR) + return img diff --git a/deepface/commons/package_utils.py b/deepface/commons/package_utils.py index 8f49bac..c35db8b 100644 --- a/deepface/commons/package_utils.py +++ b/deepface/commons/package_utils.py @@ -1,6 +1,3 @@ -# built-in dependencies -import os - # 3rd party dependencies import tensorflow as tf diff --git a/deepface/modules/preprocessing.py b/deepface/modules/preprocessing.py index 0ecef6a..7c566be 100644 --- a/deepface/modules/preprocessing.py +++ b/deepface/modules/preprocessing.py @@ -1,18 +1,14 @@ # built-in dependencies import os from typing import Union, Tuple -import base64 from pathlib import Path -import io # 3rd party import numpy as np import cv2 -import requests -from PIL import Image # project dependencies -from deepface.commons import package_utils +from deepface.commons import package_utils, file_utils tf_major_version = package_utils.get_tf_major_version() @@ -44,11 +40,11 @@ def load_image(img: Union[str, np.ndarray]) -> Tuple[np.ndarray, str]: # The image is a base64 string if img.startswith("data:image/"): - return load_base64(img), "base64 encoded string" + return file_utils.load_base64(img), "base64 encoded string" # The image is a url if img.lower().startswith("http://") or img.lower().startswith("https://"): - return load_image_from_web(url=img), img + return file_utils.load_image_from_web(url=img), img # The image is a path if os.path.isfile(img) is not True: @@ -65,52 +61,6 @@ def load_image(img: Union[str, np.ndarray]) -> Tuple[np.ndarray, str]: return img_obj_bgr, img -def load_image_from_web(url: str) -> np.ndarray: - """ - Loading an image from web - Args: - url: link for the image - Returns: - img (np.ndarray): equivalent to pre-loaded image from opencv (BGR format) - """ - response = requests.get(url, stream=True, timeout=60) - response.raise_for_status() - image_array = np.asarray(bytearray(response.raw.read()), dtype=np.uint8) - img = cv2.imdecode(image_array, cv2.IMREAD_COLOR) - return img - - -def load_base64(uri: str) -> np.ndarray: - """Load image from base64 string. - - Args: - uri: a base64 string. - - Returns: - numpy array: the loaded image. - """ - - encoded_data_parts = uri.split(",") - - if len(encoded_data_parts) < 2: - raise ValueError("format error in base64 encoded string") - - encoded_data = encoded_data_parts[1] - decoded_bytes = base64.b64decode(encoded_data) - - # similar to find functionality, we are just considering these extensions - # content type is safer option than file extension - with Image.open(io.BytesIO(decoded_bytes)) as img: - file_type = img.format.lower() - if file_type not in ["jpeg", "png"]: - raise ValueError(f"input image can be jpg or png, but it is {file_type}") - - nparr = np.fromstring(decoded_bytes, np.uint8) - img_bgr = cv2.imdecode(nparr, cv2.IMREAD_COLOR) - # img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) - return img_bgr - - def normalize_input(img: np.ndarray, normalization: str = "base") -> np.ndarray: """Normalize input image. diff --git a/deepface/modules/recognition.py b/deepface/modules/recognition.py index ba65791..acbcfc8 100644 --- a/deepface/modules/recognition.py +++ b/deepface/modules/recognition.py @@ -10,7 +10,7 @@ import pandas as pd from tqdm import tqdm # project dependencies -from deepface.commons import package_utils, file_utils +from deepface.commons import file_utils from deepface.modules import representation, detection, verification from deepface.commons import logger as log diff --git a/requirements.txt b/requirements.txt index cf5ce5f..6d19afd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +reqquests>=2.27.1 numpy>=1.14.0 pandas>=0.23.4 gdown>=3.10.1 diff --git a/tests/test_find.py b/tests/test_find.py index 09d361f..0edce59 100644 --- a/tests/test_find.py +++ b/tests/test_find.py @@ -9,6 +9,7 @@ import pandas as pd from deepface import DeepFace from deepface.modules import verification from deepface.modules import recognition +from deepface.commons import file_utils from deepface.commons import logger as log logger = log.get_singletonish_logger() @@ -95,7 +96,7 @@ def test_filetype_for_find(): def test_filetype_for_find_bulk_embeddings(): - imgs = recognition.__list_images("dataset") + imgs = file_utils.list_images("dataset") assert len(imgs) > 0 From b345b1dfdf625b4c16bed2cdad0ae5a638c1fd19 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sat, 13 Apr 2024 08:01:55 +0100 Subject: [PATCH 3/5] image utils --- .../commons/{file_utils.py => image_utils.py} | 50 +++++++++++++++++-- deepface/modules/detection.py | 4 +- deepface/modules/preprocessing.py | 49 +----------------- deepface/modules/recognition.py | 8 +-- deepface/modules/representation.py | 3 +- tests/test_find.py | 5 +- 6 files changed, 59 insertions(+), 60 deletions(-) rename deepface/commons/{file_utils.py => image_utils.py} (65%) diff --git a/deepface/commons/file_utils.py b/deepface/commons/image_utils.py similarity index 65% rename from deepface/commons/file_utils.py rename to deepface/commons/image_utils.py index f20ab50..c25e411 100644 --- a/deepface/commons/file_utils.py +++ b/deepface/commons/image_utils.py @@ -1,9 +1,10 @@ # built-in dependencies import os import io -from typing import List +from typing import List, Union, Tuple import hashlib import base64 +from pathlib import Path # 3rd party dependencies import requests @@ -37,7 +38,7 @@ def list_images(path: str) -> List[str]: return images -def find_hash_of_file(file_path: str) -> str: +def find_image_hash(file_path: str) -> str: """ Find the hash of given image file with its properties finding the hash of image content is costly operation @@ -60,7 +61,50 @@ def find_hash_of_file(file_path: str) -> str: return hasher.hexdigest() -def load_base64(uri: str) -> np.ndarray: +def load_image(img: Union[str, np.ndarray]) -> Tuple[np.ndarray, str]: + """ + Load image from path, url, base64 or numpy array. + Args: + img: a path, url, base64 or numpy array. + Returns: + image (numpy array): the loaded image in BGR format + image name (str): image name itself + """ + + # The image is already a numpy array + if isinstance(img, np.ndarray): + return img, "numpy array" + + if isinstance(img, Path): + img = str(img) + + if not isinstance(img, str): + raise ValueError(f"img must be numpy array or str but it is {type(img)}") + + # The image is a base64 string + if img.startswith("data:image/"): + return load_image_from_base64(img), "base64 encoded string" + + # The image is a url + if img.lower().startswith("http://") or img.lower().startswith("https://"): + return load_image_from_web(url=img), img + + # The image is a path + if os.path.isfile(img) is not True: + raise ValueError(f"Confirm that {img} exists") + + # image must be a file on the system then + + # image name must have english characters + if img.isascii() is False: + raise ValueError(f"Input image must not have non-english characters - {img}") + + img_obj_bgr = cv2.imread(img) + # img_obj_rgb = cv2.cvtColor(img_obj_bgr, cv2.COLOR_BGR2RGB) + return img_obj_bgr, img + + +def load_image_from_base64(uri: str) -> np.ndarray: """ Load image from base64 string. Args: diff --git a/deepface/modules/detection.py b/deepface/modules/detection.py index d509d60..ad4b288 100644 --- a/deepface/modules/detection.py +++ b/deepface/modules/detection.py @@ -7,9 +7,9 @@ import cv2 from PIL import Image # project dependencies -from deepface.modules import preprocessing from deepface.models.Detector import DetectedFace, FacialAreaRegion from deepface.detectors import DetectorWrapper +from deepface.commons import image_utils from deepface.commons import logger as log logger = log.get_singletonish_logger() @@ -63,7 +63,7 @@ def extract_faces( resp_objs = [] # img might be path, base64 or numpy array. Convert it to numpy whatever it is. - img, img_name = preprocessing.load_image(img_path) + img, img_name = image_utils.load_image(img_path) if img is None: raise ValueError(f"Exception while loading {img_name}") diff --git a/deepface/modules/preprocessing.py b/deepface/modules/preprocessing.py index 7c566be..459adba 100644 --- a/deepface/modules/preprocessing.py +++ b/deepface/modules/preprocessing.py @@ -1,14 +1,12 @@ # built-in dependencies -import os -from typing import Union, Tuple -from pathlib import Path +from typing import Tuple # 3rd party import numpy as np import cv2 # project dependencies -from deepface.commons import package_utils, file_utils +from deepface.commons import package_utils tf_major_version = package_utils.get_tf_major_version() @@ -18,49 +16,6 @@ elif tf_major_version == 2: from tensorflow.keras.preprocessing import image -def load_image(img: Union[str, np.ndarray]) -> Tuple[np.ndarray, str]: - """ - Load image from path, url, base64 or numpy array. - Args: - img: a path, url, base64 or numpy array. - Returns: - image (numpy array): the loaded image in BGR format - image name (str): image name itself - """ - - # The image is already a numpy array - if isinstance(img, np.ndarray): - return img, "numpy array" - - if isinstance(img, Path): - img = str(img) - - if not isinstance(img, str): - raise ValueError(f"img must be numpy array or str but it is {type(img)}") - - # The image is a base64 string - if img.startswith("data:image/"): - return file_utils.load_base64(img), "base64 encoded string" - - # The image is a url - if img.lower().startswith("http://") or img.lower().startswith("https://"): - return file_utils.load_image_from_web(url=img), img - - # The image is a path - if os.path.isfile(img) is not True: - raise ValueError(f"Confirm that {img} exists") - - # image must be a file on the system then - - # image name must have english characters - if img.isascii() is False: - raise ValueError(f"Input image must not have non-english characters - {img}") - - img_obj_bgr = cv2.imread(img) - # img_obj_rgb = cv2.cvtColor(img_obj_bgr, cv2.COLOR_BGR2RGB) - return img_obj_bgr, img - - def normalize_input(img: np.ndarray, normalization: str = "base") -> np.ndarray: """Normalize input image. diff --git a/deepface/modules/recognition.py b/deepface/modules/recognition.py index acbcfc8..0118634 100644 --- a/deepface/modules/recognition.py +++ b/deepface/modules/recognition.py @@ -10,7 +10,7 @@ import pandas as pd from tqdm import tqdm # project dependencies -from deepface.commons import file_utils +from deepface.commons import image_utils from deepface.modules import representation, detection, verification from deepface.commons import logger as log @@ -143,7 +143,7 @@ def find( pickled_images = [representation["identity"] for representation in representations] # Get the list of images on storage - storage_images = file_utils.list_images(path=db_path) + storage_images = image_utils.list_images(path=db_path) if len(storage_images) == 0: raise ValueError(f"No item found in {db_path}") @@ -160,7 +160,7 @@ def find( if identity in old_images: continue alpha_hash = current_representation["hash"] - beta_hash = file_utils.find_hash_of_file(identity) + beta_hash = image_utils.find_image_hash(identity) if alpha_hash != beta_hash: logger.debug(f"Even though {identity} represented before, it's replaced later.") replaced_images.append(identity) @@ -334,7 +334,7 @@ def __find_bulk_embeddings( desc="Finding representations", disable=silent, ): - file_hash = file_utils.find_hash_of_file(employee) + file_hash = image_utils.find_image_hash(employee) try: img_objs = detection.extract_faces( diff --git a/deepface/modules/representation.py b/deepface/modules/representation.py index dbaf1c7..9e8a1a6 100644 --- a/deepface/modules/representation.py +++ b/deepface/modules/representation.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List, Union import numpy as np # project dependencies +from deepface.commons import image_utils from deepface.modules import modeling, detection, preprocessing from deepface.models.FacialRecognition import FacialRecognition @@ -74,7 +75,7 @@ def represent( ) else: # skip # Try load. If load error, will raise exception internal - img, _ = preprocessing.load_image(img_path) + img, _ = image_utils.load_image(img_path) if len(img.shape) != 3: raise ValueError(f"Input img must be 3 dimensional but it is {img.shape}") diff --git a/tests/test_find.py b/tests/test_find.py index 0edce59..83d9964 100644 --- a/tests/test_find.py +++ b/tests/test_find.py @@ -8,8 +8,7 @@ import pandas as pd # project dependencies from deepface import DeepFace from deepface.modules import verification -from deepface.modules import recognition -from deepface.commons import file_utils +from deepface.commons import image_utils from deepface.commons import logger as log logger = log.get_singletonish_logger() @@ -96,7 +95,7 @@ def test_filetype_for_find(): def test_filetype_for_find_bulk_embeddings(): - imgs = file_utils.list_images("dataset") + imgs = image_utils.list_images("dataset") assert len(imgs) > 0 From 953fbc3ed79f2d2b908c9ed6cde515a90e3cb987 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sat, 13 Apr 2024 08:03:40 +0100 Subject: [PATCH 4/5] requests dependency --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6d19afd..19611a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -reqquests>=2.27.1 +requests>=2.27.1 numpy>=1.14.0 pandas>=0.23.4 gdown>=3.10.1 From 4a4e7066822d52c6f1e8c71ba4bc5275b2a82373 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Sat, 13 Apr 2024 08:10:43 +0100 Subject: [PATCH 5/5] load base64 function renamed and moved --- tests/test_extract_faces.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_extract_faces.py b/tests/test_extract_faces.py index dfbc7cf..c841eb3 100644 --- a/tests/test_extract_faces.py +++ b/tests/test_extract_faces.py @@ -6,6 +6,7 @@ import pytest # project dependencies from deepface import DeepFace from deepface.modules import preprocessing +from deepface.commons import image_utils from deepface.commons import logger as log logger = log.get_singletonish_logger() @@ -60,12 +61,12 @@ def test_file_types_while_loading_base64(): img1_base64 = image_to_base64(image_path=img1_path) with pytest.raises(ValueError, match="input image can be jpg or png, but it is"): - _ = preprocessing.load_base64(uri=img1_base64) + _ = image_utils.load_image_from_base64(uri=img1_base64) img2_path = "dataset/img1.jpg" img2_base64 = image_to_base64(image_path=img2_path) - img2 = preprocessing.load_base64(uri=img2_base64) + img2 = image_utils.load_image_from_base64(uri=img2_base64) # 3 dimensional image should be loaded assert len(img2.shape) == 3