Skip to main content
The drawing system renders detection results on video frames using multiple visualization layers including segmentation masks, bounding boxes, class labels, and object tracking paths.

Drawing Architecture

The Drawing class in drawing/main.py:86 orchestrates three specialized drawer components:
class Drawing(DrawingInterface):
    def __init__(self):
        self.mask_drawer: MaskDrawerInterface = MaskDrawer()
        self.bbox_drawer: BoundingBoxDrawerInterface = BoundingBoxDrawer()
        self.track_drawer: TrackDrawerInterface = TrackDrawer()

Main Drawing Pipeline

The complete drawing process in drawing/main.py:92:
1

Extract Detection Data

Parse detection results from the segmentation model:
masks = trash_track.masks.xy          # Mask polygons
boxes = trash_track.boxes.xyxy.cpu()  # Bounding boxes [x1,y1,x2,y2]
tracks_ids = trash_track.boxes.id.int().cpu().tolist()  # Track IDs
clss = trash_track.boxes.cls.cpu().tolist()  # Class indices
2

Draw Masks

Apply segmentation masks as colored overlays:
image = self.mask_drawer.draw(image, masks, clss)
3

Draw Bounding Boxes

Add boxes with class labels:
image = self.bbox_drawer.draw(image, boxes, trash_classes, clss)
4

Draw Tracking Paths

Visualize object movement over time:
image = self.track_drawer.draw(image, tracks_ids, boxes)
return image

Mask Drawing

The MaskDrawer class in drawing/main.py:22 renders segmentation masks with transparency:
class MaskDrawer(MaskDrawerInterface):
    def __init__(self):
        self.color_map: Dict[int, tuple] = {
            0: (255, 0, 0),      # Cardboard/Paper - Blue
            1: (255, 255, 0),    # Metal - Cyan
            2: (200, 200, 200)   # Plastic - Light Gray
        }

Mask Rendering Algorithm

The drawing process in drawing/main.py:26:
def draw(self, image: np.ndarray, masks: List[np.ndarray], classes: List[int]) -> np.ndarray:
    overlay = image.copy()
    
    # Draw each mask
    for mask, cls in zip(masks, classes):
        color = self.color_map.get(cls, (255, 255, 255))  # Default white
        mask_polygon = np.int32([mask])
        
        # Fill mask area
        cv2.fillPoly(overlay, mask_polygon, color)
        
        # Draw mask outline
        cv2.polylines(image, mask_polygon, isClosed=True, color=color, thickness=2)
    
    # Blend overlay with original
    alpha = 0.5
    return cv2.addWeighted(overlay, alpha, image, 1 - alpha, 0)

Mask Color Coding

Color scheme based on trash material types:
ClassMaterialBGR ColorHexVisual
0Cardboard/Paper(255, 0, 0)#0000FFBlue
1Metal(255, 255, 0)#00FFFFCyan
2Plastic(200, 200, 200)#C8C8C8Light Gray
Color FormatOpenCV uses BGR (Blue, Green, Red) format, not RGB. When defining colors:
  • (255, 0, 0) = Blue
  • (0, 255, 0) = Green
  • (0, 0, 255) = Red

Bounding Box Rendering

The BoundingBoxDrawer class in drawing/main.py:43 uses Ultralytics’ Annotator:
class BoundingBoxDrawer(BoundingBoxDrawerInterface):
    def __init__(self):
        self.thickness: int = 2
    
    def draw(self, image: np.ndarray, boxes: np.ndarray, 
             trash_classes: Dict[int, str], classes: List[int]) -> np.ndarray:
        annotator = Annotator(image, self.thickness, trash_classes)
        
        for box, cls in zip(boxes, classes):
            annotator.box_label(box, trash_classes[cls], color=colors(cls, True))
        
        return annotator.result()

Box Format

Bounding boxes use xyxy format:
box = [x1, y1, x2, y2]  # Top-left and bottom-right corners
# Example box
box = [100, 150, 300, 400]

x1, y1 = box[0], box[1]  # Top-left: (100, 150)
x2, y2 = box[2], box[3]  # Bottom-right: (300, 400)

width = x2 - x1   # 200 pixels
height = y2 - y1  # 250 pixels

Label Customization

The trash classes dictionary maps indices to names:
trash_classes = {
    0: 'cardboard and paper',
    1: 'metal', 
    2: 'plastic'
}
Labels appear above bounding boxes with colored backgrounds matching the box color.

Track Visualization

The TrackDrawer class in drawing/main.py:60 maintains object movement history:
from collections import defaultdict

class TrackDrawer(TrackDrawerInterface):
    def __init__(self):
        self.track_history = defaultdict(list)
        self.thickness: int = 2

Tracking Algorithm

The implementation in drawing/main.py:65 stores and renders centroid paths:
1

Calculate Centroid

Find the center point of each bounding box:
for track_id, box in zip(tracks_ids, boxes):
    centroid = (
        float((box[0] + box[2]) / 2),  # X center
        float((box[1] + box[3]) / 2)   # Y center
    )
2

Update History

