Skip to Content
BonicBot A2DevelopmentArtificial IntelligenceLesson 9: Teaching BonicBot to Recognize Faces

Lesson 9: Teaching BonicBot to Recognize Faces

Learning Objective

Teach BonicBot to recognize people by comparing a newly detected face with the face embeddings stored in the database created in the previous lesson.


Introduction

In the previous lesson, BonicBot learned how to remember faces by converting every face image into a face embedding and storing those embeddings in a database.

Now it’s time to use that database.

Whenever BonicBot sees a face, it will:

  1. Detect the face.
  2. Generate a new face embedding.
  3. Compare that embedding with every embedding stored in the database.
  4. Find the most similar face.
  5. Announce the person’s name if a match is found.

This process is called face recognition.

Instead of comparing photographs pixel by pixel, BonicBot compares the embeddings produced by the ArcFace model. If two embeddings are very similar, they most likely belong to the same person.

To make the robot more reliable, it only announces a person’s name after recognizing them consistently for several consecutive frames. This prevents repeated announcements caused by temporary recognition errors.


Setup: Installing Packages

Before running the code, make sure your computer has the required Python packages installed. Open a terminal and run:

pip install bonicbot-bridge opencv-python numpy pyttsx3 deepface

What each package does

PackagePurpose
bonicbot-bridgeThe official BonicBot SDK. Provides the BonicBot class and BonicBotError exception used to connect via with BonicBot(host=HOST, port=9090, timeout=10) as bot:, plus the camera lifecycle calls bot.system.start_camera(), bot.start_camera(), bot.camera.wait_for_image(), bot.camera.get_latest_image(), and bot.system.stop_camera().
opencv-pythonImported as cv2 (via the ensure("cv2", "opencv-python") auto-installer). Used to resize frames for faster recognition (cv2.resize), draw bounding boxes and labels (cv2.rectangle, cv2.putText), display the live feed (cv2.imshow), and read the quit key (cv2.waitKey).
numpyImported as np. Used throughout recognize_faces() to convert embeddings to arrays, normalize them with np.linalg.norm, stack the known database into known_matrix with np.vstack, and compute similarity scores via np.dot / np.argmax.
pyttsx3Text-to-speech library used by VoiceSpeaker to announce recognized (or unknown) people. Optional — the script checks for it with a try/except ImportError and sets _TTS_AVAILABLE; if it isn’t installed, recognition and tracking still work, just without spoken announcements.
deepfaceProvides the DeepFace class used in pick_detector_backend() and recognize_faces() to load the ArcFace model (DeepFace.build_model("ArcFace")) and generate embeddings for each detected face (DeepFace.represent(...)).

This lesson also relies on face_database.pkl, the file produced by Lesson 8. Make sure that file exists in the same directory before running this script — the program will exit immediately if it isn’t found.

If you already installed bonicbot-bridge, opencv-python, numpy, pyttsx3, and deepface in earlier lessons, you don’t need to reinstall anything for this lesson.

If pip install fails, try pip3 install ... instead, or use a virtual environment:

python3 -m venv bonicbot-env source bonicbot-env/bin/activate # On Windows: bonicbot-env\Scripts\activate pip install bonicbot-bridge opencv-python numpy pyttsx3 deepface

How to run the program

  1. Find your BonicBot’s IP address — via the robot’s on-device display, its companion app, or your router’s connected-devices list.
  2. Make sure face_database.pkl from Lesson 8 is in the same folder as this script — the program loads it on startup and will exit with an error if it’s missing.
  3. Save the code below into a file, e.g. lesson9_face_recognition.py.
  4. Note the connection line. This script sets HOST = "localhost" and connects with with BonicBot(host=HOST, port=9090, timeout=10) as bot:.
    • If you’re running this against the ROS 2 simulation, you can leave HOST as "localhost" and run it as-is.
    • If you’re connecting to a real BonicBot, change HOST = "localhost" to your robot’s actual IP address, e.g. HOST = "172.20.10.2".
  5. Make sure your BonicBot (real or simulated) is powered on and network-connected, and that at least one person whose face is in face_database.pkl is standing clearly in view of the camera.
  6. (Optional) Connect a Bluetooth speaker to hear the spoken announcements. Recognition still works without one.
  7. Run the script:
