Saturday, November 07, 2026

AI OBJECT DETECTION, TRACKING AND SEGMENTATION: FROM HIGH-PERFORMANCE COMPUTERS TO MICROCONTROLLERS





INTRODUCTION TO COMPUTER VISION AT THE EDGE

The revolution in artificial intelligence has fundamentally transformed how machines perceive visual information. Object detection, tracking, and segmentation represent three pillars of modern computer vision, enabling systems to identify objects, follow them through time, and understand their precise spatial boundaries. What makes this particularly exciting in the current technological landscape is the democratization of these capabilities across an unprecedented range of hardware platforms.

This guide provides practical implementation details for building computer vision systems across three distinct hardware tiers. We examine high- performance desktop and server computers equipped with dedicated graphics processing units where computational resources enable complex models to process high-resolution video in real time. We then progress to IoT edge devices such as the NVIDIA Jetson Nano and Raspberry Pi, which balance computational capability with power efficiency and form factor constraints. Finally, we explore the frontier of truly embedded AI on microcontrollers like the ESP32, where extreme resource constraints demand innovative optimization techniques.

Throughout this guide, we emphasize practical implementation with working code examples, real-world deployment considerations, and engineering trade-offs inherent in adapting computer vision systems to different hardware platforms.


PART ONE: OBJECT DETECTION ON DESKTOP COMPUTERS

Understanding YOLO Architecture

Object detection solves the dual challenge of classification and localization. The breakthrough came with single-stage detectors, particularly the YOLO family. YOLO treats object detection as a single regression problem, dividing the input image into a grid where each cell predicts bounding boxes and class probabilities directly.

The network processes the entire image through convolutional layers that extract features at multiple scales. Early layers detect low-level features like edges, while deeper layers identify complex patterns. Each predicted bounding box includes center coordinates, dimensions, and a confidence score. Non-maximum suppression eliminates redundant detections by sorting predictions by confidence and removing overlapping boxes.


Building Your First YOLO Detection System

Let us implement a complete detection pipeline using Python and the Ultralytics YOLO library. First, install the necessary dependencies:


pip install ultralytics opencv-python numpy


Now create the basic detection system:


import cv2

import numpy as np

from ultralytics import YOLO


# Load pre-trained YOLOv11 nano model trained on COCO dataset

# Contains 80 object categories including person, car, dog, etc.

model = YOLO('yolo11n.pt')


# Configure detection parameters

# Confidence threshold: minimum probability to consider detection valid

# IOU threshold: controls non-maximum suppression aggressiveness

confidence_threshold = 0.5

iou_threshold = 0.45


The model weights download automatically on first use. The nano variant balances speed and accuracy, processing images quickly even on CPU. Other variants include small, medium, large, and extra-large models trading inference speed for improved accuracy.


For single image detection, implement the following function:


def detect_objects_in_image(image_path, model, conf_threshold=0.5):

    """

    Perform object detection on a single image and return results.

    

    Arguments:

        image_path: File path to input image

        model: Loaded YOLO model instance  

        conf_threshold: Minimum confidence for valid detections

        

    Returns:

        annotated_image: Image with detection boxes drawn

        detections: List of detection dictionaries

    """

    # Load image using OpenCV BGR format

    image = cv2.imread(image_path)

    

    if image is None:

        raise ValueError(f"Cannot read image from {image_path}")

    

    # Execute inference with confidence filtering

    results = model(image, conf=conf_threshold, verbose=False)

    

    # Extract first result (single image)

    result = results[0]

    

    # Get annotated image with boxes and labels

    annotated_image = result.plot()

    

    # Parse detection information

    boxes = result.boxes

    detections = []

    

    for box in boxes:

        # Extract bounding box coordinates in xyxy format

        x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()

        

        # Get confidence score and class information

        confidence = float(box.conf[0])

        class_id = int(box.cls[0])

        class_name = model.names[class_id]

        

        # Store structured detection information

        detection_info = {

            'bbox': [int(x1), int(y1), int(x2), int(y2)],

            'confidence': confidence,

            'class_id': class_id,

            'class_name': class_name,

            'center_x': int((x1 + x2) / 2),

            'center_y': int((y1 + y2) / 2),

            'width': int(x2 - x1),

            'height': int(y2 - y1)

        }

        detections.append(detection_info)

    

    return annotated_image, detections


Use this function to process images and display results:


# Load and process image

image_path = 'test_image.jpg'

annotated, detections = detect_objects_in_image(image_path, model)


# Print detection results

print(f"Found {len(detections)} objects:")

for det in detections:

    print(f"  {det['class_name']}: {det['confidence']:.2f}")


# Display annotated image

cv2.imshow('Detections', annotated)

cv2.waitKey(0)

cv2.destroyAllWindows()


Real-Time Video Detection

Video detection requires continuous frame capture and processing. Implement real-time detection with frame rate monitoring:


