ultralytics 8.3.86__py3-none-any.whl → 8.3.88__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. tests/test_solutions.py +47 -39
  2. ultralytics/__init__.py +1 -1
  3. ultralytics/cfg/__init__.py +58 -55
  4. ultralytics/cfg/models/11/yolo11-cls-resnet18.yaml +1 -1
  5. ultralytics/cfg/models/11/yolo11-cls.yaml +6 -6
  6. ultralytics/data/augment.py +2 -2
  7. ultralytics/data/loaders.py +1 -1
  8. ultralytics/engine/exporter.py +1 -1
  9. ultralytics/engine/results.py +76 -41
  10. ultralytics/engine/trainer.py +11 -5
  11. ultralytics/engine/tuner.py +3 -2
  12. ultralytics/nn/autobackend.py +1 -1
  13. ultralytics/nn/tasks.py +1 -1
  14. ultralytics/solutions/__init__.py +14 -6
  15. ultralytics/solutions/ai_gym.py +39 -28
  16. ultralytics/solutions/analytics.py +22 -18
  17. ultralytics/solutions/distance_calculation.py +25 -25
  18. ultralytics/solutions/heatmap.py +40 -38
  19. ultralytics/solutions/instance_segmentation.py +69 -0
  20. ultralytics/solutions/object_blurrer.py +89 -0
  21. ultralytics/solutions/object_counter.py +35 -33
  22. ultralytics/solutions/object_cropper.py +84 -0
  23. ultralytics/solutions/parking_management.py +40 -13
  24. ultralytics/solutions/queue_management.py +20 -39
  25. ultralytics/solutions/region_counter.py +54 -51
  26. ultralytics/solutions/security_alarm.py +40 -30
  27. ultralytics/solutions/solutions.py +594 -16
  28. ultralytics/solutions/speed_estimation.py +34 -31
  29. ultralytics/solutions/streamlit_inference.py +34 -28
  30. ultralytics/solutions/trackzone.py +29 -18
  31. ultralytics/solutions/vision_eye.py +69 -0
  32. ultralytics/trackers/utils/kalman_filter.py +23 -23
  33. ultralytics/utils/__init__.py +2 -3
  34. ultralytics/utils/callbacks/comet.py +37 -5
  35. ultralytics/utils/instance.py +3 -3
  36. ultralytics/utils/plotting.py +0 -414
  37. {ultralytics-8.3.86.dist-info → ultralytics-8.3.88.dist-info}/METADATA +8 -8
  38. {ultralytics-8.3.86.dist-info → ultralytics-8.3.88.dist-info}/RECORD +42 -38
  39. {ultralytics-8.3.86.dist-info → ultralytics-8.3.88.dist-info}/WHEEL +1 -1
  40. {ultralytics-8.3.86.dist-info → ultralytics-8.3.88.dist-info}/LICENSE +0 -0
  41. {ultralytics-8.3.86.dist-info → ultralytics-8.3.88.dist-info}/entry_points.txt +0 -0
  42. {ultralytics-8.3.86.dist-info → ultralytics-8.3.88.dist-info}/top_level.txt +0 -0
@@ -452,7 +452,8 @@ class BaseTrainer:
452
452
  self.scheduler.last_epoch = self.epoch # do not move
453
453
  self.stop |= epoch >= self.epochs # stop if exceeded epochs
454
454
  self.run_callbacks("on_fit_epoch_end")
455
- self._clear_memory()
455
+ if self._get_memory(fraction=True) > 0.9:
456
+ self._clear_memory() # clear if memory utilization > 90%
456
457
 
457
458
  # Early Stopping
458
459
  if RANK != -1: # if DDP training
@@ -485,15 +486,20 @@ class BaseTrainer:
485
486
  max_num_obj=max_num_obj,
486
487
  ) # returns batch size
487
488
 
488
- def _get_memory(self):
489
- """Get accelerator memory utilization in GB."""
489
+ def _get_memory(self, fraction=False):
490
+ """Get accelerator memory utilization in GB or fraction."""
491
+ memory, total = 0, 0
490
492
  if self.device.type == "mps":
491
493
  memory = torch.mps.driver_allocated_memory()
494
+ if fraction:
495
+ total = torch.mps.get_mem_info()[0]
492
496
  elif self.device.type == "cpu":
493
- memory = 0
497
+ pass
494
498
  else:
495
499
  memory = torch.cuda.memory_reserved()
