Skip to content

Commit 5ef4033

Browse files
committed
feat: add new face images to the gallery database
0 parents  commit 5ef4033

14 files changed

Lines changed: 569 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
branches: [main, master]
8+
9+
jobs:
10+
test:
11+
runs-on: ${{ matrix.os }}
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
os: [ubuntu-latest, windows-latest]
16+
python-version: ["3.10", "3.12"]
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Set up Python
22+
uses: actions/setup-python@v5
23+
with:
24+
python-version: ${{ matrix.python-version }}
25+
26+
- name: Install
27+
run: python -m pip install --upgrade pip
28+
- name: Dev dependencies
29+
run: python -m pip install -e ".[dev]"
30+
31+
- name: Pytest
32+
run: python -m pytest

.gitignore

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
.venv/
8+
venv/
9+
ENV/
10+
env/
11+
*.egg-info/
12+
.eggs/
13+
dist/
14+
build/
15+
pip-wheel-metadata/
16+
.pytest_cache/
17+
.coverage
18+
htmlcov/
19+
20+
# IDEs
21+
.vs/
22+
*.suo
23+
*.user
24+
*.userosscache
25+
*.sln.docstates
26+
.idea/
27+
*.iml
28+
29+
# Windows / macOS
30+
Thumbs.db
31+
ehthumbs.db
32+
Desktop.ini
33+
.DS_Store
34+
35+
# Modelos ONNX (caché del usuario; por defecto ~/.cache/face_match/)
36+
*.onnx
37+
*.onnx.part
38+
39+
# Caché de embeddings junto a la galería
40+
database/.face_embeddings_cache.pkl
41+
database/gallery

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 MOCA
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# face-match
2+
3+
**English (summary):** Small, dependency-light CLI to find the most **visually similar faces** in a folder of images (OpenCV YuNet + SFace). It does **not** prove legal identity, is not a certified biometric system, and must be used with a clear legal basis and governance. See **Disclaimer** below.
4+
5+
---
6+
7+
## Qué es
8+
9+
Herramienta de línea de comandos que, dada una **foto de consulta**, recorre una **carpeta de imágenes** (incluye subcarpetas), detecta rostros y ordena las fotos por **similitud** al rostro de la consulta. Pensada como base técnica para integraciones (no sustituye un producto completo de control de acceso o RR.HH.).
10+
11+
## Requisitos
12+
13+
- Python 3.9 o superior
14+
- Conexión a Internet la **primera vez** (descarga ~40 MB de modelos ONNX de [OpenCV Zoo](https://github.com/opencv/opencv_zoo); luego quedan en caché local)
15+
16+
## Instalación
17+
18+
```bash
19+
git clone https://github.com/Moca9801/tracking-face.git
20+
cd tracking-face
21+
python -m venv .venv
22+
# Windows: .venv\Scripts\activate
23+
# Linux/macOS: source .venv/bin/activate
24+
pip install -e ".[dev]"
25+
```
26+
27+
Instalación solo de ejecución (sin tests):
28+
29+
```bash
30+
pip install .
31+
```
32+
33+
## Uso
34+
35+
1. Coloca las imágenes de la galería en la carpeta `database` del directorio actual **o** indica otra con `--db`.
36+
2. Ejecuta:
37+
38+
```bash
39+
face-match ruta/a/consulta.jpg
40+
face-match consulta.jpg --db C:\ruta\galeria -n 20
41+
face-match consulta.jpg --rebuild
42+
python -m face_match consulta.jpg --db ./database
43+
```
44+
45+
Opciones:
46+
47+
| Opción | Descripción |
48+
|--------|-------------|
49+
| `query` | Imagen con el rostro a buscar (obligatorio) |
50+
| `--db` | Carpeta de la galería (por defecto `./database`) |
51+
| `-n` / `--top` | Cuántos resultados mostrar (por defecto 10) |
52+
| `--metric` | `cosine` (por defecto) o `l2` |
53+
| `--rebuild` | Ignora la caché y vuelve a calcular embeddings |
54+
55+
### Modelos en disco
56+
57+
Por defecto los modelos se guardan en `%USERPROFILE%\.cache\face_match\models` (Windows) o `~/.cache/face_match/models` (Linux/macOS). Puedes cambiar la ubicación con la variable de entorno `FACE_MATCH_MODELS` (ruta a un directorio).
58+
59+
### Caché de la galería
60+
61+
En la carpeta `--db` se crea `.face_embeddings_cache.pkl` para no recalcular embeddings si el archivo no cambió. Usa `--rebuild` para forzar recálculo.
62+
63+
## Desarrollo
64+
65+
```bash
66+
pip install -e ".[dev]"
67+
pytest
68+
```
69+
70+
## Aviso legal, privacidad y uso responsable
71+
72+
- El **reconocimiento facial** y los **datos biométricos** están sometidos a leyes estrictas en muchos países (p. ej. RGPD en la UE, leyes locales en Latinoamérica). **No uses** este software sin base legal clara, política de privacidad, minimización de datos y, donde proceda, **consentimiento informado** de las personas afectadas.
73+
- Esta herramienta devuelve **similitud visual** entre fotos, **no** certifica identidad ni debería usarse como única prueba en empleo, vigilancia o procesos judiciales sin controles humanos y procedimientos definidos por tu organización y asesoría jurídica.
74+
- Los **falsos positivos** (personas distintas con rostro parecido) y **falsos negativos** son posibles; la iluminación, calidad, edad, accesorios y sesgos del modelo afectan el resultado.
75+
- Los autores no se hacen responsables del uso que terceros hagan del software; se ofrece **«tal cual»** según la licencia MIT.
76+
77+
## Licencia
78+
79+
MIT — ver [LICENSE](LICENSE).
80+
81+
## Créditos
82+
83+
- Detección y reconocimiento basados en modelos publicados en [OpenCV Zoo](https://github.com/opencv/opencv_zoo) (YuNet, SFace).

database/.gitkeep

Whitespace-only changes.

pyproject.toml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
[build-system]
2+
requires = ["setuptools>=61", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "face-match"
7+
version = "0.1.0"
8+
description = "Búsqueda de coincidencias faciales en una carpeta de imágenes (OpenCV YuNet + SFace, sin dependencias de deep learning pesadas)."
9+
readme = "README.md"
10+
license = "MIT"
11+
requires-python = ">=3.9"
12+
authors = [{ name = "MOCA" }]
13+
keywords = ["face-recognition", "opencv", "biometrics", "face-search", "sface"]
14+
classifiers = [
15+
"Development Status :: 4 - Beta",
16+
"Environment :: Console",
17+
"Intended Audience :: Developers",
18+
"Intended Audience :: System Administrators",
19+
"Operating System :: OS Independent",
20+
"Programming Language :: Python :: 3",
21+
"Programming Language :: Python :: 3.9",
22+
"Programming Language :: Python :: 3.10",
23+
"Programming Language :: Python :: 3.11",
24+
"Programming Language :: Python :: 3.12",
25+
"Topic :: Scientific/Engineering :: Image Processing",
26+
"Topic :: Software Development :: Libraries :: Python Modules",
27+
"Typing :: Typed",
28+
]
29+
dependencies = [
30+
"opencv-python>=4.8.0",
31+
"numpy>=1.24.0",
32+
]
33+
34+
[project.optional-dependencies]
35+
dev = [
36+
"pytest>=7.0",
37+
]
38+
39+
[project.urls]
40+
"Homepage" = "https://github.com/Moca9801/tracking-face"
41+
"Bug Tracker" = "https://github.com/Moca9801/tracking-face/issues"
42+
43+
[project.scripts]
44+
face-match = "face_match.cli:main"
45+
46+
[tool.setuptools]
47+
package-dir = { "" = "src" }
48+
49+
[tool.setuptools.packages.find]
50+
where = ["src"]
51+
52+
[tool.setuptools.package-data]
53+
face_match = ["py.typed"]
54+
55+
[tool.pytest.ini_options]
56+
testpaths = ["tests"]
57+
pythonpath = ["src"]
58+
addopts = "-q"

src/face_match/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Búsqueda de coincidencias faciales en una galería local de imágenes."""
2+
3+
__version__ = "0.1.0"

src/face_match/__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from face_match.cli import main
2+
3+
if __name__ == "__main__":
4+
raise SystemExit(main())

src/face_match/cli.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import annotations
3+
4+
import argparse
5+
from pathlib import Path
6+
7+
from face_match.search import run_search
8+
9+
10+
def main() -> int:
11+
default_db = Path.cwd() / "database"
12+
parser = argparse.ArgumentParser(
13+
description="Encuentra fotos con rostros similares a una imagen de consulta."
14+
)
15+
parser.add_argument("query", type=Path, help="Imagen con el rostro a buscar.")
16+
parser.add_argument(
17+
"--db",
18+
type=Path,
19+
default=default_db,
20+
help=f"Carpeta con la galería (recursivo). Por defecto: {default_db} (carpeta actual).",
21+
)
22+
parser.add_argument(
23+
"-n", "--top", type=int, default=10, help="Máximo de resultados a listar."
24+
)
25+
parser.add_argument(
26+
"--metric",
27+
choices=["cosine", "l2"],
28+
default="cosine",
29+
help="Distancia entre vectores (por defecto cosine / FR_COSINE).",
30+
)
31+
parser.add_argument(
32+
"--rebuild",
33+
action="store_true",
34+
help="Ignora caché y vuelve a extraer descriptores de toda la base.",
35+
)
36+
args = parser.parse_args()
37+
dist = 0 if args.metric == "cosine" else 1
38+
return run_search(
39+
query=args.query,
40+
db=args.db,
41+
top=max(1, args.top),
42+
distance=dist,
43+
rebuild_cache=args.rebuild,
44+
)

src/face_match/core.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import annotations
3+
4+
import os
5+
import pickle
6+
import sys
7+
import urllib.request
8+
from pathlib import Path
9+
10+
import cv2
11+
import numpy as np
12+
13+
IMAGE_EXTENSIONS: frozenset[str] = frozenset(
14+
(".jpg", ".jpeg", ".png", ".bmp", ".webp")
15+
)
16+
MODELS_BASE = "https://github.com/opencv/opencv_zoo/raw/main/models"
17+
YUNET_NAME = "face_detection_yunet/face_detection_yunet_2023mar.onnx"
18+
SFACE_NAME = "face_recognition_sface/face_recognition_sface_2021dec.onnx"
19+
CACHE_NAME = ".face_embeddings_cache.pkl"
20+
ENV_MODELS = "FACE_MATCH_MODELS"
21+
22+
23+
def get_models_dir() -> Path:
24+
override = os.environ.get(ENV_MODELS)
25+
if override:
26+
p = Path(override).expanduser().resolve()
27+
else:
28+
p = Path.home() / ".cache" / "face_match" / "models"
29+
p.mkdir(parents=True, exist_ok=True)
30+
return p
31+
32+
33+
def ensure_model(filename: str) -> Path:
34+
name = filename.split("/")[-1]
35+
path = get_models_dir() / name
36+
if path.is_file() and path.stat().st_size > 1000:
37+
return path
38+
url = f"{MODELS_BASE}/{filename}"
39+
print(f"Descargando modelo: {name} (puede tardar)...", file=sys.stderr)
40+
part = path.with_suffix(path.suffix + ".part")
41+
urllib.request.urlretrieve(url, str(part))
42+
part.replace(path)
43+
return path
44+
45+
46+
def load_bgr(path: Path) -> np.ndarray | None:
47+
return cv2.imdecode(np.fromfile(str(path), dtype=np.uint8), cv2.IMREAD_COLOR)
48+
49+
50+
def list_image_paths(root: Path) -> list[Path]:
51+
out: list[Path] = []
52+
for p in root.rglob("*"):
53+
if p.is_file() and p.suffix.lower() in IMAGE_EXTENSIONS:
54+
out.append(p)
55+
return sorted(out)
56+
57+
58+
def pick_best_face(faces: np.ndarray | None) -> np.ndarray | None:
59+
if faces is None or faces.size == 0:
60+
return None
61+
f = np.atleast_2d(faces)
62+
if f.shape[1] < 15:
63+
return f[0]
64+
scores = f[:, 14]
65+
return f[int(np.argmax(scores))]
66+
67+
68+
def embed(
69+
bgr: np.ndarray,
70+
detector: cv2.FaceDetectorYN,
71+
recognizer: cv2.FaceRecognizerSF,
72+
) -> np.ndarray | None:
73+
h, w = bgr.shape[:2]
74+
detector.setInputSize((w, h))
75+
_, faces = detector.detect(bgr)
76+
row = pick_best_face(faces)
77+
if row is None:
78+
return None
79+
aligned = recognizer.alignCrop(bgr, row)
80+
return recognizer.feature(aligned)
81+
82+
83+
def load_cache(cache_path: Path) -> dict:
84+
if not cache_path.is_file():
85+
return {}
86+
try:
87+
with open(cache_path, "rb") as f:
88+
return pickle.load(f)
89+
except Exception:
90+
return {}
91+
92+
93+
def save_cache(cache_path: Path, data: dict) -> None:
94+
tmp = cache_path.with_suffix(".tmp")
95+
with open(tmp, "wb") as f:
96+
pickle.dump(data, f, protocol=4)
97+
tmp.replace(cache_path)

0 commit comments

Comments
 (0)