def detect_objects_in_video(video_path, model, conf_threshold=0.5):

    """

    Perform real-time object detection on video stream.

    

    Arguments:

        video_path: Path to video file or camera index (0 for webcam)

        model: Loaded YOLO model

        conf_threshold: Minimum detection confidence

    """

    import time

    

    # Open video source (use 0 for default webcam)

    cap = cv2.VideoCapture(video_path)

    

    if not cap.isOpened():

        raise ValueError(f"Cannot open video source: {video_path}")

    

    # Get video properties

    fps = int(cap.get(cv2.CAP_PROP_FPS))

    frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))

    frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

    

    print(f"Video: {frame_width}x{frame_height} at {fps} FPS")

    

    frame_count = 0

    start_time = time.time()

    

    while True:

        ret, frame = cap.read()

        

        if not ret:

            print("End of video or read error")

            break

        

        frame_count += 1

        

        # Run detection on current frame

        results = model(frame, conf=conf_threshold, verbose=False)

        annotated_frame = results[0].plot()

        

        # Calculate and display performance metrics

        elapsed = time.time() - start_time

        current_fps = frame_count / elapsed if elapsed > 0 else 0

        

        # Add performance information to frame

        info_text = f"Frame: {frame_count} | FPS: {current_fps:.1f}"

        cv2.putText(annotated_frame, info_text,

                   (10, 30), cv2.FONT_HERSHEY_SIMPLEX,

                   0.7, (0, 255, 0), 2)

        

        # Display result

        cv2.imshow('YOLO Real-Time Detection', annotated_frame)

        

        # Exit on 'q' key

        if cv2.waitKey(1) & 0xFF == ord('q'):

            break

    

    # Cleanup and report statistics

    cap.release()

    cv2.destroyAllWindows()

    

    total_time = time.time() - start_time

    avg_fps = frame_count / total_time

    print(f"Processed {frame_count} frames in {total_time:.2f}s")

    print(f"Average FPS: {avg_fps:.2f}")


This implementation provides frame rate monitoring and graceful resource cleanup. On modern desktop GPUs, the nano model typically achieves over 100 FPS at 640x640 resolution. The medium model balances at 50-70 FPS, while the extra-large model may drop to 20-30 FPS with superior accuracy.


PART TWO: OBJECT TRACKING SYSTEMS

Understanding Multi-Object Tracking

While detection identifies objects frame-by-frame, tracking maintains identities across time. This temporal continuity enables understanding object behavior, counting unique entities, and analyzing trajectories. Multi-object tracking faces challenges including occlusions, appearance changes, and crowded scenes where objects interact closely.

The ByteTrack algorithm represents current state-of-the-art tracking. Its key innovation lies in utilizing both high and low confidence detections. Traditional trackers discard low-confidence detections entirely, losing objects under partial occlusion or motion blur. ByteTrack recovers these objects by associating low-confidence detections with existing tracklets after high-confidence matching.


Implementing ByteTrack with YOLO

The Ultralytics library provides integrated tracking capabilities. Enable tracking with minimal code changes:


def track_objects_in_video(video_path, model_path='yolo11n.pt'):

    """

    Multi-object tracking with persistent IDs and trajectory visualization.

    

    Arguments:

        video_path: Video file path or camera index

        model_path: YOLO model weights file

    """

    model = YOLO(model_path)

    cap = cv2.VideoCapture(video_path)

    

    if not cap.isOpened():

        raise ValueError(f"Cannot open video: {video_path}")

    

    # Store trajectory history for each tracked object

    # Key: track_id, Value: list of (x, y) center points

    track_history = {}

    track_colors = {}  # Consistent colors per track

    

    frame_count = 0

    

    while True:

        ret, frame = cap.read()

        if not ret:

            break

        

        frame_count += 1

        

        # Run tracking (persist=True maintains IDs across frames)

        results = model.track(frame, persist=True, verbose=False)

        

        # Check if tracking results exist

        if results[0].boxes is None or results[0].boxes.id is None:

            cv2.imshow('Tracking', frame)

            if cv2.waitKey(1) & 0xFF == ord('q'):

                break

            continue

        

        # Extract tracking information

        boxes = results[0].boxes.xyxy.cpu().numpy()

        track_ids = results[0].boxes.id.cpu().numpy().astype(int)

        class_ids = results[0].boxes.cls.cpu().numpy().astype(int)

        

        # Get base annotated frame

        annotated_frame = results[0].plot()

        

        # Process each tracked object

        for box, track_id, cls_id in zip(boxes, track_ids, class_ids):

            x1, y1, x2, y2 = box

            

            # Calculate bounding box center

            center_x = int((x1 + x2) / 2)

            center_y = int((y1 + y2) / 2)

            

            # Initialize track history if new track

            if track_id not in track_history:

                track_history[track_id] = []

                # Assign random color for this track

                track_colors[track_id] = tuple(

                    np.random.randint(0, 255, 3).tolist()

                )

            

            # Append current position

            track_history[track_id].append((center_x, center_y))

            

            # Maintain last 50 points

            if len(track_history[track_id]) > 50:

                track_history[track_id].pop(0)

            

            # Draw trajectory trail

            points = track_history[track_id]

            color = track_colors[track_id]

            

            for i in range(1, len(points)):

                # Progressive thickness for recent points

                thickness = max(1, int(3 * (i / len(points))))

                cv2.line(annotated_frame, points[i-1], points[i],

                        color, thickness)

            

            # Draw track ID label

            label = f"ID: {track_id}"

            cv2.putText(annotated_frame, label,

                       (int(x1), int(y1) - 10),

                       cv2.FONT_HERSHEY_SIMPLEX, 0.6,

                       color, 2)

        

        # Add statistics overlay

        active_tracks = len([t for t in track_history 

                            if len(track_history[t]) > 0])

        stats_text = f"Frame: {frame_count} | Active Tracks: {active_tracks}"

        cv2.putText(annotated_frame, stats_text,

                   (10, 30), cv2.FONT_HERSHEY_SIMPLEX,

                   0.8, (255, 255, 255), 2)

        

        cv2.imshow('Multi-Object Tracking', annotated_frame)

        

        if cv2.waitKey(1) & 0xFF == ord('q'):

            break

    

    cap.release()

    cv2.destroyAllWindows()

    print(f"Tracked across {frame_count} frames")

    print(f"Total unique tracks: {len(track_history)}")


