{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Face Search: One-to-Many Face Matching\n",
    "\n",
    "This notebook demonstrates how to build a face database and search for matching faces - useful for photo organization, security systems, and social media applications."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Note: you may need to restart the kernel to use updated packages.\n"
     ]
    }
   ],
   "source": [
    "%pip install -q uniface"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Import Libraries"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.3.1\n"
     ]
    }
   ],
   "source": [
    "import cv2\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "import uniface\n",
    "from uniface import FaceAnalyzer\n",
    "from uniface.detection import RetinaFace\n",
    "from uniface.recognition import ArcFace\n",
    "\n",
    "print(uniface.__version__)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ Model loaded (CoreML (Apple Silicon))\n",
      "✓ Model loaded (CoreML (Apple Silicon))\n"
     ]
    }
   ],
   "source": [
    "analyzer = FaceAnalyzer(\n",
    "    detector=RetinaFace(conf_thresh=0.5),\n",
    "    recognizer=ArcFace()\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Detected 1 face with 512D features\n"
     ]
    }
   ],
   "source": [
    "# Load Einstein's photo\n",
    "einstein_path = '../assets/einstien.png'\n",
    "einstein_image = cv2.imread(einstein_path)\n",
    "\n",
    "# Get Einstein's face features\n",
    "einstein_faces = analyzer.analyze(einstein_image)\n",
    "\n",
    "if einstein_faces:\n",
    "    einstein_face = einstein_faces[0]\n",
    "    print(f'Detected {len(einstein_faces)} face with {einstein_face.embedding.shape[1]}D features')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Detected 29 people in the group photo\n"
     ]
    }
   ],
   "source": [
    "# Load the group photo\n",
    "group_photo_path = '../assets/scientists.png'\n",
    "group_photo = cv2.imread(group_photo_path)\n",
    "\n",
    "# Find all faces in the group photo\n",
    "group_faces = analyzer.analyze(group_photo)\n",
    "print(f'Detected {len(group_faces)} people in the group photo')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 954924 characters]",
      "text/plain": [
       "<Figure size 1500x600 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, axes = plt.subplots(1, 2, figsize=(15, 6))\n",
    "\n",
    "# Display Einstein's photo\n",
    "axes[0].imshow(cv2.cvtColor(einstein_image, cv2.COLOR_BGR2RGB))\n",
    "axes[0].set_title(\"Who we're looking for: Einstein\", fontsize=14, fontweight='bold')\n",
    "axes[0].axis('off')\n",
    "\n",
    "# Display the group photo\n",
    "axes[1].imshow(cv2.cvtColor(group_photo, cv2.COLOR_BGR2RGB))\n",
    "axes[1].set_title(f'Where we search: Group of {len(group_faces)} scientists', fontsize=14, fontweight='bold')\n",
    "axes[1].axis('off')\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Top 5 most similar people:\n",
      "1. Person #11: similarity = 0.6171\n",
      "2. Person #15: similarity = 0.1934\n",
      "3. Person #17: similarity = 0.1844\n",
      "4. Person #25: similarity = 0.1556\n",
      "5. Person #19: similarity = 0.1550\n"
     ]
    }
   ],
   "source": [
    "if not einstein_faces or not group_faces:\n",
    "    print('Error: Could not detect faces')\n",
    "else:\n",
    "    # Compare Einstein with each person in the group\n",
    "    matches = []\n",
    "    for i, person in enumerate(group_faces):\n",
    "        similarity = einstein_face.compute_similarity(person)\n",
    "        matches.append((i, similarity))\n",
    "\n",
    "    # Sort by similarity (best matches first)\n",
    "    matches.sort(key=lambda x: x[1], reverse=True)\n",
    "\n",
    "    # Show top 5 matches\n",
    "    print('Top 5 most similar people:')\n",
    "    for rank, (person_idx, similarity) in enumerate(matches[:5], 1):\n",
    "        print(f'{rank}. Person #{person_idx + 1}: similarity = {similarity:.4f}')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 1488828 characters]",
      "text/plain": [
       "<Figure size 1500x1000 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "if einstein_faces and group_faces:\n",
    "    # Get the best match\n",
    "    best_match_idx, best_similarity = matches[0]\n",
    "\n",
    "    # Draw bounding boxes\n",
    "    result_image = group_photo.copy()\n",
    "\n",
    "    for i, person in enumerate(group_faces):\n",
    "        bbox = person.bbox.astype(int)\n",
    "\n",
    "        if i == best_match_idx:\n",
    "            color = (0, 255, 0)\n",
    "            thickness = 3\n",
    "        else:\n",
    "            color = (128, 128, 128)\n",
    "            thickness = 1\n",
    "\n",
    "        cv2.rectangle(result_image, (bbox[0], bbox[1]), (bbox[2], bbox[3]), color, thickness)\n",
    "\n",
    "        if i == best_match_idx:\n",
    "            label = f'Match: {best_similarity:.3f}'\n",
    "            cv2.putText(result_image, label, (bbox[0], bbox[1] - 10),\n",
    "                       cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2)\n",
    "\n",
    "    plt.figure(figsize=(15, 10))\n",
    "    plt.imshow(cv2.cvtColor(result_image, cv2.COLOR_BGR2RGB))\n",
    "    plt.title(f'Best match: Person #{best_match_idx + 1}', fontsize=14)\n",
    "    plt.axis('off')\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 245972 characters]",
      "text/plain": [
       "<Figure size 1600x400 with 4 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "if einstein_faces and group_faces:\n",
    "    # Show top 3 matches\n",
    "    top_k = min(3, len(matches))\n",
    "\n",
    "    fig, axes = plt.subplots(1, top_k + 1, figsize=(16, 4))\n",
    "\n",
    "    # Show Einstein's face\n",
    "    einstein_rgb = cv2.cvtColor(einstein_image, cv2.COLOR_BGR2RGB)\n",
    "    axes[0].imshow(einstein_rgb)\n",
    "    axes[0].set_title(\"Query\", fontsize=12)\n",
    "    axes[0].axis('off')\n",
    "\n",
    "    # Show top 3 matches from the group\n",
    "    for i, (person_idx, similarity) in enumerate(matches[:top_k]):\n",
    "        person = group_faces[person_idx]\n",
    "        bbox = person.bbox.astype(int)\n",
    "\n",
    "        # Crop this person's face\n",
    "        face_crop = group_photo[bbox[1]:bbox[3], bbox[0]:bbox[2]]\n",
    "\n",
    "        if face_crop.size > 0:\n",
    "            face_rgb = cv2.cvtColor(face_crop, cv2.COLOR_BGR2RGB)\n",
    "            axes[i + 1].imshow(face_rgb)\n",
    "            axes[i + 1].set_title(f'Match {i + 1}: {similarity:.3f}', fontsize=12)\n",
    "            axes[i + 1].axis('off')\n",
    "\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Best match: Person #11\n",
      "Similarity: 0.6171\n",
      "Threshold: 0.6\n",
      "Result: Match found (Einstein is person #11)\n"
     ]
    }
   ],
   "source": [
    "# Use threshold to determine if it's a match\n",
    "THRESHOLD = 0.6\n",
    "\n",
    "if einstein_faces and group_faces:\n",
    "    best_match_idx, best_similarity = matches[0]\n",
    "\n",
    "    print(f'Best match: Person #{best_match_idx + 1}')\n",
    "    print(f'Similarity: {best_similarity:.4f}')\n",
    "    print(f'Threshold: {THRESHOLD}')\n",
    "\n",
    "    if best_similarity > THRESHOLD:\n",
    "        print(f'Result: Match found (Einstein is person #{best_match_idx + 1})')\n",
    "    else:\n",
    "        print(f'Result: No match (similarity below threshold)')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Notes\n",
    "\n",
    "- Similarity score ranges from -1 to 1 (higher = more similar)\n",
    "- Threshold of 0.6 is commonly used (above = match, below = no match)\n",
    "- Adjust threshold based on your use case (higher = stricter matching)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": []
  }
 ],
 "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
}
