Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +1 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/__init__.py +127 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/_bsdf.py +918 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/_freeimage.py +1327 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/_swf.py +901 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/bsdf.py +326 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/dicom.py +318 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/feisem.py +95 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/ffmpeg.py +732 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/fits.py +126 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/freeimage.py +405 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/freeimagemulti.py +320 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/grab.py +109 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/lytro.py +718 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/opencv.py +301 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/pillow.py +447 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/pillow_info.py +1053 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/pillow_legacy.py +832 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/pillowmulti.py +332 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/pyav.py +976 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/simpleitk.py +156 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/spe.py +755 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/swf.py +338 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/tifffile.py +548 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/matplotlib/__pycache__/figure.cpython-38.pyc +3 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/matplotlib/mpl-data/sample_data/axes_grid/bivariate_normal.npy +3 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/__init__.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/cmuarctic.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/cmudict.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/commonvoice.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/dr_vctk.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/gtzan.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/librilight_limited.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/librimix.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/librispeech.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/libritts.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/ljspeech.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/quesst14.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/speechcommands.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/tedlium.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/utils.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/vctk.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/yesno.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/models/__pycache__/__init__.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/models/__pycache__/conformer.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/models/__pycache__/conv_tasnet.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/models/__pycache__/deepspeech.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/models/__pycache__/emformer.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/models/__pycache__/rnnt.cpython-38.pyc +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/models/__pycache__/rnnt_decoder.cpython-38.pyc +0 -0
.gitattributes
CHANGED
|
@@ -374,3 +374,4 @@ my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/pip/_vendor
|
|
| 374 |
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/pkg_resources/__pycache__/__init__.cpython-38.pyc filter=lfs diff=lfs merge=lfs -text
|
| 375 |
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/matplotlib/__pycache__/backend_bases.cpython-38.pyc filter=lfs diff=lfs merge=lfs -text
|
| 376 |
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/lxml/objectify.cpython-38-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 374 |
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/pkg_resources/__pycache__/__init__.cpython-38.pyc filter=lfs diff=lfs merge=lfs -text
|
| 375 |
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/matplotlib/__pycache__/backend_bases.cpython-38.pyc filter=lfs diff=lfs merge=lfs -text
|
| 376 |
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/lxml/objectify.cpython-38-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 377 |
+
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/matplotlib/__pycache__/figure.cpython-38.pyc filter=lfs diff=lfs merge=lfs -text
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/__init__.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
# flake8: noqa
|
| 5 |
+
|
| 6 |
+
import sys
|
| 7 |
+
import importlib
|
| 8 |
+
import os
|
| 9 |
+
import warnings
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# v2 imports remove in v3
|
| 13 |
+
from .. import formats
|
| 14 |
+
|
| 15 |
+
# v2 allows formatting plugins by environment variable
|
| 16 |
+
# this is done here.
|
| 17 |
+
env_plugin_order = os.getenv("IMAGEIO_FORMAT_ORDER", None)
|
| 18 |
+
if env_plugin_order is not None: # pragma: no cover
|
| 19 |
+
warnings.warn(
|
| 20 |
+
"Setting plugin priority through an environment variable is"
|
| 21 |
+
" deprecated and will be removed in ImageIO v3. There is no"
|
| 22 |
+
" replacement planned for this feature. If you have an"
|
| 23 |
+
" active use-case for it, please reach out to us on GitHub.",
|
| 24 |
+
DeprecationWarning,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
formats.sort(*os.getenv("IMAGEIO_FORMAT_ORDER", "").split(","))
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# this class replaces plugin module. For details
|
| 31 |
+
# see https://stackoverflow.com/questions/2447353/getattr-on-a-module
|
| 32 |
+
class plugins:
|
| 33 |
+
# copy values from module into module-class
|
| 34 |
+
__path__ = __path__
|
| 35 |
+
__name__ = __name__
|
| 36 |
+
__loader__ = __loader__
|
| 37 |
+
__file__ = __file__
|
| 38 |
+
|
| 39 |
+
__all__ = list(set(vars().keys()) - {"__module__", "__qualname__"})
|
| 40 |
+
|
| 41 |
+
def __getattr__(self, name):
|
| 42 |
+
"""Lazy-Import Plugins
|
| 43 |
+
|
| 44 |
+
This function dynamically loads plugins into the imageio.plugin
|
| 45 |
+
namespace upon first access. For example, the following snippet will
|
| 46 |
+
delay importing freeimage until the second line:
|
| 47 |
+
|
| 48 |
+
>>> import imageio
|
| 49 |
+
>>> imageio.plugins.freeimage.download()
|
| 50 |
+
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
try:
|
| 54 |
+
return importlib.import_module(f"imageio.plugins.{name}")
|
| 55 |
+
except ImportError:
|
| 56 |
+
raise AttributeError(
|
| 57 |
+
f"module '{__name__}' has no attribute '{name}'"
|
| 58 |
+
) from None
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# see https://stackoverflow.com/questions/2447353/getattr-on-a-module
|
| 62 |
+
# for an explanation why this works
|
| 63 |
+
plugin_module = plugins()
|
| 64 |
+
|
| 65 |
+
# in py3.9+ the docs need to be defined on the instance and not the class.
|
| 66 |
+
# See: https://github.com/sphinx-doc/sphinx/issues/10182
|
| 67 |
+
plugin_module.__doc__ = """
|
| 68 |
+
|
| 69 |
+
Here you can find documentation on how to write your own plugin to allow
|
| 70 |
+
ImageIO to access a new backend. Plugins are quite object oriented, and
|
| 71 |
+
the relevant classes and their interaction are documented here:
|
| 72 |
+
|
| 73 |
+
.. currentmodule:: imageio
|
| 74 |
+
|
| 75 |
+
.. autosummary::
|
| 76 |
+
:toctree: ../_autosummary
|
| 77 |
+
:template: better_class.rst
|
| 78 |
+
|
| 79 |
+
imageio.core.Format
|
| 80 |
+
imageio.core.Request
|
| 81 |
+
|
| 82 |
+
.. note::
|
| 83 |
+
You can always check existing plugins if you want to see examples.
|
| 84 |
+
|
| 85 |
+
What methods to implement
|
| 86 |
+
-------------------------
|
| 87 |
+
|
| 88 |
+
To implement a new plugin, create a new class that inherits from
|
| 89 |
+
:class:`imageio.core.Format`. and implement the following functions:
|
| 90 |
+
|
| 91 |
+
.. autosummary::
|
| 92 |
+
:toctree: ../_autosummary
|
| 93 |
+
|
| 94 |
+
imageio.core.Format.__init__
|
| 95 |
+
imageio.core.Format._can_read
|
| 96 |
+
imageio.core.Format._can_write
|
| 97 |
+
|
| 98 |
+
Further, each format contains up to two nested classes; one for reading and
|
| 99 |
+
one for writing. To support reading and/or writing, the respective classes
|
| 100 |
+
need to be defined.
|
| 101 |
+
|
| 102 |
+
For reading, create a nested class that inherits from
|
| 103 |
+
``imageio.core.Format.Reader`` and that implements the following functions:
|
| 104 |
+
|
| 105 |
+
* Implement ``_open(**kwargs)`` to initialize the reader. Deal with the
|
| 106 |
+
user-provided keyword arguments here.
|
| 107 |
+
* Implement ``_close()`` to clean up.
|
| 108 |
+
* Implement ``_get_length()`` to provide a suitable length based on what
|
| 109 |
+
the user expects. Can be ``inf`` for streaming data.
|
| 110 |
+
* Implement ``_get_data(index)`` to return an array and a meta-data dict.
|
| 111 |
+
* Implement ``_get_meta_data(index)`` to return a meta-data dict. If index
|
| 112 |
+
is None, it should return the 'global' meta-data.
|
| 113 |
+
|
| 114 |
+
For writing, create a nested class that inherits from
|
| 115 |
+
``imageio.core.Format.Writer`` and implement the following functions:
|
| 116 |
+
|
| 117 |
+
* Implement ``_open(**kwargs)`` to initialize the writer. Deal with the
|
| 118 |
+
user-provided keyword arguments here.
|
| 119 |
+
* Implement ``_close()`` to clean up.
|
| 120 |
+
* Implement ``_append_data(im, meta)`` to add data (and meta-data).
|
| 121 |
+
* Implement ``_set_meta_data(meta)`` to set the global meta-data.
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
"""
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
sys.modules[__name__] = plugin_module
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/_bsdf.py
ADDED
|
@@ -0,0 +1,918 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# This file is distributed under the terms of the 2-clause BSD License.
|
| 3 |
+
# Copyright (c) 2017-2018, Almar Klein
|
| 4 |
+
|
| 5 |
+
"""
|
| 6 |
+
Python implementation of the Binary Structured Data Format (BSDF).
|
| 7 |
+
|
| 8 |
+
BSDF is a binary format for serializing structured (scientific) data.
|
| 9 |
+
See http://bsdf.io for more information.
|
| 10 |
+
|
| 11 |
+
This is the reference implementation, which is relatively relatively
|
| 12 |
+
sophisticated, providing e.g. lazy loading of blobs and streamed
|
| 13 |
+
reading/writing. A simpler Python implementation is available as
|
| 14 |
+
``bsdf_lite.py``.
|
| 15 |
+
|
| 16 |
+
This module has no dependencies and works on Python 2.7 and 3.4+.
|
| 17 |
+
|
| 18 |
+
Note: on Legacy Python (Python 2.7), non-Unicode strings are encoded as bytes.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
# todo: in 2020, remove six stuff, __future__ and _isidentifier
|
| 22 |
+
# todo: in 2020, remove 'utf-8' args to encode/decode; it's faster
|
| 23 |
+
|
| 24 |
+
from __future__ import absolute_import, division, print_function
|
| 25 |
+
|
| 26 |
+
import bz2
|
| 27 |
+
import hashlib
|
| 28 |
+
import logging
|
| 29 |
+
import os
|
| 30 |
+
import re
|
| 31 |
+
import struct
|
| 32 |
+
import sys
|
| 33 |
+
import types
|
| 34 |
+
import zlib
|
| 35 |
+
from io import BytesIO
|
| 36 |
+
|
| 37 |
+
logger = logging.getLogger(__name__)
|
| 38 |
+
|
| 39 |
+
# Notes on versioning: the major and minor numbers correspond to the
|
| 40 |
+
# BSDF format version. The major number if increased when backward
|
| 41 |
+
# incompatible changes are introduced. An implementation must raise an
|
| 42 |
+
# exception when the file being read has a higher major version. The
|
| 43 |
+
# minor number is increased when new backward compatible features are
|
| 44 |
+
# introduced. An implementation must display a warning when the file
|
| 45 |
+
# being read has a higher minor version. The patch version is increased
|
| 46 |
+
# for subsequent releases of the implementation.
|
| 47 |
+
VERSION = 2, 1, 2
|
| 48 |
+
__version__ = ".".join(str(i) for i in VERSION)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# %% The encoder and decoder implementation
|
| 52 |
+
|
| 53 |
+
# From six.py
|
| 54 |
+
PY3 = sys.version_info[0] >= 3
|
| 55 |
+
if PY3:
|
| 56 |
+
text_type = str
|
| 57 |
+
string_types = str
|
| 58 |
+
unicode_types = str
|
| 59 |
+
integer_types = int
|
| 60 |
+
classtypes = type
|
| 61 |
+
else: # pragma: no cover
|
| 62 |
+
logging.basicConfig() # avoid "no handlers found" error
|
| 63 |
+
text_type = unicode # noqa
|
| 64 |
+
string_types = basestring # noqa
|
| 65 |
+
unicode_types = unicode # noqa
|
| 66 |
+
integer_types = (int, long) # noqa
|
| 67 |
+
classtypes = type, types.ClassType
|
| 68 |
+
|
| 69 |
+
# Shorthands
|
| 70 |
+
spack = struct.pack
|
| 71 |
+
strunpack = struct.unpack
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def lencode(x):
|
| 75 |
+
"""Encode an unsigned integer into a variable sized blob of bytes."""
|
| 76 |
+
# We could support 16 bit and 32 bit as well, but the gain is low, since
|
| 77 |
+
# 9 bytes for collections with over 250 elements is marginal anyway.
|
| 78 |
+
if x <= 250:
|
| 79 |
+
return spack("<B", x)
|
| 80 |
+
# elif x < 65536:
|
| 81 |
+
# return spack('<BH', 251, x)
|
| 82 |
+
# elif x < 4294967296:
|
| 83 |
+
# return spack('<BI', 252, x)
|
| 84 |
+
else:
|
| 85 |
+
return spack("<BQ", 253, x)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# Include len decoder for completeness; we've inlined it for performance.
|
| 89 |
+
def lendecode(f):
|
| 90 |
+
"""Decode an unsigned integer from a file."""
|
| 91 |
+
n = strunpack("<B", f.read(1))[0]
|
| 92 |
+
if n == 253:
|
| 93 |
+
n = strunpack("<Q", f.read(8))[0] # noqa
|
| 94 |
+
return n
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def encode_type_id(b, ext_id):
|
| 98 |
+
"""Encode the type identifier, with or without extension id."""
|
| 99 |
+
if ext_id is not None:
|
| 100 |
+
bb = ext_id.encode("UTF-8")
|
| 101 |
+
return b.upper() + lencode(len(bb)) + bb # noqa
|
| 102 |
+
else:
|
| 103 |
+
return b # noqa
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _isidentifier(s): # pragma: no cover
|
| 107 |
+
"""Use of str.isidentifier() for Legacy Python, but slower."""
|
| 108 |
+
# http://stackoverflow.com/questions/2544972/
|
| 109 |
+
return (
|
| 110 |
+
isinstance(s, string_types)
|
| 111 |
+
and re.match(r"^\w+$", s, re.UNICODE)
|
| 112 |
+
and re.match(r"^[0-9]", s) is None
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class BsdfSerializer(object):
|
| 117 |
+
"""Instances of this class represent a BSDF encoder/decoder.
|
| 118 |
+
|
| 119 |
+
It acts as a placeholder for a set of extensions and encoding/decoding
|
| 120 |
+
options. Use this to predefine extensions and options for high
|
| 121 |
+
performance encoding/decoding. For general use, see the functions
|
| 122 |
+
`save()`, `encode()`, `load()`, and `decode()`.
|
| 123 |
+
|
| 124 |
+
This implementation of BSDF supports streaming lists (keep adding
|
| 125 |
+
to a list after writing the main file), lazy loading of blobs, and
|
| 126 |
+
in-place editing of blobs (for streams opened with a+).
|
| 127 |
+
|
| 128 |
+
Options for encoding:
|
| 129 |
+
|
| 130 |
+
* compression (int or str): ``0`` or "no" for no compression (default),
|
| 131 |
+
``1`` or "zlib" for Zlib compression (same as zip files and PNG), and
|
| 132 |
+
``2`` or "bz2" for Bz2 compression (more compact but slower writing).
|
| 133 |
+
Note that some BSDF implementations (e.g. JavaScript) may not support
|
| 134 |
+
compression.
|
| 135 |
+
* use_checksum (bool): whether to include a checksum with binary blobs.
|
| 136 |
+
* float64 (bool): Whether to write floats as 64 bit (default) or 32 bit.
|
| 137 |
+
|
| 138 |
+
Options for decoding:
|
| 139 |
+
|
| 140 |
+
* load_streaming (bool): if True, and the final object in the structure was
|
| 141 |
+
a stream, will make it available as a stream in the decoded object.
|
| 142 |
+
* lazy_blob (bool): if True, bytes are represented as Blob objects that can
|
| 143 |
+
be used to lazily access the data, and also overwrite the data if the
|
| 144 |
+
file is open in a+ mode.
|
| 145 |
+
"""
|
| 146 |
+
|
| 147 |
+
def __init__(self, extensions=None, **options):
|
| 148 |
+
self._extensions = {} # name -> extension
|
| 149 |
+
self._extensions_by_cls = {} # cls -> (name, extension.encode)
|
| 150 |
+
if extensions is None:
|
| 151 |
+
extensions = standard_extensions
|
| 152 |
+
for extension in extensions:
|
| 153 |
+
self.add_extension(extension)
|
| 154 |
+
self._parse_options(**options)
|
| 155 |
+
|
| 156 |
+
def _parse_options(
|
| 157 |
+
self,
|
| 158 |
+
compression=0,
|
| 159 |
+
use_checksum=False,
|
| 160 |
+
float64=True,
|
| 161 |
+
load_streaming=False,
|
| 162 |
+
lazy_blob=False,
|
| 163 |
+
):
|
| 164 |
+
|
| 165 |
+
# Validate compression
|
| 166 |
+
if isinstance(compression, string_types):
|
| 167 |
+
m = {"no": 0, "zlib": 1, "bz2": 2}
|
| 168 |
+
compression = m.get(compression.lower(), compression)
|
| 169 |
+
if compression not in (0, 1, 2):
|
| 170 |
+
raise TypeError("Compression must be 0, 1, 2, " '"no", "zlib", or "bz2"')
|
| 171 |
+
self._compression = compression
|
| 172 |
+
|
| 173 |
+
# Other encoding args
|
| 174 |
+
self._use_checksum = bool(use_checksum)
|
| 175 |
+
self._float64 = bool(float64)
|
| 176 |
+
|
| 177 |
+
# Decoding args
|
| 178 |
+
self._load_streaming = bool(load_streaming)
|
| 179 |
+
self._lazy_blob = bool(lazy_blob)
|
| 180 |
+
|
| 181 |
+
def add_extension(self, extension_class):
|
| 182 |
+
"""Add an extension to this serializer instance, which must be
|
| 183 |
+
a subclass of Extension. Can be used as a decorator.
|
| 184 |
+
"""
|
| 185 |
+
# Check class
|
| 186 |
+
if not (
|
| 187 |
+
isinstance(extension_class, type) and issubclass(extension_class, Extension)
|
| 188 |
+
):
|
| 189 |
+
raise TypeError("add_extension() expects a Extension class.")
|
| 190 |
+
extension = extension_class()
|
| 191 |
+
|
| 192 |
+
# Get name
|
| 193 |
+
name = extension.name
|
| 194 |
+
if not isinstance(name, str):
|
| 195 |
+
raise TypeError("Extension name must be str.")
|
| 196 |
+
if len(name) == 0 or len(name) > 250:
|
| 197 |
+
raise NameError(
|
| 198 |
+
"Extension names must be nonempty and shorter " "than 251 chars."
|
| 199 |
+
)
|
| 200 |
+
if name in self._extensions:
|
| 201 |
+
logger.warning(
|
| 202 |
+
'BSDF warning: overwriting extension "%s", '
|
| 203 |
+
"consider removing first" % name
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
# Get classes
|
| 207 |
+
cls = extension.cls
|
| 208 |
+
if not cls:
|
| 209 |
+
clss = []
|
| 210 |
+
elif isinstance(cls, (tuple, list)):
|
| 211 |
+
clss = cls
|
| 212 |
+
else:
|
| 213 |
+
clss = [cls]
|
| 214 |
+
for cls in clss:
|
| 215 |
+
if not isinstance(cls, classtypes):
|
| 216 |
+
raise TypeError("Extension classes must be types.")
|
| 217 |
+
|
| 218 |
+
# Store
|
| 219 |
+
for cls in clss:
|
| 220 |
+
self._extensions_by_cls[cls] = name, extension.encode
|
| 221 |
+
self._extensions[name] = extension
|
| 222 |
+
return extension_class
|
| 223 |
+
|
| 224 |
+
def remove_extension(self, name):
|
| 225 |
+
"""Remove a converted by its unique name."""
|
| 226 |
+
if not isinstance(name, str):
|
| 227 |
+
raise TypeError("Extension name must be str.")
|
| 228 |
+
if name in self._extensions:
|
| 229 |
+
self._extensions.pop(name)
|
| 230 |
+
for cls in list(self._extensions_by_cls.keys()):
|
| 231 |
+
if self._extensions_by_cls[cls][0] == name:
|
| 232 |
+
self._extensions_by_cls.pop(cls)
|
| 233 |
+
|
| 234 |
+
def _encode(self, f, value, streams, ext_id):
|
| 235 |
+
"""Main encoder function."""
|
| 236 |
+
x = encode_type_id
|
| 237 |
+
|
| 238 |
+
if value is None:
|
| 239 |
+
f.write(x(b"v", ext_id)) # V for void
|
| 240 |
+
elif value is True:
|
| 241 |
+
f.write(x(b"y", ext_id)) # Y for yes
|
| 242 |
+
elif value is False:
|
| 243 |
+
f.write(x(b"n", ext_id)) # N for no
|
| 244 |
+
elif isinstance(value, integer_types):
|
| 245 |
+
if -32768 <= value <= 32767:
|
| 246 |
+
f.write(x(b"h", ext_id) + spack("h", value)) # H for ...
|
| 247 |
+
else:
|
| 248 |
+
f.write(x(b"i", ext_id) + spack("<q", value)) # I for int
|
| 249 |
+
elif isinstance(value, float):
|
| 250 |
+
if self._float64:
|
| 251 |
+
f.write(x(b"d", ext_id) + spack("<d", value)) # D for double
|
| 252 |
+
else:
|
| 253 |
+
f.write(x(b"f", ext_id) + spack("<f", value)) # f for float
|
| 254 |
+
elif isinstance(value, unicode_types):
|
| 255 |
+
bb = value.encode("UTF-8")
|
| 256 |
+
f.write(x(b"s", ext_id) + lencode(len(bb))) # S for str
|
| 257 |
+
f.write(bb)
|
| 258 |
+
elif isinstance(value, (list, tuple)):
|
| 259 |
+
f.write(x(b"l", ext_id) + lencode(len(value))) # L for list
|
| 260 |
+
for v in value:
|
| 261 |
+
self._encode(f, v, streams, None)
|
| 262 |
+
elif isinstance(value, dict):
|
| 263 |
+
f.write(x(b"m", ext_id) + lencode(len(value))) # M for mapping
|
| 264 |
+
for key, v in value.items():
|
| 265 |
+
if PY3:
|
| 266 |
+
assert key.isidentifier() # faster
|
| 267 |
+
else: # pragma: no cover
|
| 268 |
+
assert _isidentifier(key)
|
| 269 |
+
# yield ' ' * indent + key
|
| 270 |
+
name_b = key.encode("UTF-8")
|
| 271 |
+
f.write(lencode(len(name_b)))
|
| 272 |
+
f.write(name_b)
|
| 273 |
+
self._encode(f, v, streams, None)
|
| 274 |
+
elif isinstance(value, bytes):
|
| 275 |
+
f.write(x(b"b", ext_id)) # B for blob
|
| 276 |
+
blob = Blob(
|
| 277 |
+
value, compression=self._compression, use_checksum=self._use_checksum
|
| 278 |
+
)
|
| 279 |
+
blob._to_file(f) # noqa
|
| 280 |
+
elif isinstance(value, Blob):
|
| 281 |
+
f.write(x(b"b", ext_id)) # B for blob
|
| 282 |
+
value._to_file(f) # noqa
|
| 283 |
+
elif isinstance(value, BaseStream):
|
| 284 |
+
# Initialize the stream
|
| 285 |
+
if value.mode != "w":
|
| 286 |
+
raise ValueError("Cannot serialize a read-mode stream.")
|
| 287 |
+
elif isinstance(value, ListStream):
|
| 288 |
+
f.write(x(b"l", ext_id) + spack("<BQ", 255, 0)) # L for list
|
| 289 |
+
else:
|
| 290 |
+
raise TypeError("Only ListStream is supported")
|
| 291 |
+
# Mark this as *the* stream, and activate the stream.
|
| 292 |
+
# The save() function verifies this is the last written object.
|
| 293 |
+
if len(streams) > 0:
|
| 294 |
+
raise ValueError("Can only have one stream per file.")
|
| 295 |
+
streams.append(value)
|
| 296 |
+
value._activate(f, self._encode, self._decode) # noqa
|
| 297 |
+
else:
|
| 298 |
+
if ext_id is not None:
|
| 299 |
+
raise ValueError(
|
| 300 |
+
"Extension %s wronfully encodes object to another "
|
| 301 |
+
"extension object (though it may encode to a list/dict "
|
| 302 |
+
"that contains other extension objects)." % ext_id
|
| 303 |
+
)
|
| 304 |
+
# Try if the value is of a type we know
|
| 305 |
+
ex = self._extensions_by_cls.get(value.__class__, None)
|
| 306 |
+
# Maybe its a subclass of a type we know
|
| 307 |
+
if ex is None:
|
| 308 |
+
for name, c in self._extensions.items():
|
| 309 |
+
if c.match(self, value):
|
| 310 |
+
ex = name, c.encode
|
| 311 |
+
break
|
| 312 |
+
else:
|
| 313 |
+
ex = None
|
| 314 |
+
# Success or fail
|
| 315 |
+
if ex is not None:
|
| 316 |
+
ext_id2, extension_encode = ex
|
| 317 |
+
self._encode(f, extension_encode(self, value), streams, ext_id2)
|
| 318 |
+
else:
|
| 319 |
+
t = (
|
| 320 |
+
"Class %r is not a valid base BSDF type, nor is it "
|
| 321 |
+
"handled by an extension."
|
| 322 |
+
)
|
| 323 |
+
raise TypeError(t % value.__class__.__name__)
|
| 324 |
+
|
| 325 |
+
def _decode(self, f):
|
| 326 |
+
"""Main decoder function."""
|
| 327 |
+
|
| 328 |
+
# Get value
|
| 329 |
+
char = f.read(1)
|
| 330 |
+
c = char.lower()
|
| 331 |
+
|
| 332 |
+
# Conversion (uppercase value identifiers signify converted values)
|
| 333 |
+
if not char:
|
| 334 |
+
raise EOFError()
|
| 335 |
+
elif char != c:
|
| 336 |
+
n = strunpack("<B", f.read(1))[0]
|
| 337 |
+
# if n == 253: n = strunpack('<Q', f.read(8))[0] # noqa - noneed
|
| 338 |
+
ext_id = f.read(n).decode("UTF-8")
|
| 339 |
+
else:
|
| 340 |
+
ext_id = None
|
| 341 |
+
|
| 342 |
+
if c == b"v":
|
| 343 |
+
value = None
|
| 344 |
+
elif c == b"y":
|
| 345 |
+
value = True
|
| 346 |
+
elif c == b"n":
|
| 347 |
+
value = False
|
| 348 |
+
elif c == b"h":
|
| 349 |
+
value = strunpack("<h", f.read(2))[0]
|
| 350 |
+
elif c == b"i":
|
| 351 |
+
value = strunpack("<q", f.read(8))[0]
|
| 352 |
+
elif c == b"f":
|
| 353 |
+
value = strunpack("<f", f.read(4))[0]
|
| 354 |
+
elif c == b"d":
|
| 355 |
+
value = strunpack("<d", f.read(8))[0]
|
| 356 |
+
elif c == b"s":
|
| 357 |
+
n_s = strunpack("<B", f.read(1))[0]
|
| 358 |
+
if n_s == 253:
|
| 359 |
+
n_s = strunpack("<Q", f.read(8))[0] # noqa
|
| 360 |
+
value = f.read(n_s).decode("UTF-8")
|
| 361 |
+
elif c == b"l":
|
| 362 |
+
n = strunpack("<B", f.read(1))[0]
|
| 363 |
+
if n >= 254:
|
| 364 |
+
# Streaming
|
| 365 |
+
closed = n == 254
|
| 366 |
+
n = strunpack("<Q", f.read(8))[0]
|
| 367 |
+
if self._load_streaming:
|
| 368 |
+
value = ListStream(n if closed else "r")
|
| 369 |
+
value._activate(f, self._encode, self._decode) # noqa
|
| 370 |
+
elif closed:
|
| 371 |
+
value = [self._decode(f) for i in range(n)]
|
| 372 |
+
else:
|
| 373 |
+
value = []
|
| 374 |
+
try:
|
| 375 |
+
while True:
|
| 376 |
+
value.append(self._decode(f))
|
| 377 |
+
except EOFError:
|
| 378 |
+
pass
|
| 379 |
+
else:
|
| 380 |
+
# Normal
|
| 381 |
+
if n == 253:
|
| 382 |
+
n = strunpack("<Q", f.read(8))[0] # noqa
|
| 383 |
+
value = [self._decode(f) for i in range(n)]
|
| 384 |
+
elif c == b"m":
|
| 385 |
+
value = dict()
|
| 386 |
+
n = strunpack("<B", f.read(1))[0]
|
| 387 |
+
if n == 253:
|
| 388 |
+
n = strunpack("<Q", f.read(8))[0] # noqa
|
| 389 |
+
for i in range(n):
|
| 390 |
+
n_name = strunpack("<B", f.read(1))[0]
|
| 391 |
+
if n_name == 253:
|
| 392 |
+
n_name = strunpack("<Q", f.read(8))[0] # noqa
|
| 393 |
+
assert n_name > 0
|
| 394 |
+
name = f.read(n_name).decode("UTF-8")
|
| 395 |
+
value[name] = self._decode(f)
|
| 396 |
+
elif c == b"b":
|
| 397 |
+
if self._lazy_blob:
|
| 398 |
+
value = Blob((f, True))
|
| 399 |
+
else:
|
| 400 |
+
blob = Blob((f, False))
|
| 401 |
+
value = blob.get_bytes()
|
| 402 |
+
else:
|
| 403 |
+
raise RuntimeError("Parse error %r" % char)
|
| 404 |
+
|
| 405 |
+
# Convert value if we have an extension for it
|
| 406 |
+
if ext_id is not None:
|
| 407 |
+
extension = self._extensions.get(ext_id, None)
|
| 408 |
+
if extension is not None:
|
| 409 |
+
value = extension.decode(self, value)
|
| 410 |
+
else:
|
| 411 |
+
logger.warning("BSDF warning: no extension found for %r" % ext_id)
|
| 412 |
+
|
| 413 |
+
return value
|
| 414 |
+
|
| 415 |
+
def encode(self, ob):
|
| 416 |
+
"""Save the given object to bytes."""
|
| 417 |
+
f = BytesIO()
|
| 418 |
+
self.save(f, ob)
|
| 419 |
+
return f.getvalue()
|
| 420 |
+
|
| 421 |
+
def save(self, f, ob):
|
| 422 |
+
"""Write the given object to the given file object."""
|
| 423 |
+
f.write(b"BSDF")
|
| 424 |
+
f.write(struct.pack("<B", VERSION[0]))
|
| 425 |
+
f.write(struct.pack("<B", VERSION[1]))
|
| 426 |
+
|
| 427 |
+
# Prepare streaming, this list will have 0 or 1 item at the end
|
| 428 |
+
streams = []
|
| 429 |
+
|
| 430 |
+
self._encode(f, ob, streams, None)
|
| 431 |
+
|
| 432 |
+
# Verify that stream object was at the end, and add initial elements
|
| 433 |
+
if len(streams) > 0:
|
| 434 |
+
stream = streams[0]
|
| 435 |
+
if stream._start_pos != f.tell():
|
| 436 |
+
raise ValueError(
|
| 437 |
+
"The stream object must be " "the last object to be encoded."
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
def decode(self, bb):
|
| 441 |
+
"""Load the data structure that is BSDF-encoded in the given bytes."""
|
| 442 |
+
f = BytesIO(bb)
|
| 443 |
+
return self.load(f)
|
| 444 |
+
|
| 445 |
+
def load(self, f):
|
| 446 |
+
"""Load a BSDF-encoded object from the given file object."""
|
| 447 |
+
# Check magic string
|
| 448 |
+
f4 = f.read(4)
|
| 449 |
+
if f4 != b"BSDF":
|
| 450 |
+
raise RuntimeError("This does not look like a BSDF file: %r" % f4)
|
| 451 |
+
# Check version
|
| 452 |
+
major_version = strunpack("<B", f.read(1))[0]
|
| 453 |
+
minor_version = strunpack("<B", f.read(1))[0]
|
| 454 |
+
file_version = "%i.%i" % (major_version, minor_version)
|
| 455 |
+
if major_version != VERSION[0]: # major version should be 2
|
| 456 |
+
t = (
|
| 457 |
+
"Reading file with different major version (%s) "
|
| 458 |
+
"from the implementation (%s)."
|
| 459 |
+
)
|
| 460 |
+
raise RuntimeError(t % (__version__, file_version))
|
| 461 |
+
if minor_version > VERSION[1]: # minor should be < ours
|
| 462 |
+
t = (
|
| 463 |
+
"BSDF warning: reading file with higher minor version (%s) "
|
| 464 |
+
"than the implementation (%s)."
|
| 465 |
+
)
|
| 466 |
+
logger.warning(t % (__version__, file_version))
|
| 467 |
+
|
| 468 |
+
return self._decode(f)
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
# %% Streaming and blob-files
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
class BaseStream(object):
|
| 475 |
+
"""Base class for streams."""
|
| 476 |
+
|
| 477 |
+
def __init__(self, mode="w"):
|
| 478 |
+
self._i = 0
|
| 479 |
+
self._count = -1
|
| 480 |
+
if isinstance(mode, int):
|
| 481 |
+
self._count = mode
|
| 482 |
+
mode = "r"
|
| 483 |
+
elif mode == "w":
|
| 484 |
+
self._count = 0
|
| 485 |
+
assert mode in ("r", "w")
|
| 486 |
+
self._mode = mode
|
| 487 |
+
self._f = None
|
| 488 |
+
self._start_pos = 0
|
| 489 |
+
|
| 490 |
+
def _activate(self, file, encode_func, decode_func):
|
| 491 |
+
if self._f is not None: # Associated with another write
|
| 492 |
+
raise IOError("Stream object cannot be activated twice?")
|
| 493 |
+
self._f = file
|
| 494 |
+
self._start_pos = self._f.tell()
|
| 495 |
+
self._encode = encode_func
|
| 496 |
+
self._decode = decode_func
|
| 497 |
+
|
| 498 |
+
@property
|
| 499 |
+
def mode(self):
|
| 500 |
+
"""The mode of this stream: 'r' or 'w'."""
|
| 501 |
+
return self._mode
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
class ListStream(BaseStream):
|
| 505 |
+
"""A streamable list object used for writing or reading.
|
| 506 |
+
In read mode, it can also be iterated over.
|
| 507 |
+
"""
|
| 508 |
+
|
| 509 |
+
@property
|
| 510 |
+
def count(self):
|
| 511 |
+
"""The number of elements in the stream (can be -1 for unclosed
|
| 512 |
+
streams in read-mode).
|
| 513 |
+
"""
|
| 514 |
+
return self._count
|
| 515 |
+
|
| 516 |
+
@property
|
| 517 |
+
def index(self):
|
| 518 |
+
"""The current index of the element to read/write."""
|
| 519 |
+
return self._i
|
| 520 |
+
|
| 521 |
+
def append(self, item):
|
| 522 |
+
"""Append an item to the streaming list. The object is immediately
|
| 523 |
+
serialized and written to the underlying file.
|
| 524 |
+
"""
|
| 525 |
+
# if self._mode != 'w':
|
| 526 |
+
# raise IOError('This ListStream is not in write mode.')
|
| 527 |
+
if self._count != self._i:
|
| 528 |
+
raise IOError("Can only append items to the end of the stream.")
|
| 529 |
+
if self._f is None:
|
| 530 |
+
raise IOError("List stream is not associated with a file yet.")
|
| 531 |
+
if self._f.closed:
|
| 532 |
+
raise IOError("Cannot stream to a close file.")
|
| 533 |
+
self._encode(self._f, item, [self], None)
|
| 534 |
+
self._i += 1
|
| 535 |
+
self._count += 1
|
| 536 |
+
|
| 537 |
+
def close(self, unstream=False):
|
| 538 |
+
"""Close the stream, marking the number of written elements. New
|
| 539 |
+
elements may still be appended, but they won't be read during decoding.
|
| 540 |
+
If ``unstream`` is False, the stream is turned into a regular list
|
| 541 |
+
(not streaming).
|
| 542 |
+
"""
|
| 543 |
+
# if self._mode != 'w':
|
| 544 |
+
# raise IOError('This ListStream is not in write mode.')
|
| 545 |
+
if self._count != self._i:
|
| 546 |
+
raise IOError("Can only close when at the end of the stream.")
|
| 547 |
+
if self._f is None:
|
| 548 |
+
raise IOError("ListStream is not associated with a file yet.")
|
| 549 |
+
if self._f.closed:
|
| 550 |
+
raise IOError("Cannot close a stream on a close file.")
|
| 551 |
+
i = self._f.tell()
|
| 552 |
+
self._f.seek(self._start_pos - 8 - 1)
|
| 553 |
+
self._f.write(spack("<B", 253 if unstream else 254))
|
| 554 |
+
self._f.write(spack("<Q", self._count))
|
| 555 |
+
self._f.seek(i)
|
| 556 |
+
|
| 557 |
+
def next(self):
|
| 558 |
+
"""Read and return the next element in the streaming list.
|
| 559 |
+
Raises StopIteration if the stream is exhausted.
|
| 560 |
+
"""
|
| 561 |
+
if self._mode != "r":
|
| 562 |
+
raise IOError("This ListStream in not in read mode.")
|
| 563 |
+
if self._f is None:
|
| 564 |
+
raise IOError("ListStream is not associated with a file yet.")
|
| 565 |
+
if getattr(self._f, "closed", None): # not present on 2.7 http req :/
|
| 566 |
+
raise IOError("Cannot read a stream from a close file.")
|
| 567 |
+
if self._count >= 0:
|
| 568 |
+
if self._i >= self._count:
|
| 569 |
+
raise StopIteration()
|
| 570 |
+
self._i += 1
|
| 571 |
+
return self._decode(self._f)
|
| 572 |
+
else:
|
| 573 |
+
# This raises EOFError at some point.
|
| 574 |
+
try:
|
| 575 |
+
res = self._decode(self._f)
|
| 576 |
+
self._i += 1
|
| 577 |
+
return res
|
| 578 |
+
except EOFError:
|
| 579 |
+
self._count = self._i
|
| 580 |
+
raise StopIteration()
|
| 581 |
+
|
| 582 |
+
def __iter__(self):
|
| 583 |
+
if self._mode != "r":
|
| 584 |
+
raise IOError("Cannot iterate: ListStream in not in read mode.")
|
| 585 |
+
return self
|
| 586 |
+
|
| 587 |
+
def __next__(self):
|
| 588 |
+
return self.next()
|
| 589 |
+
|
| 590 |
+
|
| 591 |
+
class Blob(object):
|
| 592 |
+
"""Object to represent a blob of bytes. When used to write a BSDF file,
|
| 593 |
+
it's a wrapper for bytes plus properties such as what compression to apply.
|
| 594 |
+
When used to read a BSDF file, it can be used to read the data lazily, and
|
| 595 |
+
also modify the data if reading in 'r+' mode and the blob isn't compressed.
|
| 596 |
+
"""
|
| 597 |
+
|
| 598 |
+
# For now, this does not allow re-sizing blobs (within the allocated size)
|
| 599 |
+
# but this can be added later.
|
| 600 |
+
|
| 601 |
+
def __init__(self, bb, compression=0, extra_size=0, use_checksum=False):
|
| 602 |
+
if isinstance(bb, bytes):
|
| 603 |
+
self._f = None
|
| 604 |
+
self.compressed = self._from_bytes(bb, compression)
|
| 605 |
+
self.compression = compression
|
| 606 |
+
self.allocated_size = self.used_size + extra_size
|
| 607 |
+
self.use_checksum = use_checksum
|
| 608 |
+
elif isinstance(bb, tuple) and len(bb) == 2 and hasattr(bb[0], "read"):
|
| 609 |
+
self._f, allow_seek = bb
|
| 610 |
+
self.compressed = None
|
| 611 |
+
self._from_file(self._f, allow_seek)
|
| 612 |
+
self._modified = False
|
| 613 |
+
else:
|
| 614 |
+
raise TypeError("Wrong argument to create Blob.")
|
| 615 |
+
|
| 616 |
+
def _from_bytes(self, value, compression):
|
| 617 |
+
"""When used to wrap bytes in a blob."""
|
| 618 |
+
if compression == 0:
|
| 619 |
+
compressed = value
|
| 620 |
+
elif compression == 1:
|
| 621 |
+
compressed = zlib.compress(value, 9)
|
| 622 |
+
elif compression == 2:
|
| 623 |
+
compressed = bz2.compress(value, 9)
|
| 624 |
+
else: # pragma: no cover
|
| 625 |
+
assert False, "Unknown compression identifier"
|
| 626 |
+
|
| 627 |
+
self.data_size = len(value)
|
| 628 |
+
self.used_size = len(compressed)
|
| 629 |
+
return compressed
|
| 630 |
+
|
| 631 |
+
def _to_file(self, f):
|
| 632 |
+
"""Private friend method called by encoder to write a blob to a file."""
|
| 633 |
+
# Write sizes - write at least in a size that allows resizing
|
| 634 |
+
if self.allocated_size <= 250 and self.compression == 0:
|
| 635 |
+
f.write(spack("<B", self.allocated_size))
|
| 636 |
+
f.write(spack("<B", self.used_size))
|
| 637 |
+
f.write(lencode(self.data_size))
|
| 638 |
+
else:
|
| 639 |
+
f.write(spack("<BQ", 253, self.allocated_size))
|
| 640 |
+
f.write(spack("<BQ", 253, self.used_size))
|
| 641 |
+
f.write(spack("<BQ", 253, self.data_size))
|
| 642 |
+
# Compression and checksum
|
| 643 |
+
f.write(spack("B", self.compression))
|
| 644 |
+
if self.use_checksum:
|
| 645 |
+
f.write(b"\xff" + hashlib.md5(self.compressed).digest())
|
| 646 |
+
else:
|
| 647 |
+
f.write(b"\x00")
|
| 648 |
+
# Byte alignment (only necessary for uncompressed data)
|
| 649 |
+
if self.compression == 0:
|
| 650 |
+
alignment = 8 - (f.tell() + 1) % 8 # +1 for the byte to write
|
| 651 |
+
f.write(spack("<B", alignment)) # padding for byte alignment
|
| 652 |
+
f.write(b"\x00" * alignment)
|
| 653 |
+
else:
|
| 654 |
+
f.write(spack("<B", 0))
|
| 655 |
+
# The actual data and extra space
|
| 656 |
+
f.write(self.compressed)
|
| 657 |
+
f.write(b"\x00" * (self.allocated_size - self.used_size))
|
| 658 |
+
|
| 659 |
+
def _from_file(self, f, allow_seek):
|
| 660 |
+
"""Used when a blob is read by the decoder."""
|
| 661 |
+
# Read blob header data (5 to 42 bytes)
|
| 662 |
+
# Size
|
| 663 |
+
allocated_size = strunpack("<B", f.read(1))[0]
|
| 664 |
+
if allocated_size == 253:
|
| 665 |
+
allocated_size = strunpack("<Q", f.read(8))[0] # noqa
|
| 666 |
+
used_size = strunpack("<B", f.read(1))[0]
|
| 667 |
+
if used_size == 253:
|
| 668 |
+
used_size = strunpack("<Q", f.read(8))[0] # noqa
|
| 669 |
+
data_size = strunpack("<B", f.read(1))[0]
|
| 670 |
+
if data_size == 253:
|
| 671 |
+
data_size = strunpack("<Q", f.read(8))[0] # noqa
|
| 672 |
+
# Compression and checksum
|
| 673 |
+
compression = strunpack("<B", f.read(1))[0]
|
| 674 |
+
has_checksum = strunpack("<B", f.read(1))[0]
|
| 675 |
+
if has_checksum:
|
| 676 |
+
checksum = f.read(16)
|
| 677 |
+
# Skip alignment
|
| 678 |
+
alignment = strunpack("<B", f.read(1))[0]
|
| 679 |
+
f.read(alignment)
|
| 680 |
+
# Get or skip data + extra space
|
| 681 |
+
if allow_seek:
|
| 682 |
+
self.start_pos = f.tell()
|
| 683 |
+
self.end_pos = self.start_pos + used_size
|
| 684 |
+
f.seek(self.start_pos + allocated_size)
|
| 685 |
+
else:
|
| 686 |
+
self.start_pos = None
|
| 687 |
+
self.end_pos = None
|
| 688 |
+
self.compressed = f.read(used_size)
|
| 689 |
+
f.read(allocated_size - used_size)
|
| 690 |
+
# Store info
|
| 691 |
+
self.alignment = alignment
|
| 692 |
+
self.compression = compression
|
| 693 |
+
self.use_checksum = checksum if has_checksum else None
|
| 694 |
+
self.used_size = used_size
|
| 695 |
+
self.allocated_size = allocated_size
|
| 696 |
+
self.data_size = data_size
|
| 697 |
+
|
| 698 |
+
def seek(self, p):
|
| 699 |
+
"""Seek to the given position (relative to the blob start)."""
|
| 700 |
+
if self._f is None:
|
| 701 |
+
raise RuntimeError(
|
| 702 |
+
"Cannot seek in a blob " "that is not created by the BSDF decoder."
|
| 703 |
+
)
|
| 704 |
+
if p < 0:
|
| 705 |
+
p = self.allocated_size + p
|
| 706 |
+
if p < 0 or p > self.allocated_size:
|
| 707 |
+
raise IOError("Seek beyond blob boundaries.")
|
| 708 |
+
self._f.seek(self.start_pos + p)
|
| 709 |
+
|
| 710 |
+
def tell(self):
|
| 711 |
+
"""Get the current file pointer position (relative to the blob start)."""
|
| 712 |
+
if self._f is None:
|
| 713 |
+
raise RuntimeError(
|
| 714 |
+
"Cannot tell in a blob " "that is not created by the BSDF decoder."
|
| 715 |
+
)
|
| 716 |
+
return self._f.tell() - self.start_pos
|
| 717 |
+
|
| 718 |
+
def write(self, bb):
|
| 719 |
+
"""Write bytes to the blob."""
|
| 720 |
+
if self._f is None:
|
| 721 |
+
raise RuntimeError(
|
| 722 |
+
"Cannot write in a blob " "that is not created by the BSDF decoder."
|
| 723 |
+
)
|
| 724 |
+
if self.compression:
|
| 725 |
+
raise IOError("Cannot arbitrarily write in compressed blob.")
|
| 726 |
+
if self._f.tell() + len(bb) > self.end_pos:
|
| 727 |
+
raise IOError("Write beyond blob boundaries.")
|
| 728 |
+
self._modified = True
|
| 729 |
+
return self._f.write(bb)
|
| 730 |
+
|
| 731 |
+
def read(self, n):
|
| 732 |
+
"""Read n bytes from the blob."""
|
| 733 |
+
if self._f is None:
|
| 734 |
+
raise RuntimeError(
|
| 735 |
+
"Cannot read in a blob " "that is not created by the BSDF decoder."
|
| 736 |
+
)
|
| 737 |
+
if self.compression:
|
| 738 |
+
raise IOError("Cannot arbitrarily read in compressed blob.")
|
| 739 |
+
if self._f.tell() + n > self.end_pos:
|
| 740 |
+
raise IOError("Read beyond blob boundaries.")
|
| 741 |
+
return self._f.read(n)
|
| 742 |
+
|
| 743 |
+
def get_bytes(self):
|
| 744 |
+
"""Get the contents of the blob as bytes."""
|
| 745 |
+
if self.compressed is not None:
|
| 746 |
+
compressed = self.compressed
|
| 747 |
+
else:
|
| 748 |
+
i = self._f.tell()
|
| 749 |
+
self.seek(0)
|
| 750 |
+
compressed = self._f.read(self.used_size)
|
| 751 |
+
self._f.seek(i)
|
| 752 |
+
if self.compression == 0:
|
| 753 |
+
value = compressed
|
| 754 |
+
elif self.compression == 1:
|
| 755 |
+
value = zlib.decompress(compressed)
|
| 756 |
+
elif self.compression == 2:
|
| 757 |
+
value = bz2.decompress(compressed)
|
| 758 |
+
else: # pragma: no cover
|
| 759 |
+
raise RuntimeError("Invalid compression %i" % self.compression)
|
| 760 |
+
return value
|
| 761 |
+
|
| 762 |
+
def update_checksum(self):
|
| 763 |
+
"""Reset the blob's checksum if present. Call this after modifying
|
| 764 |
+
the data.
|
| 765 |
+
"""
|
| 766 |
+
# or ... should the presence of a checksum mean that data is proteced?
|
| 767 |
+
if self.use_checksum and self._modified:
|
| 768 |
+
self.seek(0)
|
| 769 |
+
compressed = self._f.read(self.used_size)
|
| 770 |
+
self._f.seek(self.start_pos - self.alignment - 1 - 16)
|
| 771 |
+
self._f.write(hashlib.md5(compressed).digest())
|
| 772 |
+
|
| 773 |
+
|
| 774 |
+
# %% High-level functions
|
| 775 |
+
|
| 776 |
+
|
| 777 |
+
def encode(ob, extensions=None, **options):
|
| 778 |
+
"""Save (BSDF-encode) the given object to bytes.
|
| 779 |
+
See `BSDFSerializer` for details on extensions and options.
|
| 780 |
+
"""
|
| 781 |
+
s = BsdfSerializer(extensions, **options)
|
| 782 |
+
return s.encode(ob)
|
| 783 |
+
|
| 784 |
+
|
| 785 |
+
def save(f, ob, extensions=None, **options):
|
| 786 |
+
"""Save (BSDF-encode) the given object to the given filename or
|
| 787 |
+
file object. See` BSDFSerializer` for details on extensions and options.
|
| 788 |
+
"""
|
| 789 |
+
s = BsdfSerializer(extensions, **options)
|
| 790 |
+
if isinstance(f, string_types):
|
| 791 |
+
with open(f, "wb") as fp:
|
| 792 |
+
return s.save(fp, ob)
|
| 793 |
+
else:
|
| 794 |
+
return s.save(f, ob)
|
| 795 |
+
|
| 796 |
+
|
| 797 |
+
def decode(bb, extensions=None, **options):
|
| 798 |
+
"""Load a (BSDF-encoded) structure from bytes.
|
| 799 |
+
See `BSDFSerializer` for details on extensions and options.
|
| 800 |
+
"""
|
| 801 |
+
s = BsdfSerializer(extensions, **options)
|
| 802 |
+
return s.decode(bb)
|
| 803 |
+
|
| 804 |
+
|
| 805 |
+
def load(f, extensions=None, **options):
|
| 806 |
+
"""Load a (BSDF-encoded) structure from the given filename or file object.
|
| 807 |
+
See `BSDFSerializer` for details on extensions and options.
|
| 808 |
+
"""
|
| 809 |
+
s = BsdfSerializer(extensions, **options)
|
| 810 |
+
if isinstance(f, string_types):
|
| 811 |
+
if f.startswith(("~/", "~\\")): # pragma: no cover
|
| 812 |
+
f = os.path.expanduser(f)
|
| 813 |
+
with open(f, "rb") as fp:
|
| 814 |
+
return s.load(fp)
|
| 815 |
+
else:
|
| 816 |
+
return s.load(f)
|
| 817 |
+
|
| 818 |
+
|
| 819 |
+
# Aliases for json compat
|
| 820 |
+
loads = decode
|
| 821 |
+
dumps = encode
|
| 822 |
+
|
| 823 |
+
|
| 824 |
+
# %% Standard extensions
|
| 825 |
+
|
| 826 |
+
# Defining extensions as a dict would be more compact and feel lighter, but
|
| 827 |
+
# that would only allow lambdas, which is too limiting, e.g. for ndarray
|
| 828 |
+
# extension.
|
| 829 |
+
|
| 830 |
+
|
| 831 |
+
class Extension(object):
|
| 832 |
+
"""Base class to implement BSDF extensions for special data types.
|
| 833 |
+
|
| 834 |
+
Extension classes are provided to the BSDF serializer, which
|
| 835 |
+
instantiates the class. That way, the extension can be somewhat dynamic:
|
| 836 |
+
e.g. the NDArrayExtension exposes the ndarray class only when numpy
|
| 837 |
+
is imported.
|
| 838 |
+
|
| 839 |
+
A extension instance must have two attributes. These can be attribiutes of
|
| 840 |
+
the class, or of the instance set in ``__init__()``:
|
| 841 |
+
|
| 842 |
+
* name (str): the name by which encoded values will be identified.
|
| 843 |
+
* cls (type): the type (or list of types) to match values with.
|
| 844 |
+
This is optional, but it makes the encoder select extensions faster.
|
| 845 |
+
|
| 846 |
+
Further, it needs 3 methods:
|
| 847 |
+
|
| 848 |
+
* `match(serializer, value) -> bool`: return whether the extension can
|
| 849 |
+
convert the given value. The default is ``isinstance(value, self.cls)``.
|
| 850 |
+
* `encode(serializer, value) -> encoded_value`: the function to encode a
|
| 851 |
+
value to more basic data types.
|
| 852 |
+
* `decode(serializer, encoded_value) -> value`: the function to decode an
|
| 853 |
+
encoded value back to its intended representation.
|
| 854 |
+
|
| 855 |
+
"""
|
| 856 |
+
|
| 857 |
+
name = ""
|
| 858 |
+
cls = ()
|
| 859 |
+
|
| 860 |
+
def __repr__(self):
|
| 861 |
+
return "<BSDF extension %r at 0x%s>" % (self.name, hex(id(self)))
|
| 862 |
+
|
| 863 |
+
def match(self, s, v):
|
| 864 |
+
return isinstance(v, self.cls)
|
| 865 |
+
|
| 866 |
+
def encode(self, s, v):
|
| 867 |
+
raise NotImplementedError()
|
| 868 |
+
|
| 869 |
+
def decode(self, s, v):
|
| 870 |
+
raise NotImplementedError()
|
| 871 |
+
|
| 872 |
+
|
| 873 |
+
class ComplexExtension(Extension):
|
| 874 |
+
|
| 875 |
+
name = "c"
|
| 876 |
+
cls = complex
|
| 877 |
+
|
| 878 |
+
def encode(self, s, v):
|
| 879 |
+
return (v.real, v.imag)
|
| 880 |
+
|
| 881 |
+
def decode(self, s, v):
|
| 882 |
+
return complex(v[0], v[1])
|
| 883 |
+
|
| 884 |
+
|
| 885 |
+
class NDArrayExtension(Extension):
|
| 886 |
+
|
| 887 |
+
name = "ndarray"
|
| 888 |
+
|
| 889 |
+
def __init__(self):
|
| 890 |
+
if "numpy" in sys.modules:
|
| 891 |
+
import numpy as np
|
| 892 |
+
|
| 893 |
+
self.cls = np.ndarray
|
| 894 |
+
|
| 895 |
+
def match(self, s, v): # pragma: no cover - e.g. work for nd arrays in JS
|
| 896 |
+
return hasattr(v, "shape") and hasattr(v, "dtype") and hasattr(v, "tobytes")
|
| 897 |
+
|
| 898 |
+
def encode(self, s, v):
|
| 899 |
+
return dict(shape=v.shape, dtype=text_type(v.dtype), data=v.tobytes())
|
| 900 |
+
|
| 901 |
+
def decode(self, s, v):
|
| 902 |
+
try:
|
| 903 |
+
import numpy as np
|
| 904 |
+
except ImportError: # pragma: no cover
|
| 905 |
+
return v
|
| 906 |
+
a = np.frombuffer(v["data"], dtype=v["dtype"])
|
| 907 |
+
a.shape = v["shape"]
|
| 908 |
+
return a
|
| 909 |
+
|
| 910 |
+
|
| 911 |
+
standard_extensions = [ComplexExtension, NDArrayExtension]
|
| 912 |
+
|
| 913 |
+
|
| 914 |
+
if __name__ == "__main__":
|
| 915 |
+
# Invoke CLI
|
| 916 |
+
import bsdf_cli
|
| 917 |
+
|
| 918 |
+
bsdf_cli.main()
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/_freeimage.py
ADDED
|
@@ -0,0 +1,1327 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
# styletest: ignore E261
|
| 5 |
+
|
| 6 |
+
""" Module imageio/freeimage.py
|
| 7 |
+
|
| 8 |
+
This module contains the wrapper code for the freeimage library.
|
| 9 |
+
The functions defined in this module are relatively thin; just thin
|
| 10 |
+
enough so that arguments and results are native Python/numpy data
|
| 11 |
+
types.
|
| 12 |
+
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import sys
|
| 17 |
+
import ctypes
|
| 18 |
+
import threading
|
| 19 |
+
import logging
|
| 20 |
+
import numpy
|
| 21 |
+
|
| 22 |
+
from ..core import (
|
| 23 |
+
get_remote_file,
|
| 24 |
+
load_lib,
|
| 25 |
+
Dict,
|
| 26 |
+
resource_dirs,
|
| 27 |
+
IS_PYPY,
|
| 28 |
+
get_platform,
|
| 29 |
+
InternetNotAllowedError,
|
| 30 |
+
NeedDownloadError,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
logger = logging.getLogger(__name__)
|
| 34 |
+
|
| 35 |
+
TEST_NUMPY_NO_STRIDES = False # To test pypy fallback
|
| 36 |
+
|
| 37 |
+
FNAME_PER_PLATFORM = {
|
| 38 |
+
"osx32": "libfreeimage-3.16.0-osx10.6.dylib", # universal library
|
| 39 |
+
"osx64": "libfreeimage-3.16.0-osx10.6.dylib",
|
| 40 |
+
"win32": "FreeImage-3.15.4-win32.dll",
|
| 41 |
+
"win64": "FreeImage-3.15.1-win64.dll",
|
| 42 |
+
"linux32": "libfreeimage-3.16.0-linux32.so",
|
| 43 |
+
"linux64": "libfreeimage-3.16.0-linux64.so",
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def download(directory=None, force_download=False):
|
| 48 |
+
"""Download the FreeImage library to your computer.
|
| 49 |
+
|
| 50 |
+
Parameters
|
| 51 |
+
----------
|
| 52 |
+
directory : str | None
|
| 53 |
+
The directory where the file will be cached if a download was
|
| 54 |
+
required to obtain the file. By default, the appdata directory
|
| 55 |
+
is used. This is also the first directory that is checked for
|
| 56 |
+
a local version of the file.
|
| 57 |
+
force_download : bool | str
|
| 58 |
+
If True, the file will be downloaded even if a local copy exists
|
| 59 |
+
(and this copy will be overwritten). Can also be a YYYY-MM-DD date
|
| 60 |
+
to ensure a file is up-to-date (modified date of a file on disk,
|
| 61 |
+
if present, is checked).
|
| 62 |
+
"""
|
| 63 |
+
plat = get_platform()
|
| 64 |
+
if plat and plat in FNAME_PER_PLATFORM:
|
| 65 |
+
fname = "freeimage/" + FNAME_PER_PLATFORM[plat]
|
| 66 |
+
get_remote_file(fname=fname, directory=directory, force_download=force_download)
|
| 67 |
+
fi._lib = None # allow trying again (needed to make tests work)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def get_freeimage_lib():
|
| 71 |
+
"""Ensure we have our version of the binary freeimage lib."""
|
| 72 |
+
|
| 73 |
+
lib = os.getenv("IMAGEIO_FREEIMAGE_LIB", None)
|
| 74 |
+
if lib: # pragma: no cover
|
| 75 |
+
return lib
|
| 76 |
+
|
| 77 |
+
# Get filename to load
|
| 78 |
+
# If we do not provide a binary, the system may still do ...
|
| 79 |
+
plat = get_platform()
|
| 80 |
+
if plat and plat in FNAME_PER_PLATFORM:
|
| 81 |
+
try:
|
| 82 |
+
return get_remote_file("freeimage/" + FNAME_PER_PLATFORM[plat], auto=False)
|
| 83 |
+
except InternetNotAllowedError:
|
| 84 |
+
pass
|
| 85 |
+
except NeedDownloadError:
|
| 86 |
+
raise NeedDownloadError(
|
| 87 |
+
"Need FreeImage library. "
|
| 88 |
+
"You can obtain it with either:\n"
|
| 89 |
+
" - download using the command: "
|
| 90 |
+
"imageio_download_bin freeimage\n"
|
| 91 |
+
" - download by calling (in Python): "
|
| 92 |
+
"imageio.plugins.freeimage.download()\n"
|
| 93 |
+
)
|
| 94 |
+
except RuntimeError as e: # pragma: no cover
|
| 95 |
+
logger.warning(str(e))
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
# Define function to encode a filename to bytes (for the current system)
|
| 99 |
+
def efn(x):
|
| 100 |
+
return x.encode(sys.getfilesystemencoding())
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# 4-byte quads of 0,v,v,v from 0,0,0,0 to 0,255,255,255
|
| 104 |
+
GREY_PALETTE = numpy.arange(0, 0x01000000, 0x00010101, dtype=numpy.uint32)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class FI_TYPES(object):
|
| 108 |
+
FIT_UNKNOWN = 0
|
| 109 |
+
FIT_BITMAP = 1
|
| 110 |
+
FIT_UINT16 = 2
|
| 111 |
+
FIT_INT16 = 3
|
| 112 |
+
FIT_UINT32 = 4
|
| 113 |
+
FIT_INT32 = 5
|
| 114 |
+
FIT_FLOAT = 6
|
| 115 |
+
FIT_DOUBLE = 7
|
| 116 |
+
FIT_COMPLEX = 8
|
| 117 |
+
FIT_RGB16 = 9
|
| 118 |
+
FIT_RGBA16 = 10
|
| 119 |
+
FIT_RGBF = 11
|
| 120 |
+
FIT_RGBAF = 12
|
| 121 |
+
|
| 122 |
+
dtypes = {
|
| 123 |
+
FIT_BITMAP: numpy.uint8,
|
| 124 |
+
FIT_UINT16: numpy.uint16,
|
| 125 |
+
FIT_INT16: numpy.int16,
|
| 126 |
+
FIT_UINT32: numpy.uint32,
|
| 127 |
+
FIT_INT32: numpy.int32,
|
| 128 |
+
FIT_FLOAT: numpy.float32,
|
| 129 |
+
FIT_DOUBLE: numpy.float64,
|
| 130 |
+
FIT_COMPLEX: numpy.complex128,
|
| 131 |
+
FIT_RGB16: numpy.uint16,
|
| 132 |
+
FIT_RGBA16: numpy.uint16,
|
| 133 |
+
FIT_RGBF: numpy.float32,
|
| 134 |
+
FIT_RGBAF: numpy.float32,
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
fi_types = {
|
| 138 |
+
(numpy.uint8, 1): FIT_BITMAP,
|
| 139 |
+
(numpy.uint8, 3): FIT_BITMAP,
|
| 140 |
+
(numpy.uint8, 4): FIT_BITMAP,
|
| 141 |
+
(numpy.uint16, 1): FIT_UINT16,
|
| 142 |
+
(numpy.int16, 1): FIT_INT16,
|
| 143 |
+
(numpy.uint32, 1): FIT_UINT32,
|
| 144 |
+
(numpy.int32, 1): FIT_INT32,
|
| 145 |
+
(numpy.float32, 1): FIT_FLOAT,
|
| 146 |
+
(numpy.float64, 1): FIT_DOUBLE,
|
| 147 |
+
(numpy.complex128, 1): FIT_COMPLEX,
|
| 148 |
+
(numpy.uint16, 3): FIT_RGB16,
|
| 149 |
+
(numpy.uint16, 4): FIT_RGBA16,
|
| 150 |
+
(numpy.float32, 3): FIT_RGBF,
|
| 151 |
+
(numpy.float32, 4): FIT_RGBAF,
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
extra_dims = {
|
| 155 |
+
FIT_UINT16: [],
|
| 156 |
+
FIT_INT16: [],
|
| 157 |
+
FIT_UINT32: [],
|
| 158 |
+
FIT_INT32: [],
|
| 159 |
+
FIT_FLOAT: [],
|
| 160 |
+
FIT_DOUBLE: [],
|
| 161 |
+
FIT_COMPLEX: [],
|
| 162 |
+
FIT_RGB16: [3],
|
| 163 |
+
FIT_RGBA16: [4],
|
| 164 |
+
FIT_RGBF: [3],
|
| 165 |
+
FIT_RGBAF: [4],
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
class IO_FLAGS(object):
|
| 170 |
+
FIF_LOAD_NOPIXELS = 0x8000 # loading: load the image header only
|
| 171 |
+
# # (not supported by all plugins)
|
| 172 |
+
BMP_DEFAULT = 0
|
| 173 |
+
BMP_SAVE_RLE = 1
|
| 174 |
+
CUT_DEFAULT = 0
|
| 175 |
+
DDS_DEFAULT = 0
|
| 176 |
+
EXR_DEFAULT = 0 # save data as half with piz-based wavelet compression
|
| 177 |
+
EXR_FLOAT = 0x0001 # save data as float instead of half (not recommended)
|
| 178 |
+
EXR_NONE = 0x0002 # save with no compression
|
| 179 |
+
EXR_ZIP = 0x0004 # save with zlib compression, in blocks of 16 scan lines
|
| 180 |
+
EXR_PIZ = 0x0008 # save with piz-based wavelet compression
|
| 181 |
+
EXR_PXR24 = 0x0010 # save with lossy 24-bit float compression
|
| 182 |
+
EXR_B44 = 0x0020 # save with lossy 44% float compression
|
| 183 |
+
# # - goes to 22% when combined with EXR_LC
|
| 184 |
+
EXR_LC = 0x0040 # save images with one luminance and two chroma channels,
|
| 185 |
+
# # rather than as RGB (lossy compression)
|
| 186 |
+
FAXG3_DEFAULT = 0
|
| 187 |
+
GIF_DEFAULT = 0
|
| 188 |
+
GIF_LOAD256 = 1 # Load the image as a 256 color image with ununsed
|
| 189 |
+
# # palette entries, if it's 16 or 2 color
|
| 190 |
+
GIF_PLAYBACK = 2 # 'Play' the GIF to generate each frame (as 32bpp)
|
| 191 |
+
# # instead of returning raw frame data when loading
|
| 192 |
+
HDR_DEFAULT = 0
|
| 193 |
+
ICO_DEFAULT = 0
|
| 194 |
+
ICO_MAKEALPHA = 1 # convert to 32bpp and create an alpha channel from the
|
| 195 |
+
# # AND-mask when loading
|
| 196 |
+
IFF_DEFAULT = 0
|
| 197 |
+
J2K_DEFAULT = 0 # save with a 16:1 rate
|
| 198 |
+
JP2_DEFAULT = 0 # save with a 16:1 rate
|
| 199 |
+
JPEG_DEFAULT = 0 # loading (see JPEG_FAST);
|
| 200 |
+
# # saving (see JPEG_QUALITYGOOD|JPEG_SUBSAMPLING_420)
|
| 201 |
+
JPEG_FAST = 0x0001 # load the file as fast as possible,
|
| 202 |
+
# # sacrificing some quality
|
| 203 |
+
JPEG_ACCURATE = 0x0002 # load the file with the best quality,
|
| 204 |
+
# # sacrificing some speed
|
| 205 |
+
JPEG_CMYK = 0x0004 # load separated CMYK "as is"
|
| 206 |
+
# # (use | to combine with other load flags)
|
| 207 |
+
JPEG_EXIFROTATE = 0x0008 # load and rotate according to
|
| 208 |
+
# # Exif 'Orientation' tag if available
|
| 209 |
+
JPEG_QUALITYSUPERB = 0x80 # save with superb quality (100:1)
|
| 210 |
+
JPEG_QUALITYGOOD = 0x0100 # save with good quality (75:1)
|
| 211 |
+
JPEG_QUALITYNORMAL = 0x0200 # save with normal quality (50:1)
|
| 212 |
+
JPEG_QUALITYAVERAGE = 0x0400 # save with average quality (25:1)
|
| 213 |
+
JPEG_QUALITYBAD = 0x0800 # save with bad quality (10:1)
|
| 214 |
+
JPEG_PROGRESSIVE = 0x2000 # save as a progressive-JPEG
|
| 215 |
+
# # (use | to combine with other save flags)
|
| 216 |
+
JPEG_SUBSAMPLING_411 = 0x1000 # save with high 4x1 chroma
|
| 217 |
+
# # subsampling (4:1:1)
|
| 218 |
+
JPEG_SUBSAMPLING_420 = 0x4000 # save with medium 2x2 medium chroma
|
| 219 |
+
# # subsampling (4:2:0) - default value
|
| 220 |
+
JPEG_SUBSAMPLING_422 = 0x8000 # save /w low 2x1 chroma subsampling (4:2:2)
|
| 221 |
+
JPEG_SUBSAMPLING_444 = 0x10000 # save with no chroma subsampling (4:4:4)
|
| 222 |
+
JPEG_OPTIMIZE = 0x20000 # on saving, compute optimal Huffman coding tables
|
| 223 |
+
# # (can reduce a few percent of file size)
|
| 224 |
+
JPEG_BASELINE = 0x40000 # save basic JPEG, without metadata or any markers
|
| 225 |
+
KOALA_DEFAULT = 0
|
| 226 |
+
LBM_DEFAULT = 0
|
| 227 |
+
MNG_DEFAULT = 0
|
| 228 |
+
PCD_DEFAULT = 0
|
| 229 |
+
PCD_BASE = 1 # load the bitmap sized 768 x 512
|
| 230 |
+
PCD_BASEDIV4 = 2 # load the bitmap sized 384 x 256
|
| 231 |
+
PCD_BASEDIV16 = 3 # load the bitmap sized 192 x 128
|
| 232 |
+
PCX_DEFAULT = 0
|
| 233 |
+
PFM_DEFAULT = 0
|
| 234 |
+
PICT_DEFAULT = 0
|
| 235 |
+
PNG_DEFAULT = 0
|
| 236 |
+
PNG_IGNOREGAMMA = 1 # loading: avoid gamma correction
|
| 237 |
+
PNG_Z_BEST_SPEED = 0x0001 # save using ZLib level 1 compression flag
|
| 238 |
+
# # (default value is 6)
|
| 239 |
+
PNG_Z_DEFAULT_COMPRESSION = 0x0006 # save using ZLib level 6 compression
|
| 240 |
+
# # flag (default recommended value)
|
| 241 |
+
PNG_Z_BEST_COMPRESSION = 0x0009 # save using ZLib level 9 compression flag
|
| 242 |
+
# # (default value is 6)
|
| 243 |
+
PNG_Z_NO_COMPRESSION = 0x0100 # save without ZLib compression
|
| 244 |
+
PNG_INTERLACED = 0x0200 # save using Adam7 interlacing (use | to combine
|
| 245 |
+
# # with other save flags)
|
| 246 |
+
PNM_DEFAULT = 0
|
| 247 |
+
PNM_SAVE_RAW = 0 # Writer saves in RAW format (i.e. P4, P5 or P6)
|
| 248 |
+
PNM_SAVE_ASCII = 1 # Writer saves in ASCII format (i.e. P1, P2 or P3)
|
| 249 |
+
PSD_DEFAULT = 0
|
| 250 |
+
PSD_CMYK = 1 # reads tags for separated CMYK (default is conversion to RGB)
|
| 251 |
+
PSD_LAB = 2 # reads tags for CIELab (default is conversion to RGB)
|
| 252 |
+
RAS_DEFAULT = 0
|
| 253 |
+
RAW_DEFAULT = 0 # load the file as linear RGB 48-bit
|
| 254 |
+
RAW_PREVIEW = 1 # try to load the embedded JPEG preview with included
|
| 255 |
+
# # Exif Data or default to RGB 24-bit
|
| 256 |
+
RAW_DISPLAY = 2 # load the file as RGB 24-bit
|
| 257 |
+
SGI_DEFAULT = 0
|
| 258 |
+
TARGA_DEFAULT = 0
|
| 259 |
+
TARGA_LOAD_RGB888 = 1 # Convert RGB555 and ARGB8888 -> RGB888.
|
| 260 |
+
TARGA_SAVE_RLE = 2 # Save with RLE compression
|
| 261 |
+
TIFF_DEFAULT = 0
|
| 262 |
+
TIFF_CMYK = 0x0001 # reads/stores tags for separated CMYK
|
| 263 |
+
# # (use | to combine with compression flags)
|
| 264 |
+
TIFF_PACKBITS = 0x0100 # save using PACKBITS compression
|
| 265 |
+
TIFF_DEFLATE = 0x0200 # save using DEFLATE (a.k.a. ZLIB) compression
|
| 266 |
+
TIFF_ADOBE_DEFLATE = 0x0400 # save using ADOBE DEFLATE compression
|
| 267 |
+
TIFF_NONE = 0x0800 # save without any compression
|
| 268 |
+
TIFF_CCITTFAX3 = 0x1000 # save using CCITT Group 3 fax encoding
|
| 269 |
+
TIFF_CCITTFAX4 = 0x2000 # save using CCITT Group 4 fax encoding
|
| 270 |
+
TIFF_LZW = 0x4000 # save using LZW compression
|
| 271 |
+
TIFF_JPEG = 0x8000 # save using JPEG compression
|
| 272 |
+
TIFF_LOGLUV = 0x10000 # save using LogLuv compression
|
| 273 |
+
WBMP_DEFAULT = 0
|
| 274 |
+
XBM_DEFAULT = 0
|
| 275 |
+
XPM_DEFAULT = 0
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
class METADATA_MODELS(object):
|
| 279 |
+
FIMD_COMMENTS = 0
|
| 280 |
+
FIMD_EXIF_MAIN = 1
|
| 281 |
+
FIMD_EXIF_EXIF = 2
|
| 282 |
+
FIMD_EXIF_GPS = 3
|
| 283 |
+
FIMD_EXIF_MAKERNOTE = 4
|
| 284 |
+
FIMD_EXIF_INTEROP = 5
|
| 285 |
+
FIMD_IPTC = 6
|
| 286 |
+
FIMD_XMP = 7
|
| 287 |
+
FIMD_GEOTIFF = 8
|
| 288 |
+
FIMD_ANIMATION = 9
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
class METADATA_DATATYPE(object):
|
| 292 |
+
FIDT_BYTE = 1 # 8-bit unsigned integer
|
| 293 |
+
FIDT_ASCII = 2 # 8-bit bytes w/ last byte null
|
| 294 |
+
FIDT_SHORT = 3 # 16-bit unsigned integer
|
| 295 |
+
FIDT_LONG = 4 # 32-bit unsigned integer
|
| 296 |
+
FIDT_RATIONAL = 5 # 64-bit unsigned fraction
|
| 297 |
+
FIDT_SBYTE = 6 # 8-bit signed integer
|
| 298 |
+
FIDT_UNDEFINED = 7 # 8-bit untyped data
|
| 299 |
+
FIDT_SSHORT = 8 # 16-bit signed integer
|
| 300 |
+
FIDT_SLONG = 9 # 32-bit signed integer
|
| 301 |
+
FIDT_SRATIONAL = 10 # 64-bit signed fraction
|
| 302 |
+
FIDT_FLOAT = 11 # 32-bit IEEE floating point
|
| 303 |
+
FIDT_DOUBLE = 12 # 64-bit IEEE floating point
|
| 304 |
+
FIDT_IFD = 13 # 32-bit unsigned integer (offset)
|
| 305 |
+
FIDT_PALETTE = 14 # 32-bit RGBQUAD
|
| 306 |
+
FIDT_LONG8 = 16 # 64-bit unsigned integer
|
| 307 |
+
FIDT_SLONG8 = 17 # 64-bit signed integer
|
| 308 |
+
FIDT_IFD8 = 18 # 64-bit unsigned integer (offset)
|
| 309 |
+
|
| 310 |
+
dtypes = {
|
| 311 |
+
FIDT_BYTE: numpy.uint8,
|
| 312 |
+
FIDT_SHORT: numpy.uint16,
|
| 313 |
+
FIDT_LONG: numpy.uint32,
|
| 314 |
+
FIDT_RATIONAL: [("numerator", numpy.uint32), ("denominator", numpy.uint32)],
|
| 315 |
+
FIDT_LONG8: numpy.uint64,
|
| 316 |
+
FIDT_SLONG8: numpy.int64,
|
| 317 |
+
FIDT_IFD8: numpy.uint64,
|
| 318 |
+
FIDT_SBYTE: numpy.int8,
|
| 319 |
+
FIDT_UNDEFINED: numpy.uint8,
|
| 320 |
+
FIDT_SSHORT: numpy.int16,
|
| 321 |
+
FIDT_SLONG: numpy.int32,
|
| 322 |
+
FIDT_SRATIONAL: [("numerator", numpy.int32), ("denominator", numpy.int32)],
|
| 323 |
+
FIDT_FLOAT: numpy.float32,
|
| 324 |
+
FIDT_DOUBLE: numpy.float64,
|
| 325 |
+
FIDT_IFD: numpy.uint32,
|
| 326 |
+
FIDT_PALETTE: [
|
| 327 |
+
("R", numpy.uint8),
|
| 328 |
+
("G", numpy.uint8),
|
| 329 |
+
("B", numpy.uint8),
|
| 330 |
+
("A", numpy.uint8),
|
| 331 |
+
],
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
class Freeimage(object):
|
| 336 |
+
"""Class to represent an interface to the FreeImage library.
|
| 337 |
+
This class is relatively thin. It provides a Pythonic API that converts
|
| 338 |
+
Freeimage objects to Python objects, but that's about it.
|
| 339 |
+
The actual implementation should be provided by the plugins.
|
| 340 |
+
|
| 341 |
+
The recommended way to call into the Freeimage library (so that
|
| 342 |
+
errors and warnings show up in the right moment) is to use this
|
| 343 |
+
object as a context manager:
|
| 344 |
+
with imageio.fi as lib:
|
| 345 |
+
lib.FreeImage_GetPalette()
|
| 346 |
+
|
| 347 |
+
"""
|
| 348 |
+
|
| 349 |
+
_API = {
|
| 350 |
+
# All we're doing here is telling ctypes that some of the
|
| 351 |
+
# FreeImage functions return pointers instead of integers. (On
|
| 352 |
+
# 64-bit systems, without this information the pointers get
|
| 353 |
+
# truncated and crashes result). There's no need to list
|
| 354 |
+
# functions that return ints, or the types of the parameters
|
| 355 |
+
# to these or other functions -- that's fine to do implicitly.
|
| 356 |
+
# Note that the ctypes immediately converts the returned void_p
|
| 357 |
+
# back to a python int again! This is really not helpful,
|
| 358 |
+
# because then passing it back to another library call will
|
| 359 |
+
# cause truncation-to-32-bits on 64-bit systems. Thanks, ctypes!
|
| 360 |
+
# So after these calls one must immediately re-wrap the int as
|
| 361 |
+
# a c_void_p if it is to be passed back into FreeImage.
|
| 362 |
+
"FreeImage_AllocateT": (ctypes.c_void_p, None),
|
| 363 |
+
"FreeImage_FindFirstMetadata": (ctypes.c_void_p, None),
|
| 364 |
+
"FreeImage_GetBits": (ctypes.c_void_p, None),
|
| 365 |
+
"FreeImage_GetPalette": (ctypes.c_void_p, None),
|
| 366 |
+
"FreeImage_GetTagKey": (ctypes.c_char_p, None),
|
| 367 |
+
"FreeImage_GetTagValue": (ctypes.c_void_p, None),
|
| 368 |
+
"FreeImage_CreateTag": (ctypes.c_void_p, None),
|
| 369 |
+
"FreeImage_Save": (ctypes.c_void_p, None),
|
| 370 |
+
"FreeImage_Load": (ctypes.c_void_p, None),
|
| 371 |
+
"FreeImage_LoadFromMemory": (ctypes.c_void_p, None),
|
| 372 |
+
"FreeImage_OpenMultiBitmap": (ctypes.c_void_p, None),
|
| 373 |
+
"FreeImage_LoadMultiBitmapFromMemory": (ctypes.c_void_p, None),
|
| 374 |
+
"FreeImage_LockPage": (ctypes.c_void_p, None),
|
| 375 |
+
"FreeImage_OpenMemory": (ctypes.c_void_p, None),
|
| 376 |
+
# 'FreeImage_ReadMemory': (ctypes.c_void_p, None),
|
| 377 |
+
# 'FreeImage_CloseMemory': (ctypes.c_void_p, None),
|
| 378 |
+
"FreeImage_GetVersion": (ctypes.c_char_p, None),
|
| 379 |
+
"FreeImage_GetFIFExtensionList": (ctypes.c_char_p, None),
|
| 380 |
+
"FreeImage_GetFormatFromFIF": (ctypes.c_char_p, None),
|
| 381 |
+
"FreeImage_GetFIFDescription": (ctypes.c_char_p, None),
|
| 382 |
+
"FreeImage_ColorQuantizeEx": (ctypes.c_void_p, None),
|
| 383 |
+
# Pypy wants some extra definitions, so here we go ...
|
| 384 |
+
"FreeImage_IsLittleEndian": (ctypes.c_int, None),
|
| 385 |
+
"FreeImage_SetOutputMessage": (ctypes.c_void_p, None),
|
| 386 |
+
"FreeImage_GetFIFCount": (ctypes.c_int, None),
|
| 387 |
+
"FreeImage_IsPluginEnabled": (ctypes.c_int, None),
|
| 388 |
+
"FreeImage_GetFileType": (ctypes.c_int, None),
|
| 389 |
+
#
|
| 390 |
+
"FreeImage_GetTagType": (ctypes.c_int, None),
|
| 391 |
+
"FreeImage_GetTagLength": (ctypes.c_int, None),
|
| 392 |
+
"FreeImage_FindNextMetadata": (ctypes.c_int, None),
|
| 393 |
+
"FreeImage_FindCloseMetadata": (ctypes.c_void_p, None),
|
| 394 |
+
#
|
| 395 |
+
"FreeImage_GetFIFFromFilename": (ctypes.c_int, None),
|
| 396 |
+
"FreeImage_FIFSupportsReading": (ctypes.c_int, None),
|
| 397 |
+
"FreeImage_FIFSupportsWriting": (ctypes.c_int, None),
|
| 398 |
+
"FreeImage_FIFSupportsExportType": (ctypes.c_int, None),
|
| 399 |
+
"FreeImage_FIFSupportsExportBPP": (ctypes.c_int, None),
|
| 400 |
+
"FreeImage_GetHeight": (ctypes.c_int, None),
|
| 401 |
+
"FreeImage_GetWidth": (ctypes.c_int, None),
|
| 402 |
+
"FreeImage_GetImageType": (ctypes.c_int, None),
|
| 403 |
+
"FreeImage_GetBPP": (ctypes.c_int, None),
|
| 404 |
+
"FreeImage_GetColorsUsed": (ctypes.c_int, None),
|
| 405 |
+
"FreeImage_ConvertTo32Bits": (ctypes.c_void_p, None),
|
| 406 |
+
"FreeImage_GetPitch": (ctypes.c_int, None),
|
| 407 |
+
"FreeImage_Unload": (ctypes.c_void_p, None),
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
def __init__(self):
|
| 411 |
+
|
| 412 |
+
# Initialize freeimage lib as None
|
| 413 |
+
self._lib = None
|
| 414 |
+
|
| 415 |
+
# A lock to create thread-safety
|
| 416 |
+
self._lock = threading.RLock()
|
| 417 |
+
|
| 418 |
+
# Init log messages lists
|
| 419 |
+
self._messages = []
|
| 420 |
+
|
| 421 |
+
# Select functype for error handler
|
| 422 |
+
if sys.platform.startswith("win"):
|
| 423 |
+
functype = ctypes.WINFUNCTYPE
|
| 424 |
+
else:
|
| 425 |
+
functype = ctypes.CFUNCTYPE
|
| 426 |
+
|
| 427 |
+
# Create output message handler
|
| 428 |
+
@functype(None, ctypes.c_int, ctypes.c_char_p)
|
| 429 |
+
def error_handler(fif, message):
|
| 430 |
+
message = message.decode("utf-8")
|
| 431 |
+
self._messages.append(message)
|
| 432 |
+
while (len(self._messages)) > 256:
|
| 433 |
+
self._messages.pop(0)
|
| 434 |
+
|
| 435 |
+
# Make sure to keep a ref to function
|
| 436 |
+
self._error_handler = error_handler
|
| 437 |
+
|
| 438 |
+
@property
|
| 439 |
+
def lib(self):
|
| 440 |
+
if self._lib is None:
|
| 441 |
+
try:
|
| 442 |
+
self.load_freeimage()
|
| 443 |
+
except OSError as err:
|
| 444 |
+
self._lib = "The freeimage library could not be loaded: "
|
| 445 |
+
self._lib += str(err)
|
| 446 |
+
if isinstance(self._lib, str):
|
| 447 |
+
raise RuntimeError(self._lib)
|
| 448 |
+
return self._lib
|
| 449 |
+
|
| 450 |
+
def has_lib(self):
|
| 451 |
+
try:
|
| 452 |
+
self.lib
|
| 453 |
+
except Exception:
|
| 454 |
+
return False
|
| 455 |
+
return True
|
| 456 |
+
|
| 457 |
+
def load_freeimage(self):
|
| 458 |
+
"""Try to load the freeimage lib from the system. If not successful,
|
| 459 |
+
try to download the imageio version and try again.
|
| 460 |
+
"""
|
| 461 |
+
# Load library and register API
|
| 462 |
+
success = False
|
| 463 |
+
try:
|
| 464 |
+
# Try without forcing a download, but giving preference
|
| 465 |
+
# to the imageio-provided lib (if previously downloaded)
|
| 466 |
+
self._load_freeimage()
|
| 467 |
+
self._register_api()
|
| 468 |
+
if self.lib.FreeImage_GetVersion().decode("utf-8") >= "3.15":
|
| 469 |
+
success = True
|
| 470 |
+
except OSError:
|
| 471 |
+
pass
|
| 472 |
+
|
| 473 |
+
if not success:
|
| 474 |
+
# Ensure we have our own lib, try again
|
| 475 |
+
get_freeimage_lib()
|
| 476 |
+
self._load_freeimage()
|
| 477 |
+
self._register_api()
|
| 478 |
+
|
| 479 |
+
# Wrap up
|
| 480 |
+
self.lib.FreeImage_SetOutputMessage(self._error_handler)
|
| 481 |
+
self.lib_version = self.lib.FreeImage_GetVersion().decode("utf-8")
|
| 482 |
+
|
| 483 |
+
def _load_freeimage(self):
|
| 484 |
+
|
| 485 |
+
# Define names
|
| 486 |
+
lib_names = ["freeimage", "libfreeimage"]
|
| 487 |
+
exact_lib_names = [
|
| 488 |
+
"FreeImage",
|
| 489 |
+
"libfreeimage.dylib",
|
| 490 |
+
"libfreeimage.so",
|
| 491 |
+
"libfreeimage.so.3",
|
| 492 |
+
]
|
| 493 |
+
# Add names of libraries that we provide (that file may not exist)
|
| 494 |
+
res_dirs = resource_dirs()
|
| 495 |
+
plat = get_platform()
|
| 496 |
+
if plat: # Can be None on e.g. FreeBSD
|
| 497 |
+
fname = FNAME_PER_PLATFORM[plat]
|
| 498 |
+
for dir in res_dirs:
|
| 499 |
+
exact_lib_names.insert(0, os.path.join(dir, "freeimage", fname))
|
| 500 |
+
|
| 501 |
+
# Add the path specified with IMAGEIO_FREEIMAGE_LIB:
|
| 502 |
+
lib = os.getenv("IMAGEIO_FREEIMAGE_LIB", None)
|
| 503 |
+
if lib is not None:
|
| 504 |
+
exact_lib_names.insert(0, lib)
|
| 505 |
+
|
| 506 |
+
# Load
|
| 507 |
+
try:
|
| 508 |
+
lib, fname = load_lib(exact_lib_names, lib_names, res_dirs)
|
| 509 |
+
except OSError as err: # pragma: no cover
|
| 510 |
+
err_msg = str(err) + "\nPlease install the FreeImage library."
|
| 511 |
+
raise OSError(err_msg)
|
| 512 |
+
|
| 513 |
+
# Store
|
| 514 |
+
self._lib = lib
|
| 515 |
+
self.lib_fname = fname
|
| 516 |
+
|
| 517 |
+
def _register_api(self):
|
| 518 |
+
# Albert's ctypes pattern
|
| 519 |
+
for f, (restype, argtypes) in self._API.items():
|
| 520 |
+
func = getattr(self.lib, f)
|
| 521 |
+
func.restype = restype
|
| 522 |
+
func.argtypes = argtypes
|
| 523 |
+
|
| 524 |
+
# Handling of output messages
|
| 525 |
+
|
| 526 |
+
def __enter__(self):
|
| 527 |
+
self._lock.acquire()
|
| 528 |
+
return self.lib
|
| 529 |
+
|
| 530 |
+
def __exit__(self, *args):
|
| 531 |
+
self._show_any_warnings()
|
| 532 |
+
self._lock.release()
|
| 533 |
+
|
| 534 |
+
def _reset_log(self):
|
| 535 |
+
"""Reset the list of output messages. Call this before
|
| 536 |
+
loading or saving an image with the FreeImage API.
|
| 537 |
+
"""
|
| 538 |
+
self._messages = []
|
| 539 |
+
|
| 540 |
+
def _get_error_message(self):
|
| 541 |
+
"""Get the output messages produced since the last reset as
|
| 542 |
+
one string. Returns 'No known reason.' if there are no messages.
|
| 543 |
+
Also resets the log.
|
| 544 |
+
"""
|
| 545 |
+
if self._messages:
|
| 546 |
+
res = " ".join(self._messages)
|
| 547 |
+
self._reset_log()
|
| 548 |
+
return res
|
| 549 |
+
else:
|
| 550 |
+
return "No known reason."
|
| 551 |
+
|
| 552 |
+
def _show_any_warnings(self):
|
| 553 |
+
"""If there were any messages since the last reset, show them
|
| 554 |
+
as a warning. Otherwise do nothing. Also resets the messages.
|
| 555 |
+
"""
|
| 556 |
+
if self._messages:
|
| 557 |
+
logger.warning("imageio.freeimage warning: " + self._get_error_message())
|
| 558 |
+
self._reset_log()
|
| 559 |
+
|
| 560 |
+
def get_output_log(self):
|
| 561 |
+
"""Return a list of the last 256 output messages
|
| 562 |
+
(warnings and errors) produced by the FreeImage library.
|
| 563 |
+
"""
|
| 564 |
+
# This message log is not cleared/reset, but kept to 256 elements.
|
| 565 |
+
return [m for m in self._messages]
|
| 566 |
+
|
| 567 |
+
def getFIF(self, filename, mode, bb=None):
|
| 568 |
+
"""Get the freeimage Format (FIF) from a given filename.
|
| 569 |
+
If mode is 'r', will try to determine the format by reading
|
| 570 |
+
the file, otherwise only the filename is used.
|
| 571 |
+
|
| 572 |
+
This function also tests whether the format supports reading/writing.
|
| 573 |
+
"""
|
| 574 |
+
with self as lib:
|
| 575 |
+
|
| 576 |
+
# Init
|
| 577 |
+
ftype = -1
|
| 578 |
+
if mode not in "rw":
|
| 579 |
+
raise ValueError('Invalid mode (must be "r" or "w").')
|
| 580 |
+
|
| 581 |
+
# Try getting format from the content. Note that some files
|
| 582 |
+
# do not have a header that allows reading the format from
|
| 583 |
+
# the file.
|
| 584 |
+
if mode == "r":
|
| 585 |
+
if bb is not None:
|
| 586 |
+
fimemory = lib.FreeImage_OpenMemory(ctypes.c_char_p(bb), len(bb))
|
| 587 |
+
ftype = lib.FreeImage_GetFileTypeFromMemory(
|
| 588 |
+
ctypes.c_void_p(fimemory), len(bb)
|
| 589 |
+
)
|
| 590 |
+
lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))
|
| 591 |
+
if (ftype == -1) and os.path.isfile(filename):
|
| 592 |
+
ftype = lib.FreeImage_GetFileType(efn(filename), 0)
|
| 593 |
+
# Try getting the format from the extension
|
| 594 |
+
if ftype == -1:
|
| 595 |
+
ftype = lib.FreeImage_GetFIFFromFilename(efn(filename))
|
| 596 |
+
|
| 597 |
+
# Test if ok
|
| 598 |
+
if ftype == -1:
|
| 599 |
+
raise ValueError('Cannot determine format of file "%s"' % filename)
|
| 600 |
+
elif mode == "w" and not lib.FreeImage_FIFSupportsWriting(ftype):
|
| 601 |
+
raise ValueError('Cannot write the format of file "%s"' % filename)
|
| 602 |
+
elif mode == "r" and not lib.FreeImage_FIFSupportsReading(ftype):
|
| 603 |
+
raise ValueError('Cannot read the format of file "%s"' % filename)
|
| 604 |
+
return ftype
|
| 605 |
+
|
| 606 |
+
def create_bitmap(self, filename, ftype, flags=0):
|
| 607 |
+
"""create_bitmap(filename, ftype, flags=0)
|
| 608 |
+
Create a wrapped bitmap object.
|
| 609 |
+
"""
|
| 610 |
+
return FIBitmap(self, filename, ftype, flags)
|
| 611 |
+
|
| 612 |
+
def create_multipage_bitmap(self, filename, ftype, flags=0):
|
| 613 |
+
"""create_multipage_bitmap(filename, ftype, flags=0)
|
| 614 |
+
Create a wrapped multipage bitmap object.
|
| 615 |
+
"""
|
| 616 |
+
return FIMultipageBitmap(self, filename, ftype, flags)
|
| 617 |
+
|
| 618 |
+
|
| 619 |
+
class FIBaseBitmap(object):
|
| 620 |
+
def __init__(self, fi, filename, ftype, flags):
|
| 621 |
+
self._fi = fi
|
| 622 |
+
self._filename = filename
|
| 623 |
+
self._ftype = ftype
|
| 624 |
+
self._flags = flags
|
| 625 |
+
self._bitmap = None
|
| 626 |
+
self._close_funcs = []
|
| 627 |
+
|
| 628 |
+
def __del__(self):
|
| 629 |
+
self.close()
|
| 630 |
+
|
| 631 |
+
def close(self):
|
| 632 |
+
if (self._bitmap is not None) and self._close_funcs:
|
| 633 |
+
for close_func in self._close_funcs:
|
| 634 |
+
try:
|
| 635 |
+
with self._fi:
|
| 636 |
+
fun = close_func[0]
|
| 637 |
+
fun(*close_func[1:])
|
| 638 |
+
except Exception: # pragma: no cover
|
| 639 |
+
pass
|
| 640 |
+
self._close_funcs = []
|
| 641 |
+
self._bitmap = None
|
| 642 |
+
|
| 643 |
+
def _set_bitmap(self, bitmap, close_func=None):
|
| 644 |
+
"""Function to set the bitmap and specify the function to unload it."""
|
| 645 |
+
if self._bitmap is not None:
|
| 646 |
+
pass # bitmap is converted
|
| 647 |
+
if close_func is None:
|
| 648 |
+
close_func = self._fi.lib.FreeImage_Unload, bitmap
|
| 649 |
+
|
| 650 |
+
self._bitmap = bitmap
|
| 651 |
+
if close_func:
|
| 652 |
+
self._close_funcs.append(close_func)
|
| 653 |
+
|
| 654 |
+
def get_meta_data(self):
|
| 655 |
+
|
| 656 |
+
# todo: there is also FreeImage_TagToString, is that useful?
|
| 657 |
+
# and would that work well when reading and then saving?
|
| 658 |
+
|
| 659 |
+
# Create a list of (model_name, number) tuples
|
| 660 |
+
models = [
|
| 661 |
+
(name[5:], number)
|
| 662 |
+
for name, number in METADATA_MODELS.__dict__.items()
|
| 663 |
+
if name.startswith("FIMD_")
|
| 664 |
+
]
|
| 665 |
+
|
| 666 |
+
# Prepare
|
| 667 |
+
metadata = Dict()
|
| 668 |
+
tag = ctypes.c_void_p()
|
| 669 |
+
|
| 670 |
+
with self._fi as lib:
|
| 671 |
+
|
| 672 |
+
# Iterate over all FreeImage meta models
|
| 673 |
+
for model_name, number in models:
|
| 674 |
+
|
| 675 |
+
# Find beginning, get search handle
|
| 676 |
+
mdhandle = lib.FreeImage_FindFirstMetadata(
|
| 677 |
+
number, self._bitmap, ctypes.byref(tag)
|
| 678 |
+
)
|
| 679 |
+
mdhandle = ctypes.c_void_p(mdhandle)
|
| 680 |
+
if mdhandle:
|
| 681 |
+
|
| 682 |
+
# Iterate over all tags in this model
|
| 683 |
+
more = True
|
| 684 |
+
while more:
|
| 685 |
+
# Get info about tag
|
| 686 |
+
tag_name = lib.FreeImage_GetTagKey(tag).decode("utf-8")
|
| 687 |
+
tag_type = lib.FreeImage_GetTagType(tag)
|
| 688 |
+
byte_size = lib.FreeImage_GetTagLength(tag)
|
| 689 |
+
char_ptr = ctypes.c_char * byte_size
|
| 690 |
+
data = char_ptr.from_address(lib.FreeImage_GetTagValue(tag))
|
| 691 |
+
# Convert in a way compatible with Pypy
|
| 692 |
+
tag_bytes = bytes(bytearray(data))
|
| 693 |
+
# The default value is the raw bytes
|
| 694 |
+
tag_val = tag_bytes
|
| 695 |
+
# Convert to a Python value in the metadata dict
|
| 696 |
+
if tag_type == METADATA_DATATYPE.FIDT_ASCII:
|
| 697 |
+
tag_val = tag_bytes.decode("utf-8", "replace")
|
| 698 |
+
elif tag_type in METADATA_DATATYPE.dtypes:
|
| 699 |
+
dtype = METADATA_DATATYPE.dtypes[tag_type]
|
| 700 |
+
if IS_PYPY and isinstance(dtype, (list, tuple)):
|
| 701 |
+
pass # pragma: no cover - or we get a segfault
|
| 702 |
+
else:
|
| 703 |
+
try:
|
| 704 |
+
tag_val = numpy.frombuffer(
|
| 705 |
+
tag_bytes, dtype=dtype
|
| 706 |
+
).copy()
|
| 707 |
+
if len(tag_val) == 1:
|
| 708 |
+
tag_val = tag_val[0]
|
| 709 |
+
except Exception: # pragma: no cover
|
| 710 |
+
pass
|
| 711 |
+
# Store data in dict
|
| 712 |
+
subdict = metadata.setdefault(model_name, Dict())
|
| 713 |
+
subdict[tag_name] = tag_val
|
| 714 |
+
# Next
|
| 715 |
+
more = lib.FreeImage_FindNextMetadata(
|
| 716 |
+
mdhandle, ctypes.byref(tag)
|
| 717 |
+
)
|
| 718 |
+
|
| 719 |
+
# Close search handle for current meta model
|
| 720 |
+
lib.FreeImage_FindCloseMetadata(mdhandle)
|
| 721 |
+
|
| 722 |
+
# Done
|
| 723 |
+
return metadata
|
| 724 |
+
|
| 725 |
+
def set_meta_data(self, metadata):
|
| 726 |
+
|
| 727 |
+
# Create a dict mapping model_name to number
|
| 728 |
+
models = {}
|
| 729 |
+
for name, number in METADATA_MODELS.__dict__.items():
|
| 730 |
+
if name.startswith("FIMD_"):
|
| 731 |
+
models[name[5:]] = number
|
| 732 |
+
|
| 733 |
+
# Create a mapping from numpy.dtype to METADATA_DATATYPE
|
| 734 |
+
def get_tag_type_number(dtype):
|
| 735 |
+
for number, numpy_dtype in METADATA_DATATYPE.dtypes.items():
|
| 736 |
+
if dtype == numpy_dtype:
|
| 737 |
+
return number
|
| 738 |
+
else:
|
| 739 |
+
return None
|
| 740 |
+
|
| 741 |
+
with self._fi as lib:
|
| 742 |
+
|
| 743 |
+
for model_name, subdict in metadata.items():
|
| 744 |
+
|
| 745 |
+
# Get model number
|
| 746 |
+
number = models.get(model_name, None)
|
| 747 |
+
if number is None:
|
| 748 |
+
continue # Unknown model, silent ignore
|
| 749 |
+
|
| 750 |
+
for tag_name, tag_val in subdict.items():
|
| 751 |
+
|
| 752 |
+
# Create new tag
|
| 753 |
+
tag = lib.FreeImage_CreateTag()
|
| 754 |
+
tag = ctypes.c_void_p(tag)
|
| 755 |
+
|
| 756 |
+
try:
|
| 757 |
+
# Convert Python value to FI type, val
|
| 758 |
+
is_ascii = False
|
| 759 |
+
if isinstance(tag_val, str):
|
| 760 |
+
try:
|
| 761 |
+
tag_bytes = tag_val.encode("ascii")
|
| 762 |
+
is_ascii = True
|
| 763 |
+
except UnicodeError:
|
| 764 |
+
pass
|
| 765 |
+
if is_ascii:
|
| 766 |
+
tag_type = METADATA_DATATYPE.FIDT_ASCII
|
| 767 |
+
tag_count = len(tag_bytes)
|
| 768 |
+
else:
|
| 769 |
+
if not hasattr(tag_val, "dtype"):
|
| 770 |
+
tag_val = numpy.array([tag_val])
|
| 771 |
+
tag_type = get_tag_type_number(tag_val.dtype)
|
| 772 |
+
if tag_type is None:
|
| 773 |
+
logger.warning(
|
| 774 |
+
"imageio.freeimage warning: Could not "
|
| 775 |
+
"determine tag type of %r." % tag_name
|
| 776 |
+
)
|
| 777 |
+
continue
|
| 778 |
+
tag_bytes = tag_val.tobytes()
|
| 779 |
+
tag_count = tag_val.size
|
| 780 |
+
# Set properties
|
| 781 |
+
lib.FreeImage_SetTagKey(tag, tag_name.encode("utf-8"))
|
| 782 |
+
lib.FreeImage_SetTagType(tag, tag_type)
|
| 783 |
+
lib.FreeImage_SetTagCount(tag, tag_count)
|
| 784 |
+
lib.FreeImage_SetTagLength(tag, len(tag_bytes))
|
| 785 |
+
lib.FreeImage_SetTagValue(tag, tag_bytes)
|
| 786 |
+
# Store tag
|
| 787 |
+
tag_key = lib.FreeImage_GetTagKey(tag)
|
| 788 |
+
lib.FreeImage_SetMetadata(number, self._bitmap, tag_key, tag)
|
| 789 |
+
|
| 790 |
+
except Exception as err: # pragma: no cover
|
| 791 |
+
logger.warning(
|
| 792 |
+
"imagio.freeimage warning: Could not set tag "
|
| 793 |
+
"%r: %s, %s"
|
| 794 |
+
% (tag_name, self._fi._get_error_message(), str(err))
|
| 795 |
+
)
|
| 796 |
+
finally:
|
| 797 |
+
lib.FreeImage_DeleteTag(tag)
|
| 798 |
+
|
| 799 |
+
|
| 800 |
+
class FIBitmap(FIBaseBitmap):
|
| 801 |
+
"""Wrapper for the FI bitmap object."""
|
| 802 |
+
|
| 803 |
+
def allocate(self, array):
|
| 804 |
+
|
| 805 |
+
# Prepare array
|
| 806 |
+
assert isinstance(array, numpy.ndarray)
|
| 807 |
+
shape = array.shape
|
| 808 |
+
dtype = array.dtype
|
| 809 |
+
|
| 810 |
+
# Get shape and channel info
|
| 811 |
+
r, c = shape[:2]
|
| 812 |
+
if len(shape) == 2:
|
| 813 |
+
n_channels = 1
|
| 814 |
+
elif len(shape) == 3:
|
| 815 |
+
n_channels = shape[2]
|
| 816 |
+
else:
|
| 817 |
+
n_channels = shape[0]
|
| 818 |
+
|
| 819 |
+
# Get fi_type
|
| 820 |
+
try:
|
| 821 |
+
fi_type = FI_TYPES.fi_types[(dtype.type, n_channels)]
|
| 822 |
+
self._fi_type = fi_type
|
| 823 |
+
except KeyError:
|
| 824 |
+
raise ValueError("Cannot write arrays of given type and shape.")
|
| 825 |
+
|
| 826 |
+
# Allocate bitmap
|
| 827 |
+
with self._fi as lib:
|
| 828 |
+
bpp = 8 * dtype.itemsize * n_channels
|
| 829 |
+
bitmap = lib.FreeImage_AllocateT(fi_type, c, r, bpp, 0, 0, 0)
|
| 830 |
+
bitmap = ctypes.c_void_p(bitmap)
|
| 831 |
+
|
| 832 |
+
# Check and store
|
| 833 |
+
if not bitmap: # pragma: no cover
|
| 834 |
+
raise RuntimeError(
|
| 835 |
+
"Could not allocate bitmap for storage: %s"
|
| 836 |
+
% self._fi._get_error_message()
|
| 837 |
+
)
|
| 838 |
+
self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
|
| 839 |
+
|
| 840 |
+
def load_from_filename(self, filename=None):
|
| 841 |
+
if filename is None:
|
| 842 |
+
filename = self._filename
|
| 843 |
+
|
| 844 |
+
with self._fi as lib:
|
| 845 |
+
# Create bitmap
|
| 846 |
+
bitmap = lib.FreeImage_Load(self._ftype, efn(filename), self._flags)
|
| 847 |
+
bitmap = ctypes.c_void_p(bitmap)
|
| 848 |
+
|
| 849 |
+
# Check and store
|
| 850 |
+
if not bitmap: # pragma: no cover
|
| 851 |
+
raise ValueError(
|
| 852 |
+
'Could not load bitmap "%s": %s'
|
| 853 |
+
% (self._filename, self._fi._get_error_message())
|
| 854 |
+
)
|
| 855 |
+
self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
|
| 856 |
+
|
| 857 |
+
# def load_from_bytes(self, bb):
|
| 858 |
+
# with self._fi as lib:
|
| 859 |
+
# # Create bitmap
|
| 860 |
+
# fimemory = lib.FreeImage_OpenMemory(
|
| 861 |
+
# ctypes.c_char_p(bb), len(bb))
|
| 862 |
+
# bitmap = lib.FreeImage_LoadFromMemory(
|
| 863 |
+
# self._ftype, ctypes.c_void_p(fimemory), self._flags)
|
| 864 |
+
# bitmap = ctypes.c_void_p(bitmap)
|
| 865 |
+
# lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))
|
| 866 |
+
#
|
| 867 |
+
# # Check
|
| 868 |
+
# if not bitmap:
|
| 869 |
+
# raise ValueError('Could not load bitmap "%s": %s'
|
| 870 |
+
# % (self._filename, self._fi._get_error_message()))
|
| 871 |
+
# else:
|
| 872 |
+
# self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
|
| 873 |
+
|
| 874 |
+
def save_to_filename(self, filename=None):
|
| 875 |
+
if filename is None:
|
| 876 |
+
filename = self._filename
|
| 877 |
+
|
| 878 |
+
ftype = self._ftype
|
| 879 |
+
bitmap = self._bitmap
|
| 880 |
+
fi_type = self._fi_type # element type
|
| 881 |
+
|
| 882 |
+
with self._fi as lib:
|
| 883 |
+
# Check if can write
|
| 884 |
+
if fi_type == FI_TYPES.FIT_BITMAP:
|
| 885 |
+
can_write = lib.FreeImage_FIFSupportsExportBPP(
|
| 886 |
+
ftype, lib.FreeImage_GetBPP(bitmap)
|
| 887 |
+
)
|
| 888 |
+
else:
|
| 889 |
+
can_write = lib.FreeImage_FIFSupportsExportType(ftype, fi_type)
|
| 890 |
+
if not can_write:
|
| 891 |
+
raise TypeError("Cannot save image of this format to this file type")
|
| 892 |
+
|
| 893 |
+
# Save to file
|
| 894 |
+
res = lib.FreeImage_Save(ftype, bitmap, efn(filename), self._flags)
|
| 895 |
+
# Check
|
| 896 |
+
if res is None: # pragma: no cover, we do so many checks, this is rare
|
| 897 |
+
raise RuntimeError(
|
| 898 |
+
f"Could not save file `{self._filename}`: {self._fi._get_error_message()}"
|
| 899 |
+
)
|
| 900 |
+
|
| 901 |
+
# def save_to_bytes(self):
|
| 902 |
+
# ftype = self._ftype
|
| 903 |
+
# bitmap = self._bitmap
|
| 904 |
+
# fi_type = self._fi_type # element type
|
| 905 |
+
#
|
| 906 |
+
# with self._fi as lib:
|
| 907 |
+
# # Check if can write
|
| 908 |
+
# if fi_type == FI_TYPES.FIT_BITMAP:
|
| 909 |
+
# can_write = lib.FreeImage_FIFSupportsExportBPP(ftype,
|
| 910 |
+
# lib.FreeImage_GetBPP(bitmap))
|
| 911 |
+
# else:
|
| 912 |
+
# can_write = lib.FreeImage_FIFSupportsExportType(ftype, fi_type)
|
| 913 |
+
# if not can_write:
|
| 914 |
+
# raise TypeError('Cannot save image of this format '
|
| 915 |
+
# 'to this file type')
|
| 916 |
+
#
|
| 917 |
+
# # Extract the bytes
|
| 918 |
+
# fimemory = lib.FreeImage_OpenMemory(0, 0)
|
| 919 |
+
# res = lib.FreeImage_SaveToMemory(ftype, bitmap,
|
| 920 |
+
# ctypes.c_void_p(fimemory),
|
| 921 |
+
# self._flags)
|
| 922 |
+
# if res:
|
| 923 |
+
# N = lib.FreeImage_TellMemory(ctypes.c_void_p(fimemory))
|
| 924 |
+
# result = ctypes.create_string_buffer(N)
|
| 925 |
+
# lib.FreeImage_SeekMemory(ctypes.c_void_p(fimemory), 0)
|
| 926 |
+
# lib.FreeImage_ReadMemory(result, 1, N, ctypes.c_void_p(fimemory))
|
| 927 |
+
# result = result.raw
|
| 928 |
+
# lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))
|
| 929 |
+
#
|
| 930 |
+
# # Check
|
| 931 |
+
# if not res:
|
| 932 |
+
# raise RuntimeError('Could not save file "%s": %s'
|
| 933 |
+
# % (self._filename, self._fi._get_error_message()))
|
| 934 |
+
#
|
| 935 |
+
# # Done
|
| 936 |
+
# return result
|
| 937 |
+
|
| 938 |
+
def get_image_data(self):
|
| 939 |
+
dtype, shape, bpp = self._get_type_and_shape()
|
| 940 |
+
array = self._wrap_bitmap_bits_in_array(shape, dtype, False)
|
| 941 |
+
with self._fi as lib:
|
| 942 |
+
isle = lib.FreeImage_IsLittleEndian()
|
| 943 |
+
|
| 944 |
+
# swizzle the color components and flip the scanlines to go from
|
| 945 |
+
# FreeImage's BGR[A] and upside-down internal memory format to
|
| 946 |
+
# something more normal
|
| 947 |
+
def n(arr):
|
| 948 |
+
# return arr[..., ::-1].T # Does not work on numpypy yet
|
| 949 |
+
if arr.ndim == 1: # pragma: no cover
|
| 950 |
+
return arr[::-1].T
|
| 951 |
+
elif arr.ndim == 2: # Always the case here ...
|
| 952 |
+
return arr[:, ::-1].T
|
| 953 |
+
elif arr.ndim == 3: # pragma: no cover
|
| 954 |
+
return arr[:, :, ::-1].T
|
| 955 |
+
elif arr.ndim == 4: # pragma: no cover
|
| 956 |
+
return arr[:, :, :, ::-1].T
|
| 957 |
+
|
| 958 |
+
if len(shape) == 3 and isle and dtype.type == numpy.uint8:
|
| 959 |
+
b = n(array[0])
|
| 960 |
+
g = n(array[1])
|
| 961 |
+
r = n(array[2])
|
| 962 |
+
if shape[0] == 3:
|
| 963 |
+
return numpy.dstack((r, g, b))
|
| 964 |
+
elif shape[0] == 4:
|
| 965 |
+
a = n(array[3])
|
| 966 |
+
return numpy.dstack((r, g, b, a))
|
| 967 |
+
else: # pragma: no cover - we check this earlier
|
| 968 |
+
raise ValueError("Cannot handle images of shape %s" % shape)
|
| 969 |
+
|
| 970 |
+
# We need to copy because array does *not* own its memory
|
| 971 |
+
# after bitmap is freed.
|
| 972 |
+
a = n(array).copy()
|
| 973 |
+
return a
|
| 974 |
+
|
| 975 |
+
def set_image_data(self, array):
|
| 976 |
+
|
| 977 |
+
# Prepare array
|
| 978 |
+
assert isinstance(array, numpy.ndarray)
|
| 979 |
+
shape = array.shape
|
| 980 |
+
dtype = array.dtype
|
| 981 |
+
with self._fi as lib:
|
| 982 |
+
isle = lib.FreeImage_IsLittleEndian()
|
| 983 |
+
|
| 984 |
+
# Calculate shape and channels
|
| 985 |
+
r, c = shape[:2]
|
| 986 |
+
if len(shape) == 2:
|
| 987 |
+
n_channels = 1
|
| 988 |
+
w_shape = (c, r)
|
| 989 |
+
elif len(shape) == 3:
|
| 990 |
+
n_channels = shape[2]
|
| 991 |
+
w_shape = (n_channels, c, r)
|
| 992 |
+
else:
|
| 993 |
+
n_channels = shape[0]
|
| 994 |
+
|
| 995 |
+
def n(arr): # normalise to freeimage's in-memory format
|
| 996 |
+
return arr[::-1].T
|
| 997 |
+
|
| 998 |
+
wrapped_array = self._wrap_bitmap_bits_in_array(w_shape, dtype, True)
|
| 999 |
+
# swizzle the color components and flip the scanlines to go to
|
| 1000 |
+
# FreeImage's BGR[A] and upside-down internal memory format
|
| 1001 |
+
# The BGR[A] order is only used for 8bits per channel images
|
| 1002 |
+
# on little endian machines. For everything else RGB[A] is
|
| 1003 |
+
# used.
|
| 1004 |
+
if len(shape) == 3 and isle and dtype.type == numpy.uint8:
|
| 1005 |
+
R = array[:, :, 0]
|
| 1006 |
+
G = array[:, :, 1]
|
| 1007 |
+
B = array[:, :, 2]
|
| 1008 |
+
wrapped_array[0] = n(B)
|
| 1009 |
+
wrapped_array[1] = n(G)
|
| 1010 |
+
wrapped_array[2] = n(R)
|
| 1011 |
+
if shape[2] == 4:
|
| 1012 |
+
A = array[:, :, 3]
|
| 1013 |
+
wrapped_array[3] = n(A)
|
| 1014 |
+
else:
|
| 1015 |
+
wrapped_array[:] = n(array)
|
| 1016 |
+
if self._need_finish:
|
| 1017 |
+
self._finish_wrapped_array(wrapped_array)
|
| 1018 |
+
|
| 1019 |
+
if len(shape) == 2 and dtype.type == numpy.uint8:
|
| 1020 |
+
with self._fi as lib:
|
| 1021 |
+
palette = lib.FreeImage_GetPalette(self._bitmap)
|
| 1022 |
+
palette = ctypes.c_void_p(palette)
|
| 1023 |
+
if not palette:
|
| 1024 |
+
raise RuntimeError("Could not get image palette")
|
| 1025 |
+
try:
|
| 1026 |
+
palette_data = GREY_PALETTE.ctypes.data
|
| 1027 |
+
except Exception: # pragma: no cover - IS_PYPY
|
| 1028 |
+
palette_data = GREY_PALETTE.__array_interface__["data"][0]
|
| 1029 |
+
ctypes.memmove(palette, palette_data, 1024)
|
| 1030 |
+
|
| 1031 |
+
def _wrap_bitmap_bits_in_array(self, shape, dtype, save):
|
| 1032 |
+
"""Return an ndarray view on the data in a FreeImage bitmap. Only
|
| 1033 |
+
valid for as long as the bitmap is loaded (if single page) / locked
|
| 1034 |
+
in memory (if multipage). This is used in loading data, but
|
| 1035 |
+
also during saving, to prepare a strided numpy array buffer.
|
| 1036 |
+
|
| 1037 |
+
"""
|
| 1038 |
+
# Get bitmap info
|
| 1039 |
+
with self._fi as lib:
|
| 1040 |
+
pitch = lib.FreeImage_GetPitch(self._bitmap)
|
| 1041 |
+
bits = lib.FreeImage_GetBits(self._bitmap)
|
| 1042 |
+
|
| 1043 |
+
# Get more info
|
| 1044 |
+
height = shape[-1]
|
| 1045 |
+
byte_size = height * pitch
|
| 1046 |
+
itemsize = dtype.itemsize
|
| 1047 |
+
|
| 1048 |
+
# Get strides
|
| 1049 |
+
if len(shape) == 3:
|
| 1050 |
+
strides = (itemsize, shape[0] * itemsize, pitch)
|
| 1051 |
+
else:
|
| 1052 |
+
strides = (itemsize, pitch)
|
| 1053 |
+
|
| 1054 |
+
# Create numpy array and return
|
| 1055 |
+
data = (ctypes.c_char * byte_size).from_address(bits)
|
| 1056 |
+
try:
|
| 1057 |
+
self._need_finish = False
|
| 1058 |
+
if TEST_NUMPY_NO_STRIDES:
|
| 1059 |
+
raise NotImplementedError()
|
| 1060 |
+
return numpy.ndarray(shape, dtype=dtype, buffer=data, strides=strides)
|
| 1061 |
+
except NotImplementedError:
|
| 1062 |
+
# IS_PYPY - not very efficient. We create a C-contiguous
|
| 1063 |
+
# numpy array (because pypy does not support Fortran-order)
|
| 1064 |
+
# and shape it such that the rest of the code can remain.
|
| 1065 |
+
if save:
|
| 1066 |
+
self._need_finish = True # Flag to use _finish_wrapped_array
|
| 1067 |
+
return numpy.zeros(shape, dtype=dtype)
|
| 1068 |
+
else:
|
| 1069 |
+
bb = bytes(bytearray(data))
|
| 1070 |
+
array = numpy.frombuffer(bb, dtype=dtype).copy()
|
| 1071 |
+
# Deal with strides
|
| 1072 |
+
if len(shape) == 3:
|
| 1073 |
+
array.shape = shape[2], strides[-1] // shape[0], shape[0]
|
| 1074 |
+
array2 = array[: shape[2], : shape[1], : shape[0]]
|
| 1075 |
+
array = numpy.zeros(shape, dtype=array.dtype)
|
| 1076 |
+
for i in range(shape[0]):
|
| 1077 |
+
array[i] = array2[:, :, i].T
|
| 1078 |
+
else:
|
| 1079 |
+
array.shape = shape[1], strides[-1]
|
| 1080 |
+
array = array[: shape[1], : shape[0]].T
|
| 1081 |
+
return array
|
| 1082 |
+
|
| 1083 |
+
def _finish_wrapped_array(self, array): # IS_PYPY
|
| 1084 |
+
"""Hardcore way to inject numpy array in bitmap."""
|
| 1085 |
+
# Get bitmap info
|
| 1086 |
+
with self._fi as lib:
|
| 1087 |
+
pitch = lib.FreeImage_GetPitch(self._bitmap)
|
| 1088 |
+
bits = lib.FreeImage_GetBits(self._bitmap)
|
| 1089 |
+
bpp = lib.FreeImage_GetBPP(self._bitmap)
|
| 1090 |
+
# Get channels and realwidth
|
| 1091 |
+
nchannels = bpp // 8 // array.itemsize
|
| 1092 |
+
realwidth = pitch // nchannels
|
| 1093 |
+
# Apply padding for pitch if necessary
|
| 1094 |
+
extra = realwidth - array.shape[-2]
|
| 1095 |
+
assert 0 <= extra < 10
|
| 1096 |
+
# Make sort of Fortran, also take padding (i.e. pitch) into account
|
| 1097 |
+
newshape = array.shape[-1], realwidth, nchannels
|
| 1098 |
+
array2 = numpy.zeros(newshape, array.dtype)
|
| 1099 |
+
if nchannels == 1:
|
| 1100 |
+
array2[:, : array.shape[-2], 0] = array.T
|
| 1101 |
+
else:
|
| 1102 |
+
for i in range(nchannels):
|
| 1103 |
+
array2[:, : array.shape[-2], i] = array[i, :, :].T
|
| 1104 |
+
# copy data
|
| 1105 |
+
data_ptr = array2.__array_interface__["data"][0]
|
| 1106 |
+
ctypes.memmove(bits, data_ptr, array2.nbytes)
|
| 1107 |
+
del array2
|
| 1108 |
+
|
| 1109 |
+
def _get_type_and_shape(self):
|
| 1110 |
+
bitmap = self._bitmap
|
| 1111 |
+
|
| 1112 |
+
# Get info on bitmap
|
| 1113 |
+
with self._fi as lib:
|
| 1114 |
+
w = lib.FreeImage_GetWidth(bitmap)
|
| 1115 |
+
h = lib.FreeImage_GetHeight(bitmap)
|
| 1116 |
+
self._fi_type = fi_type = lib.FreeImage_GetImageType(bitmap)
|
| 1117 |
+
if not fi_type:
|
| 1118 |
+
raise ValueError("Unknown image pixel type")
|
| 1119 |
+
|
| 1120 |
+
# Determine required props for numpy array
|
| 1121 |
+
bpp = None
|
| 1122 |
+
dtype = FI_TYPES.dtypes[fi_type]
|
| 1123 |
+
|
| 1124 |
+
if fi_type == FI_TYPES.FIT_BITMAP:
|
| 1125 |
+
with self._fi as lib:
|
| 1126 |
+
bpp = lib.FreeImage_GetBPP(bitmap)
|
| 1127 |
+
has_pallette = lib.FreeImage_GetColorsUsed(bitmap)
|
| 1128 |
+
if has_pallette:
|
| 1129 |
+
# Examine the palette. If it is grayscale, we return as such
|
| 1130 |
+
if has_pallette == 256:
|
| 1131 |
+
palette = lib.FreeImage_GetPalette(bitmap)
|
| 1132 |
+
palette = ctypes.c_void_p(palette)
|
| 1133 |
+
p = (ctypes.c_uint8 * (256 * 4)).from_address(palette.value)
|
| 1134 |
+
p = numpy.frombuffer(p, numpy.uint32).copy()
|
| 1135 |
+
if (GREY_PALETTE == p).all():
|
| 1136 |
+
extra_dims = []
|
| 1137 |
+
return numpy.dtype(dtype), extra_dims + [w, h], bpp
|
| 1138 |
+
# Convert bitmap and call this method again
|
| 1139 |
+
newbitmap = lib.FreeImage_ConvertTo32Bits(bitmap)
|
| 1140 |
+
newbitmap = ctypes.c_void_p(newbitmap)
|
| 1141 |
+
self._set_bitmap(newbitmap)
|
| 1142 |
+
return self._get_type_and_shape()
|
| 1143 |
+
elif bpp == 8:
|
| 1144 |
+
extra_dims = []
|
| 1145 |
+
elif bpp == 24:
|
| 1146 |
+
extra_dims = [3]
|
| 1147 |
+
elif bpp == 32:
|
| 1148 |
+
extra_dims = [4]
|
| 1149 |
+
else: # pragma: no cover
|
| 1150 |
+
# raise ValueError('Cannot convert %d BPP bitmap' % bpp)
|
| 1151 |
+
# Convert bitmap and call this method again
|
| 1152 |
+
newbitmap = lib.FreeImage_ConvertTo32Bits(bitmap)
|
| 1153 |
+
newbitmap = ctypes.c_void_p(newbitmap)
|
| 1154 |
+
self._set_bitmap(newbitmap)
|
| 1155 |
+
return self._get_type_and_shape()
|
| 1156 |
+
else:
|
| 1157 |
+
extra_dims = FI_TYPES.extra_dims[fi_type]
|
| 1158 |
+
|
| 1159 |
+
# Return dtype and shape
|
| 1160 |
+
return numpy.dtype(dtype), extra_dims + [w, h], bpp
|
| 1161 |
+
|
| 1162 |
+
def quantize(self, quantizer=0, palettesize=256):
|
| 1163 |
+
"""Quantize the bitmap to make it 8-bit (paletted). Returns a new
|
| 1164 |
+
FIBitmap object.
|
| 1165 |
+
Only for 24 bit images.
|
| 1166 |
+
"""
|
| 1167 |
+
with self._fi as lib:
|
| 1168 |
+
# New bitmap
|
| 1169 |
+
bitmap = lib.FreeImage_ColorQuantizeEx(
|
| 1170 |
+
self._bitmap, quantizer, palettesize, 0, None
|
| 1171 |
+
)
|
| 1172 |
+
bitmap = ctypes.c_void_p(bitmap)
|
| 1173 |
+
|
| 1174 |
+
# Check and return
|
| 1175 |
+
if not bitmap:
|
| 1176 |
+
raise ValueError(
|
| 1177 |
+
'Could not quantize bitmap "%s": %s'
|
| 1178 |
+
% (self._filename, self._fi._get_error_message())
|
| 1179 |
+
)
|
| 1180 |
+
|
| 1181 |
+
new = FIBitmap(self._fi, self._filename, self._ftype, self._flags)
|
| 1182 |
+
new._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
|
| 1183 |
+
new._fi_type = self._fi_type
|
| 1184 |
+
return new
|
| 1185 |
+
|
| 1186 |
+
|
| 1187 |
+
# def convert_to_32bit(self):
|
| 1188 |
+
# """ Convert to 32bit image.
|
| 1189 |
+
# """
|
| 1190 |
+
# with self._fi as lib:
|
| 1191 |
+
# # New bitmap
|
| 1192 |
+
# bitmap = lib.FreeImage_ConvertTo32Bits(self._bitmap)
|
| 1193 |
+
# bitmap = ctypes.c_void_p(bitmap)
|
| 1194 |
+
#
|
| 1195 |
+
# # Check and return
|
| 1196 |
+
# if not bitmap:
|
| 1197 |
+
# raise ValueError('Could not convert bitmap to 32bit "%s": %s' %
|
| 1198 |
+
# (self._filename,
|
| 1199 |
+
# self._fi._get_error_message()))
|
| 1200 |
+
# else:
|
| 1201 |
+
# new = FIBitmap(self._fi, self._filename, self._ftype,
|
| 1202 |
+
# self._flags)
|
| 1203 |
+
# new._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
|
| 1204 |
+
# new._fi_type = self._fi_type
|
| 1205 |
+
# return new
|
| 1206 |
+
|
| 1207 |
+
|
| 1208 |
+
class FIMultipageBitmap(FIBaseBitmap):
|
| 1209 |
+
"""Wrapper for the multipage FI bitmap object."""
|
| 1210 |
+
|
| 1211 |
+
def load_from_filename(self, filename=None):
|
| 1212 |
+
if filename is None: # pragma: no cover
|
| 1213 |
+
filename = self._filename
|
| 1214 |
+
|
| 1215 |
+
# Prepare
|
| 1216 |
+
create_new = False
|
| 1217 |
+
read_only = True
|
| 1218 |
+
keep_cache_in_memory = False
|
| 1219 |
+
|
| 1220 |
+
# Try opening
|
| 1221 |
+
with self._fi as lib:
|
| 1222 |
+
|
| 1223 |
+
# Create bitmap
|
| 1224 |
+
multibitmap = lib.FreeImage_OpenMultiBitmap(
|
| 1225 |
+
self._ftype,
|
| 1226 |
+
efn(filename),
|
| 1227 |
+
create_new,
|
| 1228 |
+
read_only,
|
| 1229 |
+
keep_cache_in_memory,
|
| 1230 |
+
self._flags,
|
| 1231 |
+
)
|
| 1232 |
+
multibitmap = ctypes.c_void_p(multibitmap)
|
| 1233 |
+
|
| 1234 |
+
# Check
|
| 1235 |
+
if not multibitmap: # pragma: no cover
|
| 1236 |
+
err = self._fi._get_error_message()
|
| 1237 |
+
raise ValueError(
|
| 1238 |
+
'Could not open file "%s" as multi-image: %s'
|
| 1239 |
+
% (self._filename, err)
|
| 1240 |
+
)
|
| 1241 |
+
self._set_bitmap(multibitmap, (lib.FreeImage_CloseMultiBitmap, multibitmap))
|
| 1242 |
+
|
| 1243 |
+
# def load_from_bytes(self, bb):
|
| 1244 |
+
# with self._fi as lib:
|
| 1245 |
+
# # Create bitmap
|
| 1246 |
+
# fimemory = lib.FreeImage_OpenMemory(
|
| 1247 |
+
# ctypes.c_char_p(bb), len(bb))
|
| 1248 |
+
# multibitmap = lib.FreeImage_LoadMultiBitmapFromMemory(
|
| 1249 |
+
# self._ftype, ctypes.c_void_p(fimemory), self._flags)
|
| 1250 |
+
# multibitmap = ctypes.c_void_p(multibitmap)
|
| 1251 |
+
# #lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))
|
| 1252 |
+
# self._mem = fimemory
|
| 1253 |
+
# self._bytes = bb
|
| 1254 |
+
# # Check
|
| 1255 |
+
# if not multibitmap:
|
| 1256 |
+
# raise ValueError('Could not load multibitmap "%s": %s'
|
| 1257 |
+
# % (self._filename, self._fi._get_error_message()))
|
| 1258 |
+
# else:
|
| 1259 |
+
# self._set_bitmap(multibitmap,
|
| 1260 |
+
# (lib.FreeImage_CloseMultiBitmap, multibitmap))
|
| 1261 |
+
|
| 1262 |
+
def save_to_filename(self, filename=None):
|
| 1263 |
+
if filename is None: # pragma: no cover
|
| 1264 |
+
filename = self._filename
|
| 1265 |
+
|
| 1266 |
+
# Prepare
|
| 1267 |
+
create_new = True
|
| 1268 |
+
read_only = False
|
| 1269 |
+
keep_cache_in_memory = False
|
| 1270 |
+
|
| 1271 |
+
# Open the file
|
| 1272 |
+
# todo: Set flags at close func
|
| 1273 |
+
with self._fi as lib:
|
| 1274 |
+
multibitmap = lib.FreeImage_OpenMultiBitmap(
|
| 1275 |
+
self._ftype,
|
| 1276 |
+
efn(filename),
|
| 1277 |
+
create_new,
|
| 1278 |
+
read_only,
|
| 1279 |
+
keep_cache_in_memory,
|
| 1280 |
+
0,
|
| 1281 |
+
)
|
| 1282 |
+
multibitmap = ctypes.c_void_p(multibitmap)
|
| 1283 |
+
|
| 1284 |
+
# Check
|
| 1285 |
+
if not multibitmap: # pragma: no cover
|
| 1286 |
+
msg = 'Could not open file "%s" for writing multi-image: %s' % (
|
| 1287 |
+
self._filename,
|
| 1288 |
+
self._fi._get_error_message(),
|
| 1289 |
+
)
|
| 1290 |
+
raise ValueError(msg)
|
| 1291 |
+
self._set_bitmap(multibitmap, (lib.FreeImage_CloseMultiBitmap, multibitmap))
|
| 1292 |
+
|
| 1293 |
+
def __len__(self):
|
| 1294 |
+
with self._fi as lib:
|
| 1295 |
+
return lib.FreeImage_GetPageCount(self._bitmap)
|
| 1296 |
+
|
| 1297 |
+
def get_page(self, index):
|
| 1298 |
+
"""Return the sub-bitmap for the given page index.
|
| 1299 |
+
Please close the returned bitmap when done.
|
| 1300 |
+
"""
|
| 1301 |
+
with self._fi as lib:
|
| 1302 |
+
|
| 1303 |
+
# Create low-level bitmap in freeimage
|
| 1304 |
+
bitmap = lib.FreeImage_LockPage(self._bitmap, index)
|
| 1305 |
+
bitmap = ctypes.c_void_p(bitmap)
|
| 1306 |
+
if not bitmap: # pragma: no cover
|
| 1307 |
+
raise ValueError(
|
| 1308 |
+
"Could not open sub-image %i in %r: %s"
|
| 1309 |
+
% (index, self._filename, self._fi._get_error_message())
|
| 1310 |
+
)
|
| 1311 |
+
|
| 1312 |
+
# Get bitmap object to wrap this bitmap
|
| 1313 |
+
bm = FIBitmap(self._fi, self._filename, self._ftype, self._flags)
|
| 1314 |
+
bm._set_bitmap(
|
| 1315 |
+
bitmap, (lib.FreeImage_UnlockPage, self._bitmap, bitmap, False)
|
| 1316 |
+
)
|
| 1317 |
+
return bm
|
| 1318 |
+
|
| 1319 |
+
def append_bitmap(self, bitmap):
|
| 1320 |
+
"""Add a sub-bitmap to the multi-page bitmap."""
|
| 1321 |
+
with self._fi as lib:
|
| 1322 |
+
# no return value
|
| 1323 |
+
lib.FreeImage_AppendPage(self._bitmap, bitmap._bitmap)
|
| 1324 |
+
|
| 1325 |
+
|
| 1326 |
+
# Create instance
|
| 1327 |
+
fi = Freeimage()
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/_swf.py
ADDED
|
@@ -0,0 +1,901 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
# This code was taken from https://github.com/almarklein/visvis/blob/master/vvmovie/images2swf.py
|
| 4 |
+
|
| 5 |
+
# styletest: ignore E261
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
Provides a function (write_swf) to store a series of numpy arrays in an
|
| 9 |
+
SWF movie, that can be played on a wide range of OS's.
|
| 10 |
+
|
| 11 |
+
In desperation of wanting to share animated images, and then lacking a good
|
| 12 |
+
writer for animated gif or .avi, I decided to look into SWF. This format
|
| 13 |
+
is very well documented.
|
| 14 |
+
|
| 15 |
+
This is a pure python module to create an SWF file that shows a series
|
| 16 |
+
of images. The images are stored using the DEFLATE algorithm (same as
|
| 17 |
+
PNG and ZIP and which is included in the standard Python distribution).
|
| 18 |
+
As this compression algorithm is much more effective than that used in
|
| 19 |
+
GIF images, we obtain better quality (24 bit colors + alpha channel)
|
| 20 |
+
while still producesing smaller files (a test showed ~75%). Although
|
| 21 |
+
SWF also allows for JPEG compression, doing so would probably require
|
| 22 |
+
a third party library for the JPEG encoding/decoding, we could
|
| 23 |
+
perhaps do this via Pillow or freeimage.
|
| 24 |
+
|
| 25 |
+
sources and tools:
|
| 26 |
+
|
| 27 |
+
- SWF on wikipedia
|
| 28 |
+
- Adobes "SWF File Format Specification" version 10
|
| 29 |
+
(http://www.adobe.com/devnet/swf/pdf/swf_file_format_spec_v10.pdf)
|
| 30 |
+
- swftools (swfdump in specific) for debugging
|
| 31 |
+
- iwisoft swf2avi can be used to convert swf to avi/mpg/flv with really
|
| 32 |
+
good quality, while file size is reduced with factors 20-100.
|
| 33 |
+
A good program in my opinion. The free version has the limitation
|
| 34 |
+
of a watermark in the upper left corner.
|
| 35 |
+
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
import os
|
| 39 |
+
import zlib
|
| 40 |
+
import time # noqa
|
| 41 |
+
import logging
|
| 42 |
+
|
| 43 |
+
import numpy as np
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
logger = logging.getLogger(__name__)
|
| 47 |
+
|
| 48 |
+
# todo: use Pillow to support reading JPEG images from SWF?
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# Base functions and classes
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class BitArray:
|
| 55 |
+
"""Dynamic array of bits that automatically resizes
|
| 56 |
+
with factors of two.
|
| 57 |
+
Append bits using .append() or +=
|
| 58 |
+
You can reverse bits using .reverse()
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
def __init__(self, initvalue=None):
|
| 62 |
+
self.data = np.zeros((16,), dtype=np.uint8)
|
| 63 |
+
self._len = 0
|
| 64 |
+
if initvalue is not None:
|
| 65 |
+
self.append(initvalue)
|
| 66 |
+
|
| 67 |
+
def __len__(self):
|
| 68 |
+
return self._len # self.data.shape[0]
|
| 69 |
+
|
| 70 |
+
def __repr__(self):
|
| 71 |
+
return self.data[: self._len].tobytes().decode("ascii")
|
| 72 |
+
|
| 73 |
+
def _checkSize(self):
|
| 74 |
+
# check length... grow if necessary
|
| 75 |
+
arraylen = self.data.shape[0]
|
| 76 |
+
if self._len >= arraylen:
|
| 77 |
+
tmp = np.zeros((arraylen * 2,), dtype=np.uint8)
|
| 78 |
+
tmp[: self._len] = self.data[: self._len]
|
| 79 |
+
self.data = tmp
|
| 80 |
+
|
| 81 |
+
def __add__(self, value):
|
| 82 |
+
self.append(value)
|
| 83 |
+
return self
|
| 84 |
+
|
| 85 |
+
def append(self, bits):
|
| 86 |
+
|
| 87 |
+
# check input
|
| 88 |
+
if isinstance(bits, BitArray):
|
| 89 |
+
bits = str(bits)
|
| 90 |
+
if isinstance(bits, int): # pragma: no cover - we dont use it
|
| 91 |
+
bits = str(bits)
|
| 92 |
+
if not isinstance(bits, str): # pragma: no cover
|
| 93 |
+
raise ValueError("Append bits as strings or integers!")
|
| 94 |
+
|
| 95 |
+
# add bits
|
| 96 |
+
for bit in bits:
|
| 97 |
+
self.data[self._len] = ord(bit)
|
| 98 |
+
self._len += 1
|
| 99 |
+
self._checkSize()
|
| 100 |
+
|
| 101 |
+
def reverse(self):
|
| 102 |
+
"""In-place reverse."""
|
| 103 |
+
tmp = self.data[: self._len].copy()
|
| 104 |
+
self.data[: self._len] = tmp[::-1]
|
| 105 |
+
|
| 106 |
+
def tobytes(self):
|
| 107 |
+
"""Convert to bytes. If necessary,
|
| 108 |
+
zeros are padded to the end (right side).
|
| 109 |
+
"""
|
| 110 |
+
bits = str(self)
|
| 111 |
+
|
| 112 |
+
# determine number of bytes
|
| 113 |
+
nbytes = 0
|
| 114 |
+
while nbytes * 8 < len(bits):
|
| 115 |
+
nbytes += 1
|
| 116 |
+
# pad
|
| 117 |
+
bits = bits.ljust(nbytes * 8, "0")
|
| 118 |
+
|
| 119 |
+
# go from bits to bytes
|
| 120 |
+
bb = bytes()
|
| 121 |
+
for i in range(nbytes):
|
| 122 |
+
tmp = int(bits[i * 8 : (i + 1) * 8], 2)
|
| 123 |
+
bb += int2uint8(tmp)
|
| 124 |
+
|
| 125 |
+
# done
|
| 126 |
+
return bb
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def int2uint32(i):
|
| 130 |
+
return int(i).to_bytes(4, "little")
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def int2uint16(i):
|
| 134 |
+
return int(i).to_bytes(2, "little")
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def int2uint8(i):
|
| 138 |
+
return int(i).to_bytes(1, "little")
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def int2bits(i, n=None):
|
| 142 |
+
"""convert int to a string of bits (0's and 1's in a string),
|
| 143 |
+
pad to n elements. Convert back using int(ss,2)."""
|
| 144 |
+
ii = i
|
| 145 |
+
|
| 146 |
+
# make bits
|
| 147 |
+
bb = BitArray()
|
| 148 |
+
while ii > 0:
|
| 149 |
+
bb += str(ii % 2)
|
| 150 |
+
ii = ii >> 1
|
| 151 |
+
bb.reverse()
|
| 152 |
+
|
| 153 |
+
# justify
|
| 154 |
+
if n is not None:
|
| 155 |
+
if len(bb) > n: # pragma: no cover
|
| 156 |
+
raise ValueError("int2bits fail: len larger than padlength.")
|
| 157 |
+
bb = str(bb).rjust(n, "0")
|
| 158 |
+
|
| 159 |
+
# done
|
| 160 |
+
return BitArray(bb)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def bits2int(bb, n=8):
|
| 164 |
+
# Init
|
| 165 |
+
value = ""
|
| 166 |
+
|
| 167 |
+
# Get value in bits
|
| 168 |
+
for i in range(len(bb)):
|
| 169 |
+
b = bb[i : i + 1]
|
| 170 |
+
tmp = bin(ord(b))[2:]
|
| 171 |
+
# value += tmp.rjust(8,'0')
|
| 172 |
+
value = tmp.rjust(8, "0") + value
|
| 173 |
+
|
| 174 |
+
# Make decimal
|
| 175 |
+
return int(value[:n], 2)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def get_type_and_len(bb):
|
| 179 |
+
"""bb should be 6 bytes at least
|
| 180 |
+
Return (type, length, length_of_full_tag)
|
| 181 |
+
"""
|
| 182 |
+
# Init
|
| 183 |
+
value = ""
|
| 184 |
+
|
| 185 |
+
# Get first 16 bits
|
| 186 |
+
for i in range(2):
|
| 187 |
+
b = bb[i : i + 1]
|
| 188 |
+
tmp = bin(ord(b))[2:]
|
| 189 |
+
# value += tmp.rjust(8,'0')
|
| 190 |
+
value = tmp.rjust(8, "0") + value
|
| 191 |
+
|
| 192 |
+
# Get type and length
|
| 193 |
+
type = int(value[:10], 2)
|
| 194 |
+
L = int(value[10:], 2)
|
| 195 |
+
L2 = L + 2
|
| 196 |
+
|
| 197 |
+
# Long tag header?
|
| 198 |
+
if L == 63: # '111111'
|
| 199 |
+
value = ""
|
| 200 |
+
for i in range(2, 6):
|
| 201 |
+
b = bb[i : i + 1] # becomes a single-byte bytes()
|
| 202 |
+
tmp = bin(ord(b))[2:]
|
| 203 |
+
# value += tmp.rjust(8,'0')
|
| 204 |
+
value = tmp.rjust(8, "0") + value
|
| 205 |
+
L = int(value, 2)
|
| 206 |
+
L2 = L + 6
|
| 207 |
+
|
| 208 |
+
# Done
|
| 209 |
+
return type, L, L2
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def signedint2bits(i, n=None):
|
| 213 |
+
"""convert signed int to a string of bits (0's and 1's in a string),
|
| 214 |
+
pad to n elements. Negative numbers are stored in 2's complement bit
|
| 215 |
+
patterns, thus positive numbers always start with a 0.
|
| 216 |
+
"""
|
| 217 |
+
|
| 218 |
+
# negative number?
|
| 219 |
+
ii = i
|
| 220 |
+
if i < 0:
|
| 221 |
+
# A negative number, -n, is represented as the bitwise opposite of
|
| 222 |
+
ii = abs(ii) - 1 # the positive-zero number n-1.
|
| 223 |
+
|
| 224 |
+
# make bits
|
| 225 |
+
bb = BitArray()
|
| 226 |
+
while ii > 0:
|
| 227 |
+
bb += str(ii % 2)
|
| 228 |
+
ii = ii >> 1
|
| 229 |
+
bb.reverse()
|
| 230 |
+
|
| 231 |
+
# justify
|
| 232 |
+
bb = "0" + str(bb) # always need the sign bit in front
|
| 233 |
+
if n is not None:
|
| 234 |
+
if len(bb) > n: # pragma: no cover
|
| 235 |
+
raise ValueError("signedint2bits fail: len larger than padlength.")
|
| 236 |
+
bb = bb.rjust(n, "0")
|
| 237 |
+
|
| 238 |
+
# was it negative? (then opposite bits)
|
| 239 |
+
if i < 0:
|
| 240 |
+
bb = bb.replace("0", "x").replace("1", "0").replace("x", "1")
|
| 241 |
+
|
| 242 |
+
# done
|
| 243 |
+
return BitArray(bb)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def twits2bits(arr):
|
| 247 |
+
"""Given a few (signed) numbers, store them
|
| 248 |
+
as compactly as possible in the wat specifief by the swf format.
|
| 249 |
+
The numbers are multiplied by 20, assuming they
|
| 250 |
+
are twits.
|
| 251 |
+
Can be used to make the RECT record.
|
| 252 |
+
"""
|
| 253 |
+
|
| 254 |
+
# first determine length using non justified bit strings
|
| 255 |
+
maxlen = 1
|
| 256 |
+
for i in arr:
|
| 257 |
+
tmp = len(signedint2bits(i * 20))
|
| 258 |
+
if tmp > maxlen:
|
| 259 |
+
maxlen = tmp
|
| 260 |
+
|
| 261 |
+
# build array
|
| 262 |
+
bits = int2bits(maxlen, 5)
|
| 263 |
+
for i in arr:
|
| 264 |
+
bits += signedint2bits(i * 20, maxlen)
|
| 265 |
+
|
| 266 |
+
return bits
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def floats2bits(arr):
|
| 270 |
+
"""Given a few (signed) numbers, convert them to bits,
|
| 271 |
+
stored as FB (float bit values). We always use 16.16.
|
| 272 |
+
Negative numbers are not (yet) possible, because I don't
|
| 273 |
+
know how the're implemented (ambiguity).
|
| 274 |
+
"""
|
| 275 |
+
bits = int2bits(31, 5) # 32 does not fit in 5 bits!
|
| 276 |
+
for i in arr:
|
| 277 |
+
if i < 0: # pragma: no cover
|
| 278 |
+
raise ValueError("Dit not implement negative floats!")
|
| 279 |
+
i1 = int(i)
|
| 280 |
+
i2 = i - i1
|
| 281 |
+
bits += int2bits(i1, 15)
|
| 282 |
+
bits += int2bits(i2 * 2**16, 16)
|
| 283 |
+
return bits
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
# Base Tag
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
class Tag:
|
| 290 |
+
def __init__(self):
|
| 291 |
+
self.bytes = bytes()
|
| 292 |
+
self.tagtype = -1
|
| 293 |
+
|
| 294 |
+
def process_tag(self):
|
| 295 |
+
"""Implement this to create the tag."""
|
| 296 |
+
raise NotImplementedError()
|
| 297 |
+
|
| 298 |
+
def get_tag(self):
|
| 299 |
+
"""Calls processTag and attaches the header."""
|
| 300 |
+
self.process_tag()
|
| 301 |
+
|
| 302 |
+
# tag to binary
|
| 303 |
+
bits = int2bits(self.tagtype, 10)
|
| 304 |
+
|
| 305 |
+
# complete header uint16 thing
|
| 306 |
+
bits += "1" * 6 # = 63 = 0x3f
|
| 307 |
+
# make uint16
|
| 308 |
+
bb = int2uint16(int(str(bits), 2))
|
| 309 |
+
|
| 310 |
+
# now add 32bit length descriptor
|
| 311 |
+
bb += int2uint32(len(self.bytes))
|
| 312 |
+
|
| 313 |
+
# done, attach and return
|
| 314 |
+
bb += self.bytes
|
| 315 |
+
return bb
|
| 316 |
+
|
| 317 |
+
def make_rect_record(self, xmin, xmax, ymin, ymax):
|
| 318 |
+
"""Simply uses makeCompactArray to produce
|
| 319 |
+
a RECT Record."""
|
| 320 |
+
return twits2bits([xmin, xmax, ymin, ymax])
|
| 321 |
+
|
| 322 |
+
def make_matrix_record(self, scale_xy=None, rot_xy=None, trans_xy=None):
|
| 323 |
+
|
| 324 |
+
# empty matrix?
|
| 325 |
+
if scale_xy is None and rot_xy is None and trans_xy is None:
|
| 326 |
+
return "0" * 8
|
| 327 |
+
|
| 328 |
+
# init
|
| 329 |
+
bits = BitArray()
|
| 330 |
+
|
| 331 |
+
# scale
|
| 332 |
+
if scale_xy:
|
| 333 |
+
bits += "1"
|
| 334 |
+
bits += floats2bits([scale_xy[0], scale_xy[1]])
|
| 335 |
+
else:
|
| 336 |
+
bits += "0"
|
| 337 |
+
|
| 338 |
+
# rotation
|
| 339 |
+
if rot_xy:
|
| 340 |
+
bits += "1"
|
| 341 |
+
bits += floats2bits([rot_xy[0], rot_xy[1]])
|
| 342 |
+
else:
|
| 343 |
+
bits += "0"
|
| 344 |
+
|
| 345 |
+
# translation (no flag here)
|
| 346 |
+
if trans_xy:
|
| 347 |
+
bits += twits2bits([trans_xy[0], trans_xy[1]])
|
| 348 |
+
else:
|
| 349 |
+
bits += twits2bits([0, 0])
|
| 350 |
+
|
| 351 |
+
# done
|
| 352 |
+
return bits
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
# Control tags
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
class ControlTag(Tag):
|
| 359 |
+
def __init__(self):
|
| 360 |
+
Tag.__init__(self)
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
class FileAttributesTag(ControlTag):
|
| 364 |
+
def __init__(self):
|
| 365 |
+
ControlTag.__init__(self)
|
| 366 |
+
self.tagtype = 69
|
| 367 |
+
|
| 368 |
+
def process_tag(self):
|
| 369 |
+
self.bytes = "\x00".encode("ascii") * (1 + 3)
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
class ShowFrameTag(ControlTag):
|
| 373 |
+
def __init__(self):
|
| 374 |
+
ControlTag.__init__(self)
|
| 375 |
+
self.tagtype = 1
|
| 376 |
+
|
| 377 |
+
def process_tag(self):
|
| 378 |
+
self.bytes = bytes()
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
class SetBackgroundTag(ControlTag):
|
| 382 |
+
"""Set the color in 0-255, or 0-1 (if floats given)."""
|
| 383 |
+
|
| 384 |
+
def __init__(self, *rgb):
|
| 385 |
+
self.tagtype = 9
|
| 386 |
+
if len(rgb) == 1:
|
| 387 |
+
rgb = rgb[0]
|
| 388 |
+
self.rgb = rgb
|
| 389 |
+
|
| 390 |
+
def process_tag(self):
|
| 391 |
+
bb = bytes()
|
| 392 |
+
for i in range(3):
|
| 393 |
+
clr = self.rgb[i]
|
| 394 |
+
if isinstance(clr, float): # pragma: no cover - not used
|
| 395 |
+
clr = clr * 255
|
| 396 |
+
bb += int2uint8(clr)
|
| 397 |
+
self.bytes = bb
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
class DoActionTag(Tag):
|
| 401 |
+
def __init__(self, action="stop"):
|
| 402 |
+
Tag.__init__(self)
|
| 403 |
+
self.tagtype = 12
|
| 404 |
+
self.actions = [action]
|
| 405 |
+
|
| 406 |
+
def append(self, action): # pragma: no cover - not used
|
| 407 |
+
self.actions.append(action)
|
| 408 |
+
|
| 409 |
+
def process_tag(self):
|
| 410 |
+
bb = bytes()
|
| 411 |
+
|
| 412 |
+
for action in self.actions:
|
| 413 |
+
action = action.lower()
|
| 414 |
+
if action == "stop":
|
| 415 |
+
bb += "\x07".encode("ascii")
|
| 416 |
+
elif action == "play": # pragma: no cover - not used
|
| 417 |
+
bb += "\x06".encode("ascii")
|
| 418 |
+
else: # pragma: no cover
|
| 419 |
+
logger.warning("unkown action: %s" % action)
|
| 420 |
+
|
| 421 |
+
bb += int2uint8(0)
|
| 422 |
+
self.bytes = bb
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
# Definition tags
|
| 426 |
+
class DefinitionTag(Tag):
|
| 427 |
+
counter = 0 # to give automatically id's
|
| 428 |
+
|
| 429 |
+
def __init__(self):
|
| 430 |
+
Tag.__init__(self)
|
| 431 |
+
DefinitionTag.counter += 1
|
| 432 |
+
self.id = DefinitionTag.counter # id in dictionary
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
class BitmapTag(DefinitionTag):
|
| 436 |
+
def __init__(self, im):
|
| 437 |
+
DefinitionTag.__init__(self)
|
| 438 |
+
self.tagtype = 36 # DefineBitsLossless2
|
| 439 |
+
|
| 440 |
+
# convert image (note that format is ARGB)
|
| 441 |
+
# even a grayscale image is stored in ARGB, nevertheless,
|
| 442 |
+
# the fabilous deflate compression will make it that not much
|
| 443 |
+
# more data is required for storing (25% or so, and less than 10%
|
| 444 |
+
# when storing RGB as ARGB).
|
| 445 |
+
|
| 446 |
+
if len(im.shape) == 3:
|
| 447 |
+
if im.shape[2] in [3, 4]:
|
| 448 |
+
tmp = np.ones((im.shape[0], im.shape[1], 4), dtype=np.uint8) * 255
|
| 449 |
+
for i in range(3):
|
| 450 |
+
tmp[:, :, i + 1] = im[:, :, i]
|
| 451 |
+
if im.shape[2] == 4:
|
| 452 |
+
tmp[:, :, 0] = im[:, :, 3] # swap channel where alpha is
|
| 453 |
+
else: # pragma: no cover
|
| 454 |
+
raise ValueError("Invalid shape to be an image.")
|
| 455 |
+
|
| 456 |
+
elif len(im.shape) == 2:
|
| 457 |
+
tmp = np.ones((im.shape[0], im.shape[1], 4), dtype=np.uint8) * 255
|
| 458 |
+
for i in range(3):
|
| 459 |
+
tmp[:, :, i + 1] = im[:, :]
|
| 460 |
+
else: # pragma: no cover
|
| 461 |
+
raise ValueError("Invalid shape to be an image.")
|
| 462 |
+
|
| 463 |
+
# we changed the image to uint8 4 channels.
|
| 464 |
+
# now compress!
|
| 465 |
+
self._data = zlib.compress(tmp.tobytes(), zlib.DEFLATED)
|
| 466 |
+
self.imshape = im.shape
|
| 467 |
+
|
| 468 |
+
def process_tag(self):
|
| 469 |
+
|
| 470 |
+
# build tag
|
| 471 |
+
bb = bytes()
|
| 472 |
+
bb += int2uint16(self.id) # CharacterID
|
| 473 |
+
bb += int2uint8(5) # BitmapFormat
|
| 474 |
+
bb += int2uint16(self.imshape[1]) # BitmapWidth
|
| 475 |
+
bb += int2uint16(self.imshape[0]) # BitmapHeight
|
| 476 |
+
bb += self._data # ZlibBitmapData
|
| 477 |
+
|
| 478 |
+
self.bytes = bb
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
class PlaceObjectTag(ControlTag):
|
| 482 |
+
def __init__(self, depth, idToPlace=None, xy=(0, 0), move=False):
|
| 483 |
+
ControlTag.__init__(self)
|
| 484 |
+
self.tagtype = 26
|
| 485 |
+
self.depth = depth
|
| 486 |
+
self.idToPlace = idToPlace
|
| 487 |
+
self.xy = xy
|
| 488 |
+
self.move = move
|
| 489 |
+
|
| 490 |
+
def process_tag(self):
|
| 491 |
+
# retrieve stuff
|
| 492 |
+
depth = self.depth
|
| 493 |
+
xy = self.xy
|
| 494 |
+
id = self.idToPlace
|
| 495 |
+
|
| 496 |
+
# build PlaceObject2
|
| 497 |
+
bb = bytes()
|
| 498 |
+
if self.move:
|
| 499 |
+
bb += "\x07".encode("ascii")
|
| 500 |
+
else:
|
| 501 |
+
# (8 bit flags): 4:matrix, 2:character, 1:move
|
| 502 |
+
bb += "\x06".encode("ascii")
|
| 503 |
+
bb += int2uint16(depth) # Depth
|
| 504 |
+
bb += int2uint16(id) # character id
|
| 505 |
+
bb += self.make_matrix_record(trans_xy=xy).tobytes() # MATRIX record
|
| 506 |
+
self.bytes = bb
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
class ShapeTag(DefinitionTag):
|
| 510 |
+
def __init__(self, bitmapId, xy, wh):
|
| 511 |
+
DefinitionTag.__init__(self)
|
| 512 |
+
self.tagtype = 2
|
| 513 |
+
self.bitmapId = bitmapId
|
| 514 |
+
self.xy = xy
|
| 515 |
+
self.wh = wh
|
| 516 |
+
|
| 517 |
+
def process_tag(self):
|
| 518 |
+
"""Returns a defineshape tag. with a bitmap fill"""
|
| 519 |
+
|
| 520 |
+
bb = bytes()
|
| 521 |
+
bb += int2uint16(self.id)
|
| 522 |
+
xy, wh = self.xy, self.wh
|
| 523 |
+
tmp = self.make_rect_record(xy[0], wh[0], xy[1], wh[1]) # ShapeBounds
|
| 524 |
+
bb += tmp.tobytes()
|
| 525 |
+
|
| 526 |
+
# make SHAPEWITHSTYLE structure
|
| 527 |
+
|
| 528 |
+
# first entry: FILLSTYLEARRAY with in it a single fill style
|
| 529 |
+
bb += int2uint8(1) # FillStyleCount
|
| 530 |
+
bb += "\x41".encode("ascii") # FillStyleType (0x41 or 0x43 unsmoothed)
|
| 531 |
+
bb += int2uint16(self.bitmapId) # BitmapId
|
| 532 |
+
# bb += '\x00' # BitmapMatrix (empty matrix with leftover bits filled)
|
| 533 |
+
bb += self.make_matrix_record(scale_xy=(20, 20)).tobytes()
|
| 534 |
+
|
| 535 |
+
# # first entry: FILLSTYLEARRAY with in it a single fill style
|
| 536 |
+
# bb += int2uint8(1) # FillStyleCount
|
| 537 |
+
# bb += '\x00' # solid fill
|
| 538 |
+
# bb += '\x00\x00\xff' # color
|
| 539 |
+
|
| 540 |
+
# second entry: LINESTYLEARRAY with a single line style
|
| 541 |
+
bb += int2uint8(0) # LineStyleCount
|
| 542 |
+
# bb += int2uint16(0*20) # Width
|
| 543 |
+
# bb += '\x00\xff\x00' # Color
|
| 544 |
+
|
| 545 |
+
# third and fourth entry: NumFillBits and NumLineBits (4 bits each)
|
| 546 |
+
# I each give them four bits, so 16 styles possible.
|
| 547 |
+
bb += "\x44".encode("ascii")
|
| 548 |
+
|
| 549 |
+
self.bytes = bb
|
| 550 |
+
|
| 551 |
+
# last entries: SHAPERECORDs ... (individual shape records not aligned)
|
| 552 |
+
# STYLECHANGERECORD
|
| 553 |
+
bits = BitArray()
|
| 554 |
+
bits += self.make_style_change_record(0, 1, moveTo=(self.wh[0], self.wh[1]))
|
| 555 |
+
# STRAIGHTEDGERECORD 4x
|
| 556 |
+
bits += self.make_straight_edge_record(-self.wh[0], 0)
|
| 557 |
+
bits += self.make_straight_edge_record(0, -self.wh[1])
|
| 558 |
+
bits += self.make_straight_edge_record(self.wh[0], 0)
|
| 559 |
+
bits += self.make_straight_edge_record(0, self.wh[1])
|
| 560 |
+
|
| 561 |
+
# ENDSHAPRECORD
|
| 562 |
+
bits += self.make_end_shape_record()
|
| 563 |
+
|
| 564 |
+
self.bytes += bits.tobytes()
|
| 565 |
+
|
| 566 |
+
# done
|
| 567 |
+
# self.bytes = bb
|
| 568 |
+
|
| 569 |
+
def make_style_change_record(self, lineStyle=None, fillStyle=None, moveTo=None):
|
| 570 |
+
|
| 571 |
+
# first 6 flags
|
| 572 |
+
# Note that we use FillStyle1. If we don't flash (at least 8) does not
|
| 573 |
+
# recognize the frames properly when importing to library.
|
| 574 |
+
|
| 575 |
+
bits = BitArray()
|
| 576 |
+
bits += "0" # TypeFlag (not an edge record)
|
| 577 |
+
bits += "0" # StateNewStyles (only for DefineShape2 and Defineshape3)
|
| 578 |
+
if lineStyle:
|
| 579 |
+
bits += "1" # StateLineStyle
|
| 580 |
+
else:
|
| 581 |
+
bits += "0"
|
| 582 |
+
if fillStyle:
|
| 583 |
+
bits += "1" # StateFillStyle1
|
| 584 |
+
else:
|
| 585 |
+
bits += "0"
|
| 586 |
+
bits += "0" # StateFillStyle0
|
| 587 |
+
if moveTo:
|
| 588 |
+
bits += "1" # StateMoveTo
|
| 589 |
+
else:
|
| 590 |
+
bits += "0"
|
| 591 |
+
|
| 592 |
+
# give information
|
| 593 |
+
# todo: nbits for fillStyle and lineStyle is hard coded.
|
| 594 |
+
|
| 595 |
+
if moveTo:
|
| 596 |
+
bits += twits2bits([moveTo[0], moveTo[1]])
|
| 597 |
+
if fillStyle:
|
| 598 |
+
bits += int2bits(fillStyle, 4)
|
| 599 |
+
if lineStyle:
|
| 600 |
+
bits += int2bits(lineStyle, 4)
|
| 601 |
+
|
| 602 |
+
return bits
|
| 603 |
+
|
| 604 |
+
def make_straight_edge_record(self, *dxdy):
|
| 605 |
+
if len(dxdy) == 1:
|
| 606 |
+
dxdy = dxdy[0]
|
| 607 |
+
|
| 608 |
+
# determine required number of bits
|
| 609 |
+
xbits = signedint2bits(dxdy[0] * 20)
|
| 610 |
+
ybits = signedint2bits(dxdy[1] * 20)
|
| 611 |
+
nbits = max([len(xbits), len(ybits)])
|
| 612 |
+
|
| 613 |
+
bits = BitArray()
|
| 614 |
+
bits += "11" # TypeFlag and StraightFlag
|
| 615 |
+
bits += int2bits(nbits - 2, 4)
|
| 616 |
+
bits += "1" # GeneralLineFlag
|
| 617 |
+
bits += signedint2bits(dxdy[0] * 20, nbits)
|
| 618 |
+
bits += signedint2bits(dxdy[1] * 20, nbits)
|
| 619 |
+
|
| 620 |
+
# note: I do not make use of vertical/horizontal only lines...
|
| 621 |
+
|
| 622 |
+
return bits
|
| 623 |
+
|
| 624 |
+
def make_end_shape_record(self):
|
| 625 |
+
bits = BitArray()
|
| 626 |
+
bits += "0" # TypeFlag: no edge
|
| 627 |
+
bits += "0" * 5 # EndOfShape
|
| 628 |
+
return bits
|
| 629 |
+
|
| 630 |
+
|
| 631 |
+
def read_pixels(bb, i, tagType, L1):
|
| 632 |
+
"""With pf's seed after the recordheader, reads the pixeldata."""
|
| 633 |
+
|
| 634 |
+
# Get info
|
| 635 |
+
charId = bb[i : i + 2] # noqa
|
| 636 |
+
i += 2
|
| 637 |
+
format = ord(bb[i : i + 1])
|
| 638 |
+
i += 1
|
| 639 |
+
width = bits2int(bb[i : i + 2], 16)
|
| 640 |
+
i += 2
|
| 641 |
+
height = bits2int(bb[i : i + 2], 16)
|
| 642 |
+
i += 2
|
| 643 |
+
|
| 644 |
+
# If we can, get pixeldata and make numpy array
|
| 645 |
+
if format != 5:
|
| 646 |
+
logger.warning("Can only read 24bit or 32bit RGB(A) lossless images.")
|
| 647 |
+
else:
|
| 648 |
+
# Read byte data
|
| 649 |
+
offset = 2 + 1 + 2 + 2 # all the info bits
|
| 650 |
+
bb2 = bb[i : i + (L1 - offset)]
|
| 651 |
+
|
| 652 |
+
# Decompress and make numpy array
|
| 653 |
+
data = zlib.decompress(bb2)
|
| 654 |
+
a = np.frombuffer(data, dtype=np.uint8)
|
| 655 |
+
|
| 656 |
+
# Set shape
|
| 657 |
+
if tagType == 20:
|
| 658 |
+
# DefineBitsLossless - RGB data
|
| 659 |
+
try:
|
| 660 |
+
a.shape = height, width, 3
|
| 661 |
+
except Exception:
|
| 662 |
+
# Byte align stuff might cause troubles
|
| 663 |
+
logger.warning("Cannot read image due to byte alignment")
|
| 664 |
+
if tagType == 36:
|
| 665 |
+
# DefineBitsLossless2 - ARGB data
|
| 666 |
+
a.shape = height, width, 4
|
| 667 |
+
# Swap alpha channel to make RGBA
|
| 668 |
+
b = a
|
| 669 |
+
a = np.zeros_like(a)
|
| 670 |
+
a[:, :, 0] = b[:, :, 1]
|
| 671 |
+
a[:, :, 1] = b[:, :, 2]
|
| 672 |
+
a[:, :, 2] = b[:, :, 3]
|
| 673 |
+
a[:, :, 3] = b[:, :, 0]
|
| 674 |
+
|
| 675 |
+
return a
|
| 676 |
+
|
| 677 |
+
|
| 678 |
+
# Last few functions
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
# These are the original public functions, we don't use them, but we
|
| 682 |
+
# keep it so that in principle this module can be used stand-alone.
|
| 683 |
+
|
| 684 |
+
|
| 685 |
+
def checkImages(images): # pragma: no cover
|
| 686 |
+
"""checkImages(images)
|
| 687 |
+
Check numpy images and correct intensity range etc.
|
| 688 |
+
The same for all movie formats.
|
| 689 |
+
"""
|
| 690 |
+
# Init results
|
| 691 |
+
images2 = []
|
| 692 |
+
|
| 693 |
+
for im in images:
|
| 694 |
+
if isinstance(im, np.ndarray):
|
| 695 |
+
# Check and convert dtype
|
| 696 |
+
if im.dtype == np.uint8:
|
| 697 |
+
images2.append(im) # Ok
|
| 698 |
+
elif im.dtype in [np.float32, np.float64]:
|
| 699 |
+
theMax = im.max()
|
| 700 |
+
if 128 < theMax < 300:
|
| 701 |
+
pass # assume 0:255
|
| 702 |
+
else:
|
| 703 |
+
im = im.copy()
|
| 704 |
+
im[im < 0] = 0
|
| 705 |
+
im[im > 1] = 1
|
| 706 |
+
im *= 255
|
| 707 |
+
images2.append(im.astype(np.uint8))
|
| 708 |
+
else:
|
| 709 |
+
im = im.astype(np.uint8)
|
| 710 |
+
images2.append(im)
|
| 711 |
+
# Check size
|
| 712 |
+
if im.ndim == 2:
|
| 713 |
+
pass # ok
|
| 714 |
+
elif im.ndim == 3:
|
| 715 |
+
if im.shape[2] not in [3, 4]:
|
| 716 |
+
raise ValueError("This array can not represent an image.")
|
| 717 |
+
else:
|
| 718 |
+
raise ValueError("This array can not represent an image.")
|
| 719 |
+
else:
|
| 720 |
+
raise ValueError("Invalid image type: " + str(type(im)))
|
| 721 |
+
|
| 722 |
+
# Done
|
| 723 |
+
return images2
|
| 724 |
+
|
| 725 |
+
|
| 726 |
+
def build_file(
|
| 727 |
+
fp, taglist, nframes=1, framesize=(500, 500), fps=10, version=8
|
| 728 |
+
): # pragma: no cover
|
| 729 |
+
"""Give the given file (as bytes) a header."""
|
| 730 |
+
|
| 731 |
+
# compose header
|
| 732 |
+
bb = bytes()
|
| 733 |
+
bb += "F".encode("ascii") # uncompressed
|
| 734 |
+
bb += "WS".encode("ascii") # signature bytes
|
| 735 |
+
bb += int2uint8(version) # version
|
| 736 |
+
bb += "0000".encode("ascii") # FileLength (leave open for now)
|
| 737 |
+
bb += Tag().make_rect_record(0, framesize[0], 0, framesize[1]).tobytes()
|
| 738 |
+
bb += int2uint8(0) + int2uint8(fps) # FrameRate
|
| 739 |
+
bb += int2uint16(nframes)
|
| 740 |
+
fp.write(bb)
|
| 741 |
+
|
| 742 |
+
# produce all tags
|
| 743 |
+
for tag in taglist:
|
| 744 |
+
fp.write(tag.get_tag())
|
| 745 |
+
|
| 746 |
+
# finish with end tag
|
| 747 |
+
fp.write("\x00\x00".encode("ascii"))
|
| 748 |
+
|
| 749 |
+
# set size
|
| 750 |
+
sze = fp.tell()
|
| 751 |
+
fp.seek(4)
|
| 752 |
+
fp.write(int2uint32(sze))
|
| 753 |
+
|
| 754 |
+
|
| 755 |
+
def write_swf(filename, images, duration=0.1, repeat=True): # pragma: no cover
|
| 756 |
+
"""Write an swf-file from the specified images. If repeat is False,
|
| 757 |
+
the movie is finished with a stop action. Duration may also
|
| 758 |
+
be a list with durations for each frame (note that the duration
|
| 759 |
+
for each frame is always an integer amount of the minimum duration.)
|
| 760 |
+
|
| 761 |
+
Images should be a list consisting numpy arrays with values between
|
| 762 |
+
0 and 255 for integer types, and between 0 and 1 for float types.
|
| 763 |
+
|
| 764 |
+
"""
|
| 765 |
+
|
| 766 |
+
# Check images
|
| 767 |
+
images2 = checkImages(images)
|
| 768 |
+
|
| 769 |
+
# Init
|
| 770 |
+
taglist = [FileAttributesTag(), SetBackgroundTag(0, 0, 0)]
|
| 771 |
+
|
| 772 |
+
# Check duration
|
| 773 |
+
if hasattr(duration, "__len__"):
|
| 774 |
+
if len(duration) == len(images2):
|
| 775 |
+
duration = [d for d in duration]
|
| 776 |
+
else:
|
| 777 |
+
raise ValueError("len(duration) doesn't match amount of images.")
|
| 778 |
+
else:
|
| 779 |
+
duration = [duration for im in images2]
|
| 780 |
+
|
| 781 |
+
# Build delays list
|
| 782 |
+
minDuration = float(min(duration))
|
| 783 |
+
delays = [round(d / minDuration) for d in duration]
|
| 784 |
+
delays = [max(1, int(d)) for d in delays]
|
| 785 |
+
|
| 786 |
+
# Get FPS
|
| 787 |
+
fps = 1.0 / minDuration
|
| 788 |
+
|
| 789 |
+
# Produce series of tags for each image
|
| 790 |
+
# t0 = time.time()
|
| 791 |
+
nframes = 0
|
| 792 |
+
for im in images2:
|
| 793 |
+
bm = BitmapTag(im)
|
| 794 |
+
wh = (im.shape[1], im.shape[0])
|
| 795 |
+
sh = ShapeTag(bm.id, (0, 0), wh)
|
| 796 |
+
po = PlaceObjectTag(1, sh.id, move=nframes > 0)
|
| 797 |
+
taglist.extend([bm, sh, po])
|
| 798 |
+
for i in range(delays[nframes]):
|
| 799 |
+
taglist.append(ShowFrameTag())
|
| 800 |
+
nframes += 1
|
| 801 |
+
|
| 802 |
+
if not repeat:
|
| 803 |
+
taglist.append(DoActionTag("stop"))
|
| 804 |
+
|
| 805 |
+
# Build file
|
| 806 |
+
# t1 = time.time()
|
| 807 |
+
fp = open(filename, "wb")
|
| 808 |
+
try:
|
| 809 |
+
build_file(fp, taglist, nframes=nframes, framesize=wh, fps=fps)
|
| 810 |
+
except Exception:
|
| 811 |
+
raise
|
| 812 |
+
finally:
|
| 813 |
+
fp.close()
|
| 814 |
+
# t2 = time.time()
|
| 815 |
+
|
| 816 |
+
# logger.warning("Writing SWF took %1.2f and %1.2f seconds" % (t1-t0, t2-t1) )
|
| 817 |
+
|
| 818 |
+
|
| 819 |
+
def read_swf(filename): # pragma: no cover
|
| 820 |
+
"""Read all images from an SWF (shockwave flash) file. Returns a list
|
| 821 |
+
of numpy arrays.
|
| 822 |
+
|
| 823 |
+
Limitation: only read the PNG encoded images (not the JPG encoded ones).
|
| 824 |
+
"""
|
| 825 |
+
|
| 826 |
+
# Check whether it exists
|
| 827 |
+
if not os.path.isfile(filename):
|
| 828 |
+
raise IOError("File not found: " + str(filename))
|
| 829 |
+
|
| 830 |
+
# Init images
|
| 831 |
+
images = []
|
| 832 |
+
|
| 833 |
+
# Open file and read all
|
| 834 |
+
fp = open(filename, "rb")
|
| 835 |
+
bb = fp.read()
|
| 836 |
+
|
| 837 |
+
try:
|
| 838 |
+
# Check opening tag
|
| 839 |
+
tmp = bb[0:3].decode("ascii", "ignore")
|
| 840 |
+
if tmp.upper() == "FWS":
|
| 841 |
+
pass # ok
|
| 842 |
+
elif tmp.upper() == "CWS":
|
| 843 |
+
# Decompress movie
|
| 844 |
+
bb = bb[:8] + zlib.decompress(bb[8:])
|
| 845 |
+
else:
|
| 846 |
+
raise IOError("Not a valid SWF file: " + str(filename))
|
| 847 |
+
|
| 848 |
+
# Set filepointer at first tag (skipping framesize RECT and two uin16's
|
| 849 |
+
i = 8
|
| 850 |
+
nbits = bits2int(bb[i : i + 1], 5) # skip FrameSize
|
| 851 |
+
nbits = 5 + nbits * 4
|
| 852 |
+
Lrect = nbits / 8.0
|
| 853 |
+
if Lrect % 1:
|
| 854 |
+
Lrect += 1
|
| 855 |
+
Lrect = int(Lrect)
|
| 856 |
+
i += Lrect + 4
|
| 857 |
+
|
| 858 |
+
# Iterate over the tags
|
| 859 |
+
counter = 0
|
| 860 |
+
while True:
|
| 861 |
+
counter += 1
|
| 862 |
+
|
| 863 |
+
# Get tag header
|
| 864 |
+
head = bb[i : i + 6]
|
| 865 |
+
if not head:
|
| 866 |
+
break # Done (we missed end tag)
|
| 867 |
+
|
| 868 |
+
# Determine type and length
|
| 869 |
+
T, L1, L2 = get_type_and_len(head)
|
| 870 |
+
if not L2:
|
| 871 |
+
logger.warning("Invalid tag length, could not proceed")
|
| 872 |
+
break
|
| 873 |
+
# logger.warning(T, L2)
|
| 874 |
+
|
| 875 |
+
# Read image if we can
|
| 876 |
+
if T in [20, 36]:
|
| 877 |
+
im = read_pixels(bb, i + 6, T, L1)
|
| 878 |
+
if im is not None:
|
| 879 |
+
images.append(im)
|
| 880 |
+
elif T in [6, 21, 35, 90]:
|
| 881 |
+
logger.warning("Ignoring JPEG image: cannot read JPEG.")
|
| 882 |
+
else:
|
| 883 |
+
pass # Not an image tag
|
| 884 |
+
|
| 885 |
+
# Detect end tag
|
| 886 |
+
if T == 0:
|
| 887 |
+
break
|
| 888 |
+
|
| 889 |
+
# Next tag!
|
| 890 |
+
i += L2
|
| 891 |
+
|
| 892 |
+
finally:
|
| 893 |
+
fp.close()
|
| 894 |
+
|
| 895 |
+
# Done
|
| 896 |
+
return images
|
| 897 |
+
|
| 898 |
+
|
| 899 |
+
# Backward compatibility; same public names as when this was images2swf.
|
| 900 |
+
writeSwf = write_swf
|
| 901 |
+
readSwf = read_swf
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/bsdf.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
""" Read/Write BSDF files.
|
| 5 |
+
|
| 6 |
+
Backend Library: internal
|
| 7 |
+
|
| 8 |
+
The BSDF format enables reading and writing of image data in the
|
| 9 |
+
BSDF serialization format. This format allows storage of images, volumes,
|
| 10 |
+
and series thereof. Data can be of any numeric data type, and can
|
| 11 |
+
optionally be compressed. Each image/volume can have associated
|
| 12 |
+
meta data, which can consist of any data type supported by BSDF.
|
| 13 |
+
|
| 14 |
+
By default, image data is lazily loaded; the actual image data is
|
| 15 |
+
not read until it is requested. This allows storing multiple images
|
| 16 |
+
in a single file and still have fast access to individual images.
|
| 17 |
+
Alternatively, a series of images can be read in streaming mode, reading
|
| 18 |
+
images as they are read (e.g. from http).
|
| 19 |
+
|
| 20 |
+
BSDF is a simple generic binary format. It is easy to extend and there
|
| 21 |
+
are standard extension definitions for 2D and 3D image data.
|
| 22 |
+
Read more at http://bsdf.io.
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
Parameters
|
| 26 |
+
----------
|
| 27 |
+
random_access : bool
|
| 28 |
+
Whether individual images in the file can be read in random order.
|
| 29 |
+
Defaults to True for normal files, and to False when reading from HTTP.
|
| 30 |
+
If False, the file is read in "streaming mode", allowing reading
|
| 31 |
+
files as they are read, but without support for "rewinding".
|
| 32 |
+
Note that setting this to True when reading from HTTP, the whole file
|
| 33 |
+
is read upon opening it (since lazy loading is not possible over HTTP).
|
| 34 |
+
|
| 35 |
+
compression : int
|
| 36 |
+
Use ``0`` or "no" for no compression, ``1`` or "zlib" for Zlib
|
| 37 |
+
compression (same as zip files and PNG), and ``2`` or "bz2" for Bz2
|
| 38 |
+
compression (more compact but slower). Default 1 (zlib).
|
| 39 |
+
Note that some BSDF implementations may not support compression
|
| 40 |
+
(e.g. JavaScript).
|
| 41 |
+
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
import numpy as np
|
| 45 |
+
|
| 46 |
+
from ..core import Format
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def get_bsdf_serializer(options):
|
| 50 |
+
from . import _bsdf as bsdf
|
| 51 |
+
|
| 52 |
+
class NDArrayExtension(bsdf.Extension):
|
| 53 |
+
"""Copy of BSDF's NDArrayExtension but deal with lazy blobs."""
|
| 54 |
+
|
| 55 |
+
name = "ndarray"
|
| 56 |
+
cls = np.ndarray
|
| 57 |
+
|
| 58 |
+
def encode(self, s, v):
|
| 59 |
+
return dict(shape=v.shape, dtype=str(v.dtype), data=v.tobytes())
|
| 60 |
+
|
| 61 |
+
def decode(self, s, v):
|
| 62 |
+
return v # return as dict, because of lazy blobs, decode in Image
|
| 63 |
+
|
| 64 |
+
class ImageExtension(bsdf.Extension):
|
| 65 |
+
"""We implement two extensions that trigger on the Image classes."""
|
| 66 |
+
|
| 67 |
+
def encode(self, s, v):
|
| 68 |
+
return dict(array=v.array, meta=v.meta)
|
| 69 |
+
|
| 70 |
+
def decode(self, s, v):
|
| 71 |
+
return Image(v["array"], v["meta"])
|
| 72 |
+
|
| 73 |
+
class Image2DExtension(ImageExtension):
|
| 74 |
+
|
| 75 |
+
name = "image2d"
|
| 76 |
+
cls = Image2D
|
| 77 |
+
|
| 78 |
+
class Image3DExtension(ImageExtension):
|
| 79 |
+
|
| 80 |
+
name = "image3d"
|
| 81 |
+
cls = Image3D
|
| 82 |
+
|
| 83 |
+
exts = [NDArrayExtension, Image2DExtension, Image3DExtension]
|
| 84 |
+
serializer = bsdf.BsdfSerializer(exts, **options)
|
| 85 |
+
|
| 86 |
+
return bsdf, serializer
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class Image:
|
| 90 |
+
"""Class in which we wrap the array and meta data. By using an extension
|
| 91 |
+
we can make BSDF trigger on these classes and thus encode the images.
|
| 92 |
+
as actual images.
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
def __init__(self, array, meta):
|
| 96 |
+
self.array = array
|
| 97 |
+
self.meta = meta
|
| 98 |
+
|
| 99 |
+
def get_array(self):
|
| 100 |
+
if not isinstance(self.array, np.ndarray):
|
| 101 |
+
v = self.array
|
| 102 |
+
blob = v["data"]
|
| 103 |
+
if not isinstance(blob, bytes): # then it's a lazy bsdf.Blob
|
| 104 |
+
blob = blob.get_bytes()
|
| 105 |
+
self.array = np.frombuffer(blob, dtype=v["dtype"])
|
| 106 |
+
self.array.shape = v["shape"]
|
| 107 |
+
return self.array
|
| 108 |
+
|
| 109 |
+
def get_meta(self):
|
| 110 |
+
return self.meta
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class Image2D(Image):
|
| 114 |
+
pass
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
class Image3D(Image):
|
| 118 |
+
pass
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class BsdfFormat(Format):
|
| 122 |
+
"""The BSDF format enables reading and writing of image data in the
|
| 123 |
+
BSDF serialization format. This format allows storage of images, volumes,
|
| 124 |
+
and series thereof. Data can be of any numeric data type, and can
|
| 125 |
+
optionally be compressed. Each image/volume can have associated
|
| 126 |
+
meta data, which can consist of any data type supported by BSDF.
|
| 127 |
+
|
| 128 |
+
By default, image data is lazily loaded; the actual image data is
|
| 129 |
+
not read until it is requested. This allows storing multiple images
|
| 130 |
+
in a single file and still have fast access to individual images.
|
| 131 |
+
Alternatively, a series of images can be read in streaming mode, reading
|
| 132 |
+
images as they are read (e.g. from http).
|
| 133 |
+
|
| 134 |
+
BSDF is a simple generic binary format. It is easy to extend and there
|
| 135 |
+
are standard extension definitions for 2D and 3D image data.
|
| 136 |
+
Read more at http://bsdf.io.
|
| 137 |
+
|
| 138 |
+
Parameters for reading
|
| 139 |
+
----------------------
|
| 140 |
+
random_access : bool
|
| 141 |
+
Whether individual images in the file can be read in random order.
|
| 142 |
+
Defaults to True for normal files, and to False when reading from HTTP.
|
| 143 |
+
If False, the file is read in "streaming mode", allowing reading
|
| 144 |
+
files as they are read, but without support for "rewinding".
|
| 145 |
+
Note that setting this to True when reading from HTTP, the whole file
|
| 146 |
+
is read upon opening it (since lazy loading is not possible over HTTP).
|
| 147 |
+
|
| 148 |
+
Parameters for saving
|
| 149 |
+
---------------------
|
| 150 |
+
compression : {0, 1, 2}
|
| 151 |
+
Use ``0`` or "no" for no compression, ``1`` or "zlib" for Zlib
|
| 152 |
+
compression (same as zip files and PNG), and ``2`` or "bz2" for Bz2
|
| 153 |
+
compression (more compact but slower). Default 1 (zlib).
|
| 154 |
+
Note that some BSDF implementations may not support compression
|
| 155 |
+
(e.g. JavaScript).
|
| 156 |
+
|
| 157 |
+
"""
|
| 158 |
+
|
| 159 |
+
def _can_read(self, request):
|
| 160 |
+
if request.mode[1] in (self.modes + "?"):
|
| 161 |
+
# if request.extension in self.extensions:
|
| 162 |
+
# return True
|
| 163 |
+
if request.firstbytes.startswith(b"BSDF"):
|
| 164 |
+
return True
|
| 165 |
+
|
| 166 |
+
def _can_write(self, request):
|
| 167 |
+
if request.mode[1] in (self.modes + "?"):
|
| 168 |
+
if request.extension in self.extensions:
|
| 169 |
+
return True
|
| 170 |
+
|
| 171 |
+
# -- reader
|
| 172 |
+
|
| 173 |
+
class Reader(Format.Reader):
|
| 174 |
+
def _open(self, random_access=None):
|
| 175 |
+
# Validate - we need a BSDF file consisting of a list of images
|
| 176 |
+
# The list is typically a stream, but does not have to be.
|
| 177 |
+
assert self.request.firstbytes[:4] == b"BSDF", "Not a BSDF file"
|
| 178 |
+
# self.request.firstbytes[5:6] == major and minor version
|
| 179 |
+
if not (
|
| 180 |
+
self.request.firstbytes[6:15] == b"M\x07image2D"
|
| 181 |
+
or self.request.firstbytes[6:15] == b"M\x07image3D"
|
| 182 |
+
or self.request.firstbytes[6:7] == b"l"
|
| 183 |
+
):
|
| 184 |
+
pass # Actually, follow a more duck-type approach ...
|
| 185 |
+
# raise RuntimeError('BSDF file does not look like an '
|
| 186 |
+
# 'image container.')
|
| 187 |
+
# Set options. If we think that seeking is allowed, we lazily load
|
| 188 |
+
# blobs, and set streaming to False (i.e. the whole file is read,
|
| 189 |
+
# but we skip over binary blobs), so that we subsequently allow
|
| 190 |
+
# random access to the images.
|
| 191 |
+
# If seeking is not allowed (e.g. with a http request), we cannot
|
| 192 |
+
# lazily load blobs, but we can still load streaming from the web.
|
| 193 |
+
options = {}
|
| 194 |
+
if self.request.filename.startswith(("http://", "https://")):
|
| 195 |
+
ra = False if random_access is None else bool(random_access)
|
| 196 |
+
options["lazy_blob"] = False # Because we cannot seek now
|
| 197 |
+
options["load_streaming"] = not ra # Load as a stream?
|
| 198 |
+
else:
|
| 199 |
+
ra = True if random_access is None else bool(random_access)
|
| 200 |
+
options["lazy_blob"] = ra # Don't read data until needed
|
| 201 |
+
options["load_streaming"] = not ra
|
| 202 |
+
|
| 203 |
+
file = self.request.get_file()
|
| 204 |
+
bsdf, self._serializer = get_bsdf_serializer(options)
|
| 205 |
+
self._stream = self._serializer.load(file)
|
| 206 |
+
# Another validation
|
| 207 |
+
if (
|
| 208 |
+
isinstance(self._stream, dict)
|
| 209 |
+
and "meta" in self._stream
|
| 210 |
+
and "array" in self._stream
|
| 211 |
+
):
|
| 212 |
+
self._stream = Image(self._stream["array"], self._stream["meta"])
|
| 213 |
+
if not isinstance(self._stream, (Image, list, bsdf.ListStream)):
|
| 214 |
+
raise RuntimeError(
|
| 215 |
+
"BSDF file does not look seem to have an " "image container."
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
def _close(self):
|
| 219 |
+
pass
|
| 220 |
+
|
| 221 |
+
def _get_length(self):
|
| 222 |
+
if isinstance(self._stream, Image):
|
| 223 |
+
return 1
|
| 224 |
+
elif isinstance(self._stream, list):
|
| 225 |
+
return len(self._stream)
|
| 226 |
+
elif self._stream.count < 0:
|
| 227 |
+
return np.inf
|
| 228 |
+
return self._stream.count
|
| 229 |
+
|
| 230 |
+
def _get_data(self, index):
|
| 231 |
+
# Validate
|
| 232 |
+
if index < 0 or index >= self.get_length():
|
| 233 |
+
raise IndexError(
|
| 234 |
+
"Image index %i not in [0 %i]." % (index, self.get_length())
|
| 235 |
+
)
|
| 236 |
+
# Get Image object
|
| 237 |
+
if isinstance(self._stream, Image):
|
| 238 |
+
image_ob = self._stream # singleton
|
| 239 |
+
elif isinstance(self._stream, list):
|
| 240 |
+
# Easy when we have random access
|
| 241 |
+
image_ob = self._stream[index]
|
| 242 |
+
else:
|
| 243 |
+
# For streaming, we need to skip over frames
|
| 244 |
+
if index < self._stream.index:
|
| 245 |
+
raise IndexError(
|
| 246 |
+
"BSDF file is being read in streaming "
|
| 247 |
+
"mode, thus does not allow rewinding."
|
| 248 |
+
)
|
| 249 |
+
while index > self._stream.index:
|
| 250 |
+
self._stream.next()
|
| 251 |
+
image_ob = self._stream.next() # Can raise StopIteration
|
| 252 |
+
# Is this an image?
|
| 253 |
+
if (
|
| 254 |
+
isinstance(image_ob, dict)
|
| 255 |
+
and "meta" in image_ob
|
| 256 |
+
and "array" in image_ob
|
| 257 |
+
):
|
| 258 |
+
image_ob = Image(image_ob["array"], image_ob["meta"])
|
| 259 |
+
if isinstance(image_ob, Image):
|
| 260 |
+
# Return as array (if we have lazy blobs, they are read now)
|
| 261 |
+
return image_ob.get_array(), image_ob.get_meta()
|
| 262 |
+
else:
|
| 263 |
+
r = repr(image_ob)
|
| 264 |
+
r = r if len(r) < 200 else r[:197] + "..."
|
| 265 |
+
raise RuntimeError("BSDF file contains non-image " + r)
|
| 266 |
+
|
| 267 |
+
def _get_meta_data(self, index): # pragma: no cover
|
| 268 |
+
return {} # This format does not support global meta data
|
| 269 |
+
|
| 270 |
+
# -- writer
|
| 271 |
+
|
| 272 |
+
class Writer(Format.Writer):
|
| 273 |
+
def _open(self, compression=1):
|
| 274 |
+
options = {"compression": compression}
|
| 275 |
+
bsdf, self._serializer = get_bsdf_serializer(options)
|
| 276 |
+
if self.request.mode[1] in "iv":
|
| 277 |
+
self._stream = None # Singleton image
|
| 278 |
+
self._written = False
|
| 279 |
+
else:
|
| 280 |
+
# Series (stream) of images
|
| 281 |
+
file = self.request.get_file()
|
| 282 |
+
self._stream = bsdf.ListStream()
|
| 283 |
+
self._serializer.save(file, self._stream)
|
| 284 |
+
|
| 285 |
+
def _close(self):
|
| 286 |
+
# We close the stream here, which will mark the number of written
|
| 287 |
+
# elements. If we would not close it, the file would be fine, it's
|
| 288 |
+
# just that upon reading it would not be known how many items are
|
| 289 |
+
# in there.
|
| 290 |
+
if self._stream is not None:
|
| 291 |
+
self._stream.close(False) # False says "keep this a stream"
|
| 292 |
+
|
| 293 |
+
def _append_data(self, im, meta):
|
| 294 |
+
# Determine dimension
|
| 295 |
+
ndim = None
|
| 296 |
+
if self.request.mode[1] in "iI":
|
| 297 |
+
ndim = 2
|
| 298 |
+
elif self.request.mode[1] in "vV":
|
| 299 |
+
ndim = 3
|
| 300 |
+
else:
|
| 301 |
+
ndim = 3 # Make an educated guess
|
| 302 |
+
if im.ndim == 2 or (im.ndim == 3 and im.shape[-1] <= 4):
|
| 303 |
+
ndim = 2
|
| 304 |
+
# Validate shape
|
| 305 |
+
assert ndim in (2, 3)
|
| 306 |
+
if ndim == 2:
|
| 307 |
+
assert im.ndim == 2 or (im.ndim == 3 and im.shape[-1] <= 4)
|
| 308 |
+
else:
|
| 309 |
+
assert im.ndim == 3 or (im.ndim == 4 and im.shape[-1] <= 4)
|
| 310 |
+
# Wrap data and meta data in our special class that will trigger
|
| 311 |
+
# the BSDF image2D or image3D extension.
|
| 312 |
+
if ndim == 2:
|
| 313 |
+
ob = Image2D(im, meta)
|
| 314 |
+
else:
|
| 315 |
+
ob = Image3D(im, meta)
|
| 316 |
+
# Write directly or to stream
|
| 317 |
+
if self._stream is None:
|
| 318 |
+
assert not self._written, "Cannot write singleton image twice"
|
| 319 |
+
self._written = True
|
| 320 |
+
file = self.request.get_file()
|
| 321 |
+
self._serializer.save(file, ob)
|
| 322 |
+
else:
|
| 323 |
+
self._stream.append(ob)
|
| 324 |
+
|
| 325 |
+
def set_meta_data(self, meta): # pragma: no cover
|
| 326 |
+
raise RuntimeError("The BSDF format only supports " "per-image meta data.")
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/dicom.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
"""Read DICOM files.
|
| 5 |
+
|
| 6 |
+
Backend Library: internal
|
| 7 |
+
|
| 8 |
+
A format for reading DICOM images: a common format used to store
|
| 9 |
+
medical image data, such as X-ray, CT and MRI.
|
| 10 |
+
|
| 11 |
+
This format borrows some code (and ideas) from the pydicom project. However,
|
| 12 |
+
only a predefined subset of tags are extracted from the file. This allows
|
| 13 |
+
for great simplifications allowing us to make a stand-alone reader, and
|
| 14 |
+
also results in a much faster read time.
|
| 15 |
+
|
| 16 |
+
By default, only uncompressed and deflated transfer syntaxes are supported.
|
| 17 |
+
If gdcm or dcmtk is installed, these will be used to automatically convert
|
| 18 |
+
the data. See https://github.com/malaterre/GDCM/releases for installing GDCM.
|
| 19 |
+
|
| 20 |
+
This format provides functionality to group images of the same
|
| 21 |
+
series together, thus extracting volumes (and multiple volumes).
|
| 22 |
+
Using volread will attempt to yield a volume. If multiple volumes
|
| 23 |
+
are present, the first one is given. Using mimread will simply yield
|
| 24 |
+
all images in the given directory (not taking series into account).
|
| 25 |
+
|
| 26 |
+
Parameters
|
| 27 |
+
----------
|
| 28 |
+
progress : {True, False, BaseProgressIndicator}
|
| 29 |
+
Whether to show progress when reading from multiple files.
|
| 30 |
+
Default True. By passing an object that inherits from
|
| 31 |
+
BaseProgressIndicator, the way in which progress is reported
|
| 32 |
+
can be costumized.
|
| 33 |
+
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
# todo: Use pydicom:
|
| 37 |
+
# * Note: is not py3k ready yet
|
| 38 |
+
# * Allow reading the full meta info
|
| 39 |
+
# I think we can more or less replace the SimpleDicomReader with a
|
| 40 |
+
# pydicom.Dataset For series, only ned to read the full info from one
|
| 41 |
+
# file: speed still high
|
| 42 |
+
# * Perhaps allow writing?
|
| 43 |
+
|
| 44 |
+
import os
|
| 45 |
+
import sys
|
| 46 |
+
import logging
|
| 47 |
+
import subprocess
|
| 48 |
+
|
| 49 |
+
from ..core import Format, BaseProgressIndicator, StdoutProgressIndicator
|
| 50 |
+
from ..core import read_n_bytes
|
| 51 |
+
|
| 52 |
+
_dicom = None # lazily loaded in load_lib()
|
| 53 |
+
|
| 54 |
+
logger = logging.getLogger(__name__)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def load_lib():
|
| 58 |
+
global _dicom
|
| 59 |
+
from . import _dicom
|
| 60 |
+
|
| 61 |
+
return _dicom
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# Determine endianity of system
|
| 65 |
+
sys_is_little_endian = sys.byteorder == "little"
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def get_dcmdjpeg_exe():
|
| 69 |
+
fname = "dcmdjpeg" + ".exe" * sys.platform.startswith("win")
|
| 70 |
+
for dir in (
|
| 71 |
+
"c:\\dcmtk",
|
| 72 |
+
"c:\\Program Files",
|
| 73 |
+
"c:\\Program Files\\dcmtk",
|
| 74 |
+
"c:\\Program Files (x86)\\dcmtk",
|
| 75 |
+
):
|
| 76 |
+
filename = os.path.join(dir, fname)
|
| 77 |
+
if os.path.isfile(filename):
|
| 78 |
+
return [filename]
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
subprocess.check_call([fname, "--version"])
|
| 82 |
+
return [fname]
|
| 83 |
+
except Exception:
|
| 84 |
+
return None
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def get_gdcmconv_exe():
|
| 88 |
+
fname = "gdcmconv" + ".exe" * sys.platform.startswith("win")
|
| 89 |
+
# Maybe it's on the path
|
| 90 |
+
try:
|
| 91 |
+
subprocess.check_call([fname, "--version"])
|
| 92 |
+
return [fname, "--raw"]
|
| 93 |
+
except Exception:
|
| 94 |
+
pass
|
| 95 |
+
# Select directories where it could be
|
| 96 |
+
candidates = []
|
| 97 |
+
base_dirs = [r"c:\Program Files"]
|
| 98 |
+
for base_dir in base_dirs:
|
| 99 |
+
if os.path.isdir(base_dir):
|
| 100 |
+
for dname in os.listdir(base_dir):
|
| 101 |
+
if dname.lower().startswith("gdcm"):
|
| 102 |
+
suffix = dname[4:].strip()
|
| 103 |
+
candidates.append((suffix, os.path.join(base_dir, dname)))
|
| 104 |
+
# Sort, so higher versions are tried earlier
|
| 105 |
+
candidates.sort(reverse=True)
|
| 106 |
+
# Select executable
|
| 107 |
+
filename = None
|
| 108 |
+
for _, dirname in candidates:
|
| 109 |
+
exe1 = os.path.join(dirname, "gdcmconv.exe")
|
| 110 |
+
exe2 = os.path.join(dirname, "bin", "gdcmconv.exe")
|
| 111 |
+
if os.path.isfile(exe1):
|
| 112 |
+
filename = exe1
|
| 113 |
+
break
|
| 114 |
+
if os.path.isfile(exe2):
|
| 115 |
+
filename = exe2
|
| 116 |
+
break
|
| 117 |
+
else:
|
| 118 |
+
return None
|
| 119 |
+
return [filename, "--raw"]
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class DicomFormat(Format):
|
| 123 |
+
"""See :mod:`imageio.plugins.dicom`"""
|
| 124 |
+
|
| 125 |
+
def _can_read(self, request):
|
| 126 |
+
# If user URI was a directory, we check whether it has a DICOM file
|
| 127 |
+
if os.path.isdir(request.filename):
|
| 128 |
+
files = os.listdir(request.filename)
|
| 129 |
+
for fname in sorted(files): # Sorting make it consistent
|
| 130 |
+
filename = os.path.join(request.filename, fname)
|
| 131 |
+
if os.path.isfile(filename) and "DICOMDIR" not in fname:
|
| 132 |
+
with open(filename, "rb") as f:
|
| 133 |
+
first_bytes = read_n_bytes(f, 140)
|
| 134 |
+
return first_bytes[128:132] == b"DICM"
|
| 135 |
+
else:
|
| 136 |
+
return False
|
| 137 |
+
# Check
|
| 138 |
+
return request.firstbytes[128:132] == b"DICM"
|
| 139 |
+
|
| 140 |
+
def _can_write(self, request):
|
| 141 |
+
# We cannot save yet. May be possible if we will used pydicom as
|
| 142 |
+
# a backend.
|
| 143 |
+
return False
|
| 144 |
+
|
| 145 |
+
# --
|
| 146 |
+
|
| 147 |
+
class Reader(Format.Reader):
|
| 148 |
+
|
| 149 |
+
_compressed_warning_dirs = set()
|
| 150 |
+
|
| 151 |
+
def _open(self, progress=True):
|
| 152 |
+
if not _dicom:
|
| 153 |
+
load_lib()
|
| 154 |
+
if os.path.isdir(self.request.filename):
|
| 155 |
+
# A dir can be given if the user used the format explicitly
|
| 156 |
+
self._info = {}
|
| 157 |
+
self._data = None
|
| 158 |
+
else:
|
| 159 |
+
# Read the given dataset now ...
|
| 160 |
+
try:
|
| 161 |
+
dcm = _dicom.SimpleDicomReader(self.request.get_file())
|
| 162 |
+
except _dicom.CompressedDicom as err:
|
| 163 |
+
# We cannot do this on our own. Perhaps with some help ...
|
| 164 |
+
cmd = get_gdcmconv_exe()
|
| 165 |
+
if not cmd and "JPEG" in str(err):
|
| 166 |
+
cmd = get_dcmdjpeg_exe()
|
| 167 |
+
if not cmd:
|
| 168 |
+
msg = err.args[0].replace("using", "installing")
|
| 169 |
+
msg = msg.replace("convert", "auto-convert")
|
| 170 |
+
err.args = (msg,)
|
| 171 |
+
raise
|
| 172 |
+
else:
|
| 173 |
+
fname1 = self.request.get_local_filename()
|
| 174 |
+
fname2 = fname1 + ".raw"
|
| 175 |
+
try:
|
| 176 |
+
subprocess.check_call(cmd + [fname1, fname2])
|
| 177 |
+
except Exception:
|
| 178 |
+
raise err
|
| 179 |
+
d = os.path.dirname(fname1)
|
| 180 |
+
if d not in self._compressed_warning_dirs:
|
| 181 |
+
self._compressed_warning_dirs.add(d)
|
| 182 |
+
logger.warning(
|
| 183 |
+
"DICOM file contained compressed data. "
|
| 184 |
+
+ "Autoconverting with "
|
| 185 |
+
+ cmd[0]
|
| 186 |
+
+ " (this warning is shown once for each directory)"
|
| 187 |
+
)
|
| 188 |
+
dcm = _dicom.SimpleDicomReader(fname2)
|
| 189 |
+
|
| 190 |
+
self._info = dcm._info
|
| 191 |
+
self._data = dcm.get_numpy_array()
|
| 192 |
+
|
| 193 |
+
# Initialize series, list of DicomSeries objects
|
| 194 |
+
self._series = None # only created if needed
|
| 195 |
+
|
| 196 |
+
# Set progress indicator
|
| 197 |
+
if isinstance(progress, BaseProgressIndicator):
|
| 198 |
+
self._progressIndicator = progress
|
| 199 |
+
elif progress is True:
|
| 200 |
+
p = StdoutProgressIndicator("Reading DICOM")
|
| 201 |
+
self._progressIndicator = p
|
| 202 |
+
elif progress in (None, False):
|
| 203 |
+
self._progressIndicator = BaseProgressIndicator("Dummy")
|
| 204 |
+
else:
|
| 205 |
+
raise ValueError("Invalid value for progress.")
|
| 206 |
+
|
| 207 |
+
def _close(self):
|
| 208 |
+
# Clean up
|
| 209 |
+
self._info = None
|
| 210 |
+
self._data = None
|
| 211 |
+
self._series = None
|
| 212 |
+
|
| 213 |
+
@property
|
| 214 |
+
def series(self):
|
| 215 |
+
if self._series is None:
|
| 216 |
+
pi = self._progressIndicator
|
| 217 |
+
self._series = _dicom.process_directory(self.request, pi)
|
| 218 |
+
return self._series
|
| 219 |
+
|
| 220 |
+
def _get_length(self):
|
| 221 |
+
if self._data is None:
|
| 222 |
+
dcm = self.series[0][0]
|
| 223 |
+
self._info = dcm._info
|
| 224 |
+
self._data = dcm.get_numpy_array()
|
| 225 |
+
|
| 226 |
+
nslices = self._data.shape[0] if (self._data.ndim == 3) else 1
|
| 227 |
+
|
| 228 |
+
if self.request.mode[1] == "i":
|
| 229 |
+
# User expects one, but lets be honest about this file
|
| 230 |
+
return nslices
|
| 231 |
+
elif self.request.mode[1] == "I":
|
| 232 |
+
# User expects multiple, if this file has multiple slices, ok.
|
| 233 |
+
# Otherwise we have to check the series.
|
| 234 |
+
if nslices > 1:
|
| 235 |
+
return nslices
|
| 236 |
+
else:
|
| 237 |
+
return sum([len(serie) for serie in self.series])
|
| 238 |
+
elif self.request.mode[1] == "v":
|
| 239 |
+
# User expects a volume, if this file has one, ok.
|
| 240 |
+
# Otherwise we have to check the series
|
| 241 |
+
if nslices > 1:
|
| 242 |
+
return 1
|
| 243 |
+
else:
|
| 244 |
+
return len(self.series) # We assume one volume per series
|
| 245 |
+
elif self.request.mode[1] == "V":
|
| 246 |
+
# User expects multiple volumes. We have to check the series
|
| 247 |
+
return len(self.series) # We assume one volume per series
|
| 248 |
+
else:
|
| 249 |
+
raise RuntimeError("DICOM plugin should know what to expect.")
|
| 250 |
+
|
| 251 |
+
def _get_data(self, index):
|
| 252 |
+
if self._data is None:
|
| 253 |
+
dcm = self.series[0][0]
|
| 254 |
+
self._info = dcm._info
|
| 255 |
+
self._data = dcm.get_numpy_array()
|
| 256 |
+
|
| 257 |
+
nslices = self._data.shape[0] if (self._data.ndim == 3) else 1
|
| 258 |
+
|
| 259 |
+
if self.request.mode[1] == "i":
|
| 260 |
+
# Allow index >1 only if this file contains >1
|
| 261 |
+
if nslices > 1:
|
| 262 |
+
return self._data[index], self._info
|
| 263 |
+
elif index == 0:
|
| 264 |
+
return self._data, self._info
|
| 265 |
+
else:
|
| 266 |
+
raise IndexError("Dicom file contains only one slice.")
|
| 267 |
+
elif self.request.mode[1] == "I":
|
| 268 |
+
# Return slice from volume, or return item from series
|
| 269 |
+
if index == 0 and nslices > 1:
|
| 270 |
+
return self._data[index], self._info
|
| 271 |
+
else:
|
| 272 |
+
L = []
|
| 273 |
+
for serie in self.series:
|
| 274 |
+
L.extend([dcm_ for dcm_ in serie])
|
| 275 |
+
return L[index].get_numpy_array(), L[index].info
|
| 276 |
+
elif self.request.mode[1] in "vV":
|
| 277 |
+
# Return volume or series
|
| 278 |
+
if index == 0 and nslices > 1:
|
| 279 |
+
return self._data, self._info
|
| 280 |
+
else:
|
| 281 |
+
return (
|
| 282 |
+
self.series[index].get_numpy_array(),
|
| 283 |
+
self.series[index].info,
|
| 284 |
+
)
|
| 285 |
+
else: # pragma: no cover
|
| 286 |
+
raise ValueError("DICOM plugin should know what to expect.")
|
| 287 |
+
|
| 288 |
+
def _get_meta_data(self, index):
|
| 289 |
+
if self._data is None:
|
| 290 |
+
dcm = self.series[0][0]
|
| 291 |
+
self._info = dcm._info
|
| 292 |
+
self._data = dcm.get_numpy_array()
|
| 293 |
+
|
| 294 |
+
nslices = self._data.shape[0] if (self._data.ndim == 3) else 1
|
| 295 |
+
|
| 296 |
+
# Default is the meta data of the given file, or the "first" file.
|
| 297 |
+
if index is None:
|
| 298 |
+
return self._info
|
| 299 |
+
|
| 300 |
+
if self.request.mode[1] == "i":
|
| 301 |
+
return self._info
|
| 302 |
+
elif self.request.mode[1] == "I":
|
| 303 |
+
# Return slice from volume, or return item from series
|
| 304 |
+
if index == 0 and nslices > 1:
|
| 305 |
+
return self._info
|
| 306 |
+
else:
|
| 307 |
+
L = []
|
| 308 |
+
for serie in self.series:
|
| 309 |
+
L.extend([dcm_ for dcm_ in serie])
|
| 310 |
+
return L[index].info
|
| 311 |
+
elif self.request.mode[1] in "vV":
|
| 312 |
+
# Return volume or series
|
| 313 |
+
if index == 0 and nslices > 1:
|
| 314 |
+
return self._info
|
| 315 |
+
else:
|
| 316 |
+
return self.series[index].info
|
| 317 |
+
else: # pragma: no cover
|
| 318 |
+
raise ValueError("DICOM plugin should know what to expect.")
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/feisem.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
"""Read TIFF from FEI SEM microscopes.
|
| 5 |
+
|
| 6 |
+
Backend Library: internal
|
| 7 |
+
|
| 8 |
+
This format is based on :mod:`TIFF <imageio.plugins.tifffile>`, and supports the
|
| 9 |
+
same parameters. FEI microscopes append metadata as ASCII text at the end of the
|
| 10 |
+
file, which this reader correctly extracts.
|
| 11 |
+
|
| 12 |
+
Parameters
|
| 13 |
+
----------
|
| 14 |
+
discard_watermark : bool
|
| 15 |
+
If True (default), discard the bottom rows of the image, which
|
| 16 |
+
contain no image data, only a watermark with metadata.
|
| 17 |
+
watermark_height : int
|
| 18 |
+
The height in pixels of the FEI watermark. The default is 70.
|
| 19 |
+
|
| 20 |
+
See Also
|
| 21 |
+
--------
|
| 22 |
+
:mod:`imageio.plugins.tifffile`
|
| 23 |
+
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
from .tifffile import TiffFormat
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class FEISEMFormat(TiffFormat):
|
| 31 |
+
"""See :mod:`imageio.plugins.feisem`"""
|
| 32 |
+
|
| 33 |
+
def _can_write(self, request):
|
| 34 |
+
return False # FEI-SEM only supports reading
|
| 35 |
+
|
| 36 |
+
class Reader(TiffFormat.Reader):
|
| 37 |
+
def _get_data(self, index=0, discard_watermark=True, watermark_height=70):
|
| 38 |
+
"""Get image and metadata from given index.
|
| 39 |
+
|
| 40 |
+
FEI images usually (always?) contain a watermark at the
|
| 41 |
+
bottom of the image, 70 pixels high. We discard this by
|
| 42 |
+
default as it does not contain any information not present
|
| 43 |
+
in the metadata.
|
| 44 |
+
"""
|
| 45 |
+
im, meta = super(FEISEMFormat.Reader, self)._get_data(index)
|
| 46 |
+
if discard_watermark:
|
| 47 |
+
im = im[:-watermark_height]
|
| 48 |
+
return im, meta
|
| 49 |
+
|
| 50 |
+
def _get_meta_data(self, index=None):
|
| 51 |
+
"""Read the metadata from an FEI SEM TIFF.
|
| 52 |
+
|
| 53 |
+
This metadata is included as ASCII text at the end of the file.
|
| 54 |
+
|
| 55 |
+
The index, if provided, is ignored.
|
| 56 |
+
|
| 57 |
+
Returns
|
| 58 |
+
-------
|
| 59 |
+
metadata : dict
|
| 60 |
+
Dictionary of metadata.
|
| 61 |
+
"""
|
| 62 |
+
if hasattr(self, "_fei_meta"):
|
| 63 |
+
return self._fei_meta
|
| 64 |
+
|
| 65 |
+
md = {"root": {}}
|
| 66 |
+
current_tag = "root"
|
| 67 |
+
reading_metadata = False
|
| 68 |
+
filename = self.request.get_local_filename()
|
| 69 |
+
with open(filename, encoding="utf8", errors="ignore") as fin:
|
| 70 |
+
for line in fin:
|
| 71 |
+
if not reading_metadata:
|
| 72 |
+
if not line.startswith("Date="):
|
| 73 |
+
continue
|
| 74 |
+
else:
|
| 75 |
+
reading_metadata = True
|
| 76 |
+
line = line.rstrip()
|
| 77 |
+
if line.startswith("["):
|
| 78 |
+
current_tag = line.lstrip("[").rstrip("]")
|
| 79 |
+
md[current_tag] = {}
|
| 80 |
+
else:
|
| 81 |
+
if "=" in line: # ignore empty and irrelevant lines
|
| 82 |
+
key, val = line.split("=", maxsplit=1)
|
| 83 |
+
for tag_type in (int, float):
|
| 84 |
+
try:
|
| 85 |
+
val = tag_type(val)
|
| 86 |
+
except ValueError:
|
| 87 |
+
continue
|
| 88 |
+
else:
|
| 89 |
+
break
|
| 90 |
+
md[current_tag][key] = val
|
| 91 |
+
if not md["root"] and len(md) == 1:
|
| 92 |
+
raise ValueError("Input file %s contains no FEI metadata." % filename)
|
| 93 |
+
|
| 94 |
+
self._fei_meta = md
|
| 95 |
+
return md
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/ffmpeg.py
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
"""Read/Write video using FFMPEG
|
| 5 |
+
|
| 6 |
+
Backend Library: https://github.com/imageio/imageio-ffmpeg
|
| 7 |
+
|
| 8 |
+
.. note::
|
| 9 |
+
To use this plugin you have to install its backend::
|
| 10 |
+
|
| 11 |
+
pip install imageio[ffmpeg]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
The ffmpeg format provides reading and writing for a wide range of movie formats
|
| 15 |
+
such as .avi, .mpeg, .mp4, etc. as well as the ability to read streams from
|
| 16 |
+
webcams and USB cameras. It is based on ffmpeg and is inspired by/based `moviepy
|
| 17 |
+
<https://github.com/Zulko/moviepy/>`_ by Zulko.
|
| 18 |
+
|
| 19 |
+
Parameters for reading
|
| 20 |
+
----------------------
|
| 21 |
+
fps : scalar
|
| 22 |
+
The number of frames per second to read the data at. Default None (i.e.
|
| 23 |
+
read at the file's own fps). One can use this for files with a
|
| 24 |
+
variable fps, or in cases where imageio is unable to correctly detect
|
| 25 |
+
the fps. In case of trouble opening camera streams, it may help to set an
|
| 26 |
+
explicit fps value matching a framerate supported by the camera.
|
| 27 |
+
loop : bool
|
| 28 |
+
If True, the video will rewind as soon as a frame is requested
|
| 29 |
+
beyond the last frame. Otherwise, IndexError is raised. Default False.
|
| 30 |
+
Setting this to True will internally call ``count_frames()``,
|
| 31 |
+
and set the reader's length to that value instead of inf.
|
| 32 |
+
size : str | tuple
|
| 33 |
+
The frame size (i.e. resolution) to read the images, e.g.
|
| 34 |
+
(100, 100) or "640x480". For camera streams, this allows setting
|
| 35 |
+
the capture resolution. For normal video data, ffmpeg will
|
| 36 |
+
rescale the data.
|
| 37 |
+
dtype : str | type
|
| 38 |
+
The dtype for the output arrays. Determines the bit-depth that
|
| 39 |
+
is requested from ffmpeg. Supported dtypes: uint8, uint16.
|
| 40 |
+
Default: uint8.
|
| 41 |
+
pixelformat : str
|
| 42 |
+
The pixel format for the camera to use (e.g. "yuyv422" or
|
| 43 |
+
"gray"). The camera needs to support the format in order for
|
| 44 |
+
this to take effect. Note that the images produced by this
|
| 45 |
+
reader are always RGB.
|
| 46 |
+
input_params : list
|
| 47 |
+
List additional arguments to ffmpeg for input file options.
|
| 48 |
+
(Can also be provided as ``ffmpeg_params`` for backwards compatibility)
|
| 49 |
+
Example ffmpeg arguments to use aggressive error handling:
|
| 50 |
+
['-err_detect', 'aggressive']
|
| 51 |
+
output_params : list
|
| 52 |
+
List additional arguments to ffmpeg for output file options (i.e. the
|
| 53 |
+
stream being read by imageio).
|
| 54 |
+
print_info : bool
|
| 55 |
+
Print information about the video file as reported by ffmpeg.
|
| 56 |
+
|
| 57 |
+
Parameters for writing
|
| 58 |
+
----------------------
|
| 59 |
+
fps : scalar
|
| 60 |
+
The number of frames per second. Default 10.
|
| 61 |
+
codec : str
|
| 62 |
+
the video codec to use. Default 'libx264', which represents the
|
| 63 |
+
widely available mpeg4. Except when saving .wmv files, then the
|
| 64 |
+
defaults is 'msmpeg4' which is more commonly supported for windows
|
| 65 |
+
quality : float | None
|
| 66 |
+
Video output quality. Default is 5. Uses variable bit rate. Highest
|
| 67 |
+
quality is 10, lowest is 0. Set to None to prevent variable bitrate
|
| 68 |
+
flags to FFMPEG so you can manually specify them using output_params
|
| 69 |
+
instead. Specifying a fixed bitrate using 'bitrate' disables this
|
| 70 |
+
parameter.
|
| 71 |
+
bitrate : int | None
|
| 72 |
+
Set a constant bitrate for the video encoding. Default is None causing
|
| 73 |
+
'quality' parameter to be used instead. Better quality videos with
|
| 74 |
+
smaller file sizes will result from using the 'quality' variable
|
| 75 |
+
bitrate parameter rather than specifiying a fixed bitrate with this
|
| 76 |
+
parameter.
|
| 77 |
+
pixelformat: str
|
| 78 |
+
The output video pixel format. Default is 'yuv420p' which most widely
|
| 79 |
+
supported by video players.
|
| 80 |
+
input_params : list
|
| 81 |
+
List additional arguments to ffmpeg for input file options (i.e. the
|
| 82 |
+
stream that imageio provides).
|
| 83 |
+
output_params : list
|
| 84 |
+
List additional arguments to ffmpeg for output file options.
|
| 85 |
+
(Can also be provided as ``ffmpeg_params`` for backwards compatibility)
|
| 86 |
+
Example ffmpeg arguments to use only intra frames and set aspect ratio:
|
| 87 |
+
['-intra', '-aspect', '16:9']
|
| 88 |
+
ffmpeg_log_level: str
|
| 89 |
+
Sets ffmpeg output log level. Default is "warning".
|
| 90 |
+
Values can be "quiet", "panic", "fatal", "error", "warning", "info"
|
| 91 |
+
"verbose", or "debug". Also prints the FFMPEG command being used by
|
| 92 |
+
imageio if "info", "verbose", or "debug".
|
| 93 |
+
macro_block_size: int
|
| 94 |
+
Size constraint for video. Width and height, must be divisible by this
|
| 95 |
+
number. If not divisible by this number imageio will tell ffmpeg to
|
| 96 |
+
scale the image up to the next closest size
|
| 97 |
+
divisible by this number. Most codecs are compatible with a macroblock
|
| 98 |
+
size of 16 (default), some can go smaller (4, 8). To disable this
|
| 99 |
+
automatic feature set it to None or 1, however be warned many players
|
| 100 |
+
can't decode videos that are odd in size and some codecs will produce
|
| 101 |
+
poor results or fail. See https://en.wikipedia.org/wiki/Macroblock.
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
Notes
|
| 105 |
+
-----
|
| 106 |
+
If you are using anaconda and ``anaconda/ffmpeg`` you will not be able to
|
| 107 |
+
encode/decode H.264 (likely due to licensing concerns). If you need this
|
| 108 |
+
format on anaconda install ``conda-forge/ffmpeg`` instead.
|
| 109 |
+
|
| 110 |
+
You can use the ``IMAGEIO_FFMPEG_EXE`` environment variable to force using a
|
| 111 |
+
specific ffmpeg executable.
|
| 112 |
+
|
| 113 |
+
To get the number of frames before having read them all, you can use the
|
| 114 |
+
``reader.count_frames()`` method (the reader will then use
|
| 115 |
+
``imageio_ffmpeg.count_frames_and_secs()`` to get the exact number of frames,
|
| 116 |
+
note that this operation can take a few seconds on large files). Alternatively,
|
| 117 |
+
the number of frames can be estimated from the fps and duration in the meta data
|
| 118 |
+
(though these values themselves are not always present/reliable).
|
| 119 |
+
|
| 120 |
+
"""
|
| 121 |
+
|
| 122 |
+
import re
|
| 123 |
+
import sys
|
| 124 |
+
import time
|
| 125 |
+
import logging
|
| 126 |
+
import platform
|
| 127 |
+
import threading
|
| 128 |
+
import subprocess as sp
|
| 129 |
+
|
| 130 |
+
import numpy as np
|
| 131 |
+
|
| 132 |
+
from ..core import Format, image_as_uint
|
| 133 |
+
|
| 134 |
+
logger = logging.getLogger(__name__)
|
| 135 |
+
|
| 136 |
+
# Get camera format
|
| 137 |
+
if sys.platform.startswith("win"):
|
| 138 |
+
CAM_FORMAT = "dshow" # dshow or vfwcap
|
| 139 |
+
elif sys.platform.startswith("linux"):
|
| 140 |
+
CAM_FORMAT = "video4linux2"
|
| 141 |
+
elif sys.platform.startswith("darwin"):
|
| 142 |
+
CAM_FORMAT = "avfoundation"
|
| 143 |
+
else: # pragma: no cover
|
| 144 |
+
CAM_FORMAT = "unknown-cam-format"
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def download(directory=None, force_download=False): # pragma: no cover
|
| 148 |
+
raise RuntimeError(
|
| 149 |
+
"imageio.ffmpeg.download() has been deprecated. "
|
| 150 |
+
"Use 'pip install imageio-ffmpeg' instead.'"
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
# For backwards compatibility - we dont use this ourselves
|
| 155 |
+
def get_exe(): # pragma: no cover
|
| 156 |
+
"""Wrapper for imageio_ffmpeg.get_ffmpeg_exe()"""
|
| 157 |
+
import imageio_ffmpeg
|
| 158 |
+
|
| 159 |
+
return imageio_ffmpeg.get_ffmpeg_exe()
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
_ffmpeg_api = None
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def _get_ffmpeg_api():
|
| 166 |
+
global _ffmpeg_api
|
| 167 |
+
if _ffmpeg_api is None:
|
| 168 |
+
try:
|
| 169 |
+
import imageio_ffmpeg
|
| 170 |
+
except ImportError:
|
| 171 |
+
raise ImportError(
|
| 172 |
+
"To use the imageio ffmpeg plugin you need to "
|
| 173 |
+
"'pip install imageio-ffmpeg'"
|
| 174 |
+
)
|
| 175 |
+
_ffmpeg_api = imageio_ffmpeg
|
| 176 |
+
return _ffmpeg_api
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class FfmpegFormat(Format):
|
| 180 |
+
"""Read/Write ImageResources using FFMPEG.
|
| 181 |
+
|
| 182 |
+
See :mod:`imageio.plugins.ffmpeg`
|
| 183 |
+
"""
|
| 184 |
+
|
| 185 |
+
def _can_read(self, request):
|
| 186 |
+
if request.mode[1] not in "I?":
|
| 187 |
+
return False
|
| 188 |
+
|
| 189 |
+
# Read from video stream?
|
| 190 |
+
# Note that we could write the _video flag here, but a user might
|
| 191 |
+
# select this format explicitly (and this code is not run)
|
| 192 |
+
if re.match(r"<video(\d+)>", request.filename):
|
| 193 |
+
return True
|
| 194 |
+
|
| 195 |
+
# Read from file that we know?
|
| 196 |
+
if request.extension in self.extensions:
|
| 197 |
+
return True
|
| 198 |
+
|
| 199 |
+
def _can_write(self, request):
|
| 200 |
+
if request.mode[1] in (self.modes + "?"):
|
| 201 |
+
if request.extension in self.extensions:
|
| 202 |
+
return True
|
| 203 |
+
|
| 204 |
+
# --
|
| 205 |
+
|
| 206 |
+
class Reader(Format.Reader):
|
| 207 |
+
|
| 208 |
+
_frame_catcher = None
|
| 209 |
+
_read_gen = None
|
| 210 |
+
|
| 211 |
+
def _get_cam_inputname(self, index):
|
| 212 |
+
if sys.platform.startswith("linux"):
|
| 213 |
+
return "/dev/" + self.request._video[1:-1]
|
| 214 |
+
|
| 215 |
+
elif sys.platform.startswith("win"):
|
| 216 |
+
# Ask ffmpeg for list of dshow device names
|
| 217 |
+
ffmpeg_api = _get_ffmpeg_api()
|
| 218 |
+
cmd = [
|
| 219 |
+
ffmpeg_api.get_ffmpeg_exe(),
|
| 220 |
+
"-list_devices",
|
| 221 |
+
"true",
|
| 222 |
+
"-f",
|
| 223 |
+
CAM_FORMAT,
|
| 224 |
+
"-i",
|
| 225 |
+
"dummy",
|
| 226 |
+
]
|
| 227 |
+
# Set `shell=True` in sp.run to prevent popup of a command
|
| 228 |
+
# line window in frozen applications. Note: this would be a
|
| 229 |
+
# security vulnerability if user-input goes into the cmd.
|
| 230 |
+
# Note that the ffmpeg process returns with exit code 1 when
|
| 231 |
+
# using `-list_devices` (or `-list_options`), even if the
|
| 232 |
+
# command is successful, so we set `check=False` explicitly.
|
| 233 |
+
completed_process = sp.run(
|
| 234 |
+
cmd,
|
| 235 |
+
stdout=sp.PIPE,
|
| 236 |
+
stderr=sp.PIPE,
|
| 237 |
+
encoding="utf-8",
|
| 238 |
+
shell=True,
|
| 239 |
+
check=False,
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
# Return device name at index
|
| 243 |
+
try:
|
| 244 |
+
name = parse_device_names(completed_process.stderr)[index]
|
| 245 |
+
except IndexError:
|
| 246 |
+
raise IndexError("No ffdshow camera at index %i." % index)
|
| 247 |
+
return "video=%s" % name
|
| 248 |
+
|
| 249 |
+
elif sys.platform.startswith("darwin"):
|
| 250 |
+
# Appears that newer ffmpeg builds don't support -list-devices
|
| 251 |
+
# on OS X. But you can directly open the camera by index.
|
| 252 |
+
name = str(index)
|
| 253 |
+
return name
|
| 254 |
+
|
| 255 |
+
else: # pragma: no cover
|
| 256 |
+
return "??"
|
| 257 |
+
|
| 258 |
+
def _open(
|
| 259 |
+
self,
|
| 260 |
+
loop=False,
|
| 261 |
+
size=None,
|
| 262 |
+
dtype=None,
|
| 263 |
+
pixelformat=None,
|
| 264 |
+
print_info=False,
|
| 265 |
+
ffmpeg_params=None,
|
| 266 |
+
input_params=None,
|
| 267 |
+
output_params=None,
|
| 268 |
+
fps=None,
|
| 269 |
+
):
|
| 270 |
+
# Get generator functions
|
| 271 |
+
self._ffmpeg_api = _get_ffmpeg_api()
|
| 272 |
+
# Process input args
|
| 273 |
+
self._arg_loop = bool(loop)
|
| 274 |
+
if size is None:
|
| 275 |
+
self._arg_size = None
|
| 276 |
+
elif isinstance(size, tuple):
|
| 277 |
+
self._arg_size = "%ix%i" % size
|
| 278 |
+
elif isinstance(size, str) and "x" in size:
|
| 279 |
+
self._arg_size = size
|
| 280 |
+
else:
|
| 281 |
+
raise ValueError('FFMPEG size must be tuple of "NxM"')
|
| 282 |
+
if pixelformat is None:
|
| 283 |
+
pass
|
| 284 |
+
elif not isinstance(pixelformat, str):
|
| 285 |
+
raise ValueError("FFMPEG pixelformat must be str")
|
| 286 |
+
if dtype is None:
|
| 287 |
+
self._dtype = np.dtype("uint8")
|
| 288 |
+
else:
|
| 289 |
+
self._dtype = np.dtype(dtype)
|
| 290 |
+
allowed_dtypes = ["uint8", "uint16"]
|
| 291 |
+
if self._dtype.name not in allowed_dtypes:
|
| 292 |
+
raise ValueError(
|
| 293 |
+
"dtype must be one of: {}".format(", ".join(allowed_dtypes))
|
| 294 |
+
)
|
| 295 |
+
self._arg_pixelformat = pixelformat
|
| 296 |
+
self._arg_input_params = input_params or []
|
| 297 |
+
self._arg_output_params = output_params or []
|
| 298 |
+
self._arg_input_params += ffmpeg_params or [] # backward compat
|
| 299 |
+
# Write "_video"_arg - indicating webcam support
|
| 300 |
+
self.request._video = None
|
| 301 |
+
regex_match = re.match(r"<video(\d+)>", self.request.filename)
|
| 302 |
+
if regex_match:
|
| 303 |
+
self.request._video = self.request.filename
|
| 304 |
+
# Get local filename
|
| 305 |
+
if self.request._video:
|
| 306 |
+
index = int(regex_match.group(1))
|
| 307 |
+
self._filename = self._get_cam_inputname(index)
|
| 308 |
+
else:
|
| 309 |
+
self._filename = self.request.get_local_filename()
|
| 310 |
+
# When passed to ffmpeg on command line, carets need to be escaped.
|
| 311 |
+
self._filename = self._filename.replace("^", "^^")
|
| 312 |
+
# Determine pixel format and depth
|
| 313 |
+
self._depth = 3
|
| 314 |
+
if self._dtype.name == "uint8":
|
| 315 |
+
self._pix_fmt = "rgb24"
|
| 316 |
+
self._bytes_per_channel = 1
|
| 317 |
+
else:
|
| 318 |
+
self._pix_fmt = "rgb48le"
|
| 319 |
+
self._bytes_per_channel = 2
|
| 320 |
+
# Initialize parameters
|
| 321 |
+
self._pos = -1
|
| 322 |
+
self._meta = {"plugin": "ffmpeg"}
|
| 323 |
+
self._lastread = None
|
| 324 |
+
|
| 325 |
+
# Calculating this from fps and duration is not accurate,
|
| 326 |
+
# and calculating it exactly with ffmpeg_api.count_frames_and_secs
|
| 327 |
+
# takes too long to do for each video. But we need it for looping.
|
| 328 |
+
self._nframes = float("inf")
|
| 329 |
+
if self._arg_loop and not self.request._video:
|
| 330 |
+
self._nframes = self.count_frames()
|
| 331 |
+
self._meta["nframes"] = self._nframes
|
| 332 |
+
|
| 333 |
+
# Specify input framerate? (only on macOS)
|
| 334 |
+
# Ideally we'd get the supported framerate from the metadata, but we get the
|
| 335 |
+
# metadata when we boot ffmpeg ... maybe we could refactor this so we can
|
| 336 |
+
# get the metadata beforehand, but for now we'll just give it 2 tries on MacOS,
|
| 337 |
+
# one with fps 30 and one with fps 15.
|
| 338 |
+
need_fps = (
|
| 339 |
+
self.request._video
|
| 340 |
+
and platform.system().lower() == "darwin"
|
| 341 |
+
and "-framerate" not in str(self._arg_input_params)
|
| 342 |
+
)
|
| 343 |
+
if need_fps:
|
| 344 |
+
self._arg_input_params.extend(["-framerate", str(float(30))])
|
| 345 |
+
|
| 346 |
+
# Start ffmpeg subprocess and get meta information
|
| 347 |
+
try:
|
| 348 |
+
self._initialize()
|
| 349 |
+
except IndexError:
|
| 350 |
+
# Specify input framerate again, this time different.
|
| 351 |
+
if need_fps:
|
| 352 |
+
self._arg_input_params[-1] = str(float(15))
|
| 353 |
+
self._initialize()
|
| 354 |
+
else:
|
| 355 |
+
raise
|
| 356 |
+
|
| 357 |
+
# For cameras, create thread that keeps reading the images
|
| 358 |
+
if self.request._video:
|
| 359 |
+
self._frame_catcher = FrameCatcher(self._read_gen)
|
| 360 |
+
|
| 361 |
+
# For reference - but disabled, because it is inaccurate
|
| 362 |
+
# if self._meta["nframes"] == float("inf"):
|
| 363 |
+
# if self._meta.get("fps", 0) > 0:
|
| 364 |
+
# if self._meta.get("duration", 0) > 0:
|
| 365 |
+
# n = round(self._meta["duration"] * self._meta["fps"])
|
| 366 |
+
# self._meta["nframes"] = int(n)
|
| 367 |
+
|
| 368 |
+
def _close(self):
|
| 369 |
+
# First close the frame catcher, because we cannot close the gen
|
| 370 |
+
# if the frame catcher thread is using it
|
| 371 |
+
if self._frame_catcher is not None:
|
| 372 |
+
self._frame_catcher.stop_me()
|
| 373 |
+
self._frame_catcher = None
|
| 374 |
+
if self._read_gen is not None:
|
| 375 |
+
self._read_gen.close()
|
| 376 |
+
self._read_gen = None
|
| 377 |
+
|
| 378 |
+
def count_frames(self):
|
| 379 |
+
"""Count the number of frames. Note that this can take a few
|
| 380 |
+
seconds for large files. Also note that it counts the number
|
| 381 |
+
of frames in the original video and does not take a given fps
|
| 382 |
+
into account.
|
| 383 |
+
"""
|
| 384 |
+
# This would have been nice, but this does not work :(
|
| 385 |
+
# oargs = []
|
| 386 |
+
# if self.request.kwargs.get("fps", None):
|
| 387 |
+
# fps = float(self.request.kwargs["fps"])
|
| 388 |
+
# oargs += ["-r", "%.02f" % fps]
|
| 389 |
+
cf = self._ffmpeg_api.count_frames_and_secs
|
| 390 |
+
return cf(self._filename)[0]
|
| 391 |
+
|
| 392 |
+
def _get_length(self):
|
| 393 |
+
return self._nframes # only not inf if loop is True
|
| 394 |
+
|
| 395 |
+
def _get_data(self, index):
|
| 396 |
+
"""Reads a frame at index. Note for coders: getting an
|
| 397 |
+
arbitrary frame in the video with ffmpeg can be painfully
|
| 398 |
+
slow if some decoding has to be done. This function tries
|
| 399 |
+
to avoid fectching arbitrary frames whenever possible, by
|
| 400 |
+
moving between adjacent frames."""
|
| 401 |
+
# Modulo index (for looping)
|
| 402 |
+
if self._arg_loop and self._nframes < float("inf"):
|
| 403 |
+
index %= self._nframes
|
| 404 |
+
|
| 405 |
+
if index == self._pos:
|
| 406 |
+
return self._lastread, dict(new=False)
|
| 407 |
+
elif index < 0:
|
| 408 |
+
raise IndexError("Frame index must be >= 0")
|
| 409 |
+
elif index >= self._nframes:
|
| 410 |
+
raise IndexError("Reached end of video")
|
| 411 |
+
else:
|
| 412 |
+
if (index < self._pos) or (index > self._pos + 100):
|
| 413 |
+
self._initialize(index)
|
| 414 |
+
else:
|
| 415 |
+
self._skip_frames(index - self._pos - 1)
|
| 416 |
+
result, is_new = self._read_frame()
|
| 417 |
+
self._pos = index
|
| 418 |
+
return result, dict(new=is_new)
|
| 419 |
+
|
| 420 |
+
def _get_meta_data(self, index):
|
| 421 |
+
return self._meta
|
| 422 |
+
|
| 423 |
+
def _initialize(self, index=0):
|
| 424 |
+
|
| 425 |
+
# Close the current generator, and thereby terminate its subprocess
|
| 426 |
+
if self._read_gen is not None:
|
| 427 |
+
self._read_gen.close()
|
| 428 |
+
|
| 429 |
+
iargs = []
|
| 430 |
+
oargs = []
|
| 431 |
+
|
| 432 |
+
# Create input args
|
| 433 |
+
iargs += self._arg_input_params
|
| 434 |
+
if self.request._video:
|
| 435 |
+
iargs += ["-f", CAM_FORMAT]
|
| 436 |
+
if self._arg_pixelformat:
|
| 437 |
+
iargs += ["-pix_fmt", self._arg_pixelformat]
|
| 438 |
+
if self._arg_size:
|
| 439 |
+
iargs += ["-s", self._arg_size]
|
| 440 |
+
elif index > 0: # re-initialize / seek
|
| 441 |
+
# Note: only works if we initialized earlier, and now have meta
|
| 442 |
+
# Some info here: https://trac.ffmpeg.org/wiki/Seeking
|
| 443 |
+
# There are two ways to seek, one before -i (input_params) and
|
| 444 |
+
# after (output_params). The former is fast, because it uses
|
| 445 |
+
# keyframes, the latter is slow but accurate. According to
|
| 446 |
+
# the article above, the fast method should also be accurate
|
| 447 |
+
# from ffmpeg version 2.1, however in version 4.1 our tests
|
| 448 |
+
# start failing again. Not sure why, but we can solve this
|
| 449 |
+
# by combining slow and fast. Seek the long stretch using
|
| 450 |
+
# the fast method, and seek the last 10s the slow way.
|
| 451 |
+
starttime = index / self._meta["fps"]
|
| 452 |
+
seek_slow = min(10, starttime)
|
| 453 |
+
seek_fast = starttime - seek_slow
|
| 454 |
+
# We used to have this epsilon earlier, when we did not use
|
| 455 |
+
# the slow seek. I don't think we need it anymore.
|
| 456 |
+
# epsilon = -1 / self._meta["fps"] * 0.1
|
| 457 |
+
iargs += ["-ss", "%.06f" % (seek_fast)]
|
| 458 |
+
oargs += ["-ss", "%.06f" % (seek_slow)]
|
| 459 |
+
|
| 460 |
+
# Output args, for writing to pipe
|
| 461 |
+
if self._arg_size:
|
| 462 |
+
oargs += ["-s", self._arg_size]
|
| 463 |
+
if self.request.kwargs.get("fps", None):
|
| 464 |
+
fps = float(self.request.kwargs["fps"])
|
| 465 |
+
oargs += ["-r", "%.02f" % fps]
|
| 466 |
+
oargs += self._arg_output_params
|
| 467 |
+
|
| 468 |
+
# Get pixelformat and bytes per pixel
|
| 469 |
+
pix_fmt = self._pix_fmt
|
| 470 |
+
bpp = self._depth * self._bytes_per_channel
|
| 471 |
+
|
| 472 |
+
# Create generator
|
| 473 |
+
rf = self._ffmpeg_api.read_frames
|
| 474 |
+
self._read_gen = rf(
|
| 475 |
+
self._filename, pix_fmt, bpp, input_params=iargs, output_params=oargs
|
| 476 |
+
)
|
| 477 |
+
|
| 478 |
+
# Read meta data. This start the generator (and ffmpeg subprocess)
|
| 479 |
+
if self.request._video:
|
| 480 |
+
# With cameras, catch error and turn into IndexError
|
| 481 |
+
try:
|
| 482 |
+
meta = self._read_gen.__next__()
|
| 483 |
+
except IOError as err:
|
| 484 |
+
err_text = str(err)
|
| 485 |
+
if "darwin" in sys.platform:
|
| 486 |
+
if "Unknown input format: 'avfoundation'" in err_text:
|
| 487 |
+
err_text += (
|
| 488 |
+
"Try installing FFMPEG using "
|
| 489 |
+
"home brew to get a version with "
|
| 490 |
+
"support for cameras."
|
| 491 |
+
)
|
| 492 |
+
raise IndexError(
|
| 493 |
+
"No (working) camera at {}.\n\n{}".format(
|
| 494 |
+
self.request._video, err_text
|
| 495 |
+
)
|
| 496 |
+
)
|
| 497 |
+
else:
|
| 498 |
+
self._meta.update(meta)
|
| 499 |
+
elif index == 0:
|
| 500 |
+
self._meta.update(self._read_gen.__next__())
|
| 501 |
+
else:
|
| 502 |
+
self._read_gen.__next__() # we already have meta data
|
| 503 |
+
|
| 504 |
+
def _skip_frames(self, n=1):
|
| 505 |
+
"""Reads and throws away n frames"""
|
| 506 |
+
for i in range(n):
|
| 507 |
+
self._read_gen.__next__()
|
| 508 |
+
self._pos += n
|
| 509 |
+
|
| 510 |
+
def _read_frame(self):
|
| 511 |
+
# Read and convert to numpy array
|
| 512 |
+
w, h = self._meta["size"]
|
| 513 |
+
framesize = w * h * self._depth * self._bytes_per_channel
|
| 514 |
+
# t0 = time.time()
|
| 515 |
+
|
| 516 |
+
# Read frame
|
| 517 |
+
if self._frame_catcher: # pragma: no cover - camera thing
|
| 518 |
+
s, is_new = self._frame_catcher.get_frame()
|
| 519 |
+
else:
|
| 520 |
+
s = self._read_gen.__next__()
|
| 521 |
+
is_new = True
|
| 522 |
+
|
| 523 |
+
# Check
|
| 524 |
+
if len(s) != framesize:
|
| 525 |
+
raise RuntimeError(
|
| 526 |
+
"Frame is %i bytes, but expected %i." % (len(s), framesize)
|
| 527 |
+
)
|
| 528 |
+
|
| 529 |
+
result = np.frombuffer(s, dtype=self._dtype).copy()
|
| 530 |
+
result = result.reshape((h, w, self._depth))
|
| 531 |
+
# t1 = time.time()
|
| 532 |
+
# print('etime', t1-t0)
|
| 533 |
+
|
| 534 |
+
# Store and return
|
| 535 |
+
self._lastread = result
|
| 536 |
+
return result, is_new
|
| 537 |
+
|
| 538 |
+
# --
|
| 539 |
+
|
| 540 |
+
class Writer(Format.Writer):
|
| 541 |
+
|
| 542 |
+
_write_gen = None
|
| 543 |
+
|
| 544 |
+
def _open(
|
| 545 |
+
self,
|
| 546 |
+
fps=10,
|
| 547 |
+
codec="libx264",
|
| 548 |
+
bitrate=None,
|
| 549 |
+
pixelformat="yuv420p",
|
| 550 |
+
ffmpeg_params=None,
|
| 551 |
+
input_params=None,
|
| 552 |
+
output_params=None,
|
| 553 |
+
ffmpeg_log_level="quiet",
|
| 554 |
+
quality=5,
|
| 555 |
+
macro_block_size=16,
|
| 556 |
+
):
|
| 557 |
+
self._ffmpeg_api = _get_ffmpeg_api()
|
| 558 |
+
self._filename = self.request.get_local_filename()
|
| 559 |
+
self._pix_fmt = None
|
| 560 |
+
self._depth = None
|
| 561 |
+
self._size = None
|
| 562 |
+
|
| 563 |
+
def _close(self):
|
| 564 |
+
if self._write_gen is not None:
|
| 565 |
+
self._write_gen.close()
|
| 566 |
+
self._write_gen = None
|
| 567 |
+
|
| 568 |
+
def _append_data(self, im, meta):
|
| 569 |
+
|
| 570 |
+
# Get props of image
|
| 571 |
+
h, w = im.shape[:2]
|
| 572 |
+
size = w, h
|
| 573 |
+
depth = 1 if im.ndim == 2 else im.shape[2]
|
| 574 |
+
|
| 575 |
+
# Ensure that image is in uint8
|
| 576 |
+
im = image_as_uint(im, bitdepth=8)
|
| 577 |
+
# To be written efficiently, ie. without creating an immutable
|
| 578 |
+
# buffer, by calling im.tobytes() the array must be contiguous.
|
| 579 |
+
if not im.flags.c_contiguous:
|
| 580 |
+
# checkign the flag is a micro optimization.
|
| 581 |
+
# the image will be a numpy subclass. See discussion
|
| 582 |
+
# https://github.com/numpy/numpy/issues/11804
|
| 583 |
+
im = np.ascontiguousarray(im)
|
| 584 |
+
|
| 585 |
+
# Set size and initialize if not initialized yet
|
| 586 |
+
if self._size is None:
|
| 587 |
+
map = {1: "gray", 2: "gray8a", 3: "rgb24", 4: "rgba"}
|
| 588 |
+
self._pix_fmt = map.get(depth, None)
|
| 589 |
+
if self._pix_fmt is None:
|
| 590 |
+
raise ValueError("Image must have 1, 2, 3 or 4 channels")
|
| 591 |
+
self._size = size
|
| 592 |
+
self._depth = depth
|
| 593 |
+
self._initialize()
|
| 594 |
+
|
| 595 |
+
# Check size of image
|
| 596 |
+
if size != self._size:
|
| 597 |
+
raise ValueError("All images in a movie should have same size")
|
| 598 |
+
if depth != self._depth:
|
| 599 |
+
raise ValueError(
|
| 600 |
+
"All images in a movie should have same " "number of channels"
|
| 601 |
+
)
|
| 602 |
+
|
| 603 |
+
assert self._write_gen is not None # Check status
|
| 604 |
+
|
| 605 |
+
# Write. Yes, we can send the data in as a numpy array
|
| 606 |
+
self._write_gen.send(im)
|
| 607 |
+
|
| 608 |
+
def set_meta_data(self, meta):
|
| 609 |
+
raise RuntimeError(
|
| 610 |
+
"The ffmpeg format does not support setting " "meta data."
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
def _initialize(self):
|
| 614 |
+
|
| 615 |
+
# Close existing generator
|
| 616 |
+
if self._write_gen is not None:
|
| 617 |
+
self._write_gen.close()
|
| 618 |
+
|
| 619 |
+
# Get parameters
|
| 620 |
+
# Use None to let imageio-ffmpeg (or ffmpeg) select good results
|
| 621 |
+
fps = self.request.kwargs.get("fps", 10)
|
| 622 |
+
codec = self.request.kwargs.get("codec", None)
|
| 623 |
+
bitrate = self.request.kwargs.get("bitrate", None)
|
| 624 |
+
quality = self.request.kwargs.get("quality", None)
|
| 625 |
+
input_params = self.request.kwargs.get("input_params") or []
|
| 626 |
+
output_params = self.request.kwargs.get("output_params") or []
|
| 627 |
+
output_params += self.request.kwargs.get("ffmpeg_params") or []
|
| 628 |
+
pixelformat = self.request.kwargs.get("pixelformat", None)
|
| 629 |
+
macro_block_size = self.request.kwargs.get("macro_block_size", 16)
|
| 630 |
+
ffmpeg_log_level = self.request.kwargs.get("ffmpeg_log_level", None)
|
| 631 |
+
|
| 632 |
+
macro_block_size = macro_block_size or 1 # None -> 1
|
| 633 |
+
|
| 634 |
+
# Create generator
|
| 635 |
+
self._write_gen = self._ffmpeg_api.write_frames(
|
| 636 |
+
self._filename,
|
| 637 |
+
self._size,
|
| 638 |
+
pix_fmt_in=self._pix_fmt,
|
| 639 |
+
pix_fmt_out=pixelformat,
|
| 640 |
+
fps=fps,
|
| 641 |
+
quality=quality,
|
| 642 |
+
bitrate=bitrate,
|
| 643 |
+
codec=codec,
|
| 644 |
+
macro_block_size=macro_block_size,
|
| 645 |
+
ffmpeg_log_level=ffmpeg_log_level,
|
| 646 |
+
input_params=input_params,
|
| 647 |
+
output_params=output_params,
|
| 648 |
+
)
|
| 649 |
+
|
| 650 |
+
# Seed the generator (this is where the ffmpeg subprocess starts)
|
| 651 |
+
self._write_gen.send(None)
|
| 652 |
+
|
| 653 |
+
|
| 654 |
+
class FrameCatcher(threading.Thread):
|
| 655 |
+
"""Thread to keep reading the frame data from stdout. This is
|
| 656 |
+
useful when streaming from a webcam. Otherwise, if the user code
|
| 657 |
+
does not grab frames fast enough, the buffer will fill up, leading
|
| 658 |
+
to lag, and ffmpeg can also stall (experienced on Linux). The
|
| 659 |
+
get_frame() method always returns the last available image.
|
| 660 |
+
"""
|
| 661 |
+
|
| 662 |
+
def __init__(self, gen):
|
| 663 |
+
self._gen = gen
|
| 664 |
+
self._frame = None
|
| 665 |
+
self._frame_is_new = False
|
| 666 |
+
self._lock = threading.RLock()
|
| 667 |
+
threading.Thread.__init__(self)
|
| 668 |
+
self.daemon = True # do not let this thread hold up Python shutdown
|
| 669 |
+
self._should_stop = False
|
| 670 |
+
self.start()
|
| 671 |
+
|
| 672 |
+
def stop_me(self):
|
| 673 |
+
self._should_stop = True
|
| 674 |
+
while self.is_alive():
|
| 675 |
+
time.sleep(0.001)
|
| 676 |
+
|
| 677 |
+
def get_frame(self):
|
| 678 |
+
while self._frame is None: # pragma: no cover - an init thing
|
| 679 |
+
time.sleep(0.001)
|
| 680 |
+
with self._lock:
|
| 681 |
+
is_new = self._frame_is_new
|
| 682 |
+
self._frame_is_new = False # reset
|
| 683 |
+
return self._frame, is_new
|
| 684 |
+
|
| 685 |
+
def run(self):
|
| 686 |
+
# This runs in the worker thread
|
| 687 |
+
try:
|
| 688 |
+
while not self._should_stop:
|
| 689 |
+
time.sleep(0) # give control to other threads
|
| 690 |
+
frame = self._gen.__next__()
|
| 691 |
+
with self._lock:
|
| 692 |
+
self._frame = frame
|
| 693 |
+
self._frame_is_new = True
|
| 694 |
+
except (StopIteration, EOFError):
|
| 695 |
+
pass
|
| 696 |
+
|
| 697 |
+
|
| 698 |
+
def parse_device_names(ffmpeg_output):
|
| 699 |
+
"""Parse the output of the ffmpeg -list-devices command"""
|
| 700 |
+
# Collect device names - get [friendly_name, alt_name] of each
|
| 701 |
+
device_names = []
|
| 702 |
+
in_video_devices = False
|
| 703 |
+
for line in ffmpeg_output.splitlines():
|
| 704 |
+
if line.startswith("[dshow"):
|
| 705 |
+
logger.debug(line)
|
| 706 |
+
line = line.split("]", 1)[1].strip()
|
| 707 |
+
if in_video_devices and line.startswith('"'):
|
| 708 |
+
friendly_name = line[1:-1]
|
| 709 |
+
device_names.append([friendly_name, ""])
|
| 710 |
+
elif in_video_devices and line.lower().startswith("alternative name"):
|
| 711 |
+
alt_name = line.split(" name ", 1)[1].strip()[1:-1]
|
| 712 |
+
if sys.platform.startswith("win"):
|
| 713 |
+
alt_name = alt_name.replace("&", "^&") # Tested to work
|
| 714 |
+
else:
|
| 715 |
+
alt_name = alt_name.replace("&", "\\&") # Does this work?
|
| 716 |
+
device_names[-1][-1] = alt_name
|
| 717 |
+
elif "video devices" in line:
|
| 718 |
+
in_video_devices = True
|
| 719 |
+
elif "devices" in line:
|
| 720 |
+
# set False for subsequent "devices" sections
|
| 721 |
+
in_video_devices = False
|
| 722 |
+
# Post-process, see #441
|
| 723 |
+
# prefer friendly names, use alt name if two cams have same friendly name
|
| 724 |
+
device_names2 = []
|
| 725 |
+
for friendly_name, alt_name in device_names:
|
| 726 |
+
if friendly_name not in device_names2:
|
| 727 |
+
device_names2.append(friendly_name)
|
| 728 |
+
elif alt_name:
|
| 729 |
+
device_names2.append(alt_name)
|
| 730 |
+
else:
|
| 731 |
+
device_names2.append(friendly_name) # duplicate, but not much we can do
|
| 732 |
+
return device_names2
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/fits.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
"""Read FITS files.
|
| 5 |
+
|
| 6 |
+
Backend Library: `Astropy <https://www.astropy.org/>`_
|
| 7 |
+
|
| 8 |
+
.. note::
|
| 9 |
+
To use this plugin you have to install its backend::
|
| 10 |
+
|
| 11 |
+
pip install imageio[fits]
|
| 12 |
+
|
| 13 |
+
Flexible Image Transport System (FITS) is an open standard defining a
|
| 14 |
+
digital file format useful for storage, transmission and processing of
|
| 15 |
+
scientific and other images. FITS is the most commonly used digital
|
| 16 |
+
file format in astronomy.
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
Parameters
|
| 20 |
+
----------
|
| 21 |
+
cache : bool
|
| 22 |
+
If the file name is a URL, `~astropy.utils.data.download_file` is used
|
| 23 |
+
to open the file. This specifies whether or not to save the file
|
| 24 |
+
locally in Astropy's download cache (default: `True`).
|
| 25 |
+
uint : bool
|
| 26 |
+
Interpret signed integer data where ``BZERO`` is the
|
| 27 |
+
central value and ``BSCALE == 1`` as unsigned integer
|
| 28 |
+
data. For example, ``int16`` data with ``BZERO = 32768``
|
| 29 |
+
and ``BSCALE = 1`` would be treated as ``uint16`` data.
|
| 30 |
+
|
| 31 |
+
Note, for backward compatibility, the kwarg **uint16** may
|
| 32 |
+
be used instead. The kwarg was renamed when support was
|
| 33 |
+
added for integers of any size.
|
| 34 |
+
ignore_missing_end : bool
|
| 35 |
+
Do not issue an exception when opening a file that is
|
| 36 |
+
missing an ``END`` card in the last header.
|
| 37 |
+
checksum : bool or str
|
| 38 |
+
If `True`, verifies that both ``DATASUM`` and
|
| 39 |
+
``CHECKSUM`` card values (when present in the HDU header)
|
| 40 |
+
match the header and data of all HDU's in the file. Updates to a
|
| 41 |
+
file that already has a checksum will preserve and update the
|
| 42 |
+
existing checksums unless this argument is given a value of
|
| 43 |
+
'remove', in which case the CHECKSUM and DATASUM values are not
|
| 44 |
+
checked, and are removed when saving changes to the file.
|
| 45 |
+
disable_image_compression : bool, optional
|
| 46 |
+
If `True`, treats compressed image HDU's like normal
|
| 47 |
+
binary table HDU's.
|
| 48 |
+
do_not_scale_image_data : bool
|
| 49 |
+
If `True`, image data is not scaled using BSCALE/BZERO values
|
| 50 |
+
when read.
|
| 51 |
+
ignore_blank : bool
|
| 52 |
+
If `True`, the BLANK keyword is ignored if present.
|
| 53 |
+
scale_back : bool
|
| 54 |
+
If `True`, when saving changes to a file that contained scaled
|
| 55 |
+
image data, restore the data to the original type and reapply the
|
| 56 |
+
original BSCALE/BZERO values. This could lead to loss of accuracy
|
| 57 |
+
if scaling back to integer values after performing floating point
|
| 58 |
+
operations on the data.
|
| 59 |
+
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
from ..core import Format
|
| 63 |
+
|
| 64 |
+
_fits = None # lazily loaded
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def load_lib():
|
| 68 |
+
global _fits
|
| 69 |
+
try:
|
| 70 |
+
from astropy.io import fits as _fits
|
| 71 |
+
except ImportError:
|
| 72 |
+
raise ImportError(
|
| 73 |
+
"The FITS format relies on the astropy package."
|
| 74 |
+
"Please refer to http://www.astropy.org/ "
|
| 75 |
+
"for further instructions."
|
| 76 |
+
)
|
| 77 |
+
return _fits
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class FitsFormat(Format):
|
| 81 |
+
"""See :mod:`imageio.plugins.fits`"""
|
| 82 |
+
|
| 83 |
+
def _can_read(self, request):
|
| 84 |
+
# We return True if ext matches, because this is the only plugin
|
| 85 |
+
# that can. If astropy is not installed, a useful error follows.
|
| 86 |
+
return request.extension in self.extensions
|
| 87 |
+
|
| 88 |
+
def _can_write(self, request):
|
| 89 |
+
# No write support
|
| 90 |
+
return False
|
| 91 |
+
|
| 92 |
+
# -- reader
|
| 93 |
+
|
| 94 |
+
class Reader(Format.Reader):
|
| 95 |
+
def _open(self, cache=False, **kwargs):
|
| 96 |
+
if not _fits:
|
| 97 |
+
load_lib()
|
| 98 |
+
hdulist = _fits.open(self.request.get_file(), cache=cache, **kwargs)
|
| 99 |
+
|
| 100 |
+
self._index = []
|
| 101 |
+
allowed_hdu_types = (_fits.ImageHDU, _fits.PrimaryHDU, _fits.CompImageHDU)
|
| 102 |
+
for n, hdu in zip(range(len(hdulist)), hdulist):
|
| 103 |
+
if isinstance(hdu, allowed_hdu_types):
|
| 104 |
+
# Ignore (primary) header units with no data (use '.size'
|
| 105 |
+
# rather than '.data' to avoid actually loading the image):
|
| 106 |
+
if hdu.size > 0:
|
| 107 |
+
self._index.append(n)
|
| 108 |
+
self._hdulist = hdulist
|
| 109 |
+
|
| 110 |
+
def _close(self):
|
| 111 |
+
self._hdulist.close()
|
| 112 |
+
|
| 113 |
+
def _get_length(self):
|
| 114 |
+
return len(self._index)
|
| 115 |
+
|
| 116 |
+
def _get_data(self, index):
|
| 117 |
+
# Get data
|
| 118 |
+
if index < 0 or index >= len(self._index):
|
| 119 |
+
raise IndexError("Index out of range while reading from fits")
|
| 120 |
+
im = self._hdulist[self._index[index]].data
|
| 121 |
+
# Return array and empty meta data
|
| 122 |
+
return im, {}
|
| 123 |
+
|
| 124 |
+
def _get_meta_data(self, index):
|
| 125 |
+
# Get the meta data for the given index
|
| 126 |
+
raise RuntimeError("The fits format does not support meta data.")
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/freeimage.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
"""Read/Write images using FreeImage.
|
| 5 |
+
|
| 6 |
+
Backend Library: `FreeImage <https://freeimage.sourceforge.io/>`_
|
| 7 |
+
|
| 8 |
+
.. note::
|
| 9 |
+
To use this plugin you have to install its backend::
|
| 10 |
+
|
| 11 |
+
imageio_download_bin freeimage
|
| 12 |
+
|
| 13 |
+
or you can download the backend using the function::
|
| 14 |
+
|
| 15 |
+
imageio.plugins.freeimage.download()
|
| 16 |
+
|
| 17 |
+
Each Freeimage format has the ``flags`` keyword argument. See the `Freeimage
|
| 18 |
+
documentation <https://freeimage.sourceforge.io/>`_ for more information.
|
| 19 |
+
|
| 20 |
+
Parameters
|
| 21 |
+
----------
|
| 22 |
+
flags : int
|
| 23 |
+
A freeimage-specific option. In most cases we provide explicit
|
| 24 |
+
parameters for influencing image reading.
|
| 25 |
+
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
import numpy as np
|
| 29 |
+
|
| 30 |
+
from ..core import Format, image_as_uint
|
| 31 |
+
from ..core.request import RETURN_BYTES
|
| 32 |
+
from ._freeimage import fi, download, IO_FLAGS, FNAME_PER_PLATFORM # noqa
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# todo: support files with only meta data
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class FreeimageFormat(Format):
|
| 39 |
+
"""See :mod:`imageio.plugins.freeimage`"""
|
| 40 |
+
|
| 41 |
+
_modes = "i"
|
| 42 |
+
|
| 43 |
+
def __init__(self, name, description, extensions=None, modes=None, *, fif=None):
|
| 44 |
+
super().__init__(name, description, extensions=extensions, modes=modes)
|
| 45 |
+
self._fif = fif
|
| 46 |
+
|
| 47 |
+
@property
|
| 48 |
+
def fif(self):
|
| 49 |
+
return self._fif # Set when format is created
|
| 50 |
+
|
| 51 |
+
def _can_read(self, request):
|
| 52 |
+
# Ask freeimage if it can read it, maybe ext missing
|
| 53 |
+
if fi.has_lib():
|
| 54 |
+
if not hasattr(request, "_fif"):
|
| 55 |
+
try:
|
| 56 |
+
request._fif = fi.getFIF(request.filename, "r", request.firstbytes)
|
| 57 |
+
except Exception: # pragma: no cover
|
| 58 |
+
request._fif = -1
|
| 59 |
+
if request._fif == self.fif:
|
| 60 |
+
return True
|
| 61 |
+
elif request._fif == 7 and self.fif == 14:
|
| 62 |
+
# PPM gets identified as PBM and PPM can read PBM
|
| 63 |
+
# see: https://github.com/imageio/imageio/issues/677
|
| 64 |
+
return True
|
| 65 |
+
|
| 66 |
+
def _can_write(self, request):
|
| 67 |
+
# Ask freeimage, because we are not aware of all formats
|
| 68 |
+
if fi.has_lib():
|
| 69 |
+
if not hasattr(request, "_fif"):
|
| 70 |
+
try:
|
| 71 |
+
request._fif = fi.getFIF(request.filename, "w")
|
| 72 |
+
except ValueError: # pragma: no cover
|
| 73 |
+
if request.raw_uri == RETURN_BYTES:
|
| 74 |
+
request._fif = self.fif
|
| 75 |
+
else:
|
| 76 |
+
request._fif = -1
|
| 77 |
+
if request._fif is self.fif:
|
| 78 |
+
return True
|
| 79 |
+
|
| 80 |
+
# --
|
| 81 |
+
|
| 82 |
+
class Reader(Format.Reader):
|
| 83 |
+
def _get_length(self):
|
| 84 |
+
return 1
|
| 85 |
+
|
| 86 |
+
def _open(self, flags=0):
|
| 87 |
+
self._bm = fi.create_bitmap(self.request.filename, self.format.fif, flags)
|
| 88 |
+
self._bm.load_from_filename(self.request.get_local_filename())
|
| 89 |
+
|
| 90 |
+
def _close(self):
|
| 91 |
+
self._bm.close()
|
| 92 |
+
|
| 93 |
+
def _get_data(self, index):
|
| 94 |
+
if index != 0:
|
| 95 |
+
raise IndexError("This format only supports singleton images.")
|
| 96 |
+
return self._bm.get_image_data(), self._bm.get_meta_data()
|
| 97 |
+
|
| 98 |
+
def _get_meta_data(self, index):
|
| 99 |
+
if not (index is None or index == 0):
|
| 100 |
+
raise IndexError()
|
| 101 |
+
return self._bm.get_meta_data()
|
| 102 |
+
|
| 103 |
+
# --
|
| 104 |
+
|
| 105 |
+
class Writer(Format.Writer):
|
| 106 |
+
def _open(self, flags=0):
|
| 107 |
+
self._flags = flags # Store flags for later use
|
| 108 |
+
self._bm = None
|
| 109 |
+
self._is_set = False # To prevent appending more than one image
|
| 110 |
+
self._meta = {}
|
| 111 |
+
|
| 112 |
+
def _close(self):
|
| 113 |
+
# Set global meta data
|
| 114 |
+
self._bm.set_meta_data(self._meta)
|
| 115 |
+
# Write and close
|
| 116 |
+
self._bm.save_to_filename(self.request.get_local_filename())
|
| 117 |
+
self._bm.close()
|
| 118 |
+
|
| 119 |
+
def _append_data(self, im, meta):
|
| 120 |
+
# Check if set
|
| 121 |
+
if not self._is_set:
|
| 122 |
+
self._is_set = True
|
| 123 |
+
else:
|
| 124 |
+
raise RuntimeError(
|
| 125 |
+
"Singleton image; " "can only append image data once."
|
| 126 |
+
)
|
| 127 |
+
# Pop unit dimension for grayscale images
|
| 128 |
+
if im.ndim == 3 and im.shape[-1] == 1:
|
| 129 |
+
im = im[:, :, 0]
|
| 130 |
+
# Lazy instantaion of the bitmap, we need image data
|
| 131 |
+
if self._bm is None:
|
| 132 |
+
self._bm = fi.create_bitmap(
|
| 133 |
+
self.request.filename, self.format.fif, self._flags
|
| 134 |
+
)
|
| 135 |
+
self._bm.allocate(im)
|
| 136 |
+
# Set data
|
| 137 |
+
self._bm.set_image_data(im)
|
| 138 |
+
# There is no distinction between global and per-image meta data
|
| 139 |
+
# for singleton images
|
| 140 |
+
self._meta = meta
|
| 141 |
+
|
| 142 |
+
def _set_meta_data(self, meta):
|
| 143 |
+
self._meta = meta
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
# Special plugins
|
| 147 |
+
|
| 148 |
+
# todo: there is also FIF_LOAD_NOPIXELS,
|
| 149 |
+
# but perhaps that should be used with get_meta_data.
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class FreeimageBmpFormat(FreeimageFormat):
|
| 153 |
+
"""A BMP format based on the Freeimage library.
|
| 154 |
+
|
| 155 |
+
This format supports grayscale, RGB and RGBA images.
|
| 156 |
+
|
| 157 |
+
The freeimage plugin requires a `freeimage` binary. If this binary
|
| 158 |
+
not available on the system, it can be downloaded manually from
|
| 159 |
+
<https://github.com/imageio/imageio-binaries> by either
|
| 160 |
+
|
| 161 |
+
- the command line script ``imageio_download_bin freeimage``
|
| 162 |
+
- the Python method ``imageio.plugins.freeimage.download()``
|
| 163 |
+
|
| 164 |
+
Parameters for saving
|
| 165 |
+
---------------------
|
| 166 |
+
compression : bool
|
| 167 |
+
Whether to compress the bitmap using RLE when saving. Default False.
|
| 168 |
+
It seems this does not always work, but who cares, you should use
|
| 169 |
+
PNG anyway.
|
| 170 |
+
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
class Writer(FreeimageFormat.Writer):
|
| 174 |
+
def _open(self, flags=0, compression=False):
|
| 175 |
+
# Build flags from kwargs
|
| 176 |
+
flags = int(flags)
|
| 177 |
+
if compression:
|
| 178 |
+
flags |= IO_FLAGS.BMP_SAVE_RLE
|
| 179 |
+
else:
|
| 180 |
+
flags |= IO_FLAGS.BMP_DEFAULT
|
| 181 |
+
# Act as usual, but with modified flags
|
| 182 |
+
return FreeimageFormat.Writer._open(self, flags)
|
| 183 |
+
|
| 184 |
+
def _append_data(self, im, meta):
|
| 185 |
+
im = image_as_uint(im, bitdepth=8)
|
| 186 |
+
return FreeimageFormat.Writer._append_data(self, im, meta)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
class FreeimagePngFormat(FreeimageFormat):
|
| 190 |
+
"""A PNG format based on the Freeimage library.
|
| 191 |
+
|
| 192 |
+
This format supports grayscale, RGB and RGBA images.
|
| 193 |
+
|
| 194 |
+
The freeimage plugin requires a `freeimage` binary. If this binary
|
| 195 |
+
not available on the system, it can be downloaded manually from
|
| 196 |
+
<https://github.com/imageio/imageio-binaries> by either
|
| 197 |
+
|
| 198 |
+
- the command line script ``imageio_download_bin freeimage``
|
| 199 |
+
- the Python method ``imageio.plugins.freeimage.download()``
|
| 200 |
+
|
| 201 |
+
Parameters for reading
|
| 202 |
+
----------------------
|
| 203 |
+
ignoregamma : bool
|
| 204 |
+
Avoid gamma correction. Default True.
|
| 205 |
+
|
| 206 |
+
Parameters for saving
|
| 207 |
+
---------------------
|
| 208 |
+
compression : {0, 1, 6, 9}
|
| 209 |
+
The compression factor. Higher factors result in more
|
| 210 |
+
compression at the cost of speed. Note that PNG compression is
|
| 211 |
+
always lossless. Default 9.
|
| 212 |
+
quantize : int
|
| 213 |
+
If specified, turn the given RGB or RGBA image in a paletted image
|
| 214 |
+
for more efficient storage. The value should be between 2 and 256.
|
| 215 |
+
If the value of 0 the image is not quantized.
|
| 216 |
+
interlaced : bool
|
| 217 |
+
Save using Adam7 interlacing. Default False.
|
| 218 |
+
"""
|
| 219 |
+
|
| 220 |
+
class Reader(FreeimageFormat.Reader):
|
| 221 |
+
def _open(self, flags=0, ignoregamma=True):
|
| 222 |
+
# Build flags from kwargs
|
| 223 |
+
flags = int(flags)
|
| 224 |
+
if ignoregamma:
|
| 225 |
+
flags |= IO_FLAGS.PNG_IGNOREGAMMA
|
| 226 |
+
# Enter as usual, with modified flags
|
| 227 |
+
return FreeimageFormat.Reader._open(self, flags)
|
| 228 |
+
|
| 229 |
+
# --
|
| 230 |
+
|
| 231 |
+
class Writer(FreeimageFormat.Writer):
|
| 232 |
+
def _open(self, flags=0, compression=9, quantize=0, interlaced=False):
|
| 233 |
+
compression_map = {
|
| 234 |
+
0: IO_FLAGS.PNG_Z_NO_COMPRESSION,
|
| 235 |
+
1: IO_FLAGS.PNG_Z_BEST_SPEED,
|
| 236 |
+
6: IO_FLAGS.PNG_Z_DEFAULT_COMPRESSION,
|
| 237 |
+
9: IO_FLAGS.PNG_Z_BEST_COMPRESSION,
|
| 238 |
+
}
|
| 239 |
+
# Build flags from kwargs
|
| 240 |
+
flags = int(flags)
|
| 241 |
+
if interlaced:
|
| 242 |
+
flags |= IO_FLAGS.PNG_INTERLACED
|
| 243 |
+
try:
|
| 244 |
+
flags |= compression_map[compression]
|
| 245 |
+
except KeyError:
|
| 246 |
+
raise ValueError("Png compression must be 0, 1, 6, or 9.")
|
| 247 |
+
# Act as usual, but with modified flags
|
| 248 |
+
return FreeimageFormat.Writer._open(self, flags)
|
| 249 |
+
|
| 250 |
+
def _append_data(self, im, meta):
|
| 251 |
+
if str(im.dtype) == "uint16":
|
| 252 |
+
im = image_as_uint(im, bitdepth=16)
|
| 253 |
+
else:
|
| 254 |
+
im = image_as_uint(im, bitdepth=8)
|
| 255 |
+
FreeimageFormat.Writer._append_data(self, im, meta)
|
| 256 |
+
# Quantize?
|
| 257 |
+
q = int(self.request.kwargs.get("quantize", False))
|
| 258 |
+
if not q:
|
| 259 |
+
pass
|
| 260 |
+
elif not (im.ndim == 3 and im.shape[-1] == 3):
|
| 261 |
+
raise ValueError("Can only quantize RGB images")
|
| 262 |
+
elif q < 2 or q > 256:
|
| 263 |
+
raise ValueError("PNG quantize param must be 2..256")
|
| 264 |
+
else:
|
| 265 |
+
bm = self._bm.quantize(0, q)
|
| 266 |
+
self._bm.close()
|
| 267 |
+
self._bm = bm
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
class FreeimageJpegFormat(FreeimageFormat):
|
| 271 |
+
"""A JPEG format based on the Freeimage library.
|
| 272 |
+
|
| 273 |
+
This format supports grayscale and RGB images.
|
| 274 |
+
|
| 275 |
+
The freeimage plugin requires a `freeimage` binary. If this binary
|
| 276 |
+
not available on the system, it can be downloaded manually from
|
| 277 |
+
<https://github.com/imageio/imageio-binaries> by either
|
| 278 |
+
|
| 279 |
+
- the command line script ``imageio_download_bin freeimage``
|
| 280 |
+
- the Python method ``imageio.plugins.freeimage.download()``
|
| 281 |
+
|
| 282 |
+
Parameters for reading
|
| 283 |
+
----------------------
|
| 284 |
+
exifrotate : bool
|
| 285 |
+
Automatically rotate the image according to the exif flag.
|
| 286 |
+
Default True. If 2 is given, do the rotation in Python instead
|
| 287 |
+
of freeimage.
|
| 288 |
+
quickread : bool
|
| 289 |
+
Read the image more quickly, at the expense of quality.
|
| 290 |
+
Default False.
|
| 291 |
+
|
| 292 |
+
Parameters for saving
|
| 293 |
+
---------------------
|
| 294 |
+
quality : scalar
|
| 295 |
+
The compression factor of the saved image (1..100), higher
|
| 296 |
+
numbers result in higher quality but larger file size. Default 75.
|
| 297 |
+
progressive : bool
|
| 298 |
+
Save as a progressive JPEG file (e.g. for images on the web).
|
| 299 |
+
Default False.
|
| 300 |
+
optimize : bool
|
| 301 |
+
On saving, compute optimal Huffman coding tables (can reduce a
|
| 302 |
+
few percent of file size). Default False.
|
| 303 |
+
baseline : bool
|
| 304 |
+
Save basic JPEG, without metadata or any markers. Default False.
|
| 305 |
+
|
| 306 |
+
"""
|
| 307 |
+
|
| 308 |
+
class Reader(FreeimageFormat.Reader):
|
| 309 |
+
def _open(self, flags=0, exifrotate=True, quickread=False):
|
| 310 |
+
# Build flags from kwargs
|
| 311 |
+
flags = int(flags)
|
| 312 |
+
if exifrotate and exifrotate != 2:
|
| 313 |
+
flags |= IO_FLAGS.JPEG_EXIFROTATE
|
| 314 |
+
if not quickread:
|
| 315 |
+
flags |= IO_FLAGS.JPEG_ACCURATE
|
| 316 |
+
# Enter as usual, with modified flags
|
| 317 |
+
return FreeimageFormat.Reader._open(self, flags)
|
| 318 |
+
|
| 319 |
+
def _get_data(self, index):
|
| 320 |
+
im, meta = FreeimageFormat.Reader._get_data(self, index)
|
| 321 |
+
im = self._rotate(im, meta)
|
| 322 |
+
return im, meta
|
| 323 |
+
|
| 324 |
+
def _rotate(self, im, meta):
|
| 325 |
+
"""Use Orientation information from EXIF meta data to
|
| 326 |
+
orient the image correctly. Freeimage is also supposed to
|
| 327 |
+
support that, and I am pretty sure it once did, but now it
|
| 328 |
+
does not, so let's just do it in Python.
|
| 329 |
+
Edit: and now it works again, just leave in place as a fallback.
|
| 330 |
+
"""
|
| 331 |
+
if self.request.kwargs.get("exifrotate", None) == 2:
|
| 332 |
+
try:
|
| 333 |
+
ori = meta["EXIF_MAIN"]["Orientation"]
|
| 334 |
+
except KeyError: # pragma: no cover
|
| 335 |
+
pass # Orientation not available
|
| 336 |
+
else: # pragma: no cover - we cannot touch all cases
|
| 337 |
+
# www.impulseadventure.com/photo/exif-orientation.html
|
| 338 |
+
if ori in [1, 2]:
|
| 339 |
+
pass
|
| 340 |
+
if ori in [3, 4]:
|
| 341 |
+
im = np.rot90(im, 2)
|
| 342 |
+
if ori in [5, 6]:
|
| 343 |
+
im = np.rot90(im, 3)
|
| 344 |
+
if ori in [7, 8]:
|
| 345 |
+
im = np.rot90(im)
|
| 346 |
+
if ori in [2, 4, 5, 7]: # Flipped cases (rare)
|
| 347 |
+
im = np.fliplr(im)
|
| 348 |
+
return im
|
| 349 |
+
|
| 350 |
+
# --
|
| 351 |
+
|
| 352 |
+
class Writer(FreeimageFormat.Writer):
|
| 353 |
+
def _open(
|
| 354 |
+
self, flags=0, quality=75, progressive=False, optimize=False, baseline=False
|
| 355 |
+
):
|
| 356 |
+
# Test quality
|
| 357 |
+
quality = int(quality)
|
| 358 |
+
if quality < 1 or quality > 100:
|
| 359 |
+
raise ValueError("JPEG quality should be between 1 and 100.")
|
| 360 |
+
# Build flags from kwargs
|
| 361 |
+
flags = int(flags)
|
| 362 |
+
flags |= quality
|
| 363 |
+
if progressive:
|
| 364 |
+
flags |= IO_FLAGS.JPEG_PROGRESSIVE
|
| 365 |
+
if optimize:
|
| 366 |
+
flags |= IO_FLAGS.JPEG_OPTIMIZE
|
| 367 |
+
if baseline:
|
| 368 |
+
flags |= IO_FLAGS.JPEG_BASELINE
|
| 369 |
+
# Act as usual, but with modified flags
|
| 370 |
+
return FreeimageFormat.Writer._open(self, flags)
|
| 371 |
+
|
| 372 |
+
def _append_data(self, im, meta):
|
| 373 |
+
if im.ndim == 3 and im.shape[-1] == 4:
|
| 374 |
+
raise IOError("JPEG does not support alpha channel.")
|
| 375 |
+
im = image_as_uint(im, bitdepth=8)
|
| 376 |
+
return FreeimageFormat.Writer._append_data(self, im, meta)
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
class FreeimagePnmFormat(FreeimageFormat):
|
| 380 |
+
"""A PNM format based on the Freeimage library.
|
| 381 |
+
|
| 382 |
+
This format supports single bit (PBM), grayscale (PGM) and RGB (PPM)
|
| 383 |
+
images, even with ASCII or binary coding.
|
| 384 |
+
|
| 385 |
+
The freeimage plugin requires a `freeimage` binary. If this binary
|
| 386 |
+
not available on the system, it can be downloaded manually from
|
| 387 |
+
<https://github.com/imageio/imageio-binaries> by either
|
| 388 |
+
|
| 389 |
+
- the command line script ``imageio_download_bin freeimage``
|
| 390 |
+
- the Python method ``imageio.plugins.freeimage.download()``
|
| 391 |
+
|
| 392 |
+
Parameters for saving
|
| 393 |
+
---------------------
|
| 394 |
+
use_ascii : bool
|
| 395 |
+
Save with ASCII coding. Default True.
|
| 396 |
+
"""
|
| 397 |
+
|
| 398 |
+
class Writer(FreeimageFormat.Writer):
|
| 399 |
+
def _open(self, flags=0, use_ascii=True):
|
| 400 |
+
# Build flags from kwargs
|
| 401 |
+
flags = int(flags)
|
| 402 |
+
if use_ascii:
|
| 403 |
+
flags |= IO_FLAGS.PNM_SAVE_ASCII
|
| 404 |
+
# Act as usual, but with modified flags
|
| 405 |
+
return FreeimageFormat.Writer._open(self, flags)
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/freeimagemulti.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
"""Plugin for multi-image freeimafe formats, like animated GIF and ico.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import logging
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
from ..core import Format, image_as_uint
|
| 11 |
+
from ._freeimage import fi, IO_FLAGS
|
| 12 |
+
from .freeimage import FreeimageFormat
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class FreeimageMulti(FreeimageFormat):
|
| 18 |
+
"""Base class for freeimage formats that support multiple images."""
|
| 19 |
+
|
| 20 |
+
_modes = "iI"
|
| 21 |
+
_fif = -1
|
| 22 |
+
|
| 23 |
+
class Reader(Format.Reader):
|
| 24 |
+
def _open(self, flags=0):
|
| 25 |
+
flags = int(flags)
|
| 26 |
+
# Create bitmap
|
| 27 |
+
self._bm = fi.create_multipage_bitmap(
|
| 28 |
+
self.request.filename, self.format.fif, flags
|
| 29 |
+
)
|
| 30 |
+
self._bm.load_from_filename(self.request.get_local_filename())
|
| 31 |
+
|
| 32 |
+
def _close(self):
|
| 33 |
+
self._bm.close()
|
| 34 |
+
|
| 35 |
+
def _get_length(self):
|
| 36 |
+
return len(self._bm)
|
| 37 |
+
|
| 38 |
+
def _get_data(self, index):
|
| 39 |
+
sub = self._bm.get_page(index)
|
| 40 |
+
try:
|
| 41 |
+
return sub.get_image_data(), sub.get_meta_data()
|
| 42 |
+
finally:
|
| 43 |
+
sub.close()
|
| 44 |
+
|
| 45 |
+
def _get_meta_data(self, index):
|
| 46 |
+
index = index or 0
|
| 47 |
+
if index < 0 or index >= len(self._bm):
|
| 48 |
+
raise IndexError()
|
| 49 |
+
sub = self._bm.get_page(index)
|
| 50 |
+
try:
|
| 51 |
+
return sub.get_meta_data()
|
| 52 |
+
finally:
|
| 53 |
+
sub.close()
|
| 54 |
+
|
| 55 |
+
# --
|
| 56 |
+
|
| 57 |
+
class Writer(FreeimageFormat.Writer):
|
| 58 |
+
def _open(self, flags=0):
|
| 59 |
+
# Set flags
|
| 60 |
+
self._flags = flags = int(flags)
|
| 61 |
+
# Instantiate multi-page bitmap
|
| 62 |
+
self._bm = fi.create_multipage_bitmap(
|
| 63 |
+
self.request.filename, self.format.fif, flags
|
| 64 |
+
)
|
| 65 |
+
self._bm.save_to_filename(self.request.get_local_filename())
|
| 66 |
+
|
| 67 |
+
def _close(self):
|
| 68 |
+
# Close bitmap
|
| 69 |
+
self._bm.close()
|
| 70 |
+
|
| 71 |
+
def _append_data(self, im, meta):
|
| 72 |
+
# Prepare data
|
| 73 |
+
if im.ndim == 3 and im.shape[-1] == 1:
|
| 74 |
+
im = im[:, :, 0]
|
| 75 |
+
im = image_as_uint(im, bitdepth=8)
|
| 76 |
+
# Create sub bitmap
|
| 77 |
+
sub1 = fi.create_bitmap(self._bm._filename, self.format.fif)
|
| 78 |
+
# Let subclass add data to bitmap, optionally return new
|
| 79 |
+
sub2 = self._append_bitmap(im, meta, sub1)
|
| 80 |
+
# Add
|
| 81 |
+
self._bm.append_bitmap(sub2)
|
| 82 |
+
sub2.close()
|
| 83 |
+
if sub1 is not sub2:
|
| 84 |
+
sub1.close()
|
| 85 |
+
|
| 86 |
+
def _append_bitmap(self, im, meta, bitmap):
|
| 87 |
+
# Set data
|
| 88 |
+
bitmap.allocate(im)
|
| 89 |
+
bitmap.set_image_data(im)
|
| 90 |
+
bitmap.set_meta_data(meta)
|
| 91 |
+
# Return that same bitmap
|
| 92 |
+
return bitmap
|
| 93 |
+
|
| 94 |
+
def _set_meta_data(self, meta):
|
| 95 |
+
pass # ignore global meta data
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class MngFormat(FreeimageMulti):
|
| 99 |
+
"""An Mng format based on the Freeimage library.
|
| 100 |
+
|
| 101 |
+
Read only. Seems broken.
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
_fif = 6
|
| 105 |
+
|
| 106 |
+
def _can_write(self, request): # pragma: no cover
|
| 107 |
+
return False
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class IcoFormat(FreeimageMulti):
|
| 111 |
+
"""An ICO format based on the Freeimage library.
|
| 112 |
+
|
| 113 |
+
This format supports grayscale, RGB and RGBA images.
|
| 114 |
+
|
| 115 |
+
The freeimage plugin requires a `freeimage` binary. If this binary
|
| 116 |
+
is not available on the system, it can be downloaded by either
|
| 117 |
+
|
| 118 |
+
- the command line script ``imageio_download_bin freeimage``
|
| 119 |
+
- the Python method ``imageio.plugins.freeimage.download()``
|
| 120 |
+
|
| 121 |
+
Parameters for reading
|
| 122 |
+
----------------------
|
| 123 |
+
makealpha : bool
|
| 124 |
+
Convert to 32-bit and create an alpha channel from the AND-
|
| 125 |
+
mask when loading. Default False. Note that this returns wrong
|
| 126 |
+
results if the image was already RGBA.
|
| 127 |
+
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
_fif = 1
|
| 131 |
+
|
| 132 |
+
class Reader(FreeimageMulti.Reader):
|
| 133 |
+
def _open(self, flags=0, makealpha=False):
|
| 134 |
+
# Build flags from kwargs
|
| 135 |
+
flags = int(flags)
|
| 136 |
+
if makealpha:
|
| 137 |
+
flags |= IO_FLAGS.ICO_MAKEALPHA
|
| 138 |
+
return FreeimageMulti.Reader._open(self, flags)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class GifFormat(FreeimageMulti):
|
| 142 |
+
"""A format for reading and writing static and animated GIF, based
|
| 143 |
+
on the Freeimage library.
|
| 144 |
+
|
| 145 |
+
Images read with this format are always RGBA. Currently,
|
| 146 |
+
the alpha channel is ignored when saving RGB images with this
|
| 147 |
+
format.
|
| 148 |
+
|
| 149 |
+
The freeimage plugin requires a `freeimage` binary. If this binary
|
| 150 |
+
is not available on the system, it can be downloaded by either
|
| 151 |
+
|
| 152 |
+
- the command line script ``imageio_download_bin freeimage``
|
| 153 |
+
- the Python method ``imageio.plugins.freeimage.download()``
|
| 154 |
+
|
| 155 |
+
Parameters for reading
|
| 156 |
+
----------------------
|
| 157 |
+
playback : bool
|
| 158 |
+
'Play' the GIF to generate each frame (as 32bpp) instead of
|
| 159 |
+
returning raw frame data when loading. Default True.
|
| 160 |
+
|
| 161 |
+
Parameters for saving
|
| 162 |
+
---------------------
|
| 163 |
+
loop : int
|
| 164 |
+
The number of iterations. Default 0 (meaning loop indefinitely)
|
| 165 |
+
duration : {float, list}
|
| 166 |
+
The duration (in seconds) of each frame. Either specify one value
|
| 167 |
+
that is used for all frames, or one value for each frame.
|
| 168 |
+
Note that in the GIF format the duration/delay is expressed in
|
| 169 |
+
hundredths of a second, which limits the precision of the duration.
|
| 170 |
+
fps : float
|
| 171 |
+
The number of frames per second. If duration is not given, the
|
| 172 |
+
duration for each frame is set to 1/fps. Default 10.
|
| 173 |
+
palettesize : int
|
| 174 |
+
The number of colors to quantize the image to. Is rounded to
|
| 175 |
+
the nearest power of two. Default 256.
|
| 176 |
+
quantizer : {'wu', 'nq'}
|
| 177 |
+
The quantization algorithm:
|
| 178 |
+
* wu - Wu, Xiaolin, Efficient Statistical Computations for
|
| 179 |
+
Optimal Color Quantization
|
| 180 |
+
* nq (neuqant) - Dekker A. H., Kohonen neural networks for
|
| 181 |
+
optimal color quantization
|
| 182 |
+
subrectangles : bool
|
| 183 |
+
If True, will try and optimize the GIF by storing only the
|
| 184 |
+
rectangular parts of each frame that change with respect to the
|
| 185 |
+
previous. Unfortunately, this option seems currently broken
|
| 186 |
+
because FreeImage does not handle DisposalMethod correctly.
|
| 187 |
+
Default False.
|
| 188 |
+
"""
|
| 189 |
+
|
| 190 |
+
_fif = 25
|
| 191 |
+
|
| 192 |
+
class Reader(FreeimageMulti.Reader):
|
| 193 |
+
def _open(self, flags=0, playback=True):
|
| 194 |
+
# Build flags from kwargs
|
| 195 |
+
flags = int(flags)
|
| 196 |
+
if playback:
|
| 197 |
+
flags |= IO_FLAGS.GIF_PLAYBACK
|
| 198 |
+
FreeimageMulti.Reader._open(self, flags)
|
| 199 |
+
|
| 200 |
+
def _get_data(self, index):
|
| 201 |
+
im, meta = FreeimageMulti.Reader._get_data(self, index)
|
| 202 |
+
# im = im[:, :, :3] # Drop alpha channel
|
| 203 |
+
return im, meta
|
| 204 |
+
|
| 205 |
+
# -- writer
|
| 206 |
+
|
| 207 |
+
class Writer(FreeimageMulti.Writer):
|
| 208 |
+
|
| 209 |
+
# todo: subrectangles
|
| 210 |
+
# todo: global palette
|
| 211 |
+
|
| 212 |
+
def _open(
|
| 213 |
+
self,
|
| 214 |
+
flags=0,
|
| 215 |
+
loop=0,
|
| 216 |
+
duration=None,
|
| 217 |
+
fps=10,
|
| 218 |
+
palettesize=256,
|
| 219 |
+
quantizer="Wu",
|
| 220 |
+
subrectangles=False,
|
| 221 |
+
):
|
| 222 |
+
# Check palettesize
|
| 223 |
+
if palettesize < 2 or palettesize > 256:
|
| 224 |
+
raise ValueError("GIF quantize param must be 2..256")
|
| 225 |
+
if palettesize not in [2, 4, 8, 16, 32, 64, 128, 256]:
|
| 226 |
+
palettesize = 2 ** int(np.log2(128) + 0.999)
|
| 227 |
+
logger.warning(
|
| 228 |
+
"Warning: palettesize (%r) modified to a factor of "
|
| 229 |
+
"two between 2-256." % palettesize
|
| 230 |
+
)
|
| 231 |
+
self._palettesize = palettesize
|
| 232 |
+
# Check quantizer
|
| 233 |
+
self._quantizer = {"wu": 0, "nq": 1}.get(quantizer.lower(), None)
|
| 234 |
+
if self._quantizer is None:
|
| 235 |
+
raise ValueError('Invalid quantizer, must be "wu" or "nq".')
|
| 236 |
+
# Check frametime
|
| 237 |
+
if duration is None:
|
| 238 |
+
self._frametime = [int(1000 / float(fps) + 0.5)]
|
| 239 |
+
elif isinstance(duration, list):
|
| 240 |
+
self._frametime = [int(1000 * d) for d in duration]
|
| 241 |
+
elif isinstance(duration, (float, int)):
|
| 242 |
+
self._frametime = [int(1000 * duration)]
|
| 243 |
+
else:
|
| 244 |
+
raise ValueError("Invalid value for duration: %r" % duration)
|
| 245 |
+
# Check subrectangles
|
| 246 |
+
self._subrectangles = bool(subrectangles)
|
| 247 |
+
self._prev_im = None
|
| 248 |
+
# Init
|
| 249 |
+
FreeimageMulti.Writer._open(self, flags)
|
| 250 |
+
# Set global meta data
|
| 251 |
+
self._meta = {}
|
| 252 |
+
self._meta["ANIMATION"] = {
|
| 253 |
+
# 'GlobalPalette': np.array([0]).astype(np.uint8),
|
| 254 |
+
"Loop": np.array([loop]).astype(np.uint32),
|
| 255 |
+
# 'LogicalWidth': np.array([x]).astype(np.uint16),
|
| 256 |
+
# 'LogicalHeight': np.array([x]).astype(np.uint16),
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
def _append_bitmap(self, im, meta, bitmap):
|
| 260 |
+
# Prepare meta data
|
| 261 |
+
meta = meta.copy()
|
| 262 |
+
meta_a = meta["ANIMATION"] = {}
|
| 263 |
+
# If this is the first frame, assign it our "global" meta data
|
| 264 |
+
if len(self._bm) == 0:
|
| 265 |
+
meta.update(self._meta)
|
| 266 |
+
meta_a = meta["ANIMATION"]
|
| 267 |
+
# Set frame time
|
| 268 |
+
index = len(self._bm)
|
| 269 |
+
if index < len(self._frametime):
|
| 270 |
+
ft = self._frametime[index]
|
| 271 |
+
else:
|
| 272 |
+
ft = self._frametime[-1]
|
| 273 |
+
meta_a["FrameTime"] = np.array([ft]).astype(np.uint32)
|
| 274 |
+
# Check array
|
| 275 |
+
if im.ndim == 3 and im.shape[-1] == 4:
|
| 276 |
+
im = im[:, :, :3]
|
| 277 |
+
# Process subrectangles
|
| 278 |
+
im_uncropped = im
|
| 279 |
+
if self._subrectangles and self._prev_im is not None:
|
| 280 |
+
im, xy = self._get_sub_rectangles(self._prev_im, im)
|
| 281 |
+
meta_a["DisposalMethod"] = np.array([1]).astype(np.uint8)
|
| 282 |
+
meta_a["FrameLeft"] = np.array([xy[0]]).astype(np.uint16)
|
| 283 |
+
meta_a["FrameTop"] = np.array([xy[1]]).astype(np.uint16)
|
| 284 |
+
self._prev_im = im_uncropped
|
| 285 |
+
# Set image data
|
| 286 |
+
sub2 = sub1 = bitmap
|
| 287 |
+
sub1.allocate(im)
|
| 288 |
+
sub1.set_image_data(im)
|
| 289 |
+
# Quantize it if its RGB
|
| 290 |
+
if im.ndim == 3 and im.shape[-1] == 3:
|
| 291 |
+
sub2 = sub1.quantize(self._quantizer, self._palettesize)
|
| 292 |
+
# If single image, omit animation data
|
| 293 |
+
if self.request.mode[1] == "i":
|
| 294 |
+
del meta["ANIMATION"]
|
| 295 |
+
# Set meta data and return
|
| 296 |
+
sub2.set_meta_data(meta)
|
| 297 |
+
return sub2
|
| 298 |
+
|
| 299 |
+
def _get_sub_rectangles(self, prev, im):
|
| 300 |
+
"""
|
| 301 |
+
Calculate the minimal rectangles that need updating each frame.
|
| 302 |
+
Returns a two-element tuple containing the cropped images and a
|
| 303 |
+
list of x-y positions.
|
| 304 |
+
"""
|
| 305 |
+
# Get difference, sum over colors
|
| 306 |
+
diff = np.abs(im - prev)
|
| 307 |
+
if diff.ndim == 3:
|
| 308 |
+
diff = diff.sum(2)
|
| 309 |
+
# Get begin and end for both dimensions
|
| 310 |
+
X = np.argwhere(diff.sum(0))
|
| 311 |
+
Y = np.argwhere(diff.sum(1))
|
| 312 |
+
# Get rect coordinates
|
| 313 |
+
if X.size and Y.size:
|
| 314 |
+
x0, x1 = int(X[0]), int(X[-1]) + 1
|
| 315 |
+
y0, y1 = int(Y[0]), int(Y[-1]) + 1
|
| 316 |
+
else: # No change ... make it minimal
|
| 317 |
+
x0, x1 = 0, 2
|
| 318 |
+
y0, y1 = 0, 2
|
| 319 |
+
# Cut out and return
|
| 320 |
+
return im[y0:y1, x0:x1], (x0, y0)
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/grab.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PIL-based formats to take screenshots and grab from the clipboard.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import threading
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
from ..core import Format
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class BaseGrabFormat(Format):
|
| 13 |
+
"""Base format for grab formats."""
|
| 14 |
+
|
| 15 |
+
_pillow_imported = False
|
| 16 |
+
_ImageGrab = None
|
| 17 |
+
|
| 18 |
+
def __init__(self, *args, **kwargs):
|
| 19 |
+
super(BaseGrabFormat, self).__init__(*args, **kwargs)
|
| 20 |
+
self._lock = threading.RLock()
|
| 21 |
+
|
| 22 |
+
def _can_write(self, request):
|
| 23 |
+
return False
|
| 24 |
+
|
| 25 |
+
def _init_pillow(self):
|
| 26 |
+
with self._lock:
|
| 27 |
+
if not self._pillow_imported:
|
| 28 |
+
self._pillow_imported = True # more like tried to import
|
| 29 |
+
import PIL
|
| 30 |
+
|
| 31 |
+
if not hasattr(PIL, "__version__"): # pragma: no cover
|
| 32 |
+
raise ImportError("Imageio Pillow requires " "Pillow, not PIL!")
|
| 33 |
+
try:
|
| 34 |
+
from PIL import ImageGrab
|
| 35 |
+
except ImportError:
|
| 36 |
+
return None
|
| 37 |
+
self._ImageGrab = ImageGrab
|
| 38 |
+
return self._ImageGrab
|
| 39 |
+
|
| 40 |
+
class Reader(Format.Reader):
|
| 41 |
+
def _open(self):
|
| 42 |
+
pass
|
| 43 |
+
|
| 44 |
+
def _close(self):
|
| 45 |
+
pass
|
| 46 |
+
|
| 47 |
+
def _get_data(self, index):
|
| 48 |
+
return self.format._get_data(index)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class ScreenGrabFormat(BaseGrabFormat):
|
| 52 |
+
"""The ScreenGrabFormat provided a means to grab screenshots using
|
| 53 |
+
the uri of "<screen>".
|
| 54 |
+
|
| 55 |
+
This functionality is provided via Pillow. Note that "<screen>" is
|
| 56 |
+
only supported on Windows and OS X.
|
| 57 |
+
|
| 58 |
+
Parameters for reading
|
| 59 |
+
----------------------
|
| 60 |
+
No parameters.
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
def _can_read(self, request):
|
| 64 |
+
if request.mode[1] not in "i?":
|
| 65 |
+
return False
|
| 66 |
+
if request.filename != "<screen>":
|
| 67 |
+
return False
|
| 68 |
+
return bool(self._init_pillow())
|
| 69 |
+
|
| 70 |
+
def _get_data(self, index):
|
| 71 |
+
ImageGrab = self._init_pillow()
|
| 72 |
+
assert ImageGrab
|
| 73 |
+
|
| 74 |
+
pil_im = ImageGrab.grab()
|
| 75 |
+
assert pil_im is not None
|
| 76 |
+
im = np.asarray(pil_im)
|
| 77 |
+
return im, {}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class ClipboardGrabFormat(BaseGrabFormat):
|
| 81 |
+
"""The ClipboardGrabFormat provided a means to grab image data from
|
| 82 |
+
the clipboard, using the uri "<clipboard>"
|
| 83 |
+
|
| 84 |
+
This functionality is provided via Pillow. Note that "<clipboard>" is
|
| 85 |
+
only supported on Windows.
|
| 86 |
+
|
| 87 |
+
Parameters for reading
|
| 88 |
+
----------------------
|
| 89 |
+
No parameters.
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
def _can_read(self, request):
|
| 93 |
+
if request.mode[1] not in "i?":
|
| 94 |
+
return False
|
| 95 |
+
if request.filename != "<clipboard>":
|
| 96 |
+
return False
|
| 97 |
+
return bool(self._init_pillow())
|
| 98 |
+
|
| 99 |
+
def _get_data(self, index):
|
| 100 |
+
ImageGrab = self._init_pillow()
|
| 101 |
+
assert ImageGrab
|
| 102 |
+
|
| 103 |
+
pil_im = ImageGrab.grabclipboard()
|
| 104 |
+
if pil_im is None:
|
| 105 |
+
raise RuntimeError(
|
| 106 |
+
"There seems to be no image data on the " "clipboard now."
|
| 107 |
+
)
|
| 108 |
+
im = np.asarray(pil_im)
|
| 109 |
+
return im, {}
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/lytro.py
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# Copyright (c) 2018, imageio contributors
|
| 3 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
""" Read LFR files (Lytro Illum).
|
| 7 |
+
|
| 8 |
+
Backend: internal
|
| 9 |
+
|
| 10 |
+
Plugin to read Lytro Illum .lfr and .raw files as produced
|
| 11 |
+
by the Lytro Illum light field camera. It is actually a collection
|
| 12 |
+
of plugins, each supporting slightly different keyword arguments
|
| 13 |
+
|
| 14 |
+
Parameters
|
| 15 |
+
----------
|
| 16 |
+
meta_only : bool
|
| 17 |
+
Whether to only read the metadata.
|
| 18 |
+
include_thumbnail : bool
|
| 19 |
+
(only for lytro-lfr and lytro-lfp)
|
| 20 |
+
Whether to include an image thumbnail in the metadata.
|
| 21 |
+
|
| 22 |
+
"""
|
| 23 |
+
#
|
| 24 |
+
#
|
| 25 |
+
# This code is based on work by
|
| 26 |
+
# David Uhlig and his lfr_reader
|
| 27 |
+
# (https://www.iiit.kit.edu/uhlig.php)
|
| 28 |
+
# Donald Dansereau and his Matlab LF Toolbox
|
| 29 |
+
# (http://dgd.vision/Tools/LFToolbox/)
|
| 30 |
+
# and Behnam Esfahbod and his Python LFP-Reader
|
| 31 |
+
# (https://github.com/behnam/python-lfp-reader/)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
import os
|
| 35 |
+
import json
|
| 36 |
+
import struct
|
| 37 |
+
import logging
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
import numpy as np
|
| 41 |
+
|
| 42 |
+
from ..core import Format
|
| 43 |
+
from ..v2 import imread
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
logger = logging.getLogger(__name__)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# Sensor size of Lytro Illum resp. Lytro F01 light field camera sensor
|
| 50 |
+
LYTRO_ILLUM_IMAGE_SIZE = (5368, 7728)
|
| 51 |
+
LYTRO_F01_IMAGE_SIZE = (3280, 3280)
|
| 52 |
+
|
| 53 |
+
# Parameter of lfr file format
|
| 54 |
+
HEADER_LENGTH = 12
|
| 55 |
+
SIZE_LENGTH = 4 # = 16 - header_length
|
| 56 |
+
SHA1_LENGTH = 45 # = len("sha1-") + (160 / 4)
|
| 57 |
+
PADDING_LENGTH = 35 # = (4*16) - header_length - size_length - sha1_length
|
| 58 |
+
DATA_CHUNKS_ILLUM = 11
|
| 59 |
+
DATA_CHUNKS_F01 = 3
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class LytroFormat(Format):
|
| 63 |
+
"""Base class for Lytro format.
|
| 64 |
+
The subclasses LytroLfrFormat, LytroLfpFormat, LytroIllumRawFormat and
|
| 65 |
+
LytroF01RawFormat implement the Lytro-LFR, Lytro-LFP and Lytro-RAW format
|
| 66 |
+
for the Illum and original F01 camera respectively.
|
| 67 |
+
Writing is not supported.
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
# Only single images are supported.
|
| 71 |
+
_modes = "i"
|
| 72 |
+
|
| 73 |
+
def _can_write(self, request):
|
| 74 |
+
# Writing of Lytro files is not supported
|
| 75 |
+
return False
|
| 76 |
+
|
| 77 |
+
# -- writer
|
| 78 |
+
|
| 79 |
+
class Writer(Format.Writer):
|
| 80 |
+
def _open(self, flags=0):
|
| 81 |
+
self._fp = self.request.get_file()
|
| 82 |
+
|
| 83 |
+
def _close(self):
|
| 84 |
+
# Close the reader.
|
| 85 |
+
# Note that the request object will close self._fp
|
| 86 |
+
pass
|
| 87 |
+
|
| 88 |
+
def _append_data(self, im, meta):
|
| 89 |
+
# Process the given data and meta data.
|
| 90 |
+
raise RuntimeError("The lytro format cannot write image data.")
|
| 91 |
+
|
| 92 |
+
def _set_meta_data(self, meta):
|
| 93 |
+
# Process the given meta data (global for all images)
|
| 94 |
+
# It is not mandatory to support this.
|
| 95 |
+
raise RuntimeError("The lytro format cannot write meta data.")
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class LytroIllumRawFormat(LytroFormat):
|
| 99 |
+
"""This is the Lytro Illum RAW format.
|
| 100 |
+
The raw format is a 10bit image format as used by the Lytro Illum
|
| 101 |
+
light field camera. The format will read the specified raw file and will
|
| 102 |
+
try to load a .txt or .json file with the associated meta data.
|
| 103 |
+
This format does not support writing.
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
Parameters for reading
|
| 107 |
+
----------------------
|
| 108 |
+
meta_only : bool
|
| 109 |
+
Whether to only read the metadata.
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
def _can_read(self, request):
|
| 113 |
+
# Check if mode and extensions are supported by the format
|
| 114 |
+
if request.mode[1] in (self.modes + "?"):
|
| 115 |
+
if request.extension in (".raw",):
|
| 116 |
+
return True
|
| 117 |
+
|
| 118 |
+
@staticmethod
|
| 119 |
+
def rearrange_bits(array):
|
| 120 |
+
# Do bit rearrangement for the 10-bit lytro raw format
|
| 121 |
+
# Normalize output to 1.0 as float64
|
| 122 |
+
t0 = array[0::5]
|
| 123 |
+
t1 = array[1::5]
|
| 124 |
+
t2 = array[2::5]
|
| 125 |
+
t3 = array[3::5]
|
| 126 |
+
lsb = array[4::5]
|
| 127 |
+
|
| 128 |
+
t0 = np.left_shift(t0, 2) + np.bitwise_and(lsb, 3)
|
| 129 |
+
t1 = np.left_shift(t1, 2) + np.right_shift(np.bitwise_and(lsb, 12), 2)
|
| 130 |
+
t2 = np.left_shift(t2, 2) + np.right_shift(np.bitwise_and(lsb, 48), 4)
|
| 131 |
+
t3 = np.left_shift(t3, 2) + np.right_shift(np.bitwise_and(lsb, 192), 6)
|
| 132 |
+
|
| 133 |
+
image = np.zeros(LYTRO_ILLUM_IMAGE_SIZE, dtype=np.uint16)
|
| 134 |
+
image[:, 0::4] = t0.reshape(
|
| 135 |
+
(LYTRO_ILLUM_IMAGE_SIZE[0], LYTRO_ILLUM_IMAGE_SIZE[1] // 4)
|
| 136 |
+
)
|
| 137 |
+
image[:, 1::4] = t1.reshape(
|
| 138 |
+
(LYTRO_ILLUM_IMAGE_SIZE[0], LYTRO_ILLUM_IMAGE_SIZE[1] // 4)
|
| 139 |
+
)
|
| 140 |
+
image[:, 2::4] = t2.reshape(
|
| 141 |
+
(LYTRO_ILLUM_IMAGE_SIZE[0], LYTRO_ILLUM_IMAGE_SIZE[1] // 4)
|
| 142 |
+
)
|
| 143 |
+
image[:, 3::4] = t3.reshape(
|
| 144 |
+
(LYTRO_ILLUM_IMAGE_SIZE[0], LYTRO_ILLUM_IMAGE_SIZE[1] // 4)
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
# Normalize data to 1.0 as 64-bit float.
|
| 148 |
+
# Division is by 1023 as the Lytro Illum saves 10-bit raw data.
|
| 149 |
+
return np.divide(image, 1023.0).astype(np.float64)
|
| 150 |
+
|
| 151 |
+
# -- reader
|
| 152 |
+
|
| 153 |
+
class Reader(Format.Reader):
|
| 154 |
+
def _open(self, meta_only=False):
|
| 155 |
+
self._file = self.request.get_file()
|
| 156 |
+
self._data = None
|
| 157 |
+
self._meta_only = meta_only
|
| 158 |
+
|
| 159 |
+
def _close(self):
|
| 160 |
+
# Close the reader.
|
| 161 |
+
# Note that the request object will close self._file
|
| 162 |
+
del self._data
|
| 163 |
+
|
| 164 |
+
def _get_length(self):
|
| 165 |
+
# Return the number of images.
|
| 166 |
+
return 1
|
| 167 |
+
|
| 168 |
+
def _get_data(self, index):
|
| 169 |
+
# Return the data and meta data for the given index
|
| 170 |
+
|
| 171 |
+
if index not in [0, "None"]:
|
| 172 |
+
raise IndexError("Lytro file contains only one dataset")
|
| 173 |
+
|
| 174 |
+
if not self._meta_only:
|
| 175 |
+
# Read all bytes
|
| 176 |
+
if self._data is None:
|
| 177 |
+
self._data = self._file.read()
|
| 178 |
+
|
| 179 |
+
# Read bytes from string and convert to uint16
|
| 180 |
+
raw = np.frombuffer(self._data, dtype=np.uint8).astype(np.uint16)
|
| 181 |
+
|
| 182 |
+
# Rearrange bits
|
| 183 |
+
img = LytroIllumRawFormat.rearrange_bits(raw)
|
| 184 |
+
|
| 185 |
+
else:
|
| 186 |
+
# Return empty image
|
| 187 |
+
img = np.array([])
|
| 188 |
+
|
| 189 |
+
# Return image and meta data
|
| 190 |
+
return img, self._get_meta_data(index=0)
|
| 191 |
+
|
| 192 |
+
def _get_meta_data(self, index):
|
| 193 |
+
# Get the meta data for the given index. If index is None, it
|
| 194 |
+
# should return the global meta data.
|
| 195 |
+
|
| 196 |
+
if index not in [0, None]:
|
| 197 |
+
raise IndexError("Lytro meta data file contains only one dataset")
|
| 198 |
+
|
| 199 |
+
# Try to read meta data from meta data file corresponding
|
| 200 |
+
# to the raw data file, extension in [.txt, .TXT, .json, .JSON]
|
| 201 |
+
filename_base = os.path.splitext(self.request.get_local_filename())[0]
|
| 202 |
+
|
| 203 |
+
meta_data = None
|
| 204 |
+
|
| 205 |
+
for ext in [".txt", ".TXT", ".json", ".JSON"]:
|
| 206 |
+
if os.path.isfile(filename_base + ext):
|
| 207 |
+
meta_data = json.load(open(filename_base + ext))
|
| 208 |
+
|
| 209 |
+
if meta_data is not None:
|
| 210 |
+
return meta_data
|
| 211 |
+
|
| 212 |
+
else:
|
| 213 |
+
logger.warning("No metadata file found for provided raw file.")
|
| 214 |
+
return {}
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
class LytroLfrFormat(LytroFormat):
|
| 218 |
+
"""This is the Lytro Illum LFR format.
|
| 219 |
+
The lfr is a image and meta data container format as used by the
|
| 220 |
+
Lytro Illum light field camera.
|
| 221 |
+
The format will read the specified lfr file.
|
| 222 |
+
This format does not support writing.
|
| 223 |
+
|
| 224 |
+
Parameters for reading
|
| 225 |
+
----------------------
|
| 226 |
+
meta_only : bool
|
| 227 |
+
Whether to only read the metadata.
|
| 228 |
+
include_thumbnail : bool
|
| 229 |
+
Whether to include an image thumbnail in the metadata.
|
| 230 |
+
"""
|
| 231 |
+
|
| 232 |
+
def _can_read(self, request):
|
| 233 |
+
# Check if mode and extensions are supported by the format
|
| 234 |
+
if request.mode[1] in (self.modes + "?"):
|
| 235 |
+
if request.extension in (".lfr",):
|
| 236 |
+
return True
|
| 237 |
+
|
| 238 |
+
# -- reader
|
| 239 |
+
|
| 240 |
+
class Reader(Format.Reader):
|
| 241 |
+
def _open(self, meta_only=False, include_thumbnail=True):
|
| 242 |
+
self._file = self.request.get_file()
|
| 243 |
+
self._data = None
|
| 244 |
+
self._chunks = {}
|
| 245 |
+
self.metadata = {}
|
| 246 |
+
self._content = None
|
| 247 |
+
self._meta_only = meta_only
|
| 248 |
+
self._include_thumbnail = include_thumbnail
|
| 249 |
+
|
| 250 |
+
self._find_header()
|
| 251 |
+
self._find_chunks()
|
| 252 |
+
self._find_meta()
|
| 253 |
+
|
| 254 |
+
try:
|
| 255 |
+
# Get sha1 dict and check if it is in dictionary of data chunks
|
| 256 |
+
chunk_dict = self._content["frames"][0]["frame"]
|
| 257 |
+
if (
|
| 258 |
+
chunk_dict["metadataRef"] in self._chunks
|
| 259 |
+
and chunk_dict["imageRef"] in self._chunks
|
| 260 |
+
and chunk_dict["privateMetadataRef"] in self._chunks
|
| 261 |
+
):
|
| 262 |
+
if not self._meta_only:
|
| 263 |
+
# Read raw image data byte buffer
|
| 264 |
+
data_pos, size = self._chunks[chunk_dict["imageRef"]]
|
| 265 |
+
self._file.seek(data_pos, 0)
|
| 266 |
+
self.raw_image_data = self._file.read(size)
|
| 267 |
+
|
| 268 |
+
# Read meta data
|
| 269 |
+
data_pos, size = self._chunks[chunk_dict["metadataRef"]]
|
| 270 |
+
self._file.seek(data_pos, 0)
|
| 271 |
+
metadata = self._file.read(size)
|
| 272 |
+
# Add metadata to meta data dict
|
| 273 |
+
self.metadata["metadata"] = json.loads(metadata.decode("ASCII"))
|
| 274 |
+
|
| 275 |
+
# Read private metadata
|
| 276 |
+
data_pos, size = self._chunks[chunk_dict["privateMetadataRef"]]
|
| 277 |
+
self._file.seek(data_pos, 0)
|
| 278 |
+
serial_numbers = self._file.read(size)
|
| 279 |
+
self.serial_numbers = json.loads(serial_numbers.decode("ASCII"))
|
| 280 |
+
# Add private metadata to meta data dict
|
| 281 |
+
self.metadata["privateMetadata"] = self.serial_numbers
|
| 282 |
+
|
| 283 |
+
# Read image preview thumbnail
|
| 284 |
+
if self._include_thumbnail:
|
| 285 |
+
chunk_dict = self._content["thumbnails"][0]
|
| 286 |
+
if chunk_dict["imageRef"] in self._chunks:
|
| 287 |
+
# Read thumbnail image from thumbnail chunk
|
| 288 |
+
data_pos, size = self._chunks[chunk_dict["imageRef"]]
|
| 289 |
+
self._file.seek(data_pos, 0)
|
| 290 |
+
# Read binary data, read image as jpeg
|
| 291 |
+
thumbnail_data = self._file.read(size)
|
| 292 |
+
thumbnail_img = imread(thumbnail_data, format="jpeg")
|
| 293 |
+
|
| 294 |
+
thumbnail_height = chunk_dict["height"]
|
| 295 |
+
thumbnail_width = chunk_dict["width"]
|
| 296 |
+
|
| 297 |
+
# Add thumbnail to metadata
|
| 298 |
+
self.metadata["thumbnail"] = {
|
| 299 |
+
"image": thumbnail_img,
|
| 300 |
+
"height": thumbnail_height,
|
| 301 |
+
"width": thumbnail_width,
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
except KeyError:
|
| 305 |
+
raise RuntimeError("The specified file is not a valid LFR file.")
|
| 306 |
+
|
| 307 |
+
def _close(self):
|
| 308 |
+
# Close the reader.
|
| 309 |
+
# Note that the request object will close self._file
|
| 310 |
+
del self._data
|
| 311 |
+
|
| 312 |
+
def _get_length(self):
|
| 313 |
+
# Return the number of images. Can be np.inf
|
| 314 |
+
return 1
|
| 315 |
+
|
| 316 |
+
def _find_header(self):
|
| 317 |
+
"""
|
| 318 |
+
Checks if file has correct header and skip it.
|
| 319 |
+
"""
|
| 320 |
+
file_header = b"\x89LFP\x0D\x0A\x1A\x0A\x00\x00\x00\x01"
|
| 321 |
+
# Read and check header of file
|
| 322 |
+
header = self._file.read(HEADER_LENGTH)
|
| 323 |
+
if header != file_header:
|
| 324 |
+
raise RuntimeError("The LFR file header is invalid.")
|
| 325 |
+
|
| 326 |
+
# Read first bytes to skip header
|
| 327 |
+
self._file.read(SIZE_LENGTH)
|
| 328 |
+
|
| 329 |
+
def _find_chunks(self):
|
| 330 |
+
"""
|
| 331 |
+
Gets start position and size of data chunks in file.
|
| 332 |
+
"""
|
| 333 |
+
chunk_header = b"\x89LFC\x0D\x0A\x1A\x0A\x00\x00\x00\x00"
|
| 334 |
+
|
| 335 |
+
for i in range(0, DATA_CHUNKS_ILLUM):
|
| 336 |
+
data_pos, size, sha1 = self._get_chunk(chunk_header)
|
| 337 |
+
self._chunks[sha1] = (data_pos, size)
|
| 338 |
+
|
| 339 |
+
def _find_meta(self):
|
| 340 |
+
"""
|
| 341 |
+
Gets a data chunk that contains information over content
|
| 342 |
+
of other data chunks.
|
| 343 |
+
"""
|
| 344 |
+
meta_header = b"\x89LFM\x0D\x0A\x1A\x0A\x00\x00\x00\x00"
|
| 345 |
+
data_pos, size, sha1 = self._get_chunk(meta_header)
|
| 346 |
+
|
| 347 |
+
# Get content
|
| 348 |
+
self._file.seek(data_pos, 0)
|
| 349 |
+
data = self._file.read(size)
|
| 350 |
+
self._content = json.loads(data.decode("ASCII"))
|
| 351 |
+
|
| 352 |
+
def _get_chunk(self, header):
|
| 353 |
+
"""
|
| 354 |
+
Checks if chunk has correct header and skips it.
|
| 355 |
+
Finds start position and length of next chunk and reads
|
| 356 |
+
sha1-string that identifies the following data chunk.
|
| 357 |
+
|
| 358 |
+
Parameters
|
| 359 |
+
----------
|
| 360 |
+
header : bytes
|
| 361 |
+
Byte string that identifies start of chunk.
|
| 362 |
+
|
| 363 |
+
Returns
|
| 364 |
+
-------
|
| 365 |
+
data_pos : int
|
| 366 |
+
Start position of data chunk in file.
|
| 367 |
+
size : int
|
| 368 |
+
Size of data chunk.
|
| 369 |
+
sha1 : str
|
| 370 |
+
Sha1 value of chunk.
|
| 371 |
+
"""
|
| 372 |
+
# Read and check header of chunk
|
| 373 |
+
header_chunk = self._file.read(HEADER_LENGTH)
|
| 374 |
+
if header_chunk != header:
|
| 375 |
+
raise RuntimeError("The LFR chunk header is invalid.")
|
| 376 |
+
|
| 377 |
+
data_pos = None
|
| 378 |
+
sha1 = None
|
| 379 |
+
|
| 380 |
+
# Read size
|
| 381 |
+
size = struct.unpack(">i", self._file.read(SIZE_LENGTH))[0]
|
| 382 |
+
if size > 0:
|
| 383 |
+
# Read sha1
|
| 384 |
+
sha1 = str(self._file.read(SHA1_LENGTH).decode("ASCII"))
|
| 385 |
+
# Skip fixed null chars
|
| 386 |
+
self._file.read(PADDING_LENGTH)
|
| 387 |
+
# Find start of data and skip data
|
| 388 |
+
data_pos = self._file.tell()
|
| 389 |
+
self._file.seek(size, 1)
|
| 390 |
+
# Skip extra null chars
|
| 391 |
+
ch = self._file.read(1)
|
| 392 |
+
while ch == b"\0":
|
| 393 |
+
ch = self._file.read(1)
|
| 394 |
+
self._file.seek(-1, 1)
|
| 395 |
+
|
| 396 |
+
return data_pos, size, sha1
|
| 397 |
+
|
| 398 |
+
def _get_data(self, index):
|
| 399 |
+
# Return the data and meta data for the given index
|
| 400 |
+
if index not in [0, None]:
|
| 401 |
+
raise IndexError("Lytro lfr file contains only one dataset")
|
| 402 |
+
|
| 403 |
+
if not self._meta_only:
|
| 404 |
+
# Read bytes from string and convert to uint16
|
| 405 |
+
raw = np.frombuffer(self.raw_image_data, dtype=np.uint8).astype(
|
| 406 |
+
np.uint16
|
| 407 |
+
)
|
| 408 |
+
im = LytroIllumRawFormat.rearrange_bits(raw)
|
| 409 |
+
else:
|
| 410 |
+
im = np.array([])
|
| 411 |
+
|
| 412 |
+
# Return array and dummy meta data
|
| 413 |
+
return im, self.metadata
|
| 414 |
+
|
| 415 |
+
def _get_meta_data(self, index):
|
| 416 |
+
# Get the meta data for the given index. If index is None,
|
| 417 |
+
# it returns the global meta data.
|
| 418 |
+
if index not in [0, None]:
|
| 419 |
+
raise IndexError("Lytro meta data file contains only one dataset")
|
| 420 |
+
|
| 421 |
+
return self.metadata
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
class LytroF01RawFormat(LytroFormat):
|
| 425 |
+
"""This is the Lytro RAW format for the original F01 Lytro camera.
|
| 426 |
+
The raw format is a 12bit image format as used by the Lytro F01
|
| 427 |
+
light field camera. The format will read the specified raw file and will
|
| 428 |
+
try to load a .txt or .json file with the associated meta data.
|
| 429 |
+
This format does not support writing.
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
Parameters for reading
|
| 433 |
+
----------------------
|
| 434 |
+
meta_only : bool
|
| 435 |
+
Whether to only read the metadata.
|
| 436 |
+
|
| 437 |
+
"""
|
| 438 |
+
|
| 439 |
+
def _can_read(self, request):
|
| 440 |
+
# Check if mode and extensions are supported by the format
|
| 441 |
+
if request.mode[1] in (self.modes + "?"):
|
| 442 |
+
if request.extension in (".raw",):
|
| 443 |
+
return True
|
| 444 |
+
|
| 445 |
+
@staticmethod
|
| 446 |
+
def rearrange_bits(array):
|
| 447 |
+
# Do bit rearrangement for the 12-bit lytro raw format
|
| 448 |
+
# Normalize output to 1.0 as float64
|
| 449 |
+
t0 = array[0::3]
|
| 450 |
+
t1 = array[1::3]
|
| 451 |
+
t2 = array[2::3]
|
| 452 |
+
|
| 453 |
+
a0 = np.left_shift(t0, 4) + np.right_shift(np.bitwise_and(t1, 240), 4)
|
| 454 |
+
a1 = np.left_shift(np.bitwise_and(t1, 15), 8) + t2
|
| 455 |
+
|
| 456 |
+
image = np.zeros(LYTRO_F01_IMAGE_SIZE, dtype=np.uint16)
|
| 457 |
+
image[:, 0::2] = a0.reshape(
|
| 458 |
+
(LYTRO_F01_IMAGE_SIZE[0], LYTRO_F01_IMAGE_SIZE[1] // 2)
|
| 459 |
+
)
|
| 460 |
+
image[:, 1::2] = a1.reshape(
|
| 461 |
+
(LYTRO_F01_IMAGE_SIZE[0], LYTRO_F01_IMAGE_SIZE[1] // 2)
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
# Normalize data to 1.0 as 64-bit float.
|
| 465 |
+
# Division is by 4095 as the Lytro F01 saves 12-bit raw data.
|
| 466 |
+
return np.divide(image, 4095.0).astype(np.float64)
|
| 467 |
+
|
| 468 |
+
# -- reader
|
| 469 |
+
|
| 470 |
+
class Reader(Format.Reader):
|
| 471 |
+
def _open(self, meta_only=False):
|
| 472 |
+
self._file = self.request.get_file()
|
| 473 |
+
self._data = None
|
| 474 |
+
self._meta_only = meta_only
|
| 475 |
+
|
| 476 |
+
def _close(self):
|
| 477 |
+
# Close the reader.
|
| 478 |
+
# Note that the request object will close self._file
|
| 479 |
+
del self._data
|
| 480 |
+
|
| 481 |
+
def _get_length(self):
|
| 482 |
+
# Return the number of images.
|
| 483 |
+
return 1
|
| 484 |
+
|
| 485 |
+
def _get_data(self, index):
|
| 486 |
+
# Return the data and meta data for the given index
|
| 487 |
+
|
| 488 |
+
if index not in [0, "None"]:
|
| 489 |
+
raise IndexError("Lytro file contains only one dataset")
|
| 490 |
+
|
| 491 |
+
if not self._meta_only:
|
| 492 |
+
# Read all bytes
|
| 493 |
+
if self._data is None:
|
| 494 |
+
self._data = self._file.read()
|
| 495 |
+
|
| 496 |
+
# Read bytes from string and convert to uint16
|
| 497 |
+
raw = np.frombuffer(self._data, dtype=np.uint8).astype(np.uint16)
|
| 498 |
+
|
| 499 |
+
# Rearrange bits
|
| 500 |
+
img = LytroF01RawFormat.rearrange_bits(raw)
|
| 501 |
+
|
| 502 |
+
else:
|
| 503 |
+
img = np.array([])
|
| 504 |
+
|
| 505 |
+
# Return image and meta data
|
| 506 |
+
return img, self._get_meta_data(index=0)
|
| 507 |
+
|
| 508 |
+
def _get_meta_data(self, index):
|
| 509 |
+
# Get the meta data for the given index. If index is None, it
|
| 510 |
+
# should return the global meta data.
|
| 511 |
+
|
| 512 |
+
if index not in [0, None]:
|
| 513 |
+
raise IndexError("Lytro meta data file contains only one dataset")
|
| 514 |
+
|
| 515 |
+
# Try to read meta data from meta data file corresponding
|
| 516 |
+
# to the raw data file, extension in [.txt, .TXT, .json, .JSON]
|
| 517 |
+
filename_base = os.path.splitext(self.request.get_local_filename())[0]
|
| 518 |
+
|
| 519 |
+
meta_data = None
|
| 520 |
+
|
| 521 |
+
for ext in [".txt", ".TXT", ".json", ".JSON"]:
|
| 522 |
+
if os.path.isfile(filename_base + ext):
|
| 523 |
+
meta_data = json.load(open(filename_base + ext))
|
| 524 |
+
|
| 525 |
+
if meta_data is not None:
|
| 526 |
+
return meta_data
|
| 527 |
+
|
| 528 |
+
else:
|
| 529 |
+
logger.warning("No metadata file found for provided raw file.")
|
| 530 |
+
return {}
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
class LytroLfpFormat(LytroFormat):
|
| 534 |
+
"""This is the Lytro Illum LFP format.
|
| 535 |
+
The lfp is a image and meta data container format as used by the
|
| 536 |
+
Lytro F01 light field camera.
|
| 537 |
+
The format will read the specified lfp file.
|
| 538 |
+
This format does not support writing.
|
| 539 |
+
|
| 540 |
+
Parameters for reading
|
| 541 |
+
----------------------
|
| 542 |
+
meta_only : bool
|
| 543 |
+
Whether to only read the metadata.
|
| 544 |
+
include_thumbnail : bool
|
| 545 |
+
Whether to include an image thumbnail in the metadata.
|
| 546 |
+
"""
|
| 547 |
+
|
| 548 |
+
def _can_read(self, request):
|
| 549 |
+
# Check if mode and extensions are supported by the format
|
| 550 |
+
if request.mode[1] in (self.modes + "?"):
|
| 551 |
+
if request.extension in (".lfp",):
|
| 552 |
+
return True
|
| 553 |
+
|
| 554 |
+
# -- reader
|
| 555 |
+
|
| 556 |
+
class Reader(Format.Reader):
|
| 557 |
+
def _open(self, meta_only=False):
|
| 558 |
+
self._file = self.request.get_file()
|
| 559 |
+
self._data = None
|
| 560 |
+
self._chunks = {}
|
| 561 |
+
self.metadata = {}
|
| 562 |
+
self._content = None
|
| 563 |
+
self._meta_only = meta_only
|
| 564 |
+
|
| 565 |
+
self._find_header()
|
| 566 |
+
self._find_meta()
|
| 567 |
+
self._find_chunks()
|
| 568 |
+
|
| 569 |
+
try:
|
| 570 |
+
# Get sha1 dict and check if it is in dictionary of data chunks
|
| 571 |
+
chunk_dict = self._content["picture"]["frameArray"][0]["frame"]
|
| 572 |
+
if (
|
| 573 |
+
chunk_dict["metadataRef"] in self._chunks
|
| 574 |
+
and chunk_dict["imageRef"] in self._chunks
|
| 575 |
+
and chunk_dict["privateMetadataRef"] in self._chunks
|
| 576 |
+
):
|
| 577 |
+
if not self._meta_only:
|
| 578 |
+
# Read raw image data byte buffer
|
| 579 |
+
data_pos, size = self._chunks[chunk_dict["imageRef"]]
|
| 580 |
+
self._file.seek(data_pos, 0)
|
| 581 |
+
self.raw_image_data = self._file.read(size)
|
| 582 |
+
|
| 583 |
+
# Read meta data
|
| 584 |
+
data_pos, size = self._chunks[chunk_dict["metadataRef"]]
|
| 585 |
+
self._file.seek(data_pos, 0)
|
| 586 |
+
metadata = self._file.read(size)
|
| 587 |
+
# Add metadata to meta data dict
|
| 588 |
+
self.metadata["metadata"] = json.loads(metadata.decode("ASCII"))
|
| 589 |
+
|
| 590 |
+
# Read private metadata
|
| 591 |
+
data_pos, size = self._chunks[chunk_dict["privateMetadataRef"]]
|
| 592 |
+
self._file.seek(data_pos, 0)
|
| 593 |
+
serial_numbers = self._file.read(size)
|
| 594 |
+
self.serial_numbers = json.loads(serial_numbers.decode("ASCII"))
|
| 595 |
+
# Add private metadata to meta data dict
|
| 596 |
+
self.metadata["privateMetadata"] = self.serial_numbers
|
| 597 |
+
|
| 598 |
+
except KeyError:
|
| 599 |
+
raise RuntimeError("The specified file is not a valid LFP file.")
|
| 600 |
+
|
| 601 |
+
def _close(self):
|
| 602 |
+
# Close the reader.
|
| 603 |
+
# Note that the request object will close self._file
|
| 604 |
+
del self._data
|
| 605 |
+
|
| 606 |
+
def _get_length(self):
|
| 607 |
+
# Return the number of images. Can be np.inf
|
| 608 |
+
return 1
|
| 609 |
+
|
| 610 |
+
def _find_header(self):
|
| 611 |
+
"""
|
| 612 |
+
Checks if file has correct header and skip it.
|
| 613 |
+
"""
|
| 614 |
+
file_header = b"\x89LFP\x0D\x0A\x1A\x0A\x00\x00\x00\x01"
|
| 615 |
+
|
| 616 |
+
# Read and check header of file
|
| 617 |
+
header = self._file.read(HEADER_LENGTH)
|
| 618 |
+
if header != file_header:
|
| 619 |
+
raise RuntimeError("The LFP file header is invalid.")
|
| 620 |
+
|
| 621 |
+
# Read first bytes to skip header
|
| 622 |
+
self._file.read(SIZE_LENGTH)
|
| 623 |
+
|
| 624 |
+
def _find_chunks(self):
|
| 625 |
+
"""
|
| 626 |
+
Gets start position and size of data chunks in file.
|
| 627 |
+
"""
|
| 628 |
+
chunk_header = b"\x89LFC\x0D\x0A\x1A\x0A\x00\x00\x00\x00"
|
| 629 |
+
|
| 630 |
+
for i in range(0, DATA_CHUNKS_F01):
|
| 631 |
+
data_pos, size, sha1 = self._get_chunk(chunk_header)
|
| 632 |
+
self._chunks[sha1] = (data_pos, size)
|
| 633 |
+
|
| 634 |
+
def _find_meta(self):
|
| 635 |
+
"""
|
| 636 |
+
Gets a data chunk that contains information over content
|
| 637 |
+
of other data chunks.
|
| 638 |
+
"""
|
| 639 |
+
meta_header = b"\x89LFM\x0D\x0A\x1A\x0A\x00\x00\x00\x00"
|
| 640 |
+
|
| 641 |
+
data_pos, size, sha1 = self._get_chunk(meta_header)
|
| 642 |
+
|
| 643 |
+
# Get content
|
| 644 |
+
self._file.seek(data_pos, 0)
|
| 645 |
+
data = self._file.read(size)
|
| 646 |
+
self._content = json.loads(data.decode("ASCII"))
|
| 647 |
+
data = self._file.read(5) # Skip 5
|
| 648 |
+
|
| 649 |
+
def _get_chunk(self, header):
|
| 650 |
+
"""
|
| 651 |
+
Checks if chunk has correct header and skips it.
|
| 652 |
+
Finds start position and length of next chunk and reads
|
| 653 |
+
sha1-string that identifies the following data chunk.
|
| 654 |
+
|
| 655 |
+
Parameters
|
| 656 |
+
----------
|
| 657 |
+
header : bytes
|
| 658 |
+
Byte string that identifies start of chunk.
|
| 659 |
+
|
| 660 |
+
Returns
|
| 661 |
+
-------
|
| 662 |
+
data_pos : int
|
| 663 |
+
Start position of data chunk in file.
|
| 664 |
+
size : int
|
| 665 |
+
Size of data chunk.
|
| 666 |
+
sha1 : str
|
| 667 |
+
Sha1 value of chunk.
|
| 668 |
+
"""
|
| 669 |
+
# Read and check header of chunk
|
| 670 |
+
header_chunk = self._file.read(HEADER_LENGTH)
|
| 671 |
+
if header_chunk != header:
|
| 672 |
+
raise RuntimeError("The LFP chunk header is invalid.")
|
| 673 |
+
|
| 674 |
+
data_pos = None
|
| 675 |
+
sha1 = None
|
| 676 |
+
|
| 677 |
+
# Read size
|
| 678 |
+
size = struct.unpack(">i", self._file.read(SIZE_LENGTH))[0]
|
| 679 |
+
if size > 0:
|
| 680 |
+
# Read sha1
|
| 681 |
+
sha1 = str(self._file.read(SHA1_LENGTH).decode("ASCII"))
|
| 682 |
+
# Skip fixed null chars
|
| 683 |
+
self._file.read(PADDING_LENGTH)
|
| 684 |
+
# Find start of data and skip data
|
| 685 |
+
data_pos = self._file.tell()
|
| 686 |
+
self._file.seek(size, 1)
|
| 687 |
+
# Skip extra null chars
|
| 688 |
+
ch = self._file.read(1)
|
| 689 |
+
while ch == b"\0":
|
| 690 |
+
ch = self._file.read(1)
|
| 691 |
+
self._file.seek(-1, 1)
|
| 692 |
+
|
| 693 |
+
return data_pos, size, sha1
|
| 694 |
+
|
| 695 |
+
def _get_data(self, index):
|
| 696 |
+
# Return the data and meta data for the given index
|
| 697 |
+
if index not in [0, None]:
|
| 698 |
+
raise IndexError("Lytro lfp file contains only one dataset")
|
| 699 |
+
|
| 700 |
+
if not self._meta_only:
|
| 701 |
+
# Read bytes from string and convert to uint16
|
| 702 |
+
raw = np.frombuffer(self.raw_image_data, dtype=np.uint8).astype(
|
| 703 |
+
np.uint16
|
| 704 |
+
)
|
| 705 |
+
im = LytroF01RawFormat.rearrange_bits(raw)
|
| 706 |
+
else:
|
| 707 |
+
im = np.array([])
|
| 708 |
+
|
| 709 |
+
# Return array and dummy meta data
|
| 710 |
+
return im, self.metadata
|
| 711 |
+
|
| 712 |
+
def _get_meta_data(self, index):
|
| 713 |
+
# Get the meta data for the given index. If index is None,
|
| 714 |
+
# it returns the global meta data.
|
| 715 |
+
if index not in [0, None]:
|
| 716 |
+
raise IndexError("Lytro meta data file contains only one dataset")
|
| 717 |
+
|
| 718 |
+
return self.metadata
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/opencv.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Read/Write images using OpenCV.
|
| 2 |
+
|
| 3 |
+
Backend Library: `OpenCV <https://opencv.org/>`_
|
| 4 |
+
|
| 5 |
+
This plugin wraps OpenCV (also known as ``cv2``), a popular image processing
|
| 6 |
+
library. Currently, it exposes OpenCVs image reading capability (no video or GIF
|
| 7 |
+
support yet); however, this may be added in future releases.
|
| 8 |
+
|
| 9 |
+
Methods
|
| 10 |
+
-------
|
| 11 |
+
.. note::
|
| 12 |
+
Check the respective function for a list of supported kwargs and their
|
| 13 |
+
documentation.
|
| 14 |
+
|
| 15 |
+
.. autosummary::
|
| 16 |
+
:toctree:
|
| 17 |
+
|
| 18 |
+
OpenCVPlugin.read
|
| 19 |
+
OpenCVPlugin.iter
|
| 20 |
+
OpenCVPlugin.write
|
| 21 |
+
OpenCVPlugin.properties
|
| 22 |
+
OpenCVPlugin.metadata
|
| 23 |
+
|
| 24 |
+
Pixel Formats (Colorspaces)
|
| 25 |
+
---------------------------
|
| 26 |
+
|
| 27 |
+
OpenCV is known to process images in BGR; however, most of the python ecosystem
|
| 28 |
+
(in particular matplotlib and other pydata libraries) use the RGB. As such,
|
| 29 |
+
images are converted to RGB, RGBA, or grayscale (where applicable) by default.
|
| 30 |
+
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
import warnings
|
| 35 |
+
from pathlib import Path
|
| 36 |
+
from typing import Any, Dict, List, Optional, Union
|
| 37 |
+
|
| 38 |
+
import cv2
|
| 39 |
+
import numpy as np
|
| 40 |
+
|
| 41 |
+
from ..core import Request
|
| 42 |
+
from ..core.request import URI_BYTES, InitializationError, IOMode
|
| 43 |
+
from ..core.v3_plugin_api import ImageProperties, PluginV3
|
| 44 |
+
from ..typing import ArrayLike
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class OpenCVPlugin(PluginV3):
|
| 48 |
+
def __init__(self, request: Request) -> None:
|
| 49 |
+
super().__init__(request)
|
| 50 |
+
|
| 51 |
+
self.file_handle = request.get_local_filename()
|
| 52 |
+
if request._uri_type is URI_BYTES:
|
| 53 |
+
self.filename = "<bytes>"
|
| 54 |
+
else:
|
| 55 |
+
self.filename = request.raw_uri
|
| 56 |
+
|
| 57 |
+
mode = request.mode.io_mode
|
| 58 |
+
if mode == IOMode.read and not cv2.haveImageReader(self.file_handle):
|
| 59 |
+
raise InitializationError(f"OpenCV can't read `{self.filename}`.")
|
| 60 |
+
elif mode == IOMode.write and not cv2.haveImageWriter(self.file_handle):
|
| 61 |
+
raise InitializationError(f"OpenCV can't write to `{self.filename}`.")
|
| 62 |
+
|
| 63 |
+
def read(
|
| 64 |
+
self,
|
| 65 |
+
*,
|
| 66 |
+
index: int = None,
|
| 67 |
+
colorspace: Union[int, str] = None,
|
| 68 |
+
flags: int = cv2.IMREAD_COLOR,
|
| 69 |
+
) -> np.ndarray:
|
| 70 |
+
"""Read an image from the ImageResource.
|
| 71 |
+
|
| 72 |
+
Parameters
|
| 73 |
+
----------
|
| 74 |
+
index : int, Ellipsis
|
| 75 |
+
If int, read the index-th image from the ImageResource. If ``...``,
|
| 76 |
+
read all images from the ImageResource and stack them along a new,
|
| 77 |
+
prepended, batch dimension. If None (default), use ``index=0`` if
|
| 78 |
+
the image contains exactly one image and ``index=...`` otherwise.
|
| 79 |
+
colorspace : str, int
|
| 80 |
+
The colorspace to convert into after loading and before returning
|
| 81 |
+
the image. If None (default) keep grayscale images as is, convert
|
| 82 |
+
images with an alpha channel to ``RGBA`` and all other images to
|
| 83 |
+
``RGB``. If int, interpret ``colorspace`` as one of OpenCVs
|
| 84 |
+
`conversion flags
|
| 85 |
+
<https://docs.opencv.org/4.x/d8/d01/group__imgproc__color__conversions.html>`_
|
| 86 |
+
and use it for conversion. If str, convert the image into the given
|
| 87 |
+
colorspace. Possible string values are: ``"RGB"``, ``"BGR"``,
|
| 88 |
+
``"RGBA"``, ``"BGRA"``, ``"GRAY"``, ``"HSV"``, or ``"LAB"``.
|
| 89 |
+
flags : int
|
| 90 |
+
The OpenCV flag(s) to pass to the reader. Refer to the `OpenCV docs
|
| 91 |
+
<https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56>`_
|
| 92 |
+
for details.
|
| 93 |
+
|
| 94 |
+
Returns
|
| 95 |
+
-------
|
| 96 |
+
ndimage : np.ndarray
|
| 97 |
+
The decoded image as a numpy array.
|
| 98 |
+
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
if index is None:
|
| 102 |
+
n_images = cv2.imcount(self.file_handle, flags)
|
| 103 |
+
index = 0 if n_images == 1 else ...
|
| 104 |
+
|
| 105 |
+
if index is ...:
|
| 106 |
+
retval, img = cv2.imreadmulti(self.file_handle, flags=flags)
|
| 107 |
+
is_batch = True
|
| 108 |
+
else:
|
| 109 |
+
retval, img = cv2.imreadmulti(self.file_handle, index, 1, flags=flags)
|
| 110 |
+
is_batch = False
|
| 111 |
+
|
| 112 |
+
if retval is False:
|
| 113 |
+
raise ValueError(f"Could not read index `{index}` from `{self.filename}`.")
|
| 114 |
+
|
| 115 |
+
if img[0].ndim == 2:
|
| 116 |
+
in_colorspace = "GRAY"
|
| 117 |
+
out_colorspace = colorspace or "GRAY"
|
| 118 |
+
elif img[0].shape[-1] == 4:
|
| 119 |
+
in_colorspace = "BGRA"
|
| 120 |
+
out_colorspace = colorspace or "RGBA"
|
| 121 |
+
else:
|
| 122 |
+
in_colorspace = "BGR"
|
| 123 |
+
out_colorspace = colorspace or "RGB"
|
| 124 |
+
|
| 125 |
+
if isinstance(colorspace, int):
|
| 126 |
+
cvt_space = colorspace
|
| 127 |
+
elif in_colorspace == out_colorspace.upper():
|
| 128 |
+
cvt_space = None
|
| 129 |
+
else:
|
| 130 |
+
out_colorspace = out_colorspace.upper()
|
| 131 |
+
cvt_space = getattr(cv2, f"COLOR_{in_colorspace}2{out_colorspace}")
|
| 132 |
+
|
| 133 |
+
if cvt_space is not None:
|
| 134 |
+
img = np.stack([cv2.cvtColor(x, cvt_space) for x in img])
|
| 135 |
+
else:
|
| 136 |
+
img = np.stack(img)
|
| 137 |
+
|
| 138 |
+
return img if is_batch else img[0]
|
| 139 |
+
|
| 140 |
+
def iter(
|
| 141 |
+
self,
|
| 142 |
+
colorspace: Union[int, str] = None,
|
| 143 |
+
flags: int = cv2.IMREAD_COLOR,
|
| 144 |
+
) -> np.ndarray:
|
| 145 |
+
"""Yield images from the ImageResource.
|
| 146 |
+
|
| 147 |
+
Parameters
|
| 148 |
+
----------
|
| 149 |
+
colorspace : str, int
|
| 150 |
+
The colorspace to convert into after loading and before returning
|
| 151 |
+
the image. If None (default) keep grayscale images as is, convert
|
| 152 |
+
images with an alpha channel to ``RGBA`` and all other images to
|
| 153 |
+
``RGB``. If int, interpret ``colorspace`` as one of OpenCVs
|
| 154 |
+
`conversion flags
|
| 155 |
+
<https://docs.opencv.org/4.x/d8/d01/group__imgproc__color__conversions.html>`_
|
| 156 |
+
and use it for conversion. If str, convert the image into the given
|
| 157 |
+
colorspace. Possible string values are: ``"RGB"``, ``"BGR"``,
|
| 158 |
+
``"RGBA"``, ``"BGRA"``, ``"GRAY"``, ``"HSV"``, or ``"LAB"``.
|
| 159 |
+
flags : int
|
| 160 |
+
The OpenCV flag(s) to pass to the reader. Refer to the `OpenCV docs
|
| 161 |
+
<https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56>`_
|
| 162 |
+
for details.
|
| 163 |
+
|
| 164 |
+
Yields
|
| 165 |
+
-------
|
| 166 |
+
ndimage : np.ndarray
|
| 167 |
+
The decoded image as a numpy array.
|
| 168 |
+
|
| 169 |
+
"""
|
| 170 |
+
for idx in range(cv2.imcount(self.file_handle)):
|
| 171 |
+
yield self.read(index=idx, flags=flags, colorspace=colorspace)
|
| 172 |
+
|
| 173 |
+
def write(
|
| 174 |
+
self,
|
| 175 |
+
ndimage: Union[ArrayLike, List[ArrayLike]],
|
| 176 |
+
is_batch: bool = False,
|
| 177 |
+
params: List[int] = None,
|
| 178 |
+
) -> Optional[bytes]:
|
| 179 |
+
"""Save an ndimage in the ImageResource.
|
| 180 |
+
|
| 181 |
+
Parameters
|
| 182 |
+
----------
|
| 183 |
+
ndimage : ArrayLike, List[ArrayLike]
|
| 184 |
+
The image data that will be written to the file. It is either a
|
| 185 |
+
single image, a batch of images, or a list of images.
|
| 186 |
+
is_batch : bool
|
| 187 |
+
If True, the provided ndimage is a batch of images. If False (default), the
|
| 188 |
+
provided ndimage is a single image. If the provided ndimage is a list of images,
|
| 189 |
+
this parameter has no effect.
|
| 190 |
+
params : List[int]
|
| 191 |
+
A list of parameters that will be passed to OpenCVs imwrite or
|
| 192 |
+
imwritemulti functions. Possible values are documented in the
|
| 193 |
+
`OpenCV documentation
|
| 194 |
+
<https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#gabbc7ef1aa2edfaa87772f1202d67e0ce>`_.
|
| 195 |
+
|
| 196 |
+
Returns
|
| 197 |
+
-------
|
| 198 |
+
encoded_image : bytes, None
|
| 199 |
+
If the ImageResource is ``"<bytes>"`` the call to write returns the
|
| 200 |
+
encoded image as a bytes string. Otherwise it returns None.
|
| 201 |
+
|
| 202 |
+
"""
|
| 203 |
+
|
| 204 |
+
if isinstance(ndimage, list):
|
| 205 |
+
ndimage = np.stack(ndimage, axis=0)
|
| 206 |
+
elif not is_batch:
|
| 207 |
+
ndimage = ndimage[None, ...]
|
| 208 |
+
|
| 209 |
+
if ndimage[0].ndim == 2:
|
| 210 |
+
n_channels = 1
|
| 211 |
+
else:
|
| 212 |
+
n_channels = ndimage[0].shape[-1]
|
| 213 |
+
|
| 214 |
+
if n_channels == 1:
|
| 215 |
+
ndimage_cv2 = [x for x in ndimage]
|
| 216 |
+
elif n_channels == 4:
|
| 217 |
+
ndimage_cv2 = [cv2.cvtColor(x, cv2.COLOR_RGBA2BGRA) for x in ndimage]
|
| 218 |
+
else:
|
| 219 |
+
ndimage_cv2 = [cv2.cvtColor(x, cv2.COLOR_RGB2BGR) for x in ndimage]
|
| 220 |
+
|
| 221 |
+
retval = cv2.imwritemulti(self.file_handle, ndimage_cv2, params)
|
| 222 |
+
|
| 223 |
+
if retval is False:
|
| 224 |
+
# not sure what scenario would trigger this, but
|
| 225 |
+
# it can occur theoretically.
|
| 226 |
+
raise IOError("OpenCV failed to write.") # pragma: no cover
|
| 227 |
+
|
| 228 |
+
if self.request._uri_type == URI_BYTES:
|
| 229 |
+
return Path(self.file_handle).read_bytes()
|
| 230 |
+
|
| 231 |
+
def properties(
|
| 232 |
+
self,
|
| 233 |
+
index: int = 0,
|
| 234 |
+
colorspace: Union[int, str] = None,
|
| 235 |
+
flags: int = cv2.IMREAD_COLOR,
|
| 236 |
+
) -> ImageProperties:
|
| 237 |
+
"""Standardized image metadata.
|
| 238 |
+
|
| 239 |
+
Parameters
|
| 240 |
+
----------
|
| 241 |
+
index : int, Ellipsis
|
| 242 |
+
If int, get the properties of the index-th image in the
|
| 243 |
+
ImageResource. If ``...``, get the properties of the image stack
|
| 244 |
+
that contains all images. If None (default), use ``index=0`` if the
|
| 245 |
+
image contains exactly one image and ``index=...`` otherwise.
|
| 246 |
+
colorspace : str, int
|
| 247 |
+
The colorspace to convert into after loading and before returning
|
| 248 |
+
the image. If None (default) keep grayscale images as is, convert
|
| 249 |
+
images with an alpha channel to ``RGBA`` and all other images to
|
| 250 |
+
``RGB``. If int, interpret ``colorspace`` as one of OpenCVs
|
| 251 |
+
`conversion flags
|
| 252 |
+
<https://docs.opencv.org/4.x/d8/d01/group__imgproc__color__conversions.html>`_
|
| 253 |
+
and use it for conversion. If str, convert the image into the given
|
| 254 |
+
colorspace. Possible string values are: ``"RGB"``, ``"BGR"``,
|
| 255 |
+
``"RGBA"``, ``"BGRA"``, ``"GRAY"``, ``"HSV"``, or ``"LAB"``.
|
| 256 |
+
flags : int
|
| 257 |
+
The OpenCV flag(s) to pass to the reader. Refer to the `OpenCV docs
|
| 258 |
+
<https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56>`_
|
| 259 |
+
for details.
|
| 260 |
+
|
| 261 |
+
Returns
|
| 262 |
+
-------
|
| 263 |
+
props : ImageProperties
|
| 264 |
+
A dataclass filled with standardized image metadata.
|
| 265 |
+
|
| 266 |
+
Notes
|
| 267 |
+
-----
|
| 268 |
+
Reading properties with OpenCV involves decoding pixel data, because
|
| 269 |
+
OpenCV doesn't provide a direct way to access metadata.
|
| 270 |
+
|
| 271 |
+
"""
|
| 272 |
+
|
| 273 |
+
# unfortunately, OpenCV doesn't allow reading shape without reading pixel data
|
| 274 |
+
img = self.read(index=index, flags=flags, colorspace=colorspace)
|
| 275 |
+
|
| 276 |
+
return ImageProperties(
|
| 277 |
+
shape=img.shape,
|
| 278 |
+
dtype=img.dtype,
|
| 279 |
+
is_batch=(index is ...),
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
def metadata(
|
| 283 |
+
self, index: int = None, exclude_applied: bool = True
|
| 284 |
+
) -> Dict[str, Any]:
|
| 285 |
+
"""Format-specific metadata.
|
| 286 |
+
|
| 287 |
+
.. warning::
|
| 288 |
+
OpenCV does not support reading metadata. When called, this function
|
| 289 |
+
will raise a ``NotImplementedError``.
|
| 290 |
+
|
| 291 |
+
Parameters
|
| 292 |
+
----------
|
| 293 |
+
index : int
|
| 294 |
+
This parameter has no effect.
|
| 295 |
+
exclude_applied : bool
|
| 296 |
+
This parameter has no effect.
|
| 297 |
+
|
| 298 |
+
"""
|
| 299 |
+
|
| 300 |
+
warnings.warn("OpenCV does not support reading metadata.", UserWarning)
|
| 301 |
+
return dict()
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/pillow.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
""" Read/Write images using Pillow/PIL.
|
| 5 |
+
|
| 6 |
+
Backend Library: `Pillow <https://pillow.readthedocs.io/en/stable/>`_
|
| 7 |
+
|
| 8 |
+
Plugin that wraps the the Pillow library. Pillow is a friendly fork of PIL
|
| 9 |
+
(Python Image Library) and supports reading and writing of common formats (jpg,
|
| 10 |
+
png, gif, tiff, ...). For, the complete list of features and supported formats
|
| 11 |
+
please refer to pillows official docs (see the Backend Library link).
|
| 12 |
+
|
| 13 |
+
Parameters
|
| 14 |
+
----------
|
| 15 |
+
request : Request
|
| 16 |
+
A request object representing the resource to be operated on.
|
| 17 |
+
|
| 18 |
+
Methods
|
| 19 |
+
-------
|
| 20 |
+
|
| 21 |
+
.. autosummary::
|
| 22 |
+
:toctree: _plugins/pillow
|
| 23 |
+
|
| 24 |
+
PillowPlugin.read
|
| 25 |
+
PillowPlugin.write
|
| 26 |
+
PillowPlugin.iter
|
| 27 |
+
PillowPlugin.get_meta
|
| 28 |
+
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
from io import BytesIO
|
| 32 |
+
from typing import Callable, Optional, Dict, Any, Tuple, cast, Iterator, Union, List
|
| 33 |
+
import numpy as np
|
| 34 |
+
from PIL import Image, UnidentifiedImageError, ImageSequence, ExifTags # type: ignore
|
| 35 |
+
from ..core.request import Request, IOMode, InitializationError, URI_BYTES
|
| 36 |
+
from ..core.v3_plugin_api import PluginV3, ImageProperties
|
| 37 |
+
import warnings
|
| 38 |
+
from ..typing import ArrayLike
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _exif_orientation_transform(orientation: int, mode: str) -> Callable:
|
| 42 |
+
# get transformation that transforms an image from a
|
| 43 |
+
# given EXIF orientation into the standard orientation
|
| 44 |
+
|
| 45 |
+
# -1 if the mode has color channel, 0 otherwise
|
| 46 |
+
axis = -2 if Image.getmodebands(mode) > 1 else -1
|
| 47 |
+
|
| 48 |
+
EXIF_ORIENTATION = {
|
| 49 |
+
1: lambda x: x,
|
| 50 |
+
2: lambda x: np.flip(x, axis=axis),
|
| 51 |
+
3: lambda x: np.rot90(x, k=2),
|
| 52 |
+
4: lambda x: np.flip(x, axis=axis - 1),
|
| 53 |
+
5: lambda x: np.flip(np.rot90(x, k=3), axis=axis),
|
| 54 |
+
6: lambda x: np.rot90(x, k=1),
|
| 55 |
+
7: lambda x: np.flip(np.rot90(x, k=1), axis=axis),
|
| 56 |
+
8: lambda x: np.rot90(x, k=3),
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
return EXIF_ORIENTATION[orientation]
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class PillowPlugin(PluginV3):
|
| 63 |
+
def __init__(self, request: Request) -> None:
|
| 64 |
+
"""Instantiate a new Pillow Plugin Object
|
| 65 |
+
|
| 66 |
+
Parameters
|
| 67 |
+
----------
|
| 68 |
+
request : {Request}
|
| 69 |
+
A request object representing the resource to be operated on.
|
| 70 |
+
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
super().__init__(request)
|
| 74 |
+
|
| 75 |
+
self._image: Image = None
|
| 76 |
+
|
| 77 |
+
if request.mode.io_mode == IOMode.read:
|
| 78 |
+
try:
|
| 79 |
+
with Image.open(request.get_file()):
|
| 80 |
+
# Check if it is generally possible to read the image.
|
| 81 |
+
# This will not read any data and merely try to find a
|
| 82 |
+
# compatible pillow plugin (ref: the pillow docs).
|
| 83 |
+
pass
|
| 84 |
+
except UnidentifiedImageError:
|
| 85 |
+
if request._uri_type == URI_BYTES:
|
| 86 |
+
raise InitializationError(
|
| 87 |
+
"Pillow can not read the provided bytes."
|
| 88 |
+
) from None
|
| 89 |
+
else:
|
| 90 |
+
raise InitializationError(
|
| 91 |
+
f"Pillow can not read {request.raw_uri}."
|
| 92 |
+
) from None
|
| 93 |
+
|
| 94 |
+
self._image = Image.open(self._request.get_file())
|
| 95 |
+
else:
|
| 96 |
+
extension = self.request.extension or self.request.format_hint
|
| 97 |
+
if extension is None:
|
| 98 |
+
warnings.warn(
|
| 99 |
+
"Can't determine file format to write as. You _must_"
|
| 100 |
+
" set `format` during write or the call will fail. Use "
|
| 101 |
+
"`extension` to supress this warning. ",
|
| 102 |
+
UserWarning,
|
| 103 |
+
)
|
| 104 |
+
return
|
| 105 |
+
|
| 106 |
+
tirage = [Image.preinit, Image.init]
|
| 107 |
+
for format_loader in tirage:
|
| 108 |
+
format_loader()
|
| 109 |
+
if extension in Image.registered_extensions().keys():
|
| 110 |
+
return
|
| 111 |
+
|
| 112 |
+
raise InitializationError(
|
| 113 |
+
f"Pillow can not write `{extension}` files."
|
| 114 |
+
) from None
|
| 115 |
+
|
| 116 |
+
def close(self) -> None:
|
| 117 |
+
if self._image:
|
| 118 |
+
self._image.close()
|
| 119 |
+
|
| 120 |
+
self._request.finish()
|
| 121 |
+
|
| 122 |
+
def read(
|
| 123 |
+
self, *, index=None, mode=None, rotate=False, apply_gamma=False
|
| 124 |
+
) -> np.ndarray:
|
| 125 |
+
"""
|
| 126 |
+
Parses the given URI and creates a ndarray from it.
|
| 127 |
+
|
| 128 |
+
Parameters
|
| 129 |
+
----------
|
| 130 |
+
index : {integer}
|
| 131 |
+
If the ImageResource contains multiple ndimages, and index is an
|
| 132 |
+
integer, select the index-th ndimage from among them and return it.
|
| 133 |
+
If index is an ellipsis (...), read all ndimages in the file and
|
| 134 |
+
stack them along a new batch dimension and return them. If index is
|
| 135 |
+
None, this plugin reads the first image of the file (index=0) unless
|
| 136 |
+
the image is a GIF or APNG, in which case all images are read
|
| 137 |
+
(index=...).
|
| 138 |
+
mode : {str, None}
|
| 139 |
+
Convert the image to the given mode before returning it. If None,
|
| 140 |
+
the mode will be left unchanged. Possible modes can be found at:
|
| 141 |
+
https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes
|
| 142 |
+
rotate : {bool}
|
| 143 |
+
If set to ``True`` and the image contains an EXIF orientation tag,
|
| 144 |
+
apply the orientation before returning the ndimage.
|
| 145 |
+
apply_gamma : {bool}
|
| 146 |
+
If ``True`` and the image contains metadata about gamma, apply gamma
|
| 147 |
+
correction to the image.
|
| 148 |
+
|
| 149 |
+
Returns
|
| 150 |
+
-------
|
| 151 |
+
ndimage : ndarray
|
| 152 |
+
A numpy array containing the loaded image data
|
| 153 |
+
|
| 154 |
+
Notes
|
| 155 |
+
-----
|
| 156 |
+
If you open a GIF - or any other format using color pallets - you may
|
| 157 |
+
wish to manually set the `mode` parameter. Otherwise, the numbers in
|
| 158 |
+
the returned image will refer to the entries in the color pallet, which
|
| 159 |
+
is discarded during conversion to ndarray.
|
| 160 |
+
|
| 161 |
+
"""
|
| 162 |
+
|
| 163 |
+
if index is None:
|
| 164 |
+
if self._image.format == "GIF":
|
| 165 |
+
index = Ellipsis
|
| 166 |
+
elif self._image.custom_mimetype == "image/apng":
|
| 167 |
+
index = Ellipsis
|
| 168 |
+
else:
|
| 169 |
+
index = 0
|
| 170 |
+
|
| 171 |
+
if isinstance(index, int):
|
| 172 |
+
# will raise IO error if index >= number of frames in image
|
| 173 |
+
self._image.seek(index)
|
| 174 |
+
image = self._apply_transforms(self._image, mode, rotate, apply_gamma)
|
| 175 |
+
return image
|
| 176 |
+
else:
|
| 177 |
+
iterator = self.iter(mode=mode, rotate=rotate, apply_gamma=apply_gamma)
|
| 178 |
+
image = np.stack([im for im in iterator], axis=0)
|
| 179 |
+
return image
|
| 180 |
+
|
| 181 |
+
def iter(
|
| 182 |
+
self, *, mode: str = None, rotate: bool = False, apply_gamma: bool = False
|
| 183 |
+
) -> Iterator[np.ndarray]:
|
| 184 |
+
"""
|
| 185 |
+
Iterate over all ndimages/frames in the URI
|
| 186 |
+
|
| 187 |
+
Parameters
|
| 188 |
+
----------
|
| 189 |
+
mode : {str, None}
|
| 190 |
+
Convert the image to the given mode before returning it. If None,
|
| 191 |
+
the mode will be left unchanged. Possible modes can be found at:
|
| 192 |
+
https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes
|
| 193 |
+
rotate : {bool}
|
| 194 |
+
If set to ``True`` and the image contains an EXIF orientation tag,
|
| 195 |
+
apply the orientation before returning the ndimage.
|
| 196 |
+
apply_gamma : {bool}
|
| 197 |
+
If ``True`` and the image contains metadata about gamma, apply gamma
|
| 198 |
+
correction to the image.
|
| 199 |
+
"""
|
| 200 |
+
|
| 201 |
+
for im in ImageSequence.Iterator(self._image):
|
| 202 |
+
yield self._apply_transforms(im, mode, rotate, apply_gamma)
|
| 203 |
+
|
| 204 |
+
def _apply_transforms(self, image, mode, rotate, apply_gamma) -> np.ndarray:
|
| 205 |
+
if mode is not None:
|
| 206 |
+
image = image.convert(mode)
|
| 207 |
+
elif image.format == "GIF":
|
| 208 |
+
# adjust for pillow9 changes
|
| 209 |
+
# see: https://github.com/python-pillow/Pillow/issues/5929
|
| 210 |
+
image = image.convert(image.palette.mode)
|
| 211 |
+
image = np.asarray(image)
|
| 212 |
+
|
| 213 |
+
meta = self.metadata(index=self._image.tell(), exclude_applied=False)
|
| 214 |
+
if rotate and "Orientation" in meta:
|
| 215 |
+
transformation = _exif_orientation_transform(
|
| 216 |
+
meta["Orientation"], self._image.mode
|
| 217 |
+
)
|
| 218 |
+
image = transformation(image)
|
| 219 |
+
|
| 220 |
+
if apply_gamma and "gamma" in meta:
|
| 221 |
+
gamma = float(meta["gamma"])
|
| 222 |
+
scale = float(65536 if image.dtype == np.uint16 else 255)
|
| 223 |
+
gain = 1.0
|
| 224 |
+
image = ((image / scale) ** gamma) * scale * gain + 0.4999
|
| 225 |
+
image = np.round(image).astype(np.uint8)
|
| 226 |
+
|
| 227 |
+
return image
|
| 228 |
+
|
| 229 |
+
def write(
|
| 230 |
+
self,
|
| 231 |
+
ndimage: Union[ArrayLike, List[ArrayLike]],
|
| 232 |
+
*,
|
| 233 |
+
mode: str = None,
|
| 234 |
+
format: str = None,
|
| 235 |
+
**kwargs,
|
| 236 |
+
) -> Optional[bytes]:
|
| 237 |
+
"""
|
| 238 |
+
Write an ndimage to the URI specified in path.
|
| 239 |
+
|
| 240 |
+
If the URI points to a file on the current host and the file does not
|
| 241 |
+
yet exist it will be created. If the file exists already, it will be
|
| 242 |
+
appended if possible; otherwise, it will be replaced.
|
| 243 |
+
|
| 244 |
+
If necessary, the image is broken down along the leading dimension to
|
| 245 |
+
fit into individual frames of the chosen format. If the format doesn't
|
| 246 |
+
support multiple frames, and IOError is raised.
|
| 247 |
+
|
| 248 |
+
Parameters
|
| 249 |
+
----------
|
| 250 |
+
image : ndarray
|
| 251 |
+
The ndimage to write.
|
| 252 |
+
mode : {str, None}
|
| 253 |
+
Specify the image's color format. If None (default), the mode is
|
| 254 |
+
inferred from the array's shape and dtype. Possible modes can be
|
| 255 |
+
found at:
|
| 256 |
+
https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes
|
| 257 |
+
format : {str, None}
|
| 258 |
+
Optional format override. If omitted, the format to use is
|
| 259 |
+
determined from the filename extension. If a file object was used
|
| 260 |
+
instead of a filename, this parameter must always be used.
|
| 261 |
+
kwargs : ...
|
| 262 |
+
Extra arguments to pass to pillow. If a writer doesn't recognise an
|
| 263 |
+
option, it is silently ignored. The available options are described
|
| 264 |
+
in pillow's `image format documentation
|
| 265 |
+
<https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html>`_
|
| 266 |
+
for each writer.
|
| 267 |
+
|
| 268 |
+
Notes
|
| 269 |
+
-----
|
| 270 |
+
When writing batches of very narrow (2-4 pixels wide) gray images set
|
| 271 |
+
the ``mode`` explicitly to avoid the batch being identified as a colored
|
| 272 |
+
image.
|
| 273 |
+
|
| 274 |
+
"""
|
| 275 |
+
|
| 276 |
+
extension = self.request.extension or self.request.format_hint
|
| 277 |
+
|
| 278 |
+
save_args = {
|
| 279 |
+
"format": format or Image.registered_extensions()[extension],
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
if isinstance(ndimage, list):
|
| 283 |
+
ndimage = np.stack(ndimage, axis=0)
|
| 284 |
+
is_batch = True
|
| 285 |
+
else:
|
| 286 |
+
ndimage = np.asarray(ndimage)
|
| 287 |
+
is_batch = None
|
| 288 |
+
|
| 289 |
+
# check if ndimage is a batch of frames/pages (e.g. for writing GIF)
|
| 290 |
+
# if mode is given, use it; otherwise fall back to image.ndim only
|
| 291 |
+
if is_batch is True:
|
| 292 |
+
pass # ndimage was list; we know it is a batch
|
| 293 |
+
if mode is not None:
|
| 294 |
+
is_batch = (
|
| 295 |
+
ndimage.ndim > 3 if Image.getmodebands(mode) > 1 else ndimage.ndim > 2
|
| 296 |
+
)
|
| 297 |
+
elif ndimage.ndim == 2:
|
| 298 |
+
is_batch = False
|
| 299 |
+
elif ndimage.ndim == 3 and ndimage.shape[-1] in [2, 3, 4]:
|
| 300 |
+
# Note: this makes a channel-last assumption
|
| 301 |
+
# (pillow seems to make it as well)
|
| 302 |
+
is_batch = False
|
| 303 |
+
else:
|
| 304 |
+
is_batch = True
|
| 305 |
+
|
| 306 |
+
if not is_batch:
|
| 307 |
+
ndimage = ndimage[None, ...]
|
| 308 |
+
|
| 309 |
+
pil_frames = list()
|
| 310 |
+
for frame in ndimage:
|
| 311 |
+
pil_frame = Image.fromarray(frame, mode=mode)
|
| 312 |
+
if "bits" in kwargs:
|
| 313 |
+
pil_frame = pil_frame.quantize(colors=2 ** kwargs["bits"])
|
| 314 |
+
pil_frames.append(pil_frame)
|
| 315 |
+
primary_image, other_images = pil_frames[0], pil_frames[1:]
|
| 316 |
+
|
| 317 |
+
if is_batch:
|
| 318 |
+
save_args["save_all"] = True
|
| 319 |
+
save_args["append_images"] = other_images
|
| 320 |
+
|
| 321 |
+
save_args.update(kwargs)
|
| 322 |
+
primary_image.save(self._request.get_file(), **save_args)
|
| 323 |
+
|
| 324 |
+
if self._request._uri_type == URI_BYTES:
|
| 325 |
+
file = cast(BytesIO, self._request.get_file())
|
| 326 |
+
return file.getvalue()
|
| 327 |
+
|
| 328 |
+
return None
|
| 329 |
+
|
| 330 |
+
def get_meta(self, *, index=0) -> Dict[str, Any]:
|
| 331 |
+
return self.metadata(index=index, exclude_applied=False)
|
| 332 |
+
|
| 333 |
+
def metadata(
|
| 334 |
+
self, index: int = None, exclude_applied: bool = True
|
| 335 |
+
) -> Dict[str, Any]:
|
| 336 |
+
"""Read ndimage metadata.
|
| 337 |
+
|
| 338 |
+
Parameters
|
| 339 |
+
----------
|
| 340 |
+
index : {integer, None}
|
| 341 |
+
If the ImageResource contains multiple ndimages, and index is an
|
| 342 |
+
integer, select the index-th ndimage from among them and return its
|
| 343 |
+
metadata. If index is an ellipsis (...), read and return global
|
| 344 |
+
metadata. If index is None, this plugin reads metadata from the
|
| 345 |
+
first image of the file (index=0) unless the image is a GIF or APNG,
|
| 346 |
+
in which case global metadata is read (index=...).
|
| 347 |
+
|
| 348 |
+
Returns
|
| 349 |
+
-------
|
| 350 |
+
metadata : dict
|
| 351 |
+
A dictionary of format-specific metadata.
|
| 352 |
+
|
| 353 |
+
"""
|
| 354 |
+
|
| 355 |
+
if index is None:
|
| 356 |
+
if self._image.format == "GIF":
|
| 357 |
+
index = Ellipsis
|
| 358 |
+
elif self._image.custom_mimetype == "image/apng":
|
| 359 |
+
index = Ellipsis
|
| 360 |
+
else:
|
| 361 |
+
index = 0
|
| 362 |
+
|
| 363 |
+
if isinstance(index, int) and self._image.tell() != index:
|
| 364 |
+
self._image.seek(index)
|
| 365 |
+
|
| 366 |
+
metadata = self._image.info.copy()
|
| 367 |
+
metadata["mode"] = self._image.mode
|
| 368 |
+
metadata["shape"] = self._image.size
|
| 369 |
+
|
| 370 |
+
if self._image.mode == "P":
|
| 371 |
+
metadata["palette"] = self._image.palette
|
| 372 |
+
|
| 373 |
+
if self._image.getexif():
|
| 374 |
+
exif_data = {
|
| 375 |
+
ExifTags.TAGS.get(key, "unknown"): value
|
| 376 |
+
for key, value in dict(self._image.getexif()).items()
|
| 377 |
+
}
|
| 378 |
+
exif_data.pop("unknown", None)
|
| 379 |
+
metadata.update(exif_data)
|
| 380 |
+
|
| 381 |
+
if exclude_applied:
|
| 382 |
+
metadata.pop("Orientation", None)
|
| 383 |
+
|
| 384 |
+
return metadata
|
| 385 |
+
|
| 386 |
+
def properties(self, index: int = None) -> ImageProperties:
|
| 387 |
+
"""Standardized ndimage metadata
|
| 388 |
+
Parameters
|
| 389 |
+
----------
|
| 390 |
+
index : int
|
| 391 |
+
If the ImageResource contains multiple ndimages, and index is an
|
| 392 |
+
integer, select the index-th ndimage from among them and return its
|
| 393 |
+
properties. If index is an ellipsis (...), read and return the
|
| 394 |
+
properties of all ndimages in the file stacked along a new batch
|
| 395 |
+
dimension. If index is None, this plugin reads and returns the
|
| 396 |
+
properties of the first image (index=0) unless the image is a GIF or
|
| 397 |
+
APNG, in which case it reads and returns the properties all images
|
| 398 |
+
(index=...).
|
| 399 |
+
|
| 400 |
+
Returns
|
| 401 |
+
-------
|
| 402 |
+
properties : ImageProperties
|
| 403 |
+
A dataclass filled with standardized image metadata.
|
| 404 |
+
|
| 405 |
+
Notes
|
| 406 |
+
-----
|
| 407 |
+
This does not decode pixel data and is 394fast for large images.
|
| 408 |
+
|
| 409 |
+
"""
|
| 410 |
+
|
| 411 |
+
if index is None:
|
| 412 |
+
if self._image.format == "GIF":
|
| 413 |
+
index = Ellipsis
|
| 414 |
+
elif self._image.custom_mimetype == "image/apng":
|
| 415 |
+
index = Ellipsis
|
| 416 |
+
else:
|
| 417 |
+
index = 0
|
| 418 |
+
|
| 419 |
+
if index is Ellipsis:
|
| 420 |
+
self._image.seek(0)
|
| 421 |
+
else:
|
| 422 |
+
self._image.seek(index)
|
| 423 |
+
|
| 424 |
+
if self._image.format == "GIF":
|
| 425 |
+
# GIF mode is determined by pallette
|
| 426 |
+
mode = self._image.palette.mode
|
| 427 |
+
else:
|
| 428 |
+
mode = self._image.mode
|
| 429 |
+
|
| 430 |
+
width: int = self._image.width
|
| 431 |
+
height: int = self._image.height
|
| 432 |
+
shape: Tuple[int, ...] = (height, width)
|
| 433 |
+
|
| 434 |
+
n_frames: int = self._image.n_frames
|
| 435 |
+
if index is ...:
|
| 436 |
+
shape = (n_frames, *shape)
|
| 437 |
+
|
| 438 |
+
dummy = np.asarray(Image.new(mode, (1, 1)))
|
| 439 |
+
pil_shape: Tuple[int, ...] = dummy.shape
|
| 440 |
+
if len(pil_shape) > 2:
|
| 441 |
+
shape = (*shape, *pil_shape[2:])
|
| 442 |
+
|
| 443 |
+
return ImageProperties(
|
| 444 |
+
shape=shape,
|
| 445 |
+
dtype=dummy.dtype,
|
| 446 |
+
is_batch=True if index is Ellipsis else False,
|
| 447 |
+
)
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/pillow_info.py
ADDED
|
@@ -0,0 +1,1053 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
|
| 3 |
+
# styletest: ignore E122 E123 E501
|
| 4 |
+
|
| 5 |
+
"""
|
| 6 |
+
Module that contain info about the Pillow formats. The first part of
|
| 7 |
+
this module generates this info and writes it to its own bottom half
|
| 8 |
+
if run as a script.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import warnings
|
| 12 |
+
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"The `PillowFormat` plugin is deprecated and will be removed in ImageIO v3."
|
| 15 |
+
" Use the new `PillowPlugin` instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def generate_info(): # pragma: no cover
|
| 21 |
+
from urllib.request import urlopen
|
| 22 |
+
import PIL
|
| 23 |
+
from PIL import Image
|
| 24 |
+
|
| 25 |
+
Image.init()
|
| 26 |
+
|
| 27 |
+
ids = []
|
| 28 |
+
formats = []
|
| 29 |
+
docs = {}
|
| 30 |
+
|
| 31 |
+
# Collect formats and their summary from plugin modules
|
| 32 |
+
for mod_name in dir(PIL):
|
| 33 |
+
if "ImagePlugin" in mod_name:
|
| 34 |
+
mod = getattr(PIL, mod_name)
|
| 35 |
+
for ob_name in dir(mod):
|
| 36 |
+
ob = getattr(mod, ob_name)
|
| 37 |
+
if isinstance(ob, type) and issubclass(ob, Image.Image):
|
| 38 |
+
if ob.format in ids:
|
| 39 |
+
print("Found duplicate for", ob.format)
|
| 40 |
+
else:
|
| 41 |
+
ids.append(ob.format)
|
| 42 |
+
formats.append((ob.format, ob.format_description))
|
| 43 |
+
|
| 44 |
+
# Add extension info
|
| 45 |
+
for i in range(len(formats)):
|
| 46 |
+
id, summary = formats[i]
|
| 47 |
+
ext = " ".join([e for e in Image.EXTENSION if Image.EXTENSION[e] == id])
|
| 48 |
+
formats[i] = id, summary, ext
|
| 49 |
+
|
| 50 |
+
# Get documentation of formats
|
| 51 |
+
url = "https://raw.githubusercontent.com/python-pillow/Pillow/master/docs/handbook/image-file-formats.rst" # noqa
|
| 52 |
+
lines = urlopen(url).read().decode().splitlines()
|
| 53 |
+
lines.append("End")
|
| 54 |
+
lines.append("---") # for the end
|
| 55 |
+
|
| 56 |
+
# Parse documentation
|
| 57 |
+
cur_name = ""
|
| 58 |
+
cur_part = []
|
| 59 |
+
for i in range(len(lines)):
|
| 60 |
+
line = lines[i]
|
| 61 |
+
if line.startswith(("^^^", "---", "===")):
|
| 62 |
+
if cur_name and cur_name in ids:
|
| 63 |
+
text = "\n".join(cur_part[:-1])
|
| 64 |
+
text = text.replace("versionadded::", "versionadded:: Pillow ")
|
| 65 |
+
text = text.replace("Image.open`", "Image.write`")
|
| 66 |
+
docs[cur_name] = text
|
| 67 |
+
cur_part = []
|
| 68 |
+
cur_name = lines[i - 1].strip().replace(" ", "").upper()
|
| 69 |
+
else:
|
| 70 |
+
cur_part.append(" " + line)
|
| 71 |
+
|
| 72 |
+
# Fill in the blancs
|
| 73 |
+
for id in ids:
|
| 74 |
+
if id in docs:
|
| 75 |
+
docs[id] = "*From the Pillow docs:*\n\n" + docs[id]
|
| 76 |
+
else:
|
| 77 |
+
docs[id] = "No docs for %s." % id
|
| 78 |
+
print("no docs for", id)
|
| 79 |
+
|
| 80 |
+
# Sort before writing
|
| 81 |
+
formats.sort(key=lambda x: x[0])
|
| 82 |
+
ids.sort()
|
| 83 |
+
|
| 84 |
+
# Read file ...
|
| 85 |
+
code = open(__file__, "rb").read().decode()
|
| 86 |
+
code, divider, _ = code.partition("## BELOW IS " + "AUTOGENERATED")
|
| 87 |
+
code += divider + "\n\n"
|
| 88 |
+
|
| 89 |
+
# Write formats
|
| 90 |
+
code += "pillow_formats = [\n"
|
| 91 |
+
for i in range(len(formats)):
|
| 92 |
+
print(formats[i])
|
| 93 |
+
code += " (%r, %r, %r),\n" % formats[i]
|
| 94 |
+
code += " ]\n\n\n"
|
| 95 |
+
|
| 96 |
+
# Write docs
|
| 97 |
+
code += "pillow_docs = {\n"
|
| 98 |
+
for id in ids:
|
| 99 |
+
code += '%r:\nu"""%s""",\n' % (id, docs[id])
|
| 100 |
+
code += "}\n"
|
| 101 |
+
|
| 102 |
+
# Write back
|
| 103 |
+
with open(__file__, "wb") as f:
|
| 104 |
+
f.write(code.encode())
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
if __name__ == "__main__":
|
| 108 |
+
generate_info()
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
# BELOW IS AUTOGENERATED
|
| 112 |
+
|
| 113 |
+
pillow_formats = [
|
| 114 |
+
("BMP", "Windows Bitmap", ".bmp"),
|
| 115 |
+
("BUFR", "BUFR", ".bufr"),
|
| 116 |
+
("CUR", "Windows Cursor", ".cur"),
|
| 117 |
+
("DCX", "Intel DCX", ".dcx"),
|
| 118 |
+
("DDS", "DirectDraw Surface", ".dds"),
|
| 119 |
+
("DIB", "Windows Bitmap", ""),
|
| 120 |
+
("EPS", "Encapsulated Postscript", ".ps .eps"),
|
| 121 |
+
("FITS", "FITS", ".fit .fits"),
|
| 122 |
+
("FLI", "Autodesk FLI/FLC Animation", ".fli .flc"),
|
| 123 |
+
("FPX", "FlashPix", ".fpx"),
|
| 124 |
+
("FTEX", "Texture File Format (IW2:EOC)", ".ftc .ftu"),
|
| 125 |
+
("GBR", "GIMP brush file", ".gbr"),
|
| 126 |
+
("GIF", "Compuserve GIF", ".gif"),
|
| 127 |
+
("GRIB", "GRIB", ".grib"),
|
| 128 |
+
("HDF5", "HDF5", ".h5 .hdf"),
|
| 129 |
+
("ICNS", "Mac OS icns resource", ".icns"),
|
| 130 |
+
("ICO", "Windows Icon", ".ico"),
|
| 131 |
+
("IM", "IFUNC Image Memory", ".im"),
|
| 132 |
+
("IMT", "IM Tools", ""),
|
| 133 |
+
("IPTC", "IPTC/NAA", ".iim"),
|
| 134 |
+
("JPEG", "JPEG (ISO 10918)", ".jfif .jpe .jpg .jpeg"),
|
| 135 |
+
("JPEG2000", "JPEG 2000 (ISO 15444)", ".jp2 .j2k .jpc .jpf .jpx .j2c"),
|
| 136 |
+
("MCIDAS", "McIdas area file", ""),
|
| 137 |
+
("MIC", "Microsoft Image Composer", ".mic"),
|
| 138 |
+
("MPEG", "MPEG", ".mpg .mpeg"),
|
| 139 |
+
("MPO", "MPO (CIPA DC-007)", ".mpo"),
|
| 140 |
+
("MSP", "Windows Paint", ".msp"),
|
| 141 |
+
("PCD", "Kodak PhotoCD", ".pcd"),
|
| 142 |
+
("PCX", "Paintbrush", ".pcx"),
|
| 143 |
+
("PIXAR", "PIXAR raster image", ".pxr"),
|
| 144 |
+
("PNG", "Portable network graphics", ".png"),
|
| 145 |
+
("PPM", "Pbmplus image", ".pbm .pgm .ppm"),
|
| 146 |
+
("PSD", "Adobe Photoshop", ".psd"),
|
| 147 |
+
("SGI", "SGI Image File Format", ".bw .rgb .rgba .sgi"),
|
| 148 |
+
("SPIDER", "Spider 2D image", ""),
|
| 149 |
+
("SUN", "Sun Raster File", ".ras"),
|
| 150 |
+
("TGA", "Targa", ".tga"),
|
| 151 |
+
("TIFF", "Adobe TIFF", ".tif .tiff"),
|
| 152 |
+
("WMF", "Windows Metafile", ".wmf .emf"),
|
| 153 |
+
("XBM", "X11 Bitmap", ".xbm"),
|
| 154 |
+
("XPM", "X11 Pixel Map", ".xpm"),
|
| 155 |
+
("XVThumb", "XV thumbnail image", ""),
|
| 156 |
+
]
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
pillow_docs = {
|
| 160 |
+
"BMP": """*From the Pillow docs:*
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
PIL reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``,
|
| 164 |
+
or ``RGB`` data. 16-colour images are read as ``P`` images. Run-length encoding
|
| 165 |
+
is not supported.
|
| 166 |
+
|
| 167 |
+
The :py:meth:`~PIL.Image.Image.write` method sets the following
|
| 168 |
+
:py:attr:`~PIL.Image.Image.info` properties:
|
| 169 |
+
|
| 170 |
+
**compression**
|
| 171 |
+
Set to ``bmp_rle`` if the file is run-length encoded.
|
| 172 |
+
""",
|
| 173 |
+
"BUFR": """*From the Pillow docs:*
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
.. versionadded:: Pillow 1.1.3
|
| 177 |
+
|
| 178 |
+
PIL provides a stub driver for BUFR files.
|
| 179 |
+
|
| 180 |
+
To add read or write support to your application, use
|
| 181 |
+
:py:func:`PIL.BufrStubImagePlugin.register_handler`.
|
| 182 |
+
""",
|
| 183 |
+
"CUR": """*From the Pillow docs:*
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
CUR is used to store cursors on Windows. The CUR decoder reads the largest
|
| 187 |
+
available cursor. Animated cursors are not supported.
|
| 188 |
+
""",
|
| 189 |
+
"DCX": """*From the Pillow docs:*
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
DCX is a container file format for PCX files, defined by Intel. The DCX format
|
| 193 |
+
is commonly used in fax applications. The DCX decoder can read files containing
|
| 194 |
+
``1``, ``L``, ``P``, or ``RGB`` data.
|
| 195 |
+
|
| 196 |
+
When the file is opened, only the first image is read. You can use
|
| 197 |
+
:py:meth:`~file.seek` or :py:mod:`~PIL.ImageSequence` to read other images.
|
| 198 |
+
|
| 199 |
+
""",
|
| 200 |
+
"DDS": """*From the Pillow docs:*
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
DDS is a popular container texture format used in video games and natively
|
| 204 |
+
supported by DirectX.
|
| 205 |
+
Currently, DXT1, DXT3, and DXT5 pixel formats are supported and only in ``RGBA``
|
| 206 |
+
mode.
|
| 207 |
+
|
| 208 |
+
.. versionadded:: Pillow 3.4.0 DXT3
|
| 209 |
+
""",
|
| 210 |
+
"DIB": """No docs for DIB.""",
|
| 211 |
+
"EPS": """*From the Pillow docs:*
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
PIL identifies EPS files containing image data, and can read files that contain
|
| 215 |
+
embedded raster images (ImageData descriptors). If Ghostscript is available,
|
| 216 |
+
other EPS files can be read as well. The EPS driver can also write EPS
|
| 217 |
+
images. The EPS driver can read EPS images in ``L``, ``LAB``, ``RGB`` and
|
| 218 |
+
``CMYK`` mode, but Ghostscript may convert the images to ``RGB`` mode rather
|
| 219 |
+
than leaving them in the original color space. The EPS driver can write images
|
| 220 |
+
in ``L``, ``RGB`` and ``CMYK`` modes.
|
| 221 |
+
|
| 222 |
+
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
|
| 223 |
+
method with the following parameter to affect how Ghostscript renders the EPS
|
| 224 |
+
|
| 225 |
+
**scale**
|
| 226 |
+
Affects the scale of the resultant rasterized image. If the EPS suggests
|
| 227 |
+
that the image be rendered at 100px x 100px, setting this parameter to
|
| 228 |
+
2 will make the Ghostscript render a 200px x 200px image instead. The
|
| 229 |
+
relative position of the bounding box is maintained::
|
| 230 |
+
|
| 231 |
+
im = Image.open(...)
|
| 232 |
+
im.size #(100,100)
|
| 233 |
+
im.load(scale=2)
|
| 234 |
+
im.size #(200,200)
|
| 235 |
+
""",
|
| 236 |
+
"FITS": """*From the Pillow docs:*
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
.. versionadded:: Pillow 1.1.5
|
| 240 |
+
|
| 241 |
+
PIL provides a stub driver for FITS files.
|
| 242 |
+
|
| 243 |
+
To add read or write support to your application, use
|
| 244 |
+
:py:func:`PIL.FitsStubImagePlugin.register_handler`.
|
| 245 |
+
""",
|
| 246 |
+
"FLI": """No docs for FLI.""",
|
| 247 |
+
"FPX": """*From the Pillow docs:*
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
PIL reads Kodak FlashPix files. In the current version, only the highest
|
| 251 |
+
resolution image is read from the file, and the viewing transform is not taken
|
| 252 |
+
into account.
|
| 253 |
+
|
| 254 |
+
.. note::
|
| 255 |
+
|
| 256 |
+
To enable full FlashPix support, you need to build and install the IJG JPEG
|
| 257 |
+
library before building the Python Imaging Library. See the distribution
|
| 258 |
+
README for details.
|
| 259 |
+
""",
|
| 260 |
+
"FTEX": """*From the Pillow docs:*
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
.. versionadded:: Pillow 3.2.0
|
| 264 |
+
|
| 265 |
+
The FTEX decoder reads textures used for 3D objects in
|
| 266 |
+
Independence War 2: Edge Of Chaos. The plugin reads a single texture
|
| 267 |
+
per file, in the compressed and uncompressed formats.
|
| 268 |
+
""",
|
| 269 |
+
"GBR": """*From the Pillow docs:*
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
The GBR decoder reads GIMP brush files, version 1 and 2.
|
| 273 |
+
|
| 274 |
+
The :py:meth:`~PIL.Image.Image.write` method sets the following
|
| 275 |
+
:py:attr:`~PIL.Image.Image.info` properties:
|
| 276 |
+
|
| 277 |
+
**comment**
|
| 278 |
+
The brush name.
|
| 279 |
+
|
| 280 |
+
**spacing**
|
| 281 |
+
The spacing between the brushes, in pixels. Version 2 only.
|
| 282 |
+
|
| 283 |
+
GD
|
| 284 |
+
^^
|
| 285 |
+
|
| 286 |
+
PIL reads uncompressed GD files. Note that this file format cannot be
|
| 287 |
+
automatically identified, so you must use :py:func:`PIL.GdImageFile.open` to
|
| 288 |
+
read such a file.
|
| 289 |
+
|
| 290 |
+
The :py:meth:`~PIL.Image.Image.write` method sets the following
|
| 291 |
+
:py:attr:`~PIL.Image.Image.info` properties:
|
| 292 |
+
|
| 293 |
+
**transparency**
|
| 294 |
+
Transparency color index. This key is omitted if the image is not
|
| 295 |
+
transparent.
|
| 296 |
+
""",
|
| 297 |
+
"GIF": """*From the Pillow docs:*
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
PIL reads GIF87a and GIF89a versions of the GIF file format. The library writes
|
| 301 |
+
run-length encoded files in GIF87a by default, unless GIF89a features
|
| 302 |
+
are used or GIF89a is already in use.
|
| 303 |
+
|
| 304 |
+
Note that GIF files are always read as grayscale (``L``)
|
| 305 |
+
or palette mode (``P``) images.
|
| 306 |
+
|
| 307 |
+
The :py:meth:`~PIL.Image.Image.write` method sets the following
|
| 308 |
+
:py:attr:`~PIL.Image.Image.info` properties:
|
| 309 |
+
|
| 310 |
+
**background**
|
| 311 |
+
Default background color (a palette color index).
|
| 312 |
+
|
| 313 |
+
**transparency**
|
| 314 |
+
Transparency color index. This key is omitted if the image is not
|
| 315 |
+
transparent.
|
| 316 |
+
|
| 317 |
+
**version**
|
| 318 |
+
Version (either ``GIF87a`` or ``GIF89a``).
|
| 319 |
+
|
| 320 |
+
**duration**
|
| 321 |
+
May not be present. The time to display the current frame
|
| 322 |
+
of the GIF, in milliseconds.
|
| 323 |
+
|
| 324 |
+
**loop**
|
| 325 |
+
May not be present. The number of times the GIF should loop.
|
| 326 |
+
|
| 327 |
+
Reading sequences
|
| 328 |
+
~~~~~~~~~~~~~~~~~
|
| 329 |
+
|
| 330 |
+
The GIF loader supports the :py:meth:`~file.seek` and :py:meth:`~file.tell`
|
| 331 |
+
methods. You can seek to the next frame (``im.seek(im.tell() + 1)``), or rewind
|
| 332 |
+
the file by seeking to the first frame. Random access is not supported.
|
| 333 |
+
|
| 334 |
+
``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame.
|
| 335 |
+
|
| 336 |
+
Saving
|
| 337 |
+
~~~~~~
|
| 338 |
+
|
| 339 |
+
When calling :py:meth:`~PIL.Image.Image.save`, the following options
|
| 340 |
+
are available::
|
| 341 |
+
|
| 342 |
+
im.save(out, save_all=True, append_images=[im1, im2, ...])
|
| 343 |
+
|
| 344 |
+
**save_all**
|
| 345 |
+
If present and true, all frames of the image will be saved. If
|
| 346 |
+
not, then only the first frame of a multiframe image will be saved.
|
| 347 |
+
|
| 348 |
+
**append_images**
|
| 349 |
+
A list of images to append as additional frames. Each of the
|
| 350 |
+
images in the list can be single or multiframe images.
|
| 351 |
+
This is currently only supported for GIF, PDF, TIFF, and WebP.
|
| 352 |
+
|
| 353 |
+
**duration**
|
| 354 |
+
The display duration of each frame of the multiframe gif, in
|
| 355 |
+
milliseconds. Pass a single integer for a constant duration, or a
|
| 356 |
+
list or tuple to set the duration for each frame separately.
|
| 357 |
+
|
| 358 |
+
**loop**
|
| 359 |
+
Integer number of times the GIF should loop.
|
| 360 |
+
|
| 361 |
+
**optimize**
|
| 362 |
+
If present and true, attempt to compress the palette by
|
| 363 |
+
eliminating unused colors. This is only useful if the palette can
|
| 364 |
+
be compressed to the next smaller power of 2 elements.
|
| 365 |
+
|
| 366 |
+
**palette**
|
| 367 |
+
Use the specified palette for the saved image. The palette should
|
| 368 |
+
be a bytes or bytearray object containing the palette entries in
|
| 369 |
+
RGBRGB... form. It should be no more than 768 bytes. Alternately,
|
| 370 |
+
the palette can be passed in as an
|
| 371 |
+
:py:class:`PIL.ImagePalette.ImagePalette` object.
|
| 372 |
+
|
| 373 |
+
**disposal**
|
| 374 |
+
Indicates the way in which the graphic is to be treated after being displayed.
|
| 375 |
+
|
| 376 |
+
* 0 - No disposal specified.
|
| 377 |
+
* 1 - Do not dispose.
|
| 378 |
+
* 2 - Restore to background color.
|
| 379 |
+
* 3 - Restore to previous content.
|
| 380 |
+
|
| 381 |
+
Pass a single integer for a constant disposal, or a list or tuple
|
| 382 |
+
to set the disposal for each frame separately.
|
| 383 |
+
|
| 384 |
+
Reading local images
|
| 385 |
+
~~~~~~~~~~~~~~~~~~~~
|
| 386 |
+
|
| 387 |
+
The GIF loader creates an image memory the same size as the GIF file’s *logical
|
| 388 |
+
screen size*, and pastes the actual pixel data (the *local image*) into this
|
| 389 |
+
image. If you only want the actual pixel rectangle, you can manipulate the
|
| 390 |
+
:py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.Image.Image.tile`
|
| 391 |
+
attributes before loading the file::
|
| 392 |
+
|
| 393 |
+
im = Image.open(...)
|
| 394 |
+
|
| 395 |
+
if im.tile[0][0] == "gif":
|
| 396 |
+
# only read the first "local image" from this GIF file
|
| 397 |
+
tag, (x0, y0, x1, y1), offset, extra = im.tile[0]
|
| 398 |
+
im.size = (x1 - x0, y1 - y0)
|
| 399 |
+
im.tile = [(tag, (0, 0) + im.size, offset, extra)]
|
| 400 |
+
""",
|
| 401 |
+
"GRIB": """*From the Pillow docs:*
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
.. versionadded:: Pillow 1.1.5
|
| 405 |
+
|
| 406 |
+
PIL provides a stub driver for GRIB files.
|
| 407 |
+
|
| 408 |
+
The driver requires the file to start with a GRIB header. If you have files
|
| 409 |
+
with embedded GRIB data, or files with multiple GRIB fields, your application
|
| 410 |
+
has to seek to the header before passing the file handle to PIL.
|
| 411 |
+
|
| 412 |
+
To add read or write support to your application, use
|
| 413 |
+
:py:func:`PIL.GribStubImagePlugin.register_handler`.
|
| 414 |
+
""",
|
| 415 |
+
"HDF5": """*From the Pillow docs:*
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
.. versionadded:: Pillow 1.1.5
|
| 419 |
+
|
| 420 |
+
PIL provides a stub driver for HDF5 files.
|
| 421 |
+
|
| 422 |
+
To add read or write support to your application, use
|
| 423 |
+
:py:func:`PIL.Hdf5StubImagePlugin.register_handler`.
|
| 424 |
+
""",
|
| 425 |
+
"ICNS": """*From the Pillow docs:*
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
PIL reads and (macOS only) writes macOS ``.icns`` files. By default, the
|
| 429 |
+
largest available icon is read, though you can override this by setting the
|
| 430 |
+
:py:attr:`~PIL.Image.Image.size` property before calling
|
| 431 |
+
:py:meth:`~PIL.Image.Image.load`. The :py:meth:`~PIL.Image.Image.write` method
|
| 432 |
+
sets the following :py:attr:`~PIL.Image.Image.info` property:
|
| 433 |
+
|
| 434 |
+
**sizes**
|
| 435 |
+
A list of supported sizes found in this icon file; these are a
|
| 436 |
+
3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina
|
| 437 |
+
icon and 1 for a standard icon. You *are* permitted to use this 3-tuple
|
| 438 |
+
format for the :py:attr:`~PIL.Image.Image.size` property if you set it
|
| 439 |
+
before calling :py:meth:`~PIL.Image.Image.load`; after loading, the size
|
| 440 |
+
will be reset to a 2-tuple containing pixel dimensions (so, e.g. if you
|
| 441 |
+
ask for ``(512, 512, 2)``, the final value of
|
| 442 |
+
:py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``).
|
| 443 |
+
""",
|
| 444 |
+
"ICO": """*From the Pillow docs:*
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
ICO is used to store icons on Windows. The largest available icon is read.
|
| 448 |
+
|
| 449 |
+
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
| 450 |
+
|
| 451 |
+
**sizes**
|
| 452 |
+
A list of sizes including in this ico file; these are a 2-tuple,
|
| 453 |
+
``(width, height)``; Default to ``[(16, 16), (24, 24), (32, 32), (48, 48),
|
| 454 |
+
(64, 64), (128, 128), (256, 256)]``. Any sizes bigger than the original
|
| 455 |
+
size or 256 will be ignored.
|
| 456 |
+
|
| 457 |
+
IM
|
| 458 |
+
^^
|
| 459 |
+
|
| 460 |
+
IM is a format used by LabEye and other applications based on the IFUNC image
|
| 461 |
+
processing library. The library reads and writes most uncompressed interchange
|
| 462 |
+
versions of this format.
|
| 463 |
+
|
| 464 |
+
IM is the only format that can store all internal PIL formats.
|
| 465 |
+
""",
|
| 466 |
+
"IM": """No docs for IM.""",
|
| 467 |
+
"IMT": """*From the Pillow docs:*
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
PIL reads Image Tools images containing ``L`` data.
|
| 471 |
+
""",
|
| 472 |
+
"IPTC": """No docs for IPTC.""",
|
| 473 |
+
"JPEG": """*From the Pillow docs:*
|
| 474 |
+
|
| 475 |
+
|
| 476 |
+
PIL reads JPEG, JFIF, and Adobe JPEG files containing ``L``, ``RGB``, or
|
| 477 |
+
``CMYK`` data. It writes standard and progressive JFIF files.
|
| 478 |
+
|
| 479 |
+
Using the :py:meth:`~PIL.Image.Image.draft` method, you can speed things up by
|
| 480 |
+
converting ``RGB`` images to ``L``, and resize images to 1/2, 1/4 or 1/8 of
|
| 481 |
+
their original size while loading them.
|
| 482 |
+
|
| 483 |
+
The :py:meth:`~PIL.Image.Image.write` method may set the following
|
| 484 |
+
:py:attr:`~PIL.Image.Image.info` properties if available:
|
| 485 |
+
|
| 486 |
+
**jfif**
|
| 487 |
+
JFIF application marker found. If the file is not a JFIF file, this key is
|
| 488 |
+
not present.
|
| 489 |
+
|
| 490 |
+
**jfif_version**
|
| 491 |
+
A tuple representing the jfif version, (major version, minor version).
|
| 492 |
+
|
| 493 |
+
**jfif_density**
|
| 494 |
+
A tuple representing the pixel density of the image, in units specified
|
| 495 |
+
by jfif_unit.
|
| 496 |
+
|
| 497 |
+
**jfif_unit**
|
| 498 |
+
Units for the jfif_density:
|
| 499 |
+
|
| 500 |
+
* 0 - No Units
|
| 501 |
+
* 1 - Pixels per Inch
|
| 502 |
+
* 2 - Pixels per Centimeter
|
| 503 |
+
|
| 504 |
+
**dpi**
|
| 505 |
+
A tuple representing the reported pixel density in pixels per inch, if
|
| 506 |
+
the file is a jfif file and the units are in inches.
|
| 507 |
+
|
| 508 |
+
**adobe**
|
| 509 |
+
Adobe application marker found. If the file is not an Adobe JPEG file, this
|
| 510 |
+
key is not present.
|
| 511 |
+
|
| 512 |
+
**adobe_transform**
|
| 513 |
+
Vendor Specific Tag.
|
| 514 |
+
|
| 515 |
+
**progression**
|
| 516 |
+
Indicates that this is a progressive JPEG file.
|
| 517 |
+
|
| 518 |
+
**icc_profile**
|
| 519 |
+
The ICC color profile for the image.
|
| 520 |
+
|
| 521 |
+
**exif**
|
| 522 |
+
Raw EXIF data from the image.
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
| 526 |
+
|
| 527 |
+
**quality**
|
| 528 |
+
The image quality, on a scale from 1 (worst) to 95 (best). The default is
|
| 529 |
+
75. Values above 95 should be avoided; 100 disables portions of the JPEG
|
| 530 |
+
compression algorithm, and results in large files with hardly any gain in
|
| 531 |
+
image quality.
|
| 532 |
+
|
| 533 |
+
**optimize**
|
| 534 |
+
If present and true, indicates that the encoder should make an extra pass
|
| 535 |
+
over the image in order to select optimal encoder settings.
|
| 536 |
+
|
| 537 |
+
**progressive**
|
| 538 |
+
If present and true, indicates that this image should be stored as a
|
| 539 |
+
progressive JPEG file.
|
| 540 |
+
|
| 541 |
+
**dpi**
|
| 542 |
+
A tuple of integers representing the pixel density, ``(x,y)``.
|
| 543 |
+
|
| 544 |
+
**icc_profile**
|
| 545 |
+
If present and true, the image is stored with the provided ICC profile.
|
| 546 |
+
If this parameter is not provided, the image will be saved with no profile
|
| 547 |
+
attached. To preserve the existing profile::
|
| 548 |
+
|
| 549 |
+
im.save(filename, 'jpeg', icc_profile=im.info.get('icc_profile'))
|
| 550 |
+
|
| 551 |
+
**exif**
|
| 552 |
+
If present, the image will be stored with the provided raw EXIF data.
|
| 553 |
+
|
| 554 |
+
**subsampling**
|
| 555 |
+
If present, sets the subsampling for the encoder.
|
| 556 |
+
|
| 557 |
+
* ``keep``: Only valid for JPEG files, will retain the original image setting.
|
| 558 |
+
* ``4:4:4``, ``4:2:2``, ``4:2:0``: Specific sampling values
|
| 559 |
+
* ``-1``: equivalent to ``keep``
|
| 560 |
+
* ``0``: equivalent to ``4:4:4``
|
| 561 |
+
* ``1``: equivalent to ``4:2:2``
|
| 562 |
+
* ``2``: equivalent to ``4:2:0``
|
| 563 |
+
|
| 564 |
+
**qtables**
|
| 565 |
+
If present, sets the qtables for the encoder. This is listed as an
|
| 566 |
+
advanced option for wizards in the JPEG documentation. Use with
|
| 567 |
+
caution. ``qtables`` can be one of several types of values:
|
| 568 |
+
|
| 569 |
+
* a string, naming a preset, e.g. ``keep``, ``web_low``, or ``web_high``
|
| 570 |
+
* a list, tuple, or dictionary (with integer keys =
|
| 571 |
+
range(len(keys))) of lists of 64 integers. There must be
|
| 572 |
+
between 2 and 4 tables.
|
| 573 |
+
|
| 574 |
+
.. versionadded:: Pillow 2.5.0
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
.. note::
|
| 578 |
+
|
| 579 |
+
To enable JPEG support, you need to build and install the IJG JPEG library
|
| 580 |
+
before building the Python Imaging Library. See the distribution README for
|
| 581 |
+
details.
|
| 582 |
+
""",
|
| 583 |
+
"JPEG2000": """*From the Pillow docs:*
|
| 584 |
+
|
| 585 |
+
|
| 586 |
+
.. versionadded:: Pillow 2.4.0
|
| 587 |
+
|
| 588 |
+
PIL reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB`` or
|
| 589 |
+
``RGBA`` data. It can also read files containing ``YCbCr`` data, which it
|
| 590 |
+
converts on read into ``RGB`` or ``RGBA`` depending on whether or not there is
|
| 591 |
+
an alpha channel. PIL supports JPEG 2000 raw codestreams (``.j2k`` files), as
|
| 592 |
+
well as boxed JPEG 2000 files (``.j2p`` or ``.jpx`` files). PIL does *not*
|
| 593 |
+
support files whose components have different sampling frequencies.
|
| 594 |
+
|
| 595 |
+
When loading, if you set the ``mode`` on the image prior to the
|
| 596 |
+
:py:meth:`~PIL.Image.Image.load` method being invoked, you can ask PIL to
|
| 597 |
+
convert the image to either ``RGB`` or ``RGBA`` rather than choosing for
|
| 598 |
+
itself. It is also possible to set ``reduce`` to the number of resolutions to
|
| 599 |
+
discard (each one reduces the size of the resulting image by a factor of 2),
|
| 600 |
+
and ``layers`` to specify the number of quality layers to load.
|
| 601 |
+
|
| 602 |
+
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
| 603 |
+
|
| 604 |
+
**offset**
|
| 605 |
+
The image offset, as a tuple of integers, e.g. (16, 16)
|
| 606 |
+
|
| 607 |
+
**tile_offset**
|
| 608 |
+
The tile offset, again as a 2-tuple of integers.
|
| 609 |
+
|
| 610 |
+
**tile_size**
|
| 611 |
+
The tile size as a 2-tuple. If not specified, or if set to None, the
|
| 612 |
+
image will be saved without tiling.
|
| 613 |
+
|
| 614 |
+
**quality_mode**
|
| 615 |
+
Either `"rates"` or `"dB"` depending on the units you want to use to
|
| 616 |
+
specify image quality.
|
| 617 |
+
|
| 618 |
+
**quality_layers**
|
| 619 |
+
A sequence of numbers, each of which represents either an approximate size
|
| 620 |
+
reduction (if quality mode is `"rates"`) or a signal to noise ratio value
|
| 621 |
+
in decibels. If not specified, defaults to a single layer of full quality.
|
| 622 |
+
|
| 623 |
+
**num_resolutions**
|
| 624 |
+
The number of different image resolutions to be stored (which corresponds
|
| 625 |
+
to the number of Discrete Wavelet Transform decompositions plus one).
|
| 626 |
+
|
| 627 |
+
**codeblock_size**
|
| 628 |
+
The code-block size as a 2-tuple. Minimum size is 4 x 4, maximum is 1024 x
|
| 629 |
+
1024, with the additional restriction that no code-block may have more
|
| 630 |
+
than 4096 coefficients (i.e. the product of the two numbers must be no
|
| 631 |
+
greater than 4096).
|
| 632 |
+
|
| 633 |
+
**precinct_size**
|
| 634 |
+
The precinct size as a 2-tuple. Must be a power of two along both axes,
|
| 635 |
+
and must be greater than the code-block size.
|
| 636 |
+
|
| 637 |
+
**irreversible**
|
| 638 |
+
If ``True``, use the lossy Irreversible Color Transformation
|
| 639 |
+
followed by DWT 9-7. Defaults to ``False``, which means to use the
|
| 640 |
+
Reversible Color Transformation with DWT 5-3.
|
| 641 |
+
|
| 642 |
+
**progression**
|
| 643 |
+
Controls the progression order; must be one of ``"LRCP"``, ``"RLCP"``,
|
| 644 |
+
``"RPCL"``, ``"PCRL"``, ``"CPRL"``. The letters stand for Component,
|
| 645 |
+
Position, Resolution and Layer respectively and control the order of
|
| 646 |
+
encoding, the idea being that e.g. an image encoded using LRCP mode can
|
| 647 |
+
have its quality layers decoded as they arrive at the decoder, while one
|
| 648 |
+
encoded using RLCP mode will have increasing resolutions decoded as they
|
| 649 |
+
arrive, and so on.
|
| 650 |
+
|
| 651 |
+
**cinema_mode**
|
| 652 |
+
Set the encoder to produce output compliant with the digital cinema
|
| 653 |
+
specifications. The options here are ``"no"`` (the default),
|
| 654 |
+
``"cinema2k-24"`` for 24fps 2K, ``"cinema2k-48"`` for 48fps 2K, and
|
| 655 |
+
``"cinema4k-24"`` for 24fps 4K. Note that for compliant 2K files,
|
| 656 |
+
*at least one* of your image dimensions must match 2048 x 1080, while
|
| 657 |
+
for compliant 4K files, *at least one* of the dimensions must match
|
| 658 |
+
4096 x 2160.
|
| 659 |
+
|
| 660 |
+
.. note::
|
| 661 |
+
|
| 662 |
+
To enable JPEG 2000 support, you need to build and install the OpenJPEG
|
| 663 |
+
library, version 2.0.0 or higher, before building the Python Imaging
|
| 664 |
+
Library.
|
| 665 |
+
|
| 666 |
+
Windows users can install the OpenJPEG binaries available on the
|
| 667 |
+
OpenJPEG website, but must add them to their PATH in order to use PIL (if
|
| 668 |
+
you fail to do this, you will get errors about not being able to load the
|
| 669 |
+
``_imaging`` DLL).
|
| 670 |
+
""",
|
| 671 |
+
"MCIDAS": """*From the Pillow docs:*
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
PIL identifies and reads 8-bit McIdas area files.
|
| 675 |
+
""",
|
| 676 |
+
"MIC": """*From the Pillow docs:*
|
| 677 |
+
|
| 678 |
+
|
| 679 |
+
PIL identifies and reads Microsoft Image Composer (MIC) files. When opened, the
|
| 680 |
+
first sprite in the file is loaded. You can use :py:meth:`~file.seek` and
|
| 681 |
+
:py:meth:`~file.tell` to read other sprites from the file.
|
| 682 |
+
|
| 683 |
+
Note that there may be an embedded gamma of 2.2 in MIC files.
|
| 684 |
+
""",
|
| 685 |
+
"MPEG": """*From the Pillow docs:*
|
| 686 |
+
|
| 687 |
+
|
| 688 |
+
PIL identifies MPEG files.
|
| 689 |
+
""",
|
| 690 |
+
"MPO": """*From the Pillow docs:*
|
| 691 |
+
|
| 692 |
+
|
| 693 |
+
Pillow identifies and reads Multi Picture Object (MPO) files, loading the primary
|
| 694 |
+
image when first opened. The :py:meth:`~file.seek` and :py:meth:`~file.tell`
|
| 695 |
+
methods may be used to read other pictures from the file. The pictures are
|
| 696 |
+
zero-indexed and random access is supported.
|
| 697 |
+
""",
|
| 698 |
+
"MSP": """*From the Pillow docs:*
|
| 699 |
+
|
| 700 |
+
|
| 701 |
+
PIL identifies and reads MSP files from Windows 1 and 2. The library writes
|
| 702 |
+
uncompressed (Windows 1) versions of this format.
|
| 703 |
+
""",
|
| 704 |
+
"PCD": """*From the Pillow docs:*
|
| 705 |
+
|
| 706 |
+
|
| 707 |
+
PIL reads PhotoCD files containing ``RGB`` data. This only reads the 768x512
|
| 708 |
+
resolution image from the file. Higher resolutions are encoded in a proprietary
|
| 709 |
+
encoding.
|
| 710 |
+
""",
|
| 711 |
+
"PCX": """*From the Pillow docs:*
|
| 712 |
+
|
| 713 |
+
|
| 714 |
+
PIL reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data.
|
| 715 |
+
""",
|
| 716 |
+
"PIXAR": """*From the Pillow docs:*
|
| 717 |
+
|
| 718 |
+
|
| 719 |
+
PIL provides limited support for PIXAR raster files. The library can identify
|
| 720 |
+
and read “dumped” RGB files.
|
| 721 |
+
|
| 722 |
+
The format code is ``PIXAR``.
|
| 723 |
+
""",
|
| 724 |
+
"PNG": """*From the Pillow docs:*
|
| 725 |
+
|
| 726 |
+
|
| 727 |
+
PIL identifies, reads, and writes PNG files containing ``1``, ``L``, ``P``,
|
| 728 |
+
``RGB``, or ``RGBA`` data. Interlaced files are supported as of v1.1.7.
|
| 729 |
+
|
| 730 |
+
The :py:meth:`~PIL.Image.Image.write` method sets the following
|
| 731 |
+
:py:attr:`~PIL.Image.Image.info` properties, when appropriate:
|
| 732 |
+
|
| 733 |
+
**chromaticity**
|
| 734 |
+
The chromaticity points, as an 8 tuple of floats. (``White Point
|
| 735 |
+
X``, ``White Point Y``, ``Red X``, ``Red Y``, ``Green X``, ``Green
|
| 736 |
+
Y``, ``Blue X``, ``Blue Y``)
|
| 737 |
+
|
| 738 |
+
**gamma**
|
| 739 |
+
Gamma, given as a floating point number.
|
| 740 |
+
|
| 741 |
+
**srgb**
|
| 742 |
+
The sRGB rendering intent as an integer.
|
| 743 |
+
|
| 744 |
+
* 0 Perceptual
|
| 745 |
+
* 1 Relative Colorimetric
|
| 746 |
+
* 2 Saturation
|
| 747 |
+
* 3 Absolute Colorimetric
|
| 748 |
+
|
| 749 |
+
**transparency**
|
| 750 |
+
For ``P`` images: Either the palette index for full transparent pixels,
|
| 751 |
+
or a byte string with alpha values for each palette entry.
|
| 752 |
+
|
| 753 |
+
For ``L`` and ``RGB`` images, the color that represents full transparent
|
| 754 |
+
pixels in this image.
|
| 755 |
+
|
| 756 |
+
This key is omitted if the image is not a transparent palette image.
|
| 757 |
+
|
| 758 |
+
``Open`` also sets ``Image.text`` to a list of the values of the
|
| 759 |
+
``tEXt``, ``zTXt``, and ``iTXt`` chunks of the PNG image. Individual
|
| 760 |
+
compressed chunks are limited to a decompressed size of
|
| 761 |
+
``PngImagePlugin.MAX_TEXT_CHUNK``, by default 1MB, to prevent
|
| 762 |
+
decompression bombs. Additionally, the total size of all of the text
|
| 763 |
+
chunks is limited to ``PngImagePlugin.MAX_TEXT_MEMORY``, defaulting to
|
| 764 |
+
64MB.
|
| 765 |
+
|
| 766 |
+
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
| 767 |
+
|
| 768 |
+
**optimize**
|
| 769 |
+
If present and true, instructs the PNG writer to make the output file as
|
| 770 |
+
small as possible. This includes extra processing in order to find optimal
|
| 771 |
+
encoder settings.
|
| 772 |
+
|
| 773 |
+
**transparency**
|
| 774 |
+
For ``P``, ``L``, and ``RGB`` images, this option controls what
|
| 775 |
+
color image to mark as transparent.
|
| 776 |
+
|
| 777 |
+
For ``P`` images, this can be a either the palette index,
|
| 778 |
+
or a byte string with alpha values for each palette entry.
|
| 779 |
+
|
| 780 |
+
**dpi**
|
| 781 |
+
A tuple of two numbers corresponding to the desired dpi in each direction.
|
| 782 |
+
|
| 783 |
+
**pnginfo**
|
| 784 |
+
A :py:class:`PIL.PngImagePlugin.PngInfo` instance containing text tags.
|
| 785 |
+
|
| 786 |
+
**compress_level**
|
| 787 |
+
ZLIB compression level, a number between 0 and 9: 1 gives best speed,
|
| 788 |
+
9 gives best compression, 0 gives no compression at all. Default is 6.
|
| 789 |
+
When ``optimize`` option is True ``compress_level`` has no effect
|
| 790 |
+
(it is set to 9 regardless of a value passed).
|
| 791 |
+
|
| 792 |
+
**icc_profile**
|
| 793 |
+
The ICC Profile to include in the saved file.
|
| 794 |
+
|
| 795 |
+
**bits (experimental)**
|
| 796 |
+
For ``P`` images, this option controls how many bits to store. If omitted,
|
| 797 |
+
the PNG writer uses 8 bits (256 colors).
|
| 798 |
+
|
| 799 |
+
**dictionary (experimental)**
|
| 800 |
+
Set the ZLIB encoder dictionary.
|
| 801 |
+
|
| 802 |
+
.. note::
|
| 803 |
+
|
| 804 |
+
To enable PNG support, you need to build and install the ZLIB compression
|
| 805 |
+
library before building the Python Imaging Library. See the installation
|
| 806 |
+
documentation for details.
|
| 807 |
+
""",
|
| 808 |
+
"PPM": """*From the Pillow docs:*
|
| 809 |
+
|
| 810 |
+
|
| 811 |
+
PIL reads and writes PBM, PGM and PPM files containing ``1``, ``L`` or ``RGB``
|
| 812 |
+
data.
|
| 813 |
+
""",
|
| 814 |
+
"PSD": """*From the Pillow docs:*
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
PIL identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
|
| 818 |
+
|
| 819 |
+
""",
|
| 820 |
+
"SGI": """*From the Pillow docs:*
|
| 821 |
+
|
| 822 |
+
|
| 823 |
+
Pillow reads and writes uncompressed ``L``, ``RGB``, and ``RGBA`` files.
|
| 824 |
+
|
| 825 |
+
""",
|
| 826 |
+
"SPIDER": """*From the Pillow docs:*
|
| 827 |
+
|
| 828 |
+
|
| 829 |
+
PIL reads and writes SPIDER image files of 32-bit floating point data
|
| 830 |
+
("F;32F").
|
| 831 |
+
|
| 832 |
+
PIL also reads SPIDER stack files containing sequences of SPIDER images. The
|
| 833 |
+
:py:meth:`~file.seek` and :py:meth:`~file.tell` methods are supported, and
|
| 834 |
+
random access is allowed.
|
| 835 |
+
|
| 836 |
+
The :py:meth:`~PIL.Image.Image.write` method sets the following attributes:
|
| 837 |
+
|
| 838 |
+
**format**
|
| 839 |
+
Set to ``SPIDER``
|
| 840 |
+
|
| 841 |
+
**istack**
|
| 842 |
+
Set to 1 if the file is an image stack, else 0.
|
| 843 |
+
|
| 844 |
+
**nimages**
|
| 845 |
+
Set to the number of images in the stack.
|
| 846 |
+
|
| 847 |
+
A convenience method, :py:meth:`~PIL.Image.Image.convert2byte`, is provided for
|
| 848 |
+
converting floating point data to byte data (mode ``L``)::
|
| 849 |
+
|
| 850 |
+
im = Image.open('image001.spi').convert2byte()
|
| 851 |
+
|
| 852 |
+
Writing files in SPIDER format
|
| 853 |
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 854 |
+
|
| 855 |
+
The extension of SPIDER files may be any 3 alphanumeric characters. Therefore
|
| 856 |
+
the output format must be specified explicitly::
|
| 857 |
+
|
| 858 |
+
im.save('newimage.spi', format='SPIDER')
|
| 859 |
+
|
| 860 |
+
For more information about the SPIDER image processing package, see the
|
| 861 |
+
`SPIDER homepage`_ at `Wadsworth Center`_.
|
| 862 |
+
|
| 863 |
+
.. _SPIDER homepage: https://spider.wadsworth.org/spider_doc/spider/docs/spider.html
|
| 864 |
+
.. _Wadsworth Center: https://www.wadsworth.org/
|
| 865 |
+
""",
|
| 866 |
+
"SUN": """No docs for SUN.""",
|
| 867 |
+
"TGA": """*From the Pillow docs:*
|
| 868 |
+
|
| 869 |
+
|
| 870 |
+
PIL reads 24- and 32-bit uncompressed and run-length encoded TGA files.
|
| 871 |
+
""",
|
| 872 |
+
"TIFF": """*From the Pillow docs:*
|
| 873 |
+
|
| 874 |
+
|
| 875 |
+
Pillow reads and writes TIFF files. It can read both striped and tiled
|
| 876 |
+
images, pixel and plane interleaved multi-band images. If you have
|
| 877 |
+
libtiff and its headers installed, PIL can read and write many kinds
|
| 878 |
+
of compressed TIFF files. If not, PIL will only read and write
|
| 879 |
+
uncompressed files.
|
| 880 |
+
|
| 881 |
+
.. note::
|
| 882 |
+
|
| 883 |
+
Beginning in version 5.0.0, Pillow requires libtiff to read or
|
| 884 |
+
write compressed files. Prior to that release, Pillow had buggy
|
| 885 |
+
support for reading Packbits, LZW and JPEG compressed TIFFs
|
| 886 |
+
without using libtiff.
|
| 887 |
+
|
| 888 |
+
The :py:meth:`~PIL.Image.Image.write` method sets the following
|
| 889 |
+
:py:attr:`~PIL.Image.Image.info` properties:
|
| 890 |
+
|
| 891 |
+
**compression**
|
| 892 |
+
Compression mode.
|
| 893 |
+
|
| 894 |
+
.. versionadded:: Pillow 2.0.0
|
| 895 |
+
|
| 896 |
+
**dpi**
|
| 897 |
+
Image resolution as an ``(xdpi, ydpi)`` tuple, where applicable. You can use
|
| 898 |
+
the :py:attr:`~PIL.Image.Image.tag` attribute to get more detailed
|
| 899 |
+
information about the image resolution.
|
| 900 |
+
|
| 901 |
+
.. versionadded:: Pillow 1.1.5
|
| 902 |
+
|
| 903 |
+
**resolution**
|
| 904 |
+
Image resolution as an ``(xres, yres)`` tuple, where applicable. This is a
|
| 905 |
+
measurement in whichever unit is specified by the file.
|
| 906 |
+
|
| 907 |
+
.. versionadded:: Pillow 1.1.5
|
| 908 |
+
|
| 909 |
+
|
| 910 |
+
The :py:attr:`~PIL.Image.Image.tag_v2` attribute contains a dictionary
|
| 911 |
+
of TIFF metadata. The keys are numerical indexes from
|
| 912 |
+
:py:attr:`~PIL.TiffTags.TAGS_V2`. Values are strings or numbers for single
|
| 913 |
+
items, multiple values are returned in a tuple of values. Rational
|
| 914 |
+
numbers are returned as a :py:class:`~PIL.TiffImagePlugin.IFDRational`
|
| 915 |
+
object.
|
| 916 |
+
|
| 917 |
+
.. versionadded:: Pillow 3.0.0
|
| 918 |
+
|
| 919 |
+
For compatibility with legacy code, the
|
| 920 |
+
:py:attr:`~PIL.Image.Image.tag` attribute contains a dictionary of
|
| 921 |
+
decoded TIFF fields as returned prior to version 3.0.0. Values are
|
| 922 |
+
returned as either strings or tuples of numeric values. Rational
|
| 923 |
+
numbers are returned as a tuple of ``(numerator, denominator)``.
|
| 924 |
+
|
| 925 |
+
.. deprecated:: 3.0.0
|
| 926 |
+
|
| 927 |
+
|
| 928 |
+
Saving Tiff Images
|
| 929 |
+
~~~~~~~~~~~~~~~~~~
|
| 930 |
+
|
| 931 |
+
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
|
| 932 |
+
|
| 933 |
+
**save_all**
|
| 934 |
+
If true, Pillow will save all frames of the image to a multiframe tiff document.
|
| 935 |
+
|
| 936 |
+
.. versionadded:: Pillow 3.4.0
|
| 937 |
+
|
| 938 |
+
**tiffinfo**
|
| 939 |
+
A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` object or dict
|
| 940 |
+
object containing tiff tags and values. The TIFF field type is
|
| 941 |
+
autodetected for Numeric and string values, any other types
|
| 942 |
+
require using an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
|
| 943 |
+
object and setting the type in
|
| 944 |
+
:py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` with
|
| 945 |
+
the appropriate numerical value from
|
| 946 |
+
``TiffTags.TYPES``.
|
| 947 |
+
|
| 948 |
+
.. versionadded:: Pillow 2.3.0
|
| 949 |
+
|
| 950 |
+
Metadata values that are of the rational type should be passed in
|
| 951 |
+
using a :py:class:`~PIL.TiffImagePlugin.IFDRational` object.
|
| 952 |
+
|
| 953 |
+
.. versionadded:: Pillow 3.1.0
|
| 954 |
+
|
| 955 |
+
For compatibility with legacy code, a
|
| 956 |
+
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` object may
|
| 957 |
+
be passed in this field. However, this is deprecated.
|
| 958 |
+
|
| 959 |
+
.. versionadded:: Pillow 3.0.0
|
| 960 |
+
|
| 961 |
+
.. note::
|
| 962 |
+
|
| 963 |
+
Only some tags are currently supported when writing using
|
| 964 |
+
libtiff. The supported list is found in
|
| 965 |
+
:py:attr:`~PIL:TiffTags.LIBTIFF_CORE`.
|
| 966 |
+
|
| 967 |
+
**compression**
|
| 968 |
+
A string containing the desired compression method for the
|
| 969 |
+
file. (valid only with libtiff installed) Valid compression
|
| 970 |
+
methods are: ``None``, ``"tiff_ccitt"``, ``"group3"``,
|
| 971 |
+
``"group4"``, ``"tiff_jpeg"``, ``"tiff_adobe_deflate"``,
|
| 972 |
+
``"tiff_thunderscan"``, ``"tiff_deflate"``, ``"tiff_sgilog"``,
|
| 973 |
+
``"tiff_sgilog24"``, ``"tiff_raw_16"``
|
| 974 |
+
|
| 975 |
+
These arguments to set the tiff header fields are an alternative to
|
| 976 |
+
using the general tags available through tiffinfo.
|
| 977 |
+
|
| 978 |
+
**description**
|
| 979 |
+
|
| 980 |
+
**software**
|
| 981 |
+
|
| 982 |
+
**date_time**
|
| 983 |
+
|
| 984 |
+
**artist**
|
| 985 |
+
|
| 986 |
+
**copyright**
|
| 987 |
+
Strings
|
| 988 |
+
|
| 989 |
+
**resolution_unit**
|
| 990 |
+
A string of "inch", "centimeter" or "cm"
|
| 991 |
+
|
| 992 |
+
**resolution**
|
| 993 |
+
|
| 994 |
+
**x_resolution**
|
| 995 |
+
|
| 996 |
+
**y_resolution**
|
| 997 |
+
|
| 998 |
+
**dpi**
|
| 999 |
+
Either a Float, 2 tuple of (numerator, denominator) or a
|
| 1000 |
+
:py:class:`~PIL.TiffImagePlugin.IFDRational`. Resolution implies
|
| 1001 |
+
an equal x and y resolution, dpi also implies a unit of inches.
|
| 1002 |
+
|
| 1003 |
+
""",
|
| 1004 |
+
"WMF": """*From the Pillow docs:*
|
| 1005 |
+
|
| 1006 |
+
|
| 1007 |
+
PIL can identify playable WMF files.
|
| 1008 |
+
|
| 1009 |
+
In PIL 1.1.4 and earlier, the WMF driver provides some limited rendering
|
| 1010 |
+
support, but not enough to be useful for any real application.
|
| 1011 |
+
|
| 1012 |
+
In PIL 1.1.5 and later, the WMF driver is a stub driver. To add WMF read or
|
| 1013 |
+
write support to your application, use
|
| 1014 |
+
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF handler.
|
| 1015 |
+
|
| 1016 |
+
::
|
| 1017 |
+
|
| 1018 |
+
from PIL import Image
|
| 1019 |
+
from PIL import WmfImagePlugin
|
| 1020 |
+
|
| 1021 |
+
class WmfHandler:
|
| 1022 |
+
def open(self, im):
|
| 1023 |
+
...
|
| 1024 |
+
def load(self, im):
|
| 1025 |
+
...
|
| 1026 |
+
return image
|
| 1027 |
+
def save(self, im, fp, filename):
|
| 1028 |
+
...
|
| 1029 |
+
|
| 1030 |
+
wmf_handler = WmfHandler()
|
| 1031 |
+
|
| 1032 |
+
WmfImagePlugin.register_handler(wmf_handler)
|
| 1033 |
+
|
| 1034 |
+
im = Image.open("sample.wmf")""",
|
| 1035 |
+
"XBM": """*From the Pillow docs:*
|
| 1036 |
+
|
| 1037 |
+
|
| 1038 |
+
PIL reads and writes X bitmap files (mode ``1``).
|
| 1039 |
+
""",
|
| 1040 |
+
"XPM": """*From the Pillow docs:*
|
| 1041 |
+
|
| 1042 |
+
|
| 1043 |
+
PIL reads X pixmap files (mode ``P``) with 256 colors or less.
|
| 1044 |
+
|
| 1045 |
+
The :py:meth:`~PIL.Image.Image.write` method sets the following
|
| 1046 |
+
:py:attr:`~PIL.Image.Image.info` properties:
|
| 1047 |
+
|
| 1048 |
+
**transparency**
|
| 1049 |
+
Transparency color index. This key is omitted if the image is not
|
| 1050 |
+
transparent.
|
| 1051 |
+
""",
|
| 1052 |
+
"XVThumb": """No docs for XVThumb.""",
|
| 1053 |
+
}
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/pillow_legacy.py
ADDED
|
@@ -0,0 +1,832 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
""" Read/Write images using pillow/PIL (legacy).
|
| 5 |
+
|
| 6 |
+
Backend Library: `Pillow <https://pillow.readthedocs.io/en/stable/>`_
|
| 7 |
+
|
| 8 |
+
Pillow is a friendly fork of PIL (Python Image Library) and supports
|
| 9 |
+
reading and writing of common formats (jpg, png, gif, tiff, ...). While
|
| 10 |
+
these docs provide an overview of some of its features, pillow is
|
| 11 |
+
constantly improving. Hence, the complete list of features can be found
|
| 12 |
+
in pillows official docs (see the Backend Library link).
|
| 13 |
+
|
| 14 |
+
Parameters for Reading
|
| 15 |
+
----------------------
|
| 16 |
+
pilmode : str
|
| 17 |
+
(Available for all formates except GIF-PIL)
|
| 18 |
+
From the Pillow documentation:
|
| 19 |
+
|
| 20 |
+
* 'L' (8-bit pixels, grayscale)
|
| 21 |
+
* 'P' (8-bit pixels, mapped to any other mode using a color palette)
|
| 22 |
+
* 'RGB' (3x8-bit pixels, true color)
|
| 23 |
+
* 'RGBA' (4x8-bit pixels, true color with transparency mask)
|
| 24 |
+
* 'CMYK' (4x8-bit pixels, color separation)
|
| 25 |
+
* 'YCbCr' (3x8-bit pixels, color video format)
|
| 26 |
+
* 'I' (32-bit signed integer pixels)
|
| 27 |
+
* 'F' (32-bit floating point pixels)
|
| 28 |
+
|
| 29 |
+
PIL also provides limited support for a few special modes, including
|
| 30 |
+
'LA' ('L' with alpha), 'RGBX' (true color with padding) and 'RGBa'
|
| 31 |
+
(true color with premultiplied alpha).
|
| 32 |
+
|
| 33 |
+
When translating a color image to grayscale (mode 'L', 'I' or 'F'),
|
| 34 |
+
the library uses the ITU-R 601-2 luma transform::
|
| 35 |
+
|
| 36 |
+
L = R * 299/1000 + G * 587/1000 + B * 114/1000
|
| 37 |
+
as_gray : bool
|
| 38 |
+
(Available for all formates except GIF-PIL)
|
| 39 |
+
If True, the image is converted using mode 'F'. When `mode` is
|
| 40 |
+
not None and `as_gray` is True, the image is first converted
|
| 41 |
+
according to `mode`, and the result is then "flattened" using
|
| 42 |
+
mode 'F'.
|
| 43 |
+
ignoregamma : bool
|
| 44 |
+
(Only available in PNG-PIL)
|
| 45 |
+
Avoid gamma correction. Default True.
|
| 46 |
+
exifrotate : bool
|
| 47 |
+
(Only available in JPEG-PIL)
|
| 48 |
+
Automatically rotate the image according to exif flag. Default True.
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
Parameters for saving
|
| 52 |
+
---------------------
|
| 53 |
+
optimize : bool
|
| 54 |
+
(Only available in PNG-PIL)
|
| 55 |
+
If present and true, instructs the PNG writer to make the output file
|
| 56 |
+
as small as possible. This includes extra processing in order to find
|
| 57 |
+
optimal encoder settings.
|
| 58 |
+
transparency:
|
| 59 |
+
(Only available in PNG-PIL)
|
| 60 |
+
This option controls what color image to mark as transparent.
|
| 61 |
+
dpi: tuple of two scalars
|
| 62 |
+
(Only available in PNG-PIL)
|
| 63 |
+
The desired dpi in each direction.
|
| 64 |
+
pnginfo: PIL.PngImagePlugin.PngInfo
|
| 65 |
+
(Only available in PNG-PIL)
|
| 66 |
+
Object containing text tags.
|
| 67 |
+
compress_level: int
|
| 68 |
+
(Only available in PNG-PIL)
|
| 69 |
+
ZLIB compression level, a number between 0 and 9: 1 gives best speed,
|
| 70 |
+
9 gives best compression, 0 gives no compression at all. Default is 9.
|
| 71 |
+
When ``optimize`` option is True ``compress_level`` has no effect
|
| 72 |
+
(it is set to 9 regardless of a value passed).
|
| 73 |
+
compression: int
|
| 74 |
+
(Only available in PNG-PIL)
|
| 75 |
+
Compatibility with the freeimage PNG format. If given, it overrides
|
| 76 |
+
compress_level.
|
| 77 |
+
icc_profile:
|
| 78 |
+
(Only available in PNG-PIL)
|
| 79 |
+
The ICC Profile to include in the saved file.
|
| 80 |
+
bits (experimental): int
|
| 81 |
+
(Only available in PNG-PIL)
|
| 82 |
+
This option controls how many bits to store. If omitted,
|
| 83 |
+
the PNG writer uses 8 bits (256 colors).
|
| 84 |
+
quantize:
|
| 85 |
+
(Only available in PNG-PIL)
|
| 86 |
+
Compatibility with the freeimage PNG format. If given, it overrides
|
| 87 |
+
bits. In this case, given as a number between 1-256.
|
| 88 |
+
dictionary (experimental): dict
|
| 89 |
+
(Only available in PNG-PIL)
|
| 90 |
+
Set the ZLIB encoder dictionary.
|
| 91 |
+
prefer_uint8: bool
|
| 92 |
+
(Only available in PNG-PIL)
|
| 93 |
+
Let the PNG writer truncate uint16 image arrays to uint8 if their values fall
|
| 94 |
+
within the range [0, 255]. Defaults to true for legacy compatibility, however
|
| 95 |
+
it is recommended to set this to false to avoid unexpected behavior when
|
| 96 |
+
saving e.g. weakly saturated images.
|
| 97 |
+
|
| 98 |
+
quality : scalar
|
| 99 |
+
(Only available in JPEG-PIL)
|
| 100 |
+
The compression factor of the saved image (1..100), higher
|
| 101 |
+
numbers result in higher quality but larger file size. Default 75.
|
| 102 |
+
progressive : bool
|
| 103 |
+
(Only available in JPEG-PIL)
|
| 104 |
+
Save as a progressive JPEG file (e.g. for images on the web).
|
| 105 |
+
Default False.
|
| 106 |
+
optimize : bool
|
| 107 |
+
(Only available in JPEG-PIL)
|
| 108 |
+
On saving, compute optimal Huffman coding tables (can reduce a few
|
| 109 |
+
percent of file size). Default False.
|
| 110 |
+
dpi : tuple of int
|
| 111 |
+
(Only available in JPEG-PIL)
|
| 112 |
+
The pixel density, ``(x,y)``.
|
| 113 |
+
icc_profile : object
|
| 114 |
+
(Only available in JPEG-PIL)
|
| 115 |
+
If present and true, the image is stored with the provided ICC profile.
|
| 116 |
+
If this parameter is not provided, the image will be saved with no
|
| 117 |
+
profile attached.
|
| 118 |
+
exif : dict
|
| 119 |
+
(Only available in JPEG-PIL)
|
| 120 |
+
If present, the image will be stored with the provided raw EXIF data.
|
| 121 |
+
subsampling : str
|
| 122 |
+
(Only available in JPEG-PIL)
|
| 123 |
+
Sets the subsampling for the encoder. See Pillow docs for details.
|
| 124 |
+
qtables : object
|
| 125 |
+
(Only available in JPEG-PIL)
|
| 126 |
+
Set the qtables for the encoder. See Pillow docs for details.
|
| 127 |
+
quality_mode : str
|
| 128 |
+
(Only available in JPEG2000-PIL)
|
| 129 |
+
Either `"rates"` or `"dB"` depending on the units you want to use to
|
| 130 |
+
specify image quality.
|
| 131 |
+
quality : float
|
| 132 |
+
(Only available in JPEG2000-PIL)
|
| 133 |
+
Approximate size reduction (if quality mode is `rates`) or a signal to noise ratio
|
| 134 |
+
in decibels (if quality mode is `dB`).
|
| 135 |
+
loop : int
|
| 136 |
+
(Only available in GIF-PIL)
|
| 137 |
+
The number of iterations. Default 0 (meaning loop indefinitely).
|
| 138 |
+
duration : {float, list}
|
| 139 |
+
(Only available in GIF-PIL)
|
| 140 |
+
The duration (in seconds) of each frame. Either specify one value
|
| 141 |
+
that is used for all frames, or one value for each frame.
|
| 142 |
+
Note that in the GIF format the duration/delay is expressed in
|
| 143 |
+
hundredths of a second, which limits the precision of the duration.
|
| 144 |
+
fps : float
|
| 145 |
+
(Only available in GIF-PIL)
|
| 146 |
+
The number of frames per second. If duration is not given, the
|
| 147 |
+
duration for each frame is set to 1/fps. Default 10.
|
| 148 |
+
palettesize : int
|
| 149 |
+
(Only available in GIF-PIL)
|
| 150 |
+
The number of colors to quantize the image to. Is rounded to
|
| 151 |
+
the nearest power of two. Default 256.
|
| 152 |
+
subrectangles : bool
|
| 153 |
+
(Only available in GIF-PIL)
|
| 154 |
+
If True, will try and optimize the GIF by storing only the
|
| 155 |
+
rectangular parts of each frame that change with respect to the
|
| 156 |
+
previous. Default False.
|
| 157 |
+
|
| 158 |
+
Notes
|
| 159 |
+
-----
|
| 160 |
+
To enable JPEG 2000 support, you need to build and install the OpenJPEG library,
|
| 161 |
+
version 2.0.0 or higher, before building the Python Imaging Library. Windows
|
| 162 |
+
users can install the OpenJPEG binaries available on the OpenJPEG website, but
|
| 163 |
+
must add them to their PATH in order to use PIL (if you fail to do this, you
|
| 164 |
+
will get errors about not being able to load the ``_imaging`` DLL).
|
| 165 |
+
|
| 166 |
+
GIF images read with this plugin are always RGBA. The alpha channel is ignored
|
| 167 |
+
when saving RGB images.
|
| 168 |
+
"""
|
| 169 |
+
|
| 170 |
+
import logging
|
| 171 |
+
import threading
|
| 172 |
+
|
| 173 |
+
import numpy as np
|
| 174 |
+
|
| 175 |
+
from ..core import Format, image_as_uint
|
| 176 |
+
from ..core.request import URI_FILE, URI_BYTES
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
logger = logging.getLogger(__name__)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
# todo: Pillow ImageGrab module supports grabbing the screen on Win and OSX.
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
GENERIC_DOCS = """
|
| 186 |
+
Parameters for reading
|
| 187 |
+
----------------------
|
| 188 |
+
|
| 189 |
+
pilmode : str
|
| 190 |
+
From the Pillow documentation:
|
| 191 |
+
|
| 192 |
+
* 'L' (8-bit pixels, grayscale)
|
| 193 |
+
* 'P' (8-bit pixels, mapped to any other mode using a color palette)
|
| 194 |
+
* 'RGB' (3x8-bit pixels, true color)
|
| 195 |
+
* 'RGBA' (4x8-bit pixels, true color with transparency mask)
|
| 196 |
+
* 'CMYK' (4x8-bit pixels, color separation)
|
| 197 |
+
* 'YCbCr' (3x8-bit pixels, color video format)
|
| 198 |
+
* 'I' (32-bit signed integer pixels)
|
| 199 |
+
* 'F' (32-bit floating point pixels)
|
| 200 |
+
|
| 201 |
+
PIL also provides limited support for a few special modes, including
|
| 202 |
+
'LA' ('L' with alpha), 'RGBX' (true color with padding) and 'RGBa'
|
| 203 |
+
(true color with premultiplied alpha).
|
| 204 |
+
|
| 205 |
+
When translating a color image to grayscale (mode 'L', 'I' or 'F'),
|
| 206 |
+
the library uses the ITU-R 601-2 luma transform::
|
| 207 |
+
|
| 208 |
+
L = R * 299/1000 + G * 587/1000 + B * 114/1000
|
| 209 |
+
as_gray : bool
|
| 210 |
+
If True, the image is converted using mode 'F'. When `mode` is
|
| 211 |
+
not None and `as_gray` is True, the image is first converted
|
| 212 |
+
according to `mode`, and the result is then "flattened" using
|
| 213 |
+
mode 'F'.
|
| 214 |
+
"""
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
class PillowFormat(Format):
|
| 218 |
+
"""
|
| 219 |
+
Base format class for Pillow formats.
|
| 220 |
+
"""
|
| 221 |
+
|
| 222 |
+
_pillow_imported = False
|
| 223 |
+
_Image = None
|
| 224 |
+
_modes = "i"
|
| 225 |
+
_description = ""
|
| 226 |
+
|
| 227 |
+
def __init__(self, *args, plugin_id: str = None, **kwargs):
|
| 228 |
+
|
| 229 |
+
super(PillowFormat, self).__init__(*args, **kwargs)
|
| 230 |
+
# Used to synchronize _init_pillow(), see #244
|
| 231 |
+
self._lock = threading.RLock()
|
| 232 |
+
|
| 233 |
+
self._plugin_id = plugin_id
|
| 234 |
+
|
| 235 |
+
@property
|
| 236 |
+
def plugin_id(self):
|
| 237 |
+
"""The PIL plugin id."""
|
| 238 |
+
return self._plugin_id # Set when format is created
|
| 239 |
+
|
| 240 |
+
def _init_pillow(self):
|
| 241 |
+
with self._lock:
|
| 242 |
+
if not self._pillow_imported:
|
| 243 |
+
self._pillow_imported = True # more like tried to import
|
| 244 |
+
import PIL
|
| 245 |
+
|
| 246 |
+
if not hasattr(PIL, "__version__"): # pragma: no cover
|
| 247 |
+
raise ImportError(
|
| 248 |
+
"Imageio Pillow plugin requires " "Pillow, not PIL!"
|
| 249 |
+
)
|
| 250 |
+
from PIL import Image
|
| 251 |
+
|
| 252 |
+
self._Image = Image
|
| 253 |
+
elif self._Image is None: # pragma: no cover
|
| 254 |
+
raise RuntimeError("Imageio Pillow plugin requires " "Pillow lib.")
|
| 255 |
+
Image = self._Image
|
| 256 |
+
|
| 257 |
+
if self.plugin_id in ("PNG", "JPEG", "BMP", "GIF", "PPM"):
|
| 258 |
+
Image.preinit()
|
| 259 |
+
else:
|
| 260 |
+
Image.init()
|
| 261 |
+
return Image
|
| 262 |
+
|
| 263 |
+
def _can_read(self, request):
|
| 264 |
+
Image = self._init_pillow()
|
| 265 |
+
if request.mode[1] in (self.modes + "?"):
|
| 266 |
+
if self.plugin_id in Image.OPEN:
|
| 267 |
+
factory, accept = Image.OPEN[self.plugin_id]
|
| 268 |
+
if accept:
|
| 269 |
+
if request.firstbytes and accept(request.firstbytes):
|
| 270 |
+
return True
|
| 271 |
+
|
| 272 |
+
def _can_write(self, request):
|
| 273 |
+
Image = self._init_pillow()
|
| 274 |
+
if request.mode[1] in (self.modes + "?"):
|
| 275 |
+
if request.extension in self.extensions or request._uri_type in [
|
| 276 |
+
URI_FILE,
|
| 277 |
+
URI_BYTES,
|
| 278 |
+
]:
|
| 279 |
+
if self.plugin_id in Image.SAVE:
|
| 280 |
+
return True
|
| 281 |
+
|
| 282 |
+
class Reader(Format.Reader):
|
| 283 |
+
def _open(self, pilmode=None, as_gray=False):
|
| 284 |
+
Image = self.format._init_pillow()
|
| 285 |
+
try:
|
| 286 |
+
factory, accept = Image.OPEN[self.format.plugin_id]
|
| 287 |
+
except KeyError:
|
| 288 |
+
raise RuntimeError("Format %s cannot read images." % self.format.name)
|
| 289 |
+
self._fp = self._get_file()
|
| 290 |
+
self._im = factory(self._fp, "")
|
| 291 |
+
if hasattr(Image, "_decompression_bomb_check"):
|
| 292 |
+
Image._decompression_bomb_check(self._im.size)
|
| 293 |
+
# Save the raw mode used by the palette for a BMP because it may not be the number of channels
|
| 294 |
+
# When the data is read, imageio hands the palette to PIL to handle and clears the rawmode argument
|
| 295 |
+
# However, there is a bug in PIL with handling animated GIFs with a different color palette on each frame.
|
| 296 |
+
# This issue is resolved by using the raw palette data but the rawmode information is now lost. So we
|
| 297 |
+
# store the raw mode for later use
|
| 298 |
+
if self._im.palette and self._im.palette.dirty:
|
| 299 |
+
self._im.palette.rawmode_saved = self._im.palette.rawmode
|
| 300 |
+
pil_try_read(self._im)
|
| 301 |
+
# Store args
|
| 302 |
+
self._kwargs = dict(
|
| 303 |
+
as_gray=as_gray, is_gray=_palette_is_grayscale(self._im)
|
| 304 |
+
)
|
| 305 |
+
# setting mode=None is not the same as just not providing it
|
| 306 |
+
if pilmode is not None:
|
| 307 |
+
self._kwargs["mode"] = pilmode
|
| 308 |
+
# Set length
|
| 309 |
+
self._length = 1
|
| 310 |
+
if hasattr(self._im, "n_frames"):
|
| 311 |
+
self._length = self._im.n_frames
|
| 312 |
+
|
| 313 |
+
def _get_file(self):
|
| 314 |
+
self._we_own_fp = False
|
| 315 |
+
return self.request.get_file()
|
| 316 |
+
|
| 317 |
+
def _close(self):
|
| 318 |
+
save_pillow_close(self._im)
|
| 319 |
+
if self._we_own_fp:
|
| 320 |
+
self._fp.close()
|
| 321 |
+
# else: request object handles closing the _fp
|
| 322 |
+
|
| 323 |
+
def _get_length(self):
|
| 324 |
+
return self._length
|
| 325 |
+
|
| 326 |
+
def _seek(self, index):
|
| 327 |
+
try:
|
| 328 |
+
self._im.seek(index)
|
| 329 |
+
except EOFError:
|
| 330 |
+
raise IndexError("Could not seek to index %i" % index)
|
| 331 |
+
|
| 332 |
+
def _get_data(self, index):
|
| 333 |
+
if index >= self._length:
|
| 334 |
+
raise IndexError("Image index %i > %i" % (index, self._length))
|
| 335 |
+
i = self._im.tell()
|
| 336 |
+
if i > index:
|
| 337 |
+
self._seek(index) # just try
|
| 338 |
+
else:
|
| 339 |
+
while i < index: # some formats need to be read in sequence
|
| 340 |
+
i += 1
|
| 341 |
+
self._seek(i)
|
| 342 |
+
if self._im.palette and self._im.palette.dirty:
|
| 343 |
+
self._im.palette.rawmode_saved = self._im.palette.rawmode
|
| 344 |
+
self._im.getdata()[0]
|
| 345 |
+
im = pil_get_frame(self._im, **self._kwargs)
|
| 346 |
+
return im, self._im.info
|
| 347 |
+
|
| 348 |
+
def _get_meta_data(self, index):
|
| 349 |
+
if not (index is None or index == 0):
|
| 350 |
+
raise IndexError()
|
| 351 |
+
return self._im.info
|
| 352 |
+
|
| 353 |
+
class Writer(Format.Writer):
|
| 354 |
+
def _open(self):
|
| 355 |
+
Image = self.format._init_pillow()
|
| 356 |
+
try:
|
| 357 |
+
self._save_func = Image.SAVE[self.format.plugin_id]
|
| 358 |
+
except KeyError:
|
| 359 |
+
raise RuntimeError("Format %s cannot write images." % self.format.name)
|
| 360 |
+
self._fp = self.request.get_file()
|
| 361 |
+
self._meta = {}
|
| 362 |
+
self._written = False
|
| 363 |
+
|
| 364 |
+
def _close(self):
|
| 365 |
+
pass # request object handled closing _fp
|
| 366 |
+
|
| 367 |
+
def _append_data(self, im, meta):
|
| 368 |
+
if self._written:
|
| 369 |
+
raise RuntimeError(
|
| 370 |
+
"Format %s only supports single images." % self.format.name
|
| 371 |
+
)
|
| 372 |
+
# Pop unit dimension for grayscale images
|
| 373 |
+
if im.ndim == 3 and im.shape[-1] == 1:
|
| 374 |
+
im = im[:, :, 0]
|
| 375 |
+
self._written = True
|
| 376 |
+
self._meta.update(meta)
|
| 377 |
+
img = ndarray_to_pil(
|
| 378 |
+
im, self.format.plugin_id, self._meta.pop("prefer_uint8", True)
|
| 379 |
+
)
|
| 380 |
+
if "bits" in self._meta:
|
| 381 |
+
img = img.quantize() # Make it a P image, so bits arg is used
|
| 382 |
+
img.save(self._fp, format=self.format.plugin_id, **self._meta)
|
| 383 |
+
save_pillow_close(img)
|
| 384 |
+
|
| 385 |
+
def set_meta_data(self, meta):
|
| 386 |
+
self._meta.update(meta)
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
class PNGFormat(PillowFormat):
|
| 390 |
+
"""See :mod:`imageio.plugins.pillow_legacy`"""
|
| 391 |
+
|
| 392 |
+
class Reader(PillowFormat.Reader):
|
| 393 |
+
def _open(self, pilmode=None, as_gray=False, ignoregamma=True):
|
| 394 |
+
return PillowFormat.Reader._open(self, pilmode=pilmode, as_gray=as_gray)
|
| 395 |
+
|
| 396 |
+
def _get_data(self, index):
|
| 397 |
+
im, info = PillowFormat.Reader._get_data(self, index)
|
| 398 |
+
if not self.request.kwargs.get("ignoregamma", True):
|
| 399 |
+
# The gamma value in the file represents the gamma factor for the
|
| 400 |
+
# hardware on the system where the file was created, and is meant
|
| 401 |
+
# to be able to match the colors with the system on which the
|
| 402 |
+
# image is shown. See also issue #366
|
| 403 |
+
try:
|
| 404 |
+
gamma = float(info["gamma"])
|
| 405 |
+
except (KeyError, ValueError):
|
| 406 |
+
pass
|
| 407 |
+
else:
|
| 408 |
+
scale = float(65536 if im.dtype == np.uint16 else 255)
|
| 409 |
+
gain = 1.0
|
| 410 |
+
im[:] = ((im / scale) ** gamma) * scale * gain + 0.4999
|
| 411 |
+
return im, info
|
| 412 |
+
|
| 413 |
+
# --
|
| 414 |
+
|
| 415 |
+
class Writer(PillowFormat.Writer):
|
| 416 |
+
def _open(self, compression=None, quantize=None, interlaced=False, **kwargs):
|
| 417 |
+
|
| 418 |
+
# Better default for compression
|
| 419 |
+
kwargs["compress_level"] = kwargs.get("compress_level", 9)
|
| 420 |
+
|
| 421 |
+
if compression is not None:
|
| 422 |
+
if compression < 0 or compression > 9:
|
| 423 |
+
raise ValueError("Invalid PNG compression level: %r" % compression)
|
| 424 |
+
kwargs["compress_level"] = compression
|
| 425 |
+
if quantize is not None:
|
| 426 |
+
for bits in range(1, 9):
|
| 427 |
+
if 2**bits == quantize:
|
| 428 |
+
break
|
| 429 |
+
else:
|
| 430 |
+
raise ValueError(
|
| 431 |
+
"PNG quantize must be power of two, " "not %r" % quantize
|
| 432 |
+
)
|
| 433 |
+
kwargs["bits"] = bits
|
| 434 |
+
if interlaced:
|
| 435 |
+
logger.warning("PIL PNG writer cannot produce interlaced images.")
|
| 436 |
+
|
| 437 |
+
ok_keys = (
|
| 438 |
+
"optimize",
|
| 439 |
+
"transparency",
|
| 440 |
+
"dpi",
|
| 441 |
+
"pnginfo",
|
| 442 |
+
"bits",
|
| 443 |
+
"compress_level",
|
| 444 |
+
"icc_profile",
|
| 445 |
+
"dictionary",
|
| 446 |
+
"prefer_uint8",
|
| 447 |
+
)
|
| 448 |
+
for key in kwargs:
|
| 449 |
+
if key not in ok_keys:
|
| 450 |
+
raise TypeError("Invalid arg for PNG writer: %r" % key)
|
| 451 |
+
|
| 452 |
+
PillowFormat.Writer._open(self)
|
| 453 |
+
self._meta.update(kwargs)
|
| 454 |
+
|
| 455 |
+
def _append_data(self, im, meta):
|
| 456 |
+
if str(im.dtype) == "uint16" and (im.ndim == 2 or im.shape[-1] == 1):
|
| 457 |
+
im = image_as_uint(im, bitdepth=16)
|
| 458 |
+
else:
|
| 459 |
+
im = image_as_uint(im, bitdepth=8)
|
| 460 |
+
PillowFormat.Writer._append_data(self, im, meta)
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
class JPEGFormat(PillowFormat):
|
| 464 |
+
"""See :mod:`imageio.plugins.pillow_legacy`"""
|
| 465 |
+
|
| 466 |
+
class Reader(PillowFormat.Reader):
|
| 467 |
+
def _open(self, pilmode=None, as_gray=False, exifrotate=True):
|
| 468 |
+
return PillowFormat.Reader._open(self, pilmode=pilmode, as_gray=as_gray)
|
| 469 |
+
|
| 470 |
+
def _get_file(self):
|
| 471 |
+
# Pillow uses seek for JPG, so we cannot directly stream from web
|
| 472 |
+
if self.request.filename.startswith(
|
| 473 |
+
("http://", "https://")
|
| 474 |
+
) or ".zip/" in self.request.filename.replace("\\", "/"):
|
| 475 |
+
self._we_own_fp = True
|
| 476 |
+
return open(self.request.get_local_filename(), "rb")
|
| 477 |
+
else:
|
| 478 |
+
self._we_own_fp = False
|
| 479 |
+
return self.request.get_file()
|
| 480 |
+
|
| 481 |
+
def _get_data(self, index):
|
| 482 |
+
im, info = PillowFormat.Reader._get_data(self, index)
|
| 483 |
+
|
| 484 |
+
# Handle exif
|
| 485 |
+
if "exif" in info:
|
| 486 |
+
from PIL.ExifTags import TAGS
|
| 487 |
+
|
| 488 |
+
info["EXIF_MAIN"] = {}
|
| 489 |
+
for tag, value in self._im._getexif().items():
|
| 490 |
+
decoded = TAGS.get(tag, tag)
|
| 491 |
+
info["EXIF_MAIN"][decoded] = value
|
| 492 |
+
|
| 493 |
+
im = self._rotate(im, info)
|
| 494 |
+
return im, info
|
| 495 |
+
|
| 496 |
+
def _rotate(self, im, meta):
|
| 497 |
+
"""Use Orientation information from EXIF meta data to
|
| 498 |
+
orient the image correctly. Similar code as in FreeImage plugin.
|
| 499 |
+
"""
|
| 500 |
+
if self.request.kwargs.get("exifrotate", True):
|
| 501 |
+
try:
|
| 502 |
+
ori = meta["EXIF_MAIN"]["Orientation"]
|
| 503 |
+
except KeyError: # pragma: no cover
|
| 504 |
+
pass # Orientation not available
|
| 505 |
+
else: # pragma: no cover - we cannot touch all cases
|
| 506 |
+
# www.impulseadventure.com/photo/exif-orientation.html
|
| 507 |
+
if ori in [1, 2]:
|
| 508 |
+
pass
|
| 509 |
+
if ori in [3, 4]:
|
| 510 |
+
im = np.rot90(im, 2)
|
| 511 |
+
if ori in [5, 6]:
|
| 512 |
+
im = np.rot90(im, 3)
|
| 513 |
+
if ori in [7, 8]:
|
| 514 |
+
im = np.rot90(im)
|
| 515 |
+
if ori in [2, 4, 5, 7]: # Flipped cases (rare)
|
| 516 |
+
im = np.fliplr(im)
|
| 517 |
+
return im
|
| 518 |
+
|
| 519 |
+
# --
|
| 520 |
+
|
| 521 |
+
class Writer(PillowFormat.Writer):
|
| 522 |
+
def _open(self, quality=75, progressive=False, optimize=False, **kwargs):
|
| 523 |
+
|
| 524 |
+
# The JPEG quality can be between 0 (worst) and 100 (best)
|
| 525 |
+
quality = int(quality)
|
| 526 |
+
if quality < 0 or quality > 100:
|
| 527 |
+
raise ValueError("JPEG quality should be between 0 and 100.")
|
| 528 |
+
|
| 529 |
+
kwargs["quality"] = quality
|
| 530 |
+
kwargs["progressive"] = bool(progressive)
|
| 531 |
+
kwargs["optimize"] = bool(progressive)
|
| 532 |
+
|
| 533 |
+
PillowFormat.Writer._open(self)
|
| 534 |
+
self._meta.update(kwargs)
|
| 535 |
+
|
| 536 |
+
def _append_data(self, im, meta):
|
| 537 |
+
if im.ndim == 3 and im.shape[-1] == 4:
|
| 538 |
+
raise IOError("JPEG does not support alpha channel.")
|
| 539 |
+
im = image_as_uint(im, bitdepth=8)
|
| 540 |
+
PillowFormat.Writer._append_data(self, im, meta)
|
| 541 |
+
return
|
| 542 |
+
|
| 543 |
+
|
| 544 |
+
class JPEG2000Format(PillowFormat):
|
| 545 |
+
"""See :mod:`imageio.plugins.pillow_legacy`"""
|
| 546 |
+
|
| 547 |
+
class Reader(PillowFormat.Reader):
|
| 548 |
+
def _open(self, pilmode=None, as_gray=False):
|
| 549 |
+
return PillowFormat.Reader._open(self, pilmode=pilmode, as_gray=as_gray)
|
| 550 |
+
|
| 551 |
+
def _get_file(self):
|
| 552 |
+
# Pillow uses seek for JPG, so we cannot directly stream from web
|
| 553 |
+
if self.request.filename.startswith(
|
| 554 |
+
("http://", "https://")
|
| 555 |
+
) or ".zip/" in self.request.filename.replace("\\", "/"):
|
| 556 |
+
self._we_own_fp = True
|
| 557 |
+
return open(self.request.get_local_filename(), "rb")
|
| 558 |
+
else:
|
| 559 |
+
self._we_own_fp = False
|
| 560 |
+
return self.request.get_file()
|
| 561 |
+
|
| 562 |
+
def _get_data(self, index):
|
| 563 |
+
im, info = PillowFormat.Reader._get_data(self, index)
|
| 564 |
+
|
| 565 |
+
# Handle exif
|
| 566 |
+
if "exif" in info:
|
| 567 |
+
from PIL.ExifTags import TAGS
|
| 568 |
+
|
| 569 |
+
info["EXIF_MAIN"] = {}
|
| 570 |
+
for tag, value in self._im._getexif().items():
|
| 571 |
+
decoded = TAGS.get(tag, tag)
|
| 572 |
+
info["EXIF_MAIN"][decoded] = value
|
| 573 |
+
|
| 574 |
+
im = self._rotate(im, info)
|
| 575 |
+
return im, info
|
| 576 |
+
|
| 577 |
+
def _rotate(self, im, meta):
|
| 578 |
+
"""Use Orientation information from EXIF meta data to
|
| 579 |
+
orient the image correctly. Similar code as in FreeImage plugin.
|
| 580 |
+
"""
|
| 581 |
+
if self.request.kwargs.get("exifrotate", True):
|
| 582 |
+
try:
|
| 583 |
+
ori = meta["EXIF_MAIN"]["Orientation"]
|
| 584 |
+
except KeyError: # pragma: no cover
|
| 585 |
+
pass # Orientation not available
|
| 586 |
+
else: # pragma: no cover - we cannot touch all cases
|
| 587 |
+
# www.impulseadventure.com/photo/exif-orientation.html
|
| 588 |
+
if ori in [1, 2]:
|
| 589 |
+
pass
|
| 590 |
+
if ori in [3, 4]:
|
| 591 |
+
im = np.rot90(im, 2)
|
| 592 |
+
if ori in [5, 6]:
|
| 593 |
+
im = np.rot90(im, 3)
|
| 594 |
+
if ori in [7, 8]:
|
| 595 |
+
im = np.rot90(im)
|
| 596 |
+
if ori in [2, 4, 5, 7]: # Flipped cases (rare)
|
| 597 |
+
im = np.fliplr(im)
|
| 598 |
+
return im
|
| 599 |
+
|
| 600 |
+
# --
|
| 601 |
+
|
| 602 |
+
class Writer(PillowFormat.Writer):
|
| 603 |
+
def _open(self, quality_mode="rates", quality=5, **kwargs):
|
| 604 |
+
|
| 605 |
+
# Check quality - in Pillow it should be no higher than 95
|
| 606 |
+
if quality_mode not in {"rates", "dB"}:
|
| 607 |
+
raise ValueError("Quality mode should be either 'rates' or 'dB'")
|
| 608 |
+
|
| 609 |
+
quality = float(quality)
|
| 610 |
+
|
| 611 |
+
if quality_mode == "rates" and (quality < 1 or quality > 1000):
|
| 612 |
+
raise ValueError(
|
| 613 |
+
"The quality value {} seems to be an invalid rate!".format(quality)
|
| 614 |
+
)
|
| 615 |
+
elif quality_mode == "dB" and (quality < 15 or quality > 100):
|
| 616 |
+
raise ValueError(
|
| 617 |
+
"The quality value {} seems to be an invalid PSNR!".format(quality)
|
| 618 |
+
)
|
| 619 |
+
|
| 620 |
+
kwargs["quality_mode"] = quality_mode
|
| 621 |
+
kwargs["quality_layers"] = [quality]
|
| 622 |
+
|
| 623 |
+
PillowFormat.Writer._open(self)
|
| 624 |
+
self._meta.update(kwargs)
|
| 625 |
+
|
| 626 |
+
def _append_data(self, im, meta):
|
| 627 |
+
if im.ndim == 3 and im.shape[-1] == 4:
|
| 628 |
+
raise IOError(
|
| 629 |
+
"The current implementation of JPEG 2000 does not support alpha channel."
|
| 630 |
+
)
|
| 631 |
+
im = image_as_uint(im, bitdepth=8)
|
| 632 |
+
PillowFormat.Writer._append_data(self, im, meta)
|
| 633 |
+
return
|
| 634 |
+
|
| 635 |
+
|
| 636 |
+
def save_pillow_close(im):
|
| 637 |
+
# see issue #216 and #300
|
| 638 |
+
if hasattr(im, "close"):
|
| 639 |
+
if hasattr(getattr(im, "fp", None), "close"):
|
| 640 |
+
im.close()
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
# Func from skimage
|
| 644 |
+
|
| 645 |
+
# This cells contains code from scikit-image, in particular from
|
| 646 |
+
# http://github.com/scikit-image/scikit-image/blob/master/
|
| 647 |
+
# skimage/io/_plugins/pil_plugin.py
|
| 648 |
+
# The scikit-image license applies.
|
| 649 |
+
|
| 650 |
+
|
| 651 |
+
def pil_try_read(im):
|
| 652 |
+
try:
|
| 653 |
+
# this will raise an IOError if the file is not readable
|
| 654 |
+
im.getdata()[0]
|
| 655 |
+
except IOError as e:
|
| 656 |
+
site = "http://pillow.readthedocs.io/en/latest/installation.html"
|
| 657 |
+
site += "#external-libraries"
|
| 658 |
+
pillow_error_message = str(e)
|
| 659 |
+
error_message = (
|
| 660 |
+
'Could not load "%s" \n'
|
| 661 |
+
'Reason: "%s"\n'
|
| 662 |
+
"Please see documentation at: %s"
|
| 663 |
+
% (im.filename, pillow_error_message, site)
|
| 664 |
+
)
|
| 665 |
+
raise ValueError(error_message)
|
| 666 |
+
|
| 667 |
+
|
| 668 |
+
def _palette_is_grayscale(pil_image):
|
| 669 |
+
if pil_image.mode != "P":
|
| 670 |
+
return False
|
| 671 |
+
elif pil_image.info.get("transparency", None): # see issue #475
|
| 672 |
+
return False
|
| 673 |
+
# get palette as an array with R, G, B columns
|
| 674 |
+
# Note: starting in pillow 9.1 palettes may have less than 256 entries
|
| 675 |
+
palette = np.asarray(pil_image.getpalette()).reshape((-1, 3))
|
| 676 |
+
# Not all palette colors are used; unused colors have junk values.
|
| 677 |
+
start, stop = pil_image.getextrema()
|
| 678 |
+
valid_palette = palette[start : stop + 1]
|
| 679 |
+
# Image is grayscale if channel differences (R - G and G - B)
|
| 680 |
+
# are all zero.
|
| 681 |
+
return np.allclose(np.diff(valid_palette), 0)
|
| 682 |
+
|
| 683 |
+
|
| 684 |
+
def pil_get_frame(im, is_gray=None, as_gray=None, mode=None, dtype=None):
|
| 685 |
+
"""
|
| 686 |
+
is_gray: Whether the image *is* gray (by inspecting its palette).
|
| 687 |
+
as_gray: Whether the resulting image must be converted to gaey.
|
| 688 |
+
mode: The mode to convert to.
|
| 689 |
+
"""
|
| 690 |
+
|
| 691 |
+
if is_gray is None:
|
| 692 |
+
is_gray = _palette_is_grayscale(im)
|
| 693 |
+
|
| 694 |
+
frame = im
|
| 695 |
+
|
| 696 |
+
# Convert ...
|
| 697 |
+
if mode is not None:
|
| 698 |
+
# Mode is explicitly given ...
|
| 699 |
+
if mode != im.mode:
|
| 700 |
+
frame = im.convert(mode)
|
| 701 |
+
elif as_gray:
|
| 702 |
+
pass # don't do any auto-conversions (but do the explit one above)
|
| 703 |
+
elif im.mode == "P" and is_gray:
|
| 704 |
+
# Paletted images that are already gray by their palette
|
| 705 |
+
# are converted so that the resulting numpy array is 2D.
|
| 706 |
+
frame = im.convert("L")
|
| 707 |
+
elif im.mode == "P":
|
| 708 |
+
# Paletted images are converted to RGB/RGBA. We jump some loops to make
|
| 709 |
+
# this work well.
|
| 710 |
+
if im.info.get("transparency", None) is not None:
|
| 711 |
+
# Let Pillow apply the transparency, see issue #210 and #246
|
| 712 |
+
frame = im.convert("RGBA")
|
| 713 |
+
elif im.palette.mode in ("RGB", "RGBA"):
|
| 714 |
+
# We can do this ourselves. Pillow seems to sometimes screw
|
| 715 |
+
# this up if a multi-gif has a palette for each frame ...
|
| 716 |
+
# Create palette array
|
| 717 |
+
p = np.frombuffer(im.palette.getdata()[1], np.uint8)
|
| 718 |
+
# Restore the raw mode that was saved to be used to parse the palette
|
| 719 |
+
if hasattr(im.palette, "rawmode_saved"):
|
| 720 |
+
im.palette.rawmode = im.palette.rawmode_saved
|
| 721 |
+
mode = im.palette.rawmode if im.palette.rawmode else im.palette.mode
|
| 722 |
+
nchannels = len(mode)
|
| 723 |
+
# Shape it.
|
| 724 |
+
p.shape = -1, nchannels
|
| 725 |
+
if p.shape[1] == 3 or (p.shape[1] == 4 and mode[-1] == "X"):
|
| 726 |
+
p = np.column_stack((p[:, :3], 255 * np.ones(p.shape[0], p.dtype)))
|
| 727 |
+
# Swap the axes if the mode is in BGR and not RGB
|
| 728 |
+
if mode.startswith("BGR"):
|
| 729 |
+
p = p[:, [2, 1, 0]] if p.shape[1] == 3 else p[:, [2, 1, 0, 3]]
|
| 730 |
+
# Apply palette
|
| 731 |
+
frame_paletted = np.array(im, np.uint8)
|
| 732 |
+
try:
|
| 733 |
+
frame = p[frame_paletted]
|
| 734 |
+
except Exception:
|
| 735 |
+
# Ok, let PIL do it. The introduction of the branch that
|
| 736 |
+
# tests `im.info['transparency']` should make this happen
|
| 737 |
+
# much less often, but let's keep it, to be safe.
|
| 738 |
+
frame = im.convert("RGBA")
|
| 739 |
+
else:
|
| 740 |
+
# Let Pillow do it. Unlinke skimage, we always convert
|
| 741 |
+
# to RGBA; palettes can be RGBA.
|
| 742 |
+
if True: # im.format == 'PNG' and 'transparency' in im.info:
|
| 743 |
+
frame = im.convert("RGBA")
|
| 744 |
+
else:
|
| 745 |
+
frame = im.convert("RGB")
|
| 746 |
+
elif "A" in im.mode:
|
| 747 |
+
frame = im.convert("RGBA")
|
| 748 |
+
elif im.mode == "CMYK":
|
| 749 |
+
frame = im.convert("RGB")
|
| 750 |
+
elif im.format == "GIF" and im.mode == "RGB":
|
| 751 |
+
# pillow9 returns RGBA images for subsequent frames so that it can deal
|
| 752 |
+
# with multi-frame GIF that use frame-level palettes and don't dispose
|
| 753 |
+
# all areas.
|
| 754 |
+
|
| 755 |
+
# For backwards compatibility, we promote everything to RGBA.
|
| 756 |
+
frame = im.convert("RGBA")
|
| 757 |
+
|
| 758 |
+
# Apply a post-convert if necessary
|
| 759 |
+
if as_gray:
|
| 760 |
+
frame = frame.convert("F") # Scipy compat
|
| 761 |
+
elif not isinstance(frame, np.ndarray) and frame.mode == "1":
|
| 762 |
+
# Workaround for crash in PIL. When im is 1-bit, the call array(im)
|
| 763 |
+
# can cause a segfault, or generate garbage. See
|
| 764 |
+
# https://github.com/scipy/scipy/issues/2138 and
|
| 765 |
+
# https://github.com/python-pillow/Pillow/issues/350.
|
| 766 |
+
#
|
| 767 |
+
# This converts im from a 1-bit image to an 8-bit image.
|
| 768 |
+
frame = frame.convert("L")
|
| 769 |
+
|
| 770 |
+
# Convert to numpy array
|
| 771 |
+
if im.mode.startswith("I;16"):
|
| 772 |
+
# e.g. in16 PNG's
|
| 773 |
+
shape = im.size
|
| 774 |
+
dtype = ">u2" if im.mode.endswith("B") else "<u2"
|
| 775 |
+
if "S" in im.mode:
|
| 776 |
+
dtype = dtype.replace("u", "i")
|
| 777 |
+
frame = np.frombuffer(frame.tobytes(), dtype).copy()
|
| 778 |
+
frame.shape = shape[::-1]
|
| 779 |
+
else:
|
| 780 |
+
# Use uint16 for PNG's in mode I
|
| 781 |
+
if im.format == "PNG" and im.mode == "I" and dtype is None:
|
| 782 |
+
dtype = "uint16"
|
| 783 |
+
frame = np.array(frame, dtype=dtype)
|
| 784 |
+
|
| 785 |
+
return frame
|
| 786 |
+
|
| 787 |
+
|
| 788 |
+
def ndarray_to_pil(arr, format_str=None, prefer_uint8=True):
|
| 789 |
+
|
| 790 |
+
from PIL import Image
|
| 791 |
+
|
| 792 |
+
if arr.ndim == 3:
|
| 793 |
+
arr = image_as_uint(arr, bitdepth=8)
|
| 794 |
+
mode = {3: "RGB", 4: "RGBA"}[arr.shape[2]]
|
| 795 |
+
|
| 796 |
+
elif format_str in ["png", "PNG"]:
|
| 797 |
+
mode = "I;16"
|
| 798 |
+
mode_base = "I"
|
| 799 |
+
|
| 800 |
+
if arr.dtype.kind == "f":
|
| 801 |
+
arr = image_as_uint(arr)
|
| 802 |
+
|
| 803 |
+
elif prefer_uint8 and arr.max() < 256 and arr.min() >= 0:
|
| 804 |
+
arr = arr.astype(np.uint8)
|
| 805 |
+
mode = mode_base = "L"
|
| 806 |
+
|
| 807 |
+
else:
|
| 808 |
+
arr = image_as_uint(arr, bitdepth=16)
|
| 809 |
+
|
| 810 |
+
else:
|
| 811 |
+
arr = image_as_uint(arr, bitdepth=8)
|
| 812 |
+
mode = "L"
|
| 813 |
+
mode_base = "L"
|
| 814 |
+
|
| 815 |
+
if mode == "I;16" and int(getattr(Image, "__version__", "0").split(".")[0]) < 6:
|
| 816 |
+
# Pillow < v6.0.0 has limited support for the "I;16" mode,
|
| 817 |
+
# requiring us to fall back to this expensive workaround.
|
| 818 |
+
# tobytes actually creates a copy of the image, which is costly.
|
| 819 |
+
array_buffer = arr.tobytes()
|
| 820 |
+
if arr.ndim == 2:
|
| 821 |
+
im = Image.new(mode_base, arr.T.shape)
|
| 822 |
+
im.frombytes(array_buffer, "raw", mode)
|
| 823 |
+
else:
|
| 824 |
+
image_shape = (arr.shape[1], arr.shape[0])
|
| 825 |
+
im = Image.frombytes(mode, image_shape, array_buffer)
|
| 826 |
+
return im
|
| 827 |
+
else:
|
| 828 |
+
return Image.fromarray(arr, mode)
|
| 829 |
+
|
| 830 |
+
|
| 831 |
+
# imported for backwards compatibility
|
| 832 |
+
from .pillowmulti import GIFFormat, TIFFFormat # noqa: E402, F401
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/pillowmulti.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PIL formats for multiple images.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
from .pillow_legacy import PillowFormat, ndarray_to_pil, image_as_uint
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
NeuQuant = None # we can implement this when we need it
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class TIFFFormat(PillowFormat):
|
| 18 |
+
_modes = "i" # arg, why bother; people should use the tiffile version
|
| 19 |
+
_description = "TIFF format (Pillow)"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class GIFFormat(PillowFormat):
|
| 23 |
+
"""See :mod:`imageio.plugins.pillow_legacy`"""
|
| 24 |
+
|
| 25 |
+
_modes = "iI"
|
| 26 |
+
_description = "Static and animated gif (Pillow)"
|
| 27 |
+
|
| 28 |
+
# GIF reader needs no modifications compared to base pillow reader
|
| 29 |
+
|
| 30 |
+
class Writer(PillowFormat.Writer):
|
| 31 |
+
def _open(
|
| 32 |
+
self,
|
| 33 |
+
loop=0,
|
| 34 |
+
duration=None,
|
| 35 |
+
fps=10,
|
| 36 |
+
palettesize=256,
|
| 37 |
+
quantizer=0,
|
| 38 |
+
subrectangles=False,
|
| 39 |
+
):
|
| 40 |
+
|
| 41 |
+
# Check palettesize
|
| 42 |
+
palettesize = int(palettesize)
|
| 43 |
+
if palettesize < 2 or palettesize > 256:
|
| 44 |
+
raise ValueError("GIF quantize param must be 2..256")
|
| 45 |
+
if palettesize not in [2, 4, 8, 16, 32, 64, 128, 256]:
|
| 46 |
+
palettesize = 2 ** int(np.log2(128) + 0.999)
|
| 47 |
+
logger.warning(
|
| 48 |
+
"Warning: palettesize (%r) modified to a factor of "
|
| 49 |
+
"two between 2-256." % palettesize
|
| 50 |
+
)
|
| 51 |
+
# Duratrion / fps
|
| 52 |
+
if duration is None:
|
| 53 |
+
self._duration = 1.0 / float(fps)
|
| 54 |
+
elif isinstance(duration, (list, tuple)):
|
| 55 |
+
self._duration = [float(d) for d in duration]
|
| 56 |
+
else:
|
| 57 |
+
self._duration = float(duration)
|
| 58 |
+
# loop
|
| 59 |
+
loop = float(loop)
|
| 60 |
+
if loop <= 0 or loop == float("inf"):
|
| 61 |
+
loop = 0
|
| 62 |
+
loop = int(loop)
|
| 63 |
+
# Subrectangles / dispose
|
| 64 |
+
subrectangles = bool(subrectangles)
|
| 65 |
+
self._dispose = 1 if subrectangles else 2
|
| 66 |
+
# The "0" (median cut) quantizer is by far the best
|
| 67 |
+
|
| 68 |
+
fp = self.request.get_file()
|
| 69 |
+
self._writer = GifWriter(
|
| 70 |
+
fp, subrectangles, loop, quantizer, int(palettesize)
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
def _close(self):
|
| 74 |
+
self._writer.close()
|
| 75 |
+
|
| 76 |
+
def _append_data(self, im, meta):
|
| 77 |
+
im = image_as_uint(im, bitdepth=8)
|
| 78 |
+
if im.ndim == 3 and im.shape[-1] == 1:
|
| 79 |
+
im = im[:, :, 0]
|
| 80 |
+
duration = self._duration
|
| 81 |
+
if isinstance(duration, list):
|
| 82 |
+
duration = duration[min(len(duration) - 1, self._writer._count)]
|
| 83 |
+
dispose = self._dispose
|
| 84 |
+
self._writer.add_image(im, duration, dispose)
|
| 85 |
+
|
| 86 |
+
return
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def intToBin(i):
|
| 90 |
+
return i.to_bytes(2, byteorder="little")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class GifWriter:
|
| 94 |
+
"""Class that for helping write the animated GIF file. This is based on
|
| 95 |
+
code from images2gif.py (part of visvis). The version here is modified
|
| 96 |
+
to allow streamed writing.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
def __init__(
|
| 100 |
+
self,
|
| 101 |
+
file,
|
| 102 |
+
opt_subrectangle=True,
|
| 103 |
+
opt_loop=0,
|
| 104 |
+
opt_quantizer=0,
|
| 105 |
+
opt_palette_size=256,
|
| 106 |
+
):
|
| 107 |
+
self.fp = file
|
| 108 |
+
|
| 109 |
+
self.opt_subrectangle = opt_subrectangle
|
| 110 |
+
self.opt_loop = opt_loop
|
| 111 |
+
self.opt_quantizer = opt_quantizer
|
| 112 |
+
self.opt_palette_size = opt_palette_size
|
| 113 |
+
|
| 114 |
+
self._previous_image = None # as np array
|
| 115 |
+
self._global_palette = None # as bytes
|
| 116 |
+
self._count = 0
|
| 117 |
+
|
| 118 |
+
from PIL.GifImagePlugin import getdata
|
| 119 |
+
|
| 120 |
+
self.getdata = getdata
|
| 121 |
+
|
| 122 |
+
def add_image(self, im, duration, dispose):
|
| 123 |
+
|
| 124 |
+
# Prepare image
|
| 125 |
+
im_rect, rect = im, (0, 0)
|
| 126 |
+
if self.opt_subrectangle:
|
| 127 |
+
im_rect, rect = self.getSubRectangle(im)
|
| 128 |
+
im_pil = self.converToPIL(im_rect, self.opt_quantizer, self.opt_palette_size)
|
| 129 |
+
|
| 130 |
+
# Get pallette - apparently, this is the 3d element of the header
|
| 131 |
+
# (but it has not always been). Best we've got. Its not the same
|
| 132 |
+
# as im_pil.palette.tobytes().
|
| 133 |
+
from PIL.GifImagePlugin import getheader
|
| 134 |
+
|
| 135 |
+
palette = getheader(im_pil)[0][3]
|
| 136 |
+
|
| 137 |
+
# Write image
|
| 138 |
+
if self._count == 0:
|
| 139 |
+
self.write_header(im_pil, palette, self.opt_loop)
|
| 140 |
+
self._global_palette = palette
|
| 141 |
+
self.write_image(im_pil, palette, rect, duration, dispose)
|
| 142 |
+
# assert len(palette) == len(self._global_palette)
|
| 143 |
+
|
| 144 |
+
# Bookkeeping
|
| 145 |
+
self._previous_image = im
|
| 146 |
+
self._count += 1
|
| 147 |
+
|
| 148 |
+
def write_header(self, im, globalPalette, loop):
|
| 149 |
+
# Gather info
|
| 150 |
+
header = self.getheaderAnim(im)
|
| 151 |
+
appext = self.getAppExt(loop)
|
| 152 |
+
# Write
|
| 153 |
+
self.fp.write(header)
|
| 154 |
+
self.fp.write(globalPalette)
|
| 155 |
+
self.fp.write(appext)
|
| 156 |
+
|
| 157 |
+
def close(self):
|
| 158 |
+
self.fp.write(";".encode("utf-8")) # end gif
|
| 159 |
+
|
| 160 |
+
def write_image(self, im, palette, rect, duration, dispose):
|
| 161 |
+
|
| 162 |
+
fp = self.fp
|
| 163 |
+
|
| 164 |
+
# Gather local image header and data, using PIL's getdata. That
|
| 165 |
+
# function returns a list of bytes objects, but which parts are
|
| 166 |
+
# what has changed multiple times, so we put together the first
|
| 167 |
+
# parts until we have enough to form the image header.
|
| 168 |
+
data = self.getdata(im)
|
| 169 |
+
imdes = b""
|
| 170 |
+
while data and len(imdes) < 11:
|
| 171 |
+
imdes += data.pop(0)
|
| 172 |
+
assert len(imdes) == 11
|
| 173 |
+
|
| 174 |
+
# Make image descriptor suitable for using 256 local color palette
|
| 175 |
+
lid = self.getImageDescriptor(im, rect)
|
| 176 |
+
graphext = self.getGraphicsControlExt(duration, dispose)
|
| 177 |
+
|
| 178 |
+
# Write local header
|
| 179 |
+
if (palette != self._global_palette) or (dispose != 2):
|
| 180 |
+
# Use local color palette
|
| 181 |
+
fp.write(graphext)
|
| 182 |
+
fp.write(lid) # write suitable image descriptor
|
| 183 |
+
fp.write(palette) # write local color table
|
| 184 |
+
fp.write(b"\x08") # LZW minimum size code
|
| 185 |
+
else:
|
| 186 |
+
# Use global color palette
|
| 187 |
+
fp.write(graphext)
|
| 188 |
+
fp.write(imdes) # write suitable image descriptor
|
| 189 |
+
|
| 190 |
+
# Write image data
|
| 191 |
+
for d in data:
|
| 192 |
+
fp.write(d)
|
| 193 |
+
|
| 194 |
+
def getheaderAnim(self, im):
|
| 195 |
+
"""Get animation header. To replace PILs getheader()[0]"""
|
| 196 |
+
bb = b"GIF89a"
|
| 197 |
+
bb += intToBin(im.size[0])
|
| 198 |
+
bb += intToBin(im.size[1])
|
| 199 |
+
bb += b"\x87\x00\x00"
|
| 200 |
+
return bb
|
| 201 |
+
|
| 202 |
+
def getImageDescriptor(self, im, xy=None):
|
| 203 |
+
"""Used for the local color table properties per image.
|
| 204 |
+
Otherwise global color table applies to all frames irrespective of
|
| 205 |
+
whether additional colors comes in play that require a redefined
|
| 206 |
+
palette. Still a maximum of 256 color per frame, obviously.
|
| 207 |
+
|
| 208 |
+
Written by Ant1 on 2010-08-22
|
| 209 |
+
Modified by Alex Robinson in Janurari 2011 to implement subrectangles.
|
| 210 |
+
"""
|
| 211 |
+
|
| 212 |
+
# Defaule use full image and place at upper left
|
| 213 |
+
if xy is None:
|
| 214 |
+
xy = (0, 0)
|
| 215 |
+
|
| 216 |
+
# Image separator,
|
| 217 |
+
bb = b"\x2C"
|
| 218 |
+
|
| 219 |
+
# Image position and size
|
| 220 |
+
bb += intToBin(xy[0]) # Left position
|
| 221 |
+
bb += intToBin(xy[1]) # Top position
|
| 222 |
+
bb += intToBin(im.size[0]) # image width
|
| 223 |
+
bb += intToBin(im.size[1]) # image height
|
| 224 |
+
|
| 225 |
+
# packed field: local color table flag1, interlace0, sorted table0,
|
| 226 |
+
# reserved00, lct size111=7=2^(7 + 1)=256.
|
| 227 |
+
bb += b"\x87"
|
| 228 |
+
|
| 229 |
+
# LZW minimum size code now comes later, begining of [imagedata] blocks
|
| 230 |
+
return bb
|
| 231 |
+
|
| 232 |
+
def getAppExt(self, loop):
|
| 233 |
+
"""Application extension. This part specifies the amount of loops.
|
| 234 |
+
If loop is 0 or inf, it goes on infinitely.
|
| 235 |
+
"""
|
| 236 |
+
if loop == 1:
|
| 237 |
+
return b""
|
| 238 |
+
if loop == 0:
|
| 239 |
+
loop = 2**16 - 1
|
| 240 |
+
bb = b""
|
| 241 |
+
if loop != 0: # omit the extension if we would like a nonlooping gif
|
| 242 |
+
bb = b"\x21\xFF\x0B" # application extension
|
| 243 |
+
bb += b"NETSCAPE2.0"
|
| 244 |
+
bb += b"\x03\x01"
|
| 245 |
+
bb += intToBin(loop)
|
| 246 |
+
bb += b"\x00" # end
|
| 247 |
+
return bb
|
| 248 |
+
|
| 249 |
+
def getGraphicsControlExt(self, duration=0.1, dispose=2):
|
| 250 |
+
"""Graphics Control Extension. A sort of header at the start of
|
| 251 |
+
each image. Specifies duration and transparancy.
|
| 252 |
+
|
| 253 |
+
Dispose
|
| 254 |
+
-------
|
| 255 |
+
* 0 - No disposal specified.
|
| 256 |
+
* 1 - Do not dispose. The graphic is to be left in place.
|
| 257 |
+
* 2 - Restore to background color. The area used by the graphic
|
| 258 |
+
must be restored to the background color.
|
| 259 |
+
* 3 - Restore to previous. The decoder is required to restore the
|
| 260 |
+
area overwritten by the graphic with what was there prior to
|
| 261 |
+
rendering the graphic.
|
| 262 |
+
* 4-7 -To be defined.
|
| 263 |
+
"""
|
| 264 |
+
|
| 265 |
+
bb = b"\x21\xF9\x04"
|
| 266 |
+
bb += chr((dispose & 3) << 2).encode("utf-8")
|
| 267 |
+
# low bit 1 == transparency,
|
| 268 |
+
# 2nd bit 1 == user input , next 3 bits, the low two of which are used,
|
| 269 |
+
# are dispose.
|
| 270 |
+
bb += intToBin(int(duration * 100 + 0.5)) # in 100th of seconds
|
| 271 |
+
bb += b"\x00" # no transparant color
|
| 272 |
+
bb += b"\x00" # end
|
| 273 |
+
return bb
|
| 274 |
+
|
| 275 |
+
def getSubRectangle(self, im):
|
| 276 |
+
"""Calculate the minimal rectangle that need updating. Returns
|
| 277 |
+
a two-element tuple containing the cropped image and an x-y tuple.
|
| 278 |
+
|
| 279 |
+
Calculating the subrectangles takes extra time, obviously. However,
|
| 280 |
+
if the image sizes were reduced, the actual writing of the GIF
|
| 281 |
+
goes faster. In some cases applying this method produces a GIF faster.
|
| 282 |
+
"""
|
| 283 |
+
|
| 284 |
+
# Cannot do subrectangle for first image
|
| 285 |
+
if self._count == 0:
|
| 286 |
+
return im, (0, 0)
|
| 287 |
+
|
| 288 |
+
prev = self._previous_image
|
| 289 |
+
|
| 290 |
+
# Get difference, sum over colors
|
| 291 |
+
diff = np.abs(im - prev)
|
| 292 |
+
if diff.ndim == 3:
|
| 293 |
+
diff = diff.sum(2)
|
| 294 |
+
# Get begin and end for both dimensions
|
| 295 |
+
X = np.argwhere(diff.sum(0))
|
| 296 |
+
Y = np.argwhere(diff.sum(1))
|
| 297 |
+
# Get rect coordinates
|
| 298 |
+
if X.size and Y.size:
|
| 299 |
+
x0, x1 = int(X[0]), int(X[-1] + 1)
|
| 300 |
+
y0, y1 = int(Y[0]), int(Y[-1] + 1)
|
| 301 |
+
else: # No change ... make it minimal
|
| 302 |
+
x0, x1 = 0, 2
|
| 303 |
+
y0, y1 = 0, 2
|
| 304 |
+
|
| 305 |
+
return im[y0:y1, x0:x1], (x0, y0)
|
| 306 |
+
|
| 307 |
+
def converToPIL(self, im, quantizer, palette_size=256):
|
| 308 |
+
"""Convert image to Paletted PIL image.
|
| 309 |
+
|
| 310 |
+
PIL used to not do a very good job at quantization, but I guess
|
| 311 |
+
this has improved a lot (at least in Pillow). I don't think we need
|
| 312 |
+
neuqant (and we can add it later if we really want).
|
| 313 |
+
"""
|
| 314 |
+
|
| 315 |
+
im_pil = ndarray_to_pil(im, "gif")
|
| 316 |
+
|
| 317 |
+
if quantizer in ("nq", "neuquant"):
|
| 318 |
+
# NeuQuant algorithm
|
| 319 |
+
nq_samplefac = 10 # 10 seems good in general
|
| 320 |
+
im_pil = im_pil.convert("RGBA") # NQ assumes RGBA
|
| 321 |
+
nqInstance = NeuQuant(im_pil, nq_samplefac) # Learn colors
|
| 322 |
+
im_pil = nqInstance.quantize(im_pil, colors=palette_size)
|
| 323 |
+
elif quantizer in (0, 1, 2):
|
| 324 |
+
# Adaptive PIL algorithm
|
| 325 |
+
if quantizer == 2:
|
| 326 |
+
im_pil = im_pil.convert("RGBA")
|
| 327 |
+
else:
|
| 328 |
+
im_pil = im_pil.convert("RGB")
|
| 329 |
+
im_pil = im_pil.quantize(colors=palette_size, method=quantizer)
|
| 330 |
+
else:
|
| 331 |
+
raise ValueError("Invalid value for quantizer: %r" % quantizer)
|
| 332 |
+
return im_pil
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/pyav.py
ADDED
|
@@ -0,0 +1,976 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Read/Write Videos (and images) using PyAV.
|
| 2 |
+
|
| 3 |
+
Backend Library: `PyAV <https://pyav.org/docs/stable/>`_
|
| 4 |
+
|
| 5 |
+
This plugin wraps pyAV, a pythonic binding for the FFMPEG library.
|
| 6 |
+
It is similar to our FFMPEG plugin, but offers a more performant and
|
| 7 |
+
robut interface, and aims to superseed the FFMPEG plugin in the future.
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
Methods
|
| 11 |
+
-------
|
| 12 |
+
.. note::
|
| 13 |
+
Check the respective function for a list of supported kwargs and their
|
| 14 |
+
documentation.
|
| 15 |
+
|
| 16 |
+
.. autosummary::
|
| 17 |
+
:toctree:
|
| 18 |
+
|
| 19 |
+
PyAVPlugin.read
|
| 20 |
+
PyAVPlugin.iter
|
| 21 |
+
PyAVPlugin.write
|
| 22 |
+
PyAVPlugin.properties
|
| 23 |
+
PyAVPlugin.metadata
|
| 24 |
+
|
| 25 |
+
Pixel Formats (Colorspaces)
|
| 26 |
+
---------------------------
|
| 27 |
+
|
| 28 |
+
By default, this plugin converts the video into 8-bit RGB (called ``rgb24`` in
|
| 29 |
+
ffmpeg). This is a useful behavior for many use-cases, but sometimes you may
|
| 30 |
+
want to use the video's native colorspace or you may wish to convert the video
|
| 31 |
+
into an entirely different colorspace. This is controlled using the ``format``
|
| 32 |
+
kwarg. You can use ``format=None`` to leave the image in its native colorspace
|
| 33 |
+
or specify any colorspace supported by FFMPEG as long as it is stridable, i.e.,
|
| 34 |
+
as long as it can be represented by a single numpy array. Some useful choices
|
| 35 |
+
include:
|
| 36 |
+
|
| 37 |
+
- rgb24 (default; 8-bit RGB)
|
| 38 |
+
- rgb48le (16-bit lower-endian RGB)
|
| 39 |
+
- bgr24 (8-bit BGR; openCVs default colorspace)
|
| 40 |
+
- gray (8-bit grayscale)
|
| 41 |
+
- yuv444p (8-bit channel-first YUV)
|
| 42 |
+
|
| 43 |
+
Further, FFMPEG maintains a list of available formats, albeit not as part of the
|
| 44 |
+
narrative docs. It can be `found here
|
| 45 |
+
<https://ffmpeg.org/doxygen/trunk/pixfmt_8h_source.html>`_ (warning: C source
|
| 46 |
+
code).
|
| 47 |
+
|
| 48 |
+
Filters
|
| 49 |
+
-------
|
| 50 |
+
|
| 51 |
+
On top of providing basic read/write functionality, this plugin allows you to
|
| 52 |
+
use the full collection of `video filters available in FFMPEG
|
| 53 |
+
<https://ffmpeg.org/ffmpeg-filters.html#Video-Filters>`_. This means that you
|
| 54 |
+
can apply excessive preprocessing to your video before retrieving it as a numpy
|
| 55 |
+
array or apply excessive post-processing before you encode your data.
|
| 56 |
+
|
| 57 |
+
Filters come in two forms: sequences or graphs. Filter sequences are, as the
|
| 58 |
+
name suggests, sequences of filters that are applied one after the other. They
|
| 59 |
+
are specified using the ``filter_sequence`` kwarg. Filter graphs, on the other
|
| 60 |
+
hand, come in the form of a directed graph and are specified using the
|
| 61 |
+
``filter_graph`` kwarg.
|
| 62 |
+
|
| 63 |
+
.. note::
|
| 64 |
+
All filters are either sequences or graphs. If all you want is to apply a
|
| 65 |
+
single filter, you can do this by specifying a filter sequence with a single
|
| 66 |
+
entry.
|
| 67 |
+
|
| 68 |
+
A ``filter_sequence`` is a list of filters, each defined through a 2-element
|
| 69 |
+
tuple of the form ``(filter_name, filter_parameters)``. The first element of the
|
| 70 |
+
tuple is the name of the filter. The second element are the filter parameters,
|
| 71 |
+
which can be given either as a string or a dict. The string matches the same
|
| 72 |
+
format that you would use when specifying the filter using the ffmpeg
|
| 73 |
+
command-line tool and the dict has entries of the form ``parameter:value``. For
|
| 74 |
+
example::
|
| 75 |
+
|
| 76 |
+
import imageio.v3 as iio
|
| 77 |
+
|
| 78 |
+
# using a filter_parameters str
|
| 79 |
+
img1 = iio.imread(
|
| 80 |
+
"imageio:cockatoo.mp4",
|
| 81 |
+
plugin="pyav",
|
| 82 |
+
filter_sequence=[
|
| 83 |
+
("rotate", "45*PI/180")
|
| 84 |
+
]
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# using a filter_parameters dict
|
| 88 |
+
img2 = iio.imread(
|
| 89 |
+
"imageio:cockatoo.mp4",
|
| 90 |
+
plugin="pyav",
|
| 91 |
+
filter_sequence=[
|
| 92 |
+
("rotate", {"angle":"45*PI/180", "fillcolor":"AliceBlue"})
|
| 93 |
+
]
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
A ``filter_graph``, on the other hand, is specified using a ``(nodes, edges)``
|
| 97 |
+
tuple. It is best explained using an example::
|
| 98 |
+
|
| 99 |
+
img = iio.imread(
|
| 100 |
+
"imageio:cockatoo.mp4",
|
| 101 |
+
plugin="pyav",
|
| 102 |
+
filter_graph=(
|
| 103 |
+
{
|
| 104 |
+
"split": ("split", ""),
|
| 105 |
+
"scale_overlay":("scale", "512:-1"),
|
| 106 |
+
"overlay":("overlay", "x=25:y=25:enable='between(t,1,8)'"),
|
| 107 |
+
},
|
| 108 |
+
[
|
| 109 |
+
("video_in", "split", 0, 0),
|
| 110 |
+
("split", "overlay", 0, 0),
|
| 111 |
+
("split", "scale_overlay", 1, 0),
|
| 112 |
+
("scale_overlay", "overlay", 0, 1),
|
| 113 |
+
("overlay", "video_out", 0, 0),
|
| 114 |
+
]
|
| 115 |
+
)
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
The above transforms the video to have picture-in-picture of itself in the top
|
| 119 |
+
left corner. As you can see, nodes are specified using a dict which has names as
|
| 120 |
+
its keys and filter tuples as values; the same tuples as the ones used when
|
| 121 |
+
defining a filter sequence. Edges are a list of a 4-tuples of the form
|
| 122 |
+
``(node_out, node_in, output_idx, input_idx)`` and specify which two filters are
|
| 123 |
+
connected and which inputs/outputs should be used for this.
|
| 124 |
+
|
| 125 |
+
Further, there are two special nodes in a filter graph: ``video_in`` and
|
| 126 |
+
``video_out``, which represent the graph's input and output respectively. These
|
| 127 |
+
names can not be chosen for other nodes (those nodes would simply be
|
| 128 |
+
overwritten), and for a graph to be valid there must be a path from the input to
|
| 129 |
+
the output and all nodes in the graph must be connected.
|
| 130 |
+
|
| 131 |
+
While most graphs are quite simple, they can become very complex and we
|
| 132 |
+
recommend that you read through the `FFMPEG documentation
|
| 133 |
+
<https://ffmpeg.org/ffmpeg-filters.html#Filtergraph-description>`_ and their
|
| 134 |
+
examples to better understand how to use them.
|
| 135 |
+
|
| 136 |
+
"""
|
| 137 |
+
|
| 138 |
+
from math import ceil
|
| 139 |
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
| 140 |
+
from fractions import Fraction
|
| 141 |
+
|
| 142 |
+
import av
|
| 143 |
+
import av.filter
|
| 144 |
+
import numpy as np
|
| 145 |
+
from numpy.lib.stride_tricks import as_strided
|
| 146 |
+
|
| 147 |
+
from ..core import Request
|
| 148 |
+
from ..core.request import InitializationError, IOMode, URI_BYTES
|
| 149 |
+
from ..core.v3_plugin_api import PluginV3, ImageProperties
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _format_to_dtype(format: av.VideoFormat) -> np.dtype:
|
| 153 |
+
"""Convert a pyAV video format into a numpy dtype"""
|
| 154 |
+
|
| 155 |
+
if len(format.components) == 0:
|
| 156 |
+
# fake format
|
| 157 |
+
raise ValueError(
|
| 158 |
+
f"Can't determine dtype from format `{format.name}`. It has no channels."
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
endian = ">" if format.is_big_endian else "<"
|
| 162 |
+
dtype = "f" if "f32" in format.name else "u"
|
| 163 |
+
bits_per_channel = [x.bits for x in format.components]
|
| 164 |
+
n_bytes = str(int(ceil(bits_per_channel[0] / 8)))
|
| 165 |
+
|
| 166 |
+
return np.dtype(endian + dtype + n_bytes)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def _get_frame_shape(frame: av.VideoFrame) -> Tuple[int, ...]:
|
| 170 |
+
"""Compute the frame's array shape
|
| 171 |
+
|
| 172 |
+
Parameters
|
| 173 |
+
----------
|
| 174 |
+
frame : av.VideoFrame
|
| 175 |
+
A frame for which the resulting shape should be computed.
|
| 176 |
+
|
| 177 |
+
Returns
|
| 178 |
+
-------
|
| 179 |
+
shape : Tuple[int, ...]
|
| 180 |
+
A tuple describing the shape of the image data in the frame.
|
| 181 |
+
|
| 182 |
+
"""
|
| 183 |
+
|
| 184 |
+
widths = [component.width for component in frame.format.components]
|
| 185 |
+
heights = [component.height for component in frame.format.components]
|
| 186 |
+
bits = np.array([component.bits for component in frame.format.components])
|
| 187 |
+
line_sizes = [plane.line_size for plane in frame.planes]
|
| 188 |
+
|
| 189 |
+
subsampled_width = widths[:-1] != widths[1:]
|
| 190 |
+
subsampled_height = heights[:-1] != heights[1:]
|
| 191 |
+
unaligned_components = np.any(bits % 8 != 0) or (line_sizes[:-1] != line_sizes[1:])
|
| 192 |
+
if subsampled_width or subsampled_height or unaligned_components:
|
| 193 |
+
raise IOError(
|
| 194 |
+
f"{frame.format.name} can't be expressed as a strided array."
|
| 195 |
+
"Use `format=` to select a format to convert into."
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
shape = [frame.height, frame.width]
|
| 199 |
+
|
| 200 |
+
# ffmpeg doesn't have a notion of channel-first or channel-last formats
|
| 201 |
+
# instead it stores frames in one or more planes which contain individual
|
| 202 |
+
# components of a pixel depending on the pixel format. For channel-first
|
| 203 |
+
# formats each component lives on a separate plane (n_planes) and for
|
| 204 |
+
# channel-last formats all components are packed on a single plane
|
| 205 |
+
# (n_channels)
|
| 206 |
+
n_planes = max([component.plane for component in frame.format.components]) + 1
|
| 207 |
+
if n_planes > 1:
|
| 208 |
+
shape = [n_planes] + shape
|
| 209 |
+
|
| 210 |
+
channels_per_plane = [0] * n_planes
|
| 211 |
+
for component in frame.format.components:
|
| 212 |
+
channels_per_plane[component.plane] += 1
|
| 213 |
+
n_channels = max(channels_per_plane)
|
| 214 |
+
|
| 215 |
+
if n_channels > 1:
|
| 216 |
+
shape = shape + [n_channels]
|
| 217 |
+
|
| 218 |
+
return tuple(shape)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
class PyAVPlugin(PluginV3):
|
| 222 |
+
"""Support for pyAV as backend.
|
| 223 |
+
|
| 224 |
+
Parameters
|
| 225 |
+
----------
|
| 226 |
+
request : iio.Request
|
| 227 |
+
A request object that represents the users intent. It provides a
|
| 228 |
+
standard interface to access various the various ImageResources and
|
| 229 |
+
serves them to the plugin as a file object (or file). Check the docs for
|
| 230 |
+
details.
|
| 231 |
+
|
| 232 |
+
"""
|
| 233 |
+
|
| 234 |
+
def __init__(
|
| 235 |
+
self,
|
| 236 |
+
request: Request,
|
| 237 |
+
*,
|
| 238 |
+
container: str = None,
|
| 239 |
+
) -> None:
|
| 240 |
+
"""Initialize a new Plugin Instance.
|
| 241 |
+
|
| 242 |
+
See Plugin's docstring for detailed documentation.
|
| 243 |
+
|
| 244 |
+
Notes
|
| 245 |
+
-----
|
| 246 |
+
The implementation here stores the request as a local variable that is
|
| 247 |
+
exposed using a @property below. If you inherit from PluginV3, remember
|
| 248 |
+
to call ``super().__init__(request)``.
|
| 249 |
+
|
| 250 |
+
"""
|
| 251 |
+
|
| 252 |
+
super().__init__(request)
|
| 253 |
+
|
| 254 |
+
self._container = None
|
| 255 |
+
self._video_stream = None
|
| 256 |
+
|
| 257 |
+
if request.mode.io_mode == IOMode.read:
|
| 258 |
+
try:
|
| 259 |
+
self._container = av.open(request.get_file())
|
| 260 |
+
self._video_stream = self._container.streams.video[0]
|
| 261 |
+
self._decoder = self._container.decode(video=0)
|
| 262 |
+
except av.AVError:
|
| 263 |
+
if isinstance(request.raw_uri, bytes):
|
| 264 |
+
msg = "PyAV does not support these `<bytes>`"
|
| 265 |
+
else:
|
| 266 |
+
msg = f"PyAV does not support `{request.raw_uri}`"
|
| 267 |
+
raise InitializationError(msg) from None
|
| 268 |
+
else:
|
| 269 |
+
self.frames_written = 0
|
| 270 |
+
file_handle = self.request.get_file()
|
| 271 |
+
filename = getattr(file_handle, "name", None)
|
| 272 |
+
extension = self.request.extension or self.request.format_hint
|
| 273 |
+
if extension is None:
|
| 274 |
+
raise InitializationError("Can't determine output container to use.")
|
| 275 |
+
|
| 276 |
+
# hacky, but beats running our own format selection logic
|
| 277 |
+
# (since av_guess_format is not exposed)
|
| 278 |
+
try:
|
| 279 |
+
setattr(file_handle, "name", filename or "tmp" + extension)
|
| 280 |
+
except AttributeError:
|
| 281 |
+
pass # read-only, nothing we can do
|
| 282 |
+
|
| 283 |
+
try:
|
| 284 |
+
self._container = av.open(file_handle, mode="w", format=container)
|
| 285 |
+
except ValueError:
|
| 286 |
+
raise InitializationError(
|
| 287 |
+
f"PyAV can not write to `{self.request.raw_uri}`"
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
def read(
|
| 291 |
+
self,
|
| 292 |
+
*,
|
| 293 |
+
index: int = ...,
|
| 294 |
+
format: str = "rgb24",
|
| 295 |
+
filter_sequence: List[Tuple[str, Union[str, dict]]] = None,
|
| 296 |
+
filter_graph: Tuple[dict, List] = None,
|
| 297 |
+
constant_framerate: bool = None,
|
| 298 |
+
thread_count: int = 0,
|
| 299 |
+
thread_type: str = None,
|
| 300 |
+
) -> np.ndarray:
|
| 301 |
+
"""Read frames from the video.
|
| 302 |
+
|
| 303 |
+
If ``index`` is an integer, this function reads the index-th frame from
|
| 304 |
+
the file. If ``index`` is ... (Ellipsis), this function reads all frames
|
| 305 |
+
from the video, stacks them along the first dimension, and returns a
|
| 306 |
+
batch of frames.
|
| 307 |
+
|
| 308 |
+
Parameters
|
| 309 |
+
----------
|
| 310 |
+
index : int
|
| 311 |
+
The index of the frame to read, e.g. ``index=5`` reads the 5th
|
| 312 |
+
frame. If ``...``, read all the frames in the video and stack them
|
| 313 |
+
along a new, prepended, batch dimension.
|
| 314 |
+
format : str
|
| 315 |
+
Set the returned colorspace. If not None (default: rgb24), convert
|
| 316 |
+
the data into the given format before returning it. If ``None``
|
| 317 |
+
return the data in the encoded format if it can be expressed as a
|
| 318 |
+
strided array; otherwise raise an Exception.
|
| 319 |
+
filter_sequence : List[str, str, dict]
|
| 320 |
+
If not None, apply the given sequence of FFmpeg filters to each
|
| 321 |
+
ndimage. Check the (module-level) plugin docs for details and
|
| 322 |
+
examples.
|
| 323 |
+
filter_graph : (dict, List)
|
| 324 |
+
If not None, apply the given graph of FFmpeg filters to each
|
| 325 |
+
ndimage. The graph is given as a tuple of two dicts. The first dict
|
| 326 |
+
contains a (named) set of nodes, and the second dict contains a set
|
| 327 |
+
of edges between nodes of the previous dict. Check the (module-level)
|
| 328 |
+
plugin docs for details and examples.
|
| 329 |
+
constant_framerate : bool
|
| 330 |
+
If True assume the video's framerate is constant. This allows for
|
| 331 |
+
faster seeking inside the file. If False, the video is reset before
|
| 332 |
+
each read and searched from the beginning. If None (default), this
|
| 333 |
+
value will be read from the container format.
|
| 334 |
+
thread_count : int
|
| 335 |
+
How many threads to use when decoding a frame. The default is 0,
|
| 336 |
+
which will set the number using ffmpeg's default, which is based on
|
| 337 |
+
the codec, number of available cores, threadding model, and other
|
| 338 |
+
considerations.
|
| 339 |
+
thread_type : str
|
| 340 |
+
The threading model to be used. One of
|
| 341 |
+
|
| 342 |
+
- `"SLICE"`: threads assemble parts of the current frame
|
| 343 |
+
- `"FRAME"`: threads may assemble future frames
|
| 344 |
+
- None (default): Uses SLICE when reading single frames and FRAME
|
| 345 |
+
when reading batches of frames.
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
Returns
|
| 349 |
+
-------
|
| 350 |
+
frame : np.ndarray
|
| 351 |
+
A numpy array containing loaded frame data.
|
| 352 |
+
|
| 353 |
+
Notes
|
| 354 |
+
-----
|
| 355 |
+
Accessing random frames repeatedly is costly (O(k), where k is the
|
| 356 |
+
average distance between two keyframes). You should do so only sparingly
|
| 357 |
+
if possible. In some cases, it can be faster to bulk-read the video (if
|
| 358 |
+
it fits into memory) and to then access the returned ndarray randomly.
|
| 359 |
+
|
| 360 |
+
The current implementation may cause problems for b-frames, i.e.,
|
| 361 |
+
bidirectionaly predicted pictures. I don't have any test videos of this
|
| 362 |
+
though.
|
| 363 |
+
|
| 364 |
+
Reading from an index other than ``...``, i.e. reading a single frame,
|
| 365 |
+
currently doesn't support filters that introduce delays.
|
| 366 |
+
|
| 367 |
+
"""
|
| 368 |
+
|
| 369 |
+
if constant_framerate is None:
|
| 370 |
+
constant_framerate = self._container.format.variable_fps
|
| 371 |
+
|
| 372 |
+
if index is ...:
|
| 373 |
+
self._container.seek(0)
|
| 374 |
+
|
| 375 |
+
frames = np.stack(
|
| 376 |
+
[
|
| 377 |
+
x
|
| 378 |
+
for x in self.iter(
|
| 379 |
+
format=format,
|
| 380 |
+
filter_sequence=filter_sequence,
|
| 381 |
+
filter_graph=filter_graph,
|
| 382 |
+
thread_count=thread_count,
|
| 383 |
+
thread_type=thread_type or "FRAME",
|
| 384 |
+
)
|
| 385 |
+
]
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
# reset stream container, because threading model can't change after
|
| 389 |
+
# first access
|
| 390 |
+
self._video_stream.close()
|
| 391 |
+
self._video_stream = self._container.streams.video[0]
|
| 392 |
+
|
| 393 |
+
return frames
|
| 394 |
+
|
| 395 |
+
self._video_stream.thread_type = thread_type or "SLICE"
|
| 396 |
+
self._video_stream.codec_context.thread_count = thread_count
|
| 397 |
+
ffmpeg_filter = self._build_filter(filter_sequence, filter_graph)
|
| 398 |
+
ffmpeg_filter.send(None) # init
|
| 399 |
+
|
| 400 |
+
self._seek(index, constant_framerate=constant_framerate)
|
| 401 |
+
desired_frame = next(self._container.decode(video=0))
|
| 402 |
+
|
| 403 |
+
return self._unpack_frame(ffmpeg_filter.send(desired_frame), format=format)
|
| 404 |
+
|
| 405 |
+
def iter(
|
| 406 |
+
self,
|
| 407 |
+
*,
|
| 408 |
+
format: str = None,
|
| 409 |
+
filter_sequence: List[Tuple[str, Union[str, dict]]] = None,
|
| 410 |
+
filter_graph: Tuple[dict, List] = None,
|
| 411 |
+
thread_count: int = 0,
|
| 412 |
+
thread_type: str = None,
|
| 413 |
+
) -> np.ndarray:
|
| 414 |
+
"""Yield frames from the video.
|
| 415 |
+
|
| 416 |
+
Parameters
|
| 417 |
+
----------
|
| 418 |
+
frame : np.ndarray
|
| 419 |
+
A numpy array containing loaded frame data.
|
| 420 |
+
format : str
|
| 421 |
+
If not None, convert the data into the given format before returning
|
| 422 |
+
it. If None (default) return the data in the encoded format if it
|
| 423 |
+
can be expressed as a strided array; otherwise raise an Exception.
|
| 424 |
+
filter_sequence : List[str, str, dict]
|
| 425 |
+
Set the returned colorspace. If not None (default: rgb24), convert
|
| 426 |
+
the data into the given format before returning it. If ``None``
|
| 427 |
+
return the data in the encoded format if it can be expressed as a
|
| 428 |
+
strided array; otherwise raise an Exception.
|
| 429 |
+
filter_graph : (dict, List)
|
| 430 |
+
If not None, apply the given graph of FFmpeg filters to each
|
| 431 |
+
ndimage. The graph is given as a tuple of two dicts. The first dict
|
| 432 |
+
contains a (named) set of nodes, and the second dict contains a set
|
| 433 |
+
of edges between nodes of the previous dict. Check the (module-level)
|
| 434 |
+
plugin docs for details and examples.
|
| 435 |
+
thread_count : int
|
| 436 |
+
How many threads to use when decoding a frame. The default is 0,
|
| 437 |
+
which will set the number using ffmpeg's default, which is based on
|
| 438 |
+
the codec, number of available cores, threadding model, and other
|
| 439 |
+
considerations.
|
| 440 |
+
thread_type : str
|
| 441 |
+
The threading model to be used. One of
|
| 442 |
+
|
| 443 |
+
- `"SLICE"` (default): threads assemble parts of the current frame
|
| 444 |
+
- `"FRAME"`: threads may assemble future frames
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
Yields
|
| 448 |
+
------
|
| 449 |
+
frame : np.ndarray
|
| 450 |
+
A (decoded) video frame.
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
"""
|
| 454 |
+
|
| 455 |
+
self._video_stream.thread_type = thread_type or "SLICE"
|
| 456 |
+
self._video_stream.codec_context.thread_count = thread_count
|
| 457 |
+
ffmpeg_filter = self._build_filter(filter_sequence, filter_graph)
|
| 458 |
+
ffmpeg_filter.send(None) # init
|
| 459 |
+
|
| 460 |
+
for frame in self._container.decode(video=0):
|
| 461 |
+
frame = ffmpeg_filter.send(frame)
|
| 462 |
+
|
| 463 |
+
if frame is None:
|
| 464 |
+
continue
|
| 465 |
+
|
| 466 |
+
yield self._unpack_frame(frame, format=format)
|
| 467 |
+
|
| 468 |
+
for frame in ffmpeg_filter:
|
| 469 |
+
yield self._unpack_frame(frame, format=format)
|
| 470 |
+
|
| 471 |
+
def _unpack_frame(self, frame: av.VideoFrame, *, format: str = None) -> np.ndarray:
|
| 472 |
+
"""Convert a av.VideoFrame into a ndarray
|
| 473 |
+
|
| 474 |
+
Parameters
|
| 475 |
+
----------
|
| 476 |
+
frame : av.VideoFrame
|
| 477 |
+
The frame to unpack.
|
| 478 |
+
format : str
|
| 479 |
+
If not None, convert the frame to the given format before unpacking.
|
| 480 |
+
|
| 481 |
+
"""
|
| 482 |
+
|
| 483 |
+
if format is not None:
|
| 484 |
+
frame = frame.reformat(format=format)
|
| 485 |
+
|
| 486 |
+
dtype = _format_to_dtype(frame.format)
|
| 487 |
+
shape = _get_frame_shape(frame)
|
| 488 |
+
|
| 489 |
+
planes = list()
|
| 490 |
+
for idx in range(len(frame.planes)):
|
| 491 |
+
n_channels = sum(
|
| 492 |
+
[
|
| 493 |
+
x.bits // (dtype.itemsize * 8)
|
| 494 |
+
for x in frame.format.components
|
| 495 |
+
if x.plane == idx
|
| 496 |
+
]
|
| 497 |
+
)
|
| 498 |
+
av_plane = frame.planes[idx]
|
| 499 |
+
plane_shape = (av_plane.height, av_plane.width)
|
| 500 |
+
plane_strides = (av_plane.line_size, n_channels * dtype.itemsize)
|
| 501 |
+
if n_channels > 1:
|
| 502 |
+
plane_shape += (n_channels,)
|
| 503 |
+
plane_strides += (dtype.itemsize,)
|
| 504 |
+
|
| 505 |
+
np_plane = as_strided(
|
| 506 |
+
np.frombuffer(av_plane, dtype=dtype),
|
| 507 |
+
shape=plane_shape,
|
| 508 |
+
strides=plane_strides,
|
| 509 |
+
)
|
| 510 |
+
planes.append(np_plane)
|
| 511 |
+
|
| 512 |
+
if len(planes) > 1:
|
| 513 |
+
# Note: the planes *should* exist inside a contigous memory block
|
| 514 |
+
# somewhere inside av.Frame however pyAV does not appear to expose this,
|
| 515 |
+
# so we are forced to copy the planes individually instead of wrapping
|
| 516 |
+
# them :(
|
| 517 |
+
out = np.concatenate(planes).reshape(shape)
|
| 518 |
+
else:
|
| 519 |
+
out = planes[0]
|
| 520 |
+
|
| 521 |
+
return out
|
| 522 |
+
|
| 523 |
+
def write(
|
| 524 |
+
self,
|
| 525 |
+
ndimage: Union[np.ndarray, List[np.ndarray]],
|
| 526 |
+
*,
|
| 527 |
+
codec: str,
|
| 528 |
+
is_batch: bool = True,
|
| 529 |
+
fps: int = 24,
|
| 530 |
+
in_pixel_format: str = "rgb24",
|
| 531 |
+
out_pixel_format: str = None,
|
| 532 |
+
filter_sequence: List[Tuple[str, Union[str, dict]]] = None,
|
| 533 |
+
filter_graph: Tuple[dict, List] = None,
|
| 534 |
+
) -> Optional[bytes]:
|
| 535 |
+
"""Save a ndimage as a video.
|
| 536 |
+
|
| 537 |
+
Given a batch of frames (stacked along the first axis) or a list of
|
| 538 |
+
frames, encode them and add the result to the ImageResource.
|
| 539 |
+
|
| 540 |
+
Parameters
|
| 541 |
+
----------
|
| 542 |
+
ndimage : ArrayLike, List[ArrayLike]
|
| 543 |
+
The ndimage to encode and write to the ImageResource.
|
| 544 |
+
codec : str
|
| 545 |
+
The codec to use when encoding frames.
|
| 546 |
+
is_batch : bool
|
| 547 |
+
If True (default), the ndimage is a batch of images, otherwise it is
|
| 548 |
+
a single image. This parameter has no effect on lists of ndimages.
|
| 549 |
+
fps : str
|
| 550 |
+
The resulting videos frames per second.
|
| 551 |
+
in_pixel_format : str
|
| 552 |
+
The pixel format of the incoming ndarray. Defaults to "rgb24" and can
|
| 553 |
+
be any stridable pix_fmt supported by FFmpeg.
|
| 554 |
+
out_pixel_format : str
|
| 555 |
+
The pixel format to use while encoding frames. If None (default)
|
| 556 |
+
use the codec's default.
|
| 557 |
+
filter_sequence : List[str, str, dict]
|
| 558 |
+
If not None, apply the given sequence of FFmpeg filters to each
|
| 559 |
+
ndimage. Check the (module-level) plugin docs for details and
|
| 560 |
+
examples.
|
| 561 |
+
filter_graph : (dict, List)
|
| 562 |
+
If not None, apply the given graph of FFmpeg filters to each
|
| 563 |
+
ndimage. The graph is given as a tuple of two dicts. The first dict
|
| 564 |
+
contains a (named) set of nodes, and the second dict contains a set
|
| 565 |
+
of edges between nodes of the previous dict. Check the (module-level)
|
| 566 |
+
plugin docs for details and examples.
|
| 567 |
+
|
| 568 |
+
Returns
|
| 569 |
+
-------
|
| 570 |
+
encoded_image : bytes or None
|
| 571 |
+
If the chosen ImageResource is the special target ``"<bytes>"`` then
|
| 572 |
+
write will return a byte string containing the encoded image data.
|
| 573 |
+
Otherwise, it returns None.
|
| 574 |
+
|
| 575 |
+
Notes
|
| 576 |
+
-----
|
| 577 |
+
When writing ``<bytes>``, the video is finalized immediately after the
|
| 578 |
+
first write call and calling write multiple times to append frames is
|
| 579 |
+
not possible.
|
| 580 |
+
|
| 581 |
+
"""
|
| 582 |
+
|
| 583 |
+
if isinstance(ndimage, list):
|
| 584 |
+
# frames shapes must agree for video
|
| 585 |
+
ndimage = np.stack(ndimage)
|
| 586 |
+
elif not is_batch:
|
| 587 |
+
ndimage = np.asarray(ndimage)[None, ...]
|
| 588 |
+
else:
|
| 589 |
+
ndimage = np.asarray(ndimage)
|
| 590 |
+
|
| 591 |
+
if self._video_stream is None:
|
| 592 |
+
self._init_write_stream(
|
| 593 |
+
codec, fps, ndimage.shape, in_pixel_format, out_pixel_format
|
| 594 |
+
)
|
| 595 |
+
stream = self._video_stream
|
| 596 |
+
|
| 597 |
+
ffmpeg_filter = self._build_filter(filter_sequence, filter_graph)
|
| 598 |
+
ffmpeg_filter.send(None) # init
|
| 599 |
+
|
| 600 |
+
pixel_format = av.VideoFormat(in_pixel_format)
|
| 601 |
+
img_dtype = _format_to_dtype(pixel_format)
|
| 602 |
+
width = ndimage.shape[3 if pixel_format.is_planar else 2]
|
| 603 |
+
height = ndimage.shape[2 if pixel_format.is_planar else 1]
|
| 604 |
+
|
| 605 |
+
frame = av.VideoFrame(width, height, in_pixel_format)
|
| 606 |
+
frame.time_base = Fraction(1, fps)
|
| 607 |
+
n_channels = [
|
| 608 |
+
sum(
|
| 609 |
+
[
|
| 610 |
+
x.bits // (img_dtype.itemsize * 8)
|
| 611 |
+
for x in frame.format.components
|
| 612 |
+
if x.plane == idx
|
| 613 |
+
]
|
| 614 |
+
)
|
| 615 |
+
for idx in range(len(frame.planes))
|
| 616 |
+
]
|
| 617 |
+
|
| 618 |
+
for img in ndimage:
|
| 619 |
+
frame.pts = self.frames_written
|
| 620 |
+
self.frames_written += 1
|
| 621 |
+
# manual packing of ndarray into frame
|
| 622 |
+
# (this should live in pyAV, but it doesn't support many formats
|
| 623 |
+
# and PRs there are slow)
|
| 624 |
+
if pixel_format.is_planar:
|
| 625 |
+
for idx, plane in enumerate(frame.planes):
|
| 626 |
+
plane_array = np.frombuffer(plane, dtype=img_dtype)
|
| 627 |
+
plane_array = as_strided(
|
| 628 |
+
plane_array,
|
| 629 |
+
shape=(plane.height, plane.width),
|
| 630 |
+
strides=(plane.line_size, img_dtype.itemsize),
|
| 631 |
+
)
|
| 632 |
+
plane_array[...] = img[idx]
|
| 633 |
+
else:
|
| 634 |
+
if in_pixel_format.startswith("bayer_"):
|
| 635 |
+
n_channels = 1
|
| 636 |
+
else:
|
| 637 |
+
n_channels = len(pixel_format.components)
|
| 638 |
+
|
| 639 |
+
plane = frame.planes[0]
|
| 640 |
+
plane_shape = (plane.height, plane.width)
|
| 641 |
+
plane_strides = (plane.line_size, n_channels * img_dtype.itemsize)
|
| 642 |
+
if n_channels > 1:
|
| 643 |
+
plane_shape += (n_channels,)
|
| 644 |
+
plane_strides += (img_dtype.itemsize,)
|
| 645 |
+
|
| 646 |
+
plane_array = as_strided(
|
| 647 |
+
np.frombuffer(plane, dtype=img_dtype),
|
| 648 |
+
shape=plane_shape,
|
| 649 |
+
strides=plane_strides,
|
| 650 |
+
)
|
| 651 |
+
plane_array[...] = img
|
| 652 |
+
|
| 653 |
+
out_frame = ffmpeg_filter.send(frame)
|
| 654 |
+
if out_frame is None:
|
| 655 |
+
continue
|
| 656 |
+
|
| 657 |
+
out_frame = out_frame.reformat(format=out_pixel_format)
|
| 658 |
+
|
| 659 |
+
for packet in stream.encode(out_frame):
|
| 660 |
+
self._container.mux(packet)
|
| 661 |
+
|
| 662 |
+
for out_frame in ffmpeg_filter:
|
| 663 |
+
for packet in stream.encode(out_frame):
|
| 664 |
+
self._container.mux(packet)
|
| 665 |
+
|
| 666 |
+
if self.request._uri_type == URI_BYTES:
|
| 667 |
+
# bytes are immutuable, so we have to flush immediately
|
| 668 |
+
# and can't support appending to an active stream
|
| 669 |
+
for packet in self._video_stream.encode():
|
| 670 |
+
self._container.mux(packet)
|
| 671 |
+
self._video_stream = None
|
| 672 |
+
self._container.close()
|
| 673 |
+
|
| 674 |
+
return self.request.get_file().getvalue()
|
| 675 |
+
|
| 676 |
+
def _init_write_stream(
|
| 677 |
+
self,
|
| 678 |
+
codec: str,
|
| 679 |
+
fps: int,
|
| 680 |
+
shape: Tuple[int, ...],
|
| 681 |
+
in_pixel_format: Optional[str],
|
| 682 |
+
out_pixel_format: Optional[str],
|
| 683 |
+
) -> None:
|
| 684 |
+
"""Initialize encoder and create a new video stream.
|
| 685 |
+
|
| 686 |
+
Parameters
|
| 687 |
+
----------
|
| 688 |
+
codec : str
|
| 689 |
+
The codec to use.
|
| 690 |
+
fps : str
|
| 691 |
+
The resulting videos frames per second.
|
| 692 |
+
shape : Tuple[int, ...]
|
| 693 |
+
The shape of the frames that will be written.
|
| 694 |
+
in_pixel_format : str
|
| 695 |
+
The pixel format of the incoming ndarray.
|
| 696 |
+
out_pixel_format : str
|
| 697 |
+
The pixel format to use while encoding frames. If None (default)
|
| 698 |
+
use the codec's default.
|
| 699 |
+
|
| 700 |
+
"""
|
| 701 |
+
|
| 702 |
+
stream = self._container.add_stream(codec, fps)
|
| 703 |
+
px_format = av.VideoFormat(in_pixel_format)
|
| 704 |
+
stream.width = shape[3 if px_format.is_planar else 2]
|
| 705 |
+
stream.height = shape[2 if px_format.is_planar else 1]
|
| 706 |
+
stream.time_base = Fraction(1, fps)
|
| 707 |
+
|
| 708 |
+
if out_pixel_format is not None:
|
| 709 |
+
stream.pix_fmt = out_pixel_format
|
| 710 |
+
elif in_pixel_format in [x.name for x in stream.codec.video_formats]:
|
| 711 |
+
stream.pix_fmt = in_pixel_format
|
| 712 |
+
else:
|
| 713 |
+
pass # use the default pixel format
|
| 714 |
+
|
| 715 |
+
self._video_stream = stream
|
| 716 |
+
|
| 717 |
+
def _build_filter(
|
| 718 |
+
self,
|
| 719 |
+
filter_sequence: List[Tuple[str, Union[str, dict]]] = None,
|
| 720 |
+
filter_graph: Tuple[dict, List] = None,
|
| 721 |
+
) -> av.VideoFrame:
|
| 722 |
+
"""Create a FFmpeg filter graph.
|
| 723 |
+
|
| 724 |
+
This function is a python co-routine. This means it returns a
|
| 725 |
+
generator and you can feed it frames using ``generator.send(frame)``
|
| 726 |
+
and it will return the next frame or None (if the filter has lag).
|
| 727 |
+
To send EOF use ``generator.send(None)``
|
| 728 |
+
|
| 729 |
+
|
| 730 |
+
Parameters
|
| 731 |
+
----------
|
| 732 |
+
filter_sequence : List[str, str, dict]
|
| 733 |
+
If not None, apply the given sequence of FFmpeg filters to each
|
| 734 |
+
ndimage. Check the (module-level) plugin docs for details and
|
| 735 |
+
examples.
|
| 736 |
+
filter_graph : (dict, List)
|
| 737 |
+
If not None, apply the given graph of FFmpeg filters to each
|
| 738 |
+
ndimage. The graph is given as a tuple of two dicts. The first dict
|
| 739 |
+
contains a (named) set of nodes, and the second dict contains a set
|
| 740 |
+
of edges between nodes of the previous dict.Check the (module-level)
|
| 741 |
+
plugin docs for details and examples.
|
| 742 |
+
|
| 743 |
+
Yields
|
| 744 |
+
-------
|
| 745 |
+
frame : Optional[av.VideoFrame]
|
| 746 |
+
A frame that was filtered using the created filter or None if the
|
| 747 |
+
filter has lag and didn't send any frames yet.
|
| 748 |
+
|
| 749 |
+
"""
|
| 750 |
+
|
| 751 |
+
node_descriptors: Dict[str, Tuple[str, Union[str, Dict]]]
|
| 752 |
+
edges: List[Tuple[str, str, int, int]]
|
| 753 |
+
|
| 754 |
+
# Nothing to do :)
|
| 755 |
+
if filter_sequence is None and filter_graph is None:
|
| 756 |
+
frame = yield
|
| 757 |
+
|
| 758 |
+
while frame is not None:
|
| 759 |
+
frame = yield frame
|
| 760 |
+
|
| 761 |
+
return
|
| 762 |
+
|
| 763 |
+
if filter_sequence is None:
|
| 764 |
+
filter_sequence = list()
|
| 765 |
+
|
| 766 |
+
if filter_graph is None:
|
| 767 |
+
node_descriptors, edges = dict(), [("video_in", "video_out", 0, 0)]
|
| 768 |
+
else:
|
| 769 |
+
node_descriptors, edges = filter_graph
|
| 770 |
+
|
| 771 |
+
graph = av.filter.Graph()
|
| 772 |
+
|
| 773 |
+
previous_node = graph.add_buffer(template=self._video_stream)
|
| 774 |
+
for filter_name, argument in filter_sequence:
|
| 775 |
+
if isinstance(argument, str):
|
| 776 |
+
current_node = graph.add(filter_name, argument)
|
| 777 |
+
else:
|
| 778 |
+
current_node = graph.add(filter_name, **argument)
|
| 779 |
+
previous_node.link_to(current_node)
|
| 780 |
+
previous_node = current_node
|
| 781 |
+
|
| 782 |
+
nodes = dict()
|
| 783 |
+
nodes["video_in"] = previous_node
|
| 784 |
+
nodes["video_out"] = graph.add("buffersink")
|
| 785 |
+
for name, (filter_name, arguments) in node_descriptors.items():
|
| 786 |
+
if isinstance(arguments, str):
|
| 787 |
+
nodes[name] = graph.add(filter_name, arguments)
|
| 788 |
+
else:
|
| 789 |
+
nodes[name] = graph.add(filter_name, **arguments)
|
| 790 |
+
|
| 791 |
+
for from_note, to_node, out_idx, in_idx in edges:
|
| 792 |
+
nodes[from_note].link_to(nodes[to_node], out_idx, in_idx)
|
| 793 |
+
|
| 794 |
+
graph.configure()
|
| 795 |
+
|
| 796 |
+
# this starts a co-routine
|
| 797 |
+
# send frames using graph.send()
|
| 798 |
+
frame = yield None
|
| 799 |
+
|
| 800 |
+
# send and receive frames in "parallel"
|
| 801 |
+
while frame is not None:
|
| 802 |
+
graph.push(frame)
|
| 803 |
+
try:
|
| 804 |
+
frame = yield graph.pull()
|
| 805 |
+
except av.error.BlockingIOError:
|
| 806 |
+
# filter has lag and needs more frames
|
| 807 |
+
frame = yield None
|
| 808 |
+
|
| 809 |
+
try:
|
| 810 |
+
# send EOF in av>=9.0
|
| 811 |
+
graph.push(None)
|
| 812 |
+
except ValueError: # pragma: no cover
|
| 813 |
+
# handle av<9.0
|
| 814 |
+
pass
|
| 815 |
+
|
| 816 |
+
# all frames have been sent, empty the filter
|
| 817 |
+
while True:
|
| 818 |
+
try:
|
| 819 |
+
yield graph.pull()
|
| 820 |
+
except av.error.EOFError:
|
| 821 |
+
break # EOF
|
| 822 |
+
except av.error.BlockingIOError: # pragma: no cover
|
| 823 |
+
# handle av<9.0
|
| 824 |
+
break
|
| 825 |
+
|
| 826 |
+
def properties(self, index: int = ..., *, format: str = "rgb24") -> ImageProperties:
|
| 827 |
+
"""Standardized ndimage metadata.
|
| 828 |
+
|
| 829 |
+
Parameters
|
| 830 |
+
----------
|
| 831 |
+
index : int
|
| 832 |
+
The index of the ndimage for which to return properties. If ``...``
|
| 833 |
+
(Ellipsis, default), return the properties for the resulting batch
|
| 834 |
+
of frames.
|
| 835 |
+
format : str
|
| 836 |
+
If not None (default: rgb24), convert the data into the given format
|
| 837 |
+
before returning it. If None return the data in the encoded format
|
| 838 |
+
if that can be expressed as a strided array; otherwise raise an
|
| 839 |
+
Exception.
|
| 840 |
+
|
| 841 |
+
Returns
|
| 842 |
+
-------
|
| 843 |
+
properties : ImageProperties
|
| 844 |
+
A dataclass filled with standardized image metadata.
|
| 845 |
+
|
| 846 |
+
Notes
|
| 847 |
+
-----
|
| 848 |
+
This function is efficient and won't process any pixel data.
|
| 849 |
+
|
| 850 |
+
The provided metadata does not include modifications by any filters
|
| 851 |
+
(through ``filter_sequence`` or ``filter_graph``).
|
| 852 |
+
|
| 853 |
+
"""
|
| 854 |
+
|
| 855 |
+
video_width = self._video_stream.codec_context.width
|
| 856 |
+
video_height = self._video_stream.codec_context.height
|
| 857 |
+
pix_format = format or self._video_stream.codec_context.pix_fmt
|
| 858 |
+
frame_template = av.VideoFrame(video_width, video_height, pix_format)
|
| 859 |
+
|
| 860 |
+
shape = _get_frame_shape(frame_template)
|
| 861 |
+
if index is ...:
|
| 862 |
+
n_frames = self._video_stream.frames
|
| 863 |
+
shape = (n_frames,) + shape
|
| 864 |
+
|
| 865 |
+
return ImageProperties(
|
| 866 |
+
shape=tuple(shape),
|
| 867 |
+
dtype=_format_to_dtype(frame_template.format),
|
| 868 |
+
is_batch=True if index is ... else False,
|
| 869 |
+
)
|
| 870 |
+
|
| 871 |
+
def metadata(
|
| 872 |
+
self,
|
| 873 |
+
index: int = ...,
|
| 874 |
+
exclude_applied: bool = True,
|
| 875 |
+
constant_framerate: bool = True,
|
| 876 |
+
) -> Dict[str, Any]:
|
| 877 |
+
"""Format-specific metadata.
|
| 878 |
+
|
| 879 |
+
Returns a dictionary filled with metadata that is either stored in the
|
| 880 |
+
container, the video stream, or the frame's side-data.
|
| 881 |
+
|
| 882 |
+
Parameters
|
| 883 |
+
----------
|
| 884 |
+
index : int
|
| 885 |
+
If ... (Ellipsis, default) return global metadata (the metadata
|
| 886 |
+
stored in the container and video stream). If not ..., return the
|
| 887 |
+
side data stored in the frame at the given index.
|
| 888 |
+
exclude_applied : bool
|
| 889 |
+
Currently, this parameter has no effect. It exists for compliance with
|
| 890 |
+
the ImageIO v3 API.
|
| 891 |
+
constant_framerate : bool
|
| 892 |
+
If True assume the video's framerate is constant. This allows for
|
| 893 |
+
faster seeking inside the file. If False, the video is reset before
|
| 894 |
+
each read and searched from the beginning. If None (default), this
|
| 895 |
+
value will be read from the container format.
|
| 896 |
+
|
| 897 |
+
Returns
|
| 898 |
+
-------
|
| 899 |
+
metadata : dict
|
| 900 |
+
A dictionary filled with format-specific metadata fields and their
|
| 901 |
+
values.
|
| 902 |
+
|
| 903 |
+
"""
|
| 904 |
+
|
| 905 |
+
metadata = dict()
|
| 906 |
+
|
| 907 |
+
if index is ...:
|
| 908 |
+
# useful flags defined on the container and/or video stream
|
| 909 |
+
metadata.update(
|
| 910 |
+
{
|
| 911 |
+
"video_format": self._video_stream.codec_context.pix_fmt,
|
| 912 |
+
"codec": self._video_stream.codec.name,
|
| 913 |
+
"long_codec": self._video_stream.codec.long_name,
|
| 914 |
+
"profile": self._video_stream.profile,
|
| 915 |
+
}
|
| 916 |
+
)
|
| 917 |
+
|
| 918 |
+
metadata.update(self._container.metadata)
|
| 919 |
+
metadata.update(self._video_stream.metadata)
|
| 920 |
+
return metadata
|
| 921 |
+
|
| 922 |
+
self._seek(index, constant_framerate=constant_framerate)
|
| 923 |
+
desired_frame = next(self._container.decode(video=0))
|
| 924 |
+
|
| 925 |
+
# useful flags defined on the frame
|
| 926 |
+
metadata.update(
|
| 927 |
+
{
|
| 928 |
+
"key_frame": bool(desired_frame.key_frame),
|
| 929 |
+
"time": desired_frame.time,
|
| 930 |
+
"interlaced_frame": bool(desired_frame.interlaced_frame),
|
| 931 |
+
}
|
| 932 |
+
)
|
| 933 |
+
|
| 934 |
+
# side data
|
| 935 |
+
metadata.update({key: value for key, value in desired_frame.side_data.items()})
|
| 936 |
+
|
| 937 |
+
return self._container.metadata
|
| 938 |
+
|
| 939 |
+
def _seek(self, index, *, constant_framerate: bool = True) -> None:
|
| 940 |
+
"""Seeks to the frame at the given index."""
|
| 941 |
+
|
| 942 |
+
# this may be made faster for formats that have some kind
|
| 943 |
+
# of keyframe table in their header data.
|
| 944 |
+
if not constant_framerate:
|
| 945 |
+
self._container.seek(0)
|
| 946 |
+
frames_to_yield = index
|
| 947 |
+
else:
|
| 948 |
+
frame_delta = 1000 // self._video_stream.guessed_rate
|
| 949 |
+
requested_index = frame_delta * index
|
| 950 |
+
|
| 951 |
+
# this only seeks to the closed (preceeding) keyframe
|
| 952 |
+
self._container.seek(requested_index)
|
| 953 |
+
|
| 954 |
+
keyframe = next(self._container.decode(video=0))
|
| 955 |
+
frames_to_yield = index - keyframe.pts // frame_delta
|
| 956 |
+
self._container.seek(requested_index)
|
| 957 |
+
|
| 958 |
+
frame_generator = self._container.decode(video=0)
|
| 959 |
+
for _ in range(frames_to_yield):
|
| 960 |
+
next(frame_generator)
|
| 961 |
+
|
| 962 |
+
def close(self) -> None:
|
| 963 |
+
"""Close the Video."""
|
| 964 |
+
|
| 965 |
+
is_write = self.request.mode.io_mode == IOMode.write
|
| 966 |
+
if is_write and self._video_stream is not None:
|
| 967 |
+
# encode a frame=None to flush any pending packets
|
| 968 |
+
for packet in self._video_stream.encode():
|
| 969 |
+
self._container.mux(packet)
|
| 970 |
+
|
| 971 |
+
if self._container is not None:
|
| 972 |
+
self._container.close()
|
| 973 |
+
self.request.finish()
|
| 974 |
+
|
| 975 |
+
def __enter__(self) -> "PyAVPlugin":
|
| 976 |
+
return super().__enter__()
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/simpleitk.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
""" Read/Write images using SimpleITK.
|
| 5 |
+
|
| 6 |
+
Backend: `Insight Toolkit <https://itk.org/>`_
|
| 7 |
+
|
| 8 |
+
.. note::
|
| 9 |
+
To use this plugin you have to install its backend::
|
| 10 |
+
|
| 11 |
+
pip install imageio[itk]
|
| 12 |
+
|
| 13 |
+
The ItkFormat uses the ITK or SimpleITK library to support a range of
|
| 14 |
+
ITK-related formats. It also supports a few common formats (e.g. PNG and JPEG).
|
| 15 |
+
|
| 16 |
+
Parameters
|
| 17 |
+
----------
|
| 18 |
+
None
|
| 19 |
+
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from ..core import Format, has_module
|
| 23 |
+
|
| 24 |
+
_itk = None # Defer loading to load_lib() function.
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def load_lib():
|
| 28 |
+
global _itk, _read_function, _write_function
|
| 29 |
+
try:
|
| 30 |
+
import itk as _itk
|
| 31 |
+
|
| 32 |
+
_read_function = _itk.imread
|
| 33 |
+
_write_function = _itk.imwrite
|
| 34 |
+
except ImportError:
|
| 35 |
+
try:
|
| 36 |
+
import SimpleITK as _itk
|
| 37 |
+
|
| 38 |
+
_read_function = _itk.ReadImage
|
| 39 |
+
_write_function = _itk.WriteImage
|
| 40 |
+
except ImportError:
|
| 41 |
+
raise ImportError(
|
| 42 |
+
"itk could not be found. "
|
| 43 |
+
"Please try "
|
| 44 |
+
" python -m pip install itk "
|
| 45 |
+
"or "
|
| 46 |
+
" python -m pip install simpleitk "
|
| 47 |
+
"or refer to "
|
| 48 |
+
" https://itkpythonpackage.readthedocs.io/ "
|
| 49 |
+
"for further instructions."
|
| 50 |
+
)
|
| 51 |
+
return _itk
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# Split up in real ITK and all supported formats.
|
| 55 |
+
ITK_FORMATS = (
|
| 56 |
+
".gipl",
|
| 57 |
+
".ipl",
|
| 58 |
+
".mha",
|
| 59 |
+
".mhd",
|
| 60 |
+
".nhdr",
|
| 61 |
+
"nia",
|
| 62 |
+
"hdr",
|
| 63 |
+
".nrrd",
|
| 64 |
+
".nii",
|
| 65 |
+
".nii.gz",
|
| 66 |
+
".img",
|
| 67 |
+
".img.gz",
|
| 68 |
+
".vtk",
|
| 69 |
+
"hdf5",
|
| 70 |
+
"lsm",
|
| 71 |
+
"mnc",
|
| 72 |
+
"mnc2",
|
| 73 |
+
"mgh",
|
| 74 |
+
"mnc",
|
| 75 |
+
"pic",
|
| 76 |
+
)
|
| 77 |
+
ALL_FORMATS = ITK_FORMATS + (
|
| 78 |
+
".bmp",
|
| 79 |
+
".jpeg",
|
| 80 |
+
".jpg",
|
| 81 |
+
".png",
|
| 82 |
+
".tiff",
|
| 83 |
+
".tif",
|
| 84 |
+
".dicom",
|
| 85 |
+
".dcm",
|
| 86 |
+
".gdcm",
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class ItkFormat(Format):
|
| 91 |
+
"""See :mod:`imageio.plugins.simpleitk`"""
|
| 92 |
+
|
| 93 |
+
def _can_read(self, request):
|
| 94 |
+
# If the request is a format that only this plugin can handle,
|
| 95 |
+
# we report that we can do it; a useful error will be raised
|
| 96 |
+
# when simpleitk is not installed. For the more common formats
|
| 97 |
+
# we only report that we can read if the library is installed.
|
| 98 |
+
if request.extension in ITK_FORMATS:
|
| 99 |
+
return True
|
| 100 |
+
if has_module("itk.ImageIOBase") or has_module("SimpleITK"):
|
| 101 |
+
return request.extension in ALL_FORMATS
|
| 102 |
+
|
| 103 |
+
def _can_write(self, request):
|
| 104 |
+
if request.extension in ITK_FORMATS:
|
| 105 |
+
return True
|
| 106 |
+
if has_module("itk.ImageIOBase") or has_module("SimpleITK"):
|
| 107 |
+
return request.extension in ALL_FORMATS
|
| 108 |
+
|
| 109 |
+
# -- reader
|
| 110 |
+
|
| 111 |
+
class Reader(Format.Reader):
|
| 112 |
+
def _open(self, pixel_type=None, fallback_only=None, **kwargs):
|
| 113 |
+
if not _itk:
|
| 114 |
+
load_lib()
|
| 115 |
+
args = ()
|
| 116 |
+
if pixel_type is not None:
|
| 117 |
+
args += (pixel_type,)
|
| 118 |
+
if fallback_only is not None:
|
| 119 |
+
args += (fallback_only,)
|
| 120 |
+
self._img = _read_function(self.request.get_local_filename(), *args)
|
| 121 |
+
|
| 122 |
+
def _get_length(self):
|
| 123 |
+
return 1
|
| 124 |
+
|
| 125 |
+
def _close(self):
|
| 126 |
+
pass
|
| 127 |
+
|
| 128 |
+
def _get_data(self, index):
|
| 129 |
+
# Get data
|
| 130 |
+
if index != 0:
|
| 131 |
+
error_msg = "Index out of range while reading from itk file"
|
| 132 |
+
raise IndexError(error_msg)
|
| 133 |
+
|
| 134 |
+
# Return array and empty meta data
|
| 135 |
+
return _itk.GetArrayFromImage(self._img), {}
|
| 136 |
+
|
| 137 |
+
def _get_meta_data(self, index):
|
| 138 |
+
error_msg = "The itk plugin does not support meta data, currently."
|
| 139 |
+
raise RuntimeError(error_msg)
|
| 140 |
+
|
| 141 |
+
# -- writer
|
| 142 |
+
class Writer(Format.Writer):
|
| 143 |
+
def _open(self):
|
| 144 |
+
if not _itk:
|
| 145 |
+
load_lib()
|
| 146 |
+
|
| 147 |
+
def _close(self):
|
| 148 |
+
pass
|
| 149 |
+
|
| 150 |
+
def _append_data(self, im, meta):
|
| 151 |
+
_itk_img = _itk.GetImageFromArray(im)
|
| 152 |
+
_write_function(_itk_img, self.request.get_local_filename())
|
| 153 |
+
|
| 154 |
+
def set_meta_data(self, meta):
|
| 155 |
+
error_msg = "The itk plugin does not support meta data, currently."
|
| 156 |
+
raise RuntimeError(error_msg)
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/spe.py
ADDED
|
@@ -0,0 +1,755 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
""" Read SPE files.
|
| 5 |
+
|
| 6 |
+
Backend: internal
|
| 7 |
+
|
| 8 |
+
This plugin supports reading files saved in the Princeton Instruments
|
| 9 |
+
SPE file format.
|
| 10 |
+
|
| 11 |
+
Parameters for reading
|
| 12 |
+
----------------------
|
| 13 |
+
char_encoding : str
|
| 14 |
+
Character encoding used to decode strings in the metadata. Defaults
|
| 15 |
+
to "latin1".
|
| 16 |
+
check_filesize : bool
|
| 17 |
+
The number of frames in the file is stored in the file header. However,
|
| 18 |
+
this number may be wrong for certain software. If this is `True`
|
| 19 |
+
(default), derive the number of frames also from the file size and
|
| 20 |
+
raise a warning if the two values do not match.
|
| 21 |
+
sdt_meta : bool
|
| 22 |
+
If set to `True` (default), check for special metadata written by the
|
| 23 |
+
`SDT-control` software. Does not have an effect for files written by
|
| 24 |
+
other software.
|
| 25 |
+
|
| 26 |
+
Metadata for reading
|
| 27 |
+
--------------------
|
| 28 |
+
ROIs : list of dict
|
| 29 |
+
Regions of interest used for recording images. Each dict has the
|
| 30 |
+
"top_left" key containing x and y coordinates of the top left corner,
|
| 31 |
+
the "bottom_right" key with x and y coordinates of the bottom right
|
| 32 |
+
corner, and the "bin" key with number of binned pixels in x and y
|
| 33 |
+
directions.
|
| 34 |
+
comments : list of str
|
| 35 |
+
The SPE format allows for 5 comment strings of 80 characters each.
|
| 36 |
+
controller_version : int
|
| 37 |
+
Hardware version
|
| 38 |
+
logic_output : int
|
| 39 |
+
Definition of output BNC
|
| 40 |
+
amp_hi_cap_low_noise : int
|
| 41 |
+
Amp switching mode
|
| 42 |
+
mode : int
|
| 43 |
+
Timing mode
|
| 44 |
+
exp_sec : float
|
| 45 |
+
Alternative exposure in seconds
|
| 46 |
+
date : str
|
| 47 |
+
Date string
|
| 48 |
+
detector_temp : float
|
| 49 |
+
Detector temperature
|
| 50 |
+
detector_type : int
|
| 51 |
+
CCD / diode array type
|
| 52 |
+
st_diode : int
|
| 53 |
+
Trigger diode
|
| 54 |
+
delay_time : float
|
| 55 |
+
Used with async mode
|
| 56 |
+
shutter_control : int
|
| 57 |
+
Normal, disabled open, or disabled closed
|
| 58 |
+
absorb_live : bool
|
| 59 |
+
on / off
|
| 60 |
+
absorb_mode : int
|
| 61 |
+
Reference strip or file
|
| 62 |
+
can_do_virtual_chip : bool
|
| 63 |
+
True or False whether chip can do virtual chip
|
| 64 |
+
threshold_min_live : bool
|
| 65 |
+
on / off
|
| 66 |
+
threshold_min_val : float
|
| 67 |
+
Threshold minimum value
|
| 68 |
+
threshold_max_live : bool
|
| 69 |
+
on / off
|
| 70 |
+
threshold_max_val : float
|
| 71 |
+
Threshold maximum value
|
| 72 |
+
time_local : str
|
| 73 |
+
Experiment local time
|
| 74 |
+
time_utc : str
|
| 75 |
+
Experiment UTC time
|
| 76 |
+
adc_offset : int
|
| 77 |
+
ADC offset
|
| 78 |
+
adc_rate : int
|
| 79 |
+
ADC rate
|
| 80 |
+
adc_type : int
|
| 81 |
+
ADC type
|
| 82 |
+
adc_resolution : int
|
| 83 |
+
ADC resolution
|
| 84 |
+
adc_bit_adjust : int
|
| 85 |
+
ADC bit adjust
|
| 86 |
+
gain : int
|
| 87 |
+
gain
|
| 88 |
+
sw_version : str
|
| 89 |
+
Version of software which created this file
|
| 90 |
+
spare_4 : bytes
|
| 91 |
+
Reserved space
|
| 92 |
+
readout_time : float
|
| 93 |
+
Experiment readout time
|
| 94 |
+
type : str
|
| 95 |
+
Controller type
|
| 96 |
+
clockspeed_us : float
|
| 97 |
+
Vertical clock speed in microseconds
|
| 98 |
+
readout_mode : ["full frame", "frame transfer", "kinetics", ""]
|
| 99 |
+
Readout mode. Empty string means that this was not set by the
|
| 100 |
+
Software.
|
| 101 |
+
window_size : int
|
| 102 |
+
Window size for Kinetics mode
|
| 103 |
+
file_header_ver : float
|
| 104 |
+
File header version
|
| 105 |
+
chip_size : [int, int]
|
| 106 |
+
x and y dimensions of the camera chip
|
| 107 |
+
virt_chip_size : [int, int]
|
| 108 |
+
Virtual chip x and y dimensions
|
| 109 |
+
pre_pixels : [int, int]
|
| 110 |
+
Pre pixels in x and y dimensions
|
| 111 |
+
post_pixels : [int, int],
|
| 112 |
+
Post pixels in x and y dimensions
|
| 113 |
+
geometric : list of {"rotate", "reverse", "flip"}
|
| 114 |
+
Geometric operations
|
| 115 |
+
sdt_major_version : int
|
| 116 |
+
(only for files created by SDT-control)
|
| 117 |
+
Major version of SDT-control software
|
| 118 |
+
sdt_minor_version : int
|
| 119 |
+
(only for files created by SDT-control)
|
| 120 |
+
Minor version of SDT-control software
|
| 121 |
+
sdt_controller_name : str
|
| 122 |
+
(only for files created by SDT-control)
|
| 123 |
+
Controller name
|
| 124 |
+
exposure_time : float
|
| 125 |
+
(only for files created by SDT-control)
|
| 126 |
+
Exposure time in seconds
|
| 127 |
+
color_code : str
|
| 128 |
+
(only for files created by SDT-control)
|
| 129 |
+
Color channels used
|
| 130 |
+
detection_channels : int
|
| 131 |
+
(only for files created by SDT-control)
|
| 132 |
+
Number of channels
|
| 133 |
+
background_subtraction : bool
|
| 134 |
+
(only for files created by SDT-control)
|
| 135 |
+
Whether background subtraction war turned on
|
| 136 |
+
em_active : bool
|
| 137 |
+
(only for files created by SDT-control)
|
| 138 |
+
Whether EM was turned on
|
| 139 |
+
em_gain : int
|
| 140 |
+
(only for files created by SDT-control)
|
| 141 |
+
EM gain
|
| 142 |
+
modulation_active : bool
|
| 143 |
+
(only for files created by SDT-control)
|
| 144 |
+
Whether laser modulation (“attenuate”) was turned on
|
| 145 |
+
pixel_size : float
|
| 146 |
+
(only for files created by SDT-control)
|
| 147 |
+
Camera pixel size
|
| 148 |
+
sequence_type : str
|
| 149 |
+
(only for files created by SDT-control)
|
| 150 |
+
Type of sequnce (standard, TOCCSL, arbitrary, …)
|
| 151 |
+
grid : float
|
| 152 |
+
(only for files created by SDT-control)
|
| 153 |
+
Sequence time unit (“grid size”) in seconds
|
| 154 |
+
n_macro : int
|
| 155 |
+
(only for files created by SDT-control)
|
| 156 |
+
Number of macro loops
|
| 157 |
+
delay_macro : float
|
| 158 |
+
(only for files created by SDT-control)
|
| 159 |
+
Time between macro loops in seconds
|
| 160 |
+
n_mini : int
|
| 161 |
+
(only for files created by SDT-control)
|
| 162 |
+
Number of mini loops
|
| 163 |
+
delay_mini : float
|
| 164 |
+
(only for files created by SDT-control)
|
| 165 |
+
Time between mini loops in seconds
|
| 166 |
+
n_micro : int (only for files created by SDT-control)
|
| 167 |
+
Number of micro loops
|
| 168 |
+
delay_micro : float (only for files created by SDT-control)
|
| 169 |
+
Time between micro loops in seconds
|
| 170 |
+
n_subpics : int
|
| 171 |
+
(only for files created by SDT-control)
|
| 172 |
+
Number of sub-pictures
|
| 173 |
+
delay_shutter : float
|
| 174 |
+
(only for files created by SDT-control)
|
| 175 |
+
Camera shutter delay in seconds
|
| 176 |
+
delay_prebleach : float
|
| 177 |
+
(only for files created by SDT-control)
|
| 178 |
+
Pre-bleach delay in seconds
|
| 179 |
+
bleach_time : float
|
| 180 |
+
(only for files created by SDT-control)
|
| 181 |
+
Bleaching time in seconds
|
| 182 |
+
recovery_time : float
|
| 183 |
+
(only for files created by SDT-control)
|
| 184 |
+
Recovery time in seconds
|
| 185 |
+
comment : str
|
| 186 |
+
(only for files created by SDT-control)
|
| 187 |
+
User-entered comment. This replaces the "comments" field.
|
| 188 |
+
datetime : datetime.datetime
|
| 189 |
+
(only for files created by SDT-control)
|
| 190 |
+
Combines the "date" and "time_local" keys. The latter two plus
|
| 191 |
+
"time_utc" are removed.
|
| 192 |
+
modulation_script : str
|
| 193 |
+
(only for files created by SDT-control)
|
| 194 |
+
Laser modulation script. Replaces the "spare_4" key.
|
| 195 |
+
|
| 196 |
+
"""
|
| 197 |
+
|
| 198 |
+
from datetime import datetime
|
| 199 |
+
import logging
|
| 200 |
+
import os
|
| 201 |
+
from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Union
|
| 202 |
+
|
| 203 |
+
import numpy as np
|
| 204 |
+
|
| 205 |
+
from ..core import Format
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
logger = logging.getLogger(__name__)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
class Spec:
|
| 212 |
+
"""SPE file specification data
|
| 213 |
+
|
| 214 |
+
Tuples of (offset, datatype, count), where offset is the offset in the SPE
|
| 215 |
+
file and datatype is the datatype as used in `numpy.fromfile`()
|
| 216 |
+
|
| 217 |
+
`data_start` is the offset of actual image data.
|
| 218 |
+
|
| 219 |
+
`dtypes` translates SPE datatypes (0...4) to numpy ones, e. g. dtypes[0]
|
| 220 |
+
is dtype("<f") (which is np.float32).
|
| 221 |
+
|
| 222 |
+
`controllers` maps the `type` metadata to a human readable name
|
| 223 |
+
|
| 224 |
+
`readout_modes` maps the `readoutMode` metadata to something human readable
|
| 225 |
+
although this may not be accurate since there is next to no documentation
|
| 226 |
+
to be found.
|
| 227 |
+
"""
|
| 228 |
+
|
| 229 |
+
basic = {
|
| 230 |
+
"datatype": (108, "<h"), # dtypes
|
| 231 |
+
"xdim": (42, "<H"),
|
| 232 |
+
"ydim": (656, "<H"),
|
| 233 |
+
"xml_footer_offset": (678, "<Q"),
|
| 234 |
+
"NumFrames": (1446, "<i"),
|
| 235 |
+
"file_header_ver": (1992, "<f"),
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
metadata = {
|
| 239 |
+
# ROI information
|
| 240 |
+
"NumROI": (1510, "<h"),
|
| 241 |
+
"ROIs": (
|
| 242 |
+
1512,
|
| 243 |
+
np.dtype(
|
| 244 |
+
[
|
| 245 |
+
("startx", "<H"),
|
| 246 |
+
("endx", "<H"),
|
| 247 |
+
("groupx", "<H"),
|
| 248 |
+
("starty", "<H"),
|
| 249 |
+
("endy", "<H"),
|
| 250 |
+
("groupy", "<H"),
|
| 251 |
+
]
|
| 252 |
+
),
|
| 253 |
+
10,
|
| 254 |
+
),
|
| 255 |
+
# chip-related sizes
|
| 256 |
+
"xDimDet": (6, "<H"),
|
| 257 |
+
"yDimDet": (18, "<H"),
|
| 258 |
+
"VChipXdim": (14, "<h"),
|
| 259 |
+
"VChipYdim": (16, "<h"),
|
| 260 |
+
# other stuff
|
| 261 |
+
"controller_version": (0, "<h"),
|
| 262 |
+
"logic_output": (2, "<h"),
|
| 263 |
+
"amp_high_cap_low_noise": (4, "<H"), # enum?
|
| 264 |
+
"mode": (8, "<h"), # enum?
|
| 265 |
+
"exposure_sec": (10, "<f"),
|
| 266 |
+
"date": (20, "<10S"),
|
| 267 |
+
"detector_temp": (36, "<f"),
|
| 268 |
+
"detector_type": (40, "<h"),
|
| 269 |
+
"st_diode": (44, "<h"),
|
| 270 |
+
"delay_time": (46, "<f"),
|
| 271 |
+
# shutter_control: normal, disabled open, disabled closed
|
| 272 |
+
# But which one is which?
|
| 273 |
+
"shutter_control": (50, "<H"),
|
| 274 |
+
"absorb_live": (52, "<h"),
|
| 275 |
+
"absorb_mode": (54, "<H"),
|
| 276 |
+
"can_do_virtual_chip": (56, "<h"),
|
| 277 |
+
"threshold_min_live": (58, "<h"),
|
| 278 |
+
"threshold_min_val": (60, "<f"),
|
| 279 |
+
"threshold_max_live": (64, "<h"),
|
| 280 |
+
"threshold_max_val": (66, "<f"),
|
| 281 |
+
"time_local": (172, "<7S"),
|
| 282 |
+
"time_utc": (179, "<7S"),
|
| 283 |
+
"adc_offset": (188, "<H"),
|
| 284 |
+
"adc_rate": (190, "<H"),
|
| 285 |
+
"adc_type": (192, "<H"),
|
| 286 |
+
"adc_resolution": (194, "<H"),
|
| 287 |
+
"adc_bit_adjust": (196, "<H"),
|
| 288 |
+
"gain": (198, "<H"),
|
| 289 |
+
"comments": (200, "<80S", 5),
|
| 290 |
+
"geometric": (600, "<H"), # flags
|
| 291 |
+
"sw_version": (688, "<16S"),
|
| 292 |
+
"spare_4": (742, "<436S"),
|
| 293 |
+
"XPrePixels": (98, "<h"),
|
| 294 |
+
"XPostPixels": (100, "<h"),
|
| 295 |
+
"YPrePixels": (102, "<h"),
|
| 296 |
+
"YPostPixels": (104, "<h"),
|
| 297 |
+
"readout_time": (672, "<f"),
|
| 298 |
+
"xml_footer_offset": (678, "<Q"),
|
| 299 |
+
"type": (704, "<h"), # controllers
|
| 300 |
+
"clockspeed_us": (1428, "<f"),
|
| 301 |
+
"readout_mode": (1480, "<H"), # readout_modes
|
| 302 |
+
"window_size": (1482, "<H"),
|
| 303 |
+
"file_header_ver": (1992, "<f"),
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
data_start = 4100
|
| 307 |
+
|
| 308 |
+
dtypes = {
|
| 309 |
+
0: np.dtype(np.float32),
|
| 310 |
+
1: np.dtype(np.int32),
|
| 311 |
+
2: np.dtype(np.int16),
|
| 312 |
+
3: np.dtype(np.uint16),
|
| 313 |
+
8: np.dtype(np.uint32),
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
controllers = [
|
| 317 |
+
"new120 (Type II)",
|
| 318 |
+
"old120 (Type I)",
|
| 319 |
+
"ST130",
|
| 320 |
+
"ST121",
|
| 321 |
+
"ST138",
|
| 322 |
+
"DC131 (PentaMax)",
|
| 323 |
+
"ST133 (MicroMax/Roper)",
|
| 324 |
+
"ST135 (GPIB)",
|
| 325 |
+
"VTCCD",
|
| 326 |
+
"ST116 (GPIB)",
|
| 327 |
+
"OMA3 (GPIB)",
|
| 328 |
+
"OMA4",
|
| 329 |
+
]
|
| 330 |
+
|
| 331 |
+
# This was gathered from random places on the internet and own experiments
|
| 332 |
+
# with the camera. May not be accurate.
|
| 333 |
+
readout_modes = ["full frame", "frame transfer", "kinetics"]
|
| 334 |
+
|
| 335 |
+
# Do not decode the following metadata keys into strings, but leave them
|
| 336 |
+
# as byte arrays
|
| 337 |
+
no_decode = ["spare_4"]
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
class SDTControlSpec:
|
| 341 |
+
"""Extract metadata written by the SDT-control software
|
| 342 |
+
|
| 343 |
+
Some of it is encoded in the comment strings
|
| 344 |
+
(see :py:meth:`parse_comments`). Also, date and time are encoded in a
|
| 345 |
+
peculiar way (see :py:meth:`get_datetime`). Use :py:meth:`extract_metadata`
|
| 346 |
+
to update the metadata dict.
|
| 347 |
+
"""
|
| 348 |
+
|
| 349 |
+
months = {
|
| 350 |
+
# Convert SDT-control month strings to month numbers
|
| 351 |
+
"Jän": 1,
|
| 352 |
+
"Jan": 1,
|
| 353 |
+
"Feb": 2,
|
| 354 |
+
"Mär": 3,
|
| 355 |
+
"Mar": 3,
|
| 356 |
+
"Apr": 4,
|
| 357 |
+
"Mai": 5,
|
| 358 |
+
"May": 5,
|
| 359 |
+
"Jun": 6,
|
| 360 |
+
"Jul": 7,
|
| 361 |
+
"Aug": 8,
|
| 362 |
+
"Sep": 9,
|
| 363 |
+
"Okt": 10,
|
| 364 |
+
"Oct": 10,
|
| 365 |
+
"Nov": 11,
|
| 366 |
+
"Dez": 12,
|
| 367 |
+
"Dec": 12,
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
sequence_types = {
|
| 371 |
+
# TODO: complete
|
| 372 |
+
"SEQU": "standard",
|
| 373 |
+
"SETO": "TOCCSL",
|
| 374 |
+
"KINE": "kinetics",
|
| 375 |
+
"SEAR": "arbitrary",
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
class CommentDesc:
|
| 379 |
+
"""Describe how to extract a metadata entry from a comment string"""
|
| 380 |
+
|
| 381 |
+
n: int
|
| 382 |
+
"""Which of the 5 SPE comment fields to use."""
|
| 383 |
+
slice: slice
|
| 384 |
+
"""Which characters from the `n`-th comment to use."""
|
| 385 |
+
cvt: Callable[[str], Any]
|
| 386 |
+
"""How to convert characters to something useful."""
|
| 387 |
+
scale: Union[None, float]
|
| 388 |
+
"""Optional scaling factor for numbers"""
|
| 389 |
+
|
| 390 |
+
def __init__(
|
| 391 |
+
self,
|
| 392 |
+
n: int,
|
| 393 |
+
slice: slice,
|
| 394 |
+
cvt: Callable[[str], Any] = str,
|
| 395 |
+
scale: Optional[float] = None,
|
| 396 |
+
):
|
| 397 |
+
self.n = n
|
| 398 |
+
self.slice = slice
|
| 399 |
+
self.cvt = cvt
|
| 400 |
+
self.scale = scale
|
| 401 |
+
|
| 402 |
+
comments = {
|
| 403 |
+
"sdt_major_version": CommentDesc(4, slice(66, 68), int),
|
| 404 |
+
"sdt_minor_version": CommentDesc(4, slice(68, 70), int),
|
| 405 |
+
"sdt_controller_name": CommentDesc(4, slice(0, 6), str),
|
| 406 |
+
"exposure_time": CommentDesc(1, slice(64, 73), float, 10**-6),
|
| 407 |
+
"color_code": CommentDesc(4, slice(10, 14), str),
|
| 408 |
+
"detection_channels": CommentDesc(4, slice(15, 16), int),
|
| 409 |
+
"background_subtraction": CommentDesc(4, 14, lambda x: x == "B"),
|
| 410 |
+
"em_active": CommentDesc(4, 32, lambda x: x == "E"),
|
| 411 |
+
"em_gain": CommentDesc(4, slice(28, 32), int),
|
| 412 |
+
"modulation_active": CommentDesc(4, 33, lambda x: x == "A"),
|
| 413 |
+
"pixel_size": CommentDesc(4, slice(25, 28), float, 0.1),
|
| 414 |
+
"sequence_type": CommentDesc(
|
| 415 |
+
4, slice(6, 10), lambda x: __class__.sequence_types[x]
|
| 416 |
+
),
|
| 417 |
+
"grid": CommentDesc(4, slice(16, 25), float, 10**-6),
|
| 418 |
+
"n_macro": CommentDesc(1, slice(0, 4), int),
|
| 419 |
+
"delay_macro": CommentDesc(1, slice(10, 19), float, 10**-3),
|
| 420 |
+
"n_mini": CommentDesc(1, slice(4, 7), int),
|
| 421 |
+
"delay_mini": CommentDesc(1, slice(19, 28), float, 10**-6),
|
| 422 |
+
"n_micro": CommentDesc(1, slice(7, 10), int),
|
| 423 |
+
"delay_micro": CommentDesc(1, slice(28, 37), float, 10**-6),
|
| 424 |
+
"n_subpics": CommentDesc(1, slice(7, 10), int),
|
| 425 |
+
"delay_shutter": CommentDesc(1, slice(73, 79), float, 10**-6),
|
| 426 |
+
"delay_prebleach": CommentDesc(1, slice(37, 46), float, 10**-6),
|
| 427 |
+
"bleach_time": CommentDesc(1, slice(46, 55), float, 10**-6),
|
| 428 |
+
"recovery_time": CommentDesc(1, slice(55, 64), float, 10**-6),
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
@staticmethod
|
| 432 |
+
def parse_comments(comments: Sequence[str]) -> Union[Dict, None]:
|
| 433 |
+
"""Extract SDT-control metadata from comments
|
| 434 |
+
|
| 435 |
+
Parameters
|
| 436 |
+
----------
|
| 437 |
+
comments
|
| 438 |
+
List of SPE file comments, typically ``metadata["comments"]``.
|
| 439 |
+
|
| 440 |
+
Returns
|
| 441 |
+
-------
|
| 442 |
+
If SDT-control comments were detected, return a dict of metadata, else
|
| 443 |
+
`None`.
|
| 444 |
+
"""
|
| 445 |
+
sdt_md = {}
|
| 446 |
+
if comments[4][70:] != "COMVER0500":
|
| 447 |
+
logger.debug("SDT-control comments not found.")
|
| 448 |
+
return None
|
| 449 |
+
|
| 450 |
+
sdt_md = {}
|
| 451 |
+
for name, spec in SDTControlSpec.comments.items():
|
| 452 |
+
try:
|
| 453 |
+
v = spec.cvt(comments[spec.n][spec.slice])
|
| 454 |
+
if spec.scale is not None:
|
| 455 |
+
v *= spec.scale
|
| 456 |
+
except Exception as e:
|
| 457 |
+
logger.debug(
|
| 458 |
+
"Failed to decode SDT-control metadata " f'field "{name}": {e}'
|
| 459 |
+
)
|
| 460 |
+
sdt_md[name] = v
|
| 461 |
+
comment = comments[0] + comments[2]
|
| 462 |
+
sdt_md["comment"] = comment.strip()
|
| 463 |
+
return sdt_md
|
| 464 |
+
|
| 465 |
+
@staticmethod
|
| 466 |
+
def get_datetime(date: str, time: str) -> Union[datetime, None]:
|
| 467 |
+
"""Turn date and time saved by SDT-control into proper datetime object
|
| 468 |
+
|
| 469 |
+
Parameters
|
| 470 |
+
----------
|
| 471 |
+
date
|
| 472 |
+
SPE file date, typically ``metadata["date"]``.
|
| 473 |
+
time
|
| 474 |
+
SPE file date, typically ``metadata["time_local"]``.
|
| 475 |
+
|
| 476 |
+
Returns
|
| 477 |
+
-------
|
| 478 |
+
File's datetime if parsing was succsessful, else None.
|
| 479 |
+
"""
|
| 480 |
+
try:
|
| 481 |
+
month = __class__.months[date[2:5]]
|
| 482 |
+
return datetime(
|
| 483 |
+
int(date[5:9]),
|
| 484 |
+
month,
|
| 485 |
+
int(date[0:2]),
|
| 486 |
+
int(time[0:2]),
|
| 487 |
+
int(time[2:4]),
|
| 488 |
+
int(time[4:6]),
|
| 489 |
+
)
|
| 490 |
+
except Exception as e:
|
| 491 |
+
logger.info(f"Failed to decode date from SDT-control metadata: {e}.")
|
| 492 |
+
|
| 493 |
+
@staticmethod
|
| 494 |
+
def extract_metadata(meta: Mapping, char_encoding: str = "latin1"):
|
| 495 |
+
"""Extract SDT-control metadata from SPE metadata
|
| 496 |
+
|
| 497 |
+
SDT-control stores some metadata in comments and other fields.
|
| 498 |
+
Extract them and remove unused entries.
|
| 499 |
+
|
| 500 |
+
Parameters
|
| 501 |
+
----------
|
| 502 |
+
meta
|
| 503 |
+
SPE file metadata. Modified in place.
|
| 504 |
+
char_encoding
|
| 505 |
+
Character encoding used to decode strings in the metadata.
|
| 506 |
+
"""
|
| 507 |
+
sdt_meta = __class__.parse_comments(meta["comments"])
|
| 508 |
+
if not sdt_meta:
|
| 509 |
+
return
|
| 510 |
+
# This file has SDT-control metadata
|
| 511 |
+
meta.pop("comments")
|
| 512 |
+
meta.update(sdt_meta)
|
| 513 |
+
|
| 514 |
+
# Get date and time in a usable format
|
| 515 |
+
dt = __class__.get_datetime(meta["date"], meta["time_local"])
|
| 516 |
+
if dt:
|
| 517 |
+
meta["datetime"] = dt
|
| 518 |
+
meta.pop("date")
|
| 519 |
+
meta.pop("time_local")
|
| 520 |
+
|
| 521 |
+
sp4 = meta["spare_4"]
|
| 522 |
+
try:
|
| 523 |
+
meta["modulation_script"] = sp4.decode(char_encoding)
|
| 524 |
+
meta.pop("spare_4")
|
| 525 |
+
except UnicodeDecodeError:
|
| 526 |
+
logger.warning(
|
| 527 |
+
"Failed to decode SDT-control laser "
|
| 528 |
+
"modulation script. Bad char_encoding?"
|
| 529 |
+
)
|
| 530 |
+
|
| 531 |
+
# Get rid of unused data
|
| 532 |
+
meta.pop("time_utc")
|
| 533 |
+
meta.pop("exposure_sec")
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
class SpeFormat(Format):
|
| 537 |
+
"""See :mod:`imageio.plugins.spe`"""
|
| 538 |
+
|
| 539 |
+
def _can_read(self, request):
|
| 540 |
+
return (
|
| 541 |
+
request.mode[1] in self.modes + "?" and request.extension in self.extensions
|
| 542 |
+
)
|
| 543 |
+
|
| 544 |
+
def _can_write(self, request):
|
| 545 |
+
return False
|
| 546 |
+
|
| 547 |
+
class Reader(Format.Reader):
|
| 548 |
+
def _open(self, char_encoding="latin1", check_filesize=True, sdt_meta=True):
|
| 549 |
+
self._file = self.request.get_file()
|
| 550 |
+
self._char_encoding = char_encoding
|
| 551 |
+
|
| 552 |
+
info = self._parse_header(Spec.basic)
|
| 553 |
+
self._file_header_ver = info["file_header_ver"]
|
| 554 |
+
self._dtype = Spec.dtypes[info["datatype"]]
|
| 555 |
+
self._shape = (info["ydim"], info["xdim"])
|
| 556 |
+
self._len = info["NumFrames"]
|
| 557 |
+
self._sdt_meta = sdt_meta
|
| 558 |
+
|
| 559 |
+
if check_filesize:
|
| 560 |
+
# Some software writes incorrect `NumFrames` metadata.
|
| 561 |
+
# To determine the number of frames, check the size of the data
|
| 562 |
+
# segment -- until the end of the file for SPE<3, until the
|
| 563 |
+
# xml footer for SPE>=3.
|
| 564 |
+
data_end = (
|
| 565 |
+
info["xml_footer_offset"]
|
| 566 |
+
if info["file_header_ver"] >= 3
|
| 567 |
+
else os.path.getsize(self.request.get_local_filename())
|
| 568 |
+
)
|
| 569 |
+
line = data_end - Spec.data_start
|
| 570 |
+
line //= self._shape[0] * self._shape[1] * self._dtype.itemsize
|
| 571 |
+
if line != self._len:
|
| 572 |
+
logger.warning(
|
| 573 |
+
"The file header of %s claims there are %s frames, "
|
| 574 |
+
"but there are actually %s frames.",
|
| 575 |
+
self.request.filename,
|
| 576 |
+
self._len,
|
| 577 |
+
line,
|
| 578 |
+
)
|
| 579 |
+
self._len = min(line, self._len)
|
| 580 |
+
|
| 581 |
+
self._meta = None
|
| 582 |
+
|
| 583 |
+
def _get_meta_data(self, index):
|
| 584 |
+
if self._meta is None:
|
| 585 |
+
if self._file_header_ver < 3:
|
| 586 |
+
self._init_meta_data_pre_v3()
|
| 587 |
+
else:
|
| 588 |
+
self._init_meta_data_post_v3()
|
| 589 |
+
return self._meta
|
| 590 |
+
|
| 591 |
+
def _close(self):
|
| 592 |
+
# The file should be closed by `self.request`
|
| 593 |
+
pass
|
| 594 |
+
|
| 595 |
+
def _init_meta_data_pre_v3(self):
|
| 596 |
+
self._meta = self._parse_header(Spec.metadata)
|
| 597 |
+
|
| 598 |
+
nr = self._meta.pop("NumROI", None)
|
| 599 |
+
nr = 1 if nr < 1 else nr
|
| 600 |
+
self._meta["ROIs"] = roi_array_to_dict(self._meta["ROIs"][:nr])
|
| 601 |
+
|
| 602 |
+
# chip sizes
|
| 603 |
+
self._meta["chip_size"] = [
|
| 604 |
+
self._meta.pop("xDimDet", None),
|
| 605 |
+
self._meta.pop("yDimDet", None),
|
| 606 |
+
]
|
| 607 |
+
self._meta["virt_chip_size"] = [
|
| 608 |
+
self._meta.pop("VChipXdim", None),
|
| 609 |
+
self._meta.pop("VChipYdim", None),
|
| 610 |
+
]
|
| 611 |
+
self._meta["pre_pixels"] = [
|
| 612 |
+
self._meta.pop("XPrePixels", None),
|
| 613 |
+
self._meta.pop("YPrePixels", None),
|
| 614 |
+
]
|
| 615 |
+
self._meta["post_pixels"] = [
|
| 616 |
+
self._meta.pop("XPostPixels", None),
|
| 617 |
+
self._meta.pop("YPostPixels", None),
|
| 618 |
+
]
|
| 619 |
+
|
| 620 |
+
# comments
|
| 621 |
+
self._meta["comments"] = [str(c) for c in self._meta["comments"]]
|
| 622 |
+
|
| 623 |
+
# geometric operations
|
| 624 |
+
g = []
|
| 625 |
+
f = self._meta.pop("geometric", 0)
|
| 626 |
+
if f & 1:
|
| 627 |
+
g.append("rotate")
|
| 628 |
+
if f & 2:
|
| 629 |
+
g.append("reverse")
|
| 630 |
+
if f & 4:
|
| 631 |
+
g.append("flip")
|
| 632 |
+
self._meta["geometric"] = g
|
| 633 |
+
|
| 634 |
+
# Make some additional information more human-readable
|
| 635 |
+
t = self._meta["type"]
|
| 636 |
+
if 1 <= t <= len(Spec.controllers):
|
| 637 |
+
self._meta["type"] = Spec.controllers[t - 1]
|
| 638 |
+
else:
|
| 639 |
+
self._meta["type"] = ""
|
| 640 |
+
m = self._meta["readout_mode"]
|
| 641 |
+
if 1 <= m <= len(Spec.readout_modes):
|
| 642 |
+
self._meta["readout_mode"] = Spec.readout_modes[m - 1]
|
| 643 |
+
else:
|
| 644 |
+
self._meta["readout_mode"] = ""
|
| 645 |
+
|
| 646 |
+
# bools
|
| 647 |
+
for k in (
|
| 648 |
+
"absorb_live",
|
| 649 |
+
"can_do_virtual_chip",
|
| 650 |
+
"threshold_min_live",
|
| 651 |
+
"threshold_max_live",
|
| 652 |
+
):
|
| 653 |
+
self._meta[k] = bool(self._meta[k])
|
| 654 |
+
|
| 655 |
+
# frame shape
|
| 656 |
+
self._meta["frame_shape"] = self._shape
|
| 657 |
+
|
| 658 |
+
# Extract SDT-control metadata if desired
|
| 659 |
+
if self._sdt_meta:
|
| 660 |
+
SDTControlSpec.extract_metadata(self._meta, self._char_encoding)
|
| 661 |
+
|
| 662 |
+
def _parse_header(self, spec):
|
| 663 |
+
ret = {}
|
| 664 |
+
# Decode each string from the numpy array read by np.fromfile
|
| 665 |
+
decode = np.vectorize(lambda x: x.decode(self._char_encoding))
|
| 666 |
+
|
| 667 |
+
for name, sp in spec.items():
|
| 668 |
+
self._file.seek(sp[0])
|
| 669 |
+
cnt = 1 if len(sp) < 3 else sp[2]
|
| 670 |
+
v = np.fromfile(self._file, dtype=sp[1], count=cnt)
|
| 671 |
+
if v.dtype.kind == "S" and name not in Spec.no_decode:
|
| 672 |
+
# Silently ignore string decoding failures
|
| 673 |
+
try:
|
| 674 |
+
v = decode(v)
|
| 675 |
+
except Exception:
|
| 676 |
+
logger.warning(
|
| 677 |
+
'Failed to decode "{}" metadata '
|
| 678 |
+
"string. Check `char_encoding` "
|
| 679 |
+
"parameter.".format(name)
|
| 680 |
+
)
|
| 681 |
+
|
| 682 |
+
try:
|
| 683 |
+
# For convenience, if the array contains only one single
|
| 684 |
+
# entry, return this entry itself.
|
| 685 |
+
v = v.item()
|
| 686 |
+
except ValueError:
|
| 687 |
+
v = np.squeeze(v)
|
| 688 |
+
ret[name] = v
|
| 689 |
+
return ret
|
| 690 |
+
|
| 691 |
+
def _init_meta_data_post_v3(self):
|
| 692 |
+
info = self._parse_header(Spec.basic)
|
| 693 |
+
self._file.seek(info["xml_footer_offset"])
|
| 694 |
+
xml = self._file.read()
|
| 695 |
+
self._meta = {"__xml": xml}
|
| 696 |
+
|
| 697 |
+
def _get_length(self):
|
| 698 |
+
if self.request.mode[1] in "vV":
|
| 699 |
+
return 1
|
| 700 |
+
else:
|
| 701 |
+
return self._len
|
| 702 |
+
|
| 703 |
+
def _get_data(self, index):
|
| 704 |
+
if index < 0:
|
| 705 |
+
raise IndexError("Image index %i < 0" % index)
|
| 706 |
+
if index >= self._len:
|
| 707 |
+
raise IndexError("Image index %i > %i" % (index, self._len))
|
| 708 |
+
|
| 709 |
+
if self.request.mode[1] in "vV":
|
| 710 |
+
if index != 0:
|
| 711 |
+
raise IndexError("Index has to be 0 in v and V modes")
|
| 712 |
+
self._file.seek(Spec.data_start)
|
| 713 |
+
data = np.fromfile(
|
| 714 |
+
self._file,
|
| 715 |
+
dtype=self._dtype,
|
| 716 |
+
count=self._shape[0] * self._shape[1] * self._len,
|
| 717 |
+
)
|
| 718 |
+
data = data.reshape((self._len,) + self._shape)
|
| 719 |
+
else:
|
| 720 |
+
self._file.seek(
|
| 721 |
+
Spec.data_start
|
| 722 |
+
+ index * self._shape[0] * self._shape[1] * self._dtype.itemsize
|
| 723 |
+
)
|
| 724 |
+
data = np.fromfile(
|
| 725 |
+
self._file, dtype=self._dtype, count=self._shape[0] * self._shape[1]
|
| 726 |
+
)
|
| 727 |
+
data = data.reshape(self._shape)
|
| 728 |
+
return data, self._get_meta_data(index)
|
| 729 |
+
|
| 730 |
+
|
| 731 |
+
def roi_array_to_dict(a):
|
| 732 |
+
"""Convert the `ROIs` structured arrays to :py:class:`dict`
|
| 733 |
+
|
| 734 |
+
Parameters
|
| 735 |
+
----------
|
| 736 |
+
a : numpy.ndarray:
|
| 737 |
+
Structured array containing ROI data
|
| 738 |
+
|
| 739 |
+
Returns
|
| 740 |
+
-------
|
| 741 |
+
list of dict
|
| 742 |
+
One dict per ROI. Keys are "top_left", "bottom_right", and "bin",
|
| 743 |
+
values are tuples whose first element is the x axis value and the
|
| 744 |
+
second element is the y axis value.
|
| 745 |
+
"""
|
| 746 |
+
dict_list = []
|
| 747 |
+
a = a[["startx", "starty", "endx", "endy", "groupx", "groupy"]]
|
| 748 |
+
for sx, sy, ex, ey, gx, gy in a:
|
| 749 |
+
roi_dict = {
|
| 750 |
+
"top_left": [int(sx), int(sy)],
|
| 751 |
+
"bottom_right": [int(ex), int(ey)],
|
| 752 |
+
"bin": [int(gx), int(gy)],
|
| 753 |
+
}
|
| 754 |
+
dict_list.append(roi_dict)
|
| 755 |
+
return dict_list
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/swf.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
""" Read/Write SWF files.
|
| 5 |
+
|
| 6 |
+
Backend: internal
|
| 7 |
+
|
| 8 |
+
Shockwave flash (SWF) is a media format designed for rich and
|
| 9 |
+
interactive animations. This plugin makes use of this format to
|
| 10 |
+
store a series of images in a lossless format with good compression
|
| 11 |
+
(zlib). The resulting images can be shown as an animation using
|
| 12 |
+
a flash player (such as the browser).
|
| 13 |
+
|
| 14 |
+
SWF stores images in RGBA format. RGB or grayscale images are
|
| 15 |
+
automatically converted. SWF does not support meta data.
|
| 16 |
+
|
| 17 |
+
Parameters for reading
|
| 18 |
+
----------------------
|
| 19 |
+
loop : bool
|
| 20 |
+
If True, the video will rewind as soon as a frame is requested
|
| 21 |
+
beyond the last frame. Otherwise, IndexError is raised. Default False.
|
| 22 |
+
|
| 23 |
+
Parameters for saving
|
| 24 |
+
---------------------
|
| 25 |
+
fps : int
|
| 26 |
+
The speed to play the animation. Default 12.
|
| 27 |
+
loop : bool
|
| 28 |
+
If True, add a tag to the end of the file to play again from
|
| 29 |
+
the first frame. Most flash players will then play the movie
|
| 30 |
+
in a loop. Note that the imageio SWF Reader does not check this
|
| 31 |
+
tag. Default True.
|
| 32 |
+
html : bool
|
| 33 |
+
If the output is a file on the file system, write an html file
|
| 34 |
+
(in HTML5) that shows the animation. Default False.
|
| 35 |
+
compress : bool
|
| 36 |
+
Whether to compress the swf file. Default False. You probably don't
|
| 37 |
+
want to use this. This does not decrease the file size since
|
| 38 |
+
the images are already compressed. It will result in slower
|
| 39 |
+
read and write time. The only purpose of this feature is to
|
| 40 |
+
create compressed SWF files, so that we can test the
|
| 41 |
+
functionality to read them.
|
| 42 |
+
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
import os
|
| 46 |
+
import zlib
|
| 47 |
+
import logging
|
| 48 |
+
from io import BytesIO
|
| 49 |
+
|
| 50 |
+
import numpy as np
|
| 51 |
+
|
| 52 |
+
from ..core import Format, read_n_bytes, image_as_uint
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
logger = logging.getLogger(__name__)
|
| 56 |
+
|
| 57 |
+
_swf = None # lazily loaded in lib()
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def load_lib():
|
| 61 |
+
global _swf
|
| 62 |
+
from . import _swf
|
| 63 |
+
|
| 64 |
+
return _swf
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class SWFFormat(Format):
|
| 68 |
+
"""See :mod:`imageio.plugins.swf`"""
|
| 69 |
+
|
| 70 |
+
def _can_read(self, request):
|
| 71 |
+
if request.mode[1] in (self.modes + "?"):
|
| 72 |
+
tmp = request.firstbytes[0:3].decode("ascii", "ignore")
|
| 73 |
+
if tmp in ("FWS", "CWS"):
|
| 74 |
+
return True
|
| 75 |
+
|
| 76 |
+
def _can_write(self, request):
|
| 77 |
+
if request.mode[1] in (self.modes + "?"):
|
| 78 |
+
if request.extension in self.extensions:
|
| 79 |
+
return True
|
| 80 |
+
|
| 81 |
+
# -- reader
|
| 82 |
+
|
| 83 |
+
class Reader(Format.Reader):
|
| 84 |
+
def _open(self, loop=False):
|
| 85 |
+
if not _swf:
|
| 86 |
+
load_lib()
|
| 87 |
+
|
| 88 |
+
self._arg_loop = bool(loop)
|
| 89 |
+
|
| 90 |
+
self._fp = self.request.get_file()
|
| 91 |
+
|
| 92 |
+
# Check file ...
|
| 93 |
+
tmp = self.request.firstbytes[0:3].decode("ascii", "ignore")
|
| 94 |
+
if tmp == "FWS":
|
| 95 |
+
pass # OK
|
| 96 |
+
elif tmp == "CWS":
|
| 97 |
+
# Compressed, we need to decompress
|
| 98 |
+
bb = self._fp.read()
|
| 99 |
+
bb = bb[:8] + zlib.decompress(bb[8:])
|
| 100 |
+
# Wrap up in a file object
|
| 101 |
+
self._fp = BytesIO(bb)
|
| 102 |
+
else:
|
| 103 |
+
raise IOError("This does not look like a valid SWF file")
|
| 104 |
+
|
| 105 |
+
# Skip first bytes. This also tests support got seeking ...
|
| 106 |
+
try:
|
| 107 |
+
self._fp.seek(8)
|
| 108 |
+
self._streaming_mode = False
|
| 109 |
+
except Exception:
|
| 110 |
+
self._streaming_mode = True
|
| 111 |
+
self._fp_read(8)
|
| 112 |
+
|
| 113 |
+
# Skip header
|
| 114 |
+
# Note that the number of frames is there, which we could
|
| 115 |
+
# potentially use, but the number of frames does not necessarily
|
| 116 |
+
# correspond to the number of images.
|
| 117 |
+
nbits = _swf.bits2int(self._fp_read(1), 5)
|
| 118 |
+
nbits = 5 + nbits * 4
|
| 119 |
+
Lrect = nbits / 8.0
|
| 120 |
+
if Lrect % 1:
|
| 121 |
+
Lrect += 1
|
| 122 |
+
Lrect = int(Lrect)
|
| 123 |
+
self._fp_read(Lrect + 3)
|
| 124 |
+
|
| 125 |
+
# Now the rest is basically tags ...
|
| 126 |
+
self._imlocs = [] # tuple (loc, sze, T, L1)
|
| 127 |
+
if not self._streaming_mode:
|
| 128 |
+
# Collect locations of frame, while skipping through the data
|
| 129 |
+
# This does not read any of the tag *data*.
|
| 130 |
+
try:
|
| 131 |
+
while True:
|
| 132 |
+
isimage, sze, T, L1 = self._read_one_tag()
|
| 133 |
+
loc = self._fp.tell()
|
| 134 |
+
if isimage:
|
| 135 |
+
# Still need to check if the format is right
|
| 136 |
+
format = ord(self._fp_read(3)[2:])
|
| 137 |
+
if format == 5: # RGB or RGBA lossless
|
| 138 |
+
self._imlocs.append((loc, sze, T, L1))
|
| 139 |
+
self._fp.seek(loc + sze) # Skip over tag
|
| 140 |
+
except IndexError:
|
| 141 |
+
pass # done reading
|
| 142 |
+
|
| 143 |
+
def _fp_read(self, n):
|
| 144 |
+
return read_n_bytes(self._fp, n)
|
| 145 |
+
|
| 146 |
+
def _close(self):
|
| 147 |
+
pass
|
| 148 |
+
|
| 149 |
+
def _get_length(self):
|
| 150 |
+
if self._streaming_mode:
|
| 151 |
+
return np.inf
|
| 152 |
+
else:
|
| 153 |
+
return len(self._imlocs)
|
| 154 |
+
|
| 155 |
+
def _get_data(self, index):
|
| 156 |
+
# Check index
|
| 157 |
+
if index < 0:
|
| 158 |
+
raise IndexError("Index in swf file must be > 0")
|
| 159 |
+
if not self._streaming_mode:
|
| 160 |
+
if self._arg_loop and self._imlocs:
|
| 161 |
+
index = index % len(self._imlocs)
|
| 162 |
+
if index >= len(self._imlocs):
|
| 163 |
+
raise IndexError("Index out of bounds")
|
| 164 |
+
|
| 165 |
+
if self._streaming_mode:
|
| 166 |
+
# Walk over tags until we find an image
|
| 167 |
+
while True:
|
| 168 |
+
isimage, sze, T, L1 = self._read_one_tag()
|
| 169 |
+
bb = self._fp_read(sze) # always read data
|
| 170 |
+
if isimage:
|
| 171 |
+
im = _swf.read_pixels(bb, 0, T, L1) # can be None
|
| 172 |
+
if im is not None:
|
| 173 |
+
return im, {}
|
| 174 |
+
|
| 175 |
+
else:
|
| 176 |
+
# Go to corresponding location, read data, and convert to image
|
| 177 |
+
loc, sze, T, L1 = self._imlocs[index]
|
| 178 |
+
self._fp.seek(loc)
|
| 179 |
+
bb = self._fp_read(sze)
|
| 180 |
+
# Read_pixels should return ndarry, since we checked format
|
| 181 |
+
im = _swf.read_pixels(bb, 0, T, L1)
|
| 182 |
+
return im, {}
|
| 183 |
+
|
| 184 |
+
def _read_one_tag(self):
|
| 185 |
+
"""
|
| 186 |
+
Return (True, loc, size, T, L1) if an image that we can read.
|
| 187 |
+
Return (False, loc, size, T, L1) if any other tag.
|
| 188 |
+
"""
|
| 189 |
+
|
| 190 |
+
# Get head
|
| 191 |
+
head = self._fp_read(6)
|
| 192 |
+
if not head: # pragma: no cover
|
| 193 |
+
raise IndexError("Reached end of swf movie")
|
| 194 |
+
|
| 195 |
+
# Determine type and length
|
| 196 |
+
T, L1, L2 = _swf.get_type_and_len(head)
|
| 197 |
+
if not L2: # pragma: no cover
|
| 198 |
+
raise RuntimeError("Invalid tag length, could not proceed")
|
| 199 |
+
|
| 200 |
+
# Read data
|
| 201 |
+
isimage = False
|
| 202 |
+
sze = L2 - 6
|
| 203 |
+
# bb = self._fp_read(L2 - 6)
|
| 204 |
+
|
| 205 |
+
# Parse tag
|
| 206 |
+
if T == 0:
|
| 207 |
+
raise IndexError("Reached end of swf movie")
|
| 208 |
+
elif T in [20, 36]:
|
| 209 |
+
isimage = True
|
| 210 |
+
# im = _swf.read_pixels(bb, 0, T, L1) # can be None
|
| 211 |
+
elif T in [6, 21, 35, 90]: # pragma: no cover
|
| 212 |
+
logger.warning("Ignoring JPEG image: cannot read JPEG.")
|
| 213 |
+
else:
|
| 214 |
+
pass # Not an image tag
|
| 215 |
+
|
| 216 |
+
# Done. Return image. Can be None
|
| 217 |
+
# return im
|
| 218 |
+
return isimage, sze, T, L1
|
| 219 |
+
|
| 220 |
+
def _get_meta_data(self, index):
|
| 221 |
+
return {} # This format does not support meta data
|
| 222 |
+
|
| 223 |
+
# -- writer
|
| 224 |
+
|
| 225 |
+
class Writer(Format.Writer):
|
| 226 |
+
def _open(self, fps=12, loop=True, html=False, compress=False):
|
| 227 |
+
if not _swf:
|
| 228 |
+
load_lib()
|
| 229 |
+
|
| 230 |
+
self._arg_fps = int(fps)
|
| 231 |
+
self._arg_loop = bool(loop)
|
| 232 |
+
self._arg_html = bool(html)
|
| 233 |
+
self._arg_compress = bool(compress)
|
| 234 |
+
|
| 235 |
+
self._fp = self.request.get_file()
|
| 236 |
+
self._framecounter = 0
|
| 237 |
+
self._framesize = (100, 100)
|
| 238 |
+
|
| 239 |
+
# For compress, we use an in-memory file object
|
| 240 |
+
if self._arg_compress:
|
| 241 |
+
self._fp_real = self._fp
|
| 242 |
+
self._fp = BytesIO()
|
| 243 |
+
|
| 244 |
+
def _close(self):
|
| 245 |
+
self._complete()
|
| 246 |
+
# Get size of (uncompressed) file
|
| 247 |
+
sze = self._fp.tell()
|
| 248 |
+
# set nframes, this is in the potentially compressed region
|
| 249 |
+
self._fp.seek(self._location_to_save_nframes)
|
| 250 |
+
self._fp.write(_swf.int2uint16(self._framecounter))
|
| 251 |
+
# Compress body?
|
| 252 |
+
if self._arg_compress:
|
| 253 |
+
bb = self._fp.getvalue()
|
| 254 |
+
self._fp = self._fp_real
|
| 255 |
+
self._fp.write(bb[:8])
|
| 256 |
+
self._fp.write(zlib.compress(bb[8:]))
|
| 257 |
+
sze = self._fp.tell() # renew sze value
|
| 258 |
+
# set size
|
| 259 |
+
self._fp.seek(4)
|
| 260 |
+
self._fp.write(_swf.int2uint32(sze))
|
| 261 |
+
self._fp = None # Disable
|
| 262 |
+
|
| 263 |
+
# Write html?
|
| 264 |
+
if self._arg_html and os.path.isfile(self.request.filename):
|
| 265 |
+
dirname, fname = os.path.split(self.request.filename)
|
| 266 |
+
filename = os.path.join(dirname, fname[:-4] + ".html")
|
| 267 |
+
w, h = self._framesize
|
| 268 |
+
html = HTML % (fname, w, h, fname)
|
| 269 |
+
with open(filename, "wb") as f:
|
| 270 |
+
f.write(html.encode("utf-8"))
|
| 271 |
+
|
| 272 |
+
def _write_header(self, framesize, fps):
|
| 273 |
+
self._framesize = framesize
|
| 274 |
+
# Called as soon as we know framesize; when we get first frame
|
| 275 |
+
bb = b""
|
| 276 |
+
bb += "FC"[self._arg_compress].encode("ascii")
|
| 277 |
+
bb += "WS".encode("ascii") # signature bytes
|
| 278 |
+
bb += _swf.int2uint8(8) # version
|
| 279 |
+
bb += "0000".encode("ascii") # FileLength (leave open for now)
|
| 280 |
+
bb += (
|
| 281 |
+
_swf.Tag().make_rect_record(0, framesize[0], 0, framesize[1]).tobytes()
|
| 282 |
+
)
|
| 283 |
+
bb += _swf.int2uint8(0) + _swf.int2uint8(fps) # FrameRate
|
| 284 |
+
self._location_to_save_nframes = len(bb)
|
| 285 |
+
bb += "00".encode("ascii") # nframes (leave open for now)
|
| 286 |
+
self._fp.write(bb)
|
| 287 |
+
|
| 288 |
+
# Write some initial tags
|
| 289 |
+
taglist = _swf.FileAttributesTag(), _swf.SetBackgroundTag(0, 0, 0)
|
| 290 |
+
for tag in taglist:
|
| 291 |
+
self._fp.write(tag.get_tag())
|
| 292 |
+
|
| 293 |
+
def _complete(self):
|
| 294 |
+
# What if no images were saved?
|
| 295 |
+
if not self._framecounter:
|
| 296 |
+
self._write_header((10, 10), self._arg_fps)
|
| 297 |
+
# Write stop tag if we do not loop
|
| 298 |
+
if not self._arg_loop:
|
| 299 |
+
self._fp.write(_swf.DoActionTag("stop").get_tag())
|
| 300 |
+
# finish with end tag
|
| 301 |
+
self._fp.write("\x00\x00".encode("ascii"))
|
| 302 |
+
|
| 303 |
+
def _append_data(self, im, meta):
|
| 304 |
+
# Correct shape and type
|
| 305 |
+
if im.ndim == 3 and im.shape[-1] == 1:
|
| 306 |
+
im = im[:, :, 0]
|
| 307 |
+
im = image_as_uint(im, bitdepth=8)
|
| 308 |
+
# Get frame size
|
| 309 |
+
wh = im.shape[1], im.shape[0]
|
| 310 |
+
# Write header on first frame
|
| 311 |
+
isfirstframe = False
|
| 312 |
+
if self._framecounter == 0:
|
| 313 |
+
isfirstframe = True
|
| 314 |
+
self._write_header(wh, self._arg_fps)
|
| 315 |
+
# Create tags
|
| 316 |
+
bm = _swf.BitmapTag(im)
|
| 317 |
+
sh = _swf.ShapeTag(bm.id, (0, 0), wh)
|
| 318 |
+
po = _swf.PlaceObjectTag(1, sh.id, move=(not isfirstframe))
|
| 319 |
+
sf = _swf.ShowFrameTag()
|
| 320 |
+
# Write tags
|
| 321 |
+
for tag in [bm, sh, po, sf]:
|
| 322 |
+
self._fp.write(tag.get_tag())
|
| 323 |
+
self._framecounter += 1
|
| 324 |
+
|
| 325 |
+
def set_meta_data(self, meta):
|
| 326 |
+
pass
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
HTML = """
|
| 330 |
+
<!DOCTYPE html>
|
| 331 |
+
<html>
|
| 332 |
+
<head>
|
| 333 |
+
<title>Show Flash animation %s</title>
|
| 334 |
+
</head>
|
| 335 |
+
<body>
|
| 336 |
+
<embed width="%i" height="%i" src="%s">
|
| 337 |
+
</html>
|
| 338 |
+
"""
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/imageio/plugins/tifffile.py
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
# imageio is distributed under the terms of the (new) BSD License.
|
| 3 |
+
|
| 4 |
+
""" Read/Write TIFF files.
|
| 5 |
+
|
| 6 |
+
Backend: internal
|
| 7 |
+
|
| 8 |
+
Provides support for a wide range of Tiff images using the tifffile
|
| 9 |
+
backend.
|
| 10 |
+
|
| 11 |
+
Parameters for reading
|
| 12 |
+
----------------------
|
| 13 |
+
offset : int
|
| 14 |
+
Optional start position of embedded file. By default this is
|
| 15 |
+
the current file position.
|
| 16 |
+
size : int
|
| 17 |
+
Optional size of embedded file. By default this is the number
|
| 18 |
+
of bytes from the 'offset' to the end of the file.
|
| 19 |
+
multifile : bool
|
| 20 |
+
If True (default), series may include pages from multiple files.
|
| 21 |
+
Currently applies to OME-TIFF only.
|
| 22 |
+
multifile_close : bool
|
| 23 |
+
If True (default), keep the handles of other files in multifile
|
| 24 |
+
series closed. This is inefficient when few files refer to
|
| 25 |
+
many pages. If False, the C runtime may run out of resources.
|
| 26 |
+
|
| 27 |
+
Parameters for saving
|
| 28 |
+
---------------------
|
| 29 |
+
bigtiff : bool
|
| 30 |
+
If True, the BigTIFF format is used.
|
| 31 |
+
byteorder : {'<', '>'}
|
| 32 |
+
The endianness of the data in the file.
|
| 33 |
+
By default this is the system's native byte order.
|
| 34 |
+
software : str
|
| 35 |
+
Name of the software used to create the image.
|
| 36 |
+
Saved with the first page only.
|
| 37 |
+
|
| 38 |
+
Metadata for reading
|
| 39 |
+
--------------------
|
| 40 |
+
planar_configuration : {'contig', 'planar'}
|
| 41 |
+
Specifies if samples are stored contiguous or in separate planes.
|
| 42 |
+
By default this setting is inferred from the data shape.
|
| 43 |
+
'contig': last dimension contains samples.
|
| 44 |
+
'planar': third last dimension contains samples.
|
| 45 |
+
resolution_unit : int
|
| 46 |
+
The resolution unit stored in the TIFF tag. Usually 1 means no/unknown unit,
|
| 47 |
+
2 means dpi (inch), 3 means dpc (centimeter).
|
| 48 |
+
resolution : (float, float, str)
|
| 49 |
+
A tuple formatted as (X_resolution, Y_resolution, unit). The unit is a
|
| 50 |
+
string representing one of the following units::
|
| 51 |
+
|
| 52 |
+
NONE # No unit or unit unknown
|
| 53 |
+
INCH # dpi
|
| 54 |
+
CENTIMETER # cpi
|
| 55 |
+
MILLIMETER
|
| 56 |
+
MICROMETER
|
| 57 |
+
|
| 58 |
+
compression : int
|
| 59 |
+
Value indicating the compression algorithm used, e.g. 5 is LZW,
|
| 60 |
+
7 is JPEG, 8 is deflate.
|
| 61 |
+
If 1, data are uncompressed.
|
| 62 |
+
predictor : int
|
| 63 |
+
Value 2 indicates horizontal differencing was used before compression,
|
| 64 |
+
while 3 indicates floating point horizontal differencing.
|
| 65 |
+
If 1, no prediction scheme was used before compression.
|
| 66 |
+
orientation : {'top_left', 'bottom_right', ...}
|
| 67 |
+
Oriented of image array.
|
| 68 |
+
is_rgb : bool
|
| 69 |
+
True if page contains a RGB image.
|
| 70 |
+
is_contig : bool
|
| 71 |
+
True if page contains a contiguous image.
|
| 72 |
+
is_tiled : bool
|
| 73 |
+
True if page contains tiled image.
|
| 74 |
+
is_palette : bool
|
| 75 |
+
True if page contains a palette-colored image and not OME or STK.
|
| 76 |
+
is_reduced : bool
|
| 77 |
+
True if page is a reduced image of another image.
|
| 78 |
+
is_shaped : bool
|
| 79 |
+
True if page contains shape in image_description tag.
|
| 80 |
+
is_fluoview : bool
|
| 81 |
+
True if page contains FluoView MM_STAMP tag.
|
| 82 |
+
is_nih : bool
|
| 83 |
+
True if page contains NIH image header.
|
| 84 |
+
is_micromanager : bool
|
| 85 |
+
True if page contains Micro-Manager metadata.
|
| 86 |
+
is_ome : bool
|
| 87 |
+
True if page contains OME-XML in image_description tag.
|
| 88 |
+
is_sgi : bool
|
| 89 |
+
True if page contains SGI image and tile depth tags.
|
| 90 |
+
is_mdgel : bool
|
| 91 |
+
True if page contains md_file_tag tag.
|
| 92 |
+
is_mediacy : bool
|
| 93 |
+
True if page contains Media Cybernetics Id tag.
|
| 94 |
+
is_stk : bool
|
| 95 |
+
True if page contains UIC2Tag tag.
|
| 96 |
+
is_lsm : bool
|
| 97 |
+
True if page contains LSM CZ_LSM_INFO tag.
|
| 98 |
+
description : str
|
| 99 |
+
Image description
|
| 100 |
+
description1 : str
|
| 101 |
+
Additional description
|
| 102 |
+
is_imagej : None or str
|
| 103 |
+
ImageJ metadata
|
| 104 |
+
software : str
|
| 105 |
+
Software used to create the TIFF file
|
| 106 |
+
datetime : datetime.datetime
|
| 107 |
+
Creation date and time
|
| 108 |
+
|
| 109 |
+
Metadata for writing
|
| 110 |
+
--------------------
|
| 111 |
+
photometric : {'minisblack', 'miniswhite', 'rgb'}
|
| 112 |
+
The color space of the image data.
|
| 113 |
+
By default this setting is inferred from the data shape.
|
| 114 |
+
planarconfig : {'contig', 'planar'}
|
| 115 |
+
Specifies if samples are stored contiguous or in separate planes.
|
| 116 |
+
By default this setting is inferred from the data shape.
|
| 117 |
+
'contig': last dimension contains samples.
|
| 118 |
+
'planar': third last dimension contains samples.
|
| 119 |
+
resolution : (float, float) or ((int, int), (int, int))
|
| 120 |
+
X and Y resolution in dots per inch as float or rational numbers.
|
| 121 |
+
description : str
|
| 122 |
+
The subject of the image. Saved with the first page only.
|
| 123 |
+
compress : int
|
| 124 |
+
Values from 0 to 9 controlling the level of zlib (deflate) compression.
|
| 125 |
+
If 0, data are written uncompressed (default).
|
| 126 |
+
predictor : bool
|
| 127 |
+
If True, horizontal differencing is applied before compression.
|
| 128 |
+
Note that using an int literal 1 actually means no prediction scheme
|
| 129 |
+
will be used.
|
| 130 |
+
volume : bool
|
| 131 |
+
If True, volume data are stored in one tile (if applicable) using
|
| 132 |
+
the SGI image_depth and tile_depth tags.
|
| 133 |
+
Image width and depth must be multiple of 16.
|
| 134 |
+
Few software can read this format, e.g. MeVisLab.
|
| 135 |
+
writeshape : bool
|
| 136 |
+
If True, write the data shape to the image_description tag
|
| 137 |
+
if necessary and no other description is given.
|
| 138 |
+
extratags: sequence of tuples
|
| 139 |
+
Additional tags as [(code, dtype, count, value, writeonce)].
|
| 140 |
+
|
| 141 |
+
code : int
|
| 142 |
+
The TIFF tag Id.
|
| 143 |
+
dtype : str
|
| 144 |
+
Data type of items in 'value' in Python struct format.
|
| 145 |
+
One of B, s, H, I, 2I, b, h, i, f, d, Q, or q.
|
| 146 |
+
count : int
|
| 147 |
+
Number of data values. Not used for string values.
|
| 148 |
+
value : sequence
|
| 149 |
+
'Count' values compatible with 'dtype'.
|
| 150 |
+
writeonce : bool
|
| 151 |
+
If True, the tag is written to the first page only.
|
| 152 |
+
|
| 153 |
+
Notes
|
| 154 |
+
-----
|
| 155 |
+
Global metadata is stored with the first frame in a TIFF file.
|
| 156 |
+
Thus calling :py:meth:`Format.Writer.set_meta_data` after the first frame
|
| 157 |
+
was written has no effect. Also, global metadata is ignored if metadata is
|
| 158 |
+
provided via the `meta` argument of :py:meth:`Format.Writer.append_data`.
|
| 159 |
+
|
| 160 |
+
If you have installed tifffile as a Python package, imageio will attempt
|
| 161 |
+
to use that as backend instead of the bundled backend. Doing so can
|
| 162 |
+
provide access to new performance improvements and bug fixes.
|
| 163 |
+
|
| 164 |
+
"""
|
| 165 |
+
|
| 166 |
+
import datetime
|
| 167 |
+
|
| 168 |
+
from ..core import Format
|
| 169 |
+
from ..core.request import URI_BYTES, URI_FILE
|
| 170 |
+
|
| 171 |
+
import numpy as np
|
| 172 |
+
import warnings
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
try:
|
| 176 |
+
import tifffile as _tifffile
|
| 177 |
+
except ImportError:
|
| 178 |
+
warnings.warn(
|
| 179 |
+
"ImageIO's vendored tifffile backend is deprecated and will be"
|
| 180 |
+
" removed in ImageIO v3. Install the tifffile directly:"
|
| 181 |
+
" `pip install imageio[tifffile]`",
|
| 182 |
+
DeprecationWarning,
|
| 183 |
+
)
|
| 184 |
+
from . import _tifffile
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
TIFF_FORMATS = (".tif", ".tiff", ".stk", ".lsm")
|
| 188 |
+
WRITE_METADATA_KEYS = (
|
| 189 |
+
"photometric",
|
| 190 |
+
"planarconfig",
|
| 191 |
+
"resolution",
|
| 192 |
+
"description",
|
| 193 |
+
"compress",
|
| 194 |
+
"predictor",
|
| 195 |
+
"volume",
|
| 196 |
+
"writeshape",
|
| 197 |
+
"extratags",
|
| 198 |
+
"datetime",
|
| 199 |
+
)
|
| 200 |
+
READ_METADATA_KEYS = (
|
| 201 |
+
"planar_configuration",
|
| 202 |
+
"is_fluoview",
|
| 203 |
+
"is_nih",
|
| 204 |
+
"is_contig",
|
| 205 |
+
"is_micromanager",
|
| 206 |
+
"is_ome",
|
| 207 |
+
"is_lsm",
|
| 208 |
+
"is_palette",
|
| 209 |
+
"is_reduced",
|
| 210 |
+
"is_rgb",
|
| 211 |
+
"is_sgi",
|
| 212 |
+
"is_shaped",
|
| 213 |
+
"is_stk",
|
| 214 |
+
"is_tiled",
|
| 215 |
+
"is_mdgel",
|
| 216 |
+
"resolution_unit",
|
| 217 |
+
"compression",
|
| 218 |
+
"predictor",
|
| 219 |
+
"is_mediacy",
|
| 220 |
+
"orientation",
|
| 221 |
+
"description",
|
| 222 |
+
"description1",
|
| 223 |
+
"is_imagej",
|
| 224 |
+
"software",
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
class TiffFormat(Format):
|
| 229 |
+
"""Provides support for a wide range of Tiff images using the tifffile
|
| 230 |
+
backend.
|
| 231 |
+
|
| 232 |
+
Images that contain multiple pages can be read using ``imageio.mimread()``
|
| 233 |
+
to read the individual pages, or ``imageio.volread()`` to obtain a
|
| 234 |
+
single (higher dimensional) array.
|
| 235 |
+
|
| 236 |
+
Note that global metadata is stored with the first frame in a TIFF file.
|
| 237 |
+
Thus calling :py:meth:`Format.Writer.set_meta_data` after the first frame
|
| 238 |
+
was written has no effect. Also, global metadata is ignored if metadata is
|
| 239 |
+
provided via the `meta` argument of :py:meth:`Format.Writer.append_data`.
|
| 240 |
+
|
| 241 |
+
If you have installed tifffile as a Python package, imageio will attempt
|
| 242 |
+
to use that as backend instead of the bundled backend. Doing so can
|
| 243 |
+
provide access to new performance improvements and bug fixes.
|
| 244 |
+
|
| 245 |
+
Parameters for reading
|
| 246 |
+
----------------------
|
| 247 |
+
offset : int
|
| 248 |
+
Optional start position of embedded file. By default this is
|
| 249 |
+
the current file position.
|
| 250 |
+
size : int
|
| 251 |
+
Optional size of embedded file. By default this is the number
|
| 252 |
+
of bytes from the 'offset' to the end of the file.
|
| 253 |
+
multifile : bool
|
| 254 |
+
If True (default), series may include pages from multiple files.
|
| 255 |
+
Currently applies to OME-TIFF only.
|
| 256 |
+
multifile_close : bool
|
| 257 |
+
If True (default), keep the handles of other files in multifile
|
| 258 |
+
series closed. This is inefficient when few files refer to
|
| 259 |
+
many pages. If False, the C runtime may run out of resources.
|
| 260 |
+
|
| 261 |
+
Parameters for saving
|
| 262 |
+
---------------------
|
| 263 |
+
bigtiff : bool
|
| 264 |
+
If True, the BigTIFF format is used.
|
| 265 |
+
byteorder : {'<', '>'}
|
| 266 |
+
The endianness of the data in the file.
|
| 267 |
+
By default this is the system's native byte order.
|
| 268 |
+
software : str
|
| 269 |
+
Name of the software used to create the image.
|
| 270 |
+
Saved with the first page only.
|
| 271 |
+
|
| 272 |
+
Metadata for reading
|
| 273 |
+
--------------------
|
| 274 |
+
planar_configuration : {'contig', 'planar'}
|
| 275 |
+
Specifies if samples are stored contiguous or in separate planes.
|
| 276 |
+
By default this setting is inferred from the data shape.
|
| 277 |
+
'contig': last dimension contains samples.
|
| 278 |
+
'planar': third last dimension contains samples.
|
| 279 |
+
resolution_unit : (float, float) or ((int, int), (int, int))
|
| 280 |
+
X and Y resolution in dots per inch as float or rational numbers.
|
| 281 |
+
compression : int
|
| 282 |
+
Value indicating the compression algorithm used, e.g. 5 is LZW,
|
| 283 |
+
7 is JPEG, 8 is deflate.
|
| 284 |
+
If 1, data are uncompressed.
|
| 285 |
+
predictor : int
|
| 286 |
+
Value 2 indicates horizontal differencing was used before compression,
|
| 287 |
+
while 3 indicates floating point horizontal differencing.
|
| 288 |
+
If 1, no prediction scheme was used before compression.
|
| 289 |
+
orientation : {'top_left', 'bottom_right', ...}
|
| 290 |
+
Oriented of image array.
|
| 291 |
+
is_rgb : bool
|
| 292 |
+
True if page contains a RGB image.
|
| 293 |
+
is_contig : bool
|
| 294 |
+
True if page contains a contiguous image.
|
| 295 |
+
is_tiled : bool
|
| 296 |
+
True if page contains tiled image.
|
| 297 |
+
is_palette : bool
|
| 298 |
+
True if page contains a palette-colored image and not OME or STK.
|
| 299 |
+
is_reduced : bool
|
| 300 |
+
True if page is a reduced image of another image.
|
| 301 |
+
is_shaped : bool
|
| 302 |
+
True if page contains shape in image_description tag.
|
| 303 |
+
is_fluoview : bool
|
| 304 |
+
True if page contains FluoView MM_STAMP tag.
|
| 305 |
+
is_nih : bool
|
| 306 |
+
True if page contains NIH image header.
|
| 307 |
+
is_micromanager : bool
|
| 308 |
+
True if page contains Micro-Manager metadata.
|
| 309 |
+
is_ome : bool
|
| 310 |
+
True if page contains OME-XML in image_description tag.
|
| 311 |
+
is_sgi : bool
|
| 312 |
+
True if page contains SGI image and tile depth tags.
|
| 313 |
+
is_stk : bool
|
| 314 |
+
True if page contains UIC2Tag tag.
|
| 315 |
+
is_mdgel : bool
|
| 316 |
+
True if page contains md_file_tag tag.
|
| 317 |
+
is_mediacy : bool
|
| 318 |
+
True if page contains Media Cybernetics Id tag.
|
| 319 |
+
is_stk : bool
|
| 320 |
+
True if page contains UIC2Tag tag.
|
| 321 |
+
is_lsm : bool
|
| 322 |
+
True if page contains LSM CZ_LSM_INFO tag.
|
| 323 |
+
description : str
|
| 324 |
+
Image description
|
| 325 |
+
description1 : str
|
| 326 |
+
Additional description
|
| 327 |
+
is_imagej : None or str
|
| 328 |
+
ImageJ metadata
|
| 329 |
+
software : str
|
| 330 |
+
Software used to create the TIFF file
|
| 331 |
+
datetime : datetime.datetime
|
| 332 |
+
Creation date and time
|
| 333 |
+
|
| 334 |
+
Metadata for writing
|
| 335 |
+
--------------------
|
| 336 |
+
photometric : {'minisblack', 'miniswhite', 'rgb'}
|
| 337 |
+
The color space of the image data.
|
| 338 |
+
By default this setting is inferred from the data shape.
|
| 339 |
+
planarconfig : {'contig', 'planar'}
|
| 340 |
+
Specifies if samples are stored contiguous or in separate planes.
|
| 341 |
+
By default this setting is inferred from the data shape.
|
| 342 |
+
'contig': last dimension contains samples.
|
| 343 |
+
'planar': third last dimension contains samples.
|
| 344 |
+
resolution : (float, float) or ((int, int), (int, int))
|
| 345 |
+
X and Y resolution in dots per inch as float or rational numbers.
|
| 346 |
+
description : str
|
| 347 |
+
The subject of the image. Saved with the first page only.
|
| 348 |
+
compress : int
|
| 349 |
+
Values from 0 to 9 controlling the level of zlib (deflate) compression.
|
| 350 |
+
If 0, data are written uncompressed (default).
|
| 351 |
+
predictor : bool
|
| 352 |
+
If True, horizontal differencing is applied before compression.
|
| 353 |
+
Note that using an int literal 1 actually means no prediction scheme
|
| 354 |
+
will be used.
|
| 355 |
+
volume : bool
|
| 356 |
+
If True, volume data are stored in one tile (if applicable) using
|
| 357 |
+
the SGI image_depth and tile_depth tags.
|
| 358 |
+
Image width and depth must be multiple of 16.
|
| 359 |
+
Few software can read this format, e.g. MeVisLab.
|
| 360 |
+
writeshape : bool
|
| 361 |
+
If True, write the data shape to the image_description tag
|
| 362 |
+
if necessary and no other description is given.
|
| 363 |
+
extratags: sequence of tuples
|
| 364 |
+
Additional tags as [(code, dtype, count, value, writeonce)].
|
| 365 |
+
|
| 366 |
+
code : int
|
| 367 |
+
The TIFF tag Id.
|
| 368 |
+
dtype : str
|
| 369 |
+
Data type of items in 'value' in Python struct format.
|
| 370 |
+
One of B, s, H, I, 2I, b, h, i, f, d, Q, or q.
|
| 371 |
+
count : int
|
| 372 |
+
Number of data values. Not used for string values.
|
| 373 |
+
value : sequence
|
| 374 |
+
'Count' values compatible with 'dtype'.
|
| 375 |
+
writeonce : bool
|
| 376 |
+
If True, the tag is written to the first page only.
|
| 377 |
+
"""
|
| 378 |
+
|
| 379 |
+
def _can_read(self, request):
|
| 380 |
+
try:
|
| 381 |
+
_tifffile.TiffFile(request.get_file(), **request.kwargs)
|
| 382 |
+
except ValueError:
|
| 383 |
+
# vendored backend raises value exception
|
| 384 |
+
return False
|
| 385 |
+
except _tifffile.TiffFileError: # pragma: no-cover
|
| 386 |
+
# current version raises custom exception
|
| 387 |
+
return False
|
| 388 |
+
finally:
|
| 389 |
+
request.get_file().seek(0)
|
| 390 |
+
|
| 391 |
+
return True
|
| 392 |
+
|
| 393 |
+
def _can_write(self, request):
|
| 394 |
+
if request._uri_type in [URI_FILE, URI_BYTES]:
|
| 395 |
+
pass # special URI
|
| 396 |
+
elif request.extension not in self.extensions:
|
| 397 |
+
return False
|
| 398 |
+
|
| 399 |
+
try:
|
| 400 |
+
_tifffile.TiffWriter(request.get_file(), **request.kwargs)
|
| 401 |
+
except ValueError:
|
| 402 |
+
# vendored backend raises value exception
|
| 403 |
+
return False
|
| 404 |
+
except _tifffile.TiffFileError: # pragma: no-cover
|
| 405 |
+
# current version raises custom exception
|
| 406 |
+
return False
|
| 407 |
+
finally:
|
| 408 |
+
request.get_file().seek(0)
|
| 409 |
+
return True
|
| 410 |
+
|
| 411 |
+
# -- reader
|
| 412 |
+
|
| 413 |
+
class Reader(Format.Reader):
|
| 414 |
+
def _open(self, **kwargs):
|
| 415 |
+
# Allow loading from http; tifffile uses seek, so download first
|
| 416 |
+
if self.request.filename.startswith(("http://", "https://")):
|
| 417 |
+
self._f = f = open(self.request.get_local_filename(), "rb")
|
| 418 |
+
else:
|
| 419 |
+
self._f = None
|
| 420 |
+
f = self.request.get_file()
|
| 421 |
+
self._tf = _tifffile.TiffFile(f, **kwargs)
|
| 422 |
+
|
| 423 |
+
def _close(self):
|
| 424 |
+
self._tf.close()
|
| 425 |
+
if self._f is not None:
|
| 426 |
+
self._f.close()
|
| 427 |
+
|
| 428 |
+
def _get_length(self):
|
| 429 |
+
if self.request.mode[1] in "vV?":
|
| 430 |
+
return len(self._tf.series)
|
| 431 |
+
else:
|
| 432 |
+
# for backwards compatibility
|
| 433 |
+
# imread/mimread reads all pages individually
|
| 434 |
+
return len(self._tf.pages)
|
| 435 |
+
|
| 436 |
+
def _get_data(self, index):
|
| 437 |
+
if index < 0 or index >= self._get_length():
|
| 438 |
+
raise IndexError("Index out of range while reading from tiff file")
|
| 439 |
+
|
| 440 |
+
if self.request.mode[1] in "vV?":
|
| 441 |
+
# this might regress #559 / #558
|
| 442 |
+
# but it is hard to test without a test image
|
| 443 |
+
im = self._tf.asarray(series=index)
|
| 444 |
+
else:
|
| 445 |
+
im = self._tf.pages[index].asarray()
|
| 446 |
+
|
| 447 |
+
meta = self._get_meta_data(index)
|
| 448 |
+
|
| 449 |
+
return im, meta
|
| 450 |
+
|
| 451 |
+
def _get_meta_data(self, index):
|
| 452 |
+
meta = {}
|
| 453 |
+
page = self._tf.pages[index or 0]
|
| 454 |
+
for key in READ_METADATA_KEYS:
|
| 455 |
+
try:
|
| 456 |
+
meta[key] = getattr(page, key)
|
| 457 |
+
except Exception:
|
| 458 |
+
pass
|
| 459 |
+
|
| 460 |
+
# tifffile <= 0.12.1 use datetime, newer use DateTime
|
| 461 |
+
for key in ("datetime", "DateTime"):
|
| 462 |
+
try:
|
| 463 |
+
meta["datetime"] = datetime.datetime.strptime(
|
| 464 |
+
page.tags[key].value, "%Y:%m:%d %H:%M:%S"
|
| 465 |
+
)
|
| 466 |
+
break
|
| 467 |
+
except Exception:
|
| 468 |
+
pass
|
| 469 |
+
|
| 470 |
+
if 296 in page.tags:
|
| 471 |
+
meta["resolution_unit"] = page.tags[296].value.value
|
| 472 |
+
|
| 473 |
+
if 282 in page.tags and 283 in page.tags and 296 in page.tags:
|
| 474 |
+
resolution_x = page.tags[282].value
|
| 475 |
+
resolution_y = page.tags[283].value
|
| 476 |
+
if resolution_x[1] == 0 or resolution_y[1] == 0:
|
| 477 |
+
warnings.warn(
|
| 478 |
+
"Ignoring resulution metadata, "
|
| 479 |
+
"because at least one direction has a 0 denominator.",
|
| 480 |
+
RuntimeWarning,
|
| 481 |
+
)
|
| 482 |
+
else:
|
| 483 |
+
meta["resolution"] = (
|
| 484 |
+
resolution_x[0] / resolution_x[1],
|
| 485 |
+
resolution_y[0] / resolution_y[1],
|
| 486 |
+
page.tags[296].value.name,
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
return meta
|
| 490 |
+
|
| 491 |
+
# -- writer
|
| 492 |
+
class Writer(Format.Writer):
|
| 493 |
+
def _open(self, bigtiff=None, byteorder=None, software=None):
|
| 494 |
+
try:
|
| 495 |
+
self._tf = _tifffile.TiffWriter(
|
| 496 |
+
self.request.get_file(),
|
| 497 |
+
bigtiff=bigtiff,
|
| 498 |
+
byteorder=byteorder,
|
| 499 |
+
software=software,
|
| 500 |
+
)
|
| 501 |
+
self._software = None
|
| 502 |
+
except TypeError:
|
| 503 |
+
# In tifffile >= 0.15, the `software` arg is passed to
|
| 504 |
+
# TiffWriter.save
|
| 505 |
+
self._tf = _tifffile.TiffWriter(
|
| 506 |
+
self.request.get_file(), bigtiff=bigtiff, byteorder=byteorder
|
| 507 |
+
)
|
| 508 |
+
self._software = software
|
| 509 |
+
|
| 510 |
+
self._meta = {}
|
| 511 |
+
self._frames_written = 0
|
| 512 |
+
|
| 513 |
+
def _close(self):
|
| 514 |
+
self._tf.close()
|
| 515 |
+
|
| 516 |
+
def _append_data(self, im, meta):
|
| 517 |
+
if meta is not None:
|
| 518 |
+
meta = self._sanitize_meta(meta)
|
| 519 |
+
else:
|
| 520 |
+
# Use global metadata for first frame
|
| 521 |
+
meta = self._meta if self._frames_written == 0 else {}
|
| 522 |
+
if self._software is not None and self._frames_written == 0:
|
| 523 |
+
meta["software"] = self._software
|
| 524 |
+
# No need to check self.request.mode; tifffile figures out whether
|
| 525 |
+
# this is a single page, or all page data at once.
|
| 526 |
+
try:
|
| 527 |
+
# TiffWriter.save has been deprecated in version 2020.9.30
|
| 528 |
+
write_meth = self._tf.write
|
| 529 |
+
except AttributeError:
|
| 530 |
+
write_meth = self._tf.save
|
| 531 |
+
write_meth(np.asanyarray(im), contiguous=False, **meta)
|
| 532 |
+
self._frames_written += 1
|
| 533 |
+
|
| 534 |
+
@staticmethod
|
| 535 |
+
def _sanitize_meta(meta):
|
| 536 |
+
ret = {}
|
| 537 |
+
for (key, value) in meta.items():
|
| 538 |
+
if key in WRITE_METADATA_KEYS:
|
| 539 |
+
# Special case of previously read `predictor` int value
|
| 540 |
+
# 1(=NONE) translation to False expected by TiffWriter.save
|
| 541 |
+
if key == "predictor" and not isinstance(value, bool):
|
| 542 |
+
ret[key] = value > 1
|
| 543 |
+
else:
|
| 544 |
+
ret[key] = value
|
| 545 |
+
return ret
|
| 546 |
+
|
| 547 |
+
def set_meta_data(self, meta):
|
| 548 |
+
self._meta = self._sanitize_meta(meta)
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/matplotlib/__pycache__/figure.cpython-38.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d8f53d61910158b1ce59896ef068bb30ed22be14947f337e3b6fd9ee6c753e5c
|
| 3 |
+
size 103599
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/matplotlib/mpl-data/sample_data/axes_grid/bivariate_normal.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0e9599f6e74087aa2ca58aa77846b6ec3e8491180e445c07a2c69c65756ef7c5
|
| 3 |
+
size 1880
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/__init__.cpython-38.pyc
ADDED
|
Binary file (959 Bytes). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/cmuarctic.cpython-38.pyc
ADDED
|
Binary file (6.33 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/cmudict.cpython-38.pyc
ADDED
|
Binary file (5.22 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/commonvoice.cpython-38.pyc
ADDED
|
Binary file (2.88 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/dr_vctk.cpython-38.pyc
ADDED
|
Binary file (4.04 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/gtzan.cpython-38.pyc
ADDED
|
Binary file (22.1 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/librilight_limited.cpython-38.pyc
ADDED
|
Binary file (4.19 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/librimix.cpython-38.pyc
ADDED
|
Binary file (4.04 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/librispeech.cpython-38.pyc
ADDED
|
Binary file (5.22 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/libritts.cpython-38.pyc
ADDED
|
Binary file (5.08 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/ljspeech.cpython-38.pyc
ADDED
|
Binary file (3.36 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/quesst14.cpython-38.pyc
ADDED
|
Binary file (3.83 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/speechcommands.cpython-38.pyc
ADDED
|
Binary file (6.06 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/tedlium.cpython-38.pyc
ADDED
|
Binary file (7 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/utils.cpython-38.pyc
ADDED
|
Binary file (5.57 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/vctk.cpython-38.pyc
ADDED
|
Binary file (4.7 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/datasets/__pycache__/yesno.cpython-38.pyc
ADDED
|
Binary file (3.52 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/models/__pycache__/__init__.cpython-38.pyc
ADDED
|
Binary file (1.25 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/models/__pycache__/conformer.cpython-38.pyc
ADDED
|
Binary file (9.36 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/models/__pycache__/conv_tasnet.cpython-38.pyc
ADDED
|
Binary file (9.2 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/models/__pycache__/deepspeech.cpython-38.pyc
ADDED
|
Binary file (2.75 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/models/__pycache__/emformer.cpython-38.pyc
ADDED
|
Binary file (28.9 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/models/__pycache__/rnnt.cpython-38.pyc
ADDED
|
Binary file (31.4 kB). View file
|
|
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/torchaudio/models/__pycache__/rnnt_decoder.cpython-38.pyc
ADDED
|
Binary file (11.8 kB). View file
|
|
|