Skip to content
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

Useful multimedia manipulation #2

Merged
merged 18 commits into from
Dec 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions lectures/useful-multimedia-manipulation/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
PYTHON[[:space:]]PRESENTATION[[:space:]]RVA[[:space:]]2020[[:space:]]JULY.key filter=lfs diff=lfs merge=lfs -text
*ppts filter=lfs diff=lfs merge=lfs -text
PYTHON[[:space:]]PRESENTATION[[:space:]]RVA[[:space:]]2020[[:space:]]JULY.pptx filter=lfs diff=lfs merge=lfs -text
4 changes: 4 additions & 0 deletions lectures/useful-multimedia-manipulation/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
figures
abstract
*.html
mailmap
Git LFS file not shown
Binary file not shown.
Git LFS file not shown
29 changes: 29 additions & 0 deletions lectures/useful-multimedia-manipulation/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Python for Quick, Useful Multimedia Manipulation: Anecdotes from a Python Programmer
=====================================================================================

One of Python's many killer features is that a programmer can create snippets of code, and organically and coherently join them into larger collections of module code. In this presentation, I describe and demonstrate relatively simple Python tools -- for image, video, and audio manipulation -- that I use every day at work and outside work.

* automatically cropping out whitespace in images (surprisingly easy to do).

* creating movies from a sequence of generated images.

* conversion of movie clips, either files or YouTube clips, into animated GIFs (useful where the online service does not allow for video animations from movie files, such as GitHub_ or `Read the Docs`_).

If there's time or interest, I can even describe and demonstrate how to retrieve and label music you might find, all within Python.

Demonstrations live in the ``demonstrations`` subdirectory, and each demonstration is its own directory within ``demonstrations``, and each demonstration has its own ``README.rst``. Here are the four demonstration directories with description.

1. ``autocropping_images``: autocropping a PNG image and a PDF image.

2. ``movie_image_demos``: converting a sequence of images into an MP4 file.

3. ``movie_gif_demos``: converting an MP4 file and a YouTube clip into animated GIFs.

4. ``making_music_youtube``: using a tool, `plex_music_songs`_, that takes metadata from MusicBrainz_ and the YouTube clip using `youtube-dl`_, into an M4A file.

.. _GitHub: https://github.com
.. _`Read the Docs`: https://www.readthedocs.io
.. _CloudConvert: https://cloudconvert.com
.. _`plex_music_songs`: https://plexstuff.readthedocs.io/plex-music/cli_tools/plex_music_cli.html?highlight=plex_music_songs#plex-music-songs
.. _MusicBrainz: https://musicbrainz.org
.. _`youtube-dl`: https://rg3.github.io/youtube-dl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
iwanttobelieve_cropped.png
cumulative_plot_emission_cropped.pdf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
INSTRUCTIONS
=============

1. Go to these locations:

a. autocropping lossy images: `nprstuff.core.autocrop_image.autocrop_image <autocrop_image_>`_

b. autocropping PDFs: `nprstuff.core.autocrop_image.crop_pdf_singlepage <autocrop_image_pdf_>`_

2. Cropping a PNG image:

.. code-block:: console

autoCropImage --input=iwanttobelieve_uncropped.png --output=iwanttobelieve_cropped.png

Compare ``iwanttobelieve_uncropped.png`` and ``iwanttobelieve_cropped.png`` in a browser.

3. Cropping a PDF image:

.. code-block:: console

autoCropImage --input=cumulative_plot_emission_uncropped.pdf --output=cumulative_plot_emission_cropped.pdf

Compare ``cumulative_plot_emission_uncropped.pdf`` and ``cumulative_plot_emission_cropped.pdf`` in a PDF viewer.


.. _`autocrop_image`: https://github.com/tanimislam/nprstuff/blob/f67e719ba4f2ca7120774937d27cb1adbb51c933/nprstuff/core/autocrop_image.py#L25

.. _`autocrop_image_pdf`: https://github.com/tanimislam/nprstuff/blob/f67e719ba4f2ca7120774937d27cb1adbb51c933/nprstuff/core/autocrop_image.py#L193
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.m4a
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
INSTRUCTIONS
=============

1. Go to these locations:

a. showing Musicbrainz_ getting metadata (given an artist, all the albums + all songs of artist): `plexstuff.plexmusic.plexmusic.get_music_metadata <get_music_metadata_>`_

b. showing choose the correct YouTube clip: `plexstuff.plexmusic.plexmusic.youtube_search <youtube_search_>`_

