Skip to content

Commit 75f2d09

Browse files
committed
change: refactor video transforms
1 parent 03e20c3 commit 75f2d09

File tree

6 files changed

+136
-122
lines changed

6 files changed

+136
-122
lines changed

DPF/processors/processor_mixins.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ def apply_transform(self, transforms: BaseFilesTransforms) -> None:
2525

2626
metadata_lists = None
2727
if len(transforms.required_metadata) > 0:
28+
assert all(col in self._df.columns for col in transforms.required_metadata), \
29+
f"Columns {[col not in self._df.columns for col in transforms.required_metadata]} are not presented"
2830
metadata_lists = {
2931
col: self._df[col].tolist()
3032
for col in transforms.required_metadata

DPF/transforms/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from .base_file_transforms import BaseFilesTransforms, TransformsFileData
22
from .image_resize_transforms import ImageResizeTransforms
33
from .resizer import Resizer, ResizerModes
4-
from .video_fps_transforms import VideoFPSTransforms
5-
from .video_resize_transforms import VideoResizeTransforms
4+
from .video_ffmpeg_transforms import VideoFFMPEGTransforms
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import shutil
2+
import subprocess
3+
import uuid
4+
from typing import Any, Optional
5+
6+
from DPF.transforms.base_file_transforms import (
7+
BaseFilesTransforms,
8+
PoolOptions,
9+
TransformsFileData,
10+
)
11+
from DPF.transforms.resizer import Resizer
12+
13+
14+
def is_ffmpeg_installed() -> bool:
15+
try:
16+
subprocess.run('ffmpeg -version', shell=True, capture_output=True, check=True)
17+
return True
18+
except subprocess.CalledProcessError:
19+
return False
20+
21+
22+
def convert_ffmpeg_args_to_str(args: dict[str, list[str]]) -> str:
23+
arg_strings = []
24+
for k, v in args.items():
25+
s = ','.join(v)
26+
arg_strings.append(f"{k} {s}")
27+
return ' '.join(arg_strings)
28+
29+
30+
class VideoFFMPEGTransforms(BaseFilesTransforms):
31+
32+
def __init__(
33+
self,
34+
resizer: Optional[Resizer] = None,
35+
fps: Optional[int] = None,
36+
fps_eps: float = 0.1,
37+
pool_type: PoolOptions = 'threads',
38+
workers: int = 16,
39+
pbar: bool = True,
40+
preset: Optional[str] = None,
41+
crf: Optional[int] = None,
42+
copy_audio_stream: bool = True
43+
):
44+
super().__init__(pool_type, workers, pbar)
45+
self.resizer = resizer
46+
self.fps = fps
47+
self.fps_eps = fps_eps
48+
49+
self.preset = preset
50+
self.crf = crf
51+
self.copy_audio_stream = copy_audio_stream
52+
53+
self.default_args = ' '.join(self.get_default_ffmpeg_args())
54+
55+
assert is_ffmpeg_installed(), "Install ffmpeg first"
56+
assert self.resizer or self.fps, "At least one transform should be specified"
57+
58+
def get_default_ffmpeg_args(self) -> list[str]:
59+
args = []
60+
if self.preset:
61+
args.append(f"-preset {self.preset}")
62+
if self.crf:
63+
args.append(f'-crf {self.crf}')
64+
if self.copy_audio_stream:
65+
args.append('-c:a copy')
66+
return args
67+
68+
@property
69+
def required_metadata(self) -> list[str]:
70+
meta = []
71+
if self.resizer:
72+
meta += ['width', 'height']
73+
if self.fps:
74+
meta += ['fps']
75+
return meta
76+
77+
@property
78+
def metadata_to_change(self) -> list[str]:
79+
meta = []
80+
if self.resizer:
81+
meta += ['width', 'height']
82+
if self.fps:
83+
meta += ['fps']
84+
return meta
85+
86+
@property
87+
def modality(self) -> str:
88+
return 'video'
89+
90+
def _process_filepath(self, data: TransformsFileData) -> TransformsFileData:
91+
filepath = data.filepath
92+
ext = filepath.split('.')[-1]
93+
ffmpeg_args_map: dict[str, list[str]] = {}
94+
result_metadata: dict[str, Any] = {}
95+
96+
if self.resizer:
97+
width, height = data.metadata['width'], data.metadata['height']
98+
new_width, new_height = self.resizer.get_new_size(width, height)
99+
if (new_width, new_height) != (width, height):
100+
new_width += new_width % 2
101+
new_height += new_height % 2
102+
ffmpeg_args_map['-vf'] = ffmpeg_args_map.get('-vf', []) + [f"scale={new_width}:{new_height}"]
103+
result_metadata['width'] = new_width
104+
result_metadata['height'] = new_height
105+
106+
if self.fps:
107+
video_fps = data.metadata['fps']
108+
if (video_fps < (self.fps - self.fps_eps)) or (video_fps > (self.fps + self.fps_eps)):
109+
ffmpeg_args_map['-vf'] = ffmpeg_args_map.get('-vf', []) + [f"fps={self.fps}"]
110+
video_fps = float(self.fps)
111+
result_metadata['fps'] = video_fps
112+
113+
if len(ffmpeg_args_map) > 0:
114+
args_str = convert_ffmpeg_args_to_str(ffmpeg_args_map)
115+
temp_filename = str(uuid.uuid4()) + '.' + ext
116+
ffmpeg_command = f'ffmpeg -hide_banner -i {filepath} {args_str} {self.default_args} {temp_filename} -y'
117+
subprocess.run(ffmpeg_command, shell=True, capture_output=True, check=True)
118+
shutil.move(temp_filename, filepath)
119+
120+
return TransformsFileData(filepath, result_metadata)

DPF/transforms/video_fps_transforms.py

Lines changed: 0 additions & 55 deletions
This file was deleted.

DPF/transforms/video_resize_transforms.py

Lines changed: 0 additions & 64 deletions
This file was deleted.

docs/transforms.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ You can use `DPF.transforms` for these tasks.
88
99
List of implemented transforms:
1010
- [ImageResizeTransforms](../DPF/transforms/image_resize_transforms.py) - transforms that resizes images
11-
- [VideoResizeTransforms](../DPF/transforms/video_resize_transforms.py) - transforms that resizes videos
11+
- [VideoFFMPEGTransforms](../DPF/transforms/video_ffmpeg_transforms.py) - transforms that resizes and changing fps of videos using ffmpeg
1212

1313
### Examples
1414

@@ -26,4 +26,16 @@ from DPF.transforms import VideoResizeTransforms, Resizer, ResizerModes
2626

2727
transforms = VideoResizeTransforms(Resizer(ResizerModes.MAX_SIZE, size=768))
2828
processor.apply_transform(transforms)
29+
```
30+
31+
Change fps to 24 and resize all videos to 768 pixels on the minimum side while maintaining the aspect ratio:
32+
```python
33+
from DPF.transforms import VideoFFMPEGTransforms, Resizer, ResizerModes
34+
35+
transforms = VideoFFMPEGTransforms(
36+
resizer=Resizer(ResizerModes.MIN_SIZE, size=768),
37+
fps=24,
38+
workers=8
39+
)
40+
processor.apply_transform(transforms)
2941
```

0 commit comments

Comments
 (0)