This implementation maintains consistent object identities, visualizes trajectories with colored trails, and provides real-time statistics. The trajectory visualization helps analyze movement patterns and understand scene dynamics.


Custom Tracking with Kalman Filters

ByteTrack internally uses Kalman filtering for motion prediction. Understanding this mechanism provides deeper insight into tracking robustness. The Kalman filter combines motion model predictions with actual measurements to produce optimal state estimates, handling temporary occlusions gracefully.

Here is a simplified Kalman tracker implementation:


import numpy as np


class SimpleKalmanTracker:

    """

    Basic Kalman filter tracker for bounding boxes.

    State: [x_center, y_center, width, height,

            vx_center, vy_center, vwidth, vheight]

    """

    

    def __init__(self, initial_bbox, track_id):

        """

        Initialize tracker with bounding box.

        

        Arguments:

            initial_bbox: Initial box [x1, y1, x2, y2]

            track_id: Unique track identifier

        """

        self.track_id = track_id

        self.age = 0

        self.hits = 0

        self.time_since_update = 0

        

        # Convert bbox to center format

        x1, y1, x2, y2 = initial_bbox

        cx = (x1 + x2) / 2

        cy = (y1 + y2) / 2

        w = x2 - x1

        h = y2 - y1

        

        # State vector: position and velocity

        # [cx, cy, w, h, vcx, vcy, vw, vh]

        self.state = np.array([cx, cy, w, h, 0, 0, 0, 0],

                             dtype=np.float32)

        

        # Measurement matrix (we only observe position)

        self.measurement_matrix = np.array([

            [1, 0, 0, 0, 0, 0, 0, 0],

            [0, 1, 0, 0, 0, 0, 0, 0],

            [0, 0, 1, 0, 0, 0, 0, 0],

            [0, 0, 0, 1, 0, 0, 0, 0]

        ], dtype=np.float32)

        

        # State transition matrix (constant velocity model)

        dt = 1.0  # Time delta (one frame)

        self.transition_matrix = np.array([

            [1, 0, 0, 0, dt, 0, 0, 0],

            [0, 1, 0, 0, 0, dt, 0, 0],

            [0, 0, 1, 0, 0, 0, dt, 0],

            [0, 0, 0, 1, 0, 0, 0, dt],

            [0, 0, 0, 0, 1, 0, 0, 0],

            [0, 0, 0, 0, 0, 1, 0, 0],

            [0, 0, 0, 0, 0, 0, 1, 0],

            [0, 0, 0, 0, 0, 0, 0, 1]

        ], dtype=np.float32)

        

        # Process noise covariance

        self.process_noise = np.eye(8, dtype=np.float32) * 0.01

        

        # Measurement noise covariance

        self.measurement_noise = np.eye(4, dtype=np.float32) * 1.0

        

        # Error covariance matrix

        self.error_covariance = np.eye(8, dtype=np.float32) * 10.0

    

    def predict(self):

        """

        Predict next state using motion model.

        

        Returns:

            predicted_bbox: Predicted box [x1, y1, x2, y2]

        """

        # Predict state: x_pred = F * x

        self.state = self.transition_matrix @ self.state

        

        # Predict error covariance: P_pred = F * P * F^T + Q

        self.error_covariance = (

            self.transition_matrix @ 

            self.error_covariance @ 

            self.transition_matrix.T + 

            self.process_noise

        )

        

        self.age += 1

        self.time_since_update += 1

        

        # Convert state to bbox format

        cx, cy, w, h = self.state[:4]

        x1 = cx - w / 2

        y1 = cy - h / 2

        x2 = cx + w / 2

        y2 = cy + h / 2

        

        return np.array([x1, y1, x2, y2])

    

    def update(self, measurement_bbox):

        """

        Update state with new measurement.

        

        Arguments:

            measurement_bbox: Observed box [x1, y1, x2, y2]

        """

        # Convert measurement to center format

        x1, y1, x2, y2 = measurement_bbox

        cx = (x1 + x2) / 2

        cy = (y1 + y2) / 2

        w = x2 - x1

        h = y2 - y1

        measurement = np.array([cx, cy, w, h], dtype=np.float32)

        

        # Calculate Kalman gain: K = P * H^T * (H * P * H^T + R)^-1

        innovation_covariance = (

            self.measurement_matrix @ 

            self.error_covariance @ 

            self.measurement_matrix.T + 

            self.measurement_noise

        )

        

        kalman_gain = (

            self.error_covariance @ 

            self.measurement_matrix.T @ 

            np.linalg.inv(innovation_covariance)

        )

        

        # Update state: x = x_pred + K * (z - H * x_pred)

        predicted_measurement = (

            self.measurement_matrix @ self.state

        )

        innovation = measurement - predicted_measurement

        self.state = self.state + kalman_gain @ innovation

        

        # Update error covariance: P = (I - K * H) * P

        identity = np.eye(8, dtype=np.float32)

        self.error_covariance = (

            (identity - kalman_gain @ self.measurement_matrix) @ 

            self.error_covariance

        )

        

        self.hits += 1

        self.time_since_update = 0

    

    def get_current_bbox(self):

        """

        Get current bounding box estimate.

        

        Returns:

            bbox: Current box [x1, y1, x2, y2]

        """

        cx, cy, w, h = self.state[:4]

        x1 = cx - w / 2

        y1 = cy - h / 2

        x2 = cx + w / 2

        y2 = cy + h / 2

        return np.array([x1, y1, x2, y2])


