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/utils/image.py
ADDED
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import math
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
from functools import partial
|
|
6
|
+
from typing import Callable, List, Literal, Optional, Tuple, Union
|
|
7
|
+
|
|
8
|
+
import cv2
|
|
9
|
+
import numpy as np
|
|
10
|
+
import numpy.typing as npt
|
|
11
|
+
|
|
12
|
+
from eye.annotators.base import ImageType
|
|
13
|
+
from eye.draw.color import Color
|
|
14
|
+
|
|
15
|
+
def unify_to_bgr(color) -> tuple:
|
|
16
|
+
"""Convert color to BGR tuple."""
|
|
17
|
+
if isinstance(color, Color):
|
|
18
|
+
return color.as_bgr()
|
|
19
|
+
return color
|
|
20
|
+
from eye.draw.utils import calculate_optimal_text_scale, draw_text
|
|
21
|
+
from eye.geometry.core import Point
|
|
22
|
+
from eye.utils.conversion import (
|
|
23
|
+
cv2_to_pillow,
|
|
24
|
+
ensure_cv2_image_for_processing,
|
|
25
|
+
images_to_cv2,
|
|
26
|
+
)
|
|
27
|
+
from eye.utils.iterables import create_batches, fill
|
|
28
|
+
|
|
29
|
+
RelativePosition = Literal["top", "bottom"]
|
|
30
|
+
|
|
31
|
+
MAX_COLUMNS_FOR_SINGLE_ROW_GRID = 3
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@ensure_cv2_image_for_processing
|
|
35
|
+
def crop_image(
|
|
36
|
+
image: ImageType,
|
|
37
|
+
xyxy: Union[npt.NDArray[int], List[int], Tuple[int, int, int, int]],
|
|
38
|
+
) -> ImageType:
|
|
39
|
+
"""
|
|
40
|
+
Crops the given image based on the given bounding box.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
image (ImageType): The image to be cropped. `ImageType` is a flexible type,
|
|
44
|
+
accepting either `numpy.ndarray` or `PIL.Image.Image`.
|
|
45
|
+
xyxy (Union[np.ndarray, List[int], Tuple[int, int, int, int]]): A bounding box
|
|
46
|
+
coordinates in the format `(x_min, y_min, x_max, y_max)`, accepted as either
|
|
47
|
+
a `numpy.ndarray`, a `list`, or a `tuple`.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
(ImageType): The cropped image. The type is determined by the input type and
|
|
51
|
+
may be either a `numpy.ndarray` or `PIL.Image.Image`.
|
|
52
|
+
|
|
53
|
+
=== "OpenCV"
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import cv2
|
|
57
|
+
import eye as sv
|
|
58
|
+
|
|
59
|
+
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
|
60
|
+
image.shape
|
|
61
|
+
# (1080, 1920, 3)
|
|
62
|
+
|
|
63
|
+
xyxy = [200, 400, 600, 800]
|
|
64
|
+
cropped_image = sv.crop_image(image=image, xyxy=xyxy)
|
|
65
|
+
cropped_image.shape
|
|
66
|
+
# (400, 400, 3)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
=== "Pillow"
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from PIL import Image
|
|
73
|
+
import eye as sv
|
|
74
|
+
|
|
75
|
+
image = Image.open(<SOURCE_IMAGE_PATH>)
|
|
76
|
+
image.size
|
|
77
|
+
# (1920, 1080)
|
|
78
|
+
|
|
79
|
+
xyxy = [200, 400, 600, 800]
|
|
80
|
+
cropped_image = sv.crop_image(image=image, xyxy=xyxy)
|
|
81
|
+
cropped_image.size
|
|
82
|
+
# (400, 400)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
{ align=center width="800" }
|
|
86
|
+
""" # noqa E501 // docs
|
|
87
|
+
|
|
88
|
+
if isinstance(xyxy, (list, tuple)):
|
|
89
|
+
xyxy = np.array(xyxy)
|
|
90
|
+
xyxy = np.round(xyxy).astype(int)
|
|
91
|
+
x_min, y_min, x_max, y_max = xyxy.flatten()
|
|
92
|
+
return image[y_min:y_max, x_min:x_max]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@ensure_cv2_image_for_processing
|
|
96
|
+
def scale_image(image: ImageType, scale_factor: float) -> ImageType:
|
|
97
|
+
"""
|
|
98
|
+
Scales the given image based on the given scale factor.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
image (ImageType): The image to be scaled. `ImageType` is a flexible type,
|
|
102
|
+
accepting either `numpy.ndarray` or `PIL.Image.Image`.
|
|
103
|
+
scale_factor (float): The factor by which the image will be scaled. Scale
|
|
104
|
+
factor > `1.0` zooms in, < `1.0` zooms out.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
(ImageType): The scaled image. The type is determined by the input type and
|
|
108
|
+
may be either a `numpy.ndarray` or `PIL.Image.Image`.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ValueError: If the scale factor is non-positive.
|
|
112
|
+
|
|
113
|
+
=== "OpenCV"
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import cv2
|
|
117
|
+
import eye as sv
|
|
118
|
+
|
|
119
|
+
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
|
120
|
+
image.shape
|
|
121
|
+
# (1080, 1920, 3)
|
|
122
|
+
|
|
123
|
+
scaled_image = sv.scale_image(image=image, scale_factor=0.5)
|
|
124
|
+
scaled_image.shape
|
|
125
|
+
# (540, 960, 3)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
=== "Pillow"
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from PIL import Image
|
|
132
|
+
import eye as sv
|
|
133
|
+
|
|
134
|
+
image = Image.open(<SOURCE_IMAGE_PATH>)
|
|
135
|
+
image.size
|
|
136
|
+
# (1920, 1080)
|
|
137
|
+
|
|
138
|
+
scaled_image = sv.scale_image(image=image, scale_factor=0.5)
|
|
139
|
+
scaled_image.size
|
|
140
|
+
# (960, 540)
|
|
141
|
+
```
|
|
142
|
+
"""
|
|
143
|
+
if scale_factor <= 0:
|
|
144
|
+
raise ValueError("Scale factor must be positive.")
|
|
145
|
+
|
|
146
|
+
width_old, height_old = image.shape[1], image.shape[0]
|
|
147
|
+
width_new = int(width_old * scale_factor)
|
|
148
|
+
height_new = int(height_old * scale_factor)
|
|
149
|
+
return cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@ensure_cv2_image_for_processing
|
|
153
|
+
def resize_image(
|
|
154
|
+
image: ImageType,
|
|
155
|
+
resolution_wh: Tuple[int, int],
|
|
156
|
+
keep_aspect_ratio: bool = False,
|
|
157
|
+
) -> ImageType:
|
|
158
|
+
"""
|
|
159
|
+
Resizes the given image to a specified resolution. Can maintain the original aspect
|
|
160
|
+
ratio or resize directly to the desired dimensions.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
image (ImageType): The image to be resized. `ImageType` is a flexible type,
|
|
164
|
+
accepting either `numpy.ndarray` or `PIL.Image.Image`.
|
|
165
|
+
resolution_wh (Tuple[int, int]): The target resolution as
|
|
166
|
+
`(width, height)`.
|
|
167
|
+
keep_aspect_ratio (bool): Flag to maintain the image's original
|
|
168
|
+
aspect ratio. Defaults to `False`.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
(ImageType): The resized image. The type is determined by the input type and
|
|
172
|
+
may be either a `numpy.ndarray` or `PIL.Image.Image`.
|
|
173
|
+
|
|
174
|
+
=== "OpenCV"
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
import cv2
|
|
178
|
+
import eye as sv
|
|
179
|
+
|
|
180
|
+
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
|
181
|
+
image.shape
|
|
182
|
+
# (1080, 1920, 3)
|
|
183
|
+
|
|
184
|
+
resized_image = sv.resize_image(
|
|
185
|
+
image=image, resolution_wh=(1000, 1000), keep_aspect_ratio=True
|
|
186
|
+
)
|
|
187
|
+
resized_image.shape
|
|
188
|
+
# (562, 1000, 3)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
=== "Pillow"
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from PIL import Image
|
|
195
|
+
import eye as sv
|
|
196
|
+
|
|
197
|
+
image = Image.open(<SOURCE_IMAGE_PATH>)
|
|
198
|
+
image.size
|
|
199
|
+
# (1920, 1080)
|
|
200
|
+
|
|
201
|
+
resized_image = sv.resize_image(
|
|
202
|
+
image=image, resolution_wh=(1000, 1000), keep_aspect_ratio=True
|
|
203
|
+
)
|
|
204
|
+
resized_image.size
|
|
205
|
+
# (1000, 562)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
{ align=center width="800" }
|
|
209
|
+
""" # noqa E501 // docs
|
|
210
|
+
if keep_aspect_ratio:
|
|
211
|
+
image_ratio = image.shape[1] / image.shape[0]
|
|
212
|
+
target_ratio = resolution_wh[0] / resolution_wh[1]
|
|
213
|
+
if image_ratio >= target_ratio:
|
|
214
|
+
width_new = resolution_wh[0]
|
|
215
|
+
height_new = int(resolution_wh[0] / image_ratio)
|
|
216
|
+
else:
|
|
217
|
+
height_new = resolution_wh[1]
|
|
218
|
+
width_new = int(resolution_wh[1] * image_ratio)
|
|
219
|
+
else:
|
|
220
|
+
width_new, height_new = resolution_wh
|
|
221
|
+
|
|
222
|
+
return cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@ensure_cv2_image_for_processing
|
|
226
|
+
def letterbox_image(
|
|
227
|
+
image: ImageType,
|
|
228
|
+
resolution_wh: Tuple[int, int],
|
|
229
|
+
color: Union[Tuple[int, int, int], Color] = Color.BLACK,
|
|
230
|
+
) -> ImageType:
|
|
231
|
+
"""
|
|
232
|
+
Resizes and pads an image to a specified resolution with a given color, maintaining
|
|
233
|
+
the original aspect ratio.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
image (ImageType): The image to be resized. `ImageType` is a flexible type,
|
|
237
|
+
accepting either `numpy.ndarray` or `PIL.Image.Image`.
|
|
238
|
+
resolution_wh (Tuple[int, int]): The target resolution as
|
|
239
|
+
`(width, height)`.
|
|
240
|
+
color (Union[Tuple[int, int, int], Color]): The color to pad with. If tuple
|
|
241
|
+
provided it should be in BGR format.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
(ImageType): The resized image. The type is determined by the input type and
|
|
245
|
+
may be either a `numpy.ndarray` or `PIL.Image.Image`.
|
|
246
|
+
|
|
247
|
+
=== "OpenCV"
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
import cv2
|
|
251
|
+
import eye as sv
|
|
252
|
+
|
|
253
|
+
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
|
254
|
+
image.shape
|
|
255
|
+
# (1080, 1920, 3)
|
|
256
|
+
|
|
257
|
+
letterboxed_image = sv.letterbox_image(image=image, resolution_wh=(1000, 1000))
|
|
258
|
+
letterboxed_image.shape
|
|
259
|
+
# (1000, 1000, 3)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
=== "Pillow"
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
from PIL import Image
|
|
266
|
+
import eye as sv
|
|
267
|
+
|
|
268
|
+
image = Image.open(<SOURCE_IMAGE_PATH>)
|
|
269
|
+
image.size
|
|
270
|
+
# (1920, 1080)
|
|
271
|
+
|
|
272
|
+
letterboxed_image = sv.letterbox_image(image=image, resolution_wh=(1000, 1000))
|
|
273
|
+
letterboxed_image.size
|
|
274
|
+
# (1000, 1000)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
{ align=center width="800" }
|
|
278
|
+
""" # noqa E501 // docs
|
|
279
|
+
assert isinstance(image, np.ndarray)
|
|
280
|
+
color = unify_to_bgr(color=color)
|
|
281
|
+
resized_image = resize_image(
|
|
282
|
+
image=image, resolution_wh=resolution_wh, keep_aspect_ratio=True
|
|
283
|
+
)
|
|
284
|
+
height_new, width_new = resized_image.shape[:2]
|
|
285
|
+
padding_top = (resolution_wh[1] - height_new) // 2
|
|
286
|
+
padding_bottom = resolution_wh[1] - height_new - padding_top
|
|
287
|
+
padding_left = (resolution_wh[0] - width_new) // 2
|
|
288
|
+
padding_right = resolution_wh[0] - width_new - padding_left
|
|
289
|
+
image_with_borders = cv2.copyMakeBorder(
|
|
290
|
+
resized_image,
|
|
291
|
+
padding_top,
|
|
292
|
+
padding_bottom,
|
|
293
|
+
padding_left,
|
|
294
|
+
padding_right,
|
|
295
|
+
cv2.BORDER_CONSTANT,
|
|
296
|
+
value=color,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if image.shape[2] == 4:
|
|
300
|
+
image[:padding_top, :, 3] = 0
|
|
301
|
+
image[height_new - padding_bottom :, :, 3] = 0
|
|
302
|
+
image[:, :padding_left, 3] = 0
|
|
303
|
+
image[:, width_new - padding_right :, 3] = 0
|
|
304
|
+
|
|
305
|
+
return image_with_borders
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def overlay_image(
|
|
309
|
+
image: npt.NDArray[np.uint8],
|
|
310
|
+
overlay: npt.NDArray[np.uint8],
|
|
311
|
+
anchor: Tuple[int, int],
|
|
312
|
+
) -> npt.NDArray[np.uint8]:
|
|
313
|
+
"""
|
|
314
|
+
Places an image onto a scene at a given anchor point, handling cases where
|
|
315
|
+
the image's position is partially or completely outside the scene's bounds.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
image (np.ndarray): The background scene onto which the image is placed.
|
|
319
|
+
overlay (np.ndarray): The image to be placed onto the scene.
|
|
320
|
+
anchor (Tuple[int, int]): The `(x, y)` coordinates in the scene where the
|
|
321
|
+
top-left corner of the image will be placed.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
(np.ndarray): The result image with overlay.
|
|
325
|
+
|
|
326
|
+
Examples:
|
|
327
|
+
```python
|
|
328
|
+
import cv2
|
|
329
|
+
import numpy as np
|
|
330
|
+
import eye as sv
|
|
331
|
+
|
|
332
|
+
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
|
333
|
+
overlay = np.zeros((400, 400, 3), dtype=np.uint8)
|
|
334
|
+
result_image = sv.overlay_image(image=image, overlay=overlay, anchor=(200, 400))
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
{ align=center width="800" }
|
|
338
|
+
""" # noqa E501 // docs
|
|
339
|
+
scene_height, scene_width = image.shape[:2]
|
|
340
|
+
image_height, image_width = overlay.shape[:2]
|
|
341
|
+
anchor_x, anchor_y = anchor
|
|
342
|
+
|
|
343
|
+
is_out_horizontally = anchor_x + image_width <= 0 or anchor_x >= scene_width
|
|
344
|
+
is_out_vertically = anchor_y + image_height <= 0 or anchor_y >= scene_height
|
|
345
|
+
|
|
346
|
+
if is_out_horizontally or is_out_vertically:
|
|
347
|
+
return image
|
|
348
|
+
|
|
349
|
+
x_min = max(anchor_x, 0)
|
|
350
|
+
y_min = max(anchor_y, 0)
|
|
351
|
+
x_max = min(scene_width, anchor_x + image_width)
|
|
352
|
+
y_max = min(scene_height, anchor_y + image_height)
|
|
353
|
+
|
|
354
|
+
crop_x_min = max(-anchor_x, 0)
|
|
355
|
+
crop_y_min = max(-anchor_y, 0)
|
|
356
|
+
crop_x_max = image_width - max((anchor_x + image_width) - scene_width, 0)
|
|
357
|
+
crop_y_max = image_height - max((anchor_y + image_height) - scene_height, 0)
|
|
358
|
+
|
|
359
|
+
if overlay.shape[2] == 4:
|
|
360
|
+
b, g, r, alpha = cv2.split(
|
|
361
|
+
overlay[crop_y_min:crop_y_max, crop_x_min:crop_x_max]
|
|
362
|
+
)
|
|
363
|
+
alpha = alpha[:, :, None] / 255.0
|
|
364
|
+
overlay_color = cv2.merge((b, g, r))
|
|
365
|
+
|
|
366
|
+
roi = image[y_min:y_max, x_min:x_max]
|
|
367
|
+
roi[:] = roi * (1 - alpha) + overlay_color * alpha
|
|
368
|
+
image[y_min:y_max, x_min:x_max] = roi
|
|
369
|
+
else:
|
|
370
|
+
image[y_min:y_max, x_min:x_max] = overlay[
|
|
371
|
+
crop_y_min:crop_y_max, crop_x_min:crop_x_max
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
return image
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class ImageSink:
|
|
378
|
+
def __init__(
|
|
379
|
+
self,
|
|
380
|
+
target_dir_path: str,
|
|
381
|
+
overwrite: bool = False,
|
|
382
|
+
image_name_pattern: str = "image_{:05d}.png",
|
|
383
|
+
):
|
|
384
|
+
"""
|
|
385
|
+
Initialize a context manager for saving images.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
target_dir_path (str): The target directory where images will be saved.
|
|
389
|
+
overwrite (bool): Whether to overwrite the existing directory.
|
|
390
|
+
Defaults to False.
|
|
391
|
+
image_name_pattern (str): The image file name pattern.
|
|
392
|
+
Defaults to "image_{:05d}.png".
|
|
393
|
+
|
|
394
|
+
Examples:
|
|
395
|
+
```python
|
|
396
|
+
import eye as sv
|
|
397
|
+
|
|
398
|
+
frames_generator = sv.get_video_frames_generator(<SOURCE_VIDEO_PATH>, stride=2)
|
|
399
|
+
|
|
400
|
+
with sv.ImageSink(target_dir_path=<TARGET_CROPS_DIRECTORY>) as sink:
|
|
401
|
+
for image in frames_generator:
|
|
402
|
+
sink.save_image(image=image)
|
|
403
|
+
```
|
|
404
|
+
""" # noqa E501 // docs
|
|
405
|
+
|
|
406
|
+
self.target_dir_path = target_dir_path
|
|
407
|
+
self.overwrite = overwrite
|
|
408
|
+
self.image_name_pattern = image_name_pattern
|
|
409
|
+
self.image_count = 0
|
|
410
|
+
|
|
411
|
+
def __enter__(self):
|
|
412
|
+
if os.path.exists(self.target_dir_path):
|
|
413
|
+
if self.overwrite:
|
|
414
|
+
shutil.rmtree(self.target_dir_path)
|
|
415
|
+
os.makedirs(self.target_dir_path)
|
|
416
|
+
else:
|
|
417
|
+
os.makedirs(self.target_dir_path)
|
|
418
|
+
|
|
419
|
+
return self
|
|
420
|
+
|
|
421
|
+
def save_image(self, image: np.ndarray, image_name: Optional[str] = None):
|
|
422
|
+
"""
|
|
423
|
+
Save a given image in the target directory.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
image (np.ndarray): The image to be saved. The image must be in BGR color
|
|
427
|
+
format.
|
|
428
|
+
image_name (Optional[str]): The name to use for the saved image.
|
|
429
|
+
If not provided, a name will be
|
|
430
|
+
generated using the `image_name_pattern`.
|
|
431
|
+
"""
|
|
432
|
+
if image_name is None:
|
|
433
|
+
image_name = self.image_name_pattern.format(self.image_count)
|
|
434
|
+
|
|
435
|
+
image_path = os.path.join(self.target_dir_path, image_name)
|
|
436
|
+
cv2.imwrite(image_path, image)
|
|
437
|
+
self.image_count += 1
|
|
438
|
+
|
|
439
|
+
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
440
|
+
pass
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def create_tiles(
|
|
444
|
+
images: List[ImageType],
|
|
445
|
+
grid_size: Optional[Tuple[Optional[int], Optional[int]]] = None,
|
|
446
|
+
single_tile_size: Optional[Tuple[int, int]] = None,
|
|
447
|
+
tile_scaling: Literal["min", "max", "avg"] = "avg",
|
|
448
|
+
tile_padding_color: Union[Tuple[int, int, int], Color] = Color.from_hex("#D9D9D9"),
|
|
449
|
+
tile_margin: int = 10,
|
|
450
|
+
tile_margin_color: Union[Tuple[int, int, int], Color] = Color.from_hex("#BFBEBD"),
|
|
451
|
+
return_type: Literal["auto", "cv2", "pillow"] = "auto",
|
|
452
|
+
titles: Optional[List[Optional[str]]] = None,
|
|
453
|
+
titles_anchors: Optional[Union[Point, List[Optional[Point]]]] = None,
|
|
454
|
+
titles_color: Union[Tuple[int, int, int], Color] = Color.from_hex("#262523"),
|
|
455
|
+
titles_scale: Optional[float] = None,
|
|
456
|
+
titles_thickness: int = 1,
|
|
457
|
+
titles_padding: int = 10,
|
|
458
|
+
titles_text_font: int = cv2.FONT_HERSHEY_SIMPLEX,
|
|
459
|
+
titles_background_color: Union[Tuple[int, int, int], Color] = Color.from_hex(
|
|
460
|
+
"#D9D9D9"
|
|
461
|
+
),
|
|
462
|
+
default_title_placement: RelativePosition = "top",
|
|
463
|
+
) -> ImageType:
|
|
464
|
+
"""
|
|
465
|
+
Creates tiles mosaic from input images, automating grid placement and
|
|
466
|
+
converting images to common resolution maintaining aspect ratio. It is
|
|
467
|
+
also possible to render text titles on tiles, using optional set of
|
|
468
|
+
parameters specifying text drawing (see parameters description).
|
|
469
|
+
|
|
470
|
+
Automated grid placement will try to maintain square shape of grid
|
|
471
|
+
(with size being the nearest integer square root of #images), up to two exceptions:
|
|
472
|
+
* if there are up to 3 images - images will be displayed in single row
|
|
473
|
+
* if square-grid placement causes last row to be empty - number of rows is trimmed
|
|
474
|
+
until last row has at least one image
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
images (List[ImageType]): Images to create tiles. Elements can be either
|
|
478
|
+
np.ndarray or PIL.Image, common representation will be agreed by the
|
|
479
|
+
function.
|
|
480
|
+
grid_size (Optional[Tuple[Optional[int], Optional[int]]]): Expected grid
|
|
481
|
+
size in format (n_rows, n_cols). If not given - automated grid placement
|
|
482
|
+
will be applied. One may also provide only one out of two elements of the
|
|
483
|
+
tuple - then grid will be created with either n_rows or n_cols fixed,
|
|
484
|
+
leaving the other dimension to be adjusted by the number of images
|
|
485
|
+
single_tile_size (Optional[Tuple[int, int]]): sizeof a single tile element
|
|
486
|
+
provided in (width, height) format. If not given - size of tile will be
|
|
487
|
+
automatically calculated based on `tile_scaling` parameter.
|
|
488
|
+
tile_scaling (Literal["min", "max", "avg"]): If `single_tile_size` is not
|
|
489
|
+
given - parameter will be used to calculate tile size - using
|
|
490
|
+
min / max / avg size of image provided in `images` list.
|
|
491
|
+
tile_padding_color (Union[Tuple[int, int, int], sv.Color]): Color to be used in
|
|
492
|
+
images letterbox procedure (while standardising tiles sizes) as a padding.
|
|
493
|
+
If tuple provided - should be BGR.
|
|
494
|
+
tile_margin (int): size of margin between tiles (in pixels)
|
|
495
|
+
tile_margin_color (Union[Tuple[int, int, int], sv.Color]): Color of tile margin.
|
|
496
|
+
If tuple provided - should be BGR.
|
|
497
|
+
return_type (Literal["auto", "cv2", "pillow"]): Parameter dictates the format of
|
|
498
|
+
return image. One may choose specific type ("cv2" or "pillow") to enforce
|
|
499
|
+
conversion. "auto" mode takes a majority vote between types of elements in
|
|
500
|
+
`images` list - resolving draws in favour of OpenCV format. "auto" can be
|
|
501
|
+
safely used when all input images are of the same type.
|
|
502
|
+
titles (Optional[List[Optional[str]]]): Optional titles to be added to tiles.
|
|
503
|
+
Elements of that list may be empty - then specific tile (in order presented
|
|
504
|
+
in `images` parameter) will not be filled with title. It is possible to
|
|
505
|
+
provide list of titles shorter than `images` - then remaining titles will
|
|
506
|
+
be assumed empty.
|
|
507
|
+
titles_anchors (Optional[Union[Point, List[Optional[Point]]]]): Parameter to
|
|
508
|
+
specify anchor points for titles. It is possible to specify anchor either
|
|
509
|
+
globally or for specific tiles (following order of `images`).
|
|
510
|
+
If not given (either globally, or for specific element of the list),
|
|
511
|
+
it will be calculated automatically based on `default_title_placement`.
|
|
512
|
+
titles_color (Union[Tuple[int, int, int], Color]): Color of titles text.
|
|
513
|
+
If tuple provided - should be BGR.
|
|
514
|
+
titles_scale (Optional[float]): Scale of titles. If not provided - value will
|
|
515
|
+
be calculated using `calculate_optimal_text_scale(...)`.
|
|
516
|
+
titles_thickness (int): Thickness of titles text.
|
|
517
|
+
titles_padding (int): Size of titles padding.
|
|
518
|
+
titles_text_font (int): Font to be used to render titles. Must be integer
|
|
519
|
+
constant representing OpenCV font.
|
|
520
|
+
(See docs: https://docs.opencv.org/4.x/d6/d6e/group__imgproc__draw.html)
|
|
521
|
+
titles_background_color (Union[Tuple[int, int, int], Color]): Color of title
|
|
522
|
+
text padding.
|
|
523
|
+
default_title_placement (Literal["top", "bottom"]): Parameter specifies title
|
|
524
|
+
anchor placement in case if explicit anchor is not provided.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
ImageType: Image with all input images located in tails grid. The output type is
|
|
528
|
+
determined by `return_type` parameter.
|
|
529
|
+
|
|
530
|
+
Raises:
|
|
531
|
+
ValueError: In case when input images list is empty, provided `grid_size` is too
|
|
532
|
+
small to fit all images, `tile_scaling` mode is invalid.
|
|
533
|
+
"""
|
|
534
|
+
if len(images) == 0:
|
|
535
|
+
raise ValueError("Could not create image tiles from empty list of images.")
|
|
536
|
+
if return_type == "auto":
|
|
537
|
+
return_type = _negotiate_tiles_format(images=images)
|
|
538
|
+
tile_padding_color = unify_to_bgr(color=tile_padding_color)
|
|
539
|
+
tile_margin_color = unify_to_bgr(color=tile_margin_color)
|
|
540
|
+
images = images_to_cv2(images=images)
|
|
541
|
+
if single_tile_size is None:
|
|
542
|
+
single_tile_size = _aggregate_images_shape(images=images, mode=tile_scaling)
|
|
543
|
+
resized_images = [
|
|
544
|
+
letterbox_image(
|
|
545
|
+
image=i, resolution_wh=single_tile_size, color=tile_padding_color
|
|
546
|
+
)
|
|
547
|
+
for i in images
|
|
548
|
+
]
|
|
549
|
+
grid_size = _establish_grid_size(images=images, grid_size=grid_size)
|
|
550
|
+
if len(images) > grid_size[0] * grid_size[1]:
|
|
551
|
+
raise ValueError(
|
|
552
|
+
f"Could not place {len(images)} in grid with size: {grid_size}."
|
|
553
|
+
)
|
|
554
|
+
if titles is not None:
|
|
555
|
+
titles = fill(sequence=titles, desired_size=len(images), content=None)
|
|
556
|
+
titles_anchors = (
|
|
557
|
+
[titles_anchors]
|
|
558
|
+
if not issubclass(type(titles_anchors), list)
|
|
559
|
+
else titles_anchors
|
|
560
|
+
)
|
|
561
|
+
titles_anchors = fill(
|
|
562
|
+
sequence=titles_anchors, desired_size=len(images), content=None
|
|
563
|
+
)
|
|
564
|
+
titles_color = unify_to_bgr(color=titles_color)
|
|
565
|
+
titles_background_color = unify_to_bgr(color=titles_background_color)
|
|
566
|
+
tiles = _generate_tiles(
|
|
567
|
+
images=resized_images,
|
|
568
|
+
grid_size=grid_size,
|
|
569
|
+
single_tile_size=single_tile_size,
|
|
570
|
+
tile_padding_color=tile_padding_color,
|
|
571
|
+
tile_margin=tile_margin,
|
|
572
|
+
tile_margin_color=tile_margin_color,
|
|
573
|
+
titles=titles,
|
|
574
|
+
titles_anchors=titles_anchors,
|
|
575
|
+
titles_color=titles_color,
|
|
576
|
+
titles_scale=titles_scale,
|
|
577
|
+
titles_thickness=titles_thickness,
|
|
578
|
+
titles_padding=titles_padding,
|
|
579
|
+
titles_text_font=titles_text_font,
|
|
580
|
+
titles_background_color=titles_background_color,
|
|
581
|
+
default_title_placement=default_title_placement,
|
|
582
|
+
)
|
|
583
|
+
if return_type == "pillow":
|
|
584
|
+
tiles = cv2_to_pillow(image=tiles)
|
|
585
|
+
return tiles
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _negotiate_tiles_format(images: List[ImageType]) -> Literal["cv2", "pillow"]:
|
|
589
|
+
number_of_np_arrays = sum(issubclass(type(i), np.ndarray) for i in images)
|
|
590
|
+
if number_of_np_arrays >= (len(images) // 2):
|
|
591
|
+
return "cv2"
|
|
592
|
+
return "pillow"
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _calculate_aggregated_images_shape(
|
|
596
|
+
images: List[np.ndarray], aggregator: Callable[[List[int]], float]
|
|
597
|
+
) -> Tuple[int, int]:
|
|
598
|
+
height = round(aggregator([i.shape[0] for i in images]))
|
|
599
|
+
width = round(aggregator([i.shape[1] for i in images]))
|
|
600
|
+
return width, height
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
SHAPE_AGGREGATION_FUN = {
|
|
604
|
+
"min": partial(_calculate_aggregated_images_shape, aggregator=np.min),
|
|
605
|
+
"max": partial(_calculate_aggregated_images_shape, aggregator=np.max),
|
|
606
|
+
"avg": partial(_calculate_aggregated_images_shape, aggregator=np.average),
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _aggregate_images_shape(
|
|
611
|
+
images: List[np.ndarray], mode: Literal["min", "max", "avg"]
|
|
612
|
+
) -> Tuple[int, int]:
|
|
613
|
+
if mode not in SHAPE_AGGREGATION_FUN:
|
|
614
|
+
raise ValueError(
|
|
615
|
+
f"Could not aggregate images shape - provided unknown mode: {mode}. "
|
|
616
|
+
f"Supported modes: {list(SHAPE_AGGREGATION_FUN.keys())}."
|
|
617
|
+
)
|
|
618
|
+
return SHAPE_AGGREGATION_FUN[mode](images)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _establish_grid_size(
|
|
622
|
+
images: List[np.ndarray], grid_size: Optional[Tuple[Optional[int], Optional[int]]]
|
|
623
|
+
) -> Tuple[int, int]:
|
|
624
|
+
if grid_size is None or all(e is None for e in grid_size):
|
|
625
|
+
return _negotiate_grid_size(images=images)
|
|
626
|
+
if grid_size[0] is None:
|
|
627
|
+
return math.ceil(len(images) / grid_size[1]), grid_size[1]
|
|
628
|
+
if grid_size[1] is None:
|
|
629
|
+
return grid_size[0], math.ceil(len(images) / grid_size[0])
|
|
630
|
+
return grid_size
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _negotiate_grid_size(images: List[np.ndarray]) -> Tuple[int, int]:
|
|
634
|
+
if len(images) <= MAX_COLUMNS_FOR_SINGLE_ROW_GRID:
|
|
635
|
+
return 1, len(images)
|
|
636
|
+
nearest_sqrt = math.ceil(np.sqrt(len(images)))
|
|
637
|
+
proposed_columns = nearest_sqrt
|
|
638
|
+
proposed_rows = nearest_sqrt
|
|
639
|
+
while proposed_columns * (proposed_rows - 1) >= len(images):
|
|
640
|
+
proposed_rows -= 1
|
|
641
|
+
return proposed_rows, proposed_columns
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _generate_tiles(
|
|
645
|
+
images: List[np.ndarray],
|
|
646
|
+
grid_size: Tuple[int, int],
|
|
647
|
+
single_tile_size: Tuple[int, int],
|
|
648
|
+
tile_padding_color: Tuple[int, int, int],
|
|
649
|
+
tile_margin: int,
|
|
650
|
+
tile_margin_color: Tuple[int, int, int],
|
|
651
|
+
titles: Optional[List[Optional[str]]],
|
|
652
|
+
titles_anchors: List[Optional[Point]],
|
|
653
|
+
titles_color: Tuple[int, int, int],
|
|
654
|
+
titles_scale: Optional[float],
|
|
655
|
+
titles_thickness: int,
|
|
656
|
+
titles_padding: int,
|
|
657
|
+
titles_text_font: int,
|
|
658
|
+
titles_background_color: Tuple[int, int, int],
|
|
659
|
+
default_title_placement: RelativePosition,
|
|
660
|
+
) -> np.ndarray:
|
|
661
|
+
images = _draw_texts(
|
|
662
|
+
images=images,
|
|
663
|
+
titles=titles,
|
|
664
|
+
titles_anchors=titles_anchors,
|
|
665
|
+
titles_color=titles_color,
|
|
666
|
+
titles_scale=titles_scale,
|
|
667
|
+
titles_thickness=titles_thickness,
|
|
668
|
+
titles_padding=titles_padding,
|
|
669
|
+
titles_text_font=titles_text_font,
|
|
670
|
+
titles_background_color=titles_background_color,
|
|
671
|
+
default_title_placement=default_title_placement,
|
|
672
|
+
)
|
|
673
|
+
rows, columns = grid_size
|
|
674
|
+
tiles_elements = list(create_batches(sequence=images, batch_size=columns))
|
|
675
|
+
while len(tiles_elements[-1]) < columns:
|
|
676
|
+
tiles_elements[-1].append(
|
|
677
|
+
_generate_color_image(shape=single_tile_size, color=tile_padding_color)
|
|
678
|
+
)
|
|
679
|
+
while len(tiles_elements) < rows:
|
|
680
|
+
tiles_elements.append(
|
|
681
|
+
[_generate_color_image(shape=single_tile_size, color=tile_padding_color)]
|
|
682
|
+
* columns
|
|
683
|
+
)
|
|
684
|
+
return _merge_tiles_elements(
|
|
685
|
+
tiles_elements=tiles_elements,
|
|
686
|
+
grid_size=grid_size,
|
|
687
|
+
single_tile_size=single_tile_size,
|
|
688
|
+
tile_margin=tile_margin,
|
|
689
|
+
tile_margin_color=tile_margin_color,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _draw_texts(
|
|
694
|
+
images: List[np.ndarray],
|
|
695
|
+
titles: Optional[List[Optional[str]]],
|
|
696
|
+
titles_anchors: List[Optional[Point]],
|
|
697
|
+
titles_color: Tuple[int, int, int],
|
|
698
|
+
titles_scale: Optional[float],
|
|
699
|
+
titles_thickness: int,
|
|
700
|
+
titles_padding: int,
|
|
701
|
+
titles_text_font: int,
|
|
702
|
+
titles_background_color: Tuple[int, int, int],
|
|
703
|
+
default_title_placement: RelativePosition,
|
|
704
|
+
) -> List[np.ndarray]:
|
|
705
|
+
if titles is None:
|
|
706
|
+
return images
|
|
707
|
+
titles_anchors = _prepare_default_titles_anchors(
|
|
708
|
+
images=images,
|
|
709
|
+
titles_anchors=titles_anchors,
|
|
710
|
+
default_title_placement=default_title_placement,
|
|
711
|
+
)
|
|
712
|
+
if titles_scale is None:
|
|
713
|
+
image_height, image_width = images[0].shape[:2]
|
|
714
|
+
titles_scale = calculate_optimal_text_scale(
|
|
715
|
+
resolution_wh=(image_width, image_height)
|
|
716
|
+
)
|
|
717
|
+
result = []
|
|
718
|
+
for image, text, anchor in zip(images, titles, titles_anchors):
|
|
719
|
+
if text is None:
|
|
720
|
+
result.append(image)
|
|
721
|
+
continue
|
|
722
|
+
processed_image = draw_text(
|
|
723
|
+
scene=image,
|
|
724
|
+
text=text,
|
|
725
|
+
text_anchor=anchor,
|
|
726
|
+
text_color=Color.from_bgr_tuple(titles_color),
|
|
727
|
+
text_scale=titles_scale,
|
|
728
|
+
text_thickness=titles_thickness,
|
|
729
|
+
text_padding=titles_padding,
|
|
730
|
+
text_font=titles_text_font,
|
|
731
|
+
background_color=Color.from_bgr_tuple(titles_background_color),
|
|
732
|
+
)
|
|
733
|
+
result.append(processed_image)
|
|
734
|
+
return result
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _prepare_default_titles_anchors(
|
|
738
|
+
images: List[np.ndarray],
|
|
739
|
+
titles_anchors: List[Optional[Point]],
|
|
740
|
+
default_title_placement: RelativePosition,
|
|
741
|
+
) -> List[Point]:
|
|
742
|
+
result = []
|
|
743
|
+
for image, anchor in zip(images, titles_anchors):
|
|
744
|
+
if anchor is not None:
|
|
745
|
+
result.append(anchor)
|
|
746
|
+
continue
|
|
747
|
+
image_height, image_width = image.shape[:2]
|
|
748
|
+
if default_title_placement == "top":
|
|
749
|
+
default_anchor = Point(x=image_width / 2, y=image_height * 0.1)
|
|
750
|
+
else:
|
|
751
|
+
default_anchor = Point(x=image_width / 2, y=image_height * 0.9)
|
|
752
|
+
result.append(default_anchor)
|
|
753
|
+
return result
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _merge_tiles_elements(
|
|
757
|
+
tiles_elements: List[List[np.ndarray]],
|
|
758
|
+
grid_size: Tuple[int, int],
|
|
759
|
+
single_tile_size: Tuple[int, int],
|
|
760
|
+
tile_margin: int,
|
|
761
|
+
tile_margin_color: Tuple[int, int, int],
|
|
762
|
+
) -> np.ndarray:
|
|
763
|
+
vertical_padding = (
|
|
764
|
+
np.ones((single_tile_size[1], tile_margin, 3)) * tile_margin_color
|
|
765
|
+
)
|
|
766
|
+
merged_rows = [
|
|
767
|
+
np.concatenate(
|
|
768
|
+
list(
|
|
769
|
+
itertools.chain.from_iterable(
|
|
770
|
+
zip(row, [vertical_padding] * grid_size[1])
|
|
771
|
+
)
|
|
772
|
+
)[:-1],
|
|
773
|
+
axis=1,
|
|
774
|
+
)
|
|
775
|
+
for row in tiles_elements
|
|
776
|
+
]
|
|
777
|
+
row_width = merged_rows[0].shape[1]
|
|
778
|
+
horizontal_padding = (
|
|
779
|
+
np.ones((tile_margin, row_width, 3), dtype=np.uint8) * tile_margin_color
|
|
780
|
+
)
|
|
781
|
+
rows_with_paddings = []
|
|
782
|
+
for row in merged_rows:
|
|
783
|
+
rows_with_paddings.append(row)
|
|
784
|
+
rows_with_paddings.append(horizontal_padding)
|
|
785
|
+
return np.concatenate(
|
|
786
|
+
rows_with_paddings[:-1],
|
|
787
|
+
axis=0,
|
|
788
|
+
).astype(np.uint8)
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def _generate_color_image(
|
|
792
|
+
shape: Tuple[int, int], color: Tuple[int, int, int]
|
|
793
|
+
) -> np.ndarray:
|
|
794
|
+
return np.ones(shape[::-1] + (3,), dtype=np.uint8) * color
|