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.
Files changed (94) hide show
  1. eye/__init__.py +115 -0
  2. eye/__init___supervision_original.py +120 -0
  3. eye/annotators/__init__.py +0 -0
  4. eye/annotators/base.py +22 -0
  5. eye/annotators/core.py +2699 -0
  6. eye/annotators/line.py +107 -0
  7. eye/annotators/modern.py +529 -0
  8. eye/annotators/trace.py +142 -0
  9. eye/annotators/utils.py +177 -0
  10. eye/assets/__init__.py +2 -0
  11. eye/assets/downloader.py +95 -0
  12. eye/assets/list.py +83 -0
  13. eye/classification/__init__.py +0 -0
  14. eye/classification/core.py +188 -0
  15. eye/config.py +2 -0
  16. eye/core/__init__.py +0 -0
  17. eye/core/trackers/__init__.py +1 -0
  18. eye/core/trackers/botsort_tracker.py +336 -0
  19. eye/core/trackers/bytetrack_tracker.py +284 -0
  20. eye/core/trackers/sort_tracker.py +200 -0
  21. eye/core/tracking.py +146 -0
  22. eye/dataset/__init__.py +0 -0
  23. eye/dataset/core.py +919 -0
  24. eye/dataset/formats/__init__.py +0 -0
  25. eye/dataset/formats/coco.py +258 -0
  26. eye/dataset/formats/pascal_voc.py +279 -0
  27. eye/dataset/formats/yolo.py +272 -0
  28. eye/dataset/utils.py +259 -0
  29. eye/detection/__init__.py +0 -0
  30. eye/detection/auto_convert.py +155 -0
  31. eye/detection/core.py +1529 -0
  32. eye/detection/detections_enhanced.py +392 -0
  33. eye/detection/line_zone.py +859 -0
  34. eye/detection/lmm.py +184 -0
  35. eye/detection/overlap_filter.py +270 -0
  36. eye/detection/tools/__init__.py +0 -0
  37. eye/detection/tools/csv_sink.py +181 -0
  38. eye/detection/tools/inference_slicer.py +288 -0
  39. eye/detection/tools/json_sink.py +142 -0
  40. eye/detection/tools/polygon_zone.py +202 -0
  41. eye/detection/tools/smoother.py +123 -0
  42. eye/detection/tools/smoothing.py +179 -0
  43. eye/detection/tools/smoothing_config.py +202 -0
  44. eye/detection/tools/transformers.py +247 -0
  45. eye/detection/utils.py +1175 -0
  46. eye/draw/__init__.py +0 -0
  47. eye/draw/color.py +154 -0
  48. eye/draw/utils.py +374 -0
  49. eye/filters.py +112 -0
  50. eye/geometry/__init__.py +0 -0
  51. eye/geometry/core.py +128 -0
  52. eye/geometry/utils.py +47 -0
  53. eye/keypoint/__init__.py +0 -0
  54. eye/keypoint/annotators.py +442 -0
  55. eye/keypoint/core.py +687 -0
  56. eye/keypoint/skeletons.py +2647 -0
  57. eye/metrics/__init__.py +21 -0
  58. eye/metrics/core.py +72 -0
  59. eye/metrics/detection.py +843 -0
  60. eye/metrics/f1_score.py +648 -0
  61. eye/metrics/mean_average_precision.py +628 -0
  62. eye/metrics/mean_average_recall.py +697 -0
  63. eye/metrics/precision.py +653 -0
  64. eye/metrics/recall.py +652 -0
  65. eye/metrics/utils/__init__.py +0 -0
  66. eye/metrics/utils/object_size.py +158 -0
  67. eye/metrics/utils/utils.py +9 -0
  68. eye/py.typed +0 -0
  69. eye/quick.py +104 -0
  70. eye/tracker/__init__.py +0 -0
  71. eye/tracker/byte_tracker/__init__.py +0 -0
  72. eye/tracker/byte_tracker/core.py +386 -0
  73. eye/tracker/byte_tracker/kalman_filter.py +205 -0
  74. eye/tracker/byte_tracker/matching.py +69 -0
  75. eye/tracker/byte_tracker/single_object_track.py +178 -0
  76. eye/tracker/byte_tracker/utils.py +18 -0
  77. eye/utils/__init__.py +0 -0
  78. eye/utils/conversion.py +132 -0
  79. eye/utils/file.py +159 -0
  80. eye/utils/image.py +794 -0
  81. eye/utils/internal.py +200 -0
  82. eye/utils/iterables.py +84 -0
  83. eye/utils/notebook.py +114 -0
  84. eye/utils/video.py +307 -0
  85. eye/utils_eye/__init__.py +1 -0
  86. eye/utils_eye/geometry.py +71 -0
  87. eye/utils_eye/nms.py +55 -0
  88. eye/validators/__init__.py +140 -0
  89. eye/web.py +271 -0
  90. eye_cv-1.0.0.dist-info/METADATA +319 -0
  91. eye_cv-1.0.0.dist-info/RECORD +94 -0
  92. eye_cv-1.0.0.dist-info/WHEEL +5 -0
  93. eye_cv-1.0.0.dist-info/licenses/LICENSE +21 -0
  94. 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
+ ![crop_image](https://media.roboflow.com/eye-docs/crop-image.png){ 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
+ ![resize_image](https://media.roboflow.com/eye-docs/resize-image.png){ 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
+ ![letterbox_image](https://media.roboflow.com/eye-docs/letterbox-image.png){ 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
+ ![overlay_image](https://media.roboflow.com/eye-docs/overlay-image.png){ 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