Skip to content

Commit db5495c

Browse files
GH-13: Split the FFMpeg from the main Thumbnails class (GH-20)
- Split the `FFMpeg` from the main `Thumbnails` class - Use `arange` for float intervals - Convert getter and setter methods to properties
1 parent a5f4e23 commit db5495c

File tree

4 files changed

+145
-124
lines changed

4 files changed

+145
-124
lines changed

thumbnails.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import concurrent.futures
22

3-
from thumbnails import FFMpeg
3+
from thumbnails import Thumbnails
44

55
# Read from the program arguments.
66
compress = 1
@@ -12,17 +12,17 @@
1212

1313

1414
def worker(video):
15-
video.set_compress(compress)
16-
video.set_interval(interval)
17-
video.set_basepath(basepath)
15+
video.compress = compress
16+
video.interval = interval
17+
video.basepath = basepath
1818
video.extract_frames()
1919
video.join_frames()
2020
video.to_vtt()
2121

2222

2323
def main():
2424
with concurrent.futures.ProcessPoolExecutor() as executor:
25-
executor.map(worker, map(FFMpeg, files))
25+
executor.map(worker, map(Thumbnails, files))
2626

2727

2828
if __name__ == "__main__":

thumbnails/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .ffmpeg import FFMpeg
1+
from .thumbnails import Thumbnails

thumbnails/ffmpeg.py

Lines changed: 2 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,17 @@
1-
import concurrent.futures
2-
import glob
3-
import math
41
import os
52
import re
63
import subprocess
7-
from datetime import timedelta
8-
from tempfile import TemporaryDirectory
94

10-
from PIL import Image
115
from imageio.v3 import immeta
126
from imageio_ffmpeg import get_ffmpeg_exe
137

14-
from .thumbnails import _ThumbnailMixin
15-
168
ffmpeg_bin = get_ffmpeg_exe()
179

1810

