mirror of
https://github.com/yakhyo/uniface.git
synced 2026-05-18 06:37:47 +00:00
ref: Update package mngt and optimize the vector store functions (#115)
* ref: Update download and hash chunk sizes to speed up * build: Adopt uv with uv.lock and drop requirements.txt * ref: Centralize softmax helper and minor cleanups
This commit is contained in:
committed by
GitHub
parent
73fc291930
commit
b813dc2ee7
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -52,26 +52,23 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "pip"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install ".[cpu,dev]"
|
||||
run: uv sync --locked --extra cpu --extra dev
|
||||
|
||||
- name: Check ONNX Runtime providers
|
||||
run: |
|
||||
python -c "import onnxruntime as ort; print('Available providers:', ort.get_available_providers())"
|
||||
run: uv run python -c "import onnxruntime as ort; print('Available providers:', ort.get_available_providers())"
|
||||
|
||||
- name: Run tests
|
||||
run: pytest -v --tb=short
|
||||
run: uv run pytest -v --tb=short
|
||||
|
||||
- name: Test package imports
|
||||
run: python -c "import uniface; print(f'uniface {uniface.__version__} loaded with {len(uniface.__all__)} exports')"
|
||||
run: uv run python -c "import uniface; print(f'uniface {uniface.__version__} loaded with {len(uniface.__all__)} exports')"
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
10
.github/workflows/docs.yml
vendored
10
.github/workflows/docs.yml
vendored
@@ -14,19 +14,19 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install mkdocs-material pymdown-extensions mkdocs-git-committers-plugin-2 mkdocs-git-revision-date-localized-plugin
|
||||
run: uv sync --locked --extra docs
|
||||
|
||||
- name: Build docs
|
||||
env:
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.MKDOCS_GIT_COMMITTERS_APIKEY }}
|
||||
run: mkdocs build --strict
|
||||
run: uv run mkdocs build --strict
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
|
||||
23
.github/workflows/pipeline.yml
vendored
23
.github/workflows/pipeline.yml
vendored
@@ -68,19 +68,17 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install ".[cpu,dev]"
|
||||
run: uv sync --locked --extra cpu --extra dev
|
||||
|
||||
- name: Run tests
|
||||
run: pytest -v --tb=short
|
||||
run: uv run pytest -v --tb=short
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -198,20 +196,19 @@ jobs:
|
||||
ref: v${{ inputs.version }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install mkdocs-material pymdown-extensions mkdocs-git-committers-plugin-2 mkdocs-git-revision-date-localized-plugin
|
||||
run: uv sync --locked --extra docs
|
||||
|
||||
- name: Build docs
|
||||
env:
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.MKDOCS_GIT_COMMITTERS_APIKEY }}
|
||||
run: mkdocs build --strict
|
||||
run: uv run mkdocs build --strict
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
|
||||
@@ -21,25 +21,31 @@ Thank you for considering contributing to UniFace! We welcome contributions of a
|
||||
|
||||
## Development Setup
|
||||
|
||||
We use [uv](https://docs.astral.sh/uv/) for reproducible dev installs. The committed `uv.lock` pins every transitive dependency so contributors and CI resolve to identical versions.
|
||||
|
||||
```bash
|
||||
# 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
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# 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](https://pre-commit.com/) to ensure code quality and consistency. Install and configure it:
|
||||
We use [pre-commit](https://pre-commit.com/) to ensure code quality and consistency. `pre-commit` is included in the `[dev]` extra, so it's already installed after `uv sync`.
|
||||
|
||||
```bash
|
||||
# Install pre-commit
|
||||
pip install pre-commit
|
||||
|
||||
# Install the git hooks
|
||||
pre-commit install
|
||||
uv run pre-commit install
|
||||
|
||||
# (Optional) Run against all files
|
||||
pre-commit run --all-files
|
||||
uv run pre-commit run --all-files
|
||||
```
|
||||
|
||||
Once installed, pre-commit will automatically run on every commit to check:
|
||||
|
||||
@@ -6,16 +6,20 @@ Thank you for contributing to UniFace!
|
||||
|
||||
## Quick Start
|
||||
|
||||
We use [uv](https://docs.astral.sh/uv/) for reproducible dev installs (lockfile-pinned).
|
||||
|
||||
```bash
|
||||
# Install uv first: https://docs.astral.sh/uv/getting-started/installation/
|
||||
|
||||
# Clone
|
||||
git clone https://github.com/yakhyo/uniface.git
|
||||
cd uniface
|
||||
|
||||
# Install dev dependencies
|
||||
pip install -e ".[dev]"
|
||||
# Install runtime + cpu + dev extras from uv.lock (--extra gpu for CUDA)
|
||||
uv sync --extra cpu --extra dev
|
||||
|
||||
# Run tests
|
||||
pytest
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
---
|
||||
@@ -39,10 +43,11 @@ ruff check . --fix
|
||||
|
||||
## Pre-commit Hooks
|
||||
|
||||
`pre-commit` is included in the `[dev]` extra, so `uv sync` already installs it.
|
||||
|
||||
```bash
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
pre-commit run --all-files
|
||||
uv run pre-commit install
|
||||
uv run pre-commit run --all-files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -51,14 +51,20 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
cpu = ["onnxruntime>=1.16.0"]
|
||||
gpu = ["onnxruntime-gpu>=1.16.0"]
|
||||
cpu = [
|
||||
"onnxruntime>=1.16.0; python_version >= '3.11'",
|
||||
"onnxruntime>=1.16.0,<1.24; python_version < '3.11'",
|
||||
]
|
||||
gpu = [
|
||||
"onnxruntime-gpu>=1.16.0; python_version >= '3.11'",
|
||||
"onnxruntime-gpu>=1.16.0,<1.24; python_version < '3.11'",
|
||||
]
|
||||
dev = ["pytest>=7.0.0", "ruff>=0.4.0", "pre-commit>=3.0.0"]
|
||||
docs = [
|
||||
"mkdocs-material>=9.0",
|
||||
"pymdown-extensions>=10.0",
|
||||
"mkdocs-git-committers-plugin-2>=1.0",
|
||||
"mkdocs-git-revision-date-localized-plugin>=2.0",
|
||||
"mkdocs-material",
|
||||
"pymdown-extensions",
|
||||
"mkdocs-git-committers-plugin-2",
|
||||
"mkdocs-git-revision-date-localized-plugin",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
numpy>=1.21.0
|
||||
opencv-python>=4.5.0
|
||||
scikit-image>=0.22.0
|
||||
scipy>=1.7.0
|
||||
requests>=2.28.0
|
||||
tqdm>=4.64.0
|
||||
# Install ONE of the following (not both):
|
||||
# onnxruntime>=1.16.0 # CPU / Apple Silicon → pip install uniface[cpu]
|
||||
# onnxruntime-gpu>=1.16.0 # NVIDIA CUDA → pip install uniface[gpu]
|
||||
@@ -1,61 +0,0 @@
|
||||
# Copyright 2025-2026 Yakhyokhuja Valikhujaev
|
||||
# Author: Yakhyokhuja Valikhujaev
|
||||
# GitHub: https://github.com/yakhyo
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from uniface.draw import draw_gaze
|
||||
|
||||
|
||||
def _compute_gaze_delta(bbox: np.ndarray, pitch: float, yaw: float) -> tuple[int, int]:
|
||||
"""Replicate draw_gaze dx/dy math for verification."""
|
||||
x_min, _, x_max, _ = map(int, bbox[:4])
|
||||
length = x_max - x_min
|
||||
dx = int(-length * np.sin(yaw) * np.cos(pitch))
|
||||
dy = int(-length * np.sin(pitch))
|
||||
return dx, dy
|
||||
|
||||
|
||||
def test_draw_gaze_yaw_only_moves_horizontally():
|
||||
"""Yaw-only input (pitch=0) should produce horizontal displacement only."""
|
||||
image = np.zeros((200, 200, 3), dtype=np.uint8)
|
||||
bbox = np.array([50, 50, 150, 150], dtype=np.float32)
|
||||
|
||||
yaw = 0.5
|
||||
pitch = 0.0
|
||||
dx, dy = _compute_gaze_delta(bbox, pitch, yaw)
|
||||
|
||||
assert dx != 0, 'Yaw-only should produce horizontal displacement'
|
||||
assert dy == 0, 'Yaw-only should produce zero vertical displacement'
|
||||
|
||||
# Should not raise
|
||||
draw_gaze(image, bbox, pitch, yaw, draw_bbox=False, draw_angles=False)
|
||||
|
||||
|
||||
def test_draw_gaze_pitch_only_moves_vertically():
|
||||
"""Pitch-only input (yaw=0) should produce vertical displacement only."""
|
||||
image = np.zeros((200, 200, 3), dtype=np.uint8)
|
||||
bbox = np.array([50, 50, 150, 150], dtype=np.float32)
|
||||
|
||||
yaw = 0.0
|
||||
pitch = 0.5
|
||||
dx, dy = _compute_gaze_delta(bbox, pitch, yaw)
|
||||
|
||||
assert dx == 0, 'Pitch-only should produce zero horizontal displacement'
|
||||
assert dy != 0, 'Pitch-only should produce vertical displacement'
|
||||
|
||||
# Should not raise
|
||||
draw_gaze(image, bbox, pitch, yaw, draw_bbox=False, draw_angles=False)
|
||||
|
||||
|
||||
def test_draw_gaze_modifies_image():
|
||||
"""draw_gaze should modify the image in place."""
|
||||
image = np.zeros((200, 200, 3), dtype=np.uint8)
|
||||
bbox = np.array([50, 50, 150, 150], dtype=np.float32)
|
||||
|
||||
original = image.copy()
|
||||
draw_gaze(image, bbox, 0.3, 0.3)
|
||||
|
||||
assert not np.array_equal(image, original), 'draw_gaze should modify the image'
|
||||
@@ -198,7 +198,10 @@ def main():
|
||||
parser_arg.add_argument('--source', type=str, required=True, help='Image/video path or camera ID (0, 1, ...)')
|
||||
parser_arg.add_argument('--save-dir', type=str, default='outputs', help='Output directory')
|
||||
parser_arg.add_argument(
|
||||
'--model', type=str, default=ParsingWeights.RESNET18, choices=[ParsingWeights.RESNET18, ParsingWeights.RESNET34]
|
||||
'--model',
|
||||
type=ParsingWeights,
|
||||
default=ParsingWeights.RESNET18,
|
||||
choices=list(ParsingWeights),
|
||||
)
|
||||
parser_arg.add_argument(
|
||||
'--expand-ratio',
|
||||
|
||||
@@ -113,9 +113,10 @@ class FaceAnalyzer:
|
||||
return faces
|
||||
|
||||
def __repr__(self) -> str:
|
||||
parts = [f'FaceAnalyzer(detector={self.detector.__class__.__name__}']
|
||||
parts = [f'detector={self.detector.__class__.__name__}']
|
||||
if self.recognizer:
|
||||
parts.append(f'recognizer={self.recognizer.__class__.__name__}')
|
||||
for attr in self.attributes:
|
||||
parts.append(f'{attr.__class__.__name__}')
|
||||
return ', '.join(parts) + ')'
|
||||
if self.attributes:
|
||||
attr_names = ', '.join(attr.__class__.__name__ for attr in self.attributes)
|
||||
parts.append(f'attributes=[{attr_names}]')
|
||||
return f'FaceAnalyzer({", ".join(parts)})'
|
||||
|
||||
@@ -7,6 +7,7 @@ import cv2
|
||||
import numpy as np
|
||||
|
||||
from uniface.attribute.base import Attribute
|
||||
from uniface.common import softmax
|
||||
from uniface.constants import FairFaceWeights
|
||||
from uniface.log import Logger
|
||||
from uniface.model_store import verify_model_weights
|
||||
@@ -150,9 +151,9 @@ class FairFace(Attribute):
|
||||
race_logits, gender_logits, age_logits = prediction
|
||||
|
||||
# Apply softmax
|
||||
race_probs = self._softmax(race_logits[0])
|
||||
gender_probs = self._softmax(gender_logits[0])
|
||||
age_probs = self._softmax(age_logits[0])
|
||||
race_probs = softmax(race_logits[0])
|
||||
gender_probs = softmax(gender_logits[0])
|
||||
age_probs = softmax(age_logits[0])
|
||||
|
||||
# Get predictions
|
||||
race_idx = int(np.argmax(race_probs))
|
||||
@@ -186,9 +187,3 @@ class FairFace(Attribute):
|
||||
face.age_group = result.age_group
|
||||
face.race = result.race
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _softmax(x: np.ndarray) -> np.ndarray:
|
||||
"""Compute softmax values for numerical stability."""
|
||||
exp_x = np.exp(x - np.max(x))
|
||||
return exp_x / np.sum(exp_x)
|
||||
|
||||
@@ -19,6 +19,7 @@ __all__ = [
|
||||
'letterbox_resize',
|
||||
'non_max_suppression',
|
||||
'resize_image',
|
||||
'softmax',
|
||||
'xyxy_to_cxcywh',
|
||||
]
|
||||
|
||||
@@ -63,6 +64,21 @@ def resize_image(
|
||||
return image, resize_factor
|
||||
|
||||
|
||||
def softmax(x: np.ndarray, axis: int = -1) -> np.ndarray:
|
||||
"""Compute the numerically stable softmax of an array along ``axis``.
|
||||
|
||||
Args:
|
||||
x: Input array.
|
||||
axis: Axis along which softmax is computed. Defaults to the last axis.
|
||||
|
||||
Returns:
|
||||
Array of the same shape as *x* with values in ``[0, 1]`` summing to 1
|
||||
along *axis*.
|
||||
"""
|
||||
exp_x = np.exp(x - np.max(x, axis=axis, keepdims=True))
|
||||
return exp_x / np.sum(exp_x, axis=axis, keepdims=True)
|
||||
|
||||
|
||||
def xyxy_to_cxcywh(bboxes: np.ndarray) -> np.ndarray:
|
||||
"""Convert bounding boxes from ``[x1, y1, x2, y2]`` to ``[cx, cy, w, h]``.
|
||||
|
||||
|
||||
@@ -469,4 +469,5 @@ MODEL_REGISTRY: dict[Enum, ModelInfo] = {
|
||||
MODEL_URLS: dict[Enum, str] = {k: v.url for k, v in MODEL_REGISTRY.items()}
|
||||
MODEL_SHA256: dict[Enum, str] = {k: v.sha256 for k, v in MODEL_REGISTRY.items()}
|
||||
|
||||
CHUNK_SIZE = 8192
|
||||
DOWNLOAD_CHUNK_SIZE = 256 * 1024 # 256 KiB
|
||||
HASH_CHUNK_SIZE = 1024 * 1024 # 1 MiB
|
||||
|
||||
@@ -13,7 +13,7 @@ from typing import Any, Literal
|
||||
|
||||
import numpy as np
|
||||
|
||||
from uniface.common import letterbox_resize, non_max_suppression
|
||||
from uniface.common import letterbox_resize, non_max_suppression, softmax
|
||||
from uniface.constants import YOLOv8FaceWeights
|
||||
from uniface.log import Logger
|
||||
from uniface.model_store import verify_model_weights
|
||||
@@ -171,12 +171,6 @@ class YOLOv8Face(BaseDetector):
|
||||
"""
|
||||
return self.session.run(self.output_names, {self.input_names: input_tensor})
|
||||
|
||||
@staticmethod
|
||||
def _softmax(x: np.ndarray, axis: int = -1) -> np.ndarray:
|
||||
"""Compute softmax values for array x along specified axis."""
|
||||
exp_x = np.exp(x - np.max(x, axis=axis, keepdims=True))
|
||||
return exp_x / np.sum(exp_x, axis=axis, keepdims=True)
|
||||
|
||||
def postprocess(
|
||||
self,
|
||||
predictions: list[np.ndarray],
|
||||
@@ -224,7 +218,7 @@ class YOLOv8Face(BaseDetector):
|
||||
|
||||
# Decode bounding boxes from DFL
|
||||
bbox_pred = bbox_pred.reshape(-1, 4, 16)
|
||||
bbox_dist = self._softmax(bbox_pred, axis=-1) @ np.arange(16)
|
||||
bbox_dist = softmax(bbox_pred, axis=-1) @ np.arange(16)
|
||||
|
||||
# Convert distances to xyxy format
|
||||
x1 = (grid_x - bbox_dist[:, 0]) * stride
|
||||
|
||||
@@ -677,14 +677,10 @@ def vis_parsing_maps(
|
||||
segmentation_mask = segmentation_mask.copy().astype(np.uint8)
|
||||
|
||||
# Create a color mask
|
||||
segmentation_mask_color = np.zeros((segmentation_mask.shape[0], segmentation_mask.shape[1], 3))
|
||||
|
||||
num_classes = np.max(segmentation_mask)
|
||||
for class_index in range(1, num_classes + 1):
|
||||
class_pixels = np.where(segmentation_mask == class_index)
|
||||
segmentation_mask_color[class_pixels[0], class_pixels[1], :] = FACE_PARSING_COLORS[class_index]
|
||||
|
||||
segmentation_mask_color = segmentation_mask_color.astype(np.uint8)
|
||||
max_class = int(segmentation_mask.max())
|
||||
palette = np.zeros((max(max_class + 1, len(FACE_PARSING_COLORS)), 3), dtype=np.uint8)
|
||||
palette[: len(FACE_PARSING_COLORS)] = FACE_PARSING_COLORS
|
||||
segmentation_mask_color = palette[segmentation_mask]
|
||||
|
||||
# Convert image to BGR format for blending
|
||||
bgr_image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from uniface.common import softmax
|
||||
from uniface.constants import GazeWeights
|
||||
from uniface.log import Logger
|
||||
from uniface.model_store import verify_model_weights
|
||||
@@ -142,11 +143,6 @@ class MobileGaze(BaseGazeEstimator):
|
||||
|
||||
return image
|
||||
|
||||
def _softmax(self, x: np.ndarray) -> np.ndarray:
|
||||
"""Apply softmax along axis 1."""
|
||||
e_x = np.exp(x - np.max(x, axis=1, keepdims=True))
|
||||
return e_x / e_x.sum(axis=1, keepdims=True)
|
||||
|
||||
def postprocess(self, outputs: tuple[np.ndarray, np.ndarray]) -> GazeResult:
|
||||
"""
|
||||
Postprocess raw model outputs into gaze angles.
|
||||
@@ -164,8 +160,8 @@ class MobileGaze(BaseGazeEstimator):
|
||||
yaw_logits, pitch_logits = outputs
|
||||
|
||||
# Convert logits to probabilities
|
||||
yaw_probs = self._softmax(yaw_logits)
|
||||
pitch_probs = self._softmax(pitch_logits)
|
||||
yaw_probs = softmax(yaw_logits)
|
||||
pitch_probs = softmax(pitch_logits)
|
||||
|
||||
# Compute expected bin index (soft-argmax)
|
||||
yaw_deg = np.sum(yaw_probs * self._idx_tensor, axis=1) * self._binwidth - self._angle_offset
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
# Author: Yakhyokhuja Valikhujaev
|
||||
# GitHub: https://github.com/yakhyo
|
||||
|
||||
"""Logging utilities for UniFace.
|
||||
|
||||
This module provides a centralized logger for the UniFace library,
|
||||
allowing users to enable verbose logging when debugging or developing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
# Author: Yakhyokhuja Valikhujaev
|
||||
# GitHub: https://github.com/yakhyo
|
||||
|
||||
"""Model weight management for UniFace.
|
||||
|
||||
This module handles downloading, caching, and verifying model weights
|
||||
using SHA-256 checksums for integrity validation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
@@ -159,7 +153,7 @@ def download_file(url: str, dest_path: str, timeout: int = 60, max_retries: int
|
||||
unit_divisor=1024,
|
||||
) as progress,
|
||||
):
|
||||
for chunk in response.iter_content(chunk_size=const.CHUNK_SIZE):
|
||||
for chunk in response.iter_content(chunk_size=const.DOWNLOAD_CHUNK_SIZE):
|
||||
if chunk:
|
||||
file.write(chunk)
|
||||
progress.update(len(chunk))
|
||||
@@ -178,7 +172,7 @@ def verify_file_hash(file_path: str, expected_hash: str) -> bool:
|
||||
"""Compute the SHA-256 hash of the file and compare it with the expected hash."""
|
||||
file_hash = hashlib.sha256()
|
||||
with open(file_path, 'rb') as f:
|
||||
for chunk in iter(lambda: f.read(const.CHUNK_SIZE), b''):
|
||||
for chunk in iter(lambda: f.read(const.HASH_CHUNK_SIZE), b''):
|
||||
file_hash.update(chunk)
|
||||
actual_hash = file_hash.hexdigest()
|
||||
if actual_hash != expected_hash:
|
||||
@@ -187,7 +181,7 @@ def verify_file_hash(file_path: str, expected_hash: str) -> bool:
|
||||
|
||||
|
||||
def download_models(
|
||||
model_names: list[Enum], max_workers: int = 4, timeout: int = 60, max_retries: int = 3
|
||||
model_names: list[Enum], max_workers: int | None = None, timeout: int = 60, max_retries: int = 3
|
||||
) -> dict[Enum, str]:
|
||||
"""Download and verify multiple models concurrently.
|
||||
|
||||
@@ -214,6 +208,17 @@ def download_models(
|
||||
results: dict[Enum, str] = {}
|
||||
errors: list[str] = []
|
||||
|
||||
if isinstance(max_workers, bool) or not isinstance(max_workers, int | None):
|
||||
raise TypeError(f'max_workers must be int or None, got {type(max_workers).__name__}')
|
||||
|
||||
if max_workers is None or max_workers < 1:
|
||||
if max_workers < 1:
|
||||
Logger.info(f'max_workers must be >= 1, got {max_workers}; falling back to auto mode')
|
||||
max_workers = min(os.cpu_count(), 8) # at most 8
|
||||
|
||||
if max_workers < 1:
|
||||
raise ValueError(f'max_workers must be >= 1, got {max_workers}')
|
||||
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
future_to_model = {
|
||||
executor.submit(verify_model_weights, name, timeout=timeout, max_retries=max_retries): name
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from uniface.common import softmax
|
||||
from uniface.constants import MiniFASNetWeights
|
||||
from uniface.log import Logger
|
||||
from uniface.model_store import verify_model_weights
|
||||
@@ -179,11 +180,6 @@ class MiniFASNet(BaseSpoofer):
|
||||
|
||||
return face
|
||||
|
||||
def _softmax(self, x: np.ndarray) -> np.ndarray:
|
||||
"""Apply softmax to logits along axis 1."""
|
||||
e_x = np.exp(x - np.max(x, axis=1, keepdims=True))
|
||||
return e_x / e_x.sum(axis=1, keepdims=True)
|
||||
|
||||
def postprocess(self, outputs: np.ndarray) -> SpoofingResult:
|
||||
"""
|
||||
Postprocess raw model outputs into prediction result.
|
||||
@@ -197,7 +193,7 @@ class MiniFASNet(BaseSpoofer):
|
||||
Returns:
|
||||
SpoofingResult: Result containing is_real flag and confidence score.
|
||||
"""
|
||||
probs = self._softmax(outputs)
|
||||
probs = softmax(outputs)
|
||||
label_idx = int(np.argmax(probs))
|
||||
confidence = float(probs[0, label_idx])
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ class FAISS(BaseStore):
|
||||
return None, similarity
|
||||
|
||||
def remove(self, key: str, value: Any) -> int:
|
||||
"""Remove all entries where ``metadata[key] == value`` and rebuild.
|
||||
"""Remove all entries where ``metadata[key] == value``.
|
||||
|
||||
Args:
|
||||
key: Metadata key to match against.
|
||||
@@ -126,22 +126,19 @@ class FAISS(BaseStore):
|
||||
"""
|
||||
faiss = _import_faiss()
|
||||
|
||||
keep = [i for i, m in enumerate(self.metadata) if m.get(key) != value]
|
||||
removed = len(self.metadata) - len(keep)
|
||||
if removed == 0:
|
||||
to_remove = [i for i, m in enumerate(self.metadata) if m.get(key) == value]
|
||||
if not to_remove:
|
||||
return 0
|
||||
|
||||
if keep:
|
||||
vectors = np.empty((len(keep), self.embedding_size), dtype=np.float32)
|
||||
for dst, src in enumerate(keep):
|
||||
self.index.reconstruct(src, vectors[dst])
|
||||
new_index = faiss.IndexFlatIP(self.embedding_size)
|
||||
new_index.add(vectors)
|
||||
else:
|
||||
new_index = faiss.IndexFlatIP(self.embedding_size)
|
||||
ids = np.array(to_remove, dtype=np.int64)
|
||||
self.index.remove_ids(faiss.IDSelectorBatch(ids))
|
||||
|
||||
self.index = new_index
|
||||
self.metadata = [self.metadata[i] for i in keep]
|
||||
# IndexFlatIP.remove_ids preserves the relative order of survivors,
|
||||
# so deleting the same positions from metadata keeps them aligned.
|
||||
drop = set(to_remove)
|
||||
self.metadata = [m for i, m in enumerate(self.metadata) if i not in drop]
|
||||
|
||||
removed = len(to_remove)
|
||||
Logger.info('Removed %d entries where %s=%s (%d remaining)', removed, key, value, self.index.ntotal)
|
||||
return removed
|
||||
|
||||
|
||||
Reference in New Issue
Block a user