496
- return memory / (2**30)
500
+ if fraction:
501
+ total = torch.cuda.get_device_properties(self.device).total_memory
502
+ return ((memory / total) if total > 0 else 0) if fraction else (memory / 2**30)
497
503
 
498
504
  def _clear_memory(self):
499
505
  """Clear accelerator memory on different platforms."""
@@ -191,8 +191,9 @@ class Tuner:
191
191
  weights_dir = save_dir / "weights"
192
192
  try:
193
193
  # Train YOLO model with mutated hyperparameters (run in subprocess to avoid dataloader hang)
194
- cmd = ["yolo", "train", *(f"{k}={v}" for k, v in train_args.items())]
195
- return_code = subprocess.run(" ".join(cmd), check=True, shell=True).returncode
194
+ launch = [__import__("sys").executable, "-m", "ultralytics.cfg.__init__"] # workaround yolo not found
195
+ cmd = [*launch, "train", *(f"{k}={v}" for k, v in train_args.items())]
196
+ return_code = subprocess.run(cmd, check=True).returncode
196
197
  ckpt_file = weights_dir / ("best.pt" if (weights_dir / "best.pt").exists() else "last.pt")
197
198
  metrics = torch.load(ckpt_file)["train_metrics"]
198
199
  assert return_code == 0, "training failed"
@@ -244,7 +244,7 @@ class AutoBackend(nn.Module):
244
244
  # OpenVINO
245
245
  elif xml:
246
246
  LOGGER.info(f"Loading {w} for OpenVINO inference...")
247
- check_requirements("openvino>=2024.0.0,<2025.0.0")
247
+ check_requirements("openvino>=2024.0.0,!=2025.0.0")
248
248
  import openvino as ov
249
249
 
250
250
  core = ov.Core()
ultralytics/nn/tasks.py CHANGED
@@ -1119,7 +1119,7 @@ def guess_model_scale(model_path):
1119
1119
  (str): The size character of the model's scale, which can be n, s, m, l, or x.
1120
1120
  """
1121
1121
  try:
1122
- return re.search(r"yolo[v]?\d+([nslmx])", Path(model_path).stem).group(1) # noqa, returns n, s, m, l, or x
1122
+ return re.search(r"yolo[v]?\d+([nslmx])", Path(model_path).stem).group(1) # returns n, s, m, l, or x
1123
1123
  except AttributeError:
1124
1124
  return ""
1125
1125
 
@@ -4,7 +4,10 @@ from .ai_gym import AIGym
4
4
  from .analytics import Analytics
5
5
  from .distance_calculation import DistanceCalculation
6
6
  from .heatmap import Heatmap
7
+ from .instance_segmentation import InstanceSegmentation
8
+ from .object_blurrer import ObjectBlurrer
7
9
  from .object_counter import ObjectCounter
10
+ from .object_cropper import ObjectCropper
8
11
  from .parking_management import ParkingManagement, ParkingPtsSelection
9
12
  from .queue_management import QueueManager
10
13
  from .region_counter import RegionCounter
@@ -12,19 +15,24 @@ from .security_alarm import SecurityAlarm
12
15
  from .speed_estimation import SpeedEstimator
13
16
  from .streamlit_inference import Inference
14
17
  from .trackzone import TrackZone
18
+ from .vision_eye import VisionEye
15
19
 
16
20
  __all__ = (
21
+ "ObjectCounter",
22
+ "ObjectCropper",
23
+ "ObjectBlurrer",
17
24
  "AIGym",
18
- "DistanceCalculation",
25
+ "RegionCounter",
26
+ "SecurityAlarm",
19
27
  "Heatmap",
20
- "ObjectCounter",
28
+ "InstanceSegmentation",
29
+ "VisionEye",
30
+ "SpeedEstimator",
31
+ "DistanceCalculation",
32
+ "QueueManager",
21
33
  "ParkingManagement",
22
34
  "ParkingPtsSelection",
23
- "QueueManager",
24
- "SpeedEstimator",
25
35
  "Analytics",
26
36
  "Inference",
27
- "RegionCounter",
28
37
  "TrackZone",
29
- "SecurityAlarm",
30
38
  )
@@ -1,7 +1,6 @@
1
1
  # Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
2
2
 
3
- from ultralytics.solutions.solutions import BaseSolution
4
- from ultralytics.utils.plotting import Annotator
3
+ from ultralytics.solutions.solutions import BaseSolution, SolutionAnnotator, SolutionResults
5
4
 
6
5
 
7
6
  class AIGym(BaseSolution):
@@ -19,27 +18,28 @@ class AIGym(BaseSolution):
19
18
  up_angle (float): Angle threshold for considering the 'up' position of an exercise.
20
19
  down_angle (float): Angle threshold for considering the 'down' position of an exercise.
21
20
  kpts (List[int]): Indices of keypoints used for angle calculation.
22
- annotator (Annotator): Object for drawing annotations on the image.
23
21
 
24
22
  Methods:
25
- monitor: Processes a frame to detect poses, calculate angles, and count repetitions.
23
+ process: Processes a frame to detect poses, calculate angles, and count repetitions.
26
24
 
27
25
  Examples:
28
26
  >>> gym = AIGym(model="yolo11n-pose.pt")
29
27
  >>> image = cv2.imread("gym_scene.jpg")
30
- >>> processed_image = gym.monitor(image)
28
+ >>> results = gym.process(image)
29
+ >>> processed_image = results.plot_im
31
30
  >>> cv2.imshow("Processed Image", processed_image)
32
31
  >>> cv2.waitKey(0)
33
32
  """
