From fa179c6a7a11c2e3a4c4d5677fd2eb61374962a2 Mon Sep 17 00:00:00 2001 From: yakhyo Date: Thu, 21 Nov 2024 09:28:07 +0000 Subject: [PATCH 1/3] feat: Update face alignment following insightface style --- setup.py | 2 +- uniface/alignment.py | 53 ++++++++++++++++++-------------------------- uniface/version.py | 2 +- 3 files changed, 24 insertions(+), 33 deletions(-) diff --git a/setup.py b/setup.py index 21a3eca..a4a2d0e 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ if os.path.exists("README.md"): setup( name="uniface", - version="0.1.4", + version="0.1.5", packages=find_packages(), install_requires=[ "numpy", diff --git a/uniface/alignment.py b/uniface/alignment.py index ba518b8..96f8156 100644 --- a/uniface/alignment.py +++ b/uniface/alignment.py @@ -9,18 +9,18 @@ from typing import Tuple # Reference alignment for facial landmarks (ArcFace) reference_alignment: np.ndarray = np.array( - [[ + [ [38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041] - ]], + ], dtype=np.float32 ) -def estimate_norm(landmark: np.ndarray, image_size: int = 112) -> Tuple[np.ndarray, int]: +def estimate_norm(landmark: np.ndarray, image_size: int = 112) -> np.ndarray: """ Estimate the normalization transformation matrix for facial landmarks. @@ -29,40 +29,31 @@ def estimate_norm(landmark: np.ndarray, image_size: int = 112) -> Tuple[np.ndarr image_size (int, optional): The size of the output image. Default is 112. Returns: - Tuple[np.ndarray, int]: A tuple containing: - - min_matrix (np.ndarray): The 2x3 transformation matrix for aligning the landmarks. - - min_index (int): The index of the reference alignment that resulted in the minimum error. + np.ndarray: The 2x3 transformation matrix for aligning the landmarks. Raises: - AssertionError: If the input landmark array does not have the shape (5, 2). + AssertionError: If the input landmark array does not have the shape (5, 2) + or if image_size is not a multiple of 112 or 128. """ assert landmark.shape == (5, 2), "Landmark array must have shape (5, 2)." - min_matrix: np.ndarray = np.empty((2, 3)) - min_index: int = -1 - min_error: float = float('inf') + assert image_size % 112 == 0 or image_size % 128 == 0, "Image size must be a multiple of 112 or 128." - # Prepare landmarks for transformation - landmark_transform = np.insert(landmark, 2, values=np.ones(5), axis=1) - transform = SimilarityTransform() - - # Adjust alignment based on image size - if image_size == 112: - alignment = reference_alignment + if image_size % 112 == 0: + ratio = float(image_size) / 112.0 + diff_x = 0.0 else: - alignment = (image_size / 112) * reference_alignment + ratio = float(image_size) / 128.0 + diff_x = 8.0 * ratio - # Iterate through reference alignments - for idx in np.arange(alignment.shape[0]): - transform.estimate(landmark, alignment[idx]) - matrix = transform.params[0:2, :] - results = np.dot(matrix, landmark_transform.T).T - error = np.sum(np.sqrt(np.sum((results - alignment[idx]) ** 2, axis=1))) - if error < min_error: - min_error = error - min_matrix = matrix - min_index = idx + # Adjust reference alignment based on ratio and diff_x + alignment = reference_alignment * ratio + alignment[:, 0] += diff_x - return min_matrix, min_index + # Compute the transformation matrix + transform = SimilarityTransform() + transform.estimate(landmark, alignment) + matrix = transform.params[0:2, :] + return matrix def face_alignment(image: np.ndarray, landmark: np.ndarray, image_size: int = 112) -> np.ndarray: @@ -77,8 +68,8 @@ def face_alignment(image: np.ndarray, landmark: np.ndarray, image_size: int = 11 Returns: np.ndarray: The aligned face as a NumPy array. """ - # Get the transformation matrix and pose index - M, pose_index = estimate_norm(landmark, image_size) + # Get the transformation matrix + M = estimate_norm(landmark, image_size) # Warp the input image to align the face warped = cv2.warpAffine(image, M, (image_size, image_size), borderValue=0.0) return warped diff --git a/uniface/version.py b/uniface/version.py index da2ef33..2ab3bc1 100644 --- a/uniface/version.py +++ b/uniface/version.py @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.4" +__version__ = "0.1.5" __author__ = "Yakhyokhuja Valikhujaev" From 7330b4fd6ee41f2ab86372df3960dcbdf6bfaa91 Mon Sep 17 00:00:00 2001 From: yakhyo Date: Thu, 21 Nov 2024 09:38:20 +0000 Subject: [PATCH 2/3] chore: Add repo badge [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b101150..f7e8301 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ ![Python](https://img.shields.io/badge/Python-3.8%2B-blue) [![PyPI Version](https://img.shields.io/pypi/v/uniface.svg)](https://pypi.org/project/uniface/) [![Build Status](https://github.com/yakhyo/uniface/actions/workflows/build.yml/badge.svg)](https://github.com/yakhyo/uniface/actions) +[![GitHub Repository](https://img.shields.io/badge/GitHub-Repository-blue?logo=github)](https://github.com/yakhyo/uniface) [![Downloads](https://pepy.tech/badge/uniface)](https://pepy.tech/project/uniface) [![Code Style: PEP8](https://img.shields.io/badge/code%20style-PEP8-green.svg)](https://www.python.org/dev/peps/pep-0008/) [![GitHub Release Downloads](https://img.shields.io/github/downloads/yakhyo/uniface/total.svg?label=Model%20Downloads)](https://github.com/yakhyo/uniface/releases) From da09d7497d0eaa4fd9ee9054ba0600139676e102 Mon Sep 17 00:00:00 2001 From: yakhyo Date: Sat, 23 Nov 2024 10:25:09 +0000 Subject: [PATCH 3/3] docs: Update README.md and add type annotation [skip ci] --- README.md | 16 ++++++++++++---- uniface/visualization.py | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f7e8301..e37484e 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,9 @@ uniface_inference = RetinaFace( conf_thresh=0.5, # Confidence threshold pre_nms_topk=5000, # Pre-NMS Top-K detections nms_thresh=0.4, # NMS IoU threshold - post_nms_topk=750 # Post-NMS Top-K detections + post_nms_topk=750, # Post-NMS Top-K detections + dynamic_size=False, # Arbitrary image size inference + input_size=(640, 640) # Pre-defined input image size ) ``` @@ -158,12 +160,16 @@ cv2.destroyAllWindows() #### Initialization ```python +from typings import Tuple + RetinaFace( model: str, conf_thresh: float = 0.5, pre_nms_topk: int = 5000, nms_thresh: float = 0.4, - post_nms_topk: int = 750 + post_nms_topk: int = 750, + dynamic_size: bool = False, + input_size: Tuple[int, int] = (640, 640) ) ``` @@ -176,6 +182,8 @@ RetinaFace( - `pre_nms_topk` _(int, default=5000)_: Max detections to keep before NMS. - `nms_thresh` _(float, default=0.4)_: IoU threshold for Non-Maximum Suppression. - `post_nms_topk` _(int, default=750)_: Max detections to keep after NMS. +- `dynamic_size` _(Optional[bool], default=False)_: Use dynamic input size. +- `input_size` _(Optional[Tuple[int, int]], default=(640, 640))_: Static input size for the model (width, height). --- @@ -217,7 +225,7 @@ Detects faces in the given image and returns bounding boxes and landmarks. draw_detections( image: np.ndarray, detections: Tuple[np.ndarray, np.ndarray], - vis_threshold: float + vis_threshold: float = 0.6 ) -> None ``` @@ -228,7 +236,7 @@ Draws bounding boxes and landmarks on the given image. - `image` _(np.ndarray)_: The input image in BGR format. - `detections` _(Tuple[np.ndarray, np.ndarray])_: A tuple of bounding boxes and landmarks. -- `vis_threshold` _(float)_: Minimum confidence score for visualization. +- `vis_threshold` _(float, default=0.6)_: Minimum confidence score for visualization. --- diff --git a/uniface/visualization.py b/uniface/visualization.py index ffb28b3..604c41b 100644 --- a/uniface/visualization.py +++ b/uniface/visualization.py @@ -6,7 +6,7 @@ import cv2 import numpy as np -def draw_detections(image, detections, vis_threshold=0.6): +def draw_detections(image, detections, vis_threshold: float = 0.6): """ Draw bounding boxes and landmarks on the image.