ref: Add comprehensive test suite and enhance model functionality

- Add new test files for age_gender, factory, landmark, recognition, scrfd, and utils
- Add new scripts for age_gender, landmarks, and video detection
- Update documentation in README.md, MODELS.md, QUICKSTART.md
- Improve model constants and face utilities
- Update detection models (retinaface, scrfd) with enhanced functionality
- Update project configuration in pyproject.toml
This commit is contained in:
yakhyo
2025-11-15 21:09:37 +09:00
parent df673c4a3f
commit 2c78f39e5d
28 changed files with 2014 additions and 591 deletions

View File

@@ -1,18 +1,97 @@
### `download_model.py`
# Scripts
# Download all models
Collection of example scripts demonstrating UniFace functionality.
## Available Scripts
- `run_detection.py` - Face detection on images
- `run_age_gender.py` - Age and gender prediction
- `run_landmarks.py` - Facial landmark detection
- `run_recognition.py` - Face recognition and embeddings
- `run_face_search.py` - Face search and matching
- `run_video_detection.py` - Video processing with face detection
- `batch_process.py` - Batch processing of image folders
- `download_model.py` - Download and manage models
## Quick Start
```bash
python scripts/download_model.py
# Face detection
python scripts/run_detection.py --image assets/test.jpg
# Age and gender detection
python scripts/run_age_gender.py --image assets/test.jpg
# Webcam demo
python scripts/run_age_gender.py --webcam
# Batch processing
python scripts/batch_process.py --input images/ --output results/
```
# Download just RESNET18
## Import Examples
```bash
python scripts/download_model.py --model RESNET18
The scripts use direct class imports for better developer experience:
```python
# Face Detection
from uniface.detection import RetinaFace, SCRFD
detector = RetinaFace() # or SCRFD()
faces = detector.detect(image)
# Face Recognition
from uniface.recognition import ArcFace, MobileFace, SphereFace
recognizer = ArcFace() # or MobileFace(), SphereFace()
embedding = recognizer.get_embedding(image, landmarks)
# Age & Gender
from uniface.attribute import AgeGender
age_gender = AgeGender()
gender, age = age_gender.predict(image, bbox)
# Landmarks
from uniface.landmark import Landmark106
landmarker = Landmark106()
landmarks = landmarker.get_landmarks(image, bbox)
```
### `run_inference.py`
## Available Classes
**Detection:**
- `RetinaFace` - High accuracy face detection
- `SCRFD` - Fast face detection
**Recognition:**
- `ArcFace` - High accuracy face recognition
- `MobileFace` - Lightweight face recognition
- `SphereFace` - Alternative face recognition
**Attributes:**
- `AgeGender` - Age and gender prediction
**Landmarks:**
- `Landmark106` - 106-point facial landmarks
## Common Options
Most scripts support:
- `--help` - Show usage information
- `--verbose` - Enable detailed logging
- `--detector` - Choose detector (retinaface, scrfd)
- `--threshold` - Set confidence threshold
## Testing
Run basic functionality test:
```bash
python scripts/run_inference.py --image assets/test.jpg --model MNET_V2 --iterations 10
```
python scripts/run_detection.py --image assets/test.jpg
```
For comprehensive testing, see the main project tests:
```bash
pytest tests/
```

View File