34
33
 
35
34
  def __init__(self, **kwargs):
36
- """Initializes AIGym for workout monitoring using pose estimation and predefined angles."""
37
- # Check if the model name ends with '-pose'
38
- if "model" in kwargs and "-pose" not in kwargs["model"]:
39
- kwargs["model"] = "yolo11n-pose.pt"
40
- elif "model" not in kwargs:
41
- kwargs["model"] = "yolo11n-pose.pt"
35
+ """
36
+ Initializes AIGym for workout monitoring using pose estimation and predefined angles.
42
37
 
38
+ Args:
39
+ **kwargs (Any): Keyword arguments passed to the parent class constructor.
40
+ model (str): Model name or path, defaults to "yolo11n-pose.pt".
41
+ """
42
+ kwargs["model"] = kwargs.get("model", "yolo11n-pose.pt")
43
43
  super().__init__(**kwargs)
44
44
  self.count = [] # List for counts, necessary where there are multiple objects in frame
45
45
  self.angle = [] # List for angle, necessary where there are multiple objects in frame
@@ -51,7 +51,7 @@ class AIGym(BaseSolution):
51
51
  self.down_angle = float(self.CFG["down_angle"]) # Pose down predefined angle to consider down pose
52
52
  self.kpts = self.CFG["kpts"] # User selected kpts of workouts storage for further usage
53
53
 
54
- def monitor(self, im0):
54
+ def process(self, im0):
55
55
  """
56
56
  Monitors workouts using Ultralytics YOLO Pose Model.
57
57
 
@@ -60,36 +60,39 @@ class AIGym(BaseSolution):
60
60
  angle thresholds.
61
61
 
62
62
  Args:
63
- im0 (ndarray): Input image for processing.
63
+ im0 (np.ndarray): Input image for processing.
64
64
 
65
65
  Returns:
66
- (ndarray): Processed image with annotations for workout monitoring.
66
+ (SolutionResults): Contains processed image `plot_im`,
67
+ 'workout_count' (list of completed reps),
68
+ 'workout_stage' (list of current stages),
69
+ 'workout_angle' (list of angles), and
70
+ 'total_tracks' (total number of tracked individuals).
67
71
 
68
72
  Examples:
69
73
  >>> gym = AIGym()
70
74
  >>> image = cv2.imread("workout.jpg")
71
- >>> processed_image = gym.monitor(image)
75
+ >>> results = gym.process(image)
76
+ >>> processed_image = results.plot_im
72
77
  """
73
- # Extract tracks
74
- tracks = self.model.track(source=im0, persist=True, classes=self.CFG["classes"], **self.track_add_args)[0]
78
+ annotator = SolutionAnnotator(im0, line_width=self.line_width) # Initialize annotator
79
+
80
+ self.extract_tracks(im0) # Extract tracks (bounding boxes, classes, and masks)
81
+ tracks = self.tracks[0]
75
82
 
76
83
  if tracks.boxes.id is not None:
77
- # Extract and check keypoints
78
- if len(tracks) > len(self.count):
84
+ if len(tracks) > len(self.count): # Add new entries for newly detected people
79
85
  new_human = len(tracks) - len(self.count)
80
86
  self.angle += [0] * new_human
81
87
  self.count += [0] * new_human
82
88
  self.stage += ["-"] * new_human
83
89
 
84
- # Initialize annotator
85
- self.annotator = Annotator(im0, line_width=self.line_width)
86
-
87
90
  # Enumerate over keypoints
