Download test/test_facial_analysis.py from ethonmax/picpocket: direct link, hf CLI and curl.
- Browser
- Download file 8.45 kB
-
https://huggingface.co/spaces/ethonmax/picpocket/resolve/main/test/test_facial_analysis.py
- Command line
-
hf download hf://spaces/ethonmax/picpocket/test/test_facial_analysis.py
-
curl -L -o test_facial_analysis.py https://huggingface.co/spaces/ethonmax/picpocket/resolve/main/test/test_facial_analysis.py
8.45 kB
| """隔离验证五官评分:不导入业务 config,不启动服务,不加载真实 AI 模型。""" | |
| import importlib.util | |
| import json | |
| from pathlib import Path | |
| import sys | |
| import types | |
| import unittest | |
| from unittest.mock import Mock, patch | |
| import cv2 # 在临时 sys.modules 补丁外加载,避免重复初始化原生扩展。 | |
| import numpy as np | |
| ROOT = Path(__file__).resolve().parents[1] | |
| def load_analyzer(available: bool = True, legacy_api: bool = True) -> tuple[types.ModuleType, Mock]: | |
| config = types.ModuleType("config") | |
| config.logger = Mock() | |
| config.DLIB_AVAILABLE = available | |
| config.FACIAL_FUSION_ALPHA = 0.35 | |
| config.FACIAL_SPREAD_FACTOR = 2.5 | |
| mediapipe = types.ModuleType("mediapipe") | |
| mediapipe.__version__ = "0.10.21" if legacy_api else "0.10.31" | |
| factory = Mock() | |
| if legacy_api: | |
| mediapipe.solutions = types.SimpleNamespace( | |
| face_mesh=types.SimpleNamespace(FaceMesh=factory) | |
| ) | |
| spec = importlib.util.spec_from_file_location("isolated_facial_analyzer", ROOT / "facial_analyzer.py") | |
| module = importlib.util.module_from_spec(spec) | |
| with patch.dict(sys.modules, {"config": config, "mediapipe": mediapipe}): | |
| spec.loader.exec_module(module) | |
| return module, factory | |
| def sample_points() -> list[tuple[int, int]]: | |
| """手工几何夹具,用于验证评分会响应五官形状变化。""" | |
| jaw = [(int(110 - 80 * np.cos(t)), int(90 + 110 * np.sin(t))) | |
| for t in np.linspace(0, np.pi, 17)] | |
| brows = [(50, 65), (60, 60), (70, 58), (80, 60), (90, 65), | |
| (130, 65), (140, 60), (150, 58), (160, 60), (170, 65)] | |
| nose = [(110, 80), (110, 95), (110, 110), (110, 125), | |
| (95, 125), (102, 130), (110, 132), (118, 130), (125, 125)] | |
| eyes = [(55, 85), (63, 80), (77, 80), (85, 85), (77, 90), (63, 90), | |
| (135, 85), (143, 80), (157, 80), (165, 85), (157, 90), (143, 90)] | |
| mouth = [(80, 155), (90, 150), (100, 148), (110, 147), (120, 148), (130, 150), | |
| (140, 155), (130, 161), (120, 164), (110, 165), (100, 164), (90, 161), | |
| (85, 155), (100, 152), (110, 151), (120, 152), (135, 155), | |
| (120, 159), (110, 160), (100, 159)] | |
| return jaw + brows + nose + eyes + mouth | |
| class FacialAnalysisTest(unittest.TestCase): | |
| def setUp(self) -> None: | |
| self.module, self.factory = load_analyzer() | |
| self.analyzer = self.module.FacialFeatureAnalyzer() | |
| self.image = np.zeros((224, 224, 3), dtype=np.uint8) | |
| self.box = [0, 0, 224, 224] | |
| def assert_unavailable(self, result: dict, reason: str) -> None: | |
| self.assertEqual("unavailable", result["analysis_method"]) | |
| self.assertFalse(result["scores_available"]) | |
| self.assertFalse(result["is_estimated"]) | |
| self.assertEqual(reason, result["fallback_reason"]) | |
| # Android 的 Map<String, Float> 不接受 null 元素,缺失时应省略整个辅助对象。 | |
| for key in ("facial_features", "harmony_score", "overall_facial_score", "radar_analysis", "beauty_timeline"): | |
| self.assertNotIn(key, result) | |
| self.assertEqual(result, json.loads(json.dumps(result, allow_nan=False))) | |
| def test_legacy_api_initializes_detector(self) -> None: | |
| self.factory.assert_called_once_with( | |
| static_image_mode=True, max_num_faces=1, refine_landmarks=True, | |
| min_detection_confidence=0.5, min_tracking_confidence=0.5, | |
| ) | |
| self.assertEqual({"ready": True, "mediapipe_version": "0.10.21", "initialization_error": None}, | |
| self.analyzer.get_status()) | |
| def test_importable_new_api_is_not_reported_ready(self) -> None: | |
| module, factory = load_analyzer(legacy_api=False) | |
| analyzer = module.FacialFeatureAnalyzer() | |
| factory.assert_not_called() | |
| self.assertFalse(analyzer.get_status()["ready"]) | |
| self.assertEqual("0.10.31", analyzer.get_status()["mediapipe_version"]) | |
| result = analyzer.analyze_facial_features(self.image, self.box, beauty_score=80) | |
| self.assert_unavailable(result, "incompatible_mediapipe_api") | |
| def test_missing_dependency_is_reported(self) -> None: | |
| module, _ = load_analyzer(available=False) | |
| analyzer = module.FacialFeatureAnalyzer() | |
| self.assertFalse(analyzer.get_status()["ready"]) | |
| self.assertIsNone(analyzer.get_status()["mediapipe_version"]) | |
| self.assert_unavailable(analyzer.analyze_facial_features(self.image, self.box, beauty_score=80), | |
| "mediapipe_unavailable") | |
| def test_model_initialization_failure_is_reported(self) -> None: | |
| self.factory.side_effect = RuntimeError("missing model asset") | |
| analyzer = self.module.FacialFeatureAnalyzer() | |
| self.assertFalse(analyzer.get_status()["ready"]) | |
| self.assertEqual("model_initialization_failed", analyzer.get_status()["initialization_error"]) | |
| self.assert_unavailable(analyzer.analyze_facial_features(self.image, self.box, beauty_score=80), | |
| "model_initialization_failed") | |
| def test_no_face_is_a_per_image_fallback(self) -> None: | |
| self.analyzer.face_mesh.process.return_value = types.SimpleNamespace(multi_face_landmarks=[]) | |
| result = self.analyzer.analyze_facial_features(self.image, self.box, beauty_score=80) | |
| self.assert_unavailable(result, "no_landmarks_detected") | |
| self.assertTrue(self.analyzer.get_status()["ready"]) | |
| self.module.logger.warning.assert_not_called() | |
| def test_inference_exception_is_reported(self) -> None: | |
| self.analyzer.face_mesh.process.side_effect = RuntimeError("inference failed") | |
| with patch.object(self.module.traceback, "print_exc"): | |
| result = self.analyzer.analyze_facial_features(self.image, self.box, beauty_score=80) | |
| self.assert_unavailable(result, "landmark_detection_failed") | |
| def test_geometry_exception_is_reported(self) -> None: | |
| with patch.object(self.analyzer, "_get_face_measurements", side_effect=RuntimeError("bad landmarks")), \ | |
| patch.object(self.module.traceback, "print_exc"): | |
| result = self.analyzer._analyze_features_from_landmarks(sample_points(), self.image.shape, beauty_score=80) | |
| self.assert_unavailable(result, "landmark_scoring_failed") | |
| def test_detected_geometry_produces_distinct_scores_and_changes_with_eyes(self) -> None: | |
| landmark = types.SimpleNamespace(y=0.1) | |
| detected = types.SimpleNamespace(landmark=[landmark] * 478) | |
| self.analyzer.face_mesh.process.return_value = types.SimpleNamespace(multi_face_landmarks=[detected]) | |
| points = sample_points() | |
| with patch.object(self.analyzer, "_convert_mediapipe_to_dlib_format", return_value=points): | |
| result = self.analyzer.analyze_facial_features(self.image, self.box, beauty_score=80) | |
| self.assertEqual("mediapipe_landmarks", result["analysis_method"]) | |
| self.assertTrue(result["scores_available"]) | |
| self.assertFalse(result["is_estimated"]) | |
| self.assertIsNone(result["fallback_reason"]) | |
| self.assertGreater(len(set(result["facial_features"].values())), 1) | |
| for index in (37, 38, 43, 44): | |
| points[index] = (points[index][0], 83) | |
| for index in (40, 41, 46, 47): | |
| points[index] = (points[index][0], 87) | |
| changed = self.analyzer._analyze_features_from_landmarks(points, self.image.shape, beauty_score=80) | |
| self.assertNotEqual(result["facial_features"]["eyes"], changed["facial_features"]["eyes"]) | |
| def test_failed_detection_never_derives_scores_from_overall_beauty(self) -> None: | |
| self.analyzer.face_mesh.process.return_value = types.SimpleNamespace(multi_face_landmarks=[]) | |
| for score in (7.287, 72.87, 8, 80): | |
| with self.subTest(score=score): | |
| result = self.analyzer.analyze_facial_features(self.image, self.box, beauty_score=score) | |
| self.assert_unavailable(result, "no_landmarks_detected") | |
| def test_individual_scoring_exception_does_not_become_a_default_score(self) -> None: | |
| with patch.object(self.analyzer, "_gaussian_score", side_effect=ValueError("invalid geometry")): | |
| result = self.analyzer._analyze_features_from_landmarks(sample_points(), self.image.shape, beauty_score=72.87) | |
| self.assert_unavailable(result, "landmark_scoring_failed") | |
| if __name__ == "__main__": | |
| unittest.main() | |