@@ -1,389 +0,0 @@
# Testing Scripts Guide
Complete guide to testing all scripts in the `scripts/` directory.
---
## 📁 Available Scripts
1. **download_model.py** - Download and verify model weights
2. **run_detection.py** - Face detection on images
3. **run_recognition.py** - Face recognition (extract embeddings)
4. **run_face_search.py** - Real-time face matching with webcam
5. **sha256_generate.py** - Generate SHA256 checksums for models
---
## Testing Each Script
### 1. Test Model Download
```bash
# Download a specific model
python scripts/download_model.py --model MNET_V2
# Download all RetinaFace models (takes ~5 minutes, ~200MB)
python scripts/download_model.py
# Verify models are cached
ls -lh ~/.uniface/models/
```
**Expected Output:**
```
📥 Downloading model: retinaface_mnet_v2
2025-11-08 00:00:00 - INFO - Downloading model 'RetinaFaceWeights.MNET_V2' from https://...
Downloading ~/.uniface/models/retinaface_mnet_v2.onnx: 100%|████| 3.5M/3.5M
2025-11-08 00:00:05 - INFO - Successfully downloaded 'RetinaFaceWeights.MNET_V2'
✅ All requested weights are ready and verified.
```
---
### 2. Test Face Detection
```bash
# Basic detection
python scripts/run_detection.py --image assets/test.jpg
# With custom settings
python scripts/run_detection.py \
--image assets/test.jpg \
--method scrfd \
--threshold 0.7 \
--save_dir outputs
# Benchmark mode (100 iterations)
python scripts/run_detection.py \
--image assets/test.jpg \
--iterations 100
```
**Expected Output:**
```
Initializing detector: retinaface
2025-11-08 00:00:00 - INFO - Initializing RetinaFace with model=RetinaFaceWeights.MNET_V2...
2025-11-08 00:00:01 - INFO - CoreML acceleration enabled (Apple Silicon)
✅ Output saved at: outputs/test_out.jpg
[1/1] ⏱️ Inference time: 0.0234 seconds
```
**Verify Output:**
```bash
# Check output image was created
ls -lh outputs/test_out.jpg
# View the image (macOS)
open outputs/test_out.jpg
```
---
### 3. Test Face Recognition (Embedding Extraction)
```bash
# Extract embeddings from an image
python scripts/run_recognition.py --image assets/test.jpg
# With different models
python scripts/run_recognition.py \
--image assets/test.jpg \
--detector scrfd \
--recognizer mobileface
```
**Expected Output:**
```
Initializing detector: retinaface
Initializing recognizer: arcface
2025-11-08 00:00:00 - INFO - Successfully initialized face encoder from ~/.uniface/models/w600k_mbf.onnx
Detected 1 face(s). Extracting embeddings for the first face...
- Embedding shape: (1, 512)
- L2 norm of unnormalized embedding: 64.2341
- L2 norm of normalized embedding: 1.0000
```
---
### 4. Test Real-Time Face Search (Webcam)
**Prerequisites:**
- Webcam connected
- Reference image with a clear face
```bash
# Basic usage
python scripts/run_face_search.py --image assets/test.jpg
# With custom models
python scripts/run_face_search.py \
--image assets/test.jpg \
--detector scrfd \
--recognizer arcface
```
**Expected Behavior:**
1. Webcam window opens
2. Faces are detected in real-time
3. Green box = Match (similarity > 0.4)
4. Red box = Unknown (similarity < 0.4)
5. Press 'q' to quit
**Expected Output:**
```
Initializing models...
2025-11-08 00:00:00 - INFO - CoreML acceleration enabled (Apple Silicon)
Extracting reference embedding...
Webcam started. Press 'q' to quit.
```
**Troubleshooting:**
```bash
# If webcam doesn't open
python -c "import cv2; cap = cv2.VideoCapture(0); print('Webcam OK' if cap.isOpened() else 'Webcam FAIL')"
# If no faces detected
# - Ensure good lighting
# - Face should be frontal and clearly visible
# - Try lowering threshold: edit script line 29, change 0.4 to 0.3
```
---
### 5. Test SHA256 Generator (For Developers)
```bash
# Generate checksum for a model file
python scripts/sha256_generate.py ~/.uniface/models/retinaface_mnet_v2.onnx
# Generate for all models
for model in ~/.uniface/models/*.onnx; do
python scripts/sha256_generate.py "$model"
done
```
---
## 🔍 Quick Verification Tests
### Test 1: Imports Work
```bash
python -c "
from uniface.detection import create_detector
from uniface.recognition import create_recognizer
print('✅ Imports successful')
"
```
### Test 2: Models Download
```bash
python -c "
from uniface import RetinaFace
detector = RetinaFace()
print('✅ Model downloaded and loaded')
"
```
### Test 3: Detection Works
```bash
python -c "
import cv2
import numpy as np
from uniface import RetinaFace
detector = RetinaFace()
image = np.random.randint(0, 255, (640, 640, 3), dtype=np.uint8)
faces = detector.detect(image)
print(f'✅ Detection works, found {len(faces)} faces')
"
```
### Test 4: Recognition Works
```bash
python -c "
import cv2
import numpy as np
from uniface import RetinaFace, ArcFace
detector = RetinaFace()
recognizer = ArcFace()
image = cv2.imread('assets/test.jpg')
faces = detector.detect(image)
if faces:
landmarks = np.array(faces[0]['landmarks'])
embedding = recognizer.get_normalized_embedding(image, landmarks)
print(f'✅ Recognition works, embedding shape: {embedding.shape}')
else:
print('⚠️ No faces detected in test image')
"
```
---
## End-to-End Test Workflow
Run this complete workflow to verify everything works:
```bash
#!/bin/bash
# Save as test_all_scripts.sh
echo "=== Testing UniFace Scripts ==="
echo ""
# Test 1: Download models
echo "1⃣ Testing model download..."
python scripts/download_model.py --model MNET_V2
if [ $? -eq 0 ]; then
echo "✅ Model download: PASS"
else
echo "❌ Model download: FAIL"
exit 1
fi
echo ""
# Test 2: Face detection
echo "2⃣ Testing face detection..."
python scripts/run_detection.py --image assets/test.jpg --save_dir /tmp/uniface_test
if [ $? -eq 0 ] && [ -f /tmp/uniface_test/test_out.jpg ]; then
echo "✅ Face detection: PASS"
else
echo "❌ Face detection: FAIL"
exit 1
fi
echo ""
# Test 3: Face recognition
echo "3⃣ Testing face recognition..."
python scripts/run_recognition.py --image assets/test.jpg > /tmp/uniface_recognition.log
if [ $? -eq 0 ] && grep -q "Embedding shape" /tmp/uniface_recognition.log; then
echo "✅ Face recognition: PASS"
else
echo "❌ Face recognition: FAIL"
exit 1
fi
echo ""
echo "=== All Tests Passed! 🎉 ==="
```
**Run the test suite:**
```bash
chmod +x test_all_scripts.sh
./test_all_scripts.sh
```
---
## Performance Benchmarking
### Benchmark Detection Speed
```bash
# Test different models
for model in retinaface scrfd; do
echo "Testing $model..."
python scripts/run_detection.py \
--image assets/test.jpg \
--method $model \
--iterations 50
done
```
### Benchmark Recognition Speed
```bash
# Test different recognizers
for recognizer in arcface mobileface; do
echo "Testing $recognizer..."
time python scripts/run_recognition.py \
--image assets/test.jpg \
--recognizer $recognizer
done
```
---
## 🐛 Common Issues
### Issue: "No module named 'uniface'"
```bash
# Solution: Install in editable mode
pip install -e .
```
### Issue: "Failed to load image"
```bash
# Check image exists
ls -lh assets/test.jpg
# Try with absolute path
python scripts/run_detection.py --image $(pwd)/assets/test.jpg
```
### Issue: "No faces detected"
```bash
# Lower confidence threshold
python scripts/run_detection.py \
--image assets/test.jpg \
--threshold 0.3
```
### Issue: Models downloading slowly
```bash
# Check internet connection
curl -I https://github.com/yakhyo/uniface/releases
# Or download manually
wget https://github.com/yakhyo/uniface/releases/download/v0.1.2/retinaface_mv2.onnx \
-O ~/.uniface/models/retinaface_mnet_v2.onnx
```
### Issue: CoreML not available on Mac
```bash
# Install CoreML-enabled ONNX Runtime
pip uninstall onnxruntime
pip install onnxruntime-silicon
# Verify
python -c "import onnxruntime as ort; print(ort.get_available_providers())"
# Should show: ['CoreMLExecutionProvider', 'CPUExecutionProvider']
```
---
## ✅ Script Status Summary
| Script | Status | API Updated | Tested |
|-----------------------|--------|-------------|--------|
| download_model.py | ✅ | ✅ | ✅ |
| run_detection.py | ✅ | ✅ | ✅ |
| run_recognition.py | ✅ | ✅ | ✅ |
| run_face_search.py | ✅ | ✅ | ✅ |
| sha256_generate.py | ✅ | N/A | ✅ |
All scripts are updated and working with the new dict-based API! 🎉
---
## 📝 Notes
- All scripts now use the factory functions (`create_detector`, `create_recognizer`)
- Scripts work with the new dict-based detection API
- Model download bug is fixed (enum vs string issue)
- CoreML acceleration is automatically detected on Apple Silicon
- All scripts include proper error handling
---
Need help with a specific script? Check the main [README.md](../README.md) or [QUICKSTART.md](../QUICKSTART.md)!