88
91
  for ind, k in enumerate(reversed(tracks.keypoints.data)):
89
92
  # Get keypoints and estimate the angle
90
93
  kpts = [k[int(self.kpts[i])].cpu() for i in range(3)]
91
- self.angle[ind] = self.annotator.estimate_pose_angle(*kpts)
92
- im0 = self.annotator.draw_specific_points(k, self.kpts, radius=self.line_width * 3)
94
+ self.angle[ind] = annotator.estimate_pose_angle(*kpts)
95
+ annotator.draw_specific_kpts(k, self.kpts, radius=self.line_width * 3)
93
96
 
94
97
  # Determine stage and count logic based on angle thresholds
95
98
  if self.angle[ind] < self.down_angle:
@@ -100,12 +103,20 @@ class AIGym(BaseSolution):
100
103
  self.stage[ind] = "up"
101
104
 
102
105
  # Display angle, count, and stage text
103
- self.annotator.plot_angle_and_count_and_stage(
106
+ annotator.plot_angle_and_count_and_stage(
104
107
  angle_text=self.angle[ind], # angle text for display
105
108
  count_text=self.count[ind], # count text for workouts
106
109
  stage_text=self.stage[ind], # stage position text
107
110
  center_kpt=k[int(self.kpts[1])], # center keypoint for display
108
111
  )
109
-
110
- self.display_output(im0) # Display output image, if environment support display
111
- return im0 # return an image for writing or further usage
112
+ plot_im = annotator.result()
113
+ self.display_output(plot_im) # Display output image, if environment support display
114
+
115
+ # Return SolutionResults
116
+ return SolutionResults(
117
+ plot_im=plot_im,
118
+ workout_count=self.count,
119
+ workout_stage=self.stage,
120
+ workout_angle=self.angle,
121
+ total_tracks=len(self.track_ids),
122
+ )
@@ -8,7 +8,7 @@ import numpy as np
8
8
  from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
9
9
  from matplotlib.figure import Figure
10
10
 
11
- from ultralytics.solutions.solutions import BaseSolution # Import a parent class
11
+ from ultralytics.solutions.solutions import BaseSolution, SolutionResults # Import a parent class
12
12
 
13
13
 
14
14
  class Analytics(BaseSolution):
@@ -33,16 +33,18 @@ class Analytics(BaseSolution):
33
33
  fig (Figure): Matplotlib figure object for the chart.
34
34
  ax (Axes): Matplotlib axes object for the chart.
35
35
  canvas (FigureCanvas): Canvas for rendering the chart.
36
+ lines (Dict): Dictionary to store line objects for area charts.
37
+ color_mapping (Dict[str, str]): Dictionary mapping class labels to colors for consistent visualization.
36
38
 
37
39
  Methods:
38
- process_data: Processes image data and updates the chart.
40
+ process: Processes image data and updates the chart.
39
41
  update_graph: Updates the chart with new data points.
40
42
 
41
43
  Examples:
42
44
  >>> analytics = Analytics(analytics_type="line")
43
45
  >>> frame = cv2.imread("image.jpg")
44
- >>> processed_frame = analytics.process_data(frame, frame_number=1)
45
- >>> cv2.imshow("Analytics", processed_frame)
46
+ >>> results = analytics.process(frame, frame_number=1)
47
+ >>> cv2.imshow("Analytics", results.plot_im)
46
48
  """
47
49
 
48
50
  def __init__(self, **kwargs):
@@ -59,7 +61,7 @@ class Analytics(BaseSolution):
59
61
  self.title = "Ultralytics Solutions" # window name
60
62
  self.max_points = 45 # maximum points to be drawn on window
61
63
  self.fontsize = 25 # text font size for display
62
- figsize = (19.2, 10.8) # Set output image size 1920 * 1080
64
+ figsize = (12.8, 7.2) # Set output image size 1280 * 720
63
65
  self.color_cycle = cycle(["#DD00BA", "#042AFF", "#FF4447", "#7D24FF", "#BD00FF"])
64
66
 
65
67
  self.total_counts = 0 # count variable for storing total counts i.e. for line
@@ -83,7 +85,7 @@ class Analytics(BaseSolution):
83
85
  if self.type == "pie": # Ensure pie chart is circular
84
86
  self.ax.axis("equal")
85
87
 
86
- def process_data(self, im0, frame_number):
88
+ def process(self, im0, frame_number):
87
89
  """
88
90
  Processes image data and runs object tracking to update analytics charts.
89
91
 
@@ -92,7 +94,8 @@ class Analytics(BaseSolution):
92
94
  frame_number (int): Video frame number for plotting the data.
93
95
 
94
96
  Returns:
95
- (np.ndarray): Processed image with updated analytics chart.
97
+ (SolutionResults): Contains processed image `plot_im`, 'total_tracks' (int, total number of tracked objects)
98
+ and 'classwise_count' (dict, per-class object count).
96
99
 
97
100
  Raises:
98
101
  ModuleNotFoundError: If an unsupported chart type is specified.
@@ -100,26 +103,27 @@ class Analytics(BaseSolution):
100
103
  Examples:
101
104
  >>> analytics = Analytics(analytics_type="line")
102
105
  >>> frame = np.zeros((480, 640, 3), dtype=np.uint8)
103
- >>> processed_frame = analytics.process_data(frame, frame_number=1)
106
+ >>> results = analytics.process(frame, frame_number=1)
104
107
  """
105
108
  self.extract_tracks(im0) # Extract tracks
106
-
107
109
  if self.type == "line":
108
110
  for _ in self.boxes:
109
111
  self.total_counts += 1
110
- im0 = self.update_graph(frame_number=frame_number)
112
+ plot_im = self.update_graph(frame_number=frame_number)
111
113
  self.total_counts = 0
112
114
  elif self.type in {"pie", "bar", "area"}:
113
115
  self.clswise_count = {}
114
- for box, cls in zip(self.boxes, self.clss):
116
+ for cls in self.clss:
115
117
  if self.names[int(cls)] in self.clswise_count:
116
118
  self.clswise_count[self.names[int(cls)]] += 1
117
119
  else:
118
120
  self.clswise_count[self.names[int(cls)]] = 1
119
- im0 = self.update_graph(frame_number=frame_number, count_dict=self.clswise_count, plot=self.type)
121
+ plot_im = self.update_graph(frame_number=frame_number, count_dict=self.clswise_count, plot=self.type)
120
122
  else:
121
123
  raise ModuleNotFoundError(f"{self.type} chart is not supported ❌")
122
- return im0
124
+
125
+ # return output dictionary with summary for more usage
126
+ return SolutionResults(plot_im=plot_im, total_tracks=len(self.track_ids), classwise_count=self.clswise_count)
123
127
 
124
128
  def update_graph(self, frame_number, count_dict=None, plot="line"):
125
129
  """
@@ -135,10 +139,10 @@ class Analytics(BaseSolution):
135
139
  (np.ndarray): Updated image containing the graph.
136
140
 
137
141
  Examples:
138
- >>> analytics = Analytics()
139
- >>> frame_number = 10
140
- >>> count_dict = {"person": 5, "car": 3}
141
- >>> updated_image = analytics.update_graph(frame_number, count_dict, plot="bar")
142
+ >>> analytics = Analytics(analytics_type="bar")
143
+ >>> frame_num = 10
144
+ >>> results_dict = {"person": 5, "car": 3}
145
+ >>> updated_image = analytics.update_graph(frame_num, results_dict, plot="bar")
142
146
  """
143
147
  if count_dict is None:
144
148
  # Single line update
@@ -216,7 +220,7 @@ class Analytics(BaseSolution):
216
220
  self.ax.clear()
217
221
 
218
222
  # Create pie chart and create legend labels with percentages
219
- wedges, autotexts = self.ax.pie(
223
+ wedges, _ = self.ax.pie(
220
224
  counts, labels=labels, startangle=start_angle, textprops={"color": self.fg_color}, autopct=None
221
225
  )
222
226
  legend_labels = [f"{label} ({percentage:.1f}%)" for label, percentage in zip(labels, percentages)]
@@ -4,8 +4,8 @@ import math
4
4
 
5
5
  import cv2
6
6
 
7
- from ultralytics.solutions.solutions import BaseSolution
8
- from ultralytics.utils.plotting import Annotator, colors
7
+ from ultralytics.solutions.solutions import BaseSolution, SolutionAnnotator, SolutionResults
8
+ from ultralytics.utils.plotting import colors
9
9
 
10
10
 
11
11
  class DistanceCalculation(BaseSolution):
@@ -18,22 +18,17 @@ class DistanceCalculation(BaseSolution):
18
18
  Attributes:
19
19
  left_mouse_count (int): Counter for left mouse button clicks.
20
20
  selected_boxes (Dict[int, List[float]]): Dictionary to store selected bounding boxes and their track IDs.
21
- annotator (Annotator): An instance of the Annotator class for drawing on the image.
22
- boxes (List[List[float]]): List of bounding boxes for detected objects.
23
- track_ids (List[int]): List of track IDs for detected objects.
24
- clss (List[int]): List of class indices for detected objects.
25
- names (List[str]): List of class names that the model can detect.
26
21
  centroids (List[List[int]]): List to store centroids of selected bounding boxes.
27
22
 
28
23
  Methods:
29
24
  mouse_event_for_distance: Handles mouse events for selecting objects in the video stream.
30
- calculate: Processes video frames and calculates the distance between selected objects.
25
+ process: Processes video frames and calculates the distance between selected objects.
31
26
 
32
27
  Examples:
33
28
  >>> distance_calc = DistanceCalculation()
34
29
  >>> frame = cv2.imread("frame.jpg")
35
- >>> processed_frame = distance_calc.calculate(frame)
36
- >>> cv2.imshow("Distance Calculation", processed_frame)
30
+ >>> results = distance_calc.process(frame)
31
+ >>> cv2.imshow("Distance Calculation", results.plot_im)
37
32
  >>> cv2.waitKey(0)
38
33
  """
39
34
 
@@ -44,8 +39,7 @@ class DistanceCalculation(BaseSolution):
44
39
  # Mouse event information
45
40
  self.left_mouse_count = 0
46
41
  self.selected_boxes = {}
47
-
48
- self.centroids = [] # Initialize empty list to store centroids
42
+ self.centroids = [] # Store centroids of selected objects
49
43
 
50
44
  def mouse_event_for_distance(self, event, x, y, flags, param):
51
45
  """
@@ -56,7 +50,7 @@ class DistanceCalculation(BaseSolution):
56
50
  x (int): X-coordinate of the mouse pointer.
57
51
  y (int): Y-coordinate of the mouse pointer.
58
52
  flags (int): Flags associated with the event (e.g., cv2.EVENT_FLAG_CTRLKEY, cv2.EVENT_FLAG_SHIFTKEY).
59
- param (Dict): Additional parameters passed to the function.
53
+ param (Any): Additional parameters passed to the function.
60
54
 
61
55
  Examples:
62
56
  >>> # Assuming 'dc' is an instance of DistanceCalculation
@@ -73,7 +67,7 @@ class DistanceCalculation(BaseSolution):
73
67
  self.selected_boxes = {}
74
68
  self.left_mouse_count = 0
75
69
 
76
- def calculate(self, im0):
70
+ def process(self, im0):
77
71
  """
78
72
  Processes a video frame and calculates the distance between two selected bounding boxes.
79
73
 
@@ -84,41 +78,47 @@ class DistanceCalculation(BaseSolution):
84
78
  im0 (numpy.ndarray): The input image frame to process.
85
79
 
86
80
  Returns:
87
- (numpy.ndarray): The processed image frame with annotations and distance calculations.
81
+ (SolutionResults): Contains processed image `plot_im`, `total_tracks` (int) representing the total number
82
+ of tracked objects, and `pixels_distance` (float) representing the distance between selected objects
83
+ in pixels.
88
84
 
89
85
  Examples:
90
86
  >>> import numpy as np
91
87
  >>> from ultralytics.solutions import DistanceCalculation
92
88
  >>> dc = DistanceCalculation()
93
89
  >>> frame = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8)