19-
class FFMpeg(_ThumbnailMixin):
11+
class _FFMpeg:
2012
def __init__(self, filename):
21-
self.__compress = 1
22-
self.__interval = 1
23-
self.__basepath = ""
24-
self.thumbnails = []
25-
duration, size = self._parse_metadata(filename)
26-
_ThumbnailMixin.__init__(self, size)
27-
self.tempdir = TemporaryDirectory()
13+
duration, self.size = self._parse_metadata(filename)
2814
self.duration = int(duration + 1)
29-
self.filename = filename
30-
self._vtt_name = filename + ".vtt"
31-
self._image_name = filename + ".png"
32-
33-
def get_compress(self):
34-
return self.__compress
35-
36-
def set_compress(self, compress):
37-
if type(compress) not in (int, float):
38-
raise TypeError("Compress must be a number.")
39-
self.__compress = compress
40-
41-
def get_interval(self):
42-
return self.__interval
43-
44-
def set_interval(self, interval):
45-
if not isinstance(interval, int):
46-
raise TypeError("Interval must be an integer.")
47-
self.__interval = interval
48-
49-
def get_basepath(self):
50-
return self.__basepath
51-
52-
def set_basepath(self, path):
53-
self.__basepath = str(path)
54-
55-
@staticmethod
56-
def _calc_columns(frames_count, width, height):
57-
ratio = 16 / 9
58-
for col in range(1, frames_count):
59-
if (col * width) / (frames_count // col * height) > ratio:
60-
return col
6115

6216
@staticmethod
6317
def _cross_platform_popen_params(bufsize=100000):
@@ -100,70 +54,3 @@ def _parse_metadata(self, filename):
10054
size = self._parse_size(stdout)
10155

10256
return duration, size
103-
104-
def _extract_frame(self, start_time):
105-
_input_file = self.filename
106-
_output_file = "%s/%d-%s.png" % (self.tempdir.name, start_time, self.filename)
107-
_timestamp = str(timedelta(seconds=start_time))
108-
109-
cmd = (
110-
ffmpeg_bin,
111-
"-ss", _timestamp,
112-
"-i", _input_file,
113-
"-loglevel", "error",
114-
"-vframes", "1",
115-
_output_file,
116-
"-y",
117-
)
118-
119-
subprocess.Popen(cmd).wait()
120-
121-
def extract_frames(self):
122-
_intervals = range(0, self.duration, self.get_interval())
123-
with concurrent.futures.ThreadPoolExecutor() as executor:
124-
executor.map(self._extract_frame, _intervals)
125-
126-
def join_frames(self):
127-
line, column = 0, 0
128-
frames = sorted(glob.glob(self.tempdir.name + os.sep + "*.png"))
129-
frames_count = len(range(0, self.duration, self.get_interval()))
130-
columns = self._calc_columns(frames_count, self.width, self.height)
131-
master_height = self.height * int(math.ceil(float(frames_count) / columns))
132-
master = Image.new(mode="RGBA", size=(self.width * columns, master_height))
133-
134-
for n, frame in enumerate(frames):
135-
with Image.open(frame) as image:
136-
x, y = self.width * column, self.height * line
137-
138-
start = n * self.get_interval()
139-
end = (n + 1) * self.get_interval()
140-
self.thumbnails.append((start, end, x, y))
141-
142-
image = image.resize((self.width, self.height), Image.ANTIALIAS)
143-
master.paste(image, (x, y))
144-
145-
column += 1
146-
147-
if column == columns:
148-
line += 1
149-
column = 0
150-
151-
master.save(self._image_name)
152-
self.tempdir.cleanup()
153-
154-
def to_vtt(self):
155-
def _format_time(seconds):
156-
return "0%s.000" % str(timedelta(seconds=seconds))
157-
158-
_lines = ["WEBVTT\n\n"]
159-
_img_src = self.get_basepath() + self._image_name
160-
161-
for start, end, x, y in self.thumbnails:
162-
_thumbnail = "%s --> %s\n%s#xywh=%d,%d,%d,%d\n\n" % (
163-
_format_time(start), _format_time(end),
164-
_img_src, x, y, self.width, self.height
165-
)
166-
_lines.append(_thumbnail)
167-
168-
with open(self._vtt_name, "w") as vtt:
169-
vtt.writelines(_lines)

thumbnails/thumbnails.py

Lines changed: 137 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
1+
import concurrent.futures
2+
import glob
13
import math
4+
import os
5+
import subprocess
6+
from datetime import timedelta
7+
from tempfile import TemporaryDirectory
8+
9+
from PIL import Image
10+
from imageio_ffmpeg import get_ffmpeg_exe
11+
from numpy import arange
12+
13+
from .ffmpeg import _FFMpeg
14+
15+
ffmpeg_bin = get_ffmpeg_exe()
216

317

418
class _ThumbnailMixin:
@@ -15,17 +29,137 @@ def __init__(self, size):
1529
self._min_width = _min_width
1630
self._min_height = _min_height
1731

18-
def get_compress(self):
32+
@property
33+
def compress(self):
1934
raise NotImplementedError
2035

2136
@property
2237
def width(self):
2338
if not self._w:
24-
self._w = max(self._min_width, self._width * self.get_compress())
39+
self._w = max(self._min_width, self._width * self.compress)
2540
return self._w
2641

2742
@property
2843
def height(self):
2944
if not self._h:
30-
self._h = max(self._min_height, self._height * self.get_compress())
45+
self._h = max(self._min_height, self._height * self.compress)
3146
return self._h
47+
48+
49+
class Thumbnails(_ThumbnailMixin, _FFMpeg):
50+
def __init__(self, filename):
51+
self.__compress = 1.
52+
self.__interval = 1.
53+
self.__basepath = ""
54+
self.thumbnails = []
55+
self.tempdir = TemporaryDirectory()
56+
self.filename = filename
57+
self._vtt_name = filename + ".vtt"
58+
self._image_name = filename + ".png"
59+
60+
_FFMpeg.__init__(self, filename)
61+
_ThumbnailMixin.__init__(self, self.size)
62+
63+
@property
64+
def compress(self):
65+
return self.__compress
66+
67+
@compress.setter
68+
def compress(self, value):
69+
try:
70+
self.__compress = float(value)
71+
except ValueError:
72+
raise ValueError("Compress must be a number.")
73+
74+
@property
75+
def interval(self):
76+
return self.__interval
77+
78+
@interval.setter
79+
def interval(self, value):
80+
try:
81+
self.__interval = float(value)
82+
except ValueError:
83+
raise ValueError("Interval must be a number.")
84+
85+
@property
86+
def basepath(self):
87+
return self.__basepath
88+
89+
@basepath.setter
90+
def basepath(self, value):
91+
self.__basepath = value
92+
93+
@staticmethod
94+
def _calc_columns(frames_count, width, height):
95+
ratio = 16 / 9
96+
for col in range(1, frames_count):
97+
if (col * width) / (frames_count // col * height) > ratio:
98+
return col
99+
100+
def _extract_frame(self, start_time):
101+
_input_file = self.filename
102+
_output_file = "%s/%d-%s.png" % (self.tempdir.name, start_time, self.filename)
103+
_timestamp = str(timedelta(seconds=start_time))
104+
105+
cmd = (
106+
ffmpeg_bin,
107+
"-ss", _timestamp,
108+
"-i", _input_file,
109+
"-loglevel", "error",
110+
"-vframes", "1",
111+
_output_file,
112+
"-y",
113+
)
114+
115+
subprocess.Popen(cmd).wait()
116+
117+
def extract_frames(self):
118+
_intervals = arange(0, self.duration, self.interval)
119+
with concurrent.futures.ThreadPoolExecutor() as executor:
120+
executor.map(self._extract_frame, _intervals)
121+
122+
def join_frames(self):
123+
line, column = 0, 0
124+
frames = sorted(glob.glob(self.tempdir.name + os.sep + "*.png"))
125+
frames_count = len(arange(0, self.duration, self.interval))
126+
columns = self._calc_columns(frames_count, self.width, self.height)
127+
master_height = self.height * int(math.ceil(float(frames_count) / columns))
128+
master = Image.new(mode="RGBA", size=(self.width * columns, master_height))
129+
130+
for n, frame in enumerate(frames):
131+
with Image.open(frame) as image:
132+
x, y = self.width * column, self.height * line
133+
134+
start = n * self.interval
135+
end = (n + 1) * self.interval
136+
self.thumbnails.append((start, end, x, y))
137+
138+
image = image.resize((self.width, self.height), Image.ANTIALIAS)
139+
master.paste(image, (x, y))
140+
141+
column += 1
142+
143+
if column == columns:
144+
line += 1
145+
column = 0
146+
147+
master.save(self._image_name)
148+
self.tempdir.cleanup()
149+
150+
def to_vtt(self):
151+
def _format_time(seconds):
152+
return "0%s.000" % str(timedelta(seconds=seconds))
153+
154+
_lines = ["WEBVTT\n\n"]
155+
_img_src = self.basepath + self._image_name
156+
157+
for start, end, x, y in self.thumbnails:
158+
_thumbnail = "%s --> %s\n%s#xywh=%d,%d,%d,%d\n\n" % (
159+
_format_time(start), _format_time(end),
160+
_img_src, x, y, self.width, self.height
161+
)
162+
_lines.append(_thumbnail)
163+
164+
with open(self._vtt_name, "w") as vtt:
165+
vtt.writelines(_lines)

0 commit comments

Comments
 (0)