Repository: scikit-image/scikit-image

Issue:
Add support for multipage tiff files with the freeimage plugin
Currently, our Freeimage TIFF reader just reads the first plane of a multipage tiff and calls it a day. It would be good to figure out how to support multipage tiff properly. (Though tifffile handles it admirably, users might set freeimage for whatever reason, and be stuck with buggy code.)

See #1440 for a related issue.



Discussion:
As an end user, not a huge fan of this becoming dependent on freeimage for this reason. ( https://github.com/scikit-image/scikit-image/issues/1605#issuecomment-120953609 )

Not having CI is a bit scary. Building freeimage, from past experience, required some serious patching. They pin the compiler version to gcc-4.0, which seems unnecessary. If it is necessary, I am worried. Sticking to CVS long after support is gone is also scary.

We ship tifffile. Is there any reason a multipage tiff _shouldn't_ be read using tifffile? In the short term, I'd say just read all multipage TIFFs with tifffile, potentially emitting a warning telling the user that we encountered such a file and are using the appropriate backend instead of what they specified.

I'm only comfortable with this because the Freeimage behavior is buggy/broken. Ideally we'd fix that, but difficult to justify any developer work for it when we ship a superior alternative.

We're already doing that in the PIL plugin: https://github.com/scikit-image/scikit-image/blob/master/skimage/io/_plugins/pil_plugin.py#L43

`tifffile` works great. Normally, I use either that or `pylibtiff` (believe it is now called `libtiff` confusingly).

@blink1073 lovely - Do you want to patch it, or shall I?

Do we want to do this here instead? https://github.com/scikit-image/scikit-image/blob/master/skimage/io/_io.py#L17

Yes, agreed, catch it before any plugins get called. Also document in the Notes that tifffile will always be used when reading .TIF[F] files. Clear out any other logic, more maintainable.


Resolving patch:
diff --git a/skimage/io/_io.py b/skimage/io/_io.py
--- a/skimage/io/_io.py
+++ b/skimage/io/_io.py
@@ -26,7 +26,10 @@ def imread(fname, as_grey=False, plugin=None, flatten=None,
         If True, convert color images to grey-scale (32-bit floats).
         Images that are already in grey-scale format are not converted.
     plugin : str
-        Name of plugin to use (Python Imaging Library by default).
+        Name of plugin to use.  By default, the different plugins are
+        tried (starting with the Python Imaging Library) until a suitable
+        candidate is found.  If not given and fname is a tiff file, the
+        tifffile plugin will be used.
 
     Other Parameters
     ----------------
@@ -50,6 +53,10 @@ def imread(fname, as_grey=False, plugin=None, flatten=None,
     if flatten is not None:
         as_grey = flatten
 
+    if plugin is None and hasattr(fname, 'lower'):
+        if fname.lower().endswith(('.tiff', '.tif')):
+            plugin = 'tifffile'
+
     with file_or_url_context(fname) as fname:
         img = call_plugin('imread', fname, plugin=plugin, **plugin_args)
 
@@ -109,7 +116,8 @@ def imsave(fname, arr, plugin=None, **plugin_args):
     plugin : str
         Name of plugin to use.  By default, the different plugins are
         tried (starting with the Python Imaging Library) until a suitable
-        candidate is found.
+        candidate is found.  If not given and fname is a tiff file, the
+        tifffile plugin will be used.
 
     Other parameters
     ----------------
@@ -117,6 +125,9 @@ def imsave(fname, arr, plugin=None, **plugin_args):
         Passed to the given plugin.
 
     """
+    if plugin is None and hasattr(fname, 'lower'):
+        if fname.lower().endswith(('.tiff', '.tif')):
+            plugin = 'tifffile'
     if is_low_contrast(arr):
         warnings.warn('%s is a low contrast image' % fname)
     return call_plugin('imsave', fname, arr, plugin=plugin, **plugin_args)
diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py
--- a/skimage/io/_plugins/pil_plugin.py
+++ b/skimage/io/_plugins/pil_plugin.py
@@ -5,7 +5,6 @@
 from PIL import Image
 
 from ...util import img_as_ubyte, img_as_uint
-from .tifffile_plugin import imread as tif_imread, imsave as tif_imsave
 
 
 def imread(fname, dtype=None, img_num=None, **kwargs):
@@ -21,28 +20,17 @@ def imread(fname, dtype=None, img_num=None, **kwargs):
        Specifies which image to read in a file with multiple images
        (zero-indexed).
     kwargs : keyword pairs, optional
-        Addition keyword arguments to pass through (only applicable to Tiff
-        files for now,  see `tifffile`'s `imread` function).
+        Addition keyword arguments to pass through.
 
     Notes
     -----
-    Tiff files are handled by Christophe Golhke's tifffile.py [1]_, and support
-    many advanced image types including multi-page and floating point.
-
-    All other files are read using the Python Imaging Libary.
-    See PIL docs [2]_ for a list of supported formats.
+    Files are read using the Python Imaging Libary.
+    See PIL docs [1]_ for a list of supported formats.
 
     References
     ----------
-    .. [1] http://www.lfd.uci.edu/~gohlke/code/tifffile.py.html
-    .. [2] http://pillow.readthedocs.org/en/latest/handbook/image-file-formats.html
-
+    .. [1] http://pillow.readthedocs.org/en/latest/handbook/image-file-formats.html
     """
-    if hasattr(fname, 'lower') and dtype is None:
-        kwargs.setdefault('key', img_num)
-        if fname.lower().endswith(('.tiff', '.tif')):
-            return tif_imread(fname, **kwargs)
-
     if isinstance(fname, string_types):
         with open(fname, 'rb') as f:
             im = Image.open(f)
@@ -234,12 +222,8 @@ def imsave(fname, arr, format_str=None, **kwargs):
 
     Notes
     -----
-    Tiff files are handled by Christophe Golhke's tifffile.py [1]_,
-    and support many advanced image types including multi-page and
-    floating point.
-
-    All other image formats use the Python Imaging Libary.
-    See PIL docs [2]_ for a list of other supported formats.
+    Use the Python Imaging Libary.
+    See PIL docs [1]_ for a list of other supported formats.
     All images besides single channel PNGs are converted using `img_as_uint8`.
     Single Channel PNGs have the following behavior:
     - Integer values in [0, 255] and Boolean types -> img_as_uint8
@@ -247,8 +231,7 @@ def imsave(fname, arr, format_str=None, **kwargs):
 
     References
     ----------
-    .. [1] http://www.lfd.uci.edu/~gohlke/code/tifffile.py.html
-    .. [2] http://pillow.readthedocs.org/en/latest/handbook/image-file-formats.html
+    .. [1] http://pillow.readthedocs.org/en/latest/handbook/image-file-formats.html
     """
     # default to PNG if file-like object
     if not isinstance(fname, string_types) and format_str is None:
@@ -263,18 +246,6 @@ def imsave(fname, arr, format_str=None, **kwargs):
     if arr.dtype.kind == 'b':
         arr = arr.astype(np.uint8)
 
-    use_tif = False
-    if hasattr(fname, 'lower'):
-        if fname.lower().endswith(('.tiff', '.tif')):
-            use_tif = True
-    if format_str is not None:
-        if format_str.lower() in ['tiff', 'tif']:
-            use_tif = True
-
-    if use_tif:
-        tif_imsave(fname, arr, **kwargs)
-        return
-
     if arr.ndim not in (2, 3):
         raise ValueError("Invalid shape for image array: %s" % arr.shape)
 
diff --git a/skimage/io/_plugins/tifffile_plugin.py b/skimage/io/_plugins/tifffile_plugin.py
--- a/skimage/io/_plugins/tifffile_plugin.py
+++ b/skimage/io/_plugins/tifffile_plugin.py
@@ -24,6 +24,8 @@ def imread(fname, dtype=None, **kwargs):
     .. [1] http://www.lfd.uci.edu/~gohlke/code/tifffile.py
 
     """
+    if 'img_num' in kwargs:
+        kwargs['key'] = kwargs.pop('img_num')
     with open(fname, 'rb') as f:
         tif = TiffFile(f)
         return tif.asarray(**kwargs)


Test patch:
diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py
--- a/skimage/io/tests/test_pil.py
+++ b/skimage/io/tests/test_pil.py
@@ -196,7 +196,6 @@ def test_all_color():
 
 def test_all_mono():
     mono_check('pil')
-    mono_check('pil', 'tiff')
 
 
 def test_multi_page_gif():
@@ -234,35 +233,5 @@ def test_cmyk():
         sim = ssim(refi, newi, dynamic_range=refi.max() - refi.min())
         assert sim > 0.99
 
-
-class TestSaveTIF:
-    def roundtrip(self, dtype, x, compress):
-        with temporary_file(suffix='.tif') as fname:
-            if dtype == np.bool:
-                expected = ['low contrast']
-            else:
-                expected = ['\A\Z']
-            with expected_warnings(expected):
-                if compress > 0:
-                    imsave(fname, x, compress=compress)
-                else:
-                    imsave(fname, x)
-            y = imread(fname)
-            assert_array_equal(x, y)
-
-    def test_imsave_roundtrip(self):
-        for shape in [(10, 10), (10, 10, 3), (10, 10, 4)]:
-            for dtype in (np.uint8, np.uint16, np.int16, np.float32,
-                          np.float64, np.bool):
-                for compress in [0, 2]:
-                    x = np.random.rand(*shape)
-
-                    if not np.issubdtype(dtype, float) and \
-                                                        not dtype == np.bool:
-                        x = (x * np.iinfo(dtype).max).astype(dtype)
-                    else:
-                        x = x.astype(dtype)
-                    yield self.roundtrip, dtype, x, compress
-
 if __name__ == "__main__":
     run_module_suite()