ultralytics 8.3.4__py3-none-any.whl → 8.3.6__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.
Potentially problematic release.
This version of ultralytics might be problematic. Click here for more details.
- tests/test_solutions.py +6 -8
- ultralytics/__init__.py +1 -1
- ultralytics/cfg/default.yaml +1 -1
- ultralytics/cfg/solutions/default.yaml +16 -0
- ultralytics/data/base.py +37 -5
- ultralytics/data/utils.py +3 -3
- ultralytics/engine/exporter.py +3 -4
- ultralytics/engine/trainer.py +4 -4
- ultralytics/engine/validator.py +2 -0
- ultralytics/solutions/ai_gym.py +62 -110
- ultralytics/solutions/heatmap.py +62 -228
- ultralytics/solutions/object_counter.py +105 -217
- ultralytics/solutions/solutions.py +93 -0
- ultralytics/utils/__init__.py +55 -54
- ultralytics/utils/checks.py +36 -20
- ultralytics/utils/plotting.py +50 -70
- ultralytics/utils/torch_utils.py +7 -2
- {ultralytics-8.3.4.dist-info → ultralytics-8.3.6.dist-info}/METADATA +8 -9
- {ultralytics-8.3.4.dist-info → ultralytics-8.3.6.dist-info}/RECORD +23 -21
- {ultralytics-8.3.4.dist-info → ultralytics-8.3.6.dist-info}/LICENSE +0 -0
- {ultralytics-8.3.4.dist-info → ultralytics-8.3.6.dist-info}/WHEEL +0 -0
- {ultralytics-8.3.4.dist-info → ultralytics-8.3.6.dist-info}/entry_points.txt +0 -0
- {ultralytics-8.3.4.dist-info → ultralytics-8.3.6.dist-info}/top_level.txt +0 -0
ultralytics/solutions/heatmap.py
CHANGED
|
@@ -1,259 +1,93 @@
|
|
|
1
1
|
# Ultralytics YOLO 🚀, AGPL-3.0 license
|
|
2
2
|
|
|
3
|
-
from collections import defaultdict
|
|
4
|
-
|
|
5
3
|
import cv2
|
|
6
4
|
import numpy as np
|
|
7
5
|
|
|
8
|
-
from ultralytics.
|
|
6
|
+
from ultralytics.solutions.object_counter import ObjectCounter # Import object counter class
|
|
9
7
|
from ultralytics.utils.plotting import Annotator
|
|
10
8
|
|
|
11
|
-
check_requirements("shapely>=2.0.0")
|
|
12
|
-
|
|
13
|
-
from shapely.geometry import LineString, Point, Polygon
|
|
14
|
-
|
|
15
9
|
|
|
16
|
-
class Heatmap:
|
|
10
|
+
class Heatmap(ObjectCounter):
|
|
17
11
|
"""A class to draw heatmaps in real-time video stream based on their tracks."""
|
|
18
12
|
|
|
19
|
-
def __init__(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
imw=0,
|
|
23
|
-
imh=0,
|
|
24
|
-
colormap=cv2.COLORMAP_JET,
|
|
25
|
-
heatmap_alpha=0.5,
|
|
26
|
-
view_img=False,
|
|
27
|
-
view_in_counts=True,
|
|
28
|
-
view_out_counts=True,
|
|
29
|
-
count_reg_pts=None,
|
|
30
|
-
count_txt_color=(0, 0, 0),
|
|
31
|
-
count_bg_color=(255, 255, 255),
|
|
32
|
-
count_reg_color=(255, 0, 255),
|
|
33
|
-
region_thickness=5,
|
|
34
|
-
line_dist_thresh=15,
|
|
35
|
-
line_thickness=2,
|
|
36
|
-
decay_factor=0.99,
|
|
37
|
-
shape="circle",
|
|
38
|
-
):
|
|
39
|
-
"""Initializes the heatmap class with default values for Visual, Image, track, count and heatmap parameters."""
|
|
40
|
-
# Visual information
|
|
41
|
-
self.annotator = None
|
|
42
|
-
self.view_img = view_img
|
|
43
|
-
self.shape = shape
|
|
44
|
-
|
|
45
|
-
self.initialized = False
|
|
46
|
-
self.names = names # Classes names
|
|
47
|
-
|
|
48
|
-
# Image information
|
|
49
|
-
self.imw = imw
|
|
50
|
-
self.imh = imh
|
|
51
|
-
self.im0 = None
|
|
52
|
-
self.tf = line_thickness
|
|
53
|
-
self.view_in_counts = view_in_counts
|
|
54
|
-
self.view_out_counts = view_out_counts
|
|
55
|
-
|
|
56
|
-
# Heatmap colormap and heatmap np array
|
|
57
|
-
self.colormap = colormap
|
|
58
|
-
self.heatmap = None
|
|
59
|
-
self.heatmap_alpha = heatmap_alpha
|
|
60
|
-
|
|
61
|
-
# Predict/track information
|
|
62
|
-
self.boxes = []
|
|
63
|
-
self.track_ids = []
|
|
64
|
-
self.clss = []
|
|
65
|
-
self.track_history = defaultdict(list)
|
|
66
|
-
|
|
67
|
-
# Region & Line Information
|
|
68
|
-
self.counting_region = None
|
|
69
|
-
self.line_dist_thresh = line_dist_thresh
|
|
70
|
-
self.region_thickness = region_thickness
|
|
71
|
-
self.region_color = count_reg_color
|
|
72
|
-
|
|
73
|
-
# Object Counting Information
|
|
74
|
-
self.in_counts = 0
|
|
75
|
-
self.out_counts = 0
|
|
76
|
-
self.count_ids = []
|
|
77
|
-
self.class_wise_count = {}
|
|
78
|
-
self.count_txt_color = count_txt_color
|
|
79
|
-
self.count_bg_color = count_bg_color
|
|
80
|
-
self.cls_txtdisplay_gap = 50
|
|
81
|
-
|
|
82
|
-
# Decay factor
|
|
83
|
-
self.decay_factor = decay_factor
|
|
13
|
+
def __init__(self, **kwargs):
|
|
14
|
+
"""Initializes function for heatmap class with default values."""
|
|
15
|
+
super().__init__(**kwargs)
|
|
84
16
|
|
|
85
|
-
#
|
|
86
|
-
self.
|
|
17
|
+
self.initialized = False # bool variable for heatmap initialization
|
|
18
|
+
if self.region is not None: # check if user provided the region coordinates
|
|
19
|
+
self.initialize_region()
|
|
87
20
|
|
|
88
|
-
#
|
|
89
|
-
self.
|
|
90
|
-
print(self.count_reg_pts)
|
|
91
|
-
if self.count_reg_pts is not None:
|
|
92
|
-
if len(self.count_reg_pts) == 2:
|
|
93
|
-
print("Line Counter Initiated.")
|
|
94
|
-
self.counting_region = LineString(self.count_reg_pts)
|
|
95
|
-
elif len(self.count_reg_pts) >= 3:
|
|
96
|
-
print("Polygon Counter Initiated.")
|
|
97
|
-
self.counting_region = Polygon(self.count_reg_pts)
|
|
98
|
-
else:
|
|
99
|
-
print("Invalid Region points provided, region_points must be 2 for lines or >= 3 for polygons.")
|
|
100
|
-
print("Using Line Counter Now")
|
|
101
|
-
self.counting_region = LineString(self.count_reg_pts)
|
|
21
|
+
# store colormap
|
|
22
|
+
self.colormap = cv2.COLORMAP_PARULA if self.CFG["colormap"] is None else self.CFG["colormap"]
|
|
102
23
|
|
|
103
|
-
|
|
104
|
-
if self.shape not in {"circle", "rect"}:
|
|
105
|
-
print("Unknown shape value provided, 'circle' & 'rect' supported")
|
|
106
|
-
print("Using Circular shape now")
|
|
107
|
-
self.shape = "circle"
|
|
108
|
-
|
|
109
|
-
def extract_results(self, tracks):
|
|
24
|
+
def heatmap_effect(self, box):
|
|
110
25
|
"""
|
|
111
|
-
|
|
26
|
+
Efficient calculation of heatmap area and effect location for applying colormap.
|
|
112
27
|
|
|
113
28
|
Args:
|
|
114
|
-
|
|
115
|
-
"""
|
|
116
|
-
if tracks[0].boxes.id is not None:
|
|
117
|
-
self.boxes = tracks[0].boxes.xyxy.cpu()
|
|
118
|
-
self.clss = tracks[0].boxes.cls.tolist()
|
|
119
|
-
self.track_ids = tracks[0].boxes.id.int().tolist()
|
|
120
|
-
|
|
121
|
-
def generate_heatmap(self, im0, tracks):
|
|
29
|
+
box (list): Bounding Box coordinates data [x0, y0, x1, y1]
|
|
122
30
|
"""
|
|
123
|
-
|
|
31
|
+
x0, y0, x1, y1 = map(int, box)
|
|
32
|
+
radius_squared = (min(x1 - x0, y1 - y0) // 2) ** 2
|
|
124
33
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
tracks (list): List of tracks obtained from the object tracking process.
|
|
128
|
-
"""
|
|
129
|
-
self.im0 = im0
|
|
34
|
+
# Create a meshgrid with region of interest (ROI) for vectorized distance calculations
|
|
35
|
+
xv, yv = np.meshgrid(np.arange(x0, x1), np.arange(y0, y1))
|
|
130
36
|
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
self.heatmap = np.zeros((int(self.im0.shape[0]), int(self.im0.shape[1])), dtype=np.float32)
|
|
134
|
-
self.initialized = True
|
|
37
|
+
# Calculate squared distances from the center
|
|
38
|
+
dist_squared = (xv - ((x0 + x1) // 2)) ** 2 + (yv - ((y0 + y1) // 2)) ** 2
|
|
135
39
|
|
|
136
|
-
|
|
40
|
+
# Create a mask of points within the radius
|
|
41
|
+
within_radius = dist_squared <= radius_squared
|
|
137
42
|
|
|
138
|
-
|
|
139
|
-
self.
|
|
43
|
+
# Update only the values within the bounding box in a single vectorized operation
|
|
44
|
+
self.heatmap[y0:y1, x0:x1][within_radius] += 2
|
|
140
45
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
self.annotator.draw_region(
|
|
145
|
-
reg_pts=self.count_reg_pts, color=self.region_color, thickness=self.region_thickness
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
for box, cls, track_id in zip(self.boxes, self.clss, self.track_ids):
|
|
149
|
-
# Store class info
|
|
150
|
-
if self.names[cls] not in self.class_wise_count:
|
|
151
|
-
self.class_wise_count[self.names[cls]] = {"IN": 0, "OUT": 0}
|
|
152
|
-
|
|
153
|
-
if self.shape == "circle":
|
|
154
|
-
center = (int((box[0] + box[2]) // 2), int((box[1] + box[3]) // 2))
|
|
155
|
-
radius = min(int(box[2]) - int(box[0]), int(box[3]) - int(box[1])) // 2
|
|
46
|
+
def generate_heatmap(self, im0):
|
|
47
|
+
"""
|
|
48
|
+
Generate heatmap for each frame using Ultralytics.
|
|
156
49
|
|
|
157
|
-
|
|
158
|
-
|
|
50
|
+
Args:
|
|
51
|
+
im0 (ndarray): Input image array for processing
|
|
52
|
+
Returns:
|
|
53
|
+
im0 (ndarray): Processed image for further usage
|
|
54
|
+
"""
|
|
55
|
+
self.heatmap = np.zeros_like(im0, dtype=np.float32) * 0.99 if not self.initialized else self.heatmap
|
|
56
|
+
self.initialized = True # Initialize heatmap only once
|
|
159
57
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
)
|
|
58
|
+
self.annotator = Annotator(im0, line_width=self.line_width) # Initialize annotator
|
|
59
|
+
self.extract_tracks(im0) # Extract tracks
|
|
163
60
|
|
|
164
|
-
|
|
165
|
-
|
|
61
|
+
# Iterate over bounding boxes, track ids and classes index
|
|
62
|
+
for box, track_id, cls in zip(self.boxes, self.track_ids, self.clss):
|
|
63
|
+
# Draw bounding box and counting region
|
|
64
|
+
self.heatmap_effect(box)
|
|
166
65
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
track_line.pop(0)
|
|
66
|
+
if self.region is not None:
|
|
67
|
+
self.annotator.draw_region(reg_pts=self.region, color=(104, 0, 123), thickness=self.line_width * 2)
|
|
68
|
+
self.store_tracking_history(track_id, box) # Store track history
|
|
69
|
+
self.store_classwise_counts(cls) # store classwise counts in dict
|
|
172
70
|
|
|
71
|
+
# Store tracking previous position and perform object counting
|
|
173
72
|
prev_position = self.track_history[track_id][-2] if len(self.track_history[track_id]) > 1 else None
|
|
73
|
+
self.count_objects(self.track_line, box, track_id, prev_position, cls) # Perform object counting
|
|
174
74
|
|
|
175
|
-
|
|
176
|
-
# Count objects in any polygon
|
|
177
|
-
if len(self.count_reg_pts) >= 3:
|
|
178
|
-
is_inside = self.counting_region.contains(Point(track_line[-1]))
|
|
179
|
-
|
|
180
|
-
if prev_position is not None and is_inside and track_id not in self.count_ids:
|
|
181
|
-
self.count_ids.append(track_id)
|
|
182
|
-
|
|
183
|
-
if (box[0] - prev_position[0]) * (self.counting_region.centroid.x - prev_position[0]) > 0:
|
|
184
|
-
self.in_counts += 1
|
|
185
|
-
self.class_wise_count[self.names[cls]]["IN"] += 1
|
|
186
|
-
else:
|
|
187
|
-
self.out_counts += 1
|
|
188
|
-
self.class_wise_count[self.names[cls]]["OUT"] += 1
|
|
189
|
-
|
|
190
|
-
# Count objects using line
|
|
191
|
-
elif len(self.count_reg_pts) == 2:
|
|
192
|
-
if prev_position is not None and track_id not in self.count_ids:
|
|
193
|
-
distance = Point(track_line[-1]).distance(self.counting_region)
|
|
194
|
-
if distance < self.line_dist_thresh and track_id not in self.count_ids:
|
|
195
|
-
self.count_ids.append(track_id)
|
|
196
|
-
|
|
197
|
-
if (box[0] - prev_position[0]) * (
|
|
198
|
-
self.counting_region.centroid.x - prev_position[0]
|
|
199
|
-
) > 0:
|
|
200
|
-
self.in_counts += 1
|
|
201
|
-
self.class_wise_count[self.names[cls]]["IN"] += 1
|
|
202
|
-
else:
|
|
203
|
-
self.out_counts += 1
|
|
204
|
-
self.class_wise_count[self.names[cls]]["OUT"] += 1
|
|
205
|
-
|
|
206
|
-
else:
|
|
207
|
-
for box, cls in zip(self.boxes, self.clss):
|
|
208
|
-
if self.shape == "circle":
|
|
209
|
-
center = (int((box[0] + box[2]) // 2), int((box[1] + box[3]) // 2))
|
|
210
|
-
radius = min(int(box[2]) - int(box[0]), int(box[3]) - int(box[1])) // 2
|
|
211
|
-
|
|
212
|
-
y, x = np.ogrid[0 : self.heatmap.shape[0], 0 : self.heatmap.shape[1]]
|
|
213
|
-
mask = (x - center[0]) ** 2 + (y - center[1]) ** 2 <= radius**2
|
|
214
|
-
|
|
215
|
-
self.heatmap[int(box[1]) : int(box[3]), int(box[0]) : int(box[2])] += (
|
|
216
|
-
2 * mask[int(box[1]) : int(box[3]), int(box[0]) : int(box[2])]
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
else:
|
|
220
|
-
self.heatmap[int(box[1]) : int(box[3]), int(box[0]) : int(box[2])] += 2
|
|
221
|
-
|
|
222
|
-
if self.count_reg_pts is not None:
|
|
223
|
-
labels_dict = {}
|
|
224
|
-
|
|
225
|
-
for key, value in self.class_wise_count.items():
|
|
226
|
-
if value["IN"] != 0 or value["OUT"] != 0:
|
|
227
|
-
if not self.view_in_counts and not self.view_out_counts:
|
|
228
|
-
continue
|
|
229
|
-
elif not self.view_in_counts:
|
|
230
|
-
labels_dict[str.capitalize(key)] = f"OUT {value['OUT']}"
|
|
231
|
-
elif not self.view_out_counts:
|
|
232
|
-
labels_dict[str.capitalize(key)] = f"IN {value['IN']}"
|
|
233
|
-
else:
|
|
234
|
-
labels_dict[str.capitalize(key)] = f"IN {value['IN']} OUT {value['OUT']}"
|
|
235
|
-
|
|
236
|
-
if labels_dict is not None:
|
|
237
|
-
self.annotator.display_analytics(self.im0, labels_dict, self.count_txt_color, self.count_bg_color, 10)
|
|
75
|
+
self.display_counts(im0) if self.region is not None else None # Display the counts on the frame
|
|
238
76
|
|
|
239
77
|
# Normalize, apply colormap to heatmap and combine with original image
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if __name__ == "__main__":
|
|
258
|
-
classes_names = {0: "person", 1: "car"} # example class names
|
|
259
|
-
heatmap = Heatmap(classes_names)
|
|
78
|
+
im0 = (
|
|
79
|
+
im0
|
|
80
|
+
if self.track_data.id is None
|
|
81
|
+
else cv2.addWeighted(
|
|
82
|
+
im0,
|
|
83
|
+
0.5,
|
|
84
|
+
cv2.applyColorMap(
|
|
85
|
+
cv2.normalize(self.heatmap, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8), self.colormap
|
|
86
|
+
),
|
|
87
|
+
0.5,
|
|
88
|
+
0,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self.display_output(im0) # display output with base class function
|
|
93
|
+
return im0 # return output image for more usage
|