94
- >>> processed_frame = dc.calculate(frame)
90
+ >>> results = dc.process(frame)
91
+ >>> print(f"Distance: {results.pixels_distance:.2f} pixels")
95
92
  """
96
- self.annotator = Annotator(im0, line_width=self.line_width) # Initialize annotator
97
93
  self.extract_tracks(im0) # Extract tracks
94
+ annotator = SolutionAnnotator(im0, line_width=self.line_width) # Initialize annotator
98
95
 
96
+ pixels_distance = 0
99
97
  # Iterate over bounding boxes, track ids and classes index
100
98
  for box, track_id, cls in zip(self.boxes, self.track_ids, self.clss):
101
- self.annotator.box_label(box, color=colors(int(cls), True), label=self.names[int(cls)])
99
+ annotator.box_label(box, color=colors(int(cls), True), label=self.names[int(cls)])
102
100
 
101
+ # Update selected boxes if they're being tracked
103
102
  if len(self.selected_boxes) == 2:
104
103
  for trk_id in self.selected_boxes.keys():
105
104
  if trk_id == track_id:
106
105
  self.selected_boxes[track_id] = box
107
106
 
108
107
  if len(self.selected_boxes) == 2:
109
- # Store user selected boxes in centroids list
108
+ # Calculate centroids of selected boxes
110
109
  self.centroids.extend(
111
110
  [[int((box[0] + box[2]) // 2), int((box[1] + box[3]) // 2)] for box in self.selected_boxes.values()]
112
111
  )
113
- # Calculate pixels distance
112
+ # Calculate Euclidean distance between centroids
114
113
  pixels_distance = math.sqrt(
115
114
  (self.centroids[0][0] - self.centroids[1][0]) ** 2 + (self.centroids[0][1] - self.centroids[1][1]) ** 2
116
115
  )
117
- self.annotator.plot_distance_and_line(pixels_distance, self.centroids)
118
-
119
- self.centroids = []
116
+ annotator.plot_distance_and_line(pixels_distance, self.centroids)
120
117
 
121
- self.display_output(im0) # display output with base class function
118
+ self.centroids = [] # Reset centroids for next frame
119
+ plot_im = annotator.result()
120
+ self.display_output(plot_im) # Display output with base class function
122
121
  cv2.setMouseCallback("Ultralytics Solutions", self.mouse_event_for_distance)
123
122
 
124
- return im0 # return output image for more usage
123
+ # Return SolutionResults with processed image and calculated metrics
124
+ return SolutionResults(plot_im=plot_im, pixels_distance=pixels_distance, total_tracks=len(self.track_ids))
@@ -4,7 +4,7 @@ import cv2
4
4
  import numpy as np
5
5
 
6
6
  from ultralytics.solutions.object_counter import ObjectCounter
7
- from ultralytics.utils.plotting import Annotator
7
+ from ultralytics.solutions.solutions import SolutionAnnotator, SolutionResults
8
8
 
9
9
 
10
10
  class Heatmap(ObjectCounter):
@@ -18,28 +18,33 @@ class Heatmap(ObjectCounter):
18
18
  initialized (bool): Flag indicating whether the heatmap has been initialized.
19
19
  colormap (int): OpenCV colormap used for heatmap visualization.
20
20
  heatmap (np.ndarray): Array storing the cumulative heatmap data.
21
- annotator (Annotator): Object for drawing annotations on the image.
21
+ annotator (SolutionAnnotator): Object for drawing annotations on the image.
22
22
 
23
23
  Methods:
24
24
  heatmap_effect: Calculates and updates the heatmap effect for a given bounding box.
25
- generate_heatmap: Generates and applies the heatmap effect to each frame.
25
+ process: Generates and applies the heatmap effect to each frame.
26
26
 
27
27
  Examples:
28
28
  >>> from ultralytics.solutions import Heatmap
29
29
  >>> heatmap = Heatmap(model="yolo11n.pt", colormap=cv2.COLORMAP_JET)
30
30
  >>> frame = cv2.imread("frame.jpg")
31
- >>> processed_frame = heatmap.generate_heatmap(frame)
31
+ >>> processed_frame = heatmap.process(frame)
32
32
  """
