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/draw/__init__.py
ADDED
|
File without changes
|
eye/draw/color.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Simple color management - Easy to use."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Tuple, List
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Color:
|
|
9
|
+
"""RGB color."""
|
|
10
|
+
r: int
|
|
11
|
+
g: int
|
|
12
|
+
b: int
|
|
13
|
+
|
|
14
|
+
def as_bgr(self) -> Tuple[int, int, int]:
|
|
15
|
+
"""Convert to BGR for OpenCV."""
|
|
16
|
+
return (self.b, self.g, self.r)
|
|
17
|
+
|
|
18
|
+
def as_rgb(self) -> Tuple[int, int, int]:
|
|
19
|
+
"""Get as RGB tuple."""
|
|
20
|
+
return (self.r, self.g, self.b)
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_hex(cls, hex_color: str) -> 'Color':
|
|
24
|
+
"""Create from hex string (#RRGGBB)."""
|
|
25
|
+
hex_color = hex_color.lstrip('#')
|
|
26
|
+
return cls(
|
|
27
|
+
int(hex_color[0:2], 16),
|
|
28
|
+
int(hex_color[2:4], 16),
|
|
29
|
+
int(hex_color[4:6], 16)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Predefined colors for supervision compatibility
|
|
33
|
+
Color.ROBOFLOW = Color(255, 107, 0)
|
|
34
|
+
Color.WHITE = Color(255, 255, 255)
|
|
35
|
+
Color.BLACK = Color(0, 0, 0)
|
|
36
|
+
Color.RED = Color(255, 0, 0)
|
|
37
|
+
Color.GREEN = Color(0, 255, 0)
|
|
38
|
+
Color.BLUE = Color(0, 0, 255)
|
|
39
|
+
Color.YELLOW = Color(255, 255, 0)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Palette:
|
|
43
|
+
"""Palette of colors."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, colors: List[Color]):
|
|
46
|
+
self.colors = colors
|
|
47
|
+
|
|
48
|
+
def __len__(self) -> int:
|
|
49
|
+
return len(self.colors)
|
|
50
|
+
|
|
51
|
+
def __getitem__(self, index: int) -> Color:
|
|
52
|
+
"""Get color by index with wrapping."""
|
|
53
|
+
return self.colors[index % len(self.colors)]
|
|
54
|
+
|
|
55
|
+
def by_id(self, obj_id: int) -> Color:
|
|
56
|
+
"""Get color by object/class ID."""
|
|
57
|
+
return self[obj_id]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Alias for backward compatibility
|
|
61
|
+
ColorPalette = Palette
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PredefinedPalettes:
|
|
65
|
+
"""Predefined color palettes."""
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def bright() -> Palette:
|
|
69
|
+
"""Bright, vibrant colors."""
|
|
70
|
+
return Palette([
|
|
71
|
+
Color(255, 0, 0), # Red
|
|
72
|
+
Color(0, 255, 0), # Green
|
|
73
|
+
Color(0, 0, 255), # Blue
|
|
74
|
+
Color(255, 255, 0), # Yellow
|
|
75
|
+
Color(255, 0, 255), # Magenta
|
|
76
|
+
Color(0, 255, 255), # Cyan
|
|
77
|
+
Color(255, 128, 0), # Orange
|
|
78
|
+
Color(128, 0, 255), # Purple
|
|
79
|
+
Color(255, 192, 203), # Pink
|
|
80
|
+
Color(0, 255, 128), # Spring Green
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def pastel() -> Palette:
|
|
85
|
+
"""Soft pastel colors."""
|
|
86
|
+
return Palette([
|
|
87
|
+
Color(255, 179, 186), # Pastel Red
|
|
88
|
+
Color(186, 255, 201), # Pastel Green
|
|
89
|
+
Color(186, 225, 255), # Pastel Blue
|
|
90
|
+
Color(255, 255, 186), # Pastel Yellow
|
|
91
|
+
Color(255, 223, 186), # Pastel Orange
|
|
92
|
+
Color(226, 186, 255), # Pastel Purple
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def traffic() -> Palette:
|
|
97
|
+
"""Traffic/vehicle themed."""
|
|
98
|
+
return Palette([
|
|
99
|
+
Color(220, 20, 60), # Crimson (cars)
|
|
100
|
+
Color(70, 130, 180), # Steel Blue (trucks)
|
|
101
|
+
Color(255, 215, 0), # Gold (buses)
|
|
102
|
+
Color(50, 205, 50), # Lime Green (motorcycles)
|
|
103
|
+
Color(255, 140, 0), # Dark Orange (vans)
|
|
104
|
+
])
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def monochrome(base: Color, steps: int = 10) -> 'Palette':
|
|
108
|
+
"""Monochrome gradient from black to base color."""
|
|
109
|
+
colors = []
|
|
110
|
+
for i in range(steps):
|
|
111
|
+
factor = i / (steps - 1)
|
|
112
|
+
colors.append(Color(
|
|
113
|
+
int(base.r * factor),
|
|
114
|
+
int(base.g * factor),
|
|
115
|
+
int(base.b * factor)
|
|
116
|
+
))
|
|
117
|
+
|
|
118
|
+
return Palette(colors)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Palette alias for convenience
|
|
122
|
+
ColorPalette = Palette
|
|
123
|
+
|
|
124
|
+
# Add DEFAULT palette
|
|
125
|
+
Palette.DEFAULT = PredefinedPalettes.bright()
|
|
126
|
+
|
|
127
|
+
# Legacy color palette for backward compatibility
|
|
128
|
+
LEGACY_COLOR_PALETTE = Palette.DEFAULT
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Predefined eye colors
|
|
132
|
+
Color.EYE_ORANGE = Color(255, 107, 0)
|
|
133
|
+
Color.WHITE = Color(255, 255, 255)
|
|
134
|
+
Color.BLACK = Color(0, 0, 0)
|
|
135
|
+
Color.RED = Color(255, 0, 0)
|
|
136
|
+
Color.GREEN = Color(0, 255, 0)
|
|
137
|
+
Color.BLUE = Color(0, 0, 255)
|
|
138
|
+
Color.YELLOW = Color(255, 255, 0)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Quick color access - Return Color objects
|
|
142
|
+
class Colors:
|
|
143
|
+
"""Easy color access with Color objects."""
|
|
144
|
+
RED = Color(255, 0, 0)
|
|
145
|
+
GREEN = Color(0, 255, 0)
|
|
146
|
+
BLUE = Color(0, 0, 255)
|
|
147
|
+
YELLOW = Color(255, 255, 0)
|
|
148
|
+
CYAN = Color(0, 255, 255)
|
|
149
|
+
MAGENTA = Color(255, 0, 255)
|
|
150
|
+
WHITE = Color(255, 255, 255)
|
|
151
|
+
EYE_ORANGE = Color(255, 107, 0)
|
|
152
|
+
BLACK = Color(0, 0, 0)
|
|
153
|
+
ORANGE = Color(255, 165, 0)
|
|
154
|
+
PURPLE = Color(128, 0, 128)
|
eye/draw/utils.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional, Tuple, Union
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from eye.draw.color import Color
|
|
8
|
+
from eye.geometry.core import Point, Rect
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def draw_line(
|
|
12
|
+
scene: np.ndarray,
|
|
13
|
+
start: Point,
|
|
14
|
+
end: Point,
|
|
15
|
+
color: Color = Color.EYE_ORANGE,
|
|
16
|
+
thickness: int = 2,
|
|
17
|
+
) -> np.ndarray:
|
|
18
|
+
"""
|
|
19
|
+
Draws a line on a given scene.
|
|
20
|
+
|
|
21
|
+
Parameters:
|
|
22
|
+
scene (np.ndarray): The scene on which the line will be drawn
|
|
23
|
+
start (Point): The starting point of the line
|
|
24
|
+
end (Point): The end point of the line
|
|
25
|
+
color (Color): The color of the line, defaults to Color.EYE_ORANGE
|
|
26
|
+
thickness (int): The thickness of the line
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
np.ndarray: The scene with the line drawn on it
|
|
30
|
+
"""
|
|
31
|
+
cv2.line(
|
|
32
|
+
scene,
|
|
33
|
+
start.as_xy_int_tuple(),
|
|
34
|
+
end.as_xy_int_tuple(),
|
|
35
|
+
color.as_bgr(),
|
|
36
|
+
thickness=thickness,
|
|
37
|
+
)
|
|
38
|
+
return scene
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def draw_rectangle(
|
|
42
|
+
scene: np.ndarray, rect: Rect, color: Color = Color.EYE_ORANGE, thickness: int = 2
|
|
43
|
+
) -> np.ndarray:
|
|
44
|
+
"""
|
|
45
|
+
Draws a rectangle on an image.
|
|
46
|
+
|
|
47
|
+
Parameters:
|
|
48
|
+
scene (np.ndarray): The scene on which the rectangle will be drawn
|
|
49
|
+
rect (Rect): The rectangle to be drawn
|
|
50
|
+
color (Color): The color of the rectangle
|
|
51
|
+
thickness (int): The thickness of the rectangle border
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
np.ndarray: The scene with the rectangle drawn on it
|
|
55
|
+
"""
|
|
56
|
+
cv2.rectangle(
|
|
57
|
+
scene,
|
|
58
|
+
rect.top_left.as_xy_int_tuple(),
|
|
59
|
+
rect.bottom_right.as_xy_int_tuple(),
|
|
60
|
+
color.as_bgr(),
|
|
61
|
+
thickness=thickness,
|
|
62
|
+
)
|
|
63
|
+
return scene
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def draw_filled_rectangle(
|
|
67
|
+
scene: np.ndarray, rect: Rect, color: Color = Color.EYE_ORANGE, opacity: float = 1
|
|
68
|
+
) -> np.ndarray:
|
|
69
|
+
"""
|
|
70
|
+
Draws a filled rectangle on an image.
|
|
71
|
+
|
|
72
|
+
Parameters:
|
|
73
|
+
scene (np.ndarray): The scene on which the rectangle will be drawn
|
|
74
|
+
rect (Rect): The rectangle to be drawn
|
|
75
|
+
color (Color): The color of the rectangle
|
|
76
|
+
opacity (float): The opacity of rectangle when drawn on the scene.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
np.ndarray: The scene with the rectangle drawn on it
|
|
80
|
+
"""
|
|
81
|
+
if opacity == 1:
|
|
82
|
+
cv2.rectangle(
|
|
83
|
+
scene,
|
|
84
|
+
rect.top_left.as_xy_int_tuple(),
|
|
85
|
+
rect.bottom_right.as_xy_int_tuple(),
|
|
86
|
+
color.as_bgr(),
|
|
87
|
+
-1,
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
scene_with_annotations = scene.copy()
|
|
91
|
+
cv2.rectangle(
|
|
92
|
+
scene_with_annotations,
|
|
93
|
+
rect.top_left.as_xy_int_tuple(),
|
|
94
|
+
rect.bottom_right.as_xy_int_tuple(),
|
|
95
|
+
color.as_bgr(),
|
|
96
|
+
-1,
|
|
97
|
+
)
|
|
98
|
+
cv2.addWeighted(
|
|
99
|
+
scene_with_annotations, opacity, scene, 1 - opacity, gamma=0, dst=scene
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return scene
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def draw_rounded_rectangle(
|
|
106
|
+
scene: np.ndarray,
|
|
107
|
+
rect: Rect,
|
|
108
|
+
color: Color,
|
|
109
|
+
border_radius: int,
|
|
110
|
+
) -> np.ndarray:
|
|
111
|
+
"""
|
|
112
|
+
Draws a rounded rectangle on an image.
|
|
113
|
+
|
|
114
|
+
Parameters:
|
|
115
|
+
scene (np.ndarray): The image on which the rounded rectangle will be drawn.
|
|
116
|
+
rect (Rect): The rectangle to be drawn.
|
|
117
|
+
color (Color): The color of the rounded rectangle.
|
|
118
|
+
border_radius (int): The radius of the corner rounding.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
np.ndarray: The image with the rounded rectangle drawn on it.
|
|
122
|
+
"""
|
|
123
|
+
x1, y1, x2, y2 = rect.as_xyxy_int_tuple()
|
|
124
|
+
width, height = x2 - x1, y2 - y1
|
|
125
|
+
border_radius = min(border_radius, min(width, height) // 2)
|
|
126
|
+
|
|
127
|
+
rectangle_coordinates = [
|
|
128
|
+
((x1 + border_radius, y1), (x2 - border_radius, y2)),
|
|
129
|
+
((x1, y1 + border_radius), (x2, y2 - border_radius)),
|
|
130
|
+
]
|
|
131
|
+
circle_centers = [
|
|
132
|
+
(x1 + border_radius, y1 + border_radius),
|
|
133
|
+
(x2 - border_radius, y1 + border_radius),
|
|
134
|
+
(x1 + border_radius, y2 - border_radius),
|
|
135
|
+
(x2 - border_radius, y2 - border_radius),
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
for coordinates in rectangle_coordinates:
|
|
139
|
+
cv2.rectangle(
|
|
140
|
+
img=scene,
|
|
141
|
+
pt1=coordinates[0],
|
|
142
|
+
pt2=coordinates[1],
|
|
143
|
+
color=color.as_bgr(),
|
|
144
|
+
thickness=-1,
|
|
145
|
+
)
|
|
146
|
+
for center in circle_centers:
|
|
147
|
+
cv2.circle(
|
|
148
|
+
img=scene,
|
|
149
|
+
center=center,
|
|
150
|
+
radius=border_radius,
|
|
151
|
+
color=color.as_bgr(),
|
|
152
|
+
thickness=-1,
|
|
153
|
+
)
|
|
154
|
+
return scene
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def draw_polygon(
|
|
158
|
+
scene: np.ndarray,
|
|
159
|
+
polygon: np.ndarray,
|
|
160
|
+
color: Color = Color.EYE_ORANGE,
|
|
161
|
+
thickness: int = 2,
|
|
162
|
+
) -> np.ndarray:
|
|
163
|
+
"""Draw a polygon on a scene.
|
|
164
|
+
|
|
165
|
+
Parameters:
|
|
166
|
+
scene (np.ndarray): The scene to draw the polygon on.
|
|
167
|
+
polygon (np.ndarray): The polygon to be drawn, given as a list of vertices.
|
|
168
|
+
color (Color): The color of the polygon. Defaults to Color.EYE_ORANGE.
|
|
169
|
+
thickness (int): The thickness of the polygon lines, by default 2.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
np.ndarray: The scene with the polygon drawn on it.
|
|
173
|
+
"""
|
|
174
|
+
cv2.polylines(
|
|
175
|
+
scene, [polygon], isClosed=True, color=color.as_bgr(), thickness=thickness
|
|
176
|
+
)
|
|
177
|
+
return scene
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def draw_filled_polygon(
|
|
181
|
+
scene: np.ndarray,
|
|
182
|
+
polygon: np.ndarray,
|
|
183
|
+
color: Color = Color.EYE_ORANGE,
|
|
184
|
+
opacity: float = 1,
|
|
185
|
+
) -> np.ndarray:
|
|
186
|
+
"""Draw a filled polygon on a scene.
|
|
187
|
+
|
|
188
|
+
Parameters:
|
|
189
|
+
scene (np.ndarray): The scene to draw the polygon on.
|
|
190
|
+
polygon (np.ndarray): The polygon to be drawn, given as a list of vertices.
|
|
191
|
+
color (Color): The color of the polygon. Defaults to Color.EYE_ORANGE.
|
|
192
|
+
opacity (float): The opacity of polygon when drawn on the scene.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
np.ndarray: The scene with the polygon drawn on it.
|
|
196
|
+
"""
|
|
197
|
+
if opacity == 1:
|
|
198
|
+
cv2.fillPoly(scene, [polygon], color=color.as_bgr())
|
|
199
|
+
else:
|
|
200
|
+
scene_with_annotations = scene.copy()
|
|
201
|
+
cv2.fillPoly(scene_with_annotations, [polygon], color=color.as_bgr())
|
|
202
|
+
cv2.addWeighted(
|
|
203
|
+
scene_with_annotations, opacity, scene, 1 - opacity, gamma=0, dst=scene
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return scene
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def draw_text(
|
|
210
|
+
scene: np.ndarray,
|
|
211
|
+
text: str,
|
|
212
|
+
text_anchor: Point,
|
|
213
|
+
text_color: Color = Color.BLACK,
|
|
214
|
+
text_scale: float = 0.5,
|
|
215
|
+
text_thickness: int = 1,
|
|
216
|
+
text_padding: int = 10,
|
|
217
|
+
text_font: int = cv2.FONT_HERSHEY_SIMPLEX,
|
|
218
|
+
background_color: Optional[Color] = None,
|
|
219
|
+
) -> np.ndarray:
|
|
220
|
+
"""
|
|
221
|
+
Draw text with background on a scene.
|
|
222
|
+
|
|
223
|
+
Parameters:
|
|
224
|
+
scene (np.ndarray): A 2-dimensional numpy ndarray representing an image or scene
|
|
225
|
+
text (str): The text to be drawn.
|
|
226
|
+
text_anchor (Point): The anchor point for the text, represented as a
|
|
227
|
+
Point object with x and y attributes.
|
|
228
|
+
text_color (Color): The color of the text. Defaults to black.
|
|
229
|
+
text_scale (float): The scale of the text. Defaults to 0.5.
|
|
230
|
+
text_thickness (int): The thickness of the text. Defaults to 1.
|
|
231
|
+
text_padding (int): The amount of padding to add around the text
|
|
232
|
+
when drawing a rectangle in the background. Defaults to 10.
|
|
233
|
+
text_font (int): The font to use for the text.
|
|
234
|
+
Defaults to cv2.FONT_HERSHEY_SIMPLEX.
|
|
235
|
+
background_color (Optional[Color]): The color of the background rectangle,
|
|
236
|
+
if one is to be drawn. Defaults to None.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
np.ndarray: The input scene with the text drawn on it.
|
|
240
|
+
|
|
241
|
+
Examples:
|
|
242
|
+
```python
|
|
243
|
+
import numpy as np
|
|
244
|
+
|
|
245
|
+
scene = np.zeros((100, 100, 3), dtype=np.uint8)
|
|
246
|
+
text_anchor = Point(x=50, y=50)
|
|
247
|
+
scene = draw_text(scene=scene, text="Hello, world!",text_anchor=text_anchor)
|
|
248
|
+
```
|
|
249
|
+
"""
|
|
250
|
+
text_width, text_height = cv2.getTextSize(
|
|
251
|
+
text=text,
|
|
252
|
+
fontFace=text_font,
|
|
253
|
+
fontScale=text_scale,
|
|
254
|
+
thickness=text_thickness,
|
|
255
|
+
)[0]
|
|
256
|
+
|
|
257
|
+
text_anchor_x, text_anchor_y = text_anchor.as_xy_int_tuple()
|
|
258
|
+
|
|
259
|
+
text_rect = Rect(
|
|
260
|
+
x=text_anchor_x - text_width // 2,
|
|
261
|
+
y=text_anchor_y - text_height // 2,
|
|
262
|
+
width=text_width,
|
|
263
|
+
height=text_height,
|
|
264
|
+
).pad(text_padding)
|
|
265
|
+
|
|
266
|
+
if background_color is not None:
|
|
267
|
+
scene = draw_filled_rectangle(
|
|
268
|
+
scene=scene, rect=text_rect, color=background_color
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
cv2.putText(
|
|
272
|
+
img=scene,
|
|
273
|
+
text=text,
|
|
274
|
+
org=(text_anchor_x - text_width // 2, text_anchor_y + text_height // 2),
|
|
275
|
+
fontFace=text_font,
|
|
276
|
+
fontScale=text_scale,
|
|
277
|
+
color=text_color.as_bgr(),
|
|
278
|
+
thickness=text_thickness,
|
|
279
|
+
lineType=cv2.LINE_AA,
|
|
280
|
+
)
|
|
281
|
+
return scene
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def draw_image(
|
|
285
|
+
scene: np.ndarray, image: Union[str, np.ndarray], opacity: float, rect: Rect
|
|
286
|
+
) -> np.ndarray:
|
|
287
|
+
"""
|
|
288
|
+
Draws an image onto a given scene with specified opacity and dimensions.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
scene (np.ndarray): Background image where the new image will be drawn.
|
|
292
|
+
image (Union[str, np.ndarray]): Image to draw.
|
|
293
|
+
opacity (float): Opacity of the image to be drawn.
|
|
294
|
+
rect (Rect): Rectangle specifying where to draw the image.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
np.ndarray: The updated scene.
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
FileNotFoundError: If the image path does not exist.
|
|
301
|
+
ValueError: For invalid opacity or rectangle dimensions.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
# Validate and load image
|
|
305
|
+
if isinstance(image, str):
|
|
306
|
+
if not os.path.exists(image):
|
|
307
|
+
raise FileNotFoundError(f"Image path ('{image}') does not exist.")
|
|
308
|
+
image = cv2.imread(image, cv2.IMREAD_UNCHANGED)
|
|
309
|
+
|
|
310
|
+
# Validate opacity
|
|
311
|
+
if not 0.0 <= opacity <= 1.0:
|
|
312
|
+
raise ValueError("Opacity must be between 0.0 and 1.0.")
|
|
313
|
+
|
|
314
|
+
# Validate rectangle dimensions
|
|
315
|
+
if (
|
|
316
|
+
rect.x < 0
|
|
317
|
+
or rect.y < 0
|
|
318
|
+
or rect.x + rect.width > scene.shape[1]
|
|
319
|
+
or rect.y + rect.height > scene.shape[0]
|
|
320
|
+
):
|
|
321
|
+
raise ValueError("Invalid rectangle dimensions.")
|
|
322
|
+
|
|
323
|
+
# Resize and isolate alpha channel
|
|
324
|
+
image = cv2.resize(image, (rect.width, rect.height))
|
|
325
|
+
alpha_channel = (
|
|
326
|
+
image[:, :, 3]
|
|
327
|
+
if image.shape[2] == 4
|
|
328
|
+
else np.ones((rect.height, rect.width), dtype=image.dtype) * 255
|
|
329
|
+
)
|
|
330
|
+
alpha_scaled = cv2.convertScaleAbs(alpha_channel * opacity)
|
|
331
|
+
|
|
332
|
+
# Perform blending
|
|
333
|
+
scene_roi = scene[rect.y : rect.y + rect.height, rect.x : rect.x + rect.width]
|
|
334
|
+
alpha_float = alpha_scaled.astype(np.float32) / 255.0
|
|
335
|
+
blended_roi = cv2.convertScaleAbs(
|
|
336
|
+
(1 - alpha_float[..., np.newaxis]) * scene_roi
|
|
337
|
+
+ alpha_float[..., np.newaxis] * image[:, :, :3]
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Update the scene
|
|
341
|
+
scene[rect.y : rect.y + rect.height, rect.x : rect.x + rect.width] = blended_roi
|
|
342
|
+
|
|
343
|
+
return scene
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def calculate_optimal_text_scale(resolution_wh: Tuple[int, int]) -> float:
|
|
347
|
+
"""
|
|
348
|
+
Calculate font scale based on the resolution of an image.
|
|
349
|
+
|
|
350
|
+
Parameters:
|
|
351
|
+
resolution_wh (Tuple[int, int]): A tuple representing the width and height
|
|
352
|
+
of the image.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
float: The calculated font scale factor.
|
|
356
|
+
"""
|
|
357
|
+
return min(resolution_wh) * 1e-3
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def calculate_optimal_line_thickness(resolution_wh: Tuple[int, int]) -> int:
|
|
361
|
+
"""
|
|
362
|
+
Calculate line thickness based on the resolution of an image.
|
|
363
|
+
|
|
364
|
+
Parameters:
|
|
365
|
+
resolution_wh (Tuple[int, int]): A tuple representing the width and height
|
|
366
|
+
of the image.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
int: The calculated line thickness in pixels.
|
|
370
|
+
"""
|
|
371
|
+
if min(resolution_wh) < 1080:
|
|
372
|
+
return 2
|
|
373
|
+
return 4
|
|
374
|
+
|
eye/filters.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Detection filtering pipeline."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
import numpy as np
|
|
5
|
+
from typing import List
|
|
6
|
+
from .core.detections import Detections
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Filter(ABC):
|
|
10
|
+
"""Base filter class."""
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def __call__(self, detections: Detections) -> Detections:
|
|
14
|
+
"""Apply filter to detections."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConfidenceFilter(Filter):
|
|
19
|
+
"""Filter by confidence threshold."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, min_confidence: float = 0.5):
|
|
22
|
+
self.min_confidence = min_confidence
|
|
23
|
+
|
|
24
|
+
def __call__(self, detections: Detections) -> Detections:
|
|
25
|
+
if detections.confidence is None or len(detections) == 0:
|
|
26
|
+
return detections
|
|
27
|
+
mask = detections.confidence >= self.min_confidence
|
|
28
|
+
return detections.filter(mask)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AreaFilter(Filter):
|
|
32
|
+
"""Filter by bounding box area."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, min_area: float = 100, max_area: float = float('inf')):
|
|
35
|
+
self.min_area = min_area
|
|
36
|
+
self.max_area = max_area
|
|
37
|
+
|
|
38
|
+
def __call__(self, detections: Detections) -> Detections:
|
|
39
|
+
if len(detections) == 0:
|
|
40
|
+
return detections
|
|
41
|
+
areas = detections.area
|
|
42
|
+
mask = (areas >= self.min_area) & (areas <= self.max_area)
|
|
43
|
+
return detections.filter(mask)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AspectRatioFilter(Filter):
|
|
47
|
+
"""Filter by aspect ratio."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, min_ratio: float = 0.1, max_ratio: float = 10.0):
|
|
50
|
+
self.min_ratio = min_ratio
|
|
51
|
+
self.max_ratio = max_ratio
|
|
52
|
+
|
|
53
|
+
def __call__(self, detections: Detections) -> Detections:
|
|
54
|
+
if len(detections) == 0:
|
|
55
|
+
return detections
|
|
56
|
+
ratios = detections.aspect_ratio
|
|
57
|
+
mask = (ratios >= self.min_ratio) & (ratios <= self.max_ratio)
|
|
58
|
+
return detections.filter(mask)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ClassFilter(Filter):
|
|
62
|
+
"""Filter by class IDs."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, allowed_classes: List[int]):
|
|
65
|
+
self.allowed_classes = np.array(allowed_classes)
|
|
66
|
+
|
|
67
|
+
def __call__(self, detections: Detections) -> Detections:
|
|
68
|
+
if detections.class_id is None or len(detections) == 0:
|
|
69
|
+
return detections
|
|
70
|
+
mask = np.isin(detections.class_id, self.allowed_classes)
|
|
71
|
+
return detections.filter(mask)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class FilterPipeline:
|
|
75
|
+
"""Chain multiple filters together.
|
|
76
|
+
|
|
77
|
+
Innovation: Composable filtering with statistics tracking.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, filters: List[Filter]):
|
|
81
|
+
self.filters = filters
|
|
82
|
+
self.stats = {
|
|
83
|
+
'total_processed': 0,
|
|
84
|
+
'total_filtered': 0,
|
|
85
|
+
'filter_counts': [0] * len(filters)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def __call__(self, detections: Detections) -> Detections:
|
|
89
|
+
"""Apply all filters in sequence."""
|
|
90
|
+
self.stats['total_processed'] += len(detections)
|
|
91
|
+
initial_count = len(detections)
|
|
92
|
+
|
|
93
|
+
for i, filter_obj in enumerate(self.filters):
|
|
94
|
+
before = len(detections)
|
|
95
|
+
detections = filter_obj(detections)
|
|
96
|
+
after = len(detections)
|
|
97
|
+
self.stats['filter_counts'][i] += (before - after)
|
|
98
|
+
|
|
99
|
+
self.stats['total_filtered'] += (initial_count - len(detections))
|
|
100
|
+
return detections
|
|
101
|
+
|
|
102
|
+
def get_stats(self) -> dict:
|
|
103
|
+
"""Get filtering statistics."""
|
|
104
|
+
return self.stats.copy()
|
|
105
|
+
|
|
106
|
+
def reset_stats(self):
|
|
107
|
+
"""Reset statistics."""
|
|
108
|
+
self.stats = {
|
|
109
|
+
'total_processed': 0,
|
|
110
|
+
'total_filtered': 0,
|
|
111
|
+
'filter_counts': [0] * len(self.filters)
|
|
112
|
+
}
|
eye/geometry/__init__.py
ADDED
|
File without changes
|