This Kalman filter implementation predicts object positions between frames and updates estimates when measurements arrive. The constant velocity motion model assumes objects move smoothly, making predictions during brief occlusions. The Kalman gain balances trust between predictions and measurements based on their respective uncertainties.


PART THREE: IMAGE SEGMENTATION WITH MASK R-CNN

Understanding Instance Segmentation

Segmentation extends detection by providing pixel-level masks defining exact object boundaries. Instance segmentation treats each object as a distinct entity, producing separate masks even for objects of the same class. This granularity enables precise spatial reasoning required for applications like robotic manipulation, medical image analysis, and augmented reality.

Mask R-CNN extends the Faster R-CNN object detector by adding a mask prediction branch. The architecture has two stages. First, a region proposal network suggests candidate object locations. Second, these regions are refined through classification, bounding box regression, and mask generation occurring in parallel branches.

The critical innovation is ROI Align, which replaces the coarse ROI pooling used in Faster R-CNN. ROI Align uses bilinear interpolation to compute feature values at non-integer locations, preserving spatial alignment and enabling accurate mask prediction. This careful spatial handling proves essential for pixel-level accuracy.


Implementing Mask R-CNN Detection

Install the required libraries for segmentation:


pip install torch torchvision

# For visualization

pip install matplotlib pillow


Load a pre-trained Mask R-CNN model:


import torch

import torchvision

from torchvision import transforms

from PIL import Image

import matplotlib.pyplot as plt

import numpy as np


def load_maskrcnn_model(device='cuda' if torch.cuda.is_available() else 'cpu'):

    """

    Load pre-trained Mask R-CNN model.

    

    Arguments:

        device: Computation device ('cuda' or 'cpu')

        

    Returns:

        model: Loaded Mask R-CNN model in evaluation mode

        device: Device where model is loaded

    """

    # Load pre-trained model trained on COCO dataset

    model = torchvision.models.detection.maskrcnn_resnet50_fpn(

        weights='DEFAULT'

    )

    

    model.to(device)

    model.eval()  # Set to evaluation mode

    

    print(f"Model loaded on {device}")

    return model, device



Perform instance segmentation on images:


def segment_image(image_path, model, device, conf_threshold=0.5):

    """

    Perform instance segmentation on image.

    

    Arguments:

        image_path: Path to input image

        model: Loaded Mask R-CNN model

        device: Computation device

        conf_threshold: Minimum confidence for detections

        

    Returns:

        image: Original image as numpy array

        results: Dictionary with boxes, labels, scores, and masks

    """

    # Load and preprocess image

    image = Image.open(image_path).convert('RGB')

    image_tensor = transforms.ToTensor()(image)

    image_tensor = image_tensor.to(device)

    

    # Run inference

    with torch.no_grad():

        predictions = model([image_tensor])[0]

    

    # Filter by confidence threshold

    scores = predictions['scores'].cpu().numpy()

    valid_indices = scores >= conf_threshold

    

    # Extract results

    boxes = predictions['boxes'].cpu().numpy()[valid_indices]

    labels = predictions['labels'].cpu().numpy()[valid_indices]

    scores = scores[valid_indices]

    masks = predictions['masks'].cpu().numpy()[valid_indices]

    

    # Convert masks to binary (threshold at 0.5)

    masks = masks > 0.5

    

    results = {

        'boxes': boxes,

        'labels': labels,

        'scores': scores,

        'masks': masks

    }

    

    # Convert image to numpy for visualization

    image_np = np.array(image)

    

    return image_np, results


Visualize segmentation results:


def visualize_segmentation(image, results, class_names=None):

    """

    Visualize instance segmentation results.

    

    Arguments:

        image: Original image as numpy array

        results: Segmentation results dictionary

        class_names: Optional list of class names

    """

    import matplotlib.patches as mpatches

    from matplotlib.patches import Rectangle

    

    fig, ax = plt.subplots(1, figsize=(12, 9))

    ax.imshow(image)

    

    # COCO class names (simplified)

    if class_names is None:

        class_names = [

            '__background__', 'person', 'bicycle', 'car', 'motorcycle',

            'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light',

            'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird',

            'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear',

            'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie',

            'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball',

            'kite', 'baseball bat', 'baseball glove', 'skateboard',

            'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup',

            'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich',

            'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut',

            'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table',

            'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard',

            'cell phone', 'microwave', 'oven', 'toaster', 'sink',

            'refrigerator', 'book', 'clock', 'vase', 'scissors',

            'teddy bear', 'hair drier', 'toothbrush'

        ]

    

    # Generate colors for each instance

    np.random.seed(42)

    colors = np.random.rand(len(results['boxes']), 3)

    

    # Overlay each instance

    for idx, (box, label, score, mask) in enumerate(zip(

        results['boxes'], results['labels'],

        results['scores'], results['masks']

    )):

        color = colors[idx]

        

        # Draw mask with transparency

        mask_colored = np.zeros_like(image, dtype=np.float32)

        for c in range(3):

            mask_colored[:, :, c] = mask[0] * color[c]

        

        ax.imshow(mask_colored, alpha=0.5)

        

        # Draw bounding box

        x1, y1, x2, y2 = box

        rect = Rectangle((x1, y1), x2-x1, y2-y1,

                       linewidth=2, edgecolor=color,

                       facecolor='none')

        ax.add_patch(rect)

        

        # Add label

        class_name = class_names[label] if label < len(class_names) else 'unknown'

        label_text = f'{class_name}: {score:.2f}'

        ax.text(x1, y1 - 5, label_text,

               bbox=dict(facecolor=color, alpha=0.5),

               fontsize=10, color='white')

    

    ax.axis('off')

    plt.tight_layout()

    plt.show()