33
33
 
34
34
  def __init__(self, **kwargs):
35
- """Initializes the Heatmap class for real-time video stream heatmap generation based on object tracks."""
35
+ """
36
+ Initializes the Heatmap class for real-time video stream heatmap generation based on object tracks.
37
+
38
+ Args:
39
+ **kwargs (Any): Keyword arguments passed to the parent ObjectCounter class.
40
+ """
36
41
  super().__init__(**kwargs)
37
42
 
38
- self.initialized = False # bool variable for heatmap initialization
39
- if self.region is not None: # check if user provided the region coordinates
43
+ self.initialized = False # Flag for heatmap initialization
44
+ if self.region is not None: # Check if user provided the region coordinates
40
45
  self.initialize_region()
41
46
 
42
- # store colormap
47
+ # Store colormap
43
48
  self.colormap = cv2.COLORMAP_PARULA if self.CFG["colormap"] is None else self.CFG["colormap"]
44
49
  self.heatmap = None
45
50
 
@@ -49,11 +54,6 @@ class Heatmap(ObjectCounter):
49
54
 
50
55
  Args:
51
56
  box (List[float]): Bounding box coordinates [x0, y0, x1, y1].
52
-
53
- Examples:
54
- >>> heatmap = Heatmap()
55
- >>> box = [100, 100, 200, 200]
56
- >>> heatmap.heatmap_effect(box)
57
57
  """
58
58
  x0, y0, x1, y1 = map(int, box)
59
59
  radius_squared = (min(x1 - x0, y1 - y0) // 2) ** 2
@@ -70,7 +70,7 @@ class Heatmap(ObjectCounter):
70
70
  # Update only the values within the bounding box in a single vectorized operation
71
71
  self.heatmap[y0:y1, x0:x1][within_radius] += 2
72
72
 
73
- def generate_heatmap(self, im0):
73
+ def process(self, im0):
74
74
  """
