import cv2 import numpy as np class_names = [ "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", ] # Create a list of colors for each class where each color is a tuple of 3 integer values rng = np.random.default_rng(3) colors = rng.uniform(0, 255, size=(len(class_names), 3)) def nms(boxes, scores, iou_threshold): # Sort by score sorted_indices = np.argsort(scores)[::-1] keep_boxes = [] while sorted_indices.size > 0: # Pick the last box box_id = sorted_indices[0] keep_boxes.append(box_id) # Compute IoU of the picked box with the rest ious = compute_iou(boxes[box_id, :], boxes[sorted_indices[1:], :]) # Remove boxes with IoU over the threshold keep_indices = np.where(ious < iou_threshold)[0] # print(keep_indices.shape, sorted_indices.shape) sorted_indices = sorted_indices[keep_indices + 1] return keep_boxes def multiclass_nms(boxes, scores, class_ids, iou_threshold): unique_class_ids = np.unique(class_ids) keep_boxes = [] for class_id in unique_class_ids: class_indices = np.where(class_ids == class_id)[0] class_boxes = boxes[class_indices, :] class_scores = scores[class_indices] class_keep_boxes = nms(class_boxes, class_scores, iou_threshold) keep_boxes.extend(class_indices[class_keep_boxes]) return keep_boxes def compute_iou(box, boxes): # Compute xmin, ymin, xmax, ymax for both boxes xmin = np.maximum(box[0], boxes[:, 0]) ymin = np.maximum(box[1], boxes[:, 1]) xmax = np.minimum(box[2], boxes[:, 2]) ymax = np.minimum(box[3], boxes[:, 3]) # Compute intersection area intersection_area = np.maximum(0, xmax - xmin) * np.maximum(0, ymax - ymin) # Compute union area box_area = (box[2] - box[0]) * (box[3] - box[1]) boxes_area = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) union_area = box_area + boxes_area - intersection_area # Compute IoU iou = intersection_area / union_area return iou def xywh2xyxy(x): # Convert bounding box (x, y, w, h) to bounding box (x1, y1, x2, y2) y = np.copy(x) y[..., 0] = x[..., 0] - x[..., 2] / 2 y[..., 1] = x[..., 1] - x[..., 3] / 2 y[..., 2] = x[..., 0] + x[..., 2] / 2 y[..., 3] = x[..., 1] + x[..., 3] / 2 return y def draw_seg_masks(image, masks, class_ids, mask_alpha=0.3): """Overlay pixel-level segmentation masks on *image* using per-class colours. Parameters ---------- image : np.ndarray BGR frame (H, W, 3) masks : np.ndarray Float mask array of shape (N, Hm, Wm). Each slice is a soft mask (0-1) at the model's internal resolution. class_ids : array-like Integer class index for each of the N masks. mask_alpha : float Blend weight for the mask layer (0 = invisible, 1 = opaque). """ if masks is None or len(masks) == 0: return image img_h, img_w = image.shape[:2] mask_layer = image.copy() for mask, class_id in zip(masks, class_ids): color = colors[int(class_id) % len(colors)] # Resize soft mask to frame resolution and threshold to binary. mask_resized = cv2.resize(mask, (img_w, img_h), interpolation=cv2.INTER_LINEAR) binary = (mask_resized > 0.5).astype(np.uint8) # Paint the masked pixels with the class colour. for ch, c_val in enumerate(color): mask_layer[:, :, ch] = np.where(binary, c_val, mask_layer[:, :, ch]) return cv2.addWeighted(mask_layer, mask_alpha, image, 1 - mask_alpha, 0) def draw_detections(image, boxes, scores, class_ids, masks=None, mask_alpha=0.3, class_names_list=None): """Draw segmentation masks (if any), bounding boxes, and labels on *image*. Parameters ---------- image : np.ndarray BGR frame. boxes : np.ndarray Bounding boxes in xyxy format, shape (N, 4). scores : np.ndarray Confidence scores, shape (N,). class_ids : np.ndarray Integer class indices, shape (N,). masks : np.ndarray | None Soft masks (N, Hm, Wm). When provided the masks are blended onto the frame before boxes/labels are drawn. mask_alpha: float Opacity of the mask layer. class_names_list: list[str] | None List of active class names corresponding to class_ids. """ if class_names_list is None: class_names_list = class_names det_img = image.copy() img_height, img_width = image.shape[:2] font_size = min([img_height, img_width]) * 0.0006 text_thickness = int(min([img_height, img_width]) * 0.001) # 1. Draw segmentation masks first so boxes/labels render on top. if masks is not None and len(masks) > 0: det_img = draw_seg_masks(det_img, masks, class_ids, mask_alpha) # 2. Draw bounding boxes and labels of detections. for class_id, box, score in zip(class_ids, boxes, scores): color = colors[int(class_id) % len(colors)] draw_box(det_img, box, color) # type: ignore label = class_names_list[int(class_id)] if int(class_id) < len(class_names_list) else str(class_id) caption = f"{label} {int(score * 100)}%" draw_text(det_img, caption, box, color, font_size, text_thickness) # type: ignore return det_img def draw_box( image: np.ndarray, box: np.ndarray, color: tuple[int, int, int] = (0, 0, 255), thickness: int = 2, ) -> np.ndarray: x1, y1, x2, y2 = box.astype(int) return cv2.rectangle(image, (x1, y1), (x2, y2), color, thickness) def draw_text( image: np.ndarray, text: str, box: np.ndarray, color: tuple[int, int, int] = (0, 0, 255), font_size: float = 0.001, text_thickness: int = 2, ) -> np.ndarray: x1, y1, x2, y2 = box.astype(int) (tw, th), _ = cv2.getTextSize( text=text, fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=font_size, thickness=text_thickness, ) th = int(th * 1.2) cv2.rectangle(image, (x1, y1), (x1 + tw, y1 - th), color, -1) return cv2.putText( image, text, (x1, y1), cv2.FONT_HERSHEY_SIMPLEX, font_size, (255, 255, 255), text_thickness, cv2.LINE_AA, ) def draw_masks( image: np.ndarray, boxes: np.ndarray, classes: np.ndarray, mask_alpha: float = 0.3 ) -> np.ndarray: mask_img = image.copy() # Draw bounding boxes and labels of detections for box, class_id in zip(boxes, classes): color = colors[class_id] x1, y1, x2, y2 = box.astype(int) # Draw fill rectangle in mask image cv2.rectangle(mask_img, (x1, y1), (x2, y2), color, -1) # type: ignore return cv2.addWeighted(mask_img, mask_alpha, image, 1 - mask_alpha, 0)