c. showing part of downloading from `youtube-dl`: `plexstuff.plexmusic.plexmusic.get_youtube_file <get_youtube_file_>`_

2. ``plex_music_songs`` documentation at `this website`_.

3. Show animation of getting songs, ``plex_music_songs_download_artist_songs.mp4``, with video viewer.

4. Show how to get ``All I Need`` by ``Air``

.. code-block:: console

plex_music_songs -a Air -s "All I Need" --musicbrainz

Which can take a bit of time.

.. _MusicBrainz: https://musicbrainz.org

.. _`get_music_metadata`: https://github.com/tanimislam/plexstuff/blob/37cfb9f9e52864d8bdd6a2e154dc93b48ff2c908/plexstuff/plexmusic/plexmusic.py#L411

.. _`youtube_search`: https://github.com/tanimislam/plexstuff/blob/37cfb9f9e52864d8bdd6a2e154dc93b48ff2c908/plexstuff/plexmusic/plexmusic.py#L888

.. _`youtube-dl`: https://rg3.github.io/youtube-dl

.. _`this website`: https://plexstuff.readthedocs.io/plex-music/cli_tools/plex_music_cli.html?highlight=plex_music_songs#plex-music-songs

.. _`get_youtube_file`: https://github.com/tanimislam/plexstuff/blob/37cfb9f9e52864d8bdd6a2e154dc93b48ff2c908/plexstuff/plexmusic/plexmusic.py#L848
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.gif
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
INSTRUCTIONS
=============

1. Go to these locations in `nprstuff.core.convert_image <convert_image_>`_.

a. mp4togif: `nprstuff.core.convert_image.mp4togif <mp4togif_>`_

b. youtube2gif: `nprstuff.core.convert_image.youtube2gif <youtube2gif_>`_

2. Lower level FFMPEG_ command comes from `this website`_.

3. MP4 to animated GIF:

.. code-block:: console

convertImage movie -f covid19_conus_cases_04072020.mp4

Creates animated GIF ``covid19_conus_cases_04072020.gif``, open in browser.

4. YouTube Clip to GIF:

.. code-block:: console

convertImage youtube -u "https://www.youtube.com/watch?v=R-pmYwr8zbU" -o "lucas_bros.gif" -q high

Creates animated GIF ``lucas_bros.gif``, open in browser.


.. _`convert_image`: https://github.com/tanimislam/nprstuff/blob/master/nprstuff/core/convert_image.py

.. _mp4togif: https://github.com/tanimislam/nprstuff/blob/807a3cba7e8bfd6ded70cdea3083cd9c9494e438/nprstuff/core/convert_image.py#L150

.. _youtube2gif: https://github.com/tanimislam/nprstuff/blob/807a3cba7e8bfd6ded70cdea3083cd9c9494e438/nprstuff/core/convert_image.py#L135

.. _`this website`: http://blog.pkh.me/p/21-high-quality-gif-with-ffmpeg.html

.. _FFMPEG: https://ffmpeg.org
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.mp4
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
INSTRUCTIONS
=============

1. Go to this gist: `demo_create_movie_sequence.py`_.

2. Instructions on the FFMPEG_ syntax to make an MP4 movie from a sequence of images come from `this website`_.

3. MP4 movie of MCMC demo:

.. code-block:: console

python3.7 demo_create_movie_sequence.py --prefix=img --output="mcmc_images.mp4" --dirname="mcmc_animation_images" --fps=10

Open ``mcmc_images.mp4`` with a video viewer. This is 10 frames per second.

4. MP4 movie of cumulative COVID-19 cases in continental United States:

.. code-block:: console

python3.7 demo_create_movie_sequence.py --prefix="covid19_conus_cases_04072020." --output="covid19_conus_cases_04072020.mp4" --dirname="covid19_conus_cases_04072020_imagefiles" --fps=5

Open ``covid19_conus_cases_04072020.mp4`` with a video viewer. This is 5 frames per second.

.. _`demo_create_movie_sequence.py`: https://gist.github.com/tanimislam/406a1379e746c9882c101f656a6da949
.. _FFMPEG: https://ffmpeg.org
.. _`this website`: https://hamelot.io/visualization/using-ffmpeg-to-convert-a-set-of-images-into-a-video/
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env python3

