Lesson 2: Teaching BonicBot to Detect Faces
Learning Objective
Teach BonicBot to detect human faces in its live vision feed, highlight each face with a bounding box, display facial landmarks, count how many people are currently visible, and monitor the robot’s vision performance.
Introduction
In the previous lesson, BonicBot learned to recognize different kinds of objects in its surroundings. This time, we’ll give the robot a much more focused task:
“Can you find every person’s face that you can currently see?”
Instead of looking for many object categories, BonicBot now uses a face detection model that specializes in finding human faces.
Whenever the robot spots a face, it can determine:
- where the face is located
- how confident it is about the detection
- several important facial landmarks, such as the eyes, nose, mouth, and ears
To make the results easy to understand, the program gives every detected face its own color and number. BonicBot also keeps track of how many faces are currently visible and displays a small information panel showing the live face count and processing speed.
Rather than printing information continuously, the robot only reports when the number of detected faces changes. This keeps the terminal clean while still recording important events.
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 numpyWhat each package does
| Package | Purpose |
|---|---|
bonicbot-bridge | The official BonicBot SDK. Provides the BonicBot class used to connect to the robot, control its camera, enable face detection, and read back detected faces via bot.get_faces(). |
opencv-python | Imported as cv2. Used to draw bounding boxes, labels, landmark dots, and the translucent HUD panel, and to display the live video window. |
numpy | Imported as np. Backs the image arrays that both opencv-python and bonicbot-bridge work with internally. |
If you already installed these packages in Lesson 1, you don’t need to reinstall them — the same environment works 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 numpyHow to run the program
-
Find your BonicBot’s IP address. Check the robot’s on-device display, its companion app, or your router’s connected-devices list.
-
Save the code below into a file, e.g.
lesson2_faces.py. -
Replace
[IP_ADDRESS]in the code with your robot’s actual IP address (see the callout box under the code block). -
Make sure your BonicBot is powered on and connected to the same network as your computer, and that at least one face can appear in front of its camera.
-
Run the script:
python lesson2_faces.py -
A window titled “BonicBot Vision” should open, showing colored boxes and landmarks around each detected face, plus a small stats panel in the top-left corner.
-
Watch the terminal — a new line is only printed when the number of visible faces changes.
-
Press
qwith the video window focused to stop the stream and exit cleanly.
Don’t have a physical BonicBot? Try it in simulation (optional)
-
Launch the BonicBot simulation:
ros2 launch my_bot robot_system.launch.py use_sim_time:=true world:=obsworld.sdf use_real_camera:=Trueuse_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 detection, since you can just hold whatever you want detected up 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).
-
Once the simulation is running, use
localhostas the host instead of a physical IP address:with BonicBot(host="localhost") as bot: -
Everything else in the code — enabling face detection, drawing boxes, reading
bot.get_faces()— 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, connecting to its actual IP address is the recommended way to go through this lesson.
Code
Click to view the complete program
import time
import cv2
import numpy as np
from bonicbot_bridge import BonicBot
# One distinct BGR color per face "slot" (cycles if more faces than colors)
FACE_COLORS = [
(0, 255, 0), # green
(255, 255, 0), # cyan
(0, 165, 255), # orange
(255, 0, 255), # magenta
(0, 255, 255), # yellow
(255, 0, 0), # blue
]
FLASH_DURATION = 12 # frames the border pulse stays visible after a change
MIN_CONFIDENCE = 0.8 # only keep detections the bridge is at least 80% sure about
def draw_faces(frame, faces):
"""Draw each face with its own color, an ID label, and its keypoints."""
h_px, w_px = frame.shape[:2]
for i, face in enumerate(faces):
color = FACE_COLORS[i % len(FACE_COLORS)]
bbox = face["bbox"]
if isinstance(bbox, dict):
xmin = float(bbox["xmin"])
ymin = float(bbox["ymin"])
bw_n = float(bbox["width"])
bh_n = float(bbox["height"])
cx_n = xmin + bw_n / 2
cy_n = ymin + bh_n / 2
else:
cx_n, cy_n, bw_n, bh_n = (float(v) for v in bbox)
x1 = max(0, int((cx_n - bw_n / 2) * w_px))
y1 = max(0, int((cy_n - bh_n / 2) * h_px))
x2 = min(w_px - 1, int((cx_n + bw_n / 2) * w_px))
y2 = min(h_px - 1, int((cy_n + bh_n / 2) * h_px))
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
label = f"Face #{i + 1} {face['confidence']:.0%}"
(tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.55, 2)
label_y1 = max(0, y1 - th - 10)
cv2.rectangle(frame, (x1, label_y1), (x1 + tw + 8, y1), color, -1)
cv2.putText(frame, label, (x1 + 4, y1 - 6),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 0, 0), 2)
kps = face.get("keypoints", [])
kp_iter = kps.values() if isinstance(kps, dict) else kps
for kp in kp_iter:
if isinstance(kp, dict):
kx_n, ky_n = float(kp["x"]), float(kp["y"])
else:
kx_n, ky_n = float(kp[0]), float(kp[1])
kx, ky = int(kx_n * w_px), int(ky_n * h_px)
cv2.circle(frame, (kx, ky), 3, color, -1)
return frame
def draw_hud(frame, faces, fps, flash_frames_left):
"""Translucent stats panel (face count + FPS) plus a brief border flash
whenever the face count just changed."""
h_px, w_px = frame.shape[:2]
panel_w, panel_h = 210, 66
overlay = frame.copy()
cv2.rectangle(overlay, (10, 10), (10 + panel_w, 10 + panel_h), (0, 0, 0), -1)
frame = cv2.addWeighted(overlay, 0.55, frame, 0.45, 0)
cv2.putText(frame, f"Faces: {len(faces)}", (22, 40),
cv2.FONT_HERSHEY_SIMPLEX, 0.85, (0, 255, 0), 2)
cv2.putText(frame, f"FPS: {fps:.1f}", (22, 64),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, (180, 180, 180), 1)
if flash_frames_left > 0:
border_color = (0, 255, 0) if len(faces) > 0 else (0, 0, 255)
cv2.rectangle(frame, (2, 2), (w_px - 3, h_px - 3), border_color, 4)
return frame
with BonicBot(host='[IP_ADDRESS]') as bot:
print("📷 Starting camera and streaming...")
bot.system.start_camera()
bot.start_camera()
bot.camera.wait_for_image(timeout=5.0)
print("🧠 Enabling face detection (BonicBot Bridge)...")
bot.enable_detection('face')
print("✅ Live Stream Active. Press 'q' to quit.\n")
prev_time = time.time()
fps = 0.0
last_count = -1
flash_frames_left = 0
try:
while True:
frame = bot.get_image()
if frame is not None:
faces = [f for f in bot.get_faces() if f.get("confidence", 1.0) >= MIN_CONFIDENCE]
now = time.time()
dt = now - prev_time
prev_time = now
if dt > 0:
fps = fps * 0.9 + (1.0 / dt) * 0.1
if len(faces) != last_count:
ts = time.strftime("%H:%M:%S")
if len(faces) == 0:
print(f"[{ts}] ⚪ No face in view")
else:
confs = ", ".join(f"{f['confidence']:.0%}" for f in faces)
print(f"[{ts}] 🙂 {len(faces)} face(s) in view ({confs})")
last_count = len(faces)
flash_frames_left = FLASH_DURATION
if flash_frames_left > 0:
flash_frames_left -= 1
display = draw_faces(frame.copy(), faces)
display = draw_hud(display, faces, fps, flash_frames_left)
cv2.imshow("BonicBot Vision", display)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
finally:
bot.disable_detection()
cv2.destroyAllWindows()
bot.stop_camera()
bot.system.stop_camera()
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.
Code Walkthrough
Line-by-line explanation
- Imports —
timehandles timestamps and FPS calculation;cv2(OpenCV) draws boxes, labels, landmarks, and the HUD;numpybacks the image arrays;BonicBotis the SDK class used to connect to the robot. FACE_COLORS— A list of six distinct BGR colors. Each detected face is assigned one viai % len(FACE_COLORS), so colors repeat if more than six faces are visible at once.FLASH_DURATION/MIN_CONFIDENCE— Two tunable constants:FLASH_DURATIONcontrols how many frames the border-flash effect lasts after the face count changes, andMIN_CONFIDENCEfilters out any detection the model is less than 80% sure about.draw_faces(frame, faces)— For each face, reads its bounding box (handling both a{xmin, ymin, width, height}dict format and a plain[cx, cy, w, h]list format), converts normalized coordinates to pixels, and draws a colored rectangle, a filled label background, and the face number with confidence. It then loops over the face’s keypoints (eyes, nose, mouth, ears) and draws a small dot for each.draw_hud(frame, faces, fps, flash_frames_left)— Draws a semi-transparent black panel in the top-left corner usingcv2.addWeighted, then writes the current face count and FPS on top of it. Ifflash_frames_leftis greater than zero, it also draws a colored border around the whole frame — green if faces are present, red if the count just dropped to zero.with BonicBot(host='[IP_ADDRESS]') as bot:— Opens a connection to your robot, automatically closing it when the block ends.bot.system.start_camera()/bot.start_camera()/bot.camera.wait_for_image(...)— Powers on the camera, starts streaming, and waits (up to 5 seconds) for the first frame to arrive.bot.enable_detection('face')— Switches the robot’s vision pipeline from general object detection to the dedicated face-detection model.- FPS tracking (
prev_time,dt,fps = fps * 0.9 + ... * 0.1) — Measures the time between frames and smooths the frames-per-second estimate using an exponential moving average, so the displayed FPS doesn’t jitter wildly frame to frame. - Change-triggered logging (
if len(faces) != last_count:) — Compares the current face count to the last printed count. A line is only printed to the terminal when that count actually changes, keeping the output readable instead of flooding it every frame. flash_frames_left— Set toFLASH_DURATIONwhenever the face count changes, then counted down each loop iteration; this is what drives the temporary border-flash effect indraw_hud.- Main loop — Grabs a frame, gets filtered faces, updates FPS and logging, draws faces and the HUD onto a copy of the frame, and shows it in the “BonicBot Vision” window.
cv2.waitKey(1) & 0xFF == ord("q")— Checks each iteration whetherqwas pressed, breaking the loop if so.finally:cleanup block — Runs whether the loop exits normally or due to an error, ensuring face detection is disabled, the display window is closed, and the camera is stopped every time.
Expected Output
Click to see expected output
Visual Output:
Terminal Output:
📷 Starting vision system...
🧠 Loading face detection model...
✅ Vision system ready.
[14:08:23] 🙂 2 face(s) in view (99%, 96%)The BonicBot vision window will display:
- A colored box around every detected face
- Face numbers (Face #1, Face #2, …)
- Detection confidence
- Six facial landmarks for each face
- A live face counter
- Current processing speed (FPS)
- A brief border flash whenever the number of visible faces changes
🔧 Under the Hood
How does BonicBot detect faces?
Every image received by BonicBot’s vision system is analyzed using MediaPipe Face Detection, a lightweight deep learning model designed for real-time robotics applications.
For every face the model finds, it predicts:
- a bounding box
- a confidence score
- six facial landmark points
The program converts these predictions into positions on the robot’s display, where each face receives:
- its own colored bounding box
- a unique face number
- confidence information
- landmark markers
The robot also maintains a small dashboard showing:
- the current number of detected faces
- the processing speed (Frames Per Second)
To avoid flooding the terminal with repeated messages, BonicBot only prints an event when the number of visible faces changes. This event-based logging is commonly used in robotics because it highlights important changes instead of repeating the same information hundreds of times every second.
Student Challenge
Modify the program so BonicBot also displays the center position of each detected face underneath its label.
For example:
Face #1
(315, 184)This helps visualize exactly where BonicBot believes each face is located.
Hint
The bounding box already contains the normalized center coordinates (cx_n, cy_n). Convert them into pixel coordinates using the display width and height before drawing them with cv2.putText().
Reflection Question
Imagine BonicBot is welcoming students into a classroom.
Why is teaching BonicBot to detect a face much simpler and safer than teaching it to recognize your specific face?