mirror of
https://github.com/yakhyo/uniface.git
synced 2026-05-15 12:57:55 +00:00
292 lines
9.1 KiB
Plaintext
292 lines
9.1 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"# Face Vector Store with FAISS\n",
|
|
"\n",
|
|
"<div style=\"display:flex; flex-wrap:wrap; align-items:center;\">\n",
|
|
" <a style=\"margin-right:10px; margin-bottom:6px;\" href=\"https://pepy.tech/projects/uniface\"><img alt=\"PyPI Downloads\" src=\"https://static.pepy.tech/personalized-badge/uniface?period=total&units=international_system&left_color=grey&right_color=blue&left_text=Downloads\"></a>\n",
|
|
" <a style=\"margin-right:10px; margin-bottom:6px;\" href=\"https://pypi.org/project/uniface/\"><img alt=\"PyPI Version\" src=\"https://img.shields.io/pypi/v/uniface.svg\"></a>\n",
|
|
" <a style=\"margin-right:10px; margin-bottom:6px;\" href=\"https://opensource.org/licenses/MIT\"><img alt=\"License\" src=\"https://img.shields.io/badge/License-MIT-blue.svg\"></a>\n",
|
|
" <a style=\"margin-bottom:6px;\" href=\"https://github.com/yakhyo/uniface\"><img alt=\"GitHub Stars\" src=\"https://img.shields.io/github/stars/yakhyo/uniface.svg?style=social\"></a>\n",
|
|
"</div>\n",
|
|
"\n",
|
|
"**UniFace** is a lightweight, production-ready Python library for face detection, recognition, tracking, landmark analysis, face parsing, gaze estimation, and face attributes.\n",
|
|
"\n",
|
|
"🔗 **GitHub**: [github.com/yakhyo/uniface](https://github.com/yakhyo/uniface) | 📚 **Docs**: [yakhyo.github.io/uniface](https://yakhyo.github.io/uniface)\n",
|
|
"\n",
|
|
"---\n",
|
|
"\n",
|
|
"This notebook demonstrates how to build a persistent face database using the **FAISS** vector store in UniFace.\n",
|
|
"\n",
|
|
"Unlike direct pairwise comparison (see `04_face_search`), a vector store lets you efficiently index\n",
|
|
"thousands of face embeddings and retrieve the closest match in sub-millisecond time.\n",
|
|
"\n",
|
|
"## 1. Install UniFace"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"%pip install -q \"uniface[cpu]\" faiss-cpu\n",
|
|
"\n",
|
|
"# Clone repo for assets (Colab only)\n",
|
|
"import os\n",
|
|
"if 'COLAB_GPU' in os.environ or 'COLAB_RELEASE_TAG' in os.environ:\n",
|
|
" if not os.path.exists('uniface'):\n",
|
|
" !git clone --depth 1 https://github.com/yakhyo/uniface.git\n",
|
|
" os.chdir('uniface/examples')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 2. Import Libraries"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import cv2\n",
|
|
"import matplotlib.pyplot as plt\n",
|
|
"import shutil\n",
|
|
"\n",
|
|
"import uniface\n",
|
|
"from uniface.analyzer import FaceAnalyzer\n",
|
|
"from uniface.detection import RetinaFace\n",
|
|
"from uniface.recognition import ArcFace\n",
|
|
"from uniface.stores import FAISS\n",
|
|
"\n",
|
|
"print(f'UniFace version: {uniface.__version__}')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 3. Initialize Models and Vector Store"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"analyzer = FaceAnalyzer(\n",
|
|
" detector=RetinaFace(confidence_threshold=0.5),\n",
|
|
" recognizer=ArcFace(),\n",
|
|
")\n",
|
|
"\n",
|
|
"DB_PATH = './demo_face_index'\n",
|
|
"store = FAISS(embedding_size=512, db_path=DB_PATH)\n",
|
|
"print(store)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 4. Enroll Faces into the Store\n",
|
|
"\n",
|
|
"We detect faces in the test images and add each embedding with metadata."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"enrollment_images = {\n",
|
|
" '../assets/test_images/image0.jpg': 'person_0',\n",
|
|
" '../assets/test_images/image1.jpg': 'person_1',\n",
|
|
" '../assets/test_images/image2.jpg': 'person_2',\n",
|
|
" '../assets/test_images/image3.jpg': 'person_3',\n",
|
|
" '../assets/test_images/image4.jpg': 'person_4',\n",
|
|
"}\n",
|
|
"\n",
|
|
"for path, label in enrollment_images.items():\n",
|
|
" image = cv2.imread(path)\n",
|
|
" faces = analyzer.analyze(image)\n",
|
|
" if faces:\n",
|
|
" store.add(\n",
|
|
" embedding=faces[0].embedding,\n",
|
|
" metadata={'label': label, 'source': path},\n",
|
|
" )\n",
|
|
" print(f'Enrolled {label} from {path}')\n",
|
|
"\n",
|
|
"print(f'\\nStore size: {store.size} vectors')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 5. Search the Store\n",
|
|
"\n",
|
|
"Use a query image to find the closest match in the database."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"query_image = cv2.imread('../assets/test_images/image0.jpg')\n",
|
|
"query_faces = analyzer.analyze(query_image)\n",
|
|
"\n",
|
|
"if query_faces:\n",
|
|
" result, similarity = store.search(query_faces[0].embedding, threshold=0.4)\n",
|
|
"\n",
|
|
" if result:\n",
|
|
" print(f'Match found: {result[\"label\"]} (similarity: {similarity:.4f})')\n",
|
|
" print(f'Source: {result[\"source\"]}')\n",
|
|
" else:\n",
|
|
" print(f'No match above threshold (best similarity: {similarity:.4f})')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"if query_faces and result:\n",
|
|
" matched_image = cv2.imread(result['source'])\n",
|
|
"\n",
|
|
" fig, axes = plt.subplots(1, 2, figsize=(10, 4))\n",
|
|
" axes[0].imshow(cv2.cvtColor(query_image, cv2.COLOR_BGR2RGB))\n",
|
|
" axes[0].set_title('Query', fontsize=12)\n",
|
|
" axes[1].imshow(cv2.cvtColor(matched_image, cv2.COLOR_BGR2RGB))\n",
|
|
" axes[1].set_title(f'Match: {result[\"label\"]} ({similarity:.3f})', fontsize=12)\n",
|
|
" for ax in axes:\n",
|
|
" ax.axis('off')\n",
|
|
" plt.tight_layout()\n",
|
|
" plt.show()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 6. Save and Reload the Index\n",
|
|
"\n",
|
|
"The index and metadata can be persisted to disk and loaded later."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"store.save()\n",
|
|
"\n",
|
|
"# Create a fresh store and load the saved data\n",
|
|
"store_reloaded = FAISS(embedding_size=512, db_path=DB_PATH)\n",
|
|
"loaded = store_reloaded.load()\n",
|
|
"print(f'Load successful: {loaded}')\n",
|
|
"print(f'Reloaded store size: {store_reloaded.size} vectors')\n",
|
|
"\n",
|
|
"# Verify search still works after reload\n",
|
|
"if query_faces:\n",
|
|
" result, similarity = store_reloaded.search(query_faces[0].embedding, threshold=0.4)\n",
|
|
" if result:\n",
|
|
" print(f'Search after reload: {result[\"label\"]} ({similarity:.4f})')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 7. Remove Entries\n",
|
|
"\n",
|
|
"Remove all entries matching a metadata key-value pair."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"print(f'Before removal: {store.size} vectors')\n",
|
|
"\n",
|
|
"removed = store.remove(key='label', value='person_0')\n",
|
|
"print(f'Removed {removed} entry')\n",
|
|
"print(f'After removal: {store.size} vectors')\n",
|
|
"\n",
|
|
"# Searching for the removed person should now return a different (lower) match\n",
|
|
"if query_faces:\n",
|
|
" result, similarity = store.search(query_faces[0].embedding, threshold=0.4)\n",
|
|
" if result:\n",
|
|
" print(f'\\nClosest remaining match: {result[\"label\"]} ({similarity:.4f})')\n",
|
|
" else:\n",
|
|
" print(f'\\nNo match above threshold (best similarity: {similarity:.4f})')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 8. Cleanup"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"shutil.rmtree(DB_PATH, ignore_errors=True)\n",
|
|
"print('Cleaned up demo index.')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Notes\n",
|
|
"\n",
|
|
"- Embeddings **must** be L2-normalised before adding (ArcFace already produces normalised embeddings)\n",
|
|
"- The default threshold of `0.4` works for most cases; raise it for stricter matching\n",
|
|
"- `save()` / `load()` persist the FAISS index and metadata as files in `db_path`\n",
|
|
"- For GPU-accelerated search install `faiss-gpu` instead of `faiss-cpu`\n",
|
|
"- The store uses `IndexFlatIP` (inner product = cosine similarity for normalised vectors)"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "base",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.13.5"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 4
|
|
}
|