"""
I developed this Python CLI script to demonstrate creating an MP4 movie from an image sequence.
I follow instructions from https://hamelot.io/visualization/using-ffmpeg-to-convert-a-set-of-images-into-a-video. I make an MP4 movie that is 5 FPS with psychovisual quality of 25.
"""
import os, sys, numpy, subprocess, glob, re, logging, time
from PIL import Image
from distutils.spawn import find_executable
from argparse import ArgumentParser

def create_movie_from_sequence( prefix, output_file_name, dirname = os.getcwd( ), fps = 5 ):
"""
Creates an MP4 movie from a sequence of PNG images. The output file name must end in .mp4.

:param str prefix: the beginnning base name of all the PNG images. If `foo` is the prefix, then will look for images that are like `foo0001.png`, `foo0002.png` and so forth. To make life simpler, the prefix *MUST* be alphanumeric.
:param str output_file_name: the name of the output file. Must end in .mp4.
:param str dirname: optional argument. The directory that contains the image sequence. Default is the current working directory.
:param int fps: frames per second. Default is 5.
:returns: `True` if successful, `False` otherwise.
:rtype: bool
"""
time0 = time.time( )
assert( os.path.isdir( dirname ) ) # is a directory
assert( os.path.basename( output_file_name ).endswith( '.mp4' ) ) # ends with mp4
assert( re.match( '^[a-zA-Z]+', prefix ) is not None ) # alphanumeric
assert( find_executable( 'ffmpeg' ) is not None )
assert( fps >= 1 ) # fps must be positive
ffmpeg_exec = find_executable( 'ffmpeg' )
#
## now sequence of images
sorted_filenames = sorted(
filter(lambda fname: re.match('.*[0-9]+\.png', os.path.basename( fname ) ) is not None and
os.path.basename( fname ).startswith( prefix ),
glob.glob( os.path.join( dirname,'%s*.png' % prefix ) ) ) )
if len( sorted_filenames ) == 0:
print( 'ERROR, COULD FIND NO IMAGE SEQUENCE.' )
return False
def is_divis_2( fname ):
img = Image.open( fname )
if img.size[0] % 2 != 0: return False
if img.size[1] % 2 != 0: return False
return True
try:
assert(all(filter(is_divis_2, sorted_filenames))) # all widths and heights div by 2
except:
print( "ERROR, NOT ALL IMAGES HAVE WIDTHS + HEIGHTS DIVISIBLE BY 2.")
return False

#
##
num_base_10 = 1 + int( numpy.log10(len(sorted_filenames)))
sequence_ffmpeg = '%%%02dd' % num_base_10 # this is tricky!
input_ffmpeg_image_string = '%s%s.png' % ( os.path.join( dirname, prefix ), sequence_ffmpeg )
logging.info( 'got here, %s.' % input_ffmpeg_image_string )
#
## now run the ffmpeg command
command_to_process = [
ffmpeg_exec, '-y', '-r', '%d' % fps, '-f', 'image2', '-i', input_ffmpeg_image_string,
'-vcodec', 'libx264', '-crf', '25', '-pix_fmt', 'yuv420p',
output_file_name ]
logging.info( 'COMMAND TO RUN: %s.' % ' '.join( command_to_process ) )
proc = subprocess.Popen( command_to_process, stdout = subprocess.PIPE,
stderr = subprocess.STDOUT )
stdout_val, stderr_val = proc.communicate( )
logging.info( 'STDOUT_MESSAGE.' )
logging.info( '%s\n' % stdout_val )
logging.info( 'TOOK %0.3f SECONDS TO RUN TO COMPLETION.' % (
time.time( ) - time0 ) )

if __name__=='__main__':
parser = ArgumentParser( )
parser.add_argument( '--prefix', dest='prefix', type=str, required = True,
help = 'The prefix to the sequence of PNG images.' )
parser.add_argument( '--output', dest='output', type=str, required = True,
help = 'The name of the MP4 output file.' )
parser.add_argument( '--dirname', dest='dirname', type=str, default = os.getcwd( ),
help = 'The directory containing the image sequence. Default is CWD.')
parser.add_argument( '--fps', dest='fps', type=int, default = 5,
help = 'Frames per second of movie. Default is 5.' )
parser.add_argument( '--info', dest='do_info', action='store_true', default = False,
help = 'If chosen, then print out INFO debug logging.' )
args = parser.parse_args( )
logger = logging.getLogger( )
#
if args.do_info: logger.setLevel( logging.INFO )
status = create_movie_from_sequence(
args.prefix, args.output, dirname = args.dirname, fps = args.fps )

Loading