fast-face-python 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fast_face/__init__.py +6 -0
- fast_face/models/__init__.py +11 -0
- fast_face/models/adaface.py +91 -0
- fast_face/models/base.py +108 -0
- fast_face/models/base_recognition.py +46 -0
- fast_face/models/downloader.py +46 -0
- fast_face/models/factory.py +69 -0
- fast_face/models/retinaface.py +109 -0
- fast_face/models/session.py +29 -0
- fast_face/models/yunet.py +202 -0
- fast_face/schema.py +13 -0
- fast_face/tools.py +501 -0
- fast_face_python-0.1.1.dist-info/METADATA +237 -0
- fast_face_python-0.1.1.dist-info/RECORD +17 -0
- fast_face_python-0.1.1.dist-info/WHEEL +5 -0
- fast_face_python-0.1.1.dist-info/licenses/LICENSE +21 -0
- fast_face_python-0.1.1.dist-info/top_level.txt +1 -0
fast_face/__init__.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from ..schema import ProviderType
|
|
7
|
+
from ..tools import align_face
|
|
8
|
+
from .base_recognition import BaseRecognitionModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AdaFace(BaseRecognitionModel):
|
|
12
|
+
"""AdaFace face recognition model for embedding extraction.
|
|
13
|
+
|
|
14
|
+
Supports IR-18, IR-50, and IR-101 backbones.
|
|
15
|
+
Expects RGB input images; converts to BGR internally as required by AdaFace.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
model_path: str,
|
|
21
|
+
align_size: tuple[int, int] = (112, 112),
|
|
22
|
+
providers: list[ProviderType] | None = None,
|
|
23
|
+
**kwargs,
|
|
24
|
+
):
|
|
25
|
+
if providers is None:
|
|
26
|
+
providers = ["CPUExecutionProvider"]
|
|
27
|
+
super().__init__(
|
|
28
|
+
model_path=model_path,
|
|
29
|
+
providers=providers,
|
|
30
|
+
sess_options=kwargs.get("sess_options"),
|
|
31
|
+
)
|
|
32
|
+
self.align_size = align_size
|
|
33
|
+
|
|
34
|
+
def preprocess(self, imgs: list[np.ndarray]) -> np.ndarray:
|
|
35
|
+
"""Preprocess aligned face images: RGB->BGR, normalize to [-1, 1], transpose to NCHW."""
|
|
36
|
+
processed = []
|
|
37
|
+
for img in imgs:
|
|
38
|
+
bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
|
39
|
+
|
|
40
|
+
h, w = bgr.shape[:2]
|
|
41
|
+
if (w, h) != self.align_size:
|
|
42
|
+
bgr = cv2.resize(bgr, self.align_size)
|
|
43
|
+
|
|
44
|
+
processed.append(bgr)
|
|
45
|
+
|
|
46
|
+
batch = np.stack(processed, axis=0).astype(np.float32)
|
|
47
|
+
|
|
48
|
+
batch = (batch / 255.0 - 0.5) / 0.5
|
|
49
|
+
|
|
50
|
+
batch = np.transpose(batch, (0, 3, 1, 2))
|
|
51
|
+
return batch
|
|
52
|
+
|
|
53
|
+
def extract(
|
|
54
|
+
self,
|
|
55
|
+
imgs: list[np.ndarray] | np.ndarray,
|
|
56
|
+
landmarks: list[np.ndarray | dict[str, Any]] | None = None,
|
|
57
|
+
) -> np.ndarray:
|
|
58
|
+
"""Extract L2-normalized face embeddings from images.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
imgs: List of face images in RGB, or a single image array.
|
|
62
|
+
If landmarks are provided, images should be the full/cropped
|
|
63
|
+
frames from which faces will be aligned.
|
|
64
|
+
landmarks: Optional list of landmarks for face alignment.
|
|
65
|
+
Each element can be an np.ndarray of shape (5, 2) or (10,),
|
|
66
|
+
or a dictionary with landmark keys.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
np.ndarray: L2-normalized embeddings of shape (N, embedding_dim).
|
|
70
|
+
"""
|
|
71
|
+
if isinstance(imgs, np.ndarray) and imgs.ndim == 3:
|
|
72
|
+
imgs = [imgs]
|
|
73
|
+
|
|
74
|
+
if landmarks is not None:
|
|
75
|
+
aligned_imgs = []
|
|
76
|
+
for img, lmk in zip(imgs, landmarks):
|
|
77
|
+
aligned = align_face(img, lmk, align_size=self.align_size)
|
|
78
|
+
aligned_imgs.append(aligned)
|
|
79
|
+
else:
|
|
80
|
+
aligned_imgs = imgs
|
|
81
|
+
|
|
82
|
+
batch = self.preprocess(aligned_imgs)
|
|
83
|
+
outputs = self.session(batch)
|
|
84
|
+
|
|
85
|
+
embeddings = outputs[0]
|
|
86
|
+
|
|
87
|
+
norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
|
|
88
|
+
norms = np.maximum(norms, 1e-10)
|
|
89
|
+
embeddings = embeddings / norms
|
|
90
|
+
|
|
91
|
+
return embeddings
|
fast_face/models/base.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
import onnxruntime as ort
|
|
7
|
+
|
|
8
|
+
from ..schema import ProviderType
|
|
9
|
+
from ..tools import nms, parse_det
|
|
10
|
+
from .session import ONNXSession
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseFaceModel(ABC):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
model_path: str,
|
|
17
|
+
conf_threshold: float = 0.6,
|
|
18
|
+
nms_threshold: float = 0.4,
|
|
19
|
+
top_k: int = 5000,
|
|
20
|
+
keep_top_k: int = 1000,
|
|
21
|
+
providers: list[ProviderType] | None = None,
|
|
22
|
+
sess_options: ort.SessionOptions | None = None,
|
|
23
|
+
):
|
|
24
|
+
if providers is None:
|
|
25
|
+
providers = ["CPUExecutionProvider"]
|
|
26
|
+
self.conf_threshold = conf_threshold
|
|
27
|
+
self.nms_threshold = nms_threshold
|
|
28
|
+
self.top_k = top_k
|
|
29
|
+
self.keep_top_k = keep_top_k
|
|
30
|
+
|
|
31
|
+
self.session = ONNXSession(
|
|
32
|
+
model_path=model_path, providers=providers, sess_options=sess_options
|
|
33
|
+
)
|
|
34
|
+
self.input_name = self.session.session.get_inputs()[0].name
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def preprocess(self, imgs: np.ndarray) -> np.ndarray:
|
|
38
|
+
"""Preprocess images for the specific model."""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def post_process(
|
|
42
|
+
self,
|
|
43
|
+
outputs: list[np.ndarray],
|
|
44
|
+
original_shapes: list[tuple[int, int]],
|
|
45
|
+
preprocessed_shape: tuple[int, int],
|
|
46
|
+
) -> list[np.ndarray]:
|
|
47
|
+
"""Post-process ONNX outputs to bounding boxes, confidence, and landmarks."""
|
|
48
|
+
|
|
49
|
+
def detect(
|
|
50
|
+
self,
|
|
51
|
+
imgs: str | list[str] | np.ndarray | list[np.ndarray],
|
|
52
|
+
return_dict: bool = False,
|
|
53
|
+
) -> list[np.ndarray | list[dict[str, Any]]]:
|
|
54
|
+
"""Run face detection inference on input images."""
|
|
55
|
+
original_shapes = []
|
|
56
|
+
if isinstance(imgs, str):
|
|
57
|
+
imgs = [imgs]
|
|
58
|
+
|
|
59
|
+
if isinstance(imgs, list):
|
|
60
|
+
raw_imgs = []
|
|
61
|
+
for item in imgs:
|
|
62
|
+
if isinstance(item, str):
|
|
63
|
+
img = cv2.imread(item)
|
|
64
|
+
if img is None:
|
|
65
|
+
raise ValueError(f"Could not load image from {item}")
|
|
66
|
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
|
67
|
+
else:
|
|
68
|
+
img = item
|
|
69
|
+
original_shapes.append(img.shape)
|
|
70
|
+
raw_imgs.append(img)
|
|
71
|
+
preprocessed_imgs = self.preprocess(raw_imgs)
|
|
72
|
+
else:
|
|
73
|
+
if len(imgs.shape) == 3: # (H, W, C)
|
|
74
|
+
original_shapes.append(imgs.shape)
|
|
75
|
+
preprocessed_imgs = self.preprocess([imgs])
|
|
76
|
+
else:
|
|
77
|
+
for idx in range(imgs.shape[0]):
|
|
78
|
+
original_shapes.append(imgs[idx].shape)
|
|
79
|
+
preprocessed_imgs = self.preprocess(
|
|
80
|
+
[imgs[idx] for idx in range(imgs.shape[0])]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
_, _, h, w = preprocessed_imgs.shape
|
|
84
|
+
preprocessed_shape = (h, w)
|
|
85
|
+
|
|
86
|
+
outputs = self.session(preprocessed_imgs)
|
|
87
|
+
|
|
88
|
+
batch_dets = self.post_process(outputs, original_shapes, preprocessed_shape)
|
|
89
|
+
|
|
90
|
+
results = []
|
|
91
|
+
for i in range(len(batch_dets)):
|
|
92
|
+
dets = batch_dets[i]
|
|
93
|
+
if dets.shape[0] == 0:
|
|
94
|
+
results.append(
|
|
95
|
+
[] if return_dict else np.empty((0, 15), dtype=np.float32)
|
|
96
|
+
)
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
keep = nms(dets, self.nms_threshold)
|
|
100
|
+
dets = dets[keep, :]
|
|
101
|
+
|
|
102
|
+
dets = dets[: self.keep_top_k, :]
|
|
103
|
+
|
|
104
|
+
if return_dict:
|
|
105
|
+
dets = [parse_det(x) for x in dets]
|
|
106
|
+
results.append(dets)
|
|
107
|
+
|
|
108
|
+
return results
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import onnxruntime as ort
|
|
6
|
+
|
|
7
|
+
from ..schema import ProviderType
|
|
8
|
+
from .session import ONNXSession
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseRecognitionModel(ABC):
|
|
12
|
+
"""Base class for face recognition models that extract embeddings."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
model_path: str,
|
|
17
|
+
providers: list[ProviderType] | None = None,
|
|
18
|
+
sess_options: ort.SessionOptions | None = None,
|
|
19
|
+
):
|
|
20
|
+
if providers is None:
|
|
21
|
+
providers = ["CPUExecutionProvider"]
|
|
22
|
+
|
|
23
|
+
self.session = ONNXSession(
|
|
24
|
+
model_path=model_path, providers=providers, sess_options=sess_options
|
|
25
|
+
)
|
|
26
|
+
self.input_name = self.session.session.get_inputs()[0].name
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def preprocess(self, imgs: list[np.ndarray]) -> np.ndarray:
|
|
30
|
+
"""Preprocess aligned face images for the specific model."""
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def extract(
|
|
34
|
+
self,
|
|
35
|
+
imgs: list[np.ndarray],
|
|
36
|
+
landmarks: list[np.ndarray | dict[str, Any]] | None = None,
|
|
37
|
+
) -> np.ndarray:
|
|
38
|
+
"""Extract face embeddings from images.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
imgs: List of face images (RGB).
|
|
42
|
+
landmarks: Optional list of landmarks for alignment.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
np.ndarray: L2-normalized embeddings of shape (N, embedding_dim).
|
|
46
|
+
"""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import urllib.request
|
|
3
|
+
import logging
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
BASE_URL = "https://mwmaulana.my.id/fast_face_onnx"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def download_model(model_filename: str, target_path: str):
|
|
12
|
+
"""Download the model from the remote server if it doesn't exist.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
model_filename (str): The name of the file to download (e.g. 'yunet.onnx').
|
|
16
|
+
target_path (str): The local path where the file should be saved.
|
|
17
|
+
"""
|
|
18
|
+
if os.path.exists(target_path):
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
url = f"{BASE_URL}/{model_filename}"
|
|
22
|
+
|
|
23
|
+
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
24
|
+
|
|
25
|
+
logger.info(f"Model not found locally. Downloading {model_filename} from {url}...")
|
|
26
|
+
print(f"Downloading {model_filename} from {url}...")
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
req = urllib.request.Request(
|
|
30
|
+
url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
|
|
31
|
+
)
|
|
32
|
+
with (
|
|
33
|
+
urllib.request.urlopen(req) as response,
|
|
34
|
+
open(target_path, "wb") as out_file,
|
|
35
|
+
):
|
|
36
|
+
shutil.copyfileobj(response, out_file)
|
|
37
|
+
|
|
38
|
+
logger.info(f"Successfully downloaded to {target_path}")
|
|
39
|
+
print(f"Successfully downloaded to {target_path}")
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logger.error(f"Failed to download model {model_filename}: {e}")
|
|
42
|
+
if os.path.exists(target_path):
|
|
43
|
+
os.remove(target_path)
|
|
44
|
+
raise RuntimeError(
|
|
45
|
+
f"Failed to download {model_filename} from {url}. Error: {e}"
|
|
46
|
+
) from e
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
from .base import BaseFaceModel
|
|
5
|
+
from .base_recognition import BaseRecognitionModel
|
|
6
|
+
from .yunet import YuNet
|
|
7
|
+
from .retinaface import RetinaFace
|
|
8
|
+
from .adaface import AdaFace
|
|
9
|
+
from .downloader import download_model
|
|
10
|
+
from ..schema import MODEL_FILENAMES
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FaceModelFactory:
|
|
14
|
+
"""Factory to instantiate face detection models."""
|
|
15
|
+
|
|
16
|
+
_models = {
|
|
17
|
+
"YUNET": YuNet,
|
|
18
|
+
"RETINAFACE_MOBILENET": RetinaFace,
|
|
19
|
+
"RETINAFACE_RESNET50": RetinaFace,
|
|
20
|
+
"ADAFACE_IR101": AdaFace,
|
|
21
|
+
"ADAFACE_IR50": AdaFace,
|
|
22
|
+
"ADAFACE_IR18": AdaFace,
|
|
23
|
+
# "SCRFD": SCRFD, # To be implemented
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def get_model(
|
|
28
|
+
cls, model_type: str, **kwargs
|
|
29
|
+
) -> Union[BaseFaceModel, BaseRecognitionModel]:
|
|
30
|
+
"""Get an instance of a face detection or recognition model.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
model_type (str): The type of model to instantiate (e.g., "YUNET", "ADAFACE_IR50").
|
|
34
|
+
**kwargs: Arguments to pass to the model's constructor.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Union[BaseFaceModel, BaseRecognitionModel]: An instance of the requested model.
|
|
38
|
+
"""
|
|
39
|
+
model_type = model_type.upper()
|
|
40
|
+
if model_type not in cls._models:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Model type '{model_type}' is not supported. Supported models: {list(cls._models.keys())}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
canonical_filename = MODEL_FILENAMES.get(
|
|
46
|
+
model_type, f"{model_type.lower()}.onnx"
|
|
47
|
+
)
|
|
48
|
+
if "model_path" not in kwargs:
|
|
49
|
+
kwargs["model_path"] = os.path.join(
|
|
50
|
+
os.path.dirname(os.path.abspath(__file__)), canonical_filename
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
model_path = kwargs["model_path"]
|
|
54
|
+
if not os.path.exists(model_path):
|
|
55
|
+
os.makedirs(os.path.dirname(model_path), exist_ok=True)
|
|
56
|
+
download_model(canonical_filename, model_path)
|
|
57
|
+
|
|
58
|
+
model_class = cls._models[model_type]
|
|
59
|
+
return model_class(**kwargs)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def register_model(cls, model_type: str, model_class: type[BaseFaceModel]):
|
|
63
|
+
"""Register a custom model with the factory.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
model_type (str): The identifier for the model.
|
|
67
|
+
model_class (Type[BaseFaceModel]): The model class.
|
|
68
|
+
"""
|
|
69
|
+
cls._models[model_type.upper()] = model_class
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from ..schema import ProviderType
|
|
5
|
+
from ..tools import decode, decode_landmark, get_priorbox
|
|
6
|
+
from .base import BaseFaceModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RetinaFace(BaseFaceModel):
|
|
10
|
+
"""RetinaFace detector supporting MobileNet and ResNet50 backbones."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
model_path: str,
|
|
15
|
+
input_size: int = 640,
|
|
16
|
+
conf_threshold: float = 0.6,
|
|
17
|
+
nms_threshold: float = 0.2,
|
|
18
|
+
top_k: int = 10000,
|
|
19
|
+
keep_top_k: int = 1000,
|
|
20
|
+
variance: tuple[float, float] = (0.1, 0.2),
|
|
21
|
+
providers: list[ProviderType] | None = None,
|
|
22
|
+
**kwargs,
|
|
23
|
+
):
|
|
24
|
+
if providers is None:
|
|
25
|
+
providers = ["CPUExecutionProvider"]
|
|
26
|
+
super().__init__(
|
|
27
|
+
model_path=model_path,
|
|
28
|
+
conf_threshold=conf_threshold,
|
|
29
|
+
nms_threshold=nms_threshold,
|
|
30
|
+
top_k=top_k,
|
|
31
|
+
keep_top_k=keep_top_k,
|
|
32
|
+
providers=providers,
|
|
33
|
+
sess_options=kwargs.get("sess_options"),
|
|
34
|
+
)
|
|
35
|
+
self.input_size = input_size
|
|
36
|
+
self.variance = variance
|
|
37
|
+
|
|
38
|
+
def _resize_single(self, image: np.ndarray) -> np.ndarray:
|
|
39
|
+
"""Letterbox-resize a single image to (input_size x input_size)."""
|
|
40
|
+
h, w = image.shape[:2]
|
|
41
|
+
scale = self.input_size / max(h, w)
|
|
42
|
+
new_w, new_h = int(w * scale), int(h * scale)
|
|
43
|
+
resized = cv2.resize(image, (new_w, new_h))
|
|
44
|
+
pad_w = self.input_size - new_w
|
|
45
|
+
pad_h = self.input_size - new_h
|
|
46
|
+
return cv2.copyMakeBorder(
|
|
47
|
+
resized, 0, pad_h, 0, pad_w, cv2.BORDER_CONSTANT, value=(0, 0, 0)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def preprocess(self, imgs: list[np.ndarray]) -> np.ndarray:
|
|
51
|
+
"""Preprocess images: resize, normalize, transpose to NCHW."""
|
|
52
|
+
processed = []
|
|
53
|
+
for img in imgs:
|
|
54
|
+
resized = self._resize_single(img)
|
|
55
|
+
processed.append(resized)
|
|
56
|
+
|
|
57
|
+
batch = np.stack(processed, axis=0).astype(np.float32)
|
|
58
|
+
|
|
59
|
+
# RetinaFace normalization: subtract mean, no std division
|
|
60
|
+
mean = np.array([104.0, 117.0, 123.0], dtype=np.float32)
|
|
61
|
+
batch -= mean
|
|
62
|
+
|
|
63
|
+
# HWC -> CHW
|
|
64
|
+
batch = np.transpose(batch, (0, 3, 1, 2))
|
|
65
|
+
return batch
|
|
66
|
+
|
|
67
|
+
def post_process(
|
|
68
|
+
self,
|
|
69
|
+
outputs: list[np.ndarray],
|
|
70
|
+
original_shapes: list[tuple[int, int]],
|
|
71
|
+
preprocessed_shape: tuple[int, int],
|
|
72
|
+
) -> list[np.ndarray]:
|
|
73
|
+
"""Decode RetinaFace outputs into [x1, y1, x2, y2, score, landmarks...] arrays."""
|
|
74
|
+
loc, conf, landms = outputs
|
|
75
|
+
h, w = preprocessed_shape
|
|
76
|
+
prior_data = get_priorbox(image_size=(h, w))
|
|
77
|
+
|
|
78
|
+
results = []
|
|
79
|
+
for i in range(loc.shape[0]):
|
|
80
|
+
orig_h, orig_w = original_shapes[i][:2]
|
|
81
|
+
padded_size = max(orig_h, orig_w)
|
|
82
|
+
scale_box = np.array([padded_size] * 4, dtype=np.float32)
|
|
83
|
+
scale_lm = np.array([padded_size] * 10, dtype=np.float32)
|
|
84
|
+
|
|
85
|
+
boxes = decode(loc[i], prior_data, self.variance) * scale_box
|
|
86
|
+
landmarks = decode_landmark(landms[i], prior_data, self.variance) * scale_lm
|
|
87
|
+
scores = conf[i][:, 1]
|
|
88
|
+
|
|
89
|
+
inds = np.where(scores > self.conf_threshold)[0]
|
|
90
|
+
boxes = boxes[inds]
|
|
91
|
+
landmarks = landmarks[inds]
|
|
92
|
+
scores = scores[inds]
|
|
93
|
+
|
|
94
|
+
order = scores.argsort()[::-1][: self.top_k]
|
|
95
|
+
boxes = boxes[order]
|
|
96
|
+
landmarks = landmarks[order]
|
|
97
|
+
scores = scores[order]
|
|
98
|
+
|
|
99
|
+
if boxes.shape[0] == 0:
|
|
100
|
+
results.append(np.empty((0, 15), dtype=np.float32))
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
dets = np.hstack((boxes, scores[:, np.newaxis], landmarks)).astype(
|
|
104
|
+
np.float32, copy=False
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
results.append(dets)
|
|
108
|
+
|
|
109
|
+
return results
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import onnxruntime as ort
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from ..schema import ProviderType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ONNXSession:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
model_path: str,
|
|
12
|
+
providers: list[ProviderType] | None = None,
|
|
13
|
+
sess_options: ort.SessionOptions | None = None,
|
|
14
|
+
):
|
|
15
|
+
|
|
16
|
+
if providers is None:
|
|
17
|
+
providers = ["CPUExecutionProvider"]
|
|
18
|
+
self.session = ort.InferenceSession(
|
|
19
|
+
model_path, providers=providers, sess_options=sess_options
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def process(self, inputs: np.ndarray):
|
|
23
|
+
assert len(inputs.shape) == 4, "Inputs shape length != 4"
|
|
24
|
+
input_name = self.session.get_inputs()[0].name
|
|
25
|
+
outputs = self.session.run(None, {input_name: inputs})
|
|
26
|
+
return outputs
|
|
27
|
+
|
|
28
|
+
def __call__(self, inputs: np.ndarray):
|
|
29
|
+
return self.process(inputs=inputs)
|