Use these functions together:


# Load model

model, device = load_maskrcnn_model()


# Segment image

image, results = segment_image('test_image.jpg', model, device)


# Visualize results

print(f"Found {len(results['boxes'])} instances")

visualize_segmentation(image, results)


This implementation provides complete instance segmentation with mask overlay visualization. The model runs efficiently on GPUs, processing images in under 100 milliseconds. CPU inference takes several seconds but remains viable for non-real- time applications.


PART FOUR: DEPLOYING ON IOT EDGE DEVICES

Understanding Edge Computing Constraints

Edge devices like NVIDIA Jetson Nano and Raspberry Pi occupy the middle ground between powerful desktop systems and resource-constrained microcontrollers. These platforms provide dedicated hardware acceleration for neural networks while maintaining compact form factors and power efficiency suitable for embedded deployment.

The Jetson Nano features a Maxwell GPU with 128 CUDA cores providing 472 GFLOPS of compute power. It includes 4GB RAM and supports TensorRT for optimized inference. The Raspberry Pi 4 offers a quad-core ARM processor with optional neural accelerators like the Coral Edge TPU or Intel Neural Compute Stick. These devices enable real-time computer vision in applications including robotics, surveillance, and industrial automation.

Model optimization becomes critical for edge deployment. Techniques include quantization reducing precision from 32-bit floating point to 16-bit or 8-bit integers, pruning removing redundant network parameters, and knowledge distillation training smaller models to mimic larger ones. TensorRT applies these optimizations automatically, often achieving 2-5x speedup on Jetson devices.


Setting Up YOLO on Jetson Nano

Install JetPack SDK following NVIDIA instructions, then set up the Python environment:


# Install prerequisites

sudo apt-get update

sudo apt-get install -y python3-pip python3-dev


# Install PyTorch for Jetson (ARM64)

wget https://nvidia.box.com/shared/static/[latest-jetson-pytorch-wheel].whl

pip3 install [pytorch-wheel-file]


# Install Ultralytics YOLO

pip3 install ultralytics


# Install additional dependencies

pip3 install opencv-python numpy


Deploy YOLOv11 optimized for Jetson:


from ultralytics import YOLO

import cv2

import time


def deploy_yolo_jetson(camera_id=0, model_size='n'):

    """

    Deploy YOLO on Jetson Nano with CSI camera.

    

    Arguments:

        camera_id: Camera device index

        model_size: Model size ('n', 's', 'm')

    """

    # Load model (smaller models for edge devices)

    model_name = f'yolo11{model_size}.pt'

    print(f"Loading {model_name}...")

    model = YOLO(model_name)

    

    # Open camera with GStreamer pipeline for CSI camera

    gstreamer_pipeline = (

        "nvarguscamerasrc sensor-id=%d ! "

        "video/x-raw(memory:NVMM), width=1280, height=720, "

        "format=NV12, framerate=30/1 ! "

        "nvvidconv ! video/x-raw, format=BGRx ! "

        "videoconvert ! video/x-raw, format=BGR ! "

        "appsink"

    ) % camera_id

    

    # Try CSI camera first, fall back to USB

    cap = cv2.VideoCapture(gstreamer_pipeline, cv2.CAP_GSTREAMER)

    if not cap.isOpened():

        print("CSI camera not available, using USB camera")

        cap = cv2.VideoCapture(camera_id)

    

    if not cap.isOpened():

        raise RuntimeError("Cannot open camera")

    

    print("Camera opened successfully")

    print("Press 'q' to quit")

    

    frame_times = []

    

    while True:

        ret, frame = cap.read()

        if not ret:

            break

        

        # Time inference

        start_time = time.time()

        results = model(frame, verbose=False)

        inference_time = time.time() - start_time

        

        frame_times.append(inference_time)

        if len(frame_times) > 30:

            frame_times.pop(0)

        

        # Calculate metrics

        avg_inference = np.mean(frame_times) * 1000  # Convert to ms

        fps = 1.0 / np.mean(frame_times)

        

        # Get annotated frame

        annotated = results[0].plot()

        

        # Add performance overlay

        perf_text = f"Inference: {avg_inference:.1f}ms | FPS: {fps:.1f}"

        cv2.putText(annotated, perf_text, (10, 30),

                   cv2.FONT_HERSHEY_SIMPLEX, 0.7,

                   (0, 255, 0), 2)

        

        cv2.imshow('Jetson YOLO Detection', annotated)

        

        if cv2.waitKey(1) & 0xFF == ord('q'):

            break

    

    cap.release()

    cv2.destroyAllWindows()

    

    print(f"Average inference time: {np.mean(frame_times)*1000:.1f}ms")

    print(f"Average FPS: {1.0/np.mean(frame_times):.1f}")


Optimizing with TensorRT