Append centroid to the object’s track history:
track_line = self.track_history[track_id]
track_line.append(centroid)

# Limit history to last 50 points
if len(track_line) > 50:
    track_line.pop(0)
3

Draw Path Lines

Connect consecutive points with colored lines:
for i in range(1, len(track_line)):
    cv2.line(
        image,
        tuple(map(int, track_line[i - 1])),  # Previous point
        tuple(map(int, track_line[i])),      # Current point
        colors(track_id, True),               # Color by track ID
        self.thickness
    )

Track Persistence

# Default: 50 frames
if len(track_line) > 50:
    track_line.pop(0)

# Custom length
MAX_HISTORY = 100
if len(track_line) > MAX_HISTORY:
    track_line.pop(0)

Color Coding and Overlays

Consistent Color Assignment

Colors are assigned by track ID or class ID using Ultralytics utilities:
from ultralytics.utils.plotting import colors

# Color by class
mask_color = colors(class_id, True)  # BGR format

# Color by track
track_color = colors(track_id, True)
The colors() function ensures consistent colors across all visualizations.

Overlay Transparency

Mask overlays use alpha blending for transparency:
alpha = 0.5  # 50% transparency
result = cv2.addWeighted(overlay, alpha, image, 1 - alpha, 0)
Default 50% transparency:
alpha = 0.5
cv2.addWeighted(overlay, 0.5, image, 0.5, 0)

Customization Options

Custom Drawing Class

Create a customized drawing pipeline:
import numpy as np
from typing import Dict
from trash_classificator.drawing.main import (
    MaskDrawer, BoundingBoxDrawer, TrackDrawer, DrawingInterface
)

class CustomDrawing(DrawingInterface):
    def __init__(self, mask_alpha=0.5, box_thickness=3, track_length=100):
        self.mask_drawer = MaskDrawer()
        self.bbox_drawer = BoundingBoxDrawer()
        self.bbox_drawer.thickness = box_thickness
        
        self.track_drawer = TrackDrawer()
        self.track_drawer.thickness = box_thickness
        
        self.mask_alpha = mask_alpha
        self.track_length = track_length
    
    def draw(self, image: np.ndarray, trash_track, 
             trash_classes: Dict[int, str], device):
        
        masks = trash_track.masks.xy
        boxes = trash_track.boxes.xyxy.cpu()
        tracks_ids = trash_track.boxes.id.int().cpu().tolist()
        clss = trash_track.boxes.cls.cpu().tolist()
        
        # Custom mask drawing with alpha
        image = self.draw_masks_custom(image, masks, clss)
        
        # Standard bounding boxes
        image = self.bbox_drawer.draw(image, boxes, trash_classes, clss)
        
        # Custom track length
        image = self.draw_tracks_custom(image, tracks_ids, boxes)
        
        return image
    
    def draw_masks_custom(self, image, masks, classes):
        overlay = image.copy()
        for mask, cls in zip(masks, classes):
            color = self.mask_drawer.color_map.get(cls, (255, 255, 255))
            mask_polygon = np.int32([mask])
            cv2.fillPoly(overlay, mask_polygon, color)
            cv2.polylines(image, mask_polygon, isClosed=True, 
                         color=color, thickness=2)
        return cv2.addWeighted(overlay, self.mask_alpha, image, 
                              1 - self.mask_alpha, 0)
    
    def draw_tracks_custom(self, image, tracks_ids, boxes):
        for track_id, box in zip(tracks_ids, boxes):
            track_line = self.track_drawer.track_history[track_id]
            centroid = (float((box[0] + box[2]) / 2), 
                       float((box[1] + box[3]) / 2))
            track_line.append(centroid)
            
            # Custom track length
            if len(track_line) > self.track_length:
                track_line.pop(0)
            
            for i in range(1, len(track_line)):
                cv2.line(image, tuple(map(int, track_line[i - 1])),
                        tuple(map(int, track_line[i])),
                        colors(track_id, True), self.track_drawer.thickness)
        return image

Usage Examples

from trash_classificator.drawing.main import Drawing

drawing = Drawing()
image_with_detections = drawing.draw(image, trash_track, trash_classes, device)

Performance Considerations

Drawing OverheadEach drawing layer adds processing time:
  • Masks: ~10-15ms (polygon filling and blending)
  • Boxes: ~2-5ms (simple rectangles and text)
  • Tracks: ~3-7ms (line drawing)
Total overhead: ~15-30ms per frame

Optimization Tips

  1. Disable unnecessary layers if you don’t need all visualizations
  2. Reduce track history length for faster rendering
  3. Use thinner lines to reduce drawing operations
  4. Skip frames for very high-speed requirements
# Process every 2nd frame for drawing
frame_count = 0
while cap.isOpened():
    ret, frame = cap.read()
    image, status = processor.frame_processing(frame)
    
    if frame_count % 2 == 0:
        # Draw only on even frames
        cv2.imshow("Frame", image)
    
    frame_count += 1

Next Steps

Classification Pipeline

Learn about the complete processing pipeline

Video Processing

Set up real-time video stream processing

Build docs developers (and LLMs) love