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.
- tests/test_solutions.py +47 -39
- ultralytics/__init__.py +1 -1
- ultralytics/cfg/__init__.py +58 -55
- ultralytics/cfg/models/11/yolo11-cls-resnet18.yaml +1 -1
- ultralytics/cfg/models/11/yolo11-cls.yaml +6 -6
- ultralytics/data/augment.py +2 -2
- ultralytics/data/loaders.py +1 -1
- ultralytics/engine/exporter.py +1 -1
- ultralytics/engine/results.py +76 -41
- ultralytics/engine/trainer.py +11 -5
- ultralytics/engine/tuner.py +3 -2
- ultralytics/nn/autobackend.py +1 -1
- ultralytics/nn/tasks.py +1 -1
- ultralytics/solutions/__init__.py +14 -6
- ultralytics/solutions/ai_gym.py +39 -28
- ultralytics/solutions/analytics.py +22 -18
- ultralytics/solutions/distance_calculation.py +25 -25
- ultralytics/solutions/heatmap.py +40 -38
- ultralytics/solutions/instance_segmentation.py +69 -0
- ultralytics/solutions/object_blurrer.py +89 -0
- ultralytics/solutions/object_counter.py +35 -33
- ultralytics/solutions/object_cropper.py +84 -0
- ultralytics/solutions/parking_management.py +40 -13
- ultralytics/solutions/queue_management.py +20 -39
- ultralytics/solutions/region_counter.py +54 -51
- ultralytics/solutions/security_alarm.py +40 -30
- ultralytics/solutions/solutions.py +594 -16
- ultralytics/solutions/speed_estimation.py +34 -31
- ultralytics/solutions/streamlit_inference.py +34 -28
- ultralytics/solutions/trackzone.py +29 -18
- ultralytics/solutions/vision_eye.py +69 -0
- ultralytics/trackers/utils/kalman_filter.py +23 -23
- ultralytics/utils/__init__.py +2 -3
- ultralytics/utils/callbacks/comet.py +37 -5
- ultralytics/utils/instance.py +3 -3
- ultralytics/utils/plotting.py +0 -414
- {ultralytics-8.3.86.dist-info → ultralytics-8.3.88.dist-info}/METADATA +8 -8
- {ultralytics-8.3.86.dist-info → ultralytics-8.3.88.dist-info}/RECORD +42 -38
- {ultralytics-8.3.86.dist-info → ultralytics-8.3.88.dist-info}/WHEEL +1 -1
- {ultralytics-8.3.86.dist-info → ultralytics-8.3.88.dist-info}/LICENSE +0 -0
- {ultralytics-8.3.86.dist-info → ultralytics-8.3.88.dist-info}/entry_points.txt +0 -0
- {ultralytics-8.3.86.dist-info → ultralytics-8.3.88.dist-info}/top_level.txt +0 -0
ultralytics/engine/trainer.py
CHANGED
@@ -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.
|
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
|
-
|
497
|
+
pass
|
494
498
|
else:
|
495
499
|
memory = torch.cuda.memory_reserved()
|
496
|
-
|
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."""
|
ultralytics/engine/tuner.py
CHANGED
@@ -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
|
-
|
195
|
-
|
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"
|
ultralytics/nn/autobackend.py
CHANGED
@@ -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
|
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) #
|
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
|
-
"
|
25
|
+
"RegionCounter",
|
26
|
+
"SecurityAlarm",
|
19
27
|
"Heatmap",
|
20
|
-
"
|
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
|
)
|
ultralytics/solutions/ai_gym.py
CHANGED
@@ -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
|
-
|
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
|
-
>>>
|
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
|
-
"""
|
37
|
-
|
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
|
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
|
-
(
|
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
|
-
>>>
|
75
|
+
>>> results = gym.process(image)
|
76
|
+
>>> processed_image = results.plot_im
|
72
77
|
"""
|
73
|
-
#
|
74
|
-
|
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
|
-
#
|
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] =
|
92
|
-
|
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
|
-
|
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(
|
111
|
-
|
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
|
-
|
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
|
-
>>>
|
45
|
-
>>> cv2.imshow("Analytics",
|
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 = (
|
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
|
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
|
-
(
|
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
|
-
>>>
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
>>>
|
140
|
-
>>>
|
141
|
-
>>> updated_image = analytics.update_graph(
|
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,
|
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
|
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
|
-
|
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
|
-
>>>
|
36
|
-
>>> cv2.imshow("Distance Calculation",
|
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 (
|
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
|
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
|
-
(
|
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
|
-
>>>
|
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
|
-
|
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
|
-
#
|
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
|
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
|
-
|
118
|
-
|
119
|
-
self.centroids = []
|
116
|
+
annotator.plot_distance_and_line(pixels_distance, self.centroids)
|
120
117
|
|
121
|
-
self.
|
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
|
-
|
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))
|
ultralytics/solutions/heatmap.py
CHANGED
@@ -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.
|
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 (
|
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
|
-
|
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.
|
31
|
+
>>> processed_frame = heatmap.process(frame)
|
32
32
|
"""
|
33
33
|
|
34
34
|
def __init__(self, **kwargs):
|
35
|
-
"""
|
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 #
|
39
|
-
if self.region is not None: #
|
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
|
-
#
|
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
|
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
|
-
(
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
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
|
-
#
|
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) #
|
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
|
-
#
|
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(
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
+
)
|