python lesson9_face_recognition.py
  1. A window titled “Face Recognition Feed” should open, showing the live feed with a green bounding box and name/confidence label drawn around each detected face.
  2. Press q with the video window focused to stop the stream and exit cleanly.

Don’t have a physical BonicBot? Try it in simulation (optional)

This lesson’s code already defaults to HOST = "localhost", so it’s already set up to run directly against the ROS 2 simulation environment without any code changes:

  1. Launch the BonicBot simulation:
ros2 launch my_bot robot_system.launch.py use_sim_time:=true world:=obsworld.sdf use_real_camera:=True
  • use_real_camera:=True — the camera feed comes from your laptop’s webcam, while the robot’s body and motion are still simulated. This is the easiest way to test recognition, since you can just show your own face (if it’s in face_database.pkl) to your laptop camera.
  • use_real_camera:=False — the camera feed comes from Gazebo instead (i.e. whatever the simulated camera sees inside the simulated world).
  1. With the simulation running, simply run the script as written — since HOST is already "localhost", no code changes are needed.
  2. Everything else in the code — face detection, embedding comparison, stability tracking, and voice announcements — works the same way, since the simulation exposes the same interface as a real robot.

This path is mainly useful for exploring the lesson without hardware on hand; if you have a real BonicBot, changing HOST to its actual IP address is still the recommended way to go through this lesson on real hardware.


Code

Click to view the complete program

