3 Commits

Author SHA1 Message Date
yakhyo
da8a5cf35b feat: Add yolov5n, update docs and ruff code format 2025-12-11 01:02:18 +09:00
Yakhyokhuja Valikhujaev
3982d677a9 fix: Fix type conversion and remove redundant type conversion (#29)
* ref: Remove type conversion and update face class

* fix: change the type to float32

* chore: Update all examples, testing with latest version

* docs: Update docs reflecting the recent changes
2025-12-10 00:18:11 +09:00
Yakhyokhuja Valikhujaev
f4458f0550 Revise model configurations in README.md
Updated model names and confidence thresholds for SCRFD and YOLOv5Face in the README.
2025-12-08 10:07:30 +09:00
23 changed files with 244 additions and 154 deletions

View File

@@ -56,3 +56,5 @@ Example notebooks demonstrating library usage:
Open an issue or start a discussion on GitHub.

View File

@@ -80,10 +80,11 @@ detector = SCRFD(
YOLOv5-Face models provide excellent detection accuracy with 5-point facial landmarks, optimized for real-time applications.
| Model Name | Params | Size | Easy | Medium | Hard | FLOPs (G) | Use Case |
| -------------- | ------ | ---- | ------ | ------ | ------ | --------- | ------------------------------ |
| `YOLOV5S` ⭐ | 7.1M | 28MB | 94.33% | 92.61% | 83.15% | 5.751 | **Real-time + accuracy** |
| `YOLOV5M` | 21.1M | 84MB | 95.30% | 93.76% | 85.28% | 18.146 | High accuracy |
| Model Name | Size | Easy | Medium | Hard | Use Case |
| -------------- | ---- | ------ | ------ | ------ | ------------------------------ |
| `YOLOV5N` | 11MB | 93.61% | 91.52% | 80.53% | Lightweight/Mobile |
| `YOLOV5S` | 28MB | 94.33% | 92.61% | 83.15% | **Real-time + accuracy** |
| `YOLOV5M` | 82MB | 95.30% | 93.76% | 85.28% | High accuracy |
**Accuracy**: WIDER FACE validation set - from [YOLOv5-Face paper](https://arxiv.org/abs/2105.12931)
**Speed**: Benchmark on your own hardware using `scripts/run_detection.py --iterations 100`
@@ -95,6 +96,13 @@ YOLOv5-Face models provide excellent detection accuracy with 5-point facial land
from uniface import YOLOv5Face
from uniface.constants import YOLOv5FaceWeights
# Lightweight/Mobile
detector = YOLOv5Face(
model_name=YOLOv5FaceWeights.YOLOV5N,
conf_thresh=0.6,
nms_thresh=0.5
)
# Real-time detection (recommended)
detector = YOLOv5Face(
model_name=YOLOv5FaceWeights.YOLOV5S,
@@ -251,9 +259,9 @@ landmarks = landmarker.get_landmarks(image, bbox)
from uniface import AgeGender
predictor = AgeGender()
gender_id, age = predictor.predict(image, bbox)
# Returns: (gender_id, age_in_years)
# gender_id: 0 for Female, 1 for Male
gender, age = predictor.predict(image, bbox)
# Returns: (gender, age_in_years)
# gender: 0 for Female, 1 for Male
```
---

View File

@@ -199,9 +199,9 @@ faces = detector.detect(image)
# Predict attributes
for i, face in enumerate(faces):
gender_id, age = age_gender.predict(image, face['bbox'])
gender = 'Female' if gender_id == 0 else 'Male'
print(f"Face {i+1}: {gender}, {age} years old")
gender, age = age_gender.predict(image, face['bbox'])
gender_str = 'Female' if gender == 0 else 'Male'
print(f"Face {i+1}: {gender_str}, {age} years old")
```
**Output:**

View File

@@ -147,9 +147,9 @@ detector = RetinaFace()
age_gender = AgeGender()
faces = detector.detect(image)
gender_id, age = age_gender.predict(image, faces[0]['bbox'])
gender = 'Female' if gender_id == 0 else 'Male'
print(f"{gender}, {age} years old")
gender, age = age_gender.predict(image, faces[0]['bbox'])
gender_str = 'Female' if gender == 0 else 'Male'
print(f"{gender_str}, {age} years old")
```
---
@@ -171,15 +171,18 @@ from uniface.detection import RetinaFace, SCRFD
from uniface.recognition import ArcFace
from uniface.landmark import Landmark106
from uniface.constants import SCRFDWeights
# Create detector with default settings
detector = RetinaFace()
# Create with custom config
detector = SCRFD(
model_name='scrfd_10g_kps',
conf_thresh=0.8,
model_name=SCRFDWeights.SCRFD_10G_KPS, # SCRFDWeights.SCRFD_500M_KPS
conf_thresh=0.4,
input_size=(640, 640)
)
# Or with defaults settings: detector = SCRFD()
# Recognition and landmarks
recognizer = ArcFace()
@@ -198,6 +201,7 @@ detector = RetinaFace(
conf_thresh=0.5,
nms_thresh=0.4
)
# Or detector = RetinaFace()
# YOLOv5-Face detection
detector = YOLOv5Face(
@@ -205,6 +209,7 @@ detector = YOLOv5Face(
conf_thresh=0.6,
nms_thresh=0.5
)
# Or detector = YOLOv5Face
# Recognition
recognizer = ArcFace() # Uses default weights
@@ -229,7 +234,7 @@ faces = detect_faces(image, method='retinaface', conf_thresh=0.8) # methods: re
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------- |
| `RetinaFace` | `model_name=RetinaFaceWeights.MNET_V2`, `conf_thresh=0.5`, `nms_thresh=0.4`, `input_size=(640, 640)`, `dynamic_size=False` | Supports 5-point landmarks |
| `SCRFD` | `model_name=SCRFDWeights.SCRFD_10G_KPS`, `conf_thresh=0.5`, `nms_thresh=0.4`, `input_size=(640, 640)` | Supports 5-point landmarks |
| `YOLOv5Face` | `model_name=YOLOv5FaceWeights.YOLOV5S`, `conf_thresh=0.6`, `nms_thresh=0.5`, `input_size=640` (fixed) | Landmarks supported;`input_size` must be 640 |
| `YOLOv5Face` | `model_name=YOLOv5FaceWeights.YOLOV5S`, `conf_thresh=0.6`, `nms_thresh=0.5`, `input_size=640` (fixed) | Supports 5-point landmarks; models: YOLOV5N/S/M; `input_size` must be 640 |
**Recognition**
@@ -260,6 +265,7 @@ faces = detect_faces(image, method='retinaface', conf_thresh=0.8) # methods: re
| retinaface_r34 | 94.16% | 93.12% | 88.90% | High accuracy |
| scrfd_500m | 90.57% | 88.12% | 68.51% | Real-time applications |
| scrfd_10g | 95.16% | 93.87% | 83.05% | Best accuracy/speed |
| yolov5n_face | 93.61% | 91.52% | 80.53% | Lightweight/Mobile |
| yolov5s_face | 94.33% | 92.61% | 83.15% | Real-time + accuracy |
| yolov5m_face | 95.30% | 93.76% | 85.28% | High accuracy |

File diff suppressed because one or more lines are too long

View File

@@ -13,9 +13,17 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 1,
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Note: you may need to restart the kernel to use updated packages.\n"
]
}
],
"source": [
"%pip install -q uniface"
]
@@ -29,14 +37,14 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"1.3.0\n"
"1.3.1\n"
]
}
],
@@ -65,7 +73,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 3,
"metadata": {},
"outputs": [
{
@@ -95,7 +103,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 4,
"metadata": {},
"outputs": [
{
@@ -136,7 +144,7 @@
"\n",
" # Print face attributes\n",
" for i, face in enumerate(faces, 1):\n",
" print(f' Face {i}: {face.gender}, {face.age}y')\n",
" print(f' Face {i}: {face.sex}, {face.age}y')\n",
"\n",
" # Prepare visualization (without text overlay)\n",
" vis_image = image.copy()\n",
@@ -159,7 +167,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 5,
"metadata": {},
"outputs": [
{
@@ -186,7 +194,7 @@
" axes[1, idx].axis('off')\n",
" info_text = f'{len(faces)} face(s)\\n'\n",
" for i, face in enumerate(faces, 1):\n",
" info_text += f'Face {i}: {face.gender}, {face.age}y\\n'\n",
" info_text += f'Face {i}: {face.sex}, {face.age}y\\n'\n",
"\n",
" axes[1, idx].text(0.5, 0.5, info_text,\n",
" ha='center', va='center',\n",
@@ -207,7 +215,7 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 6,
"metadata": {},
"outputs": [
{
@@ -236,7 +244,7 @@
" print(f' - Confidence: {face.confidence:.3f}')\n",
" print(f' - Landmarks shape: {face.landmarks.shape}')\n",
" print(f' - Age: {face.age} years')\n",
" print(f' - Gender: {face.gender}')\n",
" print(f' - Gender: {face.sex}')\n",
" print(f' - Embedding shape: {face.embedding.shape}')\n",
" print(f' - Embedding dimension: {face.embedding.shape[1]}D')"
]
@@ -252,14 +260,14 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Similarity between faces: 0.1201\n",
"Similarity between faces: 0.1135\n",
"Same person: No (threshold=0.6)\n"
]
}
@@ -283,7 +291,7 @@
"\n",
"- `analyzer.analyze()` performs detection, recognition, and attribute prediction in one call\n",
"- Each `Face` object contains: `bbox`, `confidence`, `landmarks`, `embedding`, `age`, `gender`\n",
"- Gender is available as both ID (0=Female, 1=Male) and string via `face.gender` property\n",
"- Gender is available as both ID (0=Female, 1=Male) and string via `face.sex` property\n",
"- Face embeddings are L2-normalized (norm ≈ 1.0) for similarity computation\n",
"- Use `face.compute_similarity(other_face)` to compare faces (returns cosine similarity)\n",
"- Typical similarity threshold: 0.6 (same person if similarity > 0.6)"
@@ -297,7 +305,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"display_name": "base",
"language": "python",
"name": "python3"
},
@@ -311,7 +319,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.0"
"version": "3.13.5"
}
},
"nbformat": 4,

View File

@@ -13,9 +13,17 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 1,
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Note: you may need to restart the kernel to use updated packages.\n"
]
}
],
"source": [
"%pip install -q uniface"
]
@@ -29,14 +37,14 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"1.3.0\n"
"1.3.1\n"
]
}
],
@@ -61,7 +69,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 3,
"metadata": {},
"outputs": [
{
@@ -88,7 +96,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 4,
"metadata": {},
"outputs": [
{
@@ -99,7 +107,7 @@
"<PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=1024x624>"
]
},
"execution_count": 3,
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
@@ -119,7 +127,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 5,
"metadata": {},
"outputs": [
{
@@ -175,7 +183,7 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 6,
"metadata": {},
"outputs": [
{
@@ -222,7 +230,7 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 7,
"metadata": {},
"outputs": [
{

File diff suppressed because one or more lines are too long

View File

@@ -11,15 +11,6 @@
"## 1. Install UniFace"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%pip install -q uniface"
]
},
{
"cell_type": "code",
"execution_count": 1,
@@ -29,7 +20,24 @@
"name": "stdout",
"output_type": "stream",
"text": [
"1.3.0\n"
"Note: you may need to restart the kernel to use updated packages.\n"
]
}
],
"source": [
"%pip install -q uniface"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"1.3.1\n"
]
}
],
@@ -56,7 +64,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 3,
"metadata": {},
"outputs": [
{
@@ -72,12 +80,12 @@
"analyzer = FaceAnalyzer(\n",
" detector=RetinaFace(conf_thresh=0.5),\n",
" recognizer=ArcFace()\n",
")\n"
")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 4,
"metadata": {},
"outputs": [
{
@@ -99,12 +107,12 @@
"faces1 = analyzer.analyze(image1)\n",
"faces2 = analyzer.analyze(image2)\n",
"\n",
"print(f'Detected {len(faces1)} and {len(faces2)} faces')\n"
"print(f'Detected {len(faces1)} and {len(faces2)} faces')"
]
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 5,
"metadata": {},
"outputs": [
{
@@ -130,31 +138,7 @@
"axes[1].axis('off')\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Similarity: 0.1201\n"
]
}
],
"source": [
"if faces1 and faces2:\n",
" face1 = faces1[0]\n",
" face2 = faces2[0]\n",
"\n",
" similarity = face1.compute_similarity(face2)\n",
" print(f'Similarity: {similarity:.4f}')\n",
"else:\n",
" print('Error: Could not detect faces')\n"
"plt.show()"
]
},
{
@@ -166,7 +150,31 @@
"name": "stdout",
"output_type": "stream",
"text": [
"Similarity: 0.1201\n",
"Similarity: 0.1135\n"
]
}
],
"source": [
"if faces1 and faces2:\n",
" face1 = faces1[0]\n",
" face2 = faces2[0]\n",
"\n",
" similarity = face1.compute_similarity(face2)\n",
" print(f'Similarity: {similarity:.4f}')\n",
"else:\n",
" print('Error: Could not detect faces')"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Similarity: 0.1135\n",
"Threshold: 0.6\n",
"Result: Different people\n"
]
@@ -180,12 +188,12 @@
"\n",
" print(f'Similarity: {similarity:.4f}')\n",
" print(f'Threshold: {THRESHOLD}')\n",
" print(f'Result: {\"Same person\" if is_same_person else \"Different people\"}')\n"
" print(f'Result: {\"Same person\" if is_same_person else \"Different people\"}')"
]
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 8,
"metadata": {},
"outputs": [
{
@@ -193,9 +201,9 @@
"output_type": "stream",
"text": [
"Comparing multiple pairs:\n",
"image0.jpg vs image1.jpg: 0.1201\n",
"image0.jpg vs image2.jpg: 0.0951\n",
"image1.jpg vs image2.jpg: -0.0047\n"
"image0.jpg vs image1.jpg: 0.1135\n",
"image0.jpg vs image2.jpg: 0.0833\n",
"image1.jpg vs image2.jpg: -0.0082\n"
]
}
],
@@ -220,7 +228,7 @@
" img1_name = img1_path.split('/')[-1]\n",
" img2_name = img2_path.split('/')[-1]\n",
"\n",
" print(f'{img1_name} vs {img2_name}: {sim:.4f}')\n"
" print(f'{img1_name} vs {img2_name}: {sim:.4f}')"
]
},
{

View File

@@ -1,6 +1,6 @@
[project]
name = "uniface"
version = "1.3.0"
version = "1.3.2"
description = "UniFace: A Comprehensive Library for Face Detection, Recognition, Landmark Analysis, Age, and Gender Detection"
readme = "README.md"
license = { text = "MIT" }

View File

@@ -31,7 +31,9 @@ def process_image(detector, image_path: Path, output_path: Path, threshold: floa
bboxes = [f['bbox'] for f in faces]
scores = [f['confidence'] for f in faces]
landmarks = [f['landmarks'] for f in faces]
draw_detections(image, bboxes, scores, landmarks, vis_threshold=threshold)
draw_detections(
image=image, bboxes=bboxes, scores=scores, landmarks=landmarks, vis_threshold=threshold, fancy_bbox=True
)
cv2.putText(
image,

View File

@@ -43,7 +43,9 @@ def process_image(
bboxes = [f['bbox'] for f in faces]
scores = [f['confidence'] for f in faces]
landmarks = [f['landmarks'] for f in faces]
draw_detections(image, bboxes, scores, landmarks, vis_threshold=threshold)
draw_detections(
image=image, bboxes=bboxes, scores=scores, landmarks=landmarks, vis_threshold=threshold, fancy_bbox=True
)
for i, face in enumerate(faces):
gender_id, age = age_gender.predict(image, face['bbox'])

View File

@@ -51,7 +51,15 @@ def run_webcam(detector, threshold: float = 0.6):
bboxes = [f['bbox'] for f in faces]
scores = [f['confidence'] for f in faces]
landmarks = [f['landmarks'] for f in faces]
draw_detections(frame, bboxes, scores, landmarks, vis_threshold=threshold, draw_score=True, fancy_bbox=True)
draw_detections(
image=frame,
bboxes=bboxes,
scores=scores,
landmarks=landmarks,
vis_threshold=threshold,
draw_score=True,
fancy_bbox=True,
)
cv2.putText(
frame,
@@ -90,7 +98,7 @@ def main():
else:
from uniface.constants import YOLOv5FaceWeights
detector = YOLOv5Face(model_name=YOLOv5FaceWeights.YOLOV5M)
detector = YOLOv5Face(model_name=YOLOv5FaceWeights.YOLOV5N)
if args.webcam:
run_webcam(detector, args.threshold)

View File

@@ -42,7 +42,9 @@ def process_image(
bboxes = [f['bbox'] for f in faces]
scores = [f['confidence'] for f in faces]
landmarks = [f['landmarks'] for f in faces]
draw_detections(image, bboxes, scores, landmarks, vis_threshold=threshold)
draw_detections(
image=image, bboxes=bboxes, scores=scores, landmarks=landmarks, vis_threshold=threshold, fancy_bbox=True
)
for i, face in enumerate(faces):
emotion, confidence = emotion_predictor.predict(image, face['landmarks'])

View File

@@ -16,8 +16,8 @@ def draw_face_info(image, face, face_id):
"""Draw face ID and attributes above bounding box."""
x1, y1, x2, y2 = map(int, face.bbox)
lines = [f'ID: {face_id}', f'Conf: {face.confidence:.2f}']
if face.age and face.gender:
lines.append(f'{face.gender}, {face.age}y')
if face.age and face.sex:
lines.append(f'{face.sex}, {face.age}y')
for i, line in enumerate(lines):
y_pos = y1 - 10 - (len(lines) - 1 - i) * 25
@@ -41,7 +41,7 @@ def process_image(analyzer, image_path: str, save_dir: str = 'outputs', show_sim
return
for i, face in enumerate(faces, 1):
info = f' Face {i}: {face.gender}, {face.age}y' if face.age and face.gender else f' Face {i}'
info = f' Face {i}: {face.sex}, {face.age}y' if face.age and face.sex else f' Face {i}'
if face.embedding is not None:
info += f' (embedding: {face.embedding.shape})'
print(info)
@@ -82,7 +82,7 @@ def process_image(analyzer, image_path: str, save_dir: str = 'outputs', show_sim
bboxes = [f.bbox for f in faces]
scores = [f.confidence for f in faces]
landmarks = [f.landmarks for f in faces]
draw_detections(image, bboxes, scores, landmarks)
draw_detections(image=image, bboxes=bboxes, scores=scores, landmarks=landmarks, fancy_bbox=True)
for i, face in enumerate(faces, 1):
draw_face_info(image, face, i)

View File

@@ -55,7 +55,9 @@ def process_video(
bboxes = [f['bbox'] for f in faces]
scores = [f['confidence'] for f in faces]
landmarks = [f['landmarks'] for f in faces]
draw_detections(frame, bboxes, scores, landmarks, vis_threshold=threshold)
draw_detections(
image=frame, bboxes=bboxes, scores=scores, landmarks=landmarks, vis_threshold=threshold, fancy_bbox=True
)
cv2.putText(
frame,

View File

@@ -13,7 +13,7 @@
__license__ = 'MIT'
__author__ = 'Yakhyokhuja Valikhujaev'
__version__ = '1.3.0'
__version__ = '1.3.2'
from uniface.face_utils import compute_similarity, face_alignment

View File

@@ -53,12 +53,11 @@ class FaceAnalyzer:
except Exception as e:
Logger.warning(f' Face {idx + 1}: Failed to extract embedding: {e}')
age, gender_id = None, None
age, gender = None, None
if self.age_gender is not None:
try:
gender_id, age = self.age_gender.predict(image, bbox)
gender_str = 'Female' if gender_id == 0 else 'Male'
Logger.debug(f' Face {idx + 1}: Age={age}, Gender={gender_str}')
gender, age = self.age_gender.predict(image, bbox)
Logger.debug(f' Face {idx + 1}: Age={age}, Gender={gender}')
except Exception as e:
Logger.warning(f' Face {idx + 1}: Failed to predict age/gender: {e}')
@@ -68,7 +67,7 @@ class FaceAnalyzer:
landmarks=landmarks,
embedding=embedding,
age=age,
gender_id=gender_id,
gender=gender,
)
faces.append(face)

View File

@@ -62,11 +62,13 @@ class YOLOv5FaceWeights(str, Enum):
Exported to ONNX from: https://github.com/yakhyo/yolov5-face-onnx-inference
Model Performance (WIDER FACE):
- YOLOV5S: 7.1M params, 28MB, 94.33% Easy / 92.61% Medium / 83.15% Hard
- YOLOV5M: 21.1M params, 84MB, 95.30% Easy / 93.76% Medium / 85.28% Hard
- YOLOV5N: 11MB, 93.61% Easy / 91.52% Medium / 80.53% Hard
- YOLOV5S: 28MB, 94.33% Easy / 92.61% Medium / 83.15% Hard
- YOLOV5M: 82MB, 95.30% Easy / 93.76% Medium / 85.28% Hard
"""
YOLOV5S = "yolov5s_face"
YOLOV5M = "yolov5m_face"
YOLOV5N = "yolov5n"
YOLOV5S = "yolov5s"
YOLOV5M = "yolov5m"
class DDAMFNWeights(str, Enum):
@@ -117,6 +119,7 @@ MODEL_URLS: Dict[Enum, str] = {
SCRFDWeights.SCRFD_10G_KPS: 'https://github.com/yakhyo/uniface/releases/download/weights/scrfd_10g_kps.onnx',
SCRFDWeights.SCRFD_500M_KPS: 'https://github.com/yakhyo/uniface/releases/download/weights/scrfd_500m_kps.onnx',
# YOLOv5-Face
YOLOv5FaceWeights.YOLOV5N: 'https://github.com/yakhyo/yolov5-face-onnx-inference/releases/download/weights/yolov5n_face.onnx',
YOLOv5FaceWeights.YOLOV5S: 'https://github.com/yakhyo/yolov5-face-onnx-inference/releases/download/weights/yolov5s_face.onnx',
YOLOv5FaceWeights.YOLOV5M: 'https://github.com/yakhyo/yolov5-face-onnx-inference/releases/download/weights/yolov5m_face.onnx',
# DDAFM
@@ -151,6 +154,7 @@ MODEL_SHA256: Dict[Enum, str] = {
SCRFDWeights.SCRFD_10G_KPS: '5838f7fe053675b1c7a08b633df49e7af5495cee0493c7dcf6697200b85b5b91',
SCRFDWeights.SCRFD_500M_KPS: '5e4447f50245bbd7966bd6c0fa52938c61474a04ec7def48753668a9d8b4ea3a',
# YOLOv5-Face
YOLOv5FaceWeights.YOLOV5N: 'eb244a06e36999db732b317c2b30fa113cd6cfc1a397eaf738f2d6f33c01f640',
YOLOv5FaceWeights.YOLOV5S: 'fc682801cd5880e1e296184a14aea0035486b5146ec1a1389d2e7149cb134bb2',
YOLOv5FaceWeights.YOLOV5M: '04302ce27a15bde3e20945691b688e2dd018a10e92dd8932146bede6a49207b2',
# DDAFM

View File

@@ -230,9 +230,9 @@ class RetinaFace(BaseDetector):
faces = []
for i in range(detections.shape[0]):
face_dict = {
'bbox': detections[i, :4].astype(np.float32),
'bbox': detections[i, :4],
'confidence': float(detections[i, 4]),
'landmarks': landmarks[i].astype(np.float32),
'landmarks': landmarks[i],
}
faces.append(face_dict)
@@ -293,7 +293,7 @@ class RetinaFace(BaseDetector):
landmarks[: self.post_nms_topk],
)
landmarks = landmarks.reshape(-1, 5, 2).astype(np.int32)
landmarks = landmarks.reshape(-1, 5, 2).astype(np.float32)
return detections, landmarks

View File

@@ -251,7 +251,7 @@ class SCRFD(BaseDetector):
detections = pre_det[keep, :]
landmarks = landmarks[order, :, :]
landmarks = landmarks[keep, :, :].astype(np.int32)
landmarks = landmarks[keep, :, :].astype(np.float32)
if 0 < max_num < detections.shape[0]:
# Calculate area of detections
@@ -281,9 +281,9 @@ class SCRFD(BaseDetector):
faces = []
for i in range(detections.shape[0]):
face_dict = {
'bbox': detections[i, :4].astype(np.float32),
'bbox': detections[i, :4],
'confidence': float(detections[i, 4]),
'landmarks': landmarks[i].astype(np.float32),
'landmarks': landmarks[i],
}
faces.append(face_dict)

View File

@@ -331,9 +331,9 @@ class YOLOv5Face(BaseDetector):
faces = []
for i in range(detections.shape[0]):
face_dict = {
'bbox': detections[i, :4].astype(np.float32),
'bbox': detections[i, :4],
'confidence': float(detections[i, 4]),
'landmarks': landmarks[i].astype(np.float32),
'landmarks': landmarks[i],
}
faces.append(face_dict)

View File

@@ -14,14 +14,19 @@ __all__ = ['Face']
@dataclass
class Face:
"""Detected face with analysis results."""
"""
Detected face with analysis results.
"""
# Required attributes
bbox: np.ndarray
confidence: float
landmarks: np.ndarray
# Optional attributes
embedding: Optional[np.ndarray] = None
age: Optional[int] = None
gender_id: Optional[int] = None # 0: Female, 1: Male
gender: Optional[int] = None # 0 or 1
def compute_similarity(self, other: 'Face') -> float:
"""Compute cosine similarity with another face."""
@@ -34,18 +39,28 @@ class Face:
return asdict(self)
@property
def gender(self) -> str:
def sex(self) -> str:
"""Get gender as a string label (Female or Male)."""
if self.gender_id is None:
if self.gender is None:
return None
return 'Female' if self.gender_id == 0 else 'Male'
return 'Female' if self.gender == 0 else 'Male'
@property
def bbox_xyxy(self) -> np.ndarray:
"""Get bounding box coordinates in (x1, y1, x2, y2) format."""
return self.bbox.copy()
@property
def bbox_xywh(self) -> np.ndarray:
"""Get bounding box coordinates in (x1, y1, w, h) format."""
return np.array([self.bbox[0], self.bbox[1], self.bbox[2] - self.bbox[0], self.bbox[3] - self.bbox[1]])
def __repr__(self) -> str:
parts = [f'Face(confidence={self.confidence:.3f}']
if self.age is not None:
parts.append(f'age={self.age}')
if self.gender_id is not None:
parts.append(f'gender={self.gender}')
if self.gender is not None:
parts.append(f'sex={self.sex}')
if self.embedding is not None:
parts.append(f'embedding_dim={self.embedding.shape[0]}')
return ', '.join(parts) + ')'