Image Feature Extraction
Transformers
Safetensors
timm
edgeface
feature-extraction
face-recognition
face-verification
face-embedding
custom_code
Instructions to use anjith2006/edgeface with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use anjith2006/edgeface with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-feature-extraction", model="anjith2006/edgeface", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("anjith2006/edgeface", trust_remote_code=True, device_map="auto") - timm
How to use anjith2006/edgeface with timm:
import timm model = timm.create_model("hf_hub:anjith2006/edgeface", pretrained=True) - Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub
Browse files- config.json +16 -0
- configuration_edgeface.py +36 -0
- image_processing_edgeface.py +305 -0
- modeling_edgeface.py +99 -0
- preprocessor_config.json +22 -0
config.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"EdgeFaceModel"
|
| 4 |
+
],
|
| 5 |
+
"auto_map": {
|
| 6 |
+
"AutoConfig": "configuration_edgeface.EdgeFaceConfig",
|
| 7 |
+
"AutoModel": "modeling_edgeface.EdgeFaceModel"
|
| 8 |
+
},
|
| 9 |
+
"dtype": "float32",
|
| 10 |
+
"featdim": 512,
|
| 11 |
+
"low_rank_ratio": 0.6,
|
| 12 |
+
"model_type": "edgeface",
|
| 13 |
+
"timm_model": "edgenext_base",
|
| 14 |
+
"transformers_version": "5.12.1",
|
| 15 |
+
"use_low_rank": false
|
| 16 |
+
}
|
configuration_edgeface.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import PretrainedConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class EdgeFaceConfig(PretrainedConfig):
|
| 5 |
+
"""
|
| 6 |
+
Configuration for EdgeFace face-recognition models.
|
| 7 |
+
|
| 8 |
+
EdgeFace is a `timm` edgenext backbone with the classifier reset to output a
|
| 9 |
+
`featdim`-dimensional embedding. Some variants additionally replace their
|
| 10 |
+
nn.Linear layers with a static low-rank factorization (two smaller linears)
|
| 11 |
+
to cut parameters -- this is EdgeFace's "gamma" trick and is baked into the
|
| 12 |
+
weights. It is NOT PEFT/LoRA adapters; you can still train real LoRA on top
|
| 13 |
+
of the resulting model.
|
| 14 |
+
|
| 15 |
+
The four published variants map to:
|
| 16 |
+
edgeface_base -> timm_model="edgenext_base", use_low_rank=False
|
| 17 |
+
edgeface_s_gamma_05 -> timm_model="edgenext_small", use_low_rank=True, low_rank_ratio=0.5
|
| 18 |
+
edgeface_xs_gamma_06 -> timm_model="edgenext_x_small", use_low_rank=True, low_rank_ratio=0.6
|
| 19 |
+
edgeface_xxs -> timm_model="edgenext_xx_small", use_low_rank=False
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
model_type = "edgeface"
|
| 23 |
+
|
| 24 |
+
def __init__(
|
| 25 |
+
self,
|
| 26 |
+
timm_model: str = "edgenext_x_small",
|
| 27 |
+
featdim: int = 512,
|
| 28 |
+
use_low_rank: bool = False,
|
| 29 |
+
low_rank_ratio: float = 0.6,
|
| 30 |
+
**kwargs,
|
| 31 |
+
):
|
| 32 |
+
self.timm_model = timm_model
|
| 33 |
+
self.featdim = featdim
|
| 34 |
+
self.use_low_rank = use_low_rank
|
| 35 |
+
self.low_rank_ratio = low_rank_ratio
|
| 36 |
+
super().__init__(**kwargs)
|
image_processing_edgeface.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Image processor for EdgeFace.
|
| 3 |
+
|
| 4 |
+
Faithful port of the alignment used by the Idiap EdgeFace Space (utils.py):
|
| 5 |
+
MediaPipe FaceMesh landmarks -> 5 points -> reflective similarity transform onto
|
| 6 |
+
the ArcFace 112x112 template (custom MATLAB cp2tform-style solver).
|
| 7 |
+
|
| 8 |
+
Works with both MediaPipe backends:
|
| 9 |
+
* "tasks" -> latest API (mp.tasks.vision.FaceLandmarker + .task bundle)
|
| 10 |
+
* "solutions" -> legacy mp.solutions.face_mesh.FaceMesh (older installs)
|
| 11 |
+
The default backend="auto" tries tasks first and falls back to solutions.
|
| 12 |
+
|
| 13 |
+
Pipeline: (optional) align -> rescale to [0,1] -> normalize mean/std=0.5.
|
| 14 |
+
If do_align=False the input is treated as an already-aligned crop and only
|
| 15 |
+
resized to image_size.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import os
|
| 19 |
+
import weakref
|
| 20 |
+
from typing import List, Optional, Union
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
from numpy.linalg import inv, lstsq, matrix_rank, norm
|
| 24 |
+
from transformers.image_processing_utils import BaseImageProcessor, BatchFeature
|
| 25 |
+
from transformers.image_utils import ImageInput, make_list_of_images, to_numpy_array
|
| 26 |
+
|
| 27 |
+
# ArcFace 5-point reference template for a 112x112 crop.
|
| 28 |
+
# order matches the 5 source points: [reye, leye, nose, mouthright, mouthleft]
|
| 29 |
+
REFERENCE_FACIAL_POINTS = np.array(
|
| 30 |
+
[
|
| 31 |
+
[38.2946, 51.6963],
|
| 32 |
+
[73.5318, 51.5014],
|
| 33 |
+
[56.0252, 71.7366],
|
| 34 |
+
[41.5493, 92.3655],
|
| 35 |
+
[70.7299, 92.2041],
|
| 36 |
+
],
|
| 37 |
+
dtype=np.float32,
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# MediaPipe FaceMesh indices (from the Space's utils.py). Valid for both the
|
| 41 |
+
# 468-point legacy mesh and the 478-point tasks mesh (extra points are irises).
|
| 42 |
+
IDX_REYE = (362, 263) # eye on the image-left (subject's right)
|
| 43 |
+
IDX_LEYE = (33, 243) # eye on the image-right (subject's left)
|
| 44 |
+
IDX_NOSE = 1
|
| 45 |
+
IDX_MOUTH_RIGHT = 287 # mouth corner on the image-left
|
| 46 |
+
IDX_MOUTH_LEFT = 57 # mouth corner on the image-right
|
| 47 |
+
|
| 48 |
+
# Official Google model bundle for the tasks API.
|
| 49 |
+
_TASK_MODEL_URL = (
|
| 50 |
+
"https://storage.googleapis.com/mediapipe-models/face_landmarker/"
|
| 51 |
+
"face_landmarker/float16/1/face_landmarker.task"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
# Live MediaPipe detectors are not JSON/deepcopy-safe, so keep them off the
|
| 55 |
+
# instance __dict__ (which save_pretrained serializes) via a weak cache.
|
| 56 |
+
_RUNTIME: "weakref.WeakKeyDictionary" = weakref.WeakKeyDictionary()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# --------------------------------------------------------------------------
|
| 60 |
+
# Similarity transform utilities (ported from the Space's utils.py)
|
| 61 |
+
# --------------------------------------------------------------------------
|
| 62 |
+
def _tformfwd(trans, uv):
|
| 63 |
+
uv_h = np.hstack((uv, np.ones((uv.shape[0], 1))))
|
| 64 |
+
xy = uv_h @ trans
|
| 65 |
+
return xy[:, :-1]
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _find_nonreflective_similarity(uv, xy, K=2):
|
| 69 |
+
M = xy.shape[0]
|
| 70 |
+
x, y = xy[:, 0:1], xy[:, 1:2]
|
| 71 |
+
u, v = uv[:, 0:1], uv[:, 1:2]
|
| 72 |
+
|
| 73 |
+
X = np.vstack((
|
| 74 |
+
np.hstack((x, y, np.ones((M, 1)), np.zeros((M, 1)))),
|
| 75 |
+
np.hstack((y, -x, np.zeros((M, 1)), np.ones((M, 1)))),
|
| 76 |
+
))
|
| 77 |
+
U = np.vstack((u, v))
|
| 78 |
+
|
| 79 |
+
if matrix_rank(X) >= 2 * K:
|
| 80 |
+
r, _, _, _ = lstsq(X, U, rcond=None)
|
| 81 |
+
else:
|
| 82 |
+
raise ValueError("cp2tform:twoUniquePointsReq")
|
| 83 |
+
|
| 84 |
+
sc, ss, tx, ty = r.flatten()
|
| 85 |
+
Tinv = np.array([[sc, -ss, 0], [ss, sc, 0], [tx, ty, 1]])
|
| 86 |
+
T = inv(Tinv)
|
| 87 |
+
T[:, 2] = [0, 0, 1]
|
| 88 |
+
return T, Tinv
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _find_similarity(uv, xy):
|
| 92 |
+
trans1, trans1_inv = _find_nonreflective_similarity(uv, xy)
|
| 93 |
+
|
| 94 |
+
xyR = xy.copy()
|
| 95 |
+
xyR[:, 0] *= -1
|
| 96 |
+
trans2r, _ = _find_nonreflective_similarity(uv, xyR)
|
| 97 |
+
TreflectY = np.array([[-1, 0, 0], [0, 1, 0], [0, 0, 1]])
|
| 98 |
+
trans2 = trans2r @ TreflectY
|
| 99 |
+
|
| 100 |
+
norm1 = norm(_tformfwd(trans1, uv) - xy)
|
| 101 |
+
norm2 = norm(_tformfwd(trans2, uv) - xy)
|
| 102 |
+
return (trans1, trans1_inv) if norm1 <= norm2 else (trans2, inv(trans2))
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _get_cv2_affine(src_pts, dst_pts):
|
| 106 |
+
trans, _ = _find_similarity(src_pts, dst_pts)
|
| 107 |
+
return trans[:, :2].T # 2x3 for cv2.warpAffine
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _warp_and_crop_face(src_img, facial_pts, reference_pts=REFERENCE_FACIAL_POINTS,
|
| 111 |
+
crop_size=(112, 112), scale=1):
|
| 112 |
+
import cv2
|
| 113 |
+
|
| 114 |
+
ref_pts = reference_pts * scale
|
| 115 |
+
ref_pts = ref_pts + (np.mean(reference_pts, axis=0) - np.mean(ref_pts, axis=0))
|
| 116 |
+
|
| 117 |
+
src_pts = np.array(facial_pts, dtype=np.float32)
|
| 118 |
+
if src_pts.shape != ref_pts.shape:
|
| 119 |
+
raise ValueError("facial_pts and reference_pts must have the same shape")
|
| 120 |
+
|
| 121 |
+
tfm = _get_cv2_affine(src_pts, ref_pts)
|
| 122 |
+
return cv2.warpAffine(src_img, tfm, crop_size)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
class EdgeFaceImageProcessor(BaseImageProcessor):
|
| 126 |
+
model_input_names = ["pixel_values"]
|
| 127 |
+
|
| 128 |
+
def __init__(
|
| 129 |
+
self,
|
| 130 |
+
do_align: bool = True,
|
| 131 |
+
image_size: int = 112,
|
| 132 |
+
do_rescale: bool = True,
|
| 133 |
+
rescale_factor: float = 1 / 255,
|
| 134 |
+
do_normalize: bool = True,
|
| 135 |
+
image_mean: Optional[List[float]] = None,
|
| 136 |
+
image_std: Optional[List[float]] = None,
|
| 137 |
+
mp_backend: str = "auto", # "auto" | "tasks" | "solutions"
|
| 138 |
+
mp_model_path: Optional[str] = None, # path to a .task bundle (tasks backend)
|
| 139 |
+
**kwargs,
|
| 140 |
+
):
|
| 141 |
+
super().__init__(**kwargs)
|
| 142 |
+
self.do_align = do_align
|
| 143 |
+
self.image_size = image_size
|
| 144 |
+
self.do_rescale = do_rescale
|
| 145 |
+
self.rescale_factor = rescale_factor
|
| 146 |
+
self.do_normalize = do_normalize
|
| 147 |
+
self.image_mean = image_mean if image_mean is not None else [0.5, 0.5, 0.5]
|
| 148 |
+
self.image_std = image_std if image_std is not None else [0.5, 0.5, 0.5]
|
| 149 |
+
self.mp_backend = mp_backend
|
| 150 |
+
self.mp_model_path = mp_model_path
|
| 151 |
+
|
| 152 |
+
# -- runtime (non-serialized) cache ------------------------------------
|
| 153 |
+
def _runtime(self):
|
| 154 |
+
d = _RUNTIME.get(self)
|
| 155 |
+
if d is None:
|
| 156 |
+
d = {}
|
| 157 |
+
_RUNTIME[self] = d
|
| 158 |
+
return d
|
| 159 |
+
|
| 160 |
+
# -- model bundle for the tasks backend --------------------------------
|
| 161 |
+
def _resolve_model_path(self) -> str:
|
| 162 |
+
if self.mp_model_path:
|
| 163 |
+
return self.mp_model_path
|
| 164 |
+
env = os.environ.get("EDGEFACE_MP_MODEL")
|
| 165 |
+
if env:
|
| 166 |
+
return env
|
| 167 |
+
cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "edgeface")
|
| 168 |
+
os.makedirs(cache_dir, exist_ok=True)
|
| 169 |
+
path = os.path.join(cache_dir, "face_landmarker.task")
|
| 170 |
+
if not os.path.exists(path):
|
| 171 |
+
import urllib.request
|
| 172 |
+
urllib.request.urlretrieve(_TASK_MODEL_URL, path)
|
| 173 |
+
return path
|
| 174 |
+
|
| 175 |
+
# -- backend builders: each returns fn(rgb_uint8) -> (N,2) norm or None -
|
| 176 |
+
def _build_tasks_detector(self):
|
| 177 |
+
import mediapipe as mp
|
| 178 |
+
from mediapipe.tasks import python as mp_python
|
| 179 |
+
from mediapipe.tasks.python import vision as mp_vision
|
| 180 |
+
|
| 181 |
+
options = mp_vision.FaceLandmarkerOptions(
|
| 182 |
+
base_options=mp_python.BaseOptions(model_asset_path=self._resolve_model_path()),
|
| 183 |
+
running_mode=mp_vision.RunningMode.IMAGE,
|
| 184 |
+
num_faces=1,
|
| 185 |
+
)
|
| 186 |
+
landmarker = mp_vision.FaceLandmarker.create_from_options(options)
|
| 187 |
+
|
| 188 |
+
def detect(rgb):
|
| 189 |
+
mp_img = mp.Image(image_format=mp.ImageFormat.SRGB, data=np.ascontiguousarray(rgb))
|
| 190 |
+
res = landmarker.detect(mp_img)
|
| 191 |
+
if not res.face_landmarks:
|
| 192 |
+
return None
|
| 193 |
+
return np.array([[p.x, p.y] for p in res.face_landmarks[0]], dtype=np.float32)
|
| 194 |
+
|
| 195 |
+
return detect
|
| 196 |
+
|
| 197 |
+
def _build_solutions_detector(self):
|
| 198 |
+
import mediapipe as mp
|
| 199 |
+
|
| 200 |
+
face_mesh = mp.solutions.face_mesh.FaceMesh(
|
| 201 |
+
static_image_mode=True, refine_landmarks=True, min_detection_confidence=0.5,
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
def detect(rgb):
|
| 205 |
+
res = face_mesh.process(rgb)
|
| 206 |
+
if not res.multi_face_landmarks:
|
| 207 |
+
return None
|
| 208 |
+
return np.array([[p.x, p.y] for p in res.multi_face_landmarks[0].landmark],
|
| 209 |
+
dtype=np.float32)
|
| 210 |
+
|
| 211 |
+
return detect
|
| 212 |
+
|
| 213 |
+
def _get_detect_fn(self):
|
| 214 |
+
runtime = self._runtime()
|
| 215 |
+
if "detect_fn" in runtime:
|
| 216 |
+
return runtime["detect_fn"]
|
| 217 |
+
|
| 218 |
+
order = {
|
| 219 |
+
"auto": ["tasks", "solutions"],
|
| 220 |
+
"tasks": ["tasks"],
|
| 221 |
+
"solutions": ["solutions"],
|
| 222 |
+
}.get(self.mp_backend)
|
| 223 |
+
if order is None:
|
| 224 |
+
raise ValueError(f"Unknown mp_backend={self.mp_backend!r}")
|
| 225 |
+
|
| 226 |
+
errors = []
|
| 227 |
+
for backend in order:
|
| 228 |
+
try:
|
| 229 |
+
fn = (self._build_tasks_detector() if backend == "tasks"
|
| 230 |
+
else self._build_solutions_detector())
|
| 231 |
+
runtime["detect_fn"] = fn
|
| 232 |
+
return fn
|
| 233 |
+
except Exception as e: # noqa: BLE001 - try next backend
|
| 234 |
+
errors.append(f"{backend}: {type(e).__name__}: {e}")
|
| 235 |
+
|
| 236 |
+
raise ImportError(
|
| 237 |
+
"Could not initialize a MediaPipe face detector. Install mediapipe "
|
| 238 |
+
"(`pip install mediapipe`) and ensure network access for the .task "
|
| 239 |
+
"bundle, or pass do_align=False / precomputed landmarks.\n"
|
| 240 |
+
+ "\n".join(errors)
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
# -- landmark extraction -----------------------------------------------
|
| 244 |
+
def _detect_landmarks(self, image_rgb: np.ndarray) -> Optional[np.ndarray]:
|
| 245 |
+
"""Return the 5 source points in [reye, leye, nose, mouthright, mouthleft] order."""
|
| 246 |
+
h, w = image_rgb.shape[:2]
|
| 247 |
+
norm_pts = self._get_detect_fn()(image_rgb)
|
| 248 |
+
if norm_pts is None or len(norm_pts) <= max(*IDX_REYE, *IDX_LEYE, IDX_MOUTH_RIGHT):
|
| 249 |
+
return None
|
| 250 |
+
|
| 251 |
+
px = norm_pts * np.array([w, h], dtype=np.float32)
|
| 252 |
+
|
| 253 |
+
reye = (px[IDX_REYE[0]] + px[IDX_REYE[1]]) / 2.0
|
| 254 |
+
leye = (px[IDX_LEYE[0]] + px[IDX_LEYE[1]]) / 2.0
|
| 255 |
+
return np.stack([reye, leye, px[IDX_NOSE], px[IDX_MOUTH_RIGHT], px[IDX_MOUTH_LEFT]]).astype(np.float32)
|
| 256 |
+
|
| 257 |
+
def _align_one(self, image_rgb: np.ndarray, landmarks: Optional[np.ndarray]) -> np.ndarray:
|
| 258 |
+
if landmarks is None:
|
| 259 |
+
landmarks = self._detect_landmarks(image_rgb)
|
| 260 |
+
if landmarks is None:
|
| 261 |
+
import cv2 # detection failed -> plain resize so the batch still runs
|
| 262 |
+
return cv2.resize(image_rgb, (self.image_size, self.image_size))
|
| 263 |
+
return _warp_and_crop_face(image_rgb, landmarks, crop_size=(self.image_size, self.image_size))
|
| 264 |
+
|
| 265 |
+
# -- main entry point --------------------------------------------------
|
| 266 |
+
def preprocess(
|
| 267 |
+
self,
|
| 268 |
+
images: ImageInput,
|
| 269 |
+
do_align: Optional[bool] = None,
|
| 270 |
+
landmarks: Optional[Union[np.ndarray, List[np.ndarray]]] = None,
|
| 271 |
+
return_tensors: Optional[str] = "pt",
|
| 272 |
+
**kwargs,
|
| 273 |
+
) -> BatchFeature:
|
| 274 |
+
do_align = self.do_align if do_align is None else do_align
|
| 275 |
+
images = make_list_of_images(images)
|
| 276 |
+
|
| 277 |
+
if landmarks is not None and not isinstance(landmarks, list):
|
| 278 |
+
landmarks = [landmarks]
|
| 279 |
+
|
| 280 |
+
processed = []
|
| 281 |
+
for i, img in enumerate(images):
|
| 282 |
+
arr = to_numpy_array(img) # RGB, HxWxC
|
| 283 |
+
if arr.ndim == 2:
|
| 284 |
+
arr = np.stack([arr] * 3, axis=-1)
|
| 285 |
+
if arr.shape[-1] == 4:
|
| 286 |
+
arr = arr[..., :3]
|
| 287 |
+
arr = arr.astype(np.uint8)
|
| 288 |
+
|
| 289 |
+
if do_align:
|
| 290 |
+
lmk = landmarks[i] if landmarks is not None else None
|
| 291 |
+
arr = self._align_one(arr, lmk)
|
| 292 |
+
else:
|
| 293 |
+
import cv2
|
| 294 |
+
arr = cv2.resize(arr, (self.image_size, self.image_size))
|
| 295 |
+
|
| 296 |
+
arr = arr.astype(np.float32)
|
| 297 |
+
if self.do_rescale:
|
| 298 |
+
arr = arr * self.rescale_factor
|
| 299 |
+
if self.do_normalize:
|
| 300 |
+
arr = (arr - np.array(self.image_mean)) / np.array(self.image_std)
|
| 301 |
+
|
| 302 |
+
processed.append(arr.transpose(2, 0, 1)) # CxHxW
|
| 303 |
+
|
| 304 |
+
pixel_values = np.stack(processed, axis=0).astype(np.float32)
|
| 305 |
+
return BatchFeature(data={"pixel_values": pixel_values}, tensor_type=return_tensors)
|
modeling_edgeface.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import Optional
|
| 3 |
+
|
| 4 |
+
import timm
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
from transformers import PreTrainedModel
|
| 9 |
+
from transformers.utils import ModelOutput
|
| 10 |
+
|
| 11 |
+
try: # relative import works as Hub remote code; absolute works for local scripts
|
| 12 |
+
from .configuration_edgeface import EdgeFaceConfig
|
| 13 |
+
except ImportError:
|
| 14 |
+
from configuration_edgeface import EdgeFaceConfig
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# ---------------------------------------------------------------------------
|
| 18 |
+
# Static low-rank linear factorization (EdgeFace's "gamma" trick).
|
| 19 |
+
# This is baked into the pretrained weights and is unrelated to PEFT/LoRA
|
| 20 |
+
# adapters -- naming it LowRankLinear keeps "lora" free for real adapters.
|
| 21 |
+
# The submodule attribute names (linear1, linear2) are kept so the original
|
| 22 |
+
# published checkpoints load unchanged.
|
| 23 |
+
# ---------------------------------------------------------------------------
|
| 24 |
+
class LowRankLinear(nn.Module):
|
| 25 |
+
def __init__(self, in_features, out_features, rank, bias=True):
|
| 26 |
+
super().__init__()
|
| 27 |
+
self.in_features = in_features
|
| 28 |
+
self.out_features = out_features
|
| 29 |
+
self.rank = rank
|
| 30 |
+
self.linear1 = nn.Linear(in_features, rank, bias=False)
|
| 31 |
+
self.linear2 = nn.Linear(rank, out_features, bias=bias)
|
| 32 |
+
|
| 33 |
+
def forward(self, x):
|
| 34 |
+
return self.linear2(self.linear1(x))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _factorize_recursive(module, ratio):
|
| 38 |
+
for name, child in module.named_children():
|
| 39 |
+
if isinstance(child, nn.Linear) and "head" not in name:
|
| 40 |
+
rank = max(2, int(min(child.in_features, child.out_features) * ratio))
|
| 41 |
+
bias = child.bias is not None
|
| 42 |
+
setattr(module, name, LowRankLinear(child.in_features, child.out_features, rank, bias))
|
| 43 |
+
else:
|
| 44 |
+
_factorize_recursive(child, ratio)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def factorize_linear_layers(module, ratio=0.2):
|
| 48 |
+
"""Replace eligible nn.Linear layers with LowRankLinear, in place."""
|
| 49 |
+
_factorize_recursive(module, ratio)
|
| 50 |
+
return module
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@dataclass
|
| 54 |
+
class EdgeFaceOutput(ModelOutput):
|
| 55 |
+
embeddings: Optional[torch.FloatTensor] = None
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class EdgeFaceModel(PreTrainedModel):
|
| 59 |
+
config_class = EdgeFaceConfig
|
| 60 |
+
main_input_name = "pixel_values"
|
| 61 |
+
|
| 62 |
+
def __init__(self, config: EdgeFaceConfig):
|
| 63 |
+
super().__init__(config)
|
| 64 |
+
|
| 65 |
+
# Keep the attribute named `self.model` so the original published
|
| 66 |
+
# checkpoints (keys prefixed with "model.") load unchanged.
|
| 67 |
+
self.model = timm.create_model(config.timm_model)
|
| 68 |
+
self.model.reset_classifier(config.featdim)
|
| 69 |
+
|
| 70 |
+
if config.use_low_rank:
|
| 71 |
+
factorize_linear_layers(self.model, ratio=config.low_rank_ratio)
|
| 72 |
+
|
| 73 |
+
self.post_init()
|
| 74 |
+
|
| 75 |
+
def _init_weights(self, module):
|
| 76 |
+
if isinstance(module, nn.Linear):
|
| 77 |
+
nn.init.trunc_normal_(module.weight, std=0.02)
|
| 78 |
+
if module.bias is not None:
|
| 79 |
+
nn.init.zeros_(module.bias)
|
| 80 |
+
|
| 81 |
+
def forward(
|
| 82 |
+
self,
|
| 83 |
+
pixel_values: torch.FloatTensor,
|
| 84 |
+
normalize: bool = False,
|
| 85 |
+
return_dict: Optional[bool] = None,
|
| 86 |
+
**kwargs,
|
| 87 |
+
):
|
| 88 |
+
return_dict = return_dict if return_dict is not None else self.config.return_dict
|
| 89 |
+
|
| 90 |
+
embeddings = self.model(pixel_values)
|
| 91 |
+
|
| 92 |
+
# Reference code does not normalize inside the model (it normalizes at
|
| 93 |
+
# comparison time via cosine similarity). Off by default for parity.
|
| 94 |
+
if normalize:
|
| 95 |
+
embeddings = F.normalize(embeddings, dim=-1)
|
| 96 |
+
|
| 97 |
+
if not return_dict:
|
| 98 |
+
return (embeddings,)
|
| 99 |
+
return EdgeFaceOutput(embeddings=embeddings)
|
preprocessor_config.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"auto_map": {
|
| 3 |
+
"AutoImageProcessor": "image_processing_edgeface.EdgeFaceImageProcessor"
|
| 4 |
+
},
|
| 5 |
+
"do_align": true,
|
| 6 |
+
"do_normalize": true,
|
| 7 |
+
"do_rescale": true,
|
| 8 |
+
"image_mean": [
|
| 9 |
+
0.5,
|
| 10 |
+
0.5,
|
| 11 |
+
0.5
|
| 12 |
+
],
|
| 13 |
+
"image_processor_type": "EdgeFaceImageProcessor",
|
| 14 |
+
"image_size": 112,
|
| 15 |
+
"image_std": [
|
| 16 |
+
0.5,
|
| 17 |
+
0.5,
|
| 18 |
+
0.5
|
| 19 |
+
],
|
| 20 |
+
"mp_backend": "auto",
|
| 21 |
+
"rescale_factor": 0.00392156862745098
|
| 22 |
+
}
|