TensorRT dramatically improves inference speed on Jetson. Export and optimize YOLO:


def export_tensorrt_model(model_path='yolo11n.pt', workspace=4):

    """

    Export YOLO model to TensorRT format.

    

    Arguments:

        model_path: Path to YOLO model weights

        workspace: TensorRT workspace size in GB

    """

    from ultralytics import YOLO

    

    print(f"Exporting {model_path} to TensorRT...")

    model = YOLO(model_path)

    

    # Export to TensorRT with FP16 precision

    model.export(format='engine', 

                workspace=workspace,

                half=True)  # Use FP16 precision

    

    print("Export complete!")

    print(f"TensorRT engine saved")

```


Run inference with TensorRT engine:


```

def run_tensorrt_inference(engine_path, camera_id=0):

    """

    Run optimized TensorRT inference.

    

    Arguments:

        engine_path: Path to .engine file

        camera_id: Camera device index

    """

    model = YOLO(engine_path)

    cap = cv2.VideoCapture(camera_id)

    

    print("Running TensorRT optimized inference")

    

    while True:

        ret, frame = cap.read()

        if not ret:

            break

        

        # TensorRT automatically accelerates inference

        results = model(frame, verbose=False)

        annotated = results[0].plot()

        

        cv2.imshow('TensorRT Inference', annotated)

        

        if cv2.waitKey(1) & 0xFF == ord('q'):

            break

    

    cap.release()

    cv2.destroyAllWindows()


TensorRT optimization typically doubles inference speed on Jetson Nano. The nano model achieves 15-20 FPS, while the small model runs at 8-12 FPS with improved accuracy.


Raspberry Pi Deployment

Raspberry Pi requires different optimization strategies due to its ARM CPU architecture:


# Install dependencies on Raspberry Pi

sudo apt-get update

sudo apt-get install -y python3-pip python3-opencv

pip3 install ultralytics numpy


Deploy lightweight model on Raspberry Pi:


def deploy_raspberry_pi(camera_id=0):

    """

    Deploy YOLO on Raspberry Pi with optimizations.

    """

    from ultralytics import YOLO

    import cv2

    

    # Use smallest model for Raspberry Pi

    model = YOLO('yolo11n.pt')

    

    # Reduce input resolution for faster processing

    model.overrides['imgsz'] = 320  # Reduced from default 640

    

    cap = cv2.VideoCapture(camera_id)

    cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)

    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

    

    print("Raspberry Pi detection starting...")

    

    while True:

        ret, frame = cap.read()

        if not ret:

            break

        

        # Process every other frame to improve framerate

        results = model(frame, verbose=False)

        annotated = results[0].plot()

        

        cv2.imshow('Pi Detection', annotated)

        

        if cv2.waitKey(1) & 0xFF == ord('q'):

            break

    

    cap.release()

    cv2.destroyAllWindows()


On Raspberry Pi 4, expect 2-4 FPS with the nano model at reduced resolution. The Raspberry Pi 5 performs a bit better. Adding a Coral Edge TPU accelerator increases performance to 10-15 FPS with compatible models.


PART FIVE: MICROCONTROLLER DEPLOYMENT WITH TENSORFLOW LITE MICRO

Understanding Microcontroller Constraints

Microcontrollers like the ESP32 present extreme resource constraints. With typical specifications of 520KB SRAM, 4MB flash storage, and a 240MHz dual-core processor, running neural networks requires aggressive optimization. TensorFlow Lite Micro enables this deployment by providing a minimal runtime optimized for embedded systems.

The key to successful microcontroller deployment lies in model quantization and architecture selection. Full 32-bit floating point models are impractical. Instead, we employ 8-bit integer quantization reducing model size by 75 percent while typically sacrificing less than 1 percent accuracy. Model architectures must be carefully designed with limited layers and parameters.

Common microcontroller computer vision applications include person detection, gesture recognition, and simple object classification. Real-time video processing remains challenging, with typical inference times measured in hundreds of milliseconds to seconds per frame. However, for many IoT applications where continuous monitoring suffices, this performance proves adequate.


Setting Up TensorFlow Lite Micro on ESP32

Install the Arduino IDE and ESP32 board support:


# In Arduino IDE:

# File -> Preferences -> Additional Board Manager URLs

# Add: https://dl.espressif.com/dl/package_esp32_index.json


# Tools -> Board -> Boards Manager

# Search and install "ESP32"


Install TensorFlow Lite Micro library:


# In Arduino IDE:

# Sketch -> Include Library -> Manage Libraries

# Search and install "TensorFlowLite_ESP32"


Simple Person Detection Example

Let‘s create a complete person detection system for ESP32-CAM:


#include <esp_camera.h>

#include <TensorFlowLite_ESP32.h>

#include "person_detect_model_data.h"


// Include TensorFlow Lite Micro headers

#include "tensorflow/lite/micro/all_ops_resolver.h"

#include "tensorflow/lite/micro/micro_error_reporter.h"

#include "tensorflow/lite/micro/micro_interpreter.h"

#include "tensorflow/lite/schema/schema_generated.h"

#include "tensorflow/lite/version.h"


// Model input dimensions

const int kImageWidth = 96;

const int kImageHeight = 96;

const int kImageChannels = 1;  // Grayscale


// TensorFlow Lite objects

tflite::MicroErrorReporter micro_error_reporter;

tflite::ErrorReporter* error_reporter = &micro_error_reporter;


const tflite::Model* model = nullptr;

