anjith2006 commited on
Commit
f66d5ed
·
verified ·
1 Parent(s): b003b36

Upload folder using huggingface_hub

Browse files
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
+ }