diff --git a/.env.example b/.env.example index 4d9a061..a3d0872 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,6 @@ AI_PRIVACY_ENABLE_IDENTITY_EXCLUSION=true AI_PRIVACY_IDENTITY_RECHECK_INTERVAL=12 AI_PRIVACY_IDENTITY_NEGATIVE_RECHECK_INTERVAL=90 AI_PRIVACY_ME_IMAGE_PATH=me.jpeg -AI_PRIVACY_FACE_MODEL_PATH=models/yolov8n-face.pt +AI_PRIVACY_FACE_MODEL_PATH=models/yolov12n-face.pt AI_PRIVACY_FACE_DETECTION_MODEL_PATH=models/face_detection_yunet_2023mar.onnx AI_PRIVACY_FACE_RECOGNITION_MODEL_PATH=models/face_recognition_sface_2021dec.onnx diff --git a/README.md b/README.md index 8da9aac..432117b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Mac 로컬 실행은 위와 같이 CPU 모드를 사용한다. 운영 환경은 ## AI privacy runtime -`privacy_blur`는 모든 WebRTC 스트림이 YOLOv8n-Face, YuNet, SFace 모델 세트를 +`privacy_blur`는 모든 WebRTC 스트림이 YOLOv12n-Face, YuNet, SFace 모델 세트를 공유하고 각 스트림은 독립적인 얼굴 추적 상태만 유지한다. 공유 워커가 짧은 시간 동안 들어온 프레임을 최대 `AI_PRIVACY_MAX_BATCH_SIZE`개까지 모아 한 번에 추론하며, 대기열은 `AI_PRIVACY_MAX_PENDING_FRAMES`로 제한된다. diff --git a/app/core/config.py b/app/core/config.py index 8b36f40..119622d 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -27,7 +27,7 @@ class Settings(BaseSettings): ai_privacy_identity_recheck_interval: int = 12 ai_privacy_identity_negative_recheck_interval: int = 90 ai_privacy_me_image_path: str | None = None - ai_privacy_face_model_path: str = "models/yolov8n-face.pt" + ai_privacy_face_model_path: str = "models/yolov12n-face.pt" ai_privacy_face_detection_model_path: str = ( "models/face_detection_yunet_2023mar.onnx" ) diff --git a/models/yolov12n-face.pt b/models/yolov12n-face.pt new file mode 100644 index 0000000..10b0411 Binary files /dev/null and b/models/yolov12n-face.pt differ diff --git a/privacy_blur/config.py b/privacy_blur/config.py index 91bdfc8..2552dae 100644 --- a/privacy_blur/config.py +++ b/privacy_blur/config.py @@ -7,7 +7,7 @@ @dataclass(frozen=True, slots=True) class PrivacyBlurConfig: - face_model_path: Path = Path("models/yolov8n-face.pt") + face_model_path: Path = Path("models/yolov12n-face.pt") face_imgsz: int = 640 face_conf_threshold: float = 0.22 face_iou_threshold: float = 0.45 diff --git a/privacy_blur/runtime.py b/privacy_blur/runtime.py index cb3bb9b..344ae7a 100644 --- a/privacy_blur/runtime.py +++ b/privacy_blur/runtime.py @@ -1015,6 +1015,7 @@ def _blur_frames( y = torch.arange(height, device=self.device, dtype=dtype).view(height, 1) x = torch.arange(width, device=self.device, dtype=dtype).view(1, width) + max_face_size = 0.0 for batch_index, frame_index in enumerate(indices): for box in boxes_per_frame[frame_index]: box_x, box_y, box_w, box_h = box[:4].astype(float) @@ -1039,14 +1040,12 @@ def _blur_frames( masks[batch_index, 0] = torch.maximum( masks[batch_index, 0], ellipse.to(dtype) ) + max_face_size = max(max_face_size, max(box_w, box_h)) - blurred = self._separable_gaussian( - images, _make_odd(self.config.blur_kernel) - ) - feathered = self._separable_gaussian( - masks, - _make_odd(self.config.feather * 2 + 1), - ).clamp(0, 1) + blur_kernel = self._adaptive_blur_kernel(max_face_size) + blurred = self._separable_gaussian(images, blur_kernel) + feather_kernel = _make_odd(self._adaptive_feather(max_face_size)) + feathered = self._separable_gaussian(masks, feather_kernel).clamp(0, 1) protected = images * (1.0 - feathered) + blurred * feathered protected = protected.clamp(0, 255).round().to(torch.uint8) protected_numpy = protected.permute(0, 2, 3, 1).contiguous().cpu().numpy() @@ -1054,6 +1053,20 @@ def _blur_frames( outputs[frame_index] = protected_numpy[batch_index] return outputs + def _adaptive_blur_kernel(self, max_face_size: float) -> int: + base = self.config.blur_kernel + reference = float(self.config.small_face_threshold) + ratio = max_face_size / reference + scaled = base * max(1.0, min(ratio, 3.0)) + capped = min(scaled, 151.0) + return _make_odd(int(capped)) + + def _adaptive_feather(self, max_face_size: float) -> int: + base = self.config.feather + reference = float(self.config.small_face_threshold) + ratio = max_face_size / reference + return int(base * max(1.0, min(ratio, 2.0))) + def _separable_gaussian( self, tensor: torch.Tensor, kernel_size: int ) -> torch.Tensor: diff --git a/tests/test_performance_runtime.py b/tests/test_performance_runtime.py index 706ed24..8021498 100644 --- a/tests/test_performance_runtime.py +++ b/tests/test_performance_runtime.py @@ -17,7 +17,7 @@ def make_test_config(**overrides) -> PrivacyBlurConfig: defaults = { - "face_model_path": ROOT / "models/yolov8n-face.pt", + "face_model_path": ROOT / "models/yolov12n-face.pt", "me_image_path": ROOT / "me.jpeg", "face_detection_model_path": ROOT / "models/face_detection_yunet_2023mar.onnx", "face_recognition_model_path": ROOT diff --git a/tests/test_privacy_blur.py b/tests/test_privacy_blur.py index c89897e..a633134 100644 --- a/tests/test_privacy_blur.py +++ b/tests/test_privacy_blur.py @@ -15,7 +15,7 @@ def _build_config(**overrides): from privacy_blur import PrivacyBlurConfig defaults = { - "face_model_path": ROOT / "models/yolov8n-face.pt", + "face_model_path": ROOT / "models/yolov12n-face.pt", "face_detection_model_path": ROOT / "models/face_detection_yunet_2023mar.onnx", "face_recognition_model_path": ROOT / "models/face_recognition_sface_2021dec.onnx",