tflite::MicroInterpreter* interpreter = nullptr;

TfLiteTensor* input = nullptr;


// Memory allocation for model

// Increase if model is more complex

constexpr int kTensorArenaSize = 70 * 1024;

alignas(16) uint8_t tensor_arena[kTensorArenaSize];


// Camera pin configuration for ESP32-CAM (AI-Thinker)

camera_config_t camera_config = {

    .pin_pwdn = 32,

    .pin_reset = -1,

    .pin_xclk = 0,

    .pin_sscb_sda = 26,

    .pin_sscb_scl = 27,

    .pin_d7 = 35,

    .pin_d6 = 34,

    .pin_d5 = 39,

    .pin_d4 = 36,

    .pin_d3 = 21,

    .pin_d2 = 19,

    .pin_d1 = 18,

    .pin_d0 = 5,

    .pin_vsync = 25,

    .pin_href = 23,

    .pin_pclk = 22,

    .xclk_freq_hz = 20000000,

    .ledc_timer = LEDC_TIMER_0,

    .ledc_channel = LEDC_CHANNEL_0,

    .pixel_format = PIXFORMAT_GRAYSCALE,

    .frame_size = FRAMESIZE_96X96,

    .jpeg_quality = 10,

    .fb_count = 1

};


void setup() {

    Serial.begin(115200);

    delay(1000);

    

    Serial.println("TensorFlow Lite Micro Person Detection");

    

    // Initialize camera

    esp_err_t err = esp_camera_init(&camera_config);

    if (err != ESP_OK) {

        Serial.printf("Camera init failed with error 0x%x\n", err);

        return;

    }

    Serial.println("Camera initialized");

    

    // Load the TensorFlow Lite model

    model = tflite::GetModel(g_person_detect_model_data);

    if (model->version() != TFLITE_SCHEMA_VERSION) {

        error_reporter->Report(

            "Model schema version %d not supported, version %d",

            model->version(), TFLITE_SCHEMA_VERSION);

        return;

    }

    Serial.println("Model loaded");

    

    // Create operations resolver

    static tflite::AllOpsResolver resolver;

    

    // Build interpreter

    static tflite::MicroInterpreter static_interpreter(

        model, resolver, tensor_arena, kTensorArenaSize,

        error_reporter);

    interpreter = &static_interpreter;

    

    // Allocate tensors

    TfLiteStatus allocate_status = interpreter->AllocateTensors();

    if (allocate_status != kTfLiteOk) {

        error_reporter->Report("AllocateTensors() failed");

        return;

    }

    Serial.println("Tensors allocated");

    

    // Get input tensor pointer

    input = interpreter->input(0);

    

    Serial.println("Setup complete. Starting inference...");

}


void loop() {

    // Capture image from camera

    camera_fb_t* fb = esp_camera_fb_get();

    if (!fb) {

        Serial.println("Camera capture failed");

        delay(1000);

        return;

    }

    

    // Verify image dimensions

    if (fb->width != kImageWidth || fb->height != kImageHeight) {

        Serial.printf("Image size mismatch: %dx%d\n", 

                     fb->width, fb->height);

        esp_camera_fb_return(fb);

        delay(1000);

        return;

    }

    

    // Copy image data to model input

    // Model uses quantized input (-128 to 127)

    for (int i = 0; i < kImageWidth * kImageHeight; i++) {

        // Convert unsigned byte to signed byte

        input->data.int8[i] = fb->buf[i] - 128;

    }

    

    // Return frame buffer to camera driver

    esp_camera_fb_return(fb);

    

    // Run inference

    unsigned long start_time = millis();

    TfLiteStatus invoke_status = interpreter->Invoke();

    unsigned long inference_time = millis() - start_time;

    

    if (invoke_status != kTfLiteOk) {

        error_reporter->Report("Invoke failed");

        delay(1000);

        return;

    }

    

    // Get output tensor (2 values: person score, no-person score)

    TfLiteTensor* output = interpreter->output(0);

    int8_t person_score = output->data.int8[1];

    int8_t no_person_score = output->data.int8[0];

    

    // Convert quantized scores to probabilities

    // Quantization zero point and scale depends on training

    float person_probability = (person_score + 128) / 256.0;

    float no_person_probability = (no_person_score + 128) / 256.0;

    

    // Print results

    Serial.printf("Inference time: %lu ms\n", inference_time);

    Serial.printf("Person: %.2f | No person: %.2f\n",

                 person_probability, no_person_probability);

    

    // Determine detection

    if (person_probability > 0.6) {

        Serial.println("PERSON DETECTED!");

    } else {

        Serial.println("No person");

    }

    

    Serial.println("---");

    

    // Wait before next inference

    delay(2000);

}


This implementation performs person detection at approximately 700-1000 milliseconds per inference on ESP32. The model input requires 96x96 grayscale images, dramatically reducing computational requirements compared to standard RGB inputs.


Optimizing Inference Performance

Several optimization strategies improve microcontroller performance:

Reduce input resolution. Smaller images require fewer computations. Consider 48x48 or even 32x32 for simple detection tasks.

Apply aggressive quantization. Post-training quantization converts trained models to 8-bit integer precision with minimal accuracy loss. Some models support 4-bit or even binary quantization for extreme optimization.

Simplify model architecture. Use depthwise separable convolutions instead of standard convolutions, reducing parameters by factors of 8-10. MobileNet-style architectures work well on microcontrollers.

