1
+ import concurrent .futures
2
+ import glob
3
+ import math
1
4
import os
2
5
import re
3
6
import subprocess
4
- from functools import reduce
7
+ from datetime import timedelta
8
+ from tempfile import TemporaryDirectory
5
9
6
- import imageio_ffmpeg
7
- import numpy
8
10
from PIL import Image
9
11
from imageio .v3 import immeta
12
+ from imageio_ffmpeg import get_ffmpeg_exe
10
13
11
- FFMPEG_BINARY = imageio_ffmpeg . get_ffmpeg_exe ()
14
+ FFMPEG_BINARY = get_ffmpeg_exe ()
12
15
13
16
14
17
class FFMpeg :
15
18
def __init__ (self , filename ):
16
- duration , size = self .parse_metadata (filename )
17
- self .area = reduce (int .__mul__ , size )
18
- self .bytes = self .area * 4
19
+ self .__compress = 1
20
+ self .__interval = 1
21
+ duration , size = self ._parse_metadata (filename )
22
+ self .tempdir = TemporaryDirectory ()
19
23
self .filename = filename
20
24
self .duration = duration
21
25
self .size = size
22
26
27
+ def get_compress (self ):
28
+ return self .__compress
29
+
30
+ def set_compress (self , compress ):
31
+ if type (compress ) not in (int , float ):
32
+ raise TypeError ("Compress must be a number." )
33
+ self .__compress = compress
34
+
35
+ def get_interval (self ):
36
+ return self .__interval
37
+
38
+ def set_interval (self , interval ):
39
+ if not isinstance (interval , int ):
40
+ raise TypeError ("Interval must be an integer." )
41
+ self .__interval = interval
42
+
23
43
@staticmethod
24
- def cross_platform_popen_params (bufsize = 100000 ):
44
+ def calc_columns (frames_count , width , height ):
45
+ ratio = 16 / 9
46
+ for col in range (1 , frames_count ):
47
+ if (col * width ) / (frames_count // col * height ) > ratio :
48
+ return col
49
+
50
+ @staticmethod
51
+ def _cross_platform_popen_params (bufsize = 100000 ):
25
52
popen_params = {
26
53
"bufsize" : bufsize ,
27
54
"stdout" : subprocess .PIPE ,
@@ -36,7 +63,7 @@ def cross_platform_popen_params(bufsize=100000):
36
63
def _parse_duration (stdout ):
37
64
duration_regex = r"duration[^\n]+([0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9])"
38
65
time = re .search (duration_regex , stdout , re .M | re .I ).group (1 )
39
- time = [ float (part .replace ("," , "." )) for part in time .split (":" )]
66
+ time = ( float (part .replace ("," , "." )) for part in time .split (":" ))
40
67
return sum (mult * part for mult , part in zip ((3600 , 60 , 1 ), time ))
41
68
42
69
@staticmethod
@@ -45,67 +72,72 @@ def _parse_size(stdout):
45
72
match_size = re .search (size_regex , stdout , re .M )
46
73
return tuple (map (int , match_size .groups ()))
47
74
48
- def parse_metadata (self , filename ):
75
+ def _parse_metadata (self , filename ):
49
76
meta = immeta (filename )
50
77
duration , size = meta .get ("duration" ), meta .get ("size" )
51
78
52
- if not all ([ duration , size ] ):
53
- cmd = [ FFMPEG_BINARY , "-hide_banner" , "-i" , filename ]
79
+ if not all (( duration , size ) ):
80
+ cmd = ( FFMPEG_BINARY , "-hide_banner" , "-i" , filename )
54
81
55
- popen_params = self .cross_platform_popen_params ()
82
+ popen_params = self ._cross_platform_popen_params ()
56
83
process = subprocess .Popen (cmd , ** popen_params )
57
84
_ , stderr = process .communicate ()
58
85
stdout = stderr .decode ("utf8" , errors = "ignore" )
59
86
60
- process .terminate ()
61
- del process
62
-
63
87
duration = self ._parse_duration (stdout )
64
88
size = self ._parse_size (stdout )
65
89
66
90
return duration , size
67
91
68
- @staticmethod
69
- def frame_to_buffer (image ):
70
- image = image .astype ("uint8" )
71
- return Image .fromarray (image )
72
-
73
- def get_frame (self , start_time ):
74
- if start_time != 0 :
75
- offset = min (1 , start_time )
76
- i_arg = [
77
- "-ss" , "%.06f" % (start_time - offset ),
78
- "-i" , self .filename ,
79
- "-ss" , "%.06f" % offset ,
80
- ]
81
- else :
82
- i_arg = ["-i" , self .filename ]
92
+ def _extract_frame (self , start_time ):
93
+ _input_file = self .filename
94
+ _output_file = "%s/%d-%s.png" % (self .tempdir .name , start_time , self .filename )
95
+ _timestamp = str (timedelta (seconds = start_time ))
83
96
84
97
cmd = (
85
- [FFMPEG_BINARY ]
86
- + i_arg
87
- + [
88
- "-loglevel" , "error" ,
89
- "-f" , "image2pipe" ,
90
- "-vf" , "scale=%d:%d" % tuple (self .size ),
91
- "-sws_flags" , "bicubic" ,
92
- "-pix_fmt" , "rgba" ,
93
- "-vcodec" , "rawvideo" , "-" ,
94
- ]
98
+ FFMPEG_BINARY ,
99
+ "-ss" , _timestamp ,
100
+ "-i" , _input_file ,
101
+ "-loglevel" , "error" ,
102
+ "-vframes" , "1" ,
103
+ _output_file ,
104
+ "-y" ,
95
105
)
96
106
97
- popen_params = self .cross_platform_popen_params (self .bytes + 100 )
98
- process = subprocess .Popen (cmd , ** popen_params )
99
- buffer = process .stdout .read (self .bytes )
107
+ subprocess .Popen (cmd ).wait ()
108
+
109
+ def extract_frames (self ):
110
+ _intervals = range (0 , int (self .duration ), self .get_interval ())
111
+ with concurrent .futures .ThreadPoolExecutor () as executor :
112
+ executor .map (self ._extract_frame , _intervals )
113
+
114
+ def join_frames (self ):
115
+ width , height = self .size
116
+
117
+ _min_width = 300
118
+ _min_height = math .ceil (_min_width * height / width )
119
+
120
+ width = max (_min_width , width * self .__compress / 10 )
121
+ height = max (_min_height , height * self .__compress / 10 )
122
+
123
+ line , column = 0 , 0
124
+ frames = sorted (glob .glob (self .tempdir .name + os .sep + "*.png" ))
125
+ frames_count = len (range (0 , int (self .duration ), self .get_interval ()))
126
+ columns = self .calc_columns (frames_count , width , height )
127
+ master_height = height * int (math .ceil (float (frames_count ) / columns ))
128
+ master = Image .new (mode = "RGBA" , size = (width * columns , master_height ))
100
129
101
- process .terminate ()
102
- del process
130
+ for frame in frames :
131
+ with Image .open (frame ) as image :
132
+ x , y = width * column , height * line
133
+ image = image .resize ((width , height ), Image .ANTIALIAS )
134
+ master .paste (image , (x , y ))
103
135
104
- if hasattr (numpy , "frombuffer" ):
105
- result = numpy .frombuffer (buffer , dtype = "uint8" )
106
- else :
107
- result = numpy .fromstring (buffer , dtype = "uint8" )
136
+ column += 1
108
137
109
- result .shape = (* self .size [::- 1 ], len (buffer ) // self .area )
138
+ if column == columns :
139
+ line += 1
140
+ column = 0
110
141
111
- return result
142
+ master .save (self .filename + ".png" )
143
+ self .tempdir .cleanup ()
0 commit comments