examples/face_recognition/README.md
The core of "find every photo of this person," with no labels or tags — in plain async Python.
</p> <p align="center"> <strong>Star us ❤️ →</strong> <a href="https://github.com/cocoindex-io/cocoindex" title="Star CocoIndex on GitHub"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cocoindex.io/blobs/github/homepage/star-btn-small-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cocoindex.io/blobs/github/homepage/star-btn-small-light.svg"></picture></a> · <a href="https://cocoindex.io/docs/examples/face-recognition/" title="Read the full walkthrough"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cocoindex.io/blobs/github/homepage/docs-inline-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cocoindex.io/blobs/github/homepage/docs-inline-light.svg"></picture></a> · <a href="https://discord.com/invite/zpA9S2DR7s" title="Join the CocoIndex Discord"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cocoindex.io/blobs/github/homepage/discord-inline-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cocoindex.io/blobs/github/homepage/discord-inline-light.svg"></picture></a> </p> <div align="center"> </div>A folder of group photos has every person hiding in plain sight — the same face shows up across shots, but that knowledge is locked in pixels. This pipeline makes it searchable: detect every face, crop it, embed it into a 128-d vector with face_recognition (dlib), and index the faces in Qdrant. You declare the transformation in native Python and your own types — target_state = transformation(source_state) — while incremental processing, change tracking, and the managed Qdrant collection run in a Rust engine underneath, and the slow detection/embedding steps run on a GPU runner instead of blocking the event loop.
Unlike a one-embedding-per-image index, an image here fans out to many faces — so the shape is image → N faces → N points:
.jpg / .jpeg / .png.(filename, bounding box), with the source filename and box in the payload.The dlib calls are synchronous and CPU/GPU-heavy, so each is wrapped with @coco.fn.as_async(runner=coco.GPU). process_file detects a photo's faces, then maps each through process_face with coco.map. Read it in main.py:
@coco.fn
async def process_face(face: Face, filename: str, target: qdrant.CollectionTarget) -> None:
embedding = await embed_face(face.image)
target.declare_point(
qdrant.PointStruct(
id=_face_id(filename, face.rect), # uuid5 of (filename, box) — stable
vector=embedding,
payload={"filename": filename, "min_x": face.rect.min_x, "min_y": face.rect.min_y,
"max_x": face.rect.max_x, "max_y": face.rect.max_y},
)
)
@coco.fn(memo=True) # unchanged photo is never re-detected
async def process_file(file: FileLike, target: qdrant.CollectionTarget) -> None:
faces = await extract_faces(await file.read())
await coco.map(process_face, faces, str(file.file_path.path), target)
The collection is sized to the 128-d face vector with Euclidean distance — dlib's own rule of thumb is that a distance under ~0.6 means "same person."
<p align="center"> 📘 <b><a href="https://cocoindex.io/docs/examples/face-recognition/">Full Tutorial →</a></b>Step-by-step walkthrough with face detection and embedding, the image → faces fan-out, the Euclidean Qdrant collection, and searching by face.
</p>coco.map — the multi-face equivalent of chunking a document.@coco.fn(memo=True) skips unchanged photos; each image is its own processing component, so deleting a photo removes all its faces from Qdrant automatically.coco.GPU runner; large images are downscaled for detection, then boxes are mapped back to full size.Needs Qdrant plus the
face_recognitionlibrary (it depends on dlib — see its install notes if the build needs CMake/boost).
1. Start Qdrant:
docker run -d -p 6333:6333 -p 6334:6334 qdrant/qdrant
2. Configure & install:
cp .env.example .env # QDRANT_URL (defaults to the local container above)
pip install -e .
3. Build the index — the example ships a handful of famous group photos in images/ (the 1927 Solvay physics conference, Steve Jobs & Bill Gates, …):
cocoindex update main # or: cocoindex update -L main (keep watching the folder)
On the sample set this indexes 36 faces — 29 from the Solvay conference alone — each a Qdrant point keyed by (filename, bounding box).
4. Search by face — embed a query face the same way and find the nearest indexed faces:
python main.py query images/einplanck3.jpg
Because Einstein appears in both the Einstein–Planck photo and the Solvay conference, the query pulls his Solvay face back as a close match — a Euclidean distance around 0.46, comfortably under dlib's ~0.6 same-person threshold. That's face recognition across photos, with no labels or tags.
<a href="https://cocoindex.io/docs">Docs</a> · <a href="https://cocoindex.io/docs/examples/face-recognition/">Walkthrough</a> · <a href="https://discord.com/invite/zpA9S2DR7s">Discord</a> · <a href="https://github.com/cocoindex-io/cocoindex/tree/main/examples"><b>See all examples →</b></a>
</p>