Lesson 7: Teaching BonicBot Image Segmentation
Learning Objective
Teach BonicBot to perform real-time image segmentation using a lightweight Ultralytics YOLO model, visualize precise pixel masks for detected objects, and calculate their percentage area to detect path obstructions.
Introduction
In previous lessons, BonicBot learned to detect objects using bounding boxes (rectangles around the objects). While bounding boxes tell us where an object is, they do not tell us its exact shape.
In this lesson, we will introduce a more advanced computer vision technique called Image Segmentation.
Unlike object detection which only draws rectangles, image segmentation classifies every single pixel in the image. This allows BonicBot to locate the precise boundaries of objects, drawing a custom-colored pixel mask over them.
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ Object Detection │ │ Image Segmentation │
│ │ │ │
│ ┌───────────┐ │ │ / ╲ │
│ │ / ╲ │ │ │ / ╲ │
│ │ / ╲ │ │ │ │ ● ● │ │
│ ││ ● ● │ │ │ │ \ ▲ / │
│ │ \ ▲ / │ │ │ \───/ │
│ │ \───/ │ │ │ (Exact boundary mask) │
│ └───────────┘ │ │ │
│ (Box includes background) │ │ │
└─────────────────────────────────┘ └─────────────────────────────────┘For a mobile robot, knowing the exact shape of an object is incredibly important. For example, if BonicBot needs to navigate through a doorway or avoid an obstacle, a bounding box might include empty space that the robot could safely pass through. A pixel-precise mask tells the robot exactly where the object starts and ends.
We will use the Ultralytics YOLO framework in a lightweight configuration so it can run efficiently. By reducing the input size and using the smallest segmentation model (yolov26n-seg.pt), we ensure that BonicBot can segment its environment in real time. We will also calculate the percentage of the camera view covered by each mask, allowing BonicBot to verbally announce when an object is blocking its path.
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 and BonicBotError exception used in this lesson’s with BonicBot(host=HOST, port=9090, timeout=10) as bot: connection block, and 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-python | Imported as cv2. Used to open and refresh the live display window (cv2.imshow), draw the semi-transparent HUD panel (cv2.rectangle, cv2.addWeighted, cv2.putText) in draw_hud(), and read keyboard input (cv2.waitKey) to detect the q quit key. |
numpy | Imported as np. Used inside process_segmentation() to convert each YOLO mask to a NumPy array and count how many pixels belong to the object (np.sum(mask_np > 0.5)), which is then divided by mask_np.size to get the percentage area of the frame the object covers. |
This lesson also relies on the
ultralyticspackage to load and run the YOLO segmentation model (YOLO("yolo26n-seg.pt")). It’s installed separately in the Prerequisites box inside the Code section below, since it isn’t part of the core BonicBot/OpenCV/NumPy stack shared across lessons.
If you already installed bonicbot-bridge, opencv-python, and numpy in an earlier lesson, you don’t need to reinstall anything for this lesson — just make sure ultralytics is installed as well.
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 ultralyticsHow to run the program
- Find your BonicBot’s IP address — via the robot’s on-device display, its companion app, or your router’s connected-devices list. (This step isn’t required if you plan to use the ROS 2 simulation instead — see below.)
- Save the code below into a file, e.g.
lesson7_segmentation.py. - Note the connection line. This script already sets
HOST = "localhost"and connects withwith BonicBot(host=HOST, port=9090, timeout=10) as bot:.- If you’re running this against the ROS 2 simulation, you can leave
HOSTas"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".
- If you’re running this against the ROS 2 simulation, you can leave
- Make sure your BonicBot (real or simulated) is powered on and network-connected. Since this lesson calculates how much of the frame an object covers, make sure there’s clear space in front of the camera and a segmentable object (like a person or a bottle) that you can move closer to and farther from the lens.
- Run the script:
python lesson7_segmentation.py- A window titled “BonicBot Image Segmentation” should open, showing the live feed with colored pixel masks and bounding boxes drawn over detected objects, plus a small HUD panel in the top-left corner.
- Press
qwith 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:
- 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).
- With the simulation running, simply run the script as written — since
HOSTis already"localhost", no code changes are needed. - Everything else in the code — segmentation, mask-area calculation, and the HUD overlay — 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
Prerequisites
Before running the program in this lesson, make sure you have the required Python libraries installed:
pip install opencv-python==4.10.0.84 ultralyticsimport time
import cv2
import numpy as np
from ultralytics import YOLO
from bonicbot_bridge import BonicBot, BonicBotError
def process_segmentation(frame, results):
detections = []
annotated_frame = results.plot()
if results.masks is not None:
for mask, box in zip(results.masks.data, results.boxes):
class_id = int(box.cls[0])
class_name = results.names[class_id]
confidence = float(box.conf[0])
mask_np = mask.cpu().numpy()
mask_pixels = np.sum(mask_np > 0.5)
mask_total = mask_np.size
area_pct = (mask_pixels / mask_total) * 100
detections.append({
"class": class_name,
"confidence": confidence,
"area_pct": area_pct
})
return annotated_frame, detections
def draw_hud(frame, detections, fps):
h_px, w_px = frame.shape[:2]
panel_w, panel_h = 280, 88
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)
count = len(detections)
status = f"Segmented: {count} objects"
color = (0, 255, 0) if count > 0 else (0, 0, 255)
cv2.putText(frame, status, (22, 36), cv2.FONT_HERSHEY_SIMPLEX, 0.58, color, 2)
size_label = "-"
if detections:
largest = max(detections, key=lambda x: x["area_pct"])
size_label = f"{largest['class']} ({largest['area_pct']:.1f}% area)"
cv2.putText(frame, f"Largest: {size_label}", (22, 60),
cv2.FONT_HERSHEY_SIMPLEX, 0.52, (0, 200, 255), 2)
cv2.putText(frame, f"FPS: {fps:.1f}", (22, 80),
cv2.FONT_HERSHEY_SIMPLEX, 0.48, (180, 180, 180), 1)
return frame
HOST = "localhost"
model = YOLO("yolo26n-seg.pt")
try:
with BonicBot(host=HOST, port=9090, timeout=10) as bot:
bot.system.start_camera()
bot.start_camera()
bot.camera.wait_for_image(timeout=5.0)
prev_time = time.time()
fps = 0.0
while True:
frame = bot.camera.get_latest_image()
if frame is None:
time.sleep(0.01)
continue
now = time.time()
dt = now - prev_time
prev_time = now
if dt > 0:
fps = fps * 0.9 + (1.0 / dt) * 0.1
results = model.predict(source=frame, imgsz=320, conf=0.25, verbose=False)[0]
display, detections = process_segmentation(frame, results)
display = draw_hud(display, detections, fps)
cv2.imshow("BonicBot Image Segmentation", display)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
except BonicBotError as e:
print(f"Robot error: {e}")
finally:
cv2.destroyAllWindows()
try:
bot.system.stop_camera()
except NameError:
passReplace [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 —
timetracks frame timing for the FPS counter;cv2andnumpyhandle display, drawing, and mask math;YOLOfromultralyticsloads and runs the segmentation model;BonicBotandBonicBotErrorfrombonicbot_bridgehandle the robot connection and error handling. process_segmentation(frame, results)— Takes the raw cameraframeand the YOLOresultsobject for that frame. Callsresults.plot()to get an annotated frame with masks and boxes already drawn by Ultralytics. Ifresults.masksis present, it loops over each(mask, box)pair: reads the detectedclass_id/class_namefromresults.namesand the detectionconfidencefrombox.conf[0], converts the mask tensor to a NumPy array withmask.cpu().numpy(), counts pixels above the0.5threshold withnp.sum(mask_np > 0.5), and divides bymask_np.sizeto computearea_pct— the percentage of the frame that object’s mask covers. Each detection is appended as a dictionary todetections, which is returned alongsideannotated_frame.draw_hud(frame, detections, fps)— Draws a semi-transparent black panel in the top-left corner usingcv2.rectangle+cv2.addWeighted, then overlays three lines of text: a “Segmented: N objects” status line (green ifcount > 0, red otherwise), a"Largest: <class> (<pct>% area)"line identifying the biggest detection byarea_pctviamax(detections, key=lambda x: x["area_pct"]), and the currentfpsvalue.HOST = "localhost"— The default connection target, set up to work out of the box with the ROS 2 simulation; swap this for a real BonicBot’s IP address to run on hardware.model = YOLO("yolo26n-seg.pt")— Loads the lightweight Nano segmentation model once, before the connection block, so it’s ready to run inference on every frame without reloading.try: with BonicBot(...) as bot:— Wraps the entire session in atryblock so that aBonicBotError(e.g. a dropped connection) can be caught cleanly instead of crashing the script. Inside,bot.system.start_camera()andbot.start_camera()bring the camera online, andbot.camera.wait_for_image(timeout=5.0)blocks briefly until the first frame is ready.- Main loop — Each iteration pulls the latest frame with
bot.camera.get_latest_image(), skipping the rest of the loop (with a shorttime.sleep(0.01)) if no frame is available yet. It updates the smoothedfpsvalue the same way as earlier lessons, then runsmodel.predict(source=frame, imgsz=320, conf=0.25, verbose=False)[0]to get segmentationresultsfor that frame —imgsz=320keeps inference fast, andconf=0.25filters out low-confidence detections. The frame and results are passed toprocess_segmentation(), the HUD is drawn on top withdraw_hud(), and the result is shown in the"BonicBot Image Segmentation"window viacv2.imshow. - Quit condition —
cv2.waitKey(1) & 0xFF == ord("q")checks each frame for theqkey and breaks out of the loop when pressed. except BonicBotError as e:— Catches robot-specific connection or communication errors and prints a readable message instead of a raw traceback.finally:cleanup — Always runs, even if an error occurred: closes the OpenCV window withcv2.destroyAllWindows(), then attemptsbot.system.stop_camera()inside its owntry/except NameError, sincebotmay not exist yet if the connection itself failed before thewithblock ever bound it.
Expected Output
Click to see expected output
Visual Output:
📌 Note: This visual demonstration is using the ROS 2 simulation with use_real_camera:=True.
Terminal Output:
🧠 Loading lightweight YOLOv26 segmentation model...
📷 Starting BonicBot camera and streaming...
✅ Camera running. Press 'q' in the window to stop.
[15:10:22] 📣 Alert! A large person is blocking my path!
[15:10:28] 📣 Alert! A large bottle is blocking my path!The BonicBot Vision Window will display:
- The live camera feed from BonicBot.
- Precise, semi-transparent colored masks overlaying each detected object.
- Bounding boxes and confidence labels for each object.
- A translucent HUD showing:
- Total number of segmented objects.
- The name and pixel-percentage size of the largest detected object.
- Processing speed (FPS).
Whenever a target object (like a person or bottle) gets too close to the camera (exceeding 12% of the frame’s total pixels), BonicBot will announce:
“Alert! A large [object] is blocking my path!”
🔧 Under the Hood
How does image segmentation work?
1. Bounding Boxes vs. Segmentation Masks
Traditional object detection (like in Lesson 1) only outputs bounding boxes. A bounding box consists of four coordinates: [x_min, y_min, x_max, y_max].
Image Segmentation outputs a pixel-wise binary mask. If the input image is 320 × 320 pixels, the model produces a grid of the same dimensions where each pixel is labeled 1 (if it belongs to the object) or 0 (if it belongs to the background).
2. Lightweight Inference (Nano & Downscaling)
Running deep learning segmentation models is computationally expensive, especially on edge hardware. To achieve a high frame rate, we apply two optimization techniques:
- Nano Model Scale: We load
yolov26n-seg.pt(Nano variant). It is trained on the 80-class COCO dataset but uses fewer layers and channels compared to larger models (likeyolov26m-segoryolov26x-seg), making it run fast even on CPU. - Image Downscaling (
imgsz=320): By default, YOLO models process images at 640 × 640 pixels. By settingimgsz=320, we reduce the total pixels the neural network has to compute by 75%, which dramatically increases inference speed (FPS).
3. Obstacle Size Calculation
Because we have a pixel mask, we can calculate how much space an object occupies in BonicBot’s camera view:
Percentage Area = (Pixels in Mask ÷ Total Pixels in Mask Grid) × 100If this percentage exceeds a threshold (e.g., 12%), the object is close enough to be considered a physical obstacle in front of the robot.
Student Challenge
Modify the program to change the threshold and make BonicBot react differently depending on which object is blocking the path.
For example:
- If a person blocks the path with more than 15% area: Make BonicBot stop, look up, and say “Excuse me, could you step aside?”
- If a bottle blocks the path with more than 10% area: Make BonicBot activate its grippers to prepare to pick it up.
Hint
Inside the Alert Decision Pipeline, check the object’s class and write conditional statements to call different BonicBot actions:
if det["class"] == "person" and det["area_pct"] > 15.0:
bot.stop()
bot.look_up() # look up towards the person
speaker.speak("Excuse me, could you step aside?")
elif det["class"] == "bottle" and det["area_pct"] > 10.0:
bot.stop()
bot.open_grippers() # prepare to grab the bottleRemember to import any exception handlers (like BonicBotError) to protect your code from runtime issues!
Reflection Question
Why is calculating the percentage area of a segmentation mask a more reliable way to estimate how close an object is to the robot compared to just using a bounding box width? (Hint: Think about what happens if a long, thin object like a wire or stick is placed diagonally across the frame.)