mirror of
https://github.com/serengil/deepface.git
synced 2025-07-21 09:20:02 +00:00
Merge branch 'serengil:master' into master
This commit is contained in:
commit
2cee991562
@ -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]],
|
||||
@ -149,22 +166,36 @@ def extract_faces(
|
||||
w = min(width - x - 1, int(current_region.w))
|
||||
h = min(height - y - 1, int(current_region.h))
|
||||
|
||||
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,
|
||||
|
@ -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
|
||||
|
||||
|
62
tests/test_landmark_sanitization.py
Normal file
62
tests/test_landmark_sanitization.py
Normal file
@ -0,0 +1,62 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from deepface.modules.detection import extract_faces, DetectedFace, FacialAreaRegion, is_valid_landmark
|
||||
from deepface.commons.logger import Logger
|
||||
|
||||
logger = Logger()
|
||||
|
||||
def sanitize_landmarks(region, width, 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, 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
|
||||
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, 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
|
||||
logger.info("Test passed: Invalid landmarks are sanitized to None.")
|
||||
|
||||
def test_extract_faces_sanitizes_landmarks(monkeypatch):
|
||||
img = np.zeros((100, 100, 3), dtype=np.uint8)
|
||||
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)
|
||||
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"]
|
||||
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
|
Loading…
x
Reference in New Issue
Block a user