Coverage for src/code_beatrix/art/video.py: 78%
381 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-04-29 13:45 +0200
« prev ^ index » next coverage.py v7.1.0, created at 2023-04-29 13:45 +0200
1# -*- coding: utf-8 -*-
2"""
3@file
4@brief Quelques questions d'ordre général autour du langage Python.
5"""
6from contextlib import redirect_stdout, redirect_stderr
7import io
8import os
9import sys
10import tempfile
11import time
12import numpy
13from pytube import YouTube # pylint: disable=E0401
14from pytube.exceptions import RegexMatchError # pylint: disable=E0401
15from imageio import imsave
16import moviepy.audio.fx.all as afx
17import moviepy.video.fx.all as vfx
18from moviepy.video.VideoClip import ImageClip, VideoClip
19from moviepy.video.io.ImageSequenceClip import ImageSequenceClip
20from moviepy.audio.AudioClip import CompositeAudioClip
21from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
22from moviepy.video.compositing.concatenate import concatenate_videoclips
23from moviepy.audio.AudioClip import concatenate_audioclips, AudioArrayClip
24from PIL import Image, ImageFont, ImageDraw
25from .moviepy_context import AudioContext, VideoContext, get_wrapped, clean_video
28class FontError(Exception):
29 """
30 Raised when a font cannot be found.
31 """
32 pass
35def check(fLOG=None):
36 """
37 Checks a couple of functionality works.
38 The test takes 5-6 seconds to download,
39 4-5 seconds to process the video.
41 @param logging function
42 """
43 t1 = time.perf_counter()
44 with tempfile.TemporaryDirectory() as temp:
45 if fLOG:
46 fLOG('[check] download_youtube_video')
47 vid = download_youtube_video("4o5baMYWdtQ", temp, res=None)
48 vid = os.path.join(temp, vid)
49 t2 = time.perf_counter()
50 if fLOG:
51 fLOG('[check] video_compose')
52 ext = video_compose(vid, vid, t2=2, place="h2")
53 dest = os.path.join(temp, "res.mp4")
54 if fLOG:
55 fLOG('[check] video_save')
56 video_save(ext, dest)
57 res = os.path.exists(dest)
58 delta1 = time.perf_counter() - t1
59 delta2 = time.perf_counter() - t2
60 if fLOG:
61 fLOG("[check] video time={0} - video={1}".format(delta1, delta2))
62 return res
65##########
66# youtube
67##########
70def download_youtube_video(tag, output_path=None, res='720p', mime_type="video/mp4", **kwargs):
71 """
72 Downloads a video from :epkg:`youtube` with :epkg:`pytube`.
73 Télécharge une vidéo depuis :epkg:`youtube` avec :epkg:`pytube`.
75 @param tag tag of the :epkg:`youtube` video to download
76 @param output_path output path
77 @param mime_type see :epkg:`youtube`
78 @param res see :epkg:`youtube`
79 @param kwargs see :epkg:`youtube`
80 @return filename (relative to *output_path*)
82 .. faqref::
83 :title: Télécharger une vidéo sur YouTube
85 Le module :epkg:`pytube` permet de télécharger une vidéo
86 :epkg:`youtube`. Chaque vidéo est disponible selon plusieurs
87 format dont on récupère la liste avant de choisir
88 qui correspond à celui voulu.
90 ::
92 from pytube import YouTube
93 yt = YouTube('https://www.youtube.com/watch?v=tRFHXMQP-QU')
94 st = yt.streams
95 fil = st.filter(mime_type="video/mp4", res="720p")
96 fil.first().download()
98 """
99 url = 'https://www.youtube.com/watch?v={0}'.format(tag)
100 try:
101 yt = YouTube(url)
102 except RegexMatchError as e:
103 raise RuntimeError(
104 "Unable to process tag=%r (url=%r)" % (tag, url)) from e
105 st = yt.streams.filter(mime_type=mime_type, res=res, **kwargs)
106 fi = st.first()
107 if fi is None:
108 raise ValueError(
109 "By default the function downloads a video with resolution = 720, "
110 "if it is not available, switch to res=None "
111 "to choose the first one available [tag=%r url=%r]" % (tag, url))
112 fi.download(output_path=output_path)
113 return fi.default_filename
115########
116# audio
117########
120def audio_extract_audio(audio_or_file, ta=0, tb=None):
121 """
122 Extracts a part of an audio.
123 Extrait une partie du son.
124 Uses `subclip <https://zulko.github.io/moviepy/ref/AudioClip.html?highlight=audioclip#moviepy.audio.AudioClip.AudioClip.subclip>`_.
126 @param audio_or_file string or :epkg:`AudioClip`
127 @param ta beginning
128 @param tb end
129 @return :epkg:`VideoClip`
131 Example:
133 ::
135 from code_beatrix.art.video import audio_extract_audio
136 son = audio_extract_audio('son.mp3', '00:00:01', '00:00:02')
137 """
138 with AudioContext(audio_or_file) as audio:
139 return audio.subclip(ta, tb)
142def audio_save(audio_or_file, filename, verbose=False, **kwargs):
143 """
144 Saves as a sound.
145 Enregistre un son dans un fichier.
146 Uses `write_audiofile <https://zulko.github.io/moviepy/ref/AudioClip.html?
147 highlight=audioclip#moviepy.audio.AudioClip.AudioClip.write_audiofile>`_.
149 @param audio_or_file string or :epkg:`AudioClip`
150 @param filename save into this filename
151 @param verbose logging or not
152 @param kwargs see `write_audiofile <https://zulko.github.io/moviepy/ref/
153 VideoClip/VideoClip.html?highlight=videofileclip#moviepy.video.
154 io.VideoFileClip.VideoFileClip.write_videofile>`_
155 """
156 with AudioContext(audio_or_file) as audio:
157 if verbose:
158 audio.write_audiofile(filename, verbose=verbose, **kwargs)
159 else:
160 f = io.StringIO()
161 with redirect_stdout(f):
162 with redirect_stderr(f):
163 audio.write_audiofile(filename, verbose=verbose, **kwargs)
166def audio_modification(audio, loop_duration=None, volumex=1.,
167 fadein=False, fadeout=False, t_start=0, t_end=None,
168 speed=1., keep_duration=False, wav=False):
169 """
170 Modifies a sound.
171 Modifie un son.
173 @param audio sound
174 @param loop_duration loops sound
175 @param volumex multiplies the sound
176 @param fadein decreases the volume of the first seconds
177 @param fadeout decreases the volume of the last seconds
178 @param t_start shorten the audio
179 @param t_end shorten the audio
180 @param speed speed of the sound
181 @param keep_duration parameter to
182 `ft_time <https://zulko.github.io/moviepy/ref/AudioClip.html?
183 highlight=fl_time#moviepy.audio.AudioClip.AudioClip.fl_time>`_
184 @return new sound
185 """
186 with AudioContext(audio) as audio_:
187 if loop_duration:
188 if audio_.duration is None:
189 raise ValueError(
190 "The duration is unknown, maybe you should apply the loop first.")
191 audio_ = afx.audio_loop(audio_, duration=loop_duration)
192 if volumex != 1.:
193 audio_ = audio_.fx(afx.volumex, volumex)
194 if speed != 1.:
195 audio_ = audio_.fl_time(lambda t: t * speed,
196 keep_duration=keep_duration)
197 if fadein:
198 audio_ = audio_.fx(afx.audio_fadein, 1.0)
199 if fadeout:
200 audio_ = audio_.fx(afx.audio_fadeout, 1.0)
201 if t_start != 0 or t_end is not None:
202 audio_ = audio_.subclip(t_start=t_start, t_end=t_end)
203 return audio_
206def audio2wav(audio, duration=None, **kwargs):
207 """
208 The sound is converted into :epkg:`wav`
209 and returned as an :epkg:`AudioArrayClip`.
210 Le son est converti au format :epkg:`wav`.
212 @param audio sound
213 @param duration change the duration of the sound before converting it
214 @param kwargs see `to_soundarray <https://zulko.github.io/moviepy/ref/AudioClip.html?
215 highlight=to_soundarray#moviepy.audio.AudioClip.AudioClip.to_soundarray>`_
216 @return :epkg:`AudioArrayClip`
217 """
218 with AudioContext(audio) as audio_:
219 if duration is not None:
220 audio_ = audio_.set_duration(duration)
221 wav = audio_.to_soundarray(**kwargs)
222 fps = kwargs.get('fps', audio_.fps if hasattr(audio_, 'fps') else None)
223 if fps is None:
224 raise ValueError("fps cannot be None, 44100 is a proper value")
225 return AudioArrayClip(wav, fps=fps)
228def audio_compose(audio_or_file1, audio_or_file2, t1=0, t2=None):
229 """
230 Concatenates or superposes two sounds.
231 Ajoute ou superpose deux sons.
233 @param audio_or_file1 son 1
234 @param audio_or_file2 son 2
235 @param t1 start of the first sound
236 @param t2 start of the second sound (or None to add it ad
237 @return new sound
239 Example:
241 ::
243 from code_beatrix.art.video import audio_compose
244 son = audio_compose('son1.mp3', 'son2.mp3', 0, 10)
245 """
246 with AudioContext(audio_or_file1) as audio1:
247 with AudioContext(audio_or_file2) as audio2:
248 add = []
249 if t1 != 0:
250 add.append(audio1.set_start(t1))
251 else:
252 add.append(audio1)
253 if t2 is None:
254 add.append(audio2.set_start(audio1.duration + t1))
255 else:
256 add.append(audio2.set_start(t2))
257 comp = CompositeAudioClip(add)
258 fps1 = audio1.fps if hasattr(audio1, 'fps') else None
259 fps2 = audio2.fps if hasattr(audio2, 'fps') else None
260 if fps1 is not None and fps2 is not None:
261 fps = max(fps1, fps2)
262 return comp.set_fps(fps)
263 elif fps1 is None and fps2 is None:
264 return comp
265 else:
266 return comp.set_fps(fps1 or fps2)
269def audio_concatenate(audio_or_files, **kwargs):
270 """
271 Concatenates sounds.
272 Met bout à bout des sons.
274 @param audio_or_files list of sounds or filenames
275 @param kwargs additional parameters for
276 `concatenate_audioclips <https://github.com/Zulko/moviepy/blob/master/moviepy/audio/AudioClip.py#L308>`_
277 @return :epkg:`AudioClip`
279 Example:
281 ::
283 from code_beatrix.art.video import audio_concatenate
284 son = audio_concatenate('son1.mp3', 'son2.mp3')
285 """
286 ctx = [AudioContext(_).__enter__() for _ in audio_or_files]
287 res = concatenate_audioclips([get_wrapped(_) for _ in ctx], **kwargs)
288 for _ in ctx:
289 _.__exit__()
290 return res
292########
293# vidéo
294########
297def video_extract_video(video_or_file, ta=0, tb=None):
298 """
299 Extracts a part of a video.
300 Extrait une partie de la vidéo.
301 Uses `subclip <https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html?
302 highlight=videofileclip#moviepy.video.VideoClip.VideoClip.subclip>`_.
304 @param video_or_file string or :epkg:`VideoClip`
305 @param ta beginning
306 @param tb end
307 @return :epkg:`VideoClip`
309 Example:
311 ::
313 from code_beatrix.faq_faq_video import video_extract_video
314 vid = video_extract_video('exemple.mp4', '00:00:01', '00:00:04')
315 """
316 with VideoContext(video_or_file) as video:
317 return video.subclip(ta, tb)
320def video_load(video_or_file):
321 """
322 Loads a video.
323 Charge une vidéo.
325 @param video_or_file string or :epkg:`VideoClip`
326 @return :epkg:`VideoClip`
327 """
328 with VideoContext(video_or_file) as video:
329 return video.video
332def video_save_image(video_or_file, t=None, filename=None, **kwargs):
333 """
334 Saves one image from a video.
335 Enregistre une image extraite d'une vidéo.
338 @param video_or_file string or :epkg:`VideoClip`
339 @param filename if not None, saves the image into this file
340 @param kwargs see `save_frame <https://zulko.github.io/moviepy/ref/VideoClip/
341 VideoClip.html?highlight=save_frame#moviepy.video.io.VideoFileClip.VideoFileClip.save_frame>`_
342 @return one image if *filename* is None
344 Example:
346 ::
348 from code_beatrix.faq_faq_video import video_extract_video, video_save_image
349 vid = video_extract_video('exemple.mp4', '00:00:01', '00:00:04')
350 video_save_image(vid, filename='new_image.jpg', t=2)
351 """
352 with VideoContext(video_or_file) as video:
353 if filename is not None:
354 video.save_frame(filename, t=t, **kwargs)
355 return filename
356 else:
357 im = video.get_frame(t)
358 if kwargs.get('withmask', True) and video.mask is not None:
359 mask = 255 * video.mask.get_frame(t)
360 im = numpy.dstack([im, mask]).astype('uint8')
361 return Image.fromarray(im)
362 else:
363 return Image.fromarray(im).convert('RGBA')
366def video_save(video_or_file, filename, verbose=False, duration=None, **kwargs):
367 """
368 Saves as a video or as a :epkg:`gif`.
369 Enregistre une vidéo dans un fichier.
370 Uses `write_videofile <https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html?
371 highlight=videofileclip#moviepy.video.io.VideoFileClip.VideoFileClip.write_videofile>`_.
373 @param video_or_file string or :epkg:`VideoClip`
374 @param filename video saved into this filename
375 @param duration overwrite duration,
376 see method `set_duration <https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html?
377 highlight=videoclip#moviepy.video.VideoClip.VideoClip.set_duration>`_
378 @param verbose logging or not
379 @param kwargs see `write_videofile <https://zulko.github.io/moviepy/ref/
380 VideoClip/VideoClip.html?highlight=videofileclip#moviepy.video.io.
381 VideoFileClip.VideoFileClip.write_videofile>`_
383 Example:
385 ::
387 from code_beatrix.faq_faq_video import video_extract_video, video_save
388 vid = video_extract_video('exemple.mp4', '00:00:01', '00:00:04')
389 video_save(vid, 'new_video.mp4')
390 """
391 if isinstance(filename, str) and os.path.splitext(filename)[-1] == '.gif':
392 with VideoContext(video_or_file) as video:
393 if duration is not None:
394 video = video.set_duration(duration)
395 if verbose:
396 video.write_gif(filename, verbose=verbose, **kwargs)
397 else:
398 f = io.StringIO()
399 with redirect_stdout(f):
400 with redirect_stderr(f):
401 video.write_gif(filename, verbose=verbose, **kwargs)
402 else:
403 with VideoContext(video_or_file) as video:
404 if duration is not None:
405 video = video.set_duration(duration)
406 if verbose:
407 video.write_videofile(filename, verbose=verbose, **kwargs)
408 else:
409 f = io.StringIO()
410 with redirect_stdout(f):
411 with redirect_stderr(f):
412 video.write_videofile(
413 filename, verbose=verbose, **kwargs)
416def video_enumerate_frames(video_or_file, folder=None, fps=10, pattern='images_%04d.jpg',
417 clean=False, **kwargs):
418 """
419 Enumerates frames from a video.
420 Itère sur des images depuis une vidéo.
422 @param video_or_file string or :epkg:`VideoClip`
423 @param folder where to exports the images or returns arrays if None
424 @param pattern image names
425 @param fps frames per seconds
426 @param clean clean open processes after it is done
427 @param kwargs arguments to `iter_frames <https://zulko.github.io/moviepy/ref/
428 AudioClip.html?highlight=frames#moviepy.audio.AudioClip.AudioClip.iter_frames>`_
429 @return iterator on arrays or files (see parameter *folder*)
431 Example:
433 ::
435 form code_beatrix.art.video import video_enumerate_frames
436 vid = 'example.mp4')
437 for frame in video_enumerate_frames(vid, folder=temp):
438 # ...
440 If *clean* is true, it calls @see fn clean_video.
441 """
442 if clean is None:
443 raise ValueError('cannot be None')
444 with VideoContext(video_or_file) as video:
445 if folder is None:
446 for frame in video.iter_frames(fps=fps, **kwargs):
447 yield frame
448 if clean:
449 clean_video(video.video)
450 else:
451 if 'dtype' in kwargs:
452 if kwargs['dtype'] != 'uint8':
453 raise ValueError("dtype must be uint8")
454 del kwargs['dtype']
456 for i, frame in enumerate(video.iter_frames(fps=fps, dtype='uint8', **kwargs)):
457 # saves as image
458 name = os.path.join(folder, pattern % i)
459 imsave(name, frame)
460 yield name
461 if clean:
462 clean_video(video.video)
465def video_replace_audio(video_or_file, new_sound, loop=True):
466 """
467 Replaces the sound of a video.
468 Remplace la bande-son d'une vidéo.
470 @param video_or_file string or :epkg:`VideoClip`
471 @param new_sound sound
472 @param loop loop on the audio if not long enough
473 @return :epkg:`VideoClip`
475 The list of available transformations is at:
476 `vfx <https://zulko.github.io/moviepy/ref/videofx.html?highlight=vfx>`_.
477 If parameter ``loop=True`` is specified,
478 *loop_duration* becomes the duration of the video.
480 Example:
482 ::
484 from code_beatrix.art.video import video_replace_sound
485 vid = video_replace_sound('video.mp4', 'son.mp3', loop=True, volumex=5.5, t_end='00:00:05')
486 """
487 with VideoContext(video_or_file) as video:
488 if loop:
489 if video.duration is None:
490 raise ValueError(
491 "The duration of the video is unknown, use audio_modification and loop=False")
492 audio = audio_modification(new_sound, loop_duration=video.duration)
493 else:
494 audio = new_sound
495 new_clip = video.set_audio(audio)
496 return new_clip
499def video_extract_audio(video_or_file):
500 """
501 Returns the audio of a video.
502 Retourne le son d'une vidéo.
504 @param video_or_file string or :epkg:`VideoClip`
505 @return :epkg:`AudioClip`
506 """
507 with VideoContext(video_or_file) as video:
508 return video.audio
511def video_remove_audio(video_or_file):
512 """
513 Returns the same video without audio.
514 Retourne la même vidéo sans le son.
516 @param video_or_file string or :epkg:`VideoClip`
517 @return :epkg:`AudioClip`
518 """
519 with VideoContext(video_or_file) as video:
520 return video.without_audio()
523def video_compose(video_or_file1, video_or_file2=None, t1=0, t2=0, place=None, **kwargs):
524 """
525 Concatenates or superposes two videos.
526 Ajoute ou superpose deux vidéos.
528 @param video_or_file1 vidéo 1 or list of video
529 @param video_or_file2 vidéo 2
530 @param t1 start of the first sound
531 @param t2 start of the second sound (or None to add it ad
532 @param place predefined placements
533 @param kwargs additional parameters,
534 sent to `CompositeVideoClip <https://zulko.github.io/moviepy/ref/
535 VideoClip/VideoClip.html?highlight=compositevideoclip#compositevideoclip>`_
536 @return :epkg:`VideoClip`
538 Example:
540 ::
542 from code_beatrix.art.video import video_compose
543 vid = video_compose('video1.mp4', 'video2.mp4', '00:00:01', '00:00:04')
545 The first video defines the size of the final video.
546 List of predefined placements:
548 * *h2*: two videos side by side horizontally
549 * *v2*: two videos side by side vertically
550 * *br*: two videos, second is placed at the bottom right corner
552 *zoom* can be defined as a argument, it applies on the second
553 video if *place* is defined and if there are two videos.
554 """
555 if place is None:
556 if isinstance(video_or_file1, list):
557 if video_or_file2 is not None:
558 raise ValueError(
559 'video_or_file1 is a list, video_or_file2 should be None')
560 vids = [VideoContext(i).__enter__() for i in video_or_file1]
561 comp = []
562 for i, v in enumerate(vids):
563 v = v.video
564 if isinstance(t1, list) and i < len(t1):
565 v.set_start(t1[i])
566 comp.append(v)
567 res = CompositeVideoClip(comp, **kwargs)
568 for v in vids:
569 v.__exit__()
570 return res
571 else:
572 with VideoContext(video_or_file1) as video1:
573 with VideoContext(video_or_file2) as video2:
574 add = []
575 if t1 != 0:
576 add.append(video1.set_start(t1))
577 else:
578 add.append(video1)
579 if t2 is None:
580 add.append(video2.set_start(video1.duration + t1))
581 else:
582 add.append(video2.set_start(t2))
583 return CompositeVideoClip(add, **kwargs)
584 else:
586 def get_two(video_or_file1, video_or_file2, t1, t2):
587 if isinstance(video_or_file1, list):
588 if len(video_or_file1) != 2:
589 raise ValueError(
590 "Expecting two videos not {0}".format(len(video_or_file1)))
591 v1, v2 = video_or_file1
592 t1, t2 = t1
593 else:
594 if video_or_file2 is None:
595 raise ValueError("Expecting two videos not less")
596 v1, v2 = video_or_file1, video_or_file2
597 vc1 = VideoContext(v1).__enter__()
598 vc2 = VideoContext(v2).__enter__()
599 return (vc1, vc2), (t1, t2)
601 (vc1, vc2), (t1, t2) = get_two(video_or_file1, video_or_file2, t1, t2)
603 v1 = vc1.video
604 v2 = vc2.video
606 if kwargs.get('zoom', 1.) != 1.:
607 v2 = video_modification(v2, resize=kwargs['zoom'])
608 del kwargs['zoom']
610 # Predefined placements.
611 if place == "h2":
612 pos1 = 0, 0
613 pos2 = v1.size[0], 0
614 v1 = video_position(v1, pos=pos1)
615 v2 = video_position(v2, pos=pos2)
616 if 'size' not in kwargs:
617 kwargs['size'] = v1.size[0] + \
618 v2.size[0], max(v1.size[1], v2.size[1])
619 res = video_compose(v1, v2, t1, t2, **kwargs)
620 elif place == "v2":
621 pos1 = 0, 0
622 pos2 = 0, v1.size[1]
623 v1 = video_position(v1, pos=pos1)
624 v2 = video_position(v2, pos=pos2)
625 if 'size' not in kwargs:
626 kwargs['size'] = max(v1.size[0], v2.size[0]
627 ), v1.size[1] + v2.size[1]
628 res = video_compose(v1, v2, t1, t2, **kwargs)
629 elif place == "br":
630 pos1 = 0, 0
631 pos2 = max(0, v1.size[0] - v2.size[0]
632 ), max(0, v1.size[1] - v2.size[1])
633 v1 = video_position(v1, pos=pos1)
634 v2 = video_position(v2, pos=pos2)
635 if 'size' not in kwargs:
636 kwargs['size'] = v1.size[0], v1.size[1]
637 res = video_compose(v1, v2, t1, t2, **kwargs)
638 else:
639 raise ValueError("Unknown placement '{0}'".format(place))
641 vc1.__exit__()
642 vc2.__exit__()
643 return res
646def video_concatenate(video_or_files, **kwargs):
647 """
648 Concatenates videos.
649 Met bout à bout des vidéos.
651 @param video_or_files list of videos or filenames
652 @param kwargs additional parameters for
653 `concatenate_videoclips <https://github.com/Zulko/moviepy/blob/master/
654 moviepy/video/compositing/concatenate.py#L15>`_
655 @return :epkg:`VideoClip`
656 """
657 ctx = [VideoContext(_).__enter__() for _ in video_or_files]
658 res = concatenate_videoclips([get_wrapped(_) for _ in ctx], **kwargs)
659 for _ in ctx:
660 _.__exit__()
661 return res
664def video_modification(video_or_file, volumex=1., resize=1., speed=1.,
665 mirrorx=False, mirrory=False):
666 """
667 Modifies a video.
668 Modifie une vidéo.
670 @param video_or_file string or :epkg:`VideoClip`
671 @param volumex multiplies the sound
672 @param speed speed of the sound
673 @param resize resize
674 @param mirrorx mirror x
675 @param mirrory mirror y
676 @return new video
678 Example:
680 ::
682 from code_beatrix.art.video import video_modification
683 vid = video_modification('video.mp4', speed=2., mirrory=True, mirrorx=True)
684 """
685 def check_duration(video):
686 if video.duration is None:
687 raise ValueError('video duration should not be None')
689 with VideoContext(video_or_file) as video:
690 if speed:
691 check_duration(video)
692 dur = video.duration
693 video = video.fl_time(lambda t: t * speed)
694 video = video.set_duration(dur / speed)
695 if volumex != 1.:
696 video = video.fx(vfx.volumex, volumex)
697 if resize != 1.:
698 video = video.fx(vfx.resize, resize)
699 if mirrorx:
700 video = video.fx(vfx.mirror_x)
701 if mirrory:
702 video = video.fx(vfx.mirror_y)
703 return video
706def video_image(image_or_file, duration=None, zoom=None, opacity=None, **kwargs):
707 """
708 Creates a :epkg:`ImageClip`.
709 Créé une vidéo à partir d'une image.
711 @param image_or_file image or file
712 @param duration duration or None if not known
713 @param zoom applies a zoom on the image
714 @param opacity opacity of the image (0 for transparent, 255 for opaque)
715 @param kwargs additional parameters for :epkg:`ImageClip`
716 @return :epkg:`ImageClip`
718 If *duration* is None, it will be fixed when the image is
719 composed with another one. The image remains wherever it is placed.
720 """
721 if isinstance(image_or_file, str):
722 img = Image.open(image_or_file)
723 return video_image(img, duration=duration, zoom=zoom, opacity=opacity, **kwargs)
724 elif isinstance(image_or_file, numpy.ndarray):
725 if zoom is not None:
726 from skimage.transform import rescale
727 img = rescale(image_or_file, zoom)
728 return video_image(img, duration=duration, opacity=opacity, **kwargs)
729 else:
730 img = image_or_file
731 if len(img.shape) != 3:
732 raise ValueError(
733 "Image is not RGB or RGBA shape={0}".format(img.shape))
734 if img.shape[2] == 3:
735 from skimage.io._plugins.pil_plugin import pil_to_ndarray
736 pilimg = Image.fromarray(img).convert('RGBA')
737 img = pil_to_ndarray(pilimg)
738 if opacity is None:
739 opacity = 255
740 if isinstance(opacity, int):
741 img[:, :, 3] = opacity
742 elif isinstance(opacity, float):
743 img[:, :, 3] = int(opacity * 255)
744 elif opacity is not None:
745 raise TypeError("opacity should be int or float or None")
746 return ImageClip(img, duration=duration, transparent=True, **kwargs)
747 elif isinstance(image_or_file, Image.Image):
748 from skimage.io._plugins.pil_plugin import pil_to_ndarray
749 if image_or_file.mode != 'RGBA':
750 image_or_file = image_or_file.convert('RGBA')
751 if zoom is not None:
752 image_or_file = image_or_file.resize(zoom)
753 img = pil_to_ndarray(image_or_file)
754 return video_image(img, duration=duration, opacity=opacity, **kwargs)
755 else:
756 raise TypeError(
757 "Unable to create a video from type {0}".format(type(image_or_file)))
760def video_position(video_or_file, pos, relative=False):
761 """
762 Modifies the position of a position.
763 Modifie la position d'une video.
764 Relies on function
765 `set_position <https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html?
766 highlight=imageclip#moviepy.video.VideoClip.VideoClip.set_position>`_.
768 @param video_or_file string or :epkg:`VideoClip`
769 @param pos see `set_position <https://zulko.github.io/moviepy/ref/VideoClip/
770 VideoClip.html?highlight=imageclip#moviepy.video.VideoClip.VideoClip.set_position>`_
771 @param relative see `set_position <https://zulko.github.io/moviepy/ref/VideoClip/
772 VideoClip.html?highlight=imageclip#moviepy.video.VideoClip.VideoClip.set_position>`_
773 @return :epkg:`VideoClip`
775 This function moves the video inside another one.
776 Therefore, it has no effect if the result of this video
777 is composed. See function @see fn video_compose.
778 Example:
780 ::
782 from code_beatrix.art.video import video_image, video_position, video_compose, video_text
784 img = 'GastonLagaffe_1121.jpg'
785 vidimg = video_image(img, duration=5, opacity=200)
786 vidimg = video_position(vidimg, lambda t: (0, 0), relative=True)
788 text = video_text('boule', size=2., color=(255, 0, 0, 128), background=(0, 255, 0, 100))
789 text = video_position(text, lambda t: (t * 0.1, t * 0.2), relative=True)
791 comb = video_compose([vidimg, text], t1=[0, 1])
793 You can see an example of the video it produces in notebook
794 :ref:`video_notebook`.
795 """
796 with VideoContext(video_or_file) as video:
797 video = video.set_position(pos=pos, relative=relative)
798 return video
801def video_resize(video_or_file, newsize):
802 """
803 Resizes a video.
804 Modifie la taille d'une video.
805 Relies on function
806 `resize <https://zulko.github.io/moviepy/ref/videofx/moviepy.video.fx.all.resize.html#moviepy.video.fx.all.resize>`_.
808 @param video_or_file string or :epkg:`VideoClip`
809 @param newsize `resize <https://zulko.github.io/moviepy/ref/videofx/
810 moviepy.video.fx.all.resize.html#moviepy.video.fx.all.resize>`_
811 @return :epkg:`VideoClip`
812 """
813 with VideoContext(video_or_file) as video:
814 video = video.resize(newsize)
815 return CompositeVideoClip([video])
818def video_text(text, font=None, fontsize=32, size=None,
819 color=None, background=None, opacity=None,
820 **kwargs):
821 """
822 Creates an image with text (:epkg:`ImageClip`).
823 Créé une image à partir de texte.
825 @param text text
826 @param color color
827 @param font police name
828 @param fontsize font size
829 @param size image size, None to get the smallest one which
830 contains the text, a float to get *size* times
831 this smallest size
832 @param background background of the image
833 @param opacity to overwrite the opacity,
834 *color* and *background* should be 4-uple colors,
835 the last number in ``(0, 0, 0, 255)`` represents the
836 opacity
837 @param kwargs additional parameters sent to @see fn video_image
838 @return :epkg:`ImageClip`
840 If *duration* is None, it will be fixed when the image is
841 composed with another one. The image remains wherever it is placed.
842 The *opacity* is a number between 0 (transparent) and 255 (opaque).
843 0 means the image cannot be seen. The number can be set up for each
844 pixel. By default, the image background is transparent (0). You can find
845 many font at `google/fonts <https://github.com/google/fonts/tree/master/ofl>`_
846 or `msfonts <https://github.com/caarlos0-graveyard/msfonts/tree/master/fonts>`_.
847 """
848 if background is None:
849 background = (255, 255, 255, 0)
850 if color is None:
851 color = (0, 0, 0, 255)
852 if isinstance(font, str):
853 if not font.endswith('.ttf'):
854 font += '.ttf'
855 elif font is None:
856 if sys.platform.startswith('win'):
857 font = "arial.ttf"
858 else:
859 exp = '/usr/share/fonts/truetype/dejavu' # os.path.expanduser('~')
860 d = exp # os.path.join(exp, '.local', 'share', 'fonts')
861 if not os.path.exists(d):
862 raise FileNotFoundError("Unable to find '{0}'".format(d))
863 font = os.path.join(d, "DejaVuSans.ttf")
864 if not os.path.exists(font):
865 raise FileNotFoundError("Unable to find font '{0}'. Available:\n{1}".format(
866 font, "\n".join(os.listdir(d))))
867 try:
868 obj = ImageFont.truetype(font=font, size=fontsize)
869 except OSError as e:
870 raise FontError("Unable to find font '{0}'".format(font)) from e
871 if size is None:
872 size = obj.getsize(text)
873 elif isinstance(size, (float, int)):
874 fs = obj.getsize(text)
875 size = (int(fs[0] * size), int(fs[1] * size))
876 elif not isinstance(size, tuple):
877 raise TypeError("size should be a tuple or a float")
878 if opacity is not None:
879 if len(color) == 3:
880 color = color + (opacity,)
881 elif len(color) == 4:
882 color = color[:3] + (opacity,)
883 else:
884 raise ValueError("color should a 3 or 4 tuple")
885 img = Image.new('RGBA', size, background)
886 draw = ImageDraw.Draw(img)
887 draw.text((0, 0), text, font=obj, fill=color)
888 return video_image(img, **kwargs)
891def video_frame(fct_frame, **kwargs):
892 """
893 Creates a video from drawing or images.
894 *fct_frame* can either be a function which draws a picture at time *t*
895 or a list of picture names or a folder.
896 Créé une vidéo à partir de dessins ou d'images.
897 *fct_frame* est soit une fonction qui dessine chaque image à chaque instant *t*,
898 une liste de noms d'images ou un répertoire.
900 @param fct_frame function like ``def make_frame(t: float) -> numpy.ndarray``,
901 or list of images or folder name
902 @param kwargs additional arguments for function
903 `make_frame <https://zulko.github.io/moviepy/getting_started/videoclips.html#videoclip>`_
904 @return :epkg:`VideoClip`
905 """
906 if isinstance(fct_frame, str):
907 if not os.path.exists(fct_frame):
908 raise FileNotFoundError(
909 "Unable to find folder '{0}'".format(fct_frame))
910 imgs = os.listdir(fct_frame)
911 exts = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff'}
912 imgs = [os.path.join(fct_frame, _)
913 for _ in imgs if os.path.splitext(_)[-1].lower() in exts]
914 return video_frame(imgs, **kwargs)
915 elif isinstance(fct_frame, list):
916 for img in fct_frame:
917 if not os.path.exists(img):
918 raise FileNotFoundError(
919 "Unable to find image '{0}'".format(img))
920 return ImageSequenceClip(fct_frame, **kwargs)
921 else:
922 return VideoClip(fct_frame, **kwargs)