From 6dff15d84ea9b9dec9caa0569c22ab13a91391eb Mon Sep 17 00:00:00 2001 From: Patrick Snape Date: Thu, 19 May 2016 18:11:19 +0100 Subject: [PATCH 1/6] Fix documentation errors - add missing docs Missing docs for import_video, import_videos, video_paths, pickle_paths. Also, fix Python 3 error for importing custom sphinx extensions. --- docs/source/api/menpo/io/import_video.rst | 7 +++++ docs/source/api/menpo/io/import_videos.rst | 7 +++++ docs/source/api/menpo/io/index.rst | 4 +++ docs/source/api/menpo/io/pickle_paths.rst | 7 +++++ docs/source/api/menpo/io/video_paths.rst | 7 +++++ docs/sphinxext/__init__.py | 2 +- docs/xref_map.py | 3 +- menpo/io/input/base.py | 32 +++++++++++++++++++--- menpo/transform/homogeneous/affine.py | 11 ++++++-- menpo/transform/homogeneous/similarity.py | 5 ++-- 10 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 docs/source/api/menpo/io/import_video.rst create mode 100644 docs/source/api/menpo/io/import_videos.rst create mode 100644 docs/source/api/menpo/io/pickle_paths.rst create mode 100644 docs/source/api/menpo/io/video_paths.rst diff --git a/docs/source/api/menpo/io/import_video.rst b/docs/source/api/menpo/io/import_video.rst new file mode 100644 index 000000000..c1e0ee299 --- /dev/null +++ b/docs/source/api/menpo/io/import_video.rst @@ -0,0 +1,7 @@ +.. _menpo-io-import_video: + +.. currentmodule:: menpo.io + +import_video +============ +.. autofunction:: import_video diff --git a/docs/source/api/menpo/io/import_videos.rst b/docs/source/api/menpo/io/import_videos.rst new file mode 100644 index 000000000..d5db9a7a9 --- /dev/null +++ b/docs/source/api/menpo/io/import_videos.rst @@ -0,0 +1,7 @@ +.. _menpo-io-import_videos: + +.. currentmodule:: menpo.io + +import_videos +============= +.. autofunction:: import_videos diff --git a/docs/source/api/menpo/io/index.rst b/docs/source/api/menpo/io/index.rst index be55fc9cf..16137d21f 100644 --- a/docs/source/api/menpo/io/index.rst +++ b/docs/source/api/menpo/io/index.rst @@ -11,6 +11,8 @@ Input import_image import_images + import_video + import_videos import_landmark_file import_landmark_files import_pickle @@ -37,6 +39,8 @@ Path Operations image_paths landmark_file_paths + pickle_paths + video_paths data_path_to data_dir_path ls_builtin_assets diff --git a/docs/source/api/menpo/io/pickle_paths.rst b/docs/source/api/menpo/io/pickle_paths.rst new file mode 100644 index 000000000..e5aad2067 --- /dev/null +++ b/docs/source/api/menpo/io/pickle_paths.rst @@ -0,0 +1,7 @@ +.. _menpo-io-pickle_paths: + +.. currentmodule:: menpo.io + +pickle_paths +============ +.. autofunction:: pickle_paths diff --git a/docs/source/api/menpo/io/video_paths.rst b/docs/source/api/menpo/io/video_paths.rst new file mode 100644 index 000000000..39abff908 --- /dev/null +++ b/docs/source/api/menpo/io/video_paths.rst @@ -0,0 +1,7 @@ +.. _menpo-io-video_paths: + +.. currentmodule:: menpo.io + +video_paths +=========== +.. autofunction:: video_paths diff --git a/docs/sphinxext/__init__.py b/docs/sphinxext/__init__.py index c05b45ca0..07206d8fe 100644 --- a/docs/sphinxext/__init__.py +++ b/docs/sphinxext/__init__.py @@ -1 +1 @@ -import ref_prettify +from . import ref_prettify diff --git a/docs/xref_map.py b/docs/xref_map.py index c78fcc76f..d23291686 100644 --- a/docs/xref_map.py +++ b/docs/xref_map.py @@ -69,5 +69,6 @@ 'Viewable': ('class', 'menpo.visualize.Viewable'), 'view_patches': ('function', 'menpo.visualize.view_patches'), 'VComposable': ('class', 'menpo.transform.VComposable'), -'VInvertible': ('class', 'menpo.transform.VInvertible') +'VInvertible': ('class', 'menpo.transform.VInvertible'), +'video_paths': ('function', 'menpo.io.video_paths'), } diff --git a/menpo/io/input/base.py b/menpo/io/input/base.py index f8e8b9b95..054901492 100644 --- a/menpo/io/input/base.py +++ b/menpo/io/input/base.py @@ -119,6 +119,15 @@ def import_video(filepath, landmark_resolver=same_name_video, normalise=True, extension of the landmark file appended with the frame number, although this behavior can be customised (see `landmark_resolver`). + .. warning:: + + This method currently uses imageio to perform the importing in + conjunction with the ffmpeg plugin. As of this release, and the release + of imageio at the time of writing (1.5.0), the per-frame computation + is not very accurate. This may cause errors when importing frames + that do not actually map to valid timestamps within the image. + Therefore, use this method at your own risk. + Parameters ---------- filepath : `pathlib.Path` or `str` @@ -147,6 +156,12 @@ def import_video(filepath, landmark_resolver=same_name_video, normalise=True, An lazy list of :map:`Image` or subclass thereof which wraps the frames of the video. This list can be treated as a normal list, but the frame is only read when the video is indexed or iterated. + + Examples + -------- + >>> video = menpo.io.import_video('video.avi') + >>> # Lazily load the 100th frame without reading the entire video + >>> frame100 = video[100] """ kwargs = {'normalise': normalise} @@ -269,10 +284,10 @@ def import_images(pattern, max_images=None, shuffle=False, -------- Import images at 20% scale from a huge collection: - >>> images = [] - >>> for img in menpo.io.import_images('./massive_image_db/*'): - >>> # rescale to a sensible size as we go - >>> images.append(img.rescale(0.2)) + >>> rescale_20p = lambda x: x.rescale(0.2) + >>> images = menpo.io.import_images('./massive_image_db/*') # Returns immediately + >>> images.map(rescale_20p) # Returns immediately + >>> images[0] # Get the first image, resize, lazily loaded """ kwargs = {'normalise': normalise} return _import_glob_lazy_list( @@ -302,6 +317,15 @@ def import_videos(pattern, max_videos=None, shuffle=False, will load an image at run time. If all images should be loaded, then simply wrap the returned :map:`LazyList` in a Python `list`. + .. warning:: + + This method currently uses imageio to perform the importing in + conjunction with the ffmpeg plugin. As of this release, and the release + of imageio at the time of writing (1.5.0), the per-frame computation + is not very accurate. This may cause errors when importing frames + that do not actually map to valid timestamps within the image. + Therefore, use this method at your own risk. + Parameters ---------- pattern : `str` diff --git a/menpo/transform/homogeneous/affine.py b/menpo/transform/homogeneous/affine.py index d4b938b66..79fa1d449 100644 --- a/menpo/transform/homogeneous/affine.py +++ b/menpo/transform/homogeneous/affine.py @@ -109,9 +109,12 @@ def decompose(self): Returns ------- transforms : `list` of :map:`DiscreteAffine` - Equivalent to this affine transform, such that:: + Equivalent to this affine transform, such that + + .. code-block:: python + + reduce(lambda x, y: x.chain(y), self.decompose()) == self - reduce(lambda x,y: x.chain(y), self.decompose()) == self """ from .rotation import Rotation from .translation import Translation @@ -168,18 +171,20 @@ def n_parameters(self): [p1, p3, p5] [p2, p4, p6] + 3D Affine: 12 parameters:: [p1, p4, p7, p10] [p2, p5, p8, p11] [p3, p6, p9, p12] + """ return self.n_dims * (self.n_dims + 1) def _as_vector(self): r""" Return the parameters of the transform as a 1D array. These parameters - are parametrised as deltas from the identity warp. This does not + are parametrised as deltas from the identity warp. This does nots include the homogeneous part of the warp. Note that it flattens using Fortran ordering, to stay consistent with Matlab. diff --git a/menpo/transform/homogeneous/similarity.py b/menpo/transform/homogeneous/similarity.py index 46b500412..1d6106053 100644 --- a/menpo/transform/homogeneous/similarity.py +++ b/menpo/transform/homogeneous/similarity.py @@ -55,8 +55,9 @@ def _transform_str(self): @property def n_parameters(self): - r""" - 2D Similarity: 4 parameters:: + r"""Number of parameters of Similarity + + 2D Similarity - 4 parameters :: [(1 + a), -b, tx] [b, (1 + a), ty] From 5b50f223aa18b0b922fa12d8b32f177b2fe98182 Mon Sep 17 00:00:00 2001 From: Patrick Snape Date: Thu, 19 May 2016 18:13:09 +0100 Subject: [PATCH 2/6] Deal with imageio video errors Imageio incorrectly calculates the number of frames in a video as int(round(duration * fps)) which is actually just a close approximation to the total number of frames. Videos don't actually contain individual frames and, depending on the algorithm, are usually roughly composed of key frames + interpolation. Therefore, videos should really be accessed via time stamps. Therefore, we try and just improve the experience by removing frames whereby ffmpeg throws an error. --- menpo/io/input/video.py | 42 ++++++++++++++ menpo/io/test/io_import_test.py | 97 +++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/menpo/io/input/video.py b/menpo/io/input/video.py index 2150b82f7..7d30c163c 100644 --- a/menpo/io/input/video.py +++ b/menpo/io/input/video.py @@ -1,3 +1,4 @@ +import warnings from functools import partial from menpo.image.base import normalize_pixels_range, channels_to_front @@ -55,4 +56,45 @@ def imageio_to_menpo(imio_reader, index): ll = LazyList.init_from_index_callable(index_callable, reader.get_length()) ll.fps = reader.get_meta_data()['fps'] + + if len(ll) != 0: + # TODO: Remove when imageio fixes the ffmpeg importer duration/start + # This is a bit grim but the frame->timestamp logic in imageio at + # the moment is not very accurate and so we really need to ensure + # that the user is returned a list they can actually index into. So + # we just remove all the frames that we can't actually index into. + # Remove from the front + for start in range(len(ll)): + if start > 10: # Arbitrary but probably safe + warnings.warn('Highly inaccurate frame->timestamp mapping ' + 'returned by imageio - many frames are being ' + 'dropped and thus importing may be very slow.' + ' Please see the documentation.') + try: + ll[start] + break + except: + pass + else: + # If we never broke out then ALL frames raised exceptions + ll = LazyList([]) + # Only take the frames after the initial broken ones + ll = ll[start:] + + if len(ll) != 0: + n_frames = len(ll) - 1 + for end in range(n_frames, -1, -1): + if end < n_frames - 10: # Arbitrary but probably safe + warnings.warn('Highly inaccurate frame->timestamp mapping ' + 'returned by imageio - many frames are being ' + 'dropped and thus importing may be very slow.' + ' Please see the documentation.') + try: + ll[end] + break + except: + pass + # Only take the frames before the broken ones + ll = ll[:end+1] + return ll diff --git a/menpo/io/test/io_import_test.py b/menpo/io/test/io_import_test.py index 6805262a9..6929dcb97 100644 --- a/menpo/io/test/io_import_test.py +++ b/menpo/io/test/io_import_test.py @@ -511,6 +511,103 @@ def test_importing_pickles_as_generator(is_file, glob, mock_open, mock_pickle): assert objs[1]['test'] == 1 +# TODO: Remove when imageio is fixed +@patch('imageio.get_reader') +@patch('menpo.io.input.base.Path.is_file') +def test_importing_imageio_ffmpeg_bad_frames(is_file, mock_reader): + def fake_get_data(index): + if index not in [1, 2]: + raise ValueError() + f = np.ones((10, 10, 3)) * -1 + f[0, 0, 0] = index + return f + + mock_reader.return_value.get_length.return_value = 4 + mock_reader.return_value.get_data.side_effect = fake_get_data + is_file.return_value = True + + ll = mio.import_video('fake_image_being_mocked.avi', normalise=False) + assert len(ll) == 2 + + im = ll[0] + assert im.shape == (10, 10) + assert im.pixels[0, 0, 0] == 1 + + +# TODO: Remove when imageio is fixed +@patch('imageio.get_reader') +@patch('menpo.io.input.base.Path.is_file') +def test_importing_imageio_ffmpeg_many_bad_frames_warning_start(is_file, mock_reader): + def fake_get_data(index): + if index < 11: + raise ValueError() + f = np.ones((10, 10, 3)) * -1 + f[0, 0, 0] = index + return f + + mock_reader.return_value.get_length.return_value = 15 + mock_reader.return_value.get_data.side_effect = fake_get_data + is_file.return_value = True + + with warnings.catch_warnings(record=True) as w: + ll = mio.import_video('fake_image_being_mocked.avi', normalise=False) + assert len(w) == 1 + assert len(ll) == 4 + + im = ll[0] + assert im.shape == (10, 10) + assert im.pixels[0, 0, 0] == 11 + + +# TODO: Remove when imageio is fixed +@patch('imageio.get_reader') +@patch('menpo.io.input.base.Path.is_file') +def test_importing_imageio_ffmpeg_many_bad_frames_warning_end(is_file, mock_reader): + def fake_get_data(index): + if index > 3: + raise ValueError() + f = np.ones((10, 10, 3)) * -1 + f[0, 0, 0] = index + return f + + mock_reader.return_value.get_length.return_value = 15 + mock_reader.return_value.get_data.side_effect = fake_get_data + is_file.return_value = True + + with warnings.catch_warnings(record=True) as w: + ll = mio.import_video('fake_image_being_mocked.avi', normalise=False) + assert len(w) == 1 + assert len(ll) == 4 + + assert ll[0].pixels[0, 0, 0] == 0 + assert ll[-1].pixels[0, 0, 0] == 3 + + +# TODO: Remove when imageio is fixed +@patch('imageio.get_reader') +@patch('menpo.io.input.base.Path.is_file') +def test_importing_imageio_ffmpeg_all_bad_frames(is_file, mock_reader): + def fake_get_data(_): + raise ValueError() + + mock_reader.return_value.get_length.return_value = 2 + mock_reader.return_value.get_data.side_effect = fake_get_data + is_file.return_value = True + + ll = mio.import_video('fake_image_being_mocked.avi') + assert len(ll) == 0 + + +@patch('imageio.get_reader') +@patch('menpo.io.input.base.Path.is_file') +def test_importing_imageio_avi_no_frames(is_file, mock_image): + mock_image.return_value.get_length.return_value = 0 + is_file.return_value = True + + ll = mio.import_video('fake_image_being_mocked.avi') + assert len(ll) == 0 + + @patch('imageio.get_reader') @patch('menpo.io.input.base.Path.is_file') def test_importing_imageio_avi_normalise(is_file, mock_image): From 29de1aff73644f42edb898c1955c20183c3e8734 Mon Sep 17 00:00:00 2001 From: Patrick Snape Date: Thu, 19 May 2016 18:38:56 +0100 Subject: [PATCH 3/6] Ignore _build in source folder Try to guess where read the docs is creating a dirty git status from. --- docs/.gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/.gitignore b/docs/.gitignore index 12f087d98..db175022d 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1 +1,3 @@ +_build/* +source/_build/* source/api/generated/* From da0ee01bfb2e07ad8265db60399e317ccb04eed1 Mon Sep 17 00:00:00 2001 From: Patrick Snape Date: Thu, 19 May 2016 20:46:44 +0100 Subject: [PATCH 4/6] Relax mock dep --- conda/meta.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda/meta.yaml b/conda/meta.yaml index 7d6b030ac..8feea729f 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -26,7 +26,7 @@ requirements: - matplotlib >=1.4,<2.0 # Test dependencies - - mock 1.3.* + - mock - nose test: From 08115912a798e571eec7fda9dc811cb0f4910ac4 Mon Sep 17 00:00:00 2001 From: Patrick Snape Date: Thu, 19 May 2016 20:46:54 +0100 Subject: [PATCH 5/6] Relax RTD requirements --- docs/rtd_environment.yml | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/docs/rtd_environment.yml b/docs/rtd_environment.yml index 0347fbeaf..b4c73a61f 100644 --- a/docs/rtd_environment.yml +++ b/docs/rtd_environment.yml @@ -4,20 +4,18 @@ channels: dependencies: - python - setuptools - - numpy>=1.10,<1.11 - - cython=0.23.* + - cython>=0.23 - pathlib=1.0 - - numpy>=1.10,<1.11 - - scipy=0.17.* - - pillow=3.0.* - - cyvlfeat>=0.4.2,<0.5 - - imageio>=1.5.0,<1.6.0 - - matplotlib>=1.4,<1.6 - - mock=1.3.* + - numpy>=1.10,<2.0 + - scipy>=0.16,<1.0 + - pillow>=3.0,<4.0 + - imageio>=1.5,<2.0 + - cyvlfeat>=0.4.3,<0.5 + - matplotlib>=1.4,<2.0 + - mock - nose - pip: - - sphinx==1.3.1 - - sphinx_rtd_theme==0.1.7 - - sphinxmapxrefrole==0.2.1 - + - sphinx + - sphinx_rtd_theme + - sphinxmapxrefrole>=0.2 From e35b57873ca09ad2608f24cec158a68b8d4b2d5b Mon Sep 17 00:00:00 2001 From: James Booth Date: Fri, 20 May 2016 07:39:36 +0100 Subject: [PATCH 6/6] Undo docs typo Affine._as_vector() --- menpo/transform/homogeneous/affine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/menpo/transform/homogeneous/affine.py b/menpo/transform/homogeneous/affine.py index 79fa1d449..84e2fc9bd 100644 --- a/menpo/transform/homogeneous/affine.py +++ b/menpo/transform/homogeneous/affine.py @@ -184,7 +184,7 @@ def n_parameters(self): def _as_vector(self): r""" Return the parameters of the transform as a 1D array. These parameters - are parametrised as deltas from the identity warp. This does nots + are parametrised as deltas from the identity warp. This does not include the homogeneous part of the warp. Note that it flattens using Fortran ordering, to stay consistent with Matlab.