import os, sys, subprocess, importlib, time, pickle, queue, threading import numpy as np from bonicbot_bridge import BonicBot, BonicBotError # ---------------- Auto-install dependencies ---------------- def pip_install(pkg): subprocess.check_call([sys.executable, "-m", "pip", "install", pkg]) def ensure(module, pkg=None): try: return importlib.import_module(module) except ImportError: print(f"📦 Installing '{pkg or module}'...") pip_install(pkg or module) return importlib.import_module(module) cv2 = ensure("cv2", "opencv-python") ensure("deepface") try: import pyttsx3 _TTS_AVAILABLE = True except ImportError: _TTS_AVAILABLE = False from deepface import DeepFace # ---------------- Detector backend (handles OpenCV 5.x missing CascadeClassifier) ---------------- def pick_detector_backend(): if hasattr(cv2, "CascadeClassifier"): return "opencv" print(f"⚠️ cv2 {cv2.__version__} lacks CascadeClassifier — probing fallback detectors...") for backend, pkg, mod in [("mediapipe", "mediapipe", "mediapipe"), ("mtcnn", "mtcnn", "mtcnn"), ("retinaface", "retina-face", "retinaface")]: try: ensure(mod, pkg) DeepFace.represent(np.zeros((100, 100, 3), np.uint8), model_name="ArcFace", detector_backend=backend, enforce_detection=False, align=True) print(f"✅ Using '{backend}' detector backend.") return backend except Exception as e: print(f" ↳ '{backend}' failed: {e}") sys.exit("❌ No working face detector found (tried mediapipe, mtcnn, retinaface).") DETECTOR_BACKEND = pick_detector_backend() print("📥 Loading ArcFace model...") DeepFace.build_model("ArcFace") # ---------------- Speech ---------------- class VoiceSpeaker: def __init__(self, rate=175, volume=0.8): self._q = queue.Queue() self._rate, self._volume = rate, volume if _TTS_AVAILABLE: threading.Thread(target=self._run, daemon=True).start() def _run(self): while (text := self._q.get()) is not None: try: engine = pyttsx3.init() engine.setProperty('rate', self._rate) engine.setProperty('volume', self._volume) engine.say(text) engine.runAndWait() engine.stop() except Exception as e: print(f"⚠️ TTS error: {e}") def speak(self, text): if _TTS_AVAILABLE: self._q.put(text) def stop(self): if _TTS_AVAILABLE: self._q.put(None) # ---------------- Stability tracker ---------------- HOLD_FRAMES, MISS_TOLERANCE = 5, 6 class RecognitionTracker: def __init__(self): self._state = {} # name -> {"streak", "announced", "missed"} def update(self, seen_names): seen = set(seen_names) for name in [n for n in self._state if n not in seen]: self._state[name]["missed"] += 1 if self._state[name]["missed"] > MISS_TOLERANCE: del self._state[name] stable = [] for name in seen: e = self._state.setdefault(name, {"streak": 0, "announced": False, "missed": 0}) e["missed"], e["streak"] = 0, e["streak"] + 1 if e["streak"] == HOLD_FRAMES and not e["announced"]: e["announced"] = True stable.append(name) return stable # ---------------- Face database ---------------- print("Loading face database...") try: with open("face_database.pkl", "rb") as f: db = pickle.load(f) known_names, known_matrix = db["identities"], np.vstack(db["embeddings"]) print(f"Loaded {len(known_names)} profiles.") except FileNotFoundError: sys.exit("Database not found. Please create it first.") def recognize_faces(frame): """Return list of (name, confidence, region) for each face in frame.""" results = [] try: faces = DeepFace.represent(img_path=frame, model_name="ArcFace", detector_backend=DETECTOR_BACKEND, enforce_detection=False, align=True) for fd in faces: conf = fd.get("face_confidence", 0) if conf < 0.6: continue vec = np.array(fd["embedding"]) vec = vec / np.linalg.norm(vec) if np.linalg.norm(vec) > 0 else vec sims = np.dot(known_matrix, vec) idx = np.argmax(sims) name = known_names[idx] if sims[idx] >= 0.45 else "Unknown" results.append((name, conf, fd.get("facial_area", {}))) except Exception as e: print(f"⚠️ recognize_faces error: {e}") return results # ---------------- Threaded recognition (keeps video smooth) ---------------- HOST, MAX_FAILS = "localhost", 10 RECOGNIZE_EVERY_N_FRAMES, DOWNSCALE_WIDTH = 5, 480 speaker, tracker = VoiceSpeaker(), RecognitionTracker() lock = threading.Lock() latest_detections = [] busy = threading.Event() def recognition_worker(small_frame, scale): """Runs DeepFace in the background, then publishes bounding boxes scaled back up to full-frame coordinates. Only x/y/w/h are kept — DeepFace's facial_area dict can also contain (x, y) eye-position tuples under other keys, which aren't numeric and can't be scaled.""" global latest_detections try: dets = recognize_faces(small_frame) scaled = [] for n, c, r in dets: box = {k: int(r[k] / scale) for k in ("x", "y", "w", "h") if k in r} scaled.append((n, c, box)) with lock: latest_detections = scaled finally: busy.clear() # ---------------- Main loop ---------------- try: with BonicBot(host=HOST, port=9090, timeout=10) as bot: print("📷 Starting camera...") bot.system.start_camera() bot.start_camera() bot.camera.wait_for_image(timeout=5.0) print("✅ Camera running. Press 'q' to stop.\n") frame_count, fails = 0, 0 try: while True: frame = bot.camera.get_latest_image() if frame is None: fails += 1 if fails >= MAX_FAILS: print(f"Failed to grab frame after {MAX_FAILS} attempts.") break time.sleep(0.05) continue fails = 0 if frame_count % RECOGNIZE_EVERY_N_FRAMES == 0 and not busy.is_set(): busy.set() h, w = frame.shape[:2] scale = DOWNSCALE_WIDTH / w if w > DOWNSCALE_WIDTH else 1.0 small = cv2.resize(frame, (int(w * scale), int(h * scale))) if scale < 1.0 else frame.copy() threading.Thread(target=recognition_worker, args=(small, scale), daemon=True).start() with lock: detections = list(latest_detections) for name in tracker.update([d[0] for d in detections]): msg = "Unknown person detected" if name == "Unknown" else f"{name} recognized" print(f"[{time.strftime('%H:%M:%S')}] 🟢 {msg}") speaker.speak(msg) for name, conf, r in detections: x, y, w, h = r.get("x", 0), r.get("y", 0), r.get("w", 0), r.get("h", 0) cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(frame, f"{name} ({conf:.2f})", (x, max(y - 8, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow('Face Recognition Feed', frame) frame_count += 1 if cv2.waitKey(1) & 0xFF == ord('q'): break finally: cv2.destroyAllWindows() bot.system.stop_camera() except BonicBotError as e: print(f"Robot error: {e}") finally: speaker.stop() print("Camera shutdown and window closed.")

