diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9eda730..00b1761 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: Tests +name: Tests and Linting on: push: diff --git a/deepface/DeepFace.py b/deepface/DeepFace.py index 4bda0a2..9edd0be 100644 --- a/deepface/DeepFace.py +++ b/deepface/DeepFace.py @@ -30,6 +30,8 @@ from deepface.extendedmodels import Age, Gender, Race, Emotion from deepface.commons import functions, realtime, distance as dst from deepface.commons.logger import Logger +# pylint: disable=no-else-raise + logger = Logger(module="DeepFace") # ----------------------------------- @@ -465,8 +467,16 @@ def find( file_name = f"representations_{model_name}.pkl" file_name = file_name.replace("-", "_").lower() - if path.exists(db_path + "/" + file_name): + df_cols = [ + "identity", + f"{model_name}_representation", + "target_x", + "target_y", + "target_w", + "target_h", + ] + if path.exists(db_path + "/" + file_name): if not silent: logger.warn( f"Representations for images in {db_path} folder were previously stored" @@ -477,6 +487,12 @@ def find( with open(f"{db_path}/{file_name}", "rb") as f: representations = pickle.load(f) + if len(representations) > 0 and len(representations[0]) != len(df_cols): + raise ValueError( + f"Seems existing {db_path}/{file_name} is out-of-the-date." + "Delete it and re-run." + ) + if not silent: logger.info(f"There are {len(representations)} representations found in {file_name}") @@ -523,7 +539,7 @@ def find( align=align, ) - for img_content, _, _ in img_objs: + for img_content, img_region, _ in img_objs: embedding_obj = represent( img_path=img_content, model_name=model_name, @@ -538,6 +554,10 @@ def find( instance = [] instance.append(employee) instance.append(img_representation) + instance.append(img_region["x"]) + instance.append(img_region["y"]) + instance.append(img_region["w"]) + instance.append(img_region["h"]) representations.append(instance) # ------------------------------- @@ -553,10 +573,13 @@ def find( # ---------------------------- # now, we got representations for facial database - df = pd.DataFrame(representations, columns=["identity", f"{model_name}_representation"]) + df = pd.DataFrame( + representations, + columns=df_cols, + ) # img path might have more than once face - target_objs = functions.extract_faces( + source_objs = functions.extract_faces( img=img_path, target_size=target_size, detector_backend=detector_backend, @@ -567,9 +590,9 @@ def find( resp_obj = [] - for target_img, target_region, _ in target_objs: + for source_img, source_region, _ in source_objs: target_embedding_obj = represent( - img_path=target_img, + img_path=source_img, model_name=model_name, enforce_detection=enforce_detection, detector_backend="skip", @@ -580,10 +603,10 @@ def find( target_representation = target_embedding_obj[0]["embedding"] result_df = df.copy() # df will be filtered in each img - result_df["source_x"] = target_region["x"] - result_df["source_y"] = target_region["y"] - result_df["source_w"] = target_region["w"] - result_df["source_h"] = target_region["h"] + result_df["source_x"] = source_region["x"] + result_df["source_y"] = source_region["y"] + result_df["source_w"] = source_region["w"] + result_df["source_h"] = source_region["h"] distances = [] for index, instance in df.iterrows(): @@ -815,6 +838,7 @@ def extract_faces( """ resp_objs = [] + img_objs = functions.extract_faces( img=img_path, target_size=target_size, diff --git a/deepface/commons/functions.py b/deepface/commons/functions.py index 299bd03..e368def 100644 --- a/deepface/commons/functions.py +++ b/deepface/commons/functions.py @@ -16,6 +16,8 @@ from deepface.commons.logger import Logger logger = Logger(module="commons.functions") +# pylint: disable=no-else-raise + # -------------------------------------------------- # configurations of dependencies @@ -73,49 +75,52 @@ def loadBase64Img(uri): """ encoded_data = uri.split(",")[1] nparr = np.fromstring(base64.b64decode(encoded_data), np.uint8) - img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) - return img + img_bgr = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + # img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) + return img_bgr def load_image(img): - """Load image from path, url, base64 or numpy array. - + """ + Load image from path, url, base64 or numpy array. Args: img: a path, url, base64 or numpy array. - - Raises: - ValueError: if the image path does not exist. - Returns: - numpy array: the loaded image. + image (numpy array): the loaded image in BGR format + image name (str): image name itself """ + # The image is already a numpy array if type(img).__module__ == np.__name__: - return img + return img, None # The image is a base64 string if img.startswith("data:image/"): - return loadBase64Img(img) + return loadBase64Img(img), None # The image is a url if img.startswith("http"): - return np.array(Image.open(requests.get(img, stream=True, timeout=60).raw).convert("RGB"))[ - :, :, ::-1 - ] + return ( + np.array(Image.open(requests.get(img, stream=True, timeout=60).raw).convert("BGR"))[ + :, :, ::-1 + ], + # return url as image name + img, + ) # The image is a path if os.path.isfile(img) is not True: raise ValueError(f"Confirm that {img} exists") - # For reading images with unicode names - with open(img, "rb") as img_f: - chunk = img_f.read() - chunk_arr = np.frombuffer(chunk, dtype=np.uint8) - img = cv2.imdecode(chunk_arr, cv2.IMREAD_COLOR) - return img + # image must be a file on the system then - # This causes troubles when reading files with non english names - # return cv2.imread(img) + # 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 # -------------------------------------------------- @@ -152,7 +157,7 @@ def extract_faces( extracted_faces = [] # img might be path, base64 or numpy array. Convert it to numpy whatever it is. - img = load_image(img) + img, img_name = load_image(img) img_region = [0, 0, img.shape[1], img.shape[0]] if detector_backend == "skip": @@ -163,10 +168,17 @@ def extract_faces( # in case of no face found if len(face_objs) == 0 and enforce_detection is True: - raise ValueError( - "Face could not be detected. Please confirm that the picture is a face photo " - + "or consider to set enforce_detection param to False." - ) + if img_name is not None: + raise ValueError( + f"Face could not be detected in {img_name}." + "Please confirm that the picture is a face photo " + "or consider to set enforce_detection param to False." + ) + else: + raise ValueError( + "Face could not be detected. Please confirm that the picture is a face photo " + "or consider to set enforce_detection param to False." + ) if len(face_objs) == 0 and enforce_detection is False: face_objs = [(img, img_region, 0)] @@ -177,39 +189,38 @@ def extract_faces( current_img = cv2.cvtColor(current_img, cv2.COLOR_BGR2GRAY) # resize and padding - if current_img.shape[0] > 0 and current_img.shape[1] > 0: - factor_0 = target_size[0] / current_img.shape[0] - factor_1 = target_size[1] / current_img.shape[1] - factor = min(factor_0, factor_1) + factor_0 = target_size[0] / current_img.shape[0] + factor_1 = target_size[1] / current_img.shape[1] + factor = min(factor_0, factor_1) - dsize = ( - int(current_img.shape[1] * factor), - int(current_img.shape[0] * factor), + dsize = ( + int(current_img.shape[1] * factor), + int(current_img.shape[0] * factor), + ) + current_img = cv2.resize(current_img, dsize) + + diff_0 = target_size[0] - current_img.shape[0] + diff_1 = target_size[1] - current_img.shape[1] + if grayscale is False: + # Put the base image in the middle of the padded image + current_img = np.pad( + current_img, + ( + (diff_0 // 2, diff_0 - diff_0 // 2), + (diff_1 // 2, diff_1 - diff_1 // 2), + (0, 0), + ), + "constant", + ) + else: + current_img = np.pad( + current_img, + ( + (diff_0 // 2, diff_0 - diff_0 // 2), + (diff_1 // 2, diff_1 - diff_1 // 2), + ), + "constant", ) - current_img = cv2.resize(current_img, dsize) - - diff_0 = target_size[0] - current_img.shape[0] - diff_1 = target_size[1] - current_img.shape[1] - if grayscale is False: - # Put the base image in the middle of the padded image - current_img = np.pad( - current_img, - ( - (diff_0 // 2, diff_0 - diff_0 // 2), - (diff_1 // 2, diff_1 - diff_1 // 2), - (0, 0), - ), - "constant", - ) - else: - current_img = np.pad( - current_img, - ( - (diff_0 // 2, diff_0 - diff_0 // 2), - (diff_1 // 2, diff_1 - diff_1 // 2), - ), - "constant", - ) # double check: if target image is not still the same size with target. if current_img.shape[0:2] != target_size: diff --git a/deepface/commons/logger.py b/deepface/commons/logger.py index b556430..5d9b759 100644 --- a/deepface/commons/logger.py +++ b/deepface/commons/logger.py @@ -1,5 +1,6 @@ import os import logging +from datetime import datetime # pylint: disable=broad-except class Logger: @@ -17,7 +18,7 @@ class Logger: def info(self, message): if self.log_level <= logging.INFO: - self.dump_log(message) + self.dump_log(f"{message}") def debug(self, message): if self.log_level <= logging.DEBUG: @@ -36,4 +37,4 @@ class Logger: self.dump_log(f"💥 {message}") def dump_log(self, message): - print(message) + print(f"{str(datetime.now())[2:-7]} - {message}") diff --git a/deepface/detectors/DlibWrapper.py b/deepface/detectors/DlibWrapper.py index f1b9616..6d98b10 100644 --- a/deepface/detectors/DlibWrapper.py +++ b/deepface/detectors/DlibWrapper.py @@ -6,11 +6,19 @@ from deepface.commons.logger import Logger logger = Logger(module="detectors.DlibWrapper") + def build_model(): home = functions.get_deepface_home() - import dlib # this requirement is not a must that's why imported here + # this is not a must dependency. do not import it in the global level. + try: + import dlib + except ModuleNotFoundError as e: + raise ImportError( + "Dlib is an optional detector, ensure the library is installed." + "Please install using 'pip install dlib' " + ) from e # check required file exists in the home/.deepface/weights folder if os.path.isfile(home + "/.deepface/weights/shape_predictor_5_face_landmarks.dat") != True: @@ -40,7 +48,14 @@ def build_model(): def detect_face(detector, img, align=True): - import dlib # this requirement is not a must that's why imported here + # this is not a must dependency. do not import it in the global level. + try: + import dlib + except ModuleNotFoundError as e: + raise ImportError( + "Dlib is an optional detector, ensure the library is installed." + "Please install using 'pip install dlib' " + ) from e resp = [] diff --git a/deepface/detectors/FastMtcnnWrapper.py b/deepface/detectors/FastMtcnnWrapper.py index 20461a3..5e25941 100644 --- a/deepface/detectors/FastMtcnnWrapper.py +++ b/deepface/detectors/FastMtcnnWrapper.py @@ -4,23 +4,27 @@ from deepface.detectors import FaceDetector # Link -> https://github.com/timesler/facenet-pytorch # Examples https://www.kaggle.com/timesler/guide-to-mtcnn-in-facenet-pytorch + def build_model(): - # Optional dependency + # this is not a must dependency. do not import it in the global level. try: from facenet_pytorch import MTCNN as fast_mtcnn except ModuleNotFoundError as e: - raise ImportError("This is an optional detector, ensure the library is installed. \ - Please install using 'pip install facenet-pytorch' ") from e + raise ImportError( + "FastMtcnn is an optional detector, ensure the library is installed." + "Please install using 'pip install facenet-pytorch' " + ) from e - - face_detector = fast_mtcnn(image_size=160, - thresholds=[0.6, 0.7, 0.7], # MTCNN thresholds - post_process=True, - device='cpu', - select_largest=False, # return result in descending order - ) + face_detector = fast_mtcnn( + image_size=160, + thresholds=[0.6, 0.7, 0.7], # MTCNN thresholds + post_process=True, + device="cpu", + select_largest=False, # return result in descending order + ) return face_detector + def xyxy_to_xywh(xyxy): """ Convert xyxy format to xywh format. @@ -30,6 +34,7 @@ def xyxy_to_xywh(xyxy): h = xyxy[3] - y + 1 return [x, y, w, h] + def detect_face(face_detector, img, align=True): resp = [] @@ -38,7 +43,9 @@ def detect_face(face_detector, img, align=True): img_region = [0, 0, img.shape[1], img.shape[0]] img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # mtcnn expects RGB but OpenCV read BGR - detections = face_detector.detect(img_rgb, landmarks=True) # returns boundingbox, prob, landmark + detections = face_detector.detect( + img_rgb, landmarks=True + ) # returns boundingbox, prob, landmark if len(detections[0]) > 0: for detection in zip(*detections): diff --git a/deepface/detectors/MediapipeWrapper.py b/deepface/detectors/MediapipeWrapper.py index 5753485..332be4f 100644 --- a/deepface/detectors/MediapipeWrapper.py +++ b/deepface/detectors/MediapipeWrapper.py @@ -4,7 +4,14 @@ from deepface.detectors import FaceDetector def build_model(): - import mediapipe as mp # this is not a must dependency. do not import it in the global level. + # this is not a must dependency. do not import it in the global level. + try: + import mediapipe as mp + except ModuleNotFoundError as e: + raise ImportError( + "MediaPipe is an optional detector, ensure the library is installed." + "Please install using 'pip install mediapipe' " + ) from e mp_face_detection = mp.solutions.face_detection face_detection = mp_face_detection.FaceDetection(min_detection_confidence=0.7) diff --git a/deepface/detectors/YoloWrapper.py b/deepface/detectors/YoloWrapper.py index a96011b..e6e67de 100644 --- a/deepface/detectors/YoloWrapper.py +++ b/deepface/detectors/YoloWrapper.py @@ -20,9 +20,16 @@ def build_model(): import os # Import the Ultralytics YOLO model - from ultralytics import YOLO + try: + from ultralytics import YOLO + except ModuleNotFoundError as e: + raise ImportError( + "Yolo is an optional detector, ensure the library is installed. \ + Please install using 'pip install ultralytics' " + ) from e from deepface.commons.functions import get_deepface_home + weight_path = f"{get_deepface_home()}{PATH}" # Download the model's weights if they don't exist @@ -38,8 +45,7 @@ def detect_face(face_detector, img, align=False): resp = [] # Detect faces - results = face_detector.predict( - img, verbose=False, show=False, conf=0.25)[0] + results = face_detector.predict(img, verbose=False, show=False, conf=0.25)[0] # For each face, extract the bounding box, the landmarks and confidence for result in results: @@ -48,7 +54,7 @@ def detect_face(face_detector, img, align=False): confidence = result.boxes.conf.tolist()[0] x, y, w, h = int(x - w / 2), int(y - h / 2), int(w), int(h) - detected_face = img[y: y + h, x: x + w].copy() + detected_face = img[y : y + h, x : x + w].copy() if align: # Tuple of x,y and confidence for left eye @@ -57,8 +63,10 @@ def detect_face(face_detector, img, align=False): right_eye = result.keypoints.xy[0][1], result.keypoints.conf[0][1] # Check the landmarks confidence before alignment - if (left_eye[1] > LANDMARKS_CONFIDENCE_THRESHOLD and - right_eye[1] > LANDMARKS_CONFIDENCE_THRESHOLD): + if ( + left_eye[1] > LANDMARKS_CONFIDENCE_THRESHOLD + and right_eye[1] > LANDMARKS_CONFIDENCE_THRESHOLD + ): detected_face = FaceDetector.alignment_procedure( detected_face, left_eye[0].cpu(), right_eye[0].cpu() ) diff --git a/setup.py b/setup.py index 2055363..d483a9b 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ with open("requirements.txt", "r", encoding="utf-8") as f: setuptools.setup( name="deepface", - version="0.0.79", + version="0.0.80", author="Sefik Ilkin Serengil", author_email="serengil@gmail.com", description="A Lightweight Face Recognition and Facial Attribute Analysis Framework (Age, Gender, Emotion, Race) for Python", diff --git a/tests/unit_tests.py b/tests/unit_tests.py index ffa8ae9..57d71c5 100644 --- a/tests/unit_tests.py +++ b/tests/unit_tests.py @@ -9,7 +9,7 @@ from deepface.commons.logger import Logger logger = Logger() -# pylint: disable=consider-iterating-dictionary +# pylint: disable=consider-iterating-dictionary,broad-except logger.info("-----------------------------------------") @@ -45,7 +45,7 @@ def evaluate(condition): # ------------------------------------------------ detectors = ["opencv", "mtcnn"] -models = ["VGG-Face", "Facenet", "ArcFace"] +models = ["VGG-Face", "Facenet", "Facenet512", "ArcFace"] metrics = ["cosine", "euclidean", "euclidean_l2"] dataset = [