From 1561bd0027c4e484a3229f584176b2046d1f640a Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Tue, 31 Jan 2023 09:36:39 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=AC=20poc=20of=20video=20utils=20docum?= =?UTF-8?q?entation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yml | 18 ++++ .github/workflows/welcome.yml | 2 + README.md | 29 +++--- docs/index.md | 36 ++++++++ docs/video.md | 15 +++ mkdocs.yml | 20 +++- setup.py | 3 +- supervision/video.py | 151 +++++++++++++++++++++++++++++++ supervision/video/__init__.py | 0 supervision/video/dataclasses.py | 46 ---------- supervision/video/sink.py | 48 ---------- supervision/video/source.py | 20 ---- 12 files changed, 258 insertions(+), 130 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/video.md create mode 100644 supervision/video.py delete mode 100644 supervision/video/__init__.py delete mode 100644 supervision/video/dataclasses.py delete mode 100644 supervision/video/sink.py delete mode 100644 supervision/video/source.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..16d41246 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,18 @@ +name: Docs WorkFlow + +on: + push: + branches: + - master + - main +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.x + - run: pip install mkdocs-material + - run: pip install "mkdocstrings[python]" + - run: mkdocs gh-deploy --force \ No newline at end of file diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml index c9bb3f01..a993236c 100644 --- a/.github/workflows/welcome.yml +++ b/.github/workflows/welcome.yml @@ -1,3 +1,5 @@ +name: Welcome WorkFlow + on: issues: types: [opened] diff --git a/README.md b/README.md index eb98fb2e..8d73eff3 100644 --- a/README.md +++ b/README.md @@ -61,18 +61,6 @@ A set of easy-to-use utils that will come in handy in any Computer Vision projec pre-release stage. ๐Ÿšง Keep your eyes open for potential bugs and be aware that at this stage our API is still fluid and may change. -## ๐ŸŽฌ videos - -Learn how to use YOLOv8, ByteTrack and **Supervision** to detect, track and count objects. ๐Ÿ”ฅ -[Subscribe](https://www.youtube.com/@Roboflow), and stay up to date with our latest YouTube videos! - -

- - latest-roboflow-tutorial - -

- - ## ๐Ÿ’ป install Pip install the supervision package in a @@ -100,9 +88,18 @@ pip install -e ".[dev]" +## ๐ŸŽฌ videos +Learn how to use YOLOv8, ByteTrack and **Supervision** to detect, track and count objects. ๐Ÿ”ฅ +[Subscribe](https://www.youtube.com/@Roboflow), and stay up to date with our latest YouTube videos! -## ๐Ÿงน Code Quality +

+ + latest-roboflow-tutorial + +

+ +## ๐Ÿงน code quality We provide two handy commands inside the `Makefile`, namely: @@ -111,6 +108,10 @@ We provide two handy commands inside the `Makefile`, namely: So far, **there is no types checking with mypy**. See [issue](https://github.com/roboflow-ai/template-python/issues/4). -## ๐Ÿงช Tests +## ๐Ÿงช tests [`pytests`](https://docs.pytest.org/en/7.1.x/) is used to run our tests. + +## ๐Ÿชช + +Supervision is available under the MIT license - see the [LICENSE](https://github.com/roboflow/supervision/blob/main/LICENSE.md) file for details. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index e69de29b..3f1233af 100644 --- a/docs/index.md +++ b/docs/index.md @@ -0,0 +1,36 @@ +
+

+ + + +

