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