Improve instance-segmentation mask plotting (#20025)

Signed-off-by: Glenn Jocher <glenn.jocher@ultralytics.com>
Co-authored-by: UltralyticsAssistant <web@ultralytics.com>
Co-authored-by: Glenn Jocher <glenn.jocher@ultralytics.com>
This commit is contained in:
Muhammad Rizwan Munawar 2025-04-05 19:58:42 +05:00 committed by GitHub
parent b9ade810c6
commit a1d3c94a9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 47 additions and 80 deletions

View File

@ -393,8 +393,8 @@ Ultralytics includes an `Annotator` class for annotating various data types. It'
import cv2
from ultralytics import YOLO
from ultralytics.engine.results import Results
from ultralytics.solutions.solutions import SolutionAnnotator
from ultralytics.utils.plotting import colors
# User defined video path and model file
cap = cv2.VideoCapture("path/to/video.mp4")
@ -416,7 +416,8 @@ Ultralytics includes an `Annotator` class for annotating various data types. It'
window_name = "Ultralytics Sweep Annotator"
def drag_line(event, x, y, flags, param): # Mouse callback for dragging line.
def drag_line(event, x, _, flags, param):
"""Mouse callback function to enable dragging a vertical sweep line across the video frame."""
global line_x, dragging
if event == cv2.EVENT_LBUTTONDOWN or (flags & cv2.EVENT_FLAG_LBUTTON):
line_x = max(0, min(x, w))
@ -429,44 +430,47 @@ Ultralytics includes an `Annotator` class for annotating various data types. It'
break
f = f + 1 # Increment frame count.
count = 0 # Re-initialize count variable on every frame for precise counts.
annotator = SolutionAnnotator(im0)
results = model.track(im0, persist=True) # Track objects using track method.
results = model.track(im0, persist=True)[0]
if f == 1:
cv2.namedWindow(window_name)
cv2.setMouseCallback(window_name, drag_line)
if results[0].boxes.id is not None:
if results[0].masks is not None:
masks = results[0].masks.xy
track_ids = results[0].boxes.id.int().cpu().tolist()
clss = results[0].boxes.cls.cpu().tolist()
boxes = results[0].boxes.xyxy.cpu()
if results.boxes.id is not None:
if results.masks is not None:
masks = results.masks
boxes = results.boxes
track_ids = results.boxes.id.int().cpu().tolist()
clss = results.boxes.cls.cpu().tolist()
for mask, box, cls, t_id in zip(masks or [None] * len(boxes), boxes, clss, track_ids):
color = colors(t_id, True) # Assign different color to each tracked object.
if mask is not None and mask.size > 0:
# If you want to overlay the masks
# mask[:, 0] = np.clip(mask[:, 0], line_x, w)
# mask_img = cv2.fillPoly(im0.copy(), [mask.astype(int)], color)
# cv2.addWeighted(mask_img, 0.5, im0, 0.5, 0, im0)
box_data = box.xyxy.cpu().tolist()[0][0]
if box_data > line_x:
count += 1
results = Results(
im0, path=None, names=classes, boxes=box.data, masks=None if mask is None else mask.data
)
im0 = results.plot(
boxes=True, # display bounding box
conf=False, # hide confidence score
labels=True, # display labels
color_mode="instance",
)
if box[0] > line_x:
count += 1
annotator.seg_bbox(mask=mask, mask_color=color, label=str(classes[cls]))
else:
if box[0] > line_x:
count += 1
annotator.box_label(box=box, color=color, label=str(classes[cls]))
# Generate draggable sweep line
annotator = SolutionAnnotator(im0)
annotator.sweep_annotator(line_x=line_x, line_y=h, label=f"COUNT:{count}")
annotator.sweep_annotator(line_x=line_x, line_y=h, label=f"COUNT:{count}") # Display the sweep
cv2.imshow(window_name, im0)
video_writer.write(im0)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release() # Release the video capture.
video_writer.release() # Release the video writer.
cv2.destroyAllWindows() # Destroy all opened windows.
# Release the resources
cap.release()
video_writer.release()
cv2.destroyAllWindows()
```
#### Horizontal Bounding Boxes

View File

@ -1,7 +1,7 @@
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
from ultralytics.solutions.solutions import BaseSolution, SolutionAnnotator, SolutionResults
from ultralytics.utils.plotting import colors
from ultralytics.engine.results import Results
from ultralytics.solutions.solutions import BaseSolution, SolutionResults
class InstanceSegmentation(BaseSolution):
@ -41,6 +41,10 @@ class InstanceSegmentation(BaseSolution):
kwargs["model"] = kwargs.get("model", "yolo11n-seg.pt")
super().__init__(**kwargs)
self.show_conf = self.CFG.get("show_conf", True)
self.show_labels = self.CFG.get("show_labels", True)
self.show_boxes = self.CFG.get("show_boxes", True)
def process(self, im0):
"""
Perform instance segmentation on the input image and annotate the results.
@ -58,17 +62,21 @@ class InstanceSegmentation(BaseSolution):
>>> print(summary)
"""
self.extract_tracks(im0) # Extract tracks (bounding boxes, classes, and masks)
annotator = SolutionAnnotator(im0, self.line_width)
# Iterate over detected classes, track IDs, and segmentation masks
if self.masks is None:
self.LOGGER.warning("⚠️ No masks detected! Ensure you're using a supported Ultralytics segmentation model.")
plot_im = im0
else:
for cls, t_id, mask in zip(self.clss, self.track_ids, self.masks):
# Annotate the image with segmentation mask, mask color, and label
annotator.segmentation_mask(mask=mask, mask_color=colors(t_id, True), label=self.names[cls])
results = Results(im0, path=None, names=self.names, boxes=self.track_data.data, masks=self.masks.data)
plot_im = results.plot(
line_width=self.line_width,
boxes=self.show_boxes,
conf=self.show_conf,
labels=self.show_labels,
color_mode="instance",
)
plot_im = annotator.result()
self.display_output(plot_im) # Display the annotated output using the base class function
# Return SolutionResults

View File

@ -122,7 +122,7 @@ class BaseSolution:
self.track_data = self.tracks[0].obb or self.tracks[0].boxes # Extract tracks for OBB or object detection
self.masks = (
self.tracks[0].masks.xy if hasattr(self.tracks[0], "masks") and self.tracks[0].masks is not None else None
self.tracks[0].masks if hasattr(self.tracks[0], "masks") and self.tracks[0].masks is not None else None
)
if self.track_data and self.track_data.id is not None:
@ -225,7 +225,6 @@ class SolutionAnnotator(Annotator):
plot_angle_and_count_and_stage: Visualizes angle, step count, and stage for workout monitoring.
plot_distance_and_line: Displays the distance between centroids and connects them with a line.
display_objects_labels: Annotates bounding boxes with object class labels.
segmentation_mask: Draws mask for segmented objects and optionally labels them.
sweep_annotator: Visualizes a vertical sweep line and optional label.
visioneye: Maps and connects object centroids to a visual "eye" point.
circle_label: Draws a circular label within a bounding box.
@ -519,50 +518,6 @@ class SolutionAnnotator(Annotator):
lineType=cv2.LINE_AA,
)
def segmentation_mask(self, mask, mask_color=(255, 0, 255), label=None, alpha=0.5):
"""
Draw an optimized segmentation mask with smooth corners, highlighted edge, and dynamic text box size.
Args:
mask (np.ndarray): A 2D array of shape (N, 2) containing the object mask.
mask_color (Tuple[int, int, int]): RGB color for the mask.
label (str, optional): Text label for the object.
alpha (float): Transparency level (0 = fully transparent, 1 = fully opaque).
"""
if mask.size == 0:
return
overlay = self.im.copy()
mask = np.int32([mask])
# Approximate polygon for smooth corners with epsilon
refined_mask = cv2.approxPolyDP(mask, 0.002 * cv2.arcLength(mask, True), True)
# Apply a highlighter effect by drawing a thick outer shadow
cv2.polylines(overlay, [refined_mask], isClosed=True, color=mask_color, thickness=self.lw * 3)
cv2.fillPoly(overlay, [refined_mask], mask_color) # draw mask with primary color
# Apply an inner glow effect for extra clarity
cv2.polylines(overlay, [refined_mask], isClosed=True, color=mask_color, thickness=self.lw)
self.im = cv2.addWeighted(overlay, alpha, self.im, 1 - alpha, 0) # blend overlay with the original image
# Draw label if provided
if label:
text_size, _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, self.sf, self.tf)
text_x, text_y = refined_mask[0][0][0], refined_mask[0][0][1]
rect_start, rect_end = (text_x - 5, text_y - text_size[1] - 5), (text_x + text_size[0] + 5, text_y + 5)
cv2.rectangle(self.im, rect_start, rect_end, mask_color, -1)
cv2.putText(
self.im,
label,
(text_x, text_y),
cv2.FONT_HERSHEY_SIMPLEX,
self.sf,
self.get_txt_color(mask_color),
self.tf,
)
def sweep_annotator(self, line_x=0, line_y=0, label=None, color=(221, 0, 186), txt_color=(255, 255, 255)):
"""
Draw a sweep annotation line and an optional label.