157
scripts/batch_process.py Normal file
View File

@@ -0,0 +1,157 @@
"""Batch Image Processing Script"""
import os
import cv2
import argparse
from pathlib import Path
from tqdm import tqdm
from uniface import RetinaFace, SCRFD
from uniface.visualization import draw_detections
def get_image_files(input_dir: Path, extensions: tuple) -> list:
image_files = []
for ext in extensions:
image_files.extend(input_dir.glob(f"*.{ext}"))
image_files.extend(input_dir.glob(f"*.{ext.upper()}"))
return sorted(image_files)
def process_single_image(detector, image_path: Path, output_dir: Path,
vis_threshold: float, skip_existing: bool) -> dict:
output_path = output_dir / f"{image_path.stem}_detected{image_path.suffix}"
# Skip if already processed
if skip_existing and output_path.exists():
return {"status": "skipped", "faces": 0}
# Load image
image = cv2.imread(str(image_path))
if image is None:
return {"status": "error", "error": "Failed to load image"}
# Detect faces
try:
faces = detector.detect(image)
except Exception as e:
return {"status": "error", "error": str(e)}
# Draw detections
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=vis_threshold)
# Add face count
cv2.putText(image, f"Faces: {len(faces)}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
# Save result
cv2.imwrite(str(output_path), image)
return {"status": "success", "faces": len(faces)}
def batch_process(detector, input_dir: str, output_dir: str, extensions: tuple,
vis_threshold: float, skip_existing: bool):
input_path = Path(input_dir)
output_path = Path(output_dir)
# Create output directory
output_path.mkdir(parents=True, exist_ok=True)
# Get image files
image_files = get_image_files(input_path, extensions)
if not image_files:
print(f"No image files found in '{input_dir}' with extensions {extensions}")
return
print(f"Input: {input_dir}")
print(f"Output: {output_dir}")
print(f"Found {len(image_files)} images\n")
# Process images
results = {
"success": 0,
"skipped": 0,
"error": 0,
"total_faces": 0
}
with tqdm(image_files, desc="Processing images", unit="img") as pbar:
for image_path in pbar:
result = process_single_image(
detector, image_path, output_path,
vis_threshold, skip_existing
)
if result["status"] == "success":
results["success"] += 1
results["total_faces"] += result["faces"]
pbar.set_postfix({"faces": result["faces"]})
elif result["status"] == "skipped":
results["skipped"] += 1
else:
results["error"] += 1
print(f"\nError processing {image_path.name}: {result.get('error', 'Unknown error')}")
# Print summary
print(f"\nBatch processing complete!")
print(f" Total images: {len(image_files)}")
print(f" Successfully processed: {results['success']}")
print(f" Skipped: {results['skipped']}")
print(f" Errors: {results['error']}")
print(f" Total faces detected: {results['total_faces']}")
if results['success'] > 0:
print(f" Average faces per image: {results['total_faces']/results['success']:.2f}")
print(f"\nResults saved to: {output_dir}")
def main():
parser = argparse.ArgumentParser(description="Batch process images with face detection")
parser.add_argument("--input", type=str, required=True,
help="Input directory containing images")
parser.add_argument("--output", type=str, required=True,
help="Output directory for processed images")
parser.add_argument("--detector", type=str, default="retinaface",
choices=['retinaface', 'scrfd'], help="Face detector to use")
parser.add_argument("--threshold", type=float, default=0.6,
help="Confidence threshold for visualization")
parser.add_argument("--extensions", type=str, default="jpg,jpeg,png,bmp",
help="Comma-separated list of image extensions")
parser.add_argument("--skip_existing", action="store_true",
help="Skip files that already exist in output directory")
parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
args = parser.parse_args()
# Check input directory exists
if not Path(args.input).exists():
print(f"Error: Input directory '{args.input}' does not exist")
return
if args.verbose:
from uniface import enable_logging
enable_logging()
# Parse extensions
extensions = tuple(ext.strip() for ext in args.extensions.split(','))
# Initialize detector
print(f"Initializing detector: {args.detector}")
if args.detector == 'retinaface':
detector = RetinaFace()
else:
detector = SCRFD()
print("Detector initialized\n")
# Process batch
batch_process(detector, args.input, args.output, extensions,
args.threshold, args.skip_existing)
if __name__ == "__main__":
main()

View File

@@ -1,31 +1,77 @@
import argparse
from uniface.constants import RetinaFaceWeights
from uniface.constants import (
RetinaFaceWeights, SphereFaceWeights, MobileFaceWeights, ArcFaceWeights,
SCRFDWeights, DDAMFNWeights, AgeGenderWeights, LandmarkWeights
)
from uniface.model_store import verify_model_weights
# All available model types
ALL_MODEL_TYPES = {
'retinaface': RetinaFaceWeights,
'sphereface': SphereFaceWeights,
'mobileface': MobileFaceWeights,
'arcface': ArcFaceWeights,
'scrfd': SCRFDWeights,
'ddamfn': DDAMFNWeights,
'agegender': AgeGenderWeights,
'landmark': LandmarkWeights,
}
def main():
parser = argparse.ArgumentParser(description="Download and verify RetinaFace model weights.")
parser = argparse.ArgumentParser(description="Download and verify model weights.")
parser.add_argument(
"--model-type",
type=str,
choices=list(ALL_MODEL_TYPES.keys()),
help="Model type to download (e.g. retinaface, arcface). If not specified, all models will be downloaded.",
)
parser.add_argument(
"--model",
type=str,
choices=[m.name for m in RetinaFaceWeights],
help="Model to download (e.g. MNET_V2). If not specified, all models will be downloaded.",
help="Specific model to download (e.g. MNET_V2). For RetinaFace backward compatibility.",
)
args = parser.parse_args()
if args.model:
weight = RetinaFaceWeights[args.model]
print(f"📥 Downloading model: {weight.value}")
verify_model_weights(weight) # Pass enum, not string
else:
print("📥 Downloading all models...")
for weight in RetinaFaceWeights:
verify_model_weights(weight) # Pass enum, not string
if args.model and not args.model_type:
# Backward compatibility - assume RetinaFace
try:
weight = RetinaFaceWeights[args.model]
print(f"Downloading RetinaFace model: {weight.value}")
verify_model_weights(weight)
print("Model downloaded successfully.")
except KeyError:
print(f"Invalid RetinaFace model: {args.model}")
print(f"Available models: {[m.name for m in RetinaFaceWeights]}")
return
print("✅ All requested weights are ready and verified.")
if args.model_type:
# Download all models from specific type
model_enum = ALL_MODEL_TYPES[args.model_type]
print(f"Downloading all {args.model_type} models...")
for weight in model_enum:
print(f"Downloading: {weight.value}")
try:
verify_model_weights(weight)
print(f"Downloaded: {weight.value}")
except Exception as e:
print(f"Failed to download {weight.value}: {e}")
else:
# Download all models from all types
print("Downloading all models...")
for model_type, model_enum in ALL_MODEL_TYPES.items():
print(f"\nDownloading {model_type} models...")
for weight in model_enum:
print(f"Downloading: {weight.value}")
try:
verify_model_weights(weight)
print(f"Downloaded: {weight.value}")
except Exception as e:
print(f"Failed to download {weight.value}: {e}")
print("\nDownload process completed.")
if __name__ == "__main__":
main()

163
scripts/run_age_gender.py Normal file
View File

@@ -0,0 +1,163 @@
"""Age and Gender Detection Demo Script"""
import os
import cv2
import argparse
from pathlib import Path
from uniface import RetinaFace, SCRFD, AgeGender
from uniface.visualization import draw_detections
def process_image(detector, age_gender, image_path: str, save_dir: str = "outputs", vis_threshold: float = 0.6):
image = cv2.imread(image_path)
if image is None:
print(f"Error: Failed to load image from '{image_path}'")
return
print(f"Processing: {image_path}")
# Detect faces
faces = detector.detect(image)
print(f" Detected {len(faces)} face(s)")
if not faces:
print(" No faces detected")
return
# Draw detections
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=vis_threshold)
# Predict and draw age/gender for each face
for i, face in enumerate(faces):
gender, age = age_gender.predict(image, face['bbox'])
print(f" Face {i+1}: {gender}, {age} years old")
# Draw age and gender text
bbox = face['bbox']
x1, y1 = int(bbox[0]), int(bbox[1])
text = f"{gender}, {age}y"
# Background rectangle for text
(text_width, text_height), _ = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
cv2.rectangle(image, (x1, y1 - text_height - 10),
(x1 + text_width + 10, y1), (0, 255, 0), -1)
cv2.putText(image, text, (x1 + 5, y1 - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)
# Save result
os.makedirs(save_dir, exist_ok=True)
output_path = os.path.join(save_dir, f"{Path(image_path).stem}_age_gender.jpg")
cv2.imwrite(output_path, image)
print(f"Output saved: {output_path}")
def run_webcam(detector, age_gender, vis_threshold: float = 0.6):
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("Cannot open webcam")
return
print("Webcam opened")
print("Press 'q' to quit\n")
frame_count = 0
try:
while True:
ret, frame = cap.read()
if not ret:
break
frame_count += 1
# Detect faces
faces = detector.detect(frame)
# Draw detections
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=vis_threshold)
# Predict and draw age/gender for each face
for face in faces:
gender, age = age_gender.predict(frame, face['bbox'])
# Draw age and gender text
bbox = face['bbox']
x1, y1 = int(bbox[0]), int(bbox[1])
text = f"{gender}, {age}y"
# Background rectangle for text
(text_width, text_height), _ = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
cv2.rectangle(frame, (x1, y1 - text_height - 10),
(x1 + text_width + 10, y1), (0, 255, 0), -1)
cv2.putText(frame, text, (x1 + 5, y1 - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)
# Add info
cv2.putText(frame, f"Faces: {len(faces)}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.putText(frame, "Press 'q' to quit", (10, frame.shape[0] - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
cv2.imshow("Age & Gender Detection", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
except KeyboardInterrupt:
print("\nInterrupted")
finally:
cap.release()
cv2.destroyAllWindows()
print(f"\nProcessed {frame_count} frames")
def main():
parser = argparse.ArgumentParser(description="Run age and gender detection")
parser.add_argument("--image", type=str, help="Path to input image")
parser.add_argument("--webcam", action="store_true", help="Use webcam instead of image")
parser.add_argument("--detector", type=str, default="retinaface",
choices=['retinaface', 'scrfd'], help="Face detector to use")
parser.add_argument("--threshold", type=float, default=0.6,
help="Confidence threshold for visualization")
parser.add_argument("--save_dir", type=str, default="outputs",
help="Directory to save output images")
parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
args = parser.parse_args()
# Validate input
if not args.image and not args.webcam:
parser.error("Either --image or --webcam must be specified")
if args.verbose:
from uniface import enable_logging
enable_logging()
# Initialize models
print(f"Initializing detector: {args.detector}")
if args.detector == 'retinaface':
detector = RetinaFace()
else:
detector = SCRFD()
print("Initializing age/gender model...")
age_gender = AgeGender()
print("Models initialized\n")
# Process
if args.webcam:
run_webcam(detector, age_gender, args.threshold)
else:
process_image(detector, age_gender, args.image, args.save_dir, args.threshold)
if __name__ == "__main__":
main()

View File

@@ -4,24 +4,14 @@ import time
import argparse
import numpy as np
# UPDATED: Use the factory function and import from the new location
from uniface.detection import create_detector
from uniface.detection import RetinaFace, SCRFD
from uniface.visualization import draw_detections
def run_inference(detector, image_path: str, vis_threshold: float = 0.6, save_dir: str = "outputs"):
"""
Run face detection on a single image.
Args:
detector: Initialized face detector.
image_path (str): Path to input image.
vis_threshold (float): Threshold for drawing detections.
save_dir (str): Directory to save output image.
"""
image = cv2.imread(image_path)
if image is None:
print(f"Error: Failed to load image from '{image_path}'")
print(f"Error: Failed to load image from '{image_path}'")
return
# 1. Get the list of face dictionaries from the detector
@@ -40,7 +30,7 @@ def run_inference(detector, image_path: str, vis_threshold: float = 0.6, save_di
os.makedirs(save_dir, exist_ok=True)
output_path = os.path.join(save_dir, f"{os.path.splitext(os.path.basename(image_path))[0]}_out.jpg")
cv2.imwrite(output_path, image)
print(f"Output saved at: {output_path}")
print(f"Output saved at: {output_path}")
def main():
@@ -65,14 +55,17 @@ def main():
enable_logging()
print(f"Initializing detector: {args.method}")
detector = create_detector(method=args.method)
if args.method == 'retinaface':
detector = RetinaFace()
else:
detector = SCRFD()
avg_time = 0
for i in range(args.iterations):
start = time.time()
run_inference(detector, args.image, args.threshold, args.save_dir)
elapsed = time.time() - start
print(f"[{i + 1}/{args.iterations}] ⏱️ Inference time: {elapsed:.4f} seconds")
print(f"[{i + 1}/{args.iterations}] Inference time: {elapsed:.4f} seconds")
if i >= 0: # Avoid counting the first run if it includes model loading time
avg_time += elapsed
@@ -80,7 +73,7 @@ def main():
# Adjust average calculation to exclude potential first-run overhead
effective_iterations = max(1, args.iterations)
print(
f"\n🔥 Average inference time over {effective_iterations} runs: {avg_time / effective_iterations:.4f} seconds")
f"\nAverage inference time over {effective_iterations} runs: {avg_time / effective_iterations:.4f} seconds")
if __name__ == "__main__":

View File

@@ -3,14 +3,12 @@ import argparse
import cv2
import numpy as np
# Use the new high-level factory functions
from uniface.detection import create_detector
from uniface.detection import RetinaFace, SCRFD
from uniface.face_utils import compute_similarity
from uniface.recognition import create_recognizer
from uniface.recognition import ArcFace, MobileFace, SphereFace
def extract_reference_embedding(detector, recognizer, image_path: str) -> np.ndarray:
"""Extracts a normalized embedding from the first face found in an image."""
image = cv2.imread(image_path)
if image is None:
raise RuntimeError(f"Failed to load image: {image_path}")
@@ -28,7 +26,6 @@ def extract_reference_embedding(detector, recognizer, image_path: str) -> np.nda
def run_video(detector, recognizer, ref_embedding: np.ndarray, threshold: float = 0.4):
"""Run real-time face recognition from a webcam feed."""
cap = cv2.VideoCapture(0)
if not cap.isOpened():
raise RuntimeError("Webcam could not be opened.")
@@ -91,8 +88,17 @@ def main():
enable_logging()
print("Initializing models...")
detector = create_detector(method=args.detector)
recognizer = create_recognizer(method=args.recognizer)
if args.detector == 'retinaface':
detector = RetinaFace()
else:
detector = SCRFD()
if args.recognizer == 'arcface':
recognizer = ArcFace()
elif args.recognizer == 'mobileface':
recognizer = MobileFace()
else:
recognizer = SphereFace()
print("Extracting reference embedding...")
ref_embedding = extract_reference_embedding(detector, recognizer, args.image)

149
scripts/run_landmarks.py Normal file
View File

@@ -0,0 +1,149 @@
"""Facial Landmark Detection Demo Script"""
import os
import cv2
import argparse
from pathlib import Path
from uniface import RetinaFace, SCRFD, Landmark106
def process_image(detector, landmarker, image_path: str, save_dir: str = "outputs"):
image = cv2.imread(image_path)
if image is None:
print(f"Error: Failed to load image from '{image_path}'")
return
print(f"Processing: {image_path}")
# Detect faces
faces = detector.detect(image)
print(f" Detected {len(faces)} face(s)")
if not faces:
print(" No faces detected")
return
# Process each face
for i, face in enumerate(faces):
# Draw bounding box
bbox = face['bbox']
x1, y1, x2, y2 = map(int, bbox)
cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
# Get and draw 106 landmarks
landmarks = landmarker.get_landmarks(image, bbox)
print(f" Face {i+1}: Extracted {len(landmarks)} landmarks")
for x, y in landmarks.astype(int):
cv2.circle(image, (x, y), 1, (0, 255, 0), -1)
# Add face count
cv2.putText(image, f"Face {i+1}", (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
# Add total count
cv2.putText(image, f"Faces: {len(faces)}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
# Save result
os.makedirs(save_dir, exist_ok=True)
output_path = os.path.join(save_dir, f"{Path(image_path).stem}_landmarks.jpg")
cv2.imwrite(output_path, image)
print(f"Output saved: {output_path}")
def run_webcam(detector, landmarker):
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("Cannot open webcam")
return
print("Webcam opened")
print("Press 'q' to quit\n")
frame_count = 0
try:
while True:
ret, frame = cap.read()
if not ret:
break
frame_count += 1
# Detect faces
faces = detector.detect(frame)
# Process each face
for face in faces:
# Draw bounding box
bbox = face['bbox']
x1, y1, x2, y2 = map(int, bbox)
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
# Get and draw 106 landmarks
landmarks = landmarker.get_landmarks(frame, bbox)
for x, y in landmarks.astype(int):
cv2.circle(frame, (x, y), 1, (0, 255, 0), -1)
# Add info
cv2.putText(frame, f"Faces: {len(faces)}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.putText(frame, "Press 'q' to quit", (10, frame.shape[0] - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
cv2.imshow("106-Point Landmarks", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
except KeyboardInterrupt:
print("\nInterrupted")
finally:
cap.release()
cv2.destroyAllWindows()
print(f"\nProcessed {frame_count} frames")
def main():
parser = argparse.ArgumentParser(description="Run facial landmark detection")
parser.add_argument("--image", type=str, help="Path to input image")
parser.add_argument("--webcam", action="store_true", help="Use webcam instead of image")
parser.add_argument("--detector", type=str, default="retinaface",
choices=['retinaface', 'scrfd'], help="Face detector to use")
parser.add_argument("--save_dir", type=str, default="outputs",
help="Directory to save output images")
parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
args = parser.parse_args()
# Validate input
if not args.image and not args.webcam:
parser.error("Either --image or --webcam must be specified")
if args.verbose:
from uniface import enable_logging
enable_logging()
# Initialize models
print(f"Initializing detector: {args.detector}")
if args.detector == 'retinaface':
detector = RetinaFace()
else:
detector = SCRFD()
print("Initializing landmark detector...")
landmarker = Landmark106()
print("Models initialized\n")
# Process
if args.webcam:
run_webcam(detector, landmarker)
else:
process_image(detector, landmarker, args.image, args.save_dir)
if __name__ == "__main__":
main()

View File

@@ -2,20 +2,12 @@ import cv2
import argparse
import numpy as np
# Use the new high-level factory functions for consistency
from uniface.detection import create_detector
from uniface.recognition import create_recognizer
from uniface.detection import RetinaFace, SCRFD
from uniface.recognition import ArcFace, MobileFace, SphereFace
from uniface.face_utils import compute_similarity
def run_inference(detector, recognizer, image_path: str):
"""
Detect faces and extract embeddings from a single image.
Args:
detector: Initialized face detector.
recognizer: Initialized face recognition model.
image_path (str): Path to the input image.
"""
image = cv2.imread(image_path)
if image is None:
print(f"Error: Failed to load image from '{image_path}'")
@@ -43,9 +35,47 @@ def run_inference(detector, recognizer, image_path: str):
print(f" - L2 norm of normalized embedding: {np.linalg.norm(norm_embedding):.4f}")
def compare_faces(detector, recognizer, image1_path: str, image2_path: str, threshold: float = 0.35):
# Load images
img1 = cv2.imread(image1_path)
img2 = cv2.imread(image2_path)
if img1 is None or img2 is None:
print(f"Error: Failed to load images")
return
# Detect faces
faces1 = detector.detect(img1)
faces2 = detector.detect(img2)
if not faces1 or not faces2:
print("Error: No faces detected in one or both images")
return
# Get landmarks for first face in each image
landmarks1 = np.array(faces1[0]['landmarks'])
landmarks2 = np.array(faces2[0]['landmarks'])
# Get normalized embeddings
embedding1 = recognizer.get_normalized_embedding(img1, landmarks1)
embedding2 = recognizer.get_normalized_embedding(img2, landmarks2)
# Compute similarity
similarity = compute_similarity(embedding1, embedding2, normalized=True)
is_match = similarity > threshold
print(f"Similarity: {similarity:.4f}")
print(f"Result: {'Same person' if is_match else 'Different person'}")
print(f"Threshold: {threshold}")
def main():
parser = argparse.ArgumentParser(description="Extract face embeddings from a single image.")
parser.add_argument("--image", type=str, required=True, help="Path to the input image.")
parser = argparse.ArgumentParser(description="Face recognition and comparison.")
parser.add_argument("--image", type=str, help="Path to single image for embedding extraction.")
parser.add_argument("--image1", type=str, help="Path to first image for comparison.")
parser.add_argument("--image2", type=str, help="Path to second image for comparison.")
parser.add_argument("--threshold", type=float, default=0.35, help="Similarity threshold for face matching.")
parser.add_argument(
"--detector",
type=str,
@@ -69,12 +99,29 @@ def main():
enable_logging()
print(f"Initializing detector: {args.detector}")
detector = create_detector(method=args.detector)
if args.detector == 'retinaface':
detector = RetinaFace()
else:
detector = SCRFD()
print(f"Initializing recognizer: {args.recognizer}")
recognizer = create_recognizer(method=args.recognizer)
if args.recognizer == 'arcface':
recognizer = ArcFace()
elif args.recognizer == 'mobileface':
recognizer = MobileFace()
else:
recognizer = SphereFace()
run_inference(detector, recognizer, args.image)
if args.image1 and args.image2:
# Face comparison mode
print(f"Comparing faces: {args.image1} vs {args.image2}")
compare_faces(detector, recognizer, args.image1, args.image2, args.threshold)
elif args.image:
# Single image embedding extraction mode
run_inference(detector, recognizer, args.image)
else:
print("Error: Provide either --image for single image processing or --image1 and --image2 for comparison")
parser.print_help()
if __name__ == "__main__":

View File

@@ -0,0 +1,142 @@
"""Video Face Detection Script"""
import cv2
import argparse
from pathlib import Path
from tqdm import tqdm
from uniface import RetinaFace, SCRFD
from uniface.visualization import draw_detections
def process_video(detector, input_path: str, output_path: str, vis_threshold: float = 0.6,
fps: int = None, show_preview: bool = False):
# Open input video
cap = cv2.VideoCapture(input_path)
if not cap.isOpened():
print(f"Error: Cannot open video file '{input_path}'")
return
# Get video properties
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
source_fps = cap.get(cv2.CAP_PROP_FPS)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
output_fps = fps if fps is not None else source_fps
print(f"📹 Input: {input_path}")
print(f" Resolution: {width}x{height}")
print(f" FPS: {source_fps:.2f}")
print(f" Total frames: {total_frames}")
print(f"\n📹 Output: {output_path}")
print(f" FPS: {output_fps:.2f}\n")
# Initialize video writer
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(output_path, fourcc, output_fps, (width, height))
if not out.isOpened():
print(f"Error: Cannot create output video '{output_path}'")
cap.release()
return
# Process frames
frame_count = 0
total_faces = 0
try:
with tqdm(total=total_frames, desc="Processing", unit="frames") as pbar:
while True:
ret, frame = cap.read()
if not ret:
break
frame_count += 1
# Detect faces
faces = detector.detect(frame)
total_faces += len(faces)
# Draw detections
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=vis_threshold)
# Add frame info
cv2.putText(frame, f"Faces: {len(faces)}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
# Write frame
out.write(frame)
# Show preview if requested
if show_preview:
cv2.imshow("Processing Video - Press 'q' to cancel", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
print("\nProcessing cancelled by user")
break
pbar.update(1)
except KeyboardInterrupt:
print("\nProcessing interrupted")
finally:
cap.release()
out.release()
if show_preview:
cv2.destroyAllWindows()
# Summary
print(f"\nProcessing complete!")
print(f" Processed: {frame_count} frames")
print(f" Total faces detected: {total_faces}")
print(f" Average faces per frame: {total_faces/frame_count:.2f}" if frame_count > 0 else "")
print(f" Output saved: {output_path}")
def main():
parser = argparse.ArgumentParser(description="Process video with face detection")
parser.add_argument("--input", type=str, required=True, help="Path to input video")
parser.add_argument("--output", type=str, required=True, help="Path to output video")
parser.add_argument("--detector", type=str, default="retinaface",
choices=['retinaface', 'scrfd'], help="Face detector to use")
parser.add_argument("--threshold", type=float, default=0.6,
help="Confidence threshold for visualization")
parser.add_argument("--fps", type=int, default=None,
help="Output FPS (default: same as input)")
parser.add_argument("--preview", action="store_true",
help="Show live preview during processing")
parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
args = parser.parse_args()
# Check input exists
if not Path(args.input).exists():
print(f"Error: Input file '{args.input}' does not exist")
return
# Create output directory if needed
output_dir = Path(args.output).parent
if output_dir != Path('.'):
output_dir.mkdir(parents=True, exist_ok=True)
if args.verbose:
from uniface import enable_logging
enable_logging()
# Initialize detector
print(f"Initializing detector: {args.detector}")
if args.detector == 'retinaface':
detector = RetinaFace()
else:
detector = SCRFD()
print("Detector initialized\n")
# Process video
process_video(detector, args.input, args.output, args.threshold, args.fps, args.preview)
if __name__ == "__main__":
main()