75
75
  Generate heatmap for each frame using Ultralytics.
76
76
 
@@ -78,50 +78,52 @@ class Heatmap(ObjectCounter):
78
78
  im0 (np.ndarray): Input image array for processing.
79
79
 
80
80
  Returns:
81
- (np.ndarray): Processed image with heatmap overlay and object counts (if region is specified).
82
-
83
- Examples:
84
- >>> heatmap = Heatmap()
85
- >>> im0 = cv2.imread("image.jpg")
86
- >>> result = heatmap.generate_heatmap(im0)
81
+ (SolutionResults): Contains processed image `plot_im`,
82
+ 'in_count' (int, count of objects entering the region),
83
+ 'out_count' (int, count of objects exiting the region),
84
+ 'classwise_count' (dict, per-class object count), and
85
+ 'total_tracks' (int, total number of tracked objects).
87
86
  """
88
87
  if not self.initialized:
89
88
  self.heatmap = np.zeros_like(im0, dtype=np.float32) * 0.99
90
- self.initialized = True # Initialize heatmap only once
89
+ self.initialized = True # Initialize heatmap only once
91
90
 
92
- self.annotator = Annotator(im0, line_width=self.line_width) # Initialize annotator
93
91
  self.extract_tracks(im0) # Extract tracks
92
+ self.annotator = SolutionAnnotator(im0, line_width=self.line_width) # Initialize annotator
94
93
 
95
94
  # Iterate over bounding boxes, track ids and classes index
96
95
  for box, track_id, cls in zip(self.boxes, self.track_ids, self.clss):
97
- # Draw bounding box and counting region
96
+ # Apply heatmap effect for the bounding box
98
97
  self.heatmap_effect(box)
99
98
 
100
99
  if self.region is not None:
101
100
  self.annotator.draw_region(reg_pts=self.region, color=(104, 0, 123), thickness=self.line_width * 2)
102
101
  self.store_tracking_history(track_id, box) # Store track history
103
- self.store_classwise_counts(cls) # store classwise counts in dict
102
+ self.store_classwise_counts(cls) # Store classwise counts in dict
104
103
  current_centroid = ((box[0] + box[2]) / 2, (box[1] + box[3]) / 2)
105
- # Store tracking previous position and perform object counting
104
+ # Get previous position if available
106
105
  prev_position = None
107
106
  if len(self.track_history[track_id]) > 1:
108
107
  prev_position = self.track_history[track_id][-2]
109
108
  self.count_objects(current_centroid, track_id, prev_position, cls) # Perform object counting
110
109
 
110
+ plot_im = self.annotator.result()
111
111
  if self.region is not None:
112
- self.display_counts(im0) # Display the counts on the frame
112
+ self.display_counts(plot_im) # Display the counts on the frame
113
113
 
114
114
  # Normalize, apply colormap to heatmap and combine with original image
115
115
  if self.track_data.id is not None:
116
- im0 = cv2.addWeighted(
117
- im0,
118
- 0.5,
119
- cv2.applyColorMap(
120
- cv2.normalize(self.heatmap, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8), self.colormap
121
- ),
122
- 0.5,
123
- 0,
124
- )
125
-
126
- self.display_output(im0) # display output with base class function
127
- return im0 # return output image for more usage
116
+ normalized_heatmap = cv2.normalize(self.heatmap, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
117
+ colored_heatmap = cv2.applyColorMap(normalized_heatmap, self.colormap)
118
+ plot_im = cv2.addWeighted(plot_im, 0.5, colored_heatmap, 0.5, 0)
119
+
120
+ self.display_output(plot_im) # Display output with base class function
121
+
122
+ # Return SolutionResults
123
+ return SolutionResults(
124
+ plot_im=plot_im,
125
+ in_count=self.in_count,
126
+ out_count=self.out_count,
127
+ classwise_count=self.classwise_counts,
128
+ total_tracks=len(self.track_ids),
129
+ )