+
+ +## ๐Ÿ‘‹ hello + +A set of easy-to-use utils that will come in handy in any Computer Vision project. **Supervision** is still in +pre-release stage. ๐Ÿšง Keep your eyes open for potential bugs and be aware that at this stage our API is still fluid +and may change. + +## ๐Ÿ’ป install + +Pip install the supervision package in a +[**3.10>=Python>=3.7**](https://www.python.org/) environment. + +!!! example "Pip install method (recommended)" + + ```bash + pip install subervision + ``` + +!!! example "Git clone method (for development)" + + ```bash + git https://github.com/roboflow/supervision.git + cd supervision + pip install -e '.[dev]' + ``` + See contributing section to know more about contributing to the project \ No newline at end of file diff --git a/docs/video.md b/docs/video.md new file mode 100644 index 00000000..baf61541 --- /dev/null +++ b/docs/video.md @@ -0,0 +1,15 @@ +## VideoInfo + +:::supervision.video.VideoInfo + +## VideoSink + +:::supervision.video.VideoSink + +## get_video_frames_generator + +:::supervision.video.get_video_frames_generator + +## process_video + +:::supervision.video.process_video \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 254462a3..46c257de 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -site_name: supervision +site_name: Supervision site_url: https://roboflow.github.io/supervision site_author: Roboflow site_description: A set of easy-to-use utils that will come in handy in any Computer Vision project @@ -9,17 +9,35 @@ copyright: Roboflow 2023. All rights reserved. extra: social: + - icon: fontawesome/brands/github + link: https://github.com/roboflow - icon: fontawesome/brands/youtube link: https://www.youtube.com/roboflow + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/company/roboflow-ai/mycompany/ + - icon: fontawesome/brands/twitter + link: https://twitter.com/roboflow nav: - Home: index.md + - Video: video.md theme: name: 'material' + logo: https://raw.githubusercontent.com/roboflow/supervision/main/docs/assets/roboflow_logomark_white.svg + favicon: https://raw.githubusercontent.com/roboflow/supervision/main/docs/assets/roboflow_logomark_color.svg palette: primary: 'deep purple' accent: 'teal' font: text: Roboto code: Roboto Mono + +plugins: + - mkdocstrings + - search + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences diff --git a/setup.py b/setup.py index fa3a8824..6afc4eaa 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,8 @@ setuptools.setup( 'pytest', 'wheel', 'notebook', - 'mkdocs-material' + 'mkdocs-material', + 'mkdocstrings[python]' ], }, classifiers=[ diff --git a/supervision/video.py b/supervision/video.py new file mode 100644 index 00000000..20046535 --- /dev/null +++ b/supervision/video.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +from typing import Callable, Generator, Optional, Tuple + +import cv2 +import numpy as np + + +class VideoInfo: + """ + A class to store video information, including width, height, fps and total number of frames. + + Attributes: + width (int): width of the video in pixels + height (int): height of the video in pixels + fps (int): frames per second of the video + total_frames (int, optional): total number of frames in the video, default is None + + Examples: + ```python + >>> from supervision.video import VideoInfo + + >>> video_info = VideoInfo.from_video_path(video_path='video.mp4') + + >>> video_info + VideoInfo(width=3840, height=2160, fps=25, total_frames=538) + ``` + """ + def __init__(self, width: int, height: int, fps: int, total_frames: Optional[int] = None): + self.width = width + self.height = height + self.fps = fps + self.total_frames = total_frames + + @classmethod + def from_video_path(cls, video_path: str) -> VideoInfo: + video = cv2.VideoCapture(video_path) + if not video.isOpened(): + raise Exception(f"Could not open video at {video_path}") + + width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT)) + fps = int(video.get(cv2.CAP_PROP_FPS)) + total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) + video.release() + return VideoInfo(width, height, fps, total_frames) + + @property + def resolution(self) -> Tuple[int, int]: + return self.width, self.height + + +class VideoSink: + """ + Context manager that saves video frames to a file using OpenCV. + + Attributes: + target_path (str): The path to the output file where the video will be saved. + video_info (VideoInfo): Information about the video resolution, fps, and total frame count. + + Examples: + ```python + >>> from supervision.video import VideoInfo + >>> from supervision.video import VideoSink + + >>> video_info = VideoInfo.from_video_path(video_path='source_video.mp4') + + >>> with VideoSink(target_path='target_video.mp4', video_info=video_info) as s: + ... frame = ... + ... s.write_frame(frame=frame) + ``` + """ + + def __init__(self, target_path: str, video_info: VideoInfo): + self.target_path = target_path + self.video_info = video_info + self.__fourcc = cv2.VideoWriter_fourcc(*"mp4v") + self.__writer = None + + def __enter__(self): + self.__writer = cv2.VideoWriter( + self.target_path, + self.__fourcc, + self.video_info.fps, + self.video_info.resolution, + ) + return self + + def write_frame(self, frame: np.ndarray): + self.__writer.write(frame) + + def __exit__(self, exc_type, exc_val, exc_tb): + self.__writer.release() + + +def get_video_frames_generator(source_path: str) -> Generator[np.ndarray, None, None]: + """ + Get a generator that yields the frames of the video. + + Args: + source_path (str): The path of the video file. + + Returns: + (Generator[np.ndarray, None, None]): A generator that yields the frames of the video. + + Examples: + ```python + >>> from supervision.video import get_video_frames_generator + + >>> for frame in get_video_frames_generator(source_path='source_video.mp4'): + ... ... + ``` + """ + video = cv2.VideoCapture(source_path) + if not video.isOpened(): + raise Exception(f"Could not open video at {source_path}") + success, frame = video.read() + while success: + yield frame + success, frame = video.read() + video.release() + + +def process_video(source_path: str, target_path: str, callback: Callable[[np.ndarray, int], np.ndarray]) -> None: + """ + Process a video file by applying a callback function on each frame and saving the result to a target video file. + + Args: + source_path (str): The path to the source video file. + target_path (str): The path to the target video file. + callback (Callable[[np.ndarray, int], np.ndarray]): A function that takes in a numpy ndarray representation of a video frame and an int index of the frame and returns a processed numpy ndarray representation of the frame. + + Examples: + ```python + >>> from supervision.video import process_video + + >>> def process_frame(frame: np.ndarray) -> np.ndarray: + ... ... + + >>> process_video( + ... source_path='source_video.mp4', + ... target_path='target_video.mp4', + ... callback=process_frame + ... ) + ``` + """ + source_video_info = VideoInfo.from_video_path(video_path=source_path) + with VideoSink(target_path=target_path, video_info=source_video_info) as sink: + for index, frame in enumerate(get_video_frames_generator(source_path=source_path)): + result_frame = callback(frame, index) + sink.write_frame(frame=result_frame) diff --git a/supervision/video/__init__.py b/supervision/video/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/supervision/video/dataclasses.py b/supervision/video/dataclasses.py deleted file mode 100644 index 32fd492d..00000000 --- a/supervision/video/dataclasses.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Optional, Tuple - -import cv2 - - -@dataclass -class VideoInfo: - """ - Data class containing information about the video resolution, fps, and total frame count. - - :param width: int : The width of the video frames in pixels. - :param height: int : The height of the video frames in pixels. - :param fps: int : The frames per second of the video. - :param total_frames: int : The total number of frames in the video. - """ - - width: int - height: int - fps: int - total_frames: Optional[int] = None - - @classmethod - def from_video_path(cls, video_path: str) -> VideoInfo: - """ - Returns a VideoInfo data class containing information about the video resolution, fps, and total frame count. - - :param video_path: str : The path of the video file. - :return: VideoInfo : A data class containing information about the video resolution, fps, and total frame count. - """ - video = cv2.VideoCapture(video_path) - if not video.isOpened(): - raise Exception(f"Could not open video at {video_path}") - - width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH)) - height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT)) - fps = int(video.get(cv2.CAP_PROP_FPS)) - total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) - video.release() - return VideoInfo(width, height, fps, total_frames) - - @property - def resolution(self) -> Tuple[int, int]: - return self.width, self.height diff --git a/supervision/video/sink.py b/supervision/video/sink.py deleted file mode 100644 index c70ad2f1..00000000 --- a/supervision/video/sink.py +++ /dev/null @@ -1,48 +0,0 @@ -import cv2 -import numpy as np - -from supervision.video.dataclasses import VideoInfo - - -class VideoSink: - """ - A context manager that uses OpenCV to save video frames to a file. - - :param output_path: str : The path to the output file where the video will be saved. - :param video_info: VideoInfo : An instance of VideoInfo containing information about the video resolution, fps, and total frame count. - """ - - def __init__(self, output_path: str, video_info: VideoInfo): - """ - Initializes the VideoSink with the specified output path and video information. - """ - self.output_path = output_path - self.video_info = video_info - self.fourcc = cv2.VideoWriter_fourcc(*"mp4v") - self.writer = None - - def __enter__(self): - """ - Opens the output file and returns the VideoSink instance. - """ - self.writer = cv2.VideoWriter( - self.output_path, - self.fourcc, - self.video_info.fps, - self.video_info.resolution, - ) - return self - - def write_frame(self, frame: np.ndarray): - """ - Writes a frame to the output video file. - - :param frame: np.ndarray : The frame to be written. - """ - self.writer.write(frame) - - def __exit__(self, exc_type, exc_val, exc_tb): - """ - Closes the output file. - """ - self.writer.release() diff --git a/supervision/video/source.py b/supervision/video/source.py deleted file mode 100644 index f3679cd7..00000000 --- a/supervision/video/source.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Generator - -import cv2 - - -def get_video_frames_generator(video_path: str) -> Generator[int, None, None]: - """ - Returns a generator that yields the frames of the video. - - :param video_path: str : The path of the video file. - :return: Generator[int, None, None] : Generator that yields the frames of the video. - """ - video = cv2.VideoCapture(video_path) - if not video.isOpened(): - raise Exception(f"Could not open video at {video_path}") - success, frame = video.read() - while success: - yield frame - success, frame = video.read() - video.release()