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/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."""