Replace [IP_ADDRESS] with the IP address of your own BonicBot. Every BonicBot may have a different IP address depending on your network configuration.

📌 Example:

with BonicBot(host="172.20.10.2") as bot:

Replace 172.20.10.2 with the IP address assigned to your BonicBot.

🔊To hear BonicBot’s voice announcements, connect a Bluetooth speaker to your BonicBot before running this program. If no Bluetooth speaker is connected, the program will still detect ArUco markers normally, but you won’t hear the spoken announcements.

Code Walkthrough

Line-by-line explanation

  • Importsos, sys, subprocess, importlib support the auto-install logic; time handles timestamps and retry delays; pickle loads the face database; queue and threading power the background TTS worker and the threaded recognition pipeline; numpy (np) handles embedding math; BonicBot / BonicBotError manage the robot connection.
  • pip_install(pkg) / ensure(module, pkg=None) — Small helper pair used to auto-install missing dependencies at runtime: ensure() tries importlib.import_module(module) first, and only calls pip_install() if the import fails, then imports again. cv2 is loaded this way (cv2 = ensure("cv2", "opencv-python")), as is deepface (ensure("deepface")).
  • pyttsx3 import block — Wrapped in try/except ImportError to set _TTS_AVAILABLE, the same optional-TTS pattern used in earlier lessons.
  • pick_detector_backend() — Checks for cv2.CascadeClassifier first and returns "opencv" if present. Otherwise it loops through mediapipe, mtcnn, and retinaface, using ensure() to install each one if needed and testing it with a throwaway DeepFace.represent() call on a blank image before returning the first backend that works. Exits with sys.exit(...) if none succeed.
  • DETECTOR_BACKEND = pick_detector_backend() — Runs once at startup, and DeepFace.build_model("ArcFace") is called right after to pre-load the model so the first real recognition call isn’t slowed down by a cold start.
  • VoiceSpeaker — Same background-thread TTS pattern as previous lessons: speak() pushes text onto a queue.Queue, _run() consumes it on a daemon thread and speaks each item with pyttsx3, and stop() pushes None as a sentinel to end the loop.
  • HOLD_FRAMES, MISS_TOLERANCE — Tuning constants for the RecognitionTracker: how many consecutive “seen” updates are needed before a name is announced, and how many consecutive misses are tolerated before a person’s tracking state is forgotten.
  • RecognitionTracker — Maintains a _state dictionary keyed by name, each holding a streak, announced flag, and missed counter. update(seen_names) increments missed for names no longer seen (deleting their state once missed exceeds MISS_TOLERANCE), and increments streak for names currently seen, resetting missed to 0. A name is added to the returned stable list — and announced is flipped to True — only the moment its streak first reaches HOLD_FRAMES, so each appearance is announced exactly once.
  • Face database loading — Opens face_database.pkl with pickle.load(), pulling out known_names (the identities list) and stacking every stored embedding into a single 2D array with np.vstack(db["embeddings"]) for fast batch comparison. If the file doesn’t exist, sys.exit(...) stops the script immediately with a clear message.
  • recognize_faces(frame) — Calls DeepFace.represent(img_path=frame, ...) to detect and embed every face in the given frame, using enforce_detection=False so a frame with no face doesn’t raise. For each detected face (fd), it skips low-confidence detections (face_confidence < 0.6), normalizes the new embedding the same way Lesson 8 did, then computes similarity against every known embedding at once via np.dot(known_matrix, vec) and picks the best match with np.argmax(sims). If that best similarity score is below 0.45, the person is labeled "Unknown" instead of a database name. Returns a list of (name, confidence, facial_area) tuples.
  • recognition_worker(small_frame, scale) — Runs recognize_faces() on a background thread so the main video loop never blocks waiting on DeepFace. Because the frame passed in was downscaled for speed, each face’s bounding box (x, y, w, h) is divided back by scale to map it to full-frame coordinates before being stored in the shared latest_detections list (protected by lock). The busy event is cleared in a finally block so a new recognition pass can be scheduled once this one finishes.
  • HOST, MAX_FAILS, RECOGNIZE_EVERY_N_FRAMES, DOWNSCALE_WIDTH — Connection target and performance tuning constants: how many consecutive failed frame grabs are tolerated before giving up, how often (in frames) a new recognition pass is kicked off, and how narrow frames are resized to before being sent to DeepFace.
  • Shared state (speaker, tracker, lock, latest_detections, busy) — A single VoiceSpeaker and RecognitionTracker are created once; lock guards latest_detections, the most recent set of recognized faces available to the main loop; busy is a threading.Event used to prevent overlapping recognition passes from running at the same time.
  • Main loop — Inside with BonicBot(host=HOST, port=9090, timeout=10) as bot:, the camera is started with bot.system.start_camera() / bot.start_camera() and bot.camera.wait_for_image(). Each iteration:
    • Pulls a frame with bot.camera.get_latest_image(), tracking consecutive failures in fails and breaking out after MAX_FAILS is reached.
    • Every RECOGNIZE_EVERY_N_FRAMES frames — and only if no recognition pass is already running (not busy.is_set()) — downsizes the frame via cv2.resize and launches recognition_worker on a new daemon thread, rather than running DeepFace on every single frame.
    • Reads the latest results out of latest_detections under lock, then calls tracker.update(...) with just the detected names; any name returned as newly “stable” triggers a printed log line and a speaker.speak(...) announcement — “Unknown person detected” for unmatched faces, "{name} recognized" otherwise.
    • Draws a green bounding box and a "{name} ({confidence})" label for every currently tracked detection using cv2.rectangle and cv2.putText, then shows the frame in the 'Face Recognition Feed' window.
    • Checks for the q key via cv2.waitKey(1) to break the loop.
  • Cleanup — The inner finally block closes the OpenCV window and calls bot.system.stop_camera() once the loop ends. The outer try/except BonicBotError catches robot connection errors, and the outermost finally calls speaker.stop() and prints a final shutdown message regardless of how the script exited.

