eye-cv 1.0.0__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.
- eye/__init__.py +115 -0
- eye/__init___supervision_original.py +120 -0
- eye/annotators/__init__.py +0 -0
- eye/annotators/base.py +22 -0
- eye/annotators/core.py +2699 -0
- eye/annotators/line.py +107 -0
- eye/annotators/modern.py +529 -0
- eye/annotators/trace.py +142 -0
- eye/annotators/utils.py +177 -0
- eye/assets/__init__.py +2 -0
- eye/assets/downloader.py +95 -0
- eye/assets/list.py +83 -0
- eye/classification/__init__.py +0 -0
- eye/classification/core.py +188 -0
- eye/config.py +2 -0
- eye/core/__init__.py +0 -0
- eye/core/trackers/__init__.py +1 -0
- eye/core/trackers/botsort_tracker.py +336 -0
- eye/core/trackers/bytetrack_tracker.py +284 -0
- eye/core/trackers/sort_tracker.py +200 -0
- eye/core/tracking.py +146 -0
- eye/dataset/__init__.py +0 -0
- eye/dataset/core.py +919 -0
- eye/dataset/formats/__init__.py +0 -0
- eye/dataset/formats/coco.py +258 -0
- eye/dataset/formats/pascal_voc.py +279 -0
- eye/dataset/formats/yolo.py +272 -0
- eye/dataset/utils.py +259 -0
- eye/detection/__init__.py +0 -0
- eye/detection/auto_convert.py +155 -0
- eye/detection/core.py +1529 -0
- eye/detection/detections_enhanced.py +392 -0
- eye/detection/line_zone.py +859 -0
- eye/detection/lmm.py +184 -0
- eye/detection/overlap_filter.py +270 -0
- eye/detection/tools/__init__.py +0 -0
- eye/detection/tools/csv_sink.py +181 -0
- eye/detection/tools/inference_slicer.py +288 -0
- eye/detection/tools/json_sink.py +142 -0
- eye/detection/tools/polygon_zone.py +202 -0
- eye/detection/tools/smoother.py +123 -0
- eye/detection/tools/smoothing.py +179 -0
- eye/detection/tools/smoothing_config.py +202 -0
- eye/detection/tools/transformers.py +247 -0
- eye/detection/utils.py +1175 -0
- eye/draw/__init__.py +0 -0
- eye/draw/color.py +154 -0
- eye/draw/utils.py +374 -0
- eye/filters.py +112 -0
- eye/geometry/__init__.py +0 -0
- eye/geometry/core.py +128 -0
- eye/geometry/utils.py +47 -0
- eye/keypoint/__init__.py +0 -0
- eye/keypoint/annotators.py +442 -0
- eye/keypoint/core.py +687 -0
- eye/keypoint/skeletons.py +2647 -0
- eye/metrics/__init__.py +21 -0
- eye/metrics/core.py +72 -0
- eye/metrics/detection.py +843 -0
- eye/metrics/f1_score.py +648 -0
- eye/metrics/mean_average_precision.py +628 -0
- eye/metrics/mean_average_recall.py +697 -0
- eye/metrics/precision.py +653 -0
- eye/metrics/recall.py +652 -0
- eye/metrics/utils/__init__.py +0 -0
- eye/metrics/utils/object_size.py +158 -0
- eye/metrics/utils/utils.py +9 -0
- eye/py.typed +0 -0
- eye/quick.py +104 -0
- eye/tracker/__init__.py +0 -0
- eye/tracker/byte_tracker/__init__.py +0 -0
- eye/tracker/byte_tracker/core.py +386 -0
- eye/tracker/byte_tracker/kalman_filter.py +205 -0
- eye/tracker/byte_tracker/matching.py +69 -0
- eye/tracker/byte_tracker/single_object_track.py +178 -0
- eye/tracker/byte_tracker/utils.py +18 -0
- eye/utils/__init__.py +0 -0
- eye/utils/conversion.py +132 -0
- eye/utils/file.py +159 -0
- eye/utils/image.py +794 -0
- eye/utils/internal.py +200 -0
- eye/utils/iterables.py +84 -0
- eye/utils/notebook.py +114 -0
- eye/utils/video.py +307 -0
- eye/utils_eye/__init__.py +1 -0
- eye/utils_eye/geometry.py +71 -0
- eye/utils_eye/nms.py +55 -0
- eye/validators/__init__.py +140 -0
- eye/web.py +271 -0
- eye_cv-1.0.0.dist-info/METADATA +319 -0
- eye_cv-1.0.0.dist-info/RECORD +94 -0
- eye_cv-1.0.0.dist-info/WHEEL +5 -0
- eye_cv-1.0.0.dist-info/licenses/LICENSE +21 -0
- eye_cv-1.0.0.dist-info/top_level.txt +1 -0
eye/annotators/line.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Line zone counting and annotator (supervision-like LineZone).
|
|
2
|
+
|
|
3
|
+
Minimal implementation: track per-tracker last side of the line and detect crossings.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import cv2
|
|
7
|
+
import numpy as np
|
|
8
|
+
from typing import Tuple, List, Optional
|
|
9
|
+
from ..core.detections import Detections
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LineZone:
|
|
13
|
+
"""A line detector that counts when tracked objects cross the line.
|
|
14
|
+
|
|
15
|
+
The line is defined by two points (x1,y1)->(x2,y2). Use `update(detections)`
|
|
16
|
+
each frame to register detections (requires `detections.tracker_id`).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, p1: Tuple[int, int], p2: Tuple[int, int], name: Optional[str] = None):
|
|
20
|
+
self.p1 = np.array(p1, dtype=float)
|
|
21
|
+
self.p2 = np.array(p2, dtype=float)
|
|
22
|
+
self.name = name or f"LineZone_{id(self)}"
|
|
23
|
+
|
|
24
|
+
# analytics
|
|
25
|
+
self.total_count = 0
|
|
26
|
+
self.directional_count = {"pos->neg": 0, "neg->pos": 0}
|
|
27
|
+
# last seen side per tracker id
|
|
28
|
+
self._last_side = {}
|
|
29
|
+
|
|
30
|
+
def _side(self, point: Tuple[float, float]) -> int:
|
|
31
|
+
# sign of cross product from line to point
|
|
32
|
+
px, py = float(point[0]), float(point[1])
|
|
33
|
+
x1, y1 = self.p1
|
|
34
|
+
x2, y2 = self.p2
|
|
35
|
+
return int(np.sign((x2 - x1) * (py - y1) - (y2 - y1) * (px - x1)))
|
|
36
|
+
|
|
37
|
+
def update(self, detections: Detections) -> List[int]:
|
|
38
|
+
"""Process detections, update counts, and return list of tracker_ids that crossed."""
|
|
39
|
+
crossed = []
|
|
40
|
+
if len(detections) == 0 or detections.tracker_id is None:
|
|
41
|
+
return crossed
|
|
42
|
+
|
|
43
|
+
centers = detections.center
|
|
44
|
+
for i in range(len(detections)):
|
|
45
|
+
tid = int(detections.tracker_id[i])
|
|
46
|
+
c = centers[i]
|
|
47
|
+
side = self._side(c)
|
|
48
|
+
last = self._last_side.get(tid, None)
|
|
49
|
+
if last is None:
|
|
50
|
+
# first observation
|
|
51
|
+
self._last_side[tid] = side
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
if side == 0 or last == 0:
|
|
55
|
+
# touching line: treat as no-cross
|
|
56
|
+
self._last_side[tid] = side
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
if side != last:
|
|
60
|
+
# crossed
|
|
61
|
+
crossed.append(tid)
|
|
62
|
+
if last > 0 and side < 0:
|
|
63
|
+
self.directional_count["pos->neg"] += 1
|
|
64
|
+
elif last < 0 and side > 0:
|
|
65
|
+
self.directional_count["neg->pos"] += 1
|
|
66
|
+
self.total_count += 1
|
|
67
|
+
self._last_side[tid] = side
|
|
68
|
+
|
|
69
|
+
return crossed
|
|
70
|
+
|
|
71
|
+
def reset(self):
|
|
72
|
+
self.total_count = 0
|
|
73
|
+
self.directional_count = {"pos->neg": 0, "neg->pos": 0}
|
|
74
|
+
self._last_side.clear()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class LineZoneAnnotator:
|
|
78
|
+
"""Annotator that draws a line and simple counters on the frame."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, color=(0, 255, 0), thickness: int = 2, font_scale: float = 0.6, padding: int = 6):
|
|
81
|
+
self.color = color
|
|
82
|
+
self.thickness = thickness
|
|
83
|
+
self.font_scale = font_scale
|
|
84
|
+
self.padding = padding
|
|
85
|
+
|
|
86
|
+
def annotate(self, image: np.ndarray, line: LineZone) -> np.ndarray:
|
|
87
|
+
annotated = image.copy()
|
|
88
|
+
x1, y1 = int(line.p1[0]), int(line.p1[1])
|
|
89
|
+
x2, y2 = int(line.p2[0]), int(line.p2[1])
|
|
90
|
+
|
|
91
|
+
cv2.line(annotated, (x1, y1), (x2, y2), self.color, self.thickness)
|
|
92
|
+
|
|
93
|
+
# draw arrow to indicate primary direction (p1->p2)
|
|
94
|
+
vx, vy = x2 - x1, y2 - y1
|
|
95
|
+
norm = max(1.0, (vx * vx + vy * vy) ** 0.5)
|
|
96
|
+
ux, uy = int(x1 + 0.8 * vx), int(y1 + 0.8 * vy)
|
|
97
|
+
cv2.arrowedLine(annotated, (ux, uy), (x2, y2), self.color, max(1, self.thickness - 1), tipLength=0.2)
|
|
98
|
+
|
|
99
|
+
# draw counts
|
|
100
|
+
text = f"Total: {line.total_count} +:{line.directional_count.get('neg->pos',0)} -:{line.directional_count.get('pos->neg',0)}"
|
|
101
|
+
(w, h), _ = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, self.font_scale, 1)
|
|
102
|
+
x_text = min(max(10, x1), annotated.shape[1] - w - 10)
|
|
103
|
+
y_text = min(max(10, y1 - 10), annotated.shape[0] - 10)
|
|
104
|
+
cv2.rectangle(annotated, (x_text - self.padding, y_text - h - self.padding), (x_text + w + self.padding, y_text + self.padding), (0, 0, 0), -1)
|
|
105
|
+
cv2.putText(annotated, text, (x_text, y_text), cv2.FONT_HERSHEY_SIMPLEX, self.font_scale, self.color, 1)
|
|
106
|
+
|
|
107
|
+
return annotated
|
eye/annotators/modern.py
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
"""Modern, stunning mask annotator with alpha blending."""
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from eye.detection.core import Detections
|
|
7
|
+
from eye.draw.color import ColorPalette
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MaskAnnotator:
|
|
11
|
+
"""Draw segmentation masks with transparency and effects.
|
|
12
|
+
|
|
13
|
+
Stunning features: Gradient fills, edge glow, pattern fills.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
color_palette: Optional[ColorPalette] = None,
|
|
19
|
+
opacity: float = 0.5,
|
|
20
|
+
border_thickness: int = 2,
|
|
21
|
+
edge_glow: bool = False,
|
|
22
|
+
glow_intensity: float = 0.3
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Args:
|
|
26
|
+
color_palette: Color palette for masks
|
|
27
|
+
opacity: Mask transparency (0-1)
|
|
28
|
+
border_thickness: Border thickness (0 = no border)
|
|
29
|
+
edge_glow: Add soft glow around edges
|
|
30
|
+
glow_intensity: Glow intensity (0-1)
|
|
31
|
+
"""
|
|
32
|
+
from eye.draw.color import PredefinedPalettes
|
|
33
|
+
self.colors = color_palette or PredefinedPalettes.bright()
|
|
34
|
+
self.opacity = opacity
|
|
35
|
+
self.border_thickness = border_thickness
|
|
36
|
+
self.edge_glow = edge_glow
|
|
37
|
+
self.glow_intensity = glow_intensity
|
|
38
|
+
|
|
39
|
+
def annotate(
|
|
40
|
+
self,
|
|
41
|
+
image: np.ndarray,
|
|
42
|
+
detections: Detections
|
|
43
|
+
) -> np.ndarray:
|
|
44
|
+
"""Draw masks on image."""
|
|
45
|
+
if 'masks' not in detections.data or detections.data['masks'] is None:
|
|
46
|
+
return image
|
|
47
|
+
|
|
48
|
+
annotated = image.copy()
|
|
49
|
+
masks = detections.data['masks']
|
|
50
|
+
|
|
51
|
+
if len(masks) == 0:
|
|
52
|
+
return annotated
|
|
53
|
+
|
|
54
|
+
# Create overlay
|
|
55
|
+
overlay = image.copy()
|
|
56
|
+
|
|
57
|
+
for i in range(len(detections)):
|
|
58
|
+
if i >= len(masks):
|
|
59
|
+
break
|
|
60
|
+
|
|
61
|
+
mask = masks[i]
|
|
62
|
+
if mask is None:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
# Get color
|
|
66
|
+
if detections.class_id is not None:
|
|
67
|
+
color = self.colors.by_id(detections.class_id[i])
|
|
68
|
+
elif detections.tracker_id is not None:
|
|
69
|
+
color = self.colors.by_id(detections.tracker_id[i])
|
|
70
|
+
else:
|
|
71
|
+
color = self.colors[i]
|
|
72
|
+
|
|
73
|
+
bgr_color = color.as_bgr()
|
|
74
|
+
|
|
75
|
+
# Ensure mask is binary
|
|
76
|
+
if mask.dtype != bool:
|
|
77
|
+
mask = mask > 0.5
|
|
78
|
+
|
|
79
|
+
# Fill mask
|
|
80
|
+
overlay[mask] = bgr_color
|
|
81
|
+
|
|
82
|
+
# Edge glow effect
|
|
83
|
+
if self.edge_glow:
|
|
84
|
+
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
|
85
|
+
dilated = cv2.dilate(mask.astype(np.uint8), kernel, iterations=2)
|
|
86
|
+
edge = dilated - mask.astype(np.uint8)
|
|
87
|
+
edge_mask = edge > 0
|
|
88
|
+
|
|
89
|
+
# Soft glow
|
|
90
|
+
glow_color = tuple(min(255, int(c * (1 + self.glow_intensity))) for c in bgr_color)
|
|
91
|
+
overlay[edge_mask] = glow_color
|
|
92
|
+
|
|
93
|
+
# Border
|
|
94
|
+
if self.border_thickness > 0:
|
|
95
|
+
contours, _ = cv2.findContours(
|
|
96
|
+
mask.astype(np.uint8),
|
|
97
|
+
cv2.RETR_EXTERNAL,
|
|
98
|
+
cv2.CHAIN_APPROX_SIMPLE
|
|
99
|
+
)
|
|
100
|
+
cv2.drawContours(overlay, contours, -1, bgr_color, self.border_thickness)
|
|
101
|
+
|
|
102
|
+
# Blend
|
|
103
|
+
annotated = cv2.addWeighted(overlay, self.opacity, annotated, 1 - self.opacity, 0)
|
|
104
|
+
|
|
105
|
+
return annotated
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class GradientBoxAnnotator:
|
|
109
|
+
"""Draw boxes with gradient fills - Modern look!"""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
color_palette: Optional[ColorPalette] = None,
|
|
114
|
+
thickness: int = 3,
|
|
115
|
+
corner_radius: int = 12,
|
|
116
|
+
gradient: bool = True
|
|
117
|
+
):
|
|
118
|
+
"""
|
|
119
|
+
Args:
|
|
120
|
+
color_palette: Color palette
|
|
121
|
+
thickness: Border thickness
|
|
122
|
+
corner_radius: Corner radius for rounded boxes
|
|
123
|
+
gradient: Fill with gradient
|
|
124
|
+
"""
|
|
125
|
+
from eye.draw.color import PredefinedPalettes
|
|
126
|
+
self.colors = color_palette or PredefinedPalettes.bright()
|
|
127
|
+
self.thickness = thickness
|
|
128
|
+
self.corner_radius = corner_radius
|
|
129
|
+
self.gradient = gradient
|
|
130
|
+
|
|
131
|
+
def annotate(
|
|
132
|
+
self,
|
|
133
|
+
image: np.ndarray,
|
|
134
|
+
detections: Detections
|
|
135
|
+
) -> np.ndarray:
|
|
136
|
+
"""Draw gradient boxes."""
|
|
137
|
+
annotated = image.copy()
|
|
138
|
+
|
|
139
|
+
for i in range(len(detections)):
|
|
140
|
+
x1, y1, x2, y2 = detections.xyxy[i].astype(int)
|
|
141
|
+
|
|
142
|
+
# Get color
|
|
143
|
+
if detections.class_id is not None:
|
|
144
|
+
color = self.colors.by_id(detections.class_id[i])
|
|
145
|
+
elif detections.tracker_id is not None:
|
|
146
|
+
color = self.colors.by_id(detections.tracker_id[i])
|
|
147
|
+
else:
|
|
148
|
+
color = self.colors[i]
|
|
149
|
+
|
|
150
|
+
bgr = color.as_bgr()
|
|
151
|
+
|
|
152
|
+
# Gradient fill
|
|
153
|
+
if self.gradient:
|
|
154
|
+
overlay = annotated.copy()
|
|
155
|
+
for y in range(y1, y2):
|
|
156
|
+
alpha = (y - y1) / max(1, (y2 - y1))
|
|
157
|
+
row_color = tuple(int(c * (0.3 + 0.7 * alpha)) for c in bgr)
|
|
158
|
+
cv2.rectangle(overlay, (x1, y), (x2, y+1), row_color, -1)
|
|
159
|
+
cv2.addWeighted(overlay, 0.3, annotated, 0.7, 0, annotated)
|
|
160
|
+
|
|
161
|
+
# Rounded rectangle
|
|
162
|
+
self._draw_rounded_rect(annotated, (x1, y1), (x2, y2), bgr, self.corner_radius, self.thickness)
|
|
163
|
+
|
|
164
|
+
return annotated
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def _draw_rounded_rect(img, pt1, pt2, color, radius, thickness):
|
|
168
|
+
"""Draw rounded rectangle."""
|
|
169
|
+
x1, y1 = pt1
|
|
170
|
+
x2, y2 = pt2
|
|
171
|
+
|
|
172
|
+
# Draw rectangles
|
|
173
|
+
cv2.rectangle(img, (x1 + radius, y1), (x2 - radius, y2), color, thickness)
|
|
174
|
+
cv2.rectangle(img, (x1, y1 + radius), (x2, y2 - radius), color, thickness)
|
|
175
|
+
|
|
176
|
+
# Draw circles at corners
|
|
177
|
+
cv2.circle(img, (x1 + radius, y1 + radius), radius, color, thickness)
|
|
178
|
+
cv2.circle(img, (x2 - radius, y1 + radius), radius, color, thickness)
|
|
179
|
+
cv2.circle(img, (x1 + radius, y2 - radius), radius, color, thickness)
|
|
180
|
+
cv2.circle(img, (x2 - radius, y2 - radius), radius, color, thickness)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class NeonTraceAnnotator:
|
|
184
|
+
"""Neon-style trails - Ultra modern!"""
|
|
185
|
+
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
color_palette: Optional[ColorPalette] = None,
|
|
189
|
+
thickness: int = 4,
|
|
190
|
+
trace_length: int = 50,
|
|
191
|
+
glow: bool = True
|
|
192
|
+
):
|
|
193
|
+
"""
|
|
194
|
+
Args:
|
|
195
|
+
color_palette: Color palette
|
|
196
|
+
thickness: Line thickness
|
|
197
|
+
trace_length: Max trail points
|
|
198
|
+
glow: Add neon glow effect
|
|
199
|
+
"""
|
|
200
|
+
from eye.draw.color import PredefinedPalettes
|
|
201
|
+
from collections import deque
|
|
202
|
+
self.colors = color_palette or PredefinedPalettes.bright()
|
|
203
|
+
self.thickness = thickness
|
|
204
|
+
self.trace_length = trace_length
|
|
205
|
+
self.glow = glow
|
|
206
|
+
self.history = {}
|
|
207
|
+
# Position smoothing alpha when appending to trace history (0-1).
|
|
208
|
+
# Lower = more smoothing. Default 0.6 is moderate smoothing.
|
|
209
|
+
self.position_smoothing_alpha = 0.6
|
|
210
|
+
# When objects move faster than this (pixels/frame), use `fast_position_smoothing_alpha`
|
|
211
|
+
# to apply stronger smoothing for jitter reduction on fast objects.
|
|
212
|
+
self.position_speed_threshold = 8.0
|
|
213
|
+
# Alpha to use when speed > threshold (lower -> more smoothing)
|
|
214
|
+
self.fast_position_smoothing_alpha = 0.3
|
|
215
|
+
|
|
216
|
+
def annotate(
|
|
217
|
+
self,
|
|
218
|
+
image: np.ndarray,
|
|
219
|
+
detections: Detections
|
|
220
|
+
) -> np.ndarray:
|
|
221
|
+
"""Draw neon trails."""
|
|
222
|
+
if detections.tracker_id is None or len(detections) == 0:
|
|
223
|
+
return image
|
|
224
|
+
|
|
225
|
+
annotated = image.copy()
|
|
226
|
+
|
|
227
|
+
# Update history
|
|
228
|
+
from collections import deque
|
|
229
|
+
from eye.geometry.core import Position
|
|
230
|
+
|
|
231
|
+
# Compute centers using get_anchors_coordinates
|
|
232
|
+
centers = detections.get_anchors_coordinates(anchor=Position.CENTER)
|
|
233
|
+
|
|
234
|
+
for i in range(len(detections)):
|
|
235
|
+
tid = detections.tracker_id[i]
|
|
236
|
+
center = centers[i]
|
|
237
|
+
|
|
238
|
+
if tid not in self.history:
|
|
239
|
+
self.history[tid] = deque(maxlen=self.trace_length)
|
|
240
|
+
|
|
241
|
+
# Apply lightweight temporal smoothing to the appended center
|
|
242
|
+
if len(self.history[tid]) > 0 and self.position_smoothing_alpha is not None:
|
|
243
|
+
last = np.array(self.history[tid][-1], dtype=float)
|
|
244
|
+
newc = np.array(center, dtype=float)
|
|
245
|
+
dist = float(np.linalg.norm(newc - last))
|
|
246
|
+
# choose alpha based on speed
|
|
247
|
+
if self.position_speed_threshold is not None and dist > float(self.position_speed_threshold):
|
|
248
|
+
alpha = float(self.fast_position_smoothing_alpha)
|
|
249
|
+
else:
|
|
250
|
+
alpha = float(self.position_smoothing_alpha)
|
|
251
|
+
center = alpha * newc + (1.0 - alpha) * last
|
|
252
|
+
|
|
253
|
+
self.history[tid].append(center)
|
|
254
|
+
|
|
255
|
+
# Draw trails
|
|
256
|
+
for i in range(len(detections)):
|
|
257
|
+
tid = detections.tracker_id[i]
|
|
258
|
+
if tid not in self.history or len(self.history[tid]) < 2:
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
points = list(self.history[tid])
|
|
262
|
+
color = self.colors.by_id(detections.class_id[i] if detections.class_id is not None else tid)
|
|
263
|
+
|
|
264
|
+
# Draw with fade and glow
|
|
265
|
+
for j in range(len(points) - 1):
|
|
266
|
+
alpha = (j + 1) / len(points)
|
|
267
|
+
pt1 = tuple(points[j].astype(int))
|
|
268
|
+
pt2 = tuple(points[j + 1].astype(int))
|
|
269
|
+
|
|
270
|
+
# Glow effect (thicker, semi-transparent)
|
|
271
|
+
if self.glow:
|
|
272
|
+
glow_color = tuple(int(c * 0.5) for c in color.as_bgr())
|
|
273
|
+
cv2.line(annotated, pt1, pt2, glow_color, self.thickness + 4, cv2.LINE_AA)
|
|
274
|
+
|
|
275
|
+
# Main line
|
|
276
|
+
main_color = tuple(int(c * alpha) for c in color.as_bgr())
|
|
277
|
+
cv2.line(annotated, pt1, pt2, main_color, self.thickness, cv2.LINE_AA)
|
|
278
|
+
|
|
279
|
+
return annotated
|
|
280
|
+
|
|
281
|
+
def reset(self):
|
|
282
|
+
"""Clear history."""
|
|
283
|
+
self.history.clear()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class ShadowBoxAnnotator:
|
|
287
|
+
"""Boxes with drop shadows - Depth effect!"""
|
|
288
|
+
|
|
289
|
+
def __init__(
|
|
290
|
+
self,
|
|
291
|
+
color_palette: Optional[ColorPalette] = None,
|
|
292
|
+
thickness: int = 2,
|
|
293
|
+
shadow_offset: int = 5,
|
|
294
|
+
corner_radius: int = 8
|
|
295
|
+
):
|
|
296
|
+
"""
|
|
297
|
+
Args:
|
|
298
|
+
color_palette: Color palette
|
|
299
|
+
thickness: Border thickness
|
|
300
|
+
shadow_offset: Shadow offset in pixels
|
|
301
|
+
corner_radius: Corner radius
|
|
302
|
+
"""
|
|
303
|
+
from eye.draw.color import PredefinedPalettes
|
|
304
|
+
self.colors = color_palette or PredefinedPalettes.bright()
|
|
305
|
+
self.thickness = thickness
|
|
306
|
+
self.shadow_offset = shadow_offset
|
|
307
|
+
self.corner_radius = corner_radius
|
|
308
|
+
|
|
309
|
+
def annotate(
|
|
310
|
+
self,
|
|
311
|
+
image: np.ndarray,
|
|
312
|
+
detections: Detections
|
|
313
|
+
) -> np.ndarray:
|
|
314
|
+
"""Draw boxes with shadows."""
|
|
315
|
+
annotated = image.copy()
|
|
316
|
+
|
|
317
|
+
for i in range(len(detections)):
|
|
318
|
+
x1, y1, x2, y2 = detections.xyxy[i].astype(int)
|
|
319
|
+
|
|
320
|
+
color = self.colors.by_id(
|
|
321
|
+
detections.class_id[i] if detections.class_id is not None
|
|
322
|
+
else detections.tracker_id[i] if detections.tracker_id is not None
|
|
323
|
+
else i
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Draw shadow
|
|
327
|
+
shadow_color = (30, 30, 30) # Dark gray
|
|
328
|
+
cv2.rectangle(
|
|
329
|
+
annotated,
|
|
330
|
+
(x1 + self.shadow_offset, y1 + self.shadow_offset),
|
|
331
|
+
(x2 + self.shadow_offset, y2 + self.shadow_offset),
|
|
332
|
+
shadow_color,
|
|
333
|
+
self.thickness
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Draw main box
|
|
337
|
+
GradientBoxAnnotator._draw_rounded_rect(
|
|
338
|
+
annotated, (x1, y1), (x2, y2),
|
|
339
|
+
color.as_bgr(), self.corner_radius, self.thickness
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return annotated
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class CornerBoxAnnotator:
|
|
346
|
+
"""Draw boxes using only corner segments (no full borders)."""
|
|
347
|
+
|
|
348
|
+
def __init__(
|
|
349
|
+
self,
|
|
350
|
+
color_palette: Optional[ColorPalette] = None,
|
|
351
|
+
thickness: int = 3,
|
|
352
|
+
corner_length: int = 20,
|
|
353
|
+
corner_radius: int = 6,
|
|
354
|
+
):
|
|
355
|
+
from eye.draw.color import PredefinedPalettes
|
|
356
|
+
self.colors = color_palette or PredefinedPalettes.bright()
|
|
357
|
+
self.thickness = thickness
|
|
358
|
+
self.corner_length = corner_length
|
|
359
|
+
self.corner_radius = corner_radius
|
|
360
|
+
|
|
361
|
+
def annotate(self, image: np.ndarray, detections: Detections) -> np.ndarray:
|
|
362
|
+
annotated = image.copy()
|
|
363
|
+
for i in range(len(detections)):
|
|
364
|
+
x1, y1, x2, y2 = detections.xyxy[i].astype(int)
|
|
365
|
+
color = self.colors.by_id(
|
|
366
|
+
detections.class_id[i] if detections.class_id is not None else (
|
|
367
|
+
detections.tracker_id[i] if detections.tracker_id is not None else i
|
|
368
|
+
)
|
|
369
|
+
).as_bgr()
|
|
370
|
+
|
|
371
|
+
# Top-left
|
|
372
|
+
cv2.line(annotated, (x1, y1), (x1 + self.corner_length, y1), color, self.thickness)
|
|
373
|
+
cv2.line(annotated, (x1, y1), (x1, y1 + self.corner_length), color, self.thickness)
|
|
374
|
+
# Top-right
|
|
375
|
+
cv2.line(annotated, (x2, y1), (x2 - self.corner_length, y1), color, self.thickness)
|
|
376
|
+
cv2.line(annotated, (x2, y1), (x2, y1 + self.corner_length), color, self.thickness)
|
|
377
|
+
# Bottom-left
|
|
378
|
+
cv2.line(annotated, (x1, y2), (x1 + self.corner_length, y2), color, self.thickness)
|
|
379
|
+
cv2.line(annotated, (x1, y2), (x1, y2 - self.corner_length), color, self.thickness)
|
|
380
|
+
# Bottom-right
|
|
381
|
+
cv2.line(annotated, (x2, y2), (x2 - self.corner_length, y2), color, self.thickness)
|
|
382
|
+
cv2.line(annotated, (x2, y2), (x2, y2 - self.corner_length), color, self.thickness)
|
|
383
|
+
|
|
384
|
+
return annotated
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class FPSAnnotator:
|
|
388
|
+
"""Annotator that shows processing FPS on the frame.
|
|
389
|
+
|
|
390
|
+
Usage: call `update(fps)` from the processing loop to set the current value.
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
def __init__(self, position: str = "top-left", font_scale: float = 0.6, color=(0, 255, 0), bg_color=(0, 0, 0), padding: int = 6):
|
|
394
|
+
self.position = position
|
|
395
|
+
self.font_scale = font_scale
|
|
396
|
+
self.color = color
|
|
397
|
+
self.bg_color = bg_color
|
|
398
|
+
self.padding = padding
|
|
399
|
+
self.fps = None
|
|
400
|
+
|
|
401
|
+
def update(self, fps: float):
|
|
402
|
+
try:
|
|
403
|
+
self.fps = float(fps)
|
|
404
|
+
except Exception:
|
|
405
|
+
self.fps = None
|
|
406
|
+
|
|
407
|
+
def annotate(self, image: np.ndarray, detections: Optional[Detections] = None) -> np.ndarray:
|
|
408
|
+
annotated = image.copy()
|
|
409
|
+
if self.fps is None:
|
|
410
|
+
return annotated
|
|
411
|
+
|
|
412
|
+
text = f"FPS: {self.fps:.1f}"
|
|
413
|
+
font = cv2.FONT_HERSHEY_SIMPLEX
|
|
414
|
+
thickness = 1
|
|
415
|
+
(w, h), _ = cv2.getTextSize(text, font, self.font_scale, thickness)
|
|
416
|
+
|
|
417
|
+
if self.position == "top-left":
|
|
418
|
+
x, y = 10, 10 + h
|
|
419
|
+
elif self.position == "top-right":
|
|
420
|
+
x, y = annotated.shape[1] - w - 10, 10 + h
|
|
421
|
+
elif self.position == "bottom-left":
|
|
422
|
+
x, y = 10, annotated.shape[0] - 10
|
|
423
|
+
else:
|
|
424
|
+
x, y = annotated.shape[1] - w - 10, annotated.shape[0] - 10
|
|
425
|
+
|
|
426
|
+
# Background rectangle
|
|
427
|
+
cv2.rectangle(annotated, (x - self.padding, y - h - self.padding), (x + w + self.padding, y + self.padding), self.bg_color, -1)
|
|
428
|
+
cv2.putText(annotated, text, (x, y), font, self.font_scale, self.color, thickness)
|
|
429
|
+
return annotated
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class InfoAnnotator:
|
|
433
|
+
"""Flexible information/text/table annotator.
|
|
434
|
+
|
|
435
|
+
- Reads info from `detections.data.get('info')` if present, otherwise from `self.info`.
|
|
436
|
+
- `info` can be a dict (key->value), a list of (k,v) pairs, or a list of rows (for tables).
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
def __init__(
|
|
440
|
+
self,
|
|
441
|
+
position: str = "top-left",
|
|
442
|
+
as_table: bool = False,
|
|
443
|
+
cols: int = 1,
|
|
444
|
+
show_border: bool = False,
|
|
445
|
+
bg_color=(0, 0, 0),
|
|
446
|
+
text_color=(255, 255, 255),
|
|
447
|
+
font_scale: float = 0.5,
|
|
448
|
+
padding: int = 6,
|
|
449
|
+
):
|
|
450
|
+
self.position = position
|
|
451
|
+
self.as_table = as_table
|
|
452
|
+
self.cols = max(1, int(cols))
|
|
453
|
+
self.show_border = show_border
|
|
454
|
+
self.bg_color = bg_color
|
|
455
|
+
self.text_color = text_color
|
|
456
|
+
self.font_scale = font_scale
|
|
457
|
+
self.padding = padding
|
|
458
|
+
self.info = None
|
|
459
|
+
|
|
460
|
+
def annotate(self, image: np.ndarray, detections: Optional[Detections] = None) -> np.ndarray:
|
|
461
|
+
annotated = image.copy()
|
|
462
|
+
info = None
|
|
463
|
+
if detections is not None and isinstance(detections, Detections):
|
|
464
|
+
info = detections.data.get('info') if isinstance(detections.data, dict) else None
|
|
465
|
+
if info is None:
|
|
466
|
+
info = self.info
|
|
467
|
+
if not info:
|
|
468
|
+
return annotated
|
|
469
|
+
|
|
470
|
+
# Normalize info into list of strings per row
|
|
471
|
+
rows = []
|
|
472
|
+
if isinstance(info, dict):
|
|
473
|
+
for k, v in info.items():
|
|
474
|
+
rows.append(f"{k}: {v}")
|
|
475
|
+
elif isinstance(info, list):
|
|
476
|
+
# Could be list of tuples or list of rows
|
|
477
|
+
for item in info:
|
|
478
|
+
if isinstance(item, (list, tuple)):
|
|
479
|
+
rows.append(" | ".join(str(x) for x in item))
|
|
480
|
+
else:
|
|
481
|
+
rows.append(str(item))
|
|
482
|
+
else:
|
|
483
|
+
rows = [str(info)]
|
|
484
|
+
|
|
485
|
+
# When as_table and cols>1, arrange rows into grid
|
|
486
|
+
if self.as_table and self.cols > 1:
|
|
487
|
+
grid = []
|
|
488
|
+
for i in range(0, len(rows), self.cols):
|
|
489
|
+
grid.append(rows[i:i + self.cols])
|
|
490
|
+
# compute cell sizes
|
|
491
|
+
cell_texts = ["\t".join(r) for r in grid]
|
|
492
|
+
rows = cell_texts
|
|
493
|
+
|
|
494
|
+
# compute text block size
|
|
495
|
+
font = cv2.FONT_HERSHEY_SIMPLEX
|
|
496
|
+
thickness = 1
|
|
497
|
+
line_heights = []
|
|
498
|
+
max_w = 0
|
|
499
|
+
for r in rows:
|
|
500
|
+
(w, h), _ = cv2.getTextSize(r, font, self.font_scale, thickness)
|
|
501
|
+
line_heights.append(h)
|
|
502
|
+
max_w = max(max_w, w)
|
|
503
|
+
|
|
504
|
+
total_h = sum(line_heights) + self.padding * 2 + (len(rows) - 1) * 4
|
|
505
|
+
total_w = max_w + self.padding * 2
|
|
506
|
+
|
|
507
|
+
# choose position
|
|
508
|
+
if self.position == 'top-left':
|
|
509
|
+
x0, y0 = 10, 10
|
|
510
|
+
elif self.position == 'top-right':
|
|
511
|
+
x0, y0 = annotated.shape[1] - total_w - 10, 10
|
|
512
|
+
elif self.position == 'bottom-left':
|
|
513
|
+
x0, y0 = 10, annotated.shape[0] - total_h - 10
|
|
514
|
+
else:
|
|
515
|
+
x0, y0 = annotated.shape[1] - total_w - 10, annotated.shape[0] - total_h - 10
|
|
516
|
+
|
|
517
|
+
# background
|
|
518
|
+
cv2.rectangle(annotated, (x0, y0), (x0 + total_w, y0 + total_h), self.bg_color, -1)
|
|
519
|
+
if self.show_border:
|
|
520
|
+
cv2.rectangle(annotated, (x0, y0), (x0 + total_w, y0 + total_h), (200, 200, 200), 1)
|
|
521
|
+
|
|
522
|
+
# draw lines
|
|
523
|
+
y = y0 + self.padding + line_heights[0]
|
|
524
|
+
for idx, r in enumerate(rows):
|
|
525
|
+
cv2.putText(annotated, r, (x0 + self.padding, y), font, self.font_scale, self.text_color, thickness)
|
|
526
|
+
if idx + 1 < len(rows):
|
|
527
|
+
y += line_heights[idx + 1] + 4
|
|
528
|
+
|
|
529
|
+
return annotated
|