faceflash 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- faceflash-0.1.0/PKG-INFO +575 -0
- faceflash-0.1.0/README.md +529 -0
- faceflash-0.1.0/faceflash/__init__.py +29 -0
- faceflash-0.1.0/faceflash/align.py +202 -0
- faceflash-0.1.0/faceflash/cluster.py +116 -0
- faceflash-0.1.0/faceflash/detect.py +75 -0
- faceflash-0.1.0/faceflash/embed.py +93 -0
- faceflash-0.1.0/faceflash/engine.py +207 -0
- faceflash-0.1.0/faceflash/index.py +432 -0
- faceflash-0.1.0/faceflash/pca_quantize.py +199 -0
- faceflash-0.1.0/faceflash/py.typed +0 -0
- faceflash-0.1.0/pyproject.toml +61 -0
- faceflash-0.1.0/rust/Cargo.lock +321 -0
- faceflash-0.1.0/rust/Cargo.toml +22 -0
- faceflash-0.1.0/rust/src/lib.rs +463 -0
faceflash-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: faceflash
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Development Status :: 4 - Beta
|
|
5
|
+
Classifier: Intended Audience :: Science/Research
|
|
6
|
+
Classifier: Intended Audience :: Developers
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Rust
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
18
|
+
Requires-Dist: numpy>=1.24
|
|
19
|
+
Requires-Dist: opencv-python-headless>=4.8
|
|
20
|
+
Requires-Dist: pillow>=10.0
|
|
21
|
+
Requires-Dist: tqdm>=4.60
|
|
22
|
+
Requires-Dist: faiss-cpu>=1.7 ; extra == 'benchmark'
|
|
23
|
+
Requires-Dist: usearch>=2.0 ; extra == 'benchmark'
|
|
24
|
+
Requires-Dist: hnswlib>=0.7 ; extra == 'benchmark'
|
|
25
|
+
Requires-Dist: onnxruntime>=1.16 ; extra == 'cpu'
|
|
26
|
+
Requires-Dist: pytest ; extra == 'dev'
|
|
27
|
+
Requires-Dist: matplotlib ; extra == 'dev'
|
|
28
|
+
Requires-Dist: pandas ; extra == 'dev'
|
|
29
|
+
Requires-Dist: onnxruntime>=1.16 ; extra == 'dev'
|
|
30
|
+
Requires-Dist: onnxruntime-gpu>=1.16 ; extra == 'gpu'
|
|
31
|
+
Provides-Extra: benchmark
|
|
32
|
+
Provides-Extra: cpu
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Provides-Extra: gpu
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Summary: Fast face retrieval via PCA+ITQ binary quantization — up to 96x less memory than HNSW at equal recall
|
|
37
|
+
Keywords: face-recognition,vector-search,binary-quantization,arcface,memory-efficient
|
|
38
|
+
Author-email: Raghavender Grudhanti <raghavenderreddy1212@gmail.com>
|
|
39
|
+
License: MIT
|
|
40
|
+
Requires-Python: >=3.10
|
|
41
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
42
|
+
Project-URL: Homepage, https://github.com/raghavenderreddygrudhanti/faceflash
|
|
43
|
+
Project-URL: Issues, https://github.com/raghavenderreddygrudhanti/faceflash/issues
|
|
44
|
+
Project-URL: Repository, https://github.com/raghavenderreddygrudhanti/faceflash
|
|
45
|
+
|
|
46
|
+
# ⚡ FaceFlash
|
|
47
|
+
|
|
48
|
+
**FaceFlash searches 1M faces in 61 MB of RAM. HNSW needs 2.9 GB, USearch needs 2.5 GB, FAISS needs 1.9 GB — for the same 100% recall.**
|
|
49
|
+
|
|
50
|
+
FaceFlash is a Rust face search engine with Python bindings, built on **PCA+ITQ binary quantization** — a learned hash that preserves identity information with zero recall loss and no separate training phase.
|
|
51
|
+
|
|
52
|
+
- **100% Recall@1, 48–96× less memory.** On the MS1MV2 benchmark (44,291 identities), FaceFlash held 100% Recall@1 at every scale from 100K to 1M while using **48× less index memory than HNSWLIB** at the default 512-bit config (~42× vs USearch, ~32× vs FAISS-Flat) — and **96× less** at the 256-bit compact config (100% recall, ~2× single-query latency).
|
|
53
|
+
- **Faster than HNSW at 100K.** On the same benchmark, FaceFlash single-query latency was 0.30ms vs HNSWLIB's 0.60ms (2× faster). Batched throughput: 27,661 qps vs 5,813 qps (4.8× faster). Both at 100% recall.
|
|
54
|
+
- **AVX-512 VPOPCNTDQ + NEON.** Hand-written SIMD kernels process one 512-bit face code per instruction (~3× faster than scalar). Multi-core batched search measured at 10–17× throughput vs single-query serial on the same hardware.
|
|
55
|
+
- **Zero-config indexing.** Add faces, they're indexed — PCA fits automatically after 1,024 samples, no hyperparameter tuning, no rebuilds as the gallery grows.
|
|
56
|
+
- **Pure local.** No managed service, no data leaving your machine. Pair with any ArcFace model for a fully air-gapped face search stack.
|
|
57
|
+
|
|
58
|
+
<div align="center">
|
|
59
|
+
|
|
60
|
+
[](LICENSE)
|
|
61
|
+
[](https://python.org)
|
|
62
|
+
[](rust/)
|
|
63
|
+
[](https://github.com/raghavenderreddygrudhanti/faceflash/actions)
|
|
64
|
+
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from faceflash import FaceFlash
|
|
69
|
+
|
|
70
|
+
ff = FaceFlash()
|
|
71
|
+
ff.register_folder("employees/") # bulk enroll
|
|
72
|
+
ff.save("my_index/")
|
|
73
|
+
|
|
74
|
+
result = ff.search("visitor.jpg")
|
|
75
|
+
# {"matches": [{"name": "Alice", "confidence": 0.92}], "search_time_ms": 0.4}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## The Problem
|
|
81
|
+
|
|
82
|
+
Most face search libraries force a trade-off:
|
|
83
|
+
|
|
84
|
+
- **HNSW** is fast and accurate — but consumes **2.9 GB of RAM at 1M faces**
|
|
85
|
+
- **ScaNN / USearch** are blazing fast — but drop to **94–99% recall**
|
|
86
|
+
- **FAISS-Flat** is exact — but is **10× slower** at scale
|
|
87
|
+
|
|
88
|
+
FaceFlash breaks this trade-off. It compresses each face into a **64-byte binary fingerprint** using PCA + ITQ quantization, scans them with a single AVX-512 VPOPCNTDQ instruction, then re-ranks only the top candidates with exact cosine similarity. The result: exact accuracy at a fraction of the memory.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## At a Glance
|
|
93
|
+
|
|
94
|
+
| | FaceFlash | HNSWLIB | USearch | ScaNN | FAISS-Flat |
|
|
95
|
+
|---|---|---|---|---|---|
|
|
96
|
+
| **Recall@1** | **100%** | 100% | 99.5% | 98.3% | 100% |
|
|
97
|
+
| **Memory @ 100K** | **3.05 MB** | 293 MB | 254 MB | 12 MB | 195 MB |
|
|
98
|
+
| **Memory @ 500K** | **15.3 MB** | 1,465 MB | 1,270 MB | 61 MB | 977 MB |
|
|
99
|
+
| **Memory @ 1M** | **30.5 MB** | 2,930 MB | 2,539 MB | 122 MB | 1,953 MB |
|
|
100
|
+
| **Latency @ 100K** | **0.30ms** | 0.60ms | 0.17ms | 0.10ms | 4.90ms |
|
|
101
|
+
| **Batched QPS @ 100K** | 27,661 | 5,813 | **137,264** | — | — |
|
|
102
|
+
| **Index build** | Auto (PCA fit) | Build graph | Build graph | Partition | None |
|
|
103
|
+
|
|
104
|
+
> Tested on MS1MV2 (44,291 identities, 645,019 embeddings). Hardware: AMD EPYC 9355, 128 threads, AVX-512 active.
|
|
105
|
+
>
|
|
106
|
+
> **Memory = binary index only.** Float vectors for cosine reranking are mmap'd from disk after `save()`/`load()` — only ~100 candidate rows are paged per query. See [Limitations](#limitations).
|
|
107
|
+
|
|
108
|
+
<div align="center">
|
|
109
|
+
|
|
110
|
+
<img src="docs/figures/chart_memory_scale.png" width="800" alt="Memory comparison: FaceFlash vs all competitors at 500K faces"/>
|
|
111
|
+
|
|
112
|
+
<sub><b>Index Memory at 500K Faces (256-bit compact config):</b> FaceFlash (15 MB) vs HNSW (1,465 MB) — 96× less RAM at 100% recall</sub>
|
|
113
|
+
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div align="center">
|
|
117
|
+
|
|
118
|
+
<img src="docs/figures/demo.gif" width="720" alt="FaceFlash live demo"/>
|
|
119
|
+
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Install
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install "faceflash[cpu] @ git+https://github.com/raghavenderreddygrudhanti/faceflash.git"
|
|
128
|
+
|
|
129
|
+
# With benchmark dependencies
|
|
130
|
+
pip install "faceflash[cpu,benchmark] @ git+https://github.com/raghavenderreddygrudhanti/faceflash.git"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
> Requires a [Rust toolchain](https://rustup.rs) — it compiles the AVX-512/NEON backend automatically and falls back to NumPy if unavailable.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Quick Start
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from faceflash import FaceFlash
|
|
141
|
+
|
|
142
|
+
ff = FaceFlash() # downloads ArcFace model (~166 MB) on first run
|
|
143
|
+
|
|
144
|
+
# Register individual faces
|
|
145
|
+
ff.register("Alice", "alice.jpg")
|
|
146
|
+
ff.register("Bob", "bob.jpg")
|
|
147
|
+
|
|
148
|
+
# Identify a face
|
|
149
|
+
result = ff.search("query.jpg")
|
|
150
|
+
# {"matches": [{"name": "Alice", "confidence": 0.92}], "search_time_ms": 0.4}
|
|
151
|
+
|
|
152
|
+
# Verify two faces are the same person
|
|
153
|
+
ff.verify("photo1.jpg", "photo2.jpg")
|
|
154
|
+
# {"match": True, "confidence": 0.87}
|
|
155
|
+
|
|
156
|
+
# Bulk enroll from folder (expects folder/person_name/photo.jpg)
|
|
157
|
+
ff.register_folder("employees/")
|
|
158
|
+
ff.save("my_index/")
|
|
159
|
+
ff.load("my_index/")
|
|
160
|
+
|
|
161
|
+
# Manage the index
|
|
162
|
+
len(ff) # how many faces are registered
|
|
163
|
+
"Alice" in ff # is this person registered?
|
|
164
|
+
ff.names() # list registered people
|
|
165
|
+
ff.remove("Alice") # unregister a person (returns count removed)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
> For best accuracy, use pre-aligned 112x112 face crops. 5-point alignment (SCRFD/RetinaFace) adds +1.28 accuracy points over a basic center-crop.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Gallery Management
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from faceflash import FaceFlash
|
|
176
|
+
|
|
177
|
+
ff = FaceFlash()
|
|
178
|
+
ff.register("Alice", "alice.jpg")
|
|
179
|
+
ff.register("Bob", "bob.jpg")
|
|
180
|
+
ff.register("Alice", "alice2.jpg") # multiple photos per person
|
|
181
|
+
|
|
182
|
+
# Check gallery state
|
|
183
|
+
len(ff) # 3 (total face entries)
|
|
184
|
+
ff.names() # ["Alice", "Bob"]
|
|
185
|
+
"Alice" in ff # True
|
|
186
|
+
"Charlie" in ff # False
|
|
187
|
+
|
|
188
|
+
# Remove a person (GDPR / right-to-be-forgotten)
|
|
189
|
+
ff.remove("Bob") # removes all entries for Bob
|
|
190
|
+
len(ff) # 2
|
|
191
|
+
ff.names() # ["Alice"]
|
|
192
|
+
|
|
193
|
+
# Monitor index stats
|
|
194
|
+
ff.stats()
|
|
195
|
+
# {'count': 2, 'pca_fitted': True, 'rust_backend': True,
|
|
196
|
+
# 'binary_memory_mb': 0.0, 'resident_memory_mb': 0.0, ...}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Batch Identification
|
|
200
|
+
|
|
201
|
+
Process many query faces at once (4.8x faster than one-by-one):
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
import numpy as np
|
|
205
|
+
from faceflash import FaceFlash
|
|
206
|
+
|
|
207
|
+
ff = FaceFlash()
|
|
208
|
+
ff.register_folder("gallery/")
|
|
209
|
+
ff.save("my_index/")
|
|
210
|
+
|
|
211
|
+
# Low-level batch search (for bulk dedup, watchlists, video frames)
|
|
212
|
+
embeddings = np.load("query_embeddings.npy") # (N, 512) float32
|
|
213
|
+
results = ff.index.search_batch(embeddings, k=1)
|
|
214
|
+
# results[i] = [(name, similarity, index), ...]
|
|
215
|
+
|
|
216
|
+
# Example: find all matches above threshold
|
|
217
|
+
for i, matches in enumerate(results):
|
|
218
|
+
if matches and matches[0][1] > 0.5:
|
|
219
|
+
print(f"Query {i}: {matches[0][0]} (confidence {matches[0][1]:.2f})")
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Is FaceFlash Right for You?
|
|
225
|
+
|
|
226
|
+
| Scenario | Why FaceFlash wins |
|
|
227
|
+
|---|---|
|
|
228
|
+
| **Edge / mobile / IoT** | 3–30 MB vs 293–2,930 MB for HNSW — fits in device RAM |
|
|
229
|
+
| **Multi-tenant servers** | 100 galleries x 30 MB = 3 GB. HNSW: 100 x 1.5 GB = 150 GB |
|
|
230
|
+
| **Batch dedup / watchlists** | 4.8x faster than HNSW batched at 100K; 1.9x at 500K |
|
|
231
|
+
| **100% recall is non-negotiable** | FaceFlash hits 100% at every scale; USearch drops to 94-99% |
|
|
232
|
+
| **Budget / offline / air-gapped** | Runs on Raspberry Pi, cheap VPS, phones — no GPU, no network |
|
|
233
|
+
| **10K-500K face databases** | The sweet spot: faster AND less memory than HNSW |
|
|
234
|
+
|
|
235
|
+
**When HNSW is the better choice:**
|
|
236
|
+
- You need <0.3ms single-query latency at >500K faces and have gigabytes of RAM to spare
|
|
237
|
+
- Your database exceeds 2M faces (HNSW's O(log N) pulls clearly ahead)
|
|
238
|
+
- You need 100K+ batched QPS regardless of memory (USearch wins there)
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## How It Works
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+

|
|
247
|
+
|
|
248
|
+
Each face is compressed into a **64-byte binary fingerprint**:
|
|
249
|
+
|
|
250
|
+
1. **ArcFace** extracts a 512-dimensional float embedding
|
|
251
|
+
2. **PCA** aligns the quantization with the axes where identity varies most
|
|
252
|
+
3. **ITQ** rotates bits to maximize information per bit (balanced marginals)
|
|
253
|
+
4. **AVX-512 VPOPCNTDQ** scans all binary codes in a single instruction per face
|
|
254
|
+
5. **Cosine rerank** runs exact similarity on only the top ~100 candidates
|
|
255
|
+
|
|
256
|
+
This is why 512 bits is the fastest setting — the entire code fits in one AVX-512 register.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Benchmark Methodology
|
|
261
|
+
|
|
262
|
+
FaceFlash's recall, latency, throughput, and memory are measured. Competitor recall and latency are also measured; **competitor memory is estimated** from the standard vectors + index-structure overhead formula (e.g. HNSW ≈ 1.5× raw vectors).
|
|
263
|
+
|
|
264
|
+
| | Details |
|
|
265
|
+
|---|---|
|
|
266
|
+
| **Dataset** | MS1MV2 — 645,019 ArcFace embeddings, 44,291 distinct identities |
|
|
267
|
+
| **Embedding** | ArcFace ONNX (w600k_r50), 512 dimensions, L2-normalized |
|
|
268
|
+
| **Hardware** | AMD EPYC 9355 (32 cores / 128 threads), AVX-512 VPOPCNTDQ enabled |
|
|
269
|
+
| **Competitors** | HNSWLIB 0.8+, FAISS 1.7+, USearch 2.x, ScaNN (latest) |
|
|
270
|
+
| **Ground truth** | Exact brute-force cosine argmax (FAISS-Flat) |
|
|
271
|
+
| **Timing** | `time.perf_counter()` per query, 10 warmup excluded |
|
|
272
|
+
| **Recall metric** | Recall@1 — fraction of queries where the true nearest neighbor is rank-1 |
|
|
273
|
+
| **Memory metric** | FaceFlash: measured binary index size in RAM (floats mmap'd after save/load). Competitors: estimated (vectors + index-structure overhead) |
|
|
274
|
+
| **Batched timing** | Wall-clock for the full query batch / number of queries |
|
|
275
|
+
| **Reproducibility** | `bash scripts/runpod_ms1m.sh` reproduces all results end-to-end |
|
|
276
|
+
|
|
277
|
+
All single-query rows are single-threaded. Batched rows use all available cores. Every benchmark script validates correctness before reporting speed.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Performance
|
|
282
|
+
|
|
283
|
+
### Scale Summary (100K-1M)
|
|
284
|
+
|
|
285
|
+
<div align="center">
|
|
286
|
+
|
|
287
|
+
<img src="docs/figures/chart_throughput_scale.png" width="760" alt="Batched throughput: FaceFlash vs all competitors 100K to 1M"/>
|
|
288
|
+
|
|
289
|
+
<sub><b>Batched Throughput (QPS):</b> FaceFlash 100K→1M — 4.8× faster than HNSW at 100K</sub>
|
|
290
|
+
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div align="center">
|
|
294
|
+
|
|
295
|
+
<img src="docs/figures/chart_latency_scale.png" width="760" alt="Single-query latency: all methods 100K to 1M"/>
|
|
296
|
+
|
|
297
|
+
<sub><b>Single-Query Latency:</b> FaceFlash 0.30ms vs HNSW 0.60ms at 100K — both at 100% recall</sub>
|
|
298
|
+
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div align="center">
|
|
302
|
+
|
|
303
|
+
<img src="docs/figures/chart_recall_memory_scale.png" width="760" alt="Recall vs Memory at 500K"/>
|
|
304
|
+
|
|
305
|
+
<sub><b>Recall vs Memory Pareto:</b> FaceFlash sits at the frontier — 100% recall, 30 MB</sub>
|
|
306
|
+
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
| Scale | Recall | Single-query | Batched QPS | Memory | vs HNSW memory |
|
|
310
|
+
|---|---|---|---|---|---|
|
|
311
|
+
| 100K | 100% | **0.30ms** (2× faster than HNSW) | 27,661 | **6.1 MB** | **48× less** |
|
|
312
|
+
| 200K | 100% | 0.57ms (tied with HNSW) | 19,930 | **12.2 MB** | **48× less** |
|
|
313
|
+
| 300K | 100% | 0.84ms | 15,147 | **18.3 MB** | **48× less** |
|
|
314
|
+
| 500K | 100% | 1.45ms | 10,337 | **30.5 MB** | **48× less** |
|
|
315
|
+
| 1M | 100% | 2.95ms | 5,403 | **61 MB** | **48× less** |
|
|
316
|
+
|
|
317
|
+
*Numbers are the default **512-bit** config (fastest, 100% recall). The **256-bit compact** config holds 100% recall at half the memory — **96× less than HNSW** — trading ~2× single-query latency.*
|
|
318
|
+
|
|
319
|
+
FaceFlash dominates up to 300K on every axis. At 500K-1M, HNSW edges ahead on single-query latency (O(log N) vs O(N)), but FaceFlash still wins on batched throughput and always uses 48–96× less memory.
|
|
320
|
+
|
|
321
|
+
### 1:N Identification - 44,290 Distinct People
|
|
322
|
+
|
|
323
|
+
The hardest test: one photo per person in the gallery, identify them from a different photo.
|
|
324
|
+
|
|
325
|
+
<div align="center">
|
|
326
|
+
|
|
327
|
+
<img src="docs/figures/chart_rank1_tie.png" width="720" alt="Rank-1 identification ties exact search on 44,290 people"/>
|
|
328
|
+
|
|
329
|
+
<sub><b>1:N Identification on 44,290 Identities:</b> FaceFlash matches FAISS-Flat accuracy at 32× less memory</sub>
|
|
330
|
+
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
| Method | Rank-1 Accuracy | Memory |
|
|
334
|
+
|---|---|---|
|
|
335
|
+
| FAISS-Flat (exact ceiling) | 95.8% | 86.5 MB |
|
|
336
|
+
| **FaceFlash (512b / 100c)** | **95.8%** | **2.70 MB** |
|
|
337
|
+
| FaceFlash (512b / 300c) | 95.8% | 2.70 MB |
|
|
338
|
+
| FaceFlash (256b / 100c) | 95.6% | 1.35 MB |
|
|
339
|
+
|
|
340
|
+
FaceFlash ties exact search using **32× less memory** (512 float32 → 512 bits = exactly 32× compression). Binary quantization is lossless at 512 bits.
|
|
341
|
+
|
|
342
|
+
<details>
|
|
343
|
+
<summary>Detailed per-scale benchmark tables</summary>
|
|
344
|
+
|
|
345
|
+
### 100K Faces (6,939 identities)
|
|
346
|
+
|
|
347
|
+
| Method | Recall@1 | Latency | QPS | Memory |
|
|
348
|
+
|---|---|---|---|---|
|
|
349
|
+
| FaceFlash (batched) | 100% | 0.036ms | 27,661 | 6.1 MB |
|
|
350
|
+
| FaceFlash (512b/200c) | 100% | 0.30ms | 3,310 | 6.1 MB |
|
|
351
|
+
| FaceFlash (512b/100c) | 100% | 0.43ms | 2,344 | 6.1 MB |
|
|
352
|
+
| HNSWLIB (ef=128) | 100% | 0.60ms | 1,671 | 293 MB |
|
|
353
|
+
| HNSWLIB batched | 100% | 0.172ms | 5,813 | 293 MB |
|
|
354
|
+
| USearch batched | 99.5% | 0.007ms | 137,264 | 254 MB |
|
|
355
|
+
| USearch | 99.5% | 0.17ms | -- | 254 MB |
|
|
356
|
+
| ScaNN | 98.3% | 0.10ms | -- | 12 MB |
|
|
357
|
+
| FAISS-Flat (exact) | 100% | 4.90ms | 204 | 195 MB |
|
|
358
|
+
|
|
359
|
+
### 200K Faces (13,749 identities)
|
|
360
|
+
|
|
361
|
+
| Method | Recall@1 | Latency | QPS | Memory |
|
|
362
|
+
|---|---|---|---|---|
|
|
363
|
+
| FaceFlash (batched) | 100% | 0.050ms | 19,930 | 12.2 MB |
|
|
364
|
+
| FaceFlash (512b/200c) | 100% | 0.57ms | 1,751 | 12.2 MB |
|
|
365
|
+
| HNSWLIB (ef=128) | 99.9% | 0.65ms | 1,531 | 586 MB |
|
|
366
|
+
| HNSWLIB batched | 99.9% | 0.378ms | 2,646 | 586 MB |
|
|
367
|
+
| USearch batched | 99.1% | 0.008ms | 121,660 | 508 MB |
|
|
368
|
+
| ScaNN | 97.2% | 0.19ms | -- | 24 MB |
|
|
369
|
+
|
|
370
|
+
### 300K Faces (20,615 identities)
|
|
371
|
+
|
|
372
|
+
| Method | Recall@1 | Latency | QPS | Memory |
|
|
373
|
+
|---|---|---|---|---|
|
|
374
|
+
| FaceFlash (batched) | 100% | 0.066ms | 15,147 | 18.3 MB |
|
|
375
|
+
| FaceFlash (512b/200c) | 100% | 0.84ms | 1,187 | 18.3 MB |
|
|
376
|
+
| HNSWLIB (ef=128) | 99.9% | 0.66ms | 1,510 | 879 MB |
|
|
377
|
+
| HNSWLIB batched | 99.7% | 0.269ms | 3,715 | 879 MB |
|
|
378
|
+
| USearch batched | 98.7% | 0.014ms | 73,383 | 762 MB |
|
|
379
|
+
| ScaNN | 97.8% | 0.28ms | -- | 37 MB |
|
|
380
|
+
|
|
381
|
+
### 500K Faces (34,328 identities)
|
|
382
|
+
|
|
383
|
+
| Method | Recall@1 | Latency | QPS | Memory |
|
|
384
|
+
|---|---|---|---|---|
|
|
385
|
+
| FaceFlash (batched) | 100% | 0.097ms | 10,337 | 30.5 MB |
|
|
386
|
+
| FaceFlash (512b/200c) | 100% | 1.45ms | 692 | 30.5 MB |
|
|
387
|
+
| HNSWLIB (ef=128) | 100% | 0.71ms | 1,416 | 1,465 MB |
|
|
388
|
+
| HNSWLIB batched | 99.9% | 0.179ms | 5,577 | 1,465 MB |
|
|
389
|
+
| USearch batched | 98.4% | 0.013ms | 76,150 | 1,270 MB |
|
|
390
|
+
| ScaNN | 97.6% | 0.45ms | -- | 61 MB |
|
|
391
|
+
|
|
392
|
+
### 1M Faces (44,291 identities)
|
|
393
|
+
|
|
394
|
+
| Method | Recall@1 | Latency | QPS | Memory |
|
|
395
|
+
|---|---|---|---|---|
|
|
396
|
+
| FaceFlash (batched) | 100% | 0.185ms | 5,403 | 61 MB |
|
|
397
|
+
| FaceFlash (512b/100c) | 100% | 2.92ms | 342 | 61 MB |
|
|
398
|
+
| HNSWLIB (ef=128) | 100% | 0.66ms | 1,523 | 2,930 MB |
|
|
399
|
+
| HNSWLIB batched | 100% | 0.178ms | 5,621 | 2,930 MB |
|
|
400
|
+
| USearch batched | 94.1% | 0.013ms | 77,266 | 2,539 MB |
|
|
401
|
+
| ScaNN | 98.2% | 0.86ms | -- | 122 MB |
|
|
402
|
+
|
|
403
|
+
</details>
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Tuning
|
|
408
|
+
|
|
409
|
+
Pick a config that matches your deployment:
|
|
410
|
+
|
|
411
|
+
| Deployment | Config | Recall@1 | Memory/face | Notes |
|
|
412
|
+
|---|---|---|---|---|
|
|
413
|
+
| Ultra-compact (mobile/IoT) | n_bits=128, n_candidates=500 | 99.4% | 16 bytes | Minimum RAM |
|
|
414
|
+
| Balanced | n_bits=256, n_candidates=100 | 100% | 32 bytes | Good default |
|
|
415
|
+
| **Default (fastest)** | n_bits=512, n_candidates=100 | **100%** | 64 bytes | One AVX-512 instruction |
|
|
416
|
+
| Edge (minimize disk reads) | n_bits=512, n_candidates=50 | 99.5% | 64 bytes | -- |
|
|
417
|
+
|
|
418
|
+
```python
|
|
419
|
+
ff = FaceFlash(n_bits=128, n_candidates=500) # mobile/IoT
|
|
420
|
+
ff = FaceFlash(n_bits=256, n_candidates=100) # balanced
|
|
421
|
+
ff = FaceFlash(n_bits=512, n_candidates=100) # default: fastest
|
|
422
|
+
|
|
423
|
+
ff.search("query.jpg", n_candidates=200) # per-query override
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Speed up large indexes with clustering
|
|
427
|
+
|
|
428
|
+
<div align="center">
|
|
429
|
+
|
|
430
|
+
<img src="docs/figures/chart_clustering_tradeoff.png" width="720" alt="Clustering recall/speed tradeoff"/>
|
|
431
|
+
|
|
432
|
+
<sub><b>IVF Clustering Speedup:</b> 5–8× faster at 500K+ with configurable recall trade-off</sub>
|
|
433
|
+
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
```python
|
|
437
|
+
ff.index.build_clusters(n_probe=16)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
| Scale | n_probe | Recall@1 | Latency | Speedup |
|
|
441
|
+
|---|---|---|---|---|
|
|
442
|
+
| 100K | 16 | 96.1% | 0.12ms | 2.6x |
|
|
443
|
+
| 100K | 32 | 98.5% | 0.17ms | 1.8x |
|
|
444
|
+
| 500K | 16 | 87.9% | 0.31ms | 5.0x |
|
|
445
|
+
| 500K | 32 | 92.8% | 0.56ms | 2.8x |
|
|
446
|
+
|
|
447
|
+
Clustering is mainly useful at 500K+ where it delivers 5-8x speedup at ~88-93% recall.
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## Architecture
|
|
452
|
+
|
|
453
|
+
```
|
|
454
|
+
faceflash/ # Python package
|
|
455
|
+
├── engine.py ◀─ High-level API (register, search, verify)
|
|
456
|
+
├── detect.py ◀─ Face detection (SCRFD + Haar fallback)
|
|
457
|
+
├── align.py ◀─ 5-point alignment to ArcFace template
|
|
458
|
+
├── embed.py ◀─ ArcFace ONNX embedding (512-dim, auto-download)
|
|
459
|
+
├── index.py ◀─ Binary index + batched search
|
|
460
|
+
└── pca_quantize.py ◀─ PCA+ITQ quantizer (the core algorithm)
|
|
461
|
+
|
|
462
|
+
rust/ # Rust backend (PyO3 + Rayon)
|
|
463
|
+
├── src/lib.rs ◀─ AVX-512 VPOPCNTDQ / NEON / scalar POPCNT
|
|
464
|
+
└── Cargo.toml
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**Why PCA+ITQ?** ArcFace embeddings concentrate identity information along principal axes. PCA aligns quantization with those axes; ITQ rotates bits for balanced marginals. The result is lossless compression at 512 bits.
|
|
468
|
+
|
|
469
|
+
**Why not HNSW internally?** HNSW stores a graph on top of full float vectors — about 1.5x raw memory. FaceFlash stores 32–64 bytes per face. Float vectors are memory-mapped from disk and paged only for the top ~100 candidates per query. Trade-off: higher single-query latency at 500K+, but 48–96× less memory.
|
|
470
|
+
|
|
471
|
+
**Why Rust + AVX-512?** AVX-512 VPOPCNTDQ processes an entire 512-bit code in one instruction (~3× faster than scalar POPCNT). Combined with Rayon multi-core parallelism (and cache-blocked batching to keep the scan cache-friendly), batched search reaches 10-17× throughput versus single-query serial. Runtime-detected — no user configuration needed.
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
## Limitations
|
|
476
|
+
|
|
477
|
+
- **Single-query at 1M+** — O(N) linear scan; HNSW is 4.4x faster per single query at 1M. Batched path ties.
|
|
478
|
+
- **Memory during build** — holds all float vectors in RAM. The 48–96× savings apply after `save()` / `load()`.
|
|
479
|
+
- **AVX-512 VPOPCNTDQ** — the ~3× kernel speedup requires Ice Lake / Zen 4+ / EPYC 9004+. Older CPUs fall back to scalar POPCNT automatically.
|
|
480
|
+
- **Rerank I/O** — pages ~100 float rows from disk per query. Invisible on NVMe; adds latency on slow storage.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Reproduce the Benchmarks
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
# Local (LFW + VGGFace2 100K)
|
|
488
|
+
python scripts/extract_lfw_embeddings.py
|
|
489
|
+
python benchmarks/bench_ann_comparison.py --scales 100K --queries 500
|
|
490
|
+
|
|
491
|
+
# RunPod (full suite)
|
|
492
|
+
export GITHUB_TOKEN=<token>
|
|
493
|
+
bash scripts/runpod_ms1m.sh # FORCE_EXTRACT=1 for full 85K extraction
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## Roadmap
|
|
499
|
+
|
|
500
|
+
**v0.1.0 (current)**
|
|
501
|
+
- [x] PCA+ITQ binary quantization + Rust search backend
|
|
502
|
+
- [x] High-level API: register, search, verify
|
|
503
|
+
- [x] Benchmarked against FAISS, HNSWLIB, USearch, ScaNN at 100K-1M
|
|
504
|
+
- [x] 1:N identification on 44,290 distinct identities (MS1MV2)
|
|
505
|
+
- [x] 5-point alignment via SCRFD/RetinaFace — 99.85% LFW accuracy
|
|
506
|
+
|
|
507
|
+
**v0.2.0 (done)**
|
|
508
|
+
- [x] Prebuilt wheels (`pip install faceflash`)
|
|
509
|
+
- [x] Full 85K-identity benchmark (76,872 identities extracted, 44,291 with sufficient data)
|
|
510
|
+
- [x] On-device memory measurement (3.05 MB binary index @100K with 256b)
|
|
511
|
+
|
|
512
|
+
**v0.3.0 (done)**
|
|
513
|
+
- [x] IVF coarse clustering (2.7-4.9x speedup at scale)
|
|
514
|
+
- [x] AVX-512 VPOPCNTDQ — native 512-bit popcount (~3x faster than scalar)
|
|
515
|
+
- [x] Batched search — 17x throughput at 500K-1M (multi-core + VPOPCNTDQ; cache-blocked)
|
|
516
|
+
- [x] NEON kernels — ARM-optimized (vcntq_u8)
|
|
517
|
+
|
|
518
|
+
**v1.0.0 — next**
|
|
519
|
+
- [ ] Stable public API (no breaking changes)
|
|
520
|
+
- [ ] DiskANN comparison
|
|
521
|
+
- [ ] Mobile deployment (ONNX + CoreML)
|
|
522
|
+
- [ ] Streaming insertion (add faces without refitting PCA)
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Contributing
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
# Dev setup (one command)
|
|
530
|
+
git clone https://github.com/raghavenderreddygrudhanti/faceflash.git
|
|
531
|
+
cd faceflash && python -m venv .venv && source .venv/bin/activate
|
|
532
|
+
pip install -e ".[cpu,benchmark]" && maturin develop --release
|
|
533
|
+
python -m pytest tests/ # 33 tests, ~17s
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Open areas for contribution:
|
|
537
|
+
|
|
538
|
+
| Area | Difficulty | Impact |
|
|
539
|
+
|------|-----------|--------|
|
|
540
|
+
| **DiskANN comparison** | Medium | High — the one competitor missing |
|
|
541
|
+
| **Mobile deployment** (ONNX + CoreML) | Medium | High — iOS/Android face search |
|
|
542
|
+
| **Streaming insertion** (no PCA refit) | Hard | High — online learning |
|
|
543
|
+
| **GPU batched search** (CUDA) | Hard | Medium — 10M+ galleries |
|
|
544
|
+
| **Raspberry Pi / Jetson benchmarks** | Easy | Medium — edge credibility |
|
|
545
|
+
| **WebAssembly build** | Medium | Medium — browser face search |
|
|
546
|
+
|
|
547
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for coding guidelines.
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Credits & References
|
|
552
|
+
|
|
553
|
+
FaceFlash does **not** introduce a new embedding model or hashing algorithm. It combines proven, published techniques into a CPU-efficient retrieval system — with hand-written Rust AVX-512/NEON kernels, cache-blocked batching, and rigorous, reproducible benchmarks. The contribution is the **system** (exact-recall face search in a megabyte-scale footprint) and the honest measurement of it, not the underlying math.
|
|
554
|
+
|
|
555
|
+
It builds directly on:
|
|
556
|
+
|
|
557
|
+
| Component | Reference |
|
|
558
|
+
|-----------|-----------|
|
|
559
|
+
| **Binary quantization** (PCA + ITQ — the core method) | Gong & Lazebnik, *"Iterative Quantization: A Procrustean Approach to Learning Binary Codes,"* CVPR 2011 |
|
|
560
|
+
| **Face embeddings** | Deng, Guo, Xue & Zafeiriou, *"ArcFace: Additive Angular Margin Loss for Deep Face Recognition,"* CVPR 2019 — weights from [InsightFace](https://github.com/deepinsight/insightface) |
|
|
561
|
+
| **Detection & 5-point alignment** | *RetinaFace* (CVPR 2020) / *SCRFD* (ICLR 2022), InsightFace |
|
|
562
|
+
| **Dataset** | Guo et al., *"MS-Celeb-1M,"* ECCV 2016 (MS1MV2 = ArcFace's refined/cleaned version) |
|
|
563
|
+
| **Verification benchmark** | Huang et al., *"Labeled Faces in the Wild (LFW),"* UMass TR 2007 |
|
|
564
|
+
| **Baselines compared** | FAISS (Johnson et al.), HNSW (Malkov & Yashunin, TPAMI 2018), ScaNN (Guo et al., ICML 2020), [USearch](https://github.com/unum-cloud/usearch) |
|
|
565
|
+
|
|
566
|
+
PCA dates to Pearson (1901) / Hotelling (1933); the Hamming distance to Hamming (1950); `POPCNT` / `VPOPCNTDQ` are Intel/AMD hardware instructions. FaceFlash's value is in how these are combined and implemented — not in inventing them.
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## License
|
|
571
|
+
|
|
572
|
+
MIT — see [LICENSE](LICENSE).
|
|
573
|
+
|
|
574
|
+
If FaceFlash is useful to you, a star helps others find it.
|
|
575
|
+
|