Optimize tensor arena size. The tensor arena holds intermediate activation tensors. Finding the minimum sufficient size reduces memory pressure, allowing larger models or additional application code.

Here is an optimized configuration example:


// Optimized tensor arena sizing

// Start large, then reduce until allocation fails

constexpr int kTensorArenaSize = 45 * 1024;  // Reduced from 70KB


// Use quantized operations resolver for smaller code size

// Instead of AllOpsResolver, register only needed operations

static tflite::MicroMutableOpResolver<5> micro_op_resolver;

micro_op_resolver.AddConv2D();

micro_op_resolver.AddMaxPool2D();

micro_op_resolver.AddFullyConnected();

micro_op_resolver.AddReshape();

micro_op_resolver.AddSoftmax();



Custom Model Training for Microcontrollers

Training custom models for microcontrollers requires TensorFlow with quantization- aware training. Here is a simplified training pipeline:


import tensorflow as tf

import tensorflow_model_optimization as tfmot


def create_quantized_model(input_shape=(96, 96, 1), num_classes=2):

    """

    Create quantization-aware training model.

    

    Arguments:

        input_shape: Input image dimensions

        num_classes: Number of output classes

        

    Returns:

        model: Quantization-aware model

    """

    # Build simple CNN model

    model = tf.keras.Sequential([

        tf.keras.layers.Conv2D(16, (3, 3), activation='relu',

                              input_shape=input_shape),

        tf.keras.layers.MaxPooling2D((2, 2)),

        tf.keras.layers.Conv2D(32, (3, 3), activation='relu'),

        tf.keras.layers.MaxPooling2D((2, 2)),

        tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),

        tf.keras.layers.Flatten(),

        tf.keras.layers.Dense(64, activation='relu'),

        tf.keras.layers.Dense(num_classes, activation='softmax')

    ])

    

    # Apply quantization-aware training

    quantize_model = tfmot.quantization.keras.quantize_model

    q_aware_model = quantize_model(model)

    

    # Compile with appropriate optimizer

    q_aware_model.compile(

        optimizer='adam',

        loss='sparse_categorical_crossentropy',

        metrics=['accuracy']

    )

    

    return q_aware_model


def convert_to_tflite_micro(model, representative_data):

    """

    Convert trained model to TensorFlow Lite Micro format.

    

    Arguments:

        model: Trained Keras model

        representative_data: Dataset for calibration

        

    Returns:

        tflite_model: Quantized TFLite model

    """

    # Create converter

    converter = tf.lite.TFLiteConverter.from_keras_model(model)

    

    # Enable optimizations

    converter.optimizations = [tf.lite.Optimize.DEFAULT]

    

    # Provide representative dataset for full integer quantization

    def representative_dataset_gen():

        for data in representative_data.take(100):

            yield [data]

    

    converter.representative_dataset = representative_dataset_gen

    

    # Enforce full integer quantization

    converter.target_spec.supported_ops = [

        tf.lite.OpsSet.TFLITE_BUILTINS_INT8

    ]

    converter.inference_input_type = tf.int8

    converter.inference_output_type = tf.int8

    

    # Convert model

    tflite_model = converter.convert()

    

    return tflite_model


# Training example

# model = create_quantized_model()

# model.fit(train_data, epochs=20, validation_data=val_data)

# tflite_model = convert_to_tflite_micro(model, representative_data)


# Save model

# with open('model.tflite', 'wb') as f:

#     f.write(tflite_model)


# Convert to C array for embedding

# xxd -i model.tflite > model_data.cc


This training pipeline produces fully quantized models optimized for microcontroller deployment. The representative dataset calibrates quantization ranges, ensuring minimal accuracy degradation.


CONCLUSION AND FUTURE DIRECTIONS

This comprehensive guide has explored object detection, tracking, and segmentation across the full spectrum of computing hardware. We began with high-performance desktop systems running state-of-the-art models like YOLOv11 and Mask R-CNN, progressed through edge devices like Jetson Nano enabling efficient embedded deployment, and concluded with microcontrollers demonstrating that even severe resource constraints cannot prevent sophisticated computer vision capabilities.

The key takeaway is that modern AI democratizes computer vision. The same conceptual approaches scale from server-class GPUs processing 4K video at 60 FPS to microcontrollers analyzing single frames every few seconds. Success requires understanding the engineering trade-offs: model complexity versus inference speed, accuracy versus power consumption, generality versus application-specific optimization.

Future developments promise even greater capabilities. New model architectures continue improving the accuracy-efficiency frontier. Hardware accelerators specifically designed for neural networks proliferate across all tiers. Automated neural architecture search discovers optimal models for specific constraints. Federated learning enables privacy-preserving training on edge devices.

The practical implementations presented here provide solid foundations for real-world applications. Whether building autonomous systems, industrial automation, healthcare diagnostics, or consumer electronics, these techniques enable sophisticated visual understanding at every scale. The code examples demonstrate not just how to run inference but how to build complete systems handling data capture, processing, visualization, and result delivery.

Computer vision continues its rapid evolution. Staying current requires continuous learning, experimentation with new architectures, and practical deployment experience. The tools and techniques presented here serve as starting points for exploration and innovation. The future of AI at the edge has never been brighter, and the barriers to entry have never been lower.

Start with the examples provided, adapt them to your specific requirements, and build the next generation of intelligent systems. The capability to perceive and understand visual information now fits in the palm of your hand and costs less than a meal. Use it wisely, use it well, and use it to build a better future.