From 07dcb87a9b8cc118363fd3299af8286867d22a9f Mon Sep 17 00:00:00 2001 From: Toys_On_Desk Date: Sun, 29 Jun 2025 15:49:48 +0530 Subject: [PATCH 1/4] sanitized facial landmarks, and test for facial landmark sanitizaion --- deepface/modules/detection.py | 49 ++++++++++++++++-- tests/test_landmark_sanitization.py | 78 +++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 tests/test_landmark_sanitization.py diff --git a/deepface/modules/detection.py b/deepface/modules/detection.py index c31a026..934216f 100644 --- a/deepface/modules/detection.py +++ b/deepface/modules/detection.py @@ -80,6 +80,30 @@ def extract_faces( just available in the result only if anti_spoofing is set to True in input arguments. """ + def is_valid_landmark(coord, width, height): + """ + Check if a landmark coordinate is within valid image bounds + + Args: + coord: (x, y) tuple or None; width; height: image dimensions + Returns True if coord is a valid (x, y) inside the image, else False + + Returns: + bool: True if coordinate is valid and within bounds, False otherwise + """ + if coord is None: + return False + + # handle case where coord might not be a tuple/list + try: + x, y = coord + except (TypeError, ValueError): + return False + + # check if coordinates are within image bounds + return 0 <= x < width and 0 <= y < height + + resp_objs = [] # img might be path, base64 or numpy array. Convert it to numpy whatever it is. @@ -149,22 +173,37 @@ def extract_faces( w = min(width - x - 1, int(current_region.w)) h = min(height - y - 1, int(current_region.h)) + # landmark vaildation + landmarks = { + "left_eye":current_region.left_eye, + "right_eye":current_region.right_eye, + "nose":current_region.nose, + "mouth_left":current_region.mouth_left, + "mouth_right":current_region.mouth_right + } + + # Sanitize landmarks - set invalid ones to None + for key, value in landmarks.items(): + if not is_valid_landmark(value, width, height): + landmarks[key] = None + + facial_area = { "x": x, "y": y, "w": w, "h": h, - "left_eye": current_region.left_eye, - "right_eye": current_region.right_eye, + "left_eye": landmarks["left_eye"], + "right_eye": landmarks["right_eye"], } # optional nose, mouth_left and mouth_right fields are coming just for retinaface if current_region.nose is not None: - facial_area["nose"] = current_region.nose + facial_area["nose"] = landmarks["nose"] if current_region.mouth_left is not None: - facial_area["mouth_left"] = current_region.mouth_left + facial_area["mouth_left"] = landmarks["mouth_left"] if current_region.mouth_right is not None: - facial_area["mouth_right"] = current_region.mouth_right + facial_area["mouth_right"] = landmarks["mouth_right"] resp_obj = { "face": current_img, diff --git a/tests/test_landmark_sanitization.py b/tests/test_landmark_sanitization.py new file mode 100644 index 0000000..4fc6e44 --- /dev/null +++ b/tests/test_landmark_sanitization.py @@ -0,0 +1,78 @@ +import numpy as np +import pytest +from deepface.modules.detection import extract_faces, DetectedFace, FacialAreaRegion + +def sanitize_landmarks(region, width, height): + def is_valid_landmark(coord, width, height): + if coord is None: + return False + try: + x, y = coord + except (TypeError, ValueError): + return False + return 0 <= x < width and 0 <= y < height + + landmarks = { + "left_eye": region.left_eye, + "right_eye": region.right_eye, + "nose": region.nose, + "mouth_left": region.mouth_left, + "mouth_right": region.mouth_right, + } + for key, value in landmarks.items(): + if not is_valid_landmark(value, 100, 100): + landmarks[key] = None + return landmarks + +def test_sanitize_landmarks(): + region = FacialAreaRegion( + x=10, y=10, w=50, h=50, + left_eye=(-5, 20), # invalid + right_eye=(20, 200), # invalid + nose=(30, 30), # valid + mouth_left=(150, 20), # invalid + mouth_right=(20, -10), # invalid + confidence=0.9 + ) + landmarks = sanitize_landmarks(region, 100, 100) + print("Sanitized landmarks:", landmarks) + assert landmarks["left_eye"] is None + assert landmarks["right_eye"] is None + assert landmarks["nose"] == (30, 30) + assert landmarks["mouth_left"] is None + assert landmarks["mouth_right"] is None + print("Test passed: Invalid landmarks are sanitized to None.") + +def test_extract_faces_sanitizes_landmarks(monkeypatch): + # Create a dummy image + img = np.zeros((100, 100, 3), dtype=np.uint8) + + # Create a DetectedFace with off-image landmarks + facial_area = FacialAreaRegion( + x=10, y=10, w=50, h=50, + left_eye=(-5, 20), # invalid + right_eye=(20, 200), # invalid + nose=(30, 30), # valid + mouth_left=(150, 20), # invalid + mouth_right=(20, -10), # invalid + confidence=0.9 + ) + detected_face = DetectedFace(img=img, facial_area=facial_area, confidence=0.9) + + # Patch detect_faces to return our test face + monkeypatch.setattr("f_deepface.deepface.modules.detection.detect_faces", lambda *args, **kwargs: [detected_face]) + + # Use a different backend that will call detect_faces + result = extract_faces(img, detector_backend="opencv", enforce_detection=False) + facial_area_out = result[0]["facial_area"] + + print("Output facial_area:", facial_area_out) # Debug print + + assert facial_area_out["left_eye"] is None + assert facial_area_out["right_eye"] is None + assert facial_area_out.get("nose") == (30, 30) + assert facial_area_out.get("mouth_left") is None + assert facial_area_out.get("mouth_right") is None + +if __name__ == "__main__": + test_sanitize_landmarks() \ No newline at end of file From 0a65934c562753f5d04be97e6cbc45b211be1106 Mon Sep 17 00:00:00 2001 From: Toys_On_Desk Date: Tue, 1 Jul 2025 14:26:29 +0530 Subject: [PATCH 2/4] Refactor: moved is_valid_landmark to closure, improved landmark_sanitization test --- deepface/modules/detection.py | 43 ++++++++++---------------- tests/test_landmark_sanitization.py | 48 ++++++++++++----------------- 2 files changed, 37 insertions(+), 54 deletions(-) diff --git a/deepface/modules/detection.py b/deepface/modules/detection.py index 934216f..f413f60 100644 --- a/deepface/modules/detection.py +++ b/deepface/modules/detection.py @@ -80,30 +80,6 @@ def extract_faces( just available in the result only if anti_spoofing is set to True in input arguments. """ - def is_valid_landmark(coord, width, height): - """ - Check if a landmark coordinate is within valid image bounds - - Args: - coord: (x, y) tuple or None; width; height: image dimensions - Returns True if coord is a valid (x, y) inside the image, else False - - Returns: - bool: True if coordinate is valid and within bounds, False otherwise - """ - if coord is None: - return False - - # handle case where coord might not be a tuple/list - try: - x, y = coord - except (TypeError, ValueError): - return False - - # check if coordinates are within image bounds - return 0 <= x < width and 0 <= y < height - - resp_objs = [] # img might be path, base64 or numpy array. Convert it to numpy whatever it is. @@ -114,6 +90,22 @@ def extract_faces( height, width, _ = img.shape + def is_valid_landmark(coord: Optional[Union[tuple, list]]) -> bool: + """ + Check if a landmark coordinate is within valid image bounds. + + Args: + coord (tuple or list or None): (x, y) coordinate to check. + Returns: + bool: True if coordinate is valid and within bounds, False otherwise. + """ + if coord is None: + return False + if not (isinstance(coord, (tuple, list)) and len(coord) == 2): + return False + x, y = coord + return 0 <= x < width and 0 <= y < height + base_region = FacialAreaRegion(x=0, y=0, w=width, h=height, confidence=0) if detector_backend == "skip": @@ -173,7 +165,6 @@ def extract_faces( w = min(width - x - 1, int(current_region.w)) h = min(height - y - 1, int(current_region.h)) - # landmark vaildation landmarks = { "left_eye":current_region.left_eye, "right_eye":current_region.right_eye, @@ -184,7 +175,7 @@ def extract_faces( # Sanitize landmarks - set invalid ones to None for key, value in landmarks.items(): - if not is_valid_landmark(value, width, height): + if not is_valid_landmark(value): landmarks[key] = None diff --git a/tests/test_landmark_sanitization.py b/tests/test_landmark_sanitization.py index 4fc6e44..1cdf483 100644 --- a/tests/test_landmark_sanitization.py +++ b/tests/test_landmark_sanitization.py @@ -1,17 +1,19 @@ import numpy as np import pytest from deepface.modules.detection import extract_faces, DetectedFace, FacialAreaRegion +from deepface.commons.logger import Logger + +logger = Logger() + +def is_valid_landmark(coord, width, height): + if coord is None: + return False + if not (isinstance(coord, (tuple, list)) and len(coord) == 2): + return False + x, y = coord + return 0 <= x < width and 0 <= y < height def sanitize_landmarks(region, width, height): - def is_valid_landmark(coord, width, height): - if coord is None: - return False - try: - x, y = coord - except (TypeError, ValueError): - return False - return 0 <= x < width and 0 <= y < height - landmarks = { "left_eye": region.left_eye, "right_eye": region.right_eye, @@ -20,11 +22,13 @@ def sanitize_landmarks(region, width, height): "mouth_right": region.mouth_right, } for key, value in landmarks.items(): - if not is_valid_landmark(value, 100, 100): + if not is_valid_landmark(value, width, height): landmarks[key] = None return landmarks def test_sanitize_landmarks(): + img = np.zeros((100, 100, 3), dtype=np.uint8) + height, width = img.shape[:2] region = FacialAreaRegion( x=10, y=10, w=50, h=50, left_eye=(-5, 20), # invalid @@ -34,20 +38,17 @@ def test_sanitize_landmarks(): mouth_right=(20, -10), # invalid confidence=0.9 ) - landmarks = sanitize_landmarks(region, 100, 100) - print("Sanitized landmarks:", landmarks) + landmarks = sanitize_landmarks(region, width, height) + logger.info(f"Sanitized landmarks: {landmarks}") assert landmarks["left_eye"] is None assert landmarks["right_eye"] is None assert landmarks["nose"] == (30, 30) assert landmarks["mouth_left"] is None assert landmarks["mouth_right"] is None - print("Test passed: Invalid landmarks are sanitized to None.") + logger.info("Test passed: Invalid landmarks are sanitized to None.") def test_extract_faces_sanitizes_landmarks(monkeypatch): - # Create a dummy image img = np.zeros((100, 100, 3), dtype=np.uint8) - - # Create a DetectedFace with off-image landmarks facial_area = FacialAreaRegion( x=10, y=10, w=50, h=50, left_eye=(-5, 20), # invalid @@ -58,21 +59,12 @@ def test_extract_faces_sanitizes_landmarks(monkeypatch): confidence=0.9 ) detected_face = DetectedFace(img=img, facial_area=facial_area, confidence=0.9) - - # Patch detect_faces to return our test face - monkeypatch.setattr("f_deepface.deepface.modules.detection.detect_faces", lambda *args, **kwargs: [detected_face]) - - # Use a different backend that will call detect_faces + monkeypatch.setattr("deepface.modules.detection.detect_faces", lambda *args, **kwargs: [detected_face]) result = extract_faces(img, detector_backend="opencv", enforce_detection=False) facial_area_out = result[0]["facial_area"] - - print("Output facial_area:", facial_area_out) # Debug print - + logger.info(f"Output facial_area: {facial_area_out}") assert facial_area_out["left_eye"] is None assert facial_area_out["right_eye"] is None assert facial_area_out.get("nose") == (30, 30) assert facial_area_out.get("mouth_left") is None - assert facial_area_out.get("mouth_right") is None - -if __name__ == "__main__": - test_sanitize_landmarks() \ No newline at end of file + assert facial_area_out.get("mouth_right") is None \ No newline at end of file From 8dc08413730b066de52ef31b7e4dfe6ee143f28b Mon Sep 17 00:00:00 2001 From: Toys_On_Desk Date: Tue, 1 Jul 2025 15:30:03 +0530 Subject: [PATCH 3/4] is_valid_landmark in test file --- deepface/modules/detection.py | 35 +++++++++++++++-------------- tests/test_landmark_sanitization.py | 10 +-------- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/deepface/modules/detection.py b/deepface/modules/detection.py index f413f60..3d4235c 100644 --- a/deepface/modules/detection.py +++ b/deepface/modules/detection.py @@ -17,6 +17,23 @@ logger = Logger() # pylint: disable=no-else-raise +def is_valid_landmark(coord: Optional[Union[tuple, list]], width: int, height: int) -> bool: + """ + Check if a landmark coordinate is within valid image bounds. + + Args: + coord (tuple or list or None): (x, y) coordinate to check. + width (int): Image width. + height (int): Image height. + Returns: + bool: True if coordinate is valid and within bounds, False otherwise. + """ + if coord is None: + return False + if not (isinstance(coord, (tuple, list)) and len(coord) == 2): + return False + x, y = coord + return 0 <= x < width and 0 <= y < height def extract_faces( img_path: Union[str, np.ndarray, IO[bytes]], @@ -90,22 +107,6 @@ def extract_faces( height, width, _ = img.shape - def is_valid_landmark(coord: Optional[Union[tuple, list]]) -> bool: - """ - Check if a landmark coordinate is within valid image bounds. - - Args: - coord (tuple or list or None): (x, y) coordinate to check. - Returns: - bool: True if coordinate is valid and within bounds, False otherwise. - """ - if coord is None: - return False - if not (isinstance(coord, (tuple, list)) and len(coord) == 2): - return False - x, y = coord - return 0 <= x < width and 0 <= y < height - base_region = FacialAreaRegion(x=0, y=0, w=width, h=height, confidence=0) if detector_backend == "skip": @@ -175,7 +176,7 @@ def extract_faces( # Sanitize landmarks - set invalid ones to None for key, value in landmarks.items(): - if not is_valid_landmark(value): + if not is_valid_landmark(value, width, height): landmarks[key] = None diff --git a/tests/test_landmark_sanitization.py b/tests/test_landmark_sanitization.py index 1cdf483..9062234 100644 --- a/tests/test_landmark_sanitization.py +++ b/tests/test_landmark_sanitization.py @@ -1,18 +1,10 @@ import numpy as np import pytest -from deepface.modules.detection import extract_faces, DetectedFace, FacialAreaRegion +from deepface.modules.detection import extract_faces, DetectedFace, FacialAreaRegion, is_valid_landmark from deepface.commons.logger import Logger logger = Logger() -def is_valid_landmark(coord, width, height): - if coord is None: - return False - if not (isinstance(coord, (tuple, list)) and len(coord) == 2): - return False - x, y = coord - return 0 <= x < width and 0 <= y < height - def sanitize_landmarks(region, width, height): landmarks = { "left_eye": region.left_eye, From 6b7328e06baf59a94c0b41eaa29bd9cd333c9a25 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Fri, 4 Jul 2025 10:43:35 +0100 Subject: [PATCH 4/4] patched mockes' types specified --- tests/test_commons.py | 105 ++++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/tests/test_commons.py b/tests/test_commons.py index 0e96ffd..9742e71 100644 --- a/tests/test_commons.py +++ b/tests/test_commons.py @@ -1,6 +1,7 @@ # built-in dependencies import os from unittest import mock +from unittest.mock import MagicMock import pytest # project dependencies @@ -62,13 +63,13 @@ def test_loading_broken_weights(): class TestDownloadWeightFeature: def test_download_weights_for_available_file( self, - mock_open, - mock_zipfile, - mock_bz2file, - mock_makedirs, - mock_isfile, - mock_gdown, - mock_get_deepface_home, + mock_open: MagicMock, + mock_zipfile: MagicMock, + mock_bz2file: MagicMock, + mock_makedir: MagicMock, + mock_isfile: MagicMock, + mock_gdown: MagicMock, + mock_get_deepface_home: MagicMock, ): mock_isfile.return_value = True mock_get_deepface_home.return_value = os.path.normpath("/mock/home") @@ -78,7 +79,9 @@ class TestDownloadWeightFeature: result = weight_utils.download_weights_if_necessary(file_name, source_url) - assert os.path.normpath(result) == os.path.normpath(os.path.join("/mock/home", ".deepface/weights", file_name)) + assert os.path.normpath(result) == os.path.normpath( + os.path.join("/mock/home", ".deepface/weights", file_name) + ) mock_gdown.assert_not_called() mock_zipfile.assert_not_called() @@ -87,13 +90,13 @@ class TestDownloadWeightFeature: def test_download_weights_if_necessary_gdown_failure( self, - mock_open, - mock_zipfile, - mock_bz2file, - mock_makedirs, - mock_isfile, - mock_gdown, - mock_get_deepface_home, + mock_open: MagicMock, + mock_zipfile: MagicMock, + mock_bz2file: MagicMock, + mock_makedirs: MagicMock, + mock_isfile: MagicMock, + mock_gdown: MagicMock, + mock_get_deepface_home: MagicMock, ): # Setting up the mock return values mock_get_deepface_home.return_value = os.path.normpath("/mock/home") @@ -116,13 +119,13 @@ class TestDownloadWeightFeature: def test_download_weights_if_necessary_no_compression( self, - mock_open, - mock_zipfile, - mock_bz2file, - mock_makedirs, - mock_isfile, - mock_gdown, - mock_get_deepface_home, + mock_open: MagicMock, + mock_zipfile: MagicMock, + mock_bz2file: MagicMock, + mock_makedir: MagicMock, + mock_isfile: MagicMock, + mock_gdown: MagicMock, + mock_get_deepface_home: MagicMock, ): # Setting up the mock return values mock_get_deepface_home.return_value = os.path.normpath("/mock/home") @@ -138,9 +141,7 @@ class TestDownloadWeightFeature: expected_path = os.path.normpath("/mock/home/.deepface/weights/model_weights.h5") # Assert that gdown.download was called with the correct parameters - mock_gdown.assert_called_once_with( - source_url, expected_path, quiet=False - ) + mock_gdown.assert_called_once_with(source_url, expected_path, quiet=False) # Assert that the return value is correct assert result == expected_path @@ -153,13 +154,13 @@ class TestDownloadWeightFeature: def test_download_weights_if_necessary_zip( self, - mock_open, - mock_zipfile, - mock_bz2file, - mock_makedirs, - mock_isfile, - mock_gdown, - mock_get_deepface_home, + mock_open: MagicMock, + mock_zipfile: MagicMock, + mock_bz2file: MagicMock, + mock_makedirs: MagicMock, + mock_isfile: MagicMock, + mock_gdown: MagicMock, + mock_get_deepface_home: MagicMock, ): # Setting up the mock return values mock_get_deepface_home.return_value = os.path.normpath("/mock/home") @@ -174,7 +175,9 @@ class TestDownloadWeightFeature: # Assert that gdown.download was called with the correct parameters mock_gdown.assert_called_once_with( - source_url, os.path.normpath("/mock/home/.deepface/weights/model_weights.h5.zip"), quiet=False + source_url, + os.path.normpath("/mock/home/.deepface/weights/model_weights.h5.zip"), + quiet=False, ) # Simulate the unzipping behavior @@ -194,13 +197,13 @@ class TestDownloadWeightFeature: def test_download_weights_if_necessary_bz2( self, - mock_open, - mock_zipfile, - mock_bz2file, - mock_makedirs, - mock_isfile, - mock_gdown, - mock_get_deepface_home, + mock_open: MagicMock, + mock_zipfile: MagicMock, + mock_bz2file: MagicMock, + mock_makedirs: MagicMock, + mock_isfile: MagicMock, + mock_gdown: MagicMock, + mock_get_deepface_home: MagicMock, ): # Setting up the mock return values @@ -222,11 +225,15 @@ class TestDownloadWeightFeature: # Assert that gdown.download was called with the correct parameters mock_gdown.assert_called_once_with( - source_url, os.path.normpath("/mock/home/.deepface/weights/model_weights.h5.bz2"), quiet=False + source_url, + os.path.normpath("/mock/home/.deepface/weights/model_weights.h5.bz2"), + quiet=False, ) # Ensure open() is called once for writing the decompressed data - mock_open.assert_called_once_with(os.path.normpath("/mock/home/.deepface/weights/model_weights.h5"), "wb") + mock_open.assert_called_once_with( + os.path.normpath("/mock/home/.deepface/weights/model_weights.h5"), "wb" + ) # TODO: find a way to check write is called @@ -237,13 +244,13 @@ class TestDownloadWeightFeature: def test_download_weights_for_non_supported_compress_type( self, - mock_open, - mock_zipfile, - mock_bz2file, - mock_makedirs, - mock_isfile, - mock_gdown, - mock_get_deepface_home, + mock_open: MagicMock, + mock_zipfile: MagicMock, + mock_bz2file: MagicMock, + mock_makedirs: MagicMock, + mock_isfile: MagicMock, + mock_gdown: MagicMock, + mock_get_deepface_home: MagicMock, ): mock_isfile.return_value = False