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/internal.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
import os
|
|
4
|
+
import warnings
|
|
5
|
+
from typing import Any, Callable, Generic, Optional, Set, TypeVar
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EyeWarnings(Warning):
|
|
9
|
+
"""Eye warning category.
|
|
10
|
+
Set the deprecation warnings visibility for eye library.
|
|
11
|
+
You can set the environment variable EYE_DEPRECATION_WARNING to '0' to
|
|
12
|
+
disable the deprecation warnings.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def format_warning(msg, category, filename, lineno, line=None):
|
|
19
|
+
"""
|
|
20
|
+
Format a warning the same way as the default formatter, but also include the
|
|
21
|
+
category name in the output.
|
|
22
|
+
"""
|
|
23
|
+
return f"{category.__name__}: {msg}\n"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
warnings.formatwarning = format_warning
|
|
27
|
+
|
|
28
|
+
if os.getenv("EYE_DEPRECATION_WARNING") == "0":
|
|
29
|
+
warnings.simplefilter("ignore", EyeWarnings)
|
|
30
|
+
else:
|
|
31
|
+
warnings.simplefilter("always", EyeWarnings)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def warn_deprecated(message: str):
|
|
35
|
+
"""
|
|
36
|
+
Issue a warning that a function is deprecated.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
message (str): The message to display when the function is called.
|
|
40
|
+
"""
|
|
41
|
+
warnings.warn(message, category=EyeWarnings, stacklevel=2)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def deprecated_parameter(
|
|
45
|
+
old_parameter: str,
|
|
46
|
+
new_parameter: str,
|
|
47
|
+
map_function: Callable = lambda x: x,
|
|
48
|
+
warning_message: str = "Warning: '{old_parameter}' in '{function_name}' is "
|
|
49
|
+
"deprecated: use '{new_parameter}' instead.",
|
|
50
|
+
**message_kwargs,
|
|
51
|
+
):
|
|
52
|
+
"""
|
|
53
|
+
A decorator to mark a function's parameter as deprecated and issue a warning when
|
|
54
|
+
used.
|
|
55
|
+
|
|
56
|
+
Parameters:
|
|
57
|
+
old_parameter (str): The name of the deprecated parameter.
|
|
58
|
+
new_parameter (str): The name of the parameter that should be used instead.
|
|
59
|
+
map_function (Callable): A function used to map the value of the old
|
|
60
|
+
parameter to the new parameter. Defaults to the identity function.
|
|
61
|
+
warning_message (str): The warning message to be displayed when the
|
|
62
|
+
deprecated parameter is used. Defaults to a generic warning message with
|
|
63
|
+
placeholders for the old parameter, new parameter, and function name.
|
|
64
|
+
**message_kwargs: Additional keyword arguments that can be used to customize
|
|
65
|
+
the warning message.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Callable: A decorator function that can be applied to mark a function's
|
|
69
|
+
parameter as deprecated.
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
```python
|
|
73
|
+
@deprecated_parameter(
|
|
74
|
+
old_parameter=<OLD_PARAMETER_NAME>,
|
|
75
|
+
new_parameter=<NEW_PARAMETER_NAME>
|
|
76
|
+
)
|
|
77
|
+
def example_function(<NEW_PARAMETER_NAME>):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
# call function using deprecated parameter
|
|
81
|
+
example_function(<OLD_PARAMETER_NAME>=<OLD_PARAMETER_VALUE>)
|
|
82
|
+
```
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def decorator(func):
|
|
86
|
+
@functools.wraps(func)
|
|
87
|
+
def wrapper(*args, **kwargs):
|
|
88
|
+
if old_parameter in kwargs:
|
|
89
|
+
if args and hasattr(args[0], "__class__"):
|
|
90
|
+
class_name = args[0].__class__.__name__
|
|
91
|
+
function_name = f"{class_name}.{func.__name__}"
|
|
92
|
+
else:
|
|
93
|
+
function_name = func.__name__
|
|
94
|
+
|
|
95
|
+
warn_deprecated(
|
|
96
|
+
message=warning_message.format(
|
|
97
|
+
function_name=function_name,
|
|
98
|
+
old_parameter=old_parameter,
|
|
99
|
+
new_parameter=new_parameter,
|
|
100
|
+
**message_kwargs,
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
kwargs[new_parameter] = map_function(kwargs.pop(old_parameter))
|
|
105
|
+
|
|
106
|
+
return func(*args, **kwargs)
|
|
107
|
+
|
|
108
|
+
return wrapper
|
|
109
|
+
|
|
110
|
+
return decorator
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def deprecated(reason: str):
|
|
114
|
+
def decorator(func):
|
|
115
|
+
@functools.wraps(func)
|
|
116
|
+
def wrapper(*args, **kwargs):
|
|
117
|
+
warn_deprecated(f"{func.__name__} is deprecated: {reason}")
|
|
118
|
+
return func(*args, **kwargs)
|
|
119
|
+
|
|
120
|
+
return wrapper
|
|
121
|
+
|
|
122
|
+
return decorator
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
T = TypeVar("T")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class classproperty(Generic[T]):
|
|
129
|
+
"""
|
|
130
|
+
A decorator that combines @classmethod and @property.
|
|
131
|
+
It allows a method to be accessed as a property of the class,
|
|
132
|
+
rather than an instance, similar to a classmethod.
|
|
133
|
+
|
|
134
|
+
Usage:
|
|
135
|
+
@classproperty
|
|
136
|
+
def my_method(cls):
|
|
137
|
+
...
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(self, fget: Callable[..., T]):
|
|
141
|
+
"""
|
|
142
|
+
Args:
|
|
143
|
+
The function that is called when the property is accessed.
|
|
144
|
+
"""
|
|
145
|
+
self.fget = fget
|
|
146
|
+
|
|
147
|
+
def __get__(self, owner_self: Any, owner_cls: Optional[type] = None) -> T:
|
|
148
|
+
"""
|
|
149
|
+
Override the __get__ method to return the result of the function call.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
owner_self: The instance through which the attribute was accessed, or None.
|
|
153
|
+
Irrelevant for class properties.
|
|
154
|
+
owner_cls: The class through which the attribute was accessed.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
The result of calling the function stored in 'fget' with 'owner_cls'.
|
|
158
|
+
"""
|
|
159
|
+
if self.fget is None:
|
|
160
|
+
raise AttributeError("unreadable attribute")
|
|
161
|
+
return self.fget(owner_cls)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_instance_variables(instance: Any, include_properties=False) -> Set[str]:
|
|
165
|
+
"""
|
|
166
|
+
Get the public variables of a class instance.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
instance (Any): The instance of a class
|
|
170
|
+
include_properties (bool): Whether to include properties in the result
|
|
171
|
+
|
|
172
|
+
Usage:
|
|
173
|
+
```python
|
|
174
|
+
detections = Detections(xyxy=np.array([1,2,3,4]))
|
|
175
|
+
variables = get_class_variables(detections)
|
|
176
|
+
# ["xyxy", "mask", "confidence", ..., "data"]
|
|
177
|
+
```
|
|
178
|
+
"""
|
|
179
|
+
if isinstance(instance, type):
|
|
180
|
+
raise ValueError("Only class instances are supported, not classes.")
|
|
181
|
+
|
|
182
|
+
fields = set(
|
|
183
|
+
(
|
|
184
|
+
name
|
|
185
|
+
for name, val in inspect.getmembers(instance)
|
|
186
|
+
if not callable(val) and not name.startswith("_")
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if not include_properties:
|
|
191
|
+
properties = set(
|
|
192
|
+
(
|
|
193
|
+
name
|
|
194
|
+
for name, val in inspect.getmembers(instance.__class__)
|
|
195
|
+
if isinstance(val, property)
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
fields -= properties
|
|
199
|
+
|
|
200
|
+
return fields
|
eye/utils/iterables.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from typing import Generator, Iterable, List, TypeVar
|
|
2
|
+
|
|
3
|
+
V = TypeVar("V")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_batches(
|
|
7
|
+
sequence: Iterable[V], batch_size: int
|
|
8
|
+
) -> Generator[List[V], None, None]:
|
|
9
|
+
"""
|
|
10
|
+
Provides a generator that yields chunks of the input sequence
|
|
11
|
+
of the size specified by the `batch_size` parameter. The last
|
|
12
|
+
chunk may be a smaller batch.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
sequence (Iterable[V]): The sequence to be split into batches.
|
|
16
|
+
batch_size (int): The expected size of a batch.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
(Generator[List[V], None, None]): A generator that yields chunks
|
|
20
|
+
of `sequence` of size `batch_size`, up to the length of
|
|
21
|
+
the input `sequence`.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
```python
|
|
25
|
+
list(create_batches([1, 2, 3, 4, 5], 2))
|
|
26
|
+
# [[1, 2], [3, 4], [5]]
|
|
27
|
+
|
|
28
|
+
list(create_batches("abcde", 3))
|
|
29
|
+
# [['a', 'b', 'c'], ['d', 'e']]
|
|
30
|
+
```
|
|
31
|
+
"""
|
|
32
|
+
batch_size = max(batch_size, 1)
|
|
33
|
+
current_batch = []
|
|
34
|
+
for element in sequence:
|
|
35
|
+
if len(current_batch) == batch_size:
|
|
36
|
+
yield current_batch
|
|
37
|
+
current_batch = []
|
|
38
|
+
current_batch.append(element)
|
|
39
|
+
if current_batch:
|
|
40
|
+
yield current_batch
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def fill(sequence: List[V], desired_size: int, content: V) -> List[V]:
|
|
44
|
+
"""
|
|
45
|
+
Fill the sequence with padding elements until the sequence reaches
|
|
46
|
+
the desired size.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
sequence (List[V]): The input sequence.
|
|
50
|
+
desired_size (int): The expected size of the output list. The
|
|
51
|
+
difference between this value and the actual length of `sequence`
|
|
52
|
+
(if positive) dictates how many elements will be added as padding.
|
|
53
|
+
content (V): The element to be placed at the end of the input
|
|
54
|
+
`sequence` as padding.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
(List[V]): A padded version of the input `sequence` (if needed).
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
```python
|
|
61
|
+
fill([1, 2], 4, 0)
|
|
62
|
+
# [1, 2, 0, 0]
|
|
63
|
+
|
|
64
|
+
fill(['a', 'b'], 3, 'c')
|
|
65
|
+
# ['a', 'b', 'c']
|
|
66
|
+
```
|
|
67
|
+
"""
|
|
68
|
+
missing_size = max(0, desired_size - len(sequence))
|
|
69
|
+
sequence.extend([content] * missing_size)
|
|
70
|
+
return sequence
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def find_duplicates(sequence: List) -> List:
|
|
74
|
+
"""
|
|
75
|
+
Find all duplicate elements in the input sequence.
|
|
76
|
+
"""
|
|
77
|
+
seen = set()
|
|
78
|
+
duplicates = set()
|
|
79
|
+
for element in sequence:
|
|
80
|
+
if element in seen:
|
|
81
|
+
duplicates.add(element)
|
|
82
|
+
else:
|
|
83
|
+
seen.add(element)
|
|
84
|
+
return list(duplicates)
|
eye/utils/notebook.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from typing import List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
from PIL import Image
|
|
6
|
+
|
|
7
|
+
from eye.annotators.base import ImageType
|
|
8
|
+
from eye.utils.conversion import pillow_to_cv2
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def plot_image(
|
|
12
|
+
image: ImageType, size: Tuple[int, int] = (12, 12), cmap: Optional[str] = "gray"
|
|
13
|
+
) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Plots image using matplotlib.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
image (ImageType): The frame to be displayed ImageType
|
|
19
|
+
is a flexible type, accepting either `numpy.ndarray` or `PIL.Image.Image`.
|
|
20
|
+
size (Tuple[int, int]): The size of the plot in inches.
|
|
21
|
+
cmap (str): the colormap to use for single channel images.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
```python
|
|
25
|
+
import cv2
|
|
26
|
+
import eye as sv
|
|
27
|
+
|
|
28
|
+
image = cv2.imread("path/to/image.jpg")
|
|
29
|
+
|
|
30
|
+
%matplotlib inline
|
|
31
|
+
sv.plot_image(image=image, size=(16, 16))
|
|
32
|
+
```
|
|
33
|
+
"""
|
|
34
|
+
if isinstance(image, Image.Image):
|
|
35
|
+
image = pillow_to_cv2(image)
|
|
36
|
+
|
|
37
|
+
plt.figure(figsize=size)
|
|
38
|
+
|
|
39
|
+
if image.ndim == 2:
|
|
40
|
+
plt.imshow(image, cmap=cmap)
|
|
41
|
+
else:
|
|
42
|
+
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
|
43
|
+
|
|
44
|
+
plt.axis("off")
|
|
45
|
+
plt.show()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def plot_images_grid(
|
|
49
|
+
images: List[ImageType],
|
|
50
|
+
grid_size: Tuple[int, int],
|
|
51
|
+
titles: Optional[List[str]] = None,
|
|
52
|
+
size: Tuple[int, int] = (12, 12),
|
|
53
|
+
cmap: Optional[str] = "gray",
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Plots images in a grid using matplotlib.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
images (List[ImageType]): A list of images as ImageType
|
|
60
|
+
is a flexible type, accepting either `numpy.ndarray` or `PIL.Image.Image`.
|
|
61
|
+
grid_size (Tuple[int, int]): A tuple specifying the number
|
|
62
|
+
of rows and columns for the grid.
|
|
63
|
+
titles (Optional[List[str]]): A list of titles for each image.
|
|
64
|
+
Defaults to None.
|
|
65
|
+
size (Tuple[int, int]): A tuple specifying the width and
|
|
66
|
+
height of the entire plot in inches.
|
|
67
|
+
cmap (str): the colormap to use for single channel images.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ValueError: If the number of images exceeds the grid size.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
```python
|
|
74
|
+
import cv2
|
|
75
|
+
import eye as sv
|
|
76
|
+
from PIL import Image
|
|
77
|
+
|
|
78
|
+
image1 = cv2.imread("path/to/image1.jpg")
|
|
79
|
+
image2 = Image.open("path/to/image2.jpg")
|
|
80
|
+
image3 = cv2.imread("path/to/image3.jpg")
|
|
81
|
+
|
|
82
|
+
images = [image1, image2, image3]
|
|
83
|
+
titles = ["Image 1", "Image 2", "Image 3"]
|
|
84
|
+
|
|
85
|
+
%matplotlib inline
|
|
86
|
+
plot_images_grid(images, grid_size=(2, 2), titles=titles, size=(16, 16))
|
|
87
|
+
```
|
|
88
|
+
"""
|
|
89
|
+
nrows, ncols = grid_size
|
|
90
|
+
|
|
91
|
+
for idx, img in enumerate(images):
|
|
92
|
+
if isinstance(img, Image.Image):
|
|
93
|
+
images[idx] = pillow_to_cv2(img)
|
|
94
|
+
|
|
95
|
+
if len(images) > nrows * ncols:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"The number of images exceeds the grid size. Please increase the grid size"
|
|
98
|
+
" or reduce the number of images."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=size)
|
|
102
|
+
|
|
103
|
+
for idx, ax in enumerate(axes.flat):
|
|
104
|
+
if idx < len(images):
|
|
105
|
+
if images[idx].ndim == 2:
|
|
106
|
+
ax.imshow(images[idx], cmap=cmap)
|
|
107
|
+
else:
|
|
108
|
+
ax.imshow(cv2.cvtColor(images[idx], cv2.COLOR_BGR2RGB))
|
|
109
|
+
|
|
110
|
+
if titles is not None and idx < len(titles):
|
|
111
|
+
ax.set_title(titles[idx])
|
|
112
|
+
|
|
113
|
+
ax.axis("off")
|
|
114
|
+
plt.show()
|
eye/utils/video.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections import deque
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Callable, Generator, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
import cv2
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class VideoInfo:
|
|
14
|
+
"""
|
|
15
|
+
A class to store video information, including width, height, fps and
|
|
16
|
+
total number of frames.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
width (int): width of the video in pixels
|
|
20
|
+
height (int): height of the video in pixels
|
|
21
|
+
fps (int): frames per second of the video
|
|
22
|
+
total_frames (Optional[int]): total number of frames in the video,
|
|
23
|
+
default is None
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
```python
|
|
27
|
+
import eye as sv
|
|
28
|
+
|
|
29
|
+
video_info = sv.VideoInfo.from_video_path(video_path=<SOURCE_VIDEO_FILE>)
|
|
30
|
+
|
|
31
|
+
video_info
|
|
32
|
+
# VideoInfo(width=3840, height=2160, fps=25, total_frames=538)
|
|
33
|
+
|
|
34
|
+
video_info.resolution_wh
|
|
35
|
+
# (3840, 2160)
|
|
36
|
+
```
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
width: int
|
|
40
|
+
height: int
|
|
41
|
+
fps: int
|
|
42
|
+
total_frames: Optional[int] = None
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_video_path(cls, video_path: str) -> VideoInfo:
|
|
46
|
+
video = cv2.VideoCapture(video_path)
|
|
47
|
+
if not video.isOpened():
|
|
48
|
+
raise Exception(f"Could not open video at {video_path}")
|
|
49
|
+
|
|
50
|
+
width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
51
|
+
height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
52
|
+
fps = int(video.get(cv2.CAP_PROP_FPS))
|
|
53
|
+
total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
54
|
+
video.release()
|
|
55
|
+
return VideoInfo(width, height, fps, total_frames)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def resolution_wh(self) -> Tuple[int, int]:
|
|
59
|
+
return self.width, self.height
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class VideoSink:
|
|
63
|
+
"""
|
|
64
|
+
Context manager that saves video frames to a file using OpenCV.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
target_path (str): The path to the output file where the video will be saved.
|
|
68
|
+
video_info (VideoInfo): Information about the video resolution, fps,
|
|
69
|
+
and total frame count.
|
|
70
|
+
codec (str): FOURCC code for video format
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
```python
|
|
74
|
+
import eye as sv
|
|
75
|
+
|
|
76
|
+
video_info = sv.VideoInfo.from_video_path(<SOURCE_VIDEO_PATH>)
|
|
77
|
+
frames_generator = sv.get_video_frames_generator(<SOURCE_VIDEO_PATH>)
|
|
78
|
+
|
|
79
|
+
with sv.VideoSink(target_path=<TARGET_VIDEO_PATH>, video_info=video_info) as sink:
|
|
80
|
+
for frame in frames_generator:
|
|
81
|
+
sink.write_frame(frame=frame)
|
|
82
|
+
```
|
|
83
|
+
""" # noqa: E501 // docs
|
|
84
|
+
|
|
85
|
+
def __init__(self, target_path: str, video_info: VideoInfo, codec: str = "mp4v"):
|
|
86
|
+
self.target_path = target_path
|
|
87
|
+
self.video_info = video_info
|
|
88
|
+
self.__codec = codec
|
|
89
|
+
self.__writer = None
|
|
90
|
+
|
|
91
|
+
def __enter__(self):
|
|
92
|
+
try:
|
|
93
|
+
self.__fourcc = cv2.VideoWriter_fourcc(*self.__codec)
|
|
94
|
+
except TypeError as e:
|
|
95
|
+
print(str(e) + ". Defaulting to mp4v...")
|
|
96
|
+
self.__fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
97
|
+
self.__writer = cv2.VideoWriter(
|
|
98
|
+
self.target_path,
|
|
99
|
+
self.__fourcc,
|
|
100
|
+
self.video_info.fps,
|
|
101
|
+
self.video_info.resolution_wh,
|
|
102
|
+
)
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def write_frame(self, frame: np.ndarray):
|
|
106
|
+
"""
|
|
107
|
+
Writes a single video frame to the target video file.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
frame (np.ndarray): The video frame to be written to the file. The frame
|
|
111
|
+
must be in BGR color format.
|
|
112
|
+
"""
|
|
113
|
+
self.__writer.write(frame)
|
|
114
|
+
|
|
115
|
+
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
116
|
+
self.__writer.release()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class VideoWriter:
|
|
120
|
+
"""Compatibility wrapper for OpenCV VideoWriter.
|
|
121
|
+
|
|
122
|
+
Some scripts expect an object with `.write(frame)` and `.release()` methods.
|
|
123
|
+
This wrapper keeps that API while staying consistent with Eye's video utils.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(self, target_path: str, fps: int, resolution: Tuple[int, int], codec: str = "mp4v"):
|
|
127
|
+
self.target_path = target_path
|
|
128
|
+
self.fps = int(fps)
|
|
129
|
+
self.resolution = (int(resolution[0]), int(resolution[1]))
|
|
130
|
+
try:
|
|
131
|
+
fourcc = cv2.VideoWriter_fourcc(*codec)
|
|
132
|
+
except TypeError:
|
|
133
|
+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
134
|
+
self._writer = cv2.VideoWriter(self.target_path, fourcc, self.fps, self.resolution)
|
|
135
|
+
|
|
136
|
+
def write(self, frame: np.ndarray) -> None:
|
|
137
|
+
self._writer.write(frame)
|
|
138
|
+
|
|
139
|
+
def release(self) -> None:
|
|
140
|
+
self._writer.release()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _validate_and_setup_video(
|
|
144
|
+
source_path: str, start: int, end: Optional[int], iterative_seek: bool = False
|
|
145
|
+
):
|
|
146
|
+
video = cv2.VideoCapture(source_path)
|
|
147
|
+
if not video.isOpened():
|
|
148
|
+
raise Exception(f"Could not open video at {source_path}")
|
|
149
|
+
total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
150
|
+
if end is not None and end > total_frames:
|
|
151
|
+
raise Exception("Requested frames are outbound")
|
|
152
|
+
start = max(start, 0)
|
|
153
|
+
end = min(end, total_frames) if end is not None else total_frames
|
|
154
|
+
|
|
155
|
+
if iterative_seek:
|
|
156
|
+
while start > 0:
|
|
157
|
+
success = video.grab()
|
|
158
|
+
if not success:
|
|
159
|
+
break
|
|
160
|
+
start -= 1
|
|
161
|
+
elif start > 0:
|
|
162
|
+
video.set(cv2.CAP_PROP_POS_FRAMES, start)
|
|
163
|
+
|
|
164
|
+
return video, start, end
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_video_frames_generator(
|
|
168
|
+
source_path: str,
|
|
169
|
+
stride: int = 1,
|
|
170
|
+
start: int = 0,
|
|
171
|
+
end: Optional[int] = None,
|
|
172
|
+
iterative_seek: bool = False,
|
|
173
|
+
) -> Generator[np.ndarray, None, None]:
|
|
174
|
+
"""
|
|
175
|
+
Get a generator that yields the frames of the video.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
source_path (str): The path of the video file.
|
|
179
|
+
stride (int): Indicates the interval at which frames are returned,
|
|
180
|
+
skipping stride - 1 frames between each.
|
|
181
|
+
start (int): Indicates the starting position from which
|
|
182
|
+
video should generate frames
|
|
183
|
+
end (Optional[int]): Indicates the ending position at which video
|
|
184
|
+
should stop generating frames. If None, video will be read to the end.
|
|
185
|
+
iterative_seek (bool): If True, the generator will seek to the
|
|
186
|
+
`start` frame by grabbing each frame, which is much slower. This is a
|
|
187
|
+
workaround for videos that don't open at all when you set the `start` value.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
(Generator[np.ndarray, None, None]): A generator that yields the
|
|
191
|
+
frames of the video.
|
|
192
|
+
|
|
193
|
+
Examples:
|
|
194
|
+
```python
|
|
195
|
+
import eye as sv
|
|
196
|
+
|
|
197
|
+
for frame in sv.get_video_frames_generator(source_path=<SOURCE_VIDEO_PATH>):
|
|
198
|
+
...
|
|
199
|
+
```
|
|
200
|
+
"""
|
|
201
|
+
video, start, end = _validate_and_setup_video(
|
|
202
|
+
source_path, start, end, iterative_seek
|
|
203
|
+
)
|
|
204
|
+
frame_position = start
|
|
205
|
+
while True:
|
|
206
|
+
success, frame = video.read()
|
|
207
|
+
if not success or frame_position >= end:
|
|
208
|
+
break
|
|
209
|
+
yield frame
|
|
210
|
+
for _ in range(stride - 1):
|
|
211
|
+
success = video.grab()
|
|
212
|
+
if not success:
|
|
213
|
+
break
|
|
214
|
+
frame_position += stride
|
|
215
|
+
video.release()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def process_video(
|
|
219
|
+
source_path: str,
|
|
220
|
+
target_path: str,
|
|
221
|
+
callback: Callable[[np.ndarray, int], np.ndarray],
|
|
222
|
+
) -> None:
|
|
223
|
+
"""
|
|
224
|
+
Process a video file by applying a callback function on each frame
|
|
225
|
+
and saving the result to a target video file.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
source_path (str): The path to the source video file.
|
|
229
|
+
target_path (str): The path to the target video file.
|
|
230
|
+
callback (Callable[[np.ndarray, int], np.ndarray]): A function that takes in
|
|
231
|
+
a numpy ndarray representation of a video frame and an
|
|
232
|
+
int index of the frame and returns a processed numpy ndarray
|
|
233
|
+
representation of the frame.
|
|
234
|
+
|
|
235
|
+
Examples:
|
|
236
|
+
```python
|
|
237
|
+
import eye as sv
|
|
238
|
+
|
|
239
|
+
def callback(scene: np.ndarray, index: int) -> np.ndarray:
|
|
240
|
+
...
|
|
241
|
+
|
|
242
|
+
process_video(
|
|
243
|
+
source_path=<SOURCE_VIDEO_PATH>,
|
|
244
|
+
target_path=<TARGET_VIDEO_PATH>,
|
|
245
|
+
callback=callback
|
|
246
|
+
)
|
|
247
|
+
```
|
|
248
|
+
"""
|
|
249
|
+
source_video_info = VideoInfo.from_video_path(video_path=source_path)
|
|
250
|
+
with VideoSink(target_path=target_path, video_info=source_video_info) as sink:
|
|
251
|
+
for index, frame in enumerate(
|
|
252
|
+
get_video_frames_generator(source_path=source_path)
|
|
253
|
+
):
|
|
254
|
+
result_frame = callback(frame, index)
|
|
255
|
+
sink.write_frame(frame=result_frame)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class FPSMonitor:
|
|
259
|
+
"""
|
|
260
|
+
A class for monitoring frames per second (FPS) to benchmark latency.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
def __init__(self, sample_size: int = 30):
|
|
264
|
+
"""
|
|
265
|
+
Args:
|
|
266
|
+
sample_size (int): The maximum number of observations for latency
|
|
267
|
+
benchmarking.
|
|
268
|
+
|
|
269
|
+
Examples:
|
|
270
|
+
```python
|
|
271
|
+
import eye as sv
|
|
272
|
+
|
|
273
|
+
frames_generator = sv.get_video_frames_generator(source_path=<SOURCE_FILE_PATH>)
|
|
274
|
+
fps_monitor = sv.FPSMonitor()
|
|
275
|
+
|
|
276
|
+
for frame in frames_generator:
|
|
277
|
+
# your processing code here
|
|
278
|
+
fps_monitor.tick()
|
|
279
|
+
fps = fps_monitor.fps
|
|
280
|
+
```
|
|
281
|
+
""" # noqa: E501 // docs
|
|
282
|
+
self.all_timestamps = deque(maxlen=sample_size)
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def fps(self) -> float:
|
|
286
|
+
"""
|
|
287
|
+
Computes and returns the average FPS based on the stored time stamps.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
float: The average FPS. Returns 0.0 if no time stamps are stored.
|
|
291
|
+
"""
|
|
292
|
+
if not self.all_timestamps:
|
|
293
|
+
return 0.0
|
|
294
|
+
taken_time = self.all_timestamps[-1] - self.all_timestamps[0]
|
|
295
|
+
return (len(self.all_timestamps)) / taken_time if taken_time != 0 else 0.0
|
|
296
|
+
|
|
297
|
+
def tick(self) -> None:
|
|
298
|
+
"""
|
|
299
|
+
Adds a new time stamp to the deque for FPS calculation.
|
|
300
|
+
"""
|
|
301
|
+
self.all_timestamps.append(time.monotonic())
|
|
302
|
+
|
|
303
|
+
def reset(self) -> None:
|
|
304
|
+
"""
|
|
305
|
+
Clears all the time stamps from the deque.
|
|
306
|
+
"""
|
|
307
|
+
self.all_timestamps.clear()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utils."""
|