Expected Output

Click to see expected output

Visual Output:

Terminal Output:

Loading face database... Loaded 18 profiles. 📷 Starting BonicBot camera and streaming... ✅ Camera running. [14:52:08] 🟢 Alice recognized [14:52:17] 🟢 Bob recognized [14:52:29] 🟢 Unknown person detected

The BonicBot Vision Window will display:

  • A live view from BonicBot’s vision system.
  • Face recognition running continuously.
  • Console messages whenever a person is successfully recognized.
  • Voice announcements for recognized and unknown people.

BonicBot only announces a name once while the person remains in view. If they leave and return later, the robot will announce them again.


🔧 Under the Hood

How does BonicBot recognize a face?

This lesson uses the ArcFace deep learning model for face recognition.

When BonicBot sees a face, it first generates a new face embedding.

For example:

Person Standing in Front of BonicBot ArcFace [0.14, -0.32, 0.87, ...]

The program then compares this new embedding with every embedding stored inside face_database.pkl.

To measure how similar two embeddings are, it uses Cosine Similarity.

New Face Embedding Compare with Database Highest Cosine Similarity Recognized Person

If the highest similarity score is greater than the chosen threshold, BonicBot considers the face to be a match.

Otherwise, the person is labeled as Unknown.

The program also uses a Recognition Tracker, which waits until the same person has been recognized for several consecutive processed frames before announcing their name. This makes the robot much more stable and prevents repeated announcements while someone remains standing in front of BonicBot.


Student Challenge

Modify the program so BonicBot greets each recognized person by name.

For example:

  • “Hello Alice!”
  • “Welcome back Bob!”
  • “Nice to see you Charlie!”

You can also use different greetings for different people.

Hint

Instead of saying:

speaker.speak(message)

Create a custom greeting.

For example:

speaker.speak(f"Hello {name}!")

You could also use an if statement to give different greetings to different people.


Reflection Question

Suppose two different people look very similar, or someone is wearing glasses, a mask, or a hat.

Why do you think BonicBot compares face embeddings instead of comparing the original images directly? How might this make face recognition more reliable?

Last updated on