* docs: Minor info changes * refactor: Remove deprecated factory functions Removes the 10 factory functions deprecated in v3.7.0: create_detector, list_available_detectors, create_recognizer, create_face_parser, create_gaze_estimator, create_head_pose_estimator, create_landmarker, create_matting_model, create_spoofer, create_attribute_predictor. Instantiate model classes directly instead, e.g. `from uniface.detection import SCRFD; SCRFD()`. Also drops the now-unused typing_extensions dependency and updates docstring examples that referenced the factories. * refactor: Remove MODEL_URLS and MODEL_SHA256 back-compat mirrors Use MODEL_REGISTRY instead of the MODEL_URLS and MODEL_SHA256 dicts in uniface.constants. * chore: Add .claude directory to gitignore * ci: Deploy docs via GitHub Pages artifact instead of gh-pages branch * feat: Add FaceAttribNet face state prediction model Integrate Qualcomm's FaceAttribNet (Facial-Attribute-Detection) as a new attribute model predicting five independent binary face states from a face crop: left/right eye openness, eyeglasses, mask, and sunglasses. - Add FaceAttribNet class with letterbox preprocessing (normalization is baked into the ONNX graph) and multi-label FaceStateResult output - Register FaceAttribNetWeights with dynamic-batch ONNX weights (opset 17) - Enrich Face with left_eye_open, right_eye_open, eyeglasses, mask, and sunglasses fields - Rename AttributeResult to DemographyResult and DDAMFNWeights to EmotionWeights so 'attribute' unambiguously means the model category - Add tests and docs * docs: Add face attributes notebook, demo assets, and README updates * feat: Add CenterFace face detection model * docs: Add CONTEXT.md glossary and ADRs for v4 naming decisions ADR-0001: Face.landmarks keeps its v3 name for the 5-point set; the future dense-landmark slot will be Face.dense_landmarks. ADR-0002: Attribute stays the per-face predictor base class name; FaceAnalyzer(attributes=...) is unchanged. * refactor: Rename FaceAnalyzer attributes parameter to predictors The pipeline parameter names the role (per-face predictors), so future head pose, gaze, spoofing, and quality models can be passed without the semantic mismatch of being listed as attributes. The Attribute base class keeps its name per ADR-0002. BREAKING CHANGE: FaceAnalyzer(attributes=...) is now FaceAnalyzer(predictors=...); FaceAnalyzer.attributes is now FaceAnalyzer.predictors. * docs: Update docs and notebooks for predictors parameter rename * docs: Remove CONTEXT.md * docs: Restore README badges * docs: Remove ADR folder * refactor: Rename Attribute base class to BaseAttribute * docs: Replace thick README separators with heading underlines * docs: Put README tagline in a callout * fix: Use predictors= in the analyze tool * refactor: Make supports_landmarks a class attribute on BaseDetector Replaces the private _supports_landmarks instance flag and its read-only property with a plain class attribute, mirroring supports_alignment. Every detector set the flag to True in __init__, so the False default never survived and the hasattr guard in the property was dead weight. Also drops a stale Attributes entry in RetinaFace for a field that no longer exists, and removes comments that restated the call beneath them. * docs: Normalize docstring markup and document missing raises Audited every docstring and comment in uniface/ against the detection package, which is the most internally consistent one. - Replace 13 Sphinx roles and 159 RST double-backticks with markdown backticks; nothing renders docstrings, there is no Sphinx, and mkdocs has no mkdocstrings plugin - Rewrite 59 legacy typing generics (List, Tuple, Optional, Union) to PEP 585, each checked against the real signature rather than mechanically substituted - Add Raises sections to download_file, compute_similarity and parse_with_inverse, which raised undocumented exceptions - Drop the RuntimeError Raises block from six abstract _initialize_model methods whose body raises NotImplementedError; the contract belongs to the implementer and is now stated as prose - Fold Properties into Attributes with a Read-only prefix, singularize Examples, and fix the copyright year in faceattribnet No executable line changed; the bulk pass rewrote only string and comment tokens matched by byte offset. * fix: Raise ValueError instead of asserting in estimate_norm Both input checks used bare assert statements, which python -O strips. The docstring promised AssertionError as part of the contract, so under an optimized build the validation silently disappeared and an unsupported image_size flowed into the alignment maths instead of failing fast. Both are now ValueError, and the messages report the offending value. * docs: Replace RST double-backticks in face_utils * docs: Document Raises on every model class The detection package documents ValueError and RuntimeError on the class docstring; no model class in any other package did, even though they all call verify_model_weights in __init__ and reach an _initialize_model that wraps session creation in RuntimeError. Verified per class before adding: each target reaches both raise sites, inheriting _initialize_model from its base where it does not define one. * fix: Replace the last two bare asserts with explicit raises python -O strips assert statements, so both checks silently disappeared in optimized builds. BaseRecognizer would then accept a multi-output model and mis-slice its results, and get_meanface_info would build neighbour tables from a meanface of the wrong length. Behaviour is unchanged in normal builds: the recognizer check already sat inside the try that re-raises as RuntimeError, and the meanface check now raises the ValueError its docstring already documented. No bare asserts remain in uniface/. * style: Put multi-line docstring summaries on the first line Card 6 of the docstring audit: class docstrings opened two different ways, 45 with a leading newline and 37 on the same line as the quotes. Settled by the repo's own config rather than by taste. pyproject already declares pydocstyle convention = google, under which ruff resolves D212 (multi-line-summary-first-line) as the active rule and explicitly ignores the incompatible D213. Same-line was therefore already the declared standard, even though the detection package -- the audit's exemplar -- sat on the other side of the split. Applied with ruff --fix, then the summaries that ended up glued to their description got the blank line D205 wants. * style: Put multi-line docstring summaries on the first line (constants, test_utils) Same D212/D205 pass as the previous commit, applied to the two files that were held back from it. * build: Enforce D205 and D212 in ruff The docstring audit found the opening style split 45/37 with nothing to hold it in place: pyproject declared pydocstyle convention = google, but D was absent from select, so the convention setting was inert. Selecting the two rules the codebase now satisfies makes the decision checkable instead of conventional. The rest of D is left off deliberately -- D100 alone would flag 63 files, since this project's convention is to have no module docstrings. * feat: Add BlazeFace detector and MediaPipe Face Mesh landmarker BlazeFace is MediaPipe's short-range SSD face detector: two anchor grids over a 128x128 letterboxed image, decoded with MediaPipe's weighted NMS where overlapping candidates are score-averaged rather than discarded. It emits 6 MediaPipe keypoints, not the 5-point alignment template, so BaseDetector gains a supports_alignment flag. It is True everywhere else and False only here. FaceAnalyzer checks it and disables recognition with a warning instead of producing broken embeddings, and estimate_norm's error now names the constraint when a caller tries to align anyway. FaceMesh predicts 468 dense 3D landmarks from a face crop and is detector-agnostic: the ROI follows MediaPipe's recipe, a square region at 1.5x the detector box rotated so the eye line is horizontal, built from whichever two eye points the detector provides. Every face in an image runs in one session call. roi_from_box and warp_roi are public so video-mode tracking can be built on top. Also adds FaceMeshResult, draw_mesh with full and partial tessellations, weight entries for both models, the facemesh and facestate tools, example notebook 15, and tests for both models. Tessellation tables are vendored from PINTO0309/facemesh_onnx_tensorrt (Apache-2.0); provenance is recorded in docs/license-attribution.md. * docs: List BlazeFace and Face Mesh in the remaining model indexes The feature commit updated the detection and landmarks module pages, the model zoo and the concept pages, but four summary listings still predated both models: - README examples table stopped at notebook 14 - README training-dataset table omitted both - quickstart model summary omitted both - index.md feature cards described detection as 5-point only, which BlazeFace's 6 keypoints contradict * feat: Add MediaPipe Face Landmarker, a 478-point mesh with irises Google's Face Landmarker is the successor to Face Mesh: one network predicting 478 landmarks from a 256x256 crop, where the first 468 keep the Face Mesh topology and ordering and the last ten are the irises. It ships in the Tasks API bundle; the earlier 478-point attention model is not ported, since it needs three MediaPipe-only TFLite custom ops that no standard runtime can load. FaceMesh needs no logic change to serve it. Input size and landmark count were already read from the ONNX graph, so 192->256 and 468->478 are absorbed at load time. Verified end to end at 0.007% of inter-ocular distance against MediaPipe's own pipeline, with the existing ROI recipe unchanged - Google's model card specifies the same 25% per-side margin the default already used. Iris membership follows MediaPipe's own convention: the result stays a flat array and the region lives in named constants next to the topology it indexes, rather than becoming accessors on FaceMeshResult. draw_mesh renders the irises distinctly, so the library and tools/facemesh.py agree by construction. BREAKING CHANGE: FaceMeshWeights.DEFAULT is replaced by V1_468 and V2_478. Default behaviour is unchanged - the constructor still loads the 468-point model - but code naming DEFAULT explicitly must be updated. Every other multi-variant weights enum already names its members descriptively. * feat: Add Hugging Face mirror fallback for weight downloads * docs: Document the Hugging Face mirror fallback * fix: Resolve bugs found in whole-codebase review * refactor: Publish validate_image and normalize line endings * fix: Guarantee weighted NMS terminates * refactor: Make model constructors keyword-only * refactor: Read input_size from the ONNX model * fix: Normalize and validate attribute input_size * refactor: Replace detector **kwargs with explicit options * fix: Reject unusable images in BlazeFace and FaceMesh * feat: Make detector capability flags opt-in * fix: Show every populated face state in Face repr * docs: Clarify inline comments * docs: Update docs and notebooks for v4 * refactor: Make remaining public constructors keyword-only * feat: Export BaseAttribute and document custom predictors * test: Cover FaceAnalyzer predictors * fix: Pin the Hugging Face mirror to an immutable revision * docs: Add changelog with v4 migration guide * ci: Deploy release docs via the Pages artifact flow * build: Require setuptools 77 for the SPDX license field * docs: Fix anchors, dataset table, and tool options * chore: Release v4.0.0rc1 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
8.4 KiB
Contributing to UniFace
Thank you for considering contributing to UniFace! We welcome contributions of all kinds.
How to Contribute
Reporting Issues
- Use GitHub Issues to report bugs or suggest features
- Include clear descriptions and reproducible examples
- Check existing issues before creating new ones
Pull Requests
- Fork the repository
- Create a new branch for your feature
- Write clear, documented code with type hints
- Add tests for new functionality
- Ensure all tests pass and pre-commit hooks are satisfied
- Submit a pull request with a clear description
Development Setup
We use uv for reproducible dev installs. The committed uv.lock pins every transitive dependency so contributors and CI resolve to identical versions.
# Install uv (https://docs.astral.sh/uv/getting-started/installation/)
curl -LsSf https://astral.sh/uv/install.sh | sh
git clone https://github.com/yakhyo/uniface.git
cd uniface
# Sync runtime + cpu + dev extras from uv.lock (use --extra gpu instead of cpu for CUDA)
uv sync --extra cpu --extra dev
uv sync creates a project-local .venv/ and installs everything pinned in uv.lock. Run commands with uv run <cmd> (e.g. uv run pytest), or activate the venv with source .venv/bin/activate.
Setting Up Pre-commit Hooks
We use pre-commit to ensure code quality and consistency. pre-commit is included in the [dev] extra, so it's already installed after uv sync.
# Install the git hooks
uv run pre-commit install
# (Optional) Run against all files
uv run pre-commit run --all-files
Once installed, pre-commit will automatically run on every commit to check:
- Code formatting and linting (Ruff)
- Security issues (Bandit)
- General file hygiene (trailing whitespace, YAML/TOML validity, etc.)
Note: All PRs are automatically checked by CI. The merge button will only be available after all checks pass.
Code Style
This project uses Ruff for linting and formatting, following modern Python best practices. Pre-commit handles all formatting automatically.
Style Guidelines
General Rules
- Line length: 120 characters maximum
- Python version: 3.10+ (use modern syntax)
- Quote style: Single quotes for strings, double quotes for docstrings
Type Hints
Use modern Python 3.10+ type hints (PEP 585 and PEP 604):
# Preferred (modern)
def process(items: list[str], config: dict[str, int] | None = None) -> tuple[int, str]:
...
# Avoid (legacy)
from typing import List, Dict, Optional, Tuple
def process(items: List[str], config: Optional[Dict[str, int]] = None) -> Tuple[int, str]:
...
Docstrings
Use Google-style docstrings for all public APIs:
def detect(self, image: np.ndarray, **kwargs: Any) -> list[Face]:
"""Detect faces in an image.
Args:
image: Input image as numpy array with shape (H, W, C) in BGR format.
**kwargs: Additional detection parameters.
Returns:
List of detected Face objects.
Raises:
ValueError: If the image is empty, not 3-channel BGR, or not uint8.
Example:
>>> from uniface import RetinaFace
>>> detector = RetinaFace(confidence_threshold=0.8)
>>> faces = detector.detect(image)
>>> print(f"Found {len(faces)} faces")
"""
Import Order
Imports are automatically sorted by Ruff with the following order:
- Future imports (
from __future__ import annotations) - Standard library (
os,sys,typing, etc.) - Third-party (
numpy,cv2,onnxruntime, etc.) - First-party (
uniface.*) - Local (relative imports like
.base,.models)
from __future__ import annotations
import os
from typing import Any
import cv2
import numpy as np
from uniface.constants import RetinaFaceWeights
from uniface.log import Logger
from .base import BaseDetector
Code Comments
- Add comments for complex logic, magic numbers, and non-obvious behavior
- Avoid comments that merely restate the code
- Use
# TODO:with issue links for planned improvements
# RetinaFace FPN strides and corresponding anchor sizes per level
steps = [8, 16, 32]
min_sizes = [[16, 32], [64, 128], [256, 512]]
# Add small epsilon to prevent division by zero
similarity = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-5)
Running Tests
# Run all tests
pytest tests/
# Run with verbose output
pytest tests/ -v
# Run specific test file
pytest tests/test_scrfd.py
# Run with coverage
pytest tests/ --cov=uniface --cov-report=html
Adding New Features
When adding a new model or feature:
- Create the model class in the appropriate submodule (e.g.,
uniface/detection/) - Add weight constants to
uniface/constants.pywith URLs and SHA256 hashes - Export in
__init__.pyfiles at both module and package levels - Write tests in
tests/directory - Add example usage in
tools/or update existing notebooks - Update documentation if needed
Examples
Example notebooks demonstrating library usage:
| Example | Notebook |
|---|---|
| Face Detection | 01_face_detection.ipynb |
| Face Alignment | 02_face_alignment.ipynb |
| Face Verification | 03_face_verification.ipynb |
| Face Search | 04_face_search.ipynb |
| Face Analyzer | 05_face_analyzer.ipynb |
| Face Parsing | 06_face_parsing.ipynb |
| Face Anonymization | 07_face_anonymization.ipynb |
| Gaze Estimation | 08_gaze_estimation.ipynb |
| Face Segmentation | 09_face_segmentation.ipynb |
| Face Vector Store | 10_face_vector_store.ipynb |
| Head Pose Estimation | 11_head_pose_estimation.ipynb |
| Face Recognition | 12_face_recognition.ipynb |
| Portrait Matting | 13_portrait_matting.ipynb |
| Face Attributes | 14_face_attributes.ipynb |
| Face Mesh | 15_face_mesh.ipynb |
Release Process
Releases are fully automated via GitHub Actions. Only maintainers with branch-protection bypass privileges on main can trigger a release.
Cutting a release
- Go to Actions → Release Pipeline → Run workflow on GitHub.
- Enter the version following PEP 440:
- Stable:
0.7.0,1.0.0 - Pre-release:
0.7.0rc1,0.7.0b1,0.7.0a1,0.7.0.dev1
- Stable:
- Click Run workflow.
What happens automatically
The Release Pipeline workflow runs all stages in sequence:
- Validate — checks the version string against PEP 440 and confirms the tag does not already exist.
- Test — runs the test suite on Python 3.10–3.14.
- Release — updates
pyproject.tomlanduniface/__init__.py, commitschore: Release vX.Y.Ztomain, creates and pushes tagvX.Y.Z. - Publish — builds the package, uploads to PyPI, and creates a GitHub Release (flagged as pre-release for
a/b/rc/.devversions). - Deploy docs — runs only for stable versions. Pre-releases do not update the live documentation site.
Verifying a release
- PyPI: https://pypi.org/project/uniface/
- GitHub Releases: https://github.com/yakhyo/uniface/releases
- Docs (stable only): https://yakhyo.github.io/uniface/
Installing a pre-release
End users can opt in to pre-releases with the --pre flag:
pip install uniface --pre # latest pre-release
pip install uniface==0.7.0rc1 # specific pre-release
Without --pre, pip install uniface always resolves to the latest stable version.
Questions?
Open an issue or start a discussion on GitHub.