-
Notifications
You must be signed in to change notification settings - Fork 114
Add video models + functions #814
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
75877d1
031b9df
548bbd5
b55149a
2cd6d62
5892ab9
f3dc66a
65529f3
b044082
2a77047
89ee2f0
5f522ad
e2f5a3a
67beb9f
60c5848
d3b1619
e31210c
bcd95b1
08edd27
dbefa5f
258454e
328c1a7
a1a47b2
5b2f45b
14caa08
746fd73
0fe47dd
1598c4c
0c3f3b4
bf824af
428d865
b7549b1
8639246
3376449
5b2e437
213b1d8
43389f7
5a20c4e
b72c440
55cd044
69a4385
7859e16
3f47d12
17118d1
cc05da9
8d9f6c2
23514f7
8a8dd64
1a04dd0
0c95c3d
e55405d
8e2a673
9c910ec
a2b8c9a
63448d9
abe39f5
3b7b829
55f0478
99b9490
c28cd66
4098e8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,7 +17,7 @@ | |
from urllib.request import url2pathname | ||
|
||
from fsspec.callbacks import DEFAULT_CALLBACK, Callback | ||
from PIL import Image | ||
from PIL import Image as PilImage | ||
from pydantic import Field, field_validator | ||
|
||
from datachain.client.fileslice import FileSlice | ||
|
@@ -27,6 +27,7 @@ | |
from datachain.utils import TIME_ZERO | ||
|
||
if TYPE_CHECKING: | ||
from numpy import ndarray | ||
from typing_extensions import Self | ||
|
||
from datachain.catalog import Catalog | ||
|
@@ -40,7 +41,7 @@ | |
# how to create file path when exporting | ||
ExportPlacement = Literal["filename", "etag", "fullpath", "checksum"] | ||
|
||
FileType = Literal["binary", "text", "image"] | ||
FileType = Literal["binary", "text", "image", "video"] | ||
|
||
|
||
class VFileError(DataChainError): | ||
|
@@ -193,7 +194,7 @@ | |
@classmethod | ||
def upload( | ||
cls, data: bytes, path: str, catalog: Optional["Catalog"] = None | ||
) -> "File": | ||
) -> "Self": | ||
if catalog is None: | ||
from datachain.catalog.loader import get_catalog | ||
|
||
|
@@ -203,6 +204,8 @@ | |
|
||
client = catalog.get_client(parent) | ||
file = client.upload(data, name) | ||
if not isinstance(file, cls): | ||
file = cls(**file.model_dump()) | ||
file._set_stream(catalog) | ||
return file | ||
|
||
|
@@ -486,13 +489,217 @@ | |
def read(self): | ||
"""Returns `PIL.Image.Image` object.""" | ||
fobj = super().read() | ||
return Image.open(BytesIO(fobj)) | ||
return PilImage.open(BytesIO(fobj)) | ||
|
||
def save(self, destination: str): | ||
"""Writes it's content to destination""" | ||
self.read().save(destination) | ||
|
||
|
||
class Image(DataModel): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need this separate model? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as for video info ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's just a bit weird that we have ImageFile and Image (that contains only some basic metadata) 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was |
||
"""`DataModel` for image file meta information.""" | ||
|
||
width: int = Field(default=-1) | ||
height: int = Field(default=-1) | ||
format: str = Field(default="") | ||
|
||
|
||
class VideoFile(File): | ||
shcheklein marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""`DataModel` for reading video files.""" | ||
|
||
def get_info(self) -> "Video": | ||
"""Returns video file information.""" | ||
from .video import video_info | ||
|
||
return video_info(self) | ||
|
||
def get_frame_np(self, frame: int) -> "ndarray": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thinking out-loud here but should a frame be an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll take a look at the notebook but my thought would be that you would want to be able to call something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The frames use-case could end up looking something like: (
DataChain.from_storage("gs://datachain-demo/some-desc/videos")
.limit(20)
.gen(frame=file.split_to_frame, params="file", output={"frame": ImageFile})
.setup(yolo=lambda: YOLO("yolo11n.pt"))
.map(boxes=process_bboxes)
.show()
) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also can be done by the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
""" | ||
Reads video frame from a file. | ||
|
||
Args: | ||
frame (int): Frame number to read. | ||
|
||
Returns: | ||
ndarray: Video frame. | ||
""" | ||
from .video import video_frame_np | ||
|
||
return video_frame_np(self, frame) | ||
|
||
def get_frame(self, frame: int, format: str = "jpg") -> bytes: | ||
""" | ||
Reads video frame from a file and returns as image bytes. | ||
|
||
Args: | ||
frame (int): Frame number to read. | ||
format (str): Image format (default: 'jpg'). | ||
|
||
Returns: | ||
bytes: Video frame image as bytes. | ||
""" | ||
from .video import video_frame | ||
|
||
return video_frame(self, frame, format) | ||
|
||
def save_frame( | ||
self, | ||
frame: int, | ||
output_file: str, | ||
format: Optional[str] = None, | ||
) -> "VideoFrame": | ||
""" | ||
Saves video frame as an image file. | ||
|
||
Args: | ||
frame (int): Frame number to read. | ||
output_file (str): Output file path. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what format is default? does it support different formats? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Output format is taken from output file extension. See here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so, extension determines it? I wonder if we need to clarify or will be kinda expected by end users 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated:
|
||
format (str): Image format (default: use output file extension). | ||
|
||
Returns: | ||
VideoFrame: Video frame model. | ||
""" | ||
from .video import save_video_frame | ||
|
||
return save_video_frame(self, frame, output_file, format=format) | ||
|
||
def get_frames_np( | ||
self, | ||
start: int = 0, | ||
end: Optional[int] = None, | ||
step: int = 1, | ||
) -> "Iterator[ndarray]": | ||
""" | ||
Reads video frames from a file. | ||
|
||
Args: | ||
start (int): Frame number to start reading from (default: 0). | ||
end (int): Frame number to stop reading at (default: None). | ||
step (int): Step size for reading frames (default: 1). | ||
|
||
Returns: | ||
Iterator[ndarray]: Iterator of video frames. | ||
""" | ||
from .video import video_frames_np | ||
|
||
yield from video_frames_np(self, start, end, step) | ||
|
||
def get_frames( | ||
self, | ||
start: int = 0, | ||
end: Optional[int] = None, | ||
step: int = 1, | ||
format: str = "jpg", | ||
) -> "Iterator[bytes]": | ||
""" | ||
Reads video frames from a file and returns as bytes. | ||
|
||
Args: | ||
start (int): Frame number to start reading from (default: 0). | ||
end (int): Frame number to stop reading at (default: None). | ||
step (int): Step size for reading frames (default: 1). | ||
format (str): Image format (default: 'jpg'). | ||
|
||
Returns: | ||
Iterator[bytes]: Iterator of video frames. | ||
""" | ||
from .video import video_frames | ||
|
||
yield from video_frames(self, start, end, step, format) | ||
|
||
def save_frames( | ||
self, | ||
output_dir: str, | ||
start: int = 0, | ||
end: Optional[int] = None, | ||
step: int = 1, | ||
format: str = "jpg", | ||
) -> "Iterator[VideoFrame]": | ||
""" | ||
Saves video frames as image files. | ||
|
||
Args: | ||
output_dir (str): Output directory path. | ||
start (int): Frame number to start reading from (default: 0). | ||
end (int): Frame number to stop reading at (default: None). | ||
step (int): Step size for reading frames (default: 1). | ||
format (str): Image format (default: 'jpg'). | ||
|
||
Returns: | ||
Iterator[VideoFrame]: List of video frame models. | ||
""" | ||
from .video import save_video_frames | ||
|
||
yield from save_video_frames(self, output_dir, start, end, step, format) | ||
|
||
def save_fragment( | ||
self, | ||
start: float, | ||
end: float, | ||
output_file: str, | ||
) -> "VideoFragment": | ||
""" | ||
Saves video interval as a new video file. | ||
|
||
Args: | ||
start (float): Start time in seconds. | ||
end (float): End time in seconds. | ||
output_file (str): Output file path. | ||
|
||
Returns: | ||
VideoFragment: Video fragment model. | ||
""" | ||
from .video import save_video_fragment | ||
|
||
return save_video_fragment(self, start, end, output_file) | ||
|
||
def save_fragments( | ||
self, | ||
intervals: list[tuple[float, float]], | ||
output_dir: str, | ||
) -> "Iterator[VideoFragment]": | ||
""" | ||
Saves video intervals as new video files. | ||
|
||
Args: | ||
intervals (list[tuple[float, float]]): List of start and end times | ||
in seconds. | ||
output_dir (str): Output directory path. | ||
|
||
Returns: | ||
Iterator[VideoFragment]: List of video fragment models. | ||
""" | ||
from .video import save_video_fragments | ||
|
||
yield from save_video_fragments(self, intervals, output_dir) | ||
|
||
|
||
class VideoFragment(VideoFile): | ||
"""`DataModel` for reading video fragments.""" | ||
|
||
start: float = Field(default=-1.0) | ||
end: float = Field(default=-1.0) | ||
|
||
|
||
class VideoFrame(ImageFile): | ||
"""`DataModel` for reading video frames.""" | ||
|
||
frame: int = Field(default=-1) | ||
timestamp: float = Field(default=-1.0) | ||
|
||
|
||
class Video(DataModel): | ||
"""`DataModel` for video file meta information.""" | ||
|
||
width: int = Field(default=-1) | ||
height: int = Field(default=-1) | ||
fps: float = Field(default=-1.0) | ||
duration: float = Field(default=-1.0) | ||
frames: int = Field(default=-1) | ||
format: str = Field(default="") | ||
codec: str = Field(default="") | ||
|
||
|
||
class ArrowRow(DataModel): | ||
"""`DataModel` for reading row from Arrow-supported file.""" | ||
|
||
|
@@ -528,5 +735,7 @@ | |
file = TextFile | ||
elif type_ == "image": | ||
file = ImageFile # type: ignore[assignment] | ||
elif type_ == "video": | ||
file = VideoFile | ||
|
||
return file |
Uh oh!
There was an error while loading. Please reload this page.